@indigoai-us/hq-cli 5.345.92 → 5.345.93

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 (42) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/command-catalog.generated.d.ts +252 -0
  3. package/dist/command-catalog.generated.js +306 -0
  4. package/dist/command-registration-plan.d.ts +6 -0
  5. package/dist/command-registration-plan.js +1 -0
  6. package/dist/commands/capacity.d.ts +71 -0
  7. package/dist/commands/capacity.js +666 -0
  8. package/dist/commands/integrations-connect.js +64 -5
  9. package/dist/commands/lanes-accounts.d.ts +1 -0
  10. package/dist/commands/lanes-accounts.js +6 -0
  11. package/dist/lib/capacity/accounts.d.ts +36 -0
  12. package/dist/lib/capacity/accounts.js +90 -0
  13. package/dist/lib/capacity/collect.d.ts +33 -0
  14. package/dist/lib/capacity/collect.js +164 -0
  15. package/dist/lib/capacity/collectors/claude-oauth.d.ts +145 -0
  16. package/dist/lib/capacity/collectors/claude-oauth.js +1211 -0
  17. package/dist/lib/capacity/collectors/claude-statusline.d.ts +72 -0
  18. package/dist/lib/capacity/collectors/claude-statusline.js +436 -0
  19. package/dist/lib/capacity/collectors/codex.d.ts +120 -0
  20. package/dist/lib/capacity/collectors/codex.js +1134 -0
  21. package/dist/lib/capacity/collectors/grok.d.ts +86 -0
  22. package/dist/lib/capacity/collectors/grok.js +588 -0
  23. package/dist/lib/capacity/lane-login.d.ts +29 -0
  24. package/dist/lib/capacity/lane-login.js +91 -0
  25. package/dist/lib/capacity/ledger.d.ts +399 -0
  26. package/dist/lib/capacity/ledger.js +1441 -0
  27. package/dist/lib/capacity/project.d.ts +12 -0
  28. package/dist/lib/capacity/project.js +264 -0
  29. package/dist/lib/capacity/read.d.ts +62 -0
  30. package/dist/lib/capacity/read.js +264 -0
  31. package/dist/lib/capacity/refusal.d.ts +30 -0
  32. package/dist/lib/capacity/refusal.js +183 -0
  33. package/dist/lib/capacity/scheduler.d.ts +76 -0
  34. package/dist/lib/capacity/scheduler.js +800 -0
  35. package/dist/lib/capacity/sign-in.d.ts +103 -0
  36. package/dist/lib/capacity/sign-in.js +517 -0
  37. package/dist/lib/capacity/widget.d.ts +22 -0
  38. package/dist/lib/capacity/widget.js +418 -0
  39. package/dist/lib/lanes/spawn.js +12 -0
  40. package/dist/lib/mesh/live/daemon/run.d.ts +5 -0
  41. package/dist/lib/mesh/live/daemon/run.js +29 -1
  42. package/package.json +1 -1
@@ -0,0 +1,418 @@
1
+ import { isExpiredReason, redactProviderTokens, } from "./ledger.js";
2
+ const STYLE = [
3
+ ".cap{display:flex;flex-direction:column;gap:12px;padding:4px 0}",
4
+ ".prov{border:1px solid var(--border);border-radius:12px;padding:14px 16px}",
5
+ ".ph{display:flex;align-items:center;gap:8px;margin-bottom:10px;font-size:15px;font-weight:500;color:var(--text-primary)}",
6
+ ".acct{padding:10px 0;border-top:1px solid var(--border)}",
7
+ ".acct:first-of-type{border-top:none;padding-top:0}",
8
+ ".ah{display:flex;justify-content:space-between;align-items:baseline;gap:8px;margin-bottom:8px}",
9
+ ".an{font-size:14px;font-weight:500;color:var(--text-primary)}",
10
+ ".as{font-size:12px;color:var(--text-secondary)}",
11
+ ".row{display:grid;grid-template-columns:110px 1fr 44px 150px;align-items:center;gap:10px;font-size:13px;color:var(--text-secondary);margin:5px 0}",
12
+ ".bar{height:8px;border-radius:4px;background:var(--surface-1);border:1px solid var(--border);overflow:hidden}",
13
+ ".fill{height:100%}",
14
+ ".pct{text-align:right;color:var(--text-primary);font-variant-numeric:tabular-nums}",
15
+ ".pill{font-size:11px;padding:2px 8px;border-radius:999px}",
16
+ ].join("\n");
17
+ const PROVIDER_ORDER = ["claude", "codex", "grok"];
18
+ const ICONS = {
19
+ claude: "ti-sparkles",
20
+ codex: "ti-code",
21
+ grok: "ti-bolt",
22
+ };
23
+ const PILL_STYLE = {
24
+ danger: "background:var(--bg-danger);color:var(--text-danger)",
25
+ warning: "background:var(--bg-warning);color:var(--text-warning)",
26
+ accent: "background:var(--bg-accent);color:var(--text-accent)",
27
+ success: "background:var(--bg-success);color:var(--text-success)",
28
+ muted: "background:var(--surface-1);color:var(--text-secondary);border:1px solid var(--border)",
29
+ };
30
+ const TOKEN_LEAK = /sk-ant-|Bearer\s+\S+|eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/i;
31
+ export function capacityWidgetTimeZone(env = process.env) {
32
+ const fromEnv = env.TZ?.trim();
33
+ if (fromEnv && isTimeZone(fromEnv))
34
+ return fromEnv;
35
+ const resolved = Intl.DateTimeFormat().resolvedOptions().timeZone;
36
+ if (resolved && isTimeZone(resolved))
37
+ return resolved;
38
+ return "UTC";
39
+ }
40
+ export function renderCapacityWidget(input) {
41
+ if (input.ledgerMissing) {
42
+ return renderEmpty("No capacity accounts to show.", "No accounts in the ledger. Turn collection on with hq capacity enable.");
43
+ }
44
+ if (input.collectorEnabled === false) {
45
+ return renderEmpty("Capacity collection is off.", "Collection is off. Turn it on with hq capacity enable.");
46
+ }
47
+ const view = input.view;
48
+ const providers = orderedProviders(view);
49
+ if (!view || providers.length === 0) {
50
+ return renderEmpty("No capacity accounts to show.", "No accounts in the ledger. Turn collection on with hq capacity enable.");
51
+ }
52
+ const accounts = providers.flatMap((provider) => provider.accounts ?? []);
53
+ const readAt = newestObserved(accounts, view.generatedAt, input.nowMs);
54
+ const readTime = clockTime(readAt, input.timeZone);
55
+ const names = providers.map((provider) => providerTitle(provider.provider));
56
+ const summary = `Usage and reset times for every signed-in ${joinNames(names)} account, read from the HQ capacity ledger at ${readTime}.`;
57
+ const cards = providers.map((provider) => renderProvider(provider, input)).join("\n\n");
58
+ const body = `${cards}\n\n${renderFooter(readTime, input.stale === true)}\n`;
59
+ return frame(summary, body);
60
+ }
61
+ function renderEmpty(summary, message) {
62
+ return frame(summary, `<p class="as">${esc(message)}</p>`);
63
+ }
64
+ function frame(summary, body) {
65
+ return [
66
+ `<h2 class="sr-only">${esc(summary)}</h2>`,
67
+ "<style>",
68
+ STYLE,
69
+ "</style>",
70
+ '<div class="cap">',
71
+ body,
72
+ "</div>",
73
+ ].join("\n");
74
+ }
75
+ function orderedProviders(view) {
76
+ if (!view)
77
+ return [];
78
+ return [...view.providers]
79
+ .filter((provider) => (provider.accounts?.length ?? 0) > 0)
80
+ .sort((a, b) => {
81
+ const rank = providerRank(a.provider) - providerRank(b.provider);
82
+ if (rank !== 0)
83
+ return rank;
84
+ return a.provider.localeCompare(b.provider);
85
+ });
86
+ }
87
+ function providerRank(provider) {
88
+ const index = PROVIDER_ORDER.indexOf(provider);
89
+ return index === -1 ? PROVIDER_ORDER.length : index;
90
+ }
91
+ function renderProvider(provider, input) {
92
+ const accounts = provider.accounts ?? [];
93
+ const plan = providerPlan(provider.provider, accounts);
94
+ const icon = ICONS[provider.provider];
95
+ const iconHtml = icon ? `<i class="ti ${icon}" aria-hidden="true"></i>` : "";
96
+ const planHtml = plan ? `<span class="as" style="margin-left:auto">${esc(plan)}</span>` : "";
97
+ const blocks = accounts.map((account) => renderAccount(provider.provider, account, input));
98
+ return [
99
+ '<div class="prov">',
100
+ `<div class="ph">${iconHtml}${esc(providerTitle(provider.provider))}${planHtml}</div>`,
101
+ "",
102
+ blocks.join("\n\n"),
103
+ "</div>",
104
+ ].join("\n");
105
+ }
106
+ function renderAccount(provider, account, input) {
107
+ const name = accountName(provider, account);
108
+ const usage = isUsageOnly(provider, account, input.accounts ?? [])
109
+ ? ' <span class="as">usage only</span>'
110
+ : "";
111
+ const auth = authKind(account);
112
+ if (auth === "expired" || auth === "signed_out") {
113
+ const pill = auth === "expired" ? "Token expired" : "Signed out";
114
+ return [
115
+ '<div class="acct">',
116
+ `<div class="ah"><span class="an" style="color:var(--text-secondary)">${esc(name)}${usage}</span><span class="pill" style="${PILL_STYLE.muted}">${pill}</span></div>`,
117
+ `<div class="as">${esc(lastReadLine(account, input.timeZone))}</div>`,
118
+ "</div>",
119
+ ].join("\n");
120
+ }
121
+ const limits = visibleLimits(account.limits);
122
+ const pill = statusPill(limits);
123
+ const rows = limits.map((limit) => renderLimit(limit, input));
124
+ return [
125
+ '<div class="acct">',
126
+ `<div class="ah"><span class="an">${esc(name)}${usage}</span><span class="pill" style="${PILL_STYLE[pill.tone]}">${esc(pill.text)}</span></div>`,
127
+ ...rows,
128
+ "</div>",
129
+ ].join("\n");
130
+ }
131
+ function renderLimit(limit, input) {
132
+ const percent = shownPercent(limit.usedPercent);
133
+ const color = barColor(limit.usedPercent);
134
+ const width = Math.min(100, percent);
135
+ return `<div class="row"><span>${esc(limitLabel(limit))}</span><div class="bar"><div class="fill" style="width:${width}%;background:${color}"></div></div><span class="pct">${percent}%</span><span>${esc(resetLabel(limit, input.nowMs, input.timeZone))}</span></div>`;
136
+ }
137
+ function renderFooter(readTime, stale) {
138
+ const read = `${stale ? "Stale: " : ""}Read ${readTime} from hq capacity`;
139
+ return [
140
+ '<div style="display:flex;align-items:center;gap:14px;flex-wrap:wrap;font-size:12px;color:var(--text-secondary)">',
141
+ '<span><span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:var(--text-accent);margin-right:5px"></span>under 70%</span>',
142
+ '<span><span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:var(--text-warning);margin-right:5px"></span>70 to 89%</span>',
143
+ '<span><span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:var(--text-danger);margin-right:5px"></span>90% and up</span>',
144
+ `<span style="margin-left:auto">${esc(read)}</span>`,
145
+ `<button onclick="sendPrompt('/capacity')" style="font-size:12px;padding:4px 10px;border-radius:var(--radius);border:1px solid var(--border);background:transparent;color:var(--text-primary);cursor:pointer"><i class="ti ti-refresh" aria-hidden="true"></i> Refresh</button>`,
146
+ "</div>",
147
+ ].join("\n");
148
+ }
149
+ function statusPill(limits) {
150
+ const exhausted = pickExhausted(limits);
151
+ if (exhausted)
152
+ return { text: usedUpText(exhausted), tone: "danger" };
153
+ const highest = limits.reduce((max, limit) => {
154
+ if (typeof limit.usedPercent !== "number" || !Number.isFinite(limit.usedPercent))
155
+ return max;
156
+ return Math.max(max, limit.usedPercent);
157
+ }, 0);
158
+ if (highest >= 70)
159
+ return { text: "Running low", tone: "warning" };
160
+ if (highest >= 50)
161
+ return { text: "Half used", tone: "accent" };
162
+ return { text: "Plenty left", tone: "success" };
163
+ }
164
+ function pickExhausted(limits) {
165
+ const exhausted = limits.filter((limit) => limit.state === "exhausted");
166
+ if (exhausted.length === 0)
167
+ return null;
168
+ const scoped = exhausted.filter((limit) => limit.kind === "weekly_scoped" && limit.scope?.trim());
169
+ const pool = scoped.length > 0 ? scoped : exhausted;
170
+ return [...pool].sort((a, b) => (b.usedPercent ?? -1) - (a.usedPercent ?? -1) || a.limitId.localeCompare(b.limitId))[0] ?? null;
171
+ }
172
+ function usedUpText(limit) {
173
+ const scope = limit.scope?.trim();
174
+ if (scope)
175
+ return `${scope} used up`;
176
+ if (limit.kind === "session")
177
+ return "Session used up";
178
+ if (limit.kind === "weekly_all" || limit.kind === "weekly_scoped")
179
+ return "Weekly used up";
180
+ if (limit.kind === "credits")
181
+ return "Credits used up";
182
+ if (limit.kind === "on_demand")
183
+ return "On demand used up";
184
+ if (limit.kind === "prepaid")
185
+ return "Prepaid used up";
186
+ return "Limit used up";
187
+ }
188
+ function visibleLimits(limits) {
189
+ return limits
190
+ .filter((limit) => {
191
+ if (limit.kind === "session" || limit.kind === "weekly_all" || limit.kind === "weekly_scoped")
192
+ return true;
193
+ // Credit, spend and prepaid rows only matter once something is used.
194
+ return typeof limit.usedPercent === "number" && Number.isFinite(limit.usedPercent) && limit.usedPercent > 0;
195
+ })
196
+ .sort((a, b) => {
197
+ const rank = limitRank(a) - limitRank(b);
198
+ if (rank !== 0)
199
+ return rank;
200
+ const scope = (a.scope ?? "").localeCompare(b.scope ?? "");
201
+ if (scope !== 0)
202
+ return scope;
203
+ return a.limitId.localeCompare(b.limitId);
204
+ });
205
+ }
206
+ function limitRank(limit) {
207
+ if (limit.kind === "session")
208
+ return 0;
209
+ if (limit.kind === "weekly_all")
210
+ return 1;
211
+ if (limit.kind === "weekly_scoped")
212
+ return 2;
213
+ return 3;
214
+ }
215
+ function limitLabel(limit) {
216
+ if (limit.kind === "session") {
217
+ const minutes = limit.windowMinutes;
218
+ if (minutes && minutes > 0 && minutes % 60 === 0)
219
+ return `Session (${minutes / 60}h)`;
220
+ if (minutes && minutes > 0)
221
+ return `Session (${minutes}m)`;
222
+ return "Session";
223
+ }
224
+ if (limit.kind === "weekly_all")
225
+ return "Weekly";
226
+ if (limit.kind === "weekly_scoped") {
227
+ const scope = limit.scope?.trim();
228
+ return scope ? `${scope} weekly` : "Weekly";
229
+ }
230
+ if (limit.kind === "credits")
231
+ return limit.limitId === "spend" ? "Spend" : "Extra usage";
232
+ if (limit.kind === "on_demand")
233
+ return "On demand";
234
+ if (limit.kind === "prepaid")
235
+ return "Prepaid";
236
+ return "Limit";
237
+ }
238
+ function resetLabel(limit, nowMs, timeZone) {
239
+ const raw = limit.resetsAt ? Date.parse(limit.resetsAt) : Number.NaN;
240
+ // Providers stamp resets a few milliseconds either side of the minute
241
+ // (11:59:59.970); show the minute the reset belongs to.
242
+ const resetsAtMs = Number.isNaN(raw) ? raw : Math.round(raw / 60_000) * 60_000;
243
+ const past = !Number.isNaN(resetsAtMs) && resetsAtMs < nowMs;
244
+ if (limit.state === "reset-pending" || past)
245
+ return "resetting now";
246
+ if (Number.isNaN(resetsAtMs))
247
+ return limit.usedPercent === 0 ? "idle" : "no reset time";
248
+ const days = calendarDayDiff(resetsAtMs, nowMs, timeZone);
249
+ const time = clockTime(resetsAtMs, timeZone);
250
+ if (days <= 0)
251
+ return days === 0 ? `resets today ${time}` : "resetting now";
252
+ if (days === 1)
253
+ return `resets tomorrow ${time}`;
254
+ const parts = weekMonth(resetsAtMs, timeZone);
255
+ if (days <= 6)
256
+ return `resets ${parts.weekday} ${time}`;
257
+ return `resets ${parts.weekday} ${parts.month} ${parts.day}, ${time}`;
258
+ }
259
+ function providerPlan(provider, accounts) {
260
+ const labels = new Set();
261
+ for (const account of accounts) {
262
+ for (const limit of account.limits) {
263
+ const plan = limit.planType?.trim();
264
+ if (!plan || sensitive(plan))
265
+ continue;
266
+ labels.add(plainPlan(provider, plan));
267
+ }
268
+ }
269
+ if (labels.size !== 1)
270
+ return null;
271
+ return [...labels][0] ?? null;
272
+ }
273
+ function plainPlan(provider, planType) {
274
+ if (provider === "grok")
275
+ return "Grok Build";
276
+ const key = planType.trim().toLowerCase();
277
+ if (key === "max" || key.startsWith("max ") || key.startsWith("max_") || key.startsWith("max-"))
278
+ return "Max plans";
279
+ if (key === "pro")
280
+ return "Pro plan";
281
+ if (key === "plus")
282
+ return "Plus plan";
283
+ const words = planType.trim().replace(/[_-]+/g, " ");
284
+ const sentence = words.charAt(0).toUpperCase() + words.slice(1);
285
+ if (/ plans?$/i.test(sentence))
286
+ return sentence;
287
+ return `${sentence} plan`;
288
+ }
289
+ function accountName(provider, account) {
290
+ const org = account.organization?.trim();
291
+ if (org && !sensitive(org))
292
+ return org;
293
+ const labels = account.labels.filter((label) => label.trim() && !sensitive(label));
294
+ const named = labels.find((label) => label !== "default");
295
+ if (named)
296
+ return named;
297
+ if (provider === "claude")
298
+ return "Claude Code terminal (default)";
299
+ return "Default account";
300
+ }
301
+ function isUsageOnly(provider, account, accounts) {
302
+ const labels = new Set(account.labels);
303
+ return accounts.some((row) => row.provider === provider && labels.has(row.label) && row.lanes === false);
304
+ }
305
+ function authKind(account) {
306
+ if (account.authState === "expired" || isExpiredReason(account.reason))
307
+ return "expired";
308
+ if (account.authState === "signed_out" || account.signedOut || account.reason?.startsWith("signed out:")) {
309
+ return "signed_out";
310
+ }
311
+ return "ok";
312
+ }
313
+ function lastReadLine(account, timeZone) {
314
+ const observed = account.observedAt ? Date.parse(account.observedAt) : Number.NaN;
315
+ const lead = Number.isNaN(observed) ? "Last checked at an unknown time." : `Last checked ${clockTime(observed, timeZone)}.`;
316
+ const sentence = reasonSentence(account.reason);
317
+ return sentence ? `${lead} ${sentence}` : lead;
318
+ }
319
+ function reasonSentence(reason) {
320
+ if (!reason)
321
+ return null;
322
+ let text = redactProviderTokens(reason).replace(/^(expired|signed out):\s*/i, "").trim();
323
+ if (!text || sensitive(text))
324
+ return null;
325
+ text = text.charAt(0).toUpperCase() + text.slice(1);
326
+ if (!/[.!?]$/.test(text))
327
+ text += ".";
328
+ return text;
329
+ }
330
+ function newestObserved(accounts, fallbackIso, nowMs) {
331
+ let latest = "";
332
+ for (const account of accounts) {
333
+ if (account.observedAt && account.observedAt > latest)
334
+ latest = account.observedAt;
335
+ }
336
+ const parsed = Date.parse(latest || fallbackIso);
337
+ return Number.isNaN(parsed) ? nowMs : parsed;
338
+ }
339
+ function shownPercent(value) {
340
+ if (typeof value !== "number" || !Number.isFinite(value))
341
+ return 0;
342
+ return Math.max(0, Math.round(value));
343
+ }
344
+ function barColor(value) {
345
+ const percent = typeof value === "number" && Number.isFinite(value) ? value : 0;
346
+ if (percent >= 90)
347
+ return "var(--text-danger)";
348
+ if (percent >= 70)
349
+ return "var(--text-warning)";
350
+ return "var(--text-accent)";
351
+ }
352
+ function calendarDayDiff(targetMs, nowMs, timeZone) {
353
+ const start = Date.parse(`${dateKey(nowMs, timeZone)}T00:00:00Z`);
354
+ const end = Date.parse(`${dateKey(targetMs, timeZone)}T00:00:00Z`);
355
+ return Math.round((end - start) / 86_400_000);
356
+ }
357
+ function dateKey(ms, timeZone) {
358
+ return new Intl.DateTimeFormat("en-CA", {
359
+ timeZone,
360
+ year: "numeric",
361
+ month: "2-digit",
362
+ day: "2-digit",
363
+ }).format(new Date(ms));
364
+ }
365
+ function clockTime(ms, timeZone) {
366
+ const parts = partsOf(ms, timeZone, { hour: "numeric", minute: "2-digit", hour12: true });
367
+ return `${parts.hour}:${parts.minute} ${parts.dayPeriod}`;
368
+ }
369
+ function weekMonth(ms, timeZone) {
370
+ const parts = partsOf(ms, timeZone, { weekday: "short", month: "short", day: "numeric" });
371
+ return { weekday: parts.weekday ?? "", month: parts.month ?? "", day: parts.day ?? "" };
372
+ }
373
+ function partsOf(ms, timeZone, options) {
374
+ const parts = new Intl.DateTimeFormat("en-US", { ...options, timeZone }).formatToParts(new Date(ms));
375
+ return Object.fromEntries(parts.filter((part) => part.type !== "literal").map((part) => [part.type, part.value]));
376
+ }
377
+ function providerTitle(provider) {
378
+ if (provider === "claude")
379
+ return "Claude";
380
+ if (provider === "codex")
381
+ return "Codex";
382
+ if (provider === "grok")
383
+ return "Grok";
384
+ const cleaned = provider.trim();
385
+ if (!cleaned)
386
+ return "Provider";
387
+ return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
388
+ }
389
+ function joinNames(names) {
390
+ if (names.length === 0)
391
+ return "provider";
392
+ if (names.length === 1)
393
+ return names[0] ?? "provider";
394
+ if (names.length === 2)
395
+ return `${names[0]} and ${names[1]}`;
396
+ return `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
397
+ }
398
+ function sensitive(value) {
399
+ return value.includes("@") || TOKEN_LEAK.test(value);
400
+ }
401
+ function esc(value) {
402
+ return value
403
+ .replace(/&/g, "&amp;")
404
+ .replace(/</g, "&lt;")
405
+ .replace(/>/g, "&gt;")
406
+ .replace(/"/g, "&quot;")
407
+ .replace(/'/g, "&#39;");
408
+ }
409
+ function isTimeZone(value) {
410
+ try {
411
+ new Intl.DateTimeFormat("en-US", { timeZone: value }).format(0);
412
+ return true;
413
+ }
414
+ catch {
415
+ return false;
416
+ }
417
+ }
418
+ //# sourceMappingURL=widget.js.map
@@ -36,6 +36,7 @@ import { laneCompanyUidSource, resolveLaneCompany } from "./lane-company.js";
36
36
  import { LEAD_LANE_KEEP_ALIVE_BLOCKED_RESUME_TEXT } from "./keep-alive.js";
37
37
  import { copySessionFiles, planSessionCopy } from "./move-account.js";
38
38
  import { ensureSessionHome } from "./session-relink.js";
39
+ import { emitDispatchAdvisory } from "../capacity/read.js";
39
40
  import { MESSAGE_REPLAY_STATE_NAME } from "./message-replay.js";
40
41
  import { markAnsweredQuestionsDelivered } from "./questions.js";
41
42
  import { hasEarlierEligibleQueuedLane, laneAdmissionCapReason, withLaneAdmissionLock, } from "./admission.js";
@@ -1837,6 +1838,15 @@ function spawnDetached(spawnProcess, command, args, env, cwd, stdoutPath, stderr
1837
1838
  closeFd(errFd, stderrPath);
1838
1839
  }
1839
1840
  }
1841
+ /** One advisory line before the provider process starts. Never changes the spawn. */
1842
+ function noteCapacityAdvisory(lane, env) {
1843
+ emitDispatchAdvisory({
1844
+ chosenEngine: lane.provider,
1845
+ caller: "lanes",
1846
+ story: lane.story_id.trim() ? lane.story_id : null,
1847
+ env,
1848
+ });
1849
+ }
1840
1850
  function spawnTrackedWorker(spawnProcess, command, args, env, cwd, stdoutPath, stderrPath, exitPath) {
1841
1851
  try {
1842
1852
  fs.unlinkSync(exitPath);
@@ -2724,6 +2734,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
2724
2734
  let workerPid;
2725
2735
  let watcherPid;
2726
2736
  try {
2737
+ noteCapacityAdvisory(lane, env);
2727
2738
  workerPid = spawnTrackedWorker(spawnProcess, command.command, command.args, childEnv, deps.workingDirectory ?? pendingResume?.working_directory ?? hqRoot, path.join(logsDir, "worker.out"), path.join(logsDir, "worker.err"), path.join(logsDir, "worker-exit.json"));
2728
2739
  writePidRecord(path.join(runDir, "worker.pid"), {
2729
2740
  pid: workerPid,
@@ -3034,6 +3045,7 @@ export function spawnEnvelopeRepair(hqRoot, lane, input, deps = {}) {
3034
3045
  const previous = readSpawnManifest(runDir);
3035
3046
  let workerPid;
3036
3047
  try {
3048
+ noteCapacityAdvisory(lane, env);
3037
3049
  workerPid = spawnTrackedWorker(spawnProcess, command.command, command.args, childEnv, hqRoot, path.join(logsDir, "worker.out"), path.join(logsDir, "worker.err"), path.join(logsDir, "worker-exit.json"));
3038
3050
  writePidRecord(path.join(runDir, "worker.pid"), {
3039
3051
  pid: workerPid,
@@ -19,6 +19,7 @@ import { PresenceClient, type MqttConnectFn } from "./presence.js";
19
19
  import { TranscriptWatcher, type TranscriptFs } from "./transcript-watch.js";
20
20
  import { type MeshEmitMode } from "./mode.js";
21
21
  import { type CommandRunner, type LegacyListenPlatform } from "./legacy-listen.js";
22
+ import { type StartCapacitySchedulerOptions } from "../../../capacity/scheduler.js";
22
23
  export declare const SPOOL_DEBOUNCE_MS = 2000;
23
24
  export declare const FLUSH_INTERVAL_MS = 10000;
24
25
  /** Keep startup recovery bounded: the next daemon start continues the backlog. */
@@ -123,6 +124,10 @@ export interface DaemonRunDeps {
123
124
  now?: () => Date;
124
125
  /** Host for legacy listen retirement. Tests inject this; default is process.platform. */
125
126
  legacyListenPlatform?: LegacyListenPlatform;
127
+ /** Ledger root for the capacity scheduler. Defaults to the home capacity directory. */
128
+ capacityRoot?: string;
129
+ /** Injected capacity scheduler options (tests). Timers, root, clock, and log stay with the daemon. */
130
+ capacityScheduler?: Omit<StartCapacitySchedulerOptions, "timers" | "root" | "now" | "log">;
126
131
  /** Service-manager runner for legacy listen retirement (tests inject). */
127
132
  legacyListenRun?: CommandRunner;
128
133
  windowsLegacyTaskPresent?: boolean;
@@ -40,6 +40,8 @@ import { resolveRunningCliEntry } from "./install.js";
40
40
  import { readDaemonConfig } from "./config.js";
41
41
  import { retireLegacyListen } from "./legacy-listen.js";
42
42
  import { startWorkCacheSubscriber } from "./work-subscribe.js";
43
+ import { capacityRoot } from "../../../capacity/ledger.js";
44
+ import { startCapacityScheduler, } from "../../../capacity/scheduler.js";
43
45
  export const SPOOL_DEBOUNCE_MS = 2_000;
44
46
  export const FLUSH_INTERVAL_MS = 10_000;
45
47
  /** Keep startup recovery bounded: the next daemon start continues the backlog. */
@@ -605,6 +607,7 @@ export async function runMeshDaemon(deps = {}) {
605
607
  let boardIntervalHandle = null;
606
608
  let transcriptIntervalHandle = null;
607
609
  let watcher = null;
610
+ let stopCapacity = null;
608
611
  /** Shared with transcript watcher: sessionIds seen on the hook/spool path. */
609
612
  const lastHookEventAt = new Map();
610
613
  const lastHookTurnEndAt = new Map();
@@ -621,7 +624,7 @@ export async function runMeshDaemon(deps = {}) {
621
624
  return true;
622
625
  };
623
626
  if (emitPath && deps.enableTranscriptWatch !== false) {
624
- const hqRoot = deps.hqRoot ?? env.HQ_ROOT?.trim() ?? undefined;
627
+ const hqRoot = deps.hqRoot ?? (env.HQ_ROOT?.trim() || undefined);
625
628
  const getToken = deps.getToken ?? defaultGetToken;
626
629
  transcriptWatcher = new TranscriptWatcher({
627
630
  home,
@@ -877,6 +880,14 @@ export async function runMeshDaemon(deps = {}) {
877
880
  stopped = true;
878
881
  shutdownController.abort();
879
882
  log(dir, "daemon stopping");
883
+ try {
884
+ stopCapacity?.();
885
+ }
886
+ catch (err) {
887
+ const msg = err instanceof Error ? err.message : String(err);
888
+ log(dir, `capacity scheduler stop error: ${msg.slice(0, 120)}`);
889
+ }
890
+ stopCapacity = null;
880
891
  if (debounceHandle !== null)
881
892
  timers.clearTimeout(debounceHandle);
882
893
  if (flushIntervalHandle !== null)
@@ -938,6 +949,23 @@ export async function runMeshDaemon(deps = {}) {
938
949
  }
939
950
  log(dir, "daemon stopped");
940
951
  };
952
+ try {
953
+ const capacity = startCapacityScheduler({
954
+ home,
955
+ ...deps.capacityScheduler,
956
+ timers,
957
+ root: deps.capacityRoot ?? capacityRoot(env, home),
958
+ now,
959
+ log: (line) => log(dir, line),
960
+ enumerateAccounts: true,
961
+ hqRoot: deps.hqRoot ?? (env.HQ_ROOT?.trim() || undefined),
962
+ });
963
+ stopCapacity = capacity.stop;
964
+ }
965
+ catch (err) {
966
+ const msg = err instanceof Error ? err.message : String(err);
967
+ log(dir, `capacity scheduler start failed: ${msg.slice(0, 120)}`);
968
+ }
941
969
  const onSignal = () => {
942
970
  void stop().finally(() => exit(0));
943
971
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.92",
3
+ "version": "5.345.93",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {