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/dist/mcp.d.ts CHANGED
@@ -1,57 +1,47 @@
1
1
  /**
2
- * MCP registration entry builder per client.
2
+ * Heedvane's MCP connection: the one MCP entry the installer registers, named `heedvane`, pointing at
3
+ * the organization's remote server (`<server>/api/mcp/<org>`). Remote entries take the shapes the dashboard's
4
+ * setup writes (apps/web/src/lib/agent/mcp-setup.ts `HEEDVANE_MCP_SERVER_NAME`); Claude Desktop's mcp-remote bridge
5
+ * (`bridgeEntry`, with the installing Node's folder) is this CLI's own. The agent signs in in the browser on first use.
3
6
  *
4
- * The heedvane MCP server is the universal integration channel — and the only
5
- * policy channel for agents with no native hooks (Warp). Entries are stdio
6
- * commands (`npx heedvane mcp`); an `--http` mode may be added later
7
- * without changing the shape of these builders.
8
- *
9
- * Identity: uninstall and marker detection recognize our entries by the
10
- * identity markers below (today's `npx -y heedvane mcp` args, plus the
11
- * scoped and `heedvane-hooks` names development installs wrote), or by the
12
- * reserved entry name `heedvane`.
7
+ * Identity: uninstall and detection recognize an entry by its name and shape together, never by a name
8
+ * alone: today's connection, the local helper earlier versions registered (`npx -y heedvane mcp`, under any
9
+ * name) and the connection under its earlier name `heedvane-findings`. Connect replaces the earlier ones.
13
10
  */
14
- /** Reserved server entry name. Presence of this NAME alone marks the entry ours. */
11
+ /** The entry's name in every agent's MCP config. */
15
12
  export declare const HEEDVANE_SERVER_NAME = "heedvane";
16
- /**
17
- * Identity markers: an entry whose command/args contain any of these strings
18
- * is a heedvane-managed entry, whatever name it is registered under. These are
19
- * the names development builds wrote; today's entry is recognized by its whole
20
- * argument list ({@link MCP_ENTRY_ARGS}), since the bare package name would
21
- * also match a path like `~/Projects/heedvane`.
22
- */
23
- export declare const IDENTITY_MARKERS: readonly ["@heedvane/hooks", "heedvane-hooks"];
13
+ /** Why metrics only registers no MCP connection: its tools send questions and repository names to Heedvane. */
14
+ export declare const MCP_OFF_METRICS_ONLY = "Heedvane's MCP connection: off under Metrics only, since its tools send your questions and repository names to Heedvane";
15
+ /** What to do when no organization is known, so no MCP connection can be registered. */
16
+ export declare const MCP_NEEDS_ORG = "Heedvane's MCP connection needs your organization: run the connect command from your dashboard's Settings (it has --org), or npx -y heedvane connect --org <id>";
17
+ /** How to add the organization: `setup` cannot, since it compiles heed.yaml as it is. */
18
+ export declare const ORG_REMEDIATION = "run the connect command from your dashboard's Settings, or npx -y heedvane connect --org <organization id>";
24
19
  /** Uninstall/detection marker for inline dispatcher snippets (hooks configs). */
25
20
  export declare const SNIPPET_MARKER = "heedvane-agent-hook-form";
26
- /** Args of the stdio MCP entry (command is always `npx`). */
27
- export declare const MCP_ENTRY_ARGS: string[];
28
- export interface McpServerEntry {
29
- command: string;
30
- args: string[];
31
- [key: string]: unknown;
32
- }
33
- /** The stdio MCP entry, for JSON-family configs (`mcpServers` / `servers` / `mcp`). */
34
- export declare function mcpEntry(): McpServerEntry;
35
- /**
36
- * The stdio MCP entry for TOML configs. Codex reads `~/.codex/config.toml`
37
- * where servers are `mcp_servers.<name>` and a slow `npx` cold start needs an
38
- * explicit generous startup timeout.
39
- */
40
- export declare function mcpEntryForToml(): McpServerEntry;
41
- /** Per-client desired entry. Only Codex (TOML) carries the startup timeout. */
42
- export declare function desiredServer(clientId: string): McpServerEntry;
43
- /** Warp's MCP config is project-level and approval-gated. */
44
- export declare const WARP_MCP_RELATIVE_PATH = ".warp/.mcp.json";
45
- /**
46
- * Honest limitation surfaced by init/doctor for Warp. Warp has no native
47
- * hooks and its MCP support is project-scoped and import-gated: the user must
48
- * approve the import in Warp, per project, before the server activates.
49
- * Policies therefore arrive via MCP tools, not hooks.
50
- */
51
- export declare const WARP_MCP_LIMITATION: string;
21
+ /** The connection's URL for an organization, as the dashboard builds it (`buildHeedvaneMcpUrl`). */
22
+ export declare function connectorUrl(server: string, org: string): string;
52
23
  /**
53
- * Is this server entry a heedvane-managed one? True when the entry NAME is
54
- * the reserved `heedvane` name, or when the entry's command/args carry an
55
- * identity marker. Used by detectMarker and doctor.
24
+ * The entry as drift checks compare it: a bridge's installing-Node folder is left out, since the launcher finds a Node
25
+ * without it, and connect run from another Node is no change to the connection.
56
26
  */
27
+ export declare function comparableEntry(entry: unknown): unknown;
28
+ /** The variable a software factory holds its organization API key in; the dashboard's factory commands use it. */
29
+ export declare const ORG_KEY_ENV = "HEEDVANE_ORG_KEY";
30
+ /** The software-factory connection entry for this agent; undefined where it keeps the browser sign-in. */
31
+ export declare function keyConnectorEntry(clientId: string, url: string): Record<string, unknown> | undefined;
32
+ /** The connection entry for this agent, or undefined when the installer does not register it there. `nodeDir` (POSIX only) is the installing Node's folder, for an entry that runs a local bridge. */
33
+ export declare function connectorEntry(clientId: string, url: string, nodeDir?: string): Record<string, unknown> | undefined;
34
+ /** The organization an entry's Heedvane MCP URL names at `server`, if it points there. */
35
+ export declare function connectorOrganization(entry: unknown, server: string): string | undefined;
36
+ /** The strings an entry names a server URL with: any agent's URL field, or a bridge's arguments. */
37
+ export declare function entryUrls(entry: unknown): string[];
38
+ /** Is this today's connection entry? Its name and a Heedvane MCP URL both have to match. */
39
+ export declare function isConnectorEntry(name: string, entry: unknown): boolean;
40
+ /** Is this an entry an earlier version wrote: the local helper under any name, or the connection under its earlier name? */
41
+ export declare function isLegacyEntry(name: string, entry: unknown): boolean;
42
+ /** Does this entry run the local helper an earlier version registered, under any name? */
43
+ export declare function isHelperEntry(_name: string, entry: unknown): boolean;
44
+ /** Is this server entry Heedvane's, today's or an earlier version's? Used by uninstall, detection and doctor. */
57
45
  export declare function isHeedvaneEntry(name: string, entry: unknown): boolean;
46
+ /** Names of the entries `match` accepts in a parsed config's MCP containers, as `container::name`. */
47
+ export declare function mcpEntryNames(doc: Record<string, unknown>, containerKeys: string[], match: (name: string, entry: unknown) => boolean): string[];
package/dist/mcp.js CHANGED
@@ -1,83 +1,216 @@
1
1
  /**
2
- * MCP registration entry builder per client.
2
+ * Heedvane's MCP connection: the one MCP entry the installer registers, named `heedvane`, pointing at
3
+ * the organization's remote server (`<server>/api/mcp/<org>`). Remote entries take the shapes the dashboard's
4
+ * setup writes (apps/web/src/lib/agent/mcp-setup.ts `HEEDVANE_MCP_SERVER_NAME`); Claude Desktop's mcp-remote bridge
5
+ * (`bridgeEntry`, with the installing Node's folder) is this CLI's own. The agent signs in in the browser on first use.
3
6
  *
4
- * The heedvane MCP server is the universal integration channel — and the only
5
- * policy channel for agents with no native hooks (Warp). Entries are stdio
6
- * commands (`npx heedvane mcp`); an `--http` mode may be added later
7
- * without changing the shape of these builders.
8
- *
9
- * Identity: uninstall and marker detection recognize our entries by the
10
- * identity markers below (today's `npx -y heedvane mcp` args, plus the
11
- * scoped and `heedvane-hooks` names development installs wrote), or by the
12
- * reserved entry name `heedvane`.
7
+ * Identity: uninstall and detection recognize an entry by its name and shape together, never by a name
8
+ * alone: today's connection, the local helper earlier versions registered (`npx -y heedvane mcp`, under any
9
+ * name) and the connection under its earlier name `heedvane-findings`. Connect replaces the earlier ones.
13
10
  */
14
- /** Reserved server entry name. Presence of this NAME alone marks the entry ours. */
11
+ /** The entry's name in every agent's MCP config. */
15
12
  export const HEEDVANE_SERVER_NAME = "heedvane";
16
- /**
17
- * Identity markers: an entry whose command/args contain any of these strings
18
- * is a heedvane-managed entry, whatever name it is registered under. These are
19
- * the names development builds wrote; today's entry is recognized by its whole
20
- * argument list ({@link MCP_ENTRY_ARGS}), since the bare package name would
21
- * also match a path like `~/Projects/heedvane`.
22
- */
23
- export const IDENTITY_MARKERS = ["@heedvane/hooks", "heedvane-hooks"];
13
+ /** Why metrics only registers no MCP connection: its tools send questions and repository names to Heedvane. */
14
+ export const MCP_OFF_METRICS_ONLY = "Heedvane's MCP connection: off under Metrics only, since its tools send your questions and repository names to Heedvane";
15
+ /** What to do when no organization is known, so no MCP connection can be registered. */
16
+ export const MCP_NEEDS_ORG = "Heedvane's MCP connection needs your organization: run the connect command from your dashboard's Settings (it has --org), or npx -y heedvane connect --org <id>";
17
+ /** How to add the organization: `setup` cannot, since it compiles heed.yaml as it is. */
18
+ export const ORG_REMEDIATION = "run the connect command from your dashboard's Settings, or npx -y heedvane connect --org <organization id>";
19
+ /** The connection's name before 0.3.0; connect and uninstall remove an entry under it. */
20
+ const EARLIER_CONNECTOR_NAME = "heedvane-findings";
21
+ /** The packages the local helper ran from: `heedvane` (0.1.0 to 0.2.1 on npm) and the development builds' names. */
22
+ const HELPER_PACKAGES = ["heedvane", "heedvane-hooks", "@heedvane/hooks"];
24
23
  /** Uninstall/detection marker for inline dispatcher snippets (hooks configs). */
25
24
  export const SNIPPET_MARKER = "heedvane-agent-hook-form";
26
- /** The npm package name entries carry; marker + entry builder use it. */
27
- const MCP_ENTRY_COMMAND = "heedvane";
28
- /** Args of the stdio MCP entry (command is always `npx`). */
29
- export const MCP_ENTRY_ARGS = ["-y", MCP_ENTRY_COMMAND, "mcp"];
30
- /** The stdio MCP entry, for JSON-family configs (`mcpServers` / `servers` / `mcp`). */
31
- export function mcpEntry() {
32
- return { command: "npx", args: [...MCP_ENTRY_ARGS] };
25
+ /** The connection's URL for an organization, as the dashboard builds it (`buildHeedvaneMcpUrl`). */
26
+ export function connectorUrl(server, org) {
27
+ return `${server.replace(/\/+$/, "")}/api/mcp/${encodeURIComponent(org)}`;
33
28
  }
34
29
  /**
35
- * The stdio MCP entry for TOML configs. Codex reads `~/.codex/config.toml`
36
- * where servers are `mcp_servers.<name>` and a slow `npx` cold start needs an
37
- * explicit generous startup timeout.
30
+ * The connection entry per agent, in each agent's documented remote-server (Streamable HTTP) shape. Claude Code
31
+ * and VS Code need `type: "http"` (a typeless entry is read as stdio and skipped); Codex (`codex mcp add --url`)
32
+ * and Cursor take `url` alone, as the dashboard's setup writes them. Each agent signs in in the browser itself.
38
33
  */
39
- export function mcpEntryForToml() {
40
- return { command: "npx", args: [...MCP_ENTRY_ARGS], startup_timeout_sec: 30 };
34
+ const CONNECTOR_ENTRY = {
35
+ claude: (url) => ({ type: "http", url }),
36
+ vscode: (url) => ({ type: "http", url }),
37
+ codex: (url) => ({ url }),
38
+ cursor: (url) => ({ url }),
39
+ // Gemini CLI: `httpUrl` is Streamable HTTP (`url` is SSE); OAuth is discovered on the first 401 (`/mcp auth heedvane`).
40
+ gemini: (url) => ({ httpUrl: url }),
41
+ // Qwen Code (a Gemini CLI fork) runs the OAuth flow only for a server whose `oauth` is enabled; endpoints and client are discovered.
42
+ "qwen-code": (url) => ({ httpUrl: url, oauth: { enabled: true } }),
43
+ opencode: (url) => ({ type: "remote", url, enabled: true }),
44
+ warp: (url) => ({ url }),
45
+ // Kimi Code: a `url` without `transport` is Streamable HTTP (`/mcp-config login heedvane` signs in).
46
+ kimi: (url) => ({ url }),
47
+ droid: (url) => ({ type: "http", url }),
48
+ // Antigravity and Windsurf read only `serverUrl` for a remote server.
49
+ antigravity: (url) => ({ serverUrl: url }),
50
+ windsurf: (url) => ({ serverUrl: url }),
51
+ // Copilot CLI lists the tools it may call; `*` is every tool.
52
+ copilot_cli: (url) => ({ type: "http", url, tools: ["*"] }),
53
+ // Pi signs in on the first 401 (`pi mcp login heedvane`) and offers the tools to the model only with `direct` exposure.
54
+ pi: (url) => ({ url, exposure: "direct" }),
55
+ // Claude Desktop's config file runs only local (stdio) servers, so the mcp-remote bridge carries the connection and
56
+ // its browser sign-in. `@latest` keeps npx off an older cached copy (0.1.15 and earlier had CVE-2025-6514).
57
+ claude_desktop: (url, nodeDir) => bridgeEntry(["-y", "mcp-remote@latest", url], nodeDir),
58
+ };
59
+ /**
60
+ * Finds npx when the agent starts the server: the installing Node's folder first, then the newest Node 22 or later
61
+ * from nvm, then Homebrew's and /usr/local's, so an upgrade that removes the installing folder does not break the entry.
62
+ * The folder found leads PATH, since npx finds `node` through PATH and Claude Desktop opened from Finder starts servers
63
+ * with launchd's PATH, which holds no Node from nvm or Homebrew.
64
+ */
65
+ const BRIDGE_LAUNCHER = [
66
+ 'installed="$1"; shift',
67
+ 'newest=""; newest_rank=0',
68
+ 'for bin in "$HOME"/.nvm/versions/node/v*/bin; do',
69
+ ' version="${bin%/bin}"; version="${version##*/v}"; major="${version%%.*}"; rest="${version#*.}"; minor="${rest%%.*}"; patch="${rest#*.}"',
70
+ ' case "$major$minor$patch" in ""|*[!0-9]*) continue ;; esac',
71
+ " rank=$((major * 1000000 + minor * 1000 + patch))",
72
+ ' if [ "$major" -ge 22 ] && [ "$rank" -gt "$newest_rank" ] && [ -x "$bin/npx" ]; then newest="$bin"; newest_rank="$rank"; fi',
73
+ "done",
74
+ 'for dir in "$installed" "$newest" /opt/homebrew/bin /usr/local/bin; do',
75
+ ' if [ -n "$dir" ] && [ -x "$dir/npx" ]; then PATH="$dir:/usr/bin:/bin:/usr/sbin:/sbin" exec "$dir/npx" "$@"; fi',
76
+ "done",
77
+ 'echo "heedvane: no npx found for Heedvane\'s MCP connection; install Node.js 22 or later" >&2',
78
+ "exit 127",
79
+ ].join("\n");
80
+ const BRIDGE_NAME = "heedvane-bridge";
81
+ /** A local server run by npx: through {@link BRIDGE_LAUNCHER} given the installing Node's folder, else the bare npx (Windows). */
82
+ function bridgeEntry(args, nodeDir) {
83
+ if (nodeDir === undefined)
84
+ return { command: "npx", args };
85
+ return { command: "/bin/sh", args: ["-c", BRIDGE_LAUNCHER, BRIDGE_NAME, nodeDir, ...args] };
41
86
  }
42
- /** Per-client desired entry. Only Codex (TOML) carries the startup timeout. */
43
- export function desiredServer(clientId) {
44
- return clientId === "codex" ? mcpEntryForToml() : mcpEntry();
87
+ /**
88
+ * The entry as drift checks compare it: a bridge's installing-Node folder is left out, since the launcher finds a Node
89
+ * without it, and connect run from another Node is no change to the connection.
90
+ */
91
+ export function comparableEntry(entry) {
92
+ if (entry === null || typeof entry !== "object")
93
+ return entry;
94
+ const { command, args } = entry;
95
+ if (command !== "/bin/sh" || !Array.isArray(args) || args[0] !== "-c" || args[2] !== BRIDGE_NAME)
96
+ return entry;
97
+ return { ...entry, args: [...args.slice(0, 3), ...args.slice(4)] };
45
98
  }
46
- /** Warp's MCP config is project-level and approval-gated. */
47
- export const WARP_MCP_RELATIVE_PATH = ".warp/.mcp.json";
99
+ /** The fields agents name a remote server's URL with. */
100
+ const URL_FIELDS = ["url", "httpUrl", "serverUrl"];
101
+ /** The variable a software factory holds its organization API key in; the dashboard's factory commands use it. */
102
+ export const ORG_KEY_ENV = "HEEDVANE_ORG_KEY";
48
103
  /**
49
- * Honest limitation surfaced by init/doctor for Warp. Warp has no native
50
- * hooks and its MCP support is project-scoped and import-gated: the user must
51
- * approve the import in Warp, per project, before the server activates.
52
- * Policies therefore arrive via MCP tools, not hooks.
104
+ * A software factory's connection: the agent sends the organization key from HEEDVANE_ORG_KEY as `Authorization: Bearer`
105
+ * through its own documented way to read a variable, so the key is never written to a file and no browser sign-in is
106
+ * needed. VS Code, Warp, Antigravity and Claude Desktop document none, so they keep the browser sign-in.
53
107
  */
54
- export const WARP_MCP_LIMITATION = "Warp limitation: Warp has no native hooks support, and its MCP servers are project-scoped and " +
55
- "approval-gated. Heedvane writes a project-level .warp/.mcp.json for the current project only; " +
56
- "you must approve the heedvane MCP import inside Warp (Settings > MCP) before it activates, and " +
57
- "repeat that approval in every project you use it in. Hooks are unsupported in Warp — policies " +
58
- "arrive via MCP tools once the server import is approved.";
108
+ const KEY_CONNECTOR_ENTRY = {
109
+ // Claude Code, Gemini CLI, Qwen Code, Droid, Copilot CLI and Pi expand ${VAR} in header values.
110
+ claude: (url) => ({ type: "http", url, headers: { Authorization: `Bearer \${${ORG_KEY_ENV}}` } }),
111
+ gemini: (url) => ({ httpUrl: url, headers: { Authorization: `Bearer \${${ORG_KEY_ENV}}` } }),
112
+ "qwen-code": (url) => ({ httpUrl: url, headers: { Authorization: `Bearer \${${ORG_KEY_ENV}}` } }),
113
+ droid: (url) => ({ type: "http", url, headers: { Authorization: `Bearer \${${ORG_KEY_ENV}}` } }),
114
+ copilot_cli: (url) => ({ type: "http", url, headers: { Authorization: `Bearer \${${ORG_KEY_ENV}}` }, tools: ["*"] }),
115
+ pi: (url) => ({ url, headers: { Authorization: `Bearer \${${ORG_KEY_ENV}}` }, exposure: "direct" }),
116
+ // Codex and Kimi Code name the variable that holds the bearer token.
117
+ codex: (url) => ({ url, bearer_token_env_var: ORG_KEY_ENV }),
118
+ kimi: (url) => ({ url, bearerTokenEnvVar: ORG_KEY_ENV }),
119
+ // Cursor and Windsurf (Devin Desktop) read ${env:VAR}; OpenCode reads {env:VAR} and needs its own OAuth off.
120
+ cursor: (url) => ({ url, headers: { Authorization: `Bearer \${env:${ORG_KEY_ENV}}` } }),
121
+ windsurf: (url) => ({ serverUrl: url, headers: { Authorization: `Bearer \${env:${ORG_KEY_ENV}}` } }),
122
+ opencode: (url) => ({ type: "remote", url, enabled: true, oauth: false, headers: { Authorization: `Bearer {env:${ORG_KEY_ENV}}` } }),
123
+ };
124
+ /** The software-factory connection entry for this agent; undefined where it keeps the browser sign-in. */
125
+ export function keyConnectorEntry(clientId, url) {
126
+ return KEY_CONNECTOR_ENTRY[clientId]?.(url);
127
+ }
128
+ /** The connection entry for this agent, or undefined when the installer does not register it there. `nodeDir` (POSIX only) is the installing Node's folder, for an entry that runs a local bridge. */
129
+ export function connectorEntry(clientId, url, nodeDir) {
130
+ return CONNECTOR_ENTRY[clientId]?.(url, nodeDir);
131
+ }
59
132
  /**
60
- * Does this entry's command/args carry a heedvane identity marker? This is
61
- * the STRICT check used by uninstall: an entry is only removed when its
62
- * command/args prove it is ours. A name match without the marker is a
63
- * renamed/replaced entry and is left alone.
133
+ * The server and organization of a Heedvane MCP URL exactly as {@link connectorUrl} builds it: http(s)
134
+ * `<server>/api/mcp/<organization>`, where a server under a base path keeps that path, with no credentials, query or
135
+ * fragment. A user's own URL whose path merely contains `/api/mcp/` is none.
64
136
  */
65
- function hasHeedvaneMarker(entry) {
137
+ function parsedConnectorUrl(value) {
138
+ try {
139
+ const url = new URL(value);
140
+ if (url.protocol !== "http:" && url.protocol !== "https:")
141
+ return undefined;
142
+ const path = url.pathname.endsWith("/") ? url.pathname.slice(0, -1) : url.pathname;
143
+ const segments = path.split("/");
144
+ const encodedOrg = segments.pop();
145
+ if (!encodedOrg || segments.pop() !== "mcp" || segments.pop() !== "api")
146
+ return undefined;
147
+ const written = new URL(url);
148
+ written.pathname = path;
149
+ const server = `${url.origin}${segments.join("/")}`;
150
+ const org = decodeURIComponent(encodedOrg);
151
+ return new URL(connectorUrl(server, org)).href === written.href ? { server, org } : undefined;
152
+ }
153
+ catch {
154
+ return undefined; // not a URL, or an organization that does not decode: not one connectorUrl builds
155
+ }
156
+ }
157
+ /** The organization an entry's Heedvane MCP URL names at `server`, if it points there. */
158
+ export function connectorOrganization(entry, server) {
159
+ for (const value of entryUrls(entry)) {
160
+ const parsed = parsedConnectorUrl(value);
161
+ if (parsed !== undefined && new URL(connectorUrl(parsed.server, parsed.org)).href === new URL(connectorUrl(server, parsed.org)).href)
162
+ return parsed.org;
163
+ }
164
+ return undefined;
165
+ }
166
+ /** The strings an entry names a server URL with: any agent's URL field, or a bridge's arguments. */
167
+ export function entryUrls(entry) {
66
168
  if (entry === null || typeof entry !== "object")
67
- return false;
169
+ return [];
68
170
  const e = entry;
69
- const command = typeof e.command === "string" ? e.command : "";
70
- const args = Array.isArray(e.args) ? e.args.filter((a) => typeof a === "string") : [];
71
- const haystack = [command, ...args].join(" ");
72
- return haystack.includes(MCP_ENTRY_ARGS.join(" ")) || IDENTITY_MARKERS.some((m) => haystack.includes(m));
171
+ const args = Array.isArray(e.args) ? e.args : [];
172
+ return [...URL_FIELDS.map((field) => e[field]), ...args].filter((value) => typeof value === "string");
73
173
  }
74
- /**
75
- * Is this server entry a heedvane-managed one? True when the entry NAME is
76
- * the reserved `heedvane` name, or when the entry's command/args carry an
77
- * identity marker. Used by detectMarker and doctor.
78
- */
174
+ /** Does this entry point at a Heedvane MCP URL, under any agent's name for the field or as a bridge's argument? */
175
+ function hasConnectorUrl(entry) {
176
+ return entryUrls(entry).some((url) => parsedConnectorUrl(url) !== undefined);
177
+ }
178
+ /** Is this today's connection entry? Its name and a Heedvane MCP URL both have to match. */
179
+ export function isConnectorEntry(name, entry) {
180
+ return name === HEEDVANE_SERVER_NAME && hasConnectorUrl(entry);
181
+ }
182
+ /** Is this an entry an earlier version wrote: the local helper under any name, or the connection under its earlier name? */
183
+ export function isLegacyEntry(name, entry) {
184
+ return hasHelperCommand(entry) || (name === EARLIER_CONNECTOR_NAME && hasConnectorUrl(entry));
185
+ }
186
+ /** Does this entry run the local helper an earlier version registered, under any name? */
187
+ export function isHelperEntry(_name, entry) {
188
+ return hasHelperCommand(entry);
189
+ }
190
+ /** Is this server entry Heedvane's, today's or an earlier version's? Used by uninstall, detection and doctor. */
79
191
  export function isHeedvaneEntry(name, entry) {
80
- if (name === HEEDVANE_SERVER_NAME)
81
- return true;
82
- return hasHeedvaneMarker(entry);
192
+ return isConnectorEntry(name, entry) || isLegacyEntry(name, entry);
193
+ }
194
+ /** Names of the entries `match` accepts in a parsed config's MCP containers, as `container::name`. */
195
+ export function mcpEntryNames(doc, containerKeys, match) {
196
+ const names = [];
197
+ for (const key of new Set(containerKeys)) {
198
+ const container = doc[key];
199
+ if (container === null || typeof container !== "object" || Array.isArray(container))
200
+ continue;
201
+ for (const [name, entry] of Object.entries(container)) {
202
+ if (match(name, entry))
203
+ names.push(`${key}::${name}`);
204
+ }
205
+ }
206
+ return names;
207
+ }
208
+ /** Does this entry run the local helper, exactly as earlier versions wrote it: `npx -y <helper package> mcp`? */
209
+ function hasHelperCommand(entry) {
210
+ if (entry === null || typeof entry !== "object")
211
+ return false;
212
+ const { command, args } = entry;
213
+ if (command !== "npx" || !Array.isArray(args) || args.length !== 3)
214
+ return false;
215
+ return args[0] === "-y" && HELPER_PACKAGES.includes(String(args[1])) && args[2] === "mcp";
83
216
  }
package/dist/plan.d.ts CHANGED
@@ -2,7 +2,8 @@ import { type Capability, type Env, type ResolvedClient } from "./clients/defini
2
2
  import { type DocFormat } from "./config-doc.js";
3
3
  import type { HeedFile, HeedGatingConfig } from "./heed-file.js";
4
4
  export type ActionKind = "hooks" | "mcp" | "skill" | "skip";
5
- export type Operation = "create" | "update" | "unchanged" | "skip";
5
+ /** `remove`: the action only removes entries ({@link mcpLeftovers}); the plan shows it as removed. */
6
+ export type Operation = "create" | "update" | "unchanged" | "skip" | "remove";
6
7
  export interface PlannedAction {
7
8
  clientId: string;
8
9
  kind: ActionKind;
@@ -21,11 +22,6 @@ export interface PlanInput {
21
22
  /** Desired skill digest; defaults to the canonical bundled content. */
22
23
  skillDigest?: string;
23
24
  }
24
- /** Our MCP stdio registration entry (unscoped package per the plan's decision). */
25
- export declare function mcpEntry(): {
26
- command: string;
27
- args: string[];
28
- };
29
25
  interface Desired {
30
26
  path: string;
31
27
  format: DocFormat;
@@ -42,16 +38,51 @@ export declare function hooksTarget(client: ResolvedClient): {
42
38
  export interface ClientContext {
43
39
  home: string;
44
40
  env: Env;
41
+ /** Heedvane's MCP connection URL (`connect --org`), registered where the agent takes it. */
42
+ connector?: string;
43
+ /** The folder whose npx runs a local MCP bridge (`bridgeNodeDir`); without it, the bare `npx`. */
44
+ nodeDir?: string;
45
45
  }
46
46
  /**
47
47
  * Desired write targets per client × capability. This is the desired-state
48
48
  * compiler heed.yaml → concrete files; doctor reuses it via a callback so
49
- * it never needs apply.ts. `env` is accepted for API stability — the client's
50
- * config paths arrive already resolved against it.
49
+ * it never needs apply.ts. The client's config paths arrive already resolved
50
+ * against `env`; its HEEDVANE_ORG_KEY picks the software-factory MCP connection.
51
51
  */
52
52
  export declare function desiredEntriesFor(client: ResolvedClient, capability: Capability, context: ClientContext & {
53
53
  gating?: HeedGatingConfig["mode"];
54
54
  }): Desired[];
55
+ /** The folder whose npx runs a local MCP bridge on `platform`; none on Windows, which keeps the bare `npx` its docs show (a `.cmd` path cannot be started without a shell). */
56
+ export declare function bridgeNodeDir(platform: string): string | undefined;
57
+ /**
58
+ * Why heed.yaml registers no MCP connection, the one owner of that decision: metrics only, whose promise the
59
+ * connection's tools would break (they send questions and repository names), or no organization. None when it does.
60
+ */
61
+ export declare function connectionOff(heedFile: HeedFile): "metrics-only" | "no-organization" | undefined;
62
+ /** Heedvane's MCP connection URL for the organization heed.yaml names, unless {@link connectionOff} says there is none. */
63
+ export declare function heedvaneConnector(heedFile: HeedFile): string | undefined;
64
+ /** What to tell a person who asked for the MCP connection but gets none, and why; nothing when it is registered. */
65
+ export declare function connectionNote(heedFile: HeedFile): string | undefined;
66
+ /** The organization an earlier version's connection (`heedvane-findings`) names at `server`, so connect can replace it without --org. */
67
+ export declare function legacyOrganization(clients: readonly ResolvedClient[], server: string): string | undefined;
68
+ /** The MCP containers a client's configs can hold our entries in (Codex's TOML and OpenCode's `mcp` beside the usual key). */
69
+ export declare function mcpContainers(client: ResolvedClient): string[];
70
+ /** The doc's parsed value, or null when the config is absent or unparseable — nothing marker-verified to remove. */
71
+ export declare function parseClientDoc(path: string, format: DocFormat): Record<string, unknown> | null;
72
+ /** True when the client's MCP config holds Heedvane's connection, not only a user's entry that shares its name. */
73
+ export declare function hasOwnConnection(client: ResolvedClient): boolean;
74
+ /** Whether Heedvane's connection in this client's MCP config is in the key form (mcp.ts `keyConnectorEntry`), for whatever server and organization it names. */
75
+ export declare function sendsOrgKey(client: ResolvedClient): boolean;
76
+ /** The first config of this client still holding an MCP entry an earlier version wrote, and those entries' names. */
77
+ export declare function legacyMcpEntries(client: ResolvedClient): {
78
+ path: string;
79
+ names: string[];
80
+ } | undefined;
81
+ /** The first config of this client holding any Heedvane MCP entry, today's or an earlier version's, and their names. */
82
+ export declare function heedvaneMcpEntries(client: ResolvedClient): {
83
+ path: string;
84
+ names: string[];
85
+ } | undefined;
55
86
  /**
56
87
  * The client whose hooks this one already runs, when that client's hooks are installed too, by this run or
57
88
  * an earlier one: installing both would run every event twice, recorded under two agents. Plan and doctor
@@ -67,6 +98,14 @@ interface AgentScope {
67
98
  }
68
99
  /** Whether a client is detected on this machine via its primary marker or any altMarkerPaths (e.g. GUI ADEs). */
69
100
  export declare function isClientDetected(client: ResolvedClient): boolean;
101
+ /** The client's native capability, or hooks written as whole files (definitions.ts `hookFiles`). */
102
+ export declare function supportsCapability(client: ResolvedClient, capability: "hooks" | "mcp" | "skill"): boolean;
103
+ /**
104
+ * The MCP entries connect removes: under metrics only every Heedvane entry, today's too; without an organization the
105
+ * local helper, which has no tools any more (a connection under its earlier name still works, and nothing would replace
106
+ * it); otherwise everything an earlier version registered.
107
+ */
108
+ export declare function mcpLeftovers(heedFile: HeedFile): (name: string, entry: unknown) => boolean;
70
109
  /**
71
110
  * The planner. For each client a run installs (installsClient), emit one action per enabled
72
111
  * capability the client supports — or an honest skip with a reason it doesn't.