@indigoai-us/hq-cli 5.66.0 → 5.68.0
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/dist/commands/agents.d.ts +24 -0
- package/dist/commands/agents.js +147 -2
- package/package.json +2 -2
- package/src/commands/agents.test.ts +105 -0
- package/src/commands/agents.ts +202 -0
|
@@ -126,5 +126,29 @@ export declare function deprovisionAgent(token: string, agentUid: string): Promi
|
|
|
126
126
|
}>;
|
|
127
127
|
/** Human-readable "hot-applied to the running box" vs "saved for next launch". */
|
|
128
128
|
export declare function appliedHint(applied: boolean): string;
|
|
129
|
+
/** One message in a DM thread, as returned by `GET /v1/notify/thread`. */
|
|
130
|
+
export interface DmThreadMessage {
|
|
131
|
+
eventId: string;
|
|
132
|
+
fromPersonUid: string;
|
|
133
|
+
fromDisplayName?: string;
|
|
134
|
+
fromEmail?: string;
|
|
135
|
+
body: string;
|
|
136
|
+
createdAt: string;
|
|
137
|
+
/** "out" = sent by the caller; "in" = received (the agent's reply). */
|
|
138
|
+
direction: "in" | "out";
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Resolve an agent reference to its `agt_` uid. An `agt_…` value is used
|
|
142
|
+
* directly; anything else is treated as a slug or display name and resolved
|
|
143
|
+
* against the company's roster (requires `--company` or a single active
|
|
144
|
+
* membership).
|
|
145
|
+
*/
|
|
146
|
+
export declare function resolveAgentUid(token: string, ref: string, companySlug: string | undefined): Promise<string>;
|
|
147
|
+
/** Send a DM to an agent. Returns nothing meaningful beyond success. */
|
|
148
|
+
export declare function sendAgentDm(token: string, agentUid: string, message: string): Promise<void>;
|
|
149
|
+
/** Read the two-way DM conversation with an agent (most recent `limit`). */
|
|
150
|
+
export declare function readAgentThread(token: string, agentUid: string, limit?: number): Promise<DmThreadMessage[]>;
|
|
151
|
+
/** Render one thread message as a labeled line for the terminal. */
|
|
152
|
+
export declare function formatThreadMessage(m: DmThreadMessage, agentLabel: string): string;
|
|
129
153
|
export declare function registerAgentsCommand(program: Command): void;
|
|
130
154
|
//# sourceMappingURL=agents.d.ts.map
|
package/dist/commands/agents.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* the caller's single active membership (same as `members.ts`).
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
25
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="26009666-ef42-5f7d-88eb-6452eca4b47a")}catch(e){}}();
|
|
26
26
|
import chalk from "chalk";
|
|
27
27
|
import { randomUUID } from "node:crypto";
|
|
28
28
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
@@ -144,6 +144,69 @@ export function appliedHint(applied) {
|
|
|
144
144
|
: "saved — takes effect on the agent's next launch";
|
|
145
145
|
}
|
|
146
146
|
// ---------------------------------------------------------------------------
|
|
147
|
+
// Talking to agents (DM). Agents are members addressable over HQ's DM system:
|
|
148
|
+
// a message rides `POST /v1/notify/dm` (the wire contract accepts `agt_*`), the
|
|
149
|
+
// agent's box picks it up and replies over DM (a first-class agent reply
|
|
150
|
+
// channel), and the two-way conversation is read back via `GET /v1/notify/thread`.
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
/** Agent uid shape (`agt_…`). */
|
|
153
|
+
const AGENT_UID_PATTERN = /^agt_[A-Za-z0-9_-]+$/;
|
|
154
|
+
/**
|
|
155
|
+
* Resolve an agent reference to its `agt_` uid. An `agt_…` value is used
|
|
156
|
+
* directly; anything else is treated as a slug or display name and resolved
|
|
157
|
+
* against the company's roster (requires `--company` or a single active
|
|
158
|
+
* membership).
|
|
159
|
+
*/
|
|
160
|
+
export async function resolveAgentUid(token, ref, companySlug) {
|
|
161
|
+
if (AGENT_UID_PATTERN.test(ref.trim()))
|
|
162
|
+
return ref.trim();
|
|
163
|
+
const companyUid = await getCompanyUid(token, companySlug);
|
|
164
|
+
const roster = await listAgents(token, companyUid);
|
|
165
|
+
const needle = ref.trim().toLowerCase();
|
|
166
|
+
const match = roster.find((a) => a.slug?.toLowerCase() === needle ||
|
|
167
|
+
a.name?.toLowerCase() === needle ||
|
|
168
|
+
a.uid === ref.trim());
|
|
169
|
+
if (!match) {
|
|
170
|
+
throw new AgentsHttpError(404, `No agent named "${ref}" in this company. Run \`hq agents list\` to see slugs/uids, ` +
|
|
171
|
+
`or pass the agt_ uid directly.`);
|
|
172
|
+
}
|
|
173
|
+
return match.uid;
|
|
174
|
+
}
|
|
175
|
+
/** Send a DM to an agent. Returns nothing meaningful beyond success. */
|
|
176
|
+
export async function sendAgentDm(token, agentUid, message) {
|
|
177
|
+
await agentsRequest({
|
|
178
|
+
token,
|
|
179
|
+
path: "/v1/notify/dm",
|
|
180
|
+
method: "POST",
|
|
181
|
+
body: { toPersonUid: agentUid, body: message },
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
/** Read the two-way DM conversation with an agent (most recent `limit`). */
|
|
185
|
+
export async function readAgentThread(token, agentUid, limit = 20) {
|
|
186
|
+
const data = await agentsRequest({
|
|
187
|
+
token,
|
|
188
|
+
path: "/v1/notify/thread",
|
|
189
|
+
query: { withPersonUid: agentUid, limit: String(limit) },
|
|
190
|
+
});
|
|
191
|
+
return data.messages ?? [];
|
|
192
|
+
}
|
|
193
|
+
/** Render one thread message as a labeled line for the terminal. */
|
|
194
|
+
export function formatThreadMessage(m, agentLabel) {
|
|
195
|
+
const who = m.direction === "out"
|
|
196
|
+
? chalk.cyan("you")
|
|
197
|
+
: chalk.magenta(m.fromDisplayName || agentLabel);
|
|
198
|
+
const when = chalk.dim(shortTime(m.createdAt));
|
|
199
|
+
return `${who} ${when}\n ${m.body.replace(/\n/g, "\n ")}`;
|
|
200
|
+
}
|
|
201
|
+
function shortTime(iso) {
|
|
202
|
+
// Keep it dependency-free and stable: trim the ISO string to `MM-DD HH:MM`.
|
|
203
|
+
const m = iso.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}:\d{2})/);
|
|
204
|
+
return m ? `${m[2]}-${m[3]} ${m[4]}` : iso;
|
|
205
|
+
}
|
|
206
|
+
function sleep(ms) {
|
|
207
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
208
|
+
}
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
147
210
|
// Command registration
|
|
148
211
|
// ---------------------------------------------------------------------------
|
|
149
212
|
function fail(err) {
|
|
@@ -165,6 +228,88 @@ export function registerAgentsCommand(program) {
|
|
|
165
228
|
// `hq agents --company acme list`.
|
|
166
229
|
const companyOf = (sub) => sub.opts().company ??
|
|
167
230
|
agents.opts().company;
|
|
231
|
+
agents
|
|
232
|
+
.command("message <agent> <message...>")
|
|
233
|
+
.alias("msg")
|
|
234
|
+
.description("Send a message to an agent and wait for its reply")
|
|
235
|
+
.option("--company <slug>", "Company slug (to resolve an agent by name/slug)")
|
|
236
|
+
.option("--no-wait", "Send without waiting for the agent's reply")
|
|
237
|
+
.option("--timeout <seconds>", "How long to wait for a reply", "90")
|
|
238
|
+
.option("--json", "Emit raw JSON")
|
|
239
|
+
.action(async function (agent, messageParts, opts) {
|
|
240
|
+
try {
|
|
241
|
+
const message = messageParts.join(" ").trim();
|
|
242
|
+
if (!message) {
|
|
243
|
+
console.error(chalk.red("No message given. Usage: hq agents message <agent> <text>"));
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
const token = await ensureCognitoToken();
|
|
247
|
+
const agentUid = await resolveAgentUid(token, agent, companyOf(this));
|
|
248
|
+
// Baseline the conversation so we can detect the agent's NEW reply.
|
|
249
|
+
const before = opts.wait === false ? [] : await readAgentThread(token, agentUid, 1);
|
|
250
|
+
const lastBeforeAt = before.length ? before[before.length - 1].createdAt : "";
|
|
251
|
+
await sendAgentDm(token, agentUid, message);
|
|
252
|
+
if (opts.wait === false) {
|
|
253
|
+
console.log(chalk.green(`Sent to ${agent}.`));
|
|
254
|
+
console.log(chalk.dim(`Read replies later: hq agents thread ${agent}`));
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
// Poll the thread until a new inbound (agent) message appears or timeout.
|
|
258
|
+
const timeoutMs = Math.max(5, Number(opts.timeout) || 90) * 1000;
|
|
259
|
+
const deadline = Date.now() + timeoutMs;
|
|
260
|
+
if (!opts.json)
|
|
261
|
+
console.log(chalk.dim(`Sent — waiting for ${agent} to reply…`));
|
|
262
|
+
let reply;
|
|
263
|
+
while (Date.now() < deadline) {
|
|
264
|
+
await sleep(3000);
|
|
265
|
+
const msgs = await readAgentThread(token, agentUid, 10);
|
|
266
|
+
reply = msgs.find((m) => m.direction === "in" && m.createdAt > lastBeforeAt);
|
|
267
|
+
if (reply)
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
if (opts.json) {
|
|
271
|
+
process.stdout.write(JSON.stringify({ agentUid, sent: message, reply: reply ?? null }, null, 2) + "\n");
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (reply) {
|
|
275
|
+
console.log(formatThreadMessage(reply, agent));
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
console.log(chalk.yellow(`No reply within ${Math.round(timeoutMs / 1000)}s. The agent may still respond — ` +
|
|
279
|
+
`check with: hq agents thread ${agent}`));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
fail(err);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
agents
|
|
287
|
+
.command("thread <agent>")
|
|
288
|
+
.alias("history")
|
|
289
|
+
.description("Show your recent conversation with an agent")
|
|
290
|
+
.option("--company <slug>", "Company slug (to resolve an agent by name/slug)")
|
|
291
|
+
.option("--limit <n>", "How many recent messages to show", "20")
|
|
292
|
+
.option("--json", "Emit raw JSON")
|
|
293
|
+
.action(async function (agent, opts) {
|
|
294
|
+
try {
|
|
295
|
+
const token = await ensureCognitoToken();
|
|
296
|
+
const agentUid = await resolveAgentUid(token, agent, companyOf(this));
|
|
297
|
+
const limit = Math.max(1, Number(opts.limit) || 20);
|
|
298
|
+
const msgs = await readAgentThread(token, agentUid, limit);
|
|
299
|
+
if (opts.json) {
|
|
300
|
+
process.stdout.write(JSON.stringify(msgs, null, 2) + "\n");
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (msgs.length === 0) {
|
|
304
|
+
console.log(chalk.gray(`No conversation yet with ${agent}. Start one: hq agents message ${agent} "<text>"`));
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
console.log(msgs.map((m) => formatThreadMessage(m, agent)).join("\n\n"));
|
|
308
|
+
}
|
|
309
|
+
catch (err) {
|
|
310
|
+
fail(err);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
168
313
|
agents
|
|
169
314
|
.command("provision <name>")
|
|
170
315
|
.alias("new")
|
|
@@ -476,4 +621,4 @@ export function registerAgentsCommand(program) {
|
|
|
476
621
|
});
|
|
477
622
|
}
|
|
478
623
|
//# sourceMappingURL=agents.js.map
|
|
479
|
-
//# debugId=
|
|
624
|
+
//# debugId=26009666-ef42-5f7d-88eb-6452eca4b47a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.68.0",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"clean": "rm -rf dist"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@indigoai-us/hq-cloud": "^6.14.
|
|
22
|
+
"@indigoai-us/hq-cloud": "^6.14.4",
|
|
23
23
|
"@indigoai-us/hq-onboarding": "^0.1.0",
|
|
24
24
|
"@sentry/node": "^10.49.0",
|
|
25
25
|
"better-sqlite3": "^12.11.1",
|
|
@@ -379,3 +379,108 @@ describe("hq agents provision (billing gate)", () => {
|
|
|
379
379
|
expect(fetchSpy).not.toHaveBeenCalled();
|
|
380
380
|
});
|
|
381
381
|
});
|
|
382
|
+
|
|
383
|
+
describe("hq agents talk (message / thread)", () => {
|
|
384
|
+
function agentRow(uid: string, slug: string, name: string) {
|
|
385
|
+
return { uid, slug, name, companyUid: "cmp_acme" };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
it("resolves an agent by slug and reads the thread", async () => {
|
|
389
|
+
// 1) roster lookup for slug→uid, 2) thread read
|
|
390
|
+
fetchSpy
|
|
391
|
+
.mockResolvedValueOnce(
|
|
392
|
+
jsonResponse(200, { agents: [agentRow("agt_deacon", "deacon", "Deacon")] }),
|
|
393
|
+
)
|
|
394
|
+
.mockResolvedValueOnce(
|
|
395
|
+
jsonResponse(200, {
|
|
396
|
+
messages: [
|
|
397
|
+
{
|
|
398
|
+
eventId: "e1",
|
|
399
|
+
fromPersonUid: "prs_me",
|
|
400
|
+
body: "hi",
|
|
401
|
+
createdAt: "2026-07-13T10:00",
|
|
402
|
+
direction: "out",
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
eventId: "e2",
|
|
406
|
+
fromPersonUid: "agt_deacon",
|
|
407
|
+
fromDisplayName: "Deacon",
|
|
408
|
+
body: "hello back",
|
|
409
|
+
createdAt: "2026-07-13T10:01",
|
|
410
|
+
direction: "in",
|
|
411
|
+
},
|
|
412
|
+
],
|
|
413
|
+
}),
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
const logs: string[] = [];
|
|
417
|
+
vi.spyOn(console, "log").mockImplementation((...a) => {
|
|
418
|
+
logs.push(a.map(String).join(" "));
|
|
419
|
+
});
|
|
420
|
+
await run(["agents", "--company", "acme", "thread", "deacon"]);
|
|
421
|
+
|
|
422
|
+
// roster call then thread call
|
|
423
|
+
expect(String(fetchSpy.mock.calls[0][0])).toContain("/v1/agents");
|
|
424
|
+
const threadUrl = String(fetchSpy.mock.calls[1][0]);
|
|
425
|
+
expect(threadUrl).toContain("/v1/notify/thread");
|
|
426
|
+
expect(threadUrl).toContain("withPersonUid=agt_deacon");
|
|
427
|
+
const out = logs.join("\n");
|
|
428
|
+
expect(out).toContain("hello back");
|
|
429
|
+
expect(out).toContain("hi");
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("passes an agt_ uid straight through (no roster lookup)", async () => {
|
|
433
|
+
fetchSpy.mockResolvedValueOnce(jsonResponse(200, { messages: [] }));
|
|
434
|
+
await run(["agents", "thread", "agt_xyz"]);
|
|
435
|
+
// Only the thread call — no /v1/agents roster fetch.
|
|
436
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
437
|
+
expect(String(fetchSpy.mock.calls[0][0])).toContain("withPersonUid=agt_xyz");
|
|
438
|
+
expect(mockGetCompanyUid).not.toHaveBeenCalled();
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it("message --no-wait POSTs the DM and does not poll", async () => {
|
|
442
|
+
fetchSpy.mockResolvedValueOnce(jsonResponse(200, { ok: true })); // POST /v1/notify/dm
|
|
443
|
+
await run(["agents", "message", "agt_xyz", "--no-wait", "hello", "there"]);
|
|
444
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
445
|
+
const [url, init] = fetchSpy.mock.calls[0];
|
|
446
|
+
expect(String(url)).toContain("/v1/notify/dm");
|
|
447
|
+
expect(init?.method).toBe("POST");
|
|
448
|
+
expect(JSON.parse(init?.body as string)).toEqual({
|
|
449
|
+
toPersonUid: "agt_xyz",
|
|
450
|
+
body: "hello there",
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("message waits for and prints the agent's reply", async () => {
|
|
455
|
+
fetchSpy
|
|
456
|
+
.mockResolvedValueOnce(jsonResponse(200, { messages: [] })) // baseline read
|
|
457
|
+
.mockResolvedValueOnce(jsonResponse(200, { ok: true })) // POST dm
|
|
458
|
+
.mockResolvedValueOnce(
|
|
459
|
+
jsonResponse(200, {
|
|
460
|
+
messages: [
|
|
461
|
+
{
|
|
462
|
+
eventId: "r1",
|
|
463
|
+
fromPersonUid: "agt_xyz",
|
|
464
|
+
fromDisplayName: "Xyz",
|
|
465
|
+
body: "on it",
|
|
466
|
+
createdAt: "2026-07-13T11:00",
|
|
467
|
+
direction: "in",
|
|
468
|
+
},
|
|
469
|
+
],
|
|
470
|
+
}),
|
|
471
|
+
); // first poll returns the reply
|
|
472
|
+
|
|
473
|
+
const logs: string[] = [];
|
|
474
|
+
vi.spyOn(console, "log").mockImplementation((...a) => {
|
|
475
|
+
logs.push(a.map(String).join(" "));
|
|
476
|
+
});
|
|
477
|
+
await run(["agents", "message", "agt_xyz", "--timeout", "10", "ping"]);
|
|
478
|
+
expect(logs.join("\n")).toContain("on it");
|
|
479
|
+
}, 10000);
|
|
480
|
+
|
|
481
|
+
it("requires a non-empty message", async () => {
|
|
482
|
+
await expect(
|
|
483
|
+
run(["agents", "message", "agt_xyz", " "]),
|
|
484
|
+
).rejects.toThrow("process.exit(1)");
|
|
485
|
+
});
|
|
486
|
+
});
|
package/src/commands/agents.ts
CHANGED
|
@@ -265,6 +265,107 @@ export function appliedHint(applied: boolean): string {
|
|
|
265
265
|
: "saved — takes effect on the agent's next launch";
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
// Talking to agents (DM). Agents are members addressable over HQ's DM system:
|
|
270
|
+
// a message rides `POST /v1/notify/dm` (the wire contract accepts `agt_*`), the
|
|
271
|
+
// agent's box picks it up and replies over DM (a first-class agent reply
|
|
272
|
+
// channel), and the two-way conversation is read back via `GET /v1/notify/thread`.
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
/** Agent uid shape (`agt_…`). */
|
|
276
|
+
const AGENT_UID_PATTERN = /^agt_[A-Za-z0-9_-]+$/;
|
|
277
|
+
|
|
278
|
+
/** One message in a DM thread, as returned by `GET /v1/notify/thread`. */
|
|
279
|
+
export interface DmThreadMessage {
|
|
280
|
+
eventId: string;
|
|
281
|
+
fromPersonUid: string;
|
|
282
|
+
fromDisplayName?: string;
|
|
283
|
+
fromEmail?: string;
|
|
284
|
+
body: string;
|
|
285
|
+
createdAt: string;
|
|
286
|
+
/** "out" = sent by the caller; "in" = received (the agent's reply). */
|
|
287
|
+
direction: "in" | "out";
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Resolve an agent reference to its `agt_` uid. An `agt_…` value is used
|
|
292
|
+
* directly; anything else is treated as a slug or display name and resolved
|
|
293
|
+
* against the company's roster (requires `--company` or a single active
|
|
294
|
+
* membership).
|
|
295
|
+
*/
|
|
296
|
+
export async function resolveAgentUid(
|
|
297
|
+
token: string,
|
|
298
|
+
ref: string,
|
|
299
|
+
companySlug: string | undefined,
|
|
300
|
+
): Promise<string> {
|
|
301
|
+
if (AGENT_UID_PATTERN.test(ref.trim())) return ref.trim();
|
|
302
|
+
const companyUid = await getCompanyUid(token, companySlug);
|
|
303
|
+
const roster = await listAgents(token, companyUid);
|
|
304
|
+
const needle = ref.trim().toLowerCase();
|
|
305
|
+
const match = roster.find(
|
|
306
|
+
(a) =>
|
|
307
|
+
a.slug?.toLowerCase() === needle ||
|
|
308
|
+
a.name?.toLowerCase() === needle ||
|
|
309
|
+
a.uid === ref.trim(),
|
|
310
|
+
);
|
|
311
|
+
if (!match) {
|
|
312
|
+
throw new AgentsHttpError(
|
|
313
|
+
404,
|
|
314
|
+
`No agent named "${ref}" in this company. Run \`hq agents list\` to see slugs/uids, ` +
|
|
315
|
+
`or pass the agt_ uid directly.`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
return match.uid;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Send a DM to an agent. Returns nothing meaningful beyond success. */
|
|
322
|
+
export async function sendAgentDm(
|
|
323
|
+
token: string,
|
|
324
|
+
agentUid: string,
|
|
325
|
+
message: string,
|
|
326
|
+
): Promise<void> {
|
|
327
|
+
await agentsRequest({
|
|
328
|
+
token,
|
|
329
|
+
path: "/v1/notify/dm",
|
|
330
|
+
method: "POST",
|
|
331
|
+
body: { toPersonUid: agentUid, body: message },
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Read the two-way DM conversation with an agent (most recent `limit`). */
|
|
336
|
+
export async function readAgentThread(
|
|
337
|
+
token: string,
|
|
338
|
+
agentUid: string,
|
|
339
|
+
limit = 20,
|
|
340
|
+
): Promise<DmThreadMessage[]> {
|
|
341
|
+
const data = await agentsRequest<{ messages: DmThreadMessage[] }>({
|
|
342
|
+
token,
|
|
343
|
+
path: "/v1/notify/thread",
|
|
344
|
+
query: { withPersonUid: agentUid, limit: String(limit) },
|
|
345
|
+
});
|
|
346
|
+
return data.messages ?? [];
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Render one thread message as a labeled line for the terminal. */
|
|
350
|
+
export function formatThreadMessage(m: DmThreadMessage, agentLabel: string): string {
|
|
351
|
+
const who =
|
|
352
|
+
m.direction === "out"
|
|
353
|
+
? chalk.cyan("you")
|
|
354
|
+
: chalk.magenta(m.fromDisplayName || agentLabel);
|
|
355
|
+
const when = chalk.dim(shortTime(m.createdAt));
|
|
356
|
+
return `${who} ${when}\n ${m.body.replace(/\n/g, "\n ")}`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function shortTime(iso: string): string {
|
|
360
|
+
// Keep it dependency-free and stable: trim the ISO string to `MM-DD HH:MM`.
|
|
361
|
+
const m = iso.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}:\d{2})/);
|
|
362
|
+
return m ? `${m[2]}-${m[3]} ${m[4]}` : iso;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function sleep(ms: number): Promise<void> {
|
|
366
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
367
|
+
}
|
|
368
|
+
|
|
268
369
|
// ---------------------------------------------------------------------------
|
|
269
370
|
// Command registration
|
|
270
371
|
// ---------------------------------------------------------------------------
|
|
@@ -294,6 +395,107 @@ export function registerAgentsCommand(program: Command): void {
|
|
|
294
395
|
(sub.opts().company as string | undefined) ??
|
|
295
396
|
(agents.opts().company as string | undefined);
|
|
296
397
|
|
|
398
|
+
agents
|
|
399
|
+
.command("message <agent> <message...>")
|
|
400
|
+
.alias("msg")
|
|
401
|
+
.description("Send a message to an agent and wait for its reply")
|
|
402
|
+
.option("--company <slug>", "Company slug (to resolve an agent by name/slug)")
|
|
403
|
+
.option("--no-wait", "Send without waiting for the agent's reply")
|
|
404
|
+
.option("--timeout <seconds>", "How long to wait for a reply", "90")
|
|
405
|
+
.option("--json", "Emit raw JSON")
|
|
406
|
+
.action(async function (
|
|
407
|
+
this: Command,
|
|
408
|
+
agent: string,
|
|
409
|
+
messageParts: string[],
|
|
410
|
+
opts: { company?: string; wait?: boolean; timeout?: string; json?: boolean },
|
|
411
|
+
) {
|
|
412
|
+
try {
|
|
413
|
+
const message = messageParts.join(" ").trim();
|
|
414
|
+
if (!message) {
|
|
415
|
+
console.error(chalk.red("No message given. Usage: hq agents message <agent> <text>"));
|
|
416
|
+
process.exit(1);
|
|
417
|
+
}
|
|
418
|
+
const token = await ensureCognitoToken();
|
|
419
|
+
const agentUid = await resolveAgentUid(token, agent, companyOf(this));
|
|
420
|
+
|
|
421
|
+
// Baseline the conversation so we can detect the agent's NEW reply.
|
|
422
|
+
const before = opts.wait === false ? [] : await readAgentThread(token, agentUid, 1);
|
|
423
|
+
const lastBeforeAt = before.length ? before[before.length - 1].createdAt : "";
|
|
424
|
+
|
|
425
|
+
await sendAgentDm(token, agentUid, message);
|
|
426
|
+
|
|
427
|
+
if (opts.wait === false) {
|
|
428
|
+
console.log(chalk.green(`Sent to ${agent}.`));
|
|
429
|
+
console.log(chalk.dim(`Read replies later: hq agents thread ${agent}`));
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Poll the thread until a new inbound (agent) message appears or timeout.
|
|
434
|
+
const timeoutMs = Math.max(5, Number(opts.timeout) || 90) * 1000;
|
|
435
|
+
const deadline = Date.now() + timeoutMs;
|
|
436
|
+
if (!opts.json) console.log(chalk.dim(`Sent — waiting for ${agent} to reply…`));
|
|
437
|
+
let reply: DmThreadMessage | undefined;
|
|
438
|
+
while (Date.now() < deadline) {
|
|
439
|
+
await sleep(3000);
|
|
440
|
+
const msgs = await readAgentThread(token, agentUid, 10);
|
|
441
|
+
reply = msgs.find(
|
|
442
|
+
(m) => m.direction === "in" && m.createdAt > lastBeforeAt,
|
|
443
|
+
);
|
|
444
|
+
if (reply) break;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (opts.json) {
|
|
448
|
+
process.stdout.write(
|
|
449
|
+
JSON.stringify({ agentUid, sent: message, reply: reply ?? null }, null, 2) + "\n",
|
|
450
|
+
);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
if (reply) {
|
|
454
|
+
console.log(formatThreadMessage(reply, agent));
|
|
455
|
+
} else {
|
|
456
|
+
console.log(
|
|
457
|
+
chalk.yellow(
|
|
458
|
+
`No reply within ${Math.round(timeoutMs / 1000)}s. The agent may still respond — ` +
|
|
459
|
+
`check with: hq agents thread ${agent}`,
|
|
460
|
+
),
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
} catch (err) {
|
|
464
|
+
fail(err);
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
agents
|
|
469
|
+
.command("thread <agent>")
|
|
470
|
+
.alias("history")
|
|
471
|
+
.description("Show your recent conversation with an agent")
|
|
472
|
+
.option("--company <slug>", "Company slug (to resolve an agent by name/slug)")
|
|
473
|
+
.option("--limit <n>", "How many recent messages to show", "20")
|
|
474
|
+
.option("--json", "Emit raw JSON")
|
|
475
|
+
.action(async function (
|
|
476
|
+
this: Command,
|
|
477
|
+
agent: string,
|
|
478
|
+
opts: { company?: string; limit?: string; json?: boolean },
|
|
479
|
+
) {
|
|
480
|
+
try {
|
|
481
|
+
const token = await ensureCognitoToken();
|
|
482
|
+
const agentUid = await resolveAgentUid(token, agent, companyOf(this));
|
|
483
|
+
const limit = Math.max(1, Number(opts.limit) || 20);
|
|
484
|
+
const msgs = await readAgentThread(token, agentUid, limit);
|
|
485
|
+
if (opts.json) {
|
|
486
|
+
process.stdout.write(JSON.stringify(msgs, null, 2) + "\n");
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (msgs.length === 0) {
|
|
490
|
+
console.log(chalk.gray(`No conversation yet with ${agent}. Start one: hq agents message ${agent} "<text>"`));
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
console.log(msgs.map((m) => formatThreadMessage(m, agent)).join("\n\n"));
|
|
494
|
+
} catch (err) {
|
|
495
|
+
fail(err);
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
297
499
|
agents
|
|
298
500
|
.command("provision <name>")
|
|
299
501
|
.alias("new")
|