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.
Files changed (38) hide show
  1. package/dist/commands/codex-doctor.d.ts +12 -0
  2. package/dist/commands/codex-doctor.js +167 -0
  3. package/dist/commands/codex-doctor.js.map +1 -0
  4. package/dist/commands/codex-supervisor.d.ts +1 -0
  5. package/dist/commands/codex-supervisor.js +113 -0
  6. package/dist/commands/codex-supervisor.js.map +1 -0
  7. package/dist/commands/codex.d.ts +0 -6
  8. package/dist/commands/codex.js +30 -78
  9. package/dist/commands/codex.js.map +1 -1
  10. package/dist/commands/email.d.ts +18 -0
  11. package/dist/commands/email.js +310 -0
  12. package/dist/commands/email.js.map +1 -0
  13. package/dist/commands/inbox-watch.d.ts +4 -0
  14. package/dist/commands/inbox-watch.js +4 -2
  15. package/dist/commands/inbox-watch.js.map +1 -1
  16. package/dist/commands/init.js +50 -7
  17. package/dist/commands/init.js.map +1 -1
  18. package/dist/email-pull.d.ts +84 -0
  19. package/dist/email-pull.js +203 -0
  20. package/dist/email-pull.js.map +1 -0
  21. package/dist/email-send.d.ts +64 -0
  22. package/dist/email-send.js +124 -0
  23. package/dist/email-send.js.map +1 -0
  24. package/dist/front-desk-mail.d.ts +50 -0
  25. package/dist/front-desk-mail.js +206 -0
  26. package/dist/front-desk-mail.js.map +1 -0
  27. package/dist/hook.js +61 -2
  28. package/dist/hook.js.map +1 -1
  29. package/dist/index.js +356 -142
  30. package/dist/index.js.map +1 -1
  31. package/dist/project-anchor.d.ts +6 -0
  32. package/dist/project-anchor.js +10 -0
  33. package/dist/project-anchor.js.map +1 -1
  34. package/package.json +1 -1
  35. package/scripts/postinstall.js +1 -1
  36. package/skill/greprag/SKILL.md +1 -1
  37. package/skill/greprag/docs/inbox.md +7 -5
  38. 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 messages (marks them read).
183
- greprag inbox --all Include already-read messages.
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> Filter to messages targeting one session.
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/<target> Message a session/project.`;
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 sessionId = getFlag(args, '--session');
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
- const status = msg.was_unread ? new' : '';
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.11 grammar:
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 identity-only and
397
- * cannot be sent torecipients share it as their contact form, senders
398
- * must supply a target.
479
+ * broadcast. The bare form <handle>@greprag.com (no target) is the tenant
480
+ * catch-all / cold opena 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.11 grammar is\n` +
421
- ` <handle>@greprag.com/<target>\n` +
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 receive-only; sending to it errors. --session and --project flags
448
- * were removed in v0.11 the address carries the target.
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('Error: --to is required.');
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
- // Find the body first positional arg that isn't a flag value. We need to
493
- // skip both the flag itself and the value of any preceding flag.
494
- const flagsTakingValue = new Set([
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
- // Build references from flags (or take the explicit --ref-json if provided).
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 and trust the 6 GrepRAG hooks.`;
830
- const HELP = `
831
- greprag agent memory for Claude Code, Codex, and OpenCode
832
-
833
- Commands:
834
- init [--api-key <key>] [--tenant-id <handle>]
835
- Auto-detect/ask for Codex, Claude Code, or OpenCode
836
- init --claude [--api-key <key>] [--tenant-id <handle>]
837
- Configure hooks + API key for Claude Code
838
- init --global [--name <name>] Create ~/.greprag/project.json (global anchor)
839
- init --opencode [--api-key <key>] [--tenant-id <handle>]
840
- Configure plugin + anchor for OpenCode
841
- init --codex [--api-key <key>] [--tenant-id <handle>] [--install-watcher]
842
- Configure lifecycle hooks + anchor for Codex
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 (auto-marks read). --all for history.
860
- --session filters to messages targeting this session
861
- (plus untargeted broadcasts).
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
- send --body-file <path> --to <addr>
883
- Send a long/multiline body from a file.
884
- send --stdin --to <addr> Read message body from stdin.
885
- <addr> must include a target segment see Addresses below.
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.11+):
908
- Share: <handle>@greprag.com identity-only, receive form
909
- Send: <handle>@greprag.com/<session-uuid> session-to-session (normal)
910
- <handle>@greprag.com/<project-name> project broadcast requires --broadcast
911
- The bare share form cannot be sent to — operators must pick a target.
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);