agent-procedures 0.2.0 → 0.3.1

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.
@@ -0,0 +1,942 @@
1
+ // Generated by agent-procedures@0.3.1. Do not edit.
2
+ // To upgrade: npx agent-procedures init
3
+
4
+ // lib/main.js
5
+ import path5 from "node:path";
6
+ import { spawn } from "node:child_process";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ // lib/store.js
10
+ import fs from "node:fs";
11
+ import path from "node:path";
12
+ var GROOM_LOCK_STALE_MS = 15 * 60 * 1e3;
13
+ function paths(reflexDir) {
14
+ return {
15
+ reflexDir,
16
+ tracesDir: path.join(reflexDir, "traces"),
17
+ proceduresFile: path.join(reflexDir, "procedures.jsonl"),
18
+ pendingFile: path.join(reflexDir, "pending.jsonl"),
19
+ processingFile: path.join(reflexDir, "processing.jsonl"),
20
+ groomerLockFile: path.join(reflexDir, "groomer.lock")
21
+ };
22
+ }
23
+ function readJsonl(file) {
24
+ if (!fs.existsSync(file)) return [];
25
+ return fs.readFileSync(file, "utf8").split("\n").filter(Boolean).map((l) => {
26
+ try {
27
+ return JSON.parse(l);
28
+ } catch {
29
+ return null;
30
+ }
31
+ }).filter(Boolean);
32
+ }
33
+ function sleepSync(ms) {
34
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
35
+ }
36
+ function renameWithRetry(from, to, attempts = 8) {
37
+ for (let i = 0; i < attempts; i++) {
38
+ try {
39
+ fs.renameSync(from, to);
40
+ return;
41
+ } catch (e) {
42
+ const retryable = e.code === "EBUSY" || e.code === "EPERM" || e.code === "EACCES";
43
+ if (!retryable || i === attempts - 1) throw e;
44
+ sleepSync(15 * (i + 1));
45
+ }
46
+ }
47
+ }
48
+ function unlinkWithRetry(file, attempts = 8) {
49
+ for (let i = 0; i < attempts; i++) {
50
+ try {
51
+ if (fs.existsSync(file)) fs.unlinkSync(file);
52
+ return;
53
+ } catch (e) {
54
+ const retryable = e.code === "EBUSY" || e.code === "EPERM" || e.code === "EACCES";
55
+ if (!retryable || i === attempts - 1) throw e;
56
+ sleepSync(15 * (i + 1));
57
+ }
58
+ }
59
+ }
60
+ function writeJsonlAtomic(file, nodes) {
61
+ const content = nodes.length ? nodes.map((n) => JSON.stringify(n)).join("\n") + "\n" : "";
62
+ const tmp = file + ".tmp";
63
+ fs.writeFileSync(tmp, content, "utf8");
64
+ renameWithRetry(tmp, file);
65
+ }
66
+ function traceFile(reflexDir, sessionId, promptId) {
67
+ return path.join(paths(reflexDir).tracesDir, sessionId, `${promptId}.jsonl`);
68
+ }
69
+ function startTrace(reflexDir, sessionId, promptId, goal) {
70
+ const file = traceFile(reflexDir, sessionId, promptId);
71
+ fs.mkdirSync(path.dirname(file), { recursive: true });
72
+ fs.writeFileSync(file, JSON.stringify(goal) + "\n", "utf8");
73
+ }
74
+ function appendTrace(reflexDir, sessionId, promptId, line) {
75
+ const file = traceFile(reflexDir, sessionId, promptId);
76
+ fs.mkdirSync(path.dirname(file), { recursive: true });
77
+ fs.appendFileSync(file, JSON.stringify(line) + "\n", "utf8");
78
+ }
79
+ function readTrace(reflexDir, sessionId, promptId) {
80
+ return readJsonl(traceFile(reflexDir, sessionId, promptId));
81
+ }
82
+ function deleteTrace(reflexDir, sessionId, promptId) {
83
+ try {
84
+ fs.unlinkSync(traceFile(reflexDir, sessionId, promptId));
85
+ } catch {
86
+ }
87
+ }
88
+ function readProcedures(reflexDir) {
89
+ return readJsonl(paths(reflexDir).proceduresFile);
90
+ }
91
+ function readPending(reflexDir) {
92
+ return readJsonl(paths(reflexDir).pendingFile);
93
+ }
94
+ function readProcessing(reflexDir) {
95
+ return readJsonl(paths(reflexDir).processingFile);
96
+ }
97
+ function readAllProcedures(reflexDir) {
98
+ return [
99
+ ...readProcedures(reflexDir),
100
+ ...readPending(reflexDir),
101
+ ...readProcessing(reflexDir)
102
+ ];
103
+ }
104
+ function countIngest(reflexDir) {
105
+ return readPending(reflexDir).length + readProcessing(reflexDir).length;
106
+ }
107
+ function appendPending(reflexDir, node) {
108
+ const file = paths(reflexDir).pendingFile;
109
+ fs.mkdirSync(reflexDir, { recursive: true });
110
+ fs.appendFileSync(file, JSON.stringify(node) + "\n", "utf8");
111
+ }
112
+ function migrateLegacyPending(reflexDir) {
113
+ const p = paths(reflexDir);
114
+ if (!fs.existsSync(p.proceduresFile)) return { moved: 0 };
115
+ const nodes = readJsonl(p.proceduresFile);
116
+ const pending = nodes.filter((n) => n.status === "pending_review");
117
+ if (pending.length === 0) return { moved: 0 };
118
+ const stable = nodes.filter((n) => n.status !== "pending_review");
119
+ for (const node of pending) appendPending(reflexDir, node);
120
+ writeJsonlAtomic(p.proceduresFile, stable);
121
+ return { moved: pending.length };
122
+ }
123
+ function pidAlive(pid) {
124
+ if (!Number.isInteger(pid) || pid <= 0) return false;
125
+ try {
126
+ process.kill(pid, 0);
127
+ return true;
128
+ } catch {
129
+ return false;
130
+ }
131
+ }
132
+ function readLock(lockFile) {
133
+ try {
134
+ return JSON.parse(fs.readFileSync(lockFile, "utf8"));
135
+ } catch {
136
+ return null;
137
+ }
138
+ }
139
+ function isGroomerLocked(reflexDir, now = Date.now()) {
140
+ const lockFile = paths(reflexDir).groomerLockFile;
141
+ if (!fs.existsSync(lockFile)) return false;
142
+ const lock = readLock(lockFile);
143
+ if (!lock) return false;
144
+ const age = typeof lock.startedAt === "number" ? now - lock.startedAt : Infinity;
145
+ if (age >= GROOM_LOCK_STALE_MS) return false;
146
+ return pidAlive(lock.pid);
147
+ }
148
+ function tryAcquireGroomerLock(reflexDir, now = Date.now()) {
149
+ const lockFile = paths(reflexDir).groomerLockFile;
150
+ fs.mkdirSync(reflexDir, { recursive: true });
151
+ if (fs.existsSync(lockFile)) {
152
+ if (isGroomerLocked(reflexDir, now)) return false;
153
+ try {
154
+ fs.unlinkSync(lockFile);
155
+ } catch {
156
+ return false;
157
+ }
158
+ }
159
+ try {
160
+ const fd = fs.openSync(lockFile, "wx");
161
+ fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, startedAt: now }) + "\n", "utf8");
162
+ fs.closeSync(fd);
163
+ return true;
164
+ } catch (e) {
165
+ if (e.code === "EEXIST") return false;
166
+ throw e;
167
+ }
168
+ }
169
+ function releaseGroomerLock(reflexDir) {
170
+ const lockFile = paths(reflexDir).groomerLockFile;
171
+ try {
172
+ const lock = readLock(lockFile);
173
+ if (lock && lock.pid !== process.pid) return;
174
+ fs.unlinkSync(lockFile);
175
+ } catch {
176
+ }
177
+ }
178
+ function claimPendingBatch(reflexDir) {
179
+ const { pendingFile, processingFile } = paths(reflexDir);
180
+ if (fs.existsSync(processingFile)) return processingFile;
181
+ if (!fs.existsSync(pendingFile)) return null;
182
+ const stat = fs.statSync(pendingFile);
183
+ if (stat.size === 0) {
184
+ unlinkWithRetry(pendingFile);
185
+ return null;
186
+ }
187
+ renameWithRetry(pendingFile, processingFile);
188
+ return processingFile;
189
+ }
190
+
191
+ // lib/sanitize.js
192
+ var PROVIDER_RULES = [
193
+ ["anthropic-api-key", /\b(sk-ant-api03-[a-zA-Z0-9_-]{93}AA)(?:[\x60'"\s;]|\\[nr]|$)/],
194
+ ["aws-access-token", /\b((?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z2-7]{16})\b/],
195
+ ["cloudflare-api-key", /[\w.-]{0,50}?(?:cloudflare)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9_-]{40})(?:[\x60'"\s;]|\\[nr]|$)/i],
196
+ ["datadog-access-token", /[\w.-]{0,50}?(?:datadog)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{40})(?:[\x60'"\s;]|\\[nr]|$)/i],
197
+ ["digitalocean-access-token", /\b(doo_v1_[a-f0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)/],
198
+ ["discord-api-token", /[\w.-]{0,50}?(?:discord)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)/i],
199
+ ["dropbox-short-lived-api-token", /[\w.-]{0,50}?(?:dropbox)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(sl\.[a-z0-9\-=_]{135})(?:[\x60'"\s;]|\\[nr]|$)/i],
200
+ ["gcp-api-key", /\b(AIza[\w-]{35})(?:[\x60'"\s;]|\\[nr]|$)/],
201
+ ["github-pat", /ghp_[0-9a-zA-Z]{36}/],
202
+ ["gitlab-pat", /glpat-[\w-]{20}/],
203
+ ["heroku-api-key-v2", /\b((HRKU-AA[0-9a-zA-Z_-]{58}))(?:[\x60'"\s;]|\\[nr]|$)/],
204
+ ["mailgun-private-api-token", /[\w.-]{0,50}?(?:mailgun)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(key-[a-f0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)/i],
205
+ ["npm-access-token", /\b(npm_[a-z0-9]{36})(?:[\x60'"\s;]|\\[nr]|$)/i],
206
+ ["openai-api-key", /\b(sk-(?:proj|svcacct|admin)-(?:[A-Za-z0-9_-]{74}|[A-Za-z0-9_-]{58})T3BlbkFJ(?:[A-Za-z0-9_-]{74}|[A-Za-z0-9_-]{58})\b|sk-[a-zA-Z0-9]{20}T3BlbkFJ[a-zA-Z0-9]{20})(?:[\x60'"\s;]|\\[nr]|$)/],
207
+ ["pypi-upload-token", /pypi-AgEIcHlwaS5vcmc[\w-]{50,1000}/],
208
+ ["sendgrid-api-token", /\b(SG\.[a-z0-9=_\-.]{66})(?:[\x60'"\s;]|\\[nr]|$)/i],
209
+ ["shopify-access-token", /shpat_[a-fA-F0-9]{32}/],
210
+ ["slack-bot-token", /xoxb-[0-9]{10,13}-[0-9]{10,13}[a-zA-Z0-9-]*/],
211
+ ["stripe-access-token", /\b((?:sk|rk)_(?:test|live|prod)_[a-zA-Z0-9]{10,99})(?:[\x60'"\s;]|\\[nr]|$)/],
212
+ ["twilio-api-key", /SK[0-9a-fA-F]{32}/],
213
+ ["private-key", /-----BEGIN[ A-Z0-9_-]{0,100}PRIVATE KEY(?: BLOCK)?-----[\s\S-]{64,}?KEY(?: BLOCK)?-----/i],
214
+ ["jwt", /\b(ey[a-zA-Z0-9]{17,}\.ey[a-zA-Z0-9/\\_-]{17,}\.(?:[a-zA-Z0-9/\\_-]{10,}={0,2})?)(?:[\x60'"\s;]|\\[nr]|$)/]
215
+ ];
216
+ var SENSITIVE_NAME = /(?:^|_)(?:API_?)?(?:KEY|TOKEN|SECRET|PASS|PASSWORD|AUTH|CREDENTIALS?|DATABASE_URL|DB_URL|CONNECTION_STRING|DSN)(?:_|$)/i;
217
+ var ASSIGNMENT = /\b(export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(?:"([^"]*)"|'([^']*)'|([^\s;&|"']+))(\s*)/g;
218
+ var CREDENTIAL_URL = /^[a-z][a-z0-9+.-]*:\/\/[^:\s/@]+:[^@\s/]+@/i;
219
+ function result(text, requiresEnv = [], unsafe = false) {
220
+ return { text, requiresEnv: [...new Set(requiresEnv)], unsafe };
221
+ }
222
+ function replaceDetectedSecrets(input) {
223
+ let text = input;
224
+ let unsafe = false;
225
+ for (const [id, regex] of PROVIDER_RULES) {
226
+ let match;
227
+ while (match = regex.exec(text)) {
228
+ const secret = match[1] || match[0];
229
+ const offset = match[0].indexOf(secret);
230
+ const start = match.index + offset;
231
+ text = text.slice(0, start) + `[secret:${id}]` + text.slice(start + secret.length);
232
+ unsafe = true;
233
+ }
234
+ }
235
+ return { text, unsafe };
236
+ }
237
+ function sanitizeText(input = "") {
238
+ const detected = replaceDetectedSecrets(String(input));
239
+ return result(detected.text, [], detected.unsafe);
240
+ }
241
+ function sanitizeCommand(input = "") {
242
+ let text = String(input);
243
+ const requiresEnv = [];
244
+ let removedAssignment = false;
245
+ text = text.replace(ASSIGNMENT, (match, exported, name, double, single, bare, trailing, offset, source) => {
246
+ const value = double ?? single ?? bare ?? "";
247
+ const valueIsSecret = SENSITIVE_NAME.test(name) || CREDENTIAL_URL.test(value) || PROVIDER_RULES.some(([, regex]) => regex.test(value));
248
+ if (!valueIsSecret) return match;
249
+ requiresEnv.push(name);
250
+ const before = source.slice(0, offset);
251
+ const isCommandAssignment = exported || !before.trim() || /(?:&&|\|\||;)\s*$/.test(before);
252
+ if (isCommandAssignment) {
253
+ removedAssignment = true;
254
+ return "";
255
+ }
256
+ const placeholder = bare !== void 0 ? `\${${name}}` : `"\${${name}}"`;
257
+ return `${name}=${placeholder}${trailing}`;
258
+ });
259
+ text = text.replace(
260
+ /'(\bAuthorization\s*:\s*Bearer\s+)([^']+)'/gi,
261
+ (_match, prefix) => {
262
+ requiresEnv.push("AUTH_TOKEN");
263
+ return `"${prefix}\${AUTH_TOKEN}"`;
264
+ }
265
+ );
266
+ text = text.replace(
267
+ /'(\bAuthorization\s*:\s*Basic\s+)([^']+)'/gi,
268
+ (_match, prefix) => {
269
+ requiresEnv.push("BASIC_AUTH");
270
+ return `"${prefix}\${BASIC_AUTH}"`;
271
+ }
272
+ );
273
+ text = text.replace(
274
+ /'([a-z][a-z0-9+.-]*:\/\/[^:\s/@]+:)([^@']+)(@[^']+)'/gi,
275
+ (_match, prefix, _password, suffix) => {
276
+ requiresEnv.push("PASSWORD");
277
+ return `"${prefix}\${PASSWORD}${suffix}"`;
278
+ }
279
+ );
280
+ text = text.replace(
281
+ /(\bAuthorization\s*:\s*Bearer\s+)([^"'\s]+)/gi,
282
+ (_match, prefix) => {
283
+ requiresEnv.push("AUTH_TOKEN");
284
+ return `${prefix}\${AUTH_TOKEN}`;
285
+ }
286
+ );
287
+ text = text.replace(
288
+ /(\bAuthorization\s*:\s*Basic\s+)([^"'\s]+)/gi,
289
+ (_match, prefix) => {
290
+ requiresEnv.push("BASIC_AUTH");
291
+ return `${prefix}\${BASIC_AUTH}`;
292
+ }
293
+ );
294
+ text = text.replace(
295
+ /([a-z][a-z0-9+.-]*:\/\/[^:\s/@]+:)([^@\s/]+)(@)/gi,
296
+ (_match, prefix, _password, suffix) => {
297
+ requiresEnv.push("PASSWORD");
298
+ return `${prefix}\${PASSWORD}${suffix}`;
299
+ }
300
+ );
301
+ text = text.replace(
302
+ /(--(?:password|passwd|token|secret|api-key|key)(?:=|\s+))(["']?)([^"'\s;&|]+)\2/gi,
303
+ (_match, prefix, quote) => {
304
+ const name = prefix.match(/^--([a-z-]+)/i)[1].replace(/-/g, "_").toUpperCase();
305
+ requiresEnv.push(name);
306
+ const safeQuote = quote === "'" ? '"' : quote;
307
+ return `${prefix}${safeQuote}\${${name}}${safeQuote}`;
308
+ }
309
+ );
310
+ if (removedAssignment) {
311
+ text = text.replace(/^\s*(?:&&|\|\||;)\s*/, "").replace(/\s*(?:&&|\|\||;)\s*$/, "").replace(/(?:&&|\|\||;)\s*(?=(?:&&|\|\||;))/g, "").trim();
312
+ }
313
+ const detected = replaceDetectedSecrets(text);
314
+ return result(detected.text, requiresEnv, detected.unsafe);
315
+ }
316
+
317
+ // lib/recall.js
318
+ var STOP_WORDS = /* @__PURE__ */ new Set([
319
+ "the",
320
+ "a",
321
+ "an",
322
+ "to",
323
+ "in",
324
+ "on",
325
+ "please",
326
+ "can",
327
+ "you",
328
+ "my",
329
+ "this",
330
+ "for",
331
+ "with"
332
+ ]);
333
+ function tokenize(text) {
334
+ if (!text) return [];
335
+ return text.toLowerCase().replace(/[^\w\s]+/g, " ").split(/\s+/).filter((w) => w.length > 1 && !STOP_WORDS.has(w));
336
+ }
337
+ function scoreTarget(promptTokens, targetString) {
338
+ const targetTokens = tokenize(targetString);
339
+ if (targetTokens.length === 0) return { coverage: 0, matchCount: 0, targetLen: 0 };
340
+ let matchCount = 0;
341
+ let hasHighValueWord = false;
342
+ for (const tWord of targetTokens) {
343
+ const isMatch = promptTokens.some(
344
+ (pWord) => pWord === tWord || tWord.length >= 3 && pWord.startsWith(tWord)
345
+ );
346
+ if (isMatch) {
347
+ matchCount++;
348
+ if (tWord.length >= 6) hasHighValueWord = true;
349
+ }
350
+ }
351
+ const coverage = matchCount / targetTokens.length;
352
+ const isReliable = coverage >= 0.75 && (matchCount >= 2 || hasHighValueWord);
353
+ if (!isReliable) return { coverage: 0, matchCount: 0, targetLen: 0 };
354
+ return { coverage, matchCount, targetLen: targetTokens.length };
355
+ }
356
+ function isBetter(a, b) {
357
+ if (a.coverage !== b.coverage) return a.coverage > b.coverage;
358
+ if (a.matchCount !== b.matchCount) return a.matchCount > b.matchCount;
359
+ return a.targetLen > b.targetLen;
360
+ }
361
+ function scoreProcedure(promptTokens, node) {
362
+ let best = scoreTarget(promptTokens, node.trigger || "");
363
+ if (Array.isArray(node.aliases)) {
364
+ for (const alias of node.aliases) {
365
+ const next = scoreTarget(promptTokens, alias);
366
+ if (isBetter(next, best)) best = next;
367
+ }
368
+ }
369
+ return best;
370
+ }
371
+ function formatSteps(steps, requiresEnv = []) {
372
+ const required = requiresEnv.length > 0 ? `Requires environment: ${requiresEnv.join(", ")}
373
+
374
+ ` : "";
375
+ return required + steps.map((s) => `- ${s.t}: ${s.target}`).join("\n");
376
+ }
377
+ function recall(prompt, procs) {
378
+ if (!prompt) return null;
379
+ const promptTokens = tokenize(prompt);
380
+ let best = null;
381
+ let bestScore = { coverage: 0, matchCount: 0, targetLen: 0 };
382
+ for (const node of procs) {
383
+ if (node.enabled === false || node.parent_id) continue;
384
+ const score = scoreProcedure(promptTokens, node);
385
+ if (isBetter(score, bestScore)) {
386
+ bestScore = score;
387
+ best = node;
388
+ }
389
+ }
390
+ if (!best) return null;
391
+ let text = formatSteps(best.steps, best.requires_env);
392
+ const alt = procs.find((e) => e.parent_id === best.id && e.type === "alt" && e.enabled !== false);
393
+ if (alt) text += "\n\nAlternative path that worked:\n" + formatSteps(alt.steps, alt.requires_env);
394
+ return {
395
+ node: best,
396
+ text: `Reflex memory found a procedure for this task:
397
+
398
+ ${text}
399
+
400
+ Follow these steps instead of figuring it out from scratch.`
401
+ };
402
+ }
403
+
404
+ // lib/engine.js
405
+ function handleEvent(evt, reflexDir, deps = {}) {
406
+ if (!evt || !evt.sessionId || !evt.promptId) return { context: null };
407
+ if (evt.type === "prompt") return onPrompt(evt, reflexDir);
408
+ if (evt.type === "tool") return onTool(evt, reflexDir);
409
+ if (evt.type === "stop") return onStop(evt, reflexDir, deps);
410
+ return { context: null };
411
+ }
412
+ var GROOM_THRESHOLD = 5;
413
+ function onPrompt(evt, reflexDir) {
414
+ const { sessionId, promptId, prompt } = evt;
415
+ const sanitized = sanitizeCommand(prompt);
416
+ startTrace(reflexDir, sessionId, promptId, {
417
+ t: "Goal",
418
+ prompt: sanitized.text,
419
+ ...sanitized.requiresEnv.length > 0 ? { requires_env: sanitized.requiresEnv } : {},
420
+ ...sanitized.unsafe ? { contains_unresolvable_secret: true } : {}
421
+ });
422
+ const hit = recall(sanitized.text, readAllProcedures(reflexDir));
423
+ if (!hit) return { context: null };
424
+ appendTrace(reflexDir, sessionId, promptId, { t: "Recall", hit: hit.node.id });
425
+ return { context: hit.text };
426
+ }
427
+ function onTool(evt, reflexDir) {
428
+ const sanitized = evt.tool === "Bash" || evt.tool === "bash" ? sanitizeCommand(evt.target) : sanitizeText(evt.target);
429
+ appendTrace(reflexDir, evt.sessionId, evt.promptId, {
430
+ t: evt.tool,
431
+ target: sanitized.text,
432
+ ok: evt.ok,
433
+ ...sanitized.requiresEnv.length > 0 ? { requires_env: sanitized.requiresEnv } : {},
434
+ ...sanitized.unsafe ? { contains_unresolvable_secret: true } : {}
435
+ });
436
+ return { context: null };
437
+ }
438
+ function onStop(evt, reflexDir, deps) {
439
+ const { sessionId, promptId } = evt;
440
+ const lines = readTrace(reflexDir, sessionId, promptId);
441
+ if (lines.length === 0) return { context: null };
442
+ if (lines.some((line) => line.contains_unresolvable_secret)) {
443
+ deleteTrace(reflexDir, sessionId, promptId);
444
+ return { context: null };
445
+ }
446
+ let goal = "";
447
+ let recallHit = null;
448
+ let hasGoal = false;
449
+ for (const l of lines) {
450
+ if (l.t === "Goal") {
451
+ goal = l.prompt;
452
+ hasGoal = true;
453
+ }
454
+ if (l.t === "Recall" && l.hit) recallHit = l.hit;
455
+ }
456
+ if (!hasGoal) {
457
+ deleteTrace(reflexDir, sessionId, promptId);
458
+ return { context: null };
459
+ }
460
+ if (judge(lines, evt)) {
461
+ const { steps, requiresEnv } = synthesize(lines);
462
+ if (steps.length > 0) {
463
+ const id = Math.random().toString(36).slice(2, 11);
464
+ if (!recallHit) {
465
+ appendPending(reflexDir, {
466
+ id,
467
+ title: goal,
468
+ trigger: goal,
469
+ steps,
470
+ ...requiresEnv.length > 0 ? { requires_env: requiresEnv } : {},
471
+ enabled: true,
472
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
473
+ status: "pending_review"
474
+ });
475
+ } else {
476
+ const parent = readAllProcedures(reflexDir).find((p) => p.id === recallHit);
477
+ const parentSteps = parent ? parent.steps : [];
478
+ const same = parentSteps.length === steps.length && parentSteps.every((s, i) => s.t === steps[i].t && s.target === steps[i].target);
479
+ if (!same) {
480
+ appendPending(reflexDir, {
481
+ id,
482
+ parent_id: recallHit,
483
+ type: "alt",
484
+ steps,
485
+ ...requiresEnv.length > 0 ? { requires_env: requiresEnv } : {},
486
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
487
+ status: "pending_review"
488
+ });
489
+ }
490
+ }
491
+ if (countIngest(reflexDir) >= GROOM_THRESHOLD && deps.spawnGroomer) deps.spawnGroomer();
492
+ }
493
+ }
494
+ deleteTrace(reflexDir, sessionId, promptId);
495
+ return { context: null };
496
+ }
497
+ var READ_ONLY_CMDS = ["cat", "ls", "git", "pwd", "head", "tail", "echo", "grep", "find"];
498
+ var PURE_READ_CMDS = ["cat", "ls", "pwd", "head", "tail"];
499
+ function isMutating(t, target) {
500
+ if (t === "Edit" || t === "Write" || t === "NotebookEdit") return true;
501
+ if (t === "Bash" || t === "bash") {
502
+ const cmd = (target || "").trim();
503
+ if (!cmd) return false;
504
+ const first = cmd.split(" ")[0];
505
+ if (READ_ONLY_CMDS.includes(first)) {
506
+ return first === "git" && /\s(commit|push|add|rm|mv)\b/.test(cmd);
507
+ }
508
+ return true;
509
+ }
510
+ return false;
511
+ }
512
+ function judge(lines, evt) {
513
+ const steps = lines.filter((l) => l.t !== "Goal" && l.t !== "Recall");
514
+ if (!steps.some((l) => isMutating(l.t, l.target))) return false;
515
+ if (evt.interrupted || evt.busy) return false;
516
+ const failed = /* @__PURE__ */ new Set();
517
+ for (const l of steps) {
518
+ const key = `${l.t}|${l.target}`;
519
+ if (l.ok === false) failed.add(key);
520
+ else if (l.ok === true) failed.delete(key);
521
+ }
522
+ return failed.size === 0;
523
+ }
524
+ function synthesize(lines) {
525
+ const steps = [];
526
+ const requiresEnv = /* @__PURE__ */ new Set();
527
+ for (const l of lines) {
528
+ for (const name of l.requires_env || []) requiresEnv.add(name);
529
+ if (l.t === "Goal" || l.t === "Recall" || l.ok === false) continue;
530
+ if (l.t === "Read" || l.t === "View") continue;
531
+ if (l.t === "Bash" || l.t === "bash") {
532
+ const target = l.target || "";
533
+ if (!target.trim()) continue;
534
+ if (PURE_READ_CMDS.includes(target.trim().split(" ")[0])) continue;
535
+ if (/^git (status|log|diff)/.test(target)) continue;
536
+ }
537
+ steps.push({ t: l.t, target: l.target });
538
+ }
539
+ return { steps, requiresEnv: [...requiresEnv] };
540
+ }
541
+
542
+ // lib/groomer.js
543
+ import fs3 from "node:fs";
544
+ import path3 from "node:path";
545
+
546
+ // lib/providers.js
547
+ import fs2 from "node:fs";
548
+ import path2 from "node:path";
549
+ import os from "node:os";
550
+ function reflexHome() {
551
+ return process.env.REFLEX_HOME || path2.join(os.homedir(), ".reflex");
552
+ }
553
+ function credentialsFile() {
554
+ return path2.join(reflexHome(), "credentials.json");
555
+ }
556
+ var SYSTEM_PROMPT = "You are the Reflex graph groomer. Your job is to review execution edges from coding agents, drop bad ones, and add semantic aliases.";
557
+ async function expectOk(res, label) {
558
+ if (res.ok) return;
559
+ let detail = await res.text().catch(() => "");
560
+ try {
561
+ detail = JSON.parse(detail).error?.message || detail;
562
+ } catch (e) {
563
+ }
564
+ detail = detail.replace(/\s+/g, " ").trim().slice(0, 200);
565
+ throw new Error(`${label} API error: ${res.status} ${res.statusText}${detail ? ` - ${detail}` : ""}`);
566
+ }
567
+ var PROVIDERS = {
568
+ anthropic: {
569
+ label: "Anthropic",
570
+ envVar: "ANTHROPIC_API_KEY",
571
+ defaultModel: "claude-haiku-4-5",
572
+ keyPrefix: "sk-ant-",
573
+ keysUrl: "https://console.anthropic.com/settings/keys",
574
+ async verify(apiKey) {
575
+ const res = await fetch("https://api.anthropic.com/v1/models?limit=1", {
576
+ headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" }
577
+ });
578
+ await expectOk(res, "Anthropic");
579
+ },
580
+ async complete(apiKey, model, prompt) {
581
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
582
+ method: "POST",
583
+ headers: {
584
+ "x-api-key": apiKey,
585
+ "anthropic-version": "2023-06-01",
586
+ "content-type": "application/json"
587
+ },
588
+ body: JSON.stringify({
589
+ model,
590
+ max_tokens: 1024,
591
+ system: SYSTEM_PROMPT,
592
+ messages: [{ role: "user", content: prompt }]
593
+ })
594
+ });
595
+ await expectOk(res, "Anthropic");
596
+ const data = await res.json();
597
+ return data.content[0].text;
598
+ }
599
+ },
600
+ openai: {
601
+ label: "OpenAI",
602
+ envVar: "OPENAI_API_KEY",
603
+ defaultModel: "gpt-6-luna",
604
+ keyPrefix: "sk-",
605
+ keysUrl: "https://platform.openai.com/api-keys",
606
+ async verify(apiKey) {
607
+ const res = await fetch("https://api.openai.com/v1/models", {
608
+ headers: { Authorization: `Bearer ${apiKey}` }
609
+ });
610
+ await expectOk(res, "OpenAI");
611
+ },
612
+ async complete(apiKey, model, prompt) {
613
+ const res = await fetch("https://api.openai.com/v1/chat/completions", {
614
+ method: "POST",
615
+ headers: {
616
+ Authorization: `Bearer ${apiKey}`,
617
+ "content-type": "application/json"
618
+ },
619
+ body: JSON.stringify({
620
+ model,
621
+ max_tokens: 1024,
622
+ messages: [
623
+ { role: "system", content: SYSTEM_PROMPT },
624
+ { role: "user", content: prompt }
625
+ ]
626
+ })
627
+ });
628
+ await expectOk(res, "OpenAI");
629
+ const data = await res.json();
630
+ return data.choices[0].message.content;
631
+ }
632
+ },
633
+ gemini: {
634
+ label: "Gemini",
635
+ envVar: "GEMINI_API_KEY",
636
+ defaultModel: "gemini-3.5-flash-lite",
637
+ keyPrefix: "AIza",
638
+ keysUrl: "https://aistudio.google.com/apikey",
639
+ async verify(apiKey) {
640
+ const res = await fetch(
641
+ "https://generativelanguage.googleapis.com/v1beta/models?pageSize=1",
642
+ { headers: { "x-goog-api-key": apiKey } }
643
+ );
644
+ await expectOk(res, "Gemini");
645
+ },
646
+ async complete(apiKey, model, prompt) {
647
+ const res = await fetch(
648
+ `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`,
649
+ {
650
+ method: "POST",
651
+ headers: {
652
+ "x-goog-api-key": apiKey,
653
+ "content-type": "application/json"
654
+ },
655
+ body: JSON.stringify({
656
+ system_instruction: { parts: [{ text: SYSTEM_PROMPT }] },
657
+ contents: [{ role: "user", parts: [{ text: prompt }] }],
658
+ generationConfig: { maxOutputTokens: 1024 }
659
+ })
660
+ }
661
+ );
662
+ await expectOk(res, "Gemini");
663
+ const data = await res.json();
664
+ return data.candidates[0].content.parts[0].text;
665
+ }
666
+ }
667
+ };
668
+ var PROVIDER_NAMES = Object.keys(PROVIDERS);
669
+ function readCredentials() {
670
+ const file = credentialsFile();
671
+ if (!fs2.existsSync(file)) return null;
672
+ return JSON.parse(fs2.readFileSync(file, "utf8"));
673
+ }
674
+ function resolveCredentials(env = process.env) {
675
+ let stored = null;
676
+ try {
677
+ stored = readCredentials();
678
+ } catch (e) {
679
+ throw new Error(`Failed to parse ${credentialsFile()}: ${e.message}`);
680
+ }
681
+ let provider = stored?.defaultProvider;
682
+ if (!provider) {
683
+ provider = PROVIDER_NAMES.find((p) => env[PROVIDERS[p].envVar]);
684
+ }
685
+ if (!provider || !PROVIDERS[provider]) return null;
686
+ const def = PROVIDERS[provider];
687
+ const entry = stored?.providers?.[provider] || {};
688
+ const envKey = env[def.envVar];
689
+ const apiKey = envKey || entry.apiKey;
690
+ if (!apiKey) return null;
691
+ return {
692
+ provider,
693
+ apiKey,
694
+ model: entry.model || def.defaultModel,
695
+ source: envKey ? "env" : "file"
696
+ };
697
+ }
698
+
699
+ // lib/groomer.js
700
+ var GROOM_MAX_ATTEMPTS = 3;
701
+ function logError(reflexDir, msg) {
702
+ const logFile = path3.join(reflexDir, "worker.log");
703
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
704
+ try {
705
+ fs3.appendFileSync(logFile, `[${timestamp}] ${msg}
706
+ `, "utf8");
707
+ } catch {
708
+ }
709
+ }
710
+ function mergeStable(stable, batchIds, keptFromBatch) {
711
+ const withoutBatch = stable.filter((n) => !n.id || !batchIds.has(n.id));
712
+ const byId = /* @__PURE__ */ new Map();
713
+ const anonymous = [];
714
+ for (const n of [...withoutBatch, ...keptFromBatch]) {
715
+ if (n.id) byId.set(n.id, n);
716
+ else anonymous.push(n);
717
+ }
718
+ return [...byId.values(), ...anonymous];
719
+ }
720
+ async function processBatch(reflexDir, processingFile, creds) {
721
+ const batch = readProcessing(reflexDir);
722
+ if (batch.length === 0) {
723
+ unlinkWithRetry(processingFile);
724
+ return { ok: true, deferred: 0 };
725
+ }
726
+ const prompt = `
727
+ Here are ${batch.length} new execution nodes/edges added by a coding agent.
728
+
729
+ <edges>
730
+ ${JSON.stringify(batch, null, 2)}
731
+ </edges>
732
+
733
+ For each item:
734
+ 1) Did the agent verify this worked? (e.g. ran a test, checked a log). If it just wrote code and blindly stopped without checking, it's risky.
735
+ 2) Is this a generic/reusable workflow, or a one-off anomaly?
736
+ If it's unverified and risky, or a pure anomaly, decide to DROP it.
737
+ If it's good, KEEP it.
738
+
739
+ If you KEEP a root node (has no parent_id), generate 5-10 semantic aliases (synonyms) for its 'trigger' text so we can match variations of the user's prompt later.
740
+
741
+ Return ONLY a JSON array of objects with this schema:
742
+ [
743
+ {
744
+ "id": "node_id",
745
+ "action": "KEEP" | "DROP",
746
+ "aliases": ["alias1", "alias2"] // only if KEEP and root node
747
+ }
748
+ ]
749
+ `;
750
+ let responseText;
751
+ try {
752
+ responseText = await PROVIDERS[creds.provider].complete(creds.apiKey, creds.model, prompt);
753
+ } catch (err) {
754
+ logError(reflexDir, err.message);
755
+ return { ok: false, deferred: 0 };
756
+ }
757
+ let actions = [];
758
+ try {
759
+ const jsonStr = responseText.replace(/```json/g, "").replace(/```/g, "").trim();
760
+ actions = JSON.parse(jsonStr);
761
+ } catch {
762
+ logError(reflexDir, "Failed to parse LLM JSON response.");
763
+ return { ok: false, deferred: 0 };
764
+ }
765
+ const kept = [];
766
+ const deferred = [];
767
+ for (const node of batch) {
768
+ const actionDef = actions.find((a) => a.id === node.id);
769
+ if (!actionDef) {
770
+ const attempts = (node.groom_attempts || 0) + 1;
771
+ if (attempts < GROOM_MAX_ATTEMPTS) deferred.push({ ...node, groom_attempts: attempts });
772
+ continue;
773
+ }
774
+ if (actionDef.action === "DROP") continue;
775
+ const next = { ...node };
776
+ delete next.status;
777
+ delete next.groom_attempts;
778
+ if (!next.parent_id && Array.isArray(actionDef.aliases)) {
779
+ next.aliases = actionDef.aliases;
780
+ }
781
+ kept.push(next);
782
+ }
783
+ const resolvedIds = new Set(
784
+ batch.filter((n) => actions.some((a) => a.id === n.id)).map((n) => n.id).filter(Boolean)
785
+ );
786
+ const merged = mergeStable(readProcedures(reflexDir), resolvedIds, kept);
787
+ writeJsonlAtomic(paths(reflexDir).proceduresFile, merged);
788
+ unlinkWithRetry(processingFile);
789
+ for (const node of deferred) appendPending(reflexDir, node);
790
+ return { ok: true, deferred: deferred.length };
791
+ }
792
+ async function runGroomer(reflexDir) {
793
+ if (!tryAcquireGroomerLock(reflexDir)) return { ran: false, reason: "locked" };
794
+ try {
795
+ migrateLegacyPending(reflexDir);
796
+ let creds;
797
+ try {
798
+ creds = resolveCredentials();
799
+ } catch (e) {
800
+ logError(reflexDir, e.message);
801
+ return { ran: false, reason: "creds" };
802
+ }
803
+ if (!creds) {
804
+ logError(
805
+ reflexDir,
806
+ "No API key found. Run `npx agent-procedures auth login` or set ANTHROPIC_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY."
807
+ );
808
+ return { ran: false, reason: "creds" };
809
+ }
810
+ let batches = 0;
811
+ while (batches < 8) {
812
+ const processingFile = claimPendingBatch(reflexDir);
813
+ if (!processingFile) break;
814
+ const { ok, deferred } = await processBatch(reflexDir, processingFile, creds);
815
+ if (!ok) break;
816
+ batches++;
817
+ if (deferred > 0) break;
818
+ if (countIngest(reflexDir) === 0) break;
819
+ }
820
+ return { ran: true, batches };
821
+ } finally {
822
+ releaseGroomerLock(reflexDir);
823
+ }
824
+ }
825
+
826
+ // lib/harnesses/claude.js
827
+ import fs4 from "node:fs";
828
+ import path4 from "node:path";
829
+ var EVENTS = ["UserPromptSubmit", "PostToolUse", "PostToolUseFailure", "Stop"];
830
+ var claude_default = {
831
+ id: "claude",
832
+ /** Where the generated hook shim goes, relative to the repo root. */
833
+ hookFile: ".claude/hooks/reflex.mjs",
834
+ /** Merge our hooks into .claude/settings.json without touching anything else. */
835
+ register(cwd) {
836
+ const settingsPath = path4.join(cwd, ".claude", "settings.json");
837
+ let settings = {};
838
+ if (fs4.existsSync(settingsPath)) {
839
+ try {
840
+ settings = JSON.parse(fs4.readFileSync(settingsPath, "utf8"));
841
+ } catch (e) {
842
+ console.warn("Failed to parse existing .claude/settings.json, overwriting...");
843
+ }
844
+ }
845
+ settings.hooks = settings.hooks || {};
846
+ for (const event of EVENTS) {
847
+ if (!Array.isArray(settings.hooks[event])) settings.hooks[event] = [];
848
+ const command = `node "$CLAUDE_PROJECT_DIR/${this.hookFile}" ${event}`;
849
+ const wired = settings.hooks[event].some((g) => (g.hooks || []).some((h) => h.command === command));
850
+ if (!wired) settings.hooks[event].push({ hooks: [{ type: "command", command }] });
851
+ }
852
+ fs4.mkdirSync(path4.dirname(settingsPath), { recursive: true });
853
+ fs4.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
854
+ },
855
+ /** Claude's stdin payload → normalized engine event, or null to ignore. */
856
+ normalize(eventName, payload) {
857
+ const name = payload.hook_event_name || eventName;
858
+ const base = { sessionId: payload.session_id, promptId: payload.prompt_id };
859
+ if (name === "UserPromptSubmit") {
860
+ return { type: "prompt", ...base, prompt: payload.prompt };
861
+ }
862
+ if (name === "PostToolUse" || name === "PostToolUseFailure") {
863
+ const tool = payload.tool_name;
864
+ const input = payload.tool_input || {};
865
+ let target = "";
866
+ if (tool === "Bash" || tool === "bash") target = input.command || "";
867
+ else if (typeof input === "string") target = input;
868
+ else target = input.target || input.file_path || input.path || "";
869
+ return { type: "tool", ...base, tool, target, ok: name === "PostToolUse" };
870
+ }
871
+ if (name === "Stop") {
872
+ return {
873
+ type: "stop",
874
+ ...base,
875
+ interrupted: false,
876
+ busy: Array.isArray(payload.background_tasks) && payload.background_tasks.length > 0
877
+ };
878
+ }
879
+ return null;
880
+ },
881
+ /** Engine result → what to print on stdout for Claude. */
882
+ render(evt, result2) {
883
+ if (evt.type !== "prompt") return "";
884
+ if (!result2.context) return "{}";
885
+ return JSON.stringify({
886
+ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: result2.context }
887
+ });
888
+ }
889
+ };
890
+
891
+ // lib/harnesses/index.js
892
+ var HARNESSES = { claude: claude_default };
893
+ var DEFAULT_HARNESS = "claude";
894
+ function getHarness(id = DEFAULT_HARNESS) {
895
+ const h = HARNESSES[id];
896
+ if (!h) {
897
+ throw new Error(`Unknown harness "${id}". Available: ${Object.keys(HARNESSES).join(", ")}`);
898
+ }
899
+ return h;
900
+ }
901
+
902
+ // lib/main.js
903
+ var REFLEX_DIR = path5.dirname(fileURLToPath(import.meta.url));
904
+ function readStdin() {
905
+ return new Promise((resolve) => {
906
+ let data = "";
907
+ process.stdin.setEncoding("utf8");
908
+ process.stdin.on("data", (c) => data += c);
909
+ process.stdin.on("end", () => resolve(data));
910
+ process.stdin.on("error", () => resolve(data));
911
+ });
912
+ }
913
+ function spawnGroomer() {
914
+ if (isGroomerLocked(REFLEX_DIR)) return;
915
+ const child = spawn(process.argv[0], [process.argv[1], "Groom"], { detached: true, stdio: "ignore" });
916
+ child.unref();
917
+ }
918
+ async function run(harnessId, argv = process.argv.slice(2)) {
919
+ const [eventName] = argv;
920
+ if (!eventName) return;
921
+ if (eventName === "Groom") {
922
+ await runGroomer(REFLEX_DIR);
923
+ return;
924
+ }
925
+ const raw = await readStdin();
926
+ if (!raw) return;
927
+ let payload;
928
+ try {
929
+ payload = JSON.parse(raw);
930
+ } catch (e) {
931
+ return;
932
+ }
933
+ const harness = getHarness(harnessId);
934
+ const evt = harness.normalize(eventName, payload);
935
+ if (!evt) return;
936
+ const result2 = handleEvent(evt, REFLEX_DIR, { spawnGroomer });
937
+ const out = harness.render(evt, result2);
938
+ if (out) process.stdout.write(out + "\n");
939
+ }
940
+ export {
941
+ run
942
+ };