heedvane 0.2.1 → 0.3.4

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.
Files changed (65) hide show
  1. package/README.md +60 -13
  2. package/dist/backend/credentials.d.ts +2 -0
  3. package/dist/backend/credentials.js +13 -0
  4. package/dist/backend/repo-remote.js +2 -1
  5. package/dist/backend/sign-in-change.js +3 -2
  6. package/dist/cli.d.ts +4 -2
  7. package/dist/cli.js +389 -144
  8. package/dist/clients/definitions.d.ts +8 -4
  9. package/dist/clients/definitions.js +48 -13
  10. package/dist/clients/hermes-plugin.d.ts +5 -20
  11. package/dist/clients/hermes-plugin.js +97 -71
  12. package/dist/clients/hook-files.d.ts +22 -0
  13. package/dist/clients/hook-files.js +39 -0
  14. package/dist/clients/pi-extension.d.ts +9 -0
  15. package/dist/clients/pi-extension.js +41 -0
  16. package/dist/commands/heedvane.js +18 -3
  17. package/dist/commands/mcp-stub.d.ts +1 -0
  18. package/dist/commands/mcp-stub.js +44 -0
  19. package/dist/commands/memory.js +6 -1
  20. package/dist/dispatchers/posix.d.ts +16 -19
  21. package/dist/dispatchers/posix.js +71 -91
  22. package/dist/doctor.d.ts +2 -0
  23. package/dist/doctor.js +113 -29
  24. package/dist/guard.d.ts +73 -0
  25. package/dist/guard.js +302 -0
  26. package/dist/heed-file.d.ts +19 -3
  27. package/dist/heed-file.js +46 -23
  28. package/dist/hook-fallback.d.ts +73 -0
  29. package/dist/hook-fallback.js +61 -0
  30. package/dist/installer.d.ts +13 -6
  31. package/dist/installer.js +40 -13
  32. package/dist/listener/background.d.ts +48 -4
  33. package/dist/listener/background.js +153 -19
  34. package/dist/listener/endpoint.d.ts +26 -3
  35. package/dist/listener/endpoint.js +55 -10
  36. package/dist/listener/forward.d.ts +7 -1
  37. package/dist/listener/forward.js +40 -12
  38. package/dist/listener/serve.d.ts +37 -0
  39. package/dist/listener/serve.js +196 -32
  40. package/dist/listener/server.d.ts +9 -0
  41. package/dist/listener/server.js +59 -22
  42. package/dist/listener/spool.d.ts +2 -17
  43. package/dist/listener/spool.js +6 -3
  44. package/dist/mcp.d.ts +38 -48
  45. package/dist/mcp.js +196 -63
  46. package/dist/plan.d.ts +47 -8
  47. package/dist/plan.js +164 -52
  48. package/dist/render.js +8 -8
  49. package/dist/reply/harness-output.d.ts +9 -0
  50. package/dist/reply/harness-output.js +13 -0
  51. package/dist/reply/reply.js +2 -1
  52. package/dist/skill/content.d.ts +2 -2
  53. package/dist/skill/content.js +11 -8
  54. package/dist/status.d.ts +1 -0
  55. package/dist/status.js +29 -0
  56. package/dist/steering/rules.js +4 -2
  57. package/dist/uninstall.d.ts +7 -0
  58. package/dist/uninstall.js +72 -71
  59. package/package.json +10 -3
  60. package/skills/heedvane-hooks/SKILL.md +6 -6
  61. package/skills/heedvane-hooks/references/events.md +5 -2
  62. package/dist/dispatchers/windows.d.ts +0 -15
  63. package/dist/dispatchers/windows.js +0 -50
  64. package/dist/mcp-server.d.ts +0 -9
  65. package/dist/mcp-server.js +0 -187
package/README.md CHANGED
@@ -1,15 +1,15 @@
1
1
  # heedvane
2
2
 
3
3
  Connect the coding agents on your machine to your Heedvane organization: guardrails, approved-skill
4
- checks and team memory for Claude Code, Codex, Cursor, Gemini CLI, Hermes, OpenCode and more.
4
+ checks and team memory for Claude Code, Codex, Cursor, Gemini CLI, Hermes, OpenCode, Pi and more.
5
5
 
6
6
  ```sh
7
7
  npx heedvane
8
8
  ```
9
9
 
10
- The installer shows the agents it found on your machine. Tick the ones you want, choose what Heedvane
11
- adds and does, confirm. It installs, starts Heedvane in the background and signs you in through the
12
- browser. It needs Node.js 22 or newer.
10
+ The installer shows the agents it found on your machine. Tick the ones you want, choose how much leaves
11
+ your machine, choose what Heedvane adds and does, confirm. It installs, starts Heedvane in the background and signs you in through the
12
+ browser. It needs Node.js 22.5 or newer.
13
13
 
14
14
  Your dashboard's Agents page has the same command with your server and organization filled in:
15
15
 
@@ -17,14 +17,35 @@ Your dashboard's Agents page has the same command with your server and organizat
17
17
  npx heedvane connect --server https://app.heedvane.com --org <organization id>
18
18
  ```
19
19
 
20
+ ## How much leaves your machine
21
+
22
+ | Choice | What leaves your machine |
23
+ | --- | --- |
24
+ | **Look but don't keep** (default) | Heedvane's guardrail checks read the prompt or tool call, then keep only the verdict, never the text. |
25
+ | **Metrics only** | Only counts and names. Your organization's fixed rules still run on your machine; Heedvane's own checks and team memory are off. |
26
+
27
+ Scripts choose with `--mode look-but-dont-keep` or `--mode metrics-only`. The choice is saved in
28
+ `heed.yaml` as `heedvane.dataMode`.
29
+
20
30
  ## What it changes
21
31
 
22
32
  - Writes `~/.heed/heed.yaml`, your control file, with your answers. Run `npx heedvane` again to change
23
33
  them.
24
- - Adds Heedvane's hooks, MCP server entry and skill to each agent you ticked. A config file it edits is
25
- backed up first.
26
- - Starts Heedvane's local listener, which answers the hooks. After a restart the hooks start it again by
27
- themselves, at most once a minute. Set `HEED_LISTENER_AUTOSTART=0` to turn that off.
34
+ - Adds Heedvane's hooks and skill to each agent you ticked. A config file it edits is backed up first.
35
+ - With `--org` (the dashboard's command has it), also adds Heedvane's MCP connection, named `heedvane`, for
36
+ your organization (findings, reports and agent feedback) in every agent you ticked. Claude Desktop gets it
37
+ through the `mcp-remote` bridge, run with the Node.js that installed it (run `npx heedvane connect` again after
38
+ removing that Node version). The agent signs in
39
+ through the browser the first time it uses it. It replaces the `heedvane` and
40
+ `heedvane-findings` entries an earlier version added.
41
+ - Starts Heedvane's local listener, which answers the hooks. Every command makes sure it runs your current settings
42
+ and sign-in: it replaces one from an older version and keeps a newer one. After you edit `heed.yaml` by hand, run
43
+ `npx heedvane listener restart`. After a reboot the hooks start it again by themselves, at most once a minute (on
44
+ Windows they run in Git Bash, from Git for Windows). Set `HEED_LISTENER_AUTOSTART=0` to turn that off. While the listener cannot answer, Claude Code,
45
+ Codex, Gemini CLI and Pi tell you once per session that actions are not being checked. The listener trusts its
46
+ machine: run your agents under one OS user per machine, and give each user on a shared server its own container or VM.
47
+ If you pin `HEED_AGENT_HOOK_PORT` and `HEED_AGENT_HOOK_TOKEN` in an agent's environment, pin `HEED_AGENT_HOOK_PROOF`
48
+ from `~/.heed/endpoint.env` too; without it every hook runs Node and waits a quarter second before it is answered.
28
49
 
29
50
  Run `npx heedvane connect --dry-run` to see every file it would touch without writing anything.
30
51
 
@@ -34,22 +55,47 @@ Agents that run unattended (a factory looping on a server, a CI job, a container
34
55
  package without the installer:
35
56
 
36
57
  ```sh
37
- # Once, when you build the image: install the hooks for the agent the factory runs
58
+ # Install the hooks and Heedvane's MCP connection (also fine while the image is built)
38
59
  npx -y heedvane connect --server https://app.heedvane.com --org <organization id> --agent claude --yes
39
60
 
40
- # When the container starts: sign in with an organization key from your secret store
41
- printf '%s' "$HEEDVANE_ORG_KEY" | npx heedvane auth org
61
+ # Where the agent runs, with HEEDVANE_ORG_KEY set from your secret store: sign in with the key.
62
+ # This also switches Heedvane's MCP connection to sending that key.
63
+ printf '%s' "$HEEDVANE_ORG_KEY" | npx -y heedvane auth org
42
64
 
43
65
  # Every agent run: Heedvane answers the hooks for exactly this run
44
- npx heedvane run -- claude -p "fix the failing tests"
66
+ npx -y heedvane run -- claude -p "fix the failing tests"
45
67
  ```
46
68
 
69
+ Once an organization key is saved with `auth org` (or `HEEDVANE_ORG_KEY` is set), Heedvane's MCP connection
70
+ (named `heedvane`) sends the key from `HEEDVANE_ORG_KEY` as `Authorization: Bearer`, through the agent's own way
71
+ to read an environment variable: the key is never written to a config file, and no browser sign-in is needed.
72
+ The commands work in either order: `auth org` switches a connection that `connect` installed earlier. Keep
73
+ `HEEDVANE_ORG_KEY` set while the agent runs, because the agent reads it when it starts; `doctor` says when it is
74
+ not set. Claude Code, Codex, Cursor, Gemini CLI, Qwen Code, OpenCode, Droid, Kimi Code, Copilot CLI, Windsurf and Pi
75
+ support this; VS Code, Warp, Antigravity and Claude Desktop keep the browser sign-in.
76
+
47
77
  `run` keeps the agent's terminal output and exit code, so your pipeline reads them as before. Heedvane's
48
78
  own lines go to stderr and `~/.heed/listener.log`.
49
79
 
50
80
  - Claude Code runs the hooks in print mode (`claude -p`), but not with `--bare`.
51
81
  - Codex runs hooks only after they are trusted.
52
- - The installer does not set up Pi or Flue. Connect them with a hook of your own that sends each action to `POST /api/agent/steering` with the organization key; Heedvane answers whether to refuse it.
82
+ - Pi runs Heedvane's guard as an extension: `npx -y heedvane run -- pi -p "<your task>"`.
83
+
84
+ ## Agents you build on Pi
85
+
86
+ Code built on Pi's core (`@earendil-works/pi-agent-core`) or on Pi Durable (`@earendil-works/pi-durable`) asks
87
+ Heedvane before each tool runs with the guard this package exports. Install `heedvane` beside your agent and add
88
+ one line:
89
+
90
+ ```ts
91
+ import { beforeToolCall, beforeTool } from "heedvane/guard";
92
+
93
+ new Agent({ ...options, beforeToolCall }); // Pi's core
94
+ registry.install(defineExtension({ name: "heedvane", hooks: [hook(ToolTask, { beforeTool })] })); // Pi Durable
95
+ ```
96
+
97
+ Run the agent with `npx -y heedvane run -- <command>`, or on a machine where Heedvane's listener runs. When the listener cannot
98
+ answer, the guard lets the tool run, keeps the call for the listener and starts one, as the agents' hooks do.
53
99
 
54
100
  ## Everyday commands
55
101
 
@@ -58,6 +104,7 @@ own lines go to stderr and `~/.heed/listener.log`.
58
104
  | `npx heedvane` | Opens the installer: change agents and features any time. |
59
105
  | `npx heedvane doctor` | Checks the install and prints a fix for each problem. |
60
106
  | `npx heedvane listener start` | Runs the listener in the foreground. |
107
+ | `npx heedvane listener restart` | Makes sure the background listener runs your current settings, after a hand edit of `heed.yaml`. |
61
108
  | `npx heedvane run -- <command>` | Runs one agent with Heedvane, for CI and containers. |
62
109
  | `npx heedvane uninstall` | Removes only the entries Heedvane added. `heed.yaml` stays. |
63
110
  | `npx heedvane --help` | Lists every command and flag. |
@@ -3,6 +3,8 @@ import { type LoginTokenOptions } from "./login.js";
3
3
  export type CredentialKind = "org";
4
4
  export type Credentials = Partial<Record<CredentialKind, string>>;
5
5
  export declare function credentialsPath(root: string): string;
6
+ /** True when this machine signs in with a saved organization key: a software factory (`auth org`), with no login, which would win. */
7
+ export declare function signsInWithOrgKey(root: string): boolean;
6
8
  /** Store one key beside the others, 0600. */
7
9
  export declare function saveCredential(root: string, kind: CredentialKind, key: string): Promise<void>;
8
10
  /** Heedvane at `url` when this machine has a sign-in or a key now, else null. The credential is read again on every call. */
@@ -4,6 +4,7 @@
4
4
  * `npx heedvane auth org` in `<root>/credentials.json`, mode 0600, kept apart from heed.yaml (a file
5
5
  * people share). A machine uses one of them: the login when there is one, else the key.
6
6
  */
7
+ import { existsSync, readFileSync } from "node:fs";
7
8
  import { readFile } from "node:fs/promises";
8
9
  import { join } from "node:path";
9
10
  import writeFileAtomic from "write-file-atomic";
@@ -33,6 +34,18 @@ async function readCredentials(root) {
33
34
  }
34
35
  return out;
35
36
  }
37
+ /** True when this machine signs in with a saved organization key: a software factory (`auth org`), with no login, which would win. */
38
+ export function signsInWithOrgKey(root) {
39
+ if (existsSync(join(root, "login.json")))
40
+ return false;
41
+ try {
42
+ const value = JSON.parse(readFileSync(credentialsPath(root), "utf8"));
43
+ return isRecord(value) && typeof value["org"] === "string" && value["org"] !== "";
44
+ }
45
+ catch {
46
+ return false; // no key saved, or a file that does not parse: no key to send
47
+ }
48
+ }
36
49
  /** Store one key beside the others, 0600. */
37
50
  export async function saveCredential(root, kind, key) {
38
51
  const next = { ...(await readCredentials(root)), [kind]: key };
@@ -55,7 +55,8 @@ export function withoutCredentials(remote) {
55
55
  export function repositoryName(remote) {
56
56
  const trimmed = remote.trim();
57
57
  const colon = trimmed.indexOf(":");
58
- const url = URL.parse(trimmed.includes("://") || colon <= 0 ? trimmed : `ssh://${trimmed.slice(0, colon)}/${trimmed.slice(colon + 1)}`);
58
+ const origin = trimmed.includes("://") || colon <= 0 ? trimmed : `ssh://${trimmed.slice(0, colon)}/${trimmed.slice(colon + 1)}`;
59
+ const url = URL.canParse(origin) ? new URL(origin) : null; // not URL.parse: Node 22.0 has none
59
60
  const path = url?.pathname.replace(/\/+$/, "").replace(/\.git$/i, "").replace(/^\/+/, "") ?? "";
60
61
  return path.includes("/") ? path : "";
61
62
  }
@@ -22,6 +22,7 @@ import { mkdir, readdir, readFile, rename, unlink } from "node:fs/promises";
22
22
  import { join } from "node:path";
23
23
  import writeFileAtomic from "write-file-atomic";
24
24
  import { signInIdentity } from "./credentials.js";
25
+ import { OTHER_SIGN_IN_DIR } from "../hook-fallback.js";
25
26
  import { isPaneSpoolFile, spoolDir } from "../listener/spool.js";
26
27
  import { steeringPolicyPath } from "../steering/rules.js";
27
28
  /** The file that records whose sign-in the spool, instructions and caches on this machine belong to. */
@@ -58,9 +59,9 @@ async function setAsideSignInLeftovers(root) {
58
59
  const spool = spoolDir(root);
59
60
  const names = (await readdir(spool).catch(() => [])).filter(isPaneSpoolFile);
60
61
  if (names.length > 0)
61
- await mkdir(join(spool, "other-sign-in"), { recursive: true, mode: 0o700 });
62
+ await mkdir(join(spool, OTHER_SIGN_IN_DIR), { recursive: true, mode: 0o700 });
62
63
  for (const name of names)
63
- await rename(join(spool, name), join(spool, "other-sign-in", `${Date.now()}-${name}`));
64
+ await rename(join(spool, name), join(spool, OTHER_SIGN_IN_DIR, `${Date.now()}-${name}`));
64
65
  await discardSteeringCaches(root);
65
66
  }
66
67
  /** The pre-rendered instructions and the cached policy of an earlier sign-in; the next sync writes the current one's. */
package/dist/cli.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { type ListenerOutcome } from "./listener/background.js";
2
3
  import { type PromptStreams } from "./installer.js";
3
4
  export interface MainOptions {
4
5
  /** Env override (tests inject HEED_HOME fixtures). Defaults to process.env. */
@@ -16,7 +17,7 @@ export interface MainOptions {
16
17
  /** Where the interactive installer reads keys and draws; defaults to the terminal (tests inject streams). */
17
18
  prompts?: PromptStreams;
18
19
  /** Starts the listener in the background for the installer; defaults to a detached `listener start` (tests pass a stand-in). */
19
- startListener?: (home: string) => Promise<boolean>;
20
+ ensureListener?: (home: string) => Promise<ListenerOutcome>;
20
21
  }
21
22
  declare function main(argv: string[], opts?: MainOptions): Promise<number>;
22
23
  /** All of stdin as text (where `auth` reads its key). */
@@ -25,5 +26,6 @@ declare function main(argv: string[], opts?: MainOptions): Promise<number>;
25
26
  * The command that opens `url` in the default browser. Windows goes through rundll32, not `cmd /c start`:
26
27
  * cmd would cut the sign-in URL at its first `&`.
27
28
  */
28
- export declare function browserOpener(platform: NodeJS.Platform, url: string): [string, string[]];
29
+ /** The command that opens `url`: `$BROWSER` when set (a no-op such as `true` keeps captures and tests off a real browser), else the platform's opener. */
30
+ export declare function browserOpener(platform: NodeJS.Platform, url: string, browser?: string): [string, string[]];
29
31
  export { main };