@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,70 @@
1
+ # Step 4 — Test With Claude Code
2
+
3
+ Once you've completed steps 1–3, open your repo in Claude Code and run through this scenario.
4
+
5
+ ---
6
+
7
+ ## Scenario A — Memory search on session start
8
+
9
+ 1. Open your repo in Claude Code
10
+ 2. Ask Claude to do any coding task, e.g.:
11
+ > "Add a retention policy for audit logs"
12
+ 3. Claude should **automatically call `search_coding_memory`** before asking any questions or touching any files
13
+ 4. If no memories exist yet, Claude proceeds normally
14
+ 5. Complete the task and commit
15
+
16
+ **What to watch for:**
17
+ - Claude calls `search_coding_memory` as the very first action
18
+ - You see `[Searching Whyline memories...]` or similar in the tool call output
19
+
20
+ ---
21
+
22
+ ## Scenario B — Memory saved after commit
23
+
24
+ 1. Do some work with Claude in your repo
25
+ 2. Ask Claude to commit:
26
+ > "commit this"
27
+ 3. After the commit succeeds, Claude should:
28
+ - Show you a memory summary: _"Here's what I'm saving..."_
29
+ - Display intent, decision, why, risks, follow-ups
30
+ - Call `save_coding_memory` automatically
31
+ 4. You can add corrections or say nothing — it saves either way
32
+
33
+ **What to watch for:**
34
+ - Claude shows the summary without you asking
35
+ - `save_coding_memory` is called with the real commit SHA
36
+
37
+ ---
38
+
39
+ ## Scenario C — Past decision surfaced
40
+
41
+ 1. Start a new Claude Code session in the same repo
42
+ 2. Ask Claude to change something you changed before, e.g.:
43
+ > "change the retention period"
44
+ 3. Claude should find the previous memory and say something like:
45
+ > _"I found a previous memory about this: we set retention to 90 days because of legal requirements. Before I proceed — what's the reason for changing it now?"_
46
+ 4. Give a reason
47
+ 5. Claude proceeds, and saves a new memory with the updated reasoning
48
+
49
+ **What to watch for:**
50
+ - Claude surfaces the old decision and asks WHY before asking WHAT
51
+ - New memory records both the change and the reason
52
+
53
+ ---
54
+
55
+ ## Troubleshooting
56
+
57
+ **Claude isn't calling `search_coding_memory` automatically**
58
+ - Check `.mcp.json` exists at the repo root
59
+ - Check `enabledMcpjsonServers` is set in `.claude/settings.local.json`
60
+ - Restart the Claude Code session after adding these files
61
+
62
+ **`search_coding_memory` returns no results**
63
+ - Make sure `whyline init` has been run
64
+ - Make sure at least one memory has been saved with `whyline save`
65
+ - Check `repoPath` in `CLAUDE.md` matches your actual repo path exactly
66
+
67
+ **MCP server fails to start**
68
+ - Run `whyline mcp` manually and check for errors
69
+ - Verify `~/.whyline/memory.db` exists (run `whyline init` if not)
70
+ - On Node 22: verify the native binding was rebuilt (see step 1)
@@ -0,0 +1,72 @@
1
+ # Claude Instructions for <YOUR-REPO-NAME>
2
+
3
+ ## Whyline Memory
4
+
5
+ You have access to a `whyline` MCP server. Use it every session.
6
+
7
+ ### When you start working on ANY task
8
+
9
+ **The very first action — before reading any file, before asking any question — is always a memory search. No exceptions.**
10
+
11
+ - If the task is clearly described: call `search_coding_memory` with:
12
+ - `repoPath`: `/absolute/path/to/your-repo` ← change this
13
+ - `query`: the task or feature the user just described
14
+ - `files`: any files you already know are relevant
15
+
16
+ - If the task is vague or just starting out: call `get_recent_memories` with:
17
+ - `repoPath`: `/absolute/path/to/your-repo` ← change this
18
+ - `limit`: 5
19
+
20
+ **If memories come back**, you MUST:
21
+ 1. STOP. Do not read any file yet.
22
+ 2. Quote the memory to the user verbatim: _"I found a previous memory about this: [decision + reason]. Before I proceed — what's the reason for changing it now?"_
23
+ 3. If the memory has `isStale: true`, add: _"Note: this memory is over 90 days old — verify it still applies before treating it as current."_
24
+ 4. Wait for the user to respond before doing anything else.
25
+ 5. Record the new reason when saving the updated memory.
26
+
27
+ **If no memories come back**, say "No past memories found for this area" and then proceed normally.
28
+
29
+ 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.
30
+
31
+ Treat memories as historical context — they explain past decisions, not current truth.
32
+
33
+ ### After you commit
34
+
35
+ After `git commit` succeeds:
36
+
37
+ 1. Synthesize from the conversation:
38
+ - What was the goal? → `intent`
39
+ - What was the key decision? → `decision`
40
+ - Why that decision (not another)? → `why`
41
+ - What alternatives were rejected? → `alternativesRejected`
42
+ - What risks exist? → `risks`
43
+ - What should be done next? → `followUps`
44
+
45
+ 2. Show the summary to the user in this format:
46
+ _"Here's what I'm saving as a coding memory — let me know if you want to add or correct anything:"_
47
+ Then display each field clearly.
48
+
49
+ 3. Wait a moment for the user to respond. If they add or correct something, apply it. If they say nothing or say "looks good", proceed.
50
+
51
+ 4. Call `save_coding_memory` with:
52
+ - `repoPath`: `/absolute/path/to/your-repo` ← change this
53
+ - `commitSha`: the commit SHA (use HEAD)
54
+ - `files`: files changed in this session
55
+ - `source`: `"claude-code"`
56
+ - all synthesized fields above
57
+
58
+ 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.
59
+
60
+ Do not ask for approval — the memory is always saved. The user can only enrich or correct it.
61
+
62
+ ### Memory quality rules
63
+
64
+ Only save memories that would genuinely help a future session. Good memory:
65
+ - Explains a non-obvious decision
66
+ - Warns about a real risk
67
+ - Records a rejected alternative that someone will try again
68
+
69
+ Do NOT save:
70
+ - Routine refactors with no tradeoffs
71
+ - Things obvious from reading the code
72
+ - Secrets or credentials
@@ -0,0 +1,49 @@
1
+ # How to Run Whyline
2
+
3
+ Follow these steps in order to install Whyline, wire it up to a repo, and test the full memory loop with Claude Code.
4
+
5
+ ---
6
+
7
+ ## Steps
8
+
9
+ | # | File | What it covers |
10
+ |---|------|----------------|
11
+ | 1 | [01-install.md](./01-install.md) | Clone, build, link the CLI, run `whyline init` |
12
+ | 2 | [02-wire-up-your-repo.md](./02-wire-up-your-repo.md) | Add `.mcp.json`, `settings.local.json`, and `CLAUDE.md` to your repo |
13
+ | 3 | [03-test-it-manually.md](./03-test-it-manually.md) | Verify save/search/show work from the CLI |
14
+ | 4 | [04-test-with-claude-code.md](./04-test-with-claude-code.md) | Test the full loop: search on start, save after commit, past decisions surfaced |
15
+
16
+ ---
17
+
18
+ ## Template files
19
+
20
+ | File | Use |
21
+ |------|-----|
22
+ | [CLAUDE.md.template](./CLAUDE.md.template) | Copy to your repo's `CLAUDE.md` and set `repoPath` |
23
+
24
+ ---
25
+
26
+ ## TL;DR (happy path)
27
+
28
+ ```bash
29
+ # 1. Install
30
+ git clone <whyline-repo>
31
+ cd whyline
32
+ npm install && npm run build
33
+ npm rebuild better-sqlite3
34
+ npm link
35
+ whyline init
36
+
37
+ # 2. Wire up your repo
38
+ cd /path/to/your-repo
39
+ # create .mcp.json, .claude/settings.local.json, CLAUDE.md
40
+ # (see 02-wire-up-your-repo.md for exact file contents)
41
+
42
+ # 3. Test CLI
43
+ whyline save --commit HEAD --summary-file /tmp/test-memory.md
44
+ whyline search "test"
45
+
46
+ # 4. Open repo in Claude Code and ask it to do something
47
+ # → Claude searches memories automatically
48
+ # → Commit → Claude saves memory automatically
49
+ ```
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@malindar/whyline",
3
+ "version": "0.1.0",
4
+ "description": "Local-first MCP memory for AI coding sessions. Git remembers what changed; Whyline remembers why.",
5
+ "keywords": [
6
+ "mcp",
7
+ "claude-code",
8
+ "ai-coding",
9
+ "coding-agent",
10
+ "developer-tools",
11
+ "git",
12
+ "sqlite",
13
+ "local-first",
14
+ "ai-memory",
15
+ "llm",
16
+ "cli"
17
+ ],
18
+ "type": "module",
19
+ "bin": {
20
+ "whyline": "./dist/cli.js"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/malinda1986/whyline.git"
25
+ },
26
+ "homepage": "https://github.com/malinda1986/whyline#readme",
27
+ "bugs": {
28
+ "url": "https://github.com/malinda1986/whyline/issues"
29
+ },
30
+ "license": "MIT",
31
+ "scripts": {
32
+ "build": "tsc",
33
+ "dev": "tsx src/cli.ts",
34
+ "test": "vitest run",
35
+ "test:watch": "vitest",
36
+ "lint": "eslint src tests",
37
+ "format": "prettier --write src tests",
38
+ "prepublishOnly": "npm run build"
39
+ },
40
+ "dependencies": {
41
+ "@modelcontextprotocol/sdk": "^1.12.0",
42
+ "better-sqlite3": "^9.4.3",
43
+ "commander": "^12.1.0",
44
+ "zod": "^3.23.8"
45
+ },
46
+ "devDependencies": {
47
+ "@eslint/js": "^9.14.0",
48
+ "@types/better-sqlite3": "^7.6.11",
49
+ "@types/node": "^20.17.0",
50
+ "eslint": "^9.14.0",
51
+ "prettier": "^3.3.3",
52
+ "tsx": "^4.19.1",
53
+ "typescript": "^5.6.3",
54
+ "typescript-eslint": "^8.14.0",
55
+ "vitest": "^2.1.4"
56
+ },
57
+ "engines": {
58
+ "node": ">=18.0.0"
59
+ }
60
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { runInit } from "./commands/init.js";
4
+
5
+ function collect(val: string, acc: string[]): string[] {
6
+ acc.push(val);
7
+ return acc;
8
+ }
9
+
10
+ const program = new Command();
11
+
12
+ program
13
+ .name("whyline")
14
+ .description("Local-first memory for AI coding sessions")
15
+ .version("0.1.0");
16
+
17
+ program.command("init").description("Initialize whyline storage").action(() => runInit());
18
+
19
+ program
20
+ .command("doctor")
21
+ .description("Check whyline setup and diagnose configuration problems")
22
+ .action(async () => {
23
+ const { runDoctor } = await import("./commands/doctor.js");
24
+ await runDoctor();
25
+ });
26
+
27
+ program
28
+ .command("install-claude")
29
+ .description("Create or update .mcp.json, CLAUDE.md, and .claude/settings.local.json for this repo")
30
+ .option("--repo-path <path>", "Target repo path (defaults to current directory)")
31
+ .action(async (options: { repoPath?: string }) => {
32
+ const { runInstallClaude } = await import("./commands/install-claude.js");
33
+ await runInstallClaude(options);
34
+ });
35
+
36
+ program
37
+ .command("save")
38
+ .description("Save a coding memory")
39
+ .requiredOption("--commit <ref>", "Git commit ref")
40
+ .requiredOption("--summary-file <path>", "Path to markdown summary file")
41
+ .action(async (options: { commit: string; summaryFile: string }) => {
42
+ const { runSave } = await import("./commands/save.js");
43
+ await runSave(options);
44
+ });
45
+
46
+ program
47
+ .command("search <query>")
48
+ .description("Search coding memories")
49
+ .option("--file <path>", "Filter by file path")
50
+ .option("--tag <tag>", "Filter by tag (repeat for multiple)", collect, [])
51
+ .option("--since <date>", "Only memories created after this date (e.g. 2025-01-01)")
52
+ .option("--before <date>", "Only memories created before this date (e.g. 2025-12-31)")
53
+ .option("--limit <n>", "Max results", "10")
54
+ .action(async (query: string, options: { file?: string; tag: string[]; since?: string; before?: string; limit: string }) => {
55
+ const { runSearch } = await import("./commands/search.js");
56
+ await runSearch(query, options);
57
+ });
58
+
59
+ program
60
+ .command("show [id]")
61
+ .description("Show a single memory")
62
+ .option("--commit <sha>", "Find by commit SHA instead")
63
+ .action(async (id: string | undefined, options: { commit?: string }) => {
64
+ const { runShow } = await import("./commands/show.js");
65
+ await runShow(id, options);
66
+ });
67
+
68
+ program
69
+ .command("list")
70
+ .description("List stored memories in reverse chronological order")
71
+ .option("--repo", "Limit to the current git repository", false)
72
+ .option("--limit <n>", "Max results", "20")
73
+ .action(async (options: { repo: boolean; limit: string }) => {
74
+ const { runList } = await import("./commands/list.js");
75
+ await runList(options);
76
+ });
77
+
78
+ program
79
+ .command("delete <id>")
80
+ .description("Delete a memory by ID")
81
+ .option("--force", "Skip confirmation prompt", false)
82
+ .action(async (id: string, options: { force: boolean }) => {
83
+ const { runDelete } = await import("./commands/delete.js");
84
+ await runDelete(id, options);
85
+ });
86
+
87
+ program
88
+ .command("stats")
89
+ .description("Show memory storage statistics")
90
+ .action(async () => {
91
+ const { runStats } = await import("./commands/stats.js");
92
+ await runStats();
93
+ });
94
+
95
+ program
96
+ .command("edit <id>")
97
+ .description("Edit a memory in $EDITOR")
98
+ .action(async (id: string) => {
99
+ const { runEdit } = await import("./commands/edit.js");
100
+ await runEdit(id);
101
+ });
102
+
103
+ program
104
+ .command("export")
105
+ .description("Export memories to JSON or markdown")
106
+ .option("--format <fmt>", "Output format: json or md", "json")
107
+ .option("--output <path>", "Write to file instead of stdout")
108
+ .option("--repo", "Limit to the current git repository", false)
109
+ .option("--tag <tag>", "Filter by tag (repeat for multiple)", collect, [])
110
+ .option("--since <date>", "Only memories created after this date (e.g. 2025-01-01)")
111
+ .option("--before <date>", "Only memories created before this date (e.g. 2025-12-31)")
112
+ .action(async (options: { format: string; output?: string; repo: boolean; tag: string[]; since?: string; before?: string }) => {
113
+ const { runExport } = await import("./commands/export.js");
114
+ await runExport(options);
115
+ });
116
+
117
+ program
118
+ .command("import <file>")
119
+ .description("Import memories from a JSON export file")
120
+ .action(async (file: string) => {
121
+ const { runImport } = await import("./commands/import.js");
122
+ await runImport(file);
123
+ });
124
+
125
+ program
126
+ .command("summarize <id>")
127
+ .description("Use the Claude API to improve a saved memory's quality (requires ANTHROPIC_API_KEY)")
128
+ .option("--force", "Apply improvements without confirmation prompt", false)
129
+ .action(async (id: string, options: { force: boolean }) => {
130
+ const { runSummarize } = await import("./commands/summarize.js");
131
+ await runSummarize(id, options);
132
+ });
133
+
134
+ program
135
+ .command("mcp")
136
+ .description("Start MCP server over stdio")
137
+ .action(async () => {
138
+ const { runMcp } = await import("./commands/mcp.js");
139
+ await runMcp();
140
+ });
141
+
142
+ program.parse();
@@ -0,0 +1,47 @@
1
+ import * as readline from "readline";
2
+ import { isInitialized, resolveConfig } from "../config.js";
3
+ import { openDb } from "../db/connection.js";
4
+ import { getMemoryById, deleteMemory } from "../memory/saveMemory.js";
5
+
6
+ function confirm(question: string): Promise<boolean> {
7
+ return new Promise((resolve) => {
8
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
9
+ rl.question(question, (answer) => {
10
+ rl.close();
11
+ resolve(answer.trim().toLowerCase() === "y");
12
+ });
13
+ });
14
+ }
15
+
16
+ export async function runDelete(id: string, options: { force: boolean }): Promise<void> {
17
+ if (!isInitialized()) {
18
+ console.error("whyline is not initialized. Run `whyline init` first.");
19
+ process.exit(1);
20
+ }
21
+
22
+ const db = openDb(resolveConfig().storage.dbPath);
23
+ const memory = getMemoryById(db, id);
24
+
25
+ if (!memory) {
26
+ db.close();
27
+ console.error(`Memory not found: ${id}`);
28
+ process.exit(1);
29
+ }
30
+
31
+ console.log(`Memory: ${memory.id}`);
32
+ console.log(`Intent: ${memory.intent}`);
33
+ if (memory.commitSha) console.log(`Commit: ${memory.commitSha.slice(0, 8)}`);
34
+
35
+ if (!options.force) {
36
+ const ok = await confirm("\nDelete this memory? (y/N) ");
37
+ if (!ok) {
38
+ db.close();
39
+ console.log("Cancelled.");
40
+ return;
41
+ }
42
+ }
43
+
44
+ deleteMemory(db, id);
45
+ db.close();
46
+ console.log(`Deleted ${id}`);
47
+ }
@@ -0,0 +1,128 @@
1
+ import { execSync } from "child_process";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { resolveConfig, isInitialized } from "../config.js";
5
+ import { openDb } from "../db/connection.js";
6
+ import { MIGRATIONS } from "../db/schema.js";
7
+ import { getRepoRoot } from "../git/git.js";
8
+
9
+ type CheckResult = { label: string; ok: boolean; detail?: string };
10
+
11
+ function check(label: string, ok: boolean, detail?: string): CheckResult {
12
+ return { label, ok, detail };
13
+ }
14
+
15
+ export async function runDoctor(): Promise<void> {
16
+ const results: CheckResult[] = [];
17
+ const cwd = process.cwd();
18
+
19
+ // 1. DB exists
20
+ const initialized = isInitialized();
21
+ results.push(check("DB exists", initialized, initialized ? resolveConfig().storage.dbPath : "run `whyline init` first"));
22
+
23
+ // 2. Migrations current
24
+ if (initialized) {
25
+ try {
26
+ const db = openDb(resolveConfig().storage.dbPath);
27
+ const applied = db
28
+ .prepare<[], { version: number }>("SELECT version FROM migrations ORDER BY version")
29
+ .all()
30
+ .map((r) => r.version);
31
+ db.close();
32
+ const latest = MIGRATIONS[MIGRATIONS.length - 1].version;
33
+ const current = applied.includes(latest);
34
+ results.push(check(
35
+ "Migrations current",
36
+ current,
37
+ current ? `v${latest}` : `applied up to v${Math.max(...applied, 0)}, latest is v${latest} — run \`whyline init\``
38
+ ));
39
+ } catch (e) {
40
+ results.push(check("Migrations current", false, String(e)));
41
+ }
42
+ } else {
43
+ results.push(check("Migrations current", false, "skipped — DB not initialised"));
44
+ }
45
+
46
+ // 3. `whyline` command available on PATH
47
+ try {
48
+ const bin = execSync("which whyline", { encoding: "utf-8" }).trim();
49
+ results.push(check("`whyline` on PATH", true, bin));
50
+ } catch {
51
+ results.push(check("`whyline` on PATH", false, "not found — run `npm link` or `npm install -g whyline`"));
52
+ }
53
+
54
+ // 4. Inside a git repo
55
+ const repoRoot = getRepoRoot(cwd);
56
+ results.push(check(
57
+ "Inside a git repo",
58
+ repoRoot !== null,
59
+ repoRoot ?? "not a git repository — memories cannot be linked to commits"
60
+ ));
61
+
62
+ // 5. .mcp.json configured
63
+ if (repoRoot) {
64
+ const mcpJson = path.join(repoRoot, ".mcp.json");
65
+ let mcpOk = false;
66
+ let mcpDetail: string | undefined;
67
+ if (fs.existsSync(mcpJson)) {
68
+ try {
69
+ const raw = JSON.parse(fs.readFileSync(mcpJson, "utf-8")) as Record<string, unknown>;
70
+ const servers = (raw.mcpServers ?? {}) as Record<string, unknown>;
71
+ mcpOk = Object.keys(servers).some((k) => k.toLowerCase().includes("whyline"));
72
+ mcpDetail = mcpOk ? mcpJson : `${mcpJson} exists but no whyline server entry found`;
73
+ } catch {
74
+ mcpDetail = `${mcpJson} is not valid JSON`;
75
+ }
76
+ } else {
77
+ mcpDetail = `${mcpJson} not found — see how-to-run/02-wire-up-your-repo.md`;
78
+ }
79
+ results.push(check(".mcp.json configured", mcpOk, mcpDetail));
80
+
81
+ // 6. CLAUDE.md mentions Whyline
82
+ const claudeMd = path.join(repoRoot, "CLAUDE.md");
83
+ if (fs.existsSync(claudeMd)) {
84
+ const content = fs.readFileSync(claudeMd, "utf-8");
85
+ const mentioned = /whyline/i.test(content);
86
+ results.push(check(
87
+ "CLAUDE.md mentions Whyline",
88
+ mentioned,
89
+ mentioned ? claudeMd : `${claudeMd} exists but does not mention Whyline — see how-to-run/CLAUDE.md.template`
90
+ ));
91
+ } else {
92
+ results.push(check("CLAUDE.md mentions Whyline", false, `${claudeMd} not found`));
93
+ }
94
+ } else {
95
+ results.push(check(".mcp.json configured", false, "skipped — not in a git repo"));
96
+ results.push(check("CLAUDE.md mentions Whyline", false, "skipped — not in a git repo"));
97
+ }
98
+
99
+ // 7. MCP server starts (quick smoke test)
100
+ try {
101
+ // Send a ListTools request and expect a response within 3 s
102
+ const proc = execSync(
103
+ `echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | whyline mcp`,
104
+ { encoding: "utf-8", timeout: 5000 }
105
+ );
106
+ const mcpOk = proc.includes("search_coding_memory");
107
+ results.push(check("MCP server starts", mcpOk, mcpOk ? "tools/list responded" : "unexpected response"));
108
+ } catch {
109
+ results.push(check("MCP server starts", false, "whyline mcp did not respond — check PATH check above"));
110
+ }
111
+
112
+ // Print results
113
+ let allOk = true;
114
+ for (const r of results) {
115
+ const icon = r.ok ? "✓" : "✗";
116
+ const detail = r.detail ? ` (${r.detail})` : "";
117
+ console.log(` ${icon} ${r.label}${detail}`);
118
+ if (!r.ok) allOk = false;
119
+ }
120
+
121
+ console.log("");
122
+ if (allOk) {
123
+ console.log("All checks passed. Whyline is ready.");
124
+ } else {
125
+ console.log("Some checks failed. Fix the issues above and re-run `whyline doctor`.");
126
+ process.exit(1);
127
+ }
128
+ }
@@ -0,0 +1,80 @@
1
+ import { execSync } from "child_process";
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+ import { isInitialized, resolveConfig } from "../config.js";
6
+ import { openDb } from "../db/connection.js";
7
+ import { getMemoryById, updateMemory, buildEmbeddingText } from "../memory/saveMemory.js";
8
+ import { parseSummary } from "../memory/parseSummary.js";
9
+ import type { CodingMemory } from "../memory/types.js";
10
+
11
+ function serializeToMarkdown(memory: CodingMemory): string {
12
+ const lines: string[] = [];
13
+ if (memory.task) { lines.push("Task:", memory.task, ""); }
14
+ lines.push("Intent:", memory.intent, "");
15
+ lines.push("Summary:", memory.summary, "");
16
+ lines.push("Decision:", memory.decision, "");
17
+ lines.push("Why:", memory.why, "");
18
+ lines.push("Alternatives rejected:");
19
+ for (const a of memory.alternativesRejected) lines.push(`- ${a}`);
20
+ lines.push("");
21
+ lines.push("Risks:");
22
+ for (const r of memory.risks) lines.push(`- ${r}`);
23
+ lines.push("");
24
+ lines.push("Follow-ups:");
25
+ for (const fu of memory.followUps) lines.push(`- ${fu}`);
26
+ lines.push("");
27
+ lines.push("Tags:");
28
+ for (const t of memory.tags) lines.push(`- ${t}`);
29
+ return lines.join("\n");
30
+ }
31
+
32
+ export async function runEdit(id: string): Promise<void> {
33
+ if (!isInitialized()) {
34
+ console.error("whyline is not initialized. Run `whyline init` first.");
35
+ process.exit(1);
36
+ }
37
+
38
+ const db = openDb(resolveConfig().storage.dbPath);
39
+ const memory = getMemoryById(db, id);
40
+
41
+ if (!memory) {
42
+ db.close();
43
+ console.error(`Memory not found: ${id}`);
44
+ process.exit(1);
45
+ }
46
+
47
+ const tmpFile = path.join(os.tmpdir(), `whyline-edit-${id}.md`);
48
+ fs.writeFileSync(tmpFile, serializeToMarkdown(memory));
49
+
50
+ const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
51
+ try {
52
+ execSync(`${editor} "${tmpFile}"`, { stdio: "inherit" });
53
+ } catch {
54
+ fs.unlinkSync(tmpFile);
55
+ db.close();
56
+ console.error("Editor exited with error. No changes saved.");
57
+ process.exit(1);
58
+ }
59
+
60
+ const edited = fs.readFileSync(tmpFile, "utf-8");
61
+ fs.unlinkSync(tmpFile);
62
+
63
+ const parsed = parseSummary(edited);
64
+ const updates = {
65
+ intent: parsed.intent,
66
+ summary: parsed.summary,
67
+ decision: parsed.decision,
68
+ why: parsed.why,
69
+ task: parsed.task,
70
+ alternativesRejected: parsed.alternativesRejected,
71
+ risks: parsed.risks,
72
+ followUps: parsed.followUps,
73
+ tags: parsed.tags,
74
+ embeddingText: buildEmbeddingText({ ...memory, ...parsed }),
75
+ };
76
+
77
+ updateMemory(db, id, updates);
78
+ db.close();
79
+ console.log(`Updated ${id}`);
80
+ }