@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/dist/command-catalog.generated.d.ts +59 -1
  3. package/dist/command-catalog.generated.js +77 -1
  4. package/dist/commands/agent-kit.d.ts +23 -3
  5. package/dist/commands/agent-kit.js +110 -13
  6. package/dist/commands/agent-probe.d.ts +15 -7
  7. package/dist/commands/agent-probe.js +59 -21
  8. package/dist/commands/bot.d.ts +140 -1
  9. package/dist/commands/bot.js +757 -22
  10. package/dist/commands/dm.d.ts +10 -0
  11. package/dist/commands/dm.js +80 -0
  12. package/dist/lib/agent-kit/fallback.d.ts +63 -0
  13. package/dist/lib/agent-kit/fallback.js +129 -0
  14. package/dist/lib/agent-kit/run/inbox.d.ts +18 -6
  15. package/dist/lib/agent-kit/run/inbox.js +38 -6
  16. package/dist/lib/agent-kit/run/mesh-listener.d.ts +22 -6
  17. package/dist/lib/agent-kit/run/mesh-listener.js +44 -8
  18. package/dist/lib/agent-kit/run/supervisor.d.ts +35 -0
  19. package/dist/lib/agent-kit/run/supervisor.js +85 -0
  20. package/dist/lib/bot/api.d.ts +51 -0
  21. package/dist/lib/bot/api.js +32 -0
  22. package/dist/lib/bot/daemon.d.ts +17 -0
  23. package/dist/lib/bot/daemon.js +44 -3
  24. package/dist/lib/bot/index.d.ts +4 -0
  25. package/dist/lib/bot/index.js +4 -0
  26. package/dist/lib/bot/inflight.d.ts +14 -0
  27. package/dist/lib/bot/local-config.d.ts +70 -0
  28. package/dist/lib/bot/local-config.js +147 -0
  29. package/dist/lib/bot/local-name.d.ts +54 -0
  30. package/dist/lib/bot/local-name.js +114 -0
  31. package/dist/lib/bot/run.d.ts +9 -0
  32. package/dist/lib/bot/run.js +117 -24
  33. package/dist/lib/bot/runnable.d.ts +51 -0
  34. package/dist/lib/bot/runnable.js +65 -0
  35. package/dist/lib/bot/self-heal.d.ts +52 -0
  36. package/dist/lib/bot/self-heal.js +79 -0
  37. package/dist/lib/bot/split.d.ts +32 -0
  38. package/dist/lib/bot/split.js +241 -0
  39. package/dist/lib/mesh/live/daemon/credentials.d.ts +27 -0
  40. package/dist/lib/mesh/live/daemon/credentials.js +95 -0
  41. package/package.json +1 -1
@@ -0,0 +1,241 @@
1
+ /**
2
+ * A long reply, cut into posts HQ Cloud will actually accept.
3
+ *
4
+ * `POST /v1/notify/dm` refuses a body over DM_BODY_MAX characters with
5
+ * `400: Message body exceeds 4000 characters`. Nothing used to split, so a
6
+ * model answer over the limit was sent whole, refused, saved on the in-flight
7
+ * marker and re-sent identically on every poll — the answer was lost, the
8
+ * person was never told, and the bot asked the API to do the impossible about
9
+ * twice a second for as long as it ran.
10
+ *
11
+ * `splitBody` is the fix, and it is pure so it can be tested on its own:
12
+ *
13
+ * - a body at or under the limit comes back untouched, as one part
14
+ * - a longer body is cut on paragraph boundaries first, then on sentence
15
+ * boundaries, and only hard-wrapped when a single sentence is still too long
16
+ * - a fenced code block is kept whole when it fits; when it cannot fit, the
17
+ * fence is closed at the end of one part and reopened (with its language) at
18
+ * the start of the next, so neither part renders as broken markdown
19
+ * - when there is more than one part, each is prefixed `(2/3)` on its own line
20
+ * so the reader can see the order; the prefix is paid for out of the limit,
21
+ * never added on top of it
22
+ *
23
+ * Every returned part is guaranteed to be at most `max` characters long.
24
+ */
25
+ /** The largest body HQ Cloud accepts on a DM or channel post. */
26
+ export const DM_BODY_MAX = 4000;
27
+ const FENCE_RE = /^[ \t]*(`{3,}|~{3,})(.*)$/;
28
+ function bare(line) {
29
+ return line.replace(/\r?\n$/, "");
30
+ }
31
+ /**
32
+ * The body as paragraphs and whole fenced code blocks, in order. Concatenating
33
+ * every `raw` reproduces the input exactly, so nothing is lost or reflowed
34
+ * before a cut is actually needed.
35
+ */
36
+ function segmentize(body) {
37
+ const segments = [];
38
+ let buf = "";
39
+ let fence = null;
40
+ const flush = (fenceOpen) => {
41
+ if (buf)
42
+ segments.push({ raw: buf, fenceOpen });
43
+ buf = "";
44
+ };
45
+ for (const line of body.split(/(?<=\n)/)) {
46
+ const text = bare(line);
47
+ if (fence) {
48
+ buf += line;
49
+ const m = FENCE_RE.exec(text);
50
+ // A closing fence is the same character, at least as long, and bare.
51
+ if (m && m[1][0] === fence.marker[0] && m[1].length >= fence.marker.length && m[2].trim() === "") {
52
+ flush(fence.open);
53
+ fence = null;
54
+ }
55
+ continue;
56
+ }
57
+ const m = FENCE_RE.exec(text);
58
+ if (m) {
59
+ flush(null);
60
+ fence = { open: text.trimEnd(), marker: m[1] };
61
+ buf = line;
62
+ continue;
63
+ }
64
+ if (text.trim() === "") {
65
+ // A blank line closes the paragraph it follows.
66
+ if (buf)
67
+ buf += line;
68
+ else
69
+ buf = line;
70
+ flush(null);
71
+ continue;
72
+ }
73
+ buf += line;
74
+ }
75
+ flush(fence ? fence.open : null);
76
+ return segments;
77
+ }
78
+ /** Break points inside a paragraph: after sentence-ending punctuation, or at a line end. */
79
+ function splitSentences(text) {
80
+ const parts = [];
81
+ const re = /(?:[.!?…]["'”’)\]]*|\n)[ \t]*\n?[ \t]*/g;
82
+ let start = 0;
83
+ let m;
84
+ while ((m = re.exec(text)) !== null) {
85
+ const end = m.index + m[0].length;
86
+ if (end <= start) {
87
+ re.lastIndex = start + 1;
88
+ continue;
89
+ }
90
+ parts.push(text.slice(start, end));
91
+ start = end;
92
+ }
93
+ if (start < text.length)
94
+ parts.push(text.slice(start));
95
+ return parts.length > 0 ? parts : [text];
96
+ }
97
+ /** Last resort: a run of text with no usable break, cut at whitespace when there is any. */
98
+ function hardWrap(text, budget) {
99
+ const out = [];
100
+ let rest = text;
101
+ while (rest.length > budget) {
102
+ const window = rest.slice(0, budget);
103
+ const ws = Math.max(window.lastIndexOf(" "), window.lastIndexOf("\n"));
104
+ const cut = ws > 0 && ws >= Math.floor(budget * 0.6) ? ws : budget;
105
+ const piece = rest.slice(0, cut).trim();
106
+ if (piece)
107
+ out.push(piece);
108
+ rest = rest.slice(cut).replace(/^[ \t\n]+/, "");
109
+ }
110
+ const tail = rest.trim();
111
+ if (tail)
112
+ out.push(tail);
113
+ return out;
114
+ }
115
+ /** A code block too long for one part: closed and reopened around every cut. */
116
+ function splitFenced(segment, budget) {
117
+ const open = segment.fenceOpen ?? "```";
118
+ const marker = FENCE_RE.exec(open)?.[1] ?? "```";
119
+ const overhead = open.length + marker.length + 2; // the two fence lines and their newlines
120
+ const inner = budget - overhead;
121
+ if (inner < 1)
122
+ return splitPlain(segment.raw, budget);
123
+ const lines = segment.raw.split(/(?<=\n)/).slice(1);
124
+ const last = lines[lines.length - 1];
125
+ if (last !== undefined) {
126
+ const m = FENCE_RE.exec(bare(last));
127
+ if (m && m[1][0] === marker[0] && m[2].trim() === "")
128
+ lines.pop();
129
+ }
130
+ const pieces = [];
131
+ let buf = "";
132
+ const emit = () => {
133
+ const code = buf.replace(/\n+$/, "");
134
+ buf = "";
135
+ if (code.trim() === "")
136
+ return;
137
+ pieces.push(`${open}\n${code}\n${marker}`);
138
+ };
139
+ for (const line of lines) {
140
+ if ((buf + line).replace(/\n+$/, "").length <= inner) {
141
+ buf += line;
142
+ continue;
143
+ }
144
+ emit();
145
+ const one = bare(line);
146
+ if (one.length <= inner) {
147
+ buf = line;
148
+ continue;
149
+ }
150
+ for (const wrapped of hardWrap(one, inner)) {
151
+ buf = wrapped;
152
+ emit();
153
+ }
154
+ }
155
+ emit();
156
+ return pieces.length > 0 ? pieces : splitPlain(segment.raw, budget);
157
+ }
158
+ /** A paragraph too long for one part: sentences first, hard wrap only if one sentence is still too long. */
159
+ function splitPlain(text, budget) {
160
+ const out = [];
161
+ let buf = "";
162
+ const flush = () => {
163
+ const t = buf.trim();
164
+ buf = "";
165
+ if (t)
166
+ out.push(t);
167
+ };
168
+ for (const sentence of splitSentences(text)) {
169
+ if ((buf + sentence).trim().length <= budget) {
170
+ buf += sentence;
171
+ continue;
172
+ }
173
+ flush();
174
+ if (sentence.trim().length <= budget) {
175
+ buf = sentence;
176
+ continue;
177
+ }
178
+ for (const wrapped of hardWrap(sentence.trim(), budget))
179
+ out.push(wrapped);
180
+ }
181
+ flush();
182
+ return out.length > 0 ? out : hardWrap(text.trim(), budget);
183
+ }
184
+ function pack(body, budget) {
185
+ const out = [];
186
+ let acc = "";
187
+ const flush = () => {
188
+ const t = acc.trim();
189
+ acc = "";
190
+ if (t)
191
+ out.push(t);
192
+ };
193
+ for (const segment of segmentize(body)) {
194
+ if ((acc + segment.raw).trim().length <= budget) {
195
+ acc += segment.raw;
196
+ continue;
197
+ }
198
+ flush();
199
+ if (segment.raw.trim().length <= budget) {
200
+ acc = segment.raw;
201
+ continue;
202
+ }
203
+ for (const piece of segment.fenceOpen ? splitFenced(segment, budget) : splitPlain(segment.raw, budget)) {
204
+ out.push(piece);
205
+ }
206
+ }
207
+ flush();
208
+ return out;
209
+ }
210
+ /** `(2/3)` and its newline, at the widest the count can make it. */
211
+ function prefixWidth(count) {
212
+ return `(${count}/${count})\n`.length;
213
+ }
214
+ function label(parts) {
215
+ if (parts.length < 2)
216
+ return parts;
217
+ return parts.map((part, i) => `(${i + 1}/${parts.length})\n${part}`);
218
+ }
219
+ /**
220
+ * One outbound body as the ordered parts to post, each at most `max`
221
+ * characters. A body that already fits comes back unchanged.
222
+ */
223
+ export function splitBody(body, max = DM_BODY_MAX) {
224
+ const limit = Math.max(1, Math.floor(max));
225
+ if (body.length <= limit)
226
+ return [body];
227
+ let reserve = prefixWidth(2);
228
+ let parts = [];
229
+ for (let round = 0; round < 6; round += 1) {
230
+ parts = pack(body, Math.max(1, limit - reserve));
231
+ const needed = parts.length > 1 ? prefixWidth(parts.length) : 0;
232
+ if (needed <= reserve)
233
+ break;
234
+ reserve = needed;
235
+ }
236
+ if (parts.length === 0)
237
+ parts = [body.trim() || body];
238
+ // Belt and braces: whatever the shape of the input, no part may exceed the limit.
239
+ return label(parts).flatMap((part) => (part.length <= limit ? [part] : hardWrap(part, limit)));
240
+ }
241
+ //# sourceMappingURL=split.js.map
@@ -131,4 +131,31 @@ export declare class CredentialRenewalManager {
131
131
  stop(): void;
132
132
  private clear;
133
133
  }
134
+ /**
135
+ * Contract-2 (personal) realtime vend. Backed server-side by the personal
136
+ * session policy, which grants Subscribe/Receive on the caller's own
137
+ * `hq/{principalUid}/{dm,sessions,work,notifications,meeting,sync}` topics —
138
+ * unlike contract 3, whose policy only covers company presence and the thread
139
+ * directory. Doorbell listeners on personal topics MUST use this contract;
140
+ * subscribing to personal topics with a contract-3 session makes AWS IoT drop
141
+ * the connection before SUBACK.
142
+ */
143
+ export interface PersonalRealtimeBundle {
144
+ contractVersion: 2;
145
+ credentials: IotCredentials;
146
+ iotEndpoint: string;
147
+ region: string;
148
+ clientId: string;
149
+ actorUid: string;
150
+ /** Advertised personal topics keyed by kind (dm, sessions, work, notifications, …). */
151
+ topics: Record<string, string>;
152
+ expiresAt: string;
153
+ }
154
+ export type PersonalCredentialsFetcher = () => Promise<PersonalRealtimeBundle>;
155
+ export declare function normalizePersonalRealtimeBundle(raw: unknown): PersonalRealtimeBundle;
156
+ export declare function createPersonalRealtimeFetcher(opts: {
157
+ token: string;
158
+ baseUrl?: string;
159
+ post?: (path: string, body: unknown) => Promise<CredentialVendPostResult>;
160
+ }): PersonalCredentialsFetcher;
134
161
  //# sourceMappingURL=credentials.d.ts.map
@@ -385,4 +385,99 @@ export class CredentialRenewalManager {
385
385
  }
386
386
  }
387
387
  }
388
+ export function normalizePersonalRealtimeBundle(raw) {
389
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
390
+ throw new Error("contract-2 vend response is not an object");
391
+ }
392
+ const r = raw;
393
+ if (r.contractVersion !== 2) {
394
+ throw new Error(`expected contractVersion 2, got ${String(r.contractVersion)}`);
395
+ }
396
+ const creds = r.credentials;
397
+ if (!creds ||
398
+ typeof creds.accessKeyId !== "string" ||
399
+ typeof creds.secretAccessKey !== "string" ||
400
+ typeof creds.sessionToken !== "string") {
401
+ throw new Error("contract-2 vend missing credentials");
402
+ }
403
+ for (const key of ["clientId", "iotEndpoint", "region"]) {
404
+ if (typeof r[key] !== "string" || !r[key].trim()) {
405
+ throw new Error(`contract-2 vend missing ${key}`);
406
+ }
407
+ }
408
+ const topicsRaw = r.topics && typeof r.topics === "object" && !Array.isArray(r.topics)
409
+ ? r.topics
410
+ : {};
411
+ const topics = {};
412
+ for (const [k, v] of Object.entries(topicsRaw)) {
413
+ if (typeof v === "string" && v.trim())
414
+ topics[k] = v.trim();
415
+ }
416
+ if (!topics.dm && typeof r.topic === "string")
417
+ topics.dm = r.topic;
418
+ const actorUid = (typeof r.principalUid === "string" && /^(?:prs|agt)_[A-Za-z0-9]+$/.test(r.principalUid)
419
+ ? r.principalUid
420
+ : undefined) ||
421
+ actorUidFromPersonalTopic(topics.dm) ||
422
+ "";
423
+ if (!actorUid)
424
+ throw new Error("contract-2 vend could not derive actorUid");
425
+ // Only keep topics that belong to the vended principal.
426
+ for (const [k, v] of Object.entries(topics)) {
427
+ if (actorUidFromPersonalTopic(v) !== actorUid)
428
+ delete topics[k];
429
+ }
430
+ const expiresAt = (typeof r.expiresAt === "string" && r.expiresAt) ||
431
+ (typeof creds.expiration === "string" && creds.expiration) ||
432
+ "";
433
+ if (!expiresAt)
434
+ throw new Error("contract-2 vend missing expiresAt");
435
+ return {
436
+ contractVersion: 2,
437
+ credentials: {
438
+ accessKeyId: creds.accessKeyId,
439
+ secretAccessKey: creds.secretAccessKey,
440
+ sessionToken: creds.sessionToken,
441
+ expiration: expiresAt,
442
+ },
443
+ iotEndpoint: r.iotEndpoint,
444
+ region: r.region,
445
+ clientId: r.clientId,
446
+ actorUid,
447
+ topics,
448
+ expiresAt,
449
+ };
450
+ }
451
+ export function createPersonalRealtimeFetcher(opts) {
452
+ return async () => {
453
+ if (opts.post) {
454
+ const res = await opts.post(REALTIME_CREDENTIALS_PATH, { contractVersion: 2 });
455
+ if (res.status < 200 || res.status >= 300) {
456
+ throw classifyCredentialVendFailure(res.status, res.body, headerGet(res.headers, "Retry-After"));
457
+ }
458
+ return normalizePersonalRealtimeBundle(res.body);
459
+ }
460
+ const res = await vaultApiFetch({
461
+ token: opts.token,
462
+ path: REALTIME_CREDENTIALS_PATH,
463
+ method: "POST",
464
+ body: { contractVersion: 2 },
465
+ baseUrl: opts.baseUrl ?? DEFAULT_VAULT_API_URL,
466
+ });
467
+ const text = await res.text();
468
+ let body = {};
469
+ if (text) {
470
+ try {
471
+ body = JSON.parse(text);
472
+ }
473
+ catch {
474
+ body = { raw: text.slice(0, 200) };
475
+ }
476
+ }
477
+ if (!res.ok) {
478
+ throw classifyCredentialVendFailure(res.status, body, res.headers.get("Retry-After"));
479
+ }
480
+ return normalizePersonalRealtimeBundle(body);
481
+ };
482
+ }
388
483
  //# sourceMappingURL=credentials.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.116.0",
3
+ "version": "5.117.1",
4
4
  "description": "HQ by Indigo management CLI \u2014 modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {