@indigoai-us/hq-cli 5.115.6 → 5.116.0
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/CHANGELOG.md +72 -11
- package/dist/command-catalog.generated.d.ts +162 -2
- package/dist/command-catalog.generated.js +205 -2
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/agent-enroll.d.ts +105 -0
- package/dist/commands/agent-enroll.js +273 -0
- package/dist/commands/agent-kit.d.ts +53 -0
- package/dist/commands/agent-kit.js +260 -0
- package/dist/commands/agent-mcp.d.ts +22 -0
- package/dist/commands/agent-mcp.js +104 -0
- package/dist/commands/agent-probe.d.ts +71 -0
- package/dist/commands/agent-probe.js +294 -0
- package/dist/commands/agent.d.ts +12 -0
- package/dist/commands/agent.js +23 -0
- package/dist/commands/agents.d.ts +27 -0
- package/dist/commands/agents.js +280 -6
- package/dist/commands/secrets.js +17 -5
- package/dist/lib/agent-kit/creds.d.ts +60 -0
- package/dist/lib/agent-kit/creds.js +123 -0
- package/dist/lib/agent-kit/kit-config.d.ts +29 -0
- package/dist/lib/agent-kit/kit-config.js +54 -0
- package/dist/lib/agent-kit/log.d.ts +17 -0
- package/dist/lib/agent-kit/log.js +46 -0
- package/dist/lib/agent-kit/mcp/jsonrpc.d.ts +84 -0
- package/dist/lib/agent-kit/mcp/jsonrpc.js +164 -0
- package/dist/lib/agent-kit/mcp/tools.d.ts +45 -0
- package/dist/lib/agent-kit/mcp/tools.js +280 -0
- package/dist/lib/agent-kit/paths.d.ts +42 -0
- package/dist/lib/agent-kit/paths.js +56 -0
- package/dist/lib/agent-kit/run/heartbeat.d.ts +52 -0
- package/dist/lib/agent-kit/run/heartbeat.js +97 -0
- package/dist/lib/agent-kit/run/inbox.d.ts +59 -0
- package/dist/lib/agent-kit/run/inbox.js +152 -0
- package/dist/lib/agent-kit/run/mesh-listener.d.ts +58 -0
- package/dist/lib/agent-kit/run/mesh-listener.js +193 -0
- package/dist/lib/agent-kit/run/sync.d.ts +33 -0
- package/dist/lib/agent-kit/run/sync.js +58 -0
- package/dist/lib/agent-kit/services.d.ts +21 -0
- package/dist/lib/agent-kit/services.js +46 -0
- package/dist/lib/agent-kit/skills.d.ts +18 -0
- package/dist/lib/agent-kit/skills.js +149 -0
- package/dist/lib/service-manager/index.d.ts +43 -0
- package/dist/lib/service-manager/index.js +114 -0
- package/dist/lib/service-manager/launchd.d.ts +23 -0
- package/dist/lib/service-manager/launchd.js +81 -0
- package/dist/lib/service-manager/systemd.d.ts +19 -0
- package/dist/lib/service-manager/systemd.js +72 -0
- package/dist/lib/service-manager/types.d.ts +32 -0
- package/dist/lib/service-manager/types.js +26 -0
- package/dist/utils/self-update.js +2 -30
- package/dist/utils/update-command-supervisor.cjs +194 -0
- package/dist/utils/version-gate.d.ts +18 -0
- package/dist/utils/version-gate.js +126 -7
- package/package.json +2 -2
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* On-host layout for an external agent (api-contract v1, "Host files"):
|
|
3
|
+
*
|
|
4
|
+
* ~/.hq-agent/host-key Ed25519 private key, PKCS8 PEM, 0600
|
|
5
|
+
* ~/.hq-agent/host-key.pub Ed25519 public key, SPKI PEM
|
|
6
|
+
* ~/.hq-agent/machine-creds.json credentials + hostKeyPath/companySlug/apiBaseUrl, 0600
|
|
7
|
+
* ~/.hq-agent/kit.json kit install settings (hq root, intervals)
|
|
8
|
+
* ~/.hq-agent/state/ component-{sync,mesh,inbox}, last-heartbeat.json
|
|
9
|
+
* ~/.hq-agent/inbox/ mirrored inbox items (one JSON per message)
|
|
10
|
+
* ~/.hq-agent/logs/*.log one log per kit service
|
|
11
|
+
* ~/.hq-agent/skills/<name>/SKILL.md
|
|
12
|
+
*
|
|
13
|
+
* `HQ_AGENT_DIR` relocates the whole tree (tests, containers). The creds file
|
|
14
|
+
* additionally honours `HQ_MACHINE_CREDS_FILE`, the override hq-cloud reads,
|
|
15
|
+
* so a kit pointed at a custom creds path and hq-cloud's mint agree.
|
|
16
|
+
*/
|
|
17
|
+
export declare const AGENT_DIR_ENV = "HQ_AGENT_DIR";
|
|
18
|
+
export declare const HOST_KEY_NAME = "host-key";
|
|
19
|
+
export declare const HOST_KEY_PUB_NAME = "host-key.pub";
|
|
20
|
+
export declare const MACHINE_CREDS_NAME = "machine-creds.json";
|
|
21
|
+
export declare const KIT_CONFIG_NAME = "kit.json";
|
|
22
|
+
export declare const LAST_HEARTBEAT_NAME = "last-heartbeat.json";
|
|
23
|
+
export type KitComponent = "sync" | "mesh" | "inbox";
|
|
24
|
+
export declare const KIT_COMPONENTS: readonly KitComponent[];
|
|
25
|
+
export interface AgentKitPaths {
|
|
26
|
+
home: string;
|
|
27
|
+
agentDir: string;
|
|
28
|
+
hostKeyPath: string;
|
|
29
|
+
hostKeyPubPath: string;
|
|
30
|
+
machineCredsPath: string;
|
|
31
|
+
kitConfigPath: string;
|
|
32
|
+
stateDir: string;
|
|
33
|
+
inboxDir: string;
|
|
34
|
+
logsDir: string;
|
|
35
|
+
skillsDir: string;
|
|
36
|
+
lastHeartbeatPath: string;
|
|
37
|
+
}
|
|
38
|
+
export declare function agentDir(home?: string, env?: NodeJS.ProcessEnv): string;
|
|
39
|
+
export declare function agentKitPaths(home?: string, env?: NodeJS.ProcessEnv): AgentKitPaths;
|
|
40
|
+
export declare function componentStatePath(paths: Pick<AgentKitPaths, "stateDir">, component: KitComponent): string;
|
|
41
|
+
export declare function serviceLogPath(paths: Pick<AgentKitPaths, "logsDir">, service: string): string;
|
|
42
|
+
//# sourceMappingURL=paths.d.ts.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* On-host layout for an external agent (api-contract v1, "Host files"):
|
|
3
|
+
*
|
|
4
|
+
* ~/.hq-agent/host-key Ed25519 private key, PKCS8 PEM, 0600
|
|
5
|
+
* ~/.hq-agent/host-key.pub Ed25519 public key, SPKI PEM
|
|
6
|
+
* ~/.hq-agent/machine-creds.json credentials + hostKeyPath/companySlug/apiBaseUrl, 0600
|
|
7
|
+
* ~/.hq-agent/kit.json kit install settings (hq root, intervals)
|
|
8
|
+
* ~/.hq-agent/state/ component-{sync,mesh,inbox}, last-heartbeat.json
|
|
9
|
+
* ~/.hq-agent/inbox/ mirrored inbox items (one JSON per message)
|
|
10
|
+
* ~/.hq-agent/logs/*.log one log per kit service
|
|
11
|
+
* ~/.hq-agent/skills/<name>/SKILL.md
|
|
12
|
+
*
|
|
13
|
+
* `HQ_AGENT_DIR` relocates the whole tree (tests, containers). The creds file
|
|
14
|
+
* additionally honours `HQ_MACHINE_CREDS_FILE`, the override hq-cloud reads,
|
|
15
|
+
* so a kit pointed at a custom creds path and hq-cloud's mint agree.
|
|
16
|
+
*/
|
|
17
|
+
import * as os from "node:os";
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
export const AGENT_DIR_ENV = "HQ_AGENT_DIR";
|
|
20
|
+
export const HOST_KEY_NAME = "host-key";
|
|
21
|
+
export const HOST_KEY_PUB_NAME = "host-key.pub";
|
|
22
|
+
export const MACHINE_CREDS_NAME = "machine-creds.json";
|
|
23
|
+
export const KIT_CONFIG_NAME = "kit.json";
|
|
24
|
+
export const LAST_HEARTBEAT_NAME = "last-heartbeat.json";
|
|
25
|
+
export const KIT_COMPONENTS = ["sync", "mesh", "inbox"];
|
|
26
|
+
export function agentDir(home = os.homedir(), env = process.env) {
|
|
27
|
+
const override = env[AGENT_DIR_ENV]?.trim();
|
|
28
|
+
if (override)
|
|
29
|
+
return override;
|
|
30
|
+
return path.join(home, ".hq-agent");
|
|
31
|
+
}
|
|
32
|
+
export function agentKitPaths(home = os.homedir(), env = process.env) {
|
|
33
|
+
const dir = agentDir(home, env);
|
|
34
|
+
const credsOverride = env.HQ_MACHINE_CREDS_FILE?.trim();
|
|
35
|
+
const stateDir = path.join(dir, "state");
|
|
36
|
+
return {
|
|
37
|
+
home,
|
|
38
|
+
agentDir: dir,
|
|
39
|
+
hostKeyPath: path.join(dir, HOST_KEY_NAME),
|
|
40
|
+
hostKeyPubPath: path.join(dir, HOST_KEY_PUB_NAME),
|
|
41
|
+
machineCredsPath: credsOverride || path.join(dir, MACHINE_CREDS_NAME),
|
|
42
|
+
kitConfigPath: path.join(dir, KIT_CONFIG_NAME),
|
|
43
|
+
stateDir,
|
|
44
|
+
inboxDir: path.join(dir, "inbox"),
|
|
45
|
+
logsDir: path.join(dir, "logs"),
|
|
46
|
+
skillsDir: path.join(dir, "skills"),
|
|
47
|
+
lastHeartbeatPath: path.join(stateDir, LAST_HEARTBEAT_NAME),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export function componentStatePath(paths, component) {
|
|
51
|
+
return path.join(paths.stateDir, `component-${component}`);
|
|
52
|
+
}
|
|
53
|
+
export function serviceLogPath(paths, service) {
|
|
54
|
+
return path.join(paths.logsDir, `${service}.log`);
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kit service: heartbeat reporter.
|
|
3
|
+
*
|
|
4
|
+
* Every 60 s POST /v1/agents/{uid}/heartbeat with
|
|
5
|
+
* `{ components: { sync, mesh, inbox }, cliVersion, hostInfo }` — the
|
|
6
|
+
* external-agent body from api-contract v1, mirroring the hosted box's
|
|
7
|
+
* hq-agent-heartbeat.sh (which folds per-component stamp files into one
|
|
8
|
+
* beat). Component values are read from the stamps the other three services
|
|
9
|
+
* write; a missing or stale stamp reports `error`, never silence.
|
|
10
|
+
*
|
|
11
|
+
* The last successful beat is recorded at state/last-heartbeat.json so
|
|
12
|
+
* `hq agent probe` can report its age.
|
|
13
|
+
*/
|
|
14
|
+
import { type ComponentStatus } from "../creds.js";
|
|
15
|
+
import type { KitLogger } from "../log.js";
|
|
16
|
+
import { type AgentKitPaths, type KitComponent } from "../paths.js";
|
|
17
|
+
/** A component stamp older than this reads as error. */
|
|
18
|
+
export declare const COMPONENT_STALE_AFTER_MS: Record<KitComponent, number>;
|
|
19
|
+
export interface HeartbeatBody {
|
|
20
|
+
components: Record<KitComponent, ComponentStatus>;
|
|
21
|
+
cliVersion: string;
|
|
22
|
+
hostInfo: {
|
|
23
|
+
os: string;
|
|
24
|
+
arch: string;
|
|
25
|
+
hostname: string;
|
|
26
|
+
cliVersion: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export declare function buildHeartbeatBody(paths: Pick<AgentKitPaths, "stateDir">, now?: () => Date, hostInfo?: HeartbeatBody["hostInfo"]): HeartbeatBody;
|
|
30
|
+
export interface LastHeartbeat {
|
|
31
|
+
at: string;
|
|
32
|
+
components: Record<KitComponent, ComponentStatus>;
|
|
33
|
+
status: number;
|
|
34
|
+
}
|
|
35
|
+
export declare function writeLastHeartbeat(paths: Pick<AgentKitPaths, "stateDir" | "lastHeartbeatPath">, record: LastHeartbeat): void;
|
|
36
|
+
export declare function readLastHeartbeat(paths: Pick<AgentKitPaths, "lastHeartbeatPath">): LastHeartbeat | null;
|
|
37
|
+
export interface HeartbeatDeps {
|
|
38
|
+
paths: AgentKitPaths;
|
|
39
|
+
agentUid: string;
|
|
40
|
+
apiBaseUrl: string;
|
|
41
|
+
intervalMs: number;
|
|
42
|
+
getToken: () => Promise<string>;
|
|
43
|
+
log: KitLogger;
|
|
44
|
+
post?: (token: string, body: HeartbeatBody) => Promise<number>;
|
|
45
|
+
now?: () => Date;
|
|
46
|
+
sleep?: (ms: number) => Promise<void>;
|
|
47
|
+
maxBeats?: number;
|
|
48
|
+
}
|
|
49
|
+
export declare function defaultHeartbeatPost(agentUid: string, apiBaseUrl: string): (token: string, body: HeartbeatBody) => Promise<number>;
|
|
50
|
+
export declare function beatOnce(deps: HeartbeatDeps): Promise<boolean>;
|
|
51
|
+
export declare function runHeartbeatLoop(deps: HeartbeatDeps): Promise<void>;
|
|
52
|
+
//# sourceMappingURL=heartbeat.d.ts.map
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kit service: heartbeat reporter.
|
|
3
|
+
*
|
|
4
|
+
* Every 60 s POST /v1/agents/{uid}/heartbeat with
|
|
5
|
+
* `{ components: { sync, mesh, inbox }, cliVersion, hostInfo }` — the
|
|
6
|
+
* external-agent body from api-contract v1, mirroring the hosted box's
|
|
7
|
+
* hq-agent-heartbeat.sh (which folds per-component stamp files into one
|
|
8
|
+
* beat). Component values are read from the stamps the other three services
|
|
9
|
+
* write; a missing or stale stamp reports `error`, never silence.
|
|
10
|
+
*
|
|
11
|
+
* The last successful beat is recorded at state/last-heartbeat.json so
|
|
12
|
+
* `hq agent probe` can report its age.
|
|
13
|
+
*/
|
|
14
|
+
import * as fs from "node:fs";
|
|
15
|
+
import * as os from "node:os";
|
|
16
|
+
import { CLI_VERSION } from "../../../cli-version.js";
|
|
17
|
+
import { vaultApiFetch } from "../../../utils/vault-api.js";
|
|
18
|
+
import { readComponentStatus } from "../creds.js";
|
|
19
|
+
import { KIT_COMPONENTS } from "../paths.js";
|
|
20
|
+
/** A component stamp older than this reads as error. */
|
|
21
|
+
export const COMPONENT_STALE_AFTER_MS = {
|
|
22
|
+
sync: 3 * 60 * 60_000, // sync passes every 5 min; three hours of silence is real trouble
|
|
23
|
+
mesh: 3 * 60_000,
|
|
24
|
+
inbox: 3 * 60_000,
|
|
25
|
+
};
|
|
26
|
+
export function buildHeartbeatBody(paths, now = () => new Date(), hostInfo) {
|
|
27
|
+
const components = {};
|
|
28
|
+
for (const c of KIT_COMPONENTS) {
|
|
29
|
+
components[c] = readComponentStatus(paths, c, COMPONENT_STALE_AFTER_MS[c], now).status;
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
components,
|
|
33
|
+
cliVersion: CLI_VERSION,
|
|
34
|
+
hostInfo: hostInfo ?? {
|
|
35
|
+
os: `${os.platform()} ${os.release()}`,
|
|
36
|
+
arch: os.arch(),
|
|
37
|
+
hostname: os.hostname(),
|
|
38
|
+
cliVersion: CLI_VERSION,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function writeLastHeartbeat(paths, record) {
|
|
43
|
+
fs.mkdirSync(paths.stateDir, { recursive: true, mode: 0o700 });
|
|
44
|
+
fs.writeFileSync(paths.lastHeartbeatPath, `${JSON.stringify(record)}\n`, { mode: 0o600 });
|
|
45
|
+
}
|
|
46
|
+
export function readLastHeartbeat(paths) {
|
|
47
|
+
try {
|
|
48
|
+
const raw = JSON.parse(fs.readFileSync(paths.lastHeartbeatPath, "utf8"));
|
|
49
|
+
return typeof raw.at === "string" ? raw : null;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function defaultHeartbeatPost(agentUid, apiBaseUrl) {
|
|
56
|
+
return async (token, body) => {
|
|
57
|
+
const res = await vaultApiFetch({
|
|
58
|
+
token,
|
|
59
|
+
baseUrl: apiBaseUrl,
|
|
60
|
+
path: `/v1/agents/${encodeURIComponent(agentUid)}/heartbeat`,
|
|
61
|
+
method: "POST",
|
|
62
|
+
body: body,
|
|
63
|
+
});
|
|
64
|
+
return res.status;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export async function beatOnce(deps) {
|
|
68
|
+
const now = deps.now ?? (() => new Date());
|
|
69
|
+
const post = deps.post ?? defaultHeartbeatPost(deps.agentUid, deps.apiBaseUrl);
|
|
70
|
+
const body = buildHeartbeatBody(deps.paths, now);
|
|
71
|
+
try {
|
|
72
|
+
const token = await deps.getToken();
|
|
73
|
+
const status = await post(token, body);
|
|
74
|
+
const ok = status >= 200 && status < 300;
|
|
75
|
+
if (ok) {
|
|
76
|
+
writeLastHeartbeat(deps.paths, { at: now().toISOString(), components: body.components, status });
|
|
77
|
+
}
|
|
78
|
+
deps.log(ok ? "info" : "warn", `heartbeat ${status} sync=${body.components.sync} mesh=${body.components.mesh} inbox=${body.components.inbox}`);
|
|
79
|
+
return ok;
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
deps.log("error", `heartbeat failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export async function runHeartbeatLoop(deps) {
|
|
87
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
88
|
+
let beats = 0;
|
|
89
|
+
for (;;) {
|
|
90
|
+
await beatOnce(deps);
|
|
91
|
+
beats += 1;
|
|
92
|
+
if (deps.maxBeats !== undefined && beats >= deps.maxBeats)
|
|
93
|
+
return;
|
|
94
|
+
await sleep(deps.intervalMs);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=heartbeat.js.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kit service: inbox poller.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the poll half of the hosted box's hq-agent-inbox.sh: every
|
|
5
|
+
* `inboxPollMs` GET /v1/agents/{uid}/inbox, take `.messages[]`, and stamp
|
|
6
|
+
* `component-inbox` ok on a 200 (any other status → error, token re-minted
|
|
7
|
+
* on the next pass). The hosted watcher then dispatches each item to a
|
|
8
|
+
* provider and acks it; an external bot brings its own brain, so this
|
|
9
|
+
* service only MIRRORS: each new item is written once to
|
|
10
|
+
* ~/.hq-agent/inbox/<id>.json and appended to inbox.jsonl for the bot to
|
|
11
|
+
* consume, and the work-mesh conversation cache is refreshed so `hq dm`
|
|
12
|
+
* and the skills see the same state.
|
|
13
|
+
*
|
|
14
|
+
* Acking is opt-in (kit.json `inboxAck`): with it off the server keeps the
|
|
15
|
+
* item until the bot acks (`POST /v1/agents/{uid}/inbox/{id}/ack`), and the
|
|
16
|
+
* poller dedupes by id so the same item is never mirrored twice.
|
|
17
|
+
*/
|
|
18
|
+
import type { KitLogger } from "../log.js";
|
|
19
|
+
import type { AgentKitPaths } from "../paths.js";
|
|
20
|
+
export declare const SEEN_IDS_NAME = "seen-ids.json";
|
|
21
|
+
export declare const INBOX_JSONL_NAME = "inbox.jsonl";
|
|
22
|
+
export declare const MAX_SEEN_IDS = 5000;
|
|
23
|
+
export interface InboxItem {
|
|
24
|
+
id: string;
|
|
25
|
+
raw: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
/** Pull `{id}` out of each server item; items without a string id are dropped. */
|
|
28
|
+
export declare function extractInboxItems(body: unknown): InboxItem[];
|
|
29
|
+
export declare function readSeenIds(paths: Pick<AgentKitPaths, "inboxDir">): Set<string>;
|
|
30
|
+
export declare function writeSeenIds(paths: Pick<AgentKitPaths, "inboxDir">, seen: Set<string>): void;
|
|
31
|
+
export declare function mirrorInboxItem(paths: Pick<AgentKitPaths, "inboxDir">, item: InboxItem): string;
|
|
32
|
+
export interface InboxDeps {
|
|
33
|
+
paths: AgentKitPaths;
|
|
34
|
+
agentUid: string;
|
|
35
|
+
apiBaseUrl: string;
|
|
36
|
+
pollMs: number;
|
|
37
|
+
ack: boolean;
|
|
38
|
+
getToken: () => Promise<string>;
|
|
39
|
+
log: KitLogger;
|
|
40
|
+
fetchInbox?: (token: string) => Promise<{
|
|
41
|
+
status: number;
|
|
42
|
+
body: unknown;
|
|
43
|
+
}>;
|
|
44
|
+
ackItem?: (token: string, id: string) => Promise<number>;
|
|
45
|
+
warmCache?: (token: string, agentUid: string) => Promise<unknown>;
|
|
46
|
+
sleep?: (ms: number) => Promise<void>;
|
|
47
|
+
maxPolls?: number;
|
|
48
|
+
}
|
|
49
|
+
export declare function defaultFetchInbox(agentUid: string, apiBaseUrl: string): (token: string) => Promise<{
|
|
50
|
+
status: number;
|
|
51
|
+
body: unknown;
|
|
52
|
+
}>;
|
|
53
|
+
export declare function defaultAckItem(agentUid: string, apiBaseUrl: string): (token: string, id: string) => Promise<number>;
|
|
54
|
+
export declare function pollInboxOnce(deps: InboxDeps): Promise<{
|
|
55
|
+
ok: boolean;
|
|
56
|
+
mirrored: number;
|
|
57
|
+
}>;
|
|
58
|
+
export declare function runInboxLoop(deps: InboxDeps): Promise<void>;
|
|
59
|
+
//# sourceMappingURL=inbox.d.ts.map
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kit service: inbox poller.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the poll half of the hosted box's hq-agent-inbox.sh: every
|
|
5
|
+
* `inboxPollMs` GET /v1/agents/{uid}/inbox, take `.messages[]`, and stamp
|
|
6
|
+
* `component-inbox` ok on a 200 (any other status → error, token re-minted
|
|
7
|
+
* on the next pass). The hosted watcher then dispatches each item to a
|
|
8
|
+
* provider and acks it; an external bot brings its own brain, so this
|
|
9
|
+
* service only MIRRORS: each new item is written once to
|
|
10
|
+
* ~/.hq-agent/inbox/<id>.json and appended to inbox.jsonl for the bot to
|
|
11
|
+
* consume, and the work-mesh conversation cache is refreshed so `hq dm`
|
|
12
|
+
* and the skills see the same state.
|
|
13
|
+
*
|
|
14
|
+
* Acking is opt-in (kit.json `inboxAck`): with it off the server keeps the
|
|
15
|
+
* item until the bot acks (`POST /v1/agents/{uid}/inbox/{id}/ack`), and the
|
|
16
|
+
* poller dedupes by id so the same item is never mirrored twice.
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from "node:fs";
|
|
19
|
+
import * as path from "node:path";
|
|
20
|
+
import { vaultApiFetch } from "../../../utils/vault-api.js";
|
|
21
|
+
import { warmMeshConversationCache } from "../../mesh/api.js";
|
|
22
|
+
import { writeComponentStatus } from "../creds.js";
|
|
23
|
+
export const SEEN_IDS_NAME = "seen-ids.json";
|
|
24
|
+
export const INBOX_JSONL_NAME = "inbox.jsonl";
|
|
25
|
+
export const MAX_SEEN_IDS = 5000;
|
|
26
|
+
/** Pull `{id}` out of each server item; items without a string id are dropped. */
|
|
27
|
+
export function extractInboxItems(body) {
|
|
28
|
+
const rec = body && typeof body === "object" ? body : {};
|
|
29
|
+
const messages = Array.isArray(rec.messages) ? rec.messages : [];
|
|
30
|
+
const items = [];
|
|
31
|
+
for (const m of messages) {
|
|
32
|
+
if (!m || typeof m !== "object")
|
|
33
|
+
continue;
|
|
34
|
+
const raw = m;
|
|
35
|
+
const id = typeof raw.id === "string" ? raw.id : typeof raw.messageId === "string" ? raw.messageId : null;
|
|
36
|
+
if (!id || !/^[A-Za-z0-9._:-]{1,200}$/.test(id))
|
|
37
|
+
continue;
|
|
38
|
+
items.push({ id, raw });
|
|
39
|
+
}
|
|
40
|
+
return items;
|
|
41
|
+
}
|
|
42
|
+
export function readSeenIds(paths) {
|
|
43
|
+
try {
|
|
44
|
+
const raw = JSON.parse(fs.readFileSync(path.join(paths.inboxDir, SEEN_IDS_NAME), "utf8"));
|
|
45
|
+
return new Set(Array.isArray(raw) ? raw.filter((x) => typeof x === "string") : []);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return new Set();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export function writeSeenIds(paths, seen) {
|
|
52
|
+
fs.mkdirSync(paths.inboxDir, { recursive: true, mode: 0o700 });
|
|
53
|
+
const list = [...seen].slice(-MAX_SEEN_IDS);
|
|
54
|
+
fs.writeFileSync(path.join(paths.inboxDir, SEEN_IDS_NAME), JSON.stringify(list), { mode: 0o600 });
|
|
55
|
+
}
|
|
56
|
+
export function mirrorInboxItem(paths, item) {
|
|
57
|
+
fs.mkdirSync(paths.inboxDir, { recursive: true, mode: 0o700 });
|
|
58
|
+
const dest = path.join(paths.inboxDir, `${item.id}.json`);
|
|
59
|
+
fs.writeFileSync(dest, `${JSON.stringify(item.raw, null, 2)}\n`, { mode: 0o600 });
|
|
60
|
+
fs.appendFileSync(path.join(paths.inboxDir, INBOX_JSONL_NAME), `${JSON.stringify({ id: item.id, mirroredAt: new Date().toISOString(), ...item.raw })}\n`, { mode: 0o600 });
|
|
61
|
+
return dest;
|
|
62
|
+
}
|
|
63
|
+
export function defaultFetchInbox(agentUid, apiBaseUrl) {
|
|
64
|
+
return async (token) => {
|
|
65
|
+
const res = await vaultApiFetch({
|
|
66
|
+
token,
|
|
67
|
+
baseUrl: apiBaseUrl,
|
|
68
|
+
path: `/v1/agents/${encodeURIComponent(agentUid)}/inbox`,
|
|
69
|
+
});
|
|
70
|
+
const body = await res.json().catch(() => ({}));
|
|
71
|
+
return { status: res.status, body };
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export function defaultAckItem(agentUid, apiBaseUrl) {
|
|
75
|
+
return async (token, id) => {
|
|
76
|
+
const res = await vaultApiFetch({
|
|
77
|
+
token,
|
|
78
|
+
baseUrl: apiBaseUrl,
|
|
79
|
+
path: `/v1/agents/${encodeURIComponent(agentUid)}/inbox/${encodeURIComponent(id)}/ack`,
|
|
80
|
+
method: "POST",
|
|
81
|
+
});
|
|
82
|
+
return res.status;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export async function pollInboxOnce(deps) {
|
|
86
|
+
const fetchInbox = deps.fetchInbox ?? defaultFetchInbox(deps.agentUid, deps.apiBaseUrl);
|
|
87
|
+
const ackItem = deps.ackItem ?? defaultAckItem(deps.agentUid, deps.apiBaseUrl);
|
|
88
|
+
const warmCache = deps.warmCache ?? ((token, uid) => warmMeshConversationCache(token, uid));
|
|
89
|
+
let token;
|
|
90
|
+
try {
|
|
91
|
+
token = await deps.getToken();
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
writeComponentStatus(deps.paths, "inbox", "error");
|
|
95
|
+
deps.log("error", `inbox token mint failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
96
|
+
return { ok: false, mirrored: 0 };
|
|
97
|
+
}
|
|
98
|
+
let res;
|
|
99
|
+
try {
|
|
100
|
+
res = await fetchInbox(token);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
writeComponentStatus(deps.paths, "inbox", "error");
|
|
104
|
+
deps.log("error", `inbox fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
105
|
+
return { ok: false, mirrored: 0 };
|
|
106
|
+
}
|
|
107
|
+
if (res.status !== 200) {
|
|
108
|
+
writeComponentStatus(deps.paths, "inbox", "error");
|
|
109
|
+
deps.log("warn", `inbox poll ${res.status}`);
|
|
110
|
+
return { ok: false, mirrored: 0 };
|
|
111
|
+
}
|
|
112
|
+
writeComponentStatus(deps.paths, "inbox", "ok");
|
|
113
|
+
const seen = readSeenIds(deps.paths);
|
|
114
|
+
const fresh = extractInboxItems(res.body).filter((i) => !seen.has(i.id));
|
|
115
|
+
for (const item of fresh) {
|
|
116
|
+
mirrorInboxItem(deps.paths, item);
|
|
117
|
+
seen.add(item.id);
|
|
118
|
+
if (deps.ack) {
|
|
119
|
+
try {
|
|
120
|
+
const status = await ackItem(token, item.id);
|
|
121
|
+
if (status < 200 || status >= 300)
|
|
122
|
+
deps.log("warn", `ack ${item.id} → ${status}`);
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
deps.log("warn", `ack ${item.id} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (fresh.length > 0) {
|
|
130
|
+
writeSeenIds(deps.paths, seen);
|
|
131
|
+
deps.log("info", `inbox mirrored ${fresh.length} item(s)`);
|
|
132
|
+
try {
|
|
133
|
+
await warmCache(token, deps.agentUid);
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
deps.log("warn", `cache refresh failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return { ok: true, mirrored: fresh.length };
|
|
140
|
+
}
|
|
141
|
+
export async function runInboxLoop(deps) {
|
|
142
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
143
|
+
let polls = 0;
|
|
144
|
+
for (;;) {
|
|
145
|
+
await pollInboxOnce(deps);
|
|
146
|
+
polls += 1;
|
|
147
|
+
if (deps.maxPolls !== undefined && polls >= deps.maxPolls)
|
|
148
|
+
return;
|
|
149
|
+
await sleep(deps.pollMs);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=inbox.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kit service: work-mesh doorbell listener.
|
|
3
|
+
*
|
|
4
|
+
* Policy hq-work-mesh-source-of-truth: the work mesh (REST) is the source of
|
|
5
|
+
* truth; MQTT carries IDS-ONLY doorbells on the agent's personal topics
|
|
6
|
+
* `hq/{agt_*}/{dm,work,sessions,notifications}`. This listener is the cache
|
|
7
|
+
* writer: on any doorbell it refetches through the REST API into
|
|
8
|
+
* ~/.hq/work-mesh/cache (warmMeshConversationCache) — message bodies are
|
|
9
|
+
* never taken from MQTT. A periodic refresh covers missed doorbells.
|
|
10
|
+
*
|
|
11
|
+
* Credentials come from the same contract-3 vend the mesh daemon uses
|
|
12
|
+
* (POST /v1/realtime/credentials); the WSS URL is SigV4-presigned. The
|
|
13
|
+
* connection is rebuilt before the vended credentials expire. `component-mesh`
|
|
14
|
+
* is stamped ok while subscribed and refreshing, error otherwise.
|
|
15
|
+
*/
|
|
16
|
+
import { type IClientOptions } from "mqtt";
|
|
17
|
+
import { type CredentialsFetcher } from "../../mesh/live/daemon/credentials.js";
|
|
18
|
+
import type { KitLogger } from "../log.js";
|
|
19
|
+
import type { AgentKitPaths } from "../paths.js";
|
|
20
|
+
export declare const DOORBELL_KINDS: readonly ["dm", "work", "sessions", "notifications"];
|
|
21
|
+
export declare const DOORBELL_DEBOUNCE_MS = 2000;
|
|
22
|
+
export declare const RECONNECT_BASE_MS = 1000;
|
|
23
|
+
export declare const RECONNECT_MAX_MS = 60000;
|
|
24
|
+
export declare function doorbellTopics(actorUid: string): string[];
|
|
25
|
+
/** Minimal mqtt client surface the listener needs (test seam). */
|
|
26
|
+
export interface DoorbellMqttClient {
|
|
27
|
+
on(event: string, handler: (...args: any[]) => void): unknown;
|
|
28
|
+
subscribe(topics: string | string[], opts: {
|
|
29
|
+
qos: 0 | 1 | 2;
|
|
30
|
+
}, callback?: (err: Error | null) => void): unknown;
|
|
31
|
+
end(force?: boolean, opts?: unknown, cb?: () => void): unknown;
|
|
32
|
+
}
|
|
33
|
+
export type DoorbellConnectFn = (url: string, opts: IClientOptions) => DoorbellMqttClient;
|
|
34
|
+
export interface MeshListenerDeps {
|
|
35
|
+
paths: AgentKitPaths;
|
|
36
|
+
agentUid: string;
|
|
37
|
+
apiBaseUrl: string;
|
|
38
|
+
refreshMs: number;
|
|
39
|
+
getToken: () => Promise<string>;
|
|
40
|
+
log: KitLogger;
|
|
41
|
+
fetchCredentials?: CredentialsFetcher;
|
|
42
|
+
connect?: DoorbellConnectFn;
|
|
43
|
+
refetch?: (token: string, actorUid: string) => Promise<unknown>;
|
|
44
|
+
now?: () => Date;
|
|
45
|
+
random?: () => number;
|
|
46
|
+
setTimeout?: (fn: () => void, ms: number) => unknown;
|
|
47
|
+
clearTimeout?: (handle: unknown) => void;
|
|
48
|
+
}
|
|
49
|
+
export interface MeshListenerHandle {
|
|
50
|
+
stop: () => Promise<void>;
|
|
51
|
+
/** Force a cache refetch (tests / doorbell path). */
|
|
52
|
+
refetchNow: (reason: string) => Promise<void>;
|
|
53
|
+
/** Simulate a doorbell (tests). */
|
|
54
|
+
ring: (topic: string) => void;
|
|
55
|
+
state: () => "idle" | "connecting" | "subscribed" | "closed";
|
|
56
|
+
}
|
|
57
|
+
export declare function startMeshListener(deps: MeshListenerDeps): Promise<MeshListenerHandle>;
|
|
58
|
+
//# sourceMappingURL=mesh-listener.d.ts.map
|