@pcircle/memesh 4.5.1 → 4.6.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-plugin/marketplace.json +5 -3
- package/.claude-plugin/plugin.json +6 -4
- package/AGENTS.md +95 -0
- package/README.de.md +129 -35
- package/README.md +161 -34
- package/README.zh-TW.md +130 -35
- package/dashboard/dist/index.html +10 -10
- package/dist/cli/view-live.js +3 -3
- package/dist/core/auto-tagger.d.ts.map +1 -1
- package/dist/core/auto-tagger.js +4 -9
- package/dist/core/auto-tagger.js.map +1 -1
- package/dist/core/briefing.d.ts +8 -0
- package/dist/core/briefing.d.ts.map +1 -0
- package/dist/core/briefing.js +91 -0
- package/dist/core/briefing.js.map +1 -0
- package/dist/core/capture-flag.d.ts +5 -0
- package/dist/core/capture-flag.d.ts.map +1 -0
- package/dist/core/capture-flag.js +10 -0
- package/dist/core/capture-flag.js.map +1 -0
- package/dist/core/conflict-candidates.d.ts +20 -0
- package/dist/core/conflict-candidates.d.ts.map +1 -0
- package/dist/core/conflict-candidates.js +79 -0
- package/dist/core/conflict-candidates.js.map +1 -0
- package/dist/core/conflict-judge.d.ts +47 -0
- package/dist/core/conflict-judge.d.ts.map +1 -0
- package/dist/core/conflict-judge.js +189 -0
- package/dist/core/conflict-judge.js.map +1 -0
- package/dist/core/digest-validator.d.ts.map +1 -1
- package/dist/core/digest-validator.js +3 -5
- package/dist/core/digest-validator.js.map +1 -1
- package/dist/core/doctor.d.ts +2 -0
- package/dist/core/doctor.d.ts.map +1 -1
- package/dist/core/doctor.js +34 -56
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/dreamer.d.ts +5 -2
- package/dist/core/dreamer.d.ts.map +1 -1
- package/dist/core/dreamer.js +108 -25
- package/dist/core/dreamer.js.map +1 -1
- package/dist/core/embedder.d.ts +5 -4
- package/dist/core/embedder.d.ts.map +1 -1
- package/dist/core/embedder.js +16 -8
- package/dist/core/embedder.js.map +1 -1
- package/dist/core/failure-analyzer.d.ts.map +1 -1
- package/dist/core/failure-analyzer.js +7 -12
- package/dist/core/failure-analyzer.js.map +1 -1
- package/dist/core/install-channel.d.ts +1 -1
- package/dist/core/install-channel.d.ts.map +1 -1
- package/dist/core/install-channel.js +16 -5
- package/dist/core/install-channel.js.map +1 -1
- package/dist/core/install-hooks.d.ts +5 -0
- package/dist/core/install-hooks.d.ts.map +1 -1
- package/dist/core/install-hooks.js +0 -0
- package/dist/core/install-hooks.js.map +1 -1
- package/dist/core/json-utils.d.ts +1 -0
- package/dist/core/json-utils.d.ts.map +1 -1
- package/dist/core/json-utils.js +19 -10
- package/dist/core/json-utils.js.map +1 -1
- package/dist/core/kg-backfill.d.ts +0 -1
- package/dist/core/kg-backfill.d.ts.map +1 -1
- package/dist/core/kg-backfill.js +0 -3
- package/dist/core/kg-backfill.js.map +1 -1
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +14 -21
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core/memory-tool.d.ts.map +1 -1
- package/dist/core/memory-tool.js +4 -4
- package/dist/core/memory-tool.js.map +1 -1
- package/dist/core/operations.d.ts.map +1 -1
- package/dist/core/operations.js +22 -13
- package/dist/core/operations.js.map +1 -1
- package/dist/core/prompt-safety.d.ts +1 -0
- package/dist/core/prompt-safety.d.ts.map +1 -1
- package/dist/core/prompt-safety.js +7 -0
- package/dist/core/prompt-safety.js.map +1 -1
- package/dist/core/schema-export.d.ts.map +1 -1
- package/dist/core/schema-export.js +31 -0
- package/dist/core/schema-export.js.map +1 -1
- package/dist/core/setup.d.ts +29 -0
- package/dist/core/setup.d.ts.map +1 -0
- package/dist/core/setup.js +127 -0
- package/dist/core/setup.js.map +1 -0
- package/dist/core/task-state-store.d.ts +17 -0
- package/dist/core/task-state-store.d.ts.map +1 -0
- package/dist/core/task-state-store.js +45 -0
- package/dist/core/task-state-store.js.map +1 -0
- package/dist/core/task-state.d.ts +19 -0
- package/dist/core/task-state.d.ts.map +1 -0
- package/dist/core/task-state.js +91 -0
- package/dist/core/task-state.js.map +1 -0
- package/dist/core/time-utils.d.ts +2 -0
- package/dist/core/time-utils.d.ts.map +1 -0
- package/dist/core/time-utils.js +14 -0
- package/dist/core/time-utils.js.map +1 -0
- package/dist/core/title.d.ts +5 -0
- package/dist/core/title.d.ts.map +1 -0
- package/dist/core/title.js +14 -0
- package/dist/core/title.js.map +1 -0
- package/dist/core/transcript-source.d.ts.map +1 -1
- package/dist/core/transcript-source.js +2 -3
- package/dist/core/transcript-source.js.map +1 -1
- package/dist/core/types.d.ts +4 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/work-topology.d.ts +33 -0
- package/dist/core/work-topology.d.ts.map +1 -0
- package/dist/core/work-topology.js +183 -0
- package/dist/core/work-topology.js.map +1 -0
- package/dist/db.d.ts +2 -7
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +144 -284
- package/dist/db.js.map +1 -1
- package/dist/knowledge-graph.d.ts +1 -0
- package/dist/knowledge-graph.d.ts.map +1 -1
- package/dist/knowledge-graph.js +50 -40
- package/dist/knowledge-graph.js.map +1 -1
- package/dist/skills-manifest.json +48 -18
- package/dist/storage/conflicts.d.ts.map +1 -1
- package/dist/storage/conflicts.js +2 -7
- package/dist/storage/conflicts.js.map +1 -1
- package/dist/storage/fts-index.d.ts +4 -2
- package/dist/storage/fts-index.d.ts.map +1 -1
- package/dist/storage/fts-index.js +16 -4
- package/dist/storage/fts-index.js.map +1 -1
- package/dist/storage/schema.d.ts +20 -0
- package/dist/storage/schema.d.ts.map +1 -0
- package/dist/storage/schema.js +274 -0
- package/dist/storage/schema.js.map +1 -0
- package/dist/transports/cli/cli.d.ts +1 -4
- package/dist/transports/cli/cli.d.ts.map +1 -1
- package/dist/transports/cli/cli.js +382 -6
- package/dist/transports/cli/cli.js.map +1 -1
- package/dist/transports/http/server.d.ts.map +1 -1
- package/dist/transports/http/server.js +208 -307
- package/dist/transports/http/server.js.map +1 -1
- package/dist/transports/mcp/handlers.d.ts +46 -0
- package/dist/transports/mcp/handlers.d.ts.map +1 -1
- package/dist/transports/mcp/handlers.js +57 -2
- package/dist/transports/mcp/handlers.js.map +1 -1
- package/dist/transports/schemas.d.ts +21 -10
- package/dist/transports/schemas.d.ts.map +1 -1
- package/dist/transports/schemas.js +26 -8
- package/dist/transports/schemas.js.map +1 -1
- package/llms-install.md +138 -0
- package/package.json +14 -9
- package/scripts/hooks/_generated/capture-flag.js +17 -0
- package/scripts/hooks/_generated/fts-index.js +16 -4
- package/scripts/hooks/_generated/schema.js +281 -0
- package/scripts/hooks/_generated/task-state.js +98 -0
- package/scripts/hooks/_generated/time-utils.js +21 -0
- package/scripts/hooks/_generated/title.js +21 -0
- package/scripts/hooks/_generated/work-topology.js +190 -0
- package/scripts/hooks/_shared.js +122 -478
- package/scripts/hooks/post-commit.js +4 -1
- package/scripts/hooks/pre-compact.js +13 -1
- package/scripts/hooks/pre-edit-recall.js +5 -3
- package/scripts/hooks/session-start.js +135 -59
- package/scripts/hooks/session-summary.js +59 -24
- package/skills/memesh/SKILL.md +97 -76
- package/README.es.md +0 -467
- package/README.fr.md +0 -459
- package/README.ja.md +0 -467
- package/README.ko.md +0 -467
- package/README.pt.md +0 -459
- package/README.th.md +0 -460
- package/README.vi.md +0 -459
- package/README.zh-CN.md +0 -466
- package/dist/cli/view.d.ts +0 -3
- package/dist/cli/view.d.ts.map +0 -1
- package/dist/cli/view.js +0 -523
- package/dist/cli/view.js.map +0 -1
package/llms-install.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Installing MeMesh — deterministic steps for an AI agent
|
|
2
|
+
|
|
3
|
+
This document is for an AI agent installing MeMesh (`@pcircle/memesh`) on its
|
|
4
|
+
user's machine. Execute each section exactly, verify with the stated check,
|
|
5
|
+
then stop. Once installed, read [AGENTS.md](AGENTS.md) for how to use it.
|
|
6
|
+
|
|
7
|
+
Facts that hold for every path below:
|
|
8
|
+
|
|
9
|
+
- Every host reads and writes the **same database**: `~/.memesh/knowledge-graph.db`.
|
|
10
|
+
Installing for a second host adds an access path, not a second memory.
|
|
11
|
+
- **Nothing is auto-configured beyond what each step states.** `npm install -g`
|
|
12
|
+
runs **no install scripts** — the package has none, by design (it installs
|
|
13
|
+
fully working even under `--ignore-scripts`). Nothing registers MCP servers
|
|
14
|
+
except the per-host commands in this document.
|
|
15
|
+
- Where a host asks for a configured MCP command, use `memesh-mcp` —
|
|
16
|
+
**never `npx -p @pcircle/memesh`**. `npx -p` resolves to the *local* package
|
|
17
|
+
whenever the host's working directory is inside a checkout of this
|
|
18
|
+
repository, silently running that working tree instead of the installed
|
|
19
|
+
release.
|
|
20
|
+
|
|
21
|
+
## 1. Claude Code (plugin)
|
|
22
|
+
|
|
23
|
+
Type these in Claude Code's **chat input** — they are Claude Code slash
|
|
24
|
+
commands, not shell commands:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
/plugin marketplace add PCIRCLE-AI/memesh
|
|
28
|
+
/plugin install memesh@pcircle-memesh
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Claude Code wires the MCP server, the auto-capture hooks, and the `/memesh`
|
|
32
|
+
skill automatically.
|
|
33
|
+
|
|
34
|
+
**Verify**: restart Claude Code. A status line beginning `◉ MeMesh` appears at
|
|
35
|
+
the top of the next session.
|
|
36
|
+
|
|
37
|
+
| Failure | Remedy |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `/plugin marketplace add` fails | The marketplace is fetched as a git clone. Check `git --version` succeeds and github.com is reachable. |
|
|
40
|
+
| `/plugin install` says plugin not found | The argument is exactly `memesh@pcircle-memesh` — plugin name `memesh`, marketplace name `pcircle-memesh`. Re-run the marketplace add first. |
|
|
41
|
+
| No `◉ MeMesh` line after restart | Run `/plugin` inside Claude Code and confirm the `memesh` plugin is installed and enabled, then restart again. |
|
|
42
|
+
|
|
43
|
+
The plugin does **not** put a `memesh` command on the shell PATH. For terminal
|
|
44
|
+
use — and as a prerequisite for sections 3 and 4 — also do section 2. The two
|
|
45
|
+
installs coexist and share the database.
|
|
46
|
+
|
|
47
|
+
## 2. Terminal / CLI (npm global)
|
|
48
|
+
|
|
49
|
+
Check Node **first** — the floor is 22.13.0 (`node:sqlite` is part of Node
|
|
50
|
+
itself; npm only warns on an engine mismatch, so an install onto an old Node
|
|
51
|
+
"succeeds" and fails later at runtime):
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
node --version
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Expected: `v22.13.0` or later. If lower, stop and upgrade Node before
|
|
58
|
+
continuing.
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
npm install -g @pcircle/memesh
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Expected: exits without error; `memesh`, `memesh-mcp` and `memesh-http` are
|
|
65
|
+
now in `$(npm prefix -g)/bin/`. No compiler is involved and no install script
|
|
66
|
+
runs.
|
|
67
|
+
|
|
68
|
+
**Verify**:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
memesh doctor
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Expected: a report starting `MeMesh doctor v…` with `Overall: PASS`
|
|
75
|
+
(`PASS_WITH_CONCERNS` is also functional), exit code 0.
|
|
76
|
+
|
|
77
|
+
| Failure | Remedy |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `command not found: memesh` | npm's global bin dir is not on PATH. Run `npm prefix -g`, append `/bin` to its output, and add that directory to PATH. |
|
|
80
|
+
| `No such built-in module: node:sqlite` | The running Node is older than 22.13.0. Upgrade Node, then re-run `memesh doctor`. |
|
|
81
|
+
| `EACCES` during `npm install -g` | The global prefix is not user-writable. Use a user-level Node (nvm/fnm), or `npm config set prefix ~/.npm-global` and add `~/.npm-global/bin` to PATH, then re-install. |
|
|
82
|
+
| `Overall: FAIL` (exit code 1) | The report names the failing check and prints the fix next to it. Apply that fix and re-run `memesh doctor`. |
|
|
83
|
+
|
|
84
|
+
Only if Claude Code is used **without** the section-1 plugin (the plugin wires
|
|
85
|
+
hooks itself — skip this if section 1 is done):
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
memesh install-hooks
|
|
89
|
+
memesh doctor
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## 3. Codex CLI
|
|
93
|
+
|
|
94
|
+
Prerequisite: section 2 — `memesh-mcp` must resolve on PATH.
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
codex mcp add memesh -- memesh-mcp
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Writes `[mcp_servers.memesh]` into `~/.codex/config.toml`.
|
|
101
|
+
|
|
102
|
+
**Verify**:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
codex mcp list
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Expected: `memesh` is listed as enabled.
|
|
109
|
+
|
|
110
|
+
| Failure | Remedy |
|
|
111
|
+
|---|---|
|
|
112
|
+
| `command not found: codex` | Codex CLI itself is not installed — out of scope here; install it first, then re-run the add. |
|
|
113
|
+
| `memesh` absent from the list | The add did not persist. Re-run `codex mcp add memesh -- memesh-mcp` and re-check. |
|
|
114
|
+
| Listed, but tool calls fail | Run `command -v memesh-mcp`. Empty output means section 2 is incomplete or PATH is wrong — fix per section 2's table. |
|
|
115
|
+
|
|
116
|
+
## 4. Gemini CLI
|
|
117
|
+
|
|
118
|
+
Prerequisite: section 2 — `memesh-mcp` must resolve on PATH.
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
gemini mcp add -s user memesh memesh-mcp
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`-s user` registers at user scope, so it works from every folder.
|
|
125
|
+
|
|
126
|
+
**Verify**:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
gemini mcp list
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Expected: `memesh` shows **Connected**.
|
|
133
|
+
|
|
134
|
+
| Failure | Remedy |
|
|
135
|
+
|---|---|
|
|
136
|
+
| `command not found: gemini` | Gemini CLI itself is not installed — out of scope here; install it first, then re-run the add. |
|
|
137
|
+
| Shows Disconnected | Run `command -v memesh-mcp`. Empty output means section 2 is incomplete or PATH is wrong — fix per section 2's table, then re-run `gemini mcp list`. |
|
|
138
|
+
| `memesh` absent from the list | The add was made in a different scope or did not persist. Re-run `gemini mcp add -s user memesh memesh-mcp`. |
|
package/package.json
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pcircle/memesh",
|
|
3
|
-
"version": "4.
|
|
4
|
-
"description": "MeMesh —
|
|
3
|
+
"version": "4.6.0",
|
|
4
|
+
"description": "MeMesh — agentic memory for coding agents. Captured from the agent's real work via hooks, recalled when it acts. One SQLite file, zero cloud required.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"memesh": "dist/transports/cli/cli.js",
|
|
9
9
|
"memesh-mcp": "dist/mcp/server.js",
|
|
10
|
-
"memesh-http": "dist/transports/http/server.js"
|
|
11
|
-
"memesh-view": "dist/cli/view.js"
|
|
10
|
+
"memesh-http": "dist/transports/http/server.js"
|
|
12
11
|
},
|
|
13
12
|
"files": [
|
|
14
13
|
"dist/",
|
|
@@ -20,7 +19,9 @@
|
|
|
20
19
|
"skills/",
|
|
21
20
|
".claude-plugin/",
|
|
22
21
|
"README.md",
|
|
23
|
-
"LICENSE"
|
|
22
|
+
"LICENSE",
|
|
23
|
+
"llms-install.md",
|
|
24
|
+
"AGENTS.md"
|
|
24
25
|
],
|
|
25
26
|
"scripts": {
|
|
26
27
|
"build": "node scripts/check-schema-drift.mjs && tsc && node scripts/generate-hook-core.mjs && node scripts/copy-cli-assets.mjs && node scripts/set-executable-bits.mjs && node scripts/generate-skills-manifest.mjs && npm run build:dashboard && node scripts/smoke-test.mjs",
|
|
@@ -41,9 +42,9 @@
|
|
|
41
42
|
},
|
|
42
43
|
"author": "PCIRCLE-AI",
|
|
43
44
|
"license": "MIT",
|
|
44
|
-
"homepage": "https://
|
|
45
|
+
"homepage": "https://github.com/PCIRCLE-AI/memesh",
|
|
45
46
|
"bugs": {
|
|
46
|
-
"url": "https://github.com/PCIRCLE-AI/memesh
|
|
47
|
+
"url": "https://github.com/PCIRCLE-AI/memesh/issues"
|
|
47
48
|
},
|
|
48
49
|
"keywords": [
|
|
49
50
|
"claude-code",
|
|
@@ -51,11 +52,15 @@
|
|
|
51
52
|
"model-context-protocol",
|
|
52
53
|
"memory",
|
|
53
54
|
"sqlite",
|
|
54
|
-
"knowledge-graph"
|
|
55
|
+
"knowledge-graph",
|
|
56
|
+
"agentic-memory",
|
|
57
|
+
"agent-memory",
|
|
58
|
+
"ai-agents",
|
|
59
|
+
"coding-agents"
|
|
55
60
|
],
|
|
56
61
|
"repository": {
|
|
57
62
|
"type": "git",
|
|
58
|
-
"url": "git+https://github.com/PCIRCLE-AI/memesh
|
|
63
|
+
"url": "git+https://github.com/PCIRCLE-AI/memesh.git"
|
|
59
64
|
},
|
|
60
65
|
"dependencies": {
|
|
61
66
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// AUTO-GENERATED from src/core/capture-flag.ts — DO NOT EDIT BY HAND.
|
|
3
|
+
// Regenerate with: npm run build (scripts/generate-hook-core.mjs)
|
|
4
|
+
//
|
|
5
|
+
// Claude Code hooks import this committed copy instead of dist/, so the
|
|
6
|
+
// always-on capture path survives a missing or stale dist/ while staying
|
|
7
|
+
// byte-locked to core — eliminating the hand-mirror drift behind the P0 FTS bug.
|
|
8
|
+
// ============================================================================
|
|
9
|
+
export function autoCaptureDecision(envVal, configAutoCapture) {
|
|
10
|
+
if (envVal === 'false')
|
|
11
|
+
return { enabled: false, offSource: 'env' };
|
|
12
|
+
if (envVal === 'true')
|
|
13
|
+
return { enabled: true, offSource: null };
|
|
14
|
+
if (configAutoCapture === false)
|
|
15
|
+
return { enabled: false, offSource: 'config' };
|
|
16
|
+
return { enabled: true, offSource: null };
|
|
17
|
+
}
|
|
@@ -62,9 +62,12 @@ const LONE_UNSPACED_CHAR = new RegExp(`[${UNSPACED_SCRIPT_CLASS}]`, 'u');
|
|
|
62
62
|
export function isLoneUnspacedChar(term) {
|
|
63
63
|
return [...term].length === 1 && LONE_UNSPACED_CHAR.test(term);
|
|
64
64
|
}
|
|
65
|
-
|
|
65
|
+
function foldTitleIntoObservations(title, observationsText) {
|
|
66
|
+
return title ? `${title} ${observationsText}` : observationsText;
|
|
67
|
+
}
|
|
68
|
+
export function removeFromFts(db, entityId, name, prevObsText, prevTitle) {
|
|
66
69
|
try {
|
|
67
|
-
db.prepare("INSERT INTO entities_fts (entities_fts, rowid, name, observations) VALUES('delete', ?, ?, ?)").run(entityId, toIndexForm(name), toIndexForm(prevObsText));
|
|
70
|
+
db.prepare("INSERT INTO entities_fts (entities_fts, rowid, name, observations) VALUES('delete', ?, ?, ?)").run(entityId, toIndexForm(name), toIndexForm(foldTitleIntoObservations(prevTitle, prevObsText)));
|
|
68
71
|
}
|
|
69
72
|
catch (err) {
|
|
70
73
|
if (isBenignFtsDeleteError(err))
|
|
@@ -76,6 +79,15 @@ function isBenignFtsDeleteError(err) {
|
|
|
76
79
|
const msg = err?.message ?? '';
|
|
77
80
|
return /no such rowid|values do not match|no such row\b/i.test(msg);
|
|
78
81
|
}
|
|
79
|
-
export function
|
|
80
|
-
|
|
82
|
+
export function joinIndexedObservations(contents) {
|
|
83
|
+
return contents.join(' ');
|
|
84
|
+
}
|
|
85
|
+
export function indexedObservationText(db, entityId) {
|
|
86
|
+
const rows = db
|
|
87
|
+
.prepare('SELECT content FROM observations WHERE entity_id = ? ORDER BY id')
|
|
88
|
+
.all(entityId);
|
|
89
|
+
return joinIndexedObservations(rows.map((o) => o.content));
|
|
90
|
+
}
|
|
91
|
+
export function insertFtsRow(db, entityId, name, observationsText, title) {
|
|
92
|
+
db.prepare('INSERT INTO entities_fts (rowid, name, observations) VALUES (?, ?, ?)').run(entityId, toIndexForm(name), toIndexForm(foldTitleIntoObservations(title, observationsText)));
|
|
81
93
|
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// AUTO-GENERATED from src/storage/schema.ts — DO NOT EDIT BY HAND.
|
|
3
|
+
// Regenerate with: npm run build (scripts/generate-hook-core.mjs)
|
|
4
|
+
//
|
|
5
|
+
// Claude Code hooks import this committed copy instead of dist/, so the
|
|
6
|
+
// always-on capture path survives a missing or stale dist/ while staying
|
|
7
|
+
// byte-locked to core — eliminating the hand-mirror drift behind the P0 FTS bug.
|
|
8
|
+
// ============================================================================
|
|
9
|
+
import { insertFtsRow } from './fts-index.js';
|
|
10
|
+
import { parseSqliteUtcMs } from './time-utils.js';
|
|
11
|
+
export const SCHEMA_SQL = `
|
|
12
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
13
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
14
|
+
name TEXT NOT NULL UNIQUE,
|
|
15
|
+
type TEXT NOT NULL,
|
|
16
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
17
|
+
metadata JSON
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
CREATE TABLE IF NOT EXISTS observations (
|
|
21
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
22
|
+
entity_id INTEGER NOT NULL,
|
|
23
|
+
content TEXT NOT NULL,
|
|
24
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
25
|
+
FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
CREATE TABLE IF NOT EXISTS relations (
|
|
29
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
30
|
+
from_entity_id INTEGER NOT NULL,
|
|
31
|
+
to_entity_id INTEGER NOT NULL,
|
|
32
|
+
relation_type TEXT NOT NULL,
|
|
33
|
+
metadata JSON,
|
|
34
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
35
|
+
FOREIGN KEY (from_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
|
|
36
|
+
FOREIGN KEY (to_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
|
|
37
|
+
UNIQUE(from_entity_id, to_entity_id, relation_type)
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
CREATE TABLE IF NOT EXISTS tags (
|
|
41
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
42
|
+
entity_id INTEGER NOT NULL,
|
|
43
|
+
tag TEXT NOT NULL,
|
|
44
|
+
FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
CREATE INDEX IF NOT EXISTS idx_tags_entity ON tags(entity_id);
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_tags_tag ON tags(tag);
|
|
49
|
+
-- The tags dedup DELETE + idx_tags_entity_tag_unique creation live in
|
|
50
|
+
-- ensureTagsUniqueIndex(), AFTER this exec — the DELETE is a one-time
|
|
51
|
+
-- migration, and a DML statement in this string made every open start a
|
|
52
|
+
-- write transaction even when it deleted nothing (same reader-breaking
|
|
53
|
+
-- pattern as the hook_runs_since note below).
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_observations_entity ON observations(entity_id);
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_relations_from ON relations(from_entity_id);
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_relations_to ON relations(to_entity_id);
|
|
57
|
+
CREATE INDEX IF NOT EXISTS idx_entities_type_created ON entities(type, created_at);
|
|
58
|
+
|
|
59
|
+
-- Migration markers and small bits of persistent state (index segmentation
|
|
60
|
+
-- version, embedding dimension, pending-reindex flags, backfill markers).
|
|
61
|
+
--
|
|
62
|
+
-- This used to be created ad hoc by each helper that needed it — four inline
|
|
63
|
+
-- CREATE TABLE IF NOT EXISTS copies in src/db.ts, none of them visible to
|
|
64
|
+
-- schema checks. A column added to one copy would not have been caught. It
|
|
65
|
+
-- also meant the hook-side schema had no metadata table at all, so hooks
|
|
66
|
+
-- could not participate in migrations even in principle.
|
|
67
|
+
CREATE TABLE IF NOT EXISTS memesh_metadata (
|
|
68
|
+
key TEXT PRIMARY KEY,
|
|
69
|
+
value TEXT NOT NULL
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
-- Proof that a capture hook actually RAN. Nothing else in this schema can
|
|
73
|
+
-- give it: every other signal is "a row was written", and "the hook ran and
|
|
74
|
+
-- found nothing worth saving" is the healthy case that produces no row at
|
|
75
|
+
-- all. So a quiet day and a dead capture loop were byte-identical in the
|
|
76
|
+
-- database, and the one doctor message that had to cover both cried wolf on
|
|
77
|
+
-- the first and stayed silent on the second.
|
|
78
|
+
--
|
|
79
|
+
-- Written by the three hooks that hold a read-write handle (Stop, PreCompact,
|
|
80
|
+
-- PostToolUse), each calling recordHookRun() at its own SUCCESSFUL exit —
|
|
81
|
+
-- after capture, not at open. Stamping at open certified the wrong thing: a
|
|
82
|
+
-- hook that opened the database and then died mid-capture looked alive for a
|
|
83
|
+
-- day. "Successful" is precise: a completed capture, or a well-formed
|
|
84
|
+
-- payload the hook correctly decided not to capture (dedup, low-signal
|
|
85
|
+
-- session). A malformed payload (schema-flip shapes) and a write that did
|
|
86
|
+
-- not land both leave no stamp — either would make the heartbeat mask the
|
|
87
|
+
-- exact dropout it exists to expose. The recall-side hooks open read-only
|
|
88
|
+
-- and deliberately do not appear here: their liveness answers a different
|
|
89
|
+
-- question, and giving them a write handle would put a lock acquisition on
|
|
90
|
+
-- the SessionStart hot path.
|
|
91
|
+
--
|
|
92
|
+
-- One row per hook, upserted. It does not grow.
|
|
93
|
+
CREATE TABLE IF NOT EXISTS hook_runs (
|
|
94
|
+
hook TEXT PRIMARY KEY,
|
|
95
|
+
last_run_at TIMESTAMP NOT NULL,
|
|
96
|
+
run_count INTEGER NOT NULL DEFAULT 0
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
-- The 'hook_runs_since' metadata key (when this database first became able
|
|
100
|
+
-- to record hook runs) is stamped by ensureHookRunsSince() AFTER this exec,
|
|
101
|
+
-- NOT here. It used to be an INSERT OR IGNORE in this string, and that made
|
|
102
|
+
-- every open — including opens that only ever read — start a write
|
|
103
|
+
-- transaction: an INSERT statement takes the WAL writer lock even when
|
|
104
|
+
-- OR IGNORE ends up changing nothing. Two states regressed from "reads work"
|
|
105
|
+
-- to "open throws": a peer holding the writer lock past busy_timeout, and a
|
|
106
|
+
-- read-only database FILE (measured: "attempt to write a readonly database"
|
|
107
|
+
-- killed recall along with capture). The helper SELECTs first and writes
|
|
108
|
+
-- only when the key is genuinely absent — once per database lifetime.
|
|
109
|
+
`;
|
|
110
|
+
export const FTS_SQL = `
|
|
111
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
|
|
112
|
+
name, observations, content='',
|
|
113
|
+
tokenize='unicode61 remove_diacritics 1'
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
-- Term -> document-count view over the index above. Stores nothing of its own;
|
|
117
|
+
-- it exists so search() can drop query terms that appear in most of the corpus,
|
|
118
|
+
-- which are the ones BM25 already scores near zero. See dropUbiquitousTerms().
|
|
119
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS fts_vocab USING fts5vocab(entities_fts, 'row');
|
|
120
|
+
`;
|
|
121
|
+
export function safeAlter(db, sql) {
|
|
122
|
+
try {
|
|
123
|
+
db.exec(sql);
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
127
|
+
if (!/duplicate column name/i.test(msg))
|
|
128
|
+
throw e;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export function migrateEntitiesSchema(db) {
|
|
132
|
+
const entityColumns = new Set(db.prepare("PRAGMA table_info(entities)").all().map((c) => c.name));
|
|
133
|
+
if (!entityColumns.has('status')) {
|
|
134
|
+
safeAlter(db, "ALTER TABLE entities ADD COLUMN status TEXT NOT NULL DEFAULT 'active'");
|
|
135
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_entities_status ON entities(status)");
|
|
136
|
+
}
|
|
137
|
+
if (!entityColumns.has('access_count')) {
|
|
138
|
+
safeAlter(db, "ALTER TABLE entities ADD COLUMN access_count INTEGER DEFAULT 0");
|
|
139
|
+
safeAlter(db, "ALTER TABLE entities ADD COLUMN last_accessed_at TIMESTAMP");
|
|
140
|
+
safeAlter(db, "ALTER TABLE entities ADD COLUMN confidence REAL DEFAULT 1.0");
|
|
141
|
+
safeAlter(db, "ALTER TABLE entities ADD COLUMN valid_from TIMESTAMP");
|
|
142
|
+
safeAlter(db, "ALTER TABLE entities ADD COLUMN valid_until TIMESTAMP");
|
|
143
|
+
}
|
|
144
|
+
if (!entityColumns.has('namespace')) {
|
|
145
|
+
safeAlter(db, "ALTER TABLE entities ADD COLUMN namespace TEXT DEFAULT 'personal'");
|
|
146
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_entities_namespace ON entities(namespace)");
|
|
147
|
+
}
|
|
148
|
+
if (!entityColumns.has('recall_hits')) {
|
|
149
|
+
safeAlter(db, "ALTER TABLE entities ADD COLUMN recall_hits INTEGER DEFAULT 0");
|
|
150
|
+
safeAlter(db, "ALTER TABLE entities ADD COLUMN recall_misses INTEGER DEFAULT 0");
|
|
151
|
+
}
|
|
152
|
+
if (!entityColumns.has('title')) {
|
|
153
|
+
safeAlter(db, "ALTER TABLE entities ADD COLUMN title TEXT");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
export function ensureTagsUniqueIndex(db) {
|
|
157
|
+
try {
|
|
158
|
+
const present = db
|
|
159
|
+
.prepare("SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = 'idx_tags_entity_tag_unique'")
|
|
160
|
+
.get();
|
|
161
|
+
if (present)
|
|
162
|
+
return;
|
|
163
|
+
db.exec('BEGIN IMMEDIATE; ' +
|
|
164
|
+
'DELETE FROM tags WHERE id NOT IN (SELECT MIN(id) FROM tags GROUP BY entity_id, tag); ' +
|
|
165
|
+
'CREATE UNIQUE INDEX IF NOT EXISTS idx_tags_entity_tag_unique ON tags(entity_id, tag); ' +
|
|
166
|
+
'COMMIT;');
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
try {
|
|
170
|
+
db.exec('ROLLBACK');
|
|
171
|
+
}
|
|
172
|
+
catch { }
|
|
173
|
+
try {
|
|
174
|
+
process.stderr.write(`MeMesh: could not create the tags unique index (${err instanceof Error ? err.message : String(err)}). ` +
|
|
175
|
+
`Reads are unaffected; the next open retries the dedup and index together.\n`);
|
|
176
|
+
}
|
|
177
|
+
catch { }
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
export function ensureHookRunsSince(db) {
|
|
181
|
+
try {
|
|
182
|
+
const row = db
|
|
183
|
+
.prepare("SELECT value FROM memesh_metadata WHERE key = 'hook_runs_since'")
|
|
184
|
+
.get();
|
|
185
|
+
if (row) {
|
|
186
|
+
const then = parseSqliteUtcMs(row.value ?? '');
|
|
187
|
+
if (then !== null && then <= Date.now() + 5 * 60 * 1000)
|
|
188
|
+
return;
|
|
189
|
+
db
|
|
190
|
+
.prepare("UPDATE memesh_metadata SET value = datetime('now') WHERE key = 'hook_runs_since'")
|
|
191
|
+
.run();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
db
|
|
195
|
+
.prepare("INSERT OR IGNORE INTO memesh_metadata (key, value) VALUES ('hook_runs_since', datetime('now'))")
|
|
196
|
+
.run();
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
try {
|
|
200
|
+
process.stderr.write(`MeMesh: could not stamp hook_runs_since (${err instanceof Error ? err.message : String(err)}). ` +
|
|
201
|
+
`Reads are unaffected; doctor's hook-activity tracking starts once the database is writable.\n`);
|
|
202
|
+
}
|
|
203
|
+
catch { }
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
export function isTransientDbError(err) {
|
|
207
|
+
const code = err?.code ?? '';
|
|
208
|
+
const msg = err?.message ?? '';
|
|
209
|
+
return (/SQLITE_BUSY|SQLITE_LOCKED|SQLITE_PROTOCOL/.test(code) ||
|
|
210
|
+
/database is locked|database table is locked|locking protocol/i.test(msg));
|
|
211
|
+
}
|
|
212
|
+
export const MIGRATION_RETRY_BACKOFF_MS = 24 * 60 * 60 * 1000;
|
|
213
|
+
export function runOnceMigration(db, opts) {
|
|
214
|
+
const { key, version, describe, migrate } = opts;
|
|
215
|
+
const attemptKey = `${key}_last_attempt`;
|
|
216
|
+
const readMarker = (k) => db.prepare('SELECT value FROM memesh_metadata WHERE key = ?').get(k)?.value;
|
|
217
|
+
const stored = readMarker(key);
|
|
218
|
+
if (stored && parseInt(stored, 10) >= version)
|
|
219
|
+
return false;
|
|
220
|
+
const lastAttempt = readMarker(attemptKey);
|
|
221
|
+
if (lastAttempt && Date.now() - parseInt(lastAttempt, 10) < MIGRATION_RETRY_BACKOFF_MS) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
db.transaction(() => {
|
|
226
|
+
const current = readMarker(key);
|
|
227
|
+
if (current && parseInt(current, 10) >= version)
|
|
228
|
+
return;
|
|
229
|
+
migrate(db, current ? parseInt(current, 10) : 0);
|
|
230
|
+
db.prepare('INSERT OR REPLACE INTO memesh_metadata (key, value) VALUES (?, ?)').run(key, String(version));
|
|
231
|
+
db.prepare('DELETE FROM memesh_metadata WHERE key = ?').run(attemptKey);
|
|
232
|
+
}).immediate();
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
if (isTransientDbError(err)) {
|
|
237
|
+
process.stderr.write(`MeMesh: ${describe} deferred (${err instanceof Error ? err.message : String(err)}). ` +
|
|
238
|
+
`Another process holds the database; it will run on the next start.\n`);
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
db.prepare('INSERT OR REPLACE INTO memesh_metadata (key, value) VALUES (?, ?)').run(attemptKey, String(Date.now()));
|
|
243
|
+
}
|
|
244
|
+
catch { }
|
|
245
|
+
process.stderr.write(`MeMesh: ${describe} failed (${err instanceof Error ? err.message : String(err)}). ` +
|
|
246
|
+
`Your memories are unaffected — this rebuilds a derived index. ` +
|
|
247
|
+
`It will retry in 24h, or run 'memesh reindex --fts' to retry now.\n`);
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
export const FTS_SEGMENTATION_VERSION = 3;
|
|
252
|
+
export const FTS_REBUILD_PAGE_SIZE = 500;
|
|
253
|
+
export function rebuildFtsIndex(db) {
|
|
254
|
+
db.exec("INSERT INTO entities_fts (entities_fts) VALUES('delete-all')");
|
|
255
|
+
const page = db.prepare(`SELECT e.id, e.name, e.title, COALESCE(group_concat(o.content, ' ' ORDER BY o.id), '') AS obs
|
|
256
|
+
FROM entities e
|
|
257
|
+
LEFT JOIN observations o ON o.entity_id = e.id
|
|
258
|
+
WHERE e.status = 'active' AND e.id > ?
|
|
259
|
+
GROUP BY e.id
|
|
260
|
+
ORDER BY e.id
|
|
261
|
+
LIMIT ?`);
|
|
262
|
+
let afterId = 0;
|
|
263
|
+
for (;;) {
|
|
264
|
+
const rows = page.all(afterId, FTS_REBUILD_PAGE_SIZE);
|
|
265
|
+
if (rows.length === 0)
|
|
266
|
+
break;
|
|
267
|
+
for (const row of rows)
|
|
268
|
+
insertFtsRow(db, row.id, row.name, row.obs, row.title);
|
|
269
|
+
afterId = rows[rows.length - 1].id;
|
|
270
|
+
if (rows.length < FTS_REBUILD_PAGE_SIZE)
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
export function ensureFtsSegmentation(db) {
|
|
275
|
+
runOnceMigration(db, {
|
|
276
|
+
key: 'fts_segmentation_version',
|
|
277
|
+
version: FTS_SEGMENTATION_VERSION,
|
|
278
|
+
describe: 'search index rebuild',
|
|
279
|
+
migrate: rebuildFtsIndex,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// AUTO-GENERATED from src/core/task-state.ts — DO NOT EDIT BY HAND.
|
|
3
|
+
// Regenerate with: npm run build (scripts/generate-hook-core.mjs)
|
|
4
|
+
//
|
|
5
|
+
// Claude Code hooks import this committed copy instead of dist/, so the
|
|
6
|
+
// always-on capture path survives a missing or stale dist/ while staying
|
|
7
|
+
// byte-locked to core — eliminating the hand-mirror drift behind the P0 FTS bug.
|
|
8
|
+
// ============================================================================
|
|
9
|
+
export const TASK_STATE_TYPE = 'task-state';
|
|
10
|
+
export const TASK_STATE_FIELDS = ['goal', 'next', 'blocked', 'done'];
|
|
11
|
+
export const MAX_FIELD_CHARS = 300;
|
|
12
|
+
export function taskStateName(project) {
|
|
13
|
+
return `${TASK_STATE_TYPE}:${project}`;
|
|
14
|
+
}
|
|
15
|
+
export function parseTaskState(metadata) {
|
|
16
|
+
const state = {};
|
|
17
|
+
if (!metadata || typeof metadata !== 'object')
|
|
18
|
+
return state;
|
|
19
|
+
const raw = metadata.task_state;
|
|
20
|
+
if (!raw || typeof raw !== 'object')
|
|
21
|
+
return state;
|
|
22
|
+
const bag = raw;
|
|
23
|
+
for (const field of TASK_STATE_FIELDS) {
|
|
24
|
+
const value = bag[field];
|
|
25
|
+
if (typeof value !== 'string')
|
|
26
|
+
continue;
|
|
27
|
+
const trimmed = value.trim();
|
|
28
|
+
if (trimmed)
|
|
29
|
+
state[field] = trimmed;
|
|
30
|
+
}
|
|
31
|
+
const updated = bag.updated_at;
|
|
32
|
+
if (typeof updated === 'string' && updated.trim())
|
|
33
|
+
state.updated_at = updated.trim();
|
|
34
|
+
return state;
|
|
35
|
+
}
|
|
36
|
+
export function normalizeFieldValue(value) {
|
|
37
|
+
const flat = value.replace(/\s+/g, ' ').trim();
|
|
38
|
+
if (!flat)
|
|
39
|
+
return null;
|
|
40
|
+
return flat.length > MAX_FIELD_CHARS ? `${flat.slice(0, MAX_FIELD_CHARS - 1).trimEnd()}…` : flat;
|
|
41
|
+
}
|
|
42
|
+
export function mergeTaskState(previous, patch, now) {
|
|
43
|
+
const state = { ...previous };
|
|
44
|
+
const changed = [];
|
|
45
|
+
const observations = [];
|
|
46
|
+
for (const field of TASK_STATE_FIELDS) {
|
|
47
|
+
const incoming = patch[field];
|
|
48
|
+
if (incoming === undefined)
|
|
49
|
+
continue;
|
|
50
|
+
const normalized = normalizeFieldValue(incoming);
|
|
51
|
+
const current = state[field];
|
|
52
|
+
if (normalized === (current ?? null))
|
|
53
|
+
continue;
|
|
54
|
+
changed.push(field);
|
|
55
|
+
if (normalized === null) {
|
|
56
|
+
delete state[field];
|
|
57
|
+
observations.push(`${field} cleared`);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
state[field] = normalized;
|
|
61
|
+
observations.push(`${field}: ${normalized}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (changed.length > 0)
|
|
65
|
+
state.updated_at = now;
|
|
66
|
+
return { state, changed, observations };
|
|
67
|
+
}
|
|
68
|
+
export function isEmptyTaskState(state) {
|
|
69
|
+
return TASK_STATE_FIELDS.every((field) => !state[field]);
|
|
70
|
+
}
|
|
71
|
+
const FIELD_LABELS = {
|
|
72
|
+
goal: 'Goal',
|
|
73
|
+
next: 'Next',
|
|
74
|
+
blocked: 'Blocked',
|
|
75
|
+
done: 'Just finished',
|
|
76
|
+
};
|
|
77
|
+
function ageInDays(updatedAt, now) {
|
|
78
|
+
if (!updatedAt)
|
|
79
|
+
return null;
|
|
80
|
+
const then = Date.parse(updatedAt);
|
|
81
|
+
if (Number.isNaN(then))
|
|
82
|
+
return null;
|
|
83
|
+
const days = Math.floor((now.getTime() - then) / 86_400_000);
|
|
84
|
+
return days >= 0 ? days : null;
|
|
85
|
+
}
|
|
86
|
+
export function taskStateLines(state, project, now = new Date()) {
|
|
87
|
+
if (isEmptyTaskState(state))
|
|
88
|
+
return [];
|
|
89
|
+
const days = ageInDays(state.updated_at, now);
|
|
90
|
+
const age = days === null ? '' : days === 0 ? ' (today)' : days === 1 ? ' (yesterday)' : ` (${days} days ago)`;
|
|
91
|
+
const lines = [`Where "${project}" was left off${age}:`];
|
|
92
|
+
for (const field of TASK_STATE_FIELDS) {
|
|
93
|
+
const value = state[field];
|
|
94
|
+
if (value)
|
|
95
|
+
lines.push(`- ${FIELD_LABELS[field]}: ${value}`);
|
|
96
|
+
}
|
|
97
|
+
return lines;
|
|
98
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// AUTO-GENERATED from src/core/time-utils.ts — DO NOT EDIT BY HAND.
|
|
3
|
+
// Regenerate with: npm run build (scripts/generate-hook-core.mjs)
|
|
4
|
+
//
|
|
5
|
+
// Claude Code hooks import this committed copy instead of dist/, so the
|
|
6
|
+
// always-on capture path survives a missing or stale dist/ while staying
|
|
7
|
+
// byte-locked to core — eliminating the hand-mirror drift behind the P0 FTS bug.
|
|
8
|
+
// ============================================================================
|
|
9
|
+
export function parseSqliteUtcMs(sqliteTimestamp) {
|
|
10
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})$/.exec(sqliteTimestamp ?? '');
|
|
11
|
+
if (!m)
|
|
12
|
+
return null;
|
|
13
|
+
const then = Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6]);
|
|
14
|
+
if (!Number.isFinite(then))
|
|
15
|
+
return null;
|
|
16
|
+
const d = new Date(then);
|
|
17
|
+
if (d.getUTCFullYear() !== +m[1] || d.getUTCMonth() !== +m[2] - 1 || d.getUTCDate() !== +m[3]
|
|
18
|
+
|| d.getUTCHours() !== +m[4] || d.getUTCMinutes() !== +m[5] || d.getUTCSeconds() !== +m[6])
|
|
19
|
+
return null;
|
|
20
|
+
return then;
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// AUTO-GENERATED from src/core/title.ts — DO NOT EDIT BY HAND.
|
|
3
|
+
// Regenerate with: npm run build (scripts/generate-hook-core.mjs)
|
|
4
|
+
//
|
|
5
|
+
// Claude Code hooks import this committed copy instead of dist/, so the
|
|
6
|
+
// always-on capture path survives a missing or stale dist/ while staying
|
|
7
|
+
// byte-locked to core — eliminating the hand-mirror drift behind the P0 FTS bug.
|
|
8
|
+
// ============================================================================
|
|
9
|
+
export const TITLE_MAX_LENGTH = 200;
|
|
10
|
+
export function truncateTitle(text) {
|
|
11
|
+
if (!text)
|
|
12
|
+
return text;
|
|
13
|
+
const trimmed = text.trim();
|
|
14
|
+
return trimmed.length > TITLE_MAX_LENGTH
|
|
15
|
+
? trimmed.slice(0, TITLE_MAX_LENGTH - 1).trimEnd() + '…'
|
|
16
|
+
: trimmed;
|
|
17
|
+
}
|
|
18
|
+
export const BOILERPLATE_OBSERVATION_PATTERN = /^(Steps|Commits|Branch|Diff stats|Compaction reason|Tool calls|Plan ".+" completed)[:\s]/;
|
|
19
|
+
export function isBoilerplateObservation(text) {
|
|
20
|
+
return BOILERPLATE_OBSERVATION_PATTERN.test(text.trim());
|
|
21
|
+
}
|