@malindar/whyline 0.1.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 (164) hide show
  1. package/.claude/settings.local.json +33 -0
  2. package/.github/workflows/ci.yml +35 -0
  3. package/.github/workflows/publish.yml +37 -0
  4. package/.prettierrc.json +7 -0
  5. package/CLAUDE.md +74 -0
  6. package/LICENSE +21 -0
  7. package/README.md +359 -0
  8. package/dist/cli.d.ts +2 -0
  9. package/dist/cli.js +125 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/commands/delete.d.ts +3 -0
  12. package/dist/commands/delete.js +42 -0
  13. package/dist/commands/delete.js.map +1 -0
  14. package/dist/commands/doctor.d.ts +1 -0
  15. package/dist/commands/doctor.js +111 -0
  16. package/dist/commands/doctor.js.map +1 -0
  17. package/dist/commands/edit.d.ts +1 -0
  18. package/dist/commands/edit.js +78 -0
  19. package/dist/commands/edit.js.map +1 -0
  20. package/dist/commands/export.d.ts +8 -0
  21. package/dist/commands/export.js +90 -0
  22. package/dist/commands/export.js.map +1 -0
  23. package/dist/commands/import.d.ts +1 -0
  24. package/dist/commands/import.js +110 -0
  25. package/dist/commands/import.js.map +1 -0
  26. package/dist/commands/init.d.ts +5 -0
  27. package/dist/commands/init.js +23 -0
  28. package/dist/commands/init.js.map +1 -0
  29. package/dist/commands/install-claude.d.ts +3 -0
  30. package/dist/commands/install-claude.js +180 -0
  31. package/dist/commands/install-claude.js.map +1 -0
  32. package/dist/commands/list.d.ts +4 -0
  33. package/dist/commands/list.js +35 -0
  34. package/dist/commands/list.js.map +1 -0
  35. package/dist/commands/mcp.d.ts +1 -0
  36. package/dist/commands/mcp.js +10 -0
  37. package/dist/commands/mcp.js.map +1 -0
  38. package/dist/commands/save.d.ts +4 -0
  39. package/dist/commands/save.js +74 -0
  40. package/dist/commands/save.js.map +1 -0
  41. package/dist/commands/search.d.ts +7 -0
  42. package/dist/commands/search.js +46 -0
  43. package/dist/commands/search.js.map +1 -0
  44. package/dist/commands/show.d.ts +3 -0
  45. package/dist/commands/show.js +30 -0
  46. package/dist/commands/show.js.map +1 -0
  47. package/dist/commands/stats.d.ts +1 -0
  48. package/dist/commands/stats.js +27 -0
  49. package/dist/commands/stats.js.map +1 -0
  50. package/dist/commands/summarize.d.ts +3 -0
  51. package/dist/commands/summarize.js +140 -0
  52. package/dist/commands/summarize.js.map +1 -0
  53. package/dist/config.d.ts +11 -0
  54. package/dist/config.js +17 -0
  55. package/dist/config.js.map +1 -0
  56. package/dist/db/connection.d.ts +2 -0
  57. package/dist/db/connection.js +8 -0
  58. package/dist/db/connection.js.map +1 -0
  59. package/dist/db/migrations.d.ts +2 -0
  60. package/dist/db/migrations.js +19 -0
  61. package/dist/db/migrations.js.map +1 -0
  62. package/dist/db/schema.d.ts +5 -0
  63. package/dist/db/schema.js +64 -0
  64. package/dist/db/schema.js.map +1 -0
  65. package/dist/git/diff.d.ts +2 -0
  66. package/dist/git/diff.js +45 -0
  67. package/dist/git/diff.js.map +1 -0
  68. package/dist/git/git.d.ts +3 -0
  69. package/dist/git/git.js +25 -0
  70. package/dist/git/git.js.map +1 -0
  71. package/dist/git/repoId.d.ts +3 -0
  72. package/dist/git/repoId.js +49 -0
  73. package/dist/git/repoId.js.map +1 -0
  74. package/dist/mcp/server.d.ts +1 -0
  75. package/dist/mcp/server.js +296 -0
  76. package/dist/mcp/server.js.map +1 -0
  77. package/dist/mcp/tools.d.ts +119 -0
  78. package/dist/mcp/tools.js +43 -0
  79. package/dist/mcp/tools.js.map +1 -0
  80. package/dist/memory/parseSummary.d.ts +14 -0
  81. package/dist/memory/parseSummary.js +53 -0
  82. package/dist/memory/parseSummary.js.map +1 -0
  83. package/dist/memory/qualityCheck.d.ts +13 -0
  84. package/dist/memory/qualityCheck.js +78 -0
  85. package/dist/memory/qualityCheck.js.map +1 -0
  86. package/dist/memory/redactSecrets.d.ts +7 -0
  87. package/dist/memory/redactSecrets.js +29 -0
  88. package/dist/memory/redactSecrets.js.map +1 -0
  89. package/dist/memory/repoContext.d.ts +2 -0
  90. package/dist/memory/repoContext.js +23 -0
  91. package/dist/memory/repoContext.js.map +1 -0
  92. package/dist/memory/saveMemory.d.ts +40 -0
  93. package/dist/memory/saveMemory.js +223 -0
  94. package/dist/memory/saveMemory.js.map +1 -0
  95. package/dist/memory/searchMemory.d.ts +17 -0
  96. package/dist/memory/searchMemory.js +122 -0
  97. package/dist/memory/searchMemory.js.map +1 -0
  98. package/dist/memory/types.d.ts +48 -0
  99. package/dist/memory/types.js +2 -0
  100. package/dist/memory/types.js.map +1 -0
  101. package/dist/output/format.d.ts +3 -0
  102. package/dist/output/format.js +43 -0
  103. package/dist/output/format.js.map +1 -0
  104. package/docs/architecture.md +387 -0
  105. package/docs/ec6ab3bf-60cf-4629-ad9e-3048e8e3c43a.png +0 -0
  106. package/docs/logo.png +0 -0
  107. package/eslint.config.js +16 -0
  108. package/how-to-run/01-install.md +69 -0
  109. package/how-to-run/02-wire-up-your-repo.md +80 -0
  110. package/how-to-run/03-test-it-manually.md +91 -0
  111. package/how-to-run/04-test-with-claude-code.md +70 -0
  112. package/how-to-run/CLAUDE.md.template +72 -0
  113. package/how-to-run/README.md +49 -0
  114. package/package.json +60 -0
  115. package/src/cli.ts +142 -0
  116. package/src/commands/delete.ts +47 -0
  117. package/src/commands/doctor.ts +128 -0
  118. package/src/commands/edit.ts +80 -0
  119. package/src/commands/export.ts +95 -0
  120. package/src/commands/import.ts +119 -0
  121. package/src/commands/init.ts +31 -0
  122. package/src/commands/install-claude.ts +203 -0
  123. package/src/commands/list.ts +41 -0
  124. package/src/commands/mcp.ts +12 -0
  125. package/src/commands/save.ts +85 -0
  126. package/src/commands/search.ts +56 -0
  127. package/src/commands/show.ts +37 -0
  128. package/src/commands/stats.ts +31 -0
  129. package/src/commands/summarize.ts +183 -0
  130. package/src/config.ts +26 -0
  131. package/src/db/connection.ts +8 -0
  132. package/src/db/migrations.ts +26 -0
  133. package/src/db/schema.ts +68 -0
  134. package/src/git/diff.ts +43 -0
  135. package/src/git/git.ts +25 -0
  136. package/src/git/repoId.ts +49 -0
  137. package/src/hooks/post-commit.sample.sh +9 -0
  138. package/src/mcp/server.ts +326 -0
  139. package/src/mcp/tools.ts +53 -0
  140. package/src/memory/parseSummary.ts +72 -0
  141. package/src/memory/qualityCheck.ts +102 -0
  142. package/src/memory/redactSecrets.ts +32 -0
  143. package/src/memory/repoContext.ts +25 -0
  144. package/src/memory/saveMemory.ts +369 -0
  145. package/src/memory/searchMemory.ts +153 -0
  146. package/src/memory/types.ts +57 -0
  147. package/src/output/format.ts +44 -0
  148. package/src/skill/SKILL.md +95 -0
  149. package/tests/cliV02.test.ts +213 -0
  150. package/tests/doctor.test.ts +253 -0
  151. package/tests/exportImport.test.ts +248 -0
  152. package/tests/fileRename.test.ts +156 -0
  153. package/tests/gitHelpers.test.ts +94 -0
  154. package/tests/init.test.ts +93 -0
  155. package/tests/installClaude.test.ts +157 -0
  156. package/tests/parseSummary.test.ts +111 -0
  157. package/tests/qualityCheck.test.ts +182 -0
  158. package/tests/redactSecrets.test.ts +75 -0
  159. package/tests/saveMemory.test.ts +196 -0
  160. package/tests/searchFilters.test.ts +139 -0
  161. package/tests/searchMemory.test.ts +273 -0
  162. package/tests/stale.test.ts +47 -0
  163. package/tsconfig.json +18 -0
  164. package/vitest.config.ts +8 -0
@@ -0,0 +1,95 @@
1
+ import fs from "fs";
2
+ import { isInitialized, resolveConfig } from "../config.js";
3
+ import { openDb } from "../db/connection.js";
4
+ import { getRepoRoot } from "../git/git.js";
5
+ import { getRepoId } from "../git/repoId.js";
6
+ import { getAllMemories } from "../memory/saveMemory.js";
7
+ import type { CodingMemory } from "../memory/types.js";
8
+
9
+ function memoryToMarkdown(memory: CodingMemory): string {
10
+ const lines: string[] = [];
11
+ lines.push(`# ${memory.id}`);
12
+ lines.push(`Created: ${memory.createdAt}`);
13
+ if (memory.commitSha) lines.push(`Commit: ${memory.commitSha}`);
14
+ if (memory.repoName) lines.push(`Repo: ${memory.repoName}`);
15
+ if (memory.files.length) lines.push(`Files: ${memory.files.join(", ")}`);
16
+ if (memory.tags.length) lines.push(`Tags: ${memory.tags.join(", ")}`);
17
+ lines.push("");
18
+ if (memory.task) { lines.push("Task:", memory.task, ""); }
19
+ lines.push("Intent:", memory.intent, "");
20
+ lines.push("Summary:", memory.summary, "");
21
+ lines.push("Decision:", memory.decision, "");
22
+ lines.push("Why:", memory.why, "");
23
+ if (memory.alternativesRejected.length) {
24
+ lines.push("Alternatives rejected:");
25
+ for (const a of memory.alternativesRejected) lines.push(`- ${a}`);
26
+ lines.push("");
27
+ }
28
+ if (memory.risks.length) {
29
+ lines.push("Risks:");
30
+ for (const r of memory.risks) lines.push(`- ${r}`);
31
+ lines.push("");
32
+ }
33
+ if (memory.followUps.length) {
34
+ lines.push("Follow-ups:");
35
+ for (const fu of memory.followUps) lines.push(`- ${fu}`);
36
+ lines.push("");
37
+ }
38
+ return lines.join("\n");
39
+ }
40
+
41
+ export async function runExport(options: {
42
+ format: string;
43
+ output?: string;
44
+ repo: boolean;
45
+ since?: string;
46
+ before?: string;
47
+ tag: string[];
48
+ }): Promise<void> {
49
+ if (!isInitialized()) {
50
+ console.error("whyline is not initialized. Run `whyline init` first.");
51
+ process.exit(1);
52
+ }
53
+
54
+ const db = openDb(resolveConfig().storage.dbPath);
55
+ let memories = getAllMemories(db);
56
+ db.close();
57
+
58
+ if (options.repo) {
59
+ const repoRoot = getRepoRoot(process.cwd());
60
+ if (!repoRoot) {
61
+ console.error("Not inside a git repository.");
62
+ process.exit(1);
63
+ }
64
+ const repoId = getRepoId(repoRoot);
65
+ memories = memories.filter((m) => m.repoId === repoId);
66
+ }
67
+
68
+ if (options.since) {
69
+ const sinceMs = new Date(options.since).getTime();
70
+ memories = memories.filter((m) => new Date(m.createdAt).getTime() >= sinceMs);
71
+ }
72
+ if (options.before) {
73
+ const beforeMs = new Date(options.before).getTime();
74
+ memories = memories.filter((m) => new Date(m.createdAt).getTime() <= beforeMs);
75
+ }
76
+ if (options.tag.length > 0) {
77
+ const lowerTags = options.tag.map((t) => t.toLowerCase());
78
+ memories = memories.filter((m) => {
79
+ const mTags = m.tags.map((t) => t.toLowerCase());
80
+ return lowerTags.every((t) => mTags.includes(t));
81
+ });
82
+ }
83
+
84
+ const output =
85
+ options.format === "md"
86
+ ? memories.map(memoryToMarkdown).join("\n---\n\n")
87
+ : JSON.stringify({ schemaVersion: 1, memories }, null, 2);
88
+
89
+ if (options.output) {
90
+ fs.writeFileSync(options.output, output, "utf-8");
91
+ console.error(`Exported ${memories.length} memory(s) to ${options.output}`);
92
+ } else {
93
+ process.stdout.write(output + "\n");
94
+ }
95
+ }
@@ -0,0 +1,119 @@
1
+ import fs from "fs";
2
+ import { isInitialized, resolveConfig } from "../config.js";
3
+ import { openDb } from "../db/connection.js";
4
+ import { saveMemory, getMemoryById } from "../memory/saveMemory.js";
5
+ import { redactSecrets } from "../memory/redactSecrets.js";
6
+ import type { CodingMemory } from "../memory/types.js";
7
+
8
+ const VALID_SOURCES = new Set(["manual", "claude-code", "cli", "hook"]);
9
+
10
+ function validateMemory(obj: unknown): CodingMemory | null {
11
+ if (!obj || typeof obj !== "object") return null;
12
+ const m = obj as Record<string, unknown>;
13
+ if (
14
+ typeof m.id !== "string" ||
15
+ typeof m.createdAt !== "string" ||
16
+ typeof m.updatedAt !== "string" ||
17
+ typeof m.repoId !== "string" ||
18
+ typeof m.intent !== "string" ||
19
+ typeof m.summary !== "string" ||
20
+ typeof m.decision !== "string" ||
21
+ typeof m.why !== "string" ||
22
+ !Array.isArray(m.files) ||
23
+ !Array.isArray(m.tags) ||
24
+ !Array.isArray(m.alternativesRejected) ||
25
+ !Array.isArray(m.risks) ||
26
+ !Array.isArray(m.followUps) ||
27
+ typeof m.embeddingText !== "string"
28
+ ) {
29
+ return null;
30
+ }
31
+ const source = VALID_SOURCES.has(m.source as string)
32
+ ? (m.source as CodingMemory["source"])
33
+ : "manual";
34
+ return {
35
+ id: m.id,
36
+ createdAt: m.createdAt,
37
+ updatedAt: m.updatedAt,
38
+ repoId: m.repoId,
39
+ repoPath: typeof m.repoPath === "string" ? m.repoPath : undefined,
40
+ repoName: typeof m.repoName === "string" ? m.repoName : undefined,
41
+ branch: typeof m.branch === "string" ? m.branch : undefined,
42
+ commitSha: typeof m.commitSha === "string" ? m.commitSha : undefined,
43
+ task: typeof m.task === "string" ? m.task : undefined,
44
+ intent: redactSecrets(m.intent),
45
+ summary: redactSecrets(m.summary),
46
+ decision: redactSecrets(m.decision),
47
+ why: redactSecrets(m.why),
48
+ files: (m.files as unknown[]).filter((f): f is string => typeof f === "string"),
49
+ tags: (m.tags as unknown[]).filter((t): t is string => typeof t === "string"),
50
+ alternativesRejected: (m.alternativesRejected as unknown[]).filter((a): a is string => typeof a === "string").map(redactSecrets),
51
+ risks: (m.risks as unknown[]).filter((r): r is string => typeof r === "string").map(redactSecrets),
52
+ followUps: (m.followUps as unknown[]).filter((f): f is string => typeof f === "string").map(redactSecrets),
53
+ source,
54
+ embeddingText: m.embeddingText,
55
+ rawTranscriptPath: typeof m.rawTranscriptPath === "string" ? m.rawTranscriptPath : undefined,
56
+ };
57
+ }
58
+
59
+ export async function runImport(filePath: string): Promise<void> {
60
+ if (!isInitialized()) {
61
+ console.error("whyline is not initialized. Run `whyline init` first.");
62
+ process.exit(1);
63
+ }
64
+
65
+ let raw: string;
66
+ try {
67
+ raw = fs.readFileSync(filePath, "utf-8");
68
+ } catch {
69
+ console.error(`Cannot read file: ${filePath}`);
70
+ process.exit(1);
71
+ }
72
+
73
+ let parsed: unknown;
74
+ try {
75
+ parsed = JSON.parse(raw);
76
+ } catch {
77
+ console.error("File is not valid JSON. Only JSON exports are supported for import.");
78
+ process.exit(1);
79
+ }
80
+
81
+ // Accept both v1 envelope { schemaVersion, memories } and legacy bare arrays
82
+ let items: unknown[];
83
+ if (Array.isArray(parsed)) {
84
+ items = parsed;
85
+ } else if (
86
+ parsed &&
87
+ typeof parsed === "object" &&
88
+ "memories" in (parsed as object) &&
89
+ Array.isArray((parsed as Record<string, unknown>).memories)
90
+ ) {
91
+ items = (parsed as Record<string, unknown>).memories as unknown[];
92
+ } else {
93
+ items = [parsed];
94
+ }
95
+ const db = openDb(resolveConfig().storage.dbPath);
96
+
97
+ let imported = 0;
98
+ let skipped = 0;
99
+ let invalid = 0;
100
+
101
+ for (const item of items) {
102
+ const memory = validateMemory(item);
103
+ if (!memory) {
104
+ invalid++;
105
+ continue;
106
+ }
107
+ const existing = getMemoryById(db, memory.id);
108
+ if (existing) {
109
+ skipped++;
110
+ continue;
111
+ }
112
+ saveMemory(db, memory);
113
+ imported++;
114
+ }
115
+
116
+ db.close();
117
+
118
+ console.log(`Import complete: ${imported} imported, ${skipped} skipped (already exist), ${invalid} invalid.`);
119
+ }
@@ -0,0 +1,31 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { DATA_DIR, DB_PATH, CONFIG_PATH, AppConfig } from "../config.js";
4
+ import { openDb } from "../db/connection.js";
5
+ import { runMigrations } from "../db/migrations.js";
6
+
7
+ type InitOptions = {
8
+ dataDir?: string;
9
+ };
10
+
11
+ export function runInit(options: InitOptions = {}): void {
12
+ const dataDir = options.dataDir ?? DATA_DIR;
13
+ const dbPath = options.dataDir ? path.join(options.dataDir, "memory.db") : DB_PATH;
14
+ const configPath = options.dataDir ? path.join(options.dataDir, "config.json") : CONFIG_PATH;
15
+
16
+ fs.mkdirSync(dataDir, { recursive: true });
17
+
18
+ if (!fs.existsSync(configPath)) {
19
+ const config: AppConfig = {
20
+ version: 1,
21
+ storage: { dbPath },
22
+ };
23
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
24
+ }
25
+
26
+ const db = openDb(dbPath);
27
+ runMigrations(db);
28
+ db.close();
29
+
30
+ console.log(`Initialized whyline at ${dataDir}`);
31
+ }
@@ -0,0 +1,203 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { getRepoRoot } from "../git/git.js";
4
+ import { getRepoName } from "../git/repoId.js";
5
+
6
+ const MCP_TOOL_PERMISSIONS = [
7
+ "mcp__whyline__save_coding_memory",
8
+ "mcp__whyline__search_coding_memory",
9
+ "mcp__whyline__get_recent_memories",
10
+ "mcp__whyline__get_file_memories",
11
+ "mcp__whyline__get_commit_memory",
12
+ ];
13
+
14
+ function claudeMdSection(repoPath: string): string {
15
+ return `
16
+ ## Whyline Memory
17
+
18
+ You have access to a \`whyline\` MCP server. Use it every session.
19
+
20
+ ### When you start working on ANY task
21
+
22
+ **Before asking any clarifying question or touching any file**, search for past context:
23
+
24
+ - If the task is clearly described: call \`search_coding_memory\` with:
25
+ - \`repoPath\`: \`${repoPath}\`
26
+ - \`query\`: the task or feature the user just described
27
+ - \`files\`: any files you already know are relevant
28
+
29
+ - If the task is vague or just starting out: call \`get_recent_memories\` with:
30
+ - \`repoPath\`: \`${repoPath}\`
31
+ - \`limit\`: 5
32
+
33
+ **If memories come back**, you MUST:
34
+ 1. Show the user what was previously decided and why: _"I found a previous memory about this: [decision + reason]. Before I proceed — what's the reason for changing it now?"_
35
+ 2. Wait for the user to give a reason before asking implementation questions
36
+ 3. Record the new reason when saving the updated memory
37
+
38
+ Do not skip straight to implementation questions when a past memory exists for the same area. The reason matters — it goes into the next memory.
39
+
40
+ Treat memories as historical context — they explain past decisions, not current truth.
41
+
42
+ ### After you commit
43
+
44
+ After \`git commit\` succeeds:
45
+
46
+ 1. Synthesize from the conversation:
47
+ - What was the goal? → \`intent\`
48
+ - What was the key decision? → \`decision\`
49
+ - Why that decision (not another)? → \`why\`
50
+ - What alternatives were rejected? → \`alternativesRejected\`
51
+ - What risks exist? → \`risks\`
52
+ - What should be done next? → \`followUps\`
53
+
54
+ 2. Show the summary to the user:
55
+ _"Here's what I'm saving as a coding memory — let me know if you want to add or correct anything:"_
56
+ Then display each field clearly.
57
+
58
+ 3. Wait a moment for the user to respond. If they add or correct something, apply it.
59
+
60
+ 4. Call \`save_coding_memory\` with:
61
+ - \`repoPath\`: \`${repoPath}\`
62
+ - \`commitSha\`: the commit SHA (use HEAD)
63
+ - \`files\`: files changed in this session
64
+ - \`source\`: \`"claude-code"\`
65
+ - all synthesized fields above
66
+
67
+ 5. If the response contains a non-empty \`warnings\` array, show each warning to the user and offer to update the memory with richer detail.
68
+
69
+ ### Memory quality rules
70
+
71
+ Only save memories that would genuinely help a future session. Good memory:
72
+ - Explains a non-obvious decision
73
+ - Warns about a real risk
74
+ - Records a rejected alternative that someone will try again
75
+
76
+ Do NOT save:
77
+ - Routine refactors with no tradeoffs
78
+ - Things obvious from reading the code
79
+ - Secrets or credentials
80
+ `;
81
+ }
82
+
83
+ function writeMcpJson(repoRoot: string): "created" | "updated" | "unchanged" {
84
+ const mcpPath = path.join(repoRoot, ".mcp.json");
85
+ const whylineEntry = { command: "whyline", args: ["mcp"] };
86
+
87
+ let existing: Record<string, unknown> = {};
88
+ if (fs.existsSync(mcpPath)) {
89
+ try {
90
+ existing = JSON.parse(fs.readFileSync(mcpPath, "utf-8")) as Record<string, unknown>;
91
+ } catch {
92
+ // invalid JSON — overwrite
93
+ }
94
+ }
95
+
96
+ const servers = (existing.mcpServers ?? {}) as Record<string, unknown>;
97
+ const alreadyThere =
98
+ JSON.stringify((servers.whyline ?? null)) === JSON.stringify(whylineEntry);
99
+
100
+ if (alreadyThere) return "unchanged";
101
+
102
+ servers.whyline = whylineEntry;
103
+ existing.mcpServers = servers;
104
+ fs.writeFileSync(mcpPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
105
+ return fs.existsSync(mcpPath) && Object.keys(servers).length > 1 ? "updated" : "created";
106
+ }
107
+
108
+ function writeClaudeMd(repoRoot: string, repoPath: string): "created" | "updated" | "unchanged" {
109
+ const claudePath = path.join(repoRoot, "CLAUDE.md");
110
+ const section = claudeMdSection(repoPath);
111
+
112
+ if (!fs.existsSync(claudePath)) {
113
+ const repoName = getRepoName(repoRoot);
114
+ const header = `# Claude Instructions for ${repoName}\n`;
115
+ fs.writeFileSync(claudePath, header + section, "utf-8");
116
+ return "created";
117
+ }
118
+
119
+ const existing = fs.readFileSync(claudePath, "utf-8");
120
+
121
+ // Already has the section — update repoPath lines in place
122
+ if (/whyline/i.test(existing)) {
123
+ const updated = existing.replace(
124
+ /(`repoPath`:\s*`)([^`]+)(`)/g,
125
+ `$1${repoPath}$3`
126
+ );
127
+ if (updated === existing) return "unchanged";
128
+ fs.writeFileSync(claudePath, updated, "utf-8");
129
+ return "updated";
130
+ }
131
+
132
+ // Exists but no Whyline section — append
133
+ fs.writeFileSync(claudePath, existing.trimEnd() + "\n" + section, "utf-8");
134
+ return "updated";
135
+ }
136
+
137
+ function writeSettingsJson(repoRoot: string): "created" | "updated" | "unchanged" {
138
+ const settingsDir = path.join(repoRoot, ".claude");
139
+ const settingsPath = path.join(settingsDir, "settings.local.json");
140
+
141
+ let existing: Record<string, unknown> = {};
142
+ if (fs.existsSync(settingsPath)) {
143
+ try {
144
+ existing = JSON.parse(fs.readFileSync(settingsPath, "utf-8")) as Record<string, unknown>;
145
+ } catch {
146
+ // invalid JSON — overwrite
147
+ }
148
+ }
149
+
150
+ const perms = (existing.permissions ?? {}) as Record<string, unknown>;
151
+ const allowed = Array.isArray(perms.allow) ? (perms.allow as string[]) : [];
152
+ const enabledServers = Array.isArray(existing.enabledMcpjsonServers)
153
+ ? (existing.enabledMcpjsonServers as string[])
154
+ : [];
155
+
156
+ const newAllowed = [...new Set([...allowed, ...MCP_TOOL_PERMISSIONS])];
157
+ const newEnabled = [...new Set([...enabledServers, "whyline"])];
158
+
159
+ const unchanged =
160
+ newAllowed.length === allowed.length && newEnabled.length === enabledServers.length;
161
+ if (unchanged) return "unchanged";
162
+
163
+ perms.allow = newAllowed;
164
+ existing.permissions = perms;
165
+ existing.enabledMcpjsonServers = newEnabled;
166
+
167
+ if (!fs.existsSync(settingsDir)) fs.mkdirSync(settingsDir, { recursive: true });
168
+ const wasNew = !fs.existsSync(settingsPath);
169
+ fs.writeFileSync(settingsPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
170
+ return wasNew ? "created" : "updated";
171
+ }
172
+
173
+ export async function runInstallClaude(options: { repoPath?: string }): Promise<void> {
174
+ const cwd = options.repoPath ?? process.cwd();
175
+ const repoRoot = getRepoRoot(cwd);
176
+
177
+ if (!repoRoot) {
178
+ console.error("Not inside a git repository. Run this command from your project root.");
179
+ process.exit(1);
180
+ }
181
+
182
+ const repoPath = repoRoot;
183
+
184
+ const mcpStatus = writeMcpJson(repoRoot);
185
+ const claudeStatus = writeClaudeMd(repoRoot, repoPath);
186
+ const settingsStatus = writeSettingsJson(repoRoot);
187
+
188
+ const label = (status: string, file: string) => {
189
+ const icon = status === "unchanged" ? "·" : "✓";
190
+ return ` ${icon} ${file} (${status})`;
191
+ };
192
+
193
+ console.log(label(mcpStatus, ".mcp.json"));
194
+ console.log(label(claudeStatus, "CLAUDE.md"));
195
+ console.log(label(settingsStatus, ".claude/settings.local.json"));
196
+ console.log("");
197
+
198
+ if (mcpStatus === "unchanged" && claudeStatus === "unchanged" && settingsStatus === "unchanged") {
199
+ console.log("Whyline is already configured in this repo.");
200
+ } else {
201
+ console.log("Done. Open this repo in Claude Code and run `whyline doctor` to verify.");
202
+ }
203
+ }
@@ -0,0 +1,41 @@
1
+ import { isInitialized, resolveConfig } from "../config.js";
2
+ import { openDb } from "../db/connection.js";
3
+ import { getRepoRoot } from "../git/git.js";
4
+ import { getRepoId } from "../git/repoId.js";
5
+ import { listMemories } from "../memory/saveMemory.js";
6
+ import { formatMemory } from "../output/format.js";
7
+
8
+ export async function runList(options: { repo: boolean; limit: string }): Promise<void> {
9
+ if (!isInitialized()) {
10
+ console.error("whyline is not initialized. Run `whyline init` first.");
11
+ process.exit(1);
12
+ }
13
+
14
+ const limit = parseInt(options.limit, 10) || 20;
15
+ let repoId: string | undefined;
16
+
17
+ if (options.repo) {
18
+ const repoRoot = getRepoRoot(process.cwd());
19
+ if (!repoRoot) {
20
+ console.error("Not inside a git repository.");
21
+ process.exit(1);
22
+ }
23
+ repoId = getRepoId(repoRoot);
24
+ }
25
+
26
+ const db = openDb(resolveConfig().storage.dbPath);
27
+ const memories = listMemories(db, { repoId, limit });
28
+ db.close();
29
+
30
+ if (memories.length === 0) {
31
+ console.log("No memories found.");
32
+ return;
33
+ }
34
+
35
+ for (const memory of memories) {
36
+ console.log(formatMemory(memory));
37
+ console.log("\n---");
38
+ }
39
+
40
+ console.log(`\n${memories.length} memory(s) shown.`);
41
+ }
@@ -0,0 +1,12 @@
1
+ import { isInitialized } from "../config.js";
2
+ import { createMcpServer } from "../mcp/server.js";
3
+
4
+ export async function runMcp(): Promise<void> {
5
+ if (!isInitialized()) {
6
+ process.stderr.write(
7
+ "coding-memory is not initialized. Run `coding-memory init` first.\n"
8
+ );
9
+ process.exit(1);
10
+ }
11
+ await createMcpServer();
12
+ }
@@ -0,0 +1,85 @@
1
+ import fs from "fs";
2
+ import { isInitialized, resolveConfig } from "../config.js";
3
+ import { openDb } from "../db/connection.js";
4
+ import { getRepoContext } from "../memory/repoContext.js";
5
+ import { parseSummary } from "../memory/parseSummary.js";
6
+ import { redactSecrets } from "../memory/redactSecrets.js";
7
+ import { saveMemory, generateMemoryId, buildEmbeddingText } from "../memory/saveMemory.js";
8
+ import { checkQuality, checkDuplicates } from "../memory/qualityCheck.js";
9
+ import type { CodingMemory } from "../memory/types.js";
10
+
11
+ export async function runSave(options: { commit: string; summaryFile: string }): Promise<void> {
12
+ if (!isInitialized()) {
13
+ console.error("whyline is not initialized. Run `whyline init` first.");
14
+ process.exit(1);
15
+ }
16
+
17
+ const cwd = process.cwd();
18
+
19
+ let ctx;
20
+ try {
21
+ ctx = getRepoContext(cwd, options.commit);
22
+ } catch (err) {
23
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
24
+ process.exit(1);
25
+ }
26
+
27
+ if (!fs.existsSync(options.summaryFile)) {
28
+ console.error(`Error: summary file not found: ${options.summaryFile}`);
29
+ process.exit(1);
30
+ }
31
+
32
+ const rawMarkdown = fs.readFileSync(options.summaryFile, "utf-8");
33
+ const parsed = parseSummary(rawMarkdown);
34
+
35
+ const now = new Date().toISOString();
36
+ const id = generateMemoryId();
37
+
38
+ const memory: CodingMemory = {
39
+ id,
40
+ createdAt: now,
41
+ updatedAt: now,
42
+ repoId: ctx.repoId,
43
+ repoPath: ctx.repoPath,
44
+ repoName: ctx.repoName,
45
+ branch: ctx.branch ?? undefined,
46
+ commitSha: ctx.commitSha,
47
+ files: ctx.changedFiles,
48
+ tags: parsed.tags.map(redactSecrets),
49
+ task: parsed.task ? redactSecrets(parsed.task) : undefined,
50
+ intent: redactSecrets(parsed.intent),
51
+ summary: redactSecrets(parsed.summary),
52
+ decision: redactSecrets(parsed.decision),
53
+ why: redactSecrets(parsed.why),
54
+ alternativesRejected: parsed.alternativesRejected.map(redactSecrets),
55
+ risks: parsed.risks.map(redactSecrets),
56
+ followUps: parsed.followUps.map(redactSecrets),
57
+ source: "cli",
58
+ embeddingText: "",
59
+ };
60
+
61
+ memory.embeddingText = buildEmbeddingText(memory);
62
+
63
+ const db = openDb(resolveConfig().storage.dbPath);
64
+
65
+ const qualityWarnings = checkQuality(memory);
66
+ const duplicateWarnings = checkDuplicates(db, memory);
67
+
68
+ saveMemory(db, memory);
69
+ db.close();
70
+
71
+ console.log(`Saved memory ${memory.id}`);
72
+ console.log(`Repo: ${ctx.repoName}`);
73
+ console.log(`Commit: ${ctx.commitSha.slice(0, 8)}`);
74
+ console.log(`Files: ${ctx.changedFiles.length}`);
75
+
76
+ if (qualityWarnings.length > 0) {
77
+ console.log("\nQuality warnings:");
78
+ for (const w of qualityWarnings) console.warn(` ⚠ ${w.message}`);
79
+ }
80
+
81
+ if (duplicateWarnings.length > 0) {
82
+ console.log("\nDuplicate warnings:");
83
+ for (const w of duplicateWarnings) console.warn(` ⚠ ${w.message}`);
84
+ }
85
+ }
@@ -0,0 +1,56 @@
1
+ import { isInitialized, resolveConfig } from "../config.js";
2
+ import { openDb } from "../db/connection.js";
3
+ import { getRepoRoot } from "../git/git.js";
4
+ import { getRepoId } from "../git/repoId.js";
5
+ import { getFileRenameHistory } from "../git/diff.js";
6
+ import { searchMemory } from "../memory/searchMemory.js";
7
+ import { formatSearchResult } from "../output/format.js";
8
+
9
+ export async function runSearch(
10
+ query: string,
11
+ options: { file?: string; tag?: string[]; since?: string; before?: string; limit: string }
12
+ ): Promise<void> {
13
+ if (!isInitialized()) {
14
+ console.error("whyline is not initialized. Run `whyline init` first.");
15
+ process.exit(1);
16
+ }
17
+
18
+ const limit = parseInt(options.limit, 10) || 10;
19
+ const cwd = process.cwd();
20
+
21
+ let repoId: string | undefined;
22
+ let repoPath: string | undefined;
23
+
24
+ const repoRoot = getRepoRoot(cwd);
25
+ if (repoRoot) {
26
+ repoId = getRepoId(repoRoot);
27
+ repoPath = repoRoot;
28
+ }
29
+
30
+ const files = options.file
31
+ ? (repoPath ? getFileRenameHistory(repoPath, options.file) : [options.file])
32
+ : [];
33
+
34
+ const db = openDb(resolveConfig().storage.dbPath);
35
+ const results = searchMemory(db, {
36
+ query,
37
+ repoId,
38
+ repoPath,
39
+ files,
40
+ tags: options.tag ?? [],
41
+ since: options.since,
42
+ before: options.before,
43
+ limit,
44
+ });
45
+ db.close();
46
+
47
+ if (results.length === 0) {
48
+ console.log("No memories found.");
49
+ return;
50
+ }
51
+
52
+ for (const result of results) {
53
+ console.log(formatSearchResult(result));
54
+ console.log("\n---");
55
+ }
56
+ }
@@ -0,0 +1,37 @@
1
+ import { isInitialized, resolveConfig } from "../config.js";
2
+ import { openDb } from "../db/connection.js";
3
+ import { getMemoryById, getMemoryByCommit } from "../memory/saveMemory.js";
4
+ import { formatMemory } from "../output/format.js";
5
+
6
+ export async function runShow(
7
+ id: string | undefined,
8
+ options: { commit?: string }
9
+ ): Promise<void> {
10
+ if (!isInitialized()) {
11
+ console.error("whyline is not initialized. Run `whyline init` first.");
12
+ process.exit(1);
13
+ }
14
+
15
+ const db = openDb(resolveConfig().storage.dbPath);
16
+
17
+ let memory = null;
18
+
19
+ if (options.commit) {
20
+ memory = getMemoryByCommit(db, options.commit);
21
+ } else if (id) {
22
+ memory = getMemoryById(db, id);
23
+ } else {
24
+ db.close();
25
+ console.error("Error: provide a memory ID or use --commit <sha>");
26
+ process.exit(1);
27
+ }
28
+
29
+ db.close();
30
+
31
+ if (!memory) {
32
+ console.error(`Memory not found: ${options.commit ?? id}`);
33
+ process.exit(1);
34
+ }
35
+
36
+ console.log(formatMemory(memory, true));
37
+ }