agent-procedures 0.3.2 → 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 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 is built to plug into different agent platforms (harnesses). Right now, **Claude Code is the only one built** and is the default.
21
+ Early. Reflex plugs into agent platforms through thin harness adapters.
22
22
 
23
- I set the engine up so Cursor and others can plug in later, but the adapters for those don't exist yet.
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` (the hook wiring), so anyone who clones the repo gets the same memory. If you upgrade the package, run `init` again. That overwrites `runtime.js`.
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.3.2. Do not edit.
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 path5 from "node:path";
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 = path5.dirname(fileURLToPath(import.meta.url));
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
+ };
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-procedures",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
4
4
  "description": "Procedural memory for coding agents",
5
5
  "type": "module",
6
6
  "bin": {