@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.
Files changed (118) hide show
  1. package/README.de.md +227 -53
  2. package/README.es.md +230 -56
  3. package/README.fr.md +230 -56
  4. package/README.ja.md +229 -55
  5. package/README.ko.md +230 -56
  6. package/README.md +54 -3
  7. package/README.pt.md +230 -56
  8. package/README.th.md +230 -56
  9. package/README.vi.md +228 -54
  10. package/README.zh-CN.md +230 -56
  11. package/README.zh-TW.md +228 -54
  12. package/dashboard/dist/index.html +3 -3
  13. package/dist/core/analytics.d.ts +53 -0
  14. package/dist/core/analytics.d.ts.map +1 -0
  15. package/dist/core/analytics.js +127 -0
  16. package/dist/core/analytics.js.map +1 -0
  17. package/dist/core/auto-tagger.d.ts.map +1 -1
  18. package/dist/core/auto-tagger.js +14 -54
  19. package/dist/core/auto-tagger.js.map +1 -1
  20. package/dist/core/config.d.ts +1 -0
  21. package/dist/core/config.d.ts.map +1 -1
  22. package/dist/core/config.js +6 -10
  23. package/dist/core/config.js.map +1 -1
  24. package/dist/core/consolidator.d.ts.map +1 -1
  25. package/dist/core/consolidator.js +11 -61
  26. package/dist/core/consolidator.js.map +1 -1
  27. package/dist/core/doctor.d.ts +40 -0
  28. package/dist/core/doctor.d.ts.map +1 -0
  29. package/dist/core/doctor.js +264 -0
  30. package/dist/core/doctor.js.map +1 -0
  31. package/dist/core/embedder.js.map +1 -1
  32. package/dist/core/failure-analyzer.d.ts +2 -1
  33. package/dist/core/failure-analyzer.d.ts.map +1 -1
  34. package/dist/core/failure-analyzer.js +14 -47
  35. package/dist/core/failure-analyzer.js.map +1 -1
  36. package/dist/core/graph.d.ts +13 -0
  37. package/dist/core/graph.d.ts.map +1 -0
  38. package/dist/core/graph.js +13 -0
  39. package/dist/core/graph.js.map +1 -0
  40. package/dist/core/lesson-engine.d.ts +2 -1
  41. package/dist/core/lesson-engine.d.ts.map +1 -1
  42. package/dist/core/lesson-engine.js +2 -0
  43. package/dist/core/lesson-engine.js.map +1 -1
  44. package/dist/core/lifecycle.d.ts.map +1 -1
  45. package/dist/core/lifecycle.js +2 -1
  46. package/dist/core/lifecycle.js.map +1 -1
  47. package/dist/core/llm-client.d.ts +6 -0
  48. package/dist/core/llm-client.d.ts.map +1 -0
  49. package/dist/core/llm-client.js +64 -0
  50. package/dist/core/llm-client.js.map +1 -0
  51. package/dist/core/operations.d.ts +0 -1
  52. package/dist/core/operations.d.ts.map +1 -1
  53. package/dist/core/operations.js +40 -61
  54. package/dist/core/operations.js.map +1 -1
  55. package/dist/core/prompt-safety.d.ts +3 -0
  56. package/dist/core/prompt-safety.d.ts.map +1 -0
  57. package/dist/core/prompt-safety.js +13 -0
  58. package/dist/core/prompt-safety.js.map +1 -0
  59. package/dist/core/query-expander.d.ts.map +1 -1
  60. package/dist/core/query-expander.js +11 -72
  61. package/dist/core/query-expander.js.map +1 -1
  62. package/dist/core/schema-export.d.ts.map +1 -1
  63. package/dist/core/schema-export.js +72 -0
  64. package/dist/core/schema-export.js.map +1 -1
  65. package/dist/core/skill-usage-log.d.ts +11 -0
  66. package/dist/core/skill-usage-log.d.ts.map +1 -0
  67. package/dist/core/skill-usage-log.js +125 -0
  68. package/dist/core/skill-usage-log.js.map +1 -0
  69. package/dist/core/stats.d.ts +12 -0
  70. package/dist/core/stats.d.ts.map +1 -0
  71. package/dist/core/stats.js +19 -0
  72. package/dist/core/stats.js.map +1 -0
  73. package/dist/core/types.d.ts +9 -2
  74. package/dist/core/types.d.ts.map +1 -1
  75. package/dist/core/verifier.d.ts +37 -0
  76. package/dist/core/verifier.d.ts.map +1 -0
  77. package/dist/core/verifier.js +195 -0
  78. package/dist/core/verifier.js.map +1 -0
  79. package/dist/db.d.ts.map +1 -1
  80. package/dist/db.js +14 -0
  81. package/dist/db.js.map +1 -1
  82. package/dist/knowledge-graph.d.ts.map +1 -1
  83. package/dist/knowledge-graph.js +8 -43
  84. package/dist/knowledge-graph.js.map +1 -1
  85. package/dist/skills-manifest.json +71 -0
  86. package/dist/storage/conflicts.d.ts +4 -0
  87. package/dist/storage/conflicts.d.ts.map +1 -0
  88. package/dist/storage/conflicts.js +32 -0
  89. package/dist/storage/conflicts.js.map +1 -0
  90. package/dist/storage/fts-index.d.ts +4 -0
  91. package/dist/storage/fts-index.d.ts.map +1 -0
  92. package/dist/storage/fts-index.js +18 -0
  93. package/dist/storage/fts-index.js.map +1 -0
  94. package/dist/transports/cli/cli.js +203 -69
  95. package/dist/transports/cli/cli.js.map +1 -1
  96. package/dist/transports/http/server.d.ts +1 -0
  97. package/dist/transports/http/server.d.ts.map +1 -1
  98. package/dist/transports/http/server.js +138 -231
  99. package/dist/transports/http/server.js.map +1 -1
  100. package/dist/transports/mcp/handlers.d.ts +93 -0
  101. package/dist/transports/mcp/handlers.d.ts.map +1 -1
  102. package/dist/transports/mcp/handlers.js +51 -1
  103. package/dist/transports/mcp/handlers.js.map +1 -1
  104. package/dist/transports/schemas.d.ts +28 -0
  105. package/dist/transports/schemas.d.ts.map +1 -1
  106. package/dist/transports/schemas.js +25 -0
  107. package/dist/transports/schemas.js.map +1 -1
  108. package/hooks/hooks.json +10 -0
  109. package/package.json +5 -3
  110. package/plugin.json +1 -1
  111. package/scripts/hooks/_shared.js +207 -1
  112. package/scripts/hooks/post-commit.js +5 -76
  113. package/scripts/hooks/pre-bash-orchestration-nudge.js +150 -0
  114. package/scripts/hooks/pre-compact.js +8 -76
  115. package/scripts/hooks/pre-edit-recall.js +0 -0
  116. package/scripts/hooks/session-start.js +69 -8
  117. package/scripts/hooks/session-summary.js +12 -81
  118. package/skills/agentic-orchestration/SKILL.md +399 -0
@@ -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 { homedir } from 'os';
6
- import { join, basename } from 'path';
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 database (create dir if needed)
92
- const dbPath = process.env.MEMESH_DB_PATH || join(homedir(), '.memesh', 'knowledge-graph.db');
93
- const dbDir = process.env.MEMESH_DB_PATH
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 { createRequire } from 'module';
4
- import { homedir } from 'os';
5
- import { join, basename } from 'path';
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.MEMESH_AUTO_CAPTURE === 'false') {
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 (or create) database
134
- const dbPath = process.env.MEMESH_DB_PATH || join(homedir(), '.memesh', 'knowledge-graph.db');
135
- const dbDir = process.env.MEMESH_DB_PATH
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