@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.
- package/CHANGELOG.md +23 -0
- package/dist/command-catalog.generated.d.ts +126 -0
- package/dist/command-catalog.generated.js +155 -0
- package/dist/commands/bot.d.ts +29 -1
- package/dist/commands/bot.js +282 -22
- package/dist/commands/channels.d.ts +18 -1
- package/dist/commands/channels.js +185 -1
- package/dist/commands/lanes.d.ts +1 -1
- package/dist/commands/lanes.js +6 -2
- package/dist/commands/mesh.js +19 -0
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2285 -1743
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/bot/config.d.ts +8 -0
- package/dist/lib/bot/fleet-local.d.ts +7 -3
- package/dist/lib/bot/fleet-local.js +234 -64
- package/dist/lib/bot/run.js +39 -2
- package/dist/lib/bot/runtime-auth.d.ts +119 -0
- package/dist/lib/bot/runtime-auth.js +251 -0
- package/dist/lib/daemon/catalog.d.ts +1 -0
- package/dist/lib/daemon/catalog.js +1 -1
- package/dist/lib/daemon/run.d.ts +2 -0
- package/dist/lib/daemon/run.js +64 -16
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/hot-channels/adapters/meetings.js +3 -1
- package/dist/lib/hot-channels/contract.d.ts +6 -0
- package/dist/lib/hot-channels/ledger.js +17 -0
- package/dist/lib/hot-channels/loop.js +2 -0
- package/dist/lib/lanes/accounts.d.ts +1 -17
- package/dist/lib/lanes/accounts.js +5 -45
- package/dist/lib/lanes/provider-home.d.ts +24 -0
- package/dist/lib/lanes/provider-home.js +52 -0
- package/dist/lib/lanes/spawn.d.ts +12 -0
- package/dist/lib/lanes/spawn.js +27 -0
- package/dist/lib/mesh/live/daemon/doctor.d.ts +3 -0
- package/dist/lib/mesh/live/daemon/doctor.js +13 -0
- package/dist/lib/mesh/live/daemon/owner-inbox-push.d.ts +94 -0
- package/dist/lib/mesh/live/daemon/owner-inbox-push.js +273 -0
- package/dist/lib/mesh/live/daemon/run.js +31 -0
- package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +14 -0
- package/dist/lib/mesh/live/daemon/work-subscribe.js +210 -3
- package/dist/lib/mesh/live/paths.d.ts +2 -0
- package/dist/lib/mesh/live/paths.js +2 -0
- package/dist/lib/sync-core/flag.d.ts +1 -1
- package/dist/lib/sync-core/flag.js +1 -1
- package/dist/lib/sync-core/reconcile.d.ts +1 -1
- package/dist/lib/sync-core/reconcile.js +9 -7
- package/dist/lib/sync-core/release.d.ts +3 -3
- package/dist/lib/sync-core/release.js +4 -4
- package/dist/lib/sync-core/rollout-state.d.ts +1 -0
- package/dist/lib/sync-core/rollout-state.js +1 -0
- 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
|
package/dist/commands/lanes.d.ts
CHANGED
|
@@ -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
|
|
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";
|
package/dist/commands/lanes.js
CHANGED
|
@@ -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
|
|
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) {
|
package/dist/commands/mesh.js
CHANGED
|
@@ -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");
|