coding-friend-cli 1.19.0 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,8 +5,26 @@ CLI companion for the [coding-friend](https://github.com/dinhanhthi/coding-frien
5
5
  ## Requirements
6
6
 
7
7
  - Node.js >= 18
8
+ - npm (included with Node.js, but on some Linux distros you may need to install it separately)
8
9
  - The [coding-friend plugin](https://github.com/dinhanhthi/coding-friend) installed in Claude Code
9
10
 
11
+ ### Additional requirements for `cf memory init`
12
+
13
+ The memory system's Tier 1 (SQLite) uses native Node.js modules that require compilation. On **Linux** (Ubuntu/Debian), install the following system packages before running `cf memory init`:
14
+
15
+ ```bash
16
+ sudo apt update
17
+ sudo apt install -y build-essential python3
18
+ ```
19
+
20
+ On **macOS**, install Xcode Command Line Tools:
21
+
22
+ ```bash
23
+ xcode-select --install
24
+ ```
25
+
26
+ Without these, `cf memory init` will fail when installing SQLite dependencies (`better-sqlite3`, `sqlite-vec`). If you don't need Tier 1, you can choose the **lite** (Tier 2) or **markdown** (Tier 3) tier during init — these have no native dependencies.
27
+
10
28
  ## Install
11
29
 
12
30
  ```bash
@@ -55,6 +73,8 @@ cf permission --project # Save to project-level settings (.claude/settings.lo
55
73
  cf permission --all --user # Apply all recommended permissions to user settings
56
74
  cf statusline # Setup coding-friend statusline
57
75
  cf update # Update plugin + CLI + statusline
76
+ # 💡 If update fails, open Claude Code (`claude`) and run:
77
+ # /plugins → Installed → coding-friend → Update now → /reload-plugins
58
78
  cf update --cli # Update only the CLI (npm package)
59
79
  cf update --plugin # Update only the Claude Code plugin
60
80
  cf update --statusline # Update only the statusline
@@ -198,7 +198,7 @@ async function updateCommand(opts) {
198
198
  log.dim(`stderr: ${result.stderr}`);
199
199
  }
200
200
  log.dim(
201
- "Try manually: claude plugin update coding-friend@coding-friend-marketplace"
201
+ "Try manually in Claude Code: /plugins \u2192 Installed \u2192 coding-friend \u2192 Update now \u2192 /reload-plugins"
202
202
  );
203
203
  }
204
204
  }
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ program.name("cf").description(
14
14
  "coding-friend CLI \u2014 host learning docs, setup MCP, init projects"
15
15
  ).version(pkg.version, "-v, --version");
16
16
  program.command("install").description("Install the Coding Friend plugin into Claude Code").option("--user", "Install at user scope (all projects)").option("--global", "Install at user scope (all projects)").option("--project", "Install at project scope (shared via git)").option("--local", "Install at local scope (this machine only)").action(async (opts) => {
17
- const { installCommand } = await import("./install-HLCVBOXO.js");
17
+ const { installCommand } = await import("./install-35IWHBIS.js");
18
18
  await installCommand(opts);
19
19
  });
20
20
  program.command("uninstall").description("Uninstall the Coding Friend plugin from Claude Code").option("--user", "Uninstall from user scope (all projects)").option("--global", "Uninstall from user scope (all projects)").option("--project", "Uninstall from project scope").option("--local", "Uninstall from local scope").action(async (opts) => {
@@ -57,11 +57,11 @@ program.command("statusline").description("Setup coding-friend statusline in Cla
57
57
  await statuslineCommand();
58
58
  });
59
59
  program.command("update").description("Update coding-friend plugin, CLI, and statusline").option("--cli", "Update only the CLI (npm package)").option("--plugin", "Update only the Claude Code plugin").option("--statusline", "Update only the statusline").option("--user", "Update plugin at user scope (all projects)").option("--global", "Update plugin at user scope (all projects)").option("--project", "Update plugin at project scope").option("--local", "Update plugin at local scope").action(async (opts) => {
60
- const { updateCommand } = await import("./update-EVOGWLKX.js");
60
+ const { updateCommand } = await import("./update-NZ2HRWEN.js");
61
61
  await updateCommand(opts);
62
62
  });
63
63
  program.command("status").description("Show comprehensive Coding Friend status").action(async () => {
64
- const { statusCommand } = await import("./status-V324NM64.js");
64
+ const { statusCommand } = await import("./status-SENJZQ3G.js");
65
65
  await statusCommand();
66
66
  });
67
67
  var session = program.command("session").description("Save and load Claude Code sessions across machines");
@@ -100,43 +100,43 @@ Memory subcommands:
100
100
  memory mcp Show MCP server setup instructions`
101
101
  );
102
102
  memory.command("status").description("Show memory system status").action(async () => {
103
- const { memoryStatusCommand } = await import("./memory-BL37DXPU.js");
103
+ const { memoryStatusCommand } = await import("./memory-47RXG7VL.js");
104
104
  await memoryStatusCommand();
105
105
  });
106
106
  memory.command("search").description("Search memories by query").argument("<query>", "search query").action(async (query) => {
107
- const { memorySearchCommand } = await import("./memory-BL37DXPU.js");
107
+ const { memorySearchCommand } = await import("./memory-47RXG7VL.js");
108
108
  await memorySearchCommand(query);
109
109
  });
110
110
  memory.command("list").description(
111
111
  "List memories in current project, or all projects with --projects"
112
112
  ).option("--projects", "List all project databases with size and metadata").action(async (opts) => {
113
- const { memoryListCommand } = await import("./memory-BL37DXPU.js");
113
+ const { memoryListCommand } = await import("./memory-47RXG7VL.js");
114
114
  await memoryListCommand(opts);
115
115
  });
116
116
  memory.command("init").description(
117
117
  "Initialize memory system \u2014 interactive wizard (first time) or config menu"
118
118
  ).action(async () => {
119
- const { memoryInitCommand } = await import("./memory-BL37DXPU.js");
119
+ const { memoryInitCommand } = await import("./memory-47RXG7VL.js");
120
120
  await memoryInitCommand();
121
121
  });
122
122
  memory.command("config").description("Configure memory system settings").action(async () => {
123
- const { memoryConfigCommand } = await import("./memory-BL37DXPU.js");
123
+ const { memoryConfigCommand } = await import("./memory-47RXG7VL.js");
124
124
  await memoryConfigCommand();
125
125
  });
126
126
  memory.command("start-daemon").description("Start the memory daemon (Tier 2 \u2014 MiniSearch)").action(async () => {
127
- const { memoryStartDaemonCommand } = await import("./memory-BL37DXPU.js");
127
+ const { memoryStartDaemonCommand } = await import("./memory-47RXG7VL.js");
128
128
  await memoryStartDaemonCommand();
129
129
  });
130
130
  memory.command("stop-daemon").description("Stop the memory daemon").action(async () => {
131
- const { memoryStopDaemonCommand } = await import("./memory-BL37DXPU.js");
131
+ const { memoryStopDaemonCommand } = await import("./memory-47RXG7VL.js");
132
132
  await memoryStopDaemonCommand();
133
133
  });
134
134
  memory.command("rebuild").description("Rebuild the daemon search index").action(async () => {
135
- const { memoryRebuildCommand } = await import("./memory-BL37DXPU.js");
135
+ const { memoryRebuildCommand } = await import("./memory-47RXG7VL.js");
136
136
  await memoryRebuildCommand();
137
137
  });
138
138
  memory.command("mcp").description("Show MCP server setup instructions").action(async () => {
139
- const { memoryMcpCommand } = await import("./memory-BL37DXPU.js");
139
+ const { memoryMcpCommand } = await import("./memory-47RXG7VL.js");
140
140
  await memoryMcpCommand();
141
141
  });
142
142
  memory.command("rm").description("Remove a project database").option("--project-id <id>", "Project ID to remove").option("--all", "Remove all project databases").option(
@@ -144,7 +144,7 @@ memory.command("rm").description("Remove a project database").option("--project-
144
144
  "Remove orphaned projects (source dir missing or 0 memories)"
145
145
  ).action(
146
146
  async (opts) => {
147
- const { memoryRmCommand } = await import("./memory-BL37DXPU.js");
147
+ const { memoryRmCommand } = await import("./memory-47RXG7VL.js");
148
148
  await memoryRmCommand(opts);
149
149
  }
150
150
  );
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getLatestVersion,
3
3
  semverCompare
4
- } from "./chunk-IRPW2BMP.js";
4
+ } from "./chunk-PHQK2MMO.js";
5
5
  import {
6
6
  enableMarketplaceAutoUpdate,
7
7
  isMarketplaceRegistered,
@@ -18,7 +18,8 @@ import {
18
18
  showConfigHint
19
19
  } from "./chunk-C5LYVVEI.js";
20
20
  import {
21
- run
21
+ run,
22
+ runWithStderr
22
23
  } from "./chunk-CYQU33FY.js";
23
24
  import {
24
25
  globalConfigPath,
@@ -50,21 +51,32 @@ function countMdFiles(dir) {
50
51
  function getMemoryDir(path) {
51
52
  return resolveMemoryDir(path);
52
53
  }
54
+ var MAX_ERROR_LINES = 30;
55
+ function truncateError(text) {
56
+ const lines = text.split("\n");
57
+ if (lines.length <= MAX_ERROR_LINES) return text;
58
+ const head = lines.slice(0, 20);
59
+ const tail = lines.slice(-8);
60
+ const skipped = lines.length - 28;
61
+ return [...head, ` ... (${skipped} lines omitted) ...`, ...tail].join("\n");
62
+ }
53
63
  function ensureBuilt(mcpDir) {
54
64
  if (!existsSync(join(mcpDir, "node_modules"))) {
55
65
  log.step("Installing memory server dependencies (one-time setup)...");
56
- const result = run("npm", ["install", "--silent"], { cwd: mcpDir });
57
- if (result === null) {
66
+ const result = runWithStderr("npm", ["install"], { cwd: mcpDir });
67
+ if (result.exitCode !== 0) {
58
68
  log.error("Failed to install dependencies");
69
+ if (result.stderr) log.error(truncateError(result.stderr));
59
70
  process.exit(1);
60
71
  }
61
72
  log.success("Done.");
62
73
  }
63
74
  if (!existsSync(join(mcpDir, "dist"))) {
64
75
  log.step("Building memory server...");
65
- const result = run("npm", ["run", "build", "--silent"], { cwd: mcpDir });
66
- if (result === null) {
76
+ const result = runWithStderr("npm", ["run", "build"], { cwd: mcpDir });
77
+ if (result.exitCode !== 0) {
67
78
  log.error("Failed to build memory server");
79
+ if (result.stderr) log.error(truncateError(result.stderr));
68
80
  process.exit(1);
69
81
  }
70
82
  log.success("Done.");
@@ -6,7 +6,7 @@ import {
6
6
  getLatestCliVersion,
7
7
  getLatestVersion,
8
8
  semverCompare
9
- } from "./chunk-IRPW2BMP.js";
9
+ } from "./chunk-PHQK2MMO.js";
10
10
  import {
11
11
  detectPluginScope,
12
12
  isPluginDisabled
@@ -63,19 +63,21 @@ function formatScalar(value) {
63
63
  if (typeof value === "number") return String(value);
64
64
  return String(value);
65
65
  }
66
- function pad(label, width) {
67
- return ` ${label}${" ".repeat(Math.max(1, width - label.length))}`;
66
+ function pad(label, width, color) {
67
+ const displayed = color ? color(label) : label;
68
+ return ` ${displayed}${" ".repeat(Math.max(1, width - label.length))}`;
68
69
  }
69
- function subLine(key, value, overrides) {
70
+ function subLine(key, value, overrides, color) {
71
+ const displayed = color ? color(key) : key;
70
72
  console.log(
71
- ` ${key}${" ".repeat(Math.max(1, CONFIG_SUB_COL - key.length))}${value}${overrides}`
73
+ ` ${displayed}${" ".repeat(Math.max(1, CONFIG_SUB_COL - key.length))}${value}${overrides}`
72
74
  );
73
75
  }
74
76
  function printConfig(obj, otherConfig) {
75
77
  for (const [key, value] of Object.entries(obj)) {
76
78
  const overrides = otherConfig && key in otherConfig ? ` ${chalk.yellow("(overrides global)")}` : "";
77
79
  if (typeof value === "object" && value !== null && !Array.isArray(value)) {
78
- console.log(pad(key, CONFIG_KEY_COL) + chalk.dim("\u2500") + overrides);
80
+ console.log(pad(key, CONFIG_KEY_COL, chalk.cyan) + chalk.dim("\u2500") + overrides);
79
81
  const nested = value;
80
82
  const nestedOther = otherConfig && typeof otherConfig[key] === "object" ? otherConfig[key] : null;
81
83
  for (const [subKey, subVal] of Object.entries(nested)) {
@@ -84,7 +86,7 @@ function printConfig(obj, otherConfig) {
84
86
  const names = subVal.map(
85
87
  (c) => typeof c === "object" && c !== null && "name" in c ? c.name : String(c)
86
88
  ).join(", ");
87
- subLine(subKey, names, subOverrides);
89
+ subLine(subKey, names, subOverrides, chalk.cyan);
88
90
  continue;
89
91
  }
90
92
  if (typeof subVal === "object" && subVal !== null && !Array.isArray(subVal)) {
@@ -92,29 +94,30 @@ function printConfig(obj, otherConfig) {
92
94
  subVal
93
95
  );
94
96
  const inline = innerEntries.map(([k, v]) => `${k}: ${formatScalar(v)}`).join(", ");
95
- subLine(subKey, inline, subOverrides);
97
+ subLine(subKey, inline, subOverrides, chalk.cyan);
96
98
  continue;
97
99
  }
98
100
  if (Array.isArray(subVal)) {
99
101
  subLine(
100
102
  subKey,
101
103
  subVal.map((v) => formatScalar(v)).join(", "),
102
- subOverrides
104
+ subOverrides,
105
+ chalk.cyan
103
106
  );
104
107
  continue;
105
108
  }
106
- subLine(subKey, formatScalar(subVal), subOverrides);
109
+ subLine(subKey, formatScalar(subVal), subOverrides, chalk.cyan);
107
110
  }
108
111
  continue;
109
112
  }
110
113
  if (Array.isArray(value)) {
111
114
  console.log(
112
- `${pad(key, CONFIG_KEY_COL)}${value.map((v) => formatScalar(v)).join(", ")}${overrides}`
115
+ `${pad(key, CONFIG_KEY_COL, chalk.cyan)}${value.map((v) => formatScalar(v)).join(", ")}${overrides}`
113
116
  );
114
117
  continue;
115
118
  }
116
119
  console.log(
117
- `${pad(key, CONFIG_KEY_COL)}${formatScalar(value)}${overrides}`
120
+ `${pad(key, CONFIG_KEY_COL, chalk.cyan)}${formatScalar(value)}${overrides}`
118
121
  );
119
122
  }
120
123
  }
@@ -4,7 +4,7 @@ import {
4
4
  getLatestVersion,
5
5
  semverCompare,
6
6
  updateCommand
7
- } from "./chunk-IRPW2BMP.js";
7
+ } from "./chunk-PHQK2MMO.js";
8
8
  import "./chunk-ORACWEDN.js";
9
9
  import "./chunk-POC2WHU2.js";
10
10
  import "./chunk-NEQZP5D4.js";
@@ -1,5 +1,9 @@
1
1
  # CF Memory Changelog
2
2
 
3
+ ## v0.2.0 (2026-03-21)
4
+
5
+ - Add `index_only` option to `memory_store` MCP tool — skip file writing when file already exists on disk, enabling clean separation between file creation and indexing [#7f56711](https://github.com/dinhanhthi/coding-friend/commit/7f56711)
6
+
3
7
  ## v0.1.3 (2026-03-19)
4
8
 
5
9
  - Use path-based project IDs instead of SHA256 hashes for human-readable project directories (e.g. `-Users-thi-git-foo` instead of `a1b2c3d4e5f6`) [#9c4cac0](https://github.com/dinhanhthi/coding-friend/commit/9c4cac0)
@@ -265,9 +265,37 @@ The `cf` CLI exposes memory commands that use this package:
265
265
  | `cf memory start-daemon` | Start the MiniSearch daemon (Tier 2) |
266
266
  | `cf memory stop-daemon` | Stop the daemon |
267
267
  | `cf memory rebuild` | Rebuild search index (Tier 1 direct or via daemon) |
268
- | `cf memory init` | Install Tier 1 deps + import existing memories into SQLite |
268
+ | `cf memory init` | Install Tier 1 deps + import existing memories into SQLite (see [prerequisites](#prerequisites-for-tier-1-on-linux)) |
269
269
  | `cf memory mcp` | Print MCP server config for use in Claude Desktop / other clients |
270
270
 
271
+ ## Prerequisites for Tier 1 on Linux
272
+
273
+ Tier 1 uses `better-sqlite3` and `sqlite-vec`, which are native Node.js modules requiring C++ compilation. On a fresh Linux install, you need build tools before running `cf memory init`:
274
+
275
+ **Ubuntu/Debian:**
276
+
277
+ ```bash
278
+ sudo apt update
279
+ sudo apt install -y build-essential python3
280
+ ```
281
+
282
+ **Fedora/RHEL:**
283
+
284
+ ```bash
285
+ sudo dnf groupinstall "Development Tools"
286
+ sudo dnf install python3
287
+ ```
288
+
289
+ **Arch Linux:**
290
+
291
+ ```bash
292
+ sudo pacman -S base-devel python
293
+ ```
294
+
295
+ If these are missing, `cf memory init` will fail at the "Installing SQLite dependencies" step. You can still use Tier 2 (lite) or Tier 3 (markdown) without native dependencies — choose them during the init wizard.
296
+
297
+ **macOS** users need Xcode Command Line Tools: `xcode-select --install`.
298
+
271
299
  ## Environment Variables
272
300
 
273
301
  | Variable | Default | Description |
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-friend-cf-memory",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
- import { mkdirSync, rmSync, existsSync, readFileSync } from "fs";
2
+ import { mkdirSync, rmSync, existsSync, readFileSync, readdirSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { tmpdir } from "os";
5
5
  import matter from "gray-matter";
@@ -85,6 +85,46 @@ describe("MarkdownBackend", () => {
85
85
  expect(m2.slug).toContain("api-authentication-pattern-");
86
86
  });
87
87
 
88
+ it("index_only=true returns Memory without writing when file exists", async () => {
89
+ // Pre-create the file via normal store
90
+ const original = await backend.store(sampleInput);
91
+ const filePath = join(
92
+ testDir,
93
+ "features",
94
+ "api-authentication-pattern.md",
95
+ );
96
+ expect(existsSync(filePath)).toBe(true);
97
+
98
+ // Snapshot file content before index_only call
99
+ const contentBefore = readFileSync(filePath, "utf-8");
100
+
101
+ // Now store with index_only — should NOT create a second file
102
+ const indexed = await backend.store({ ...sampleInput, index_only: true });
103
+
104
+ // Should return clean slug (no timestamp suffix)
105
+ expect(indexed.slug).toBe("api-authentication-pattern");
106
+ expect(indexed.id).toBe("features/api-authentication-pattern");
107
+ expect(indexed.category).toBe("features");
108
+ expect(indexed.frontmatter.title).toBe(sampleInput.title);
109
+ expect(indexed.frontmatter.type).toBe(sampleInput.type);
110
+ expect(indexed.content).toBe(sampleInput.content);
111
+
112
+ // Verify no duplicate file was created (only one .md file in features/)
113
+ const files = readdirSync(join(testDir, "features")).filter((f: string) =>
114
+ f.endsWith(".md"),
115
+ );
116
+ expect(files.length).toBe(1);
117
+
118
+ // Verify existing file was not modified
119
+ expect(readFileSync(filePath, "utf-8")).toBe(contentBefore);
120
+ });
121
+
122
+ it("index_only=true throws when file does not exist", async () => {
123
+ await expect(
124
+ backend.store({ ...sampleInput, index_only: true }),
125
+ ).rejects.toThrow(/index_only.*file not found/i);
126
+ });
127
+
88
128
  it("respects custom importance and source", async () => {
89
129
  const memory = await backend.store({
90
130
  ...sampleInput,
@@ -112,18 +112,11 @@ export class MarkdownBackend implements MemoryBackend {
112
112
  fs.mkdirSync(catDir, { recursive: true });
113
113
  }
114
114
 
115
- let slug = slugify(input.title);
115
+ const slug = slugify(input.title);
116
116
  const filePath = path.join(catDir, `${slug}.md`);
117
-
118
- // Handle duplicate slugs
119
- if (fs.existsSync(filePath)) {
120
- slug = `${slug}-${Date.now()}`;
121
- }
122
-
123
- const finalPath = path.join(catDir, `${slug}.md`);
124
117
  const now = today();
125
118
 
126
- const frontmatter: MemoryFrontmatter = {
119
+ const buildFrontmatter = (): MemoryFrontmatter => ({
127
120
  title: input.title,
128
121
  description: input.description,
129
122
  type: input.type,
@@ -132,7 +125,30 @@ export class MarkdownBackend implements MemoryBackend {
132
125
  created: now,
133
126
  updated: now,
134
127
  source: input.source ?? "conversation",
135
- };
128
+ });
129
+
130
+ // index_only: verify file exists, return Memory from input without writing
131
+ if (input.index_only) {
132
+ if (!fs.existsSync(filePath)) {
133
+ throw new Error(`index_only: file not found for "${category}/${slug}"`);
134
+ }
135
+ return {
136
+ id: `${category}/${slug}`,
137
+ slug,
138
+ category,
139
+ frontmatter: buildFrontmatter(),
140
+ content: input.content,
141
+ };
142
+ }
143
+
144
+ // Handle duplicate slugs
145
+ let finalSlug = slug;
146
+ if (fs.existsSync(filePath)) {
147
+ finalSlug = `${slug}-${Date.now()}`;
148
+ }
149
+
150
+ const finalPath = path.join(catDir, `${finalSlug}.md`);
151
+ const frontmatter = buildFrontmatter();
136
152
 
137
153
  const doc = matter.stringify(
138
154
  input.content,
@@ -141,8 +157,8 @@ export class MarkdownBackend implements MemoryBackend {
141
157
  fs.writeFileSync(finalPath, doc, "utf-8");
142
158
 
143
159
  return {
144
- id: `${category}/${slug}`,
145
- slug,
160
+ id: `${category}/${finalSlug}`,
161
+ slug: finalSlug,
146
162
  category,
147
163
  frontmatter,
148
164
  content: input.content,
@@ -71,6 +71,7 @@ export interface StoreInput {
71
71
  content: string;
72
72
  importance?: number;
73
73
  source?: string;
74
+ index_only?: boolean;
74
75
  }
75
76
 
76
77
  export interface SearchInput {
@@ -26,8 +26,23 @@ export function registerStore(server: McpServer, backend: MemoryBackend): void {
26
26
  .string()
27
27
  .optional()
28
28
  .describe("Source: conversation, auto-capture, manual"),
29
+ index_only: z
30
+ .boolean()
31
+ .optional()
32
+ .describe(
33
+ "When true, skip file creation and return a Memory object for indexing. File must already exist on disk.",
34
+ ),
29
35
  },
30
- async ({ title, description, type, tags, content, importance, source }) => {
36
+ async ({
37
+ title,
38
+ description,
39
+ type,
40
+ tags,
41
+ content,
42
+ importance,
43
+ source,
44
+ index_only,
45
+ }) => {
31
46
  const input = {
32
47
  title,
33
48
  description,
@@ -36,13 +51,16 @@ export function registerStore(server: McpServer, backend: MemoryBackend): void {
36
51
  content,
37
52
  importance,
38
53
  source,
54
+ index_only,
39
55
  };
40
56
 
41
57
  let dedup: Awaited<ReturnType<typeof checkDuplicate>> | null = null;
42
- try {
43
- dedup = await checkDuplicate(backend, input);
44
- } catch {
45
- // Dedup is best-effort — don't block store on search errors
58
+ if (!index_only) {
59
+ try {
60
+ dedup = await checkDuplicate(backend, input);
61
+ } catch {
62
+ // Dedup is best-effort — don't block store on search errors
63
+ }
46
64
  }
47
65
 
48
66
  const memory = await backend.store(input);
@@ -1,5 +1,10 @@
1
1
  # Changelog (Learn Host)
2
2
 
3
+ ## v0.3.0 (2026-03-21)
4
+
5
+ - Improve date display — show only updated date when different from created date, hide if same [#1fc2e65](https://github.com/dinhanhthi/coding-friend/commit/1fc2e65)
6
+ - Increase sidebar font size for better readability [#1fc2e65](https://github.com/dinhanhthi/coding-friend/commit/1fc2e65)
7
+
3
8
  ## v0.2.1 (2026-03-05)
4
9
 
5
10
  - Fix TOC heading text stripping markdown links from slug generation ([#9a8fb5c](https://github.com/dinhanhthi/coding-friend/commit/9a8fb5c))
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-friend-learn-host",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "dev": "next dev -p 3333",
@@ -45,9 +45,10 @@ export default async function DocPage({
45
45
  {doc.frontmatter.created && (
46
46
  <span>Created: {doc.frontmatter.created}</span>
47
47
  )}
48
- {doc.frontmatter.updated && (
49
- <span>Updated: {doc.frontmatter.updated}</span>
50
- )}
48
+ {doc.frontmatter.updated &&
49
+ doc.frontmatter.updated !== doc.frontmatter.created && (
50
+ <span>Updated: {doc.frontmatter.updated}</span>
51
+ )}
51
52
  </div>
52
53
  )}
53
54
  {doc.frontmatter.tags.length > 0 && (
@@ -22,7 +22,7 @@ export default function Breadcrumbs({ crumbs }: { crumbs: Crumb[] }) {
22
22
  {crumb.label}
23
23
  </Link>
24
24
  ) : (
25
- <span className="text-slate-700 dark:text-slate-300">
25
+ <span className="text-slate-800 dark:text-slate-200">
26
26
  {crumb.label}
27
27
  </span>
28
28
  )}
@@ -43,7 +43,10 @@ export default function DocCard({ doc }: { doc: DocMeta }) {
43
43
  )}
44
44
  </div>
45
45
  <span className="text-xs text-slate-400">
46
- {doc.frontmatter.updated || doc.frontmatter.created}
46
+ {doc.frontmatter.updated &&
47
+ doc.frontmatter.updated !== doc.frontmatter.created
48
+ ? doc.frontmatter.updated
49
+ : doc.frontmatter.created}
47
50
  </span>
48
51
  </div>
49
52
  </div>
@@ -23,7 +23,7 @@ export default function Sidebar({
23
23
  <Link
24
24
  key={cat.name}
25
25
  href={`/${cat.name}/`}
26
- className={`flex items-center justify-between rounded-full py-1.5 pr-2 pl-4 text-sm capitalize transition-colors duration-200 ${
26
+ className={`flex items-center justify-between rounded-full py-1.5 pr-2 pl-4 text-[0.938rem] capitalize transition-colors duration-200 ${
27
27
  isActive
28
28
  ? "font-medium text-amber-700 dark:text-amber-400"
29
29
  : "dark:hover:bg-navy-800/70 text-slate-600 hover:bg-slate-200/50 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-friend-cli",
3
- "version": "1.19.0",
3
+ "version": "1.20.0",
4
4
  "description": "CLI for coding-friend — host learning docs, setup MCP server, initialize projects",
5
5
  "type": "module",
6
6
  "bin": {