@indigoai-us/hq-cli 5.118.2 → 5.119.1
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 +46 -0
- package/dist/command-catalog.generated.d.ts +7 -1
- package/dist/command-catalog.generated.js +9 -1
- package/dist/commands/agent-kit.d.ts +7 -0
- package/dist/commands/agent-kit.js +32 -10
- package/dist/commands/agent-mcp.js +2 -0
- package/dist/commands/agent-probe.d.ts +6 -2
- package/dist/commands/agent-probe.js +48 -20
- package/dist/lib/agent-kit/kit-config.d.ts +33 -1
- package/dist/lib/agent-kit/kit-config.js +11 -0
- package/dist/lib/agent-kit/mcp/tools.d.ts +7 -0
- package/dist/lib/agent-kit/mcp/tools.js +11 -1
- package/dist/lib/agent-kit/run/heartbeat.d.ts +13 -3
- package/dist/lib/agent-kit/run/heartbeat.js +6 -4
- package/dist/lib/agent-kit/services.d.ts +12 -1
- package/dist/lib/agent-kit/services.js +12 -2
- package/dist/lib/agent-kit/skills.js +30 -14
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.119.1] — 2026-09-17
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- A kit.json written before `syncFiles` existed now reads as NOT syncing, the
|
|
10
|
+
same as a fresh install. 5.119.0 read it as `true` to preserve the behaviour
|
|
11
|
+
it recorded, which was wrong twice over: the bots with such a file are
|
|
12
|
+
exactly the ones the mirror is failing on, and because a bare reinstall keeps
|
|
13
|
+
recorded settings, the documented fix — reinstall the kit — left the sync
|
|
14
|
+
loop running and `hq agent probe` still pulling the whole vault. A reinstall
|
|
15
|
+
now removes that loop. A bot that genuinely wants the local mirror passes
|
|
16
|
+
`--sync-files` once.
|
|
17
|
+
|
|
18
|
+
## [5.119.0] — 2026-09-17
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- External agents no longer copy the company vault onto their own machine.
|
|
23
|
+
`hq agent kit install` used to install a sync loop that mirrored the whole
|
|
24
|
+
vault to disk; a bot reads and writes vault files on demand through the `hq`
|
|
25
|
+
MCP server and the `hq files` commands, so the copy bought nothing for the
|
|
26
|
+
common case and cost a full download of the company's assets. On a team with
|
|
27
|
+
a large brand library that is hundreds of megabytes, and on a sandboxed bot
|
|
28
|
+
host whose egress truncates large responses it could never finish at all —
|
|
29
|
+
every file retried three times and failed with `ERR_HQ_TRUNCATED_RESPONSE`,
|
|
30
|
+
which left the probe stuck and the agent reporting a sync error while it was
|
|
31
|
+
otherwise a healthy team member.
|
|
32
|
+
|
|
33
|
+
Pass `--sync-files` if the bot genuinely needs files on disk for shell-based
|
|
34
|
+
work; a reinstall without it removes a sync loop an earlier install left
|
|
35
|
+
behind. An existing install is unchanged until it is reinstalled.
|
|
36
|
+
|
|
37
|
+
- `hq agent probe` reports a `files` check in place of `team-sync`. Without a
|
|
38
|
+
local copy it proves the read path the bot actually uses — a vault listing
|
|
39
|
+
answers — instead of downloading the vault to prove a folder exists. With
|
|
40
|
+
`--sync-files` it checks the pull exactly as before.
|
|
41
|
+
|
|
42
|
+
- The heartbeat reports only the components the kit runs. A component that was
|
|
43
|
+
never installed is omitted rather than reported as an error, so a bot with no
|
|
44
|
+
sync loop no longer looks broken.
|
|
45
|
+
|
|
46
|
+
- `hq_search` no longer runs a local index search on an agent that has no local
|
|
47
|
+
copy, where it could only ever return nothing. It runs the server-side vault
|
|
48
|
+
search instead, and says plainly that content and semantic search need a kit
|
|
49
|
+
installed with `--sync-files`.
|
|
50
|
+
|
|
5
51
|
## [5.118.2] — 2026-09-17
|
|
6
52
|
|
|
7
53
|
### Changed
|
|
@@ -4181,7 +4181,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
4181
4181
|
readonly options: readonly [];
|
|
4182
4182
|
readonly subcommands: readonly [{
|
|
4183
4183
|
readonly name: "install";
|
|
4184
|
-
readonly description: "Install
|
|
4184
|
+
readonly description: "Install the work-mesh, inbox and heartbeat services plus the skills directory";
|
|
4185
4185
|
readonly aliases: readonly [];
|
|
4186
4186
|
readonly hidden: false;
|
|
4187
4187
|
readonly usage: "[options]";
|
|
@@ -4189,6 +4189,12 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
4189
4189
|
readonly options: readonly [{
|
|
4190
4190
|
readonly flags: "--hq-root <path>";
|
|
4191
4191
|
readonly description: "Local HQ tree to sync the company vault into (default: ~/.hq-agent/hq)";
|
|
4192
|
+
}, {
|
|
4193
|
+
readonly flags: "--sync-files";
|
|
4194
|
+
readonly description: "Also mirror the company vault to disk. Off by default: files are read on demand through the hq MCP server";
|
|
4195
|
+
}, {
|
|
4196
|
+
readonly flags: "--no-sync-files";
|
|
4197
|
+
readonly description: "Do not mirror the vault to disk (default; removes the loop if an earlier install added it)";
|
|
4192
4198
|
}, {
|
|
4193
4199
|
readonly flags: "--inbox-ack";
|
|
4194
4200
|
readonly description: "Ack mirrored inbox items on the server (default: leave acking to the bot)";
|
|
@@ -5410,7 +5410,7 @@ export const COMMAND_CATALOG = [
|
|
|
5410
5410
|
"subcommands": [
|
|
5411
5411
|
{
|
|
5412
5412
|
"name": "install",
|
|
5413
|
-
"description": "Install
|
|
5413
|
+
"description": "Install the work-mesh, inbox and heartbeat services plus the skills directory",
|
|
5414
5414
|
"aliases": [],
|
|
5415
5415
|
"hidden": false,
|
|
5416
5416
|
"usage": "[options]",
|
|
@@ -5420,6 +5420,14 @@ export const COMMAND_CATALOG = [
|
|
|
5420
5420
|
"flags": "--hq-root <path>",
|
|
5421
5421
|
"description": "Local HQ tree to sync the company vault into (default: ~/.hq-agent/hq)"
|
|
5422
5422
|
},
|
|
5423
|
+
{
|
|
5424
|
+
"flags": "--sync-files",
|
|
5425
|
+
"description": "Also mirror the company vault to disk. Off by default: files are read on demand through the hq MCP server"
|
|
5426
|
+
},
|
|
5427
|
+
{
|
|
5428
|
+
"flags": "--no-sync-files",
|
|
5429
|
+
"description": "Do not mirror the vault to disk (default; removes the loop if an earlier install added it)"
|
|
5430
|
+
},
|
|
5423
5431
|
{
|
|
5424
5432
|
"flags": "--inbox-ack",
|
|
5425
5433
|
"description": "Ack mirrored inbox items on the server (default: leave acking to the bot)"
|
|
@@ -35,6 +35,13 @@ export interface KitInstallOptions {
|
|
|
35
35
|
hqRoot?: string;
|
|
36
36
|
inboxAck?: boolean;
|
|
37
37
|
activate?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Mirror the company vault onto local disk. Off unless asked for: a bot
|
|
40
|
+
* reads and writes vault files on demand through the `hq` MCP server, so the
|
|
41
|
+
* sync loop is not installed at all rather than installed and left failing
|
|
42
|
+
* on a host that cannot finish a full-copy download.
|
|
43
|
+
*/
|
|
44
|
+
syncFiles?: boolean;
|
|
38
45
|
}
|
|
39
46
|
export interface KitFallbackInfo {
|
|
40
47
|
mode: "fallback";
|
|
@@ -22,10 +22,10 @@ import * as os from "node:os";
|
|
|
22
22
|
import { CLI_VERSION } from "../cli-version.js";
|
|
23
23
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
24
24
|
import { readExternalMachineCreds, } from "../lib/agent-kit/creds.js";
|
|
25
|
-
import { defaultKitConfig, readKitConfig, writeKitConfig, } from "../lib/agent-kit/kit-config.js";
|
|
25
|
+
import { defaultKitConfig, enabledKitComponents, readKitConfig, writeKitConfig, } from "../lib/agent-kit/kit-config.js";
|
|
26
26
|
import { createKitLogger } from "../lib/agent-kit/log.js";
|
|
27
27
|
import { agentKitPaths } from "../lib/agent-kit/paths.js";
|
|
28
|
-
import { isKitService, KIT_SERVICES, kitServiceSpecs, } from "../lib/agent-kit/services.js";
|
|
28
|
+
import { isKitService, KIT_SERVICES, enabledKitServices, kitServiceSpec, kitServiceSpecs, } from "../lib/agent-kit/services.js";
|
|
29
29
|
import { writeKitSkills } from "../lib/agent-kit/skills.js";
|
|
30
30
|
import { runHeartbeatLoop } from "../lib/agent-kit/run/heartbeat.js";
|
|
31
31
|
import { pollInboxOnce, runInboxLoop } from "../lib/agent-kit/run/inbox.js";
|
|
@@ -76,13 +76,24 @@ export function installKit(paths, opts, host, deps = {}, fallbackDeps = {}) {
|
|
|
76
76
|
...existing,
|
|
77
77
|
...(opts.hqRoot ? { hqRoot: opts.hqRoot } : {}),
|
|
78
78
|
...(opts.inboxAck !== undefined ? { inboxAck: opts.inboxAck } : {}),
|
|
79
|
+
...(opts.syncFiles !== undefined ? { syncFiles: opts.syncFiles } : {}),
|
|
79
80
|
cliVersion: CLI_VERSION,
|
|
80
81
|
};
|
|
81
82
|
fs.mkdirSync(config.hqRoot, { recursive: true, mode: 0o700 });
|
|
82
83
|
writeKitConfig(paths, config);
|
|
83
84
|
const skills = writeKitSkills(paths);
|
|
84
85
|
const activate = opts.activate ?? deps.activate ?? true;
|
|
85
|
-
|
|
86
|
+
// A reinstall that turns file sync off must also remove the unit an earlier
|
|
87
|
+
// install left behind, or the loop keeps running and keeps failing.
|
|
88
|
+
if (!config.syncFiles) {
|
|
89
|
+
try {
|
|
90
|
+
uninstallServices([kitServiceSpec("sync", paths)], host, deps);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
/* best effort: an absent unit is the desired end state either way */
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const services = installServices(kitServiceSpecs(paths, { syncFiles: config.syncFiles }), host, {
|
|
86
97
|
...deps,
|
|
87
98
|
activate,
|
|
88
99
|
});
|
|
@@ -149,6 +160,7 @@ export async function runKitService(service, rt) {
|
|
|
149
160
|
intervalMs: rt.config.heartbeatIntervalMs,
|
|
150
161
|
getToken: rt.getToken,
|
|
151
162
|
log: rt.log,
|
|
163
|
+
components: enabledKitComponents(rt.config),
|
|
152
164
|
});
|
|
153
165
|
return;
|
|
154
166
|
case "inbox":
|
|
@@ -210,9 +222,10 @@ export async function runKitService(service, rt) {
|
|
|
210
222
|
}
|
|
211
223
|
/** `kit run all`: supervise the four services as child processes with restart backoff. */
|
|
212
224
|
export async function runAllKitServices(rt, host) {
|
|
213
|
-
|
|
225
|
+
const services = enabledKitServices({ syncFiles: rt.config.syncFiles });
|
|
226
|
+
rt.log("info", `supervisor starting ${services.join(",")} agent=${rt.creds.entityUid} cli=${CLI_VERSION}`);
|
|
214
227
|
const handle = superviseKitServices({
|
|
215
|
-
services
|
|
228
|
+
services,
|
|
216
229
|
log: rt.log,
|
|
217
230
|
spawnService: (service) => {
|
|
218
231
|
const fd = fs.openSync(serviceLogPath(rt.paths, service), "a", 0o600);
|
|
@@ -265,21 +278,30 @@ export function registerAgentKitCommand(agent) {
|
|
|
265
278
|
.description("Install, inspect, or run the external-agent background services");
|
|
266
279
|
kit
|
|
267
280
|
.command("install")
|
|
268
|
-
.description("Install
|
|
281
|
+
.description("Install the work-mesh, inbox and heartbeat services plus the skills directory")
|
|
269
282
|
.option("--hq-root <path>", "Local HQ tree to sync the company vault into (default: ~/.hq-agent/hq)")
|
|
283
|
+
.option("--sync-files", "Also mirror the company vault to disk. Off by default: files are read on demand through the hq MCP server")
|
|
284
|
+
.option("--no-sync-files", "Do not mirror the vault to disk (default; removes the loop if an earlier install added it)")
|
|
270
285
|
.option("--inbox-ack", "Ack mirrored inbox items on the server (default: leave acking to the bot)")
|
|
271
286
|
.option("--no-activate", "Write unit files but do not load them")
|
|
272
287
|
.option("--json", "Print machine-readable JSON")
|
|
273
288
|
.action((opts) => {
|
|
274
289
|
try {
|
|
275
290
|
const paths = agentKitPaths(os.homedir(), process.env);
|
|
276
|
-
const result = installKit(paths, {
|
|
291
|
+
const result = installKit(paths, {
|
|
292
|
+
hqRoot: opts.hqRoot,
|
|
293
|
+
inboxAck: opts.inboxAck,
|
|
294
|
+
syncFiles: opts.syncFiles,
|
|
295
|
+
activate: opts.activate,
|
|
296
|
+
}, resolveServiceHost(paths));
|
|
277
297
|
if (opts.json) {
|
|
278
298
|
console.log(JSON.stringify({ ok: true, ...result }, null, 2));
|
|
279
299
|
return;
|
|
280
300
|
}
|
|
281
301
|
console.log(chalk.green("HQ agent kit installed."));
|
|
282
|
-
console.log(
|
|
302
|
+
console.log(result.config.syncFiles
|
|
303
|
+
? ` files: mirrored to ${result.config.hqRoot}`
|
|
304
|
+
: " files: read on demand through the hq MCP server (no local copy)");
|
|
283
305
|
console.log(` logs: ${paths.logsDir}`);
|
|
284
306
|
console.log(` skills: ${paths.skillsDir} (${result.skills.length} skills)`);
|
|
285
307
|
if (result.fallback) {
|
|
@@ -310,7 +332,7 @@ export function registerAgentKitCommand(agent) {
|
|
|
310
332
|
try {
|
|
311
333
|
const paths = agentKitPaths(os.homedir(), process.env);
|
|
312
334
|
const fallback = fallbackStatus(paths);
|
|
313
|
-
const result = servicesStatus(kitServiceSpecs(paths), resolveServiceHost(paths));
|
|
335
|
+
const result = servicesStatus(kitServiceSpecs(paths, { syncFiles: true }), resolveServiceHost(paths));
|
|
314
336
|
if (opts.json) {
|
|
315
337
|
console.log(JSON.stringify({ ok: true, ...result, ...(fallback ? { fallback } : {}) }, null, 2));
|
|
316
338
|
return;
|
|
@@ -335,7 +357,7 @@ export function registerAgentKitCommand(agent) {
|
|
|
335
357
|
try {
|
|
336
358
|
const paths = agentKitPaths(os.homedir(), process.env);
|
|
337
359
|
const fallback = stopFallback(paths);
|
|
338
|
-
const result = uninstallServices(kitServiceSpecs(paths), resolveServiceHost(paths));
|
|
360
|
+
const result = uninstallServices(kitServiceSpecs(paths, { syncFiles: true }), resolveServiceHost(paths));
|
|
339
361
|
if (opts.json) {
|
|
340
362
|
console.log(JSON.stringify({ ok: true, ...result, fallback }, null, 2));
|
|
341
363
|
return;
|
|
@@ -15,6 +15,7 @@ import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
|
15
15
|
import { vaultApiFetch } from "../utils/vault-api.js";
|
|
16
16
|
import { McpServer } from "../lib/agent-kit/mcp/jsonrpc.js";
|
|
17
17
|
import { buildAgentMcpTools } from "../lib/agent-kit/mcp/tools.js";
|
|
18
|
+
import { readKitConfig } from "../lib/agent-kit/kit-config.js";
|
|
18
19
|
import { agentKitPaths } from "../lib/agent-kit/paths.js";
|
|
19
20
|
import { requireExternalCreds } from "./agent-kit.js";
|
|
20
21
|
export const AGENT_MCP_SERVER_NAME = "hq-agent";
|
|
@@ -56,6 +57,7 @@ export function defaultMcpClients(creds, credsPath, paths = agentKitPaths(os.hom
|
|
|
56
57
|
return {
|
|
57
58
|
creds,
|
|
58
59
|
paths,
|
|
60
|
+
syncFiles: readKitConfig(agentKitPaths(os.homedir(), process.env), CLI_VERSION).syncFiles,
|
|
59
61
|
runHq: defaultRunHq(process.execPath, process.argv[1], env),
|
|
60
62
|
getToken: () => ensureCognitoToken({ tokenSource: "machine", interactive: false }),
|
|
61
63
|
apiJson: async (token, path, init) => {
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* team member, one line per check, non-zero exit on any failure:
|
|
4
4
|
*
|
|
5
5
|
* whoami mint as the machine identity; token names this agent
|
|
6
|
-
*
|
|
6
|
+
* files a vault listing answers on demand (or, with --sync-files, a
|
|
7
|
+
* pull succeeds and the company folder exists)
|
|
7
8
|
* work-mesh the personal (contract-2) realtime vend names this agent, the
|
|
8
9
|
* agent-authorized inbox route answers 200, and this host's own
|
|
9
10
|
* presence is live: a fresh last-heartbeat.json plus an ok
|
|
@@ -46,7 +47,10 @@ export interface ProbeDeps {
|
|
|
46
47
|
hqRoot: string;
|
|
47
48
|
getToken: () => Promise<string>;
|
|
48
49
|
resolveCompanyUid: (token: string, slug: string) => Promise<string>;
|
|
49
|
-
|
|
50
|
+
/** Runs the `hq` CLI with these args as a child; resolves with its exit code. */
|
|
51
|
+
runHq: (args: string[]) => Promise<number>;
|
|
52
|
+
/** Mirror the vault to disk. Off by default: files are read through the API. */
|
|
53
|
+
syncFiles?: boolean;
|
|
50
54
|
vendRealtime: (token: string) => Promise<{
|
|
51
55
|
actorUid: string;
|
|
52
56
|
}>;
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* team member, one line per check, non-zero exit on any failure:
|
|
4
4
|
*
|
|
5
5
|
* whoami mint as the machine identity; token names this agent
|
|
6
|
-
*
|
|
6
|
+
* files a vault listing answers on demand (or, with --sync-files, a
|
|
7
|
+
* pull succeeds and the company folder exists)
|
|
7
8
|
* work-mesh the personal (contract-2) realtime vend names this agent, the
|
|
8
9
|
* agent-authorized inbox route answers 200, and this host's own
|
|
9
10
|
* presence is live: a fresh last-heartbeat.json plus an ok
|
|
@@ -91,29 +92,55 @@ export async function runProbe(deps) {
|
|
|
91
92
|
checks.push({ name: "company", ok: false, detail: `cannot resolve ${deps.creds.companySlug}: ${errText(err)}` });
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
|
-
// 2.
|
|
95
|
+
// 2. files
|
|
96
|
+
//
|
|
97
|
+
// Two shapes, because the kit has two. With file sync off (the default) the
|
|
98
|
+
// bot reads and writes vault files on demand, so what has to be proven is
|
|
99
|
+
// that a vault listing answers — not that a full local copy exists. Pulling
|
|
100
|
+
// the whole vault here would also make the probe fail for a reason that does
|
|
101
|
+
// not affect the bot: a team with a large asset library is hundreds of
|
|
102
|
+
// megabytes, and a sandboxed bot host whose egress truncates large responses
|
|
103
|
+
// can never finish it.
|
|
95
104
|
if (token) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
105
|
+
if (deps.syncFiles) {
|
|
106
|
+
try {
|
|
107
|
+
const code = await deps.runHq(syncPullArgs(deps.hqRoot));
|
|
108
|
+
const companyDir = path.join(deps.hqRoot, "companies", deps.creds.companySlug);
|
|
109
|
+
const present = fs.existsSync(companyDir);
|
|
110
|
+
const ok = code === 0 && present;
|
|
111
|
+
checks.push({
|
|
112
|
+
name: "files",
|
|
113
|
+
ok,
|
|
114
|
+
detail: ok
|
|
115
|
+
? `synced into ${companyDir}`
|
|
116
|
+
: code !== 0
|
|
117
|
+
? `hq sync pull exited ${code}`
|
|
118
|
+
: `sync exited 0 but ${companyDir} is missing`,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
checks.push({ name: "files", ok: false, detail: errText(err) });
|
|
123
|
+
}
|
|
110
124
|
}
|
|
111
|
-
|
|
112
|
-
|
|
125
|
+
else {
|
|
126
|
+
const target = `companies/${deps.creds.companySlug}`;
|
|
127
|
+
try {
|
|
128
|
+
const code = await deps.runHq(["files", "browse", target]);
|
|
129
|
+
checks.push({
|
|
130
|
+
name: "files",
|
|
131
|
+
ok: code === 0,
|
|
132
|
+
detail: code === 0
|
|
133
|
+
? `${target} is readable on demand (no local copy)`
|
|
134
|
+
: `hq files browse ${target} exited ${code}`,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
checks.push({ name: "files", ok: false, detail: errText(err) });
|
|
139
|
+
}
|
|
113
140
|
}
|
|
114
141
|
}
|
|
115
142
|
else {
|
|
116
|
-
checks.push({ name: "
|
|
143
|
+
checks.push({ name: "files", ok: false, detail: "skipped: no token" });
|
|
117
144
|
}
|
|
118
145
|
// 3. work-mesh: vend + agent-authorized reachability + self presence
|
|
119
146
|
if (token && companyUid) {
|
|
@@ -258,9 +285,10 @@ export function defaultProbeDeps(paths, creds) {
|
|
|
258
285
|
paths,
|
|
259
286
|
creds,
|
|
260
287
|
hqRoot: config.hqRoot,
|
|
288
|
+
syncFiles: config.syncFiles,
|
|
261
289
|
getToken: () => ensureCognitoToken({ tokenSource: "machine", interactive: false }),
|
|
262
290
|
resolveCompanyUid: (token, slug) => getCompanyUid(token, slug),
|
|
263
|
-
|
|
291
|
+
runHq: defaultRunPull(process.execPath, process.argv[1], process.env),
|
|
264
292
|
vendRealtime: async (token) => {
|
|
265
293
|
const bundle = await createPersonalRealtimeFetcher({ token, baseUrl: base })();
|
|
266
294
|
return { actorUid: bundle.actorUid };
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* sync and how often. Kept separate from machine-creds.json, which is the
|
|
5
5
|
* contract-shaped credential file and nothing else.
|
|
6
6
|
*/
|
|
7
|
-
import type
|
|
7
|
+
import { type AgentKitPaths, type KitComponent } from "./paths.js";
|
|
8
8
|
export declare const DEFAULT_SYNC_INTERVAL_MS: number;
|
|
9
9
|
export declare const DEFAULT_INBOX_POLL_MS = 15000;
|
|
10
10
|
export declare const DEFAULT_HEARTBEAT_INTERVAL_MS = 60000;
|
|
@@ -18,9 +18,41 @@ export interface KitConfig {
|
|
|
18
18
|
meshRefreshMs: number;
|
|
19
19
|
/** Ack mirrored inbox items on the server after writing them locally. */
|
|
20
20
|
inboxAck: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Mirror the company vault onto local disk.
|
|
23
|
+
*
|
|
24
|
+
* Off by default. An external bot reads and writes vault files on demand
|
|
25
|
+
* through the `hq` MCP server (hq_files_read / hq_files_list /
|
|
26
|
+
* hq_files_write) and the `hq files` CLI, so a local mirror buys nothing
|
|
27
|
+
* for the common case and costs a full copy of the company's assets — on a
|
|
28
|
+
* team with a large brand library that is hundreds of megabytes, and on a
|
|
29
|
+
* sandboxed bot host whose egress proxy truncates large responses it can
|
|
30
|
+
* never finish at all (every file retried three times, all failing with
|
|
31
|
+
* ERR_HQ_TRUNCATED_RESPONSE, leaving the probe stuck and component-sync in
|
|
32
|
+
* error while the bot was otherwise a healthy team member).
|
|
33
|
+
*
|
|
34
|
+
* Turn it on with `hq agent kit install --sync-files` when the bot genuinely
|
|
35
|
+
* needs files on disk for shell-based work (grep, build tools, a local
|
|
36
|
+
* index).
|
|
37
|
+
*
|
|
38
|
+
* A kit.json written before this field existed reads as `false`, the same as
|
|
39
|
+
* a fresh install. Reading it as `true` (its behaviour at the time) was the
|
|
40
|
+
* safer-looking choice and was wrong: the bots with such a file are exactly
|
|
41
|
+
* the ones the mirror is failing on, and because a bare reinstall preserves
|
|
42
|
+
* recorded settings, the documented fix — reinstall the kit — left the sync
|
|
43
|
+
* loop running and the probe still pulling. A bot that genuinely wants the
|
|
44
|
+
* mirror passes `--sync-files` once.
|
|
45
|
+
*/
|
|
46
|
+
syncFiles: boolean;
|
|
21
47
|
installedAt: string;
|
|
22
48
|
cliVersion: string;
|
|
23
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Kit components whose health this install actually reports. `sync` drops out
|
|
52
|
+
* when the vault is not mirrored to disk — there is no loop to be healthy or
|
|
53
|
+
* unhealthy, so reporting it either way would be a lie.
|
|
54
|
+
*/
|
|
55
|
+
export declare function enabledKitComponents(config: Pick<KitConfig, "syncFiles">): readonly KitComponent[];
|
|
24
56
|
export declare function defaultHqRoot(paths: Pick<AgentKitPaths, "agentDir">): string;
|
|
25
57
|
export declare function defaultKitConfig(paths: Pick<AgentKitPaths, "agentDir">, cliVersion: string, now?: () => Date): KitConfig;
|
|
26
58
|
export declare function writeKitConfig(paths: Pick<AgentKitPaths, "kitConfigPath">, config: KitConfig): void;
|
|
@@ -6,10 +6,19 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
+
import { KIT_COMPONENTS } from "./paths.js";
|
|
9
10
|
export const DEFAULT_SYNC_INTERVAL_MS = 5 * 60_000;
|
|
10
11
|
export const DEFAULT_INBOX_POLL_MS = 15_000;
|
|
11
12
|
export const DEFAULT_HEARTBEAT_INTERVAL_MS = 60_000;
|
|
12
13
|
export const DEFAULT_MESH_REFRESH_MS = 5 * 60_000;
|
|
14
|
+
/**
|
|
15
|
+
* Kit components whose health this install actually reports. `sync` drops out
|
|
16
|
+
* when the vault is not mirrored to disk — there is no loop to be healthy or
|
|
17
|
+
* unhealthy, so reporting it either way would be a lie.
|
|
18
|
+
*/
|
|
19
|
+
export function enabledKitComponents(config) {
|
|
20
|
+
return KIT_COMPONENTS.filter((c) => c !== "sync" || config.syncFiles);
|
|
21
|
+
}
|
|
13
22
|
export function defaultHqRoot(paths) {
|
|
14
23
|
return path.join(paths.agentDir, "hq");
|
|
15
24
|
}
|
|
@@ -21,6 +30,7 @@ export function defaultKitConfig(paths, cliVersion, now = () => new Date()) {
|
|
|
21
30
|
heartbeatIntervalMs: DEFAULT_HEARTBEAT_INTERVAL_MS,
|
|
22
31
|
meshRefreshMs: DEFAULT_MESH_REFRESH_MS,
|
|
23
32
|
inboxAck: false,
|
|
33
|
+
syncFiles: false,
|
|
24
34
|
installedAt: now().toISOString(),
|
|
25
35
|
cliVersion,
|
|
26
36
|
};
|
|
@@ -47,6 +57,7 @@ export function readKitConfig(paths, cliVersion) {
|
|
|
47
57
|
heartbeatIntervalMs: num("heartbeatIntervalMs"),
|
|
48
58
|
meshRefreshMs: num("meshRefreshMs"),
|
|
49
59
|
inboxAck: raw.inboxAck === true,
|
|
60
|
+
syncFiles: raw.syncFiles === true,
|
|
50
61
|
installedAt: typeof raw.installedAt === "string" ? raw.installedAt : base.installedAt,
|
|
51
62
|
cliVersion: typeof raw.cliVersion === "string" ? raw.cliVersion : base.cliVersion,
|
|
52
63
|
};
|
|
@@ -30,6 +30,13 @@ export interface McpToolClients {
|
|
|
30
30
|
/** Run `hq <args…>` as the machine identity; never throws. */
|
|
31
31
|
runHq: (args: string[]) => Promise<HqRunResult>;
|
|
32
32
|
getToken: () => Promise<string>;
|
|
33
|
+
/**
|
|
34
|
+
* Whether this kit mirrors the vault to disk. Off by default, and it decides
|
|
35
|
+
* what `hq_search` can honestly do: the local qmd index only exists when
|
|
36
|
+
* there is a local copy to index, so without one the search is a server-side
|
|
37
|
+
* match on file names and paths, not on contents.
|
|
38
|
+
*/
|
|
39
|
+
syncFiles?: boolean;
|
|
33
40
|
/** Authenticated JSON call against the control plane. */
|
|
34
41
|
apiJson: (token: string, path: string, init?: {
|
|
35
42
|
method?: string;
|
|
@@ -99,7 +99,9 @@ export function buildAgentMcpTools(clients) {
|
|
|
99
99
|
},
|
|
100
100
|
{
|
|
101
101
|
name: "hq_search",
|
|
102
|
-
description:
|
|
102
|
+
description: clients.syncFiles
|
|
103
|
+
? "Search the synced company vault (local qmd index). Modes: keyword (default), semantic, hybrid."
|
|
104
|
+
: "Search the company vault by file name and path, server-side (no local copy is kept). Content and semantic search need a kit installed with --sync-files.",
|
|
103
105
|
inputSchema: {
|
|
104
106
|
type: "object",
|
|
105
107
|
properties: {
|
|
@@ -117,6 +119,14 @@ export function buildAgentMcpTools(clients) {
|
|
|
117
119
|
throw new McpToolInputError('"mode" must be keyword, semantic or hybrid');
|
|
118
120
|
}
|
|
119
121
|
const count = int(args, "count", { min: 1, max: 50, fallback: 10 });
|
|
122
|
+
if (!clients.syncFiles) {
|
|
123
|
+
// No local copy means no local index. Say so plainly instead of
|
|
124
|
+
// running a local search that can only ever return nothing.
|
|
125
|
+
if (mode && mode !== "keyword") {
|
|
126
|
+
throw new McpToolInputError(`"${mode}" search needs a local copy of the vault; this agent reads files on demand. Use keyword, or reinstall the kit with \`hq agent kit install --sync-files\`.`);
|
|
127
|
+
}
|
|
128
|
+
return fromRun(await clients.runHq(["files", "search", query, "--company", company]), "hq files search");
|
|
129
|
+
}
|
|
120
130
|
const hqArgs = ["search", query, "--mode", mode ?? "keyword", "-n", String(count), "--json"];
|
|
121
131
|
return fromRun(await clients.runHq(hqArgs), "hq search");
|
|
122
132
|
},
|
|
@@ -17,7 +17,15 @@ import { type AgentKitPaths, type KitComponent } from "../paths.js";
|
|
|
17
17
|
/** A component stamp older than this reads as error. */
|
|
18
18
|
export declare const COMPONENT_STALE_AFTER_MS: Record<KitComponent, number>;
|
|
19
19
|
export interface HeartbeatBody {
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Only the components this kit actually runs. A component that was never
|
|
22
|
+
* installed is OMITTED rather than reported `error`: with file sync off (the
|
|
23
|
+
* default — the bot reads vault files on demand through the MCP server)
|
|
24
|
+
* there is no sync loop to be unhealthy, and stamping it `error` would make
|
|
25
|
+
* every healthy bot look broken. hq-pro records the components it is sent
|
|
26
|
+
* and ignores absent names.
|
|
27
|
+
*/
|
|
28
|
+
components: Partial<Record<KitComponent, ComponentStatus>>;
|
|
21
29
|
cliVersion: string;
|
|
22
30
|
hostInfo: {
|
|
23
31
|
os: string;
|
|
@@ -26,10 +34,10 @@ export interface HeartbeatBody {
|
|
|
26
34
|
cliVersion: string;
|
|
27
35
|
};
|
|
28
36
|
}
|
|
29
|
-
export declare function buildHeartbeatBody(paths: Pick<AgentKitPaths, "stateDir">, now?: () => Date, hostInfo?: HeartbeatBody["hostInfo"]): HeartbeatBody;
|
|
37
|
+
export declare function buildHeartbeatBody(paths: Pick<AgentKitPaths, "stateDir">, now?: () => Date, hostInfo?: HeartbeatBody["hostInfo"], components_?: readonly KitComponent[]): HeartbeatBody;
|
|
30
38
|
export interface LastHeartbeat {
|
|
31
39
|
at: string;
|
|
32
|
-
components: Record<KitComponent, ComponentStatus
|
|
40
|
+
components: Partial<Record<KitComponent, ComponentStatus>>;
|
|
33
41
|
status: number;
|
|
34
42
|
}
|
|
35
43
|
export declare function writeLastHeartbeat(paths: Pick<AgentKitPaths, "stateDir" | "lastHeartbeatPath">, record: LastHeartbeat): void;
|
|
@@ -41,6 +49,8 @@ export interface HeartbeatDeps {
|
|
|
41
49
|
intervalMs: number;
|
|
42
50
|
getToken: () => Promise<string>;
|
|
43
51
|
log: KitLogger;
|
|
52
|
+
/** Components this kit runs; defaults to all of them. */
|
|
53
|
+
components?: readonly KitComponent[];
|
|
44
54
|
post?: (token: string, body: HeartbeatBody) => Promise<number>;
|
|
45
55
|
now?: () => Date;
|
|
46
56
|
sleep?: (ms: number) => Promise<void>;
|
|
@@ -23,9 +23,9 @@ export const COMPONENT_STALE_AFTER_MS = {
|
|
|
23
23
|
mesh: 3 * 60_000,
|
|
24
24
|
inbox: 3 * 60_000,
|
|
25
25
|
};
|
|
26
|
-
export function buildHeartbeatBody(paths, now = () => new Date(), hostInfo) {
|
|
26
|
+
export function buildHeartbeatBody(paths, now = () => new Date(), hostInfo, components_ = KIT_COMPONENTS) {
|
|
27
27
|
const components = {};
|
|
28
|
-
for (const c of
|
|
28
|
+
for (const c of components_) {
|
|
29
29
|
components[c] = readComponentStatus(paths, c, COMPONENT_STALE_AFTER_MS[c], now).status;
|
|
30
30
|
}
|
|
31
31
|
return {
|
|
@@ -67,7 +67,7 @@ export function defaultHeartbeatPost(agentUid, apiBaseUrl) {
|
|
|
67
67
|
export async function beatOnce(deps) {
|
|
68
68
|
const now = deps.now ?? (() => new Date());
|
|
69
69
|
const post = deps.post ?? defaultHeartbeatPost(deps.agentUid, deps.apiBaseUrl);
|
|
70
|
-
const body = buildHeartbeatBody(deps.paths, now);
|
|
70
|
+
const body = buildHeartbeatBody(deps.paths, now, undefined, deps.components ?? KIT_COMPONENTS);
|
|
71
71
|
try {
|
|
72
72
|
const token = await deps.getToken();
|
|
73
73
|
const status = await post(token, body);
|
|
@@ -75,7 +75,9 @@ export async function beatOnce(deps) {
|
|
|
75
75
|
if (ok) {
|
|
76
76
|
writeLastHeartbeat(deps.paths, { at: now().toISOString(), components: body.components, status });
|
|
77
77
|
}
|
|
78
|
-
deps.log(ok ? "info" : "warn", `heartbeat ${status}
|
|
78
|
+
deps.log(ok ? "info" : "warn", `heartbeat ${status} ${Object.entries(body.components)
|
|
79
|
+
.map(([name, health]) => `${name}=${health}`)
|
|
80
|
+
.join(" ")}`);
|
|
79
81
|
return ok;
|
|
80
82
|
}
|
|
81
83
|
catch (err) {
|
|
@@ -17,5 +17,16 @@ export declare function isKitService(value: string): value is KitService;
|
|
|
17
17
|
*/
|
|
18
18
|
export declare function kitServiceEnv(paths: AgentKitPaths): Record<string, string>;
|
|
19
19
|
export declare function kitServiceSpec(service: KitService, paths: AgentKitPaths): ServiceSpec;
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Services to install. `sync` is included only when the kit is configured to
|
|
22
|
+
* mirror the vault to disk; by default a bot reads files on demand through the
|
|
23
|
+
* `hq` MCP server, so the loop (and its full-copy download) is not installed at
|
|
24
|
+
* all rather than installed and left failing.
|
|
25
|
+
*/
|
|
26
|
+
export declare function kitServiceSpecs(paths: AgentKitPaths, options?: {
|
|
27
|
+
syncFiles?: boolean;
|
|
28
|
+
}): ServiceSpec[];
|
|
29
|
+
export declare function enabledKitServices(options?: {
|
|
30
|
+
syncFiles?: boolean;
|
|
31
|
+
}): readonly KitService[];
|
|
21
32
|
//# sourceMappingURL=services.d.ts.map
|
|
@@ -40,7 +40,17 @@ export function kitServiceSpec(service, paths) {
|
|
|
40
40
|
restartSec: 10,
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Services to install. `sync` is included only when the kit is configured to
|
|
45
|
+
* mirror the vault to disk; by default a bot reads files on demand through the
|
|
46
|
+
* `hq` MCP server, so the loop (and its full-copy download) is not installed at
|
|
47
|
+
* all rather than installed and left failing.
|
|
48
|
+
*/
|
|
49
|
+
export function kitServiceSpecs(paths, options = {}) {
|
|
50
|
+
return enabledKitServices(options).map((s) => kitServiceSpec(s, paths));
|
|
51
|
+
}
|
|
52
|
+
export function enabledKitServices(options = {}) {
|
|
53
|
+
const syncFiles = options.syncFiles === true;
|
|
54
|
+
return KIT_SERVICES.filter((s) => s !== "sync" || syncFiles);
|
|
45
55
|
}
|
|
46
56
|
//# sourceMappingURL=services.js.map
|
|
@@ -50,21 +50,29 @@ mark each item done.
|
|
|
50
50
|
},
|
|
51
51
|
{
|
|
52
52
|
name: "search",
|
|
53
|
-
description: "
|
|
53
|
+
description: "Find things in the company vault (documents, knowledge, meeting notes) with the hq CLI.",
|
|
54
54
|
body: `# HQ search
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
By default this agent keeps no local copy of the vault, so search runs
|
|
57
|
+
server-side and matches file NAMES and paths, not file contents:
|
|
58
58
|
|
|
59
59
|
\`\`\`bash
|
|
60
|
-
hq search "<query>"
|
|
61
|
-
hq
|
|
62
|
-
hq
|
|
63
|
-
hq files search "<query>" --company <slug> # server-side vault search (no local index needed)
|
|
60
|
+
hq files search "<query>" --company <slug> # find files by name/path
|
|
61
|
+
hq files browse companies/<slug>/<folder> # list a folder
|
|
62
|
+
hq files cat companies/<slug>/<file> # read one file
|
|
64
63
|
\`\`\`
|
|
65
64
|
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
A good pattern for a question you cannot answer from names alone: search or
|
|
66
|
+
browse to narrow to a few likely files, then read them with \`hq files cat\`.
|
|
67
|
+
|
|
68
|
+
Content and semantic search need a local copy and its index, which only exists
|
|
69
|
+
if the kit was installed with \`--sync-files\`. In that case (check
|
|
70
|
+
\`~/.hq-agent/kit.json\` → \`syncFiles\`) you also have:
|
|
71
|
+
|
|
72
|
+
\`\`\`bash
|
|
73
|
+
hq search "<query>" # ranked full-text over the local copy
|
|
74
|
+
hq search "<query>" --mode hybrid # keyword + semantic
|
|
75
|
+
\`\`\`
|
|
68
76
|
|
|
69
77
|
Cite the file path of anything you quote so a teammate can open it.
|
|
70
78
|
`,
|
|
@@ -82,11 +90,19 @@ hq files share companies/<slug>/<path> # share a path with a teammate
|
|
|
82
90
|
hq files versions companies/<slug>/<file> # version history
|
|
83
91
|
\`\`\`
|
|
84
92
|
|
|
85
|
-
Paths are vault-relative and start with \`companies/<slug>/\`.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
93
|
+
Paths are vault-relative and start with \`companies/<slug>/\`. Read them on
|
|
94
|
+
demand with the commands above — this agent keeps no local copy of the vault
|
|
95
|
+
unless the kit was installed with \`--sync-files\` (check \`~/.hq-agent/kit.json\`
|
|
96
|
+
→ \`syncFiles\`), so never assume a file is already on disk.
|
|
97
|
+
|
|
98
|
+
To publish a change you need that local copy: with \`syncFiles\` on, edit under
|
|
99
|
+
the kit's HQ root (\`kit.json\` → \`hqRoot\`) and run
|
|
100
|
+
\`hq sync push --hq-root <hqRoot>\`. Without it, \`hq files get\` a file, and if
|
|
101
|
+
you need to write one back, say so and ask — do not improvise an upload.
|
|
102
|
+
|
|
103
|
+
Writes are audited under this agent's identity — only write where you were
|
|
104
|
+
asked to, and prefer creating a new file over overwriting one you did not
|
|
105
|
+
author.
|
|
90
106
|
`,
|
|
91
107
|
},
|
|
92
108
|
{
|