@indigoai-us/hq-cli 5.345.27 → 5.345.29

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.
@@ -20,6 +20,7 @@ export function registerCoreResolveCompanyNativeCommand(core) {
20
20
  args: operands,
21
21
  cwd: root,
22
22
  hqRoot: root,
23
+ env: { HQ_CALLER_CWD: process.cwd() },
23
24
  });
24
25
  if (code !== 0)
25
26
  process.exit(code);
@@ -10,7 +10,7 @@ type HqResult = {
10
10
  code: number;
11
11
  stdout: string;
12
12
  };
13
- type ResolveCompanyFs = Pick<typeof fs, "readFileSync" | "statSync" | "accessSync">;
13
+ type ResolveCompanyFs = Pick<typeof fs, "readFileSync" | "statSync" | "lstatSync" | "accessSync">;
14
14
  export type ResolveCompanyDependencies = {
15
15
  cwd: string;
16
16
  hqRoot?: string;
@@ -18,7 +18,7 @@ export type ResolveCompanyDependencies = {
18
18
  fs?: ResolveCompanyFs;
19
19
  stdin?: string;
20
20
  stdinIsTty?: boolean;
21
- spawnHq?: (args: string[], env: NodeJS.ProcessEnv, timeoutMs: number, cwd?: string) => Promise<HqResult>;
21
+ spawnHq?: (args: string[], env: NodeJS.ProcessEnv, timeoutMs: number, cwd?: string, command?: string) => Promise<HqResult>;
22
22
  pid?: number;
23
23
  random?: () => number;
24
24
  };
@@ -1,8 +1,9 @@
1
1
  /** Native port of resolve-company.sh. This module stays on the fast import path. */
2
- import { spawn } from "node:child_process";
2
+ import { spawn, spawnSync } from "node:child_process";
3
3
  import * as fs from "node:fs";
4
4
  import * as path from "node:path";
5
5
  import { awkCannotOpenFileDiagnostic } from "../lib/core-utils/awk-diagnostics.js";
6
+ import { buildCmdExeSpawnPlan, isWindowsBatchFile } from "../utils/windows-spawn.js";
6
7
  export const RESOLVE_COMPANY_ASSET = "core/scripts/resolve-company.sh";
7
8
  export const RESOLVE_COMPANY_HELP = [
8
9
  "hq-core: public",
@@ -15,6 +16,13 @@ export const RESOLVE_COMPANY_HELP = [
15
16
  "Output: {\"company\":\"<slug>\",\"source\":\"prompt|session|device_default|none\"}",
16
17
  "Always exits 0 and never writes state.",
17
18
  "",
19
+ "Folder mode (--path <dir>): ask the repo-to-company registry through",
20
+ "`$HQ_CLI_BIN resolve-company --path <dir> --json` (HQ_CLI_BIN defaults to",
21
+ "`hq`). Sources: registry (linked folder), registry_miss (not linked), or",
22
+ "unavailable (CLI missing, failed, timed out, or returned bad output). A",
23
+ "registry answer is accepted only when it names a real companies/<slug>",
24
+ "directory. Folder mode never falls back to prompt, session, or device default.",
25
+ "",
18
26
  "set -uo pipefail",
19
27
  "",
20
28
  "ROOT=\"\"",
@@ -222,21 +230,49 @@ function validPreflight(text, slug, session, operation) {
222
230
  value.companySlug === slug &&
223
231
  typeof value.companyUid === "string" && value.companyUid.length > 0);
224
232
  }
225
- function commandOnPath(name, env) {
226
- const pathValue = env.PATH ?? "";
227
- for (const directory of pathValue.split(path.delimiter)) {
228
- const candidate = path.join(directory || ".", name);
233
+ function commandPath(name, env) {
234
+ const hasPathSeparator = name.includes("/") || name.includes("\\");
235
+ const pathKey = Object.keys(env).find((key) => key.toUpperCase() === "PATH");
236
+ const candidates = hasPathSeparator
237
+ ? [name]
238
+ : (pathKey ? env[pathKey] ?? "" : "").split(path.delimiter).flatMap((directory) => {
239
+ const base = path.join(directory || ".", name);
240
+ return process.platform === "win32" && !path.extname(name)
241
+ ? [`${base}.com`, `${base}.exe`, `${base}.bat`, `${base}.cmd`]
242
+ : [base];
243
+ });
244
+ for (const candidate of candidates) {
229
245
  try {
230
246
  fs.accessSync(candidate, fs.constants.X_OK);
231
- return true;
247
+ return candidate;
232
248
  }
233
249
  catch {
234
250
  // `command -v` simply advances to the next PATH entry.
235
251
  }
236
252
  }
253
+ return undefined;
254
+ }
255
+ function commandOnPath(name, env) {
256
+ if (commandPath(name, env))
257
+ return true;
258
+ if (process.platform !== "win32" || name.includes("/") || name.includes("\\") || path.extname(name))
259
+ return false;
260
+ const pathKey = Object.keys(env).find((key) => key.toUpperCase() === "PATH");
261
+ for (const directory of (pathKey ? env[pathKey] ?? "" : "").split(path.delimiter)) {
262
+ try {
263
+ // Git Bash considers an executable extensionless script a command. Keep
264
+ // the presence check faithful to that shell contract; actual Windows
265
+ // process launch still selects a PATHEXT executable or batch shim.
266
+ fs.accessSync(path.join(directory || ".", name), fs.constants.X_OK);
267
+ return true;
268
+ }
269
+ catch {
270
+ // `command -v` advances through each PATH entry.
271
+ }
272
+ }
237
273
  return false;
238
274
  }
239
- function runHq(args, env, timeoutMs, cwd) {
275
+ function runHq(args, env, timeoutMs, cwd, command = "hq") {
240
276
  return new Promise((resolve) => {
241
277
  let settled = false;
242
278
  let stdout = "";
@@ -247,7 +283,15 @@ function runHq(args, env, timeoutMs, cwd) {
247
283
  if (!child?.pid)
248
284
  return;
249
285
  try {
250
- process.kill(-child.pid, signal);
286
+ if (process.platform === "win32") {
287
+ const taskkill = path.join(env.SystemRoot ?? env.WINDIR ?? "C:\\Windows", "System32", "taskkill.exe");
288
+ const result = spawnSync(taskkill, ["/PID", String(child.pid), "/T", "/F"], { windowsHide: true });
289
+ if (result.error || result.status !== 0)
290
+ child.kill(signal);
291
+ }
292
+ else {
293
+ process.kill(-child.pid, signal);
294
+ }
251
295
  }
252
296
  catch (error) {
253
297
  if (errorCode(error) !== "ESRCH")
@@ -279,7 +323,24 @@ function runHq(args, env, timeoutMs, cwd) {
279
323
  }
280
324
  }, timeoutMs);
281
325
  try {
282
- child = spawn("hq", args, { cwd, detached: true, stdio: ["ignore", "pipe", "ignore"], env });
326
+ const resolved = commandPath(command, env);
327
+ if (!resolved) {
328
+ finish({ code: 127, stdout });
329
+ return;
330
+ }
331
+ let executable = resolved;
332
+ let executableArgs = args;
333
+ let windowsVerbatimArguments;
334
+ if (process.platform === "win32" && isWindowsBatchFile(resolved)) {
335
+ const plan = buildCmdExeSpawnPlan(resolved, args, env);
336
+ executable = plan.cmd;
337
+ executableArgs = plan.args;
338
+ windowsVerbatimArguments = plan.windowsVerbatimArguments;
339
+ }
340
+ child = spawn(executable, executableArgs, {
341
+ cwd, detached: process.platform !== "win32", stdio: ["ignore", "pipe", "ignore"], env,
342
+ windowsVerbatimArguments,
343
+ });
283
344
  }
284
345
  catch {
285
346
  finish({ code: 127, stdout });
@@ -301,6 +362,8 @@ function parseArgs(args) {
301
362
  let root = "";
302
363
  let prompt = "";
303
364
  let havePrompt = false;
365
+ let folder = "";
366
+ let haveFolder = false;
304
367
  for (let index = 0; index < args.length;) {
305
368
  const arg = args[index];
306
369
  if (arg === "--root") {
@@ -312,14 +375,84 @@ function parseArgs(args) {
312
375
  havePrompt = true;
313
376
  index += Math.min(2, args.length - index);
314
377
  }
378
+ else if (arg === "--path") {
379
+ folder = args[index + 1] ?? "";
380
+ haveFolder = true;
381
+ index += Math.min(2, args.length - index);
382
+ }
315
383
  else if (arg === "--help" || arg === "-h") {
316
- return { root, prompt, havePrompt, help: true };
384
+ return { root, prompt, havePrompt, folder, haveFolder, help: true };
317
385
  }
318
386
  else {
319
387
  index += 1;
320
388
  }
321
389
  }
322
- return { root, prompt, havePrompt, help: false };
390
+ return { root, prompt, havePrompt, folder, haveFolder, help: false };
391
+ }
392
+ function unavailable(reason) {
393
+ return {
394
+ ...output("", "unavailable"),
395
+ stderr: Buffer.from(`resolve-company: WARNING company registry unavailable (${reason}); binding personal, not guessing a company\n`),
396
+ };
397
+ }
398
+ async function resolveFolder(folder, root, cwd, env, fileSystem, invoke) {
399
+ if (!folder)
400
+ return unavailable("empty --path");
401
+ const cli = env.HQ_CLI_BIN || "hq";
402
+ if (!commandPath(cli, env))
403
+ return unavailable(`'${cli}' not found`);
404
+ if (!commandPath("jq", env))
405
+ return unavailable("jq not found");
406
+ // The registry command is the contract boundary. Calling it preserves its
407
+ // own hq-anywhere gate and avoids importing the full command graph here.
408
+ const hqEnv = {
409
+ ...EMPTY_ENV,
410
+ ...env,
411
+ HQ_NO_UPDATE_CHECK: "1",
412
+ HQ_RESOLVE_TIMEOUT_MS: env.HQ_RESOLVE_TIMEOUT_MS || "5000",
413
+ };
414
+ const parsedTimeout = Number(hqEnv.HQ_RESOLVE_TIMEOUT_MS);
415
+ const timeoutMs = parsedTimeout || 2_000;
416
+ const response = await invoke(["resolve-company", "--path", folder, "--json"], hqEnv, timeoutMs, cwd, cli);
417
+ const value = record(parseJson(response.stdout));
418
+ if (response.code !== 0 && !(response.code === 1 && value?.company === null)) {
419
+ return unavailable(`'${cli} resolve-company' exited ${response.code}`);
420
+ }
421
+ if (!value)
422
+ return unavailable(`'${cli} resolve-company' returned unreadable output`);
423
+ if (value.company === null || value.company === undefined)
424
+ return output("", "registry_miss");
425
+ if (typeof value.company !== "string")
426
+ return unavailable(`'${cli} resolve-company' returned unreadable output`);
427
+ // jq -r plus command substitution removes trailing newlines before the
428
+ // shell validates the returned slug.
429
+ const slug = value.company.replace(/\n+$/, "");
430
+ if (!slug || slug === "personal" || slug.startsWith("_") || !/^[A-Za-z0-9_-]+$/.test(slug)) {
431
+ return unavailable(`registry returned invalid company '${slug}'`);
432
+ }
433
+ const companyPath = path.join(root, "companies", slug);
434
+ let isDirectory = false;
435
+ let isSymlink = false;
436
+ try {
437
+ isDirectory = fileSystem.statSync(companyPath).isDirectory();
438
+ }
439
+ catch (error) {
440
+ const code = errorCode(error);
441
+ if (code !== "ENOENT" && code !== "ENOTDIR" && code !== "EACCES" && code !== "ELOOP")
442
+ throw error;
443
+ }
444
+ try {
445
+ isSymlink = fileSystem.lstatSync(companyPath).isSymbolicLink();
446
+ }
447
+ catch (error) {
448
+ const code = errorCode(error);
449
+ if (code !== "ENOENT" && code !== "ENOTDIR" && code !== "EACCES" && code !== "ELOOP")
450
+ throw error;
451
+ }
452
+ if (!isDirectory || isSymlink) {
453
+ return unavailable(`registry company '${slug}' has no companies/${slug} directory in ${root}`);
454
+ }
455
+ return output(slug, "registry");
323
456
  }
324
457
  async function readStdin() {
325
458
  const chunks = [];
@@ -333,11 +466,14 @@ export async function resolveCompany(args, dependencies) {
333
466
  const parsed = parseArgs(args);
334
467
  if (parsed.help)
335
468
  return { code: 0, stdout: Buffer.from(RESOLVE_COMPANY_HELP), stderr: Buffer.alloc(0) };
469
+ const root = parsed.root || dependencies.hqRoot || env.HQ_ROOT || env.CLAUDE_PROJECT_DIR || dependencies.cwd;
470
+ if (parsed.haveFolder) {
471
+ return await resolveFolder(parsed.folder, root, dependencies.cwd, env, fileSystem, dependencies.spawnHq ?? runHq);
472
+ }
336
473
  let prompt = parsed.prompt;
337
474
  if (!parsed.havePrompt && dependencies.stdinIsTty !== true) {
338
475
  prompt = dependencies.stdin ?? await readStdin();
339
476
  }
340
- const root = parsed.root || dependencies.hqRoot || env.HQ_ROOT || env.CLAUDE_PROJECT_DIR || dependencies.cwd;
341
477
  const manifest = path.join(root, "companies/manifest.yaml");
342
478
  if (!isFile(manifest, fileSystem))
343
479
  return output("", "none");
@@ -393,6 +529,7 @@ export async function resolveCompany(args, dependencies) {
393
529
  export async function runResolveCompanyFast(args, stdin, cwd, hqRoot, _commandName, _hqRootArg, stdinReader = readStdin) {
394
530
  const parsed = parseArgs(args);
395
531
  const actualStdin = parsed.havePrompt || process.stdin.isTTY === true
532
+ || parsed.haveFolder
396
533
  ? stdin
397
534
  : stdin || await stdinReader();
398
535
  const result = await resolveCompany(args, {
@@ -3,15 +3,16 @@ import { readFile } from "node:fs/promises";
3
3
  import { ensureCognitoToken } from "../utils/cognito-session.js";
4
4
  import { vaultApiFetch, getCompanyUid, resolveCallerPersonUid, } from "../utils/vault-api.js";
5
5
  import { isPlanGateError, planGateErrorFromPayload, } from "../utils/plan-gate-error.js";
6
- const LIVE_BOT_STATUSES = new Set([
7
- "scheduled",
6
+ import { mapWithConcurrency } from "./sync-manifest.js";
7
+ const LIVE_SNAPSHOT_STATUSES = new Set([
8
8
  "joining",
9
9
  "recording",
10
10
  "in_call",
11
- "in-call",
12
- "processing",
13
- "completed",
14
11
  ]);
12
+ const RECENT_LIVE_BOT_STATUSES = new Set(["processing", "completed", "no_transcript"]);
13
+ const LIVE_LIST_SNAPSHOT_CONCURRENCY = 10;
14
+ const LIVE_LIST_RECENT_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
15
+ const LIVE_LIST_UPCOMING_WINDOW_MS = 15 * 60 * 1000;
15
16
  const TERMINAL_LIVE_BOT_STATUSES = new Set(["completed", "failed", "done", "ended"]);
16
17
  // Two minutes gives a large transcript time to arrive over a slow connection,
17
18
  // while keeping the interactive command from waiting indefinitely.
@@ -91,15 +92,54 @@ function liveBotId(bot) {
91
92
  return typeof bot.recallBotId === "string" ? bot.recallBotId : bot.botId;
92
93
  }
93
94
  function liveBotLineCount(bot) {
95
+ if (!LIVE_SNAPSHOT_STATUSES.has(normalizedLiveBotStatus(bot)))
96
+ return "-";
94
97
  const value = bot.linesSoFar ?? bot.transcriptLineCount;
95
98
  return typeof value === "number" ? String(value) : "-";
96
99
  }
100
+ function normalizedLiveBotStatus(bot) {
101
+ return String(bot.status ?? "").toLowerCase().replaceAll("-", "_");
102
+ }
103
+ function liveBotTime(bot) {
104
+ const times = [bot.scheduledStartTime, bot.createdAt]
105
+ .filter((value) => typeof value === "string")
106
+ .map((value) => Date.parse(value))
107
+ .filter(Number.isFinite);
108
+ return times.length > 0 ? Math.max(...times) : Number.NEGATIVE_INFINITY;
109
+ }
110
+ /** Select bots intended for the default `meetings live` listing. */
111
+ function selectLiveMeetingBots(bots, companyId, includeAll, now = new Date()) {
112
+ const companyBots = bots.filter((bot) => bot.companyId === companyId);
113
+ const newestFirst = (a, b) => liveBotTime(b) - liveBotTime(a);
114
+ if (includeAll)
115
+ return companyBots.sort(newestFirst);
116
+ const nowMs = now.getTime();
117
+ const recentSince = nowMs - LIVE_LIST_RECENT_WINDOW_MS;
118
+ const upcomingUntil = nowMs + LIVE_LIST_UPCOMING_WINDOW_MS;
119
+ const active = [];
120
+ const recent = [];
121
+ const imminent = [];
122
+ for (const bot of companyBots) {
123
+ const status = normalizedLiveBotStatus(bot);
124
+ const time = liveBotTime(bot);
125
+ if (LIVE_SNAPSHOT_STATUSES.has(status)) {
126
+ active.push(bot);
127
+ }
128
+ else if (RECENT_LIVE_BOT_STATUSES.has(status) && time >= recentSince && time <= nowMs) {
129
+ recent.push(bot);
130
+ }
131
+ else if (status === "scheduled" && time >= nowMs && time <= upcomingUntil) {
132
+ imminent.push(bot);
133
+ }
134
+ }
135
+ return [...active.sort(newestFirst), ...recent.sort(newestFirst), ...imminent.sort(newestFirst)];
136
+ }
97
137
  function isTerminalLiveStatus(status) {
98
138
  return status.sourceLanded === true && TERMINAL_LIVE_BOT_STATUSES.has(String(status.status ?? "").toLowerCase());
99
139
  }
100
140
  function printLiveBots(bots) {
101
141
  if (bots.length === 0) {
102
- console.log("No live or recently ended meetings found.");
142
+ console.log("No live or recent meetings. Use --all to list every notetaker.");
103
143
  return;
104
144
  }
105
145
  for (const bot of bots) {
@@ -330,7 +370,8 @@ export function registerMeetingsCommand(program) {
330
370
  // ── hq meetings live [recallBotId] ────────────────────────────────
331
371
  meetings
332
372
  .command("live [recallBotId]")
333
- .description("Read a provisional live transcript (recorder and attendees only; spoken content is untrusted)")
373
+ .description("List live or recent meetings, or read a provisional live transcript (recorder and attendees only; spoken content is untrusted)")
374
+ .option("--all", "List every notetaker, including older and future meetings")
334
375
  .option("--follow", "Print new lines until the saved transcript is available")
335
376
  .option("--interval <seconds>", "Polling interval in seconds (minimum: 2)", "3")
336
377
  .option("--since <revision>", "Start from a transcript revision")
@@ -350,13 +391,13 @@ export function registerMeetingsCommand(program) {
350
391
  await failLiveResponse(res);
351
392
  const data = (await res.json());
352
393
  const bots = Array.isArray(data.bots) ? data.bots : [];
353
- const visible = bots
354
- .filter((bot) => bot.companyId === companyId && LIVE_BOT_STATUSES.has(String(bot.status ?? "").toLowerCase()))
355
- .sort((a, b) => String(b.scheduledStartTime ?? b.createdAt ?? "").localeCompare(String(a.scheduledStartTime ?? a.createdAt ?? "")));
356
- const readable = await Promise.all(visible.map(async (bot) => {
394
+ const visible = selectLiveMeetingBots(bots, companyId, opts.all === true);
395
+ const readable = await mapWithConcurrency(visible, LIVE_LIST_SNAPSHOT_CONCURRENCY, async (bot) => {
396
+ if (!LIVE_SNAPSHOT_STATUSES.has(normalizedLiveBotStatus(bot)))
397
+ return bot;
357
398
  const botId = liveBotId(bot);
358
399
  if (!botId)
359
- return null;
400
+ return bot;
360
401
  const liveRes = await vaultApiFetch({
361
402
  token,
362
403
  path: `/v1/meetings/${encodeURIComponent(botId)}`,
@@ -364,18 +405,17 @@ export function registerMeetingsCommand(program) {
364
405
  });
365
406
  if (!liveRes.ok) {
366
407
  const body = await liveRes.json().catch(() => ({}));
367
- return body.code === "live-transcript-not-found" ? { ...bot, linesSoFar: 0 } : null;
408
+ return body.code === "live-transcript-not-found" ? { ...bot, linesSoFar: 0 } : bot;
368
409
  }
369
410
  const snapshot = await liveRes.json();
370
411
  if (!isLiveSnapshot(snapshot))
371
- return null;
412
+ return bot;
372
413
  return { ...bot, linesSoFar: snapshot.segments.length };
373
- }));
374
- const readableBots = readable.filter((bot) => bot !== null);
414
+ });
375
415
  if (meetings.opts().json)
376
- console.log(JSON.stringify(readableBots, null, 2));
416
+ console.log(JSON.stringify(readable, null, 2));
377
417
  else
378
- printLiveBots(readableBots);
418
+ printLiveBots(readable);
379
419
  return;
380
420
  }
381
421
  const intervalSeconds = Number(opts.interval ?? "3");
@@ -184,6 +184,16 @@ export const FAST_SCAFFOLD_SCRIPTS = [
184
184
  shellHqRoot: "live",
185
185
  readsStdin: true,
186
186
  },
187
+ {
188
+ name: "checkpoint",
189
+ kind: "native",
190
+ modulePath: "./commands/core-checkpoint-fast.js",
191
+ exportName: "runCheckpointFast",
192
+ flagKey: "hq-core-native-fast-route",
193
+ familyFlagKey: "core-native-session",
194
+ fallbackToCommandGraph: true,
195
+ root: "live",
196
+ },
187
197
  {
188
198
  name: "hq-session",
189
199
  kind: "native",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.27",
3
+ "version": "5.345.29",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {