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.
- package/dist/commands/arm-reminder.js +4 -2
- package/dist/commands/codex.js +2 -7
- package/dist/commands/inbox-primer-reminder.js +16 -11
- package/dist/commands/inbox-watch-supervisor.js +17 -7
- package/dist/commands/inbox-watch.js +53 -29
- package/dist/commands/init.js +163 -4
- package/dist/commands/pipe-wrap.js +6 -5
- package/dist/commands/search-guard.js +2 -1
- package/dist/commands/status.js +29 -1
- package/dist/commands/watcher-registry.js +67 -22
- package/dist/grok-session.js +125 -0
- package/dist/harness.js +6 -0
- package/dist/hook-once.js +67 -0
- package/dist/hook-runtime.js +43 -0
- package/dist/hook.js +78 -17
- package/dist/index.js +17 -11
- package/dist/opencode-plugin.bundle.js +14 -7
- package/dist/session-id.js +46 -8
- package/package.json +3 -2
- package/skill/greprag/SKILL.md +6 -1
|
@@ -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
|
-
?
|
|
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),
|
package/dist/commands/codex.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
43
|
-
? '[
|
|
44
|
-
:
|
|
45
|
-
? '[
|
|
46
|
-
:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
170
|
-
* Used for both the JSON --json line and
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
-
|
|
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 --
|
|
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
|
|
286
|
-
* qualifies for wrapping, null for silence. NO permissionDecision:
|
|
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
|
|
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
|
|
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())
|