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.
- package/dist/commands/codex-doctor.d.ts +12 -0
- package/dist/commands/codex-doctor.js +167 -0
- package/dist/commands/codex-doctor.js.map +1 -0
- package/dist/commands/codex-supervisor.js +13 -3
- package/dist/commands/codex-supervisor.js.map +1 -1
- package/dist/commands/codex.js +19 -65
- package/dist/commands/codex.js.map +1 -1
- package/dist/commands/email.d.ts +18 -0
- package/dist/commands/email.js +310 -0
- package/dist/commands/email.js.map +1 -0
- package/dist/commands/inbox-watch.d.ts +4 -0
- package/dist/commands/inbox-watch.js +4 -2
- package/dist/commands/inbox-watch.js.map +1 -1
- package/dist/commands/init.js +96 -38
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/status.js +17 -6
- package/dist/commands/status.js.map +1 -1
- package/dist/email-pull.d.ts +84 -0
- package/dist/email-pull.js +203 -0
- package/dist/email-pull.js.map +1 -0
- package/dist/email-send.d.ts +64 -0
- package/dist/email-send.js +124 -0
- package/dist/email-send.js.map +1 -0
- package/dist/front-desk-mail.d.ts +50 -0
- package/dist/front-desk-mail.js +206 -0
- package/dist/front-desk-mail.js.map +1 -0
- package/dist/hook.js +80 -10
- package/dist/hook.js.map +1 -1
- package/dist/index.js +372 -142
- package/dist/index.js.map +1 -1
- package/dist/project-anchor.d.ts +6 -0
- package/dist/project-anchor.js +10 -0
- package/dist/project-anchor.js.map +1 -1
- package/package.json +1 -1
- package/scripts/postinstall.js +1 -1
- package/skill/greprag/SKILL.md +1 -1
- package/skill/greprag/docs/inbox.md +7 -5
- package/skill/greprag/docs/setup.md +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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|