greprag 5.22.2 → 5.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/codex-doctor.d.ts +12 -0
- package/dist/commands/codex-doctor.js +167 -0
- package/dist/commands/codex-doctor.js.map +1 -0
- package/dist/commands/codex-supervisor.d.ts +1 -0
- package/dist/commands/codex-supervisor.js +113 -0
- package/dist/commands/codex-supervisor.js.map +1 -0
- package/dist/commands/codex.d.ts +0 -6
- package/dist/commands/codex.js +30 -78
- package/dist/commands/codex.js.map +1 -1
- package/dist/commands/email.d.ts +18 -0
- package/dist/commands/email.js +310 -0
- package/dist/commands/email.js.map +1 -0
- package/dist/commands/inbox-watch.d.ts +4 -0
- package/dist/commands/inbox-watch.js +4 -2
- package/dist/commands/inbox-watch.js.map +1 -1
- package/dist/commands/init.js +50 -7
- package/dist/commands/init.js.map +1 -1
- package/dist/email-pull.d.ts +84 -0
- package/dist/email-pull.js +203 -0
- package/dist/email-pull.js.map +1 -0
- package/dist/email-send.d.ts +64 -0
- package/dist/email-send.js +124 -0
- package/dist/email-send.js.map +1 -0
- package/dist/front-desk-mail.d.ts +50 -0
- package/dist/front-desk-mail.js +206 -0
- package/dist/front-desk-mail.js.map +1 -0
- package/dist/hook.js +61 -2
- package/dist/hook.js.map +1 -1
- package/dist/index.js +356 -142
- package/dist/index.js.map +1 -1
- package/dist/project-anchor.d.ts +6 -0
- package/dist/project-anchor.js +10 -0
- package/dist/project-anchor.js.map +1 -1
- package/package.json +1 -1
- package/scripts/postinstall.js +1 -1
- package/skill/greprag/SKILL.md +1 -1
- package/skill/greprag/docs/inbox.md +7 -5
- package/skill/greprag/docs/setup.md +14 -1
|
@@ -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 ---------------------------------------------------
|
|
@@ -1194,6 +1196,60 @@ async function notify(input, source = 'claude-code') {
|
|
|
1194
1196
|
return; // armed OR couldn't tell → silent
|
|
1195
1197
|
writeAdditionalContext('UserPromptSubmit', (0, session_id_1.buildArmDirective)(short, (0, session_id_1.readIdentityAlias)()));
|
|
1196
1198
|
}
|
|
1199
|
+
/** "You've got mail" turn hook (Chip B) — wire as a UserPromptSubmit hook.
|
|
1200
|
+
* Each turn, ask the server for ENVELOPE-ONLY pending front-desk mail and
|
|
1201
|
+
* inject a one-line "you've got mail from X" notice for any arrival not yet
|
|
1202
|
+
* announced on this machine. It feeds the human sign-off gate — it NEVER
|
|
1203
|
+
* ingests body (front-desk store contract invariant 2/3); the data path is
|
|
1204
|
+
* body-free end to end (envelope type has no body field, server projection
|
|
1205
|
+
* withholds it). Self-silences once each record is announced; the agent stops
|
|
1206
|
+
* the loop by acting on the mail (`greprag email` marks it read).
|
|
1207
|
+
*
|
|
1208
|
+
* Unconfigured / no API key → silent. Tenant-scoped by the API key, so a
|
|
1209
|
+
* session only ever sees its own tenant's front desk.
|
|
1210
|
+
*
|
|
1211
|
+
* Auto-save (Chip E): when a project opts in (`email_autosave=true` in
|
|
1212
|
+
* .greprag/project.json, or env GREPRAG_EMAIL_AUTOSAVE), the hook also drains
|
|
1213
|
+
* new attachments to the configured dir each turn and appends a one-line saved
|
|
1214
|
+
* summary. Default OFF — no opt-in, no pull. Best-effort: a drain failure never
|
|
1215
|
+
* blocks the turn or suppresses the envelope notice. */
|
|
1216
|
+
async function mail(input) {
|
|
1217
|
+
const cwd = input.cwd || process.cwd();
|
|
1218
|
+
const cfg = getConfig(cwd);
|
|
1219
|
+
if (!cfg.enabled || !cfg.apiKey)
|
|
1220
|
+
return; // unconfigured → silent
|
|
1221
|
+
const parts = [];
|
|
1222
|
+
const notice = await (0, front_desk_mail_1.buildFrontDeskNotice)(cfg.apiUrl, cfg.apiKey);
|
|
1223
|
+
if (notice)
|
|
1224
|
+
parts.push(notice);
|
|
1225
|
+
try {
|
|
1226
|
+
const auto = await maybeAutoSaveAttachments(cwd, cfg.apiUrl, cfg.apiKey);
|
|
1227
|
+
if (auto)
|
|
1228
|
+
parts.push(auto);
|
|
1229
|
+
}
|
|
1230
|
+
catch { /* drain is best-effort — never block a turn */ }
|
|
1231
|
+
if (parts.length) {
|
|
1232
|
+
writeAdditionalContext(input.hook_event_name || 'UserPromptSubmit', parts.join('\n\n'));
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
/** Per-turn attachment auto-save. Returns a one-line summary of freshly-saved
|
|
1236
|
+
* files, or null when auto-save is off or nothing new landed. Gated on an
|
|
1237
|
+
* explicit per-project opt-in so it never surprises a project that didn't
|
|
1238
|
+
* ask for local writes. */
|
|
1239
|
+
async function maybeAutoSaveAttachments(cwd, apiUrl, apiKey) {
|
|
1240
|
+
const anchor = (0, project_anchor_1.readAnchor)(cwd);
|
|
1241
|
+
const envOptIn = /^(1|true|yes|on)$/i.test(process.env.GREPRAG_EMAIL_AUTOSAVE || '');
|
|
1242
|
+
if (anchor.emailAutosave !== true && !envOptIn)
|
|
1243
|
+
return null;
|
|
1244
|
+
const dir = (0, email_pull_1.resolveEmailDir)({
|
|
1245
|
+
explicit: null,
|
|
1246
|
+
env: process.env.GREPRAG_EMAIL_DIR,
|
|
1247
|
+
anchorDir: anchor.emailDir,
|
|
1248
|
+
projectName: anchor.projectName,
|
|
1249
|
+
});
|
|
1250
|
+
const { saved } = await (0, email_pull_1.pullAllPending)(apiUrl, apiKey, dir);
|
|
1251
|
+
return (0, email_pull_1.buildAutoSaveSummary)(saved, dir);
|
|
1252
|
+
}
|
|
1197
1253
|
/** UserPromptSubmit PROBE / manual diagnostic — prints a per-turn readout of
|
|
1198
1254
|
* whether THIS session has a live watcher (the same isSessionArmed signal
|
|
1199
1255
|
* `notify` gates on). Kept as a separate subcommand for visibility/debugging:
|
|
@@ -1270,11 +1326,11 @@ function handlePreSpawnCheck(input) {
|
|
|
1270
1326
|
async function main() {
|
|
1271
1327
|
const subcommand = process.argv[2];
|
|
1272
1328
|
const validSubs = new Set([
|
|
1273
|
-
'store', 'recap', 'notify', 'session-id', 'pre-spawn-check', 'armcheck',
|
|
1329
|
+
'store', 'recap', 'notify', 'mail', 'session-id', 'pre-spawn-check', 'armcheck',
|
|
1274
1330
|
'codex-store', 'codex-notify', 'codex-inbox',
|
|
1275
1331
|
]);
|
|
1276
1332
|
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`);
|
|
1333
|
+
process.stderr.write(`Usage: greprag-hook <store|recap|notify|mail|session-id|pre-spawn-check|armcheck|codex-store|codex-notify|codex-inbox>\n`);
|
|
1278
1334
|
process.exit(1);
|
|
1279
1335
|
}
|
|
1280
1336
|
let input = {};
|
|
@@ -1296,6 +1352,9 @@ async function main() {
|
|
|
1296
1352
|
else if (subcommand === 'notify') {
|
|
1297
1353
|
await notify(input);
|
|
1298
1354
|
}
|
|
1355
|
+
else if (subcommand === 'mail') {
|
|
1356
|
+
await mail(input);
|
|
1357
|
+
}
|
|
1299
1358
|
else if (subcommand === 'codex-notify') {
|
|
1300
1359
|
await notify(input, 'codex');
|
|
1301
1360
|
}
|