greprag 5.22.3 → 5.24.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.js +13 -3
  5. package/dist/commands/codex-supervisor.js.map +1 -1
  6. package/dist/commands/codex.js +19 -65
  7. package/dist/commands/codex.js.map +1 -1
  8. package/dist/commands/email.d.ts +18 -0
  9. package/dist/commands/email.js +310 -0
  10. package/dist/commands/email.js.map +1 -0
  11. package/dist/commands/inbox-watch.d.ts +4 -0
  12. package/dist/commands/inbox-watch.js +4 -2
  13. package/dist/commands/inbox-watch.js.map +1 -1
  14. package/dist/commands/init.js +96 -38
  15. package/dist/commands/init.js.map +1 -1
  16. package/dist/commands/status.js +17 -6
  17. package/dist/commands/status.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 +80 -10
  28. package/dist/hook.js.map +1 -1
  29. package/dist/index.js +372 -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 +11 -2
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,30 @@ async function inbox(args) {
327
392
  sessionParts.push(`to session ${toShort}`);
328
393
  console.log(` · ${sessionParts.join(' → ')}`);
329
394
  }
395
+ // Fell-back marker: this cold open was a session-targeted send to a session
396
+ // id we didn't recognize, downgraded to the front desk. Surface the intended
397
+ // target so you can route it. adr: adr/address-grammar.md
398
+ const intended = (0, session_id_1.truncateSessionId)(msg.intended_session ?? null);
399
+ if (intended)
400
+ console.log(` · ⤷ meant for session ${intended} (unknown id — fell back to front desk)`);
401
+ // Front-desk envelope line (cold opens / inbound email). The receptionist
402
+ // gates on trust.verdict: 'verified' → may auto-route live; everything else
403
+ // parks for human sign-off. handled_by = already claimed (stand down).
404
+ // adr: adr/address-grammar.md
405
+ if (msg.to_scope === 'tenant') {
406
+ const fd = [];
407
+ if (msg.subject)
408
+ fd.push(`subj: ${msg.subject}`);
409
+ if (msg.ingress === 'email' || msg.trust) {
410
+ const verdict = msg.trust?.verdict ?? 'unevaluated';
411
+ const gate = verdict === 'verified' ? 'auto-route ok' : 'park for sign-off';
412
+ fd.push(`trust: ${verdict} → ${gate}`);
413
+ }
414
+ if (msg.handled_by)
415
+ fd.push(`handled by ${(0, session_id_1.truncateSessionId)(msg.handled_by) ?? msg.handled_by}`);
416
+ if (fd.length)
417
+ console.log(` · ${fd.join(' · ')}`);
418
+ }
330
419
  // Indent each body line so multi-line markdown stays visually grouped
331
420
  for (const line of msg.body.split('\n'))
332
421
  console.log(` ${line}`);
@@ -389,13 +478,14 @@ function parseFileFlag(raw) {
389
478
  * look like UUIDs are rejected at registration time so this is unambiguous.
390
479
  * adr: adr/session-id-awareness.md, adr/address-grammar.md */
391
480
  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>
481
+ /** Parse a send address under the v0.12 grammar:
482
+ * <handle>@greprag.com[/<target>]
394
483
  * where <target> is exactly one segment — a session UUID (or 8-hex short
395
484
  * 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.
485
+ * broadcast. The bare form <handle>@greprag.com (no target) is the tenant
486
+ * catch-all / cold opena first-contact channel for a sender who does not
487
+ * know the recipient's session. The message lands in the recipient's inbox
488
+ * for a manual check; it does not wake their session watchers.
399
489
  *
400
490
  * Returns a discriminated result so the caller can print the helpful
401
491
  * migration hint on failure. */
@@ -411,14 +501,9 @@ function parseSendAddress(addr) {
411
501
  return { ok: true, targetKind: 'discord', target: snowflake };
412
502
  }
413
503
  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
504
  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` +
505
+ return { ok: false, error: `address "${addr}" has too many segments. The v0.12 grammar is\n` +
506
+ ` <handle>@greprag.com[/<target>]\n` +
422
507
  `where <target> is exactly one of: a session UUID OR a project name.\n` +
423
508
  `Legacy <email>/<project>/<session> form is no longer accepted —\n` +
424
509
  `pick the session you actually meant.` };
@@ -427,12 +512,158 @@ function parseSendAddress(addr) {
427
512
  if (!emailPart.includes('@')) {
428
513
  return { ok: false, error: `address "${addr}" missing @greprag.com handle.` };
429
514
  }
515
+ // Bare handle (no target segment) — the tenant catch-all (cold open).
516
+ if (segments.length === 1) {
517
+ return { ok: true, targetKind: 'catchall', target: '' };
518
+ }
430
519
  if (!target) {
431
520
  return { ok: false, error: `address "${addr}" has an empty target segment.` };
432
521
  }
433
522
  const kind = SESSION_ID_PATTERN.test(target) ? 'session' : 'project';
434
523
  return { ok: true, targetKind: kind, target };
435
524
  }
525
+ /** Internal (self-desk) message types — KEEP IN SYNC with
526
+ * packages/core/src/inbox-types.ts INTERNAL_MESSAGE_TYPES. The CLI is a thin
527
+ * HTTP client and does not import @greprag/core (see turn-provenance.ts for the
528
+ * same pattern), so the set is duplicated here for the `--type` fast-fail gate
529
+ * and the `--all` audit-view tag. The server is the authority.
530
+ * adr: adr/address-grammar.md */
531
+ const INTERNAL_MESSAGE_TYPES = ['lore_smell'];
532
+ function isInternalMessageType(t) {
533
+ return !!t && INTERNAL_MESSAGE_TYPES.includes(t);
534
+ }
535
+ /** Flags whose following arg is a value (not the message body). Shared by the
536
+ * `--to` and `--to-desk` send paths so the body-finder skips flag values. */
537
+ const SEND_FLAGS_TAKING_VALUE = new Set([
538
+ '--to', '--memory', '--artifact', '--file', '--ref-json',
539
+ '--from-session', '--in-reply-to', '--type', '--body-file',
540
+ ]);
541
+ /** First positional arg that isn't a flag or a flag's value → the message body. */
542
+ function extractBody(args) {
543
+ // --body-file <path>: read a long/multiline body from a file.
544
+ const bodyFile = getFlag(args, '--body-file');
545
+ if (bodyFile) {
546
+ try {
547
+ return fs.readFileSync(bodyFile, 'utf-8');
548
+ }
549
+ catch (e) {
550
+ console.error(`Error: --body-file could not be read: ${e.message}`);
551
+ process.exit(1);
552
+ }
553
+ }
554
+ // --stdin: read the body from stdin.
555
+ if (args.includes('--stdin')) {
556
+ try {
557
+ return fs.readFileSync(0, 'utf-8');
558
+ }
559
+ catch (e) {
560
+ console.error(`Error: failed reading body from stdin: ${e.message}`);
561
+ process.exit(1);
562
+ }
563
+ }
564
+ // Otherwise the first positional arg that isn't a flag value.
565
+ for (let i = 0; i < args.length; i++) {
566
+ if (args[i].startsWith('--'))
567
+ continue;
568
+ if (i > 0 && SEND_FLAGS_TAKING_VALUE.has(args[i - 1]))
569
+ continue;
570
+ return args[i];
571
+ }
572
+ return undefined;
573
+ }
574
+ /** Build the references object from --memory/--artifact/--file/--ref-json/
575
+ * --in-reply-to flags. Shared by both send paths. Exits on invalid --ref-json. */
576
+ function buildReferences(args) {
577
+ let references;
578
+ const refJsonRaw = getFlag(args, '--ref-json');
579
+ if (refJsonRaw) {
580
+ try {
581
+ references = JSON.parse(refJsonRaw);
582
+ }
583
+ catch (e) {
584
+ console.error(`--ref-json: invalid JSON (${e.message})`);
585
+ process.exit(1);
586
+ }
587
+ }
588
+ else {
589
+ const memoryIds = getFlags(args, '--memory');
590
+ const artifacts = getFlags(args, '--artifact')
591
+ .map(parseArtifactFlag)
592
+ .filter((a) => a !== null);
593
+ const files = getFlags(args, '--file').map(parseFileFlag);
594
+ if (memoryIds.length || artifacts.length || files.length) {
595
+ references = {};
596
+ if (memoryIds.length)
597
+ references.memory_ids = memoryIds;
598
+ if (artifacts.length)
599
+ references.artifacts = artifacts;
600
+ if (files.length)
601
+ references.files = files;
602
+ }
603
+ }
604
+ // --in-reply-to <front-desk-id>: thread the reply back to a cold open /
605
+ // inbound-email record so the ensuing private line continues that
606
+ // conversation (front-desk invariant 5). adr: adr/address-grammar.md
607
+ const inReplyTo = getFlag(args, '--in-reply-to');
608
+ if (inReplyTo) {
609
+ references = { ...(references ?? {}), in_reply_to: inReplyTo };
610
+ }
611
+ return references;
612
+ }
613
+ /** greprag send "<text>" --to-desk --type <internal-type>
614
+ *
615
+ * Self-addressed internal mail: posts to THIS project's own desk (no external
616
+ * recipient) carrying a known internal messageType. Trusted by construction
617
+ * (from.tenant === to.tenant) — bypasses the trust gate, the receptionist
618
+ * wake, and sign-off; never wakes a live watcher. Accrues silently and is
619
+ * drained later by its typed consumer (e.g. lore_smell → /lore-advisor).
620
+ * The generic primitive `greprag lore smell` wraps. adr: adr/address-grammar.md */
621
+ async function sendToDesk(args, cfg, to) {
622
+ if (to) {
623
+ console.error('--to-desk is self-addressed (your own project desk) — drop --to.');
624
+ process.exit(1);
625
+ }
626
+ const type = getFlag(args, '--type');
627
+ const known = INTERNAL_MESSAGE_TYPES.join(', ');
628
+ if (!type) {
629
+ console.error(`--to-desk requires --type <internal-type>.\n Known internal types: ${known}.`);
630
+ process.exit(1);
631
+ }
632
+ if (!isInternalMessageType(type)) {
633
+ console.error(`--type "${type}" is not a known internal type.\n Known internal types: ${known}.`);
634
+ process.exit(1);
635
+ }
636
+ // The desk = the caller's anchor project (same resolver lore/memory use).
637
+ const anchor = (0, project_anchor_1.readAnchor)(process.cwd());
638
+ if (!anchor.projectId) {
639
+ console.error('No project anchor in current directory. Run: greprag init (the desk is this project).');
640
+ process.exit(1);
641
+ }
642
+ const body = extractBody(args);
643
+ if (!body) {
644
+ console.error('Usage: greprag send "<text>" --to-desk --type <internal-type>');
645
+ process.exit(1);
646
+ }
647
+ const payload = {
648
+ to_desk: true,
649
+ type,
650
+ body,
651
+ from_project_id: anchor.projectId,
652
+ };
653
+ const references = buildReferences(args);
654
+ if (references)
655
+ payload.references = references;
656
+ const fromSessionId = getFlag(args, '--from-session');
657
+ if (fromSessionId)
658
+ payload.from_session_id = fromSessionId;
659
+ const result = await apiCall(`${cfg.apiUrl}/v1/inbox/send`, cfg.apiKey, payload);
660
+ const idShort = typeof result.id === 'string' ? result.id.slice(0, 8) : '????????';
661
+ console.log(`Queued to desk (type=${type}, project=${anchor.projectName}) (${idShort})`);
662
+ console.log(` (internal queue item — not inbox mail; drained by its consumer, no live ping. Audit: greprag inbox --all)`);
663
+ const retractCode = result.retract_code;
664
+ if (retractCode)
665
+ console.log(`Retract: greprag retract ${retractCode}`);
666
+ }
436
667
  /** greprag send "body" --to <handle>@greprag.com/<target>
437
668
  * [--from-session <id>] sender's own session — denormalized
438
669
  * onto the message so the recipient can
@@ -444,8 +675,10 @@ function parseSendAddress(addr) {
444
675
  *
445
676
  * <target> is exactly one segment — a session UUID (normal case) or a
446
677
  * 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.
678
+ * (no target) is the tenant catch-all / cold open sendable, no --broadcast
679
+ * needed, lands quietly in the recipient's inbox for a manual check.
680
+ * --session and --project flags were removed in v0.11 — the address carries
681
+ * the target. adr: adr/address-grammar.md
449
682
  */
450
683
  async function send(args) {
451
684
  const cfg = getConfig();
@@ -454,10 +687,15 @@ async function send(args) {
454
687
  process.exit(1);
455
688
  }
456
689
  const to = getFlag(args, '--to');
690
+ // Self-addressed internal mail: `greprag send "<text>" --to-desk --type <t>`.
691
+ // No external recipient — lands on this project's own desk and is drained by
692
+ // its typed consumer. Branch out before the --to-dependent path below.
693
+ // adr: adr/address-grammar.md
694
+ if (args.includes('--to-desk')) {
695
+ return sendToDesk(args, cfg, to);
696
+ }
457
697
  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>');
698
+ 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
699
  process.exit(1);
462
700
  }
463
701
  // Early validation — fail before the round-trip so the operator sees the
@@ -489,42 +727,9 @@ async function send(args) {
489
727
  process.exit(1);
490
728
  }
491
729
  }
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
- }
730
+ // Body + references via the shared helpers (also used by the --to-desk path).
731
+ // extractBody handles --body-file / --stdin / positional.
732
+ const body = extractBody(args);
528
733
  if (!body || body.trim().length === 0) {
529
734
  console.error('Error: message body is missing.');
530
735
  console.error('Use one of:');
@@ -533,34 +738,7 @@ async function send(args) {
533
738
  console.error(' type report.md | greprag send --stdin --to <handle>@greprag.com/<target>');
534
739
  process.exit(1);
535
740
  }
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
- }
741
+ const references = buildReferences(args);
564
742
  const payload = { to, body };
565
743
  if (references)
566
744
  payload.references = references;
@@ -588,6 +766,21 @@ async function send(args) {
588
766
  : (delivered.project ? '/' + delivered.project : '');
589
767
  const idShort = typeof result.id === 'string' ? result.id.slice(0, 8) : '????????';
590
768
  console.log(`Sent to ${base}${targetSegment} (${idShort})`);
769
+ // Unknown-session fallback: the server downgraded a session-targeted send to
770
+ // the recipient's front desk because the session id wasn't a real session
771
+ // under their tenant. Warn loudly so the sender fixes the id (or retracts)
772
+ // instead of believing it reached the session. adr: adr/address-grammar.md
773
+ const fellBack = result.fell_back_to_desk;
774
+ if (fellBack) {
775
+ console.warn(` ⚠ session ${(0, session_id_1.truncateSessionId)(fellBack) ?? fellBack} is not a known session under ${base} —\n` +
776
+ ` delivered to their front desk (cold open) instead, marked "meant for ${(0, session_id_1.truncateSessionId)(fellBack) ?? fellBack}".\n` +
777
+ ` If that id is wrong, retract and resend with the correct session.`);
778
+ }
779
+ else if (parsed.ok && parsed.targetKind === 'catchall') {
780
+ // Cold-open hint: a catch-all drop waits for a manual `greprag inbox` check
781
+ // on the recipient's side; it does not wake their live session watcher.
782
+ console.log(` (tenant catch-all — lands in their inbox for a manual check, no live ping)`);
783
+ }
591
784
  const retractCode = result.retract_code;
592
785
  if (retractCode)
593
786
  console.log(`Retract: greprag retract ${retractCode}`);
@@ -807,39 +1000,40 @@ async function retract(args) {
807
1000
  console.log((0, inbox_retract_1.formatRetractResult)(result.status));
808
1001
  }
809
1002
  // -- 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
1003
+ const INIT_HELP = `greprag init — configure GrepRAG for an agent client.
1004
+
1005
+ greprag init
1006
+ greprag init --codex [--tenant-id <handle>|--api-key <key>] [--install-watcher]
1007
+ greprag init --claude [--tenant-id <handle>|--api-key <key>]
1008
+ greprag init --opencode [--tenant-id <handle>|--api-key <key>]
1009
+ greprag init --all [--root <path>]
1010
+ greprag init --global [--name <name>]
1011
+
1012
+ Options:
1013
+ --tenant-id <handle> Provision/use public handle <handle>@greprag.com.
1014
+ Example: --tenant-id tanya -> tanya@greprag.com
1015
+ --api-key <key> Use an existing GrepRAG API key instead of provisioning.
1016
+ --codex Configure Codex hooks + /greprag skill.
1017
+ --install-watcher With --codex, install the live inbox watcher at login.
1018
+ --claude Configure Claude Code hooks + /greprag skill.
1019
+ --opencode Configure OpenCode plugin.
1020
+
1021
+ Codex Desktop trust step:
1022
+ After init, start a fresh Codex session, then open Settings -> Settings -> Hooks
1023
+ and trust the 6 GrepRAG hooks.`;
1024
+ const HELP = `
1025
+ greprag — agent memory for Claude Code, Codex, and OpenCode
1026
+
1027
+ Commands:
1028
+ init [--api-key <key>] [--tenant-id <handle>]
1029
+ Auto-detect/ask for Codex, Claude Code, or OpenCode
1030
+ init --claude [--api-key <key>] [--tenant-id <handle>]
1031
+ Configure hooks + API key for Claude Code
1032
+ init --global [--name <name>] Create ~/.greprag/project.json (global anchor)
1033
+ init --opencode [--api-key <key>] [--tenant-id <handle>]
1034
+ Configure plugin + anchor for OpenCode
1035
+ init --codex [--api-key <key>] [--tenant-id <handle>] [--install-watcher]
1036
+ Configure lifecycle hooks + anchor for Codex
843
1037
  init --all [--root <path>] Standard init for cwd, then bulk-register every
844
1038
  other git repo at depth 1 under <path> (default:
845
1039
  parent of repo root). Each becomes inbox-addressable.
@@ -849,20 +1043,22 @@ Commands:
849
1043
  codex startup install Start the Codex live-push sidecar at login.
850
1044
  project-id Print the current project_id
851
1045
  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):
1046
+ discover [--json] Tenant-wide structure: every project, per-shape row counts,
1047
+ activity ranges. For cross-project advisors.
1048
+ doctor [--inspect] [--yes] Diagnose + repair orphan project_ids and identity drift
1049
+ smell "<text>" Shortcut for \`greprag lore smell "<text>"\`.
1050
+
1051
+ Inbox (email-style messaging across tenants):
858
1052
  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).
1053
+ List unread, auto-scoped to THIS session: its own
1054
+ lines (sender OR recipient) + the front desk
1055
+ (tenant catch-all) + project broadcasts. Other
1056
+ sessions' private lines are hidden by default.
1057
+ --all = audit peek: drop the per-session scope
1058
+ (every session's lines) AND include read history.
1059
+ --session scopes to one specific session's view.
862
1060
  --project filters by project name.
863
1061
  --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
1062
  inbox watchers [--json] List currently-attached live watchers under this
867
1063
  tenant. Each row: project · title (session_id) —
868
1064
  project + nano title resolved from session memory.
@@ -874,24 +1070,39 @@ Inbox (email-style messaging across tenants):
874
1070
  [--project <name>] Filter to one project inbox (default: tenant-wide).
875
1071
  [--session <id>] Filter to messages targeting this session
876
1072
  (plus untargeted broadcasts).
1073
+ [--receptionist] Attend the front desk — wake live on a tenant
1074
+ catch-all (cold open / inbound email). Without it,
1075
+ cold opens surface only on a manual inbox check.
877
1076
  [--since <id|iso>] Resume after a message id or timestamp.
878
1077
  [--json] Emit raw JSON per line (for piping to Monitor).
1078
+ inbox claim <id> Receptionist: claim a front-desk record (cold open /
1079
+ inbound email). First claimant wins; a co-armed
1080
+ receptionist stands down — no double-reply.
879
1081
  inbox keep <id> Extend a read message's TTL (default delete after 14 days)
880
1082
  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.
1083
+ send "body" --to <addr> Send a message. Body is markdown.
1084
+ <addr> is a bare handle (tenant catch-all) or
1085
+ handle/<target> see Addresses below.
1086
+ send --body-file <path> --to <addr>
1087
+ Send a long/multiline body from a file.
1088
+ send --stdin --to <addr> Read message body from stdin.
886
1089
  --from-session <id> Sender's session — denormalized so the recipient can
887
1090
  reply by session without re-discovery.
888
1091
  --memory <uuid> (repeatable) back-pointer to a memory row
889
1092
  --artifact <type:id> (repeatable) e.g. commit:abc123, pr:#42, deploy:def
890
1093
  --file <path[:lines]> (repeatable) e.g. src/auth.ts, src/auth.ts:42, src/x.ts:10-15
891
1094
  --ref-json '<json>' escape hatch — full references object
1095
+ --in-reply-to <id> Thread a reply back to a front-desk record (cold open /
1096
+ inbound email) — sets references.in_reply_to so the
1097
+ ensuing private line continues that conversation.
892
1098
  --broadcast Required when <addr> targets a project — every watcher in
893
1099
  that project receives the message. Default behavior is
894
1100
  session-scoped delivery; this flag is the explicit opt-in.
1101
+ --to-desk Self-addressed internal mail — posts to THIS project's own
1102
+ desk (no recipient; omit --to). Requires --type.
1103
+ --type <internal-type> Internal messageType for --to-desk (e.g. lore_smell). Queue
1104
+ item drained by its consumer — never wakes a watcher and is
1105
+ hidden from the default inbox (audit via 'inbox --all').
895
1106
  retract <code> Pull back a previously-sent message (code is printed on send).
896
1107
  discord pair Pair Discord DMs to this project — generates a code to DM the bot.
897
1108
  discord unpair Remove the Discord pairing.
@@ -904,13 +1115,30 @@ Inbox (email-style messaging across tenants):
904
1115
  Unread → hard delete. Read → body replaced with a retracted notice.
905
1116
  Idempotent: double-retract is a no-op.
906
1117
 
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.
1118
+ Addresses (v0.12+):
1119
+ Cold open: <handle>@greprag.com tenant catch-all first contact when
1120
+ you don't know their session. Lands in
1121
+ their inbox for a manual check; no live
1122
+ ping. No --broadcast needed.
1123
+ Session: <handle>@greprag.com/<session-uuid> session-to-session (normal, live)
1124
+ Project: <handle>@greprag.com/<project-name> project broadcast — requires --broadcast
1125
+ Self-desk: --to-desk --type <internal-type> internal queue item on your own project
1126
+ desk — no recipient, no wake; drained by
1127
+ its typed consumer (e.g. lore_smell).
912
1128
  --session and --project flags were removed; the address carries the target.
913
1129
 
1130
+ Email (agent-email front desk — drain inbound attachments to local disk):
1131
+ email [pending] List pending front-desk email (envelope only).
1132
+ email pull --id <record> Pull ONE record's attachments to local disk.
1133
+ email pull --all-pending Pull EVERY pending record's attachments.
1134
+ [--to <dir>] Save dir. Default: --to > $GREPRAG_EMAIL_DIR >
1135
+ anchor email_dir > ~/.greprag/email/<project>.
1136
+ [--quiet] Only print the one-line summary.
1137
+ Saved as <subject-slug>-<index>.<ext>; re-pulls
1138
+ are idempotent. Auto-save: set email_autosave=true
1139
+ in .greprag/project.json so the mail hook pulls
1140
+ new attachments each turn.
1141
+
914
1142
  Memory (episodic project memory — turn/hourly/daily/weekly/ship-event):
915
1143
  memory search "<query>" Lexical retrieval over the project's memory.
916
1144
  Same v5 RRF + adjacency pipeline as 'corpus
@@ -1040,6 +1268,7 @@ Examples:
1040
1268
  * test flags any dispatched command that never appears in a help surface.) */
1041
1269
  const HELP_ALL_GROUPS = [
1042
1270
  ['inbox', inbox],
1271
+ ['email', email_1.runEmail],
1043
1272
  ['memory', memory_1.runMemory],
1044
1273
  ['corpus', corpus_1.runCorpus],
1045
1274
  ['lore', lore_1.runLore],
@@ -1128,6 +1357,7 @@ async function main() {
1128
1357
  return;
1129
1358
  }
1130
1359
  case 'inbox': return inbox(subArgs);
1360
+ case 'email': return (0, email_1.runEmail)(subArgs);
1131
1361
  case 'send': return send(subArgs);
1132
1362
  case 'retract': return retract(subArgs);
1133
1363
  case 'discord': return discord(subArgs);