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
@@ -0,0 +1,64 @@
1
+ /** Outbound email send — the SEND half of `greprag email`, mirror of the
2
+ * `email-pull` drain. Thin HTTP client: reads the body (+ optional html),
3
+ * base64s any `--attach` files LOCALLY, POSTs the payload to /v1/email/send
4
+ * (authed by the API key — no wrangler/R2 creds), prints the result.
5
+ *
6
+ * The CLI NEVER asserts a From identity: the server resolves + guards it from
7
+ * the authenticated tenant (a caller can only send as its own @greprag.com
8
+ * handle). `--from` is at most a preference among the caller's own handles,
9
+ * validated server-side. The pure payload builder is unit-tested without
10
+ * network. adr: docs/agent-email.md
11
+ */
12
+ export interface SendAttachmentInput {
13
+ filename: string;
14
+ mime: string | null;
15
+ /** base64-encoded content (inline — the Worker has no disk to fetch from). */
16
+ base64: string;
17
+ }
18
+ /** Wire payload for POST /v1/email/send (snake_case mirrors the route). */
19
+ export interface SendPayload {
20
+ to: string[];
21
+ subject: string;
22
+ body?: string;
23
+ html?: string;
24
+ cc?: string[];
25
+ bcc?: string[];
26
+ reply_to?: string;
27
+ from?: string;
28
+ attachments?: SendAttachmentInput[];
29
+ }
30
+ export interface SendOptions {
31
+ to: string[];
32
+ subject: string;
33
+ bodyText?: string | null;
34
+ htmlText?: string | null;
35
+ cc?: string[];
36
+ bcc?: string[];
37
+ replyTo?: string | null;
38
+ from?: string | null;
39
+ /** Local file paths to attach — read + base64'd by buildSendPayload. */
40
+ attachPaths?: string[];
41
+ }
42
+ /** Best-effort MIME from a filename extension; octet-stream when unknown. */
43
+ export declare function guessMime(filename: string): string;
44
+ /** Read a body source: a file path, or '-' for stdin. */
45
+ export declare function readBodySource(src: string): string;
46
+ /** base64-encode a local file into an attachment entry. */
47
+ export declare function fileToAttachment(filePath: string): SendAttachmentInput;
48
+ /** Build the wire payload from resolved options. Reads + base64s any
49
+ * attachment files (the only I/O); everything else is a pure map. Keeping the
50
+ * shape here (not in the arg parser) makes the payload unit-testable. */
51
+ export declare function buildSendPayload(opts: SendOptions): SendPayload;
52
+ export interface SendResult {
53
+ ok: boolean;
54
+ from?: string;
55
+ to?: string[];
56
+ cc?: string[];
57
+ subject?: string;
58
+ message_id?: string | null;
59
+ attachments?: number;
60
+ egress_key?: string | null;
61
+ error?: string;
62
+ }
63
+ /** POST the payload to /v1/email/send. Throws on a non-ok response. */
64
+ export declare function postSend(apiUrl: string, apiKey: string, payload: SendPayload): Promise<SendResult>;
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ /** Outbound email send — the SEND half of `greprag email`, mirror of the
3
+ * `email-pull` drain. Thin HTTP client: reads the body (+ optional html),
4
+ * base64s any `--attach` files LOCALLY, POSTs the payload to /v1/email/send
5
+ * (authed by the API key — no wrangler/R2 creds), prints the result.
6
+ *
7
+ * The CLI NEVER asserts a From identity: the server resolves + guards it from
8
+ * the authenticated tenant (a caller can only send as its own @greprag.com
9
+ * handle). `--from` is at most a preference among the caller's own handles,
10
+ * validated server-side. The pure payload builder is unit-tested without
11
+ * network. adr: docs/agent-email.md
12
+ */
13
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ var desc = Object.getOwnPropertyDescriptor(m, k);
16
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
17
+ desc = { enumerable: true, get: function() { return m[k]; } };
18
+ }
19
+ Object.defineProperty(o, k2, desc);
20
+ }) : (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ o[k2] = m[k];
23
+ }));
24
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
25
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
26
+ }) : function(o, v) {
27
+ o["default"] = v;
28
+ });
29
+ var __importStar = (this && this.__importStar) || (function () {
30
+ var ownKeys = function(o) {
31
+ ownKeys = Object.getOwnPropertyNames || function (o) {
32
+ var ar = [];
33
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
34
+ return ar;
35
+ };
36
+ return ownKeys(o);
37
+ };
38
+ return function (mod) {
39
+ if (mod && mod.__esModule) return mod;
40
+ var result = {};
41
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
42
+ __setModuleDefault(result, mod);
43
+ return result;
44
+ };
45
+ })();
46
+ Object.defineProperty(exports, "__esModule", { value: true });
47
+ exports.guessMime = guessMime;
48
+ exports.readBodySource = readBodySource;
49
+ exports.fileToAttachment = fileToAttachment;
50
+ exports.buildSendPayload = buildSendPayload;
51
+ exports.postSend = postSend;
52
+ const fs = __importStar(require("fs"));
53
+ const path = __importStar(require("path"));
54
+ const MIME_BY_EXT = {
55
+ pdf: 'application/pdf', png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg',
56
+ gif: 'image/gif', webp: 'image/webp', svg: 'image/svg+xml',
57
+ txt: 'text/plain', md: 'text/markdown', csv: 'text/csv', html: 'text/html',
58
+ json: 'application/json', xml: 'application/xml',
59
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
60
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
61
+ pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
62
+ zip: 'application/zip',
63
+ };
64
+ /** Best-effort MIME from a filename extension; octet-stream when unknown. */
65
+ function guessMime(filename) {
66
+ const m = (filename || '').toLowerCase().match(/\.([a-z0-9]+)$/);
67
+ return (m && MIME_BY_EXT[m[1]]) || 'application/octet-stream';
68
+ }
69
+ /** Read a body source: a file path, or '-' for stdin. */
70
+ function readBodySource(src) {
71
+ if (src === '-')
72
+ return fs.readFileSync(0, 'utf-8');
73
+ return fs.readFileSync(src, 'utf-8');
74
+ }
75
+ /** base64-encode a local file into an attachment entry. */
76
+ function fileToAttachment(filePath) {
77
+ const buf = fs.readFileSync(filePath);
78
+ return {
79
+ filename: path.basename(filePath),
80
+ mime: guessMime(filePath),
81
+ base64: buf.toString('base64'),
82
+ };
83
+ }
84
+ /** Build the wire payload from resolved options. Reads + base64s any
85
+ * attachment files (the only I/O); everything else is a pure map. Keeping the
86
+ * shape here (not in the arg parser) makes the payload unit-testable. */
87
+ function buildSendPayload(opts) {
88
+ const payload = { to: opts.to, subject: opts.subject };
89
+ if (opts.bodyText)
90
+ payload.body = opts.bodyText;
91
+ if (opts.htmlText)
92
+ payload.html = opts.htmlText;
93
+ if (opts.cc && opts.cc.length)
94
+ payload.cc = opts.cc;
95
+ if (opts.bcc && opts.bcc.length)
96
+ payload.bcc = opts.bcc;
97
+ if (opts.replyTo)
98
+ payload.reply_to = opts.replyTo;
99
+ if (opts.from)
100
+ payload.from = opts.from;
101
+ const atts = (opts.attachPaths || []).map(fileToAttachment);
102
+ if (atts.length)
103
+ payload.attachments = atts;
104
+ return payload;
105
+ }
106
+ /** POST the payload to /v1/email/send. Throws on a non-ok response. */
107
+ async function postSend(apiUrl, apiKey, payload) {
108
+ const url = `${apiUrl.replace(/\/+$/, '')}/v1/email/send`;
109
+ const res = await fetch(url, {
110
+ method: 'POST',
111
+ headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
112
+ body: JSON.stringify(payload),
113
+ });
114
+ let data = { ok: false };
115
+ try {
116
+ data = await res.json();
117
+ }
118
+ catch { /* non-JSON error body */ }
119
+ if (!res.ok || !data.ok) {
120
+ throw new Error(data.error || `API ${res.status}`);
121
+ }
122
+ return data;
123
+ }
124
+ //# sourceMappingURL=email-send.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-send.js","sourceRoot":"","sources":["../src/email-send.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ /** "You've got mail" — the front-desk turn notification (Chip B).
2
+ *
3
+ * A turn-based announcement, NOT a live watcher interrupt: each turn the hook
4
+ * asks the server for ENVELOPE-ONLY pending front-desk mail and, for any
5
+ * arrival it hasn't announced yet this machine, injects a one-line "you've got
6
+ * mail from X" notice. It feeds the human sign-off gate — it announces that a
7
+ * record arrived; it NEVER ingests body content (front-desk store contract
8
+ * invariant 2/3). The envelope type carries no `body` field, so body-leakage is
9
+ * impossible by construction here, and the server's `/v1/email/pending`
10
+ * projection withholds body too — defense at both ends.
11
+ *
12
+ * De-dupe: each record id is announced once (stamped in ~/.greprag/state.json),
13
+ * so a still-unread message doesn't re-nag every single turn — only genuinely
14
+ * new arrivals fire. The agent makes it stop by acting on the mail
15
+ * (`greprag email`), which marks it read and drops it from `pending`.
16
+ */
17
+ /** Envelope of a pending front-desk record. NO `body` — by design. */
18
+ export interface FrontDeskEnvelope {
19
+ id: string;
20
+ nodeId: string;
21
+ from: string | null;
22
+ subject: string | null;
23
+ trustVerdict: 'verified' | 'unverified' | 'failed' | null;
24
+ attachmentCount: number;
25
+ createdAt: string;
26
+ }
27
+ /** Strip control chars / newlines and truncate untrusted envelope text before
28
+ * it enters the agent's context. The subject + from are attacker-controllable;
29
+ * rendering them on one clean line (no embedded newlines / escape sequences)
30
+ * blunts the obvious injection shapes without pretending the text is trusted.
31
+ *
32
+ * Done as a codepoint scan rather than a control-char regex on purpose — it is
33
+ * unambiguous in source (no literal control bytes in the file) and replaces
34
+ * every C0 control char + DEL with a space. */
35
+ export declare function sanitizeEnvelopeText(text: string | null, max: number): string;
36
+ /** Render the envelope-only notice. Pure: takes envelopes, returns a string (or
37
+ * null when there's nothing to announce). Provably body-free — `FrontDeskEnvelope`
38
+ * has no body field to render. */
39
+ export declare function renderFrontDeskNotice(envelopes: FrontDeskEnvelope[]): string | null;
40
+ /** Fetch envelope-only pending front-desk mail. Returns [] on any error so the
41
+ * hook never blocks a turn. */
42
+ export declare function fetchPendingMail(apiUrl: string, apiKey: string): Promise<FrontDeskEnvelope[]>;
43
+ /** Filter envelopes to those NOT yet announced. Pure given `stamps`. */
44
+ export declare function filterUnannounced(envelopes: FrontDeskEnvelope[], stamps: Record<string, string>): FrontDeskEnvelope[];
45
+ /** Mark a batch of ids announced and persist (pruning stale stamps). Best-effort. */
46
+ export declare function stampAnnounced(ids: string[]): void;
47
+ /** End-to-end: fetch pending mail, drop already-announced ids, render the
48
+ * envelope-only notice, stamp the freshly-announced ids. Returns null when
49
+ * there's nothing new. The data path is body-free throughout. */
50
+ export declare function buildFrontDeskNotice(apiUrl: string, apiKey: string): Promise<string | null>;
@@ -0,0 +1,206 @@
1
+ "use strict";
2
+ /** "You've got mail" — the front-desk turn notification (Chip B).
3
+ *
4
+ * A turn-based announcement, NOT a live watcher interrupt: each turn the hook
5
+ * asks the server for ENVELOPE-ONLY pending front-desk mail and, for any
6
+ * arrival it hasn't announced yet this machine, injects a one-line "you've got
7
+ * mail from X" notice. It feeds the human sign-off gate — it announces that a
8
+ * record arrived; it NEVER ingests body content (front-desk store contract
9
+ * invariant 2/3). The envelope type carries no `body` field, so body-leakage is
10
+ * impossible by construction here, and the server's `/v1/email/pending`
11
+ * projection withholds body too — defense at both ends.
12
+ *
13
+ * De-dupe: each record id is announced once (stamped in ~/.greprag/state.json),
14
+ * so a still-unread message doesn't re-nag every single turn — only genuinely
15
+ * new arrivals fire. The agent makes it stop by acting on the mail
16
+ * (`greprag email`), which marks it read and drops it from `pending`.
17
+ */
18
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ var desc = Object.getOwnPropertyDescriptor(m, k);
21
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
22
+ desc = { enumerable: true, get: function() { return m[k]; } };
23
+ }
24
+ Object.defineProperty(o, k2, desc);
25
+ }) : (function(o, m, k, k2) {
26
+ if (k2 === undefined) k2 = k;
27
+ o[k2] = m[k];
28
+ }));
29
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
30
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
31
+ }) : function(o, v) {
32
+ o["default"] = v;
33
+ });
34
+ var __importStar = (this && this.__importStar) || (function () {
35
+ var ownKeys = function(o) {
36
+ ownKeys = Object.getOwnPropertyNames || function (o) {
37
+ var ar = [];
38
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
39
+ return ar;
40
+ };
41
+ return ownKeys(o);
42
+ };
43
+ return function (mod) {
44
+ if (mod && mod.__esModule) return mod;
45
+ var result = {};
46
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
47
+ __setModuleDefault(result, mod);
48
+ return result;
49
+ };
50
+ })();
51
+ Object.defineProperty(exports, "__esModule", { value: true });
52
+ exports.sanitizeEnvelopeText = sanitizeEnvelopeText;
53
+ exports.renderFrontDeskNotice = renderFrontDeskNotice;
54
+ exports.fetchPendingMail = fetchPendingMail;
55
+ exports.filterUnannounced = filterUnannounced;
56
+ exports.stampAnnounced = stampAnnounced;
57
+ exports.buildFrontDeskNotice = buildFrontDeskNotice;
58
+ const path = __importStar(require("path"));
59
+ const fs = __importStar(require("fs"));
60
+ const SUBJECT_MAX = 80;
61
+ const FROM_MAX = 60;
62
+ /** Strip control chars / newlines and truncate untrusted envelope text before
63
+ * it enters the agent's context. The subject + from are attacker-controllable;
64
+ * rendering them on one clean line (no embedded newlines / escape sequences)
65
+ * blunts the obvious injection shapes without pretending the text is trusted.
66
+ *
67
+ * Done as a codepoint scan rather than a control-char regex on purpose — it is
68
+ * unambiguous in source (no literal control bytes in the file) and replaces
69
+ * every C0 control char + DEL with a space. */
70
+ function sanitizeEnvelopeText(text, max) {
71
+ if (!text)
72
+ return '';
73
+ let out = '';
74
+ for (const ch of text) {
75
+ const code = ch.codePointAt(0) ?? 0;
76
+ out += (code < 0x20 || code === 0x7f) ? ' ' : ch;
77
+ }
78
+ const oneLine = out.replace(/\s+/g, ' ').trim();
79
+ return oneLine.length > max ? oneLine.slice(0, max - 1) + '…' : oneLine;
80
+ }
81
+ function verdictTag(v) {
82
+ switch (v) {
83
+ case 'verified': return 'verified';
84
+ case 'failed': return '⚠ FAILED-AUTH';
85
+ case 'unverified': return 'unverified';
86
+ default: return 'unverified';
87
+ }
88
+ }
89
+ /** Render the envelope-only notice. Pure: takes envelopes, returns a string (or
90
+ * null when there's nothing to announce). Provably body-free — `FrontDeskEnvelope`
91
+ * has no body field to render. */
92
+ function renderFrontDeskNotice(envelopes) {
93
+ if (!envelopes || envelopes.length === 0)
94
+ return null;
95
+ const n = envelopes.length;
96
+ const lines = [];
97
+ lines.push(`📬 You've got mail — ${n} new front-desk message${n === 1 ? '' : 's'} (envelope only; body stays sealed until you sign off):`);
98
+ for (const e of envelopes) {
99
+ const from = sanitizeEnvelopeText(e.from, FROM_MAX) || '(unknown sender)';
100
+ const subject = sanitizeEnvelopeText(e.subject, SUBJECT_MAX) || '(no subject)';
101
+ const att = e.attachmentCount > 0
102
+ ? ` (${e.attachmentCount} attachment${e.attachmentCount === 1 ? '' : 's'})`
103
+ : '';
104
+ lines.push(` · [${verdictTag(e.trustVerdict)}] from ${from} — "${subject}"${att}`);
105
+ }
106
+ lines.push(`Act on it: \`greprag email\` reads the body deliberately. unverified/failed mail never auto-acts — you sign off first.`);
107
+ return lines.join('\n');
108
+ }
109
+ /** Fetch envelope-only pending front-desk mail. Returns [] on any error so the
110
+ * hook never blocks a turn. */
111
+ async function fetchPendingMail(apiUrl, apiKey) {
112
+ try {
113
+ const url = `${apiUrl.replace(/\/+$/, '')}/v1/email/pending`;
114
+ const res = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
115
+ if (!res.ok)
116
+ return [];
117
+ const data = await res.json();
118
+ return (data.pending || []).map(r => ({
119
+ id: r.id,
120
+ nodeId: r.node_id,
121
+ from: r.from,
122
+ subject: r.subject,
123
+ trustVerdict: r.trust_verdict,
124
+ attachmentCount: r.attachment_count ?? 0,
125
+ createdAt: r.created_at,
126
+ }));
127
+ }
128
+ catch {
129
+ return [];
130
+ }
131
+ }
132
+ // ---- Once-per-arrival de-dupe (state.json) ---------------------------------
133
+ const ANNOUNCE_TTL_MS = 30 * 86_400_000; // prune stamps older than 30 days
134
+ function statePath() {
135
+ const home = process.env.HOME || process.env.USERPROFILE || '';
136
+ if (!home)
137
+ return null;
138
+ return path.join(home, '.greprag', 'state.json');
139
+ }
140
+ function readState() {
141
+ const file = statePath();
142
+ if (!file)
143
+ return {};
144
+ try {
145
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf-8'));
146
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
147
+ }
148
+ catch {
149
+ return {};
150
+ }
151
+ }
152
+ function readAnnouncedStamps(state) {
153
+ const stamps = state.frontDeskAnnounced;
154
+ return stamps && typeof stamps === 'object' && !Array.isArray(stamps)
155
+ ? stamps
156
+ : {};
157
+ }
158
+ /** Filter envelopes to those NOT yet announced. Pure given `stamps`. */
159
+ function filterUnannounced(envelopes, stamps) {
160
+ return envelopes.filter(e => !stamps[e.id]);
161
+ }
162
+ /** Mark a batch of ids announced and persist (pruning stale stamps). Best-effort. */
163
+ function stampAnnounced(ids) {
164
+ const file = statePath();
165
+ if (!file || ids.length === 0)
166
+ return;
167
+ try {
168
+ const state = readState();
169
+ const stamps = readAnnouncedStamps(state);
170
+ const nowIso = new Date().toISOString();
171
+ const nowMs = Date.now();
172
+ for (const id of ids)
173
+ stamps[id] = nowIso;
174
+ // Prune stale stamps so the map can't grow without bound.
175
+ for (const [id, iso] of Object.entries(stamps)) {
176
+ const ms = Date.parse(iso);
177
+ if (Number.isFinite(ms) && nowMs - ms > ANNOUNCE_TTL_MS)
178
+ delete stamps[id];
179
+ }
180
+ state.frontDeskAnnounced = stamps;
181
+ const dir = path.dirname(file);
182
+ if (!fs.existsSync(dir))
183
+ fs.mkdirSync(dir, { recursive: true });
184
+ fs.writeFileSync(file, JSON.stringify(state, null, 2) + '\n');
185
+ }
186
+ catch {
187
+ /* best-effort — a missed stamp just means a possible re-announce */
188
+ }
189
+ }
190
+ /** End-to-end: fetch pending mail, drop already-announced ids, render the
191
+ * envelope-only notice, stamp the freshly-announced ids. Returns null when
192
+ * there's nothing new. The data path is body-free throughout. */
193
+ async function buildFrontDeskNotice(apiUrl, apiKey) {
194
+ const envelopes = await fetchPendingMail(apiUrl, apiKey);
195
+ if (envelopes.length === 0)
196
+ return null;
197
+ const stamps = readAnnouncedStamps(readState());
198
+ const fresh = filterUnannounced(envelopes, stamps);
199
+ if (fresh.length === 0)
200
+ return null;
201
+ const notice = renderFrontDeskNotice(fresh);
202
+ if (notice)
203
+ stampAnnounced(fresh.map(e => e.id));
204
+ return notice;
205
+ }
206
+ //# sourceMappingURL=front-desk-mail.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"front-desk-mail.js","sourceRoot":"","sources":["../src/front-desk-mail.ts"],"names":[],"mappings":"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"}
package/dist/hook.js CHANGED
@@ -51,6 +51,8 @@ const session_id_1 = require("./session-id");
51
51
  // elide it so it never lands as a searchable turn node.
52
52
  const turn_provenance_1 = require("./turn-provenance");
53
53
  const codex_steering_1 = require("./codex-steering");
54
+ const front_desk_mail_1 = require("./front-desk-mail");
55
+ const email_pull_1 = require("./email-pull");
54
56
  const API_URL_DEFAULT = 'https://api.greprag.com';
55
57
  const MAX_FIELD_CHARS = 500_000; // safety cap per text field
56
58
  // ---------- Env + config ---------------------------------------------------
@@ -977,13 +979,23 @@ function shouldEmitDriftWarning(storedId, derivedId) {
977
979
  return true;
978
980
  return (Date.now() - lastMs) >= DRIFT_WARNING_TTL_MS;
979
981
  }
982
+ function writeRecapOutput(text, mode) {
983
+ if (!text)
984
+ return;
985
+ if (mode === 'additionalContext') {
986
+ writeAdditionalContext('SessionStart', text);
987
+ return;
988
+ }
989
+ process.stdout.write(text);
990
+ }
980
991
  /** SessionStart — fetch recent episodic activity for this project and print
981
- * to stdout. Claude Code injects the printed text as session context. Fires
992
+ * to stdout. Claude Code injects the printed text as session context. Codex
993
+ * uses the same content via `hookSpecificOutput.additionalContext`. Fires
982
994
  * once per session. No LLM call.
983
995
  *
984
996
  * Storage and display are both UTC. The agent can compute local time itself
985
997
  * if needed — a server-side UTC display avoids straddle-day confusion. */
986
- async function recap(input) {
998
+ async function recap(input, mode = 'plain') {
987
999
  const cwd = input.cwd || process.cwd();
988
1000
  const cfg = getConfig(cwd);
989
1001
  if (!cfg.enabled || !cfg.apiKey)
@@ -1044,8 +1056,7 @@ async function recap(input) {
1044
1056
  // hook globally. Setup warning + inbox header still fire above.
1045
1057
  if (!anchor.sessionStartRecap) {
1046
1058
  const out = preamble();
1047
- if (out)
1048
- process.stdout.write(out);
1059
+ writeRecapOutput(out, mode);
1049
1060
  return;
1050
1061
  }
1051
1062
  const now = new Date();
@@ -1088,8 +1099,7 @@ async function recap(input) {
1088
1099
  // surface via preamble if they're pending.
1089
1100
  if (recentHourlies.length === 0) {
1090
1101
  const out = preamble();
1091
- if (out)
1092
- process.stdout.write(out);
1102
+ writeRecapOutput(out, mode);
1093
1103
  return;
1094
1104
  }
1095
1105
  const parts = [];
@@ -1112,7 +1122,7 @@ async function recap(input) {
1112
1122
  parts.push(`${recency}:`);
1113
1123
  parts.push(stripRecapNoise(h.content.trim(), 'hourly'));
1114
1124
  }
1115
- process.stdout.write(parts.join('\n') + '\n');
1125
+ writeRecapOutput(parts.join('\n') + '\n', mode);
1116
1126
  }
1117
1127
  /** Self-scoped arm-state check: does THIS session have a live inbox watcher?
1118
1128
  * Asks the server's watcher registry (GET /v1/inbox/watchers — the same live-
@@ -1194,6 +1204,60 @@ async function notify(input, source = 'claude-code') {
1194
1204
  return; // armed OR couldn't tell → silent
1195
1205
  writeAdditionalContext('UserPromptSubmit', (0, session_id_1.buildArmDirective)(short, (0, session_id_1.readIdentityAlias)()));
1196
1206
  }
1207
+ /** "You've got mail" turn hook (Chip B) — wire as a UserPromptSubmit hook.
1208
+ * Each turn, ask the server for ENVELOPE-ONLY pending front-desk mail and
1209
+ * inject a one-line "you've got mail from X" notice for any arrival not yet
1210
+ * announced on this machine. It feeds the human sign-off gate — it NEVER
1211
+ * ingests body (front-desk store contract invariant 2/3); the data path is
1212
+ * body-free end to end (envelope type has no body field, server projection
1213
+ * withholds it). Self-silences once each record is announced; the agent stops
1214
+ * the loop by acting on the mail (`greprag email` marks it read).
1215
+ *
1216
+ * Unconfigured / no API key → silent. Tenant-scoped by the API key, so a
1217
+ * session only ever sees its own tenant's front desk.
1218
+ *
1219
+ * Auto-save (Chip E): when a project opts in (`email_autosave=true` in
1220
+ * .greprag/project.json, or env GREPRAG_EMAIL_AUTOSAVE), the hook also drains
1221
+ * new attachments to the configured dir each turn and appends a one-line saved
1222
+ * summary. Default OFF — no opt-in, no pull. Best-effort: a drain failure never
1223
+ * blocks the turn or suppresses the envelope notice. */
1224
+ async function mail(input) {
1225
+ const cwd = input.cwd || process.cwd();
1226
+ const cfg = getConfig(cwd);
1227
+ if (!cfg.enabled || !cfg.apiKey)
1228
+ return; // unconfigured → silent
1229
+ const parts = [];
1230
+ const notice = await (0, front_desk_mail_1.buildFrontDeskNotice)(cfg.apiUrl, cfg.apiKey);
1231
+ if (notice)
1232
+ parts.push(notice);
1233
+ try {
1234
+ const auto = await maybeAutoSaveAttachments(cwd, cfg.apiUrl, cfg.apiKey);
1235
+ if (auto)
1236
+ parts.push(auto);
1237
+ }
1238
+ catch { /* drain is best-effort — never block a turn */ }
1239
+ if (parts.length) {
1240
+ writeAdditionalContext(input.hook_event_name || 'UserPromptSubmit', parts.join('\n\n'));
1241
+ }
1242
+ }
1243
+ /** Per-turn attachment auto-save. Returns a one-line summary of freshly-saved
1244
+ * files, or null when auto-save is off or nothing new landed. Gated on an
1245
+ * explicit per-project opt-in so it never surprises a project that didn't
1246
+ * ask for local writes. */
1247
+ async function maybeAutoSaveAttachments(cwd, apiUrl, apiKey) {
1248
+ const anchor = (0, project_anchor_1.readAnchor)(cwd);
1249
+ const envOptIn = /^(1|true|yes|on)$/i.test(process.env.GREPRAG_EMAIL_AUTOSAVE || '');
1250
+ if (anchor.emailAutosave !== true && !envOptIn)
1251
+ return null;
1252
+ const dir = (0, email_pull_1.resolveEmailDir)({
1253
+ explicit: null,
1254
+ env: process.env.GREPRAG_EMAIL_DIR,
1255
+ anchorDir: anchor.emailDir,
1256
+ projectName: anchor.projectName,
1257
+ });
1258
+ const { saved } = await (0, email_pull_1.pullAllPending)(apiUrl, apiKey, dir);
1259
+ return (0, email_pull_1.buildAutoSaveSummary)(saved, dir);
1260
+ }
1197
1261
  /** UserPromptSubmit PROBE / manual diagnostic — prints a per-turn readout of
1198
1262
  * whether THIS session has a live watcher (the same isSessionArmed signal
1199
1263
  * `notify` gates on). Kept as a separate subcommand for visibility/debugging:
@@ -1270,11 +1334,11 @@ function handlePreSpawnCheck(input) {
1270
1334
  async function main() {
1271
1335
  const subcommand = process.argv[2];
1272
1336
  const validSubs = new Set([
1273
- 'store', 'recap', 'notify', 'session-id', 'pre-spawn-check', 'armcheck',
1274
- 'codex-store', 'codex-notify', 'codex-inbox',
1337
+ 'store', 'recap', 'notify', 'mail', 'session-id', 'pre-spawn-check', 'armcheck',
1338
+ 'codex-store', 'codex-notify', 'codex-inbox', 'codex-recap',
1275
1339
  ]);
1276
1340
  if (!validSubs.has(subcommand)) {
1277
- process.stderr.write(`Usage: greprag-hook <store|recap|notify|session-id|pre-spawn-check|armcheck|codex-store|codex-notify|codex-inbox>\n`);
1341
+ process.stderr.write(`Usage: greprag-hook <store|recap|notify|mail|session-id|pre-spawn-check|armcheck|codex-store|codex-notify|codex-inbox|codex-recap>\n`);
1278
1342
  process.exit(1);
1279
1343
  }
1280
1344
  let input = {};
@@ -1293,9 +1357,15 @@ async function main() {
1293
1357
  if (subcommand === 'recap') {
1294
1358
  await recap(input);
1295
1359
  }
1360
+ else if (subcommand === 'codex-recap') {
1361
+ await recap(input, 'additionalContext');
1362
+ }
1296
1363
  else if (subcommand === 'notify') {
1297
1364
  await notify(input);
1298
1365
  }
1366
+ else if (subcommand === 'mail') {
1367
+ await mail(input);
1368
+ }
1299
1369
  else if (subcommand === 'codex-notify') {
1300
1370
  await notify(input, 'codex');
1301
1371
  }