@pcircle/memesh 4.8.5 → 4.9.3

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 (232) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/mcp.json +6 -4
  4. package/.codex-plugin/plugin.json +1 -1
  5. package/AGENTS.md +60 -14
  6. package/README.de.md +83 -556
  7. package/README.md +83 -582
  8. package/README.zh-TW.md +84 -573
  9. package/dashboard/dist/index.html +10 -10
  10. package/dist/cli/view-live.d.ts.map +1 -1
  11. package/dist/cli/view-live.js +154 -384
  12. package/dist/cli/view-live.js.map +1 -1
  13. package/dist/core/agent-router.d.ts +11 -10
  14. package/dist/core/agent-router.d.ts.map +1 -1
  15. package/dist/core/agent-router.js +29 -14
  16. package/dist/core/agent-router.js.map +1 -1
  17. package/dist/core/agent-scope-id.d.ts.map +1 -1
  18. package/dist/core/agent-scope-id.js.map +1 -1
  19. package/dist/core/analytics.d.ts.map +1 -1
  20. package/dist/core/analytics.js.map +1 -1
  21. package/dist/core/config.d.ts +4 -40
  22. package/dist/core/config.d.ts.map +1 -1
  23. package/dist/core/config.js +75 -141
  24. package/dist/core/config.js.map +1 -1
  25. package/dist/core/demo.d.ts.map +1 -1
  26. package/dist/core/demo.js +6 -6
  27. package/dist/core/demo.js.map +1 -1
  28. package/dist/core/doctor-fixes.d.ts +19 -0
  29. package/dist/core/doctor-fixes.d.ts.map +1 -0
  30. package/dist/core/doctor-fixes.js +104 -0
  31. package/dist/core/doctor-fixes.js.map +1 -0
  32. package/dist/core/doctor.d.ts +2 -7
  33. package/dist/core/doctor.d.ts.map +1 -1
  34. package/dist/core/doctor.js +57 -229
  35. package/dist/core/doctor.js.map +1 -1
  36. package/dist/core/dreamer.d.ts +32 -47
  37. package/dist/core/dreamer.d.ts.map +1 -1
  38. package/dist/core/dreamer.js +211 -703
  39. package/dist/core/dreamer.js.map +1 -1
  40. package/dist/core/install-channel.d.ts.map +1 -1
  41. package/dist/core/install-channel.js +4 -47
  42. package/dist/core/install-channel.js.map +1 -1
  43. package/dist/core/install-id.d.ts.map +1 -1
  44. package/dist/core/install-id.js.map +1 -1
  45. package/dist/core/kg-backfill.d.ts.map +1 -1
  46. package/dist/core/kg-backfill.js.map +1 -1
  47. package/dist/core/lesson-engine.d.ts +0 -5
  48. package/dist/core/lesson-engine.d.ts.map +1 -1
  49. package/dist/core/lesson-engine.js +0 -25
  50. package/dist/core/lesson-engine.js.map +1 -1
  51. package/dist/core/lifecycle.d.ts.map +1 -1
  52. package/dist/core/lifecycle.js +54 -48
  53. package/dist/core/lifecycle.js.map +1 -1
  54. package/dist/core/memory-tool.d.ts.map +1 -1
  55. package/dist/core/memory-tool.js +17 -17
  56. package/dist/core/memory-tool.js.map +1 -1
  57. package/dist/core/operations.d.ts +2 -26
  58. package/dist/core/operations.d.ts.map +1 -1
  59. package/dist/core/operations.js +11 -246
  60. package/dist/core/operations.js.map +1 -1
  61. package/dist/core/paths.d.ts +4 -1
  62. package/dist/core/paths.d.ts.map +1 -1
  63. package/dist/core/paths.js +88 -14
  64. package/dist/core/paths.js.map +1 -1
  65. package/dist/core/product-improvements.js +2 -2
  66. package/dist/core/product-improvements.js.map +1 -1
  67. package/dist/core/schema-export.d.ts.map +1 -1
  68. package/dist/core/schema-export.js +11 -1
  69. package/dist/core/schema-export.js.map +1 -1
  70. package/dist/core/semver.d.ts +7 -0
  71. package/dist/core/semver.d.ts.map +1 -0
  72. package/dist/core/semver.js +49 -0
  73. package/dist/core/semver.js.map +1 -0
  74. package/dist/core/serializer.d.ts.map +1 -1
  75. package/dist/core/serializer.js +69 -57
  76. package/dist/core/serializer.js.map +1 -1
  77. package/dist/core/signal-scorer.d.ts.map +1 -1
  78. package/dist/core/signal-scorer.js.map +1 -1
  79. package/dist/core/transcript-extractor.d.ts +1 -85
  80. package/dist/core/transcript-extractor.d.ts.map +1 -1
  81. package/dist/core/transcript-extractor.js +5 -364
  82. package/dist/core/transcript-extractor.js.map +1 -1
  83. package/dist/core/transcript-source.d.ts +22 -6
  84. package/dist/core/transcript-source.d.ts.map +1 -1
  85. package/dist/core/transcript-source.js +108 -69
  86. package/dist/core/transcript-source.js.map +1 -1
  87. package/dist/core/types.d.ts +1 -17
  88. package/dist/core/types.d.ts.map +1 -1
  89. package/dist/core/version-check.d.ts.map +1 -1
  90. package/dist/core/version-check.js +11 -1
  91. package/dist/core/version-check.js.map +1 -1
  92. package/dist/db.d.ts +0 -34
  93. package/dist/db.d.ts.map +1 -1
  94. package/dist/db.js +3 -287
  95. package/dist/db.js.map +1 -1
  96. package/dist/host-runtime/acp.d.ts.map +1 -1
  97. package/dist/host-runtime/acp.js +2 -2
  98. package/dist/host-runtime/acp.js.map +1 -1
  99. package/dist/host-runtime/claude.d.ts.map +1 -1
  100. package/dist/host-runtime/claude.js +9 -7
  101. package/dist/host-runtime/claude.js.map +1 -1
  102. package/dist/host-runtime/codex-session.d.ts +9 -1
  103. package/dist/host-runtime/codex-session.d.ts.map +1 -1
  104. package/dist/host-runtime/codex-session.js +474 -29
  105. package/dist/host-runtime/codex-session.js.map +1 -1
  106. package/dist/host-runtime/codex.d.ts.map +1 -1
  107. package/dist/host-runtime/codex.js +2 -2
  108. package/dist/host-runtime/codex.js.map +1 -1
  109. package/dist/host-runtime/config.d.ts +1 -0
  110. package/dist/host-runtime/config.d.ts.map +1 -1
  111. package/dist/host-runtime/config.js +4 -0
  112. package/dist/host-runtime/config.js.map +1 -1
  113. package/dist/host-runtime/router-client.d.ts.map +1 -1
  114. package/dist/host-runtime/router-client.js +62 -15
  115. package/dist/host-runtime/router-client.js.map +1 -1
  116. package/dist/host-runtime/router.js +2 -2
  117. package/dist/host-runtime/router.js.map +1 -1
  118. package/dist/knowledge-graph.d.ts.map +1 -1
  119. package/dist/knowledge-graph.js +84 -55
  120. package/dist/knowledge-graph.js.map +1 -1
  121. package/dist/mcp/THIRD_PARTY_NOTICES.txt +217 -0
  122. package/dist/mcp/server.js +30714 -38
  123. package/dist/mcp/server.js.map +6 -1
  124. package/dist/skills-manifest.json +24 -24
  125. package/dist/storage/entity-index.d.ts +0 -1
  126. package/dist/storage/entity-index.d.ts.map +1 -1
  127. package/dist/storage/entity-index.js +0 -7
  128. package/dist/storage/entity-index.js.map +1 -1
  129. package/dist/storage/fts-index.d.ts.map +1 -1
  130. package/dist/storage/fts-index.js +6 -7
  131. package/dist/storage/fts-index.js.map +1 -1
  132. package/dist/storage/graph-repairs.d.ts +0 -5
  133. package/dist/storage/graph-repairs.d.ts.map +1 -1
  134. package/dist/storage/graph-repairs.js +4 -91
  135. package/dist/storage/graph-repairs.js.map +1 -1
  136. package/dist/storage/schema.d.ts +1 -1
  137. package/dist/storage/schema.d.ts.map +1 -1
  138. package/dist/storage/schema.js +1 -2
  139. package/dist/storage/schema.js.map +1 -1
  140. package/dist/storage/sqlite.d.ts +0 -1
  141. package/dist/storage/sqlite.d.ts.map +1 -1
  142. package/dist/storage/sqlite.js.map +1 -1
  143. package/dist/transports/agent-messaging.d.ts +4 -0
  144. package/dist/transports/agent-messaging.d.ts.map +1 -1
  145. package/dist/transports/agent-messaging.js +16 -8
  146. package/dist/transports/agent-messaging.js.map +1 -1
  147. package/dist/transports/cli/cli.d.ts.map +1 -1
  148. package/dist/transports/cli/cli.js +60751 -2408
  149. package/dist/transports/cli/cli.js.map +6 -1
  150. package/dist/transports/http/retired-routes.js +1 -1
  151. package/dist/transports/http/retired-routes.js.map +1 -1
  152. package/dist/transports/http/server.d.ts.map +1 -1
  153. package/dist/transports/http/server.js +45 -233
  154. package/dist/transports/http/server.js.map +1 -1
  155. package/dist/transports/mcp/handlers.d.ts +130 -4
  156. package/dist/transports/mcp/handlers.d.ts.map +1 -1
  157. package/dist/transports/mcp/handlers.js +56 -7
  158. package/dist/transports/mcp/handlers.js.map +1 -1
  159. package/dist/transports/schemas.d.ts +52 -0
  160. package/dist/transports/schemas.d.ts.map +1 -1
  161. package/dist/transports/schemas.js +39 -0
  162. package/dist/transports/schemas.js.map +1 -1
  163. package/docs/platforms/README.md +5 -4
  164. package/docs/platforms/agent-messaging.md +134 -61
  165. package/hooks/hooks.json +13 -2
  166. package/llms-install.md +62 -30
  167. package/package.json +4 -5
  168. package/scripts/hooks/_generated/core-paths.js +88 -14
  169. package/scripts/hooks/_generated/fts-index.js +6 -7
  170. package/scripts/hooks/_generated/schema.js +1 -2
  171. package/scripts/hooks/_shared.js +164 -21
  172. package/scripts/hooks/session-start.js +98 -10
  173. package/scripts/hooks/session-summary.js +34 -376
  174. package/scripts/hooks/user-prompt-intent.js +71 -11
  175. package/skills/memesh/SKILL.md +19 -14
  176. package/skills/memesh-review/SKILL.md +7 -6
  177. package/dist/core/auto-tagger.d.ts +0 -10
  178. package/dist/core/auto-tagger.d.ts.map +0 -1
  179. package/dist/core/auto-tagger.js +0 -63
  180. package/dist/core/auto-tagger.js.map +0 -1
  181. package/dist/core/conflict-candidates.d.ts +0 -20
  182. package/dist/core/conflict-candidates.d.ts.map +0 -1
  183. package/dist/core/conflict-candidates.js +0 -71
  184. package/dist/core/conflict-candidates.js.map +0 -1
  185. package/dist/core/conflict-judge.d.ts +0 -58
  186. package/dist/core/conflict-judge.d.ts.map +0 -1
  187. package/dist/core/conflict-judge.js +0 -189
  188. package/dist/core/conflict-judge.js.map +0 -1
  189. package/dist/core/digest-validator.d.ts +0 -18
  190. package/dist/core/digest-validator.d.ts.map +0 -1
  191. package/dist/core/digest-validator.js +0 -85
  192. package/dist/core/digest-validator.js.map +0 -1
  193. package/dist/core/embedder.d.ts +0 -20
  194. package/dist/core/embedder.d.ts.map +0 -1
  195. package/dist/core/embedder.js +0 -245
  196. package/dist/core/embedder.js.map +0 -1
  197. package/dist/core/failure-analyzer.d.ts +0 -19
  198. package/dist/core/failure-analyzer.d.ts.map +0 -1
  199. package/dist/core/failure-analyzer.js +0 -83
  200. package/dist/core/failure-analyzer.js.map +0 -1
  201. package/dist/core/json-utils.d.ts +0 -3
  202. package/dist/core/json-utils.d.ts.map +0 -1
  203. package/dist/core/json-utils.js +0 -46
  204. package/dist/core/json-utils.js.map +0 -1
  205. package/dist/core/llm-client.d.ts +0 -22
  206. package/dist/core/llm-client.d.ts.map +0 -1
  207. package/dist/core/llm-client.js +0 -203
  208. package/dist/core/llm-client.js.map +0 -1
  209. package/dist/core/llm-telemetry.d.ts +0 -47
  210. package/dist/core/llm-telemetry.d.ts.map +0 -1
  211. package/dist/core/llm-telemetry.js +0 -117
  212. package/dist/core/llm-telemetry.js.map +0 -1
  213. package/dist/core/llm-validator.d.ts +0 -20
  214. package/dist/core/llm-validator.d.ts.map +0 -1
  215. package/dist/core/llm-validator.js +0 -231
  216. package/dist/core/llm-validator.js.map +0 -1
  217. package/dist/core/ollama-host.d.ts +0 -6
  218. package/dist/core/ollama-host.d.ts.map +0 -1
  219. package/dist/core/ollama-host.js +0 -62
  220. package/dist/core/ollama-host.js.map +0 -1
  221. package/dist/core/output-language.d.ts +0 -6
  222. package/dist/core/output-language.d.ts.map +0 -1
  223. package/dist/core/output-language.js +0 -25
  224. package/dist/core/output-language.js.map +0 -1
  225. package/dist/core/prompt-safety.d.ts +0 -4
  226. package/dist/core/prompt-safety.d.ts.map +0 -1
  227. package/dist/core/prompt-safety.js +0 -20
  228. package/dist/core/prompt-safety.js.map +0 -1
  229. package/dist/storage/vector-index.d.ts +0 -3
  230. package/dist/storage/vector-index.d.ts.map +0 -1
  231. package/dist/storage/vector-index.js +0 -13
  232. package/dist/storage/vector-index.js.map +0 -1
@@ -11,6 +11,8 @@ import os from 'os';
11
11
  import path from 'path';
12
12
  import { createHash } from 'crypto';
13
13
  import { execFileSync } from 'child_process';
14
+ export const AGENT_ROUTER_SOCKET_FILENAME = 'agent-router-v2.sock';
15
+ const LEGACY_AGENT_ROUTER_SOCKET_FILENAME = 'agent-router.sock';
14
16
  export function homeDir() {
15
17
  const home = process.env.HOME;
16
18
  if (home && home.length > 0)
@@ -31,6 +33,15 @@ export function getMemeshDirFromDbPath() {
31
33
  ? path.dirname(process.env.MEMESH_DB_PATH)
32
34
  : memeshDir();
33
35
  }
36
+ export function getAgentRouterSocketPath() {
37
+ return path.join(getMemeshDirFromDbPath(), AGENT_ROUTER_SOCKET_FILENAME);
38
+ }
39
+ export function normalizeAgentRouterSocketPath(socketPath) {
40
+ const dataDir = getMemeshDirFromDbPath();
41
+ return socketPath === path.join(dataDir, LEGACY_AGENT_ROUTER_SOCKET_FILENAME)
42
+ ? getAgentRouterSocketPath()
43
+ : socketPath;
44
+ }
34
45
  export function getProjectName(cwdInput) {
35
46
  const cwd = cwdInput && cwdInput.length > 0 ? cwdInput : process.cwd();
36
47
  const cached = projectNameCache.get(cwd);
@@ -44,22 +55,36 @@ const projectNameCache = new Map();
44
55
  function resolveProjectIdentity(cwd) {
45
56
  const remote = tryGit(cwd, ['config', '--get', 'remote.origin.url']);
46
57
  if (remote) {
47
- const slug = slugFromRemoteUrl(remote);
48
- if (slug)
49
- return slug;
58
+ const locator = canonicalRemoteLocator(remote);
59
+ if (locator) {
60
+ const label = path.posix.basename(locator).replace(/\.git$/i, '');
61
+ return projectIdentity(label, locator);
62
+ }
50
63
  }
51
64
  const root = tryGit(cwd, ['rev-parse', '--show-toplevel']);
52
- if (root)
53
- return path.basename(root);
65
+ const commonDir = root
66
+ ? tryGit(cwd, ['rev-parse', '--git-common-dir'])
67
+ : null;
68
+ const absoluteCommonDir = commonDir ? path.resolve(cwd, commonDir) : null;
69
+ const localPath = absoluteCommonDir && path.basename(absoluteCommonDir) === '.git'
70
+ ? path.dirname(absoluteCommonDir)
71
+ : (root ?? cwd);
54
72
  let real;
55
73
  try {
56
- real = fs.realpathSync.native(cwd);
74
+ real = fs.realpathSync.native(localPath);
57
75
  }
58
76
  catch {
59
- real = path.resolve(cwd);
77
+ real = path.resolve(localPath);
60
78
  }
61
- const suffix = createHash('sha256').update(real).digest('hex').slice(0, 8);
62
- return `${path.basename(real)}-${suffix}`;
79
+ return projectIdentity(path.basename(real), real);
80
+ }
81
+ const PROJECT_HASH_HEX_LENGTH = 32;
82
+ const PROJECT_ID_MAX_LENGTH = 200;
83
+ const PROJECT_LABEL_MAX_LENGTH = PROJECT_ID_MAX_LENGTH - PROJECT_HASH_HEX_LENGTH - 1;
84
+ function projectIdentity(label, locator) {
85
+ const readable = label.normalize('NFC').slice(0, PROJECT_LABEL_MAX_LENGTH) || 'project';
86
+ const suffix = createHash('sha256').update(locator).digest('hex').slice(0, PROJECT_HASH_HEX_LENGTH);
87
+ return `${readable}~${suffix}`;
63
88
  }
64
89
  function tryGit(cwd, args) {
65
90
  try {
@@ -75,12 +100,61 @@ function tryGit(cwd, args) {
75
100
  return null;
76
101
  }
77
102
  }
78
- export function slugFromRemoteUrl(url) {
79
- const cleaned = url.trim().replace(/\.git$/i, '').replace(/[/\\]+$/, '');
80
- if (!cleaned)
103
+ export function canonicalRemoteLocator(remote) {
104
+ const value = remote.trim();
105
+ if (!value)
106
+ return null;
107
+ if (path.isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value) || /^\\\\/.test(value))
108
+ return null;
109
+ let host;
110
+ let port = '';
111
+ let user;
112
+ let remotePath;
113
+ let transport;
114
+ if (/^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(value)) {
115
+ let parsed;
116
+ try {
117
+ parsed = new URL(value);
118
+ }
119
+ catch {
120
+ return null;
121
+ }
122
+ if (parsed.protocol === 'file:' || !parsed.hostname)
123
+ return null;
124
+ host = parsed.hostname.toLowerCase();
125
+ port = parsed.port;
126
+ const protocol = parsed.protocol.toLowerCase();
127
+ if ((protocol === 'ssh:' || protocol === 'git+ssh:') && port === '22')
128
+ port = '';
129
+ user = parsed.username;
130
+ remotePath = parsed.pathname;
131
+ transport = protocol === 'ssh:' || protocol === 'git+ssh:'
132
+ ? 'ssh-absolute'
133
+ : protocol.slice(0, -1);
134
+ }
135
+ else {
136
+ const scp = /^(?:([^@]+)@)?(\[[^\]]+\]|[^:/]+):(.+)$/.exec(value);
137
+ if (!scp)
138
+ return null;
139
+ user = scp[1] ?? '';
140
+ host = scp[2].toLowerCase();
141
+ remotePath = scp[3];
142
+ transport = remotePath.startsWith('/') ? 'ssh-absolute' : 'ssh-relative';
143
+ }
144
+ const pathWithoutSlashes = remotePath.replace(/^\/+|\/+$/g, '');
145
+ if (!host || !pathWithoutSlashes)
81
146
  return null;
82
- const seg = cleaned.split(/[/:\\]/).filter(Boolean).pop();
83
- return seg && seg.length > 0 ? seg : null;
147
+ const endpoint = `${host}${port ? `:${port}` : ''}`;
148
+ const standardGithub = host === 'github.com'
149
+ && port === ''
150
+ && (transport === 'https' || ((transport === 'ssh-relative' || transport === 'ssh-absolute') && user === 'git'));
151
+ const normalizedPath = standardGithub
152
+ ? pathWithoutSlashes.replace(/\.git$/i, '')
153
+ : pathWithoutSlashes;
154
+ if (standardGithub)
155
+ return `${endpoint}/${normalizedPath}`;
156
+ const authority = transport.startsWith('ssh-') && user ? `${user}@${endpoint}` : endpoint;
157
+ return `${transport}://${authority}/${normalizedPath}`;
84
158
  }
85
159
  export function _clearProjectNameCache() {
86
160
  projectNameCache.clear();
@@ -75,15 +75,14 @@ export function removeFromFts(db, entityId, name, prevObsText, prevTitle) {
75
75
  db.prepare("INSERT INTO entities_fts (entities_fts, rowid, name, observations) VALUES('delete', ?, ?, ?)").run(entityId, toIndexForm(name), toIndexForm(foldTitleIntoObservations(prevTitle, prevObsText)));
76
76
  }
77
77
  catch (err) {
78
- if (isBenignFtsDeleteError(err))
79
- return;
80
- process.stderr.write(`[memesh fts-index] removeFromFts(rowid=${entityId}) failed: ${err instanceof Error ? err.message : String(err)}\n`);
78
+ try {
79
+ process.stderr.write(`[memesh fts-index] removeFromFts(rowid=${entityId}) failed: ${err instanceof Error ? err.message : String(err)}\n`);
80
+ }
81
+ catch {
82
+ }
83
+ throw err;
81
84
  }
82
85
  }
83
- function isBenignFtsDeleteError(err) {
84
- const msg = err?.message ?? '';
85
- return /no such rowid|values do not match|no such row\b/i.test(msg);
86
- }
87
86
  export function joinIndexedObservations(contents) {
88
87
  return contents.join(' ');
89
88
  }
@@ -56,8 +56,7 @@ CREATE INDEX IF NOT EXISTS idx_relations_from ON relations(from_entity_id);
56
56
  CREATE INDEX IF NOT EXISTS idx_relations_to ON relations(to_entity_id);
57
57
  CREATE INDEX IF NOT EXISTS idx_entities_type_created ON entities(type, created_at);
58
58
 
59
- -- Migration markers and small bits of persistent state (index segmentation
60
- -- version, embedding dimension, pending-reindex flags, backfill markers).
59
+ -- Migration markers and small bits of persistent state.
61
60
  --
62
61
  -- This used to be created ad hoc by each helper that needed it — four inline
63
62
  -- CREATE TABLE IF NOT EXISTS copies in src/db.ts, none of them visible to
@@ -1,4 +1,5 @@
1
- import { appendFileSync, chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from 'fs';
1
+ import { appendFileSync, chmodSync, closeSync, constants as fsConstants, existsSync, mkdirSync, openSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'fs';
2
+ import { createHash } from 'crypto';
2
3
  import { spawn } from 'child_process';
3
4
  import { MemeshDatabase } from './_generated/sqlite.js';
4
5
  import { dirname, join } from 'path';
@@ -31,7 +32,7 @@ import {
31
32
  getMemeshDirFromDbPath,
32
33
  getProjectName,
33
34
  redactSecrets,
34
- slugFromRemoteUrl,
35
+ canonicalRemoteLocator,
35
36
  } from './_generated/core-paths.js';
36
37
  import { autoCaptureDecision } from './_generated/capture-flag.js';
37
38
  export { assembleTopologyBlock, buildReferenceContext, extractCitedMemoryIds, DEFAULT_TOPOLOGY_BUDGET, GLOBAL_TOPOLOGY_LIMIT, SNIPPET_FETCH_CHARS, TOPOLOGY_CANDIDATE_CAP } from './_generated/work-topology.js';
@@ -126,15 +127,15 @@ import {
126
127
  tokenizeQuery,
127
128
  } from './_generated/fts-index.js';
128
129
 
129
- export { homeDir, memeshDir, getDbPath, getMemeshDirFromDbPath, getProjectName, redactSecrets, slugFromRemoteUrl };
130
+ export { homeDir, memeshDir, getDbPath, getMemeshDirFromDbPath, getProjectName, redactSecrets, canonicalRemoteLocator };
130
131
 
131
132
  /**
132
133
  * Resolve the package root from a hook file's `import.meta.url`.
133
134
  *
134
135
  * Hooks live at `<pkgRoot>/scripts/hooks/<file>.js`, so the path needs
135
136
  * three `dirname()` hops to reach `<pkgRoot>`. Centralising this here
136
- * prevents the off-by-one regression that silently disabled noise
137
- * compression and LLM failure analysis between 4.0.4–4.1.0 (the inline
137
+ * prevents the off-by-one regression that silently disabled hook-to-core
138
+ * imports between 4.0.4–4.1.0 (the inline
138
139
  * `dirname(dirname(...))` only reached `<pkgRoot>/scripts`, and the
139
140
  * surrounding `catch` swallowed the resulting ENOENT).
140
141
  *
@@ -163,7 +164,7 @@ export function resolvePluginRoot(metaUrl) {
163
164
  *
164
165
  * That error is caught by each caller's surrounding try/catch and only
165
166
  * traced to stderr, so on Windows every hook that reaches for a dist module
166
- * — LLM failure analysis, lesson creation, dream auto-trigger, auto-decay —
167
+ * — lifecycle reads, lesson creation, and auto-decay —
167
168
  * silently did nothing, while macOS/Linux and `memesh doctor` stayed green.
168
169
  * A textbook fake-working boundary: the discipline of converting the path
169
170
  * (already applied to the install-channel import above via
@@ -363,19 +364,15 @@ export function openHookDb(env = process.env, opts = {}) {
363
364
  const dbDir = env.MEMESH_DB_PATH ? dirname(env.MEMESH_DB_PATH) : memeshDir();
364
365
  if (!existsSync(dbDir)) mkdirSync(dbDir, { recursive: true });
365
366
 
366
- // `allowExtension` matches src/db.ts: it only permits a later
367
- // `enableLoadExtension(true)`, and session-summary.js needs one to load
368
- // sqlite-vec through this handle. The switch itself stays off.
369
- const db = new MemeshDatabase(dbPath, { allowExtension: true });
367
+ const db = new MemeshDatabase(dbPath);
370
368
  db.pragma('journal_mode = WAL');
371
369
  db.pragma('foreign_keys = ON');
372
370
  // A hook waits for a held write lock for less time than Claude Code will
373
371
  // wait for the hook.
374
372
  //
375
- // `MemeshDatabase` sets `busy_timeout = 30000`, and that number is right
376
- // for the processes it was chosen for: a 30k-vector `swapVectorGeneration`
377
- // holds the write lock for ~9s, and the CLI, the MCP server and the HTTP
378
- // server should WAIT for it rather than fail. A hook cannot. Its budget in
373
+ // `MemeshDatabase` sets `busy_timeout = 30000`, which lets ordinary CLI,
374
+ // MCP and HTTP operations wait through brief write contention. A hook
375
+ // cannot use that whole budget. Its timeout in
379
376
  // `hooks/hooks.json` is 3s (UserPromptSubmit) to 10s (Stop, PreCompact),
380
377
  // so a 30s wait has exactly one possible ending: the harness kills the
381
378
  // hook. The capture is lost either way — the difference is that the user
@@ -559,9 +556,8 @@ export { truncateTitle } from './_generated/title.js';
559
556
  * as two implementations of the same contract).
560
557
  *
561
558
  * The caller MUST open its DB with `openHookDb(env, { fts: true })` so the FTS
562
- * table is guaranteed present. Embeddings + auto-tagging + signal-scoring are
563
- * deliberately NOT done here: hooks are cheap always-on capture, and those are
564
- * the heavier, user-initiated `remember` concerns (core owns them).
559
+ * table is guaranteed present. Signal scoring is deliberately not done here:
560
+ * hooks stay a cheap always-on capture path and core owns later enrichment.
565
561
  *
566
562
  * @param {import('./_generated/sqlite.js').MemeshDatabase} db - an open hook DB handle
567
563
  * @param {{name: string, type: string, observations?: string[], tags?: string[], title?: string | null, metadata?: Record<string, unknown>}} entity
@@ -601,10 +597,9 @@ function captureEntityInner(db, { name, type, observations, tags, title, metadat
601
597
  // earlier writer (possibly another host, via MCP) already recorded.
602
598
  //
603
599
  // title_source: every title a hook writes is machine-derived, so it is
604
- // marked 'heuristic'. The mark is what lets a later LLM titling pass
605
- // (dreamer backfill) know which titles it may replace an UNMARKED title
606
- // is treated as human-provided and never touched, so omitting the mark
607
- // here would make today's date+verb titles permanent.
600
+ // marked 'heuristic'. The mark distinguishes generated display text from an
601
+ // unmarked human-provided title, so later reviewed edits can preserve the
602
+ // ownership boundary.
608
603
  const insertMetadata = { ...(metadata ?? {}), provenance: { source_host: 'claude-code' } };
609
604
  if (title != null) insertMetadata.title_source = 'heuristic';
610
605
  const insertResult = db
@@ -833,6 +828,154 @@ const POLICY_RANK = { off: 0, patch: 1, minor: 2, major: 3 };
833
828
  const BUMP_RANK = { patch: 1, minor: 2, major: 3 };
834
829
  const AUTO_UPDATE_CACHE_FRESHNESS_MS = 24 * 60 * 60 * 1000;
835
830
 
831
+ function autoUpdateConsentPath(sessionId, currentVersion, latestVersion, channel = 'unknown') {
832
+ if (typeof sessionId !== 'string' || sessionId.length === 0 || sessionId === 'unknown') return null;
833
+ const key = createHash('sha256')
834
+ .update(`${sessionId}\0${currentVersion}\0${latestVersion}\0${channel}`)
835
+ .digest('hex');
836
+ return join(memeshDir(), 'update-consent', `${key}.json`);
837
+ }
838
+
839
+ function updatePromptClaimPath(sessionId, currentVersion, latestVersion) {
840
+ if (typeof sessionId !== 'string' || sessionId.length === 0 || sessionId === 'unknown') return null;
841
+ const key = createHash('sha256')
842
+ .update(`${sessionId}\0${currentVersion}\0${latestVersion}`)
843
+ .digest('hex');
844
+ return join(memeshDir(), 'update-prompt-claims', `${key}.json`);
845
+ }
846
+
847
+ /**
848
+ * Atomically claim the one first-use update notice for a session/version.
849
+ * Separate host hooks can start at the same time; a read-then-write pending
850
+ * marker lets both print the prompt. O_EXCL makes the claim the decision.
851
+ */
852
+ export function claimUpdatePrompt(sessionId, currentVersion, latestVersion, channel) {
853
+ const path = updatePromptClaimPath(sessionId, currentVersion, latestVersion);
854
+ if (!path || typeof channel !== 'string' || channel.length === 0) return false;
855
+ try {
856
+ ensurePrivateDir(join(memeshDir(), 'update-prompt-claims'));
857
+ // A crash can leave a pending claim before the hook emits its output.
858
+ // Reclaim only when that owner process is definitely gone; emitted
859
+ // claims remain session-scoped and continue suppressing duplicates.
860
+ try {
861
+ const existing = JSON.parse(readFileSync(path, 'utf8'));
862
+ const ownerPid = Number(existing?.ownerPid);
863
+ if (existing?.decision === 'pending' && Number.isInteger(ownerPid) && ownerPid > 0) {
864
+ try {
865
+ process.kill(ownerPid, 0);
866
+ } catch (err) {
867
+ if (err?.code === 'ESRCH') unlinkSync(path);
868
+ }
869
+ }
870
+ } catch {
871
+ // A corrupt claim fails closed at the O_EXCL step below.
872
+ }
873
+ const fd = openSync(path, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL, 0o600);
874
+ try {
875
+ writeFileSync(fd, JSON.stringify({
876
+ sessionId,
877
+ currentVersion,
878
+ latestVersion,
879
+ channel,
880
+ decision: 'pending',
881
+ ownerPid: process.pid,
882
+ recordedAt: new Date().toISOString(),
883
+ }));
884
+ } finally {
885
+ closeSync(fd);
886
+ }
887
+ return true;
888
+ } catch {
889
+ return false;
890
+ }
891
+ }
892
+
893
+ export function finalizeUpdatePromptClaim(sessionId, currentVersion, latestVersion) {
894
+ const path = updatePromptClaimPath(sessionId, currentVersion, latestVersion);
895
+ if (!path || !existsSync(path)) return false;
896
+ try {
897
+ const value = JSON.parse(readFileSync(path, 'utf8'));
898
+ if (value?.decision !== 'pending' || Number(value.ownerPid) !== process.pid) return false;
899
+ writePrivateJson(path, { ...value, decision: 'emitted', emittedAt: new Date().toISOString() });
900
+ return true;
901
+ } catch {
902
+ return false;
903
+ }
904
+ }
905
+
906
+ export function readUpdatePromptClaim(sessionId, currentVersion, latestVersion) {
907
+ const path = updatePromptClaimPath(sessionId, currentVersion, latestVersion);
908
+ if (!path || !existsSync(path)) return null;
909
+ try {
910
+ const value = JSON.parse(readFileSync(path, 'utf8'));
911
+ return value && typeof value === 'object' ? value : null;
912
+ } catch {
913
+ return null;
914
+ }
915
+ }
916
+
917
+ export function readAutoUpdateConsent(sessionId, currentVersion, latestVersion, channel = 'unknown') {
918
+ const path = autoUpdateConsentPath(sessionId, currentVersion, latestVersion, channel);
919
+ if (!path || !existsSync(path)) return null;
920
+ try {
921
+ const value = JSON.parse(readFileSync(path, 'utf8'));
922
+ return value && typeof value === 'object' ? value : null;
923
+ } catch {
924
+ return null;
925
+ }
926
+ }
927
+
928
+ /**
929
+ * @param {string} sessionId
930
+ * @param {string} currentVersion
931
+ * @param {string} latestVersion
932
+ * @param {string|null} [channel=null]
933
+ */
934
+ export function findAutoUpdateConsent(sessionId, currentVersion, latestVersion, channel = null) {
935
+ if (typeof sessionId !== 'string' || !sessionId || sessionId === 'unknown') return null;
936
+ try {
937
+ const dir = join(memeshDir(), 'update-consent');
938
+ for (const file of readdirSync(dir)) {
939
+ if (!file.endsWith('.json')) continue;
940
+ try {
941
+ const value = JSON.parse(readFileSync(join(dir, file), 'utf8'));
942
+ if (value?.sessionId === sessionId
943
+ && value?.currentVersion === currentVersion
944
+ && value?.latestVersion === latestVersion
945
+ && (channel === null || value?.channel === channel)) return value;
946
+ } catch { /* ignore one corrupt marker */ }
947
+ }
948
+ } catch { /* missing/unreadable consent dir */ }
949
+ return null;
950
+ }
951
+
952
+ export function writeAutoUpdateConsent(sessionId, currentVersion, latestVersion, channel, decision) {
953
+ const path = autoUpdateConsentPath(sessionId, currentVersion, latestVersion, channel);
954
+ if (!path || !['pending', 'approved', 'declined'].includes(decision)) return false;
955
+ try {
956
+ ensurePrivateDir(join(memeshDir(), 'update-consent'));
957
+ writePrivateJson(path, {
958
+ sessionId,
959
+ currentVersion,
960
+ latestVersion,
961
+ channel,
962
+ decision,
963
+ recordedAt: new Date().toISOString(),
964
+ });
965
+ return true;
966
+ } catch {
967
+ return false;
968
+ }
969
+ }
970
+
971
+ export function parseAutoUpdateConsent(prompt) {
972
+ if (typeof prompt !== 'string') return null;
973
+ const value = prompt.trim().toLowerCase().replace(/[.!?。!?]+$/u, '');
974
+ if (/^(?:yes|y|upgrade|update|install(?: it)?|go ahead|是|好|升級|更新|安裝)$/.test(value)) return 'approved';
975
+ if (/^(?:no|n|not now|later|不要|不用|稍後|暫時不要)$/.test(value)) return 'declined';
976
+ return null;
977
+ }
978
+
836
979
  export function decideAutoUpdateHook(currentVersion, cache, policy) {
837
980
  if (!cache || cache.currentVersion !== currentVersion) return { run: false };
838
981
  const latest = cache.latestVersion;
@@ -26,6 +26,10 @@ import {
26
26
  memeshDir as memeshHomeDir,
27
27
  parseTaskState,
28
28
  readRepoState,
29
+ readAutoUpdateConsent,
30
+ claimUpdatePrompt,
31
+ finalizeUpdatePromptClaim,
32
+ readUpdatePromptClaim,
29
33
  readUpdateCheckCache,
30
34
  repoStateLines,
31
35
  resolvePluginRoot,
@@ -34,6 +38,7 @@ import {
34
38
  homeDir,
35
39
  taskStateName,
36
40
  writeCitationRule,
41
+ writeAutoUpdateConsent,
37
42
  writePrivateJson,
38
43
  } from './_shared.js';
39
44
  import { MemeshDatabase } from './_generated/sqlite.js';
@@ -326,6 +331,43 @@ function detectInstallChannelHook(pluginRoot) {
326
331
  }
327
332
  }
328
333
 
334
+ function buildUpdateConsentPrompt(sessionId, currentVersion, cache, channel) {
335
+ if (!sessionId || sessionId === 'unknown' || !cache || cache.currentVersion !== currentVersion) return null;
336
+ if (!cache.latestVersion || !isStrictlyOlder(currentVersion, cache.latestVersion)) return null;
337
+ const existing = readAutoUpdateConsent(sessionId, currentVersion, cache.latestVersion, channel);
338
+ if (existing?.decision) return null;
339
+ // Claim the session-level notice before emitting it. A resumed hook or a
340
+ // concurrent host process (including a second host channel) sees the
341
+ // atomic claim and does not duplicate the ask.
342
+ if (readUpdatePromptClaim(sessionId, currentVersion, cache.latestVersion)
343
+ || !claimUpdatePrompt(sessionId, currentVersion, cache.latestVersion, channel)) return null;
344
+ const target = channel === 'plugin-marketplace'
345
+ ? 'the installed marketplace plugin'
346
+ : channel === 'npm-global' ? 'the global memesh installation' : 'this MeMesh installation';
347
+ if (channel !== 'npm-global') {
348
+ const pluginRoot = resolvePluginRoot(import.meta.url);
349
+ const action = channel === 'plugin-marketplace'
350
+ ? pluginUpgradeLine(pluginRoot)
351
+ : channel === 'source-checkout'
352
+ ? ' Source checkout: pull and rebuild (`git pull && npm install && npm run build`).'
353
+ : channel === 'npm-local'
354
+ ? ' Project-local install: run `npm install @pcircle/memesh@latest` in the project that installed it.'
355
+ : ' Update it through the tool or package manager that installed MeMesh.';
356
+ return {
357
+ system: `\nℹ️ MeMesh ${cache.latestVersion} is available (you're on ${currentVersion}) for ${target}. This installation cannot be upgraded automatically from this session.\n${action}`,
358
+ context: `MeMesh ${cache.latestVersion} is available for ${target}, but this channel has no safe in-session installer. Show the user the channel-specific update action and do not claim that an Upgrade reply will install it.`,
359
+ };
360
+ }
361
+ // The npm-global channel is the only hook-owned installer. Record a
362
+ // channel-specific pending consent for the Stop hook after the global
363
+ // session notice has been claimed.
364
+ if (!writeAutoUpdateConsent(sessionId, currentVersion, cache.latestVersion, channel, 'pending')) return null;
365
+ return {
366
+ system: `\nℹ️ MeMesh ${cache.latestVersion} is available (you're on ${currentVersion}) for ${target}. Reply “Upgrade” to install it, or “Not now” to skip for this session.`,
367
+ context: `MeMesh update consent is pending for this session. Ask the user whether to upgrade from ${currentVersion} to ${cache.latestVersion} for the ${target}. Wait for an explicit Upgrade or Not now response; do not install without affirmative consent.`,
368
+ };
369
+ }
370
+
329
371
  /**
330
372
  * Which plugin runtime owns this copy: 'claude-code', 'codex', or null.
331
373
  *
@@ -503,6 +545,15 @@ function runPostBannerUpdateTasks() {
503
545
  if (__postBannerRan) return;
504
546
  __postBannerRan = true;
505
547
  try {
548
+ // A first-use session has no database yet. Starting the detached
549
+ // `memesh status` refresh in that state makes status create/migrate the
550
+ // database while the next SessionStart may already be opening it
551
+ // read-only. SQLite can expose that window as a partially-created schema
552
+ // (for example, `entities` exists while `tags` does not), turning a
553
+ // harmless update notice into "memories not loaded". The next session
554
+ // after the first capture will refresh the cache once the database is
555
+ // fully established; consent itself was already emitted above.
556
+ if (!existsSync(dbPath)) return;
506
557
  let installedVersion = null;
507
558
  try {
508
559
  const pluginRoot = resolvePluginRoot(import.meta.url);
@@ -679,7 +730,21 @@ process.stdin.on('end', async () => {
679
730
  // With no database there is nothing to recall either — the warning IS
680
731
  // the whole truth, and "memories will be created as you work" would
681
732
  // contradict it one line later.
682
- output(combineWithBanner(captureWarning ?? '◉ MeMesh ready · no database yet, memories will be created as you work'));
733
+ const emptySummary = combineWithBanner(captureWarning ?? '◉ MeMesh ready · no database yet, memories will be created as you work');
734
+ let consent = null;
735
+ let consentVersion = null;
736
+ let consentCache = null;
737
+ try {
738
+ const pluginRoot = resolvePluginRoot(import.meta.url);
739
+ const pkg = JSON.parse(readFileSync(join(pluginRoot, 'package.json'), 'utf8'));
740
+ consentVersion = typeof pkg.version === 'string' ? pkg.version : null;
741
+ consentCache = readUpdateCheckCache(consentVersion);
742
+ const channel = detectInstallChannelHook(pluginRoot);
743
+ consent = buildUpdateConsentPrompt(data.session_id, consentVersion, consentCache, channel);
744
+ } catch { /* best-effort */ }
745
+ output(consent ? `${consent.system}\n${emptySummary}` : emptySummary,
746
+ consent ? `${consent.context}\n\n${workPackageGuidance}` : workPackageGuidance);
747
+ if (consent) finalizeUpdatePromptClaim(data.session_id, consentVersion, consentCache?.latestVersion);
683
748
  return;
684
749
  }
685
750
 
@@ -1042,7 +1107,7 @@ process.stdin.on('end', async () => {
1042
1107
  // nothing — a fenced block containing only a branch name tells the
1043
1108
  // agent something it can already see. briefing.test.ts's parity case
1044
1109
  // is what keeps this identical to the tool side.
1045
- let memoryContext = '';
1110
+ let memoryContext = workPackageGuidance;
1046
1111
  if (memoryLines.length > 0) {
1047
1112
  const repoLines = repoStateLines(readRepoState(data.cwd));
1048
1113
  if (repoLines.length > 0) memoryLines.unshift(...repoLines, '');
@@ -1055,7 +1120,7 @@ process.stdin.on('end', async () => {
1055
1120
  // charges task state plus project/foreign sections against the main
1056
1121
  // ceiling and global context against its small additive ceiling. It
1057
1122
  // returns whole lines only, so the closing fence cannot be cut.
1058
- memoryContext = buildReferenceContext(memoryLines);
1123
+ memoryContext = buildReferenceContext(memoryLines) + '\n\n' + workPackageGuidance;
1059
1124
  // The citation contract — OUTSIDE the fence on purpose: the fence
1060
1125
  // declares its content "background data, not instructions", and
1061
1126
  // this line IS an instruction. One line is the entire write side of
@@ -1150,20 +1215,41 @@ process.stdin.on('end', async () => {
1150
1215
  }
1151
1216
  const updateCache = readUpdateCheckCache(installedVersion);
1152
1217
  let bannerLines = [];
1218
+ let updateConsentContext = null;
1153
1219
  if (installedVersion) {
1154
1220
  const deprecation = buildDeprecationBanner(installedVersion, updateCache);
1155
1221
  if (deprecation.length > 0) {
1156
1222
  bannerLines = deprecation;
1157
1223
  } else {
1158
- bannerLines = buildUpdateAvailableBanner(installedVersion, updateCache,
1159
- () => detectInstallChannelHook(resolvePluginRoot(import.meta.url)));
1224
+ const channel = detectInstallChannelHook(resolvePluginRoot(import.meta.url));
1225
+ const consent = buildUpdateConsentPrompt(data.session_id, installedVersion, updateCache, channel);
1226
+ if (consent) {
1227
+ // The first-use notice is the authoritative update message for
1228
+ // this session. Do not append the softer 24h banner as well; a
1229
+ // source/plugin user would otherwise see two contradictory
1230
+ // update instructions in one SessionStart payload.
1231
+ bannerLines = [consent.system];
1232
+ updateConsentContext = consent.context;
1233
+ } else if (readUpdatePromptClaim(data.session_id, installedVersion, updateCache?.latestVersion)) {
1234
+ // A first-use notice was already shown (and may have been
1235
+ // answered by UserPromptSubmit). Suppress the routine banner for
1236
+ // the rest of this session as well.
1237
+ bannerLines = [];
1238
+ } else {
1239
+ bannerLines = buildUpdateAvailableBanner(installedVersion, updateCache, () => channel);
1240
+ }
1160
1241
  }
1161
1242
  }
1162
1243
  const finalMessage = bannerLines.length > 0
1163
1244
  ? [...bannerLines.filter(l => l.length > 0), '', summary].join('\n')
1164
1245
  : summary;
1165
1246
 
1166
- output(withCaptureWarning(finalMessage), memoryContext);
1247
+ output(withCaptureWarning(finalMessage), updateConsentContext
1248
+ ? `${updateConsentContext}\n\n${memoryContext}`
1249
+ : memoryContext);
1250
+ if (updateConsentContext) {
1251
+ finalizeUpdatePromptClaim(data.session_id, installedVersion, updateCache?.latestVersion);
1252
+ }
1167
1253
 
1168
1254
  // Pre-read the noise-compression throttle on the handle we already
1169
1255
  // hold. compressWeeklyNoise() re-checks under its own connection, but
@@ -1221,9 +1307,9 @@ process.stdin.on('end', async () => {
1221
1307
  } catch (err) {
1222
1308
  // Non-critical — noise compression failed, will retry next session.
1223
1309
  // Trace because this catch previously hid an off-by-one regression
1224
- // in resolvePluginRoot (4.0.4-4.1.0) that silently disabled both
1225
- // noise compression AND LLM failure analysis for three minor
1226
- // releases. A one-line stderr would have surfaced it on day 1.
1310
+ // in resolvePluginRoot (4.0.4-4.1.0) that silently disabled noise
1311
+ // compression for three minor releases. A one-line stderr would have
1312
+ // surfaced it on day 1.
1227
1313
  try { process.stderr.write(`[memesh session-start] noise-compression: ${err?.message || err}\n`); } catch {}
1228
1314
  }
1229
1315
 
@@ -1267,7 +1353,9 @@ process.stdin.on('end', async () => {
1267
1353
  *
1268
1354
  * The shape is asserted by tests/helpers/hook-output-contract.ts.
1269
1355
  */
1270
- function output(text, memoryContext) {
1356
+ const workPackageGuidance = 'Work packages: check work_package prepare for this project (digest or transcript). When available, offer a concise host-native interactive choice in the user’s conversation language: dispatch an agent task, later (defer not_now), or stop suggesting for this session. Never dispatch without the user choosing it. The Dashboard cannot dispatch agents, and no durable opt-out is implied.';
1357
+
1358
+ function output(text, memoryContext = workPackageGuidance) {
1271
1359
  const payload = { systemMessage: text };
1272
1360
  if (memoryContext) {
1273
1361
  payload.hookSpecificOutput = {