greprag 5.74.21 → 5.75.0

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.
@@ -40,7 +40,9 @@ function buildArmReminder(d, env) {
40
40
  return null;
41
41
  const unread = Number((d.detail && d.detail.unread) || 0);
42
42
  const arm = env
43
- ? `ToolSearch select:Monitor → persistent Monitor (timeout_ms:3600000) command: \`${(0, session_id_1.armMonitorCommand)(env.short, env.ownerPid, env.assistant, env.mechanic)}\``
43
+ ? (env.platform === 'grok'
44
+ ? `Grok \`monitor\` tool persistent:true command: \`${(0, session_id_1.armMonitorCommand)(env.short, env.ownerPid, env.assistant, env.mechanic, 'grok')}\``
45
+ : `ToolSearch select:Monitor → persistent Monitor (timeout_ms:3600000) command: \`${(0, session_id_1.armMonitorCommand)(env.short, env.ownerPid, env.assistant, env.mechanic)}\``)
44
46
  : `arm your Monitor inbox watcher`;
45
47
  const grounded = `greprag reads your watcher as DOWN (isLocallyArmed=false — a real pidfile check on your session, NOT a timer; the hook CAN see your watcher)`;
46
48
  if (unread > 0) {
@@ -67,7 +69,7 @@ exports.watcherArmModule = {
67
69
  // keeps its historical full-registry behavior. opencode is EXCLUDED: its inbox
68
70
  // delivery is the plugin-armed relay (startSessionRelay), so "arm your Monitor"
69
71
  // is unactionable noise there.
70
- harnesses: ['claude-code', 'codex'],
72
+ harnesses: ['claude-code', 'codex', 'grok'],
71
73
  detect: armDetect,
72
74
  announce: () => null,
73
75
  reminder: (d, env) => buildArmReminder(d, env),
@@ -62,11 +62,6 @@ Object.defineProperty(exports, "removeCodexStartup", { enumerable: true, get: fu
62
62
  Object.defineProperty(exports, "statusCodexStartup", { enumerable: true, get: function () { return codex_startup_2.statusCodexStartup; } });
63
63
  const API_URL_DEFAULT = 'https://api.greprag.com';
64
64
  const HEARTBEAT_IDLE_MS = 70_000;
65
- const FULL_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
66
- function sameSessionId(left, right) {
67
- return !!left && !!right && (FULL_UUID_RE.test(left) && FULL_UUID_RE.test(right)
68
- ? left.toLowerCase() === right.toLowerCase() : (0, session_id_1.truncateSessionId)(left) === (0, session_id_1.truncateSessionId)(right));
69
- }
70
65
  function home() {
71
66
  return process.env.HOME || process.env.USERPROFILE || os.homedir();
72
67
  }
@@ -233,12 +228,12 @@ async function handleMessage(msg, opts, session8, codexSession) {
233
228
  return result;
234
229
  }
235
230
  function shouldDeliverToThisSession(msg, sessionId) {
236
- if (msg.to_session_id && !sameSessionId(msg.to_session_id, sessionId))
231
+ if (msg.to_session_id && !(0, session_id_1.sameSessionId)(msg.to_session_id, sessionId))
237
232
  return false;
238
233
  const fromSession = msg.from?.session_id ?? null;
239
234
  if (msg.message_type === mechanic_friction_1.MECHANIC_FRICTION_MESSAGE_TYPE)
240
235
  return true;
241
- if (fromSession && sameSessionId(fromSession, sessionId))
236
+ if (fromSession && (0, session_id_1.sameSessionId)(fromSession, sessionId))
242
237
  return false;
243
238
  return true;
244
239
  }
@@ -30,7 +30,8 @@ const session_id_1 = require("../session-id");
30
30
  function buildInboxPrimer(env) {
31
31
  const codex = env.platform === 'codex';
32
32
  const opencode = env.platform === 'opencode';
33
- const arm = (0, session_id_1.armMonitorCommand)(env.short, env.ownerPid, env.assistant, env.mechanic);
33
+ const grok = env.platform === 'grok';
34
+ const arm = (0, session_id_1.armMonitorCommand)(env.short, env.ownerPid, env.assistant, env.mechanic, env.platform);
34
35
  return [
35
36
  // LEAD — the forcing function (restored 2026-06-22). ARM is an ALARM, not reference:
36
37
  // you are unreachable until you do it, and it must be re-done every time the watcher
@@ -39,16 +40,20 @@ function buildInboxPrimer(env) {
39
40
  // opencode gets NO alarm: its relay self-arms in the plugin (startSessionRelay) —
40
41
  // "ToolSearch select:Monitor" is unactionable there (neither tool exists) and an
41
42
  // un-followable alarm decays into banner-blindness for the announces around it.
42
- codex
43
- ? '[CODEX INBOX — greprag is your agent-to-agent mesh. Codex-to-Codex: discover tasks and their repo/workspace with `codex_app.list_threads`, then coordinate with `codex_app.send_message_to_thread`. Reaching ANY non-Codex peer (Claude Code / opencode / another tenant\'s agent): `greprag send` IS the live primary path native Codex messaging cannot reach them.]'
44
- : opencode
45
- ? '[OPENCODE INBOX — greprag is your agent-to-agent mesh. Inbound delivery is AUTOMATIC here: the greprag plugin arms a relay for this session (nothing to arm or re-arm) and inbound peer messages arrive as injected turns.]'
46
- : '[⚠ ARM YOUR INBOX WATCHER NOW — you are UNREACHABLE until you do. Peers are live in your mesh trying to coordinate with this session, and the operator can DM you — but they reach you ONLY while armed. Unarmed, you are invisible and their messages silently pile up. The watcher can DROP ANYTIME (crash / reload / OOM) — EVERY turn you are flagged unarmed you MUST re-arm, or coordination breaks.]',
47
- codex
48
- ? 'CODEX: Native Codex tools are the source of truth: `codex_app.list_threads` discovers tasks plus their repo/workspace, and `codex_app.send_message_to_thread` handles Codex-to-Codex coordination. GrepRAG inbox rows for Codex surface through turn hooks only: SessionStart `drain` and UserPromptSubmit `codex-notify`. There is no Codex startup watcher to install. If hooks do not fire, open Codex Desktop Settings -> Settings -> Hooks, trust the GrepRAG commands, start a fresh Codex session, then drain what is waiting: `greprag inbox`.'
49
- : opencode
50
- ? 'DELIVERY: the relay arms itself on this session\'s first turn and injects inbound messages as they land treat an injected `Message from <handle> (session <8hex>):` turn as async peer mail, not the operator typing. Drain anything already waiting: `greprag inbox`.'
51
- : `ARM: Monitor is a deferred tool — \`ToolSearch select:Monitor\`, then call Monitor with persistent:true, timeout_ms:3600000, description:"greprag inbox ${env.short}", command: \`${arm}\`. The \`--session ${env.short}\` is REQUIRED — a bare \`greprag inbox watch\` writes no pidfile, so the arm-check reads you unarmed forever. NEVER run it bare also because it is a foreground stream and hangs the turn. Then drain what is waiting: \`greprag inbox\`.`,
43
+ grok
44
+ ? '[GROK INBOX — greprag is your agent-to-agent mesh. Idle wake is Grok `monitor` (NOT Claude Monitor, NOT a bash while-true). Turn-boundary drain also rides Stop additionalContext. Unarmed, idle mail waits until the next Stop or user prompt.]'
45
+ : codex
46
+ ? '[CODEX INBOX — greprag is your agent-to-agent mesh. Codex-to-Codex: discover tasks and their repo/workspace with `codex_app.list_threads`, then coordinate with `codex_app.send_message_to_thread`. Reaching ANY non-Codex peer (Claude Code / opencode / Grok / another tenant\'s agent): `greprag send` IS the live primary path — native Codex messaging cannot reach them.]'
47
+ : opencode
48
+ ? '[OPENCODE INBOX — greprag is your agent-to-agent mesh. Inbound delivery is AUTOMATIC here: the greprag plugin arms a relay for this session (nothing to arm or re-arm) and inbound peer messages arrive as injected turns.]'
49
+ : '[⚠ ARM YOUR INBOX WATCHER NOW you are UNREACHABLE until you do. Peers are live in your mesh trying to coordinate with this session, and the operator can DM you but they reach you ONLY while armed. Unarmed, you are invisible and their messages silently pile up. The watcher can DROP ANYTIME (crash / reload / OOM) EVERY turn you are flagged unarmed you MUST re-arm, or coordination breaks.]',
50
+ grok
51
+ ? `ARM (idle wake): Grok \`monitor\` tool, persistent:true, description:"greprag inbox ${env.short}", command: \`${arm}\`. \`--quiet\` is REQUIRED Grok treats stderr as wake events; the Claude bash wrapper is PowerShell-invalid. Use full UUID / 16-hex, never 8-hex. Second session: spawn_subagent (child arms its own watch; parent keeps ONE). Floor: Stop-hook drain injects unread mail even if you never arm. Then \`greprag inbox\`.`
52
+ : codex
53
+ ? 'CODEX: Native Codex tools are the source of truth: `codex_app.list_threads` discovers tasks plus their repo/workspace, and `codex_app.send_message_to_thread` handles Codex-to-Codex coordination. GrepRAG inbox rows for Codex surface through turn hooks only: SessionStart `drain` and UserPromptSubmit `codex-notify`. There is no Codex startup watcher to install. If hooks do not fire, open Codex Desktop Settings -> Settings -> Hooks, trust the GrepRAG commands, start a fresh Codex session, then drain what is waiting: `greprag inbox`.'
54
+ : opencode
55
+ ? 'DELIVERY: the relay arms itself on this session\'s first turn and injects inbound messages as they land — treat an injected `Message from <handle> (session <8hex>):` turn as async peer mail, not the operator typing. Drain anything already waiting: `greprag inbox`.'
56
+ : `ARM: Monitor is a deferred tool — \`ToolSearch select:Monitor\`, then call Monitor with persistent:true, timeout_ms:3600000, description:"greprag inbox ${env.short}", command: \`${arm}\`. The \`--session ${env.short}\` is REQUIRED — a bare \`greprag inbox watch\` writes no pidfile, so the arm-check reads you unarmed forever. NEVER run it bare also because it is a foreground stream and hangs the turn. Then drain what is waiting: \`greprag inbox\`.`,
52
57
  '',
53
58
  // The facts + directives — a flat list (no MODEL/RULES scaffolding; the labels were
54
59
  // human doc-structure, dead weight to an agent). Everything that helps the agent DECIDE
@@ -36,6 +36,16 @@ const child_process_1 = require("child_process");
36
36
  const inbox_watch_1 = require("./inbox-watch");
37
37
  const watcher_registry_1 = require("./watcher-registry");
38
38
  const LOG_PREFIX = '[greprag inbox watch]';
39
+ function watchQuiet() {
40
+ return process.env.GREPRAG_WATCH_QUIET === '1'
41
+ || !!process.env.GROK_SESSION_ID
42
+ || process.argv.includes('--quiet');
43
+ }
44
+ function slog(msg) {
45
+ if (watchQuiet())
46
+ return;
47
+ console.error(msg);
48
+ }
39
49
  // ---- Supervisor-driven watcher liveness (registry freshness) -------------
40
50
  // adr: docs/registry-freshness-design.md
41
51
  //
@@ -107,12 +117,12 @@ exports.FATAL_EXIT_CODE = 64;
107
117
  function installLastResortHandlers() {
108
118
  process.on('uncaughtException', (err) => {
109
119
  const msg = (err && err.message) || String(err);
110
- console.error(`${LOG_PREFIX} uncaughtException: ${msg}`);
120
+ slog(`${LOG_PREFIX} uncaughtException: ${msg}`);
111
121
  process.exit(2);
112
122
  });
113
123
  process.on('unhandledRejection', (reason) => {
114
124
  const msg = (reason && reason.message) || String(reason);
115
- console.error(`${LOG_PREFIX} unhandledRejection: ${msg}`);
125
+ slog(`${LOG_PREFIX} unhandledRejection: ${msg}`);
116
126
  process.exit(2);
117
127
  });
118
128
  }
@@ -263,7 +273,7 @@ async function runSupervisor() {
263
273
  // (`armMonitorCommand`) relaunches a fresh supervisor — turn-free recovery, the
264
274
  // missing rung for supervisor death. adr: adr/monitor-resilience.md
265
275
  const onFatalError = (label) => (err) => {
266
- console.error(`${LOG_PREFIX} supervisor ${label}: ${(err && err.message) || String(err)}`);
276
+ slog(`${LOG_PREFIX} supervisor ${label}: ${(err && err.message) || String(err)}`);
267
277
  process.exit(1); // non-zero (not 0/64) → the relauncher relaunches us
268
278
  };
269
279
  const onUncaught = onFatalError('uncaughtException');
@@ -289,7 +299,7 @@ async function runSupervisor() {
289
299
  // genuinely live sibling. adr: adr/monitor-resilience.md
290
300
  if (session && (0, watcher_registry_1.isLocallyArmed)(session)) {
291
301
  (0, watcher_registry_1.watcherAuditLog)(`arm-skip session=${session} singleton-guard (live sibling pidfile)`);
292
- console.error(`${LOG_PREFIX} session ${session} already has a live watcher — not starting a second.`);
302
+ slog(`${LOG_PREFIX} session ${session} already has a live watcher — not starting a second.`);
293
303
  return;
294
304
  }
295
305
  // Local pidfile (Part 1 of the local-liveness fix): record THIS supervisor's
@@ -333,7 +343,7 @@ async function runSupervisor() {
333
343
  }
334
344
  if (spawnErr || !currentChild) {
335
345
  const reason = spawnErr ? spawnErr.message : 'spawn returned null child';
336
- console.error(`${LOG_PREFIX} supervisor: spawn failed (${reason}) — retry in ${(0, inbox_watch_1.fmtDuration)(backoff)}`);
346
+ slog(`${LOG_PREFIX} supervisor: spawn failed (${reason}) — retry in ${(0, inbox_watch_1.fmtDuration)(backoff)}`);
337
347
  await sleepUntilOrShutdown(backoff, () => shutdownRequested);
338
348
  backoff = Math.min(backoff * SUPERVISOR_BACKOFF_FACTOR, SUPERVISOR_BACKOFF_MAX_MS);
339
349
  continue;
@@ -402,7 +412,7 @@ async function runSupervisor() {
402
412
  const hint = native
403
413
  ? `the child is dying at the OS level (${reason}) before it can run — characteristic of system commit-charge / page-file exhaustion (NOT physical RAM; see docs/session-heap-ceiling.md). Standing down so we stop feeding the exhaustion; the next-turn arm reminder retries once the machine recovers.`
404
414
  : `the child cannot stay up (${reason}). Run \`greprag inbox watch --no-supervise\` to see the underlying error. Standing down to avoid a respawn storm.`;
405
- console.error(`${LOG_PREFIX} supervisor: child crash-looped ${consecutiveFastDeaths}× (each < ${(0, inbox_watch_1.fmtDuration)(CRASH_LOOP_LIFETIME_MS)}) — ${hint}`);
415
+ slog(`${LOG_PREFIX} supervisor: child crash-looped ${consecutiveFastDeaths}× (each < ${(0, inbox_watch_1.fmtDuration)(CRASH_LOOP_LIFETIME_MS)}) — ${hint}`);
406
416
  (0, watcher_registry_1.watcherAuditLog)(`crash-loop session=${session ?? 'tenant'} deaths=${consecutiveFastDeaths} last=${reason}`);
407
417
  terminalReason = `crash-loop-standdown (${reason})`;
408
418
  // Exit FATAL (64) so the bash relauncher BREAKs rather than relaunching us
@@ -411,7 +421,7 @@ async function runSupervisor() {
411
421
  process.exitCode = exports.FATAL_EXIT_CODE;
412
422
  break;
413
423
  }
414
- console.error(`${LOG_PREFIX} supervisor: child died (${reason}) after ${(0, inbox_watch_1.fmtDuration)(liveMs)} — respawning in ${(0, inbox_watch_1.fmtDuration)(backoff)}`);
424
+ slog(`${LOG_PREFIX} supervisor: child died (${reason}) after ${(0, inbox_watch_1.fmtDuration)(liveMs)} — respawning in ${(0, inbox_watch_1.fmtDuration)(backoff)}`);
415
425
  await sleepUntilOrShutdown(backoff, () => shutdownRequested);
416
426
  backoff = Math.min(backoff * SUPERVISOR_BACKOFF_FACTOR, SUPERVISOR_BACKOFF_MAX_MS);
417
427
  }
@@ -54,6 +54,7 @@ exports.getConfig = getConfig;
54
54
  exports.fmtDuration = fmtDuration;
55
55
  exports.parseEventBlock = parseEventBlock;
56
56
  exports.withTruncatedSessions = withTruncatedSessions;
57
+ exports.isWatchQuiet = isWatchQuiet;
57
58
  exports.runInboxWatch = runInboxWatch;
58
59
  const fs = __importStar(require("fs"));
59
60
  const path = __importStar(require("path"));
@@ -166,9 +167,9 @@ function parseEventBlock(block) {
166
167
  return null;
167
168
  return { event, id, data: dataLines.join('\n') };
168
169
  }
169
- /** Truncate every session_id field on a StreamMessage to its 8-hex form.
170
- * Used for both the JSON --json line and the pretty-printed display.
171
- * Exported for tests. adr: adr/session-id-awareness.md */
170
+ /** Truncate every session_id field on a StreamMessage to its short form
171
+ * (8-hex, or 16-hex for UUIDv7). Used for both the JSON --json line and
172
+ * the pretty-printed display. Exported for tests. adr: adr/session-id-awareness.md */
172
173
  function withTruncatedSessions(msg) {
173
174
  const fromShort = (0, session_id_1.truncateSessionId)(msg.from.session_id ?? null);
174
175
  const toShort = (0, session_id_1.truncateSessionId)(msg.to_session_id ?? null);
@@ -347,6 +348,9 @@ async function readStream(url, apiKey, signal, initialId, json, idleTimeoutMs, s
347
348
  catch {
348
349
  continue;
349
350
  }
351
+ if (session && msg.to_session_id && !(0, session_id_1.sameSessionId)(session, msg.to_session_id)) {
352
+ continue;
353
+ }
350
354
  const displayMsg = withTruncatedSessions(msg);
351
355
  emitDmReplyDirective(displayMsg, session);
352
356
  if (json)
@@ -362,14 +366,14 @@ async function readStream(url, apiKey, signal, initialId, json, idleTimeoutMs, s
362
366
  const data = JSON.parse(ev.data || '{}');
363
367
  if (typeof data.count === 'number' && data.count > 0) {
364
368
  const plural = data.count === 1 ? '' : 's';
365
- console.error(`${LOG_PREFIX} replayed ${data.count} missed row${plural}, resuming live tail`);
369
+ watchErr(`${LOG_PREFIX} replayed ${data.count} missed row${plural}, resuming live tail`);
366
370
  }
367
371
  }
368
372
  catch { /* malformed control payload — ignore */ }
369
373
  }
370
374
  else if (ev.event === 'error') {
371
375
  // Surface server-side error then let the loop reconnect.
372
- console.error(`${LOG_PREFIX} server error: ${ev.data}`);
376
+ watchErr(`${LOG_PREFIX} server error: ${ev.data}`);
373
377
  return lastId;
374
378
  }
375
379
  // "open" event is informational — ignored at print time.
@@ -423,13 +427,30 @@ function buildUrl(apiUrl, project, session, since, receptionist, assistant, mech
423
427
  * carries its stdout into chat is destroyed on reload, by design). See
424
428
  * adr/monitor-resilience.md (2026-06-02 + 2026-06-03 entries).
425
429
  * adr: adr/monitor-resilience.md */
430
+ function isWatchQuiet(opts) {
431
+ if (opts?.quiet)
432
+ return true;
433
+ if (process.env.GREPRAG_WATCH_QUIET === '1')
434
+ return true;
435
+ if (process.env.GROK_SESSION_ID)
436
+ return true;
437
+ return process.argv.includes('--quiet');
438
+ }
439
+ function watchErr(msg, opts) {
440
+ if (isWatchQuiet(opts))
441
+ return;
442
+ console.error(msg);
443
+ }
426
444
  async function runInboxWatch(opts) {
445
+ const quiet = isWatchQuiet(opts);
446
+ if (quiet)
447
+ process.env.GREPRAG_WATCH_QUIET = '1';
427
448
  const directMode = !!opts.signal || !!opts.noSupervise;
428
449
  if (directMode)
429
- return runWatchLoop(opts);
450
+ return runWatchLoop({ ...opts, quiet });
430
451
  if (process.env[inbox_watch_supervisor_1.SUPERVISE_ENV] === '1') {
431
452
  (0, inbox_watch_supervisor_1.installLastResortHandlers)();
432
- return runWatchLoop(opts);
453
+ return runWatchLoop({ ...opts, quiet });
433
454
  }
434
455
  return (0, inbox_watch_supervisor_1.runSupervisor)();
435
456
  }
@@ -446,7 +467,7 @@ async function runInboxWatch(opts) {
446
467
  * the deleted `while true` wrapper's job, done right: the watcher notices its
447
468
  * own consumer is gone instead of an external shell keeping it alive forever.
448
469
  * adr: adr/monitor-resilience.md */
449
- function bindToConsumer(onGone) {
470
+ function bindToConsumer(onGone, quiet = false) {
450
471
  let gone = false;
451
472
  const trip = () => {
452
473
  if (gone)
@@ -457,26 +478,29 @@ function bindToConsumer(onGone) {
457
478
  const onErr = () => trip();
458
479
  process.stdout.on('error', onErr);
459
480
  process.stderr.on('error', onErr);
460
- // A single NUL byte on stderr the diagnostic channel, never surfaced as a
461
- // Monitor notification (those key on stdout JSON lines), and invisible in
462
- // logs. The write itself IS the probe: its callback fires with an error (and
463
- // the stream emits 'error') when the read end has closed, i.e. the consumer is
464
- // gone. Harmless while the pipe is alive.
465
- const probe = setInterval(() => {
466
- try {
467
- process.stderr.write(String.fromCharCode(0), (err) => { if (err)
468
- trip(); });
469
- }
470
- catch {
471
- trip();
472
- }
473
- }, CONSUMER_PROBE_MS);
474
- if (typeof probe.unref === 'function')
475
- probe.unref();
481
+ // Claude Monitor keys notifications on stdout JSON; a NUL on stderr is
482
+ // invisible there. Grok monitor merges stderr into wake events, so the
483
+ // probe would ping the model every 30s and trip Grok's volume killer.
484
+ // adr: adr/grok-platform.md
485
+ let probe = null;
486
+ if (!quiet) {
487
+ probe = setInterval(() => {
488
+ try {
489
+ process.stderr.write(String.fromCharCode(0), (err) => { if (err)
490
+ trip(); });
491
+ }
492
+ catch {
493
+ trip();
494
+ }
495
+ }, CONSUMER_PROBE_MS);
496
+ if (typeof probe.unref === 'function')
497
+ probe.unref();
498
+ }
476
499
  return {
477
500
  gone: () => gone,
478
501
  dispose: () => {
479
- clearInterval(probe);
502
+ if (probe)
503
+ clearInterval(probe);
480
504
  process.stdout.removeListener('error', onErr);
481
505
  process.stderr.removeListener('error', onErr);
482
506
  },
@@ -514,11 +538,11 @@ async function runWatchLoop(opts) {
514
538
  // the consuming session is gone — abort and exit terminal so we don't orphan.
515
539
  // Skip in SDK/test mode (opts.signal present): those callers own the process
516
540
  // and their pipes aren't a Monitor consumer.
517
- const consumer = opts.signal ? null : bindToConsumer(() => controller.abort());
541
+ const consumer = opts.signal ? null : bindToConsumer(() => controller.abort(), isWatchQuiet(opts));
518
542
  while (!controller.signal.aborted) {
519
543
  if (!isFirstAttempt) {
520
544
  const lastSeen = cursor ? cursor.slice(0, 8) : 'none';
521
- console.error(`${LOG_PREFIX} reconnecting (last_seen_id=${lastSeen})`);
545
+ watchErr(`${LOG_PREFIX} reconnecting (last_seen_id=${lastSeen})`, opts);
522
546
  }
523
547
  isFirstAttempt = false;
524
548
  const url = buildUrl(cfg.apiUrl, opts.project, opts.session, cursor, !!opts.receptionist, !!opts.assistant, !!opts.mechanic);
@@ -543,11 +567,11 @@ async function runWatchLoop(opts) {
543
567
  // (no supervisor) still sees a non-zero exit.
544
568
  const m = /^HTTP (4\d\d)/.exec(msg);
545
569
  if (m) {
546
- console.error(`${LOG_PREFIX} ${msg}`);
570
+ watchErr(`${LOG_PREFIX} ${msg}`, opts);
547
571
  process.exit(inbox_watch_supervisor_1.FATAL_EXIT_CODE);
548
572
  }
549
573
  const lastSeen = cursor ? cursor.slice(0, 8) : 'none';
550
- console.error(`${LOG_PREFIX} disconnect: ${msg} — retrying in ${fmtDuration(backoff)} (last_seen_id=${lastSeen})`);
574
+ watchErr(`${LOG_PREFIX} disconnect: ${msg} — retrying in ${fmtDuration(backoff)} (last_seen_id=${lastSeen})`, opts);
551
575
  }
552
576
  if (controller.signal.aborted)
553
577
  break;
@@ -40,6 +40,7 @@ var __importStar = (this && this.__importStar) || (function () {
40
40
  Object.defineProperty(exports, "__esModule", { value: true });
41
41
  exports.GREPRAG_SECTIONS = void 0;
42
42
  exports.runInit = runInit;
43
+ exports.applyGrokHooks = applyGrokHooks;
43
44
  exports.applyCodexHooks = applyCodexHooks;
44
45
  exports.applySettings = applySettings;
45
46
  exports.patchClaudeMdContent = patchClaudeMdContent;
@@ -140,6 +141,13 @@ function detectAgentTargets(cwd = process.cwd()) {
140
141
  opencodeEvidence.push('~/.config/opencode exists');
141
142
  if (opencodeEvidence.length > 0)
142
143
  detections.push({ target: 'opencode', evidence: opencodeEvidence });
144
+ const grokEvidence = [];
145
+ if (process.env.GROK_SESSION_ID || process.env.GROK_HOOK_EVENT)
146
+ grokEvidence.push('GROK_* environment is set');
147
+ if (home && fs.existsSync(path.join(home, '.grok')))
148
+ grokEvidence.push('~/.grok exists');
149
+ if (grokEvidence.length > 0)
150
+ detections.push({ target: 'grok', evidence: grokEvidence });
143
151
  return detections;
144
152
  }
145
153
  function describeTarget(target) {
@@ -147,6 +155,8 @@ function describeTarget(target) {
147
155
  return 'Claude Code';
148
156
  if (target === 'codex')
149
157
  return 'Codex';
158
+ if (target === 'grok')
159
+ return 'Grok Build';
150
160
  return 'OpenCode';
151
161
  }
152
162
  function explicitInitHint() {
@@ -155,6 +165,7 @@ function explicitInitHint() {
155
165
  ' greprag init --codex --tenant-id <your-handle>',
156
166
  ' greprag init --claude --tenant-id <your-handle>',
157
167
  ' greprag init --opencode --tenant-id <your-handle>',
168
+ ' greprag init --grok --tenant-id <your-handle>',
158
169
  ].join('\n');
159
170
  }
160
171
  function normalizeHandle(raw) {
@@ -220,13 +231,16 @@ async function chooseInitTarget() {
220
231
  console.log(' 1. Codex');
221
232
  console.log(' 2. Claude Code');
222
233
  console.log(' 3. OpenCode');
223
- const answer = (await prompt(' Choose 1, 2, or 3: ')).trim().toLowerCase();
234
+ console.log(' 4. Grok Build');
235
+ const answer = (await prompt(' Choose 1, 2, 3, or 4: ')).trim().toLowerCase();
224
236
  if (answer === '1' || answer === 'codex')
225
237
  return 'codex';
226
238
  if (answer === '2' || answer === 'claude' || answer === 'claude code')
227
239
  return 'claude';
228
240
  if (answer === '3' || answer === 'opencode' || answer === 'open code')
229
241
  return 'opencode';
242
+ if (answer === '4' || answer === 'grok' || answer === 'grok build')
243
+ return 'grok';
230
244
  console.error(' Error: no agent target selected.');
231
245
  console.error(explicitInitHint());
232
246
  process.exit(1);
@@ -247,9 +261,9 @@ async function runInit(opts) {
247
261
  if (opts.global) {
248
262
  return runGlobalInit(opts.name);
249
263
  }
250
- const explicitTargets = [opts.claude, opts.codex, opts.opencode].filter(Boolean).length;
264
+ const explicitTargets = [opts.claude, opts.codex, opts.opencode, opts.grok].filter(Boolean).length;
251
265
  if (explicitTargets > 1) {
252
- console.error(' Error: choose only one target: --claude, --codex, or --opencode.');
266
+ console.error(' Error: choose only one target: --claude, --codex, --opencode, or --grok.');
253
267
  process.exit(1);
254
268
  }
255
269
  // --opencode: configure for OpenCode instead of Claude Code.
@@ -260,12 +274,17 @@ async function runInit(opts) {
260
274
  if (opts.codex) {
261
275
  return runCodexInit(opts);
262
276
  }
277
+ if (opts.grok) {
278
+ return runGrokInit(opts);
279
+ }
263
280
  if (!opts.claude) {
264
281
  const target = await chooseInitTarget();
265
282
  if (target === 'codex')
266
283
  return runCodexInit(opts);
267
284
  if (target === 'opencode')
268
285
  return runOpenCodeInit(opts);
286
+ if (target === 'grok')
287
+ return runGrokInit(opts);
269
288
  }
270
289
  console.log('\n greprag init — Setting up agent memory for Claude Code\n');
271
290
  // Step 1: Get API key
@@ -640,6 +659,146 @@ async function runCodexInit(opts) {
640
659
  console.log(' Inbox messages for Codex surface on SessionStart and UserPromptSubmit hook boundaries.');
641
660
  console.log(' Hooks will activate after the Codex host restart; starting only a new task is not sufficient after hook changes.\n');
642
661
  }
662
+ const GROK_RULES = `# GrepRAG on Grok Build
663
+
664
+ Before other work this session:
665
+ 1. If \`~/.greprag/grok-context/<16-hex>.md\` exists, Read it. Else run \`greprag memory recap\`.
666
+ 2. Session id is \`$GROK_SESSION_ID\` (UUIDv7). Short form is **16 hex**, not 8. 8-hex collides for ~65s. Send with \`--from-session $GROK_SESSION_ID\` (full UUID).
667
+ 3. Idle wake: Grok \`monitor\` persistent:true, command:
668
+ \`greprag inbox watch --session $GROK_SESSION_ID --json --quiet\`
669
+ Not Claude Monitor. No bash while-true. \`--quiet\` required (stderr is a wake).
670
+ If a greprag inbox monitor is already listed in this session, do not start another. Instant exit = already armed, not a crash.
671
+ 4. Second session: \`spawn_subagent\` background=true. Child arms its own quiet watch with ITS \`$GROK_SESSION_ID\`. Parent sends:
672
+ \`greprag send "…" --to travis@greprag.com/<child-full-uuid> --from-session $GROK_SESSION_ID\`
673
+ Parent keeps ONE watch, for itself. Child watch events may appear on the parent TUI — resume the child to act. Do not arm a second parent watch.
674
+ `;
675
+ function getGrokHooksPath() {
676
+ const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
677
+ return path.join(home, '.grok', 'hooks', 'greprag.json');
678
+ }
679
+ function getGrokRulesPath() {
680
+ const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
681
+ return path.join(home, '.grok', 'rules', 'greprag.md');
682
+ }
683
+ function grokCommand(sub, timeout) {
684
+ const script = path.resolve(__dirname, '..', 'hook.js');
685
+ const command = fs.existsSync(script)
686
+ ? `node ${shellQuote(script)} ${sub}`
687
+ : (process.platform === 'win32' ? `greprag-hook.cmd ${sub}` : `greprag-hook ${sub}`);
688
+ return { type: 'command', command, timeout };
689
+ }
690
+ function applyGrokHooks(config) {
691
+ const changes = [];
692
+ if (!config.hooks)
693
+ config.hooks = {};
694
+ const add = (event, matcher, sub, timeout, label) => {
695
+ if (hasGrepragHook(config.hooks[event], sub)) {
696
+ changes.push(`Grok ${event} ${label} already configured (skipped)`);
697
+ return;
698
+ }
699
+ if (!config.hooks[event])
700
+ config.hooks[event] = [];
701
+ config.hooks[event].push({ matcher, hooks: [grokCommand(sub, timeout)] });
702
+ changes.push(`Added Grok ${event} hook (${label})`);
703
+ };
704
+ add('SessionStart', 'startup|resume', 'recap', 15, 'memory recap');
705
+ add('SessionStart', 'startup|resume', 'session-id', 5, 'session-id');
706
+ add('SessionStart', 'startup|resume', 'drain', 8, 'inbox drain');
707
+ add('UserPromptSubmit', '', 'notify', 8, 'prompt cache');
708
+ add('PreToolUse', 'Bash|run_terminal_command', 'crush-wrap', 5, 'crush-wrap');
709
+ add('PreToolUse', '*', 'guard', 5, 'guard');
710
+ add('Stop', '', 'store', 15, 'turn capture');
711
+ add('Stop', '', 'drain', 8, 'inbox drain');
712
+ add('PostCompact', '', 'session-id', 5, 'session-id');
713
+ return changes;
714
+ }
715
+ function writeGrokRules() {
716
+ const file = getGrokRulesPath();
717
+ fs.mkdirSync(path.dirname(file), { recursive: true });
718
+ if (fs.existsSync(file) && fs.readFileSync(file, 'utf-8') === GROK_RULES) {
719
+ return 'Grok rules already current';
720
+ }
721
+ fs.writeFileSync(file, GROK_RULES);
722
+ return `Wrote ${file}`;
723
+ }
724
+ /** greprag init --grok
725
+ * Dedicated Grok Build adapter. Does not ride ~/.claude/settings.json.
726
+ * adr: adr/grok-platform.md */
727
+ async function runGrokInit(opts) {
728
+ console.log('\n greprag init — Setting up agent memory for Grok Build\n');
729
+ let apiKey = opts.apiKey || readSharedApiKey();
730
+ if (!apiKey) {
731
+ const tenantId = await resolveProvisionHandle(opts);
732
+ console.log(` Provisioning API key for public handle ${tenantId}@greprag.com...`);
733
+ const result = await provision(tenantId);
734
+ if (!result.ok) {
735
+ console.error(` Error: ${result.error}`);
736
+ process.exit(1);
737
+ }
738
+ apiKey = result.apiKey;
739
+ console.log(` API key created: ${apiKey.slice(0, 17)}...`);
740
+ }
741
+ console.log(' Validating API key...');
742
+ const valid = await validateKey(apiKey);
743
+ if (!valid) {
744
+ console.error(' Error: API key validation failed.');
745
+ process.exit(1);
746
+ }
747
+ console.log(' Key validated against api.greprag.com');
748
+ const changes = [];
749
+ writeGrepragEnv(apiKey);
750
+ changes.push('Wrote ~/.greprag/.env for Grok hooks');
751
+ await cacheIdentity(apiKey);
752
+ const anchor = (0, project_anchor_1.ensureAnchor)(process.cwd());
753
+ if (anchor.source === 'git') {
754
+ changes.push(`Project anchor: ${anchor.projectName} (${anchor.projectId.slice(0, 8)}) — derived from git history`);
755
+ }
756
+ else {
757
+ changes.push(`Project anchor: ${anchor.projectName} (${anchor.projectId.slice(0, 8)}) — stored in .greprag/project.json`);
758
+ const gitignoreResult = ensureAnchorTrackable(process.cwd());
759
+ if (gitignoreResult)
760
+ changes.push(gitignoreResult);
761
+ }
762
+ const globalAnchor = (0, project_anchor_1.ensureGlobalAnchor)();
763
+ changes.push(`Global anchor: ${globalAnchor.projectName} (${globalAnchor.projectId.slice(0, 8)})`);
764
+ writeProjectPath(anchor.projectName, process.cwd());
765
+ changes.push(`Registered repo path for ${anchor.projectName} → ~/.greprag/projects.json`);
766
+ const grokSkill = installCoreSkill('grok');
767
+ if (grokSkill)
768
+ changes.push(`Skill: installed → ${grokSkill}`);
769
+ changes.push(writeGrokRules());
770
+ try {
771
+ const res = await fetch(`${API_URL}/v1/inbox/projects/register`, {
772
+ method: 'POST',
773
+ headers: {
774
+ 'Authorization': `Bearer ${apiKey}`,
775
+ 'Content-Type': 'application/json',
776
+ },
777
+ body: JSON.stringify({ project_id: anchor.projectId, project_name: anchor.projectName }),
778
+ });
779
+ const reg = await res.json();
780
+ changes.push(reg.ok ? 'Project registered for inbox addressing' : `Inbox registration: ${reg.error || 'failed'}`);
781
+ }
782
+ catch {
783
+ changes.push('Inbox registration skipped (network)');
784
+ }
785
+ const hooksPath = getGrokHooksPath();
786
+ const hooks = readCodexHooks(hooksPath);
787
+ const hookChanges = applyGrokHooks(hooks);
788
+ writeCodexHooks(hooksPath, hooks);
789
+ changes.push(...hookChanges);
790
+ console.log('\n Setup complete!\n');
791
+ for (const change of changes) {
792
+ console.log(` - ${change}`);
793
+ }
794
+ console.log(`\n Grok hooks file: ${hooksPath}`);
795
+ console.log(` Grok rules: ${getGrokRulesPath()}`);
796
+ console.log(` Project anchor: ${anchor.anchorPath}`);
797
+ console.log(' Reload hooks in Grok (/hooks then r) or start a fresh session.');
798
+ console.log(' Recap lands in ~/.greprag/grok-context/<16-hex>.md — Grok ignores SessionStart stdout.');
799
+ console.log(' Idle inbox: Grok monitor + `greprag inbox watch --session $GROK_SESSION_ID --json --quiet`.');
800
+ console.log(' Second session: spawn_subagent; child arms its own watch; parent keeps one.\n');
801
+ }
643
802
  /** greprag init --global
644
803
  * Creates ~/.greprag/project.json with a stable UUID.
645
804
  * Designed for Cowork / any agent session whose cwd has no repo-level anchor.
@@ -979,7 +1138,7 @@ function getSkillRoot() {
979
1138
  return path.join(__dirname, '../../skill');
980
1139
  }
981
1140
  function skillTargetRoot(target) {
982
- const dir = target === 'codex' ? '.codex' : '.claude';
1141
+ const dir = target === 'codex' ? '.codex' : target === 'grok' ? '.grok' : '.claude';
983
1142
  return path.join(os.homedir(), dir, 'skills');
984
1143
  }
985
1144
  function installCoreSkill(target) {
@@ -68,6 +68,7 @@ exports.readPipeWrapConfig = readPipeWrapConfig;
68
68
  exports.runPipeWrap = runPipeWrap;
69
69
  const fs = __importStar(require("fs"));
70
70
  const project_anchor_1 = require("../project-anchor");
71
+ const hook_runtime_1 = require("../hook-runtime");
71
72
  // ---------- Structural safety gates (pure) ----------------------------------
72
73
  /** True when single/double quotes balance with bash-style escaping
73
74
  * (backslash escapes inside double quotes and bare text; not inside
@@ -282,11 +283,11 @@ function readPipeWrapConfig(cwd) {
282
283
  enabled = false;
283
284
  return { enabled };
284
285
  }
285
- /** PreToolUse[Bash] — returns the updatedInput envelope when the command
286
- * qualifies for wrapping, null for silence. NO permissionDecision: the
287
- * rewritten command still goes through the normal permission flow. */
286
+ /** PreToolUse[Bash / run_terminal_command] — returns the updatedInput envelope
287
+ * when the command qualifies for wrapping, null for silence. NO permissionDecision:
288
+ * the rewritten command still goes through the normal permission flow. */
288
289
  function runPipeWrap(input) {
289
- if (input.tool_name !== 'Bash')
290
+ if (!(0, hook_runtime_1.isShellTool)(input.tool_name))
290
291
  return null;
291
292
  const command = input.tool_input?.command;
292
293
  if (typeof command !== 'string' || !command.trim())
@@ -317,7 +318,7 @@ function runPipeWrap(input) {
317
318
  return {
318
319
  hookSpecificOutput: {
319
320
  hookEventName: 'PreToolUse',
320
- updatedInput: { command: rewriteCommand(target, decision) },
321
+ updatedInput: { ...(input.tool_input || {}), command: rewriteCommand(target, decision) },
321
322
  },
322
323
  };
323
324
  }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.hazardousRgTargets = hazardousRgTargets;
4
4
  exports.buildSearchGuardReason = buildSearchGuardReason;
5
5
  exports.runSearchGuard = runSearchGuard;
6
+ const hook_runtime_1 = require("../hook-runtime");
6
7
  const HAZARDOUS_DIRS = new Set([
7
8
  '.next',
8
9
  '.turbo',
@@ -151,7 +152,7 @@ function buildSearchGuardReason(targets) {
151
152
  ].join(' ');
152
153
  }
153
154
  function runSearchGuard(input) {
154
- if (input.tool_name !== 'Bash')
155
+ if (!(0, hook_runtime_1.isShellTool)(input.tool_name))
155
156
  return null;
156
157
  const command = input.tool_input?.command;
157
158
  if (typeof command !== 'string' || !command.trim())