greprag 5.22.2 → 5.23.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/codex-doctor.d.ts +12 -0
- package/dist/commands/codex-doctor.js +167 -0
- package/dist/commands/codex-doctor.js.map +1 -0
- package/dist/commands/codex-supervisor.d.ts +1 -0
- package/dist/commands/codex-supervisor.js +113 -0
- package/dist/commands/codex-supervisor.js.map +1 -0
- package/dist/commands/codex.d.ts +0 -6
- package/dist/commands/codex.js +30 -78
- package/dist/commands/codex.js.map +1 -1
- package/dist/commands/email.d.ts +18 -0
- package/dist/commands/email.js +310 -0
- package/dist/commands/email.js.map +1 -0
- package/dist/commands/inbox-watch.d.ts +4 -0
- package/dist/commands/inbox-watch.js +4 -2
- package/dist/commands/inbox-watch.js.map +1 -1
- package/dist/commands/init.js +50 -7
- package/dist/commands/init.js.map +1 -1
- package/dist/email-pull.d.ts +84 -0
- package/dist/email-pull.js +203 -0
- package/dist/email-pull.js.map +1 -0
- package/dist/email-send.d.ts +64 -0
- package/dist/email-send.js +124 -0
- package/dist/email-send.js.map +1 -0
- package/dist/front-desk-mail.d.ts +50 -0
- package/dist/front-desk-mail.js +206 -0
- package/dist/front-desk-mail.js.map +1 -0
- package/dist/hook.js +61 -2
- package/dist/hook.js.map +1 -1
- package/dist/index.js +356 -142
- package/dist/index.js.map +1 -1
- package/dist/project-anchor.d.ts +6 -0
- package/dist/project-anchor.js +10 -0
- package/dist/project-anchor.js.map +1 -1
- package/package.json +1 -1
- package/scripts/postinstall.js +1 -1
- package/skill/greprag/SKILL.md +1 -1
- package/skill/greprag/docs/inbox.md +7 -5
- package/skill/greprag/docs/setup.md +14 -1
package/dist/index.js
CHANGED
|
@@ -55,6 +55,7 @@ const checkpoint_1 = require("./commands/checkpoint");
|
|
|
55
55
|
const identity_1 = require("./commands/identity");
|
|
56
56
|
const skill_1 = require("./commands/skill");
|
|
57
57
|
const project_1 = require("./commands/project");
|
|
58
|
+
const email_1 = require("./commands/email");
|
|
58
59
|
const codex_1 = require("./commands/codex");
|
|
59
60
|
const inbox_retract_1 = require("./commands/inbox-retract");
|
|
60
61
|
const inbox_watch_1 = require("./commands/inbox-watch");
|
|
@@ -179,19 +180,30 @@ async function apiDelete(url, apiKey) {
|
|
|
179
180
|
}
|
|
180
181
|
const INBOX_HELP = `greprag inbox — read + manage your async message inbox.
|
|
181
182
|
|
|
182
|
-
greprag inbox List unread
|
|
183
|
-
|
|
183
|
+
greprag inbox List unread for THIS session: its own lines
|
|
184
|
+
(sender or recipient) + the front desk
|
|
185
|
+
(tenant catch-all) + project broadcasts.
|
|
186
|
+
Other sessions' private lines are hidden.
|
|
187
|
+
greprag inbox --all Audit peek: drop the per-session scope (every
|
|
188
|
+
session's lines) AND include already-read.
|
|
184
189
|
greprag inbox --peek List WITHOUT marking read (non-mutating).
|
|
185
|
-
greprag inbox --session <8hex>
|
|
190
|
+
greprag inbox --session <8hex> Scope to one specific session's view.
|
|
186
191
|
greprag inbox --project <name> Filter to one project's inbox.
|
|
187
192
|
greprag inbox watch [...] Long-lived SSE stream (run under Monitor).
|
|
193
|
+
[--receptionist] Attend the front desk — wake live on a cold
|
|
194
|
+
open / inbound email (else only manual check).
|
|
188
195
|
greprag inbox watchers [--json] List live armed watchers under your tenant.
|
|
196
|
+
greprag inbox claim <id> Receptionist: claim a front-desk record so a
|
|
197
|
+
co-armed receptionist stands down (no dup reply).
|
|
189
198
|
greprag inbox keep <id> Extend a message's TTL.
|
|
190
199
|
greprag inbox delete <id> Delete a message.
|
|
191
200
|
|
|
192
201
|
To SEND or REPLY (top-level command, NOT an inbox subcommand):
|
|
193
202
|
greprag send --to discord:<snowflake> "reply" Reply to a Discord DM.
|
|
194
|
-
greprag send "msg" --to <handle>@greprag.com
|
|
203
|
+
greprag send "msg" --to <handle>@greprag.com Cold open (tenant catch-all).
|
|
204
|
+
greprag send "msg" --to <handle>@greprag.com/<target> Message a session/project.
|
|
205
|
+
greprag send "msg" --to <h>@greprag.com/<session> --in-reply-to <front-desk-id>
|
|
206
|
+
Reply that threads a cold open.`;
|
|
195
207
|
/** greprag inbox [--all] [--session <id>] | inbox keep <id> | inbox delete <id> | inbox watch */
|
|
196
208
|
async function inbox(args) {
|
|
197
209
|
const cfg = getConfig();
|
|
@@ -207,7 +219,7 @@ async function inbox(args) {
|
|
|
207
219
|
// A non-flag first arg that isn't a known subcommand is a typo — do NOT fall
|
|
208
220
|
// through to list mode (which auto-marks every message read). The classic
|
|
209
221
|
// slip is `inbox send` (the real reply command is top-level `greprag send`).
|
|
210
|
-
if (sub && !sub.startsWith('-') && !['watch', 'watchers', 'keep', 'delete'].includes(sub)) {
|
|
222
|
+
if (sub && !sub.startsWith('-') && !['watch', 'watchers', 'keep', 'delete', 'claim'].includes(sub)) {
|
|
211
223
|
if (sub === 'send') {
|
|
212
224
|
console.error('`greprag inbox send` is not a command. To reply (e.g. to a Discord DM):');
|
|
213
225
|
console.error(' greprag send --to discord:<snowflake> "your message"');
|
|
@@ -225,6 +237,9 @@ async function inbox(args) {
|
|
|
225
237
|
session: getFlag(subArgs, '--session'),
|
|
226
238
|
since: getFlag(subArgs, '--since'),
|
|
227
239
|
json: subArgs.includes('--json'),
|
|
240
|
+
// --receptionist: attend the front desk — wake live on tenant catch-all
|
|
241
|
+
// (cold open / inbound email). adr: adr/address-grammar.md
|
|
242
|
+
receptionist: subArgs.includes('--receptionist'),
|
|
228
243
|
});
|
|
229
244
|
return;
|
|
230
245
|
}
|
|
@@ -271,12 +286,47 @@ async function inbox(args) {
|
|
|
271
286
|
}
|
|
272
287
|
return;
|
|
273
288
|
}
|
|
289
|
+
if (sub === 'claim') {
|
|
290
|
+
// Receptionist claims a front-desk record (cold open / inbound email).
|
|
291
|
+
// First claimant wins; a loser stands down — no double-reply when more
|
|
292
|
+
// than one receptionist is armed. adr: adr/address-grammar.md
|
|
293
|
+
const id = args[1];
|
|
294
|
+
if (!id) {
|
|
295
|
+
console.error('Usage: greprag inbox claim <front-desk-id> [--session <8-hex>]');
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
const claimant = getFlag(args, '--session')
|
|
299
|
+
|| process.env.CLAUDE_SESSION_ID || process.env.GREPRAG_SESSION_ID;
|
|
300
|
+
if (!claimant) {
|
|
301
|
+
console.error('No session id to claim as. Pass --session <8-hex>, or run inside a Claude Code session.');
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
const r = await apiCall(`${cfg.apiUrl}/v1/inbox/handle/${id}`, cfg.apiKey, { session_id: claimant });
|
|
305
|
+
if (r.claimed) {
|
|
306
|
+
console.log(`Claimed ${id.slice(0, 8)} as receptionist (session ${(0, session_id_1.truncateSessionId)(claimant) ?? claimant}).`);
|
|
307
|
+
console.log(` Reply with: greprag send "..." --to <sender>@greprag.com/<their-session> --in-reply-to ${id.slice(0, 8)} --from-session ${(0, session_id_1.truncateSessionId)(claimant) ?? claimant}`);
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
const handledBy = r.handled_by ?? null;
|
|
311
|
+
const holder = handledBy ? ((0, session_id_1.truncateSessionId)(handledBy) ?? handledBy) : 'another session';
|
|
312
|
+
console.log(`Already handled by ${holder} — standing down (no double-reply).`);
|
|
313
|
+
}
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
274
316
|
const includeAll = args.includes('--all');
|
|
275
|
-
const
|
|
317
|
+
const explicitSession = getFlag(args, '--session');
|
|
276
318
|
const projectFlag = getFlag(args, '--project');
|
|
277
319
|
// --peek: non-mutating inspection. Orchestrator-mode uses this to survey
|
|
278
320
|
// other sessions' inboxes without burning unread state for them.
|
|
279
321
|
const peek = args.includes('--peek');
|
|
322
|
+
// Default view auto-scopes to THIS session — its own lines (sender OR
|
|
323
|
+
// recipient) + the front desk (tenant catch-all) + project broadcasts. Other
|
|
324
|
+
// sessions' private session↔session lines are hidden. `--all` drops the scope
|
|
325
|
+
// for a tenant-wide audit peek across every session. An explicit `--session`
|
|
326
|
+
// always scopes to that id. adr: adr/address-grammar.md
|
|
327
|
+
const autoSession = process.env.CLAUDE_SESSION_ID || process.env.GREPRAG_SESSION_ID || null;
|
|
328
|
+
const sessionId = explicitSession || (includeAll ? null : autoSession);
|
|
329
|
+
const scopedToSelf = !explicitSession && !includeAll && !!autoSession;
|
|
280
330
|
const params = new URLSearchParams();
|
|
281
331
|
if (includeAll)
|
|
282
332
|
params.set('include', 'all');
|
|
@@ -299,8 +349,15 @@ async function inbox(args) {
|
|
|
299
349
|
const url = `${cfg.apiUrl}/v1/inbox${qs ? '?' + qs : ''}`;
|
|
300
350
|
const result = await apiGet(url, cfg.apiKey);
|
|
301
351
|
const messages = (result.messages || []);
|
|
352
|
+
// Faint reminder that the default view is scoped to this session — other
|
|
353
|
+
// sessions' private lines (and the rest) are one `--all` away.
|
|
354
|
+
const scopeHint = scopedToSelf
|
|
355
|
+
? `(scoped to session ${(0, session_id_1.truncateSessionId)(autoSession) ?? autoSession} — \`greprag inbox --all\` shows every session)`
|
|
356
|
+
: '';
|
|
302
357
|
if (messages.length === 0) {
|
|
303
358
|
console.log(includeAll ? 'No messages.' : 'No unread messages.');
|
|
359
|
+
if (scopeHint)
|
|
360
|
+
console.log(scopeHint);
|
|
304
361
|
return;
|
|
305
362
|
}
|
|
306
363
|
const unreadCount = messages.filter(m => m.was_unread).length;
|
|
@@ -310,10 +367,18 @@ async function inbox(args) {
|
|
|
310
367
|
else {
|
|
311
368
|
console.log(`${messages.length} message(s):\n`);
|
|
312
369
|
}
|
|
370
|
+
if (scopeHint)
|
|
371
|
+
console.log(`${scopeHint}\n`);
|
|
313
372
|
for (const msg of messages) {
|
|
314
373
|
const date = new Date(msg.created_at).toLocaleString();
|
|
315
374
|
const sender = msg.from.handle || msg.from.tenant + '@greprag.com';
|
|
316
|
-
|
|
375
|
+
// Cold open = a bare-handle catch-all from a sender who doesn't know your
|
|
376
|
+
// session. Reply to from.session_id (shown below) to establish the line.
|
|
377
|
+
const coldOpen = msg.to_scope === 'tenant' ? '· cold open' : '';
|
|
378
|
+
// Internal self-desk rows (lore_smell, …) only surface under `--all` — tag
|
|
379
|
+
// them so the audit view shows they're queue items, not mail.
|
|
380
|
+
const internalTag = isInternalMessageType(msg.message_type) ? `· internal:${msg.message_type}` : '';
|
|
381
|
+
const status = [msg.was_unread ? '· new' : '', coldOpen, internalTag].filter(Boolean).join(' ');
|
|
317
382
|
console.log(`[${sender}] ${date} ${msg.id.slice(0, 8)} ${status}`);
|
|
318
383
|
// Session ids in 8-hex when set on either side.
|
|
319
384
|
// adr: adr/session-id-awareness.md
|
|
@@ -327,6 +392,24 @@ async function inbox(args) {
|
|
|
327
392
|
sessionParts.push(`to session ${toShort}`);
|
|
328
393
|
console.log(` · ${sessionParts.join(' → ')}`);
|
|
329
394
|
}
|
|
395
|
+
// Front-desk envelope line (cold opens / inbound email). The receptionist
|
|
396
|
+
// gates on trust.verdict: 'verified' → may auto-route live; everything else
|
|
397
|
+
// parks for human sign-off. handled_by = already claimed (stand down).
|
|
398
|
+
// adr: adr/address-grammar.md
|
|
399
|
+
if (msg.to_scope === 'tenant') {
|
|
400
|
+
const fd = [];
|
|
401
|
+
if (msg.subject)
|
|
402
|
+
fd.push(`subj: ${msg.subject}`);
|
|
403
|
+
if (msg.ingress === 'email' || msg.trust) {
|
|
404
|
+
const verdict = msg.trust?.verdict ?? 'unevaluated';
|
|
405
|
+
const gate = verdict === 'verified' ? 'auto-route ok' : 'park for sign-off';
|
|
406
|
+
fd.push(`trust: ${verdict} → ${gate}`);
|
|
407
|
+
}
|
|
408
|
+
if (msg.handled_by)
|
|
409
|
+
fd.push(`handled by ${(0, session_id_1.truncateSessionId)(msg.handled_by) ?? msg.handled_by}`);
|
|
410
|
+
if (fd.length)
|
|
411
|
+
console.log(` · ${fd.join(' · ')}`);
|
|
412
|
+
}
|
|
330
413
|
// Indent each body line so multi-line markdown stays visually grouped
|
|
331
414
|
for (const line of msg.body.split('\n'))
|
|
332
415
|
console.log(` ${line}`);
|
|
@@ -389,13 +472,14 @@ function parseFileFlag(raw) {
|
|
|
389
472
|
* look like UUIDs are rejected at registration time so this is unambiguous.
|
|
390
473
|
* adr: adr/session-id-awareness.md, adr/address-grammar.md */
|
|
391
474
|
const SESSION_ID_PATTERN = /^([0-9a-f]{8}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
|
|
392
|
-
/** Parse a send address under the v0.
|
|
393
|
-
* <handle>@greprag.com/<target>
|
|
475
|
+
/** Parse a send address under the v0.12 grammar:
|
|
476
|
+
* <handle>@greprag.com[/<target>]
|
|
394
477
|
* where <target> is exactly one segment — a session UUID (or 8-hex short
|
|
395
478
|
* form) for a session-targeted send, or a project name for a project
|
|
396
|
-
* broadcast. The bare form <handle>@greprag.com is
|
|
397
|
-
*
|
|
398
|
-
*
|
|
479
|
+
* broadcast. The bare form <handle>@greprag.com (no target) is the tenant
|
|
480
|
+
* catch-all / cold open — a first-contact channel for a sender who does not
|
|
481
|
+
* know the recipient's session. The message lands in the recipient's inbox
|
|
482
|
+
* for a manual check; it does not wake their session watchers.
|
|
399
483
|
*
|
|
400
484
|
* Returns a discriminated result so the caller can print the helpful
|
|
401
485
|
* migration hint on failure. */
|
|
@@ -411,14 +495,9 @@ function parseSendAddress(addr) {
|
|
|
411
495
|
return { ok: true, targetKind: 'discord', target: snowflake };
|
|
412
496
|
}
|
|
413
497
|
const segments = addr.split('/');
|
|
414
|
-
if (segments.length === 1) {
|
|
415
|
-
return { ok: false, error: `address "${addr}" has no target — the bare handle form is receive-only.\n` +
|
|
416
|
-
` Add /<session-uuid> for the normal session-to-session send, or\n` +
|
|
417
|
-
` add /<project-name> for an intentional project-wide broadcast.` };
|
|
418
|
-
}
|
|
419
498
|
if (segments.length > 2) {
|
|
420
|
-
return { ok: false, error: `address "${addr}" has too many segments. The v0.
|
|
421
|
-
` <handle>@greprag.com/<target
|
|
499
|
+
return { ok: false, error: `address "${addr}" has too many segments. The v0.12 grammar is\n` +
|
|
500
|
+
` <handle>@greprag.com[/<target>]\n` +
|
|
422
501
|
`where <target> is exactly one of: a session UUID OR a project name.\n` +
|
|
423
502
|
`Legacy <email>/<project>/<session> form is no longer accepted —\n` +
|
|
424
503
|
`pick the session you actually meant.` };
|
|
@@ -427,12 +506,158 @@ function parseSendAddress(addr) {
|
|
|
427
506
|
if (!emailPart.includes('@')) {
|
|
428
507
|
return { ok: false, error: `address "${addr}" missing @greprag.com handle.` };
|
|
429
508
|
}
|
|
509
|
+
// Bare handle (no target segment) — the tenant catch-all (cold open).
|
|
510
|
+
if (segments.length === 1) {
|
|
511
|
+
return { ok: true, targetKind: 'catchall', target: '' };
|
|
512
|
+
}
|
|
430
513
|
if (!target) {
|
|
431
514
|
return { ok: false, error: `address "${addr}" has an empty target segment.` };
|
|
432
515
|
}
|
|
433
516
|
const kind = SESSION_ID_PATTERN.test(target) ? 'session' : 'project';
|
|
434
517
|
return { ok: true, targetKind: kind, target };
|
|
435
518
|
}
|
|
519
|
+
/** Internal (self-desk) message types — KEEP IN SYNC with
|
|
520
|
+
* packages/core/src/inbox-types.ts INTERNAL_MESSAGE_TYPES. The CLI is a thin
|
|
521
|
+
* HTTP client and does not import @greprag/core (see turn-provenance.ts for the
|
|
522
|
+
* same pattern), so the set is duplicated here for the `--type` fast-fail gate
|
|
523
|
+
* and the `--all` audit-view tag. The server is the authority.
|
|
524
|
+
* adr: adr/address-grammar.md */
|
|
525
|
+
const INTERNAL_MESSAGE_TYPES = ['lore_smell'];
|
|
526
|
+
function isInternalMessageType(t) {
|
|
527
|
+
return !!t && INTERNAL_MESSAGE_TYPES.includes(t);
|
|
528
|
+
}
|
|
529
|
+
/** Flags whose following arg is a value (not the message body). Shared by the
|
|
530
|
+
* `--to` and `--to-desk` send paths so the body-finder skips flag values. */
|
|
531
|
+
const SEND_FLAGS_TAKING_VALUE = new Set([
|
|
532
|
+
'--to', '--memory', '--artifact', '--file', '--ref-json',
|
|
533
|
+
'--from-session', '--in-reply-to', '--type', '--body-file',
|
|
534
|
+
]);
|
|
535
|
+
/** First positional arg that isn't a flag or a flag's value → the message body. */
|
|
536
|
+
function extractBody(args) {
|
|
537
|
+
// --body-file <path>: read a long/multiline body from a file.
|
|
538
|
+
const bodyFile = getFlag(args, '--body-file');
|
|
539
|
+
if (bodyFile) {
|
|
540
|
+
try {
|
|
541
|
+
return fs.readFileSync(bodyFile, 'utf-8');
|
|
542
|
+
}
|
|
543
|
+
catch (e) {
|
|
544
|
+
console.error(`Error: --body-file could not be read: ${e.message}`);
|
|
545
|
+
process.exit(1);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
// --stdin: read the body from stdin.
|
|
549
|
+
if (args.includes('--stdin')) {
|
|
550
|
+
try {
|
|
551
|
+
return fs.readFileSync(0, 'utf-8');
|
|
552
|
+
}
|
|
553
|
+
catch (e) {
|
|
554
|
+
console.error(`Error: failed reading body from stdin: ${e.message}`);
|
|
555
|
+
process.exit(1);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
// Otherwise the first positional arg that isn't a flag value.
|
|
559
|
+
for (let i = 0; i < args.length; i++) {
|
|
560
|
+
if (args[i].startsWith('--'))
|
|
561
|
+
continue;
|
|
562
|
+
if (i > 0 && SEND_FLAGS_TAKING_VALUE.has(args[i - 1]))
|
|
563
|
+
continue;
|
|
564
|
+
return args[i];
|
|
565
|
+
}
|
|
566
|
+
return undefined;
|
|
567
|
+
}
|
|
568
|
+
/** Build the references object from --memory/--artifact/--file/--ref-json/
|
|
569
|
+
* --in-reply-to flags. Shared by both send paths. Exits on invalid --ref-json. */
|
|
570
|
+
function buildReferences(args) {
|
|
571
|
+
let references;
|
|
572
|
+
const refJsonRaw = getFlag(args, '--ref-json');
|
|
573
|
+
if (refJsonRaw) {
|
|
574
|
+
try {
|
|
575
|
+
references = JSON.parse(refJsonRaw);
|
|
576
|
+
}
|
|
577
|
+
catch (e) {
|
|
578
|
+
console.error(`--ref-json: invalid JSON (${e.message})`);
|
|
579
|
+
process.exit(1);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
const memoryIds = getFlags(args, '--memory');
|
|
584
|
+
const artifacts = getFlags(args, '--artifact')
|
|
585
|
+
.map(parseArtifactFlag)
|
|
586
|
+
.filter((a) => a !== null);
|
|
587
|
+
const files = getFlags(args, '--file').map(parseFileFlag);
|
|
588
|
+
if (memoryIds.length || artifacts.length || files.length) {
|
|
589
|
+
references = {};
|
|
590
|
+
if (memoryIds.length)
|
|
591
|
+
references.memory_ids = memoryIds;
|
|
592
|
+
if (artifacts.length)
|
|
593
|
+
references.artifacts = artifacts;
|
|
594
|
+
if (files.length)
|
|
595
|
+
references.files = files;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
// --in-reply-to <front-desk-id>: thread the reply back to a cold open /
|
|
599
|
+
// inbound-email record so the ensuing private line continues that
|
|
600
|
+
// conversation (front-desk invariant 5). adr: adr/address-grammar.md
|
|
601
|
+
const inReplyTo = getFlag(args, '--in-reply-to');
|
|
602
|
+
if (inReplyTo) {
|
|
603
|
+
references = { ...(references ?? {}), in_reply_to: inReplyTo };
|
|
604
|
+
}
|
|
605
|
+
return references;
|
|
606
|
+
}
|
|
607
|
+
/** greprag send "<text>" --to-desk --type <internal-type>
|
|
608
|
+
*
|
|
609
|
+
* Self-addressed internal mail: posts to THIS project's own desk (no external
|
|
610
|
+
* recipient) carrying a known internal messageType. Trusted by construction
|
|
611
|
+
* (from.tenant === to.tenant) — bypasses the trust gate, the receptionist
|
|
612
|
+
* wake, and sign-off; never wakes a live watcher. Accrues silently and is
|
|
613
|
+
* drained later by its typed consumer (e.g. lore_smell → /lore-advisor).
|
|
614
|
+
* The generic primitive `greprag lore smell` wraps. adr: adr/address-grammar.md */
|
|
615
|
+
async function sendToDesk(args, cfg, to) {
|
|
616
|
+
if (to) {
|
|
617
|
+
console.error('--to-desk is self-addressed (your own project desk) — drop --to.');
|
|
618
|
+
process.exit(1);
|
|
619
|
+
}
|
|
620
|
+
const type = getFlag(args, '--type');
|
|
621
|
+
const known = INTERNAL_MESSAGE_TYPES.join(', ');
|
|
622
|
+
if (!type) {
|
|
623
|
+
console.error(`--to-desk requires --type <internal-type>.\n Known internal types: ${known}.`);
|
|
624
|
+
process.exit(1);
|
|
625
|
+
}
|
|
626
|
+
if (!isInternalMessageType(type)) {
|
|
627
|
+
console.error(`--type "${type}" is not a known internal type.\n Known internal types: ${known}.`);
|
|
628
|
+
process.exit(1);
|
|
629
|
+
}
|
|
630
|
+
// The desk = the caller's anchor project (same resolver lore/memory use).
|
|
631
|
+
const anchor = (0, project_anchor_1.readAnchor)(process.cwd());
|
|
632
|
+
if (!anchor.projectId) {
|
|
633
|
+
console.error('No project anchor in current directory. Run: greprag init (the desk is this project).');
|
|
634
|
+
process.exit(1);
|
|
635
|
+
}
|
|
636
|
+
const body = extractBody(args);
|
|
637
|
+
if (!body) {
|
|
638
|
+
console.error('Usage: greprag send "<text>" --to-desk --type <internal-type>');
|
|
639
|
+
process.exit(1);
|
|
640
|
+
}
|
|
641
|
+
const payload = {
|
|
642
|
+
to_desk: true,
|
|
643
|
+
type,
|
|
644
|
+
body,
|
|
645
|
+
from_project_id: anchor.projectId,
|
|
646
|
+
};
|
|
647
|
+
const references = buildReferences(args);
|
|
648
|
+
if (references)
|
|
649
|
+
payload.references = references;
|
|
650
|
+
const fromSessionId = getFlag(args, '--from-session');
|
|
651
|
+
if (fromSessionId)
|
|
652
|
+
payload.from_session_id = fromSessionId;
|
|
653
|
+
const result = await apiCall(`${cfg.apiUrl}/v1/inbox/send`, cfg.apiKey, payload);
|
|
654
|
+
const idShort = typeof result.id === 'string' ? result.id.slice(0, 8) : '????????';
|
|
655
|
+
console.log(`Queued to desk (type=${type}, project=${anchor.projectName}) (${idShort})`);
|
|
656
|
+
console.log(` (internal queue item — not inbox mail; drained by its consumer, no live ping. Audit: greprag inbox --all)`);
|
|
657
|
+
const retractCode = result.retract_code;
|
|
658
|
+
if (retractCode)
|
|
659
|
+
console.log(`Retract: greprag retract ${retractCode}`);
|
|
660
|
+
}
|
|
436
661
|
/** greprag send "body" --to <handle>@greprag.com/<target>
|
|
437
662
|
* [--from-session <id>] sender's own session — denormalized
|
|
438
663
|
* onto the message so the recipient can
|
|
@@ -444,8 +669,10 @@ function parseSendAddress(addr) {
|
|
|
444
669
|
*
|
|
445
670
|
* <target> is exactly one segment — a session UUID (normal case) or a
|
|
446
671
|
* project name (intentional broadcast). The bare <handle>@greprag.com form
|
|
447
|
-
* is
|
|
448
|
-
*
|
|
672
|
+
* (no target) is the tenant catch-all / cold open — sendable, no --broadcast
|
|
673
|
+
* needed, lands quietly in the recipient's inbox for a manual check.
|
|
674
|
+
* --session and --project flags were removed in v0.11 — the address carries
|
|
675
|
+
* the target. adr: adr/address-grammar.md
|
|
449
676
|
*/
|
|
450
677
|
async function send(args) {
|
|
451
678
|
const cfg = getConfig();
|
|
@@ -454,10 +681,15 @@ async function send(args) {
|
|
|
454
681
|
process.exit(1);
|
|
455
682
|
}
|
|
456
683
|
const to = getFlag(args, '--to');
|
|
684
|
+
// Self-addressed internal mail: `greprag send "<text>" --to-desk --type <t>`.
|
|
685
|
+
// No external recipient — lands on this project's own desk and is drained by
|
|
686
|
+
// its typed consumer. Branch out before the --to-dependent path below.
|
|
687
|
+
// adr: adr/address-grammar.md
|
|
688
|
+
if (args.includes('--to-desk')) {
|
|
689
|
+
return sendToDesk(args, cfg, to);
|
|
690
|
+
}
|
|
457
691
|
if (!to) {
|
|
458
|
-
console.error('
|
|
459
|
-
console.error('Usage: greprag send "body" --to <handle>@greprag.com/<target>');
|
|
460
|
-
console.error(' greprag send --body-file report.md --to <handle>@greprag.com/<target>');
|
|
692
|
+
console.error('Usage: greprag send "body" --to <handle>@greprag.com[/<target>] [--from-session <id>] [--memory <uuid>] [--artifact <type:id>] [--file <path[:lines]>] [--broadcast]\n bare handle: tenant catch-all (cold open). <target>: session UUID (normal) or project name (broadcast — requires --broadcast).\n self-desk: --to-desk --type <internal-type> (internal queue item, no recipient).');
|
|
461
693
|
process.exit(1);
|
|
462
694
|
}
|
|
463
695
|
// Early validation — fail before the round-trip so the operator sees the
|
|
@@ -489,42 +721,9 @@ async function send(args) {
|
|
|
489
721
|
process.exit(1);
|
|
490
722
|
}
|
|
491
723
|
}
|
|
492
|
-
//
|
|
493
|
-
//
|
|
494
|
-
const
|
|
495
|
-
'--to', '--memory', '--artifact', '--file', '--ref-json',
|
|
496
|
-
'--from-session', '--body-file',
|
|
497
|
-
]);
|
|
498
|
-
let body;
|
|
499
|
-
const bodyFile = getFlag(args, '--body-file');
|
|
500
|
-
if (bodyFile) {
|
|
501
|
-
try {
|
|
502
|
-
body = fs.readFileSync(bodyFile, 'utf-8');
|
|
503
|
-
}
|
|
504
|
-
catch (e) {
|
|
505
|
-
console.error(`Error: --body-file could not be read: ${e.message}`);
|
|
506
|
-
process.exit(1);
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
else if (args.includes('--stdin')) {
|
|
510
|
-
try {
|
|
511
|
-
body = fs.readFileSync(0, 'utf-8');
|
|
512
|
-
}
|
|
513
|
-
catch (e) {
|
|
514
|
-
console.error(`Error: failed reading message body from stdin: ${e.message}`);
|
|
515
|
-
process.exit(1);
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
for (let i = 0; i < args.length; i++) {
|
|
519
|
-
if (body)
|
|
520
|
-
break;
|
|
521
|
-
if (args[i].startsWith('--'))
|
|
522
|
-
continue;
|
|
523
|
-
if (i > 0 && flagsTakingValue.has(args[i - 1]))
|
|
524
|
-
continue;
|
|
525
|
-
body = args[i];
|
|
526
|
-
break;
|
|
527
|
-
}
|
|
724
|
+
// Body + references via the shared helpers (also used by the --to-desk path).
|
|
725
|
+
// extractBody handles --body-file / --stdin / positional.
|
|
726
|
+
const body = extractBody(args);
|
|
528
727
|
if (!body || body.trim().length === 0) {
|
|
529
728
|
console.error('Error: message body is missing.');
|
|
530
729
|
console.error('Use one of:');
|
|
@@ -533,34 +732,7 @@ async function send(args) {
|
|
|
533
732
|
console.error(' type report.md | greprag send --stdin --to <handle>@greprag.com/<target>');
|
|
534
733
|
process.exit(1);
|
|
535
734
|
}
|
|
536
|
-
|
|
537
|
-
let references;
|
|
538
|
-
const refJsonRaw = getFlag(args, '--ref-json');
|
|
539
|
-
if (refJsonRaw) {
|
|
540
|
-
try {
|
|
541
|
-
references = JSON.parse(refJsonRaw);
|
|
542
|
-
}
|
|
543
|
-
catch (e) {
|
|
544
|
-
console.error(`--ref-json: invalid JSON (${e.message})`);
|
|
545
|
-
process.exit(1);
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
else {
|
|
549
|
-
const memoryIds = getFlags(args, '--memory');
|
|
550
|
-
const artifacts = getFlags(args, '--artifact')
|
|
551
|
-
.map(parseArtifactFlag)
|
|
552
|
-
.filter((a) => a !== null);
|
|
553
|
-
const files = getFlags(args, '--file').map(parseFileFlag);
|
|
554
|
-
if (memoryIds.length || artifacts.length || files.length) {
|
|
555
|
-
references = {};
|
|
556
|
-
if (memoryIds.length)
|
|
557
|
-
references.memory_ids = memoryIds;
|
|
558
|
-
if (artifacts.length)
|
|
559
|
-
references.artifacts = artifacts;
|
|
560
|
-
if (files.length)
|
|
561
|
-
references.files = files;
|
|
562
|
-
}
|
|
563
|
-
}
|
|
735
|
+
const references = buildReferences(args);
|
|
564
736
|
const payload = { to, body };
|
|
565
737
|
if (references)
|
|
566
738
|
payload.references = references;
|
|
@@ -588,6 +760,11 @@ async function send(args) {
|
|
|
588
760
|
: (delivered.project ? '/' + delivered.project : '');
|
|
589
761
|
const idShort = typeof result.id === 'string' ? result.id.slice(0, 8) : '????????';
|
|
590
762
|
console.log(`Sent to ${base}${targetSegment} (${idShort})`);
|
|
763
|
+
// Cold-open hint: a catch-all drop waits for a manual `greprag inbox` check
|
|
764
|
+
// on the recipient's side; it does not wake their live session watcher.
|
|
765
|
+
if (parsed.ok && parsed.targetKind === 'catchall') {
|
|
766
|
+
console.log(` (tenant catch-all — lands in their inbox for a manual check, no live ping)`);
|
|
767
|
+
}
|
|
591
768
|
const retractCode = result.retract_code;
|
|
592
769
|
if (retractCode)
|
|
593
770
|
console.log(`Retract: greprag retract ${retractCode}`);
|
|
@@ -807,39 +984,40 @@ async function retract(args) {
|
|
|
807
984
|
console.log((0, inbox_retract_1.formatRetractResult)(result.status));
|
|
808
985
|
}
|
|
809
986
|
// -- Main ------------------------------------------------------------------
|
|
810
|
-
const INIT_HELP = `greprag init — configure GrepRAG for an agent client.
|
|
811
|
-
|
|
812
|
-
greprag init
|
|
813
|
-
greprag init --codex [--tenant-id <handle>|--api-key <key>] [--install-watcher]
|
|
814
|
-
greprag init --claude [--tenant-id <handle>|--api-key <key>]
|
|
815
|
-
greprag init --opencode [--tenant-id <handle>|--api-key <key>]
|
|
816
|
-
greprag init --all [--root <path>]
|
|
817
|
-
greprag init --global [--name <name>]
|
|
818
|
-
|
|
819
|
-
Options:
|
|
820
|
-
--tenant-id <handle> Provision/use public handle <handle>@greprag.com.
|
|
821
|
-
Example: --tenant-id tanya -> tanya@greprag.com
|
|
822
|
-
--api-key <key> Use an existing GrepRAG API key instead of provisioning.
|
|
823
|
-
--codex Configure Codex hooks + /greprag skill.
|
|
824
|
-
--install-watcher With --codex, install the live inbox watcher at login.
|
|
825
|
-
--claude Configure Claude Code hooks + /greprag skill.
|
|
826
|
-
--opencode Configure OpenCode plugin.
|
|
827
|
-
|
|
828
|
-
Codex Desktop trust step:
|
|
829
|
-
After init, open Settings -> Settings -> Hooks
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
init --
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
987
|
+
const INIT_HELP = `greprag init — configure GrepRAG for an agent client.
|
|
988
|
+
|
|
989
|
+
greprag init
|
|
990
|
+
greprag init --codex [--tenant-id <handle>|--api-key <key>] [--install-watcher]
|
|
991
|
+
greprag init --claude [--tenant-id <handle>|--api-key <key>]
|
|
992
|
+
greprag init --opencode [--tenant-id <handle>|--api-key <key>]
|
|
993
|
+
greprag init --all [--root <path>]
|
|
994
|
+
greprag init --global [--name <name>]
|
|
995
|
+
|
|
996
|
+
Options:
|
|
997
|
+
--tenant-id <handle> Provision/use public handle <handle>@greprag.com.
|
|
998
|
+
Example: --tenant-id tanya -> tanya@greprag.com
|
|
999
|
+
--api-key <key> Use an existing GrepRAG API key instead of provisioning.
|
|
1000
|
+
--codex Configure Codex hooks + /greprag skill.
|
|
1001
|
+
--install-watcher With --codex, install the live inbox watcher at login.
|
|
1002
|
+
--claude Configure Claude Code hooks + /greprag skill.
|
|
1003
|
+
--opencode Configure OpenCode plugin.
|
|
1004
|
+
|
|
1005
|
+
Codex Desktop trust step:
|
|
1006
|
+
After init, start a fresh Codex session, then open Settings -> Settings -> Hooks
|
|
1007
|
+
and trust the 6 GrepRAG hooks.`;
|
|
1008
|
+
const HELP = `
|
|
1009
|
+
greprag — agent memory for Claude Code, Codex, and OpenCode
|
|
1010
|
+
|
|
1011
|
+
Commands:
|
|
1012
|
+
init [--api-key <key>] [--tenant-id <handle>]
|
|
1013
|
+
Auto-detect/ask for Codex, Claude Code, or OpenCode
|
|
1014
|
+
init --claude [--api-key <key>] [--tenant-id <handle>]
|
|
1015
|
+
Configure hooks + API key for Claude Code
|
|
1016
|
+
init --global [--name <name>] Create ~/.greprag/project.json (global anchor)
|
|
1017
|
+
init --opencode [--api-key <key>] [--tenant-id <handle>]
|
|
1018
|
+
Configure plugin + anchor for OpenCode
|
|
1019
|
+
init --codex [--api-key <key>] [--tenant-id <handle>] [--install-watcher]
|
|
1020
|
+
Configure lifecycle hooks + anchor for Codex
|
|
843
1021
|
init --all [--root <path>] Standard init for cwd, then bulk-register every
|
|
844
1022
|
other git repo at depth 1 under <path> (default:
|
|
845
1023
|
parent of repo root). Each becomes inbox-addressable.
|
|
@@ -849,20 +1027,22 @@ Commands:
|
|
|
849
1027
|
codex startup install Start the Codex live-push sidecar at login.
|
|
850
1028
|
project-id Print the current project_id
|
|
851
1029
|
session-id [--full] Print this session's id (8-hex, or full UUID with --full).
|
|
852
|
-
discover [--json] Tenant-wide structure: every project, per-shape row counts,
|
|
853
|
-
activity ranges. For cross-project advisors.
|
|
854
|
-
doctor [--inspect] [--yes] Diagnose + repair orphan project_ids and identity drift
|
|
855
|
-
smell "<text>" Shortcut for \`greprag lore smell "<text>"\`.
|
|
856
|
-
|
|
857
|
-
Inbox (email-style messaging across tenants):
|
|
1030
|
+
discover [--json] Tenant-wide structure: every project, per-shape row counts,
|
|
1031
|
+
activity ranges. For cross-project advisors.
|
|
1032
|
+
doctor [--inspect] [--yes] Diagnose + repair orphan project_ids and identity drift
|
|
1033
|
+
smell "<text>" Shortcut for \`greprag lore smell "<text>"\`.
|
|
1034
|
+
|
|
1035
|
+
Inbox (email-style messaging across tenants):
|
|
858
1036
|
inbox [--all] [--session <id>] [--project <name>] [--peek]
|
|
859
|
-
List unread
|
|
860
|
-
|
|
861
|
-
(
|
|
1037
|
+
List unread, auto-scoped to THIS session: its own
|
|
1038
|
+
lines (sender OR recipient) + the front desk
|
|
1039
|
+
(tenant catch-all) + project broadcasts. Other
|
|
1040
|
+
sessions' private lines are hidden by default.
|
|
1041
|
+
--all = audit peek: drop the per-session scope
|
|
1042
|
+
(every session's lines) AND include read history.
|
|
1043
|
+
--session scopes to one specific session's view.
|
|
862
1044
|
--project filters by project name.
|
|
863
1045
|
--peek: NON-MUTATING — does not mark any message read.
|
|
864
|
-
Used by orchestrator-mode sessions to inspect other
|
|
865
|
-
sessions' inboxes without burning their unread state.
|
|
866
1046
|
inbox watchers [--json] List currently-attached live watchers under this
|
|
867
1047
|
tenant. Each row: project · title (session_id) —
|
|
868
1048
|
project + nano title resolved from session memory.
|
|
@@ -874,24 +1054,39 @@ Inbox (email-style messaging across tenants):
|
|
|
874
1054
|
[--project <name>] Filter to one project inbox (default: tenant-wide).
|
|
875
1055
|
[--session <id>] Filter to messages targeting this session
|
|
876
1056
|
(plus untargeted broadcasts).
|
|
1057
|
+
[--receptionist] Attend the front desk — wake live on a tenant
|
|
1058
|
+
catch-all (cold open / inbound email). Without it,
|
|
1059
|
+
cold opens surface only on a manual inbox check.
|
|
877
1060
|
[--since <id|iso>] Resume after a message id or timestamp.
|
|
878
1061
|
[--json] Emit raw JSON per line (for piping to Monitor).
|
|
1062
|
+
inbox claim <id> Receptionist: claim a front-desk record (cold open /
|
|
1063
|
+
inbound email). First claimant wins; a co-armed
|
|
1064
|
+
receptionist stands down — no double-reply.
|
|
879
1065
|
inbox keep <id> Extend a read message's TTL (default delete after 14 days)
|
|
880
1066
|
inbox delete <id> Permanently delete a message
|
|
881
|
-
send "body" --to <addr> Send a message. Body is markdown.
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
send --
|
|
885
|
-
|
|
1067
|
+
send "body" --to <addr> Send a message. Body is markdown.
|
|
1068
|
+
<addr> is a bare handle (tenant catch-all) or
|
|
1069
|
+
handle/<target> — see Addresses below.
|
|
1070
|
+
send --body-file <path> --to <addr>
|
|
1071
|
+
Send a long/multiline body from a file.
|
|
1072
|
+
send --stdin --to <addr> Read message body from stdin.
|
|
886
1073
|
--from-session <id> Sender's session — denormalized so the recipient can
|
|
887
1074
|
reply by session without re-discovery.
|
|
888
1075
|
--memory <uuid> (repeatable) back-pointer to a memory row
|
|
889
1076
|
--artifact <type:id> (repeatable) e.g. commit:abc123, pr:#42, deploy:def
|
|
890
1077
|
--file <path[:lines]> (repeatable) e.g. src/auth.ts, src/auth.ts:42, src/x.ts:10-15
|
|
891
1078
|
--ref-json '<json>' escape hatch — full references object
|
|
1079
|
+
--in-reply-to <id> Thread a reply back to a front-desk record (cold open /
|
|
1080
|
+
inbound email) — sets references.in_reply_to so the
|
|
1081
|
+
ensuing private line continues that conversation.
|
|
892
1082
|
--broadcast Required when <addr> targets a project — every watcher in
|
|
893
1083
|
that project receives the message. Default behavior is
|
|
894
1084
|
session-scoped delivery; this flag is the explicit opt-in.
|
|
1085
|
+
--to-desk Self-addressed internal mail — posts to THIS project's own
|
|
1086
|
+
desk (no recipient; omit --to). Requires --type.
|
|
1087
|
+
--type <internal-type> Internal messageType for --to-desk (e.g. lore_smell). Queue
|
|
1088
|
+
item drained by its consumer — never wakes a watcher and is
|
|
1089
|
+
hidden from the default inbox (audit via 'inbox --all').
|
|
895
1090
|
retract <code> Pull back a previously-sent message (code is printed on send).
|
|
896
1091
|
discord pair Pair Discord DMs to this project — generates a code to DM the bot.
|
|
897
1092
|
discord unpair Remove the Discord pairing.
|
|
@@ -904,13 +1099,30 @@ Inbox (email-style messaging across tenants):
|
|
|
904
1099
|
Unread → hard delete. Read → body replaced with a retracted notice.
|
|
905
1100
|
Idempotent: double-retract is a no-op.
|
|
906
1101
|
|
|
907
|
-
Addresses (v0.
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1102
|
+
Addresses (v0.12+):
|
|
1103
|
+
Cold open: <handle>@greprag.com tenant catch-all — first contact when
|
|
1104
|
+
you don't know their session. Lands in
|
|
1105
|
+
their inbox for a manual check; no live
|
|
1106
|
+
ping. No --broadcast needed.
|
|
1107
|
+
Session: <handle>@greprag.com/<session-uuid> session-to-session (normal, live)
|
|
1108
|
+
Project: <handle>@greprag.com/<project-name> project broadcast — requires --broadcast
|
|
1109
|
+
Self-desk: --to-desk --type <internal-type> internal queue item on your own project
|
|
1110
|
+
desk — no recipient, no wake; drained by
|
|
1111
|
+
its typed consumer (e.g. lore_smell).
|
|
912
1112
|
--session and --project flags were removed; the address carries the target.
|
|
913
1113
|
|
|
1114
|
+
Email (agent-email front desk — drain inbound attachments to local disk):
|
|
1115
|
+
email [pending] List pending front-desk email (envelope only).
|
|
1116
|
+
email pull --id <record> Pull ONE record's attachments to local disk.
|
|
1117
|
+
email pull --all-pending Pull EVERY pending record's attachments.
|
|
1118
|
+
[--to <dir>] Save dir. Default: --to > $GREPRAG_EMAIL_DIR >
|
|
1119
|
+
anchor email_dir > ~/.greprag/email/<project>.
|
|
1120
|
+
[--quiet] Only print the one-line summary.
|
|
1121
|
+
Saved as <subject-slug>-<index>.<ext>; re-pulls
|
|
1122
|
+
are idempotent. Auto-save: set email_autosave=true
|
|
1123
|
+
in .greprag/project.json so the mail hook pulls
|
|
1124
|
+
new attachments each turn.
|
|
1125
|
+
|
|
914
1126
|
Memory (episodic project memory — turn/hourly/daily/weekly/ship-event):
|
|
915
1127
|
memory search "<query>" Lexical retrieval over the project's memory.
|
|
916
1128
|
Same v5 RRF + adjacency pipeline as 'corpus
|
|
@@ -1040,6 +1252,7 @@ Examples:
|
|
|
1040
1252
|
* test flags any dispatched command that never appears in a help surface.) */
|
|
1041
1253
|
const HELP_ALL_GROUPS = [
|
|
1042
1254
|
['inbox', inbox],
|
|
1255
|
+
['email', email_1.runEmail],
|
|
1043
1256
|
['memory', memory_1.runMemory],
|
|
1044
1257
|
['corpus', corpus_1.runCorpus],
|
|
1045
1258
|
['lore', lore_1.runLore],
|
|
@@ -1128,6 +1341,7 @@ async function main() {
|
|
|
1128
1341
|
return;
|
|
1129
1342
|
}
|
|
1130
1343
|
case 'inbox': return inbox(subArgs);
|
|
1344
|
+
case 'email': return (0, email_1.runEmail)(subArgs);
|
|
1131
1345
|
case 'send': return send(subArgs);
|
|
1132
1346
|
case 'retract': return retract(subArgs);
|
|
1133
1347
|
case 'discord': return discord(subArgs);
|