@indigoai-us/hq-cli 5.115.6 → 5.117.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.
Files changed (76) hide show
  1. package/CHANGELOG.md +169 -11
  2. package/dist/command-catalog.generated.d.ts +220 -2
  3. package/dist/command-catalog.generated.js +281 -2
  4. package/dist/command-registration-plan.d.ts +6 -0
  5. package/dist/command-registration-plan.js +1 -0
  6. package/dist/commands/agent-enroll.d.ts +105 -0
  7. package/dist/commands/agent-enroll.js +273 -0
  8. package/dist/commands/agent-kit.d.ts +53 -0
  9. package/dist/commands/agent-kit.js +260 -0
  10. package/dist/commands/agent-mcp.d.ts +22 -0
  11. package/dist/commands/agent-mcp.js +104 -0
  12. package/dist/commands/agent-probe.d.ts +71 -0
  13. package/dist/commands/agent-probe.js +294 -0
  14. package/dist/commands/agent.d.ts +12 -0
  15. package/dist/commands/agent.js +23 -0
  16. package/dist/commands/agents.d.ts +27 -0
  17. package/dist/commands/agents.js +280 -6
  18. package/dist/commands/bot.d.ts +140 -1
  19. package/dist/commands/bot.js +757 -22
  20. package/dist/commands/secrets.js +17 -5
  21. package/dist/lib/agent-kit/creds.d.ts +60 -0
  22. package/dist/lib/agent-kit/creds.js +123 -0
  23. package/dist/lib/agent-kit/kit-config.d.ts +29 -0
  24. package/dist/lib/agent-kit/kit-config.js +54 -0
  25. package/dist/lib/agent-kit/log.d.ts +17 -0
  26. package/dist/lib/agent-kit/log.js +46 -0
  27. package/dist/lib/agent-kit/mcp/jsonrpc.d.ts +84 -0
  28. package/dist/lib/agent-kit/mcp/jsonrpc.js +164 -0
  29. package/dist/lib/agent-kit/mcp/tools.d.ts +45 -0
  30. package/dist/lib/agent-kit/mcp/tools.js +280 -0
  31. package/dist/lib/agent-kit/paths.d.ts +42 -0
  32. package/dist/lib/agent-kit/paths.js +56 -0
  33. package/dist/lib/agent-kit/run/heartbeat.d.ts +52 -0
  34. package/dist/lib/agent-kit/run/heartbeat.js +97 -0
  35. package/dist/lib/agent-kit/run/inbox.d.ts +59 -0
  36. package/dist/lib/agent-kit/run/inbox.js +152 -0
  37. package/dist/lib/agent-kit/run/mesh-listener.d.ts +58 -0
  38. package/dist/lib/agent-kit/run/mesh-listener.js +193 -0
  39. package/dist/lib/agent-kit/run/sync.d.ts +33 -0
  40. package/dist/lib/agent-kit/run/sync.js +58 -0
  41. package/dist/lib/agent-kit/services.d.ts +21 -0
  42. package/dist/lib/agent-kit/services.js +46 -0
  43. package/dist/lib/agent-kit/skills.d.ts +18 -0
  44. package/dist/lib/agent-kit/skills.js +149 -0
  45. package/dist/lib/bot/api.d.ts +51 -0
  46. package/dist/lib/bot/api.js +32 -0
  47. package/dist/lib/bot/daemon.d.ts +17 -0
  48. package/dist/lib/bot/daemon.js +44 -3
  49. package/dist/lib/bot/index.d.ts +4 -0
  50. package/dist/lib/bot/index.js +4 -0
  51. package/dist/lib/bot/inflight.d.ts +14 -0
  52. package/dist/lib/bot/local-config.d.ts +70 -0
  53. package/dist/lib/bot/local-config.js +147 -0
  54. package/dist/lib/bot/local-name.d.ts +54 -0
  55. package/dist/lib/bot/local-name.js +114 -0
  56. package/dist/lib/bot/run.d.ts +9 -0
  57. package/dist/lib/bot/run.js +117 -24
  58. package/dist/lib/bot/runnable.d.ts +51 -0
  59. package/dist/lib/bot/runnable.js +65 -0
  60. package/dist/lib/bot/self-heal.d.ts +52 -0
  61. package/dist/lib/bot/self-heal.js +79 -0
  62. package/dist/lib/bot/split.d.ts +32 -0
  63. package/dist/lib/bot/split.js +241 -0
  64. package/dist/lib/service-manager/index.d.ts +43 -0
  65. package/dist/lib/service-manager/index.js +114 -0
  66. package/dist/lib/service-manager/launchd.d.ts +23 -0
  67. package/dist/lib/service-manager/launchd.js +81 -0
  68. package/dist/lib/service-manager/systemd.d.ts +19 -0
  69. package/dist/lib/service-manager/systemd.js +72 -0
  70. package/dist/lib/service-manager/types.d.ts +32 -0
  71. package/dist/lib/service-manager/types.js +26 -0
  72. package/dist/utils/self-update.js +2 -30
  73. package/dist/utils/update-command-supervisor.cjs +194 -0
  74. package/dist/utils/version-gate.d.ts +18 -0
  75. package/dist/utils/version-gate.js +126 -7
  76. package/package.json +2 -2
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The `hq agent mcp` tool surface. Each tool wraps an existing hq CLI
3
+ * capability so a bot framework (OpenClaw, grokbot, any MCP client)
4
+ * can act as the enrolled agent without a token ever crossing the MCP
5
+ * channel: the machine identity lives in machine-creds.json and every call
6
+ * mints through hq-cloud.
7
+ *
8
+ * Two seams keep this testable and honest:
9
+ * - `runHq(args)` — spawn the SAME hq binary for capabilities that are
10
+ * already complete CLI commands (search, files, secrets exec, work mesh).
11
+ * - `api` — token + vault-API fetch for the notify surface (DM, inbox).
12
+ *
13
+ * `hq_secrets_exec` runs a command with secrets injected and returns ONLY
14
+ * the command's exit code and (bounded) output — never an environment dump,
15
+ * never a secret value. `hq_secrets_list` names secrets; values are never a
16
+ * tool result.
17
+ */
18
+ import type { ExternalMachineCreds } from "../creds.js";
19
+ import { type McpTool } from "./jsonrpc.js";
20
+ export interface HqRunResult {
21
+ code: number;
22
+ stdout: string;
23
+ stderr: string;
24
+ }
25
+ export interface McpToolClients {
26
+ creds: ExternalMachineCreds;
27
+ /** Run `hq <args…>` as the machine identity; never throws. */
28
+ runHq: (args: string[]) => Promise<HqRunResult>;
29
+ getToken: () => Promise<string>;
30
+ /** Authenticated JSON call against the control plane. */
31
+ apiJson: (token: string, path: string, init?: {
32
+ method?: string;
33
+ body?: Record<string, unknown>;
34
+ query?: Record<string, string>;
35
+ }) => Promise<{
36
+ status: number;
37
+ body: unknown;
38
+ }>;
39
+ }
40
+ export declare const MAX_TOOL_OUTPUT_CHARS = 40000;
41
+ export declare function clampOutput(text: string, max?: number): string;
42
+ /** Vault paths are company-anchored and must not escape. */
43
+ export declare function assertVaultPath(p: string): string;
44
+ export declare function buildAgentMcpTools(clients: McpToolClients): McpTool[];
45
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +1,280 @@
1
+ /**
2
+ * The `hq agent mcp` tool surface. Each tool wraps an existing hq CLI
3
+ * capability so a bot framework (OpenClaw, grokbot, any MCP client)
4
+ * can act as the enrolled agent without a token ever crossing the MCP
5
+ * channel: the machine identity lives in machine-creds.json and every call
6
+ * mints through hq-cloud.
7
+ *
8
+ * Two seams keep this testable and honest:
9
+ * - `runHq(args)` — spawn the SAME hq binary for capabilities that are
10
+ * already complete CLI commands (search, files, secrets exec, work mesh).
11
+ * - `api` — token + vault-API fetch for the notify surface (DM, inbox).
12
+ *
13
+ * `hq_secrets_exec` runs a command with secrets injected and returns ONLY
14
+ * the command's exit code and (bounded) output — never an environment dump,
15
+ * never a secret value. `hq_secrets_list` names secrets; values are never a
16
+ * tool result.
17
+ */
18
+ import { CLI_VERSION } from "../../../cli-version.js";
19
+ import { peekIdToken } from "../../../utils/id-token.js";
20
+ import { errorResult, McpToolInputError, textResult } from "./jsonrpc.js";
21
+ export const MAX_TOOL_OUTPUT_CHARS = 40_000;
22
+ function str(args, key, opts = {}) {
23
+ const v = args[key];
24
+ if (v === undefined || v === null) {
25
+ if (opts.required)
26
+ throw new McpToolInputError(`"${key}" is required`);
27
+ return undefined;
28
+ }
29
+ if (typeof v !== "string")
30
+ throw new McpToolInputError(`"${key}" must be a string`);
31
+ if (opts.required && v.trim().length === 0)
32
+ throw new McpToolInputError(`"${key}" must not be empty`);
33
+ if (opts.max !== undefined && v.length > opts.max)
34
+ throw new McpToolInputError(`"${key}" exceeds ${opts.max} characters`);
35
+ return v;
36
+ }
37
+ function int(args, key, opts) {
38
+ const v = args[key];
39
+ if (v === undefined || v === null)
40
+ return opts.fallback;
41
+ if (typeof v !== "number" || !Number.isInteger(v))
42
+ throw new McpToolInputError(`"${key}" must be an integer`);
43
+ return Math.min(opts.max, Math.max(opts.min, v));
44
+ }
45
+ function strList(args, key) {
46
+ const v = args[key];
47
+ if (!Array.isArray(v) || v.length === 0 || !v.every((x) => typeof x === "string" && x.length > 0)) {
48
+ throw new McpToolInputError(`"${key}" must be a non-empty array of strings`);
49
+ }
50
+ return v;
51
+ }
52
+ export function clampOutput(text, max = MAX_TOOL_OUTPUT_CHARS) {
53
+ return text.length > max ? `${text.slice(0, max)}\n… [truncated ${text.length - max} chars]` : text;
54
+ }
55
+ /** Vault paths are company-anchored and must not escape. */
56
+ export function assertVaultPath(p) {
57
+ const clean = p.trim().replace(/^\/+/, "");
58
+ if (!/^companies\/[a-z0-9][a-z0-9-]*(\/[^\0]*)?$/i.test(clean) || clean.split("/").includes("..")) {
59
+ throw new McpToolInputError(`"path" must be vault-relative and start with companies/<slug>/ (got ${JSON.stringify(p)})`);
60
+ }
61
+ return clean;
62
+ }
63
+ const SECRET_NAME = /^[A-Z][A-Z0-9_]*(\/[A-Z][A-Z0-9_]+)*$/;
64
+ function fromRun(r, label) {
65
+ const out = [r.stdout.trim(), r.stderr.trim()].filter(Boolean).join("\n");
66
+ if (r.code !== 0)
67
+ return errorResult(clampOutput(`${label} failed (exit ${r.code})\n${out}`));
68
+ return textResult(clampOutput(out || `${label}: (no output)`));
69
+ }
70
+ export function buildAgentMcpTools(clients) {
71
+ const { creds } = clients;
72
+ const company = creds.companySlug;
73
+ return [
74
+ {
75
+ name: "hq_whoami",
76
+ description: "Identity this MCP server acts as: the enrolled external agent, its company, and the control plane.",
77
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
78
+ handler: async () => {
79
+ let tokenOk;
80
+ let claimUid;
81
+ try {
82
+ const claims = peekIdToken(await clients.getToken());
83
+ claimUid = typeof claims["custom:entityUid"] === "string" ? claims["custom:entityUid"] : undefined;
84
+ tokenOk = claimUid === creds.entityUid;
85
+ }
86
+ catch {
87
+ tokenOk = false;
88
+ }
89
+ return textResult(JSON.stringify({
90
+ agentUid: creds.entityUid,
91
+ runtime: "external",
92
+ companySlug: company,
93
+ apiBaseUrl: creds.apiBaseUrl,
94
+ cliVersion: CLI_VERSION,
95
+ session: tokenOk ? "ok" : `unavailable${claimUid ? ` (token names ${claimUid})` : ""}`,
96
+ }, null, 2));
97
+ },
98
+ },
99
+ {
100
+ name: "hq_search",
101
+ description: "Search the synced company vault (local qmd index). Modes: keyword (default), semantic, hybrid.",
102
+ inputSchema: {
103
+ type: "object",
104
+ properties: {
105
+ query: { type: "string", description: "Search text" },
106
+ mode: { type: "string", enum: ["keyword", "semantic", "hybrid"] },
107
+ count: { type: "integer", minimum: 1, maximum: 50 },
108
+ },
109
+ required: ["query"],
110
+ additionalProperties: false,
111
+ },
112
+ handler: async (args) => {
113
+ const query = str(args, "query", { required: true, max: 2000 });
114
+ const mode = str(args, "mode");
115
+ if (mode !== undefined && !["keyword", "semantic", "hybrid"].includes(mode)) {
116
+ throw new McpToolInputError('"mode" must be keyword, semantic or hybrid');
117
+ }
118
+ const count = int(args, "count", { min: 1, max: 50, fallback: 10 });
119
+ const hqArgs = ["search", query, "--mode", mode ?? "keyword", "-n", String(count), "--json"];
120
+ return fromRun(await clients.runHq(hqArgs), "hq search");
121
+ },
122
+ },
123
+ {
124
+ name: "hq_files_list",
125
+ description: "List vault objects under a company path without syncing them (hq files browse).",
126
+ inputSchema: {
127
+ type: "object",
128
+ properties: { path: { type: "string", description: "companies/<slug>/<folder>" } },
129
+ required: ["path"],
130
+ additionalProperties: false,
131
+ },
132
+ handler: async (args) => {
133
+ const p = assertVaultPath(str(args, "path", { required: true, max: 1000 }));
134
+ return fromRun(await clients.runHq(["files", "browse", p]), "hq files browse");
135
+ },
136
+ },
137
+ {
138
+ name: "hq_files_read",
139
+ description: "Read one vault file's contents (hq files cat). Text files only; output is truncated at 40k chars.",
140
+ inputSchema: {
141
+ type: "object",
142
+ properties: { path: { type: "string", description: "companies/<slug>/<file>" } },
143
+ required: ["path"],
144
+ additionalProperties: false,
145
+ },
146
+ handler: async (args) => {
147
+ const p = assertVaultPath(str(args, "path", { required: true, max: 1000 }));
148
+ return fromRun(await clients.runHq(["files", "cat", p]), "hq files cat");
149
+ },
150
+ },
151
+ {
152
+ name: "hq_secrets_list",
153
+ description: "List the NAMES of company secrets this agent can use. Values are never returned.",
154
+ inputSchema: {
155
+ type: "object",
156
+ properties: { prefix: { type: "string", description: "Optional path prefix, e.g. DEV" } },
157
+ additionalProperties: false,
158
+ },
159
+ handler: async (args) => {
160
+ const prefix = str(args, "prefix", { max: 200 });
161
+ const hqArgs = ["secrets", "--company", company, "list"];
162
+ if (prefix)
163
+ hqArgs.push("--prefix", prefix);
164
+ return fromRun(await clients.runHq(hqArgs), "hq secrets list");
165
+ },
166
+ },
167
+ {
168
+ name: "hq_secrets_exec",
169
+ description: "Run a command with the named company secrets injected as environment variables. " +
170
+ "Returns the command's exit code and output only — never the secret values. " +
171
+ "Do not run commands that print their environment.",
172
+ inputSchema: {
173
+ type: "object",
174
+ properties: {
175
+ secrets: { type: "array", items: { type: "string" }, description: "Secret names to inject (UPPER_SNAKE, optional / path)" },
176
+ command: { type: "array", items: { type: "string" }, description: "argv of the command to run" },
177
+ },
178
+ required: ["secrets", "command"],
179
+ additionalProperties: false,
180
+ },
181
+ handler: async (args) => {
182
+ const secrets = strList(args, "secrets");
183
+ for (const s of secrets) {
184
+ if (!SECRET_NAME.test(s))
185
+ throw new McpToolInputError(`invalid secret name ${JSON.stringify(s)}`);
186
+ }
187
+ const command = strList(args, "command");
188
+ if (/^(env|printenv|export|set)$/.test(command[0])) {
189
+ throw new McpToolInputError("refusing to run an environment-dumping command under hq_secrets_exec");
190
+ }
191
+ const r = await clients.runHq(["secrets", "--company", company, "exec", "--only", secrets.join(","), "--", ...command]);
192
+ const out = [r.stdout.trim(), r.stderr.trim()].filter(Boolean).join("\n");
193
+ const text = clampOutput(`exit ${r.code}\n${out}`);
194
+ return r.code === 0 ? textResult(text) : errorResult(text);
195
+ },
196
+ },
197
+ {
198
+ name: "hq_dm_send",
199
+ description: "Send an HQ direct message as this agent to a person (email or prs_…) or agent (agt_…).",
200
+ inputSchema: {
201
+ type: "object",
202
+ properties: {
203
+ to: { type: "string", description: "Email, prs_… or agt_… uid" },
204
+ message: { type: "string" },
205
+ },
206
+ required: ["to", "message"],
207
+ additionalProperties: false,
208
+ },
209
+ handler: async (args) => {
210
+ const to = str(args, "to", { required: true, max: 320 }).trim();
211
+ const message = str(args, "message", { required: true, max: 20_000 }).trim();
212
+ const body = /^[^\s]+@[^\s]+$/.test(to)
213
+ ? { toEmail: to.toLowerCase(), body: message }
214
+ : /^(prs|agt)_[A-Za-z0-9_-]+$/.test(to)
215
+ ? { toPersonUid: to, body: message }
216
+ : (() => {
217
+ throw new McpToolInputError('"to" must be an email, prs_… or agt_… uid');
218
+ })();
219
+ const token = await clients.getToken();
220
+ const res = await clients.apiJson(token, "/v1/notify/dm", { method: "POST", body });
221
+ if (res.status < 200 || res.status >= 300) {
222
+ return errorResult(`DM send failed (${res.status}): ${describeError(res.body)}`);
223
+ }
224
+ return textResult(`sent to ${to}`);
225
+ },
226
+ },
227
+ {
228
+ name: "hq_inbox_read",
229
+ description: "Read this agent's recent incoming direct messages (newest first). Optionally only unread.",
230
+ inputSchema: {
231
+ type: "object",
232
+ properties: {
233
+ limit: { type: "integer", minimum: 1, maximum: 100 },
234
+ unread_only: { type: "boolean" },
235
+ },
236
+ additionalProperties: false,
237
+ },
238
+ handler: async (args) => {
239
+ const limit = int(args, "limit", { min: 1, max: 100, fallback: 20 });
240
+ const unreadOnly = args.unread_only === true;
241
+ const token = await clients.getToken();
242
+ const res = await clients.apiJson(token, "/v1/notify/inbox", { query: { limit: String(limit) } });
243
+ if (res.status !== 200)
244
+ return errorResult(`inbox read failed (${res.status}): ${describeError(res.body)}`);
245
+ const events = Array.isArray(res.body?.events)
246
+ ? (res.body.events)
247
+ : [];
248
+ const rows = events
249
+ .filter((e) => !unreadOnly || !e.acknowledgedAt)
250
+ .map((e) => ({
251
+ eventId: e.eventId,
252
+ from: e.fromDisplayName ?? e.fromEmail ?? e.fromPersonUid ?? "unknown",
253
+ fromUid: e.fromPersonUid,
254
+ at: e.createdAt,
255
+ unread: !e.acknowledgedAt,
256
+ body: e.body,
257
+ ...(e.prompt ? { prompt: e.prompt } : {}),
258
+ }));
259
+ return textResult(clampOutput(JSON.stringify({ count: rows.length, messages: rows }, null, 2)));
260
+ },
261
+ },
262
+ {
263
+ name: "hq_work_mesh_status",
264
+ description: "Live read of what the team is working on right now (work-mesh session status for this agent's company).",
265
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
266
+ handler: async () => fromRun(await clients.runHq(["mesh", "session", "status", "--company", company, "--json"]), "hq mesh session status"),
267
+ },
268
+ ];
269
+ }
270
+ function describeError(body) {
271
+ if (body && typeof body === "object") {
272
+ const b = body;
273
+ const code = typeof b.code === "string" ? `${b.code}: ` : "";
274
+ const msg = typeof b.message === "string" ? b.message : typeof b.error === "string" ? b.error : "";
275
+ if (code || msg)
276
+ return `${code}${msg}`;
277
+ }
278
+ return "request failed";
279
+ }
280
+ //# sourceMappingURL=tools.js.map
@@ -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