heedvane 0.3.3 → 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 (60) hide show
  1. package/README.md +41 -9
  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 +2 -1
  7. package/dist/cli.js +325 -118
  8. package/dist/clients/definitions.d.ts +7 -3
  9. package/dist/clients/definitions.js +27 -7
  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/dispatchers/posix.d.ts +16 -19
  20. package/dist/dispatchers/posix.js +71 -91
  21. package/dist/doctor.d.ts +2 -0
  22. package/dist/doctor.js +96 -28
  23. package/dist/guard.d.ts +73 -0
  24. package/dist/guard.js +302 -0
  25. package/dist/heed-file.d.ts +5 -2
  26. package/dist/heed-file.js +7 -7
  27. package/dist/hook-fallback.d.ts +73 -0
  28. package/dist/hook-fallback.js +61 -0
  29. package/dist/installer.d.ts +5 -5
  30. package/dist/installer.js +13 -12
  31. package/dist/listener/background.d.ts +48 -4
  32. package/dist/listener/background.js +153 -19
  33. package/dist/listener/endpoint.d.ts +26 -3
  34. package/dist/listener/endpoint.js +55 -10
  35. package/dist/listener/forward.d.ts +7 -1
  36. package/dist/listener/forward.js +40 -12
  37. package/dist/listener/serve.d.ts +37 -0
  38. package/dist/listener/serve.js +196 -32
  39. package/dist/listener/server.d.ts +9 -0
  40. package/dist/listener/server.js +59 -22
  41. package/dist/listener/spool.d.ts +2 -17
  42. package/dist/listener/spool.js +6 -3
  43. package/dist/mcp.d.ts +23 -4
  44. package/dist/mcp.js +124 -22
  45. package/dist/plan.d.ts +35 -6
  46. package/dist/plan.js +125 -40
  47. package/dist/render.js +2 -1
  48. package/dist/reply/harness-output.d.ts +9 -0
  49. package/dist/reply/harness-output.js +13 -0
  50. package/dist/skill/content.d.ts +2 -2
  51. package/dist/skill/content.js +6 -3
  52. package/dist/status.js +1 -1
  53. package/dist/steering/rules.js +4 -2
  54. package/dist/uninstall.d.ts +7 -2
  55. package/dist/uninstall.js +49 -44
  56. package/package.json +10 -3
  57. package/skills/heedvane-hooks/SKILL.md +3 -3
  58. package/skills/heedvane-hooks/references/events.md +3 -0
  59. package/dist/dispatchers/windows.d.ts +0 -15
  60. package/dist/dispatchers/windows.js +0 -50
package/README.md CHANGED
@@ -1,7 +1,7 @@
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
@@ -9,7 +9,7 @@ npx heedvane
9
9
 
10
10
  The installer shows the agents it found on your machine. Tick the ones you want, choose how much leaves
11
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 or newer.
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
 
@@ -38,8 +38,14 @@ Scripts choose with `--mode look-but-dont-keep` or `--mode metrics-only`. The ch
38
38
  removing that Node version). The agent signs in
39
39
  through the browser the first time it uses it. It replaces the `heedvane` and
40
40
  `heedvane-findings` entries an earlier version added.
41
- - Starts Heedvane's local listener, which answers the hooks. After a restart the hooks start it again by
42
- themselves, at most once a minute. Set `HEED_LISTENER_AUTOSTART=0` to turn that off.
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.
43
49
 
44
50
  Run `npx heedvane connect --dry-run` to see every file it would touch without writing anything.
45
51
 
@@ -49,22 +55,47 @@ Agents that run unattended (a factory looping on a server, a CI job, a container
49
55
  package without the installer:
50
56
 
51
57
  ```sh
52
- # 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)
53
59
  npx -y heedvane connect --server https://app.heedvane.com --org <organization id> --agent claude --yes
54
60
 
55
- # When the container starts: sign in with an organization key from your secret store
56
- 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
57
64
 
58
65
  # Every agent run: Heedvane answers the hooks for exactly this run
59
- npx heedvane run -- claude -p "fix the failing tests"
66
+ npx -y heedvane run -- claude -p "fix the failing tests"
60
67
  ```
61
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
+
62
77
  `run` keeps the agent's terminal output and exit code, so your pipeline reads them as before. Heedvane's
63
78
  own lines go to stderr and `~/.heed/listener.log`.
64
79
 
65
80
  - Claude Code runs the hooks in print mode (`claude -p`), but not with `--bare`.
66
81
  - Codex runs hooks only after they are trusted.
67
- - 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.
68
99
 
69
100
  ## Everyday commands
70
101
 
@@ -73,6 +104,7 @@ own lines go to stderr and `~/.heed/listener.log`.
73
104
  | `npx heedvane` | Opens the installer: change agents and features any time. |
74
105
  | `npx heedvane doctor` | Checks the install and prints a fix for each problem. |
75
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`. |
76
108
  | `npx heedvane run -- <command>` | Runs one agent with Heedvane, for CI and containers. |
77
109
  | `npx heedvane uninstall` | Removes only the entries Heedvane added. `heed.yaml` stays. |
78
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). */