@indigoai-us/hq-cli 5.118.2 → 5.119.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 CHANGED
@@ -2,6 +2,39 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.119.0] — 2026-09-17
6
+
7
+ ### Changed
8
+
9
+ - External agents no longer copy the company vault onto their own machine.
10
+ `hq agent kit install` used to install a sync loop that mirrored the whole
11
+ vault to disk; a bot reads and writes vault files on demand through the `hq`
12
+ MCP server and the `hq files` commands, so the copy bought nothing for the
13
+ common case and cost a full download of the company's assets. On a team with
14
+ a large brand library that is hundreds of megabytes, and on a sandboxed bot
15
+ host whose egress truncates large responses it could never finish at all —
16
+ every file retried three times and failed with `ERR_HQ_TRUNCATED_RESPONSE`,
17
+ which left the probe stuck and the agent reporting a sync error while it was
18
+ otherwise a healthy team member.
19
+
20
+ Pass `--sync-files` if the bot genuinely needs files on disk for shell-based
21
+ work; a reinstall without it removes a sync loop an earlier install left
22
+ behind. An existing install is unchanged until it is reinstalled.
23
+
24
+ - `hq agent probe` reports a `files` check in place of `team-sync`. Without a
25
+ local copy it proves the read path the bot actually uses — a vault listing
26
+ answers — instead of downloading the vault to prove a folder exists. With
27
+ `--sync-files` it checks the pull exactly as before.
28
+
29
+ - The heartbeat reports only the components the kit runs. A component that was
30
+ never installed is omitted rather than reported as an error, so a bot with no
31
+ sync loop no longer looks broken.
32
+
33
+ - `hq_search` no longer runs a local index search on an agent that has no local
34
+ copy, where it could only ever return nothing. It runs the server-side vault
35
+ search instead, and says plainly that content and semantic search need a kit
36
+ installed with `--sync-files`.
37
+
5
38
  ## [5.118.2] — 2026-09-17
6
39
 
7
40
  ### 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 sync, work-mesh, inbox and heartbeat services plus the skills directory";
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 sync, work-mesh, inbox and heartbeat services plus the skills directory",
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
- const services = installServices(kitServiceSpecs(paths), host, {
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
- rt.log("info", `supervisor starting ${KIT_SERVICES.join(",")} agent=${rt.creds.entityUid} cli=${CLI_VERSION}`);
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: KIT_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 sync, work-mesh, inbox and heartbeat services plus the skills directory")
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, { hqRoot: opts.hqRoot, inboxAck: opts.inboxAck, activate: opts.activate }, resolveServiceHost(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(` hq root: ${result.config.hqRoot}`);
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
- * team-sync `hq sync pull --all` succeeds and the company folder exists
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
- runPull: (args: string[]) => Promise<number>;
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
- * team-sync `hq sync pull --all` succeeds and the company folder exists
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. team-sync
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
- try {
97
- const code = await deps.runPull(syncPullArgs(deps.hqRoot));
98
- const companyDir = path.join(deps.hqRoot, "companies", deps.creds.companySlug);
99
- const present = fs.existsSync(companyDir);
100
- const ok = code === 0 && present;
101
- checks.push({
102
- name: "team-sync",
103
- ok,
104
- detail: ok
105
- ? `synced into ${companyDir}`
106
- : code !== 0
107
- ? `hq sync pull exited ${code}`
108
- : `sync exited 0 but ${companyDir} is missing`,
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
- catch (err) {
112
- checks.push({ name: "team-sync", ok: false, detail: errText(err) });
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: "team-sync", ok: false, detail: "skipped: no token" });
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
- runPull: defaultRunPull(process.execPath, process.argv[1], process.env),
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 { AgentKitPaths } from "./paths.js";
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,34 @@ 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). A kit.json written before this field existed is read as `true`,
37
+ * because every install of that vintage ran the sync loop.
38
+ */
39
+ syncFiles: boolean;
21
40
  installedAt: string;
22
41
  cliVersion: string;
23
42
  }
43
+ /**
44
+ * Kit components whose health this install actually reports. `sync` drops out
45
+ * when the vault is not mirrored to disk — there is no loop to be healthy or
46
+ * unhealthy, so reporting it either way would be a lie.
47
+ */
48
+ export declare function enabledKitComponents(config: Pick<KitConfig, "syncFiles">): readonly KitComponent[];
24
49
  export declare function defaultHqRoot(paths: Pick<AgentKitPaths, "agentDir">): string;
25
50
  export declare function defaultKitConfig(paths: Pick<AgentKitPaths, "agentDir">, cliVersion: string, now?: () => Date): KitConfig;
26
51
  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,8 @@ export function readKitConfig(paths, cliVersion) {
47
57
  heartbeatIntervalMs: num("heartbeatIntervalMs"),
48
58
  meshRefreshMs: num("meshRefreshMs"),
49
59
  inboxAck: raw.inboxAck === true,
60
+ // Absent means a pre-`syncFiles` install, which always synced.
61
+ syncFiles: typeof raw.syncFiles === "boolean" ? raw.syncFiles : true,
50
62
  installedAt: typeof raw.installedAt === "string" ? raw.installedAt : base.installedAt,
51
63
  cliVersion: typeof raw.cliVersion === "string" ? raw.cliVersion : base.cliVersion,
52
64
  };
@@ -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: "Search the synced company vault (local qmd index). Modes: keyword (default), semantic, hybrid.",
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
- components: Record<KitComponent, ComponentStatus>;
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 KIT_COMPONENTS) {
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} sync=${body.components.sync} mesh=${body.components.mesh} inbox=${body.components.inbox}`);
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
- export declare function kitServiceSpecs(paths: AgentKitPaths): ServiceSpec[];
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
- export function kitServiceSpecs(paths) {
44
- return KIT_SERVICES.map((s) => kitServiceSpec(s, paths));
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: "Search the company vault (documents, knowledge, meeting notes) with the hq CLI's local index.",
53
+ description: "Find things in the company vault (documents, knowledge, meeting notes) with the hq CLI.",
54
54
  body: `# HQ search
55
55
 
56
- The company vault is synced to the kit's HQ root (see \`~/.hq-agent/kit.json\`,
57
- \`hqRoot\`). Search it with:
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>" # ranked results across the synced vault
61
- hq search "<query>" --mode hybrid # keyword + semantic
62
- hq search "<query>" --json # machine-readable
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
- If results look stale, the sync loop may not have run yet:
67
- \`hq sync pull --all --on-conflict keep --hq-root <hqRoot>\` pulls now.
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>/\`. The synced copy
86
- lives under the kit's HQ root (\`~/.hq-agent/kit.json\` \`hqRoot\`); edit files
87
- there and run \`hq sync push --hq-root <hqRoot>\` to publish. Writes are
88
- audited under this agent's identity only write where you were asked to,
89
- and prefer creating a new file over overwriting one you did not author.
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
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.118.2",
3
+ "version": "5.119.0",
4
4
  "description": "HQ by Indigo management CLI \u2014 modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {