Skip to main content

Organize your daily.dev bookmarks

Be honest: how many bookmarks do you have right now that you will never read?

Bookmarking is the easiest promise in software. One click, zero commitment. Which is why every developer's reading list turns into a graveyard: dozens of saved posts, half of them about a framework you no longer use, sorted by nothing except the order you were procrastinating in.

Saving liberally is fine. Future you might need it. What's missing is structure. The four posts you kept because you're finally going to learn async Rust belong together, and a flat list has no way to say so.

So here we teach the CLI to group things: a local-bookmarks command that keeps folders of daily.dev posts on your own machine. It follows the setup, terminal CLI and morning briefing pages, and the finished code is in dailydotdev/dd-cli-example.

A folder is a name and some ids

Every command we've built prints a post id next to its title, so feed, popular, and discussed already give you the only identifier this feature needs. A group is a name and a list of ids, which means the whole thing fits in a JSON file:

{
  "Rust async deep-dive": ["abc123", "def456"],
  "Postgres 18 migration": ["ghi789"]
}

That's a Record<string, string[]>, which zod describes in one line. Add it to src/schema.ts next to the others:

export const localBookmarksSchema = z.record(
  z.string(),
  z.array(z.string()),
);

export type LocalBookmarks = z.infer<typeof localBookmarksSchema>;

Then a new src/store.ts, the only file that knows where local bookmarks are kept. They go in your home directory rather than the working directory, so your groups don't disappear when you run the CLI from a different folder:

const STORE =
  process.env.DAILYDEV_LOCAL_BOOKMARKS ??
  join(homedir(), '.dailydev', 'local-bookmarks.json');

export const readLocalBookmarks = async (): Promise<LocalBookmarks> => {
  const raw = await readFile(STORE, 'utf8').catch(
    (error: NodeJS.ErrnoException) => {
      if (error.code === 'ENOENT') {
        return null;
      }

      throw error;
    },
  );

  return raw === null ? {} : localBookmarksSchema.parse(JSON.parse(raw));
};

export const writeLocalBookmarks = async (folders: LocalBookmarks) => {
  await mkdir(dirname(STORE), { recursive: true });
  await writeFile(STORE, `${JSON.stringify(folders, null, 2)}\n`);
};

A missing file is not an error. It means you haven't filed anything yet, so you get an empty set of groups. Anything else, like a permissions problem or a corrupt file, still throws, because starting from scratch without telling you would lose your groups.

Filing is a read, a check, and a write:

export const fileBookmark = async (postId: string, folder: string) => {
  const folders = await readLocalBookmarks();
  const current = folders[folder] ?? [];

  if (current.includes(postId)) {
    return false;
  }

  await writeLocalBookmarks({ ...folders, [folder]: [...current, postId] });

  return true;
};

The false lets the command say "already filed" instead of pretending it did something. Unfiling is the mirror image, with one extra decision: with no folder given it removes the id from every group, and it drops any group left empty rather than keeping a dead name around.

Wiring up the commands

This is the first command with more than one verb, which needs one addition to the pattern from the CLI page: a second positional. local-bookmarks is the command, and list, folders, add, move, or remove is the action after it. Leave it out and you get list.

The args union then validates the combinations, because add without a post id and add without a folder should both fail before anything touches the store:

pnpm dailydev local-bookmarks add abc123 -f "Rust async deep-dive"
pnpm dailydev local-bookmarks folders     # groups and their sizes
pnpm dailydev local-bookmarks             # groups with their post ids
pnpm dailydev local-bookmarks move abc123 -f "Postgres 18 migration"
pnpm dailydev local-bookmarks remove abc123   # -f to leave one group only
$ pnpm dailydev local-bookmarks folders
Rust async deep-dive	4 saved
Postgres 18 migration	3 saved

Add --json to any of them and you get the store as it sits on disk, which is what your agent wants when you ask it to reorganize things for you.

Because the file holds daily.dev post ids rather than copies, it never competes with your actual library. Delete it and you lose the grouping, not the bookmarks.

The store is swappable

Everything goes through readLocalBookmarks and writeLocalBookmarks, so a JSON file in ~/.dailydev is only the default. Point those two functions at Firebase or Supabase and your groups follow you between laptop and phone. Point them at SQLite or Postgres if you'd rather query them, or at a KV store on the edge if the CLI runs there too. The commands don't change, only those two functions do.

Keep that boundary in place as you extend this. The local-bookmarks actions know that a group is a name and some ids. They don't need to know it's a file.

If you have Plus

A file on your laptop has one obvious limit: it's on your laptop. The groups you build in the terminal aren't there when you're reading on your phone on the train, or in the browser extension on your work machine.

daily.dev Plus includes bookmark folders, which solve exactly that. Your folders live with your account, so they show up everywhere you read daily.dev.

If you have Plus, you don't need the local store at all. The CLI has a bookmarks command that takes the same actions against your account, so the difference is one word:

pnpm dailydev bookmarks add abc123 -f "Rust async deep-dive"

Same actions, same flags, and the folders are no longer tied to one machine.

What's next

Last in this group: we stop treating daily.dev as your reading list and start treating it as your agent's knowledge source, replacing generic web search with articles developers have read and argued about.

Next: Point your agent at daily.dev instead of web search, pointing your agent at community-vetted articles instead of whatever won the SEO game this week.

Spotted something to fix? Contact us
Link copied!