heedvane 0.2.1 → 0.3.3
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 +19 -4
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +86 -48
- package/dist/clients/definitions.d.ts +1 -1
- package/dist/clients/definitions.js +24 -9
- package/dist/commands/memory.js +6 -1
- package/dist/doctor.js +25 -9
- package/dist/heed-file.d.ts +14 -1
- package/dist/heed-file.js +43 -20
- package/dist/installer.d.ts +9 -2
- package/dist/installer.js +30 -4
- package/dist/listener/serve.js +1 -1
- package/dist/mcp.d.ts +21 -50
- package/dist/mcp.js +96 -65
- package/dist/plan.d.ts +15 -5
- package/dist/plan.js +53 -26
- package/dist/render.js +6 -7
- package/dist/reply/reply.js +2 -1
- package/dist/skill/content.d.ts +2 -2
- package/dist/skill/content.js +5 -5
- package/dist/status.d.ts +1 -0
- package/dist/status.js +29 -0
- package/dist/uninstall.d.ts +2 -0
- package/dist/uninstall.js +27 -31
- package/package.json +1 -1
- package/skills/heedvane-hooks/SKILL.md +3 -3
- package/skills/heedvane-hooks/references/events.md +2 -2
- package/dist/mcp-server.d.ts +0 -9
- package/dist/mcp-server.js +0 -187
package/README.md
CHANGED
|
@@ -7,8 +7,8 @@ checks and team memory for Claude Code, Codex, Cursor, Gemini CLI, Hermes, OpenC
|
|
|
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
|
|
11
|
-
adds and does, confirm. It installs, starts Heedvane in the background and signs you in through the
|
|
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
12
|
browser. It needs Node.js 22 or newer.
|
|
13
13
|
|
|
14
14
|
Your dashboard's Agents page has the same command with your server and organization filled in:
|
|
@@ -17,12 +17,27 @@ 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
|
|
25
|
-
|
|
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.
|
|
26
41
|
- Starts Heedvane's local listener, which answers the hooks. After a restart the hooks start it again by
|
|
27
42
|
themselves, at most once a minute. Set `HEED_LISTENER_AUTOSTART=0` to turn that off.
|
|
28
43
|
|
package/dist/cli.d.ts
CHANGED
|
@@ -25,5 +25,6 @@ declare function main(argv: string[], opts?: MainOptions): Promise<number>;
|
|
|
25
25
|
* The command that opens `url` in the default browser. Windows goes through rundll32, not `cmd /c start`:
|
|
26
26
|
* cmd would cut the sign-in URL at its first `&`.
|
|
27
27
|
*/
|
|
28
|
-
|
|
28
|
+
/** 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. */
|
|
29
|
+
export declare function browserOpener(platform: NodeJS.Platform, url: string, browser?: string): [string, string[]];
|
|
29
30
|
export { main };
|
package/dist/cli.js
CHANGED
|
@@ -7,20 +7,21 @@
|
|
|
7
7
|
import { createWriteStream, existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
9
|
import { constants as osConstants, homedir, platform } from "node:os";
|
|
10
|
-
import {
|
|
10
|
+
import { join } from "node:path";
|
|
11
11
|
import { supportedClients } from "./clients/definitions.js";
|
|
12
12
|
import { installHermesPlugin } from "./clients/hermes-plugin.js";
|
|
13
13
|
import { applyEntry } from "./apply.js";
|
|
14
|
-
import { DEFAULT_HEED_YAML_TEMPLATE, heedvaneConfigAt, loadHeedFile, loadHeedFileAt, withInstallChoices,
|
|
15
|
-
import { planActions, desiredEntriesFor, isClientDetected } from "./plan.js";
|
|
14
|
+
import { DATA_MODES, DEFAULT_HEED_YAML_TEMPLATE, heedvaneConfigAt, loadHeedFile, loadHeedFileAt, withInstallChoices, withSettings } from "./heed-file.js";
|
|
15
|
+
import { planActions, desiredEntriesFor, heedvaneConnector, installingNodeDir, isClientDetected } from "./plan.js";
|
|
16
16
|
import { appliedFailed, capabilitySummary, renderApplied, renderDetection, renderPlan, wouldInstallSummary } from "./render.js";
|
|
17
17
|
import { runDoctor } from "./doctor.js";
|
|
18
18
|
import { installSkill } from "./skill/bundle.js";
|
|
19
19
|
import { canonicalSkillFiles, skillDigest } from "./skill/content.js";
|
|
20
20
|
import { emptyState, readState, writeStateAtomic } from "./state.js";
|
|
21
21
|
import { writeDispatcherScript } from "./dispatchers/posix.js";
|
|
22
|
-
import { removeDeferredHookGroups, removeStaleHookGroups, uninstall } from "./uninstall.js";
|
|
23
|
-
import { statusText
|
|
22
|
+
import { removeDeferredHookGroups, removeLegacyMcpEntries, removeStaleHookGroups, uninstall } from "./uninstall.js";
|
|
23
|
+
import { statusText } from "./status.js";
|
|
24
|
+
import { MCP_NEEDS_ORG } from "./mcp.js";
|
|
24
25
|
import { errorMessage } from "./guards.js";
|
|
25
26
|
import { ensureEndpoint } from "./listener/endpoint.js";
|
|
26
27
|
import { listenerLogPath, startListenerInBackground } from "./listener/background.js";
|
|
@@ -29,7 +30,7 @@ import { cmdAuth, cmdLogin, cmdSkills } from "./commands/heedvane.js";
|
|
|
29
30
|
import { cmdMemory } from "./commands/memory.js";
|
|
30
31
|
import { backendAt } from "./backend/credentials.js";
|
|
31
32
|
import { readLogin } from "./backend/login.js";
|
|
32
|
-
import { agentLines, askChoices, brandIntro, confirmInstall, installCancelled, installFinished, installWithSpinner, listenerStep, promptLine, showInstalled } from "./installer.js";
|
|
33
|
+
import { agentLines, askChoices, brandIntro, confirmInstall, installCancelled, installDetails, installFinished, installWithSpinner, listenerStep, promptLine, showInstalled } from "./installer.js";
|
|
33
34
|
const USAGE = ` heedvane-hook — cross-agent steering, guardrails & analysis
|
|
34
35
|
|
|
35
36
|
Usage: npx heedvane [command] [flags] (installed: heedvane <command>)
|
|
@@ -48,11 +49,11 @@ const USAGE = ` heedvane-hook — cross-agent steering, guardrails & analysis
|
|
|
48
49
|
skills sync install the organization's approved skills and cache the list hooks enforce
|
|
49
50
|
memory list|show|search browse this repository's team memory (a folder, one memory, or a question)
|
|
50
51
|
status quick state summary
|
|
51
|
-
mcp run the MCP stdio server (what agents register)
|
|
52
52
|
|
|
53
53
|
Flags: --dry-run --yes --json --agent <id> (repeatable) --client <id> --purge-config --project
|
|
54
54
|
--server <url> (connect/init: the Heedvane app to use, saved as heedvane.url; --url also works)
|
|
55
|
-
--org <id> (connect/init: the organization the sign-in must belong to)
|
|
55
|
+
--org <id> (connect/init: the organization the sign-in must belong to; also registers Heedvane's MCP connection)
|
|
56
|
+
--mode <look-but-dont-keep|metrics-only> (connect/init: what leaves this machine, saved as heedvane.dataMode)`;
|
|
56
57
|
function resolveHome(env) {
|
|
57
58
|
return env.HEED_HOME ?? homedir();
|
|
58
59
|
}
|
|
@@ -98,13 +99,14 @@ function parseInvocation(args) {
|
|
|
98
99
|
clients: parseRepeatedFlag(args, "--client"),
|
|
99
100
|
server: flagValue(args, "--server") ?? flagValue(args, "--url"),
|
|
100
101
|
org: flagValue(args, "--org"),
|
|
102
|
+
mode: flagValue(args, "--mode"),
|
|
101
103
|
positional: positionalArgs(args, VALUE_FLAGS),
|
|
102
104
|
};
|
|
103
105
|
}
|
|
104
106
|
/** Flags without a value. */
|
|
105
107
|
const SWITCH_FLAGS = ["--dry-run", "--yes", "--json", "--purge-config", "--project", "--help", "-h"];
|
|
106
108
|
/** Flags followed by a value. */
|
|
107
|
-
const VALUE_FLAGS = ["--agent", "--client", "--folder", "--offset", "--url", "--server", "--org", "--token"];
|
|
109
|
+
const VALUE_FLAGS = ["--agent", "--client", "--folder", "--offset", "--url", "--server", "--org", "--mode", "--token"];
|
|
108
110
|
/** Heedvane's own arguments: everything before `--`, after which `run` passes the rest to the agent. */
|
|
109
111
|
function ownArgs(args) {
|
|
110
112
|
const separator = args.indexOf("--");
|
|
@@ -168,7 +170,7 @@ function commandContext(argv, opts) {
|
|
|
168
170
|
out: opts.write ?? ((line) => console.log(line)),
|
|
169
171
|
args,
|
|
170
172
|
readStdin: opts.readStdin ?? readProcessStdin,
|
|
171
|
-
openBrowser: opts.openBrowser ?? openSystemBrowser,
|
|
173
|
+
openBrowser: opts.openBrowser ?? ((url) => openSystemBrowser(url, env.BROWSER)),
|
|
172
174
|
interactive: opts.interactive ?? (process.stdin.isTTY === true && process.stdout.isTTY === true),
|
|
173
175
|
cwd: opts.cwd ?? process.cwd(),
|
|
174
176
|
prompts: opts.prompts ?? {},
|
|
@@ -197,10 +199,6 @@ async function routeServiceCommand(command, ctx) {
|
|
|
197
199
|
ctx.out(await statusText());
|
|
198
200
|
return 0;
|
|
199
201
|
}
|
|
200
|
-
case "mcp": {
|
|
201
|
-
runMcpStdio({ write: (line) => ctx.out(line) });
|
|
202
|
-
return 0;
|
|
203
|
-
}
|
|
204
202
|
case "listener": {
|
|
205
203
|
return cmdListener({ home: ctx.home, out: ctx.out, rest: ctx.args.slice(ctx.args.indexOf("listener") + 1) });
|
|
206
204
|
}
|
|
@@ -260,11 +258,11 @@ async function cmdInit(ctx) {
|
|
|
260
258
|
}
|
|
261
259
|
let serverEdit;
|
|
262
260
|
try {
|
|
263
|
-
serverEdit =
|
|
261
|
+
serverEdit = withFlags(home, loaded, ctx);
|
|
264
262
|
}
|
|
265
263
|
catch (err) {
|
|
266
264
|
out("");
|
|
267
|
-
out(` ✗
|
|
265
|
+
out(` ✗ ${errorMessage(err)}`);
|
|
268
266
|
return 1;
|
|
269
267
|
}
|
|
270
268
|
const { heedFile, existed } = serverEdit ?? loaded;
|
|
@@ -289,11 +287,27 @@ async function cmdInit(ctx) {
|
|
|
289
287
|
// --yes: first non-dry run has approval; proceed to write.
|
|
290
288
|
return applyAndReport(ctx, { heedFile, agents, existed, ...(serverEdit === null ? {} : { text: serverEdit.text }) });
|
|
291
289
|
}
|
|
292
|
-
/** heed.yaml
|
|
293
|
-
function
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
290
|
+
/** The heed.yaml settings `--server`, `--org` and `--mode` name; a refusal when a value cannot be used. */
|
|
291
|
+
function flagSettings({ server, org, mode }) {
|
|
292
|
+
if (mode !== undefined && !DATA_MODES.some((known) => known === mode))
|
|
293
|
+
throw new Error(`--mode ${mode} is not one of ${DATA_MODES.join(", ")}; nothing was changed`);
|
|
294
|
+
const settings = [[["heedvane", "url"], server], [["heedvane", "org"], org], [["heedvane", "dataMode"], mode]];
|
|
295
|
+
return settings.filter((setting) => setting[1] !== undefined);
|
|
296
|
+
}
|
|
297
|
+
/** heed.yaml (or the template) with the flags' settings; null when they change nothing. Throws on a value it cannot use. */
|
|
298
|
+
function withFlags(home, loaded, flags) {
|
|
299
|
+
const settings = flagSettings(flags);
|
|
300
|
+
if (settings.length === 0)
|
|
301
|
+
return null;
|
|
302
|
+
const text = withSettings(heedText(home, loaded.existed), settings);
|
|
303
|
+
let heedFile;
|
|
304
|
+
try {
|
|
305
|
+
heedFile = loadHeedFile(text);
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
throw new Error(`${flags.server === undefined ? "a flag" : `--server ${flags.server}`} cannot be used: ${errorMessage(err)}`, { cause: err });
|
|
309
|
+
}
|
|
310
|
+
if (loaded.existed && JSON.stringify(heedFile.heedvane) === JSON.stringify(loaded.heedFile.heedvane))
|
|
297
311
|
return null;
|
|
298
312
|
return { heedFile, existed: loaded.existed, text };
|
|
299
313
|
}
|
|
@@ -305,7 +319,9 @@ async function applyAndReport(ctx, spec) {
|
|
|
305
319
|
if (!flags.dryRun)
|
|
306
320
|
await signInIfNeeded(ctx, spec.heedFile.heedvane.url);
|
|
307
321
|
out("");
|
|
308
|
-
out(" Next: open a new agent session
|
|
322
|
+
out(" Next: open a new agent session; Heedvane joins it at session start.");
|
|
323
|
+
if (spec.heedFile.install.mcp && heedvaneConnector(spec.heedFile) === undefined)
|
|
324
|
+
out(` ${MCP_NEEDS_ORG}`);
|
|
309
325
|
out(" Re-run `npx heedvane doctor` after any heed.yaml edit.");
|
|
310
326
|
const failed = applied.some(appliedFailed);
|
|
311
327
|
if (flags.json)
|
|
@@ -318,7 +334,13 @@ function writeControlFile({ home, out }, { heedFile, existed, text }) {
|
|
|
318
334
|
return;
|
|
319
335
|
mkdirSync(join(home, ".heed"), { recursive: true, mode: 0o700 });
|
|
320
336
|
writeFileSync(join(home, ".heed", "heed.yaml"), text ?? DEFAULT_HEED_YAML_TEMPLATE, { mode: 0o600 });
|
|
321
|
-
|
|
337
|
+
if (!existed)
|
|
338
|
+
return out(` ✓ Wrote ~/.heed/heed.yaml (your control file — edit any time)`);
|
|
339
|
+
const { url, org, dataMode } = heedFile.heedvane;
|
|
340
|
+
for (const [name, value] of [["url", url], ["org", org], ["dataMode", dataMode]]) {
|
|
341
|
+
if (value !== undefined)
|
|
342
|
+
out(` ✓ Set heedvane.${name} in ~/.heed/heed.yaml to ${value}`);
|
|
343
|
+
}
|
|
322
344
|
}
|
|
323
345
|
/** Without a sign-in to `url` (and to `--org`, when named), sign in now when a person is at the terminal, else say how. Never fails setup. */
|
|
324
346
|
/** Writes heed.yaml, applies the plan, issues the listener endpoint and records the machine state. Prints only what applying prints. */
|
|
@@ -331,9 +353,9 @@ async function installPlan(ctx, spec) {
|
|
|
331
353
|
const dispatcher = {
|
|
332
354
|
denySkillsWhenUnchecked: heedFile.heedvane.skills.enforce && heedFile.heedvane.skills.whenUnknown === "deny",
|
|
333
355
|
gating: heedFile.gating.mode,
|
|
334
|
-
nodeDir:
|
|
356
|
+
nodeDir: installingNodeDir(),
|
|
335
357
|
};
|
|
336
|
-
const applied = await applyPlan(actions, { home, env, out, dispatcher });
|
|
358
|
+
const applied = await applyPlan(actions, { home, env, out, dispatcher, connector: heedvaneConnector(heedFile) });
|
|
337
359
|
// The listener port and token every dispatcher reads; kept once issued.
|
|
338
360
|
await ensureEndpoint(join(home, ".heed"));
|
|
339
361
|
// Machine state: CLI-owned; preserve an existing endpoint.
|
|
@@ -421,7 +443,7 @@ async function cmdInstall(ctx) {
|
|
|
421
443
|
const spec = { heedFile: loadHeedFile(text), agents: choices.agents === "auto" ? [] : choices.agents, existed: base.existed, text };
|
|
422
444
|
const names = new Map(detectionRows(ctx.home, ctx.env).map((row) => [row.client.id, row.client.displayName]));
|
|
423
445
|
const planned = agentLines(planFor({ home: ctx.home, env: ctx.env, ...spec }), names);
|
|
424
|
-
if (!(await confirmInstall(planned, prompts)))
|
|
446
|
+
if (!(await confirmInstall(planned, installDetails(spec.heedFile, heedvaneConnector(spec.heedFile)), prompts)))
|
|
425
447
|
return installCancelled(prompts);
|
|
426
448
|
const notes = [];
|
|
427
449
|
const { actions, applied } = await installWithSpinner(planned.length, () => installPlan({ ...ctx, out: (line) => notes.push(line) }, spec), prompts);
|
|
@@ -429,29 +451,29 @@ async function cmdInstall(ctx) {
|
|
|
429
451
|
showInstalled(agentLines(actions, names, applied), prompts);
|
|
430
452
|
const listening = await listenerStep(() => ctx.startListener(ctx.home), listenerLogPath(join(ctx.home, ".heed")), prompts);
|
|
431
453
|
await signInIfNeeded({ ...ctx, out: promptLine(prompts) }, spec.heedFile.heedvane.url);
|
|
432
|
-
installFinished(nextSteps(listening, spec.heedFile
|
|
454
|
+
installFinished(nextSteps(listening, spec.heedFile), prompts);
|
|
433
455
|
return applied.some(appliedFailed) ? 1 : 0;
|
|
434
456
|
}
|
|
435
|
-
/** heed.yaml's current text (or the template) with `--server` applied, parsed; a message when
|
|
436
|
-
function installerBase(
|
|
437
|
-
const loaded = loadOrTemplate(home);
|
|
457
|
+
/** heed.yaml's current text (or the template) with `--server`, `--org` and `--mode` applied, parsed; a message when it cannot be used. */
|
|
458
|
+
function installerBase(ctx) {
|
|
459
|
+
const loaded = loadOrTemplate(ctx.home);
|
|
438
460
|
if (loaded.error)
|
|
439
461
|
return `~/.heed/heed.yaml does not parse: ${loaded.error}`;
|
|
440
462
|
try {
|
|
441
|
-
|
|
442
|
-
const text = server === undefined ? current : withServerUrl(current, server);
|
|
443
|
-
return { heedFile: loadHeedFile(text), existed: loaded.existed, text };
|
|
463
|
+
return withFlags(ctx.home, loaded, ctx) ?? { heedFile: loaded.heedFile, existed: loaded.existed, text: heedText(ctx.home, loaded.existed) };
|
|
444
464
|
}
|
|
445
465
|
catch (err) {
|
|
446
|
-
return
|
|
466
|
+
return errorMessage(err);
|
|
447
467
|
}
|
|
448
468
|
}
|
|
449
469
|
function agentOptions({ home, env }) {
|
|
450
470
|
return detectionRows(home, env).map(({ client, detected }) => ({ id: client.id, name: client.displayName, detected, capability: capabilitySummary(client) }));
|
|
451
471
|
}
|
|
452
|
-
function nextSteps(listening,
|
|
472
|
+
function nextSteps(listening, heedFile) {
|
|
473
|
+
const server = heedFile.heedvane.url;
|
|
453
474
|
return [
|
|
454
475
|
"Open a new agent session: Heedvane joins it at session start.",
|
|
476
|
+
...(heedFile.install.mcp && heedvaneConnector(heedFile) === undefined ? [MCP_NEEDS_ORG] : []),
|
|
455
477
|
`Watch your sessions: ${server}`,
|
|
456
478
|
...(listening ? [] : ["Heedvane starts by itself with your next agent session. To see why it did not start now: npx heedvane listener start"]),
|
|
457
479
|
"Check the install: npx heedvane doctor",
|
|
@@ -467,24 +489,38 @@ async function applyHermesPlugin(client, ctx) {
|
|
|
467
489
|
}
|
|
468
490
|
return { clientId: client.id, kind: "hooks", status: status === "installed" ? "create" : "unchanged", path };
|
|
469
491
|
}
|
|
492
|
+
/** Folds removals into an apply result: a failed removal becomes the result, any other makes an unchanged one an update. */
|
|
493
|
+
function withRemovals(items, result) {
|
|
494
|
+
let worst = result;
|
|
495
|
+
for (const item of items) {
|
|
496
|
+
if (item.status === "failed")
|
|
497
|
+
return { status: "failed", path: item.path, reason: item.detail };
|
|
498
|
+
if (worst.status === "unchanged")
|
|
499
|
+
worst = { status: "update", path: item.path };
|
|
500
|
+
}
|
|
501
|
+
return worst;
|
|
502
|
+
}
|
|
470
503
|
/** Drop our groups under event keys this client no longer installs; a failed removal becomes the result. */
|
|
471
504
|
async function withoutStaleHookGroups(client, writes, result) {
|
|
472
505
|
let worst = result;
|
|
473
506
|
for (const { target, containerKey } of writes) {
|
|
474
507
|
if (containerKey !== "hooks")
|
|
475
508
|
continue;
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
if (worst.status === "unchanged")
|
|
480
|
-
worst = { status: "update", path: item.path };
|
|
481
|
-
}
|
|
509
|
+
worst = withRemovals(await removeStaleHookGroups(client, target), worst);
|
|
510
|
+
if (worst.status === "failed")
|
|
511
|
+
return worst;
|
|
482
512
|
}
|
|
483
513
|
return worst;
|
|
484
514
|
}
|
|
515
|
+
/** After the writes: our hook groups under events no longer installed, and what an earlier version registered as MCP (the local helper, the connection's earlier name), with or without a connection to write. */
|
|
516
|
+
async function withoutLeftovers(kind, client, done) {
|
|
517
|
+
if (kind === "hooks")
|
|
518
|
+
return withoutStaleHookGroups(client, done.writes, done.result);
|
|
519
|
+
return kind === "mcp" ? withRemovals(await removeLegacyMcpEntries(client), done.result) : done.result;
|
|
520
|
+
}
|
|
485
521
|
/** Patch every desired entry into the client's config files, then refresh the dispatcher script. */
|
|
486
522
|
async function applyConfigEntries(action, client, ctx) {
|
|
487
|
-
const desireds = desiredEntriesFor(client, action.kind === "hooks" ? "hooks" : "mcp", { home: ctx.home, env: ctx.env });
|
|
523
|
+
const desireds = desiredEntriesFor(client, action.kind === "hooks" ? "hooks" : "mcp", { home: ctx.home, env: ctx.env, connector: ctx.connector });
|
|
488
524
|
// One write per file and container: every hook event lands (and is backed up) together.
|
|
489
525
|
const writes = new Map();
|
|
490
526
|
for (const d of desireds) {
|
|
@@ -503,9 +539,8 @@ async function applyConfigEntries(action, client, ctx) {
|
|
|
503
539
|
if (res.status !== "unchanged")
|
|
504
540
|
worst = res;
|
|
505
541
|
}
|
|
506
|
-
if (
|
|
507
|
-
worst = await
|
|
508
|
-
}
|
|
542
|
+
if (worst.status !== "failed" && worst.status !== "invalid")
|
|
543
|
+
worst = await withoutLeftovers(action.kind, client, { writes: writes.values(), result: worst });
|
|
509
544
|
// The dispatcher script the snippet delegates to.
|
|
510
545
|
await writeDispatcherScript(ctx.home, client.id, ctx.dispatcher);
|
|
511
546
|
return {
|
|
@@ -676,12 +711,15 @@ function backendCommandContext(ctx) {
|
|
|
676
711
|
* The command that opens `url` in the default browser. Windows goes through rundll32, not `cmd /c start`:
|
|
677
712
|
* cmd would cut the sign-in URL at its first `&`.
|
|
678
713
|
*/
|
|
679
|
-
|
|
714
|
+
/** 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. */
|
|
715
|
+
export function browserOpener(platform, url, browser) {
|
|
716
|
+
if (browser)
|
|
717
|
+
return [browser, [url]];
|
|
680
718
|
const openers = { darwin: ["open", [url]], win32: ["rundll32", ["url.dll,FileProtocolHandler", url]] };
|
|
681
719
|
return openers[platform] ?? ["xdg-open", [url]];
|
|
682
720
|
}
|
|
683
|
-
function openSystemBrowser(url) {
|
|
684
|
-
const [command, args] = browserOpener(process.platform, url);
|
|
721
|
+
function openSystemBrowser(url, browser) {
|
|
722
|
+
const [command, args] = browserOpener(process.platform, url, browser);
|
|
685
723
|
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
686
724
|
child.on("error", () => undefined); // no browser opener: the printed URL is the way in
|
|
687
725
|
child.unref();
|
|
@@ -34,7 +34,7 @@ export interface ResolveContext {
|
|
|
34
34
|
/** Root a client's directory or detection marker is resolved against. */
|
|
35
35
|
export type ClientPathBase = "home" | "xdg-config" | "platform-config";
|
|
36
36
|
/** Base a writable config target is resolved against. */
|
|
37
|
-
export type PathBase = "client" | "home" | "platform-config";
|
|
37
|
+
export type PathBase = "client" | "home" | "platform-config" | "xdg-config";
|
|
38
38
|
/** One writable config file for a client. */
|
|
39
39
|
export interface ConfigTarget {
|
|
40
40
|
/** Path segments relative to {@link PathBase}. */
|
|
@@ -155,13 +155,13 @@ const SUPPORTED_CLIENTS = [
|
|
|
155
155
|
base: "client",
|
|
156
156
|
format: "json",
|
|
157
157
|
purpose: "mcp",
|
|
158
|
-
limitation: "Warp has no native hook events (hooks unavailable); ~/.warp/.mcp.json is
|
|
158
|
+
limitation: "Warp has no native hook events (hooks unavailable); ~/.warp/.mcp.json is Warp's global MCP file, and a remote server signs in through the browser when it first starts.",
|
|
159
159
|
},
|
|
160
160
|
],
|
|
161
161
|
capabilities: ["mcp", "skill"],
|
|
162
162
|
events: [],
|
|
163
163
|
mcpServersKey: "mcpServers",
|
|
164
|
-
limitation: "Warp exposes no native hook events
|
|
164
|
+
limitation: "Warp exposes no native hook events, so Heedvane adds only its MCP connection and the skill there.",
|
|
165
165
|
},
|
|
166
166
|
{
|
|
167
167
|
id: "opencode",
|
|
@@ -227,11 +227,12 @@ const SUPPORTED_CLIENTS = [
|
|
|
227
227
|
homeOverrideEnv: "KIMI_CODE_HOME",
|
|
228
228
|
configs: [
|
|
229
229
|
{ segments: ["config.toml"], base: "client", format: "toml", purpose: "hooks" },
|
|
230
|
-
|
|
230
|
+
// MCP servers live in mcp.json (Kimi Code's MCP docs); config.toml is listed with hooks, so cleanup still finds the helper 0.3.0 and earlier wrote there.
|
|
231
|
+
{ segments: ["mcp.json"], base: "client", format: "json", purpose: "mcp" },
|
|
231
232
|
],
|
|
232
233
|
capabilities: ["hooks", "mcp", "skill"],
|
|
233
234
|
events: KIMI_EVENTS,
|
|
234
|
-
mcpServersKey: "
|
|
235
|
+
mcpServersKey: "mcpServers",
|
|
235
236
|
},
|
|
236
237
|
{
|
|
237
238
|
id: "droid",
|
|
@@ -253,6 +254,8 @@ const SUPPORTED_CLIENTS = [
|
|
|
253
254
|
base: "home",
|
|
254
255
|
configs: [
|
|
255
256
|
{ segments: [".gemini", "config", "hooks.json"], base: "home", format: "json", purpose: "hooks" },
|
|
257
|
+
// Antigravity's documented global MCP file; the second is where 0.3.0 and earlier wrote, kept only so cleanup and uninstall reach it.
|
|
258
|
+
{ segments: [".gemini", "config", "mcp_config.json"], base: "home", format: "json", purpose: "mcp" },
|
|
256
259
|
{ segments: ["mcp_config.json"], base: "client", format: "json", purpose: "mcp" },
|
|
257
260
|
],
|
|
258
261
|
capabilities: ["hooks", "mcp", "skill"],
|
|
@@ -266,6 +269,8 @@ const SUPPORTED_CLIENTS = [
|
|
|
266
269
|
base: "home",
|
|
267
270
|
homeOverrideEnv: "COPILOT_HOME",
|
|
268
271
|
configs: [
|
|
272
|
+
// Copilot CLI's documented MCP file; the second is where 0.3.0 and earlier wrote, kept only so cleanup and uninstall reach it.
|
|
273
|
+
{ segments: ["mcp-config.json"], base: "client", format: "json", purpose: "mcp" },
|
|
269
274
|
{ segments: ["mcp.json"], base: "client", format: "json", purpose: "mcp" },
|
|
270
275
|
],
|
|
271
276
|
capabilities: ["mcp", "skill"],
|
|
@@ -276,8 +281,12 @@ const SUPPORTED_CLIENTS = [
|
|
|
276
281
|
id: "windsurf",
|
|
277
282
|
displayName: "Windsurf",
|
|
278
283
|
markerSegments: [".codeium", "windsurf"],
|
|
284
|
+
// Devin Desktop's folder: $XDG_CONFIG_HOME/devin, %APPDATA%\devin on Windows.
|
|
285
|
+
altMarkers: [{ segments: ["devin"], base: "xdg-config" }],
|
|
279
286
|
base: "home",
|
|
280
287
|
configs: [
|
|
288
|
+
// The file Windsurf's docs (now Devin Desktop's) name; the second is where 0.3.1 and earlier wrote, kept only so cleanup and uninstall reach it.
|
|
289
|
+
{ segments: ["devin", "mcp_config.json"], base: "xdg-config", format: "json", purpose: "mcp" },
|
|
281
290
|
{ segments: ["mcp_config.json"], base: "client", format: "json", purpose: "mcp" },
|
|
282
291
|
],
|
|
283
292
|
capabilities: ["mcp"],
|
|
@@ -324,14 +333,16 @@ function platformConfigDir(def, ctx, p) {
|
|
|
324
333
|
switch (ctx.platform) {
|
|
325
334
|
case "darwin":
|
|
326
335
|
return p.join(ctx.home, "Library", "Application Support", appName);
|
|
327
|
-
case "win32":
|
|
328
|
-
|
|
329
|
-
return p.join(absoluteOrHome(appData, ctx, p), appName);
|
|
330
|
-
}
|
|
336
|
+
case "win32":
|
|
337
|
+
return p.join(appDataRoot(ctx, p), appName);
|
|
331
338
|
default:
|
|
332
339
|
return p.join(xdgConfigRoot(ctx, p), appName);
|
|
333
340
|
}
|
|
334
341
|
}
|
|
342
|
+
/** %APPDATA%, else the default roaming folder under the home. */
|
|
343
|
+
function appDataRoot(ctx, p) {
|
|
344
|
+
return absoluteOrHome(ctx.env["APPDATA"] ?? p.join(ctx.home, "AppData", "Roaming"), ctx, p);
|
|
345
|
+
}
|
|
335
346
|
/** Treat a relative env override as home-relative so resolution never throws. */
|
|
336
347
|
function absoluteOrHome(value, ctx, p) {
|
|
337
348
|
return p.isAbsolute(value) ? value : p.join(ctx.home, value);
|
|
@@ -357,7 +368,8 @@ function resolveMarkerBase(target, ctx, p) {
|
|
|
357
368
|
case "home":
|
|
358
369
|
return p.join(ctx.home, ...segments);
|
|
359
370
|
case "xdg-config":
|
|
360
|
-
|
|
371
|
+
// As for config targets: %APPDATA% on Windows (Devin Desktop's folder for Windsurf).
|
|
372
|
+
return p.join(ctx.platform === "win32" ? appDataRoot(ctx, p) : xdgConfigRoot(ctx, p), ...segments);
|
|
361
373
|
case "platform-config":
|
|
362
374
|
return p.join(platformConfigDir(def, ctx, p), ...segments);
|
|
363
375
|
}
|
|
@@ -388,6 +400,9 @@ function resolveConfigTarget(def, target, resolve) {
|
|
|
388
400
|
case "platform-config":
|
|
389
401
|
path = p.join(platformConfigDir(def, ctx, p), ...target.segments);
|
|
390
402
|
break;
|
|
403
|
+
case "xdg-config":
|
|
404
|
+
path = p.join(ctx.platform === "win32" ? appDataRoot(ctx, p) : xdgConfigRoot(ctx, p), ...target.segments);
|
|
405
|
+
break;
|
|
391
406
|
}
|
|
392
407
|
return target.limitation === undefined
|
|
393
408
|
? { path, format: target.format, purpose: target.purpose }
|
package/dist/commands/memory.js
CHANGED
|
@@ -37,7 +37,12 @@ async function ask(ctx, request) {
|
|
|
37
37
|
return 1;
|
|
38
38
|
}
|
|
39
39
|
const root = join(ctx.home, ".heed");
|
|
40
|
-
const
|
|
40
|
+
const config = heedvaneConfigAt(root);
|
|
41
|
+
if (config.dataMode === "metrics-only") {
|
|
42
|
+
ctx.out(" ✗ team memory is off on this machine: heed.yaml heedvane.dataMode is metrics-only, so no question or repository leaves it");
|
|
43
|
+
return 1;
|
|
44
|
+
}
|
|
45
|
+
const backend = await backendAt(root, config.url);
|
|
41
46
|
if (backend === null) {
|
|
42
47
|
ctx.out(" ✗ not signed in — run: npx heedvane login (or, headless: npx heedvane auth org < key-file)");
|
|
43
48
|
return 1;
|
package/dist/doctor.js
CHANGED
|
@@ -8,7 +8,8 @@ import { join } from "node:path";
|
|
|
8
8
|
import { supportedClients } from "./clients/definitions.js";
|
|
9
9
|
import { entryValue, parseDoc, readEntry, valuesEqual } from "./config-doc.js";
|
|
10
10
|
import { loadHeedFileAt } from "./heed-file.js";
|
|
11
|
-
import { desiredEntriesFor, hooksDeferredTo, isClientDetected } from "./plan.js";
|
|
11
|
+
import { desiredEntriesFor, heedvaneConnector, hooksDeferredTo, isClientDetected, legacyMcpEntries } from "./plan.js";
|
|
12
|
+
import { MCP_NEEDS_ORG, connectorEntry } from "./mcp.js";
|
|
12
13
|
import { readInstalledSkillDigest } from "./skill/bundle.js";
|
|
13
14
|
import { HeedStateCorruptError, readState } from "./state.js";
|
|
14
15
|
import { readApprovedSkills } from "./skills/approved.js";
|
|
@@ -109,13 +110,14 @@ void checkState;
|
|
|
109
110
|
function scopeSet(clients) {
|
|
110
111
|
return clients && clients.length > 0 ? new Set(clients) : null;
|
|
111
112
|
}
|
|
112
|
-
/** The pure-planner desired-entry lookup, resolving each client id against home/env. */
|
|
113
|
-
function defaultDesiredLookup(home, env) {
|
|
113
|
+
/** The pure-planner desired-entry lookup, resolving each client id against home/env and heed.yaml's organization. */
|
|
114
|
+
function defaultDesiredLookup(home, env, heedFile) {
|
|
115
|
+
const connector = heedFile === undefined ? undefined : heedvaneConnector(heedFile);
|
|
114
116
|
return (clientId, capability) => {
|
|
115
117
|
const client = supportedClients({ home, platform: process.platform, env }).find((c) => c.id === clientId);
|
|
116
118
|
if (!client)
|
|
117
119
|
return undefined;
|
|
118
|
-
return desiredEntriesFor(client, capability, { home, env }).map((d) => ({
|
|
120
|
+
return desiredEntriesFor(client, capability, { home, env, connector }).map((d) => ({
|
|
119
121
|
path: d.path,
|
|
120
122
|
format: d.format,
|
|
121
123
|
containerKey: d.containerKey,
|
|
@@ -160,13 +162,27 @@ function collectCapabilityStates(client, ctx) {
|
|
|
160
162
|
const supported = client.capabilities.includes(capability) || (capability === "hooks" && client.id === "hermes" && client.capabilities.includes("plugin-install"));
|
|
161
163
|
if (!supported || !heedFileWants(client, capability, ctx))
|
|
162
164
|
continue;
|
|
163
|
-
const desireds = ctx.desired(client.id, capability);
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
165
|
+
const desireds = ctx.desired(client.id, capability) ?? [];
|
|
166
|
+
const result = capability === "mcp" ? mcpConnectionState(client, desireds, ctx.home) : hooksState(desireds, ctx.home);
|
|
167
|
+
if (result !== undefined)
|
|
168
|
+
states.push({ capability, result });
|
|
167
169
|
}
|
|
168
170
|
return states;
|
|
169
171
|
}
|
|
172
|
+
/** The hooks' registration verdict, when this client has hooks to register. */
|
|
173
|
+
function hooksState(desireds, home) {
|
|
174
|
+
return desireds.length > 0 ? registrationState(desireds, home) : undefined;
|
|
175
|
+
}
|
|
176
|
+
/** Heedvane's MCP connection: stale while an earlier version's entry is left, missing without an organization, else the registration verdict. */
|
|
177
|
+
function mcpConnectionState(client, desireds, home) {
|
|
178
|
+
const legacy = legacyMcpEntries(client);
|
|
179
|
+
if (legacy !== undefined)
|
|
180
|
+
return { state: "stale", detail: `${legacy.names.join(", ")} in ${legacy.path} is an earlier version's entry; npx heedvane connect replaces it` };
|
|
181
|
+
if (desireds.length > 0)
|
|
182
|
+
return registrationState(desireds, home);
|
|
183
|
+
// Agents the connection is not registered in have nothing to check.
|
|
184
|
+
return connectorEntry(client.id, "") === undefined ? undefined : { state: "missing", detail: MCP_NEEDS_ORG };
|
|
185
|
+
}
|
|
170
186
|
/** One detected client's registration check (aligned / stale / missing with per-entry detail). */
|
|
171
187
|
function pushRegistrationChecks(checks, client, ctx) {
|
|
172
188
|
const detected = isClientDetected(client);
|
|
@@ -233,7 +249,7 @@ export async function runDoctor(input) {
|
|
|
233
249
|
const root = join(home, ".heed");
|
|
234
250
|
const state = await readStateOrReport(root, checks);
|
|
235
251
|
// 4. Desired-entry lookup: injected, falling back to the pure planner.
|
|
236
|
-
const desired = input.desired ?? defaultDesiredLookup(home, env);
|
|
252
|
+
const desired = input.desired ?? defaultDesiredLookup(home, env, heedFile);
|
|
237
253
|
// 5. Per detected client: registration state aligned | stale | missing.
|
|
238
254
|
const scope = scopeSet(input.clients);
|
|
239
255
|
const allClients = supportedClients({ home, platform: process.platform, env });
|
package/dist/heed-file.d.ts
CHANGED
|
@@ -27,9 +27,19 @@ export interface HeedSkillsConfig {
|
|
|
27
27
|
/** No synced approved list, or no listener to check it: let the skill run with a warning, or deny it. */
|
|
28
28
|
whenUnknown: "allow" | "deny";
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* What leaves this machine (the installer and data modes proposal, "Proposed data modes"). `look-but-dont-keep`:
|
|
32
|
+
* Heedvane's guardrail checks read the prompt or tool call and keep only the verdict. `metrics-only`: only counts and
|
|
33
|
+
* names leave; the organization's fixed rules still run here, Heedvane's own checks and team memory do not.
|
|
34
|
+
*/
|
|
35
|
+
export declare const DATA_MODES: readonly ["look-but-dont-keep", "metrics-only"];
|
|
36
|
+
export type DataMode = (typeof DATA_MODES)[number];
|
|
30
37
|
/** The Heedvane backend the listener asks for steering, approved skills and memory. */
|
|
31
38
|
export interface HeedvaneConfig {
|
|
32
39
|
url: string;
|
|
40
|
+
/** The organization `connect --org` named: Heedvane's MCP connection URL carries it. */
|
|
41
|
+
org?: string;
|
|
42
|
+
dataMode: DataMode;
|
|
33
43
|
steering: boolean;
|
|
34
44
|
memory: boolean;
|
|
35
45
|
skills: HeedSkillsConfig;
|
|
@@ -47,7 +57,7 @@ export interface HeedFile {
|
|
|
47
57
|
* The commented control file written by `npx heedvane init`.
|
|
48
58
|
* Kept loadable: loadHeedFile(DEFAULT_HEED_YAML_TEMPLATE) yields every default.
|
|
49
59
|
*/
|
|
50
|
-
export declare const DEFAULT_HEED_YAML_TEMPLATE = "# ~/.heed/heed.yaml \u2014 Heedvane agent hooks control file (v1)\nschemaVersion: v1\n\n# Where managed scripts and spool live. Default below; one install per machine.\nroot: ~/.heed\n\n# The hook listener (Orca's pattern): loopback HTTP, token-authed, per-agent routes.\nlistener:\n port: 0 # 0 = pick a free port at first start, persist to endpoint.env\n spool: true # spool to disk when listener is unreachable (5 MB cap, 7-day TTL)\n\n# Which agents to manage. `auto` = every detected agent on the machine.\nagents: auto # or an explicit list: [claude, codex, warp]\n\n# Which integrations to install per agent where supported.\ninstall:\n hooks: true # native hook config injection (skipped where the agent has none)\n skill: true # heedvane-hooks skill bundle -> ~/.agents/skills/heedvane-hooks/\n mcp: true # heedvane MCP server registration (universal fallback, incl. Warp)\n\n# Session gating (Orca's env-gated dispatcher pattern).\ngating:\n mode: always # always = every session; pane = only sessions launched with HEED_SESSION env\n\n# The Heedvane backend (credentials: npx heedvane auth, stored in ~/.heed/credentials.json).\nheedvane:\n url: https://app.heedvane.com\n steering: true # ask the organization's steering rules before tool calls, prompts and stops\n memory: true # add your private memories (~/.heed/memory, local only) at session start\n skills:\n enforce: true # deny skills that are not on the organization's approved list\n strict: false # also require the local SKILL.md to match the approved revision\n whenUnknown: allow # no synced list (run npx heedvane skills sync) or listener down: allow or deny\n";
|
|
60
|
+
export declare const DEFAULT_HEED_YAML_TEMPLATE = "# ~/.heed/heed.yaml \u2014 Heedvane agent hooks control file (v1)\nschemaVersion: v1\n\n# Where managed scripts and spool live. Default below; one install per machine.\nroot: ~/.heed\n\n# The hook listener (Orca's pattern): loopback HTTP, token-authed, per-agent routes.\nlistener:\n port: 0 # 0 = pick a free port at first start, persist to endpoint.env\n spool: true # spool to disk when listener is unreachable (5 MB cap, 7-day TTL)\n\n# Which agents to manage. `auto` = every detected agent on the machine.\nagents: auto # or an explicit list: [claude, codex, warp]\n\n# Which integrations to install per agent where supported.\ninstall:\n hooks: true # native hook config injection (skipped where the agent has none)\n skill: true # heedvane-hooks skill bundle -> ~/.agents/skills/heedvane-hooks/\n mcp: true # heedvane MCP server registration (universal fallback, incl. Warp)\n\n# Session gating (Orca's env-gated dispatcher pattern).\ngating:\n mode: always # always = every session; pane = only sessions launched with HEED_SESSION env\n\n# The Heedvane backend (credentials: npx heedvane auth, stored in ~/.heed/credentials.json).\nheedvane:\n url: https://app.heedvane.com\n dataMode: look-but-dont-keep # what leaves this machine: look-but-dont-keep (checks read prompts and tool calls, keep only verdicts) or metrics-only (only counts and names)\n steering: true # ask the organization's steering rules before tool calls, prompts and stops\n memory: true # add your private memories (~/.heed/memory, local only) at session start\n skills:\n enforce: true # deny skills that are not on the organization's approved list\n strict: false # also require the local SKILL.md to match the approved revision\n whenUnknown: allow # no synced list (run npx heedvane skills sync) or listener down: allow or deny\n";
|
|
51
61
|
/**
|
|
52
62
|
* Parse and validate a heed.yaml document, returning the fully normalized file.
|
|
53
63
|
* Throws {@link HeedFileError} (message prefixed "invalid heed.yaml:") on any problem:
|
|
@@ -55,6 +65,8 @@ export declare const DEFAULT_HEED_YAML_TEMPLATE = "# ~/.heed/heed.yaml \u2014 He
|
|
|
55
65
|
* (top-level or nested), or wrong-typed values.
|
|
56
66
|
*/
|
|
57
67
|
export declare function loadHeedFile(text: string): HeedFile;
|
|
68
|
+
/** `text` with each `[path, value]` written in place; the file's comments stay. Validate with {@link loadHeedFile}. */
|
|
69
|
+
export declare function withSettings(text: string, settings: ReadonlyArray<readonly [string[], unknown]>): string;
|
|
58
70
|
/** `text` with `heedvane.url` set to `url` (`connect --server`); the file's comments stay. Validate with {@link loadHeedFile}. */
|
|
59
71
|
export declare function withServerUrl(text: string, url: string): string;
|
|
60
72
|
/** What the interactive installer asks: which agents, which integrations, and which Heedvane features. */
|
|
@@ -62,6 +74,7 @@ export interface InstallChoices {
|
|
|
62
74
|
/** Chosen agent ids, or "auto" for every detected agent. */
|
|
63
75
|
agents: "auto" | string[];
|
|
64
76
|
install: HeedInstallConfig;
|
|
77
|
+
dataMode: DataMode;
|
|
65
78
|
steering: boolean;
|
|
66
79
|
memory: boolean;
|
|
67
80
|
skillChecks: boolean;
|