@indigoai-us/hq-cli 5.116.0 → 5.117.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 +125 -0
- package/dist/command-catalog.generated.d.ts +59 -1
- package/dist/command-catalog.generated.js +77 -1
- package/dist/commands/agent-kit.d.ts +23 -3
- package/dist/commands/agent-kit.js +110 -13
- package/dist/commands/agent-probe.d.ts +15 -7
- package/dist/commands/agent-probe.js +59 -21
- package/dist/commands/bot.d.ts +140 -1
- package/dist/commands/bot.js +757 -22
- package/dist/commands/dm.d.ts +10 -0
- package/dist/commands/dm.js +80 -0
- package/dist/lib/agent-kit/fallback.d.ts +63 -0
- package/dist/lib/agent-kit/fallback.js +129 -0
- package/dist/lib/agent-kit/run/inbox.d.ts +18 -6
- package/dist/lib/agent-kit/run/inbox.js +38 -6
- package/dist/lib/agent-kit/run/mesh-listener.d.ts +22 -6
- package/dist/lib/agent-kit/run/mesh-listener.js +44 -8
- package/dist/lib/agent-kit/run/supervisor.d.ts +35 -0
- package/dist/lib/agent-kit/run/supervisor.js +85 -0
- package/dist/lib/bot/api.d.ts +51 -0
- package/dist/lib/bot/api.js +32 -0
- package/dist/lib/bot/daemon.d.ts +17 -0
- package/dist/lib/bot/daemon.js +44 -3
- package/dist/lib/bot/index.d.ts +4 -0
- package/dist/lib/bot/index.js +4 -0
- package/dist/lib/bot/inflight.d.ts +14 -0
- package/dist/lib/bot/local-config.d.ts +70 -0
- package/dist/lib/bot/local-config.js +147 -0
- package/dist/lib/bot/local-name.d.ts +54 -0
- package/dist/lib/bot/local-name.js +114 -0
- package/dist/lib/bot/run.d.ts +9 -0
- package/dist/lib/bot/run.js +117 -24
- package/dist/lib/bot/runnable.d.ts +51 -0
- package/dist/lib/bot/runnable.js +65 -0
- package/dist/lib/bot/self-heal.d.ts +52 -0
- package/dist/lib/bot/self-heal.js +79 -0
- package/dist/lib/bot/split.d.ts +32 -0
- package/dist/lib/bot/split.js +241 -0
- package/dist/lib/mesh/live/daemon/credentials.d.ts +27 -0
- package/dist/lib/mesh/live/daemon/credentials.js +95 -0
- package/package.json +1 -1
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
*
|
|
5
5
|
* whoami mint as the machine identity; token names this agent
|
|
6
6
|
* team-sync `hq sync pull --all` succeeds and the company folder exists
|
|
7
|
-
* work-mesh realtime
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* work-mesh the personal (contract-2) realtime vend names this agent, the
|
|
8
|
+
* agent-authorized inbox route answers 200, and this host's own
|
|
9
|
+
* presence is live: a fresh last-heartbeat.json plus an ok
|
|
10
|
+
* component-mesh stamp from the doorbell listener. (The company
|
|
11
|
+
* roster route is owner/admin-only and 404s for agt_ callers.)
|
|
12
|
+
* dm send a DM to self via POST /v1/notify/dm and read it back
|
|
13
|
+
* through GET /v1/agents/{uid}/inbox (the agent read surface;
|
|
14
|
+
* /v1/notify/thread is intentionally closed to agents)
|
|
10
15
|
* secrets `GET /secrets/{companyUid}` answers 200
|
|
11
16
|
*
|
|
12
17
|
* Output is the exact shape the console recipes promise: one
|
|
@@ -26,8 +31,11 @@ import { CLI_VERSION } from "../cli-version.js";
|
|
|
26
31
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
27
32
|
import { peekIdToken } from "../utils/id-token.js";
|
|
28
33
|
import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
34
|
+
import { sendAgentDm } from "./agents.js";
|
|
35
|
+
import { createPersonalRealtimeFetcher } from "../lib/mesh/live/daemon/credentials.js";
|
|
36
|
+
import { readComponentStatus } from "../lib/agent-kit/creds.js";
|
|
37
|
+
import { COMPONENT_STALE_AFTER_MS } from "../lib/agent-kit/run/heartbeat.js";
|
|
38
|
+
import { defaultFetchInbox } from "../lib/agent-kit/run/inbox.js";
|
|
31
39
|
import { agentKitPaths } from "../lib/agent-kit/paths.js";
|
|
32
40
|
import { readKitConfig } from "../lib/agent-kit/kit-config.js";
|
|
33
41
|
import { readLastHeartbeat } from "../lib/agent-kit/run/heartbeat.js";
|
|
@@ -35,6 +43,8 @@ import { defaultRunPull, syncPullArgs } from "../lib/agent-kit/run/sync.js";
|
|
|
35
43
|
import { requireExternalCreds } from "./agent-kit.js";
|
|
36
44
|
export const DM_ROUNDTRIP_TIMEOUT_MS = 20_000;
|
|
37
45
|
export const DM_ROUNDTRIP_POLL_MS = 2_000;
|
|
46
|
+
/** A heartbeat older than this does not count as live presence (3 missed 60 s beats). */
|
|
47
|
+
export const PRESENCE_FRESH_SECONDS = 180;
|
|
38
48
|
function errText(err) {
|
|
39
49
|
return err instanceof Error ? err.message : String(err);
|
|
40
50
|
}
|
|
@@ -108,7 +118,7 @@ export async function runProbe(deps) {
|
|
|
108
118
|
else {
|
|
109
119
|
checks.push({ name: "team-sync", ok: false, detail: "skipped: no token" });
|
|
110
120
|
}
|
|
111
|
-
// 3. mesh-presence
|
|
121
|
+
// 3. work-mesh: vend + agent-authorized reachability + self presence
|
|
112
122
|
if (token && companyUid) {
|
|
113
123
|
try {
|
|
114
124
|
const vend = await deps.vendRealtime(token);
|
|
@@ -116,17 +126,23 @@ export async function runProbe(deps) {
|
|
|
116
126
|
checks.push({ name: "work-mesh", ok: false, detail: `realtime vend is for ${vend.actorUid}` });
|
|
117
127
|
}
|
|
118
128
|
else {
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
const
|
|
129
|
+
const inbox = await deps.readInbox(token);
|
|
130
|
+
const age = heartbeatAgeSeconds(deps.paths, now);
|
|
131
|
+
const mesh = readComponentStatus(deps.paths, "mesh", COMPONENT_STALE_AFTER_MS.mesh, now);
|
|
132
|
+
const reachable = inbox.status === 200;
|
|
133
|
+
const fresh = age !== null && age <= PRESENCE_FRESH_SECONDS;
|
|
134
|
+
const meshOk = mesh.status === "ok";
|
|
135
|
+
const ok = reachable && fresh && meshOk;
|
|
136
|
+
const parts = [
|
|
137
|
+
"realtime vend ok",
|
|
138
|
+
`inbox GET → ${inbox.status}`,
|
|
139
|
+
`heartbeat ${age === null ? "never" : `${age}s ago`}`,
|
|
140
|
+
`mesh listener ${mesh.status}${mesh.at ? ` @${mesh.at.toISOString()}` : ""}`,
|
|
141
|
+
];
|
|
124
142
|
checks.push({
|
|
125
143
|
name: "work-mesh",
|
|
126
144
|
ok,
|
|
127
|
-
detail:
|
|
128
|
-
? `realtime vend ok; presence=${presence} lastHeartbeatAt=${last}${ok ? "" : " — is `hq agent kit` installed and running?"}`
|
|
129
|
-
: "realtime vend ok but this agent is not on the company roster",
|
|
145
|
+
detail: `${parts.join("; ")}${ok ? "" : " — is `hq agent kit` installed and running? see ~/.hq-agent/logs/mesh.log"}`,
|
|
130
146
|
});
|
|
131
147
|
}
|
|
132
148
|
}
|
|
@@ -137,27 +153,37 @@ export async function runProbe(deps) {
|
|
|
137
153
|
else {
|
|
138
154
|
checks.push({ name: "work-mesh", ok: false, detail: "skipped: no token/company" });
|
|
139
155
|
}
|
|
140
|
-
// 4. dm-
|
|
156
|
+
// 4. dm round-trip: send via notify, read back via the agent inbox
|
|
141
157
|
if (token) {
|
|
142
158
|
try {
|
|
143
159
|
const body = `hq agent probe ${nonce}`;
|
|
144
160
|
await deps.sendDm(token, self, body);
|
|
145
161
|
const deadline = now().getTime() + (deps.dmTimeoutMs ?? DM_ROUNDTRIP_TIMEOUT_MS);
|
|
146
162
|
let found = false;
|
|
163
|
+
let lastStatus = 0;
|
|
147
164
|
for (;;) {
|
|
148
|
-
const
|
|
149
|
-
|
|
165
|
+
const res = await deps.readInbox(token);
|
|
166
|
+
lastStatus = res.status;
|
|
167
|
+
if (res.status === 200 && inboxContainsNonce(res.body, nonce)) {
|
|
150
168
|
found = true;
|
|
151
169
|
break;
|
|
152
170
|
}
|
|
171
|
+
// A 4xx will not heal by polling (auth/route refusal): stop now.
|
|
172
|
+
if (res.status >= 400 && res.status < 500)
|
|
173
|
+
break;
|
|
153
174
|
if (now().getTime() >= deadline)
|
|
154
175
|
break;
|
|
155
176
|
await sleep(DM_ROUNDTRIP_POLL_MS);
|
|
156
177
|
}
|
|
178
|
+
const secs = Math.round((deps.dmTimeoutMs ?? DM_ROUNDTRIP_TIMEOUT_MS) / 1000);
|
|
157
179
|
checks.push({
|
|
158
180
|
name: "dm",
|
|
159
181
|
ok: found,
|
|
160
|
-
detail: found
|
|
182
|
+
detail: found
|
|
183
|
+
? "sent to self via /v1/notify/dm and read back from /v1/agents/{uid}/inbox"
|
|
184
|
+
: lastStatus === 200
|
|
185
|
+
? `sent but not visible in /v1/agents/{uid}/inbox after ${secs}s`
|
|
186
|
+
: `sent but /v1/agents/{uid}/inbox answered ${lastStatus}`,
|
|
161
187
|
});
|
|
162
188
|
}
|
|
163
189
|
catch (err) {
|
|
@@ -205,6 +231,19 @@ export async function runProbe(deps) {
|
|
|
205
231
|
}
|
|
206
232
|
return result;
|
|
207
233
|
}
|
|
234
|
+
/** True when any inbox item (whatever its channel shape) carries the nonce. */
|
|
235
|
+
export function inboxContainsNonce(body, nonce) {
|
|
236
|
+
const rec = body && typeof body === "object" ? body : {};
|
|
237
|
+
const messages = Array.isArray(rec.messages) ? rec.messages : [];
|
|
238
|
+
return messages.some((m) => {
|
|
239
|
+
try {
|
|
240
|
+
return JSON.stringify(m).includes(nonce);
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
208
247
|
export function formatHeartbeatAge(age) {
|
|
209
248
|
if (age === null)
|
|
210
249
|
return "never (kit heartbeat has not posted yet)";
|
|
@@ -241,12 +280,11 @@ export function defaultProbeDeps(paths, creds) {
|
|
|
241
280
|
resolveCompanyUid: (token, slug) => getCompanyUid(token, slug),
|
|
242
281
|
runPull: defaultRunPull(process.execPath, process.argv[1], process.env),
|
|
243
282
|
vendRealtime: async (token) => {
|
|
244
|
-
const bundle = await
|
|
283
|
+
const bundle = await createPersonalRealtimeFetcher({ token, baseUrl: base })();
|
|
245
284
|
return { actorUid: bundle.actorUid };
|
|
246
285
|
},
|
|
247
|
-
|
|
286
|
+
readInbox: (token) => defaultFetchInbox(creds.entityUid, base)(token),
|
|
248
287
|
sendDm: (token, to, body) => sendAgentDm(token, to, body),
|
|
249
|
-
readThread: async (token, withUid) => readAgentThread(token, withUid, 20),
|
|
250
288
|
listSecrets: async (token, companyUid) => {
|
|
251
289
|
const res = await vaultApiFetch({
|
|
252
290
|
token,
|
package/dist/commands/bot.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { type RuntimeSignInIssue } from "../lib/bot/runtime-sign-in.js";
|
|
3
|
-
import { type BotKind, type BotMemoryMode, type BotRuntimeId, type LaunchctlExec } from "../lib/bot/index.js";
|
|
3
|
+
import { type BotLocalConfig, type MyLocalBot, type BotKind, type BotMemoryMode, type BotNotRunnableReason, type BotConfig, type BotRuntimeId, type LaunchctlExec } from "../lib/bot/index.js";
|
|
4
4
|
/** Real launchctl; every failure is a non-ok result, never a throw. */
|
|
5
5
|
export declare const defaultLaunchctl: LaunchctlExec;
|
|
6
6
|
/** The desktop app creates HQ's setup bot under this name, always from the `setup` worker. */
|
|
@@ -101,5 +101,144 @@ export declare function runBotSet(nameArg: string, opts: {
|
|
|
101
101
|
effort?: string;
|
|
102
102
|
json?: boolean;
|
|
103
103
|
}): Promise<void>;
|
|
104
|
+
/**
|
|
105
|
+
* The one bot the caller means. Matches the local folder name, the cloud slug,
|
|
106
|
+
* the display name, or the agt_ uid; an ambiguous name is refused rather than
|
|
107
|
+
* guessed at.
|
|
108
|
+
*/
|
|
109
|
+
export declare function matchRemoteBots(bots: readonly MyLocalBot[], wanted: string): MyLocalBot[];
|
|
110
|
+
export declare function resolveRemoteBot(bots: readonly MyLocalBot[], wanted: string): MyLocalBot;
|
|
111
|
+
/**
|
|
112
|
+
* Why HQ could not say which bots the account owns. The desktop app switches
|
|
113
|
+
* on these values, so they are contract, not prose:
|
|
114
|
+
*
|
|
115
|
+
* server-unsupported the HQ Cloud in front of us has no listing route
|
|
116
|
+
* auth the sign-in was refused
|
|
117
|
+
* network HQ could not be reached at all
|
|
118
|
+
* server-error HQ answered 5xx
|
|
119
|
+
* error anything else
|
|
120
|
+
*/
|
|
121
|
+
export type RemoteBotsUnavailableReason = "server-unsupported" | "auth" | "network" | "server-error" | "error";
|
|
122
|
+
export interface RemoteBotsUnavailable {
|
|
123
|
+
reason: RemoteBotsUnavailableReason;
|
|
124
|
+
message: string;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Turn any failure of `GET /v1/agents/mine` into one plain sentence plus a
|
|
128
|
+
* stable reason. Production HQ Cloud does not serve that route yet, and an
|
|
129
|
+
* uncaught 404 reached the person verbatim as "HQ API /v1/agents/mine → 404:
|
|
130
|
+
* Not found" — and, under --json, as no document at all.
|
|
131
|
+
*/
|
|
132
|
+
export declare function remoteBotsUnavailable(err: unknown): RemoteBotsUnavailable;
|
|
133
|
+
/**
|
|
134
|
+
* A cloud teardown that only got part of the way, said in the same vocabulary
|
|
135
|
+
* the desktop switches on, plus the one decision that follows from it.
|
|
136
|
+
*
|
|
137
|
+
* `DELETE /v1/agents/{uid}` answers 200 with `terminal:false` and a per-step
|
|
138
|
+
* `failures` map when it could do only part of the job. Those strings are raw
|
|
139
|
+
* infrastructure: one real answer carried the AWS account id, an assumed-role
|
|
140
|
+
* ARN and a DynamoDB table ARN. They are classified here and never repeated —
|
|
141
|
+
* `detail` is a plain clause, `steps` is the server's own step names and
|
|
142
|
+
* nothing of what they said.
|
|
143
|
+
*
|
|
144
|
+
* `retryable` is the local-state decision. Running `hq bot rm` again can only
|
|
145
|
+
* finish the job when the step that failed might behave differently next time.
|
|
146
|
+
* A refusal, or a step whose target was already gone, answers the same way
|
|
147
|
+
* forever, so the local copy goes and the message says the leftover is HQ's.
|
|
148
|
+
*
|
|
149
|
+
* `reason` reuses {@link RemoteBotsUnavailableReason}: `auth` for a refused
|
|
150
|
+
* step (a permission problem, on either side of the call), `error` for a step
|
|
151
|
+
* whose target was already gone, `server-error` for anything HQ may yet
|
|
152
|
+
* finish.
|
|
153
|
+
*/
|
|
154
|
+
export interface BotRemovalIncomplete {
|
|
155
|
+
reason: RemoteBotsUnavailableReason;
|
|
156
|
+
/** One plain clause naming what stopped. Never a transport string. */
|
|
157
|
+
detail: string;
|
|
158
|
+
/** Whether running the same command again can still finish the removal. */
|
|
159
|
+
retryable: boolean;
|
|
160
|
+
/** The server's step names only — no ids, ARNs or messages. */
|
|
161
|
+
steps: string[];
|
|
162
|
+
}
|
|
163
|
+
export declare function botRemovalIncomplete(failures?: Record<string, string>): BotRemovalIncomplete;
|
|
164
|
+
/**
|
|
165
|
+
* Rebuild a bot.json from the settings the cloud kept for it. A bot created
|
|
166
|
+
* before HQ saved settings comes back on defaults, and anything this HQ can no
|
|
167
|
+
* longer provide (a worker that is not here, a thinking level the chosen
|
|
168
|
+
* runtime does not accept) is reported rather than silently kept.
|
|
169
|
+
*/
|
|
170
|
+
export declare function configFromLocalConfig(input: {
|
|
171
|
+
name: string;
|
|
172
|
+
agentUid: string;
|
|
173
|
+
ownerUid: string;
|
|
174
|
+
hqRoot: string;
|
|
175
|
+
dir: string;
|
|
176
|
+
local: BotLocalConfig | null;
|
|
177
|
+
overrides?: {
|
|
178
|
+
runtime?: BotRuntimeId;
|
|
179
|
+
model?: string;
|
|
180
|
+
};
|
|
181
|
+
now?: () => Date;
|
|
182
|
+
}): {
|
|
183
|
+
config: BotConfig;
|
|
184
|
+
notes: string[];
|
|
185
|
+
};
|
|
186
|
+
export interface BotAdoptResult {
|
|
187
|
+
name: string;
|
|
188
|
+
agentUid: string;
|
|
189
|
+
runtime: BotRuntimeId;
|
|
190
|
+
kind: BotKind;
|
|
191
|
+
companies: string[];
|
|
192
|
+
dir: string;
|
|
193
|
+
worker: string;
|
|
194
|
+
memory: BotMemoryMode;
|
|
195
|
+
memoryDir: string;
|
|
196
|
+
/** True when the cloud had no saved settings and defaults were used. */
|
|
197
|
+
defaulted: boolean;
|
|
198
|
+
notes: string[];
|
|
199
|
+
daemon: string | null;
|
|
200
|
+
}
|
|
201
|
+
export declare function runBotAdopt(nameArg: string, opts: {
|
|
202
|
+
runtime?: string;
|
|
203
|
+
model?: string;
|
|
204
|
+
daemon?: boolean;
|
|
205
|
+
start?: boolean;
|
|
206
|
+
json?: boolean;
|
|
207
|
+
}): Promise<void>;
|
|
208
|
+
export interface BotRestoreRow {
|
|
209
|
+
name: string;
|
|
210
|
+
agentUid: string;
|
|
211
|
+
/** restored = brought back here; repaired = new credentials for one already here. */
|
|
212
|
+
action: "restored" | "repaired" | "skipped" | "failed" | "would-restore" | "would-repair" | "would-skip";
|
|
213
|
+
detail: string;
|
|
214
|
+
/** Present only on a bot skipped because this computer cannot run it. */
|
|
215
|
+
reason?: BotNotRunnableReason;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* `hq bot restore` — every local bot this account owns that is not set up on
|
|
219
|
+
* this computer comes back. This is what the desktop app and a post-install
|
|
220
|
+
* hook call after a reinstall or on a new Mac.
|
|
221
|
+
*/
|
|
222
|
+
export declare function runBotRestore(opts: {
|
|
223
|
+
all?: boolean;
|
|
224
|
+
dryRun?: boolean;
|
|
225
|
+
daemon?: boolean;
|
|
226
|
+
start?: boolean;
|
|
227
|
+
json?: boolean;
|
|
228
|
+
}): Promise<void>;
|
|
229
|
+
export interface RemoteBotRow {
|
|
230
|
+
name: string;
|
|
231
|
+
agentUid: string;
|
|
232
|
+
kind: string;
|
|
233
|
+
online: boolean;
|
|
234
|
+
lastHeartbeatAt: string | null;
|
|
235
|
+
/** True when this bot is set up on this computer. */
|
|
236
|
+
here: boolean;
|
|
237
|
+
settings: string;
|
|
238
|
+
/** False when this computer cannot run it, whatever the listing says. */
|
|
239
|
+
runnable: boolean;
|
|
240
|
+
/** Why not, when `runnable` is false: the same vocabulary adopt and restore use. */
|
|
241
|
+
reason: BotNotRunnableReason | null;
|
|
242
|
+
}
|
|
104
243
|
export declare function registerBotCommand(program: Command): void;
|
|
105
244
|
//# sourceMappingURL=bot.d.ts.map
|