agent-procedures 0.3.1 → 0.4.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.
- package/README.md +8 -5
- package/bin/cli.js +1 -1
- package/dist/runtime.js +97 -9
- package/lib/engine.js +5 -4
- package/lib/harnesses/claude.js +5 -2
- package/lib/harnesses/cursor.js +101 -0
- package/lib/harnesses/index.js +2 -1
- package/lib/recall.js +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,9 +18,10 @@ You don't run either of these manually. You just use your agent.
|
|
|
18
18
|
|
|
19
19
|
## Status & Harnesses
|
|
20
20
|
|
|
21
|
-
Early. Reflex
|
|
21
|
+
Early. Reflex plugs into agent platforms through thin harness adapters.
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
- **Claude Code** — default (`npx agent-procedures init`)
|
|
24
|
+
- **Cursor** — `npx agent-procedures init --harness cursor`
|
|
24
25
|
|
|
25
26
|
## Install
|
|
26
27
|
|
|
@@ -28,12 +29,14 @@ Run this in the repo you want Reflex in:
|
|
|
28
29
|
|
|
29
30
|
```bash
|
|
30
31
|
npx agent-procedures init
|
|
32
|
+
# or
|
|
33
|
+
npx agent-procedures init --harness cursor
|
|
31
34
|
```
|
|
32
35
|
|
|
33
|
-
By default this installs the Claude Code harness. If you were using a different one later, you'd run `npx agent-procedures init --harness cursor`.
|
|
34
|
-
|
|
35
36
|
Installing the package on its own won't do anything. You need `init`. It creates the folders, copies the runtime in, and adds a hook so your agent knows to call it.
|
|
36
37
|
|
|
38
|
+
For Cursor, init writes `.cursor/hooks.json` and a shim under `.cursor/hooks/`. The hook command uses the absolute path to the `node` that ran init, because Cursor's hook shell often doesn't see nvm on `PATH`.
|
|
39
|
+
|
|
37
40
|
## API key (for the background groomer)
|
|
38
41
|
|
|
39
42
|
Because the groomer uses an LLM to review procedures, it needs a cheap model key.
|
|
@@ -63,7 +66,7 @@ The key gets checked before it's saved, so a typo fails right away instead of a
|
|
|
63
66
|
|
|
64
67
|
The hot path only appends to `pending.jsonl`. A background groomer renames that buffer to `processing.jsonl`, reviews it, and merges keepers into `procedures.jsonl`. Recall reads all three, so memory works before grooming finishes.
|
|
65
68
|
|
|
66
|
-
Init ignores everything under `.reflex/` except `config.json`, `procedures.jsonl`, `runs.jsonl`, and `runtime.js`. **Commit those four files**, plus `.claude/settings.json`
|
|
69
|
+
Init ignores everything under `.reflex/` except `config.json`, `procedures.jsonl`, `runs.jsonl`, and `runtime.js`. **Commit those four files**, plus the harness wiring (`.claude/settings.json` for Claude Code, or `.cursor/hooks.json` for Cursor), so anyone who clones the repo gets the same memory. If you upgrade the package, run `init` again. That overwrites `runtime.js`.
|
|
67
70
|
|
|
68
71
|
## Secrets
|
|
69
72
|
|
package/bin/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ if (!command || command === "--help" || command === "-h") {
|
|
|
8
8
|
console.log(`agent-procedures — procedural memory for coding agents (Reflex)
|
|
9
9
|
|
|
10
10
|
Usage:
|
|
11
|
-
npx agent-procedures init [--harness claude] Create .reflex/ and wire hooks
|
|
11
|
+
npx agent-procedures init [--harness claude|cursor] Create .reflex/ and wire hooks
|
|
12
12
|
npx agent-procedures auth login Store an API key for the background groomer
|
|
13
13
|
npx agent-procedures auth status Show which provider/key the groomer will use
|
|
14
14
|
`);
|
package/dist/runtime.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// Generated by agent-procedures@0.
|
|
1
|
+
// Generated by agent-procedures@0.4.1. Do not edit.
|
|
2
2
|
// To upgrade: npx agent-procedures init
|
|
3
3
|
|
|
4
4
|
// lib/main.js
|
|
5
|
-
import
|
|
5
|
+
import path6 from "node:path";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
|
|
@@ -391,13 +391,15 @@ function recall(prompt, procs) {
|
|
|
391
391
|
let text = formatSteps(best.steps, best.requires_env);
|
|
392
392
|
const alt = procs.find((e) => e.parent_id === best.id && e.type === "alt" && e.enabled !== false);
|
|
393
393
|
if (alt) text += "\n\nAlternative path that worked:\n" + formatSteps(alt.steps, alt.requires_env);
|
|
394
|
+
const label = best.trigger || "procedure";
|
|
394
395
|
return {
|
|
395
396
|
node: best,
|
|
396
397
|
text: `Reflex memory found a procedure for this task:
|
|
397
398
|
|
|
398
399
|
${text}
|
|
399
400
|
|
|
400
|
-
Follow these steps instead of figuring it out from scratch
|
|
401
|
+
Follow these steps instead of figuring it out from scratch.`,
|
|
402
|
+
notice: `Reflex recalled: "${label}"`
|
|
401
403
|
};
|
|
402
404
|
}
|
|
403
405
|
|
|
@@ -420,9 +422,9 @@ function onPrompt(evt, reflexDir) {
|
|
|
420
422
|
...sanitized.unsafe ? { contains_unresolvable_secret: true } : {}
|
|
421
423
|
});
|
|
422
424
|
const hit = recall(sanitized.text, readAllProcedures(reflexDir));
|
|
423
|
-
if (!hit) return { context: null };
|
|
425
|
+
if (!hit) return { context: null, notice: null };
|
|
424
426
|
appendTrace(reflexDir, sessionId, promptId, { t: "Recall", hit: hit.node.id });
|
|
425
|
-
return { context: hit.text };
|
|
427
|
+
return { context: hit.text, notice: hit.notice };
|
|
426
428
|
}
|
|
427
429
|
function onTool(evt, reflexDir) {
|
|
428
430
|
const sanitized = evt.tool === "Bash" || evt.tool === "bash" ? sanitizeCommand(evt.target) : sanitizeText(evt.target);
|
|
@@ -882,14 +884,100 @@ var claude_default = {
|
|
|
882
884
|
render(evt, result2) {
|
|
883
885
|
if (evt.type !== "prompt") return "";
|
|
884
886
|
if (!result2.context) return "{}";
|
|
885
|
-
|
|
887
|
+
const out = {
|
|
886
888
|
hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: result2.context }
|
|
887
|
-
}
|
|
889
|
+
};
|
|
890
|
+
if (result2.notice) out.systemMessage = result2.notice;
|
|
891
|
+
return JSON.stringify(out);
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
// lib/harnesses/cursor.js
|
|
896
|
+
import fs5 from "node:fs";
|
|
897
|
+
import path5 from "node:path";
|
|
898
|
+
var EVENTS2 = ["beforeSubmitPrompt", "postToolUse", "postToolUseFailure", "stop"];
|
|
899
|
+
function toolTarget(tool, input) {
|
|
900
|
+
if (!input || typeof input !== "object") return typeof input === "string" ? input : "";
|
|
901
|
+
if (tool === "Shell" || tool === "Bash" || tool === "bash") return input.command || "";
|
|
902
|
+
return input.target || input.file_path || input.path || "";
|
|
903
|
+
}
|
|
904
|
+
function normalizeToolName(tool) {
|
|
905
|
+
if (tool === "Shell") return "Bash";
|
|
906
|
+
return tool || "";
|
|
907
|
+
}
|
|
908
|
+
var cursor_default = {
|
|
909
|
+
id: "cursor",
|
|
910
|
+
/** Where the generated hook shim goes, relative to the repo root. */
|
|
911
|
+
hookFile: ".cursor/hooks/reflex.mjs",
|
|
912
|
+
/** Merge our hooks into .cursor/hooks.json without touching anything else. */
|
|
913
|
+
register(cwd) {
|
|
914
|
+
const settingsPath = path5.join(cwd, ".cursor", "hooks.json");
|
|
915
|
+
let settings = {};
|
|
916
|
+
if (fs5.existsSync(settingsPath)) {
|
|
917
|
+
try {
|
|
918
|
+
settings = JSON.parse(fs5.readFileSync(settingsPath, "utf8"));
|
|
919
|
+
} catch (e) {
|
|
920
|
+
console.warn("Failed to parse existing .cursor/hooks.json, overwriting...");
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
settings.version = settings.version || 1;
|
|
924
|
+
settings.hooks = settings.hooks || {};
|
|
925
|
+
const node = process.execPath;
|
|
926
|
+
for (const event of EVENTS2) {
|
|
927
|
+
if (!Array.isArray(settings.hooks[event])) settings.hooks[event] = [];
|
|
928
|
+
const command = `"${node}" ${this.hookFile} ${event}`;
|
|
929
|
+
const wired = settings.hooks[event].some((h) => h && h.command === command);
|
|
930
|
+
if (!wired) settings.hooks[event].push({ command });
|
|
931
|
+
}
|
|
932
|
+
fs5.mkdirSync(path5.dirname(settingsPath), { recursive: true });
|
|
933
|
+
fs5.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
934
|
+
},
|
|
935
|
+
/** Cursor's stdin payload → normalized engine event, or null to ignore. */
|
|
936
|
+
normalize(eventName, payload) {
|
|
937
|
+
const name = payload.hook_event_name || eventName;
|
|
938
|
+
const sessionId = payload.conversation_id || payload.session_id;
|
|
939
|
+
const promptId = payload.generation_id || payload.prompt_id;
|
|
940
|
+
const base = { sessionId, promptId };
|
|
941
|
+
if (name === "beforeSubmitPrompt") {
|
|
942
|
+
return { type: "prompt", ...base, prompt: payload.prompt || "" };
|
|
943
|
+
}
|
|
944
|
+
if (name === "postToolUse" || name === "postToolUseFailure") {
|
|
945
|
+
const rawTool = payload.tool_name || "";
|
|
946
|
+
const tool = normalizeToolName(rawTool);
|
|
947
|
+
const input = payload.tool_input || {};
|
|
948
|
+
return {
|
|
949
|
+
type: "tool",
|
|
950
|
+
...base,
|
|
951
|
+
tool,
|
|
952
|
+
target: toolTarget(rawTool, input),
|
|
953
|
+
ok: name === "postToolUse"
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
if (name === "stop") {
|
|
957
|
+
return {
|
|
958
|
+
type: "stop",
|
|
959
|
+
...base,
|
|
960
|
+
interrupted: payload.status === "aborted",
|
|
961
|
+
busy: false
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
return null;
|
|
965
|
+
},
|
|
966
|
+
/** Engine result → what to print on stdout for Cursor. */
|
|
967
|
+
render(evt, result2) {
|
|
968
|
+
if (evt.type !== "prompt") return "";
|
|
969
|
+
const out = { continue: true };
|
|
970
|
+
if (result2.context) {
|
|
971
|
+
out.additional_context = result2.notice ? `${result2.notice}
|
|
972
|
+
|
|
973
|
+
${result2.context}` : result2.context;
|
|
974
|
+
}
|
|
975
|
+
return JSON.stringify(out);
|
|
888
976
|
}
|
|
889
977
|
};
|
|
890
978
|
|
|
891
979
|
// lib/harnesses/index.js
|
|
892
|
-
var HARNESSES = { claude: claude_default };
|
|
980
|
+
var HARNESSES = { claude: claude_default, cursor: cursor_default };
|
|
893
981
|
var DEFAULT_HARNESS = "claude";
|
|
894
982
|
function getHarness(id = DEFAULT_HARNESS) {
|
|
895
983
|
const h = HARNESSES[id];
|
|
@@ -900,7 +988,7 @@ function getHarness(id = DEFAULT_HARNESS) {
|
|
|
900
988
|
}
|
|
901
989
|
|
|
902
990
|
// lib/main.js
|
|
903
|
-
var REFLEX_DIR =
|
|
991
|
+
var REFLEX_DIR = path6.dirname(fileURLToPath(import.meta.url));
|
|
904
992
|
function readStdin() {
|
|
905
993
|
return new Promise((resolve) => {
|
|
906
994
|
let data = "";
|
package/lib/engine.js
CHANGED
|
@@ -18,8 +18,9 @@ import { recall } from "./recall.js";
|
|
|
18
18
|
* { type: "tool", sessionId, promptId, tool, target, ok }
|
|
19
19
|
* { type: "stop", sessionId, promptId, interrupted, busy }
|
|
20
20
|
*
|
|
21
|
-
* Returns { context: string | null
|
|
22
|
-
* inject into the agent's next turn
|
|
21
|
+
* Returns { context: string | null, notice?: string | null }.
|
|
22
|
+
* `context` is text the harness should inject into the agent's next turn.
|
|
23
|
+
* `notice` is a short user-visible line; the harness maps it if the host supports it.
|
|
23
24
|
*/
|
|
24
25
|
export function handleEvent(evt, reflexDir, deps = {}) {
|
|
25
26
|
if (!evt || !evt.sessionId || !evt.promptId) return { context: null };
|
|
@@ -45,10 +46,10 @@ function onPrompt(evt, reflexDir) {
|
|
|
45
46
|
});
|
|
46
47
|
|
|
47
48
|
const hit = recall(sanitized.text, readAllProcedures(reflexDir));
|
|
48
|
-
if (!hit) return { context: null };
|
|
49
|
+
if (!hit) return { context: null, notice: null };
|
|
49
50
|
|
|
50
51
|
appendTrace(reflexDir, sessionId, promptId, { t: "Recall", hit: hit.node.id });
|
|
51
|
-
return { context: hit.text };
|
|
52
|
+
return { context: hit.text, notice: hit.notice };
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
// --- tool: trace ----------------------------------------------------------
|
package/lib/harnesses/claude.js
CHANGED
|
@@ -70,8 +70,11 @@ export default {
|
|
|
70
70
|
render(evt, result) {
|
|
71
71
|
if (evt.type !== "prompt") return "";
|
|
72
72
|
if (!result.context) return "{}";
|
|
73
|
-
|
|
73
|
+
const out = {
|
|
74
74
|
hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: result.context },
|
|
75
|
-
}
|
|
75
|
+
};
|
|
76
|
+
// Top-level systemMessage is shown to the user; not model context.
|
|
77
|
+
if (result.notice) out.systemMessage = result.notice;
|
|
78
|
+
return JSON.stringify(out);
|
|
76
79
|
},
|
|
77
80
|
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
// Cursor adapter. Everything Cursor-specific lives here.
|
|
5
|
+
// Docs: https://cursor.com/docs/hooks
|
|
6
|
+
// beforeSubmitPrompt → additional_context is undocumented but verified on Cursor 3.22.x.
|
|
7
|
+
|
|
8
|
+
const EVENTS = ["beforeSubmitPrompt", "postToolUse", "postToolUseFailure", "stop"];
|
|
9
|
+
|
|
10
|
+
function toolTarget(tool, input) {
|
|
11
|
+
if (!input || typeof input !== "object") return typeof input === "string" ? input : "";
|
|
12
|
+
if (tool === "Shell" || tool === "Bash" || tool === "bash") return input.command || "";
|
|
13
|
+
return input.target || input.file_path || input.path || "";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Cursor Shell → engine Bash so command sanitization still applies. */
|
|
17
|
+
function normalizeToolName(tool) {
|
|
18
|
+
if (tool === "Shell") return "Bash";
|
|
19
|
+
return tool || "";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default {
|
|
23
|
+
id: "cursor",
|
|
24
|
+
|
|
25
|
+
/** Where the generated hook shim goes, relative to the repo root. */
|
|
26
|
+
hookFile: ".cursor/hooks/reflex.mjs",
|
|
27
|
+
|
|
28
|
+
/** Merge our hooks into .cursor/hooks.json without touching anything else. */
|
|
29
|
+
register(cwd) {
|
|
30
|
+
const settingsPath = path.join(cwd, ".cursor", "hooks.json");
|
|
31
|
+
let settings = {};
|
|
32
|
+
if (fs.existsSync(settingsPath)) {
|
|
33
|
+
try {
|
|
34
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
|
|
35
|
+
} catch (e) {
|
|
36
|
+
console.warn("Failed to parse existing .cursor/hooks.json, overwriting...");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
settings.version = settings.version || 1;
|
|
40
|
+
settings.hooks = settings.hooks || {};
|
|
41
|
+
|
|
42
|
+
// Cursor's hook shell often lacks nvm on PATH; bake the node that ran init.
|
|
43
|
+
const node = process.execPath;
|
|
44
|
+
|
|
45
|
+
for (const event of EVENTS) {
|
|
46
|
+
if (!Array.isArray(settings.hooks[event])) settings.hooks[event] = [];
|
|
47
|
+
const command = `"${node}" ${this.hookFile} ${event}`;
|
|
48
|
+
const wired = settings.hooks[event].some((h) => h && h.command === command);
|
|
49
|
+
if (!wired) settings.hooks[event].push({ command });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
53
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
/** Cursor's stdin payload → normalized engine event, or null to ignore. */
|
|
57
|
+
normalize(eventName, payload) {
|
|
58
|
+
const name = payload.hook_event_name || eventName;
|
|
59
|
+
const sessionId = payload.conversation_id || payload.session_id;
|
|
60
|
+
const promptId = payload.generation_id || payload.prompt_id;
|
|
61
|
+
const base = { sessionId, promptId };
|
|
62
|
+
|
|
63
|
+
if (name === "beforeSubmitPrompt") {
|
|
64
|
+
return { type: "prompt", ...base, prompt: payload.prompt || "" };
|
|
65
|
+
}
|
|
66
|
+
if (name === "postToolUse" || name === "postToolUseFailure") {
|
|
67
|
+
const rawTool = payload.tool_name || "";
|
|
68
|
+
const tool = normalizeToolName(rawTool);
|
|
69
|
+
const input = payload.tool_input || {};
|
|
70
|
+
return {
|
|
71
|
+
type: "tool",
|
|
72
|
+
...base,
|
|
73
|
+
tool,
|
|
74
|
+
target: toolTarget(rawTool, input),
|
|
75
|
+
ok: name === "postToolUse",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (name === "stop") {
|
|
79
|
+
return {
|
|
80
|
+
type: "stop",
|
|
81
|
+
...base,
|
|
82
|
+
interrupted: payload.status === "aborted",
|
|
83
|
+
busy: false,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
/** Engine result → what to print on stdout for Cursor. */
|
|
90
|
+
render(evt, result) {
|
|
91
|
+
if (evt.type !== "prompt") return "";
|
|
92
|
+
const out = { continue: true };
|
|
93
|
+
if (result.context) {
|
|
94
|
+
// No Claude-style systemMessage on continue:true; put the notice in model context.
|
|
95
|
+
out.additional_context = result.notice
|
|
96
|
+
? `${result.notice}\n\n${result.context}`
|
|
97
|
+
: result.context;
|
|
98
|
+
}
|
|
99
|
+
return JSON.stringify(out);
|
|
100
|
+
},
|
|
101
|
+
};
|
package/lib/harnesses/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import claude from "./claude.js";
|
|
2
|
+
import cursor from "./cursor.js";
|
|
2
3
|
|
|
3
4
|
// To add a harness: write lib/harnesses/<id>.js with the same shape as claude.js
|
|
4
5
|
// (id, hookFile, register, normalize, render) and list it here.
|
|
5
|
-
export const HARNESSES = { claude };
|
|
6
|
+
export const HARNESSES = { claude, cursor };
|
|
6
7
|
|
|
7
8
|
export const DEFAULT_HARNESS = "claude";
|
|
8
9
|
|
package/lib/recall.js
CHANGED
|
@@ -77,7 +77,7 @@ function formatSteps(steps, requiresEnv = []) {
|
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
79
|
* Best-scoring root procedure for this prompt, or null.
|
|
80
|
-
* Returns { node, text }
|
|
80
|
+
* Returns { node, text, notice } — text for the agent, notice for the harness UI.
|
|
81
81
|
*/
|
|
82
82
|
export function recall(prompt, procs) {
|
|
83
83
|
if (!prompt) return null;
|
|
@@ -101,8 +101,10 @@ export function recall(prompt, procs) {
|
|
|
101
101
|
const alt = procs.find((e) => e.parent_id === best.id && e.type === "alt" && e.enabled !== false);
|
|
102
102
|
if (alt) text += "\n\nAlternative path that worked:\n" + formatSteps(alt.steps, alt.requires_env);
|
|
103
103
|
|
|
104
|
+
const label = best.trigger || "procedure";
|
|
104
105
|
return {
|
|
105
106
|
node: best,
|
|
106
107
|
text: `Reflex memory found a procedure for this task:\n\n${text}\n\nFollow these steps instead of figuring it out from scratch.`,
|
|
108
|
+
notice: `Reflex recalled: "${label}"`,
|
|
107
109
|
};
|
|
108
110
|
}
|