gm-plugkit 2.0.1220 → 2.0.1222

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-plugkit",
3
- "version": "2.0.1220",
3
+ "version": "2.0.1222",
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": {
@@ -49,7 +49,7 @@ const SPOOL_POLL_PATTERNS = [
49
49
  /\\b(?:test|Test-Path|tp)\\s+(?:-[A-Za-z]+\\s+)?['"]?[^'"|;&]*\\.gm[\\\\/](?:exec-spool|spool)[\\\\/]/i,
50
50
  ];
51
51
 
52
- const SPOOL_POLL_REASON = 'spool polling and bash-reads of .gm/exec-spool/ are forbidden — plugkit is synchronous from your view, and the canonical way to inspect spool files is the Read tool. Use Read on .gm/exec-spool/out/<verb>-<N>.json directly. If the response file does not exist, the watcher is either dead (Read .gm/exec-spool/.status.json and check its mtime against now) or the verb is genuinely slow (Read .gm/exec-spool/.watcher.log for the dispatch trace). You are the state machine; plugkit serves the response the moment you write the request, and Read is how you observe the result.';
52
+ const SPOOL_POLL_REASON = 'spool polling and bash-reads of .gm/exec-spool/ are forbidden — plugkit is synchronous from your view, and the Read tool is the canonical way to inspect spool files. Specific replacements:\\n\\n- Instead of \`cat .gm/exec-spool/.status.json\` → use the Read tool: \`Read .gm/exec-spool/.status.json\`\\n- Instead of \`ls .gm/exec-spool/out/\` → check the specific response file you wrote, e.g. \`Read .gm/exec-spool/out/<verb>-<N>.json\`\\n- Instead of \`cat .gm/exec-spool/.watcher.log\` use the Read tool with offset for tailing\\n- Instead of \`sleep N; cat .gm/exec-spool/<...>\` → just Read the response file directly; if it doesn\\'t exist yet, the watcher is dead (Read .gm/exec-spool/.status.json fresh ts means alive) or the verb is slow (Read .gm/exec-spool/.watcher.log for the dispatch trace)\\n\\nYou are the state machine. Plugkit serves the response the moment you write the request file. If you find yourself thinking "let me just check whether the file is there yet" — use Read. If you find yourself thinking "the watcher might have died" — Read .gm/exec-spool/.status.json. Bash on .gm/exec-spool/ is wrong every single time.';
53
53
 
54
54
  function stripHeredocsAndStringLiterals(command) {
55
55
  let s = String(command);
@@ -78,6 +78,17 @@ function isBrowserRunningFileLocal(rel) {
78
78
  return false;
79
79
  }
80
80
 
81
+ function hashFileShortLocal(cwd, rel) {
82
+ try {
83
+ const fs = require('fs');
84
+ const path = require('path');
85
+ const crypto = require('crypto');
86
+ const abs = path.isAbsolute(rel) ? rel : path.join(cwd, rel);
87
+ const buf = fs.readFileSync(abs);
88
+ return crypto.createHash('sha256').update(buf).digest('hex').slice(0, 12);
89
+ } catch (_) { return ''; }
90
+ }
91
+
81
92
  function recordBrowserEditLocal(cwd, filePath) {
82
93
  try {
83
94
  const fs = require('fs');
@@ -89,8 +100,11 @@ function recordBrowserEditLocal(cwd, filePath) {
89
100
  fs.mkdirSync(path.dirname(editsFile), { recursive: true });
90
101
  let list = [];
91
102
  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);
103
+ const relPath = rel.replace(/\\\\/g, '/');
104
+ const hash = hashFileShortLocal(cwd, relPath);
105
+ const idx = list.findIndex(e => e && e.file === relPath);
106
+ const entry = { file: relPath, ts: Date.now(), hash };
107
+ if (idx === -1) list.push(entry); else list[idx] = entry;
94
108
  fs.writeFileSync(editsFile, JSON.stringify(list));
95
109
  return true;
96
110
  } catch (_) { return false; }
@@ -282,11 +296,29 @@ function dispatchVerbToWasmInternal(instance, verb, body) {
282
296
  }
283
297
  }
284
298
 
285
- function tryAutoRecallForTurnEntry(instance, sess, cwd) {
299
+ const AUTO_RECALL_STOPWORDS = new Set([
300
+ 'the','a','an','and','or','but','if','then','else','for','of','to','in','on','at','by','with','from','as','is','are','was','were','be','been','being','have','has','had','do','does','did','will','would','should','could','can','may','might','must','shall',
301
+ 'look','check','see','use','make','run','get','set','put','take','give','find','show','tell','let','keep','try','add','new','old','this','that','these','those','it','its','their','there','here','about','into','over','under','also','just','some','any','all','more','less','most','past','minutes','minute','hours','hour','seconds','second','days','day',
302
+ ]);
303
+
304
+ function deriveFallbackQuery(prompt) {
286
305
  try {
287
- const prompt = readUserPromptForRecall(cwd);
288
- if (!prompt) return null;
289
- const out = dispatchVerbToWasmInternal(instance, 'auto-recall', prompt);
306
+ const tokens = String(prompt).toLowerCase().split(/[^a-z0-9_]+/).filter(Boolean);
307
+ const freq = new Map();
308
+ for (const t of tokens) {
309
+ if (t.length < 4) continue;
310
+ if (AUTO_RECALL_STOPWORDS.has(t)) continue;
311
+ freq.set(t, (freq.get(t) || 0) + 1);
312
+ }
313
+ const ranked = Array.from(freq.entries()).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
314
+ const top = ranked.slice(0, 3).map(([w]) => w);
315
+ return top.join(' ');
316
+ } catch (_) { return ''; }
317
+ }
318
+
319
+ function dispatchAutoRecall(instance, queryPrompt) {
320
+ try {
321
+ const out = dispatchVerbToWasmInternal(instance, 'auto-recall', queryPrompt);
290
322
  if (!out) return null;
291
323
  let parsed;
292
324
  try { parsed = JSON.parse(out); } catch (_) { return null; }
@@ -297,8 +329,61 @@ function tryAutoRecallForTurnEntry(instance, sess, cwd) {
297
329
  }
298
330
  if (!inner || typeof inner !== 'object') return null;
299
331
  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 });
332
+ return { query: inner.query || '', hits };
333
+ } catch (_) { return null; }
334
+ }
335
+
336
+ function tryAutoRecallForTurnEntry(instance, sess, cwd) {
337
+ try {
338
+ const prompt = readUserPromptForRecall(cwd);
339
+ let emptyPromptFallback = false;
340
+ let effectivePrompt = prompt;
341
+ if (!prompt || !String(prompt).trim()) {
342
+ emptyPromptFallback = true;
343
+ const key = sess || '(no-session)';
344
+ const t = _turns.get(key);
345
+ const phase = (t && t.lastPhase) || 'PLAN';
346
+ const phaseQueryMap = {
347
+ PLAN: 'PLAN orient',
348
+ EXECUTE: 'EXECUTE work',
349
+ EMIT: 'EMIT closure',
350
+ VERIFY: 'VERIFY trajectory',
351
+ COMPLETE: 'COMPLETE residual',
352
+ };
353
+ effectivePrompt = phaseQueryMap[phase] || 'PLAN orient';
354
+ }
355
+ const primary = dispatchAutoRecall(instance, effectivePrompt);
356
+ const fallbackQuery = deriveFallbackQuery(effectivePrompt);
357
+ let fallback = null;
358
+ if (fallbackQuery && fallbackQuery !== (primary && primary.query)) {
359
+ fallback = dispatchAutoRecall(instance, fallbackQuery);
360
+ }
361
+ const seen = new Set();
362
+ const merged = [];
363
+ for (const src of [primary, fallback]) {
364
+ if (!src || !Array.isArray(src.hits)) continue;
365
+ for (const h of src.hits) {
366
+ const id = h && (h.id || h.hash || h.key || JSON.stringify(h));
367
+ if (id && seen.has(id)) continue;
368
+ if (id) seen.add(id);
369
+ merged.push(h);
370
+ }
371
+ }
372
+ const queries = [];
373
+ if (primary && primary.query) queries.push(primary.query);
374
+ if (fallback && fallback.query && !queries.includes(fallback.query)) queries.push(fallback.query);
375
+ const payload = {
376
+ query: (primary && primary.query) || effectivePrompt || '',
377
+ queries,
378
+ hits: merged.slice(0, 20),
379
+ fired_at: new Date().toISOString(),
380
+ turn_entry: true,
381
+ };
382
+ if (emptyPromptFallback) {
383
+ payload.fallback_reason = 'empty-prompt';
384
+ if (!payload.query) payload.query = effectivePrompt;
385
+ }
386
+ logEvent('plugkit', 'auto_recall.turn-entry', { sess, queries, count: merged.length });
302
387
  return payload;
303
388
  } catch (e) {
304
389
  logEvent('plugkit', 'auto_recall.error', { sess, error: String(e && e.message || e) });
@@ -1224,6 +1309,7 @@ function makeHostFunctions(instanceRef) {
1224
1309
  const lang = opts.lang || 'nodejs';
1225
1310
  const cwd = opts.cwd || process.cwd();
1226
1311
  const rawTimeout = opts.timeoutMs;
1312
+ const MIN_TIMEOUT_MS = 100;
1227
1313
  if (rawTimeout === undefined || rawTimeout === null || typeof rawTimeout !== 'number' || !Number.isFinite(rawTimeout) || rawTimeout <= 0 || !Number.isInteger(rawTimeout)) {
1228
1314
  return writeWasmJson(instanceRef.value, {
1229
1315
  ok: false,
@@ -1233,6 +1319,15 @@ function makeHostFunctions(instanceRef) {
1233
1319
  received: rawTimeout === undefined ? null : rawTimeout,
1234
1320
  });
1235
1321
  }
1322
+ if (rawTimeout < MIN_TIMEOUT_MS) {
1323
+ return writeWasmJson(instanceRef.value, {
1324
+ ok: false,
1325
+ error: 'timeoutMs below floor',
1326
+ min: MIN_TIMEOUT_MS,
1327
+ received: rawTimeout,
1328
+ paper_ref: '§20',
1329
+ });
1330
+ }
1236
1331
  const timeoutMs = rawTimeout;
1237
1332
  let cmd, args;
1238
1333
  if (lang === 'nodejs' || lang === 'js') { cmd = process.execPath; args = ['-e', code]; }
@@ -1800,10 +1895,23 @@ async function runSpoolWatcher(instance, spoolDir) {
1800
1895
 
1801
1896
  if (verb === 'browser') {
1802
1897
  try {
1803
- const witnessFile = path.join(process.cwd(), '.gm', 'exec-spool', '.turn-browser-witnessed');
1898
+ const cwd_ = process.cwd();
1899
+ const editsFile = path.join(cwd_, '.gm', 'exec-spool', '.turn-browser-edits.json');
1900
+ const witnessFile = path.join(cwd_, '.gm', 'exec-spool', '.turn-browser-witnessed');
1804
1901
  fs.mkdirSync(path.dirname(witnessFile), { recursive: true });
1805
- fs.writeFileSync(witnessFile, JSON.stringify({ ts: Date.now(), task: taskBase, dur_ms }));
1806
- logEvent('plugkit', 'browser.witness-marked', { task: taskBase });
1902
+ let edits = [];
1903
+ try { edits = JSON.parse(fs.readFileSync(editsFile, 'utf8')); if (!Array.isArray(edits)) edits = []; } catch (_) {}
1904
+ const witnessed_hashes = {};
1905
+ for (const e of edits) {
1906
+ if (!e || !e.file) continue;
1907
+ try {
1908
+ const abs = path.isAbsolute(e.file) ? e.file : path.join(cwd_, e.file);
1909
+ const buf = fs.readFileSync(abs);
1910
+ witnessed_hashes[e.file] = crypto.createHash('sha256').update(buf).digest('hex').slice(0, 12);
1911
+ } catch (_) { witnessed_hashes[e.file] = ''; }
1912
+ }
1913
+ fs.writeFileSync(witnessFile, JSON.stringify({ ts: Date.now(), task: taskBase, dur_ms, witnessed_hashes }));
1914
+ logEvent('plugkit', 'browser.witness-marked', { task: taskBase, files: Object.keys(witnessed_hashes) });
1807
1915
  } catch (_) {}
1808
1916
  }
1809
1917
 
package/plugkit.sha256 CHANGED
@@ -1 +1 @@
1
- {"plugkit.wasm":"9bafe43d985cebd33c63433b532a7f396599905fa1d90c9f411ea292f10fd7c1"}
1
+ {"plugkit.wasm":"0cac0a48e7346d2f75779c9972efe47c3f4ab38063a077660906a53028d2e70a"}
package/plugkit.version CHANGED
@@ -1 +1 @@
1
- 0.1.448
1
+ 0.1.449