@indigoai-us/hq-cli 5.345.92 → 5.345.94
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 +17 -0
- package/dist/command-catalog.generated.d.ts +252 -0
- package/dist/command-catalog.generated.js +306 -0
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/capacity.d.ts +71 -0
- package/dist/commands/capacity.js +666 -0
- package/dist/commands/integrations-connect.js +64 -5
- package/dist/commands/lanes-accounts.d.ts +1 -0
- package/dist/commands/lanes-accounts.js +6 -0
- package/dist/lib/bot/api.d.ts +21 -0
- package/dist/lib/bot/api.js +26 -0
- package/dist/lib/bot/prompt.js +2 -1
- package/dist/lib/bot/room-mentions.d.ts +26 -0
- package/dist/lib/bot/room-mentions.js +40 -0
- package/dist/lib/bot/run.js +44 -6
- package/dist/lib/capacity/accounts.d.ts +36 -0
- package/dist/lib/capacity/accounts.js +90 -0
- package/dist/lib/capacity/collect.d.ts +33 -0
- package/dist/lib/capacity/collect.js +164 -0
- package/dist/lib/capacity/collectors/claude-oauth.d.ts +145 -0
- package/dist/lib/capacity/collectors/claude-oauth.js +1211 -0
- package/dist/lib/capacity/collectors/claude-statusline.d.ts +72 -0
- package/dist/lib/capacity/collectors/claude-statusline.js +436 -0
- package/dist/lib/capacity/collectors/codex.d.ts +120 -0
- package/dist/lib/capacity/collectors/codex.js +1134 -0
- package/dist/lib/capacity/collectors/grok.d.ts +86 -0
- package/dist/lib/capacity/collectors/grok.js +588 -0
- package/dist/lib/capacity/lane-login.d.ts +29 -0
- package/dist/lib/capacity/lane-login.js +91 -0
- package/dist/lib/capacity/ledger.d.ts +399 -0
- package/dist/lib/capacity/ledger.js +1441 -0
- package/dist/lib/capacity/project.d.ts +12 -0
- package/dist/lib/capacity/project.js +264 -0
- package/dist/lib/capacity/read.d.ts +62 -0
- package/dist/lib/capacity/read.js +264 -0
- package/dist/lib/capacity/refusal.d.ts +30 -0
- package/dist/lib/capacity/refusal.js +183 -0
- package/dist/lib/capacity/scheduler.d.ts +76 -0
- package/dist/lib/capacity/scheduler.js +800 -0
- package/dist/lib/capacity/sign-in.d.ts +103 -0
- package/dist/lib/capacity/sign-in.js +517 -0
- package/dist/lib/capacity/widget.d.ts +22 -0
- package/dist/lib/capacity/widget.js +418 -0
- package/dist/lib/lanes/spawn.js +12 -0
- package/dist/lib/mesh/live/daemon/run.d.ts +5 -0
- package/dist/lib/mesh/live/daemon/run.js +29 -1
- package/package.json +1 -1
|
@@ -305,6 +305,57 @@ async function resolveKey(opts, appLabel) {
|
|
|
305
305
|
}
|
|
306
306
|
throw new IntegrationsCliError(`${appLabel} needs an API key. Pass it with --token-stdin (recommended, keeps it out of shell history) or --token <key>.`, { expected: true });
|
|
307
307
|
}
|
|
308
|
+
/**
|
|
309
|
+
* Remote MCP endpoints the integrations.sh catalog lists as MCP without a URL.
|
|
310
|
+
*
|
|
311
|
+
* Gmail's documented server is `https://gmailmcp.googleapis.com/mcp/v1`
|
|
312
|
+
* (Google Workspace "Configure the Gmail MCP server"). Catalog connect and
|
|
313
|
+
* reconnect otherwise POST domain/query, and hq-pro throws
|
|
314
|
+
* FactoryInstallationUnsupported — printed as a failure that still exited 0
|
|
315
|
+
* when the command returned instead of throwing.
|
|
316
|
+
*/
|
|
317
|
+
const DOCUMENTED_REMOTE_MCP_BY_DOMAIN = {
|
|
318
|
+
"gmail.com": { mcpUrl: "https://gmailmcp.googleapis.com/mcp/v1" },
|
|
319
|
+
};
|
|
320
|
+
function documentedRemoteMcp(domainOrName) {
|
|
321
|
+
const want = domainOrName?.trim().toLowerCase() ?? "";
|
|
322
|
+
if (!want)
|
|
323
|
+
return undefined;
|
|
324
|
+
const direct = DOCUMENTED_REMOTE_MCP_BY_DOMAIN[want];
|
|
325
|
+
if (direct)
|
|
326
|
+
return { mcpUrl: direct.mcpUrl, domain: want };
|
|
327
|
+
for (const [domain, overlay] of Object.entries(DOCUMENTED_REMOTE_MCP_BY_DOMAIN)) {
|
|
328
|
+
if (domainLabel(domain) === want)
|
|
329
|
+
return { mcpUrl: overlay.mcpUrl, domain };
|
|
330
|
+
}
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* When the catalog names an MCP app but has no server URL, use the documented
|
|
335
|
+
* endpoint and drop catalog/query handles so hq-pro takes the direct `mcpUrl`
|
|
336
|
+
* path. Keep `domain` + `provider` so a revoked factory:gmail row is revived
|
|
337
|
+
* instead of creating a second connection keyed on gmailmcp.googleapis.com.
|
|
338
|
+
*/
|
|
339
|
+
function withDocumentedRemoteMcp(target, domainHint) {
|
|
340
|
+
if (target.ref.mcpUrl || target.ref.discoveryReceiptId)
|
|
341
|
+
return target;
|
|
342
|
+
const overlay = documentedRemoteMcp(domainHint) ?? documentedRemoteMcp(target.ref.domain);
|
|
343
|
+
if (!overlay)
|
|
344
|
+
return target;
|
|
345
|
+
return {
|
|
346
|
+
...target,
|
|
347
|
+
ref: {
|
|
348
|
+
mcpUrl: overlay.mcpUrl,
|
|
349
|
+
domain: overlay.domain,
|
|
350
|
+
provider: target.ref.provider ?? domainLabel(overlay.domain),
|
|
351
|
+
displayName: target.ref.displayName ?? target.label,
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
function unsupportedCatalogSurfaceError(label, cause) {
|
|
356
|
+
return new IntegrationsCliError(`${label} cannot be connected because the app catalog has no MCP server URL. ` +
|
|
357
|
+
"Re-run with --mcp-url <endpoint> (find it with `hq integrations inspect`).", { expected: true, status: cause.status, code: cause.code });
|
|
358
|
+
}
|
|
308
359
|
/**
|
|
309
360
|
* Turn the caller's arguments into one unambiguous target. Precedence is
|
|
310
361
|
* most-specific-first so an explicit flag always wins over the positional
|
|
@@ -354,13 +405,13 @@ async function resolveTarget(token, companyUid, app, opts, preserveDomain = fals
|
|
|
354
405
|
// hq-pro. A catalog entry id is useful auth metadata, but replacing the
|
|
355
406
|
// domain with it can create a distinct connection instead of reviving
|
|
356
407
|
// the original acct_ row.
|
|
357
|
-
return {
|
|
408
|
+
return withDocumentedRemoteMcp({
|
|
358
409
|
ref: { domain: app },
|
|
359
410
|
...(match?.authClass ? { authClass: match.authClass } : {}),
|
|
360
411
|
label: redactUrlsInText(match?.name || app),
|
|
361
|
-
};
|
|
412
|
+
}, app);
|
|
362
413
|
}
|
|
363
|
-
return catalogEntryToTarget(match, { ref: { domain: app }, label: redactUrlsInText(app) });
|
|
414
|
+
return withDocumentedRemoteMcp(catalogEntryToTarget(match, { ref: { domain: app }, label: redactUrlsInText(app) }), match?.domain ?? app);
|
|
364
415
|
}
|
|
365
416
|
// A bare name (`notion`, `atlassian`) is how the catalog reads to a person —
|
|
366
417
|
// the rows are titled `notion.com`, `atlassian.com`. Resolve it to that row
|
|
@@ -370,9 +421,9 @@ async function resolveTarget(token, companyUid, app, opts, preserveDomain = fals
|
|
|
370
421
|
// unmatched → fall back to the server-side query resolver.
|
|
371
422
|
const named = await findCatalogEntryByName(token, companyUid, app);
|
|
372
423
|
if (named) {
|
|
373
|
-
return catalogEntryToTarget(named, { ref: { query: app }, label: redactUrlsInText(app) });
|
|
424
|
+
return withDocumentedRemoteMcp(catalogEntryToTarget(named, { ref: { query: app }, label: redactUrlsInText(app) }), named.domain);
|
|
374
425
|
}
|
|
375
|
-
return { ref: { query: app }, label: redactUrlsInText(app) };
|
|
426
|
+
return withDocumentedRemoteMcp({ ref: { query: app }, label: redactUrlsInText(app) }, app);
|
|
376
427
|
}
|
|
377
428
|
/**
|
|
378
429
|
* Turn a matched catalog entry into a connect target, preferring its opaque
|
|
@@ -1065,6 +1116,10 @@ async function connectApp(token, companyUid, app, opts, expectedRevivedConnectio
|
|
|
1065
1116
|
}
|
|
1066
1117
|
return;
|
|
1067
1118
|
}
|
|
1119
|
+
if (err instanceof IntegrationsCliError &&
|
|
1120
|
+
err.code === "INTEGRATION_FACTORY_UNSUPPORTED_SURFACE") {
|
|
1121
|
+
throw unsupportedCatalogSurfaceError(target.label, err);
|
|
1122
|
+
}
|
|
1068
1123
|
throw err;
|
|
1069
1124
|
}
|
|
1070
1125
|
}
|
|
@@ -1407,6 +1462,10 @@ export function registerConnectCommands(integrations) {
|
|
|
1407
1462
|
reportInstall(result, opts);
|
|
1408
1463
|
return;
|
|
1409
1464
|
}
|
|
1465
|
+
if (err instanceof IntegrationsCliError &&
|
|
1466
|
+
err.code === "INTEGRATION_FACTORY_UNSUPPORTED_SURFACE") {
|
|
1467
|
+
throw unsupportedCatalogSurfaceError(target.label, err);
|
|
1468
|
+
}
|
|
1410
1469
|
throw err;
|
|
1411
1470
|
}
|
|
1412
1471
|
});
|
|
@@ -12,6 +12,7 @@ export declare function createLaneAccountsHandlers(deps: LaneAccountsHandlerDeps
|
|
|
12
12
|
home?: string;
|
|
13
13
|
deviceAuth?: boolean;
|
|
14
14
|
company?: string;
|
|
15
|
+
capacityOnly?: boolean;
|
|
15
16
|
}) => Promise<void>;
|
|
16
17
|
runAccountsSetHome: (ctx: Ctx, providerRaw: string, accountIdRaw: string, homeRaw: string, opts: {
|
|
17
18
|
host?: string;
|
|
@@ -8,6 +8,7 @@ import { AccountResolutionError, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, PROVIDER
|
|
|
8
8
|
import { assertUniqueAccountHomes, providerCompactAfterMs } from "../lib/lanes/config.js";
|
|
9
9
|
import { resolveLaneEffort } from "../lib/lanes/effort.js";
|
|
10
10
|
import { LanesError } from "../lib/lanes/errors.js";
|
|
11
|
+
import { CAPACITY_ONLY_REASON } from "../lib/capacity/accounts.js";
|
|
11
12
|
import { accountQuotaView, clearAccountQuota, loadQuotaState } from "../lib/lanes/quota.js";
|
|
12
13
|
import { resolveLaneModel } from "../lib/lanes/spawn.js";
|
|
13
14
|
import { leadLaneIds } from "../lib/lanes/admission.js";
|
|
@@ -362,6 +363,11 @@ export function createLaneAccountsHandlers(deps) {
|
|
|
362
363
|
}
|
|
363
364
|
}
|
|
364
365
|
delete nextAccount.disabled_reason;
|
|
366
|
+
if (opts.capacityOnly && registeredAccount?.enabled !== true) {
|
|
367
|
+
const prior = registeredAccount?.disabled_reason;
|
|
368
|
+
nextAccount.enabled = false;
|
|
369
|
+
nextAccount.disabled_reason = prior && prior !== CAPACITY_ONLY_REASON ? prior : CAPACITY_ONLY_REASON;
|
|
370
|
+
}
|
|
365
371
|
assertUniqueAccountHomes(provider, {
|
|
366
372
|
...config.providers[provider].accounts,
|
|
367
373
|
[id]: nextAccount,
|
package/dist/lib/bot/api.d.ts
CHANGED
|
@@ -249,12 +249,33 @@ export declare class BotApi {
|
|
|
249
249
|
* (someone else does). Anything else throws.
|
|
250
250
|
*/
|
|
251
251
|
claimChannelUnit(channelId: string, unitId: string): Promise<boolean>;
|
|
252
|
+
/**
|
|
253
|
+
* GET /v1/notify/channels/{id}/members — roster used to resolve `@Name`
|
|
254
|
+
* into structured mentions on a room post. Agents may read members of a
|
|
255
|
+
* channel they belong to (same gate as reading messages).
|
|
256
|
+
*/
|
|
257
|
+
fetchChannelMembers(channelId: string): Promise<Array<{
|
|
258
|
+
personUid: string;
|
|
259
|
+
displayName?: string;
|
|
260
|
+
email?: string;
|
|
261
|
+
slug?: string;
|
|
262
|
+
handle?: string;
|
|
263
|
+
}>>;
|
|
252
264
|
/** POST /v1/notify/channels/{id}/messages — post in a room as the bot. */
|
|
253
265
|
sendChannelMessage(input: {
|
|
254
266
|
channelId: string;
|
|
255
267
|
body: string;
|
|
256
268
|
rootEventId?: string;
|
|
257
269
|
audience?: "human" | "agent" | "both";
|
|
270
|
+
/**
|
|
271
|
+
* Structured HQ mentions. The server never parses `@Name` from the body;
|
|
272
|
+
* without this array the addressed bot is not written to the agent inbox.
|
|
273
|
+
*/
|
|
274
|
+
mentions?: ReadonlyArray<{
|
|
275
|
+
participantUid: string;
|
|
276
|
+
participantType: "agent" | "human";
|
|
277
|
+
displayName: string;
|
|
278
|
+
}>;
|
|
258
279
|
}): Promise<{
|
|
259
280
|
eventId?: string;
|
|
260
281
|
createdAt?: string;
|
package/dist/lib/bot/api.js
CHANGED
|
@@ -192,6 +192,30 @@ export class BotApi {
|
|
|
192
192
|
throw err;
|
|
193
193
|
}
|
|
194
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* GET /v1/notify/channels/{id}/members — roster used to resolve `@Name`
|
|
197
|
+
* into structured mentions on a room post. Agents may read members of a
|
|
198
|
+
* channel they belong to (same gate as reading messages).
|
|
199
|
+
*/
|
|
200
|
+
async fetchChannelMembers(channelId) {
|
|
201
|
+
const { body } = await this.call(`/v1/notify/channels/${encodeURIComponent(channelId)}/members`);
|
|
202
|
+
const rows = Array.isArray(body.members) ? body.members : [];
|
|
203
|
+
const out = [];
|
|
204
|
+
for (const row of rows) {
|
|
205
|
+
if (typeof row.personUid !== "string" || !row.personUid.trim())
|
|
206
|
+
continue;
|
|
207
|
+
out.push({
|
|
208
|
+
personUid: row.personUid.trim(),
|
|
209
|
+
...(typeof row.displayName === "string" && row.displayName.trim()
|
|
210
|
+
? { displayName: row.displayName.trim() }
|
|
211
|
+
: {}),
|
|
212
|
+
...(typeof row.email === "string" && row.email.trim() ? { email: row.email.trim() } : {}),
|
|
213
|
+
...(typeof row.slug === "string" && row.slug.trim() ? { slug: row.slug.trim() } : {}),
|
|
214
|
+
...(typeof row.handle === "string" && row.handle.trim() ? { handle: row.handle.trim() } : {}),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
195
219
|
/** POST /v1/notify/channels/{id}/messages — post in a room as the bot. */
|
|
196
220
|
async sendChannelMessage(input) {
|
|
197
221
|
const payload = { body: input.body };
|
|
@@ -199,6 +223,8 @@ export class BotApi {
|
|
|
199
223
|
payload.rootEventId = input.rootEventId;
|
|
200
224
|
if (input.audience)
|
|
201
225
|
payload.audience = input.audience;
|
|
226
|
+
if (input.mentions && input.mentions.length > 0)
|
|
227
|
+
payload.mentions = input.mentions;
|
|
202
228
|
const { body } = await this.call(`/v1/notify/channels/${encodeURIComponent(input.channelId)}/messages`, { method: "POST", body: payload });
|
|
203
229
|
return body;
|
|
204
230
|
}
|
package/dist/lib/bot/prompt.js
CHANGED
|
@@ -382,7 +382,8 @@ export function buildSystemPrompt(input) {
|
|
|
382
382
|
`- \`agent\`: status pings, tool handoffs, and in-progress notes that only another agent needs to read.\n` +
|
|
383
383
|
`- \`human\`: anything a person must read — answers, results, questions, summaries.\n` +
|
|
384
384
|
`- \`both\`: when the content is relevant to both a person and a downstream agent.\n` +
|
|
385
|
-
`When unsure, use \`human\`. Progress messages sent before the final answer should use \`agent\`; the final answer should use \`human
|
|
385
|
+
`When unsure, use \`human\`. Progress messages sent before the final answer should use \`agent\`; the final answer should use \`human\`.\n\n` +
|
|
386
|
+
`In a group room, write \`@Name\` to address another person or bot on the roster. That becomes a real mention and puts the post in the addressed bot's inbox. A room post that does not mention another bot does not wake that bot.`);
|
|
386
387
|
return `${parts.join("\n\n")}\n`;
|
|
387
388
|
}
|
|
388
389
|
/**
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort structured mentions for a local bot's room post.
|
|
3
|
+
*
|
|
4
|
+
* HQ Rooms mentions are structured-only: the server never parses `@Name` out of
|
|
5
|
+
* the body. `hq dm` already resolves names against the roster and sends the
|
|
6
|
+
* `mentions` array. Bot room posts did not, so an addressed bot never got an
|
|
7
|
+
* inbox item (feedback #2774). Unknown or ambiguous `@` words are skipped so a
|
|
8
|
+
* model typo cannot swallow the visible reply; matched roster members become
|
|
9
|
+
* real mentions. The sender is never included.
|
|
10
|
+
*/
|
|
11
|
+
import { type ChannelMember } from "../../commands/dm.js";
|
|
12
|
+
export type StructuredChannelMention = {
|
|
13
|
+
participantUid: string;
|
|
14
|
+
participantType: "agent" | "human";
|
|
15
|
+
displayName: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function roomPostHasMentionTokens(body: string): boolean;
|
|
18
|
+
export declare function resolveRoomMentionsForBot(args: {
|
|
19
|
+
body: string;
|
|
20
|
+
members: readonly ChannelMember[];
|
|
21
|
+
selfUid: string;
|
|
22
|
+
}): {
|
|
23
|
+
body: string;
|
|
24
|
+
mentions: StructuredChannelMention[];
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=room-mentions.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort structured mentions for a local bot's room post.
|
|
3
|
+
*
|
|
4
|
+
* HQ Rooms mentions are structured-only: the server never parses `@Name` out of
|
|
5
|
+
* the body. `hq dm` already resolves names against the roster and sends the
|
|
6
|
+
* `mentions` array. Bot room posts did not, so an addressed bot never got an
|
|
7
|
+
* inbox item (feedback #2774). Unknown or ambiguous `@` words are skipped so a
|
|
8
|
+
* model typo cannot swallow the visible reply; matched roster members become
|
|
9
|
+
* real mentions. The sender is never included.
|
|
10
|
+
*/
|
|
11
|
+
import { parseMentionTokens, resolveMentions, stripMentionQuotes, } from "../../commands/dm.js";
|
|
12
|
+
export function roomPostHasMentionTokens(body) {
|
|
13
|
+
return parseMentionTokens(body).length > 0;
|
|
14
|
+
}
|
|
15
|
+
export function resolveRoomMentionsForBot(args) {
|
|
16
|
+
const tokens = parseMentionTokens(args.body);
|
|
17
|
+
if (tokens.length === 0)
|
|
18
|
+
return { body: args.body, mentions: [] };
|
|
19
|
+
const members = [...args.members];
|
|
20
|
+
const mentions = [];
|
|
21
|
+
const claimed = new Set();
|
|
22
|
+
for (const token of tokens) {
|
|
23
|
+
const resolved = resolveMentions([token], members);
|
|
24
|
+
if (!resolved.ok)
|
|
25
|
+
continue;
|
|
26
|
+
for (const mention of resolved.mentions) {
|
|
27
|
+
if (mention.participantUid === args.selfUid)
|
|
28
|
+
continue;
|
|
29
|
+
if (claimed.has(mention.participantUid))
|
|
30
|
+
continue;
|
|
31
|
+
claimed.add(mention.participantUid);
|
|
32
|
+
mentions.push(mention);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
body: mentions.length > 0 ? stripMentionQuotes(args.body) : args.body,
|
|
37
|
+
mentions,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=room-mentions.js.map
|
package/dist/lib/bot/run.js
CHANGED
|
@@ -73,6 +73,7 @@ import { appendDmHistory, dmHistoryHasModelThread, markDmHistoryModelTurn, readD
|
|
|
73
73
|
import { attachmentTurnBlock, downloadTurnAttachments } from "./attachments.js";
|
|
74
74
|
import { inboxEntryAttachments } from "../agent-kit/inbox-state.js";
|
|
75
75
|
import { isAgentSender, isDmItem, isMetaNonReply, resolveReplyTarget } from "./room-policy.js";
|
|
76
|
+
import { resolveRoomMentionsForBot, roomPostHasMentionTokens, } from "./room-mentions.js";
|
|
76
77
|
import { buildJobTurnPrompt, isCatchUpRun, isJobItem, jobReportHeader, jobRunOutput, machineTimeZone, planJobRuns, stripJobHeader, SUPERSEDED_DISPOSITION } from "./job-run.js";
|
|
77
78
|
import { botJobQuietOptionEnabled } from "./job-quiet-flag.js";
|
|
78
79
|
import { clearPendingJobCompletion, isCompletionDue, isCompletionExpired, isCompletionForOtherAgent, readPendingJobCompletions, recordPendingJobCompletion } from "./job-completions.js";
|
|
@@ -755,6 +756,15 @@ export async function runBot(deps) {
|
|
|
755
756
|
}
|
|
756
757
|
return `${header}\n${recent}${from}: ${text}`;
|
|
757
758
|
};
|
|
759
|
+
const channelMemberCache = new Map();
|
|
760
|
+
const loadChannelMembers = async (channelId) => {
|
|
761
|
+
const cached = channelMemberCache.get(channelId);
|
|
762
|
+
if (cached)
|
|
763
|
+
return cached;
|
|
764
|
+
const members = await api.fetchChannelMembers(channelId);
|
|
765
|
+
channelMemberCache.set(channelId, members);
|
|
766
|
+
return members;
|
|
767
|
+
};
|
|
758
768
|
const rememberDm = (scope, message) => {
|
|
759
769
|
if (!isDmSessionScope(scope))
|
|
760
770
|
return;
|
|
@@ -1052,13 +1062,16 @@ export async function runBot(deps) {
|
|
|
1052
1062
|
}
|
|
1053
1063
|
await sendToTarget(target, notice.text, rootEventId, "human");
|
|
1054
1064
|
};
|
|
1055
|
-
const postOne = (target, body, rootEventId, audience) => {
|
|
1065
|
+
const postOne = async (target, body, rootEventId, audience) => {
|
|
1056
1066
|
const root = rootEventId ? { rootEventId } : {};
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1067
|
+
if (target.kind === "dm") {
|
|
1068
|
+
const posted = await api.sendDm({
|
|
1069
|
+
toPersonUid: target.peerUid,
|
|
1070
|
+
body,
|
|
1071
|
+
...root,
|
|
1072
|
+
...(audience ? { audience } : {}),
|
|
1073
|
+
});
|
|
1074
|
+
if (audience !== "agent") {
|
|
1062
1075
|
const eventId = posted && typeof posted === "object" && "eventId" in posted && typeof posted.eventId === "string"
|
|
1063
1076
|
? posted.eventId
|
|
1064
1077
|
: undefined;
|
|
@@ -1072,6 +1085,31 @@ export async function runBot(deps) {
|
|
|
1072
1085
|
});
|
|
1073
1086
|
}
|
|
1074
1087
|
return posted;
|
|
1088
|
+
}
|
|
1089
|
+
let bodyToSend = body;
|
|
1090
|
+
let mentions;
|
|
1091
|
+
if (roomPostHasMentionTokens(body)) {
|
|
1092
|
+
try {
|
|
1093
|
+
const members = await loadChannelMembers(target.channelId);
|
|
1094
|
+
const resolved = resolveRoomMentionsForBot({
|
|
1095
|
+
body,
|
|
1096
|
+
members,
|
|
1097
|
+
selfUid: config.agentUid,
|
|
1098
|
+
});
|
|
1099
|
+
bodyToSend = resolved.body;
|
|
1100
|
+
if (resolved.mentions.length > 0)
|
|
1101
|
+
mentions = resolved.mentions;
|
|
1102
|
+
}
|
|
1103
|
+
catch (err) {
|
|
1104
|
+
log("warn", `room mentions for ${target.channelId} unresolved: ${err instanceof Error ? err.message : String(err)}`);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
return api.sendChannelMessage({
|
|
1108
|
+
channelId: target.channelId,
|
|
1109
|
+
body: bodyToSend,
|
|
1110
|
+
...root,
|
|
1111
|
+
...(audience ? { audience } : {}),
|
|
1112
|
+
...(mentions ? { mentions } : {}),
|
|
1075
1113
|
});
|
|
1076
1114
|
};
|
|
1077
1115
|
/**
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capacity account list (US-011).
|
|
3
|
+
*
|
|
4
|
+
* The lane account registry plus each provider's default home. A missing
|
|
5
|
+
* config file is an empty registry: this module does not call
|
|
6
|
+
* documentedDefaultConfig, which would invent an enabled account "a".
|
|
7
|
+
*/
|
|
8
|
+
import { type LaneProvider, type LanesConfig } from "../lanes/types.js";
|
|
9
|
+
export type CapacityAccountSource = "default" | "registry" | "host";
|
|
10
|
+
export interface CapacityAccount {
|
|
11
|
+
provider: LaneProvider;
|
|
12
|
+
label: string;
|
|
13
|
+
home: string;
|
|
14
|
+
source: CapacityAccountSource;
|
|
15
|
+
/** False for a capacity-only account: read for usage, never used by lanes. */
|
|
16
|
+
lanes?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* `disabled_reason` that marks a registry account as capacity-only. Signing in
|
|
20
|
+
* for usage visibility must not opt an account into lane work, because lanes
|
|
21
|
+
* spread work across every enabled account and the accounts can belong to
|
|
22
|
+
* different companies. `hq lanes accounts enable` opts one in.
|
|
23
|
+
*/
|
|
24
|
+
export declare const CAPACITY_ONLY_REASON = "capacity-only: read for usage, not used by lanes";
|
|
25
|
+
export interface ListCapacityAccountsOptions {
|
|
26
|
+
home?: string;
|
|
27
|
+
hqRoot?: string;
|
|
28
|
+
hostname?: string;
|
|
29
|
+
/** Injected config. `null` means the registry is empty. Omit to read the file. */
|
|
30
|
+
config?: LanesConfig | null;
|
|
31
|
+
env?: NodeJS.ProcessEnv;
|
|
32
|
+
}
|
|
33
|
+
export declare function defaultProviderHome(provider: LaneProvider, home?: string): string;
|
|
34
|
+
export declare function listCapacityAccounts(options?: ListCapacityAccountsOptions): CapacityAccount[];
|
|
35
|
+
export declare function registryAccountIds(hqRoot: string, provider: string): string[];
|
|
36
|
+
//# sourceMappingURL=accounts.d.ts.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capacity account list (US-011).
|
|
3
|
+
*
|
|
4
|
+
* The lane account registry plus each provider's default home. A missing
|
|
5
|
+
* config file is an empty registry: this module does not call
|
|
6
|
+
* documentedDefaultConfig, which would invent an enabled account "a".
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as os from "node:os";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import { effectiveAccountHome } from "../lanes/account-home.js";
|
|
12
|
+
import { lanesConfigPath, loadLanesConfig } from "../lanes/config.js";
|
|
13
|
+
import { resolveLanesHqRoot } from "../lanes/hq-root.js";
|
|
14
|
+
import { LANE_PROVIDERS } from "../lanes/types.js";
|
|
15
|
+
/**
|
|
16
|
+
* `disabled_reason` that marks a registry account as capacity-only. Signing in
|
|
17
|
+
* for usage visibility must not opt an account into lane work, because lanes
|
|
18
|
+
* spread work across every enabled account and the accounts can belong to
|
|
19
|
+
* different companies. `hq lanes accounts enable` opts one in.
|
|
20
|
+
*/
|
|
21
|
+
export const CAPACITY_ONLY_REASON = "capacity-only: read for usage, not used by lanes";
|
|
22
|
+
const DEFAULT_DIR = {
|
|
23
|
+
claude: ".claude",
|
|
24
|
+
codex: ".codex",
|
|
25
|
+
grok: ".grok",
|
|
26
|
+
};
|
|
27
|
+
export function defaultProviderHome(provider, home = os.homedir()) {
|
|
28
|
+
return path.join(home, DEFAULT_DIR[provider]);
|
|
29
|
+
}
|
|
30
|
+
export function listCapacityAccounts(options = {}) {
|
|
31
|
+
const home = options.home ?? os.homedir();
|
|
32
|
+
const hostname = options.hostname ?? os.hostname();
|
|
33
|
+
const config = resolveConfig(options);
|
|
34
|
+
const accounts = [];
|
|
35
|
+
for (const provider of LANE_PROVIDERS) {
|
|
36
|
+
accounts.push({
|
|
37
|
+
provider,
|
|
38
|
+
label: "default",
|
|
39
|
+
home: defaultProviderHome(provider, home),
|
|
40
|
+
source: "default",
|
|
41
|
+
});
|
|
42
|
+
const configured = config?.providers[provider]?.accounts ?? {};
|
|
43
|
+
for (const [label, account] of Object.entries(configured)) {
|
|
44
|
+
if (!account)
|
|
45
|
+
continue;
|
|
46
|
+
const capacityOnly = !account.enabled && account.disabled_reason === CAPACITY_ONLY_REASON;
|
|
47
|
+
if (!account.enabled && !capacityOnly)
|
|
48
|
+
continue;
|
|
49
|
+
const effective = effectiveAccountHome(account, hostname);
|
|
50
|
+
accounts.push({
|
|
51
|
+
provider,
|
|
52
|
+
label,
|
|
53
|
+
home: effective.home,
|
|
54
|
+
source: effective.source === "host" ? "host" : "registry",
|
|
55
|
+
lanes: account.enabled,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return accounts;
|
|
60
|
+
}
|
|
61
|
+
export function registryAccountIds(hqRoot, provider) {
|
|
62
|
+
if (!LANE_PROVIDERS.includes(provider))
|
|
63
|
+
return [];
|
|
64
|
+
const filePath = lanesConfigPath(hqRoot);
|
|
65
|
+
if (!fs.existsSync(filePath))
|
|
66
|
+
return [];
|
|
67
|
+
const config = loadLanesConfig(hqRoot);
|
|
68
|
+
return Object.keys(config.providers[provider].accounts);
|
|
69
|
+
}
|
|
70
|
+
function resolveConfig(options) {
|
|
71
|
+
if (options.config === null)
|
|
72
|
+
return null;
|
|
73
|
+
if (options.config)
|
|
74
|
+
return options.config;
|
|
75
|
+
const explicit = options.hqRoot?.trim() || options.env?.HQ_ROOT?.trim() || "";
|
|
76
|
+
let hqRoot = explicit;
|
|
77
|
+
if (!hqRoot) {
|
|
78
|
+
try {
|
|
79
|
+
hqRoot = resolveLanesHqRoot({ env: options.env });
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const filePath = lanesConfigPath(hqRoot);
|
|
86
|
+
if (!fs.existsSync(filePath))
|
|
87
|
+
return null;
|
|
88
|
+
return loadLanesConfig(hqRoot);
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=accounts.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-shot capacity collection (US-011).
|
|
3
|
+
*
|
|
4
|
+
* The daemon is the resident collector. This runs the same readers once
|
|
5
|
+
* into whatever ledger root the caller passes, so a check never writes
|
|
6
|
+
* ~/.hq/capacity unless that root is requested.
|
|
7
|
+
*/
|
|
8
|
+
import { type CapacityAccount } from "./accounts.js";
|
|
9
|
+
import { type CodexAppServerSpawn } from "./collectors/codex.js";
|
|
10
|
+
import { type CollectClaudeOAuthOptions } from "./collectors/claude-oauth.js";
|
|
11
|
+
import { type CollectGrokOptions } from "./collectors/grok.js";
|
|
12
|
+
import { type NowView } from "./ledger.js";
|
|
13
|
+
export interface CollectCapacityOnceOptions {
|
|
14
|
+
root?: string;
|
|
15
|
+
home?: string;
|
|
16
|
+
hqRoot?: string;
|
|
17
|
+
hostname?: string;
|
|
18
|
+
env?: NodeJS.ProcessEnv;
|
|
19
|
+
now?: () => Date;
|
|
20
|
+
/** Replaces registry discovery. Tests pass fixture homes here. */
|
|
21
|
+
accounts?: readonly CapacityAccount[];
|
|
22
|
+
claude?: Pick<CollectClaudeOAuthOptions, "platform" | "securityExec" | "fetchImpl" | "cliVersion" | "readVersion" | "endpoint">;
|
|
23
|
+
grok?: Pick<CollectGrokOptions, "fetchImpl" | "cliVersion" | "readVersion" | "endpoint">;
|
|
24
|
+
codex?: {
|
|
25
|
+
spawn?: CodexAppServerSpawn;
|
|
26
|
+
cliVersion?: string;
|
|
27
|
+
skipAppServer?: boolean;
|
|
28
|
+
};
|
|
29
|
+
/** Login passes false so a partial account list cannot label the whole spool. */
|
|
30
|
+
includeSpool?: boolean;
|
|
31
|
+
}
|
|
32
|
+
export declare function collectCapacityOnce(options?: CollectCapacityOnceOptions): Promise<NowView>;
|
|
33
|
+
//# sourceMappingURL=collect.d.ts.map
|