kyp-mem 0.2.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -54
- package/bin/cli.mjs +56 -1
- package/kyp_mem/__init__.py +1 -1
- package/kyp_mem/cli.py +101 -0
- package/kyp_mem/hooks.py +150 -0
- package/kyp_mem/server.py +140 -9
- package/kyp_mem/static/index.html +1438 -324
- package/kyp_mem/ui.py +135 -2
- package/package.json +2 -2
- package/pyproject.toml +2 -2
package/README.md
CHANGED
|
@@ -1,49 +1,59 @@
|
|
|
1
1
|
# KYP-MEM — Know Your Project Memory
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Persistent knowledge base for AI agents.** Markdown vault with wikilinks, backlinks, tags, graph navigation, and auto-learning — all powered by an MCP server so Claude (or any AI) can read and write project knowledge across sessions.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
|
|
8
|
+
npm install -g kyp-mem
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Or
|
|
11
|
+
Or run directly:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
|
|
14
|
+
npx -y kyp-mem
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
## Setup
|
|
17
|
+
## Setup
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
#
|
|
21
|
-
|
|
20
|
+
kyp-mem init # Choose vault location
|
|
21
|
+
kyp-mem setup-claude # Auto-configure Claude Code MCP
|
|
22
|
+
kyp-mem install-hooks # Enable auto-learning from sessions
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Restart Claude Code. Done — kyp-mem runs headlessly every session with 9 tools available.
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
npx -y kyp-mem setup-claude
|
|
27
|
+
## Auto-Learning
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
KYP-MEM can automatically capture what happens in every Claude Code session:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
kyp-mem install-hooks --global
|
|
29
33
|
```
|
|
30
34
|
|
|
31
|
-
|
|
35
|
+
This installs two hooks:
|
|
36
|
+
- **PostToolUse** — captures file edits, writes, and commands (pure Node, fast)
|
|
37
|
+
- **Stop** — compiles session activity into a vault note under `Sessions/`
|
|
32
38
|
|
|
33
|
-
|
|
39
|
+
Sessions with fewer than 3 substantive actions are automatically skipped.
|
|
40
|
+
|
|
41
|
+
## Web UI
|
|
34
42
|
|
|
35
43
|
```bash
|
|
36
|
-
|
|
44
|
+
kyp-mem ui
|
|
37
45
|
```
|
|
38
46
|
|
|
39
|
-
Opens
|
|
40
|
-
-
|
|
41
|
-
- Rendered markdown with syntax highlighting
|
|
42
|
-
- Clickable `[[wikilinks]]`
|
|
43
|
-
- Backlinks and related notes panel
|
|
44
|
-
- Interactive D3 graph view (toggleable)
|
|
47
|
+
Opens at `localhost:3333` with:
|
|
48
|
+
- Quick switcher (`Cmd+O`) — fuzzy jump to any note
|
|
45
49
|
- Full-text search (`Cmd+K`)
|
|
46
|
-
-
|
|
50
|
+
- Tag filtering — clickable tag cloud, AND-filter
|
|
51
|
+
- Outline panel — heading TOC with click-to-scroll
|
|
52
|
+
- Backlink context — shows the surrounding line
|
|
53
|
+
- Unlinked mentions — finds references without `[[wikilinks]]`
|
|
54
|
+
- Inline editing — edit notes directly in the browser (`Cmd+S`)
|
|
55
|
+
- Local graph view — D3 force-directed graph of connections
|
|
56
|
+
- Resizable panels, collapsible tree, rendered markdown
|
|
47
57
|
|
|
48
58
|
## How It Works
|
|
49
59
|
|
|
@@ -55,85 +65,74 @@ Opens a rich interface at `localhost:3333` with:
|
|
|
55
65
|
└──────────────┘
|
|
56
66
|
```
|
|
57
67
|
|
|
58
|
-
- **Headless by default** —
|
|
59
|
-
- **Markdown
|
|
60
|
-
- **In-memory index** —
|
|
61
|
-
- **
|
|
68
|
+
- **Headless by default** — MCP server over stdio, no GUI needed
|
|
69
|
+
- **Markdown on disk** — plain `.md` files with YAML frontmatter, no database
|
|
70
|
+
- **In-memory index** — wikilinks, backlinks, tags, word-level search index
|
|
71
|
+
- **Lightweight reads** — brief mode by default (~100 tokens), full content opt-in
|
|
72
|
+
- **Graph navigation** — follow `[[links]]` instead of searching broadly
|
|
62
73
|
|
|
63
74
|
## Commands
|
|
64
75
|
|
|
65
76
|
| Command | What it does |
|
|
66
77
|
|---------|-------------|
|
|
67
78
|
| `kyp-mem init` | First-time setup — choose vault location |
|
|
68
|
-
| `kyp-mem setup-claude` | Register
|
|
79
|
+
| `kyp-mem setup-claude` | Register MCP server with Claude Code |
|
|
69
80
|
| `kyp-mem setup-claude --global` | Configure globally (all projects) |
|
|
81
|
+
| `kyp-mem install-hooks` | Enable auto-learning from sessions |
|
|
82
|
+
| `kyp-mem install-hooks --remove` | Remove auto-learning hooks |
|
|
70
83
|
| `kyp-mem serve` | Start MCP server (used by Claude, not you) |
|
|
71
84
|
| `kyp-mem ui` | Open web UI at localhost:3333 |
|
|
72
85
|
| `kyp-mem stats` | Print vault statistics |
|
|
73
86
|
| `kyp-mem tree` | Print vault tree |
|
|
74
87
|
| `kyp-mem doctor` | Check installation health |
|
|
75
88
|
|
|
76
|
-
## MCP Tools (
|
|
89
|
+
## MCP Tools (9 tools)
|
|
77
90
|
|
|
78
91
|
| Tool | Description |
|
|
79
92
|
|------|-------------|
|
|
80
|
-
| `kyp_list` | Browse
|
|
81
|
-
| `kyp_read` |
|
|
93
|
+
| `kyp_list` | Browse folders and notes with inline tags |
|
|
94
|
+
| `kyp_read` | Brief summary by default; `full=True` for complete content |
|
|
82
95
|
| `kyp_write` | Create or update a note with tags and properties |
|
|
83
96
|
| `kyp_delete` | Delete a note |
|
|
84
|
-
| `kyp_search` | Full-text search
|
|
97
|
+
| `kyp_search` | Full-text search with optional tag filter |
|
|
85
98
|
| `kyp_tags` | List all tags or filter notes by tag |
|
|
86
|
-
| `kyp_related` | Find related notes by links, tags, proximity |
|
|
99
|
+
| `kyp_related` | Find related notes by links, tags, folder proximity |
|
|
87
100
|
| `kyp_recent` | Recently modified notes |
|
|
88
101
|
| `kyp_stats` | Vault statistics |
|
|
89
102
|
|
|
90
103
|
## Note Format
|
|
91
104
|
|
|
92
|
-
Standard markdown with YAML frontmatter:
|
|
93
|
-
|
|
94
105
|
```markdown
|
|
95
106
|
---
|
|
96
107
|
tags: [project, trading, config]
|
|
97
|
-
source: config.py
|
|
98
108
|
created: 2026-05-12
|
|
99
|
-
updated: 2026-05-12
|
|
100
109
|
---
|
|
101
110
|
|
|
102
111
|
# Configuration
|
|
103
112
|
|
|
104
|
-
Settings are
|
|
113
|
+
Settings are in `HedgeConfig`. See [[Risk Management]] for safety checks.
|
|
105
114
|
```
|
|
106
115
|
|
|
107
|
-
`[[Wikilinks]]` are
|
|
116
|
+
`[[Wikilinks]]` are parsed, indexed, and resolved into navigable backlinks automatically.
|
|
108
117
|
|
|
109
118
|
## Manual Claude Code Config
|
|
110
119
|
|
|
111
|
-
If you prefer to configure manually instead of using `setup-claude`:
|
|
112
|
-
|
|
113
120
|
```bash
|
|
114
|
-
claude mcp add -s
|
|
121
|
+
claude mcp add -s user -e KYP_VAULT="$HOME/.kyp-mem/vault" kyp-mem -- npx -y kyp-mem serve
|
|
115
122
|
```
|
|
116
123
|
|
|
117
|
-
Use `-s user` instead of `-s local` to make it available in all projects.
|
|
118
|
-
|
|
119
124
|
## Architecture
|
|
120
125
|
|
|
121
126
|
```
|
|
122
127
|
~/.kyp-mem/
|
|
123
|
-
├── config.json # vault path
|
|
124
|
-
|
|
128
|
+
├── config.json # vault path
|
|
129
|
+
├── sessions/ # auto-learning session logs
|
|
130
|
+
└── vault/
|
|
125
131
|
├── Project A/
|
|
126
132
|
│ ├── Architecture.md
|
|
127
|
-
│ ├── Configuration.md
|
|
128
133
|
│ └── Bugs.md
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
## Publishing to npm
|
|
134
|
-
|
|
135
|
-
```bash
|
|
136
|
-
npm publish
|
|
134
|
+
├── Sessions/ # auto-captured session notes
|
|
135
|
+
└── ...
|
|
137
136
|
```
|
|
138
137
|
|
|
139
138
|
## License
|
package/bin/cli.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { spawnSync } from "child_process";
|
|
4
|
-
import {
|
|
4
|
+
import { appendFileSync, mkdirSync } from "fs";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import { delimiter, dirname, join, resolve } from "path";
|
|
5
7
|
import { fileURLToPath } from "url";
|
|
6
8
|
|
|
7
9
|
const args = process.argv.slice(2);
|
|
@@ -47,6 +49,59 @@ function findPython() {
|
|
|
47
49
|
return null;
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
// --- Hook fast path (pure Node, no Python startup) ---
|
|
53
|
+
if (args[0] === "hook") {
|
|
54
|
+
const hookType = args[1];
|
|
55
|
+
const sessionDir = join(homedir(), ".kyp-mem", "sessions");
|
|
56
|
+
const sessionFile = join(sessionDir, "current.jsonl");
|
|
57
|
+
|
|
58
|
+
const chunks = [];
|
|
59
|
+
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
60
|
+
await new Promise((r) => process.stdin.on("end", r));
|
|
61
|
+
const raw = Buffer.concat(chunks).toString();
|
|
62
|
+
|
|
63
|
+
if (hookType === "post-tool-use") {
|
|
64
|
+
try {
|
|
65
|
+
const data = JSON.parse(raw);
|
|
66
|
+
const tool = data.tool_name || "";
|
|
67
|
+
if (tool.includes("kyp-mem") || tool.includes("kyp_mem")) process.exit(0);
|
|
68
|
+
|
|
69
|
+
const input = data.tool_input || {};
|
|
70
|
+
const entry = { ts: new Date().toISOString(), tool, cwd: process.cwd() };
|
|
71
|
+
|
|
72
|
+
if (tool === "Edit" || tool === "Write") {
|
|
73
|
+
entry.file = input.file_path || "";
|
|
74
|
+
entry.action = tool === "Edit" ? "edit" : "create";
|
|
75
|
+
} else if (tool === "Bash") {
|
|
76
|
+
entry.command = (input.command || "").slice(0, 300);
|
|
77
|
+
entry.action = "command";
|
|
78
|
+
} else {
|
|
79
|
+
entry.action = "other";
|
|
80
|
+
entry.detail = tool;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
84
|
+
appendFileSync(sessionFile, JSON.stringify(entry) + "\n");
|
|
85
|
+
} catch (_) {
|
|
86
|
+
// silent — hooks must never break the flow
|
|
87
|
+
}
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (hookType === "stop") {
|
|
92
|
+
const py = findPython();
|
|
93
|
+
if (py) {
|
|
94
|
+
const [cmd, pre] = py;
|
|
95
|
+
const r = run(cmd, [...pre, "-m", "kyp_mem.hooks", "stop"], "inherit");
|
|
96
|
+
process.exit(r.status ?? 0);
|
|
97
|
+
}
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.error("Unknown hook type:", hookType);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
50
105
|
const python = findPython();
|
|
51
106
|
|
|
52
107
|
if (python) {
|
package/kyp_mem/__init__.py
CHANGED
package/kyp_mem/cli.py
CHANGED
|
@@ -36,6 +36,11 @@ def main():
|
|
|
36
36
|
|
|
37
37
|
subparsers.add_parser("stats", help="Print vault statistics")
|
|
38
38
|
subparsers.add_parser("tree", help="Print vault tree")
|
|
39
|
+
ih = subparsers.add_parser("install-hooks", help="Set up auto-learning hooks for Claude Code")
|
|
40
|
+
ih.add_argument("--global", dest="global_config", action="store_true",
|
|
41
|
+
help="Add hooks to global ~/.claude/settings.json (default: project)")
|
|
42
|
+
ih.add_argument("--remove", action="store_true", help="Remove KYP-MEM hooks")
|
|
43
|
+
|
|
39
44
|
subparsers.add_parser("doctor", help="Check installation and config health")
|
|
40
45
|
|
|
41
46
|
args = parser.parse_args()
|
|
@@ -57,6 +62,8 @@ def main():
|
|
|
57
62
|
_run_stats()
|
|
58
63
|
elif args.command == "tree":
|
|
59
64
|
_run_tree()
|
|
65
|
+
elif args.command == "install-hooks":
|
|
66
|
+
_run_install_hooks(global_config=args.global_config, remove=args.remove)
|
|
60
67
|
elif args.command == "doctor":
|
|
61
68
|
_run_doctor()
|
|
62
69
|
else:
|
|
@@ -237,6 +244,78 @@ def _write_legacy_claude_settings(
|
|
|
237
244
|
return settings_path
|
|
238
245
|
|
|
239
246
|
|
|
247
|
+
def _run_install_hooks(global_config: bool = False, remove: bool = False):
|
|
248
|
+
mcp_command, _ = _get_mcp_command()
|
|
249
|
+
|
|
250
|
+
if global_config:
|
|
251
|
+
settings_path = Path.home() / ".claude" / "settings.json"
|
|
252
|
+
scope_label = "global"
|
|
253
|
+
else:
|
|
254
|
+
settings_path = Path.cwd() / ".claude" / "settings.json"
|
|
255
|
+
scope_label = "project"
|
|
256
|
+
|
|
257
|
+
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
258
|
+
settings = {}
|
|
259
|
+
if settings_path.exists():
|
|
260
|
+
try:
|
|
261
|
+
settings = json.loads(settings_path.read_text())
|
|
262
|
+
except json.JSONDecodeError:
|
|
263
|
+
settings = {}
|
|
264
|
+
|
|
265
|
+
hooks = settings.setdefault("hooks", {})
|
|
266
|
+
|
|
267
|
+
if remove:
|
|
268
|
+
changed = False
|
|
269
|
+
for event in ("PostToolUse", "Stop"):
|
|
270
|
+
if event in hooks:
|
|
271
|
+
hooks[event] = [h for h in hooks[event] if "kyp-mem hook" not in h.get("command", "")]
|
|
272
|
+
if not hooks[event]:
|
|
273
|
+
del hooks[event]
|
|
274
|
+
changed = True
|
|
275
|
+
if not hooks:
|
|
276
|
+
del settings["hooks"]
|
|
277
|
+
settings_path.write_text(json.dumps(settings, indent=2) + "\n")
|
|
278
|
+
print()
|
|
279
|
+
print(f" {G}✓{R} KYP-MEM hooks removed from {scope_label} settings")
|
|
280
|
+
print(f" {D} File: {settings_path}{R}")
|
|
281
|
+
print()
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
post_tool_hooks = hooks.setdefault("PostToolUse", [])
|
|
285
|
+
stop_hooks = hooks.setdefault("Stop", [])
|
|
286
|
+
|
|
287
|
+
post_tool_hooks = [h for h in post_tool_hooks if "kyp-mem hook" not in h.get("command", "")]
|
|
288
|
+
stop_hooks = [h for h in stop_hooks if "kyp-mem hook" not in h.get("command", "")]
|
|
289
|
+
|
|
290
|
+
post_tool_hooks.append({
|
|
291
|
+
"matcher": "Edit|Write|Bash",
|
|
292
|
+
"hooks": [{"type": "command", "command": f"{mcp_command} hook post-tool-use"}],
|
|
293
|
+
})
|
|
294
|
+
stop_hooks.append({
|
|
295
|
+
"hooks": [{"type": "command", "command": f"{mcp_command} hook stop"}],
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
hooks["PostToolUse"] = post_tool_hooks
|
|
299
|
+
hooks["Stop"] = stop_hooks
|
|
300
|
+
|
|
301
|
+
settings_path.write_text(json.dumps(settings, indent=2) + "\n")
|
|
302
|
+
|
|
303
|
+
print()
|
|
304
|
+
print(f" {C}KYP-MEM{R} — Auto-Learning Hooks")
|
|
305
|
+
print()
|
|
306
|
+
print(f" {G}✓{R} Hooks installed ({scope_label})")
|
|
307
|
+
print(f" {D} File: {settings_path}{R}")
|
|
308
|
+
print()
|
|
309
|
+
print(f" How it works:")
|
|
310
|
+
print(f" {D} • PostToolUse hook captures file edits, writes, and commands{R}")
|
|
311
|
+
print(f" {D} • Stop hook compiles the session into a vault note{R}")
|
|
312
|
+
print(f" {D} • Notes saved under Sessions/ with timestamps and tags{R}")
|
|
313
|
+
print(f" {D} • Sessions with < 3 substantive actions are skipped{R}")
|
|
314
|
+
print()
|
|
315
|
+
print(f" {C}Done!{R} Restart Claude Code. Sessions will auto-save to your vault.")
|
|
316
|
+
print()
|
|
317
|
+
|
|
318
|
+
|
|
240
319
|
def _run_stats():
|
|
241
320
|
from .config import get_vault_path
|
|
242
321
|
from .vault import Vault
|
|
@@ -314,6 +393,28 @@ def _run_doctor():
|
|
|
314
393
|
if "kyp-mem" in s.get("mcpServers", {}):
|
|
315
394
|
print(f" {D}·{R} Legacy Claude settings ({label}): kyp-mem entry present")
|
|
316
395
|
|
|
396
|
+
# Hooks
|
|
397
|
+
for label, path in legacy_paths:
|
|
398
|
+
if not path.exists():
|
|
399
|
+
continue
|
|
400
|
+
try:
|
|
401
|
+
s = json.loads(path.read_text())
|
|
402
|
+
except json.JSONDecodeError:
|
|
403
|
+
continue
|
|
404
|
+
hooks = s.get("hooks", {})
|
|
405
|
+
has_post = any("kyp-mem hook" in h.get("command", "") for h in hooks.get("PostToolUse", []))
|
|
406
|
+
has_stop = any("kyp-mem hook" in h.get("command", "") for h in hooks.get("Stop", []))
|
|
407
|
+
if has_post and has_stop:
|
|
408
|
+
print(f" {G}✓{R} Auto-learning hooks installed ({label})")
|
|
409
|
+
elif has_post or has_stop:
|
|
410
|
+
print(f" {Y}!{R} Partial hooks installed ({label}) — run: kyp-mem install-hooks")
|
|
411
|
+
|
|
412
|
+
# Session log
|
|
413
|
+
session_file = Path.home() / ".kyp-mem" / "sessions" / "current.jsonl"
|
|
414
|
+
if session_file.exists():
|
|
415
|
+
line_count = len(session_file.read_text().strip().split("\n"))
|
|
416
|
+
print(f" {D}·{R} Active session log: {line_count} entries")
|
|
417
|
+
|
|
317
418
|
# Binary
|
|
318
419
|
kyp_bin = shutil.which("kyp-mem")
|
|
319
420
|
if kyp_bin:
|
package/kyp_mem/hooks.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""KYP-MEM session hooks — compile captured tool activity into vault notes."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import json
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
SESSION_DIR = Path.home() / ".kyp-mem" / "sessions"
|
|
9
|
+
CURRENT_SESSION = SESSION_DIR / "current.jsonl"
|
|
10
|
+
|
|
11
|
+
MIN_ACTIONS = 3
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def handle_stop():
|
|
15
|
+
if not CURRENT_SESSION.exists():
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
text = CURRENT_SESSION.read_text().strip()
|
|
19
|
+
if not text:
|
|
20
|
+
CURRENT_SESSION.unlink(missing_ok=True)
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
entries = []
|
|
24
|
+
for line in text.split("\n"):
|
|
25
|
+
try:
|
|
26
|
+
entries.append(json.loads(line))
|
|
27
|
+
except json.JSONDecodeError:
|
|
28
|
+
continue
|
|
29
|
+
|
|
30
|
+
if not entries:
|
|
31
|
+
CURRENT_SESSION.unlink(missing_ok=True)
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
write_actions = [e for e in entries if e.get("action") in ("edit", "create", "command")]
|
|
35
|
+
if len(write_actions) < MIN_ACTIONS:
|
|
36
|
+
CURRENT_SESSION.unlink(missing_ok=True)
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
project_dir = entries[0].get("cwd", "unknown")
|
|
40
|
+
project_name = Path(project_dir).name
|
|
41
|
+
session_id = datetime.now().strftime("%Y-%m-%d_%H%M%S")
|
|
42
|
+
|
|
43
|
+
files_edited = set()
|
|
44
|
+
files_created = set()
|
|
45
|
+
commands = []
|
|
46
|
+
timeline = []
|
|
47
|
+
|
|
48
|
+
for e in entries:
|
|
49
|
+
ts_raw = e.get("ts", "")
|
|
50
|
+
ts = ts_raw[11:19] if len(ts_raw) >= 19 else ""
|
|
51
|
+
action = e.get("action", "")
|
|
52
|
+
|
|
53
|
+
if action == "edit":
|
|
54
|
+
fp = e.get("file", "")
|
|
55
|
+
files_edited.add(fp)
|
|
56
|
+
timeline.append(f" {ts} — Edit `{Path(fp).name}`")
|
|
57
|
+
elif action == "create":
|
|
58
|
+
fp = e.get("file", "")
|
|
59
|
+
files_created.add(fp)
|
|
60
|
+
timeline.append(f" {ts} — Write `{Path(fp).name}`")
|
|
61
|
+
elif action == "command":
|
|
62
|
+
cmd = e.get("command", "")
|
|
63
|
+
commands.append(cmd)
|
|
64
|
+
short = cmd[:80] + "..." if len(cmd) > 80 else cmd
|
|
65
|
+
timeline.append(f" {ts} — `{short}`")
|
|
66
|
+
|
|
67
|
+
summary_items = []
|
|
68
|
+
if files_edited:
|
|
69
|
+
summary_items.append(f"Modified {len(files_edited)} file{'s' if len(files_edited) != 1 else ''}")
|
|
70
|
+
if files_created:
|
|
71
|
+
summary_items.append(f"Created {len(files_created)} file{'s' if len(files_created) != 1 else ''}")
|
|
72
|
+
if commands:
|
|
73
|
+
summary_items.append(f"Ran {len(commands)} command{'s' if len(commands) != 1 else ''}")
|
|
74
|
+
|
|
75
|
+
investigate_keywords = {'grep', 'find', 'cat', 'head', 'tail', 'less', 'ls', 'tree', 'rg', 'ag', 'fd', 'wc', 'diff'}
|
|
76
|
+
investigated_cmds = []
|
|
77
|
+
for cmd in commands:
|
|
78
|
+
first_word = cmd.strip().split()[0] if cmd.strip() else ''
|
|
79
|
+
if first_word in investigate_keywords:
|
|
80
|
+
investigated_cmds.append(cmd)
|
|
81
|
+
|
|
82
|
+
parts = [f"# Session {session_id}", ""]
|
|
83
|
+
parts.append(f"**Project:** `{project_dir}`")
|
|
84
|
+
parts.append(f"**Actions:** {len(entries)} total, {len(write_actions)} substantive")
|
|
85
|
+
parts.append("")
|
|
86
|
+
|
|
87
|
+
parts.append("## Summary")
|
|
88
|
+
parts.append(", ".join(summary_items) + f" in `{project_name}`." if summary_items else "")
|
|
89
|
+
parts.append("")
|
|
90
|
+
|
|
91
|
+
parts.append("## INVESTIGATED")
|
|
92
|
+
if investigated_cmds:
|
|
93
|
+
for cmd in investigated_cmds[:15]:
|
|
94
|
+
short = cmd[:120] + "..." if len(cmd) > 120 else cmd
|
|
95
|
+
parts.append(f"- `{short}`")
|
|
96
|
+
parts.append("")
|
|
97
|
+
|
|
98
|
+
parts.append("## LEARNED")
|
|
99
|
+
parts.append("")
|
|
100
|
+
parts.append("")
|
|
101
|
+
|
|
102
|
+
parts.append("## COMPLETED")
|
|
103
|
+
if files_edited:
|
|
104
|
+
for f in sorted(files_edited):
|
|
105
|
+
parts.append(f"- Modified `{Path(f).name}`")
|
|
106
|
+
if files_created:
|
|
107
|
+
for f in sorted(files_created):
|
|
108
|
+
parts.append(f"- Created `{Path(f).name}`")
|
|
109
|
+
parts.append("")
|
|
110
|
+
|
|
111
|
+
parts.append("## NEXT STEPS")
|
|
112
|
+
parts.append("")
|
|
113
|
+
parts.append("")
|
|
114
|
+
|
|
115
|
+
if timeline:
|
|
116
|
+
parts.append("## Timeline")
|
|
117
|
+
for line in timeline[:40]:
|
|
118
|
+
parts.append(line)
|
|
119
|
+
if len(timeline) > 40:
|
|
120
|
+
parts.append(f" ... and {len(timeline) - 40} more actions")
|
|
121
|
+
|
|
122
|
+
content = "\n".join(parts)
|
|
123
|
+
tags = ["session", "auto-captured", project_name]
|
|
124
|
+
|
|
125
|
+
from .config import get_vault_path
|
|
126
|
+
from .vault import Vault
|
|
127
|
+
|
|
128
|
+
vault = Vault(get_vault_path())
|
|
129
|
+
vault.write_note(f"{project_name}/Sessions/{session_id}.md", content, tags, {})
|
|
130
|
+
|
|
131
|
+
CURRENT_SESSION.unlink(missing_ok=True)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def main():
|
|
135
|
+
if len(sys.argv) > 1 and sys.argv[1] == "stop":
|
|
136
|
+
handle_stop()
|
|
137
|
+
else:
|
|
138
|
+
raw = sys.stdin.read().strip()
|
|
139
|
+
if not raw:
|
|
140
|
+
return
|
|
141
|
+
try:
|
|
142
|
+
data = json.loads(raw)
|
|
143
|
+
except json.JSONDecodeError:
|
|
144
|
+
return
|
|
145
|
+
if "stop_reason" in data:
|
|
146
|
+
handle_stop()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
main()
|