coding-friend-cli 1.17.1 → 1.17.4

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
@@ -70,8 +70,8 @@ cf memory list --projects # List all project databases with size and metadata
70
70
  cf memory rm --project-id <id> # Remove a specific project database
71
71
  cf memory rm --all # Remove all project databases
72
72
  cf memory rm --prune # Remove orphaned projects (source dir missing or 0 memories)
73
- cf memory start # Start memory daemon (enables Tier 2 search)
74
- cf memory stop # Stop memory daemon
73
+ cf memory start-daemon # Start memory daemon (enables Tier 2 search)
74
+ cf memory stop-daemon # Stop memory daemon
75
75
  cf memory rebuild # Rebuild search index from markdown files
76
76
  cf memory init # Initialize SQLite backend (Tier 1) and import memories
77
77
  cf memory mcp # Show MCP server config for clients
@@ -33,7 +33,7 @@ _cf_completions() {
33
33
 
34
34
  # Subcommands for 'memory'
35
35
  if [[ "\${COMP_WORDS[1]}" == "memory" && \${COMP_CWORD} -eq 2 ]]; then
36
- COMPREPLY=($(compgen -W "status search list rm init start stop rebuild mcp" -- "$cur"))
36
+ COMPREPLY=($(compgen -W "status search list rm init start-daemon stop-daemon rebuild mcp" -- "$cur"))
37
37
  return
38
38
  fi
39
39
 
@@ -129,8 +129,8 @@ var ZSH_FUNCTION_BODY = `_cf() {
129
129
  'list:List memories (--projects for all DBs)'
130
130
  'rm:Remove a project database'
131
131
  'init:Initialize Tier 1 (SQLite + Hybrid Search)'
132
- 'start:Start the memory daemon (Tier 2)'
133
- 'stop:Stop the memory daemon'
132
+ 'start-daemon:Start the memory daemon (Tier 2)'
133
+ 'stop-daemon:Stop the memory daemon'
134
134
  'rebuild:Rebuild the daemon search index'
135
135
  'mcp:Show MCP server setup instructions'
136
136
  )
@@ -198,8 +198,8 @@ complete -c cf -n "__fish_seen_subcommand_from memory" -a search -d "Search memo
198
198
  complete -c cf -n "__fish_seen_subcommand_from memory" -a list -d "List memories (--projects for all DBs)"
199
199
  complete -c cf -n "__fish_seen_subcommand_from memory" -a rm -d "Remove a project database"
200
200
  complete -c cf -n "__fish_seen_subcommand_from memory" -a init -d "Initialize Tier 1 (SQLite + Hybrid Search)"
201
- complete -c cf -n "__fish_seen_subcommand_from memory" -a start -d "Start the memory daemon (Tier 2)"
202
- complete -c cf -n "__fish_seen_subcommand_from memory" -a stop -d "Stop the memory daemon"
201
+ complete -c cf -n "__fish_seen_subcommand_from memory" -a start-daemon -d "Start the memory daemon (Tier 2)"
202
+ complete -c cf -n "__fish_seen_subcommand_from memory" -a stop-daemon -d "Stop the memory daemon"
203
203
  complete -c cf -n "__fish_seen_subcommand_from memory" -a rebuild -d "Rebuild the daemon search index"
204
204
  complete -c cf -n "__fish_seen_subcommand_from memory" -a mcp -d "Show MCP server setup instructions"
205
205
  # Session subcommands
@@ -213,7 +213,7 @@ Register-ArgumentCompleter -Native -CommandName cf -ScriptBlock {
213
213
  param($wordToComplete, $commandAst, $cursorPosition)
214
214
  $commands = @('install','uninstall','disable','enable','init','config','host','mcp','memory','permission','statusline','update','dev','session')
215
215
  $devSubcommands = @('on','off','status','restart','sync','update')
216
- $memorySubcommands = @('status','search','list','rm','init','start','stop','rebuild','mcp')
216
+ $memorySubcommands = @('status','search','list','rm','init','start-daemon','stop-daemon','rebuild','mcp')
217
217
  $sessionSubcommands = @('save','load')
218
218
  $scopeFlags = @('--user','--global','--project','--local')
219
219
  $updateFlags = @('--cli','--plugin','--statusline','--user','--global','--project','--local')
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-ORACWEDN.js";
5
5
  import {
6
6
  ensureShellCompletion
7
- } from "./chunk-YO6JKGR3.js";
7
+ } from "./chunk-DVMWMXDZ.js";
8
8
  import {
9
9
  resolveScope
10
10
  } from "./chunk-C5LYVVEI.js";
@@ -13,7 +13,7 @@ import {
13
13
  ensureShellCompletion,
14
14
  hasShellCompletion,
15
15
  removeShellCompletion
16
- } from "./chunk-YO6JKGR3.js";
16
+ } from "./chunk-DVMWMXDZ.js";
17
17
  import {
18
18
  BACK,
19
19
  applyDocsDirChange,
@@ -8,7 +8,7 @@ import {
8
8
  import "./chunk-POC2WHU2.js";
9
9
  import {
10
10
  ensureShellCompletion
11
- } from "./chunk-YO6JKGR3.js";
11
+ } from "./chunk-DVMWMXDZ.js";
12
12
  import {
13
13
  commandExists,
14
14
  run
package/dist/index.js CHANGED
@@ -14,11 +14,11 @@ 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-Q4PWEU43.js");
17
+ const { installCommand } = await import("./install-USFLRCS5.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) => {
21
- const { uninstallCommand } = await import("./uninstall-3PSUDGI4.js");
21
+ const { uninstallCommand } = await import("./uninstall-QSNKGNHR.js");
22
22
  await uninstallCommand(opts);
23
23
  });
24
24
  program.command("disable").description("Disable the Coding Friend plugin without uninstalling").option("--user", "Disable at user scope (all projects)").option("--global", "Disable at user scope (all projects)").option("--project", "Disable at project scope").option("--local", "Disable at local scope").action(async (opts) => {
@@ -30,11 +30,11 @@ program.command("enable").description("Re-enable the Coding Friend plugin").opti
30
30
  await enableCommand(opts);
31
31
  });
32
32
  program.command("init").description("Initialize coding-friend in current project").action(async () => {
33
- const { initCommand } = await import("./init-MF7ISADJ.js");
33
+ const { initCommand } = await import("./init-73ECEDU7.js");
34
34
  await initCommand();
35
35
  });
36
36
  program.command("config").description("Manage Coding Friend configuration").action(async () => {
37
- const { configCommand } = await import("./config-LZFXXOI4.js");
37
+ const { configCommand } = await import("./config-HVWEV2K6.js");
38
38
  await configCommand();
39
39
  });
40
40
  program.command("host").description("Build and serve learning docs as a static website").argument("[path]", "path to docs folder").option("-p, --port <port>", "port number", "3333").action(async (path, opts) => {
@@ -54,7 +54,7 @@ program.command("statusline").description("Setup coding-friend statusline in Cla
54
54
  await statuslineCommand();
55
55
  });
56
56
  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) => {
57
- const { updateCommand } = await import("./update-WL6SFGGO.js");
57
+ const { updateCommand } = await import("./update-PNHTIB6M.js");
58
58
  await updateCommand(opts);
59
59
  });
60
60
  var session = program.command("session").description("Save and load Claude Code sessions across machines");
@@ -86,45 +86,45 @@ Memory subcommands:
86
86
  memory list List memories in current project (--projects for all DBs)
87
87
  memory rm Remove a project database (--project-id <id>, --all, or --prune)
88
88
  memory init Initialize Tier 1 (install SQLite deps, import existing memories)
89
- memory start Start the memory daemon (Tier 2)
90
- memory stop Stop the memory daemon
89
+ memory start-daemon Start the memory daemon (Tier 2)
90
+ memory stop-daemon Stop the memory daemon
91
91
  memory rebuild Rebuild the daemon search index
92
92
  memory mcp Show MCP server setup instructions`
93
93
  );
94
94
  memory.command("status").description("Show memory system status").action(async () => {
95
- const { memoryStatusCommand } = await import("./memory-RGLM35HC.js");
95
+ const { memoryStatusCommand } = await import("./memory-BQK2R7BV.js");
96
96
  await memoryStatusCommand();
97
97
  });
98
98
  memory.command("search").description("Search memories by query").argument("<query>", "search query").action(async (query) => {
99
- const { memorySearchCommand } = await import("./memory-RGLM35HC.js");
99
+ const { memorySearchCommand } = await import("./memory-BQK2R7BV.js");
100
100
  await memorySearchCommand(query);
101
101
  });
102
102
  memory.command("list").description(
103
103
  "List memories in current project, or all projects with --projects"
104
104
  ).option("--projects", "List all project databases with size and metadata").action(async (opts) => {
105
- const { memoryListCommand } = await import("./memory-RGLM35HC.js");
105
+ const { memoryListCommand } = await import("./memory-BQK2R7BV.js");
106
106
  await memoryListCommand(opts);
107
107
  });
108
108
  memory.command("init").description(
109
109
  "Initialize Tier 1 \u2014 install SQLite deps and import existing memories"
110
110
  ).action(async () => {
111
- const { memoryInitCommand } = await import("./memory-RGLM35HC.js");
111
+ const { memoryInitCommand } = await import("./memory-BQK2R7BV.js");
112
112
  await memoryInitCommand();
113
113
  });
114
- memory.command("start").description("Start the memory daemon (Tier 2 \u2014 MiniSearch)").action(async () => {
115
- const { memoryStartCommand } = await import("./memory-RGLM35HC.js");
116
- await memoryStartCommand();
114
+ memory.command("start-daemon").description("Start the memory daemon (Tier 2 \u2014 MiniSearch)").action(async () => {
115
+ const { memoryStartDaemonCommand } = await import("./memory-BQK2R7BV.js");
116
+ await memoryStartDaemonCommand();
117
117
  });
118
- memory.command("stop").description("Stop the memory daemon").action(async () => {
119
- const { memoryStopCommand } = await import("./memory-RGLM35HC.js");
120
- await memoryStopCommand();
118
+ memory.command("stop-daemon").description("Stop the memory daemon").action(async () => {
119
+ const { memoryStopDaemonCommand } = await import("./memory-BQK2R7BV.js");
120
+ await memoryStopDaemonCommand();
121
121
  });
122
122
  memory.command("rebuild").description("Rebuild the daemon search index").action(async () => {
123
- const { memoryRebuildCommand } = await import("./memory-RGLM35HC.js");
123
+ const { memoryRebuildCommand } = await import("./memory-BQK2R7BV.js");
124
124
  await memoryRebuildCommand();
125
125
  });
126
126
  memory.command("mcp").description("Show MCP server setup instructions").action(async () => {
127
- const { memoryMcpCommand } = await import("./memory-RGLM35HC.js");
127
+ const { memoryMcpCommand } = await import("./memory-BQK2R7BV.js");
128
128
  await memoryMcpCommand();
129
129
  });
130
130
  memory.command("rm").description("Remove a project database").option("--project-id <id>", "Project ID to remove").option("--all", "Remove all project databases").option(
@@ -132,7 +132,7 @@ memory.command("rm").description("Remove a project database").option("--project-
132
132
  "Remove orphaned projects (source dir missing or 0 memories)"
133
133
  ).action(
134
134
  async (opts) => {
135
- const { memoryRmCommand } = await import("./memory-RGLM35HC.js");
135
+ const { memoryRmCommand } = await import("./memory-BQK2R7BV.js");
136
136
  await memoryRmCommand(opts);
137
137
  }
138
138
  );
@@ -149,35 +149,35 @@ Dev subcommands:
149
149
  dev update [path] Update local dev plugin to latest version`
150
150
  );
151
151
  dev.command("on").description("Switch to local plugin source").argument("[path]", "path to local coding-friend repo (default: cwd)").action(async (path) => {
152
- const { devOnCommand } = await import("./dev-R3IYWZ3M.js");
152
+ const { devOnCommand } = await import("./dev-AZSOM775.js");
153
153
  await devOnCommand(path);
154
154
  });
155
155
  dev.command("off").description("Switch back to remote marketplace").action(async () => {
156
- const { devOffCommand } = await import("./dev-R3IYWZ3M.js");
156
+ const { devOffCommand } = await import("./dev-AZSOM775.js");
157
157
  await devOffCommand();
158
158
  });
159
159
  dev.command("status").description("Show current dev mode").action(async () => {
160
- const { devStatusCommand } = await import("./dev-R3IYWZ3M.js");
160
+ const { devStatusCommand } = await import("./dev-AZSOM775.js");
161
161
  await devStatusCommand();
162
162
  });
163
163
  dev.command("sync").description(
164
164
  "Copy local source files to plugin cache (no version bump needed)"
165
165
  ).action(async () => {
166
- const { devSyncCommand } = await import("./dev-R3IYWZ3M.js");
166
+ const { devSyncCommand } = await import("./dev-AZSOM775.js");
167
167
  await devSyncCommand();
168
168
  });
169
169
  dev.command("restart").description("Reinstall local dev plugin (off + on)").argument(
170
170
  "[path]",
171
171
  "path to local coding-friend repo (default: saved path or cwd)"
172
172
  ).action(async (path) => {
173
- const { devRestartCommand } = await import("./dev-R3IYWZ3M.js");
173
+ const { devRestartCommand } = await import("./dev-AZSOM775.js");
174
174
  await devRestartCommand(path);
175
175
  });
176
176
  dev.command("update").description("Update local dev plugin to latest version (off + on)").argument(
177
177
  "[path]",
178
178
  "path to local coding-friend repo (default: saved path or cwd)"
179
179
  ).action(async (path) => {
180
- const { devUpdateCommand } = await import("./dev-R3IYWZ3M.js");
180
+ const { devUpdateCommand } = await import("./dev-AZSOM775.js");
181
181
  await devUpdateCommand(path);
182
182
  });
183
183
  program.parse();
@@ -20,7 +20,7 @@ import {
20
20
  import {
21
21
  ensureShellCompletion,
22
22
  hasShellCompletion
23
- } from "./chunk-YO6JKGR3.js";
23
+ } from "./chunk-DVMWMXDZ.js";
24
24
  import {
25
25
  BACK,
26
26
  applyDocsDirChange,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getLatestVersion,
3
3
  semverCompare
4
- } from "./chunk-G6CEEMAR.js";
4
+ } from "./chunk-UWQPMVJY.js";
5
5
  import {
6
6
  enableMarketplaceAutoUpdate,
7
7
  isMarketplaceRegistered,
@@ -13,7 +13,7 @@ import {
13
13
  import "./chunk-POC2WHU2.js";
14
14
  import {
15
15
  ensureShellCompletion
16
- } from "./chunk-YO6JKGR3.js";
16
+ } from "./chunk-DVMWMXDZ.js";
17
17
  import {
18
18
  resolveScope
19
19
  } from "./chunk-C5LYVVEI.js";
@@ -15,7 +15,6 @@ import {
15
15
  } from "./chunk-W5CD7WTX.js";
16
16
 
17
17
  // src/commands/memory.ts
18
- import { createHash } from "crypto";
19
18
  import { existsSync, readdirSync, statSync, rmSync } from "fs";
20
19
  import { join, resolve, sep } from "path";
21
20
  import { homedir } from "os";
@@ -87,7 +86,7 @@ async function memoryStatusCommand() {
87
86
  if (running && daemonInfo) {
88
87
  const uptime = (Date.now() - daemonInfo.startedAt) / 1e3;
89
88
  log.info(
90
- `Daemon: ${chalk.green("running")} (PID ${daemonInfo.pid}, uptime ${formatUptime(uptime)}) ${chalk.dim('Turn it off by "cf memory stop"')}`
89
+ `Daemon: ${chalk.green("running")} (PID ${daemonInfo.pid}, uptime ${formatUptime(uptime)}) ${chalk.dim('Turn it off by "cf memory stop-daemon"')}`
91
90
  );
92
91
  } else if (sqliteAvailable) {
93
92
  log.info(
@@ -95,7 +94,7 @@ async function memoryStatusCommand() {
95
94
  );
96
95
  } else {
97
96
  log.info(
98
- `Daemon: ${chalk.dim("stopped")} ${chalk.dim('(run "cf memory start" for Tier 2 search)')}`
97
+ `Daemon: ${chalk.dim("stopped")} ${chalk.dim('(run "cf memory start-daemon" for Tier 2 search)')}`
99
98
  );
100
99
  }
101
100
  if (sqliteAvailable) {
@@ -204,7 +203,7 @@ async function memoryListCommand(opts) {
204
203
  console.log(result);
205
204
  }
206
205
  }
207
- async function memoryStartCommand() {
206
+ async function memoryStartDaemonCommand() {
208
207
  const memoryDir = getMemoryDir();
209
208
  const mcpDir = getLibPath("cf-memory");
210
209
  ensureBuilt(mcpDir);
@@ -226,7 +225,7 @@ async function memoryStartCommand() {
226
225
  process.exit(1);
227
226
  }
228
227
  }
229
- async function memoryStopCommand() {
228
+ async function memoryStopDaemonCommand() {
230
229
  const mcpDir = getLibPath("cf-memory");
231
230
  ensureBuilt(mcpDir);
232
231
  const { stopDaemon, isDaemonRunning } = await import(join(mcpDir, "dist/daemon/process.js"));
@@ -273,7 +272,7 @@ async function memoryRebuildCommand() {
273
272
  if (!await isDaemonRunning()) {
274
273
  log.info("No SQLite deps and daemon not running. Nothing to rebuild.");
275
274
  log.dim("Install Tier 1 deps: cf memory init");
276
- log.dim("Or start the daemon: cf memory start");
275
+ log.dim("Or start the daemon: cf memory start-daemon");
277
276
  return;
278
277
  }
279
278
  const { DaemonClient } = await import(join(mcpDir, "dist/lib/daemon-client.js"));
@@ -362,6 +361,8 @@ function formatSize(bytes) {
362
361
  return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
363
362
  }
364
363
  function formatDate(raw) {
364
+ if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(raw)) return raw;
365
+ if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) return raw;
365
366
  const d = new Date(raw);
366
367
  if (isNaN(d.getTime())) return raw;
367
368
  return d.toISOString().slice(0, 16).replace("T", " ");
@@ -441,12 +442,13 @@ async function memoryListProjectsCommand() {
441
442
  return;
442
443
  }
443
444
  const currentMemoryDir = resolve(getMemoryDir());
444
- const currentHash = createHash("sha256").update(currentMemoryDir).digest("hex").slice(0, 12);
445
+ const { projectId } = await import(join(mcpDir, "dist/backends/sqlite/index.js"));
446
+ const currentProjectId = projectId(currentMemoryDir);
445
447
  log.step(`Scanning ${dirs.length} project(s)...
446
448
  `);
447
449
  const projects = [];
448
450
  for (const id of dirs) {
449
- const knownDir = id === currentHash ? currentMemoryDir : void 0;
451
+ const knownDir = id === currentProjectId ? currentMemoryDir : void 0;
450
452
  projects.push(getProjectInfo(join(baseDir, id), id, mcpDir, knownDir));
451
453
  }
452
454
  projects.sort((a, b) => b.size - a.size);
@@ -641,7 +643,7 @@ export {
641
643
  memoryRebuildCommand,
642
644
  memoryRmCommand,
643
645
  memorySearchCommand,
644
- memoryStartCommand,
646
+ memoryStartDaemonCommand,
645
647
  memoryStatusCommand,
646
- memoryStopCommand
648
+ memoryStopDaemonCommand
647
649
  };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ensureShellCompletion
4
- } from "./chunk-YO6JKGR3.js";
4
+ } from "./chunk-DVMWMXDZ.js";
5
5
  import "./chunk-W5CD7WTX.js";
6
6
 
7
7
  // src/postinstall.ts
@@ -5,7 +5,7 @@ import {
5
5
  import {
6
6
  hasShellCompletion,
7
7
  removeShellCompletion
8
- } from "./chunk-YO6JKGR3.js";
8
+ } from "./chunk-DVMWMXDZ.js";
9
9
  import {
10
10
  resolveScope
11
11
  } from "./chunk-C5LYVVEI.js";
@@ -2,10 +2,10 @@ import {
2
2
  getLatestVersion,
3
3
  semverCompare,
4
4
  updateCommand
5
- } from "./chunk-G6CEEMAR.js";
5
+ } from "./chunk-UWQPMVJY.js";
6
6
  import "./chunk-ORACWEDN.js";
7
7
  import "./chunk-POC2WHU2.js";
8
- import "./chunk-YO6JKGR3.js";
8
+ import "./chunk-DVMWMXDZ.js";
9
9
  import "./chunk-C5LYVVEI.js";
10
10
  import "./chunk-CYQU33FY.js";
11
11
  import "./chunk-RWUTFVRB.js";
@@ -1,5 +1,14 @@
1
1
  # CF Memory Changelog
2
2
 
3
+ ## v0.1.3 (2026-03-19)
4
+
5
+ - 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)
6
+ - Rename `cf memory start`/`stop` to `cf memory start-daemon`/`stop-daemon` in documentation [#acbe789](https://github.com/dinhanhthi/coding-friend/commit/acbe789)
7
+
8
+ ## v0.1.1 (2026-03-17)
9
+
10
+ - Fix `today()` to capture full timestamp (`YYYY-MM-DD HH:MM`) instead of date-only for memory created/updated fields [#31e0824](https://github.com/dinhanhthi/coding-friend/commit/31e0824)
11
+
3
12
  ## v0.1.0 (2026-03-17)
4
13
 
5
14
  - Auto-start daemon from MCP server for file watching — daemon spawns automatically when Tier 1 or 2 is detected, no manual `cf memory start` needed [#2211b84](https://github.com/dinhanhthi/coding-friend/commit/2211b84)
@@ -262,8 +262,8 @@ The `cf` CLI exposes memory commands that use this package:
262
262
  | `cf memory status` | Show current tier, daemon status, memory count |
263
263
  | `cf memory search <query>` | Search memories from the terminal |
264
264
  | `cf memory list` | List all stored memories |
265
- | `cf memory start` | Start the MiniSearch daemon (Tier 2) |
266
- | `cf memory stop` | Stop the daemon |
265
+ | `cf memory start-daemon` | Start the MiniSearch daemon (Tier 2) |
266
+ | `cf memory stop-daemon` | Stop the daemon |
267
267
  | `cf memory rebuild` | Rebuild search index (Tier 1 direct or via daemon) |
268
268
  | `cf memory init` | Install Tier 1 deps + import existing memories into SQLite |
269
269
  | `cf memory mcp` | Print MCP server config for use in Claude Desktop / other clients |
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-friend-cf-memory",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { projectId } from "../backends/sqlite/index.js";
3
+
4
+ describe("projectId", () => {
5
+ it("strips /docs/memory suffix and encodes path", () => {
6
+ expect(projectId("/Users/thi/git/coding-friend/docs/memory")).toBe(
7
+ "-Users-thi-git-coding-friend",
8
+ );
9
+ });
10
+
11
+ it("strips /memory suffix when docsDir is custom", () => {
12
+ expect(projectId("/Users/thi/git/foo/memory")).toBe("-Users-thi-git-foo");
13
+ });
14
+
15
+ it("handles path without known suffix", () => {
16
+ expect(projectId("/Users/thi/git/foo/custom-docs")).toBe(
17
+ "-Users-thi-git-foo-custom-docs",
18
+ );
19
+ });
20
+
21
+ it("strips trailing slash before encoding", () => {
22
+ expect(projectId("/Users/thi/git/foo/docs/memory/")).toBe(
23
+ projectId("/Users/thi/git/foo/docs/memory"),
24
+ );
25
+ });
26
+
27
+ it("strips multiple trailing slashes", () => {
28
+ expect(projectId("/Users/thi/git/foo/docs/memory///")).toBe(
29
+ "-Users-thi-git-foo",
30
+ );
31
+ });
32
+
33
+ it("produces same result with and without trailing slash", () => {
34
+ const withSlash = projectId("/a/b/c/docs/memory/");
35
+ const withoutSlash = projectId("/a/b/c/docs/memory");
36
+ expect(withSlash).toBe(withoutSlash);
37
+ expect(withSlash).toBe("-a-b-c");
38
+ });
39
+
40
+ it("handles deeply nested paths", () => {
41
+ expect(projectId("/a/b/c/d/e/f/g")).toBe("-a-b-c-d-e-f-g");
42
+ });
43
+ });
@@ -57,7 +57,9 @@ function parseFrontmatter(
57
57
  }
58
58
 
59
59
  function today(): string {
60
- return new Date().toISOString().split("T")[0];
60
+ const d = new Date();
61
+ const pad = (n: number) => String(n).padStart(2, "0");
62
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
61
63
  }
62
64
 
63
65
  export class MarkdownBackend implements MemoryBackend {
@@ -7,11 +7,10 @@
7
7
  * Markdown files remain the source of truth. SQLite is a derived index
8
8
  * that can be rebuilt from markdown at any time.
9
9
  *
10
- * DB path: ~/.coding-friend/memory/projects/{12-char-sha256}/db.sqlite
10
+ * DB path: ~/.coding-friend/memory/projects/{encoded-path}/db.sqlite
11
11
  */
12
12
  import fs from "node:fs";
13
13
  import path from "node:path";
14
- import crypto from "node:crypto";
15
14
  import type { MemoryBackend } from "../../lib/backend.js";
16
15
  import { MarkdownBackend } from "../markdown.js";
17
16
  import {
@@ -46,14 +45,17 @@ import {
46
45
  import { hybridSearch } from "./search.js";
47
46
 
48
47
  /**
49
- * Compute a short hash of the docsDir path for project isolation.
48
+ * Derive the project root from a docsDir path by stripping known suffixes.
49
+ * Then encode it into a filesystem-safe directory name for project isolation.
50
+ * Uses the same encoding as Claude Code's project directories:
51
+ * resolve + strip known suffixes + strip trailing slashes + replace all "/" with "-"
52
+ * e.g. /Users/thi/git/foo/docs/memory → -Users-thi-git-foo
50
53
  */
51
- function projectHash(docsDir: string): string {
52
- return crypto
53
- .createHash("sha256")
54
- .update(path.resolve(docsDir))
55
- .digest("hex")
56
- .slice(0, 12);
54
+ export function projectId(docsDir: string): string {
55
+ let resolved = path.resolve(docsDir).replace(/\/+$/, "");
56
+ // Strip known memory directory suffixes to get the project root
57
+ resolved = resolved.replace(/\/docs\/memory$/, "").replace(/\/memory$/, "");
58
+ return resolved.replace(/\//g, "-");
57
59
  }
58
60
 
59
61
  export interface SqliteBackendOptions {
@@ -83,8 +85,8 @@ export class SqliteBackend implements MemoryBackend {
83
85
  this.dbPath = opts.dbPath;
84
86
  } else {
85
87
  const depsDir = opts?.depsDir ?? this.getDefaultDepsDir();
86
- const hash = projectHash(docsDir);
87
- const projectDir = path.join(depsDir, "projects", hash);
88
+ const id = projectId(docsDir);
89
+ const projectDir = path.join(depsDir, "projects", id);
88
90
  this.dbPath = path.join(projectDir, "db.sqlite");
89
91
  }
90
92
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-friend-cli",
3
- "version": "1.17.1",
3
+ "version": "1.17.4",
4
4
  "description": "CLI for coding-friend — host learning docs, setup MCP server, initialize projects",
5
5
  "type": "module",
6
6
  "bin": {