@pcircle/memesh 4.0.3 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.de.md +227 -53
- package/README.es.md +230 -56
- package/README.fr.md +230 -56
- package/README.ja.md +229 -55
- package/README.ko.md +230 -56
- package/README.md +54 -3
- package/README.pt.md +230 -56
- package/README.th.md +230 -56
- package/README.vi.md +228 -54
- package/README.zh-CN.md +230 -56
- package/README.zh-TW.md +228 -54
- package/dashboard/dist/index.html +3 -3
- package/dist/core/analytics.d.ts +53 -0
- package/dist/core/analytics.d.ts.map +1 -0
- package/dist/core/analytics.js +127 -0
- package/dist/core/analytics.js.map +1 -0
- package/dist/core/auto-tagger.d.ts.map +1 -1
- package/dist/core/auto-tagger.js +14 -54
- package/dist/core/auto-tagger.js.map +1 -1
- package/dist/core/config.d.ts +1 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +6 -10
- package/dist/core/config.js.map +1 -1
- package/dist/core/consolidator.d.ts.map +1 -1
- package/dist/core/consolidator.js +11 -61
- package/dist/core/consolidator.js.map +1 -1
- package/dist/core/doctor.d.ts +40 -0
- package/dist/core/doctor.d.ts.map +1 -0
- package/dist/core/doctor.js +264 -0
- package/dist/core/doctor.js.map +1 -0
- package/dist/core/embedder.js.map +1 -1
- package/dist/core/failure-analyzer.d.ts +2 -1
- package/dist/core/failure-analyzer.d.ts.map +1 -1
- package/dist/core/failure-analyzer.js +14 -47
- package/dist/core/failure-analyzer.js.map +1 -1
- package/dist/core/graph.d.ts +13 -0
- package/dist/core/graph.d.ts.map +1 -0
- package/dist/core/graph.js +13 -0
- package/dist/core/graph.js.map +1 -0
- package/dist/core/lesson-engine.d.ts +2 -1
- package/dist/core/lesson-engine.d.ts.map +1 -1
- package/dist/core/lesson-engine.js +2 -0
- package/dist/core/lesson-engine.js.map +1 -1
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +2 -1
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core/llm-client.d.ts +6 -0
- package/dist/core/llm-client.d.ts.map +1 -0
- package/dist/core/llm-client.js +64 -0
- package/dist/core/llm-client.js.map +1 -0
- package/dist/core/operations.d.ts +0 -1
- package/dist/core/operations.d.ts.map +1 -1
- package/dist/core/operations.js +40 -61
- package/dist/core/operations.js.map +1 -1
- package/dist/core/prompt-safety.d.ts +3 -0
- package/dist/core/prompt-safety.d.ts.map +1 -0
- package/dist/core/prompt-safety.js +13 -0
- package/dist/core/prompt-safety.js.map +1 -0
- package/dist/core/query-expander.d.ts.map +1 -1
- package/dist/core/query-expander.js +11 -72
- package/dist/core/query-expander.js.map +1 -1
- package/dist/core/schema-export.d.ts.map +1 -1
- package/dist/core/schema-export.js +72 -0
- package/dist/core/schema-export.js.map +1 -1
- package/dist/core/skill-usage-log.d.ts +11 -0
- package/dist/core/skill-usage-log.d.ts.map +1 -0
- package/dist/core/skill-usage-log.js +125 -0
- package/dist/core/skill-usage-log.js.map +1 -0
- package/dist/core/stats.d.ts +12 -0
- package/dist/core/stats.d.ts.map +1 -0
- package/dist/core/stats.js +19 -0
- package/dist/core/stats.js.map +1 -0
- package/dist/core/types.d.ts +9 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/verifier.d.ts +37 -0
- package/dist/core/verifier.d.ts.map +1 -0
- package/dist/core/verifier.js +195 -0
- package/dist/core/verifier.js.map +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +14 -0
- package/dist/db.js.map +1 -1
- package/dist/knowledge-graph.d.ts.map +1 -1
- package/dist/knowledge-graph.js +8 -43
- package/dist/knowledge-graph.js.map +1 -1
- package/dist/skills-manifest.json +71 -0
- package/dist/storage/conflicts.d.ts +4 -0
- package/dist/storage/conflicts.d.ts.map +1 -0
- package/dist/storage/conflicts.js +32 -0
- package/dist/storage/conflicts.js.map +1 -0
- package/dist/storage/fts-index.d.ts +4 -0
- package/dist/storage/fts-index.d.ts.map +1 -0
- package/dist/storage/fts-index.js +18 -0
- package/dist/storage/fts-index.js.map +1 -0
- package/dist/transports/cli/cli.js +203 -69
- package/dist/transports/cli/cli.js.map +1 -1
- package/dist/transports/http/server.d.ts +1 -0
- package/dist/transports/http/server.d.ts.map +1 -1
- package/dist/transports/http/server.js +138 -231
- package/dist/transports/http/server.js.map +1 -1
- package/dist/transports/mcp/handlers.d.ts +93 -0
- package/dist/transports/mcp/handlers.d.ts.map +1 -1
- package/dist/transports/mcp/handlers.js +51 -1
- package/dist/transports/mcp/handlers.js.map +1 -1
- package/dist/transports/schemas.d.ts +28 -0
- package/dist/transports/schemas.d.ts.map +1 -1
- package/dist/transports/schemas.js +25 -0
- package/dist/transports/schemas.js.map +1 -1
- package/hooks/hooks.json +10 -0
- package/package.json +5 -3
- package/plugin.json +1 -1
- package/scripts/hooks/_shared.js +207 -1
- package/scripts/hooks/post-commit.js +5 -76
- package/scripts/hooks/pre-bash-orchestration-nudge.js +150 -0
- package/scripts/hooks/pre-compact.js +8 -76
- package/scripts/hooks/pre-edit-recall.js +0 -0
- package/scripts/hooks/session-start.js +69 -8
- package/scripts/hooks/session-summary.js +12 -81
- package/skills/agentic-orchestration/SKILL.md +399 -0
package/scripts/hooks/_shared.js
CHANGED
|
@@ -1,6 +1,212 @@
|
|
|
1
|
-
import { chmodSync, mkdirSync, writeFileSync } from 'fs';
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { createRequire } from 'module';
|
|
2
3
|
import { homedir } from 'os';
|
|
3
4
|
import { dirname, join } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the package root from a hook file's `import.meta.url`.
|
|
11
|
+
*
|
|
12
|
+
* Hooks live at `<pkgRoot>/scripts/hooks/<file>.js`, so the path needs
|
|
13
|
+
* three `dirname()` hops to reach `<pkgRoot>`. Centralising this here
|
|
14
|
+
* prevents the off-by-one regression that silently disabled noise
|
|
15
|
+
* compression and LLM failure analysis between 4.0.4–4.1.0 (the inline
|
|
16
|
+
* `dirname(dirname(...))` only reached `<pkgRoot>/scripts`, and the
|
|
17
|
+
* surrounding `catch` swallowed the resulting ENOENT).
|
|
18
|
+
*
|
|
19
|
+
* The path is derived strictly from the caller's URL — there is no env
|
|
20
|
+
* override — so a malicious project's `.envrc` cannot redirect dynamic
|
|
21
|
+
* imports to attacker-controlled code (the F5 boundary).
|
|
22
|
+
*
|
|
23
|
+
* @param {string} metaUrl - typically `import.meta.url` of the caller
|
|
24
|
+
* @returns {string} absolute path to the package root
|
|
25
|
+
*/
|
|
26
|
+
export function resolvePluginRoot(metaUrl) {
|
|
27
|
+
return dirname(dirname(dirname(fileURLToPath(metaUrl))));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Read ~/.memesh/config.json directly. Hooks must not depend on dist/
|
|
32
|
+
* (F5 boundary), so this reads the JSON as a plain file rather than
|
|
33
|
+
* importing readConfig from src/core/config.ts.
|
|
34
|
+
*
|
|
35
|
+
* Always reads `~/.memesh/config.json` to stay consistent with
|
|
36
|
+
* `src/core/config.ts`, which is the single writer. Earlier versions
|
|
37
|
+
* read `dirname(MEMESH_DB_PATH)/config.json`, which silently diverged
|
|
38
|
+
* from the CLI-managed config any time a custom DB path was set: hooks
|
|
39
|
+
* would ignore `memesh config set autoCapture …` and friends. Fixed
|
|
40
|
+
* by treating the homedir path as the canonical source.
|
|
41
|
+
*
|
|
42
|
+
* Returns an empty object on missing/unreadable/malformed file —
|
|
43
|
+
* callers must always be defensive about which fields are set.
|
|
44
|
+
*
|
|
45
|
+
* @param {NodeJS.ProcessEnv} [_env=process.env] - kept for signature
|
|
46
|
+
* compatibility (env was the prior MEMESH_DB_PATH source); ignored.
|
|
47
|
+
* @returns {Record<string, any>}
|
|
48
|
+
*/
|
|
49
|
+
export function readHookConfig(_env = process.env) {
|
|
50
|
+
const path = join(homedir(), '.memesh', 'config.json');
|
|
51
|
+
if (!existsSync(path)) return {};
|
|
52
|
+
try {
|
|
53
|
+
const raw = readFileSync(path, 'utf8');
|
|
54
|
+
const parsed = JSON.parse(raw);
|
|
55
|
+
return (parsed && typeof parsed === 'object') ? parsed : {};
|
|
56
|
+
} catch {
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Resolve the agentic-orchestration opt-in flag.
|
|
63
|
+
* Precedence: env > config > default(false).
|
|
64
|
+
* Env semantics preserved: only `=== '1'` enables (avoids accidental
|
|
65
|
+
* truthy unlock from a stray env value).
|
|
66
|
+
*
|
|
67
|
+
* @param {NodeJS.ProcessEnv} [env=process.env]
|
|
68
|
+
* @returns {boolean}
|
|
69
|
+
*/
|
|
70
|
+
export function isAgenticOrchestrationEnabled(env = process.env) {
|
|
71
|
+
const envVal = env.MEMESH_ENABLE_AGENTIC_ORCHESTRATION;
|
|
72
|
+
if (envVal !== undefined) return envVal === '1';
|
|
73
|
+
const cfg = readHookConfig(env);
|
|
74
|
+
return cfg.enableAgenticOrchestration === true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Resolve the auto-capture flag.
|
|
79
|
+
* Precedence: env > config > default(true).
|
|
80
|
+
* Env semantics preserved: explicit `=== 'false'` disables; any other
|
|
81
|
+
* value (including undefined) leaves it on or defers to config.
|
|
82
|
+
*
|
|
83
|
+
* @param {NodeJS.ProcessEnv} [env=process.env]
|
|
84
|
+
* @returns {boolean}
|
|
85
|
+
*/
|
|
86
|
+
export function isAutoCaptureEnabled(env = process.env) {
|
|
87
|
+
const envVal = env.MEMESH_AUTO_CAPTURE;
|
|
88
|
+
if (envVal === 'false') return false;
|
|
89
|
+
if (envVal === 'true') return true;
|
|
90
|
+
// env unset or other value — fall through to config
|
|
91
|
+
const cfg = readHookConfig(env);
|
|
92
|
+
if (cfg.autoCapture === false) return false;
|
|
93
|
+
return true; // default
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolve the session-start memory-injection top-N limit.
|
|
98
|
+
* Precedence: env > config > default(10).
|
|
99
|
+
* @param {NodeJS.ProcessEnv} [env=process.env]
|
|
100
|
+
* @returns {number}
|
|
101
|
+
*/
|
|
102
|
+
export function resolveSessionLimit(env = process.env) {
|
|
103
|
+
const envVal = env.MEMESH_SESSION_LIMIT;
|
|
104
|
+
if (envVal !== undefined) {
|
|
105
|
+
const n = parseInt(envVal, 10);
|
|
106
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
107
|
+
}
|
|
108
|
+
const cfg = readHookConfig(env);
|
|
109
|
+
if (typeof cfg.sessionLimit === 'number' && cfg.sessionLimit > 0) {
|
|
110
|
+
return cfg.sessionLimit;
|
|
111
|
+
}
|
|
112
|
+
return 10;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Canonical SQLite schema for hook-written entities. Mirrors src/db.ts.
|
|
116
|
+
// Hooks must NOT depend on dist/ (F5 security boundary), so this is a
|
|
117
|
+
// duplicate string by necessity. When src/db.ts changes, this must
|
|
118
|
+
// change in lockstep.
|
|
119
|
+
export const SCHEMA_SQL = `
|
|
120
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
121
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
122
|
+
name TEXT NOT NULL UNIQUE,
|
|
123
|
+
type TEXT NOT NULL,
|
|
124
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
125
|
+
metadata JSON
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
CREATE TABLE IF NOT EXISTS observations (
|
|
129
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
130
|
+
entity_id INTEGER NOT NULL,
|
|
131
|
+
content TEXT NOT NULL,
|
|
132
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
133
|
+
FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
CREATE TABLE IF NOT EXISTS relations (
|
|
137
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
138
|
+
from_entity_id INTEGER NOT NULL,
|
|
139
|
+
to_entity_id INTEGER NOT NULL,
|
|
140
|
+
relation_type TEXT NOT NULL,
|
|
141
|
+
metadata JSON,
|
|
142
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
143
|
+
FOREIGN KEY (from_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
|
|
144
|
+
FOREIGN KEY (to_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
|
|
145
|
+
UNIQUE(from_entity_id, to_entity_id, relation_type)
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
CREATE TABLE IF NOT EXISTS tags (
|
|
149
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
150
|
+
entity_id INTEGER NOT NULL,
|
|
151
|
+
tag TEXT NOT NULL,
|
|
152
|
+
FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
CREATE INDEX IF NOT EXISTS idx_tags_entity ON tags(entity_id);
|
|
156
|
+
CREATE INDEX IF NOT EXISTS idx_tags_tag ON tags(tag);
|
|
157
|
+
DELETE FROM tags
|
|
158
|
+
WHERE id NOT IN (
|
|
159
|
+
SELECT MIN(id)
|
|
160
|
+
FROM tags
|
|
161
|
+
GROUP BY entity_id, tag
|
|
162
|
+
);
|
|
163
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_tags_entity_tag_unique ON tags(entity_id, tag);
|
|
164
|
+
CREATE INDEX IF NOT EXISTS idx_observations_entity ON observations(entity_id);
|
|
165
|
+
CREATE INDEX IF NOT EXISTS idx_relations_from ON relations(from_entity_id);
|
|
166
|
+
CREATE INDEX IF NOT EXISTS idx_relations_to ON relations(to_entity_id);
|
|
167
|
+
`;
|
|
168
|
+
|
|
169
|
+
// FTS5 virtual table — separate so hooks that don't need it stay lean.
|
|
170
|
+
export const FTS_SQL = `
|
|
171
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
|
|
172
|
+
name, observations, content='',
|
|
173
|
+
tokenize='unicode61 remove_diacritics 1'
|
|
174
|
+
);
|
|
175
|
+
`;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Open the hook-side memesh DB with schema + status migration applied.
|
|
179
|
+
*
|
|
180
|
+
* Two of three hooks (post-commit, pre-compact) previously inlined the
|
|
181
|
+
* same SCHEMA_SQL but skipped the v2.11→v2.12 status migration that
|
|
182
|
+
* session-summary.js ran. This unified helper closes the drift gap so
|
|
183
|
+
* every hook converges on the same shape regardless of order.
|
|
184
|
+
*
|
|
185
|
+
* @param {NodeJS.ProcessEnv} [env=process.env]
|
|
186
|
+
* @param {object} [opts]
|
|
187
|
+
* @param {boolean} [opts.fts=false] - Also create the FTS5 virtual table.
|
|
188
|
+
* @returns {{ db: any, dbPath: string }}
|
|
189
|
+
*/
|
|
190
|
+
export function openHookDb(env = process.env, opts = {}) {
|
|
191
|
+
const dbPath = env.MEMESH_DB_PATH || join(homedir(), '.memesh', 'knowledge-graph.db');
|
|
192
|
+
const dbDir = env.MEMESH_DB_PATH ? dirname(env.MEMESH_DB_PATH) : join(homedir(), '.memesh');
|
|
193
|
+
if (!existsSync(dbDir)) mkdirSync(dbDir, { recursive: true });
|
|
194
|
+
|
|
195
|
+
const Database = require('better-sqlite3');
|
|
196
|
+
const db = new Database(dbPath);
|
|
197
|
+
db.pragma('journal_mode = WAL');
|
|
198
|
+
db.pragma('foreign_keys = ON');
|
|
199
|
+
db.exec(SCHEMA_SQL);
|
|
200
|
+
if (opts.fts) db.exec(FTS_SQL);
|
|
201
|
+
|
|
202
|
+
const cols = db.prepare("PRAGMA table_info(entities)").all();
|
|
203
|
+
if (!cols.some((c) => c.name === 'status')) {
|
|
204
|
+
db.exec("ALTER TABLE entities ADD COLUMN status TEXT NOT NULL DEFAULT 'active'");
|
|
205
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_entities_status ON entities(status)");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return { db, dbPath };
|
|
209
|
+
}
|
|
4
210
|
|
|
5
211
|
const PRIVATE_DIR_MODE = 0o700;
|
|
6
212
|
const PRIVATE_FILE_MODE = 0o600;
|
|
@@ -1,67 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { createRequire } from 'module';
|
|
4
3
|
import { execFileSync } from 'child_process';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { existsSync, mkdirSync } from 'fs';
|
|
8
|
-
|
|
9
|
-
const require = createRequire(import.meta.url);
|
|
10
|
-
|
|
11
|
-
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
|
-
DELETE FROM tags
|
|
50
|
-
WHERE id NOT IN (
|
|
51
|
-
SELECT MIN(id)
|
|
52
|
-
FROM tags
|
|
53
|
-
GROUP BY entity_id, tag
|
|
54
|
-
);
|
|
55
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_tags_entity_tag_unique ON tags(entity_id, tag);
|
|
56
|
-
CREATE INDEX IF NOT EXISTS idx_observations_entity ON observations(entity_id);
|
|
57
|
-
CREATE INDEX IF NOT EXISTS idx_relations_from ON relations(from_entity_id);
|
|
58
|
-
CREATE INDEX IF NOT EXISTS idx_relations_to ON relations(to_entity_id);
|
|
59
|
-
|
|
60
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
|
|
61
|
-
name, observations, content='',
|
|
62
|
-
tokenize='unicode61 remove_diacritics 1'
|
|
63
|
-
);
|
|
64
|
-
`;
|
|
4
|
+
import { basename } from 'path';
|
|
5
|
+
import { openHookDb } from './_shared.js';
|
|
65
6
|
|
|
66
7
|
let input = '';
|
|
67
8
|
process.stdin.setEncoding('utf8');
|
|
@@ -88,22 +29,10 @@ process.stdin.on('end', () => {
|
|
|
88
29
|
const commitMsg = commitMatch[2];
|
|
89
30
|
const projectName = basename(data.cwd || process.cwd());
|
|
90
31
|
|
|
91
|
-
// Open
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
? join(process.env.MEMESH_DB_PATH, '..')
|
|
95
|
-
: join(homedir(), '.memesh');
|
|
96
|
-
if (!existsSync(dbDir)) mkdirSync(dbDir, { recursive: true });
|
|
97
|
-
|
|
98
|
-
const Database = require('better-sqlite3');
|
|
99
|
-
const db = new Database(dbPath);
|
|
32
|
+
// Open DB via shared helper — applies SCHEMA_SQL + status migration.
|
|
33
|
+
// Pass fts:true so the FTS5 entity-search index is also available.
|
|
34
|
+
const { db } = openHookDb(process.env, { fts: true });
|
|
100
35
|
try {
|
|
101
|
-
db.pragma('journal_mode = WAL');
|
|
102
|
-
db.pragma('foreign_keys = ON');
|
|
103
|
-
|
|
104
|
-
// Ensure schema exists
|
|
105
|
-
db.exec(SCHEMA_SQL);
|
|
106
|
-
|
|
107
36
|
const entityName = `commit-${commitHash}`;
|
|
108
37
|
|
|
109
38
|
// Check if this is a new or existing entity
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Orchestration Nudge — PreToolUse hook for Bash
|
|
4
|
+
// When Claude is about to run a high-verifiability command (tests, builds, lint,
|
|
5
|
+
// deploys, etc.), injects a one-time-per-category advisory hint to consider
|
|
6
|
+
// dispatching it as a background agent instead of blocking the conversation.
|
|
7
|
+
//
|
|
8
|
+
// Throttle: one nudge per category per session. State is one marker file per
|
|
9
|
+
// category in ${memeshDir}/agent-nudge-flags/${category}.flag. The flag IS
|
|
10
|
+
// the lock — created with O_EXCL so concurrent hooks racing on the same
|
|
11
|
+
// category resolve atomically (winner emits the nudge, loser silently passes).
|
|
12
|
+
// session-start.js clears the directory at the start of each session.
|
|
13
|
+
|
|
14
|
+
import { join } from 'path';
|
|
15
|
+
import { openSync, closeSync } from 'fs';
|
|
16
|
+
import { ensurePrivateDir, getMemeshDir, isAgenticOrchestrationEnabled } from './_shared.js';
|
|
17
|
+
|
|
18
|
+
const memeshDir = getMemeshDir(process.env);
|
|
19
|
+
const FLAGS_DIR = join(memeshDir, 'agent-nudge-flags');
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Pattern definitions
|
|
23
|
+
// Each entry has:
|
|
24
|
+
// category: string key used for per-session throttle dedup
|
|
25
|
+
// test: RegExp that must match somewhere in the command (case-insensitive)
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
const PATTERNS = [
|
|
28
|
+
{
|
|
29
|
+
category: 'test',
|
|
30
|
+
test: /\bnpm\s+(?:run\s+)?test\b|\bvitest\b|\bjest\b|\bpytest\b|\bgo\s+test\b|\bcargo\s+test\b|\bmocha\b|\bava\b/i,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
category: 'build',
|
|
34
|
+
test: /\bnpm\s+run\s+build\b|\btsc\b|\bnext\s+build\b|\bnest\s+build\b|\bcargo\s+build\b|\bvite\s+build\b|\bwebpack\b/i,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
category: 'lint',
|
|
38
|
+
test: /\bnpm\s+run\s+lint\b|\beslint\b|\bprettier\b(?!\s+--version|\s+-v)|\btsc\s+--noEmit\b|\bbiome\b/i,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
category: 'migrate',
|
|
42
|
+
test: /\bprisma\s+(?:migrate|db\s+push)\b|\bknex\s+migrate\b|\bsequelize\s+db:migrate\b/i,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
category: 'benchmark',
|
|
46
|
+
test: /\bnpm\s+run\s+bench\b|\bbenchmark\b/i,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
category: 'deploy',
|
|
50
|
+
test: /\bvercel\s+(?:deploy|--prod)\b|\bgh\s+(?:workflow\s+run|run)\b|\bwrangler\s+deploy\b|\bfly\s+deploy\b/i,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
category: 'npm-run-check',
|
|
54
|
+
// Generic: `npm run <script>` where the script name suggests slow CI-like work
|
|
55
|
+
test: /\bnpm\s+run\s+(?:check|typecheck|type-check|ci|e2e|integration|coverage|storybook|compile)\b/i,
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
// Commands that are definitely NOT long-running, even if they somehow matched
|
|
60
|
+
// a pattern above. Short-circuit exclusions — if the command matches any of
|
|
61
|
+
// these, skip the nudge.
|
|
62
|
+
//
|
|
63
|
+
// Note: short-form `-v` was intentionally removed. In test runners (pytest,
|
|
64
|
+
// go test, cargo test, bun test) `-v` is the *verbose* flag, and those
|
|
65
|
+
// invocations are genuinely long-running. `node -v` / `npm -v` style
|
|
66
|
+
// version checks don't reach this exclusion list anyway because they
|
|
67
|
+
// never match any PATTERN entry above.
|
|
68
|
+
const NOISE_EXCLUSIONS = /--version\b|--help\b|\b-h\b|\bls\b|\bcat\b|\becho\b|\bpwd\b|\bwhich\b|\bwhere\b/i;
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Main
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
let input = '';
|
|
74
|
+
process.stdin.setEncoding('utf8');
|
|
75
|
+
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
76
|
+
process.stdin.on('end', () => {
|
|
77
|
+
try {
|
|
78
|
+
// Opt-in gate: the agentic-orchestration protocol is a separable
|
|
79
|
+
// experiment from memesh's core memory features. Enable with
|
|
80
|
+
// MEMESH_ENABLE_AGENTIC_ORCHESTRATION=1. The default is OFF so users
|
|
81
|
+
// who installed memesh purely for memory don't see protocol nudges
|
|
82
|
+
// they never asked for.
|
|
83
|
+
if (!isAgenticOrchestrationEnabled(process.env)) return pass();
|
|
84
|
+
|
|
85
|
+
const data = JSON.parse(input);
|
|
86
|
+
const command = (data.tool_input?.command ?? '').trim();
|
|
87
|
+
|
|
88
|
+
if (!command) return pass();
|
|
89
|
+
|
|
90
|
+
// Short-circuit: known noise commands never warrant a nudge
|
|
91
|
+
if (NOISE_EXCLUSIONS.test(command)) return pass();
|
|
92
|
+
|
|
93
|
+
// Find the first matching category
|
|
94
|
+
const match = PATTERNS.find((p) => p.test.test(command));
|
|
95
|
+
if (!match) return pass();
|
|
96
|
+
|
|
97
|
+
// Throttle: only nudge once per category per session.
|
|
98
|
+
//
|
|
99
|
+
// Per-category marker file with O_EXCL atomic create. The flag IS the
|
|
100
|
+
// lock — there is no shared state to read-modify-write, so concurrent
|
|
101
|
+
// hooks for *different* categories cannot clobber each other's marker
|
|
102
|
+
// (the previous shared-JSON design lost one of two parallel writes).
|
|
103
|
+
// session-start.js wipes ${FLAGS_DIR} at the top of each new session.
|
|
104
|
+
//
|
|
105
|
+
// No pre-check (existsSync) before the open — that would be a TOCTOU
|
|
106
|
+
// window. The openSync('wx') is itself the atomic check-and-create:
|
|
107
|
+
// EEXIST means already-throttled, success means we won the race.
|
|
108
|
+
const flagPath = join(FLAGS_DIR, `${match.category}.flag`);
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
ensurePrivateDir(FLAGS_DIR);
|
|
112
|
+
const fd = openSync(flagPath, 'wx', 0o600);
|
|
113
|
+
closeSync(fd);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
if (err && err.code === 'EEXIST') {
|
|
116
|
+
// Marker already exists — this category was already nudged in this
|
|
117
|
+
// session (either by an earlier hook invocation or a parallel one).
|
|
118
|
+
return pass();
|
|
119
|
+
}
|
|
120
|
+
// Other errors (permission, FS full, EROFS): silent degrade — without
|
|
121
|
+
// a marker we cannot honor the once-per-session contract, so emitting
|
|
122
|
+
// the nudge would spam every matching command. Better to skip the
|
|
123
|
+
// nudge entirely and let the user notice the FS issue via
|
|
124
|
+
// `memesh doctor` if they care.
|
|
125
|
+
return pass();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Emit the advisory nudge
|
|
129
|
+
const nudge = [
|
|
130
|
+
'Orchestration hint: this looks like high-verifiability work.',
|
|
131
|
+
'If it will take >10s, consider dispatching it as a background agent',
|
|
132
|
+
'(Task tool with run_in_background:true) instead of blocking the conversation.',
|
|
133
|
+
'See the agentic-orchestration skill for the dispatch pattern.',
|
|
134
|
+
].join('\n');
|
|
135
|
+
|
|
136
|
+
console.log(JSON.stringify({
|
|
137
|
+
hookSpecificOutput: {
|
|
138
|
+
hookEventName: 'PreToolUse',
|
|
139
|
+
additionalContext: nudge,
|
|
140
|
+
},
|
|
141
|
+
}));
|
|
142
|
+
} catch {
|
|
143
|
+
// Never crash Claude Code — always exit cleanly
|
|
144
|
+
pass();
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
function pass() {
|
|
149
|
+
process.exit(0);
|
|
150
|
+
}
|
|
@@ -1,66 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { existsSync, mkdirSync, readFileSync } from 'fs';
|
|
7
|
-
|
|
8
|
-
const require = createRequire(import.meta.url);
|
|
9
|
-
|
|
10
|
-
const SCHEMA_SQL = `
|
|
11
|
-
CREATE TABLE IF NOT EXISTS entities (
|
|
12
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
13
|
-
name TEXT NOT NULL UNIQUE,
|
|
14
|
-
type TEXT NOT NULL,
|
|
15
|
-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
16
|
-
metadata JSON
|
|
17
|
-
);
|
|
18
|
-
|
|
19
|
-
CREATE TABLE IF NOT EXISTS observations (
|
|
20
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
21
|
-
entity_id INTEGER NOT NULL,
|
|
22
|
-
content TEXT NOT NULL,
|
|
23
|
-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
24
|
-
FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
CREATE TABLE IF NOT EXISTS relations (
|
|
28
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
29
|
-
from_entity_id INTEGER NOT NULL,
|
|
30
|
-
to_entity_id INTEGER NOT NULL,
|
|
31
|
-
relation_type TEXT NOT NULL,
|
|
32
|
-
metadata JSON,
|
|
33
|
-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
34
|
-
FOREIGN KEY (from_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
|
|
35
|
-
FOREIGN KEY (to_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
|
|
36
|
-
UNIQUE(from_entity_id, to_entity_id, relation_type)
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
CREATE TABLE IF NOT EXISTS tags (
|
|
40
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
41
|
-
entity_id INTEGER NOT NULL,
|
|
42
|
-
tag TEXT NOT NULL,
|
|
43
|
-
FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
CREATE INDEX IF NOT EXISTS idx_tags_entity ON tags(entity_id);
|
|
47
|
-
CREATE INDEX IF NOT EXISTS idx_tags_tag ON tags(tag);
|
|
48
|
-
DELETE FROM tags
|
|
49
|
-
WHERE id NOT IN (
|
|
50
|
-
SELECT MIN(id)
|
|
51
|
-
FROM tags
|
|
52
|
-
GROUP BY entity_id, tag
|
|
53
|
-
);
|
|
54
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_tags_entity_tag_unique ON tags(entity_id, tag);
|
|
55
|
-
CREATE INDEX IF NOT EXISTS idx_observations_entity ON observations(entity_id);
|
|
56
|
-
CREATE INDEX IF NOT EXISTS idx_relations_from ON relations(from_entity_id);
|
|
57
|
-
CREATE INDEX IF NOT EXISTS idx_relations_to ON relations(to_entity_id);
|
|
58
|
-
|
|
59
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
|
|
60
|
-
name, observations, content='',
|
|
61
|
-
tokenize='unicode61 remove_diacritics 1'
|
|
62
|
-
);
|
|
63
|
-
`;
|
|
3
|
+
import { basename } from 'path';
|
|
4
|
+
import { existsSync, readFileSync } from 'fs';
|
|
5
|
+
import { isAutoCaptureEnabled, openHookDb } from './_shared.js';
|
|
64
6
|
|
|
65
7
|
// Timeout guard: always exit within 10 seconds
|
|
66
8
|
const TIMEOUT_MS = 10000;
|
|
@@ -75,8 +17,8 @@ process.stdin.setEncoding('utf8');
|
|
|
75
17
|
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
76
18
|
process.stdin.on('end', () => {
|
|
77
19
|
try {
|
|
78
|
-
// Opt-out check
|
|
79
|
-
if (process.env
|
|
20
|
+
// Opt-out check (env > config > default-on)
|
|
21
|
+
if (!isAutoCaptureEnabled(process.env)) {
|
|
80
22
|
return exit0();
|
|
81
23
|
}
|
|
82
24
|
|
|
@@ -130,20 +72,10 @@ process.stdin.on('end', () => {
|
|
|
130
72
|
obsLines.push(`Files edited: ${Array.from(editedFiles).join(', ')}`);
|
|
131
73
|
}
|
|
132
74
|
|
|
133
|
-
// Open
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
? join(process.env.MEMESH_DB_PATH, '..')
|
|
137
|
-
: join(homedir(), '.memesh');
|
|
138
|
-
if (!existsSync(dbDir)) mkdirSync(dbDir, { recursive: true });
|
|
139
|
-
|
|
140
|
-
const Database = require('better-sqlite3');
|
|
141
|
-
const db = new Database(dbPath);
|
|
75
|
+
// Open DB via shared helper — applies SCHEMA_SQL + status migration.
|
|
76
|
+
// FTS5 needed for the entity-search index updates below.
|
|
77
|
+
const { db } = openHookDb(process.env, { fts: true });
|
|
142
78
|
try {
|
|
143
|
-
db.pragma('journal_mode = WAL');
|
|
144
|
-
db.pragma('foreign_keys = ON');
|
|
145
|
-
db.exec(SCHEMA_SQL);
|
|
146
|
-
|
|
147
79
|
// Upsert entity
|
|
148
80
|
const insertResult = db.prepare('INSERT OR IGNORE INTO entities (name, type) VALUES (?, ?)').run(entityName, 'session-summary');
|
|
149
81
|
const isNew = insertResult.changes > 0;
|
|
File without changes
|