coding-friend-cli 1.27.1 → 1.28.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.
Files changed (31) hide show
  1. package/README.md +9 -3
  2. package/dist/{chunk-PRIH34UB.js → chunk-DHH6SRXV.js} +5 -1
  3. package/dist/{chunk-75IEE2OE.js → chunk-E7OY4UWW.js} +2 -2
  4. package/dist/chunk-J4N2ODQ5.js +191 -0
  5. package/dist/{chunk-YCWRBOB3.js → chunk-RGQGKOBH.js} +1 -1
  6. package/dist/{chunk-GDZTU2Z4.js → chunk-XQ5KAXKL.js} +1 -1
  7. package/dist/{chunk-BX3DRKTU.js → chunk-XROT7L2F.js} +6 -5
  8. package/dist/{config-N55LTJ76.js → config-U2WEZXF6.js} +4 -4
  9. package/dist/{dev-BSXR55PZ.js → dev-4OAJCQRQ.js} +29 -5
  10. package/dist/guide-F2FLCDYO.js +113 -0
  11. package/dist/{host-JVLWYY5A.js → host-2KBOPER6.js} +2 -2
  12. package/dist/index.js +35 -26
  13. package/dist/{init-KX6Y3ELD.js → init-R5L2GAFV.js} +6 -6
  14. package/dist/{install-G4XYNVL3.js → install-XZC6UO54.js} +3 -3
  15. package/dist/{mcp-DABSO5PE.js → mcp-CNZLJ57B.js} +4 -4
  16. package/dist/{memory-7Q5P7LIU.js → memory-KU2MOCPQ.js} +4 -4
  17. package/dist/{session-DMQZXZ7F.js → session-ZGNQKV4M.js} +26 -7
  18. package/dist/{status-K7TBUAXT.js → status-JXUXVHRH.js} +4 -4
  19. package/dist/{statusline-CDGADB3C.js → statusline-RMTO4MQA.js} +2 -2
  20. package/dist/{update-6M3YFHS6.js → update-HS6A2C5V.js} +3 -3
  21. package/lib/cf-memory/README.md +4 -4
  22. package/lib/cf-memory/src/__tests__/claude-md.test.ts +105 -0
  23. package/lib/cf-memory/src/__tests__/tier.test.ts +59 -0
  24. package/lib/cf-memory/src/daemon/process.ts +1 -1
  25. package/lib/cf-memory/src/lib/claude-md.ts +12 -3
  26. package/lib/cf-memory/src/lib/tier.ts +19 -7
  27. package/lib/cf-memory/src/tools/delete.ts +3 -3
  28. package/lib/cf-memory/src/tools/store.ts +14 -2
  29. package/lib/cf-memory/src/tools/update.ts +21 -3
  30. package/package.json +7 -4
  31. package/dist/chunk-2HQVNQB7.js +0 -71
@@ -14,7 +14,10 @@
14
14
  import fs from "node:fs";
15
15
  import path from "node:path";
16
16
 
17
- export const SECTION_HEADER = "## CF Memory: Conventions";
17
+ export const SECTION_HEADER = "## CF Memory: Project Rules";
18
+
19
+ /** @deprecated Old header — kept for migration. Removed after reading. */
20
+ const LEGACY_HEADER = "## CF Memory: Conventions";
18
21
 
19
22
  // Tested per-line via .split("\n"), not against the full document,
20
23
  // so the $ anchor correctly matches end-of-line without the `m` flag.
@@ -52,13 +55,19 @@ function parseClaudeMd(content: string): {
52
55
  after: string;
53
56
  untrackedLines: string[];
54
57
  } {
55
- const headerIdx = content.indexOf(SECTION_HEADER);
58
+ // Try current header first, then legacy header for migration
59
+ let headerIdx = content.indexOf(SECTION_HEADER);
60
+ let headerLen = SECTION_HEADER.length;
61
+ if (headerIdx === -1) {
62
+ headerIdx = content.indexOf(LEGACY_HEADER);
63
+ headerLen = LEGACY_HEADER.length;
64
+ }
56
65
  if (headerIdx === -1) {
57
66
  return { before: content, entries: [], after: "", untrackedLines: [] };
58
67
  }
59
68
 
60
69
  const before = content.slice(0, headerIdx);
61
- const rest = content.slice(headerIdx + SECTION_HEADER.length);
70
+ const rest = content.slice(headerIdx + headerLen);
62
71
 
63
72
  // Find the next section header (## ) to determine where our section ends
64
73
  const nextSectionMatch = rest.match(/\n(## )/);
@@ -50,17 +50,29 @@ export async function detectTier(configTier?: TierConfig): Promise<TierInfo> {
50
50
  return TIERS.markdown;
51
51
  }
52
52
 
53
- /** Build a respawn callback that DaemonClient can call when the daemon is gone. */
54
- function makeRespawn(
53
+ const RESPAWN_MAX_RETRIES = 3;
54
+ const RESPAWN_RETRY_DELAY_MS = 1000;
55
+
56
+ /** Build a respawn callback with retry logic (up to 3 attempts). */
57
+ export function createRespawnWithRetry(
55
58
  docsDir: string,
56
59
  embeddingConfig?: Partial<EmbeddingConfig>,
57
60
  idleTimeoutMs?: number,
58
61
  ): () => Promise<boolean> {
59
62
  return async () => {
60
- const result = await spawnDaemon(docsDir, embeddingConfig, {
61
- idleTimeoutMs,
62
- });
63
- return result !== null;
63
+ for (let attempt = 1; attempt <= RESPAWN_MAX_RETRIES; attempt++) {
64
+ // spawnDaemon returns null both when "already running" and "truly failed"
65
+ // Check isDaemonRunning after null to distinguish the two cases
66
+ const result = await spawnDaemon(docsDir, embeddingConfig, {
67
+ idleTimeoutMs,
68
+ });
69
+ if (result !== null) return true;
70
+ if (await isDaemonRunning()) return true;
71
+ if (attempt < RESPAWN_MAX_RETRIES) {
72
+ await new Promise((r) => setTimeout(r, RESPAWN_RETRY_DELAY_MS));
73
+ }
74
+ }
75
+ return false;
64
76
  };
65
77
  }
66
78
 
@@ -75,7 +87,7 @@ export async function createBackendForTier(
75
87
  daemonOptions?: { idleTimeoutMs?: number },
76
88
  ): Promise<{ backend: MemoryBackend; tier: TierInfo }> {
77
89
  const tier = await detectTier(configTier);
78
- const respawn = makeRespawn(
90
+ const respawn = createRespawnWithRetry(
79
91
  docsDir,
80
92
  embeddingConfig,
81
93
  daemonOptions?.idleTimeoutMs,
@@ -28,12 +28,12 @@ export function registerDelete(
28
28
  isError: true,
29
29
  };
30
30
  }
31
- // Remove from CLAUDE.md if it was a conventions entry
32
- if (id.startsWith("conventions/") && ctx?.docsDir) {
31
+ // Remove from CLAUDE.md (any category may have been synced via sync_to_claude_md)
32
+ if (ctx?.docsDir) {
33
33
  try {
34
34
  removeFromClaudeMd(ctx.docsDir, id);
35
35
  } catch {
36
- // Best-effort
36
+ // Best-effort — removeFromClaudeMd is idempotent
37
37
  }
38
38
  }
39
39
 
@@ -40,6 +40,14 @@ export function registerStore(
40
40
  .describe(
41
41
  "When true, skip file creation and return a Memory object for indexing. File must already exist on disk.",
42
42
  ),
43
+ sync_to_claude_md: z
44
+ .boolean()
45
+ .optional()
46
+ .describe(
47
+ "When true, sync this memory to the project's CLAUDE.md regardless of category. " +
48
+ "Use for memories containing project-wide rules, conventions, or decisions that should be visible in CLAUDE.md. " +
49
+ "Convention memories (type: preference) are always synced automatically.",
50
+ ),
43
51
  },
44
52
  async ({
45
53
  title,
@@ -50,6 +58,7 @@ export function registerStore(
50
58
  importance,
51
59
  source,
52
60
  index_only,
61
+ sync_to_claude_md,
53
62
  }) => {
54
63
  const input = {
55
64
  title,
@@ -73,9 +82,12 @@ export function registerStore(
73
82
 
74
83
  const memory = await backend.store(input);
75
84
 
76
- // Sync convention memories to CLAUDE.md
85
+ // Sync to CLAUDE.md: auto for conventions, opt-in for other categories
77
86
  let claudeMdUpdated = false;
78
- if (memory.category === "conventions" && ctx?.docsDir && !index_only) {
87
+ const shouldSync =
88
+ (memory.category === "conventions" || sync_to_claude_md) &&
89
+ ctx?.docsDir;
90
+ if (shouldSync) {
79
91
  try {
80
92
  syncToClaudeMd(ctx.docsDir, memory.id, description);
81
93
  claudeMdUpdated = true;
@@ -29,8 +29,23 @@ export function registerUpdate(
29
29
  .max(5)
30
30
  .optional()
31
31
  .describe("New importance"),
32
+ sync_to_claude_md: z
33
+ .boolean()
34
+ .optional()
35
+ .describe(
36
+ "When true, sync this memory to the project's CLAUDE.md regardless of category. " +
37
+ "Convention memories (type: preference) are always synced automatically.",
38
+ ),
32
39
  },
33
- async ({ id, title, description, tags, content, importance }) => {
40
+ async ({
41
+ id,
42
+ title,
43
+ description,
44
+ tags,
45
+ content,
46
+ importance,
47
+ sync_to_claude_md,
48
+ }) => {
34
49
  const memory = await backend.update({
35
50
  id,
36
51
  title,
@@ -51,9 +66,12 @@ export function registerUpdate(
51
66
  };
52
67
  }
53
68
 
54
- // Sync convention memories to CLAUDE.md
69
+ // Sync to CLAUDE.md: auto for conventions, opt-in for other categories
55
70
  let claudeMdUpdated = false;
56
- if (memory.category === "conventions" && ctx?.docsDir) {
71
+ const shouldSync =
72
+ (memory.category === "conventions" || sync_to_claude_md) &&
73
+ ctx?.docsDir;
74
+ if (shouldSync) {
57
75
  try {
58
76
  if (description) {
59
77
  // Description changed — update the CLAUDE.md entry
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-friend-cli",
3
- "version": "1.27.1",
3
+ "version": "1.28.0",
4
4
  "description": "CLI for coding-friend — host learning docs, setup MCP server, initialize projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,8 +12,10 @@
12
12
  "prepublishOnly": "npm run build",
13
13
  "dev": "tsx src/index.ts",
14
14
  "watch": "tsup src/index.ts src/postinstall.ts --format esm --dts --watch",
15
- "test": "vitest run",
16
- "test:watch": "vitest"
15
+ "test": "vitest run && vitest run --config vitest.hooks.config.ts",
16
+ "test:watch": "vitest",
17
+ "test:hooks": "vitest run --config vitest.hooks.config.ts",
18
+ "test:e2e": "vitest run --config vitest.e2e.config.ts"
17
19
  },
18
20
  "files": [
19
21
  "dist",
@@ -43,7 +45,8 @@
43
45
  "dependencies": {
44
46
  "@inquirer/prompts": "^7.0.0",
45
47
  "chalk": "^5.0.0",
46
- "commander": "^13.0.0"
48
+ "commander": "^13.0.0",
49
+ "zod": "^4.3.6"
47
50
  },
48
51
  "devDependencies": {
49
52
  "@types/node": "^22.0.0",
@@ -1,71 +0,0 @@
1
- import {
2
- DEFAULT_CONFIG
3
- } from "./chunk-PRIH34UB.js";
4
- import {
5
- globalConfigPath,
6
- localConfigPath,
7
- resolvePath
8
- } from "./chunk-TWKNGPBO.js";
9
- import {
10
- readJson
11
- } from "./chunk-5UVDWG5L.js";
12
-
13
- // src/lib/config.ts
14
- function deepMerge(base, override) {
15
- const result = { ...base };
16
- for (const key of Object.keys(override)) {
17
- const baseVal = result[key];
18
- const overVal = override[key];
19
- if (baseVal && overVal && typeof baseVal === "object" && typeof overVal === "object" && !Array.isArray(baseVal) && !Array.isArray(overVal)) {
20
- result[key] = deepMerge(
21
- baseVal,
22
- overVal
23
- );
24
- } else {
25
- result[key] = overVal;
26
- }
27
- }
28
- return result;
29
- }
30
- function loadConfig() {
31
- const global = readJson(globalConfigPath());
32
- const local = readJson(localConfigPath());
33
- const base = deepMerge(
34
- DEFAULT_CONFIG,
35
- global ?? {}
36
- );
37
- return deepMerge(
38
- base,
39
- local ?? {}
40
- );
41
- }
42
- function resolveDocsDir(explicitPath) {
43
- if (explicitPath) {
44
- return resolvePath(explicitPath);
45
- }
46
- const local = readJson(localConfigPath());
47
- if (local?.learn?.outputDir) {
48
- return resolvePath(local.learn.outputDir);
49
- }
50
- const global = readJson(globalConfigPath());
51
- if (global?.learn?.outputDir) {
52
- return resolvePath(global.learn.outputDir);
53
- }
54
- return resolvePath("docs/learn");
55
- }
56
- function resolveMemoryDir(explicitPath) {
57
- if (explicitPath) {
58
- return resolvePath(explicitPath);
59
- }
60
- const config = loadConfig();
61
- if (config.docsDir) {
62
- return resolvePath(`${config.docsDir}/memory`);
63
- }
64
- return resolvePath("docs/memory");
65
- }
66
-
67
- export {
68
- loadConfig,
69
- resolveDocsDir,
70
- resolveMemoryDir
71
- };