@indigoai-us/hq-cli 5.297.0 → 5.299.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/CHANGELOG.md +11 -0
- package/dist/command-catalog.generated.d.ts +23 -0
- package/dist/command-catalog.generated.js +30 -0
- package/dist/commands/agents.js +59 -5
- package/dist/commands/daemon.js +1 -0
- package/dist/commands/files-browse.d.ts +3 -1
- package/dist/commands/files-browse.js +30 -7
- package/dist/commands/integrations-api.js +14 -3
- package/dist/commands/lanes-accounts.d.ts +3 -0
- package/dist/commands/lanes-accounts.js +25 -1
- package/dist/commands/lanes.js +10 -0
- package/dist/lib/bot/prompt.d.ts +2 -0
- package/dist/lib/bot/prompt.js +29 -4
- package/dist/lib/daemon/legacy.d.ts +5 -2
- package/dist/lib/daemon/legacy.js +6 -4
- package/dist/lib/daemon/migrate.d.ts +10 -1
- package/dist/lib/daemon/migrate.js +22 -7
- package/dist/lib/daemon/run.js +19 -7
- package/dist/lib/daemon/supervisor.js +9 -1
- package/dist/lib/lanes/profile.js +5 -0
- package/dist/lib/lanes/quota.d.ts +19 -0
- package/dist/lib/lanes/quota.js +106 -29
- package/dist/lib/lanes/reconcile.js +12 -1
- package/dist/lib/lanes/store.d.ts +2 -0
- package/dist/lib/lanes/store.js +4 -0
- package/dist/lib/lanes/watcher.js +10 -1
- package/dist/lib/mesh/live/daemon/install.d.ts +3 -0
- package/dist/lib/mesh/live/daemon/install.js +13 -2
- package/dist/lib/policy-validator.d.ts +5 -0
- package/dist/lib/policy-validator.js +69 -18
- package/dist/utils/self-update.js +249 -2
- package/dist/utils/staged-install.js +71 -21
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.299.0] — 2026-09-30
|
|
6
|
+
- Add `hq lanes accounts clear-quota` and clear future quota marks after successful lane turns.
|
|
7
|
+
- On macOS, `hq daemon` retires a loaded legacy mesh LaunchAgent even when its plist is missing, and daemon uninstall boots the job out before deleting its plist. Mesh-owner wait logs now repeat at most every ten minutes.
|
|
8
|
+
- Restore npm command links after a failed hq-cli self-update and repair or report missing links after successful installs.
|
|
9
|
+
|
|
10
|
+
## [5.298.0] — 2026-09-30
|
|
11
|
+
- [feedback_d4a080e9-de79-4290-81c3-06ba26990a38] Preserve raw substring matches while normalizing company path searches.
|
|
12
|
+
- `hq agents` now prints the Slack app settings link and the console paste link whenever an agent is waiting on its Slack app-level token, and follows the pending actions and paste URL the server returns.
|
|
13
|
+
- When an app install is already running, `hq integrations connect` tells you to wait instead of showing a generic conflict error.
|
|
14
|
+
- Validate policy trigger expressions when loading worker policies (feedback_3982db47-ceb8-4148-8ee0-4dd533f5138b).
|
|
15
|
+
|
|
5
16
|
## [5.297.0] — 2026-09-30
|
|
6
17
|
- When enabled by hq-flags, show the quarantined work-context outbox count and latest reason in hq doctor and mesh daemon status.
|
|
7
18
|
- `hq daemon` now exits after shutdown when handles remain open, and its mesh service waits when another live mesh daemon owns the process lock.
|
|
@@ -8990,6 +8990,29 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
8990
8990
|
readonly description: "JSON on stdout (always)";
|
|
8991
8991
|
}];
|
|
8992
8992
|
readonly subcommands: readonly [];
|
|
8993
|
+
}, {
|
|
8994
|
+
readonly name: "clear-quota";
|
|
8995
|
+
readonly description: "Clear quota reset marks for a provider account";
|
|
8996
|
+
readonly aliases: readonly [];
|
|
8997
|
+
readonly hidden: false;
|
|
8998
|
+
readonly usage: "[options] <provider> <account-id>";
|
|
8999
|
+
readonly arguments: readonly [{
|
|
9000
|
+
readonly name: "provider";
|
|
9001
|
+
readonly required: true;
|
|
9002
|
+
readonly variadic: false;
|
|
9003
|
+
}, {
|
|
9004
|
+
readonly name: "account-id";
|
|
9005
|
+
readonly required: true;
|
|
9006
|
+
readonly variadic: false;
|
|
9007
|
+
}];
|
|
9008
|
+
readonly options: readonly [{
|
|
9009
|
+
readonly flags: "--model <model>";
|
|
9010
|
+
readonly description: "Clear only this model's reset";
|
|
9011
|
+
}, {
|
|
9012
|
+
readonly flags: "--json";
|
|
9013
|
+
readonly description: "JSON on stdout (always)";
|
|
9014
|
+
}];
|
|
9015
|
+
readonly subcommands: readonly [];
|
|
8993
9016
|
}, {
|
|
8994
9017
|
readonly name: "enable";
|
|
8995
9018
|
readonly description: "Enable an account without touching credentials";
|
|
@@ -11601,6 +11601,36 @@ export const COMMAND_CATALOG = [
|
|
|
11601
11601
|
],
|
|
11602
11602
|
"subcommands": []
|
|
11603
11603
|
},
|
|
11604
|
+
{
|
|
11605
|
+
"name": "clear-quota",
|
|
11606
|
+
"description": "Clear quota reset marks for a provider account",
|
|
11607
|
+
"aliases": [],
|
|
11608
|
+
"hidden": false,
|
|
11609
|
+
"usage": "[options] <provider> <account-id>",
|
|
11610
|
+
"arguments": [
|
|
11611
|
+
{
|
|
11612
|
+
"name": "provider",
|
|
11613
|
+
"required": true,
|
|
11614
|
+
"variadic": false
|
|
11615
|
+
},
|
|
11616
|
+
{
|
|
11617
|
+
"name": "account-id",
|
|
11618
|
+
"required": true,
|
|
11619
|
+
"variadic": false
|
|
11620
|
+
}
|
|
11621
|
+
],
|
|
11622
|
+
"options": [
|
|
11623
|
+
{
|
|
11624
|
+
"flags": "--model <model>",
|
|
11625
|
+
"description": "Clear only this model's reset"
|
|
11626
|
+
},
|
|
11627
|
+
{
|
|
11628
|
+
"flags": "--json",
|
|
11629
|
+
"description": "JSON on stdout (always)"
|
|
11630
|
+
}
|
|
11631
|
+
],
|
|
11632
|
+
"subcommands": []
|
|
11633
|
+
},
|
|
11604
11634
|
{
|
|
11605
11635
|
"name": "enable",
|
|
11606
11636
|
"description": "Enable an account without touching credentials",
|
package/dist/commands/agents.js
CHANGED
|
@@ -549,13 +549,59 @@ function currentDeviceSignInAction(status) {
|
|
|
549
549
|
code,
|
|
550
550
|
};
|
|
551
551
|
}
|
|
552
|
+
const SLACK_APP_TOKEN_INSTRUCTION = "Create an app-level token with the connections:write scope, then paste it into HQ. HQ verifies the token and resumes setup; if Slack asks you to install the app, follow the install link shown there.";
|
|
553
|
+
function consolePasteUrl(companyKey, agentUid) {
|
|
554
|
+
const base = process.env.HQ_CONSOLE_URL?.trim().replace(/\/+$/, "") || "https://hq.computer";
|
|
555
|
+
return `${base}/companies/${encodeURIComponent(companyKey)}/agents?setup=${encodeURIComponent(agentUid)}`;
|
|
556
|
+
}
|
|
557
|
+
function slackAppIdFromSetupState(status) {
|
|
558
|
+
const steps = recordValue(status.setupState)?.steps;
|
|
559
|
+
if (!Array.isArray(steps))
|
|
560
|
+
return null;
|
|
561
|
+
for (const step of steps) {
|
|
562
|
+
const item = recordValue(step);
|
|
563
|
+
if (item?.name !== "channels" || item.status === "done")
|
|
564
|
+
continue;
|
|
565
|
+
const match = /https:\/\/api\.slack\.com\/apps\/(A[A-Z0-9]+)/.exec(JSON.stringify(item));
|
|
566
|
+
if (match)
|
|
567
|
+
return match[1];
|
|
568
|
+
}
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
function serverPendingActions(status) {
|
|
572
|
+
if (!Array.isArray(status.pendingActions))
|
|
573
|
+
return [];
|
|
574
|
+
const actions = [];
|
|
575
|
+
for (const raw of status.pendingActions) {
|
|
576
|
+
const item = recordValue(raw);
|
|
577
|
+
const type = nonEmptyString(item?.type);
|
|
578
|
+
const title = nonEmptyString(item?.title);
|
|
579
|
+
if (!item || !type || !title)
|
|
580
|
+
continue;
|
|
581
|
+
const url = safeActionUrl(item.url) ?? undefined;
|
|
582
|
+
const pasteUrl = safeActionUrl(item.pasteUrl) ?? undefined;
|
|
583
|
+
const rawCode = nonEmptyString(item.code);
|
|
584
|
+
actions.push({
|
|
585
|
+
type,
|
|
586
|
+
title,
|
|
587
|
+
summary: title.charAt(0).toLowerCase() + title.slice(1),
|
|
588
|
+
instruction: nonEmptyString(item.instruction) ?? "Open this page and follow the instructions.",
|
|
589
|
+
...(url ? { url, urlLabel: type === "slack-app-token" ? "Get token" : "Open" } : {}),
|
|
590
|
+
...(pasteUrl ? { pasteUrl } : {}),
|
|
591
|
+
...(rawCode && /^[A-Z0-9]{4,8}-[A-Z0-9]{4,8}$/i.test(rawCode) ? { code: rawCode } : {}),
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
return actions;
|
|
595
|
+
}
|
|
552
596
|
function pendingSlackAction(status, companySlug) {
|
|
553
597
|
const agent = recordValue(status.agent);
|
|
554
598
|
const configuredSlack = recordValue(recordValue(agent?.channels)?.slack);
|
|
555
|
-
const
|
|
599
|
+
const stepAppId = slackAppIdFromSetupState(status);
|
|
600
|
+
const appId = nonEmptyString(configuredSlack?.appId) ?? stepAppId;
|
|
556
601
|
const agentUid = nonEmptyString(agent?.uid);
|
|
557
602
|
const companyUid = nonEmptyString(agent?.companyUid);
|
|
558
|
-
const tokenPage = safeActionUrl(configuredSlack?.appTokenPendingUrl)
|
|
603
|
+
const tokenPage = safeActionUrl(configuredSlack?.appTokenPendingUrl) ??
|
|
604
|
+
(stepAppId ? `https://api.slack.com/apps/${stepAppId}/general` : null);
|
|
559
605
|
if (tokenPage &&
|
|
560
606
|
appId && /^A[A-Z0-9]+$/.test(appId) &&
|
|
561
607
|
new URL(tokenPage).hostname === "api.slack.com" &&
|
|
@@ -568,10 +614,10 @@ function pendingSlackAction(status, companySlug) {
|
|
|
568
614
|
type: "slack-app-token",
|
|
569
615
|
title: `Finish ${nonEmptyString(agent?.name) ?? "the agent"}'s Slack connection`,
|
|
570
616
|
summary: "connect it to Slack",
|
|
571
|
-
instruction:
|
|
617
|
+
instruction: SLACK_APP_TOKEN_INSTRUCTION,
|
|
572
618
|
url: `https://api.slack.com/apps/${appId}/general`,
|
|
573
619
|
urlLabel: "Get token",
|
|
574
|
-
pasteUrl:
|
|
620
|
+
pasteUrl: consolePasteUrl(companyUrlKey, agentUid),
|
|
575
621
|
};
|
|
576
622
|
}
|
|
577
623
|
const diagnostics = recordValue(agent?.channelDiagnostics);
|
|
@@ -587,6 +633,11 @@ function pendingSlackAction(status, companySlug) {
|
|
|
587
633
|
instruction: "Open this page, create the app-level token, then provide it in the agent's Slack setup.",
|
|
588
634
|
url,
|
|
589
635
|
urlLabel: "Open",
|
|
636
|
+
...(agentUid && AGENT_UID_PATTERN.test(agentUid) && companyUid && /^cmp_[A-Za-z0-9_-]+$/.test(companyUid)
|
|
637
|
+
? {
|
|
638
|
+
pasteUrl: consolePasteUrl(companySlug && /^[a-z0-9][a-z0-9-]*$/.test(companySlug) ? companySlug : companyUid, agentUid),
|
|
639
|
+
}
|
|
640
|
+
: {}),
|
|
590
641
|
};
|
|
591
642
|
}
|
|
592
643
|
if (slack?.inboundCapability !== "pending-install")
|
|
@@ -606,6 +657,9 @@ function pendingSlackAction(status, companySlug) {
|
|
|
606
657
|
* changing the renderers or command control flow.
|
|
607
658
|
*/
|
|
608
659
|
function pendingOperatorActions(status, companySlug) {
|
|
660
|
+
const fromServer = serverPendingActions(status);
|
|
661
|
+
if (fromServer.length > 0)
|
|
662
|
+
return fromServer;
|
|
609
663
|
return [
|
|
610
664
|
currentDeviceSignInAction(status),
|
|
611
665
|
pendingSlackAction(status, companySlug),
|
|
@@ -616,7 +670,7 @@ async function consoleCompanySlug(token, status, companyRef) {
|
|
|
616
670
|
return companyRef;
|
|
617
671
|
const agent = recordValue(status.agent);
|
|
618
672
|
const slack = recordValue(recordValue(agent?.channels)?.slack);
|
|
619
|
-
if (!slack?.appTokenPendingUrl)
|
|
673
|
+
if (!slack?.appTokenPendingUrl && !slackAppIdFromSetupState(status))
|
|
620
674
|
return undefined;
|
|
621
675
|
const companyUid = nonEmptyString(agent?.companyUid);
|
|
622
676
|
if (!companyUid)
|
package/dist/commands/daemon.js
CHANGED
|
@@ -91,6 +91,7 @@ async function loadDefaults(over) {
|
|
|
91
91
|
home: dir ?? home,
|
|
92
92
|
existsSync: fs.existsSync,
|
|
93
93
|
listDir: (dir) => fs.readdirSync(dir),
|
|
94
|
+
launchdJobLoaded: (label) => run("launchctl", ["print", `gui/${process.getuid?.() ?? 0}/${label}`]).status === 0,
|
|
94
95
|
windowsTaskExists: (task) => run("schtasks.exe", ["/Query", "/TN", task]).status === 0,
|
|
95
96
|
})),
|
|
96
97
|
resolveHqRoot: over.resolveHqRoot ?? ((flag) => resolveDaemonHqRoot(flag, { env, cwd: process.cwd(), home })),
|
|
@@ -502,6 +502,8 @@ export declare function resolveBrowseSession(): Promise<{
|
|
|
502
502
|
* and registers `share`/`unshare`/`acl`; this function appends the two
|
|
503
503
|
* new browse-vs-sync subcommands so they share the `--company` switch.
|
|
504
504
|
*/
|
|
505
|
-
export declare function registerFilesBrowseCommands(filesCmd: Command
|
|
505
|
+
export declare function registerFilesBrowseCommands(filesCmd: Command, deps?: {
|
|
506
|
+
personalS3Factory?: S3ClientFactory;
|
|
507
|
+
}): void;
|
|
506
508
|
export {};
|
|
507
509
|
//# sourceMappingURL=files-browse.d.ts.map
|
|
@@ -192,6 +192,18 @@ export function formatBrowseTable(rows) {
|
|
|
192
192
|
lines.push(renderRow(row));
|
|
193
193
|
return lines.join("\n");
|
|
194
194
|
}
|
|
195
|
+
function formatSearchResults(rows, query) {
|
|
196
|
+
if (rows.length === 0) {
|
|
197
|
+
return `No files found for search ${JSON.stringify(query)}.`;
|
|
198
|
+
}
|
|
199
|
+
return formatBrowseTable(rows);
|
|
200
|
+
}
|
|
201
|
+
function normalizeCompanySearchPath(value) {
|
|
202
|
+
return value
|
|
203
|
+
.split("/")
|
|
204
|
+
.map((segment) => (segment === "company" ? "companies" : segment))
|
|
205
|
+
.join("/");
|
|
206
|
+
}
|
|
195
207
|
/** Guard: the personal path needs an S3 factory. */
|
|
196
208
|
function requirePersonalS3Factory(f) {
|
|
197
209
|
if (!f) {
|
|
@@ -530,7 +542,17 @@ export async function runSearch(input) {
|
|
|
530
542
|
region: input.region,
|
|
531
543
|
});
|
|
532
544
|
const q = input.query.toLowerCase();
|
|
533
|
-
|
|
545
|
+
// Only a leading `company/<slug>...` path form is an alias for the
|
|
546
|
+
// `companies/` anchor. A bare `company` stays a plain substring, since every
|
|
547
|
+
// company-mode key starts with `companies/<slug>/`.
|
|
548
|
+
const normalizedQuery = /^company\/[^/]+/.test(q)
|
|
549
|
+
? normalizeCompanySearchPath(q)
|
|
550
|
+
: q;
|
|
551
|
+
return rows.filter((r) => {
|
|
552
|
+
const key = r.key.toLowerCase();
|
|
553
|
+
return (key.includes(q) ||
|
|
554
|
+
normalizeCompanySearchPath(key).includes(normalizedQuery));
|
|
555
|
+
});
|
|
534
556
|
}
|
|
535
557
|
/** Per-machine pin set path: `<hqRoot>/.hq/pins.json`. */
|
|
536
558
|
export function pinFilePath(hqRoot) {
|
|
@@ -927,7 +949,8 @@ export async function resolveBrowseSession() {
|
|
|
927
949
|
* and registers `share`/`unshare`/`acl`; this function appends the two
|
|
928
950
|
* new browse-vs-sync subcommands so they share the `--company` switch.
|
|
929
951
|
*/
|
|
930
|
-
export function registerFilesBrowseCommands(filesCmd) {
|
|
952
|
+
export function registerFilesBrowseCommands(filesCmd, deps = {}) {
|
|
953
|
+
const personalS3Factory = deps.personalS3Factory ?? defaultS3Factory;
|
|
931
954
|
filesCmd
|
|
932
955
|
.command("browse [path]")
|
|
933
956
|
.description("List vault objects under [path] without syncing them locally. Private-folder rows (foo/) are labelled and do not imply child access; their children are creator-only. Uses the browse-vend path (role-bypass allowed). Pass --personal to browse the caller's personal vault; otherwise [path] must start with companies/<slug>/.")
|
|
@@ -961,7 +984,7 @@ export function registerFilesBrowseCommands(filesCmd) {
|
|
|
961
984
|
personalMode: true,
|
|
962
985
|
personalUid,
|
|
963
986
|
vaultClient: cognito,
|
|
964
|
-
s3Factory:
|
|
987
|
+
s3Factory: personalS3Factory,
|
|
965
988
|
region: DEFAULT_COGNITO.region,
|
|
966
989
|
});
|
|
967
990
|
console.log(formatBrowseTable(result.rows));
|
|
@@ -1053,7 +1076,7 @@ export function registerFilesBrowseCommands(filesCmd) {
|
|
|
1053
1076
|
personalMode: true,
|
|
1054
1077
|
personalUid,
|
|
1055
1078
|
vaultClient: cognito,
|
|
1056
|
-
s3Factory:
|
|
1079
|
+
s3Factory: personalS3Factory,
|
|
1057
1080
|
region: DEFAULT_COGNITO.region,
|
|
1058
1081
|
});
|
|
1059
1082
|
if (result.destination.kind === "file") {
|
|
@@ -1170,10 +1193,10 @@ export function registerFilesBrowseCommands(filesCmd) {
|
|
|
1170
1193
|
personalMode: true,
|
|
1171
1194
|
personalUid,
|
|
1172
1195
|
vaultClient: cognito,
|
|
1173
|
-
s3Factory:
|
|
1196
|
+
s3Factory: personalS3Factory,
|
|
1174
1197
|
region: DEFAULT_COGNITO.region,
|
|
1175
1198
|
});
|
|
1176
|
-
console.log(
|
|
1199
|
+
console.log(formatSearchResults(rows, query));
|
|
1177
1200
|
return;
|
|
1178
1201
|
}
|
|
1179
1202
|
if (!company) {
|
|
@@ -1187,7 +1210,7 @@ export function registerFilesBrowseCommands(filesCmd) {
|
|
|
1187
1210
|
companyClient: makeCompanyPresignFactory(accessToken, company),
|
|
1188
1211
|
region: DEFAULT_COGNITO.region,
|
|
1189
1212
|
});
|
|
1190
|
-
console.log(
|
|
1213
|
+
console.log(formatSearchResults(rows, query));
|
|
1191
1214
|
}
|
|
1192
1215
|
catch (err) {
|
|
1193
1216
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* is optional and read defensively.
|
|
9
9
|
*/
|
|
10
10
|
import { vaultApiFetch } from "../utils/vault-api.js";
|
|
11
|
-
import { raiseForResponse, } from "./integrations-core.js";
|
|
11
|
+
import { IntegrationsCliError, raiseForResponse, } from "./integrations-core.js";
|
|
12
12
|
export async function listCatalog(token, companyUid, opts = {}) {
|
|
13
13
|
const query = {
|
|
14
14
|
companyUid,
|
|
@@ -59,8 +59,19 @@ export async function installIntegration(token, companyUid, input) {
|
|
|
59
59
|
method: "POST",
|
|
60
60
|
body: { companyUid, ...input },
|
|
61
61
|
});
|
|
62
|
-
if (!res.ok)
|
|
63
|
-
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
try {
|
|
64
|
+
await raiseForResponse(res, "Failed to connect the app");
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (error instanceof IntegrationsCliError &&
|
|
68
|
+
error.status === 409 &&
|
|
69
|
+
error.code === "INTEGRATION_FACTORY_INSTALL_IN_PROGRESS") {
|
|
70
|
+
throw new IntegrationsCliError("An install for this app is already running. Wait for it to finish before trying again.", { expected: true, status: 409, code: error.code });
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
64
75
|
return (await res.json());
|
|
65
76
|
}
|
|
66
77
|
export async function uninstallIntegration(token, companyUid, installationId) {
|
|
@@ -17,6 +17,9 @@ export declare function createLaneAccountsHandlers(deps: LaneAccountsHandlerDeps
|
|
|
17
17
|
host?: string;
|
|
18
18
|
company?: string;
|
|
19
19
|
}) => Promise<void>;
|
|
20
|
+
runAccountsClearQuota: (ctx: Ctx, providerRaw: string, accountIdRaw: string, opts: {
|
|
21
|
+
model?: string;
|
|
22
|
+
}) => Promise<void>;
|
|
20
23
|
runAccountsEnable: (ctx: Ctx, providerRaw: string, accountId: string) => Promise<void>;
|
|
21
24
|
runAccountsDisable: (ctx: Ctx, providerRaw: string, accountId: string, opts: {
|
|
22
25
|
reason?: string;
|
|
@@ -8,7 +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 { accountQuotaView, loadQuotaState } from "../lib/lanes/quota.js";
|
|
11
|
+
import { accountQuotaView, clearAccountQuota, loadQuotaState } from "../lib/lanes/quota.js";
|
|
12
12
|
import { resolveLaneModel } from "../lib/lanes/spawn.js";
|
|
13
13
|
export function createLaneAccountsHandlers(deps) {
|
|
14
14
|
const { automaticBalanceMoveHistoryReason, ensureDir, laneWithRecoveredProviderSession, loadClaudeBalanceHostSnapshots, parseRemoteJson, prepareAccountMove, printJson, reconcileHostFromCtx, resolveCallerPath } = deps;
|
|
@@ -489,6 +489,29 @@ export function createLaneAccountsHandlers(deps) {
|
|
|
489
489
|
home_source: home === account.home ? "default" : "host",
|
|
490
490
|
});
|
|
491
491
|
}
|
|
492
|
+
async function runAccountsClearQuota(ctx, providerRaw, accountIdRaw, opts) {
|
|
493
|
+
const provider = parseLaneProvider(providerRaw);
|
|
494
|
+
const account = accountIdRaw.trim();
|
|
495
|
+
if (!account) {
|
|
496
|
+
throw new AccountResolutionError("unknown_account", "account-id must be non-empty", { provider, account: accountIdRaw, enabled: [] });
|
|
497
|
+
}
|
|
498
|
+
const model = opts.model?.trim();
|
|
499
|
+
if (opts.model !== undefined && !model) {
|
|
500
|
+
throw new LanesError("invalid_model", "--model must be non-empty", {
|
|
501
|
+
provider,
|
|
502
|
+
account,
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
const cleared = clearAccountQuota(ctx.hqRoot(), provider, account, model === undefined ? {} : { model });
|
|
506
|
+
printJson({
|
|
507
|
+
ok: true,
|
|
508
|
+
action: "accounts.clear-quota",
|
|
509
|
+
provider,
|
|
510
|
+
account,
|
|
511
|
+
all_models_quota_reset_at: cleared.all_models_quota_reset_at,
|
|
512
|
+
model_quota_resets: cleared.model_quota_resets,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
492
515
|
async function runAccountsEnable(ctx, providerRaw, accountId) {
|
|
493
516
|
const hqRoot = ctx.hqRoot();
|
|
494
517
|
const provider = parseLaneProvider(providerRaw);
|
|
@@ -562,6 +585,7 @@ export function createLaneAccountsHandlers(deps) {
|
|
|
562
585
|
runAccountsBalance,
|
|
563
586
|
runAccountsLogin,
|
|
564
587
|
runAccountsSetHome,
|
|
588
|
+
runAccountsClearQuota,
|
|
565
589
|
runAccountsEnable,
|
|
566
590
|
runAccountsDisable
|
|
567
591
|
};
|
package/dist/commands/lanes.js
CHANGED
|
@@ -8453,6 +8453,16 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8453
8453
|
.action(wrap(async (provider, accountId, opts) => {
|
|
8454
8454
|
await accountHandlers.runAccountsLogin(ctx, provider, accountId, opts);
|
|
8455
8455
|
}));
|
|
8456
|
+
accounts
|
|
8457
|
+
.command("clear-quota")
|
|
8458
|
+
.description("Clear quota reset marks for a provider account")
|
|
8459
|
+
.argument("<provider>", "claude | codex | grok")
|
|
8460
|
+
.argument("<account-id>", "Account id")
|
|
8461
|
+
.option("--model <model>", "Clear only this model's reset")
|
|
8462
|
+
.option("--json", "JSON on stdout (always)")
|
|
8463
|
+
.action(wrap(async (provider, accountId, opts) => {
|
|
8464
|
+
await accountHandlers.runAccountsClearQuota(ctx, provider, accountId, opts);
|
|
8465
|
+
}));
|
|
8456
8466
|
accounts
|
|
8457
8467
|
.command("enable")
|
|
8458
8468
|
.description("Enable an account without touching credentials")
|
package/dist/lib/bot/prompt.d.ts
CHANGED
|
@@ -47,6 +47,8 @@ export interface WorkerPromptSource {
|
|
|
47
47
|
contextPaths: string[];
|
|
48
48
|
/** companies/<co>/policies/*.md, hqRoot-relative. */
|
|
49
49
|
policyPaths: string[];
|
|
50
|
+
/** Invalid policy files excluded from policyPaths, with a safe diagnostic. */
|
|
51
|
+
policyDiagnostics?: string[];
|
|
50
52
|
}
|
|
51
53
|
export interface PromptSources {
|
|
52
54
|
workerYaml?: string;
|
package/dist/lib/bot/prompt.js
CHANGED
|
@@ -20,6 +20,7 @@ import * as fs from "node:fs";
|
|
|
20
20
|
import * as path from "node:path";
|
|
21
21
|
import * as yaml from "js-yaml";
|
|
22
22
|
import { packageRoot } from "../../utils/hq-roots.js";
|
|
23
|
+
import { policyLoadDiagnostic } from "../policy-validator.js";
|
|
23
24
|
export const MEMORY_FILE_BUDGET_BYTES = 64 * 1024;
|
|
24
25
|
export const MEMORY_TOTAL_BUDGET_BYTES = 192 * 1024;
|
|
25
26
|
export const CONTEXT_FILE_BUDGET_BYTES = 32 * 1024;
|
|
@@ -117,7 +118,7 @@ function resolveSkillFile(hqRoot, workerDir, name, doc, io) {
|
|
|
117
118
|
return undefined;
|
|
118
119
|
}
|
|
119
120
|
function readWorkerSource(hqRoot, workerDir, workerYaml, io, companyOverride) {
|
|
120
|
-
const out = { skills: [], contextFiles: [], contextPaths: [], policyPaths: [] };
|
|
121
|
+
const out = { skills: [], contextFiles: [], contextPaths: [], policyPaths: [], policyDiagnostics: [] };
|
|
121
122
|
let doc = {};
|
|
122
123
|
if (workerYaml) {
|
|
123
124
|
try {
|
|
@@ -197,9 +198,28 @@ function readWorkerSource(hqRoot, workerDir, workerYaml, io, companyOverride) {
|
|
|
197
198
|
catch {
|
|
198
199
|
names = [];
|
|
199
200
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
for (const name of names) {
|
|
202
|
+
if (!name.endsWith(".md") || name.startsWith(".") || name.includes(".conflict-"))
|
|
203
|
+
continue;
|
|
204
|
+
const relative = path.join(policiesRel, name);
|
|
205
|
+
const absolute = insideHqRoot(hqRoot, relative);
|
|
206
|
+
if (!absolute)
|
|
207
|
+
continue;
|
|
208
|
+
let content;
|
|
209
|
+
try {
|
|
210
|
+
content = io.readFileSync(absolute);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
out.policyPaths.push(relative);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const diagnostic = policyLoadDiagnostic(content, absolute);
|
|
217
|
+
if (diagnostic) {
|
|
218
|
+
out.policyDiagnostics.push(`${relative}: ${diagnostic.split("\n", 1)[0]}`);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
out.policyPaths.push(relative);
|
|
222
|
+
}
|
|
203
223
|
}
|
|
204
224
|
}
|
|
205
225
|
return out;
|
|
@@ -331,6 +351,11 @@ export function buildSystemPrompt(input) {
|
|
|
331
351
|
`Hard rules for \`${company}\`. Read the relevant ones before acting; they override anything else in this prompt.\n\n` +
|
|
332
352
|
worker.policyPaths.map((p) => `- ${p}`).join("\n"));
|
|
333
353
|
}
|
|
354
|
+
if (worker.policyDiagnostics && worker.policyDiagnostics.length > 0) {
|
|
355
|
+
parts.push(`## Policy load diagnostics\n\n` +
|
|
356
|
+
`These company policy files were not loaded because their frontmatter is invalid. Correct each file before relying on its rules.\n\n` +
|
|
357
|
+
worker.policyDiagnostics.map((diagnostic) => `- ${diagnostic}`).join("\n"));
|
|
358
|
+
}
|
|
334
359
|
if (worker.contextFiles.length > 0 || worker.contextPaths.length > 0) {
|
|
335
360
|
const blocks = worker.contextFiles.map((c) => `### ${c.path}\n\n${c.body.trim()}`);
|
|
336
361
|
const more = worker.contextPaths.length > 0
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
* still runs a service, the daemon leaves that service alone instead of
|
|
4
4
|
* starting a second copy, and `hq daemon migrate` lists it.
|
|
5
5
|
*
|
|
6
|
-
* Detection
|
|
7
|
-
*
|
|
6
|
+
* Detection reads unit files and checks for an orphaned loaded macOS mesh job
|
|
7
|
+
* whose plist was removed. Windows scheduled tasks are probed through
|
|
8
|
+
* schtasks. Nothing here stops or removes a unit.
|
|
8
9
|
*/
|
|
9
10
|
import type { ServiceName } from "./catalog.js";
|
|
10
11
|
export interface LegacyUnit {
|
|
@@ -22,6 +23,8 @@ export interface LegacyProbeDeps {
|
|
|
22
23
|
home: string;
|
|
23
24
|
existsSync: (p: string) => boolean;
|
|
24
25
|
listDir: (dir: string) => string[];
|
|
26
|
+
/** True when launchctl can print the GUI-domain job despite a missing plist. */
|
|
27
|
+
launchdJobLoaded: (label: string) => boolean;
|
|
25
28
|
windowsTaskExists: (taskName: string) => boolean;
|
|
26
29
|
}
|
|
27
30
|
export declare function describeLegacyUnit(unit: LegacyUnit): string;
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
* still runs a service, the daemon leaves that service alone instead of
|
|
4
4
|
* starting a second copy, and `hq daemon migrate` lists it.
|
|
5
5
|
*
|
|
6
|
-
* Detection
|
|
7
|
-
*
|
|
6
|
+
* Detection reads unit files and checks for an orphaned loaded macOS mesh job
|
|
7
|
+
* whose plist was removed. Windows scheduled tasks are probed through
|
|
8
|
+
* schtasks. Nothing here stops or removes a unit.
|
|
8
9
|
*/
|
|
9
10
|
import * as path from "node:path";
|
|
10
11
|
const KIT_SERVICES = ["sync", "inbox", "heartbeat"];
|
|
@@ -52,8 +53,9 @@ function darwinUnits(deps) {
|
|
|
52
53
|
const units = [];
|
|
53
54
|
const at = (label) => path.join(dir, `${label}.plist`);
|
|
54
55
|
const add = (unitId, service, label, migratable = true) => units.push({ unitId, service, kind: "launchd", name: label, path: at(label), migratable });
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
const meshLabel = "ai.getindigo.hq-mesh-daemon";
|
|
57
|
+
if (deps.existsSync(at(meshLabel)) || deps.launchdJobLoaded(meshLabel))
|
|
58
|
+
add("mesh", "mesh", meshLabel);
|
|
57
59
|
for (const file of safeList(deps, dir).sort()) {
|
|
58
60
|
const m = /^ai\.getindigo\.hq-bot\.(.+)\.plist$/.exec(file);
|
|
59
61
|
if (m)
|
|
@@ -17,11 +17,13 @@
|
|
|
17
17
|
* folder, so nobody but root decides what gets installed there.
|
|
18
18
|
*/
|
|
19
19
|
import type { RunCommand } from "../service-manager/launchd.js";
|
|
20
|
-
import type
|
|
20
|
+
import { type LegacyUnit } from "./legacy.js";
|
|
21
21
|
export interface AdoptedUnit extends LegacyUnit {
|
|
22
22
|
/** Where the unit's definition (plist, unit file, task XML) was moved. */
|
|
23
23
|
movedTo: string;
|
|
24
24
|
adoptedAt: number;
|
|
25
|
+
/** Launchd job was loaded, but its source plist no longer existed. */
|
|
26
|
+
definitionMissing?: true;
|
|
25
27
|
}
|
|
26
28
|
export interface MigrateDeps {
|
|
27
29
|
run: RunCommand;
|
|
@@ -64,5 +66,12 @@ export interface RestoreResult {
|
|
|
64
66
|
}
|
|
65
67
|
export declare function readAdoptedUnits(recordPath: string): AdoptedUnit[];
|
|
66
68
|
export declare function adoptLegacyUnits(units: readonly LegacyUnit[], deps: MigrateDeps): AdoptResult;
|
|
69
|
+
/** Retire the migratable legacy mesh job and emit the daemon's operator-facing result lines. */
|
|
70
|
+
export declare function retireLegacyMeshUnits(units: readonly LegacyUnit[], options: {
|
|
71
|
+
platform: NodeJS.Platform;
|
|
72
|
+
managed: boolean;
|
|
73
|
+
hosted: boolean;
|
|
74
|
+
meshEnabled: boolean;
|
|
75
|
+
}, deps: MigrateDeps, log: (level: "info" | "warn", message: string) => void): AdoptResult;
|
|
67
76
|
export declare function restoreLegacyUnits(deps: MigrateDeps): RestoreResult;
|
|
68
77
|
//# sourceMappingURL=migrate.d.ts.map
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import * as fs from "node:fs";
|
|
20
20
|
import * as path from "node:path";
|
|
21
|
+
import { legacyMeshUnitsToRetire } from "./legacy.js";
|
|
21
22
|
import { ensurePrivateDir } from "./paths.js";
|
|
22
23
|
export function readAdoptedUnits(recordPath) {
|
|
23
24
|
let text;
|
|
@@ -99,8 +100,10 @@ function adoptOne(unit, deps) {
|
|
|
99
100
|
if (bootout.status !== 0 && deps.run("launchctl", ["print", target]).status === 0) {
|
|
100
101
|
must(deps.run, "launchctl", ["bootout", target]);
|
|
101
102
|
}
|
|
103
|
+
if (!fs.existsSync(unit.path))
|
|
104
|
+
return { movedTo: unit.path, definitionMissing: true };
|
|
102
105
|
moveFile(unit.path, to);
|
|
103
|
-
return to;
|
|
106
|
+
return { movedTo: to };
|
|
104
107
|
}
|
|
105
108
|
case "systemd-user":
|
|
106
109
|
case "systemd-system": {
|
|
@@ -109,7 +112,7 @@ function adoptOne(unit, deps) {
|
|
|
109
112
|
must(deps.run, ...systemctl(unit, deps, ["disable", "--now", unit.name]));
|
|
110
113
|
moveFile(unit.path, to);
|
|
111
114
|
deps.run(...systemctl(unit, deps, ["daemon-reload"]));
|
|
112
|
-
return to;
|
|
115
|
+
return { movedTo: to };
|
|
113
116
|
}
|
|
114
117
|
case "schtasks": {
|
|
115
118
|
const xml = must(deps.run, "schtasks.exe", ["/Query", "/TN", unit.name, "/XML"]);
|
|
@@ -117,7 +120,7 @@ function adoptOne(unit, deps) {
|
|
|
117
120
|
fs.writeFileSync(to, encodeTaskXml(xml), { mode: 0o600 });
|
|
118
121
|
deps.run("schtasks.exe", ["/End", "/TN", unit.name]);
|
|
119
122
|
must(deps.run, "schtasks.exe", ["/Delete", "/TN", unit.name, "/F"]);
|
|
120
|
-
return to;
|
|
123
|
+
return { movedTo: to };
|
|
121
124
|
}
|
|
122
125
|
}
|
|
123
126
|
}
|
|
@@ -141,15 +144,15 @@ export function adoptLegacyUnits(units, deps) {
|
|
|
141
144
|
continue;
|
|
142
145
|
}
|
|
143
146
|
const store = storeFor(unit.kind, deps);
|
|
144
|
-
let
|
|
147
|
+
let adoptedFields;
|
|
145
148
|
try {
|
|
146
|
-
|
|
149
|
+
adoptedFields = adoptOne(unit, deps);
|
|
147
150
|
}
|
|
148
151
|
catch (err) {
|
|
149
152
|
result.failed.push({ unit, error: err.message });
|
|
150
153
|
continue;
|
|
151
154
|
}
|
|
152
|
-
const adopted = { ...unit,
|
|
155
|
+
const adopted = { ...unit, ...adoptedFields, adoptedAt: now() };
|
|
153
156
|
const next = [...recordOf(store), adopted];
|
|
154
157
|
try {
|
|
155
158
|
// Record each unit as soon as it is moved, so a failure later in the
|
|
@@ -165,7 +168,7 @@ export function adoptLegacyUnits(units, deps) {
|
|
|
165
168
|
restoreOne(adopted, deps);
|
|
166
169
|
}
|
|
167
170
|
catch (restoreErr) {
|
|
168
|
-
rollback = `; putting it back also failed (${restoreErr.message}), its definition is at ${movedTo}`;
|
|
171
|
+
rollback = `; putting it back also failed (${restoreErr.message}), its definition is at ${adopted.movedTo}`;
|
|
169
172
|
}
|
|
170
173
|
result.failed.push({ unit, error: `could not record the takeover: ${err.message}${rollback}` });
|
|
171
174
|
}
|
|
@@ -192,6 +195,8 @@ function restoreOne(unit, deps) {
|
|
|
192
195
|
case "launchd": {
|
|
193
196
|
if (!unit.path)
|
|
194
197
|
throw new Error(`no plist path recorded for ${unit.name}`);
|
|
198
|
+
if (unit.definitionMissing)
|
|
199
|
+
throw new Error("the original LaunchAgent plist is missing; this unit cannot be restored");
|
|
195
200
|
moveBack(unit, unit.path);
|
|
196
201
|
must(deps.run, "launchctl", ["bootstrap", `gui/${deps.uid}`, unit.path]);
|
|
197
202
|
return;
|
|
@@ -216,6 +221,16 @@ function restoreOne(unit, deps) {
|
|
|
216
221
|
return;
|
|
217
222
|
}
|
|
218
223
|
}
|
|
224
|
+
/** Retire the migratable legacy mesh job and emit the daemon's operator-facing result lines. */
|
|
225
|
+
export function retireLegacyMeshUnits(units, options, deps, log) {
|
|
226
|
+
const selected = legacyMeshUnitsToRetire(units, options);
|
|
227
|
+
const result = adoptLegacyUnits(selected, deps);
|
|
228
|
+
for (const unit of result.adopted)
|
|
229
|
+
log("info", `retired legacy ${unit.unitId} unit ${unit.name}; hq daemon now owns mesh`);
|
|
230
|
+
for (const failure of result.failed)
|
|
231
|
+
log("warn", `could not retire legacy ${failure.unit.unitId} unit ${failure.unit.name}: ${failure.error}`);
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
219
234
|
function restoreFrom(record, deps, check, result) {
|
|
220
235
|
const units = readAdoptedUnits(record);
|
|
221
236
|
if (units.length === 0)
|