@pcircle/memesh 4.5.0 → 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.
Files changed (256) hide show
  1. package/.claude-plugin/marketplace.json +5 -3
  2. package/.claude-plugin/plugin.json +6 -4
  3. package/.mcp.json +1 -1
  4. package/AGENTS.md +95 -0
  5. package/README.de.md +130 -39
  6. package/README.md +188 -43
  7. package/README.zh-TW.md +133 -41
  8. package/dashboard/dist/index.html +9 -9
  9. package/dist/cli/view-live.js +3 -3
  10. package/dist/core/analytics.d.ts +3 -3
  11. package/dist/core/analytics.d.ts.map +1 -1
  12. package/dist/core/analytics.js.map +1 -1
  13. package/dist/core/auto-tagger.d.ts.map +1 -1
  14. package/dist/core/auto-tagger.js +4 -9
  15. package/dist/core/auto-tagger.js.map +1 -1
  16. package/dist/core/briefing.d.ts +8 -0
  17. package/dist/core/briefing.d.ts.map +1 -0
  18. package/dist/core/briefing.js +91 -0
  19. package/dist/core/briefing.js.map +1 -0
  20. package/dist/core/capture-flag.d.ts +5 -0
  21. package/dist/core/capture-flag.d.ts.map +1 -0
  22. package/dist/core/capture-flag.js +10 -0
  23. package/dist/core/capture-flag.js.map +1 -0
  24. package/dist/core/config.d.ts +0 -1
  25. package/dist/core/config.d.ts.map +1 -1
  26. package/dist/core/config.js +2 -1
  27. package/dist/core/config.js.map +1 -1
  28. package/dist/core/conflict-candidates.d.ts +20 -0
  29. package/dist/core/conflict-candidates.d.ts.map +1 -0
  30. package/dist/core/conflict-candidates.js +79 -0
  31. package/dist/core/conflict-candidates.js.map +1 -0
  32. package/dist/core/conflict-judge.d.ts +47 -0
  33. package/dist/core/conflict-judge.d.ts.map +1 -0
  34. package/dist/core/conflict-judge.js +189 -0
  35. package/dist/core/conflict-judge.js.map +1 -0
  36. package/dist/core/demo.d.ts +2 -2
  37. package/dist/core/demo.d.ts.map +1 -1
  38. package/dist/core/demo.js.map +1 -1
  39. package/dist/core/digest-validator.d.ts.map +1 -1
  40. package/dist/core/digest-validator.js +3 -5
  41. package/dist/core/digest-validator.js.map +1 -1
  42. package/dist/core/doctor.d.ts +3 -0
  43. package/dist/core/doctor.d.ts.map +1 -1
  44. package/dist/core/doctor.js +207 -85
  45. package/dist/core/doctor.js.map +1 -1
  46. package/dist/core/dreamer.d.ts +20 -9
  47. package/dist/core/dreamer.d.ts.map +1 -1
  48. package/dist/core/dreamer.js +416 -58
  49. package/dist/core/dreamer.js.map +1 -1
  50. package/dist/core/embedder.d.ts +7 -5
  51. package/dist/core/embedder.d.ts.map +1 -1
  52. package/dist/core/embedder.js +38 -9
  53. package/dist/core/embedder.js.map +1 -1
  54. package/dist/core/extractor.d.ts.map +1 -1
  55. package/dist/core/extractor.js +2 -1
  56. package/dist/core/extractor.js.map +1 -1
  57. package/dist/core/failure-analyzer.d.ts.map +1 -1
  58. package/dist/core/failure-analyzer.js +7 -12
  59. package/dist/core/failure-analyzer.js.map +1 -1
  60. package/dist/core/graph.d.ts +4 -4
  61. package/dist/core/graph.d.ts.map +1 -1
  62. package/dist/core/graph.js.map +1 -1
  63. package/dist/core/install-channel.d.ts +1 -1
  64. package/dist/core/install-channel.d.ts.map +1 -1
  65. package/dist/core/install-channel.js +16 -5
  66. package/dist/core/install-channel.js.map +1 -1
  67. package/dist/core/install-hooks.d.ts +6 -0
  68. package/dist/core/install-hooks.d.ts.map +1 -1
  69. package/dist/core/install-hooks.js +0 -0
  70. package/dist/core/install-hooks.js.map +1 -1
  71. package/dist/core/json-utils.d.ts +1 -0
  72. package/dist/core/json-utils.d.ts.map +1 -1
  73. package/dist/core/json-utils.js +19 -10
  74. package/dist/core/json-utils.js.map +1 -1
  75. package/dist/core/kg-backfill.d.ts +3 -4
  76. package/dist/core/kg-backfill.d.ts.map +1 -1
  77. package/dist/core/kg-backfill.js +1 -4
  78. package/dist/core/kg-backfill.js.map +1 -1
  79. package/dist/core/lesson-engine.d.ts +1 -0
  80. package/dist/core/lesson-engine.d.ts.map +1 -1
  81. package/dist/core/lesson-engine.js +1 -0
  82. package/dist/core/lesson-engine.js.map +1 -1
  83. package/dist/core/lifecycle.d.ts +4 -4
  84. package/dist/core/lifecycle.d.ts.map +1 -1
  85. package/dist/core/lifecycle.js +15 -22
  86. package/dist/core/lifecycle.js.map +1 -1
  87. package/dist/core/llm-client.d.ts.map +1 -1
  88. package/dist/core/llm-client.js +3 -6
  89. package/dist/core/llm-client.js.map +1 -1
  90. package/dist/core/llm-telemetry.d.ts +4 -4
  91. package/dist/core/llm-telemetry.d.ts.map +1 -1
  92. package/dist/core/llm-telemetry.js +1 -1
  93. package/dist/core/llm-telemetry.js.map +1 -1
  94. package/dist/core/memory-tool.d.ts.map +1 -1
  95. package/dist/core/memory-tool.js +8 -4
  96. package/dist/core/memory-tool.js.map +1 -1
  97. package/dist/core/operations.d.ts.map +1 -1
  98. package/dist/core/operations.js +41 -17
  99. package/dist/core/operations.js.map +1 -1
  100. package/dist/core/paths.d.ts +3 -0
  101. package/dist/core/paths.d.ts.map +1 -1
  102. package/dist/core/paths.js +67 -1
  103. package/dist/core/paths.js.map +1 -1
  104. package/dist/core/patterns.d.ts +2 -2
  105. package/dist/core/patterns.d.ts.map +1 -1
  106. package/dist/core/patterns.js.map +1 -1
  107. package/dist/core/project-tags.d.ts +3 -3
  108. package/dist/core/project-tags.d.ts.map +1 -1
  109. package/dist/core/project-tags.js.map +1 -1
  110. package/dist/core/projects.d.ts +2 -2
  111. package/dist/core/projects.d.ts.map +1 -1
  112. package/dist/core/projects.js.map +1 -1
  113. package/dist/core/prompt-safety.d.ts +1 -0
  114. package/dist/core/prompt-safety.d.ts.map +1 -1
  115. package/dist/core/prompt-safety.js +7 -0
  116. package/dist/core/prompt-safety.js.map +1 -1
  117. package/dist/core/schema-export.d.ts.map +1 -1
  118. package/dist/core/schema-export.js +27 -30
  119. package/dist/core/schema-export.js.map +1 -1
  120. package/dist/core/serializer.d.ts.map +1 -1
  121. package/dist/core/serializer.js +45 -4
  122. package/dist/core/serializer.js.map +1 -1
  123. package/dist/core/setup.d.ts +29 -0
  124. package/dist/core/setup.d.ts.map +1 -0
  125. package/dist/core/setup.js +127 -0
  126. package/dist/core/setup.js.map +1 -0
  127. package/dist/core/stats.d.ts +2 -2
  128. package/dist/core/stats.d.ts.map +1 -1
  129. package/dist/core/stats.js.map +1 -1
  130. package/dist/core/task-state-store.d.ts +17 -0
  131. package/dist/core/task-state-store.d.ts.map +1 -0
  132. package/dist/core/task-state-store.js +45 -0
  133. package/dist/core/task-state-store.js.map +1 -0
  134. package/dist/core/task-state.d.ts +19 -0
  135. package/dist/core/task-state.d.ts.map +1 -0
  136. package/dist/core/task-state.js +91 -0
  137. package/dist/core/task-state.js.map +1 -0
  138. package/dist/core/time-utils.d.ts +2 -0
  139. package/dist/core/time-utils.d.ts.map +1 -0
  140. package/dist/core/time-utils.js +14 -0
  141. package/dist/core/time-utils.js.map +1 -0
  142. package/dist/core/title.d.ts +5 -0
  143. package/dist/core/title.d.ts.map +1 -0
  144. package/dist/core/title.js +14 -0
  145. package/dist/core/title.js.map +1 -0
  146. package/dist/core/transcript-extractor.d.ts +5 -6
  147. package/dist/core/transcript-extractor.d.ts.map +1 -1
  148. package/dist/core/transcript-extractor.js +4 -24
  149. package/dist/core/transcript-extractor.js.map +1 -1
  150. package/dist/core/transcript-source.d.ts.map +1 -1
  151. package/dist/core/transcript-source.js +2 -3
  152. package/dist/core/transcript-source.js.map +1 -1
  153. package/dist/core/types.d.ts +21 -7
  154. package/dist/core/types.d.ts.map +1 -1
  155. package/dist/core/types.js +2 -0
  156. package/dist/core/types.js.map +1 -1
  157. package/dist/core/work-topology.d.ts +33 -0
  158. package/dist/core/work-topology.d.ts.map +1 -0
  159. package/dist/core/work-topology.js +183 -0
  160. package/dist/core/work-topology.js.map +1 -0
  161. package/dist/db.d.ts +5 -10
  162. package/dist/db.d.ts.map +1 -1
  163. package/dist/db.js +194 -196
  164. package/dist/db.js.map +1 -1
  165. package/dist/knowledge-graph.d.ts +4 -2
  166. package/dist/knowledge-graph.d.ts.map +1 -1
  167. package/dist/knowledge-graph.js +68 -49
  168. package/dist/knowledge-graph.js.map +1 -1
  169. package/dist/mcp/server.js +2 -1
  170. package/dist/mcp/server.js.map +1 -1
  171. package/dist/skills-manifest.json +61 -36
  172. package/dist/storage/conflicts.d.ts +3 -3
  173. package/dist/storage/conflicts.d.ts.map +1 -1
  174. package/dist/storage/conflicts.js +2 -7
  175. package/dist/storage/conflicts.js.map +1 -1
  176. package/dist/storage/fts-index.d.ts +6 -4
  177. package/dist/storage/fts-index.d.ts.map +1 -1
  178. package/dist/storage/fts-index.js +16 -4
  179. package/dist/storage/fts-index.js.map +1 -1
  180. package/dist/storage/schema.d.ts +20 -0
  181. package/dist/storage/schema.d.ts.map +1 -0
  182. package/dist/storage/schema.js +274 -0
  183. package/dist/storage/schema.js.map +1 -0
  184. package/dist/storage/sqlite.d.ts +20 -0
  185. package/dist/storage/sqlite.d.ts.map +1 -0
  186. package/dist/storage/sqlite.js +64 -0
  187. package/dist/storage/sqlite.js.map +1 -0
  188. package/dist/storage/vector-index.d.ts +3 -0
  189. package/dist/storage/vector-index.d.ts.map +1 -0
  190. package/dist/storage/vector-index.js +7 -0
  191. package/dist/storage/vector-index.js.map +1 -0
  192. package/dist/transports/cli/cli.d.ts +1 -4
  193. package/dist/transports/cli/cli.d.ts.map +1 -1
  194. package/dist/transports/cli/cli.js +494 -76
  195. package/dist/transports/cli/cli.js.map +1 -1
  196. package/dist/transports/http/retired-routes.d.ts.map +1 -1
  197. package/dist/transports/http/retired-routes.js +1 -0
  198. package/dist/transports/http/retired-routes.js.map +1 -1
  199. package/dist/transports/http/server.d.ts.map +1 -1
  200. package/dist/transports/http/server.js +243 -323
  201. package/dist/transports/http/server.js.map +1 -1
  202. package/dist/transports/mcp/handlers.d.ts +48 -94
  203. package/dist/transports/mcp/handlers.d.ts.map +1 -1
  204. package/dist/transports/mcp/handlers.js +77 -56
  205. package/dist/transports/mcp/handlers.js.map +1 -1
  206. package/dist/transports/schemas.d.ts +31 -40
  207. package/dist/transports/schemas.d.ts.map +1 -1
  208. package/dist/transports/schemas.js +31 -37
  209. package/dist/transports/schemas.js.map +1 -1
  210. package/hooks/hooks.json +0 -10
  211. package/llms-install.md +138 -0
  212. package/package.json +19 -18
  213. package/scripts/hooks/_generated/capture-flag.js +17 -0
  214. package/scripts/hooks/_generated/core-paths.js +67 -1
  215. package/scripts/hooks/_generated/fts-index.js +16 -4
  216. package/scripts/hooks/_generated/schema.js +281 -0
  217. package/scripts/hooks/_generated/sqlite.js +71 -0
  218. package/scripts/hooks/_generated/task-state.js +98 -0
  219. package/scripts/hooks/_generated/time-utils.js +21 -0
  220. package/scripts/hooks/_generated/title.js +21 -0
  221. package/scripts/hooks/_generated/work-topology.js +190 -0
  222. package/scripts/hooks/_shared.js +269 -534
  223. package/scripts/hooks/post-commit.js +55 -10
  224. package/scripts/hooks/pre-compact.js +22 -7
  225. package/scripts/hooks/pre-edit-recall.js +9 -11
  226. package/scripts/hooks/session-start.js +230 -106
  227. package/scripts/hooks/session-summary.js +176 -73
  228. package/scripts/hooks/user-prompt-intent.js +3 -2
  229. package/skills/memesh/SKILL.md +97 -77
  230. package/README.es.md +0 -470
  231. package/README.fr.md +0 -462
  232. package/README.ja.md +0 -470
  233. package/README.ko.md +0 -470
  234. package/README.pt.md +0 -462
  235. package/README.th.md +0 -463
  236. package/README.vi.md +0 -462
  237. package/README.zh-CN.md +0 -469
  238. package/dist/cli/view.d.ts +0 -3
  239. package/dist/cli/view.d.ts.map +0 -1
  240. package/dist/cli/view.js +0 -523
  241. package/dist/cli/view.js.map +0 -1
  242. package/dist/core/skill-usage-log.d.ts +0 -11
  243. package/dist/core/skill-usage-log.d.ts.map +0 -1
  244. package/dist/core/skill-usage-log.js +0 -125
  245. package/dist/core/skill-usage-log.js.map +0 -1
  246. package/dist/core/verifier.d.ts +0 -40
  247. package/dist/core/verifier.d.ts.map +0 -1
  248. package/dist/core/verifier.js +0 -206
  249. package/dist/core/verifier.js.map +0 -1
  250. package/dist/mcp/launcher.d.ts +0 -3
  251. package/dist/mcp/launcher.d.ts.map +0 -1
  252. package/dist/mcp/launcher.js +0 -37
  253. package/dist/mcp/launcher.js.map +0 -1
  254. package/scripts/hooks/pre-bash-orchestration-nudge.js +0 -155
  255. package/scripts/postinstall-rebuild.mjs +0 -41
  256. package/skills/agentic-orchestration/SKILL.md +0 -399
@@ -6,8 +6,10 @@
6
6
  // always-on capture path survives a missing or stale dist/ while staying
7
7
  // byte-locked to core — eliminating the hand-mirror drift behind the P0 FTS bug.
8
8
  // ============================================================================
9
+ import fs from 'fs';
9
10
  import os from 'os';
10
11
  import path from 'path';
12
+ import { createHash } from 'crypto';
11
13
  import { execFileSync } from 'child_process';
12
14
  export function homeDir() {
13
15
  const home = process.env.HOME;
@@ -49,7 +51,15 @@ function resolveProjectIdentity(cwd) {
49
51
  const root = tryGit(cwd, ['rev-parse', '--show-toplevel']);
50
52
  if (root)
51
53
  return path.basename(root);
52
- return path.basename(cwd);
54
+ let real;
55
+ try {
56
+ real = fs.realpathSync.native(cwd);
57
+ }
58
+ catch {
59
+ real = path.resolve(cwd);
60
+ }
61
+ const suffix = createHash('sha256').update(real).digest('hex').slice(0, 8);
62
+ return `${path.basename(real)}-${suffix}`;
53
63
  }
54
64
  function tryGit(cwd, args) {
55
65
  try {
@@ -75,3 +85,59 @@ export function slugFromRemoteUrl(url) {
75
85
  export function _clearProjectNameCache() {
76
86
  projectNameCache.clear();
77
87
  }
88
+ export const SECRET_PATTERN_SOURCES = [
89
+ '-----BEGIN[A-Z ]*PRIVATE KEY-----[\\s\\S]*?-----END[A-Z ]*PRIVATE KEY-----',
90
+ '-----BEGIN[A-Z ]*PRIVATE KEY-----[\\s\\S]*?(?=\\n[ \\t]*\\n|$)',
91
+ '(?:postgres|postgresql|mysql|mariadb|mongodb(?:\\+srv)?|redis|rediss|amqp|amqps)://[^\\s:@/]+:[^\\s:@/]+@',
92
+ 'eyJ[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}',
93
+ 'SG\\.[A-Za-z0-9_-]{16,}\\.[A-Za-z0-9_-]{16,}',
94
+ '[srp]k_(?:live|test)_[A-Za-z0-9]{16,}',
95
+ 'npm_[A-Za-z0-9]{36}',
96
+ 'sk-ant-[A-Za-z0-9_-]{16,}',
97
+ 'sk-[A-Za-z0-9_-]{16,}',
98
+ 'sk_[A-Za-z0-9]{16,}',
99
+ 'ghp_[A-Za-z0-9]{30,}',
100
+ 'gho_[A-Za-z0-9]{30,}',
101
+ 'gh[sur]_[A-Za-z0-9]{30,}',
102
+ 'github_pat_[A-Za-z0-9_]{20,}',
103
+ 'A(?:KIA|SIA)[A-Z0-9]{16}',
104
+ 'AIza[A-Za-z0-9_-]{30,}',
105
+ 'xox[baprs]-[A-Za-z0-9-]{10,}',
106
+ 'Bearer(?:\\s|\\\\[nrt])+[A-Za-z0-9_.\\-]{16,}',
107
+ ];
108
+ export function redactSecrets(input) {
109
+ let out = input;
110
+ for (const s of SECRET_PATTERN_SOURCES)
111
+ out = out.replace(new RegExp(s, 'gi'), '***REDACTED***');
112
+ return out;
113
+ }
114
+ export function redactUserPaths(text) {
115
+ const home = homeDir();
116
+ const roots = new Set();
117
+ const add = (root) => {
118
+ if (!root || !path.isAbsolute(root))
119
+ return;
120
+ roots.add(root);
121
+ try {
122
+ roots.add(fs.realpathSync(root));
123
+ }
124
+ catch { }
125
+ };
126
+ add(home);
127
+ const isInside = (child) => {
128
+ const rel = path.relative(home, child);
129
+ return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
130
+ };
131
+ for (const dir of [memeshDir(), path.dirname(getDbPath())]) {
132
+ if (dir && !isInside(dir))
133
+ add(dir);
134
+ }
135
+ const flags = process.platform === 'linux' ? 'g' : 'gi';
136
+ let out = text;
137
+ for (const root of [...roots].sort((a, b) => b.length - a.length)) {
138
+ const escaped = root.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
139
+ const body = escaped.replace(/\\\\|\//g, '[\\\\/]{1,2}');
140
+ out = out.replace(new RegExp(`(?<![\\w~](?:[\\\\/]{1,2})?)${body}(?=[\\\\/]|$)`, flags), '~');
141
+ }
142
+ return out;
143
+ }
@@ -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
- export function removeFromFts(db, entityId, name, prevObsText) {
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 insertFtsRow(db, entityId, name, observationsText) {
80
- db.prepare('INSERT INTO entities_fts (rowid, name, observations) VALUES (?, ?, ?)').run(entityId, toIndexForm(name), toIndexForm(observationsText));
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,71 @@
1
+ // ============================================================================
2
+ // AUTO-GENERATED from src/storage/sqlite.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 { createRequire } from 'node:module';
10
+ function loadNodeSqlite() {
11
+ const require = createRequire(import.meta.url);
12
+ const original = process.emitWarning;
13
+ process.emitWarning = function (warning, ...rest) {
14
+ const text = typeof warning === 'string' ? warning : String(warning?.message ?? warning);
15
+ if (text.includes('SQLite is an experimental feature'))
16
+ return;
17
+ return original.call(process, warning, ...rest);
18
+ };
19
+ try {
20
+ return require('node:sqlite');
21
+ }
22
+ catch (err) {
23
+ const detail = err instanceof Error ? err.message : String(err);
24
+ throw new Error(`memesh needs Node's built-in SQLite, which this runtime (${process.version}) does not provide. ` +
25
+ 'It became usable without a flag in Node 22.13.0 — upgrade Node to 22.13 or newer and run memesh again. ' +
26
+ `(${detail})`, { cause: err });
27
+ }
28
+ finally {
29
+ process.emitWarning = original;
30
+ }
31
+ }
32
+ const { DatabaseSync } = loadNodeSqlite();
33
+ const BUSY_TIMEOUT_MS = 5000;
34
+ export class MemeshDatabase extends DatabaseSync {
35
+ #depth = 0;
36
+ constructor(path, options = {}) {
37
+ super(path, options);
38
+ this.pragma(`busy_timeout = ${BUSY_TIMEOUT_MS}`);
39
+ }
40
+ pragma(statement) {
41
+ this.exec(`PRAGMA ${statement}`);
42
+ }
43
+ transaction(fn) {
44
+ const run = (mode, ...args) => {
45
+ const nested = this.#depth > 0;
46
+ const savepoint = `memesh_sp_${this.#depth}`;
47
+ this.exec(nested ? `SAVEPOINT ${savepoint}` : `BEGIN${mode}`);
48
+ this.#depth++;
49
+ try {
50
+ const result = fn(...args);
51
+ this.exec(nested ? `RELEASE ${savepoint}` : 'COMMIT');
52
+ return result;
53
+ }
54
+ catch (err) {
55
+ try {
56
+ this.exec(nested ? `ROLLBACK TO ${savepoint}` : 'ROLLBACK');
57
+ if (nested)
58
+ this.exec(`RELEASE ${savepoint}`);
59
+ }
60
+ catch { }
61
+ throw err;
62
+ }
63
+ finally {
64
+ this.#depth--;
65
+ }
66
+ };
67
+ const callable = ((...args) => run('', ...args));
68
+ callable.immediate = (...args) => run(' IMMEDIATE', ...args);
69
+ return callable;
70
+ }
71
+ }
@@ -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
+ }