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.
- package/README.md +9 -3
- package/dist/{chunk-PRIH34UB.js → chunk-DHH6SRXV.js} +5 -1
- package/dist/{chunk-75IEE2OE.js → chunk-E7OY4UWW.js} +2 -2
- package/dist/chunk-J4N2ODQ5.js +191 -0
- package/dist/{chunk-YCWRBOB3.js → chunk-RGQGKOBH.js} +1 -1
- package/dist/{chunk-GDZTU2Z4.js → chunk-XQ5KAXKL.js} +1 -1
- package/dist/{chunk-BX3DRKTU.js → chunk-XROT7L2F.js} +6 -5
- package/dist/{config-N55LTJ76.js → config-U2WEZXF6.js} +4 -4
- package/dist/{dev-BSXR55PZ.js → dev-4OAJCQRQ.js} +29 -5
- package/dist/guide-F2FLCDYO.js +113 -0
- package/dist/{host-JVLWYY5A.js → host-2KBOPER6.js} +2 -2
- package/dist/index.js +35 -26
- package/dist/{init-KX6Y3ELD.js → init-R5L2GAFV.js} +6 -6
- package/dist/{install-G4XYNVL3.js → install-XZC6UO54.js} +3 -3
- package/dist/{mcp-DABSO5PE.js → mcp-CNZLJ57B.js} +4 -4
- package/dist/{memory-7Q5P7LIU.js → memory-KU2MOCPQ.js} +4 -4
- package/dist/{session-DMQZXZ7F.js → session-ZGNQKV4M.js} +26 -7
- package/dist/{status-K7TBUAXT.js → status-JXUXVHRH.js} +4 -4
- package/dist/{statusline-CDGADB3C.js → statusline-RMTO4MQA.js} +2 -2
- package/dist/{update-6M3YFHS6.js → update-HS6A2C5V.js} +3 -3
- package/lib/cf-memory/README.md +4 -4
- package/lib/cf-memory/src/__tests__/claude-md.test.ts +105 -0
- package/lib/cf-memory/src/__tests__/tier.test.ts +59 -0
- package/lib/cf-memory/src/daemon/process.ts +1 -1
- package/lib/cf-memory/src/lib/claude-md.ts +12 -3
- package/lib/cf-memory/src/lib/tier.ts +19 -7
- package/lib/cf-memory/src/tools/delete.ts +3 -3
- package/lib/cf-memory/src/tools/store.ts +14 -2
- package/lib/cf-memory/src/tools/update.ts +21 -3
- package/package.json +7 -4
- 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:
|
|
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
|
-
|
|
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 +
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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 =
|
|
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
|
|
32
|
-
if (
|
|
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
|
|
85
|
+
// Sync to CLAUDE.md: auto for conventions, opt-in for other categories
|
|
77
86
|
let claudeMdUpdated = false;
|
|
78
|
-
|
|
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 ({
|
|
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
|
|
69
|
+
// Sync to CLAUDE.md: auto for conventions, opt-in for other categories
|
|
55
70
|
let claudeMdUpdated = false;
|
|
56
|
-
|
|
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.
|
|
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",
|
package/dist/chunk-2HQVNQB7.js
DELETED
|
@@ -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
|
-
};
|