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.
- package/README.md +60 -13
- package/dist/backend/credentials.d.ts +2 -0
- package/dist/backend/credentials.js +13 -0
- package/dist/backend/repo-remote.js +2 -1
- package/dist/backend/sign-in-change.js +3 -2
- package/dist/cli.d.ts +4 -2
- package/dist/cli.js +389 -144
- package/dist/clients/definitions.d.ts +8 -4
- package/dist/clients/definitions.js +48 -13
- package/dist/clients/hermes-plugin.d.ts +5 -20
- package/dist/clients/hermes-plugin.js +97 -71
- package/dist/clients/hook-files.d.ts +22 -0
- package/dist/clients/hook-files.js +39 -0
- package/dist/clients/pi-extension.d.ts +9 -0
- package/dist/clients/pi-extension.js +41 -0
- package/dist/commands/heedvane.js +18 -3
- package/dist/commands/mcp-stub.d.ts +1 -0
- package/dist/commands/mcp-stub.js +44 -0
- package/dist/commands/memory.js +6 -1
- package/dist/dispatchers/posix.d.ts +16 -19
- package/dist/dispatchers/posix.js +71 -91
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +113 -29
- package/dist/guard.d.ts +73 -0
- package/dist/guard.js +302 -0
- package/dist/heed-file.d.ts +19 -3
- package/dist/heed-file.js +46 -23
- package/dist/hook-fallback.d.ts +73 -0
- package/dist/hook-fallback.js +61 -0
- package/dist/installer.d.ts +13 -6
- package/dist/installer.js +40 -13
- package/dist/listener/background.d.ts +48 -4
- package/dist/listener/background.js +153 -19
- package/dist/listener/endpoint.d.ts +26 -3
- package/dist/listener/endpoint.js +55 -10
- package/dist/listener/forward.d.ts +7 -1
- package/dist/listener/forward.js +40 -12
- package/dist/listener/serve.d.ts +37 -0
- package/dist/listener/serve.js +196 -32
- package/dist/listener/server.d.ts +9 -0
- package/dist/listener/server.js +59 -22
- package/dist/listener/spool.d.ts +2 -17
- package/dist/listener/spool.js +6 -3
- package/dist/mcp.d.ts +38 -48
- package/dist/mcp.js +196 -63
- package/dist/plan.d.ts +47 -8
- package/dist/plan.js +164 -52
- package/dist/render.js +8 -8
- package/dist/reply/harness-output.d.ts +9 -0
- package/dist/reply/harness-output.js +13 -0
- package/dist/reply/reply.js +2 -1
- package/dist/skill/content.d.ts +2 -2
- package/dist/skill/content.js +11 -8
- package/dist/status.d.ts +1 -0
- package/dist/status.js +29 -0
- package/dist/steering/rules.js +4 -2
- package/dist/uninstall.d.ts +7 -0
- package/dist/uninstall.js +72 -71
- package/package.json +10 -3
- package/skills/heedvane-hooks/SKILL.md +6 -6
- package/skills/heedvane-hooks/references/events.md +5 -2
- package/dist/dispatchers/windows.d.ts +0 -15
- package/dist/dispatchers/windows.js +0 -50
- package/dist/mcp-server.d.ts +0 -9
- package/dist/mcp-server.js +0 -187
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,13 +65,14 @@ 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;
|
|
58
|
-
/** `text` with `
|
|
59
|
-
export declare function
|
|
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;
|
|
60
70
|
/** What the interactive installer asks: which agents, which integrations, and which Heedvane features. */
|
|
61
71
|
export interface InstallChoices {
|
|
62
72
|
/** Chosen agent ids, or "auto" for every detected agent. */
|
|
63
73
|
agents: "auto" | string[];
|
|
64
74
|
install: HeedInstallConfig;
|
|
75
|
+
dataMode: DataMode;
|
|
65
76
|
steering: boolean;
|
|
66
77
|
memory: boolean;
|
|
67
78
|
skillChecks: boolean;
|
|
@@ -74,5 +85,10 @@ export declare function withInstallChoices(text: string, choices: InstallChoices
|
|
|
74
85
|
* the file cannot be rewritten the key is ignored in memory and the line says that instead.
|
|
75
86
|
*/
|
|
76
87
|
export declare function loadHeedFileAt(path: string, log?: (line: string) => void): HeedFile;
|
|
88
|
+
/** {@link loadHeedFileAt}, with the file's text as it is on disk afterwards: what the settings it holds hash from. */
|
|
89
|
+
export declare function readHeedFileAt(path: string, log?: (line: string) => void): {
|
|
90
|
+
heedFile: HeedFile;
|
|
91
|
+
text: string;
|
|
92
|
+
};
|
|
77
93
|
/** heed.yaml's `heedvane` section under `root`; the template's defaults before init has written one. */
|
|
78
94
|
export declare function heedvaneConfigAt(root: string): HeedvaneConfig;
|
package/dist/heed-file.js
CHANGED
|
@@ -3,6 +3,12 @@ import { join } from "node:path";
|
|
|
3
3
|
import writeFileAtomic from "write-file-atomic";
|
|
4
4
|
import { isScalar, parse, parseDocument } from "yaml";
|
|
5
5
|
import { errorMessage, isRecord } from "./guards.js";
|
|
6
|
+
/**
|
|
7
|
+
* What leaves this machine (the installer and data modes proposal, "Proposed data modes"). `look-but-dont-keep`:
|
|
8
|
+
* Heedvane's guardrail checks read the prompt or tool call and keep only the verdict. `metrics-only`: only counts and
|
|
9
|
+
* names leave; the organization's fixed rules still run here, Heedvane's own checks and team memory do not.
|
|
10
|
+
*/
|
|
11
|
+
export const DATA_MODES = ["look-but-dont-keep", "metrics-only"];
|
|
6
12
|
/** Error raised for any invalid heed.yaml. The message is user-facing: keep it precise. */
|
|
7
13
|
class HeedFileError extends Error {
|
|
8
14
|
constructor(message) {
|
|
@@ -50,6 +56,7 @@ gating:
|
|
|
50
56
|
# The Heedvane backend (credentials: npx heedvane auth, stored in ~/.heed/credentials.json).
|
|
51
57
|
heedvane:
|
|
52
58
|
url: https://app.heedvane.com
|
|
59
|
+
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)
|
|
53
60
|
steering: true # ask the organization's steering rules before tool calls, prompts and stops
|
|
54
61
|
memory: true # add your private memories (~/.heed/memory, local only) at session start
|
|
55
62
|
skills:
|
|
@@ -165,12 +172,29 @@ function validateSkills(raw) {
|
|
|
165
172
|
}
|
|
166
173
|
return out;
|
|
167
174
|
}
|
|
175
|
+
/** `heedvane.org` and `heedvane.dataMode`, when set. */
|
|
176
|
+
function validateOrgAndDataMode(map) {
|
|
177
|
+
const out = {};
|
|
178
|
+
const { org, dataMode } = map;
|
|
179
|
+
if (org !== undefined) {
|
|
180
|
+
if (typeof org !== "string" || org.trim() === "")
|
|
181
|
+
throw new HeedFileError(`"heedvane.org" must be a non-empty string, got ${describeType(org)}`);
|
|
182
|
+
out.org = org.trim();
|
|
183
|
+
}
|
|
184
|
+
if (dataMode !== undefined) {
|
|
185
|
+
const known = DATA_MODES.find((mode) => mode === dataMode);
|
|
186
|
+
if (known === undefined)
|
|
187
|
+
throw new HeedFileError(`"heedvane.dataMode" must be one of ${DATA_MODES.join(", ")}, got ${JSON.stringify(dataMode)}`);
|
|
188
|
+
out.dataMode = known;
|
|
189
|
+
}
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
168
192
|
function validateHeedvane(raw) {
|
|
169
|
-
const out = { url: "https://app.heedvane.com", steering: true, memory: true, skills: validateSkills(undefined) };
|
|
193
|
+
const out = { url: "https://app.heedvane.com", dataMode: "look-but-dont-keep", steering: true, memory: true, skills: validateSkills(undefined) };
|
|
170
194
|
if (raw === undefined)
|
|
171
195
|
return out;
|
|
172
196
|
const map = expectMapping(raw, "heedvane");
|
|
173
|
-
rejectUnknownKeys(map, ["url", "steering", "memory", "skills"], "heedvane");
|
|
197
|
+
rejectUnknownKeys(map, ["url", "org", "dataMode", "steering", "memory", "skills"], "heedvane");
|
|
174
198
|
const url = map["url"];
|
|
175
199
|
if (url !== undefined) {
|
|
176
200
|
if (typeof url !== "string" || !URL.canParse(url) || !/^https?:$/.test(new URL(url).protocol)) {
|
|
@@ -178,6 +202,7 @@ function validateHeedvane(raw) {
|
|
|
178
202
|
}
|
|
179
203
|
out.url = url.replace(/\/+$/, "");
|
|
180
204
|
}
|
|
205
|
+
Object.assign(out, validateOrgAndDataMode(map));
|
|
181
206
|
if (map["steering"] !== undefined)
|
|
182
207
|
out.steering = expectBool(map["steering"], "heedvane.steering");
|
|
183
208
|
if (map["memory"] !== undefined)
|
|
@@ -225,36 +250,30 @@ export function loadHeedFile(text) {
|
|
|
225
250
|
heedvane: validateHeedvane(raw["heedvane"]),
|
|
226
251
|
};
|
|
227
252
|
}
|
|
228
|
-
/** `text` with `
|
|
229
|
-
export function
|
|
253
|
+
/** `text` with each `[path, value]` written in place; the file's comments stay. Validate with {@link loadHeedFile}. */
|
|
254
|
+
export function withSettings(text, settings) {
|
|
230
255
|
const document = parseDocument(text);
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
current
|
|
234
|
-
|
|
235
|
-
|
|
256
|
+
for (const [path, value] of settings) {
|
|
257
|
+
const current = document.getIn(path, true);
|
|
258
|
+
if (isScalar(current) && typeof value !== "object")
|
|
259
|
+
current.value = value;
|
|
260
|
+
else
|
|
261
|
+
document.setIn(path, value);
|
|
262
|
+
}
|
|
236
263
|
return String(document);
|
|
237
264
|
}
|
|
238
265
|
/** `text` with the installer's choices written into the fields that already hold them; the file's comments stay. */
|
|
239
266
|
export function withInstallChoices(text, choices) {
|
|
240
|
-
|
|
241
|
-
const fields = [
|
|
267
|
+
return withSettings(text, [
|
|
242
268
|
[["agents"], choices.agents],
|
|
243
269
|
[["install", "hooks"], choices.install.hooks],
|
|
244
270
|
[["install", "skill"], choices.install.skill],
|
|
245
271
|
[["install", "mcp"], choices.install.mcp],
|
|
272
|
+
[["heedvane", "dataMode"], choices.dataMode],
|
|
246
273
|
[["heedvane", "steering"], choices.steering],
|
|
247
274
|
[["heedvane", "memory"], choices.memory],
|
|
248
275
|
[["heedvane", "skills", "enforce"], choices.skillChecks],
|
|
249
|
-
];
|
|
250
|
-
for (const [path, value] of fields) {
|
|
251
|
-
const current = document.getIn(path, true);
|
|
252
|
-
if (isScalar(current) && typeof value !== "object")
|
|
253
|
-
current.value = value;
|
|
254
|
-
else
|
|
255
|
-
document.setIn(path, value);
|
|
256
|
-
}
|
|
257
|
-
return String(document);
|
|
276
|
+
]);
|
|
258
277
|
}
|
|
259
278
|
/** Keys earlier versions wrote into heed.yaml that nothing reads any more (policies are assigned on the server). */
|
|
260
279
|
const RETIRED_KEYS = [["heedvane", "steeringTimeoutMs"], ["policy"]];
|
|
@@ -265,22 +284,26 @@ const toStderr = (line) => void process.stderr.write(`${line}\n`);
|
|
|
265
284
|
* the file cannot be rewritten the key is ignored in memory and the line says that instead.
|
|
266
285
|
*/
|
|
267
286
|
export function loadHeedFileAt(path, log = toStderr) {
|
|
287
|
+
return readHeedFileAt(path, log).heedFile;
|
|
288
|
+
}
|
|
289
|
+
/** {@link loadHeedFileAt}, with the file's text as it is on disk afterwards: what the settings it holds hash from. */
|
|
290
|
+
export function readHeedFileAt(path, log = toStderr) {
|
|
268
291
|
const text = readFileSync(path, "utf8");
|
|
269
292
|
const document = parseDocument(text);
|
|
270
293
|
const retired = document.errors.length > 0 ? [] : RETIRED_KEYS.filter((path) => document.hasIn(path));
|
|
271
294
|
if (retired.length === 0)
|
|
272
|
-
return loadHeedFile(text);
|
|
295
|
+
return { heedFile: loadHeedFile(text), text };
|
|
273
296
|
const names = retired.map((path) => path.join(".")).join(", ");
|
|
274
297
|
for (const path of retired)
|
|
275
298
|
document.deleteIn(path);
|
|
276
299
|
const refusal = rewriteInPlace(path, document);
|
|
277
300
|
if (refusal === null) {
|
|
278
301
|
log(`[heedvane] removed ${names} from ${path}: no hook waits on Heedvane any more`);
|
|
279
|
-
return loadHeedFile(String(document));
|
|
302
|
+
return { heedFile: loadHeedFile(String(document)), text: String(document) };
|
|
280
303
|
}
|
|
281
304
|
// The file stays as it was (an alias to the removed key cannot be written back, or the disk refused): load it without the key.
|
|
282
305
|
log(`[heedvane] could not remove ${names} from ${path} (${errorMessage(refusal)}); it is ignored`);
|
|
283
|
-
return loadHeedFile(JSON.stringify(withoutKeys(parse(text), retired)));
|
|
306
|
+
return { heedFile: loadHeedFile(JSON.stringify(withoutKeys(parse(text), retired))), text };
|
|
284
307
|
}
|
|
285
308
|
/** Writes the document over `path`; the error that stopped it, or null. */
|
|
286
309
|
function rewriteInPlace(path, document) {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an agent's hook does when Heedvane's listener cannot answer, owned here once. Pi's guard (src/guard.ts) is the
|
|
3
|
+
* one fallback that acts on these values: Pi runs it in process, and the POSIX dispatchers (src/dispatchers/posix.ts)
|
|
4
|
+
* and Hermes' plugin (src/clients/hermes-plugin.ts) run its installed copy, {@link FALLBACK_FILE}. The listener reads
|
|
5
|
+
* the spool they describe (src/listener/spool.ts). It imports nothing, so each copy of the guard carries it inline
|
|
6
|
+
* (src/clients/pi-extension.ts).
|
|
7
|
+
*/
|
|
8
|
+
/** The listener's refusal of a GET on a hook route: the answer that tells it apart from another program on its port. */
|
|
9
|
+
export declare const PROBE_REFUSAL = "method not allowed; POST only";
|
|
10
|
+
/**
|
|
11
|
+
* The header every answer of this machine's listener (0.3.4 on) carries, and the endpoint.env line the dispatchers read it
|
|
12
|
+
* from: {@link listenerProof} of the token, so it proves the answer is that listener's without showing the token.
|
|
13
|
+
*/
|
|
14
|
+
export declare const PROOF_HEADER = "X-Heed-Listener";
|
|
15
|
+
export declare const PROOF_KEY = "HEED_AGENT_HOOK_PROOF";
|
|
16
|
+
/** What the proof signs; Hermes' plugin signs the same text in Python. */
|
|
17
|
+
export declare const PROOF_MESSAGE = "heedvane listener";
|
|
18
|
+
/** The proof of the listener with `token`: HMAC-SHA-256 of {@link PROOF_MESSAGE} under it, 32 hex digits. WebCrypto, so this file imports nothing. */
|
|
19
|
+
export declare function listenerProof(token: string): Promise<string>;
|
|
20
|
+
/** Events no listener answered: `<heed root>/spool/pane-<pane key, else session, else "default">.jsonl`, one JSON record a line. */
|
|
21
|
+
export declare const SPOOL_DIR = "spool";
|
|
22
|
+
export declare const SPOOL_FILE_PREFIX = "pane-";
|
|
23
|
+
export declare const SPOOL_FILE_SUFFIX = ".jsonl";
|
|
24
|
+
/** A pane's spool file stops growing at 5 MB. */
|
|
25
|
+
export declare const SPOOL_CAP_BYTES = 5242880;
|
|
26
|
+
/** Spool files, and the events set aside after a change of sign-in, older than this are removed. */
|
|
27
|
+
export declare const SPOOL_TTL_DAYS = 7;
|
|
28
|
+
export declare const OTHER_SIGN_IN_DIR = "other-sign-in";
|
|
29
|
+
/** One spooled event. `payload` is the hook's JSON text, `source` the agent id. */
|
|
30
|
+
export interface SpoolRecord {
|
|
31
|
+
paneKey: string;
|
|
32
|
+
tabId: string;
|
|
33
|
+
worktreeId: string;
|
|
34
|
+
launchToken: string;
|
|
35
|
+
source: string;
|
|
36
|
+
receivedAt: string;
|
|
37
|
+
payload: string;
|
|
38
|
+
/** The event's unique id (absent in lines from scripts older than ids). */
|
|
39
|
+
id?: string;
|
|
40
|
+
}
|
|
41
|
+
/** The record fields copied from the agent's environment, and the pane key's variables in order. */
|
|
42
|
+
export declare const SPOOL_ENV: {
|
|
43
|
+
readonly tabId: "HEED_TAB_ID";
|
|
44
|
+
readonly worktreeId: "HEED_WORKTREE_ID";
|
|
45
|
+
readonly launchToken: "HEED_LAUNCH_TOKEN";
|
|
46
|
+
};
|
|
47
|
+
export declare const PANE_KEY_ENV: readonly ["HEED_PANE_KEY", "HEED_SESSION"];
|
|
48
|
+
/** Told once per session, where the agent can show it, when the hook cannot ask the listener; the agent still continues. */
|
|
49
|
+
export declare const UNCHECKED_NOTICE = "Heedvane is not reachable; actions are not being checked.";
|
|
50
|
+
/** Told once per session when the listener answers a call with an error of its own; that call ran unchecked. */
|
|
51
|
+
export declare const UNCHECKED_ERROR_NOTICE = "Heedvane's listener answered with an error, so an action was not checked; ~/.heed/listener.log says why.";
|
|
52
|
+
/** The marker, one per session in the spool folder, that the session has been told. */
|
|
53
|
+
export declare const UNCHECKED_MARK_PREFIX = "unchecked-";
|
|
54
|
+
/** How long a hook waits for the listener: to connect, to answer, and at a session start (its one policy fetch). */
|
|
55
|
+
export declare const HOOK_CONNECT_TIMEOUT_MS = 500;
|
|
56
|
+
export declare const HOOK_TIMEOUT_MS = 800;
|
|
57
|
+
export declare const SESSION_START_TIMEOUT_MS = 2000;
|
|
58
|
+
/** The guard's copy beside the dispatchers' scripts and in Hermes' plugin, which they run with this flag when the listener did not answer. */
|
|
59
|
+
export declare const FALLBACK_FILE = "fallback.mjs";
|
|
60
|
+
export declare const UNANSWERED_FLAG = "--heedvane-unanswered";
|
|
61
|
+
/** How long the fallback waits before it asks the listener once more: one taking the port over binds within milliseconds. */
|
|
62
|
+
export declare const RETRY_AFTER_MS = 250;
|
|
63
|
+
/** The longest Hermes waits for the fallback: a second for Node's start and keeping the event, the pause, the probe and the ask. */
|
|
64
|
+
export declare const FALLBACK_TIMEOUT_MS: number;
|
|
65
|
+
/** The file beside the dispatchers' scripts that names the Node the fallback runs with: the one that last installed the hooks. */
|
|
66
|
+
export declare const HOOK_NODE_FILE = "node";
|
|
67
|
+
/** With nothing listening, the hook starts the listener with this command from the heed folder, at most once a minute. */
|
|
68
|
+
export declare const LISTENER_START: readonly ["npx", "-y", "heedvane", "listener", "start"];
|
|
69
|
+
/** The lock folder in the heed folder that keeps it to one start a minute; the listener removes it once it listens. */
|
|
70
|
+
export declare const START_LOCK = "listener.starting";
|
|
71
|
+
export declare const START_LOCK_MINUTES = 1;
|
|
72
|
+
/** The file in the start lock naming the pid of the start that holds it. */
|
|
73
|
+
export declare const START_LOCK_HOLDER = "pid";
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an agent's hook does when Heedvane's listener cannot answer, owned here once. Pi's guard (src/guard.ts) is the
|
|
3
|
+
* one fallback that acts on these values: Pi runs it in process, and the POSIX dispatchers (src/dispatchers/posix.ts)
|
|
4
|
+
* and Hermes' plugin (src/clients/hermes-plugin.ts) run its installed copy, {@link FALLBACK_FILE}. The listener reads
|
|
5
|
+
* the spool they describe (src/listener/spool.ts). It imports nothing, so each copy of the guard carries it inline
|
|
6
|
+
* (src/clients/pi-extension.ts).
|
|
7
|
+
*/
|
|
8
|
+
/** The listener's refusal of a GET on a hook route: the answer that tells it apart from another program on its port. */
|
|
9
|
+
export const PROBE_REFUSAL = "method not allowed; POST only";
|
|
10
|
+
/**
|
|
11
|
+
* The header every answer of this machine's listener (0.3.4 on) carries, and the endpoint.env line the dispatchers read it
|
|
12
|
+
* from: {@link listenerProof} of the token, so it proves the answer is that listener's without showing the token.
|
|
13
|
+
*/
|
|
14
|
+
export const PROOF_HEADER = "X-Heed-Listener";
|
|
15
|
+
export const PROOF_KEY = "HEED_AGENT_HOOK_PROOF";
|
|
16
|
+
/** What the proof signs; Hermes' plugin signs the same text in Python. */
|
|
17
|
+
export const PROOF_MESSAGE = "heedvane listener";
|
|
18
|
+
/** The proof of the listener with `token`: HMAC-SHA-256 of {@link PROOF_MESSAGE} under it, 32 hex digits. WebCrypto, so this file imports nothing. */
|
|
19
|
+
export async function listenerProof(token) {
|
|
20
|
+
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(token), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
21
|
+
const mac = new Uint8Array(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(PROOF_MESSAGE)));
|
|
22
|
+
return Array.from(mac.subarray(0, 16), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
23
|
+
}
|
|
24
|
+
/** Events no listener answered: `<heed root>/spool/pane-<pane key, else session, else "default">.jsonl`, one JSON record a line. */
|
|
25
|
+
export const SPOOL_DIR = "spool";
|
|
26
|
+
export const SPOOL_FILE_PREFIX = "pane-";
|
|
27
|
+
export const SPOOL_FILE_SUFFIX = ".jsonl";
|
|
28
|
+
/** A pane's spool file stops growing at 5 MB. */
|
|
29
|
+
export const SPOOL_CAP_BYTES = 5_242_880;
|
|
30
|
+
/** Spool files, and the events set aside after a change of sign-in, older than this are removed. */
|
|
31
|
+
export const SPOOL_TTL_DAYS = 7;
|
|
32
|
+
export const OTHER_SIGN_IN_DIR = "other-sign-in";
|
|
33
|
+
/** The record fields copied from the agent's environment, and the pane key's variables in order. */
|
|
34
|
+
export const SPOOL_ENV = { tabId: "HEED_TAB_ID", worktreeId: "HEED_WORKTREE_ID", launchToken: "HEED_LAUNCH_TOKEN" };
|
|
35
|
+
export const PANE_KEY_ENV = ["HEED_PANE_KEY", "HEED_SESSION"];
|
|
36
|
+
/** Told once per session, where the agent can show it, when the hook cannot ask the listener; the agent still continues. */
|
|
37
|
+
export const UNCHECKED_NOTICE = "Heedvane is not reachable; actions are not being checked.";
|
|
38
|
+
/** Told once per session when the listener answers a call with an error of its own; that call ran unchecked. */
|
|
39
|
+
export const UNCHECKED_ERROR_NOTICE = "Heedvane's listener answered with an error, so an action was not checked; ~/.heed/listener.log says why.";
|
|
40
|
+
/** The marker, one per session in the spool folder, that the session has been told. */
|
|
41
|
+
export const UNCHECKED_MARK_PREFIX = "unchecked-";
|
|
42
|
+
/** How long a hook waits for the listener: to connect, to answer, and at a session start (its one policy fetch). */
|
|
43
|
+
export const HOOK_CONNECT_TIMEOUT_MS = 500;
|
|
44
|
+
export const HOOK_TIMEOUT_MS = 800;
|
|
45
|
+
export const SESSION_START_TIMEOUT_MS = 2_000;
|
|
46
|
+
/** The guard's copy beside the dispatchers' scripts and in Hermes' plugin, which they run with this flag when the listener did not answer. */
|
|
47
|
+
export const FALLBACK_FILE = "fallback.mjs";
|
|
48
|
+
export const UNANSWERED_FLAG = "--heedvane-unanswered";
|
|
49
|
+
/** How long the fallback waits before it asks the listener once more: one taking the port over binds within milliseconds. */
|
|
50
|
+
export const RETRY_AFTER_MS = 250;
|
|
51
|
+
/** The longest Hermes waits for the fallback: a second for Node's start and keeping the event, the pause, the probe and the ask. */
|
|
52
|
+
export const FALLBACK_TIMEOUT_MS = 1_000 + RETRY_AFTER_MS + 2 * HOOK_TIMEOUT_MS;
|
|
53
|
+
/** The file beside the dispatchers' scripts that names the Node the fallback runs with: the one that last installed the hooks. */
|
|
54
|
+
export const HOOK_NODE_FILE = "node";
|
|
55
|
+
/** With nothing listening, the hook starts the listener with this command from the heed folder, at most once a minute. */
|
|
56
|
+
export const LISTENER_START = ["npx", "-y", "heedvane", "listener", "start"];
|
|
57
|
+
/** The lock folder in the heed folder that keeps it to one start a minute; the listener removes it once it listens. */
|
|
58
|
+
export const START_LOCK = "listener.starting";
|
|
59
|
+
export const START_LOCK_MINUTES = 1;
|
|
60
|
+
/** The file in the start lock naming the pid of the start that holds it. */
|
|
61
|
+
export const START_LOCK_HOLDER = "pid";
|
package/dist/installer.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Readable, Writable } from "node:stream";
|
|
2
|
-
import type { HeedFile, InstallChoices } from "./heed-file.js";
|
|
3
|
-
import type { PlannedAction } from "./plan.js";
|
|
2
|
+
import type { DataMode, HeedFile, InstallChoices } from "./heed-file.js";
|
|
4
3
|
import { type AppliedResult } from "./render.js";
|
|
4
|
+
import { type PlannedAction } from "./plan.js";
|
|
5
5
|
/** Where the questions are read and drawn: the process's terminal unless a test injects streams. */
|
|
6
6
|
export interface PromptStreams {
|
|
7
7
|
input?: Readable;
|
|
@@ -27,7 +27,9 @@ export declare function brandIntro(server: string, org: string | undefined, stre
|
|
|
27
27
|
/** The person's choices, or null when they cancelled (Ctrl-C) at any question. */
|
|
28
28
|
export declare function askChoices(agents: AgentOption[], current: HeedFile, streams: PromptStreams): Promise<InstallChoices | null>;
|
|
29
29
|
/** Shows what the install will do and asks to go ahead; false when the person declines or cancels. */
|
|
30
|
-
export declare function confirmInstall(agents: AgentLine[], streams: PromptStreams): Promise<boolean>;
|
|
30
|
+
export declare function confirmInstall(agents: AgentLine[], details: string[], streams: PromptStreams): Promise<boolean>;
|
|
31
|
+
/** The summary's lines about the whole install: what leaves the machine, and where the MCP connection points. */
|
|
32
|
+
export declare function installDetails(heedFile: HeedFile): string[];
|
|
31
33
|
/** Runs the install behind a spinner; the spinner ends with the outcome either way. */
|
|
32
34
|
export declare function installWithSpinner<T>(agentCount: number, work: () => Promise<T>, streams: PromptStreams): Promise<T>;
|
|
33
35
|
/** One line per agent: a green tick and what it got, or what failed and why. */
|
|
@@ -35,7 +37,7 @@ export declare function showInstalled(agents: AgentLine[], streams: PromptStream
|
|
|
35
37
|
/** A line the install prints along the way (the sign-in link, for one), shown inside the installer's frame. */
|
|
36
38
|
export declare function promptLine(streams: PromptStreams): (line: string) => void;
|
|
37
39
|
/** Starts the listener in the background behind a spinner; true when it answers. */
|
|
38
|
-
export declare function listenerStep(start: () => Promise<
|
|
40
|
+
export declare function listenerStep(start: () => Promise<true | string>, streams: PromptStreams): Promise<boolean>;
|
|
39
41
|
/**
|
|
40
42
|
* One line per agent from the plan, in plan order: what it gets ("hooks · MCP · skill"), "already set up"
|
|
41
43
|
* when nothing changes, or why it gets nothing. `applied` adds what failed when the install ran.
|
|
@@ -43,5 +45,10 @@ export declare function listenerStep(start: () => Promise<boolean>, logPath: str
|
|
|
43
45
|
export declare function agentLines(actions: PlannedAction[], names: ReadonlyMap<string, string>, applied?: AppliedResult[]): AgentLine[];
|
|
44
46
|
/** The closing line after a cancel or a "no": nothing was written. Returns the exit code, 1. */
|
|
45
47
|
export declare function installCancelled(streams: PromptStreams): number;
|
|
46
|
-
/** The closing lines after an install. */
|
|
47
|
-
export declare function installFinished(lines: string[], streams: PromptStreams): void;
|
|
48
|
+
/** The closing lines after an install; without a sign-in it is installed but not yet set up. */
|
|
49
|
+
export declare function installFinished(lines: string[], signedIn: boolean, streams: PromptStreams): void;
|
|
50
|
+
/** What each data mode lets leave this machine, as the installer says it. */
|
|
51
|
+
export declare const DATA_MODE_LABEL: Record<DataMode, {
|
|
52
|
+
label: string;
|
|
53
|
+
hint: string;
|
|
54
|
+
}>;
|
package/dist/installer.js
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* the answers become `InstallChoices`, which cli.ts writes into heed.yaml and applies through the same
|
|
5
5
|
* plan as every other install, so a second run starts from the last answers.
|
|
6
6
|
*/
|
|
7
|
-
import { cancel, confirm, intro, isCancel, log, multiselect, note, outro, spinner } from "@clack/prompts";
|
|
7
|
+
import { cancel, confirm, intro, isCancel, log, multiselect, note, outro, select, spinner } from "@clack/prompts";
|
|
8
8
|
import { styleText } from "node:util";
|
|
9
9
|
import { appliedFailed } from "./render.js";
|
|
10
|
+
import { connectionNote, heedvaneConnector } from "./plan.js";
|
|
10
11
|
const INTEGRATION_LABEL = { hooks: "hooks", mcp: "MCP server", skill: "skill", skip: "" };
|
|
11
12
|
/** The header: the name, what Heedvane does, and which server and organization this install connects to. */
|
|
12
13
|
export function brandIntro(server, org, streams) {
|
|
@@ -19,6 +20,9 @@ export async function askChoices(agents, current, streams) {
|
|
|
19
20
|
const picked = await pickAgents(agents, current, streams);
|
|
20
21
|
if (picked === null)
|
|
21
22
|
return null;
|
|
23
|
+
const dataMode = await pickDataMode(current.heedvane.dataMode, streams);
|
|
24
|
+
if (dataMode === null)
|
|
25
|
+
return null;
|
|
22
26
|
const install = await pickIntegrations(current.install, streams);
|
|
23
27
|
if (install === null)
|
|
24
28
|
return null;
|
|
@@ -28,18 +32,27 @@ export async function askChoices(agents, current, streams) {
|
|
|
28
32
|
return {
|
|
29
33
|
agents: sameAsDetected(picked, agents) ? "auto" : picked,
|
|
30
34
|
install,
|
|
35
|
+
dataMode,
|
|
31
36
|
steering: features.includes("steering"),
|
|
32
37
|
skillChecks: features.includes("skillChecks"),
|
|
33
38
|
memory: features.includes("memory"),
|
|
34
39
|
};
|
|
35
40
|
}
|
|
36
41
|
/** Shows what the install will do and asks to go ahead; false when the person declines or cancels. */
|
|
37
|
-
export async function confirmInstall(agents, streams) {
|
|
42
|
+
export async function confirmInstall(agents, details, streams) {
|
|
38
43
|
const footer = styleText("dim", "Choices are saved in ~/.heed/heed.yaml. Each config file is backed up before it changes.");
|
|
39
|
-
note([...agentRows(agents), "", footer].join("\n"), "Ready to install", streams);
|
|
44
|
+
note([...agentRows(agents), "", ...details, footer].join("\n"), "Ready to install", streams);
|
|
40
45
|
const answer = await confirm({ message: "Install now?", initialValue: true, ...streams });
|
|
41
46
|
return !isCancel(answer) && answer;
|
|
42
47
|
}
|
|
48
|
+
/** The summary's lines about the whole install: what leaves the machine, and where the MCP connection points. */
|
|
49
|
+
export function installDetails(heedFile) {
|
|
50
|
+
const lines = [`What leaves this machine: ${DATA_MODE_LABEL[heedFile.heedvane.dataMode].label}`];
|
|
51
|
+
if (!heedFile.install.mcp)
|
|
52
|
+
return lines;
|
|
53
|
+
const connector = heedvaneConnector(heedFile);
|
|
54
|
+
return [...lines, connector === undefined ? connectionNote(heedFile) : `Heedvane's MCP connection: ${connector}`];
|
|
55
|
+
}
|
|
43
56
|
/** Runs the install behind a spinner; the spinner ends with the outcome either way. */
|
|
44
57
|
export async function installWithSpinner(agentCount, work, streams) {
|
|
45
58
|
const progress = spinner({ output: streams.output });
|
|
@@ -73,15 +86,15 @@ export function promptLine(streams) {
|
|
|
73
86
|
};
|
|
74
87
|
}
|
|
75
88
|
/** Starts the listener in the background behind a spinner; true when it answers. */
|
|
76
|
-
export async function listenerStep(start,
|
|
89
|
+
export async function listenerStep(start, streams) {
|
|
77
90
|
const progress = spinner({ output: streams.output });
|
|
78
91
|
progress.start("Starting Heedvane in the background");
|
|
79
|
-
const started = await start().catch(() =>
|
|
80
|
-
if (started)
|
|
92
|
+
const started = await start().catch(() => "Heedvane did not start");
|
|
93
|
+
if (started === true)
|
|
81
94
|
progress.stop("Heedvane is running in the background");
|
|
82
95
|
else
|
|
83
|
-
progress.error(
|
|
84
|
-
return started;
|
|
96
|
+
progress.error(started);
|
|
97
|
+
return started === true;
|
|
85
98
|
}
|
|
86
99
|
/**
|
|
87
100
|
* One line per agent from the plan, in plan order: what it gets ("hooks · MCP · skill"), "already set up"
|
|
@@ -101,10 +114,10 @@ export function installCancelled(streams) {
|
|
|
101
114
|
cancel("Nothing was installed. Run npx heedvane again any time.", streams);
|
|
102
115
|
return 1;
|
|
103
116
|
}
|
|
104
|
-
/** The closing lines after an install. */
|
|
105
|
-
export function installFinished(lines, streams) {
|
|
117
|
+
/** The closing lines after an install; without a sign-in it is installed but not yet set up. */
|
|
118
|
+
export function installFinished(lines, signedIn, streams) {
|
|
106
119
|
note(lines.join("\n"), "Next", streams);
|
|
107
|
-
outro("Heedvane is set up.", streams);
|
|
120
|
+
outro(signedIn ? "Heedvane is set up." : "Installed, but not signed in yet: run npx -y heedvane login", streams);
|
|
108
121
|
}
|
|
109
122
|
async function pickAgents(agents, current, streams) {
|
|
110
123
|
const ordered = [...agents.filter((agent) => agent.detected), ...agents.filter((agent) => !agent.detected)];
|
|
@@ -118,12 +131,26 @@ async function pickAgents(agents, current, streams) {
|
|
|
118
131
|
});
|
|
119
132
|
return isCancel(answer) ? null : answer;
|
|
120
133
|
}
|
|
134
|
+
/** What each data mode lets leave this machine, as the installer says it. */
|
|
135
|
+
export const DATA_MODE_LABEL = {
|
|
136
|
+
"look-but-dont-keep": { label: "Look but don't keep", hint: "Heedvane's checks read prompts and tool calls, then keep only the verdict" },
|
|
137
|
+
"metrics-only": { label: "Metrics only", hint: "only counts and names leave; your organization's fixed rules still run here" },
|
|
138
|
+
};
|
|
139
|
+
async function pickDataMode(current, streams) {
|
|
140
|
+
const answer = await select({
|
|
141
|
+
message: "How much should leave this machine?",
|
|
142
|
+
options: Object.keys(DATA_MODE_LABEL).map((mode) => ({ value: mode, ...DATA_MODE_LABEL[mode] })),
|
|
143
|
+
initialValue: current,
|
|
144
|
+
...streams,
|
|
145
|
+
});
|
|
146
|
+
return isCancel(answer) ? null : answer;
|
|
147
|
+
}
|
|
121
148
|
async function pickIntegrations(install, streams) {
|
|
122
149
|
const answer = await multiselect({
|
|
123
150
|
message: "What should Heedvane add to each agent?",
|
|
124
151
|
options: [
|
|
125
152
|
{ value: "hooks", label: "Hooks", hint: "guardrails, skill checks and memory in every session" },
|
|
126
|
-
{ value: "mcp", label: "MCP server", hint: "Heedvane's tools inside the agent" },
|
|
153
|
+
{ value: "mcp", label: "MCP server", hint: "findings, reports and Heedvane's tools inside the agent" },
|
|
127
154
|
{ value: "skill", label: "Skill", hint: "teaches the agent how to use Heedvane" },
|
|
128
155
|
],
|
|
129
156
|
initialValues: ["hooks", "mcp", "skill"].filter((kind) => install[kind]),
|
|
@@ -156,7 +183,7 @@ function brandBadge(text, output) {
|
|
|
156
183
|
}
|
|
157
184
|
/** What one agent gets: its integrations, "already set up" when nothing changes, or why it gets nothing. */
|
|
158
185
|
function agentParts(id, own) {
|
|
159
|
-
const installs = own.filter((action) => action.kind !== "skip");
|
|
186
|
+
const installs = own.filter((action) => action.kind !== "skip" && action.operation !== "remove");
|
|
160
187
|
if (installs.length === 0)
|
|
161
188
|
return own.map((action) => styleText("dim", action.reason?.replace(`${id}: `, "") ?? "nothing to add"));
|
|
162
189
|
if (installs.every((action) => action.operation === "unchanged"))
|
|
@@ -1,8 +1,52 @@
|
|
|
1
|
+
import { type Endpoint } from "./endpoint.js";
|
|
2
|
+
/** `listener start`'s exit code when the port stays taken: the Heedvane listener there did not stop when asked. */
|
|
3
|
+
export declare const PORT_TAKEN_EXIT = 3;
|
|
1
4
|
/** Where a background listener writes what it prints. */
|
|
2
5
|
export declare function listenerLogPath(root: string): string;
|
|
6
|
+
/** What {@link ensureListener} found and did. */
|
|
7
|
+
export type ListenerOutcome =
|
|
8
|
+
/** No endpoint.env: Heedvane's hooks were never set up on this machine, so no listener is wanted. */
|
|
9
|
+
{
|
|
10
|
+
kind: "not set up";
|
|
11
|
+
}
|
|
12
|
+
/** One of this version already ran with the current settings. */
|
|
13
|
+
| {
|
|
14
|
+
kind: "running";
|
|
15
|
+
}
|
|
16
|
+
/** None ran, an older version ran, or this version on other settings: one with the current settings answers now. */
|
|
17
|
+
| {
|
|
18
|
+
kind: "started" | "replaced" | "restarted";
|
|
19
|
+
}
|
|
20
|
+
/** A newer version runs and was kept, as this CLI is the older; `earlierSettings` when it runs other settings than these. */
|
|
21
|
+
| {
|
|
22
|
+
kind: "newer";
|
|
23
|
+
version: string;
|
|
24
|
+
earlierSettings: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** The one at the port did not stop when asked (0.2.0 and earlier have no way to be asked). */
|
|
27
|
+
| {
|
|
28
|
+
kind: "did not stop";
|
|
29
|
+
port: number;
|
|
30
|
+
}
|
|
31
|
+
/** None answers with the current settings; why, or the listener's log says why. */
|
|
32
|
+
| {
|
|
33
|
+
kind: "did not start";
|
|
34
|
+
log: string;
|
|
35
|
+
why?: string;
|
|
36
|
+
};
|
|
3
37
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
38
|
+
* The one owner of which listener runs: it compares what the running listener reports about itself (its version and
|
|
39
|
+
* the hash of its settings) with the settings a listener would run now, and acts on {@link listenerVerdict}. A listener
|
|
40
|
+
* to start, replace or restart is started in the background with this package's own CLI (`listener start`, which takes
|
|
41
|
+
* the port over from the old one), and this waits until a listener it can keep answers, whatever version won the port.
|
|
42
|
+
* Every command that changes heed.yaml or the sign-in, `listener restart` and `run` call it last.
|
|
7
43
|
*/
|
|
8
|
-
export declare function
|
|
44
|
+
export declare function ensureListener(home: string, cliPath: string, env: NodeJS.ProcessEnv): Promise<ListenerOutcome>;
|
|
45
|
+
/** What stops a listener that does not stop when asked, ready to paste. */
|
|
46
|
+
export declare function stopCommand(port: number): string;
|
|
47
|
+
/**
|
|
48
|
+
* Asks the listener at `endpoint` to stop (POST /shutdown) and waits until its process has exited (until it no longer
|
|
49
|
+
* answers, for one older than 0.3.4, which does not name its pid), as long as its last reports may take to send.
|
|
50
|
+
* "no stop route" for a listener from 0.2.0 or earlier, which runs until logout or restart.
|
|
51
|
+
*/
|
|
52
|
+
export declare function stopRunningListener(endpoint: Endpoint, reason?: string): Promise<"stopped" | "no stop route" | "still running">;
|