agent-procedures 0.3.2 → 0.4.2
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 +10 -5
- package/bin/cli.js +1 -1
- package/dist/runtime.js +88 -4
- package/lib/harnesses/cursor.js +101 -0
- package/lib/harnesses/index.js +2 -1
- package/lib/init.js +7 -0
- 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,9 @@ 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
|
|
69
|
+
Init ignores everything under `.reflex/` except `config.json`, `package.json`, `procedures.jsonl`, `runs.jsonl`, and `runtime.js`. **Commit those five files**, plus the harness wiring (`.claude/settings.json` for Claude Code, and/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`.
|
|
70
|
+
|
|
71
|
+
You can wire both harnesses in one repo if you use Claude Code and Cursor. Cursor may also load Claude’s hooks via third-party compatibility — if you see double hook runs, turn that off in Cursor settings and keep `.cursor/hooks.json` for Cursor / `.claude/settings.json` for Claude Code.
|
|
67
72
|
|
|
68
73
|
## Secrets
|
|
69
74
|
|
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.2. 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
|
|
|
@@ -892,8 +892,92 @@ var claude_default = {
|
|
|
892
892
|
}
|
|
893
893
|
};
|
|
894
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);
|
|
976
|
+
}
|
|
977
|
+
};
|
|
978
|
+
|
|
895
979
|
// lib/harnesses/index.js
|
|
896
|
-
var HARNESSES = { claude: claude_default };
|
|
980
|
+
var HARNESSES = { claude: claude_default, cursor: cursor_default };
|
|
897
981
|
var DEFAULT_HARNESS = "claude";
|
|
898
982
|
function getHarness(id = DEFAULT_HARNESS) {
|
|
899
983
|
const h = HARNESSES[id];
|
|
@@ -904,7 +988,7 @@ function getHarness(id = DEFAULT_HARNESS) {
|
|
|
904
988
|
}
|
|
905
989
|
|
|
906
990
|
// lib/main.js
|
|
907
|
-
var REFLEX_DIR =
|
|
991
|
+
var REFLEX_DIR = path6.dirname(fileURLToPath(import.meta.url));
|
|
908
992
|
function readStdin() {
|
|
909
993
|
return new Promise((resolve) => {
|
|
910
994
|
let data = "";
|
|
@@ -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/init.js
CHANGED
|
@@ -42,6 +42,7 @@ function ensureGitignore(cwd) {
|
|
|
42
42
|
"# Reflex: ignore runtime state; commit only the shared allowlist",
|
|
43
43
|
".reflex/*",
|
|
44
44
|
"!.reflex/config.json",
|
|
45
|
+
"!.reflex/package.json",
|
|
45
46
|
"!.reflex/procedures.jsonl",
|
|
46
47
|
"!.reflex/runs.jsonl",
|
|
47
48
|
"!.reflex/runtime.js",
|
|
@@ -77,6 +78,12 @@ function installRuntime(reflexDir) {
|
|
|
77
78
|
throw new Error("Missing dist/runtime.js. Run `npm run build` in the agent-procedures repo first.");
|
|
78
79
|
}
|
|
79
80
|
fs.copyFileSync(RUNTIME_SRC, path.join(reflexDir, "runtime.js"));
|
|
81
|
+
// Host apps are often "type":"commonjs". Without this, Node treats runtime.js as
|
|
82
|
+
// CJS and the .mjs shim's `import { run }` crashes before any event is handled.
|
|
83
|
+
writeAlways(
|
|
84
|
+
path.join(reflexDir, "package.json"),
|
|
85
|
+
`${JSON.stringify({ type: "module", private: true }, null, 2)}\n`,
|
|
86
|
+
);
|
|
80
87
|
const engineDir = path.join(reflexDir, "engine");
|
|
81
88
|
if (fs.existsSync(engineDir)) fs.rmSync(engineDir, { recursive: true, force: true });
|
|
82
89
|
return "updated";
|