@indigoai-us/hq-cli 5.345.179 → 5.345.181

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 (52) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/command-catalog.generated.d.ts +126 -0
  3. package/dist/command-catalog.generated.js +155 -0
  4. package/dist/commands/bot.d.ts +29 -1
  5. package/dist/commands/bot.js +282 -22
  6. package/dist/commands/channels.d.ts +18 -1
  7. package/dist/commands/channels.js +185 -1
  8. package/dist/commands/lanes.d.ts +1 -1
  9. package/dist/commands/lanes.js +6 -2
  10. package/dist/commands/mesh.js +19 -0
  11. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2285 -1743
  12. package/dist/hq-cloud-sync-runner/package.json +1 -1
  13. package/dist/lib/bot/config.d.ts +8 -0
  14. package/dist/lib/bot/fleet-local.d.ts +7 -3
  15. package/dist/lib/bot/fleet-local.js +234 -64
  16. package/dist/lib/bot/run.js +39 -2
  17. package/dist/lib/bot/runtime-auth.d.ts +119 -0
  18. package/dist/lib/bot/runtime-auth.js +251 -0
  19. package/dist/lib/daemon/catalog.d.ts +1 -0
  20. package/dist/lib/daemon/catalog.js +1 -1
  21. package/dist/lib/daemon/run.d.ts +2 -0
  22. package/dist/lib/daemon/run.js +64 -16
  23. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  24. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  25. package/dist/lib/hot-channels/adapters/meetings.js +3 -1
  26. package/dist/lib/hot-channels/contract.d.ts +6 -0
  27. package/dist/lib/hot-channels/ledger.js +17 -0
  28. package/dist/lib/hot-channels/loop.js +2 -0
  29. package/dist/lib/lanes/accounts.d.ts +1 -17
  30. package/dist/lib/lanes/accounts.js +5 -45
  31. package/dist/lib/lanes/provider-home.d.ts +24 -0
  32. package/dist/lib/lanes/provider-home.js +52 -0
  33. package/dist/lib/lanes/spawn.d.ts +12 -0
  34. package/dist/lib/lanes/spawn.js +27 -0
  35. package/dist/lib/mesh/live/daemon/doctor.d.ts +3 -0
  36. package/dist/lib/mesh/live/daemon/doctor.js +13 -0
  37. package/dist/lib/mesh/live/daemon/owner-inbox-push.d.ts +94 -0
  38. package/dist/lib/mesh/live/daemon/owner-inbox-push.js +273 -0
  39. package/dist/lib/mesh/live/daemon/run.js +31 -0
  40. package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +14 -0
  41. package/dist/lib/mesh/live/daemon/work-subscribe.js +210 -3
  42. package/dist/lib/mesh/live/paths.d.ts +2 -0
  43. package/dist/lib/mesh/live/paths.js +2 -0
  44. package/dist/lib/sync-core/flag.d.ts +1 -1
  45. package/dist/lib/sync-core/flag.js +1 -1
  46. package/dist/lib/sync-core/reconcile.d.ts +1 -1
  47. package/dist/lib/sync-core/reconcile.js +9 -7
  48. package/dist/lib/sync-core/release.d.ts +3 -3
  49. package/dist/lib/sync-core/release.js +4 -4
  50. package/dist/lib/sync-core/rollout-state.d.ts +1 -0
  51. package/dist/lib/sync-core/rollout-state.js +1 -0
  52. package/package.json +8 -8
@@ -2,7 +2,7 @@ import chalk from "chalk";
2
2
  import { ensureCognitoToken } from "../utils/cognito-session.js";
3
3
  import { vaultApiFetch } from "../utils/vault-api.js";
4
4
  import { suggestChannels, withChannelSuggestions } from "./channel-suggest.js";
5
- import { callerFromToken, channelSlug, fetchChannelMembers, fetchChannels, matchChannelsByName, } from "./dm.js";
5
+ import { callerFromToken, channelSlug, fetchChannelMembers, fetchChannels, matchChannelsByName, matchContactByName, } from "./dm.js";
6
6
  /**
7
7
  * Human label for a channel row. Named channels (personal/company) show their
8
8
  * name + a `hq dm <slug>` hint; unnamed group DMs are identified by their
@@ -294,6 +294,176 @@ async function runChannelsDelete(channelName, opts) {
294
294
  process.exit(1);
295
295
  }
296
296
  }
297
+ // ─── Channel membership: add / remove ────────────────────────────────────────
298
+ /**
299
+ * Resolve the `<member>` argument of `hq channels add|remove` to a person or
300
+ * agent uid. Accepts a uid (`prs_…`, `agt_…`), an email, or a display name /
301
+ * @handle matched against the caller's DM contacts (which include the bots the
302
+ * caller owns). Pure → unit-testable. Returns an error sentence on no or
303
+ * ambiguous match so the caller never guesses. Feedback e165b286.
304
+ */
305
+ export function resolveMemberTarget(contacts, raw) {
306
+ const target = raw.trim().replace(/^@/, "");
307
+ if (!target)
308
+ return { ok: false, error: "Name a member: a @handle, an email, or a prs_/agt_ uid." };
309
+ if (/^(prs|agt)_[A-Za-z0-9_-]+$/.test(target)) {
310
+ const known = contacts.find((c) => c.personUid === target);
311
+ return { ok: true, personUid: target, displayName: known?.displayName?.trim() || target };
312
+ }
313
+ if (target.includes("@")) {
314
+ const email = target.toLowerCase();
315
+ const hits = contacts.filter((c) => c.personUid && c.email?.trim().toLowerCase() === email);
316
+ const unique = [...new Map(hits.map((c) => [c.personUid, c])).values()];
317
+ if (unique.length === 1) {
318
+ return { ok: true, personUid: unique[0].personUid, displayName: unique[0].displayName?.trim() || email };
319
+ }
320
+ return {
321
+ ok: false,
322
+ error: unique.length === 0
323
+ ? `No contact with the email '${target}'. They must share a company with you; pass their prs_/agt_ uid to add them anyway.`
324
+ : `'${target}' matches ${unique.length} contacts. Pass the prs_/agt_ uid instead.`,
325
+ };
326
+ }
327
+ const match = matchContactByName(contacts, target);
328
+ if (match?.personUid) {
329
+ return { ok: true, personUid: match.personUid, displayName: match.displayName?.trim() || target };
330
+ }
331
+ const names = contacts
332
+ .map((c) => c.displayName?.trim())
333
+ .filter((n) => Boolean(n))
334
+ .sort((a, b) => a.localeCompare(b));
335
+ return {
336
+ ok: false,
337
+ error: `No contact matches '${raw.trim()}'. Contacts: ${names.slice(0, 12).join(", ") || "(none)"}` +
338
+ `${names.length > 12 ? ", …" : ""}. Use the exact name, an email, or a prs_/agt_ uid.`,
339
+ };
340
+ }
341
+ async function fetchDmContacts(token) {
342
+ const res = await vaultApiFetch({ token, path: "/v1/notify/contacts" });
343
+ if (!res.ok)
344
+ return [];
345
+ const data = (await res.json().catch(() => ({})));
346
+ return Array.isArray(data.contacts) ? data.contacts : [];
347
+ }
348
+ async function resolveOneChannel(token, channelName) {
349
+ const channels = await fetchChannels(token);
350
+ const matches = matchChannelsByName(channels, channelName);
351
+ if (matches.length === 0) {
352
+ const message = `No channel named '${channelName}'. Run \`hq channels\` to see your channels.`;
353
+ throw new Error(withChannelSuggestions(message, suggestChannels(channels, channelName)));
354
+ }
355
+ if (matches.length > 1) {
356
+ const scopes = matches.map((match) => match.scope ?? "?").join(", ");
357
+ throw new Error(`'${channelName}' matches ${matches.length} channels (${scopes}). Name one of them.`);
358
+ }
359
+ return matches[0];
360
+ }
361
+ /** The server's refusal, as one plain sentence. */
362
+ export async function channelMembershipError(res, verb, fallback) {
363
+ const err = (await res.json().catch(() => ({})));
364
+ switch (err.code) {
365
+ case "CHANNEL_NOT_MEMBER":
366
+ return "Join the channel before adding members (so far you only browse it). Post once with `hq dm <channel> \"…\"`, or add yourself first with your own uid.";
367
+ case "INVITEE_NOT_FOUND":
368
+ return "That person or bot is not reachable from this company.";
369
+ case "CHANNEL_NOT_OWNER":
370
+ return verb === "add"
371
+ ? "Only the channel owner or a company admin can add members."
372
+ : "Only the channel owner or a company admin can remove other members, and the channel owner cannot be removed.";
373
+ default: {
374
+ const detail = err.error ?? err.message;
375
+ return detail ? `${fallback} (HTTP ${res.status}): ${detail}` : `${fallback} (HTTP ${res.status})`;
376
+ }
377
+ }
378
+ }
379
+ /**
380
+ * `hq channels add <channel> <member…>`: POST /v1/notify/channels/{id}/members
381
+ * once per member. Agents (`agt_…`) are allowed; this is how an owner adds a
382
+ * personal bot to a channel so everyone there can @mention it. The server
383
+ * reports `alreadyMember` on a repeat, which is printed, not treated as an error.
384
+ */
385
+ async function runChannelsAdd(channelName, members, opts) {
386
+ let failed;
387
+ try {
388
+ const token = await ensureCognitoToken();
389
+ const channel = await resolveOneChannel(token, channelName);
390
+ const contacts = await fetchDmContacts(token);
391
+ const results = [];
392
+ for (const raw of members) {
393
+ const target = resolveMemberTarget(contacts, raw);
394
+ if (!target.ok) {
395
+ results.push({ member: raw, ok: false, error: target.error });
396
+ continue;
397
+ }
398
+ const res = await vaultApiFetch({
399
+ token,
400
+ path: `/v1/notify/channels/${encodeURIComponent(channel.channelId)}/members`,
401
+ method: "POST",
402
+ body: { toPersonUid: target.personUid },
403
+ });
404
+ if (!res.ok) {
405
+ results.push({ member: raw, personUid: target.personUid, ok: false, error: await channelMembershipError(res, "add", "Could not add the member") });
406
+ continue;
407
+ }
408
+ const data = (await res.json().catch(() => ({})));
409
+ results.push({ member: target.displayName, personUid: target.personUid, ok: true, alreadyMember: data.alreadyMember === true });
410
+ }
411
+ const label = channel.name ?? channelName;
412
+ if (opts.json) {
413
+ console.log(JSON.stringify({ channelId: channel.channelId, name: label, results }, null, 2));
414
+ }
415
+ else {
416
+ for (const r of results) {
417
+ if (r.ok) {
418
+ console.log(r.alreadyMember ? chalk.dim(`${r.member} is already in #${label}.`) : chalk.green(`Added ${r.member} to #${label}.`));
419
+ }
420
+ else {
421
+ console.error(chalk.red(`${r.member}: ${r.error}`));
422
+ }
423
+ }
424
+ }
425
+ failed = results.some((r) => !r.ok);
426
+ }
427
+ catch (err) {
428
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
429
+ failed = true;
430
+ }
431
+ if (failed)
432
+ process.exit(1);
433
+ }
434
+ /** `hq channels remove <channel> <member>`: DELETE /v1/notify/channels/{id}/members/{uid}. */
435
+ async function runChannelsRemove(channelName, member, opts) {
436
+ try {
437
+ const token = await ensureCognitoToken();
438
+ const channel = await resolveOneChannel(token, channelName);
439
+ // The member is in the channel, so resolve against the roster first, then contacts.
440
+ const roster = await fetchChannelMembers(token, channel.channelId);
441
+ const rosterContacts = roster.map((m) => ({ personUid: m.personUid, email: m.email, displayName: m.displayName }));
442
+ let target = resolveMemberTarget(rosterContacts, member);
443
+ if (!target.ok)
444
+ target = resolveMemberTarget(await fetchDmContacts(token), member);
445
+ if (!target.ok)
446
+ throw new Error(target.error);
447
+ const res = await vaultApiFetch({
448
+ token,
449
+ path: `/v1/notify/channels/${encodeURIComponent(channel.channelId)}/members/${encodeURIComponent(target.personUid)}`,
450
+ method: "DELETE",
451
+ });
452
+ if (!res.ok)
453
+ throw new Error(await channelMembershipError(res, "remove", "Could not remove the member"));
454
+ const label = channel.name ?? channelName;
455
+ if (opts.json) {
456
+ console.log(JSON.stringify({ channelId: channel.channelId, personUid: target.personUid, removed: true }, null, 2));
457
+ }
458
+ else {
459
+ console.log(chalk.green(`Removed ${target.displayName} from #${label}.`));
460
+ }
461
+ }
462
+ catch (err) {
463
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
464
+ process.exit(1);
465
+ }
466
+ }
297
467
  export function registerChannelsCommand(program) {
298
468
  const channels = program
299
469
  .command("channels")
@@ -329,5 +499,19 @@ export function registerChannelsCommand(program) {
329
499
  .action(async (channelName, opts) => {
330
500
  await runChannelsDelete(channelName, opts);
331
501
  });
502
+ channels
503
+ .command("add <channel> <member...>")
504
+ .description("Add people or your own bots to a channel, by @handle, email, or prs_/agt_ uid.")
505
+ .option("--json", "Output the per-member results as JSON.")
506
+ .action(async (channelName, members, opts) => {
507
+ await runChannelsAdd(channelName, members, opts);
508
+ });
509
+ channels
510
+ .command("remove <channel> <member>")
511
+ .description("Remove a member or bot from a channel (owner or company admin), or leave it yourself.")
512
+ .option("--json", "Output the result as JSON.")
513
+ .action(async (channelName, member, opts) => {
514
+ await runChannelsRemove(channelName, member, opts);
515
+ });
332
516
  }
333
517
  //# sourceMappingURL=channels.js.map
@@ -147,7 +147,7 @@ export declare function checkpointInstruction(worker: Pick<ResolvedLaneWorker, "
147
147
  export declare function composeWorkerBrief(worker: ResolvedLaneWorker, taskBrief: string, hqRoot: string): string;
148
148
  /**
149
149
  * A lane with no worker still carries the standing authorization paragraph,
150
- * the foreground-only rule and the temp-home rule.
150
+ * the foreground-only rule, the temp-home rule and the self-review rule.
151
151
  */
152
152
  export declare function composeGeneralBrief(taskBrief: string, hqRoot: string): string;
153
153
  export declare const DISPATCH_TERMINAL_REASON: "outpost_dispatch_failed";
@@ -108,7 +108,7 @@ import { resolveKrFreshnessForShow } from "../lib/lanes/kr-freshness.js";
108
108
  import { ensureSessionHome } from "../lib/lanes/session-relink.js";
109
109
  import { defaultProbe, validateProfileLaneModel, validateLaneModel, } from "../lib/lanes/models.js";
110
110
  import { readLaneClaudeCredential } from "../lib/lanes/claude-credential.js";
111
- import { parseCliModel, refuseOutpostSelfTarget, resolveLaneModel, FOREGROUND_ONLY_RULE, STANDING_AUTHORIZATION, tempHomeRule, spawnLaneWorker, spawnEnvelopeRepairRequest, laneRunDir, readSpawnManifest, stopLaneProcesses, } from "../lib/lanes/spawn.js";
111
+ import { parseCliModel, refuseOutpostSelfTarget, resolveLaneModel, FOREGROUND_ONLY_RULE, STANDING_AUTHORIZATION, selfReviewRule, tempHomeRule, spawnLaneWorker, spawnEnvelopeRepairRequest, laneRunDir, readSpawnManifest, stopLaneProcesses, } from "../lib/lanes/spawn.js";
112
112
  import { handOffInstallHere } from "../lib/daemon/handoff.js";
113
113
  export { spawnLaneWorker };
114
114
  export function openPullRequestsForProject(hqRoot, metadata) {
@@ -3041,11 +3041,13 @@ export function composeWorkerBrief(worker, taskBrief, hqRoot) {
3041
3041
  "",
3042
3042
  tempHomeRule(hqRoot),
3043
3043
  "",
3044
+ selfReviewRule(hqRoot),
3045
+ "",
3044
3046
  ].join("\n");
3045
3047
  }
3046
3048
  /**
3047
3049
  * A lane with no worker still carries the standing authorization paragraph,
3048
- * the foreground-only rule and the temp-home rule.
3050
+ * the foreground-only rule, the temp-home rule and the self-review rule.
3049
3051
  */
3050
3052
  export function composeGeneralBrief(taskBrief, hqRoot) {
3051
3053
  return [
@@ -3058,6 +3060,8 @@ export function composeGeneralBrief(taskBrief, hqRoot) {
3058
3060
  "",
3059
3061
  tempHomeRule(hqRoot),
3060
3062
  "",
3063
+ selfReviewRule(hqRoot),
3064
+ "",
3061
3065
  ].join("\n");
3062
3066
  }
3063
3067
  function parseMaxRuntimeSeconds(raw, workerId) {
@@ -3071,6 +3071,25 @@ export function registerMeshCommand(program, deps = {}) {
3071
3071
  .description("Report daemon running/MQTT/spool/held/dead-letter/outbox/flush health")
3072
3072
  .option("--json", "Print machine-readable JSON")
3073
3073
  .action((opts) => wrap(() => runDaemonDoctorCmd(opts))());
3074
+ mesh
3075
+ .command("push-status")
3076
+ .description("Report whether the owner DM push signal is live or stale, with the last event time")
3077
+ .option("--json", "Print machine-readable JSON")
3078
+ .action((opts) => wrap(() => runMeshPushStatus(opts))());
3079
+ }
3080
+ async function runMeshPushStatus(opts) {
3081
+ const { ownerPushStatusForAccount, readOwnerInboxPushState, formatOwnerPushStatus } = await import("../lib/mesh/live/daemon/owner-inbox-push.js");
3082
+ const { describeCognitoTokenSource } = await import("../utils/cognito-session.js");
3083
+ const { actorKind } = describeCognitoTokenSource({ home: os.homedir(), env: process.env });
3084
+ const status = ownerPushStatusForAccount(readOwnerInboxPushState(workMeshRoot(undefined, process.env)), Date.now(), actorKind);
3085
+ if (opts.json) {
3086
+ console.log(JSON.stringify(status, null, 2));
3087
+ }
3088
+ else {
3089
+ console.log(formatOwnerPushStatus(status));
3090
+ }
3091
+ if (status.status === "stale")
3092
+ process.exitCode = 1;
3074
3093
  }
3075
3094
  async function runDaemonRun(opts = {}) {
3076
3095
  const { runMeshDaemon } = await import("../lib/mesh/live/daemon/run.js");