@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.
- package/.claude/settings.local.json +33 -0
- package/.github/workflows/ci.yml +35 -0
- package/.github/workflows/publish.yml +37 -0
- package/.prettierrc.json +7 -0
- package/CLAUDE.md +74 -0
- package/LICENSE +21 -0
- package/README.md +359 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +125 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/delete.d.ts +3 -0
- package/dist/commands/delete.js +42 -0
- package/dist/commands/delete.js.map +1 -0
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +111 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/edit.d.ts +1 -0
- package/dist/commands/edit.js +78 -0
- package/dist/commands/edit.js.map +1 -0
- package/dist/commands/export.d.ts +8 -0
- package/dist/commands/export.js +90 -0
- package/dist/commands/export.js.map +1 -0
- package/dist/commands/import.d.ts +1 -0
- package/dist/commands/import.js +110 -0
- package/dist/commands/import.js.map +1 -0
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.js +23 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/install-claude.d.ts +3 -0
- package/dist/commands/install-claude.js +180 -0
- package/dist/commands/install-claude.js.map +1 -0
- package/dist/commands/list.d.ts +4 -0
- package/dist/commands/list.js +35 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/mcp.d.ts +1 -0
- package/dist/commands/mcp.js +10 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/save.d.ts +4 -0
- package/dist/commands/save.js +74 -0
- package/dist/commands/save.js.map +1 -0
- package/dist/commands/search.d.ts +7 -0
- package/dist/commands/search.js +46 -0
- package/dist/commands/search.js.map +1 -0
- package/dist/commands/show.d.ts +3 -0
- package/dist/commands/show.js +30 -0
- package/dist/commands/show.js.map +1 -0
- package/dist/commands/stats.d.ts +1 -0
- package/dist/commands/stats.js +27 -0
- package/dist/commands/stats.js.map +1 -0
- package/dist/commands/summarize.d.ts +3 -0
- package/dist/commands/summarize.js +140 -0
- package/dist/commands/summarize.js.map +1 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.js +17 -0
- package/dist/config.js.map +1 -0
- package/dist/db/connection.d.ts +2 -0
- package/dist/db/connection.js +8 -0
- package/dist/db/connection.js.map +1 -0
- package/dist/db/migrations.d.ts +2 -0
- package/dist/db/migrations.js +19 -0
- package/dist/db/migrations.js.map +1 -0
- package/dist/db/schema.d.ts +5 -0
- package/dist/db/schema.js +64 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/git/diff.d.ts +2 -0
- package/dist/git/diff.js +45 -0
- package/dist/git/diff.js.map +1 -0
- package/dist/git/git.d.ts +3 -0
- package/dist/git/git.js +25 -0
- package/dist/git/git.js.map +1 -0
- package/dist/git/repoId.d.ts +3 -0
- package/dist/git/repoId.js +49 -0
- package/dist/git/repoId.js.map +1 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +296 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +119 -0
- package/dist/mcp/tools.js +43 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/memory/parseSummary.d.ts +14 -0
- package/dist/memory/parseSummary.js +53 -0
- package/dist/memory/parseSummary.js.map +1 -0
- package/dist/memory/qualityCheck.d.ts +13 -0
- package/dist/memory/qualityCheck.js +78 -0
- package/dist/memory/qualityCheck.js.map +1 -0
- package/dist/memory/redactSecrets.d.ts +7 -0
- package/dist/memory/redactSecrets.js +29 -0
- package/dist/memory/redactSecrets.js.map +1 -0
- package/dist/memory/repoContext.d.ts +2 -0
- package/dist/memory/repoContext.js +23 -0
- package/dist/memory/repoContext.js.map +1 -0
- package/dist/memory/saveMemory.d.ts +40 -0
- package/dist/memory/saveMemory.js +223 -0
- package/dist/memory/saveMemory.js.map +1 -0
- package/dist/memory/searchMemory.d.ts +17 -0
- package/dist/memory/searchMemory.js +122 -0
- package/dist/memory/searchMemory.js.map +1 -0
- package/dist/memory/types.d.ts +48 -0
- package/dist/memory/types.js +2 -0
- package/dist/memory/types.js.map +1 -0
- package/dist/output/format.d.ts +3 -0
- package/dist/output/format.js +43 -0
- package/dist/output/format.js.map +1 -0
- package/docs/architecture.md +387 -0
- package/docs/ec6ab3bf-60cf-4629-ad9e-3048e8e3c43a.png +0 -0
- package/docs/logo.png +0 -0
- package/eslint.config.js +16 -0
- package/how-to-run/01-install.md +69 -0
- package/how-to-run/02-wire-up-your-repo.md +80 -0
- package/how-to-run/03-test-it-manually.md +91 -0
- package/how-to-run/04-test-with-claude-code.md +70 -0
- package/how-to-run/CLAUDE.md.template +72 -0
- package/how-to-run/README.md +49 -0
- package/package.json +60 -0
- package/src/cli.ts +142 -0
- package/src/commands/delete.ts +47 -0
- package/src/commands/doctor.ts +128 -0
- package/src/commands/edit.ts +80 -0
- package/src/commands/export.ts +95 -0
- package/src/commands/import.ts +119 -0
- package/src/commands/init.ts +31 -0
- package/src/commands/install-claude.ts +203 -0
- package/src/commands/list.ts +41 -0
- package/src/commands/mcp.ts +12 -0
- package/src/commands/save.ts +85 -0
- package/src/commands/search.ts +56 -0
- package/src/commands/show.ts +37 -0
- package/src/commands/stats.ts +31 -0
- package/src/commands/summarize.ts +183 -0
- package/src/config.ts +26 -0
- package/src/db/connection.ts +8 -0
- package/src/db/migrations.ts +26 -0
- package/src/db/schema.ts +68 -0
- package/src/git/diff.ts +43 -0
- package/src/git/git.ts +25 -0
- package/src/git/repoId.ts +49 -0
- package/src/hooks/post-commit.sample.sh +9 -0
- package/src/mcp/server.ts +326 -0
- package/src/mcp/tools.ts +53 -0
- package/src/memory/parseSummary.ts +72 -0
- package/src/memory/qualityCheck.ts +102 -0
- package/src/memory/redactSecrets.ts +32 -0
- package/src/memory/repoContext.ts +25 -0
- package/src/memory/saveMemory.ts +369 -0
- package/src/memory/searchMemory.ts +153 -0
- package/src/memory/types.ts +57 -0
- package/src/output/format.ts +44 -0
- package/src/skill/SKILL.md +95 -0
- package/tests/cliV02.test.ts +213 -0
- package/tests/doctor.test.ts +253 -0
- package/tests/exportImport.test.ts +248 -0
- package/tests/fileRename.test.ts +156 -0
- package/tests/gitHelpers.test.ts +94 -0
- package/tests/init.test.ts +93 -0
- package/tests/installClaude.test.ts +157 -0
- package/tests/parseSummary.test.ts +111 -0
- package/tests/qualityCheck.test.ts +182 -0
- package/tests/redactSecrets.test.ts +75 -0
- package/tests/saveMemory.test.ts +196 -0
- package/tests/searchFilters.test.ts +139 -0
- package/tests/searchMemory.test.ts +273 -0
- package/tests/stale.test.ts +47 -0
- package/tsconfig.json +18 -0
- 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
|
+
}
|