@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/mcp.json +6 -4
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +60 -14
- package/README.de.md +83 -556
- package/README.md +83 -582
- package/README.zh-TW.md +84 -573
- package/dashboard/dist/index.html +10 -10
- package/dist/cli/view-live.d.ts.map +1 -1
- package/dist/cli/view-live.js +154 -384
- package/dist/cli/view-live.js.map +1 -1
- package/dist/core/agent-router.d.ts +11 -10
- package/dist/core/agent-router.d.ts.map +1 -1
- package/dist/core/agent-router.js +29 -14
- package/dist/core/agent-router.js.map +1 -1
- package/dist/core/agent-scope-id.d.ts.map +1 -1
- package/dist/core/agent-scope-id.js.map +1 -1
- package/dist/core/analytics.d.ts.map +1 -1
- package/dist/core/analytics.js.map +1 -1
- package/dist/core/config.d.ts +4 -40
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +75 -141
- package/dist/core/config.js.map +1 -1
- package/dist/core/demo.d.ts.map +1 -1
- package/dist/core/demo.js +6 -6
- package/dist/core/demo.js.map +1 -1
- package/dist/core/doctor-fixes.d.ts +19 -0
- package/dist/core/doctor-fixes.d.ts.map +1 -0
- package/dist/core/doctor-fixes.js +104 -0
- package/dist/core/doctor-fixes.js.map +1 -0
- package/dist/core/doctor.d.ts +2 -7
- package/dist/core/doctor.d.ts.map +1 -1
- package/dist/core/doctor.js +57 -229
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/dreamer.d.ts +32 -47
- package/dist/core/dreamer.d.ts.map +1 -1
- package/dist/core/dreamer.js +211 -703
- package/dist/core/dreamer.js.map +1 -1
- package/dist/core/install-channel.d.ts.map +1 -1
- package/dist/core/install-channel.js +4 -47
- package/dist/core/install-channel.js.map +1 -1
- package/dist/core/install-id.d.ts.map +1 -1
- package/dist/core/install-id.js.map +1 -1
- package/dist/core/kg-backfill.d.ts.map +1 -1
- package/dist/core/kg-backfill.js.map +1 -1
- package/dist/core/lesson-engine.d.ts +0 -5
- package/dist/core/lesson-engine.d.ts.map +1 -1
- package/dist/core/lesson-engine.js +0 -25
- package/dist/core/lesson-engine.js.map +1 -1
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +54 -48
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core/memory-tool.d.ts.map +1 -1
- package/dist/core/memory-tool.js +17 -17
- package/dist/core/memory-tool.js.map +1 -1
- package/dist/core/operations.d.ts +2 -26
- package/dist/core/operations.d.ts.map +1 -1
- package/dist/core/operations.js +11 -246
- package/dist/core/operations.js.map +1 -1
- package/dist/core/paths.d.ts +4 -1
- package/dist/core/paths.d.ts.map +1 -1
- package/dist/core/paths.js +88 -14
- package/dist/core/paths.js.map +1 -1
- package/dist/core/product-improvements.js +2 -2
- package/dist/core/product-improvements.js.map +1 -1
- package/dist/core/schema-export.d.ts.map +1 -1
- package/dist/core/schema-export.js +11 -1
- package/dist/core/schema-export.js.map +1 -1
- package/dist/core/semver.d.ts +7 -0
- package/dist/core/semver.d.ts.map +1 -0
- package/dist/core/semver.js +49 -0
- package/dist/core/semver.js.map +1 -0
- package/dist/core/serializer.d.ts.map +1 -1
- package/dist/core/serializer.js +69 -57
- package/dist/core/serializer.js.map +1 -1
- package/dist/core/signal-scorer.d.ts.map +1 -1
- package/dist/core/signal-scorer.js.map +1 -1
- package/dist/core/transcript-extractor.d.ts +1 -85
- package/dist/core/transcript-extractor.d.ts.map +1 -1
- package/dist/core/transcript-extractor.js +5 -364
- package/dist/core/transcript-extractor.js.map +1 -1
- package/dist/core/transcript-source.d.ts +22 -6
- package/dist/core/transcript-source.d.ts.map +1 -1
- package/dist/core/transcript-source.js +108 -69
- package/dist/core/transcript-source.js.map +1 -1
- package/dist/core/types.d.ts +1 -17
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/version-check.d.ts.map +1 -1
- package/dist/core/version-check.js +11 -1
- package/dist/core/version-check.js.map +1 -1
- package/dist/db.d.ts +0 -34
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +3 -287
- package/dist/db.js.map +1 -1
- package/dist/host-runtime/acp.d.ts.map +1 -1
- package/dist/host-runtime/acp.js +2 -2
- package/dist/host-runtime/acp.js.map +1 -1
- package/dist/host-runtime/claude.d.ts.map +1 -1
- package/dist/host-runtime/claude.js +9 -7
- package/dist/host-runtime/claude.js.map +1 -1
- package/dist/host-runtime/codex-session.d.ts +9 -1
- package/dist/host-runtime/codex-session.d.ts.map +1 -1
- package/dist/host-runtime/codex-session.js +474 -29
- package/dist/host-runtime/codex-session.js.map +1 -1
- package/dist/host-runtime/codex.d.ts.map +1 -1
- package/dist/host-runtime/codex.js +2 -2
- package/dist/host-runtime/codex.js.map +1 -1
- package/dist/host-runtime/config.d.ts +1 -0
- package/dist/host-runtime/config.d.ts.map +1 -1
- package/dist/host-runtime/config.js +4 -0
- package/dist/host-runtime/config.js.map +1 -1
- package/dist/host-runtime/router-client.d.ts.map +1 -1
- package/dist/host-runtime/router-client.js +62 -15
- package/dist/host-runtime/router-client.js.map +1 -1
- package/dist/host-runtime/router.js +2 -2
- package/dist/host-runtime/router.js.map +1 -1
- package/dist/knowledge-graph.d.ts.map +1 -1
- package/dist/knowledge-graph.js +84 -55
- package/dist/knowledge-graph.js.map +1 -1
- package/dist/mcp/THIRD_PARTY_NOTICES.txt +217 -0
- package/dist/mcp/server.js +30714 -38
- package/dist/mcp/server.js.map +6 -1
- package/dist/skills-manifest.json +24 -24
- package/dist/storage/entity-index.d.ts +0 -1
- package/dist/storage/entity-index.d.ts.map +1 -1
- package/dist/storage/entity-index.js +0 -7
- package/dist/storage/entity-index.js.map +1 -1
- package/dist/storage/fts-index.d.ts.map +1 -1
- package/dist/storage/fts-index.js +6 -7
- package/dist/storage/fts-index.js.map +1 -1
- package/dist/storage/graph-repairs.d.ts +0 -5
- package/dist/storage/graph-repairs.d.ts.map +1 -1
- package/dist/storage/graph-repairs.js +4 -91
- package/dist/storage/graph-repairs.js.map +1 -1
- package/dist/storage/schema.d.ts +1 -1
- package/dist/storage/schema.d.ts.map +1 -1
- package/dist/storage/schema.js +1 -2
- package/dist/storage/schema.js.map +1 -1
- package/dist/storage/sqlite.d.ts +0 -1
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/transports/agent-messaging.d.ts +4 -0
- package/dist/transports/agent-messaging.d.ts.map +1 -1
- package/dist/transports/agent-messaging.js +16 -8
- package/dist/transports/agent-messaging.js.map +1 -1
- package/dist/transports/cli/cli.d.ts.map +1 -1
- package/dist/transports/cli/cli.js +60751 -2408
- package/dist/transports/cli/cli.js.map +6 -1
- package/dist/transports/http/retired-routes.js +1 -1
- package/dist/transports/http/retired-routes.js.map +1 -1
- package/dist/transports/http/server.d.ts.map +1 -1
- package/dist/transports/http/server.js +45 -233
- package/dist/transports/http/server.js.map +1 -1
- package/dist/transports/mcp/handlers.d.ts +130 -4
- package/dist/transports/mcp/handlers.d.ts.map +1 -1
- package/dist/transports/mcp/handlers.js +56 -7
- package/dist/transports/mcp/handlers.js.map +1 -1
- package/dist/transports/schemas.d.ts +52 -0
- package/dist/transports/schemas.d.ts.map +1 -1
- package/dist/transports/schemas.js +39 -0
- package/dist/transports/schemas.js.map +1 -1
- package/docs/platforms/README.md +5 -4
- package/docs/platforms/agent-messaging.md +134 -61
- package/hooks/hooks.json +13 -2
- package/llms-install.md +62 -30
- package/package.json +4 -5
- package/scripts/hooks/_generated/core-paths.js +88 -14
- package/scripts/hooks/_generated/fts-index.js +6 -7
- package/scripts/hooks/_generated/schema.js +1 -2
- package/scripts/hooks/_shared.js +164 -21
- package/scripts/hooks/session-start.js +98 -10
- package/scripts/hooks/session-summary.js +34 -376
- package/scripts/hooks/user-prompt-intent.js +71 -11
- package/skills/memesh/SKILL.md +19 -14
- package/skills/memesh-review/SKILL.md +7 -6
- package/dist/core/auto-tagger.d.ts +0 -10
- package/dist/core/auto-tagger.d.ts.map +0 -1
- package/dist/core/auto-tagger.js +0 -63
- package/dist/core/auto-tagger.js.map +0 -1
- package/dist/core/conflict-candidates.d.ts +0 -20
- package/dist/core/conflict-candidates.d.ts.map +0 -1
- package/dist/core/conflict-candidates.js +0 -71
- package/dist/core/conflict-candidates.js.map +0 -1
- package/dist/core/conflict-judge.d.ts +0 -58
- package/dist/core/conflict-judge.d.ts.map +0 -1
- package/dist/core/conflict-judge.js +0 -189
- package/dist/core/conflict-judge.js.map +0 -1
- package/dist/core/digest-validator.d.ts +0 -18
- package/dist/core/digest-validator.d.ts.map +0 -1
- package/dist/core/digest-validator.js +0 -85
- package/dist/core/digest-validator.js.map +0 -1
- package/dist/core/embedder.d.ts +0 -20
- package/dist/core/embedder.d.ts.map +0 -1
- package/dist/core/embedder.js +0 -245
- package/dist/core/embedder.js.map +0 -1
- package/dist/core/failure-analyzer.d.ts +0 -19
- package/dist/core/failure-analyzer.d.ts.map +0 -1
- package/dist/core/failure-analyzer.js +0 -83
- package/dist/core/failure-analyzer.js.map +0 -1
- package/dist/core/json-utils.d.ts +0 -3
- package/dist/core/json-utils.d.ts.map +0 -1
- package/dist/core/json-utils.js +0 -46
- package/dist/core/json-utils.js.map +0 -1
- package/dist/core/llm-client.d.ts +0 -22
- package/dist/core/llm-client.d.ts.map +0 -1
- package/dist/core/llm-client.js +0 -203
- package/dist/core/llm-client.js.map +0 -1
- package/dist/core/llm-telemetry.d.ts +0 -47
- package/dist/core/llm-telemetry.d.ts.map +0 -1
- package/dist/core/llm-telemetry.js +0 -117
- package/dist/core/llm-telemetry.js.map +0 -1
- package/dist/core/llm-validator.d.ts +0 -20
- package/dist/core/llm-validator.d.ts.map +0 -1
- package/dist/core/llm-validator.js +0 -231
- package/dist/core/llm-validator.js.map +0 -1
- package/dist/core/ollama-host.d.ts +0 -6
- package/dist/core/ollama-host.d.ts.map +0 -1
- package/dist/core/ollama-host.js +0 -62
- package/dist/core/ollama-host.js.map +0 -1
- package/dist/core/output-language.d.ts +0 -6
- package/dist/core/output-language.d.ts.map +0 -1
- package/dist/core/output-language.js +0 -25
- package/dist/core/output-language.js.map +0 -1
- package/dist/core/prompt-safety.d.ts +0 -4
- package/dist/core/prompt-safety.d.ts.map +0 -1
- package/dist/core/prompt-safety.js +0 -20
- package/dist/core/prompt-safety.js.map +0 -1
- package/dist/storage/vector-index.d.ts +0 -3
- package/dist/storage/vector-index.d.ts.map +0 -1
- package/dist/storage/vector-index.js +0 -13
- 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
|
|
48
|
-
if (
|
|
49
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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(
|
|
74
|
+
real = fs.realpathSync.native(localPath);
|
|
57
75
|
}
|
|
58
76
|
catch {
|
|
59
|
-
real = path.resolve(
|
|
77
|
+
real = path.resolve(localPath);
|
|
60
78
|
}
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
79
|
-
const
|
|
80
|
-
if (!
|
|
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
|
|
83
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
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
|
package/scripts/hooks/_shared.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
|
137
|
-
*
|
|
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
|
-
* —
|
|
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
|
-
|
|
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`,
|
|
376
|
-
//
|
|
377
|
-
//
|
|
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.
|
|
563
|
-
*
|
|
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
|
|
605
|
-
//
|
|
606
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
1159
|
-
|
|
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),
|
|
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
|
|
1225
|
-
//
|
|
1226
|
-
//
|
|
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
|
-
|
|
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 = {
|