greprag 5.74.21 → 5.76.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.
@@ -50,14 +50,17 @@ var __importStar = (this && this.__importStar) || (function () {
50
50
  })();
51
51
  Object.defineProperty(exports, "__esModule", { value: true });
52
52
  exports.CONSUMER_GONE_EXIT_CODE = void 0;
53
+ exports.resolveWatchPlatform = resolveWatchPlatform;
53
54
  exports.getConfig = getConfig;
54
55
  exports.fmtDuration = fmtDuration;
55
56
  exports.parseEventBlock = parseEventBlock;
56
57
  exports.withTruncatedSessions = withTruncatedSessions;
58
+ exports.isWatchQuiet = isWatchQuiet;
57
59
  exports.runInboxWatch = runInboxWatch;
58
60
  const fs = __importStar(require("fs"));
59
61
  const path = __importStar(require("path"));
60
62
  const session_id_1 = require("../session-id");
63
+ const harness_1 = require("../harness");
61
64
  const mechanic_friction_1 = require("../mechanic-friction");
62
65
  const inbox_watch_supervisor_1 = require("./inbox-watch-supervisor");
63
66
  const API_URL_DEFAULT = 'https://api.greprag.com';
@@ -83,6 +86,14 @@ exports.CONSUMER_GONE_EXIT_CODE = 65;
83
86
  // than waiting for the next real message that may never come. Env-overridable
84
87
  // for tests (the idle path otherwise takes the full 30s to observe).
85
88
  const CONSUMER_PROBE_MS = Number(process.env.GREPRAG_WATCH_PROBE_MS) || 30_000;
89
+ const WATCH_PLATFORMS = new Set(['claude-code', 'codex', 'opencode', 'grok']);
90
+ /** Platform tag for this watch attach. Explicit opt wins; else infer. */
91
+ function resolveWatchPlatform(explicit) {
92
+ if (explicit && WATCH_PLATFORMS.has(explicit))
93
+ return explicit;
94
+ const h = (0, harness_1.inferCurrentHarness)();
95
+ return (h && WATCH_PLATFORMS.has(h)) ? h : undefined;
96
+ }
86
97
  // -- Config (mirrors the loader in index.ts/discover.ts) -------------------
87
98
  function loadEnvFile(filePath) {
88
99
  try {
@@ -166,9 +177,9 @@ function parseEventBlock(block) {
166
177
  return null;
167
178
  return { event, id, data: dataLines.join('\n') };
168
179
  }
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 */
180
+ /** Truncate every session_id field on a StreamMessage to its short form
181
+ * (8-hex, or 16-hex for UUIDv7). Used for both the JSON --json line and
182
+ * the pretty-printed display. Exported for tests. adr: adr/session-id-awareness.md */
172
183
  function withTruncatedSessions(msg) {
173
184
  const fromShort = (0, session_id_1.truncateSessionId)(msg.from.session_id ?? null);
174
185
  const toShort = (0, session_id_1.truncateSessionId)(msg.to_session_id ?? null);
@@ -347,6 +358,9 @@ async function readStream(url, apiKey, signal, initialId, json, idleTimeoutMs, s
347
358
  catch {
348
359
  continue;
349
360
  }
361
+ if (session && msg.to_session_id && !(0, session_id_1.sameSessionId)(session, msg.to_session_id)) {
362
+ continue;
363
+ }
350
364
  const displayMsg = withTruncatedSessions(msg);
351
365
  emitDmReplyDirective(displayMsg, session);
352
366
  if (json)
@@ -362,14 +376,14 @@ async function readStream(url, apiKey, signal, initialId, json, idleTimeoutMs, s
362
376
  const data = JSON.parse(ev.data || '{}');
363
377
  if (typeof data.count === 'number' && data.count > 0) {
364
378
  const plural = data.count === 1 ? '' : 's';
365
- console.error(`${LOG_PREFIX} replayed ${data.count} missed row${plural}, resuming live tail`);
379
+ watchErr(`${LOG_PREFIX} replayed ${data.count} missed row${plural}, resuming live tail`);
366
380
  }
367
381
  }
368
382
  catch { /* malformed control payload — ignore */ }
369
383
  }
370
384
  else if (ev.event === 'error') {
371
385
  // Surface server-side error then let the loop reconnect.
372
- console.error(`${LOG_PREFIX} server error: ${ev.data}`);
386
+ watchErr(`${LOG_PREFIX} server error: ${ev.data}`);
373
387
  return lastId;
374
388
  }
375
389
  // "open" event is informational — ignored at print time.
@@ -388,7 +402,7 @@ async function readStream(url, apiKey, signal, initialId, json, idleTimeoutMs, s
388
402
  signal.removeEventListener('abort', onOuterAbort);
389
403
  }
390
404
  }
391
- function buildUrl(apiUrl, project, session, since, receptionist, assistant, mechanic) {
405
+ function buildUrl(apiUrl, project, session, since, receptionist, assistant, mechanic, platform) {
392
406
  const u = new URL(apiUrl.replace(/\/+$/, '') + '/v1/inbox/stream');
393
407
  if (project)
394
408
  u.searchParams.set('project', project);
@@ -402,6 +416,8 @@ function buildUrl(apiUrl, project, session, since, receptionist, assistant, mech
402
416
  u.searchParams.set('role', 'mechanic');
403
417
  else if (assistant)
404
418
  u.searchParams.set('role', 'assistant');
419
+ if (platform)
420
+ u.searchParams.set('platform', platform);
405
421
  return u.toString();
406
422
  }
407
423
  /** Public entry for `greprag inbox watch`. Dispatches by mode:
@@ -423,13 +439,30 @@ function buildUrl(apiUrl, project, session, since, receptionist, assistant, mech
423
439
  * carries its stdout into chat is destroyed on reload, by design). See
424
440
  * adr/monitor-resilience.md (2026-06-02 + 2026-06-03 entries).
425
441
  * adr: adr/monitor-resilience.md */
442
+ function isWatchQuiet(opts) {
443
+ if (opts?.quiet)
444
+ return true;
445
+ if (process.env.GREPRAG_WATCH_QUIET === '1')
446
+ return true;
447
+ if (process.env.GROK_SESSION_ID)
448
+ return true;
449
+ return process.argv.includes('--quiet');
450
+ }
451
+ function watchErr(msg, opts) {
452
+ if (isWatchQuiet(opts))
453
+ return;
454
+ console.error(msg);
455
+ }
426
456
  async function runInboxWatch(opts) {
457
+ const quiet = isWatchQuiet(opts);
458
+ if (quiet)
459
+ process.env.GREPRAG_WATCH_QUIET = '1';
427
460
  const directMode = !!opts.signal || !!opts.noSupervise;
428
461
  if (directMode)
429
- return runWatchLoop(opts);
462
+ return runWatchLoop({ ...opts, quiet });
430
463
  if (process.env[inbox_watch_supervisor_1.SUPERVISE_ENV] === '1') {
431
464
  (0, inbox_watch_supervisor_1.installLastResortHandlers)();
432
- return runWatchLoop(opts);
465
+ return runWatchLoop({ ...opts, quiet });
433
466
  }
434
467
  return (0, inbox_watch_supervisor_1.runSupervisor)();
435
468
  }
@@ -446,7 +479,7 @@ async function runInboxWatch(opts) {
446
479
  * the deleted `while true` wrapper's job, done right: the watcher notices its
447
480
  * own consumer is gone instead of an external shell keeping it alive forever.
448
481
  * adr: adr/monitor-resilience.md */
449
- function bindToConsumer(onGone) {
482
+ function bindToConsumer(onGone, quiet = false) {
450
483
  let gone = false;
451
484
  const trip = () => {
452
485
  if (gone)
@@ -457,26 +490,29 @@ function bindToConsumer(onGone) {
457
490
  const onErr = () => trip();
458
491
  process.stdout.on('error', onErr);
459
492
  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();
493
+ // Claude Monitor keys notifications on stdout JSON; a NUL on stderr is
494
+ // invisible there. Grok monitor merges stderr into wake events, so the
495
+ // probe would ping the model every 30s and trip Grok's volume killer.
496
+ // adr: adr/grok-platform.md
497
+ let probe = null;
498
+ if (!quiet) {
499
+ probe = setInterval(() => {
500
+ try {
501
+ process.stderr.write(String.fromCharCode(0), (err) => { if (err)
502
+ trip(); });
503
+ }
504
+ catch {
505
+ trip();
506
+ }
507
+ }, CONSUMER_PROBE_MS);
508
+ if (typeof probe.unref === 'function')
509
+ probe.unref();
510
+ }
476
511
  return {
477
512
  gone: () => gone,
478
513
  dispose: () => {
479
- clearInterval(probe);
514
+ if (probe)
515
+ clearInterval(probe);
480
516
  process.stdout.removeListener('error', onErr);
481
517
  process.stderr.removeListener('error', onErr);
482
518
  },
@@ -514,14 +550,14 @@ async function runWatchLoop(opts) {
514
550
  // the consuming session is gone — abort and exit terminal so we don't orphan.
515
551
  // Skip in SDK/test mode (opts.signal present): those callers own the process
516
552
  // and their pipes aren't a Monitor consumer.
517
- const consumer = opts.signal ? null : bindToConsumer(() => controller.abort());
553
+ const consumer = opts.signal ? null : bindToConsumer(() => controller.abort(), isWatchQuiet(opts));
518
554
  while (!controller.signal.aborted) {
519
555
  if (!isFirstAttempt) {
520
556
  const lastSeen = cursor ? cursor.slice(0, 8) : 'none';
521
- console.error(`${LOG_PREFIX} reconnecting (last_seen_id=${lastSeen})`);
557
+ watchErr(`${LOG_PREFIX} reconnecting (last_seen_id=${lastSeen})`, opts);
522
558
  }
523
559
  isFirstAttempt = false;
524
- const url = buildUrl(cfg.apiUrl, opts.project, opts.session, cursor, !!opts.receptionist, !!opts.assistant, !!opts.mechanic);
560
+ const url = buildUrl(cfg.apiUrl, opts.project, opts.session, cursor, !!opts.receptionist, !!opts.assistant, !!opts.mechanic, resolveWatchPlatform(opts.platform));
525
561
  try {
526
562
  const lastId = await readStream(url, cfg.apiKey, controller.signal, cursor, !!opts.json, idleTimeoutMs, opts.session);
527
563
  if (lastId)
@@ -543,11 +579,11 @@ async function runWatchLoop(opts) {
543
579
  // (no supervisor) still sees a non-zero exit.
544
580
  const m = /^HTTP (4\d\d)/.exec(msg);
545
581
  if (m) {
546
- console.error(`${LOG_PREFIX} ${msg}`);
582
+ watchErr(`${LOG_PREFIX} ${msg}`, opts);
547
583
  process.exit(inbox_watch_supervisor_1.FATAL_EXIT_CODE);
548
584
  }
549
585
  const lastSeen = cursor ? cursor.slice(0, 8) : 'none';
550
- console.error(`${LOG_PREFIX} disconnect: ${msg} — retrying in ${fmtDuration(backoff)} (last_seen_id=${lastSeen})`);
586
+ watchErr(`${LOG_PREFIX} disconnect: ${msg} — retrying in ${fmtDuration(backoff)} (last_seen_id=${lastSeen})`, opts);
551
587
  }
552
588
  if (controller.signal.aborted)
553
589
  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,156 @@ 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. Chip: \`greprag load grok-chip-spawn\` then \`greprag grok spawn\` (real TUI + inbox). Helper: \`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
+ 5. Tool shell: stdin redirected + \`-NonInteractive\`. \`Read-Host\` throws. \`Start-Process\` windows die with the tool Job Object. Secret paste: \`~/.claude/scripts/prompt-secret.ps1\`. Sticky console: \`schtasks /Create … /IT\` then \`schtasks /Run\`. npm Hello: \`npm login --auth-type=web\` in that window; browser Use security key; page token ≠ Hello PIN.
675
+ `;
676
+ function getGrokHooksPath() {
677
+ const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
678
+ return path.join(home, '.grok', 'hooks', 'greprag.json');
679
+ }
680
+ function getGrokRulesPath() {
681
+ const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
682
+ return path.join(home, '.grok', 'rules', 'greprag.md');
683
+ }
684
+ function grokCommand(sub, timeout) {
685
+ const script = path.resolve(__dirname, '..', 'hook.js');
686
+ const command = fs.existsSync(script)
687
+ ? `node ${shellQuote(script)} ${sub}`
688
+ : (process.platform === 'win32' ? `greprag-hook.cmd ${sub}` : `greprag-hook ${sub}`);
689
+ return { type: 'command', command, timeout };
690
+ }
691
+ function applyGrokHooks(config) {
692
+ const changes = [];
693
+ if (!config.hooks)
694
+ config.hooks = {};
695
+ // Empty matcher = every SessionStart source (startup, resume, /new, dashboard
696
+ // dispatch, fork). `startup|resume` missed /new and dashboard agents, so this
697
+ // session never got a recap sidecar. adr: adr/grok-platform.md
698
+ const sessionStartMatcher = '';
699
+ if (!config.hooks.SessionStart)
700
+ config.hooks.SessionStart = [];
701
+ const retargeted = ['recap', 'session-id', 'drain'].reduce((n, sub) => (n + retargetGrepragHookMatcher(config.hooks.SessionStart, sub, ['startup|resume'], sessionStartMatcher)), 0);
702
+ if (retargeted)
703
+ changes.push(`Retargeted Grok SessionStart matcher (${retargeted})`);
704
+ const add = (event, matcher, sub, timeout, label) => {
705
+ if (hasGrepragHook(config.hooks[event], sub)) {
706
+ changes.push(`Grok ${event} ${label} already configured (skipped)`);
707
+ return;
708
+ }
709
+ if (!config.hooks[event])
710
+ config.hooks[event] = [];
711
+ config.hooks[event].push({ matcher, hooks: [grokCommand(sub, timeout)] });
712
+ changes.push(`Added Grok ${event} hook (${label})`);
713
+ };
714
+ add('SessionStart', sessionStartMatcher, 'recap', 15, 'memory recap');
715
+ add('SessionStart', sessionStartMatcher, 'session-id', 5, 'session-id');
716
+ add('SessionStart', sessionStartMatcher, 'drain', 8, 'inbox drain');
717
+ add('UserPromptSubmit', '', 'notify', 8, 'prompt cache');
718
+ add('PreToolUse', 'Bash|run_terminal_command', 'crush-wrap', 5, 'crush-wrap');
719
+ add('PreToolUse', '*', 'guard', 5, 'guard');
720
+ add('Stop', '', 'store', 15, 'turn capture');
721
+ add('Stop', '', 'drain', 8, 'inbox drain');
722
+ add('PostCompact', '', 'session-id', 5, 'session-id');
723
+ return changes;
724
+ }
725
+ function writeGrokRules() {
726
+ const file = getGrokRulesPath();
727
+ fs.mkdirSync(path.dirname(file), { recursive: true });
728
+ if (fs.existsSync(file) && fs.readFileSync(file, 'utf-8') === GROK_RULES) {
729
+ return 'Grok rules already current';
730
+ }
731
+ fs.writeFileSync(file, GROK_RULES);
732
+ return `Wrote ${file}`;
733
+ }
734
+ /** greprag init --grok
735
+ * Dedicated Grok Build adapter. Does not ride ~/.claude/settings.json.
736
+ * adr: adr/grok-platform.md */
737
+ async function runGrokInit(opts) {
738
+ console.log('\n greprag init — Setting up agent memory for Grok Build\n');
739
+ let apiKey = opts.apiKey || readSharedApiKey();
740
+ if (!apiKey) {
741
+ const tenantId = await resolveProvisionHandle(opts);
742
+ console.log(` Provisioning API key for public handle ${tenantId}@greprag.com...`);
743
+ const result = await provision(tenantId);
744
+ if (!result.ok) {
745
+ console.error(` Error: ${result.error}`);
746
+ process.exit(1);
747
+ }
748
+ apiKey = result.apiKey;
749
+ console.log(` API key created: ${apiKey.slice(0, 17)}...`);
750
+ }
751
+ console.log(' Validating API key...');
752
+ const valid = await validateKey(apiKey);
753
+ if (!valid) {
754
+ console.error(' Error: API key validation failed.');
755
+ process.exit(1);
756
+ }
757
+ console.log(' Key validated against api.greprag.com');
758
+ const changes = [];
759
+ writeGrepragEnv(apiKey);
760
+ changes.push('Wrote ~/.greprag/.env for Grok hooks');
761
+ await cacheIdentity(apiKey);
762
+ const anchor = (0, project_anchor_1.ensureAnchor)(process.cwd());
763
+ if (anchor.source === 'git') {
764
+ changes.push(`Project anchor: ${anchor.projectName} (${anchor.projectId.slice(0, 8)}) — derived from git history`);
765
+ }
766
+ else {
767
+ changes.push(`Project anchor: ${anchor.projectName} (${anchor.projectId.slice(0, 8)}) — stored in .greprag/project.json`);
768
+ const gitignoreResult = ensureAnchorTrackable(process.cwd());
769
+ if (gitignoreResult)
770
+ changes.push(gitignoreResult);
771
+ }
772
+ const globalAnchor = (0, project_anchor_1.ensureGlobalAnchor)();
773
+ changes.push(`Global anchor: ${globalAnchor.projectName} (${globalAnchor.projectId.slice(0, 8)})`);
774
+ writeProjectPath(anchor.projectName, process.cwd());
775
+ changes.push(`Registered repo path for ${anchor.projectName} → ~/.greprag/projects.json`);
776
+ const grokSkill = installCoreSkill('grok');
777
+ if (grokSkill)
778
+ changes.push(`Skill: installed → ${grokSkill}`);
779
+ changes.push(writeGrokRules());
780
+ try {
781
+ const res = await fetch(`${API_URL}/v1/inbox/projects/register`, {
782
+ method: 'POST',
783
+ headers: {
784
+ 'Authorization': `Bearer ${apiKey}`,
785
+ 'Content-Type': 'application/json',
786
+ },
787
+ body: JSON.stringify({ project_id: anchor.projectId, project_name: anchor.projectName }),
788
+ });
789
+ const reg = await res.json();
790
+ changes.push(reg.ok ? 'Project registered for inbox addressing' : `Inbox registration: ${reg.error || 'failed'}`);
791
+ }
792
+ catch {
793
+ changes.push('Inbox registration skipped (network)');
794
+ }
795
+ const hooksPath = getGrokHooksPath();
796
+ const hooks = readCodexHooks(hooksPath);
797
+ const hookChanges = applyGrokHooks(hooks);
798
+ writeCodexHooks(hooksPath, hooks);
799
+ changes.push(...hookChanges);
800
+ console.log('\n Setup complete!\n');
801
+ for (const change of changes) {
802
+ console.log(` - ${change}`);
803
+ }
804
+ console.log(`\n Grok hooks file: ${hooksPath}`);
805
+ console.log(` Grok rules: ${getGrokRulesPath()}`);
806
+ console.log(` Project anchor: ${anchor.anchorPath}`);
807
+ console.log(' Reload hooks in Grok (/hooks then r) or start a fresh session.');
808
+ console.log(' Recap lands in ~/.greprag/grok-context/<16-hex>.md — Grok ignores SessionStart stdout.');
809
+ console.log(' Idle inbox: Grok monitor + `greprag inbox watch --session $GROK_SESSION_ID --json --quiet`.');
810
+ console.log(' Second session: `greprag grok spawn` (TUI) or spawn_subagent; child arms its own watch; parent keeps one.\n');
811
+ }
643
812
  /** greprag init --global
644
813
  * Creates ~/.greprag/project.json with a stable UUID.
645
814
  * Designed for Cowork / any agent session whose cwd has no repo-level anchor.
@@ -979,7 +1148,7 @@ function getSkillRoot() {
979
1148
  return path.join(__dirname, '../../skill');
980
1149
  }
981
1150
  function skillTargetRoot(target) {
982
- const dir = target === 'codex' ? '.codex' : '.claude';
1151
+ const dir = target === 'codex' ? '.codex' : target === 'grok' ? '.grok' : '.claude';
983
1152
  return path.join(os.homedir(), dir, 'skills');
984
1153
  }
985
1154
  function installCoreSkill(target) {
@@ -19,7 +19,7 @@
19
19
  * load-primer` / `chip-spawn-pointer`). Announce-only: detect → silent,
20
20
  * reminder → null. adr: docs/load-system.md, docs/reminder-interrupt.md */
21
21
  Object.defineProperty(exports, "__esModule", { value: true });
22
- exports.codexChipSpawnPointerModule = exports.chipSpawnPointerModule = exports.loadPrimerModule = exports.CODEX_CHIP_SPAWN_POINTER = exports.OPENCODE_CHIP_SPAWN_POINTER = exports.CHIP_SPAWN_POINTER = exports.LOAD_PRIMER = void 0;
22
+ exports.codexChipSpawnPointerModule = exports.chipSpawnPointerModule = exports.loadPrimerModule = exports.CODEX_CHIP_SPAWN_POINTER = exports.GROK_CHIP_SPAWN_POINTER = exports.OPENCODE_CHIP_SPAWN_POINTER = exports.CHIP_SPAWN_POINTER = exports.LOAD_PRIMER = void 0;
23
23
  /** PRIMER — teaches the loader itself. Resident (fires every SessionStart +
24
24
  * re-teaches after compaction). Terse: it is in context every session. */
25
25
  exports.LOAD_PRIMER = [
@@ -43,6 +43,11 @@ exports.OPENCODE_CHIP_SPAWN_POINTER = [
43
43
  '[greprag chips — agent coordination is built in (messaging plumbing in the inbox primer).]',
44
44
  '• Delegating a component of a plan to an isolated chip session, or `greprag fix spawn` just printed a FIX-chip mission? → run `greprag load chip-bootloader` FIRST for the method: OpenCode chips spawn natively via `greprag opencode chip goal create` + `greprag opencode chip spawn` (HTTP child session; manual paste only when the API is down). ≥2 chips at one objective → `greprag load chip-leader-opencode` BEFORE the first spawn.',
45
45
  ].join('\n');
46
+ /** POINTER — Grok Build TUI chip. Real window + inbox mesh, not spawn_subagent. */
47
+ exports.GROK_CHIP_SPAWN_POINTER = [
48
+ '[greprag chips — agent coordination is built in (messaging plumbing in the inbox primer).]',
49
+ '• About to spawn a chip? → run `greprag load grok-chip-spawn` FIRST. Vehicle is `greprag grok spawn` (cmd /k TUI bootloader). Child arms its own quiet inbox watch; parent keeps one and talks via `greprag send`. `spawn_subagent` is a helper, not a chip. ≥2 chips at one objective → `greprag load chip-leader` BEFORE the first spawn.',
50
+ ].join('\n');
46
51
  /** POINTER — Codex Desktop's native chip path. This is intentionally distinct
47
52
  * from Claude Code spawn_task: different lifecycle, runtime, and handoff. */
48
53
  exports.CODEX_CHIP_SPAWN_POINTER = [
@@ -70,6 +75,8 @@ exports.chipSpawnPointerModule = {
70
75
  return null;
71
76
  if (env.platform === 'opencode')
72
77
  return exports.OPENCODE_CHIP_SPAWN_POINTER;
78
+ if (env.platform === 'grok')
79
+ return exports.GROK_CHIP_SPAWN_POINTER;
73
80
  return exports.CHIP_SPAWN_POINTER;
74
81
  },
75
82
  reminder: () => null,
@@ -73,6 +73,10 @@ const LIBRARY = {
73
73
  files: ['skill/templates/codex-chip-spawn.md'],
74
74
  purpose: 'Spawn an independent writable native Codex Desktop task with an isolated worktree and native completion/cleanup.',
75
75
  },
76
+ 'grok-chip-spawn': {
77
+ files: ['skill/templates/grok-chip-spawn.md'],
78
+ purpose: 'Spawn a Grok TUI chip (cmd /k bootloader + isolated worktree). Child arms its own inbox watch; parent talks via greprag send. Not spawn_subagent.',
79
+ },
76
80
  'skill-change': {
77
81
  files: ['skill/templates/skill-change.md'],
78
82
  purpose: 'Internal bundled schema for safely updating a skill after a run: when to edit, Convention A/B shapes, and when to propose instead.',
@@ -20,7 +20,8 @@ function buildOsPrimer(env) {
20
20
  // vehicle is the chip-bootloader entry: native `greprag opencode chip` spawn).
21
21
  const spawnEntry = env?.platform === 'codex' ? 'codex-chip-spawn'
22
22
  : env?.platform === 'opencode' ? 'chip-bootloader'
23
- : 'chip-spawn';
23
+ : env?.platform === 'grok' ? 'grok-chip-spawn'
24
+ : 'chip-spawn';
24
25
  return [
25
26
  '[grepragOS — the operating laws. Full doctrine: `greprag load os`.]',
26
27
  '• Doctrine vs state: methods ship in the CLI (`greprag load`); live state lives in the repo. A skill that depends on repo state carries a "STATE — read these first" block naming exact paths.',
@@ -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())
@@ -146,6 +146,9 @@ function readVersion() {
146
146
  return 'unknown';
147
147
  }
148
148
  }
149
+ function grokHooksPath() {
150
+ return path.join(home(), '.grok', 'hooks', 'greprag.json');
151
+ }
149
152
  function platformFromArgs(args) {
150
153
  if (args.includes('--claude'))
151
154
  return 'claude';
@@ -153,6 +156,8 @@ function platformFromArgs(args) {
153
156
  return 'codex';
154
157
  if (args.includes('--opencode'))
155
158
  return 'opencode';
159
+ if (args.includes('--grok'))
160
+ return 'grok';
156
161
  if (args.includes('--all-platforms'))
157
162
  return 'all';
158
163
  return 'all';
@@ -167,6 +172,8 @@ function buildStatus(cwd, platform = 'all') {
167
172
  const identityCache = (0, identity_1.readCache)(identityCachePath);
168
173
  const codexSkillPath = path.join(home(), '.codex', 'skills', 'greprag');
169
174
  const claudeSkillPath = path.join(home(), '.claude', 'skills', 'greprag');
175
+ const grokSkillPath = path.join(home(), '.grok', 'skills', 'greprag');
176
+ const grokHooks = readJson(grokHooksPath(), {});
170
177
  const opencodePluginPath = path.join(home(), '.config', 'opencode', 'plugins', 'greprag-memory.js');
171
178
  const claudeHooks = {
172
179
  session_start_recap: hasHook(settings.hooks?.SessionStart, 'recap'),
@@ -187,6 +194,15 @@ function buildStatus(cwd, platform = 'all') {
187
194
  post_compact_session_id: hasCodexHook(codexHooks.hooks?.PostCompact, 'session-id'),
188
195
  };
189
196
  const opencodePluginInstalled = fs.existsSync(opencodePluginPath);
197
+ const grokHookStatus = {
198
+ session_start_recap: hasCodexHook(grokHooks.hooks?.SessionStart, 'recap'),
199
+ session_id: hasCodexHook(grokHooks.hooks?.SessionStart, 'session-id'),
200
+ session_start_drain: hasCodexHook(grokHooks.hooks?.SessionStart, 'drain'),
201
+ user_prompt_submit: hasCodexHook(grokHooks.hooks?.UserPromptSubmit, 'notify'),
202
+ crush_wrap: hasCodexHook(grokHooks.hooks?.PreToolUse, 'crush-wrap'),
203
+ stop_store: hasCodexHook(grokHooks.hooks?.Stop, 'store'),
204
+ stop_drain: hasCodexHook(grokHooks.hooks?.Stop, 'drain'),
205
+ };
190
206
  return {
191
207
  installed: true,
192
208
  version: readVersion(),
@@ -212,6 +228,8 @@ function buildStatus(cwd, platform = 'all') {
212
228
  codex_pre_tool_use_safety: codexHookStatus.pre_tool_use_safety,
213
229
  codex_pre_tool_use_chip_guard: codexHookStatus.pre_tool_use_chip_guard,
214
230
  codex_stop_store: codexHookStatus.stop_store,
231
+ grok_session_start_recap: grokHookStatus.session_start_recap,
232
+ grok_stop_store: grokHookStatus.stop_store,
215
233
  },
216
234
  platforms: {
217
235
  claude: {
@@ -241,6 +259,16 @@ function buildStatus(cwd, platform = 'all') {
241
259
  plugin_installed: opencodePluginInstalled,
242
260
  note: 'OpenCode loads the GrepRAG plugin from its global plugin directory.',
243
261
  },
262
+ grok: {
263
+ configured: !!api.key && grokHookStatus.session_start_recap && grokHookStatus.stop_store
264
+ && fs.existsSync(grokSkillPath),
265
+ config_path: grokHooksPath(),
266
+ config_present: fs.existsSync(grokHooksPath()),
267
+ skill_path: grokSkillPath,
268
+ skill_installed: fs.existsSync(grokSkillPath),
269
+ hooks: grokHookStatus,
270
+ note: 'Grok recap is a sidecar + ~/.grok/rules (SessionStart stdout is ignored). Idle inbox: Grok monitor + quiet watch. Stop drain injects unread mail only when unarmed.',
271
+ },
244
272
  },
245
273
  project: {
246
274
  cwd: path.resolve(cwd),
@@ -293,7 +321,7 @@ function renderPlatform(name, p) {
293
321
  }
294
322
  function renderHuman(s, docMirror) {
295
323
  const yes = (b) => b ? 'yes' : 'no';
296
- const platformNames = s.platform === 'all' ? ['claude', 'codex', 'opencode'] : [s.platform];
324
+ const platformNames = s.platform === 'all' ? ['claude', 'codex', 'opencode', 'grok'] : [s.platform];
297
325
  return [
298
326
  `greprag ${s.version}`,
299
327
  `Shared env: ${s.shared_env_path}`,