gm-plugkit 2.0.1216 → 2.0.1218
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/package.json +1 -1
- package/plugkit-wasm-wrapper.js +202 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gm-plugkit",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1218",
|
|
4
4
|
"description": "Bootstrap and daemon-spawn tool for gm plugkit binary. Downloads the correct platform binary, verifies SHA256, and starts the spool watcher daemon. Includes plugkit-wasm-wrapper for WASM-based spool watching.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
package/plugkit-wasm-wrapper.js
CHANGED
|
@@ -70,6 +70,32 @@ function isSpoolPollCommand(command) {
|
|
|
70
70
|
return null;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
function isBrowserRunningFileLocal(rel) {
|
|
74
|
+
if (!rel) return false;
|
|
75
|
+
const norm = String(rel).replace(/\\\\/g, '/');
|
|
76
|
+
if (/\\.(html?|tsx|jsx|vue|svelte)$/i.test(norm)) return true;
|
|
77
|
+
if (/\\.(mjs|cjs|js|ts|css|scss|sass)$/i.test(norm) && /^(src|public|site|app|pages|components|client|web)\\//i.test(norm)) return true;
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function recordBrowserEditLocal(cwd, filePath) {
|
|
82
|
+
try {
|
|
83
|
+
const fs = require('fs');
|
|
84
|
+
const path = require('path');
|
|
85
|
+
let rel = filePath;
|
|
86
|
+
try { rel = path.relative(cwd, filePath); } catch (_) {}
|
|
87
|
+
if (!isBrowserRunningFileLocal(rel)) return false;
|
|
88
|
+
const editsFile = path.join(cwd, '.gm', 'exec-spool', '.turn-browser-edits.json');
|
|
89
|
+
fs.mkdirSync(path.dirname(editsFile), { recursive: true });
|
|
90
|
+
let list = [];
|
|
91
|
+
try { list = JSON.parse(fs.readFileSync(editsFile, 'utf8')); if (!Array.isArray(list)) list = []; } catch (_) {}
|
|
92
|
+
const entry = { file: rel.replace(/\\\\/g, '/'), ts: Date.now() };
|
|
93
|
+
if (!list.some(e => e && e.file === entry.file)) list.push(entry);
|
|
94
|
+
fs.writeFileSync(editsFile, JSON.stringify(list));
|
|
95
|
+
return true;
|
|
96
|
+
} catch (_) { return false; }
|
|
97
|
+
}
|
|
98
|
+
|
|
73
99
|
let raw = '';
|
|
74
100
|
process.stdin.setEncoding('utf8');
|
|
75
101
|
process.stdin.on('data', (chunk) => { raw += chunk; });
|
|
@@ -78,6 +104,17 @@ process.stdin.on('end', () => {
|
|
|
78
104
|
try { event = JSON.parse(raw || '{}'); } catch (_) { event = {}; }
|
|
79
105
|
const tool = event.tool_name || event.tool || '';
|
|
80
106
|
const input = event.tool_input || event.input || {};
|
|
107
|
+
const cwd = event.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
108
|
+
|
|
109
|
+
if (tool === 'Write' || tool === 'Edit' || tool === 'MultiEdit') {
|
|
110
|
+
const fp = input.file_path || input.filePath || input.path || '';
|
|
111
|
+
if (fp) {
|
|
112
|
+
try { recordBrowserEditLocal(cwd, fp); } catch (_) {}
|
|
113
|
+
}
|
|
114
|
+
process.stdout.write(JSON.stringify({ continue: true }));
|
|
115
|
+
process.exit(0);
|
|
116
|
+
}
|
|
117
|
+
|
|
81
118
|
if (tool !== 'Bash') {
|
|
82
119
|
process.stdout.write(JSON.stringify({ continue: true }));
|
|
83
120
|
process.exit(0);
|
|
@@ -124,11 +161,35 @@ function ensureSpoolPollGate(cwd) {
|
|
|
124
161
|
const gateScript = path.join(gmHooks, 'spool-poll-gate.js');
|
|
125
162
|
const want = spoolPollGateScript();
|
|
126
163
|
let need = true;
|
|
164
|
+
let oldSha = '';
|
|
165
|
+
let existed = false;
|
|
127
166
|
try {
|
|
128
167
|
const existing = fs.readFileSync(gateScript, 'utf8');
|
|
168
|
+
existed = true;
|
|
129
169
|
if (existing === want) need = false;
|
|
170
|
+
else {
|
|
171
|
+
try {
|
|
172
|
+
const _crypto = require('crypto');
|
|
173
|
+
oldSha = _crypto.createHash('sha256').update(existing).digest('hex').slice(0, 12);
|
|
174
|
+
} catch (_) {}
|
|
175
|
+
}
|
|
130
176
|
} catch (_) {}
|
|
131
|
-
if (need)
|
|
177
|
+
if (need) {
|
|
178
|
+
fs.writeFileSync(gateScript, want);
|
|
179
|
+
try {
|
|
180
|
+
const _crypto = require('crypto');
|
|
181
|
+
const newSha = _crypto.createHash('sha256').update(want).digest('hex').slice(0, 12);
|
|
182
|
+
try {
|
|
183
|
+
logEvent('bootstrap', existed ? 'gate.refreshed' : 'gate.installed', {
|
|
184
|
+
cwd,
|
|
185
|
+
path: gateScript,
|
|
186
|
+
old_sha: oldSha || null,
|
|
187
|
+
new_sha: newSha,
|
|
188
|
+
bytes: Buffer.byteLength(want, 'utf8'),
|
|
189
|
+
});
|
|
190
|
+
} catch (_) {}
|
|
191
|
+
} catch (_) {}
|
|
192
|
+
}
|
|
132
193
|
|
|
133
194
|
const claudeDir = path.join(cwd, '.claude');
|
|
134
195
|
fs.mkdirSync(claudeDir, { recursive: true });
|
|
@@ -141,15 +202,17 @@ function ensureSpoolPollGate(cwd) {
|
|
|
141
202
|
if (!settings.hooks || typeof settings.hooks !== 'object') settings.hooks = {};
|
|
142
203
|
if (!Array.isArray(settings.hooks.PreToolUse)) settings.hooks.PreToolUse = [];
|
|
143
204
|
const wantCommand = `node "\${CLAUDE_PROJECT_DIR}/.gm/hooks/spool-poll-gate.js"`;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
205
|
+
for (const matcher of ['Bash', 'Write', 'Edit', 'MultiEdit']) {
|
|
206
|
+
let entry = settings.hooks.PreToolUse.find(e => e && e.matcher === matcher);
|
|
207
|
+
if (!entry) {
|
|
208
|
+
entry = { matcher, hooks: [] };
|
|
209
|
+
settings.hooks.PreToolUse.push(entry);
|
|
210
|
+
}
|
|
211
|
+
if (!Array.isArray(entry.hooks)) entry.hooks = [];
|
|
212
|
+
const already = entry.hooks.some(h => h && typeof h.command === 'string' && h.command.includes('spool-poll-gate.js'));
|
|
213
|
+
if (!already) {
|
|
214
|
+
entry.hooks.push({ type: 'command', command: wantCommand });
|
|
215
|
+
}
|
|
153
216
|
}
|
|
154
217
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
155
218
|
} catch (_) {}
|
|
@@ -172,6 +235,99 @@ function applyDisciplineSigil(rawBody) {
|
|
|
172
235
|
return JSON.stringify(parsed);
|
|
173
236
|
}
|
|
174
237
|
|
|
238
|
+
function isInstructionTurnStart(sess) {
|
|
239
|
+
const key = sess || '(no-session)';
|
|
240
|
+
const now = Date.now();
|
|
241
|
+
const t = _turns.get(key);
|
|
242
|
+
if (!t) return true;
|
|
243
|
+
if ((now - t.lastTs) > TURN_IDLE_MS) return true;
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function readUserPromptForRecall(cwd) {
|
|
248
|
+
const root = cwd || process.cwd();
|
|
249
|
+
try {
|
|
250
|
+
const p = path.join(root, '.gm', 'last-prompt.txt');
|
|
251
|
+
const txt = fs.readFileSync(p, 'utf8').trim();
|
|
252
|
+
if (txt) return txt;
|
|
253
|
+
} catch (_) {}
|
|
254
|
+
try {
|
|
255
|
+
const p = path.join(root, '.gm', 'turn-state.json');
|
|
256
|
+
const obj = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
257
|
+
if (obj && typeof obj.last_prompt === 'string' && obj.last_prompt.trim()) return obj.last_prompt.trim();
|
|
258
|
+
if (obj && typeof obj.prompt === 'string' && obj.prompt.trim()) return obj.prompt.trim();
|
|
259
|
+
} catch (_) {}
|
|
260
|
+
return '';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function dispatchVerbToWasmInternal(instance, verb, body) {
|
|
264
|
+
const dispatch = instance.exports.dispatch_verb;
|
|
265
|
+
if (!dispatch) return null;
|
|
266
|
+
const verbBytes = new TextEncoder().encode(verb);
|
|
267
|
+
const bodyBytes = new TextEncoder().encode(body || '');
|
|
268
|
+
const verbPtr = instance.exports.plugkit_alloc(verbBytes.length);
|
|
269
|
+
const bodyPtr = instance.exports.plugkit_alloc(bodyBytes.length);
|
|
270
|
+
try {
|
|
271
|
+
new Uint8Array(instance.exports.memory.buffer, verbPtr, verbBytes.length).set(verbBytes);
|
|
272
|
+
new Uint8Array(instance.exports.memory.buffer, bodyPtr, bodyBytes.length).set(bodyBytes);
|
|
273
|
+
const result = dispatch(verbPtr, verbBytes.length, bodyPtr, bodyBytes.length);
|
|
274
|
+
const ptr = Number(result & 0xffffffffn);
|
|
275
|
+
const len = Number(result >> 32n);
|
|
276
|
+
const out = new TextDecoder().decode(new Uint8Array(instance.exports.memory.buffer, ptr, len));
|
|
277
|
+
try { instance.exports.plugkit_free(ptr, len); } catch (_) {}
|
|
278
|
+
return out;
|
|
279
|
+
} finally {
|
|
280
|
+
try { instance.exports.plugkit_free(verbPtr, verbBytes.length); } catch (_) {}
|
|
281
|
+
try { instance.exports.plugkit_free(bodyPtr, bodyBytes.length); } catch (_) {}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function tryAutoRecallForTurnEntry(instance, sess, cwd) {
|
|
286
|
+
try {
|
|
287
|
+
const prompt = readUserPromptForRecall(cwd);
|
|
288
|
+
if (!prompt) return null;
|
|
289
|
+
const out = dispatchVerbToWasmInternal(instance, 'auto-recall', prompt);
|
|
290
|
+
if (!out) return null;
|
|
291
|
+
let parsed;
|
|
292
|
+
try { parsed = JSON.parse(out); } catch (_) { return null; }
|
|
293
|
+
if (!parsed || parsed.ok !== true) return null;
|
|
294
|
+
let inner = parsed.data;
|
|
295
|
+
if (typeof parsed.stdout === 'string' && parsed.stdout.length > 0) {
|
|
296
|
+
try { inner = JSON.parse(parsed.stdout); } catch (_) {}
|
|
297
|
+
}
|
|
298
|
+
if (!inner || typeof inner !== 'object') return null;
|
|
299
|
+
const hits = Array.isArray(inner.results) ? inner.results : (Array.isArray(inner.hits) ? inner.hits : []);
|
|
300
|
+
const payload = { query: inner.query || '', hits, fired_at: new Date().toISOString(), turn_entry: true };
|
|
301
|
+
logEvent('plugkit', 'auto_recall.turn-entry', { sess, query: payload.query, count: hits.length });
|
|
302
|
+
return payload;
|
|
303
|
+
} catch (e) {
|
|
304
|
+
logEvent('plugkit', 'auto_recall.error', { sess, error: String(e && e.message || e) });
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function mergeAutoRecallIntoInstructionResponse(resultStr, autoRecall) {
|
|
310
|
+
if (!autoRecall) return resultStr;
|
|
311
|
+
let parsed;
|
|
312
|
+
try { parsed = JSON.parse(resultStr); } catch (_) { return resultStr; }
|
|
313
|
+
if (!parsed || typeof parsed !== 'object') return resultStr;
|
|
314
|
+
if (parsed.data && typeof parsed.data === 'object') {
|
|
315
|
+
parsed.data.auto_recall = autoRecall;
|
|
316
|
+
} else {
|
|
317
|
+
parsed.auto_recall = autoRecall;
|
|
318
|
+
}
|
|
319
|
+
if (typeof parsed.stdout === 'string' && parsed.stdout.length > 0) {
|
|
320
|
+
try {
|
|
321
|
+
const inner = JSON.parse(parsed.stdout);
|
|
322
|
+
if (inner && typeof inner === 'object') {
|
|
323
|
+
inner.auto_recall = autoRecall;
|
|
324
|
+
parsed.stdout = JSON.stringify(inner);
|
|
325
|
+
}
|
|
326
|
+
} catch (_) {}
|
|
327
|
+
}
|
|
328
|
+
return JSON.stringify(parsed);
|
|
329
|
+
}
|
|
330
|
+
|
|
175
331
|
function turnTick(sess, verb, taskBase, phase) {
|
|
176
332
|
const key = sess || '(no-session)';
|
|
177
333
|
const now = Date.now();
|
|
@@ -1203,6 +1359,8 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
1203
1359
|
fs.mkdirSync(inDir, { recursive: true });
|
|
1204
1360
|
fs.mkdirSync(outDir, { recursive: true });
|
|
1205
1361
|
|
|
1362
|
+
try { ensureSpoolPollGate(process.env.CLAUDE_PROJECT_DIR || process.cwd()); } catch (_) {}
|
|
1363
|
+
|
|
1206
1364
|
const LOCK_PATH = path.join(spoolDir, '.watcher.lock');
|
|
1207
1365
|
let _ownWrapperSha12 = '';
|
|
1208
1366
|
try {
|
|
@@ -1506,18 +1664,35 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
1506
1664
|
prior_status: _priorStatus,
|
|
1507
1665
|
prior_status_age_ms: _priorStatus && Number.isFinite(_priorStatus.ts) ? Date.now() - _priorStatus.ts : null,
|
|
1508
1666
|
};
|
|
1509
|
-
const
|
|
1667
|
+
const _PLANNED_REASONS = new Set(['idle', 'sigterm', 'version-change', 'wrapper-change', 'peer-stale-takeover', 'version-drift', 'external-planned']);
|
|
1668
|
+
const _isPlannedBoot = _priorShutdown && _PLANNED_REASONS.has(_priorShutdown.reason);
|
|
1510
1669
|
const _isFirstBoot = !_priorShutdown && !_priorStatus;
|
|
1511
1670
|
const UNPLANNED_RESTART_MARKER = path.join(spoolDir, '.unplanned-restart.json');
|
|
1512
|
-
|
|
1671
|
+
const HEARTBEAT_RECENT_MS = 60_000;
|
|
1672
|
+
const HEARTBEAT_DEAD_MS = 5 * 60_000;
|
|
1673
|
+
let _severity = 'critical';
|
|
1674
|
+
if (_isPlannedBoot) {
|
|
1675
|
+
_severity = 'info';
|
|
1676
|
+
} else if (!_priorShutdown && _priorStatus && Number.isFinite(_priorStatus.ts)) {
|
|
1677
|
+
const _statusAge = Date.now() - _priorStatus.ts;
|
|
1678
|
+
if (_statusAge <= HEARTBEAT_RECENT_MS) _severity = 'warn';
|
|
1679
|
+
else if (_statusAge < HEARTBEAT_DEAD_MS) _severity = 'warn';
|
|
1680
|
+
else _severity = 'critical';
|
|
1681
|
+
}
|
|
1682
|
+
if (!_isFirstBoot) {
|
|
1513
1683
|
const incidentPayload = {
|
|
1514
1684
|
ts: Date.now(),
|
|
1515
1685
|
version: _bootVersion,
|
|
1516
|
-
severity:
|
|
1686
|
+
severity: _severity,
|
|
1687
|
+
planned: _isPlannedBoot,
|
|
1517
1688
|
...restartContext,
|
|
1518
1689
|
log_tail_path: path.join(spoolDir, '.watcher.log'),
|
|
1519
1690
|
gm_log_dir: GM_LOG_ROOT,
|
|
1520
|
-
instruction:
|
|
1691
|
+
instruction: _isPlannedBoot
|
|
1692
|
+
? `Planned restart: prior watcher exited with reason="${_priorShutdown.reason}". No action required.`
|
|
1693
|
+
: (_severity === 'warn'
|
|
1694
|
+
? 'Prior watcher disappeared with a recent heartbeat — likely a clean shutdown that did not write .shutdown-reason.json. Inspect .watcher.log if recurrent.'
|
|
1695
|
+
: 'Prior watcher died without a planned shutdown and without a recent heartbeat. This is treated as a critical failure. Inspect .watcher.log and gm-log/<day>/plugkit.jsonl events supervisor.watcher-exited-unexpectedly + supervisor.heartbeat-stale around the prior_status.ts timestamp to diagnose root cause.'),
|
|
1521
1696
|
};
|
|
1522
1697
|
logEvent('plugkit', 'watcher.unplanned-restart', incidentPayload);
|
|
1523
1698
|
try {
|
|
@@ -1575,6 +1750,14 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
1575
1750
|
console.log(`[dispatch] → verb=${verb} task=${taskBase} body=${bodyBytes.length}b`);
|
|
1576
1751
|
logEvent('plugkit', 'dispatch.start', { verb, task: taskBase, body_bytes: bodyBytes.length, cwd: process.cwd() });
|
|
1577
1752
|
|
|
1753
|
+
let autoRecallPayload = null;
|
|
1754
|
+
if (verb === 'instruction') {
|
|
1755
|
+
const sessForRecall = readCurrentSess();
|
|
1756
|
+
if (isInstructionTurnStart(sessForRecall)) {
|
|
1757
|
+
autoRecallPayload = tryAutoRecallForTurnEntry(instance, sessForRecall, process.cwd());
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1578
1761
|
const verbPtr = instance.exports.plugkit_alloc(verbBytes.length);
|
|
1579
1762
|
const bodyPtr = instance.exports.plugkit_alloc(bodyBytes.length);
|
|
1580
1763
|
new Uint8Array(instance.exports.memory.buffer, verbPtr, verbBytes.length).set(verbBytes);
|
|
@@ -1585,7 +1768,11 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
1585
1768
|
const ptr = Number(result & 0xffffffffn);
|
|
1586
1769
|
const len = Number(result >> 32n);
|
|
1587
1770
|
const resultBytes = new Uint8Array(instance.exports.memory.buffer, ptr, len);
|
|
1588
|
-
|
|
1771
|
+
let resultStr = new TextDecoder().decode(resultBytes);
|
|
1772
|
+
|
|
1773
|
+
if (autoRecallPayload) {
|
|
1774
|
+
resultStr = mergeAutoRecallIntoInstructionResponse(resultStr, autoRecallPayload);
|
|
1775
|
+
}
|
|
1589
1776
|
|
|
1590
1777
|
const outName = dir === '.' ? `${taskBase}.json` : `${verb}-${taskBase}.json`;
|
|
1591
1778
|
fs.writeFileSync(path.join(outDir, outName), resultStr);
|