@indigoai-us/hq-cli 5.345.202 → 5.345.204
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 +18 -0
- package/dist/command-catalog.generated.d.ts +24 -0
- package/dist/command-catalog.generated.js +26 -0
- package/dist/commands/bot.js +7 -2
- package/dist/commands/capacity.js +10 -2
- package/dist/commands/cloud.d.ts +14 -2
- package/dist/commands/cloud.js +25 -16
- package/dist/commands/daemon.d.ts +2 -1
- package/dist/commands/daemon.js +225 -4
- package/dist/commands/integrations-core.d.ts +3 -0
- package/dist/commands/integrations-core.js +11 -0
- package/dist/commands/integrations-manage.js +2 -2
- package/dist/commands/integrations.js +5 -5
- package/dist/commands/sync-core.js +4 -1
- package/dist/lib/bot/claude-login.d.ts +7 -1
- package/dist/lib/bot/claude-login.js +56 -16
- package/dist/lib/bot/executive-settings.d.ts +1 -1
- package/dist/lib/bot/executive-settings.js +8 -6
- package/dist/lib/bot/jobs/meeting-detect.js +2 -2
- package/dist/lib/bot/jobs/meeting-rider.js +2 -2
- package/dist/lib/bot/run.js +3 -3
- package/dist/lib/capacity/codex-availability.d.ts +6 -0
- package/dist/lib/capacity/codex-availability.js +33 -0
- package/dist/lib/capacity/collectors/claude-oauth.d.ts +53 -5
- package/dist/lib/capacity/collectors/claude-oauth.js +251 -71
- package/dist/lib/capacity/ledger.d.ts +0 -11
- package/dist/lib/capacity/ledger.js +30 -3
- package/dist/lib/capacity/route.js +11 -5
- package/dist/lib/daemon/catalog.d.ts +3 -0
- package/dist/lib/daemon/catalog.js +17 -4
- package/dist/lib/daemon/handoff.d.ts +2 -0
- package/dist/lib/daemon/handoff.js +9 -2
- package/dist/lib/daemon/install.d.ts +99 -0
- package/dist/lib/daemon/install.js +316 -7
- package/dist/lib/daemon/migrate.d.ts +3 -1
- package/dist/lib/daemon/migrate.js +14 -2
- package/dist/lib/daemon/plist-refresh.d.ts +34 -0
- package/dist/lib/daemon/plist-refresh.js +62 -0
- package/dist/lib/daemon/run.js +27 -2
- package/dist/lib/daemon/selfcheck.d.ts +71 -0
- package/dist/lib/daemon/selfcheck.js +150 -0
- package/dist/lib/daemon/supervisor.d.ts +31 -0
- package/dist/lib/daemon/supervisor.js +130 -6
- package/dist/lib/daemon/unit-holds.js +2 -1
- package/dist/lib/lanes/config.js +13 -0
- package/dist/lib/lanes/throttle.d.ts +12 -1
- package/dist/lib/lanes/throttle.js +27 -8
- package/dist/lib/lanes/types.d.ts +5 -0
- package/dist/lib/service-manager/launchd.d.ts +25 -0
- package/dist/lib/service-manager/launchd.js +51 -3
- package/dist/lib/service-manager/migration.d.ts +14 -0
- package/dist/lib/service-manager/migration.js +34 -0
- package/dist/lib/service-manager/systemd.d.ts +13 -0
- package/dist/lib/service-manager/systemd.js +19 -0
- package/dist/lib/service-manager/types.d.ts +8 -0
- package/dist/lib/signals/steps/extract.d.ts +110 -4
- package/dist/lib/signals/steps/extract.js +520 -39
- package/dist/lib/sync-core/engine-home.d.ts +2 -1
- package/dist/lib/sync-core/engine-home.js +25 -8
- package/dist/lib/sync-core/reconcile.d.ts +7 -2
- package/dist/lib/sync-core/reconcile.js +32 -9
- package/dist/lib/sync-core/release.d.ts +35 -3
- package/dist/lib/sync-core/release.js +95 -12
- package/package.json +7 -7
|
@@ -34,6 +34,23 @@ export const MEMORY_PRESSURE_SYSCTLS = [
|
|
|
34
34
|
"kern.memorystatus_vm_pressure_level",
|
|
35
35
|
"hw.memsize",
|
|
36
36
|
];
|
|
37
|
+
/**
|
|
38
|
+
* Default for throttle.memory_warn_min_free_percent. On darwin a warn pressure
|
|
39
|
+
* level holds admission only while the free percentage is at or below this.
|
|
40
|
+
*/
|
|
41
|
+
export const DEFAULT_MEMORY_WARN_MIN_FREE_PERCENT = 20;
|
|
42
|
+
/**
|
|
43
|
+
* Whether a darwin memory_pressure level holds admission on its own. Critical
|
|
44
|
+
* always holds. Warn holds only at or below warnMinFreePercent free, or when
|
|
45
|
+
* the free percentage is unknown.
|
|
46
|
+
*/
|
|
47
|
+
export function pressureLevelHolds(signal, warnMinFreePercent = DEFAULT_MEMORY_WARN_MIN_FREE_PERCENT) {
|
|
48
|
+
if (signal.pressure_level === "critical")
|
|
49
|
+
return true;
|
|
50
|
+
if (signal.pressure_level !== "warn")
|
|
51
|
+
return false;
|
|
52
|
+
return signal.free_percent === undefined || signal.free_percent <= warnMinFreePercent;
|
|
53
|
+
}
|
|
37
54
|
export const LANES_CPU_BUSY_OVERRIDE = "HQ_LANES_CPU_BUSY_OVERRIDE";
|
|
38
55
|
export const LANES_CPU_CHECK = "HQ_LANES_CPU_CHECK";
|
|
39
56
|
function defaultSleep(ms) {
|
|
@@ -340,11 +357,11 @@ function formatMb(mb) {
|
|
|
340
357
|
return `${Math.round(mb)} MB`;
|
|
341
358
|
}
|
|
342
359
|
/** Plain explanation of a memory hold: which signal, its value, and the threshold. */
|
|
343
|
-
export function formatMemoryHoldMessage(signal, thresholdMb) {
|
|
360
|
+
export function formatMemoryHoldMessage(signal, thresholdMb, warnMinFreePercent = DEFAULT_MEMORY_WARN_MIN_FREE_PERCENT) {
|
|
344
361
|
if (signal.source === "memory_pressure") {
|
|
345
362
|
const reading = `memory_pressure reports ${signal.free_percent}% free (about ${formatMb(signal.available_mb)}), pressure level ${signal.pressure_level}`;
|
|
346
|
-
if (signal
|
|
347
|
-
return `Holding new lanes: ${reading}. Lanes wait while the level is warn or
|
|
363
|
+
if (pressureLevelHolds(signal, warnMinFreePercent)) {
|
|
364
|
+
return `Holding new lanes: ${reading}. Lanes wait while the level is critical, or warn with ${warnMinFreePercent}% free or less (throttle.memory_warn_min_free_percent; throttle.min_available_mem_mb is ${thresholdMb}).`;
|
|
348
365
|
}
|
|
349
366
|
return `Holding new lanes: ${reading}, below throttle.min_available_mem_mb ${thresholdMb}.`;
|
|
350
367
|
}
|
|
@@ -379,6 +396,8 @@ export function evaluateThrottle(input) {
|
|
|
379
396
|
const pause = input.throttle.cpu_busy_pause;
|
|
380
397
|
const resume = input.throttle.cpu_busy_resume;
|
|
381
398
|
const minMem = input.throttle.min_available_mem_mb;
|
|
399
|
+
const warnMinFree = input.throttle.memory_warn_min_free_percent ??
|
|
400
|
+
DEFAULT_MEMORY_WARN_MIN_FREE_PERCENT;
|
|
382
401
|
// Each gate is opt-in: an unset threshold never blocks admission.
|
|
383
402
|
let cpuBlocks = false;
|
|
384
403
|
if (pause !== undefined && input.cpuCheckEnabled && input.busy !== null) {
|
|
@@ -389,8 +408,9 @@ export function evaluateThrottle(input) {
|
|
|
389
408
|
cpuBlocks = input.busy >= pause;
|
|
390
409
|
}
|
|
391
410
|
}
|
|
392
|
-
// On darwin the signal is memory_pressure's: a
|
|
393
|
-
//
|
|
411
|
+
// On darwin the signal is memory_pressure's: a critical level holds even
|
|
412
|
+
// above the threshold, a warn level holds only at or below
|
|
413
|
+
// memory_warn_min_free_percent free, and otherwise only below the threshold.
|
|
394
414
|
let memBlocks = false;
|
|
395
415
|
let memorySignal;
|
|
396
416
|
if (minMem !== undefined && input.availableMemMb !== null) {
|
|
@@ -398,8 +418,7 @@ export function evaluateThrottle(input) {
|
|
|
398
418
|
? input.memory
|
|
399
419
|
: { source: "meminfo", available_mb: input.availableMemMb };
|
|
400
420
|
memBlocks = input.availableMemMb < minMem ||
|
|
401
|
-
memorySignal
|
|
402
|
-
memorySignal.pressure_level === "critical";
|
|
421
|
+
pressureLevelHolds(memorySignal, warnMinFree);
|
|
403
422
|
}
|
|
404
423
|
if (!cpuBlocks && !memBlocks) {
|
|
405
424
|
return {
|
|
@@ -420,7 +439,7 @@ export function evaluateThrottle(input) {
|
|
|
420
439
|
memory_hold: {
|
|
421
440
|
...memorySignal,
|
|
422
441
|
threshold_mb: minMem,
|
|
423
|
-
message: formatMemoryHoldMessage(memorySignal, minMem),
|
|
442
|
+
message: formatMemoryHoldMessage(memorySignal, minMem, warnMinFree),
|
|
424
443
|
},
|
|
425
444
|
};
|
|
426
445
|
}
|
|
@@ -1226,6 +1226,11 @@ export interface ThrottleConfig {
|
|
|
1226
1226
|
cpu_busy_pause?: number;
|
|
1227
1227
|
cpu_busy_resume?: number;
|
|
1228
1228
|
min_available_mem_mb?: number;
|
|
1229
|
+
/**
|
|
1230
|
+
* darwin only: a warn memory_pressure level holds admission only at or
|
|
1231
|
+
* below this free percentage (0-100). Unset means 20. Critical always holds.
|
|
1232
|
+
*/
|
|
1233
|
+
memory_warn_min_free_percent?: number;
|
|
1229
1234
|
}
|
|
1230
1235
|
/** Session liveness windows from spec §9. Missing keys keep these documented defaults. */
|
|
1231
1236
|
export interface LivenessWindows {
|
|
@@ -114,6 +114,31 @@ export declare function activateLaunchd(plistPath: string, run: RunCommand, uid?
|
|
|
114
114
|
sleep?: SleepFn;
|
|
115
115
|
unloadDeadlineMs?: number;
|
|
116
116
|
}): void;
|
|
117
|
+
export interface LaunchdBootstrapResult {
|
|
118
|
+
ok: boolean;
|
|
119
|
+
attempts: number;
|
|
120
|
+
error?: string;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Bootstrap a plist that is not loaded, retrying launchd's transient EIO with
|
|
124
|
+
* backoff. A label that is already loaded counts as success. Unlike
|
|
125
|
+
* reloadLaunchd this never boots anything out, so it is safe to call on a
|
|
126
|
+
* running job.
|
|
127
|
+
*/
|
|
128
|
+
export declare function bootstrapLaunchd(plistPath: string, run: RunCommand, uid?: number, opts?: {
|
|
129
|
+
sleep?: SleepFn;
|
|
130
|
+
}): LaunchdBootstrapResult;
|
|
131
|
+
export interface LaunchdJobInfo {
|
|
132
|
+
loaded: boolean;
|
|
133
|
+
/** launchd's `state = ...` line (running, not running, spawn scheduled, ...). */
|
|
134
|
+
state?: string;
|
|
135
|
+
pid?: number;
|
|
136
|
+
/** The loaded job's program arguments (`arguments = { ... }`), when printed. */
|
|
137
|
+
arguments?: string[];
|
|
138
|
+
}
|
|
139
|
+
/** What `launchctl print gui/<uid>/<label>` says about one job. */
|
|
140
|
+
export declare function launchdJobInfo(label: string, run: RunCommand, uid?: number): LaunchdJobInfo;
|
|
141
|
+
export declare function parseLaunchctlPrintJob(stdout: string): LaunchdJobInfo;
|
|
117
142
|
export declare function deactivateLaunchd(plistPath: string, run: RunCommand, uid?: number): void;
|
|
118
143
|
export declare function launchdIsLoaded(label: string, run: RunCommand, uid?: number): boolean;
|
|
119
144
|
//# sourceMappingURL=launchd.d.ts.map
|
|
@@ -58,9 +58,11 @@ export function launchdPlistPath(home, spec) {
|
|
|
58
58
|
return path.join(launchAgentsDir(home), `${spec.label}.plist`);
|
|
59
59
|
}
|
|
60
60
|
export function renderLaunchdPlist(spec, host) {
|
|
61
|
-
const discovered = discoverLaunchdIdentity(host.home);
|
|
62
|
-
const associatedBundleIdentifier =
|
|
63
|
-
|
|
61
|
+
const discovered = spec.independentOfApp ? {} : discoverLaunchdIdentity(host.home);
|
|
62
|
+
const associatedBundleIdentifier = spec.independentOfApp
|
|
63
|
+
? undefined
|
|
64
|
+
: host.associatedBundleIdentifier ?? discovered.associatedBundleIdentifier;
|
|
65
|
+
const launcherBinary = spec.independentOfApp ? undefined : host.launchdLauncherBinary ?? discovered.launcherBinary;
|
|
64
66
|
const args = launchdProgramArguments({ ...host, launchdLauncherBinary: launcherBinary }, spec.args)
|
|
65
67
|
.map((a) => ` <string>${escapeXml(a)}</string>`)
|
|
66
68
|
.join("\n");
|
|
@@ -303,6 +305,52 @@ export function activateLaunchd(plistPath, run, uid, opts) {
|
|
|
303
305
|
}
|
|
304
306
|
throw new Error(`${detail}. ${target} is NOT loaded and will not run until it is. Load it with: launchctl bootstrap ${domain} ${plistPath}`);
|
|
305
307
|
}
|
|
308
|
+
/**
|
|
309
|
+
* Bootstrap a plist that is not loaded, retrying launchd's transient EIO with
|
|
310
|
+
* backoff. A label that is already loaded counts as success. Unlike
|
|
311
|
+
* reloadLaunchd this never boots anything out, so it is safe to call on a
|
|
312
|
+
* running job.
|
|
313
|
+
*/
|
|
314
|
+
export function bootstrapLaunchd(plistPath, run, uid, opts) {
|
|
315
|
+
const domain = guiDomain(uid);
|
|
316
|
+
const target = launchdServiceTarget(plistPath, uid);
|
|
317
|
+
const sleep = opts?.sleep ?? blockingSleep;
|
|
318
|
+
if (run("launchctl", ["print", target]).status === 0)
|
|
319
|
+
return { ok: true, attempts: 0 };
|
|
320
|
+
let last = run("launchctl", ["bootstrap", domain, plistPath]);
|
|
321
|
+
let attempts = 1;
|
|
322
|
+
for (const backoff of LAUNCHD_BOOTSTRAP_BACKOFF_MS) {
|
|
323
|
+
if (last.status === 0 || !isRetryableLaunchdBootstrap(last))
|
|
324
|
+
break;
|
|
325
|
+
sleep(backoff);
|
|
326
|
+
last = run("launchctl", ["bootstrap", domain, plistPath]);
|
|
327
|
+
attempts++;
|
|
328
|
+
}
|
|
329
|
+
// "already bootstrapped" races (another caller loaded it) are success.
|
|
330
|
+
if (last.status === 0 || run("launchctl", ["print", target]).status === 0)
|
|
331
|
+
return { ok: true, attempts };
|
|
332
|
+
return { ok: false, attempts, error: last.stderr.trim() || last.stdout.trim() || `exit ${last.status}` };
|
|
333
|
+
}
|
|
334
|
+
/** What `launchctl print gui/<uid>/<label>` says about one job. */
|
|
335
|
+
export function launchdJobInfo(label, run, uid) {
|
|
336
|
+
const r = run("launchctl", ["print", `${guiDomain(uid)}/${label}`]);
|
|
337
|
+
if (r.status !== 0)
|
|
338
|
+
return { loaded: false };
|
|
339
|
+
return parseLaunchctlPrintJob(r.stdout);
|
|
340
|
+
}
|
|
341
|
+
export function parseLaunchctlPrintJob(stdout) {
|
|
342
|
+
const state = /^\s*state\s*=\s*(.+?)\s*$/m.exec(stdout)?.[1];
|
|
343
|
+
const pidText = /^\s*pid\s*=\s*(\d+)\s*$/m.exec(stdout)?.[1];
|
|
344
|
+
const pid = pidText ? Number(pidText) : undefined;
|
|
345
|
+
const argsBlock = /^\s*arguments\s*=\s*\{\s*\n([\s\S]*?)\n\s*\}/m.exec(stdout)?.[1];
|
|
346
|
+
const args = argsBlock?.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
347
|
+
return {
|
|
348
|
+
loaded: true,
|
|
349
|
+
...(state ? { state } : {}),
|
|
350
|
+
...(pid && pid > 0 ? { pid } : {}),
|
|
351
|
+
...(args && args.length > 0 ? { arguments: args } : {}),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
306
354
|
export function deactivateLaunchd(plistPath, run, uid) {
|
|
307
355
|
run("launchctl", ["bootout", guiDomain(uid), plistPath]);
|
|
308
356
|
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import { type RunCommand, type SleepFn } from "./launchd.js";
|
|
3
|
+
/**
|
|
4
|
+
* Agents that must load at login without the app, so they are never wrapped
|
|
5
|
+
* in the HQ.app launcher or attributed to its bundle. Kept in step with
|
|
6
|
+
* DAEMON_LAUNCHD_LABEL in daemon/install.ts.
|
|
7
|
+
*/
|
|
8
|
+
export declare const APP_INDEPENDENT_LAUNCHD_LABELS: ReadonlySet<string>;
|
|
3
9
|
export interface LaunchdMigrationOptions {
|
|
4
10
|
home: string;
|
|
5
11
|
run: RunCommand;
|
|
@@ -19,4 +25,12 @@ export interface LaunchdMigrationResult {
|
|
|
19
25
|
}
|
|
20
26
|
/** First-CLI-run cleanup for HQ's old Node-first LaunchAgents and backup plists. */
|
|
21
27
|
export declare function migrateLegacyNodeLaunchAgents(options: LaunchdMigrationOptions): LaunchdMigrationResult;
|
|
28
|
+
/**
|
|
29
|
+
* The app-independent form of an HQ LaunchAgent plist written by an older
|
|
30
|
+
* install: the HQ.app launcher (`hq-sync-menubar --hq-launchd-task`) is
|
|
31
|
+
* dropped from ProgramArguments and the AssociatedBundleIdentifiers entry is
|
|
32
|
+
* removed. Everything else (environment, logs, KeepAlive) is kept as written.
|
|
33
|
+
* Returns null when the plist is already app-independent.
|
|
34
|
+
*/
|
|
35
|
+
export declare function appIndependentLaunchdPlist(source: string): string | null;
|
|
22
36
|
//# sourceMappingURL=migration.d.ts.map
|
|
@@ -5,6 +5,12 @@ import { isVersionedNodePath, preferStableNodeBinary } from "../node-binary.js";
|
|
|
5
5
|
import { preferStableHqBinary } from "../mesh/live/daemon/install.js";
|
|
6
6
|
import { launchdMigrationMarkerPath, shouldRunLaunchdMigration, writeLaunchdMigrationMarker } from "./migration-state.js";
|
|
7
7
|
const HQ_LABEL_PREFIXES = ["ai.indigo.hq-", "ai.getindigo.hq-"];
|
|
8
|
+
/**
|
|
9
|
+
* Agents that must load at login without the app, so they are never wrapped
|
|
10
|
+
* in the HQ.app launcher or attributed to its bundle. Kept in step with
|
|
11
|
+
* DAEMON_LAUNCHD_LABEL in daemon/install.ts.
|
|
12
|
+
*/
|
|
13
|
+
export const APP_INDEPENDENT_LAUNCHD_LABELS = new Set(["ai.getindigo.hq-daemon"]);
|
|
8
14
|
function xml(value) {
|
|
9
15
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
10
16
|
}
|
|
@@ -36,6 +42,8 @@ export function migrateLegacyNodeLaunchAgents(options) {
|
|
|
36
42
|
}
|
|
37
43
|
if (!name.endsWith(".plist") || !HQ_LABEL_PREFIXES.some((prefix) => name.startsWith(prefix)))
|
|
38
44
|
continue;
|
|
45
|
+
if (APP_INDEPENDENT_LAUNCHD_LABELS.has(name.slice(0, -".plist".length)))
|
|
46
|
+
continue;
|
|
39
47
|
const source = files.readFileSync(fullPath, "utf8");
|
|
40
48
|
const args = launchdPlistProgramArguments(source);
|
|
41
49
|
if (args.length < 2 || !isNodeProgram(args[0]))
|
|
@@ -82,4 +90,30 @@ export function migrateLegacyNodeLaunchAgents(options) {
|
|
|
82
90
|
writeLaunchdMigrationMarker(options.home, deferred);
|
|
83
91
|
return { migrated, backupsRemoved, deferred, alreadyDone: false };
|
|
84
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* The app-independent form of an HQ LaunchAgent plist written by an older
|
|
95
|
+
* install: the HQ.app launcher (`hq-sync-menubar --hq-launchd-task`) is
|
|
96
|
+
* dropped from ProgramArguments and the AssociatedBundleIdentifiers entry is
|
|
97
|
+
* removed. Everything else (environment, logs, KeepAlive) is kept as written.
|
|
98
|
+
* Returns null when the plist is already app-independent.
|
|
99
|
+
*/
|
|
100
|
+
export function appIndependentLaunchdPlist(source) {
|
|
101
|
+
const args = launchdPlistProgramArguments(source);
|
|
102
|
+
const launcherIndex = args.indexOf("--hq-launchd-task");
|
|
103
|
+
const wrapped = launcherIndex === 1 && args.length > 3;
|
|
104
|
+
const associated = /<key>AssociatedBundleIdentifiers<\/key>/.test(source);
|
|
105
|
+
if (!wrapped && !associated)
|
|
106
|
+
return null;
|
|
107
|
+
let updated = source;
|
|
108
|
+
if (wrapped) {
|
|
109
|
+
updated = updated.replace(/<key>ProgramArguments<\/key>\s*<array>[\s\S]*?<\/array>/, plistArgs(args.slice(2)).trimStart());
|
|
110
|
+
}
|
|
111
|
+
if (associated) {
|
|
112
|
+
updated = updated.replace(/[ \t]*<key>AssociatedBundleIdentifiers<\/key>\s*(?:<array>[\s\S]*?<\/array>|<string>[^<]*<\/string>)[ \t]*\n?/, "");
|
|
113
|
+
}
|
|
114
|
+
if (updated === source || /<key>AssociatedBundleIdentifiers<\/key>/.test(updated) || launchdPlistProgramArguments(updated).includes("--hq-launchd-task")) {
|
|
115
|
+
throw new Error("Could not safely rewrite the LaunchAgent to load without HQ.app");
|
|
116
|
+
}
|
|
117
|
+
return updated;
|
|
118
|
+
}
|
|
85
119
|
//# sourceMappingURL=migration.js.map
|
|
@@ -16,4 +16,17 @@ export declare function renderSystemdUserUnit(spec: ServiceSpec, host: ServiceHo
|
|
|
16
16
|
export declare function activateSystemd(unitName: string, run: RunCommand): void;
|
|
17
17
|
export declare function deactivateSystemd(unitName: string, run: RunCommand): void;
|
|
18
18
|
export declare function systemdIsActive(unitName: string, run: RunCommand): boolean;
|
|
19
|
+
export interface SystemdUnitInfo {
|
|
20
|
+
/** `systemctl is-enabled` printed enabled (or enabled-runtime / linked / alias). */
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
/** `systemctl is-active` printed active (or activating / reloading). */
|
|
23
|
+
active: boolean;
|
|
24
|
+
/** The raw `is-enabled` / `is-active` answers, for status output. */
|
|
25
|
+
enabledState: string;
|
|
26
|
+
activeState: string;
|
|
27
|
+
/** MainPID when systemd reports one. */
|
|
28
|
+
pid?: number;
|
|
29
|
+
}
|
|
30
|
+
/** Ask systemd about one unit; `scope: "user"` adds `--user`. */
|
|
31
|
+
export declare function systemdUnitInfo(unitName: string, run: RunCommand, scope?: "user" | "system"): SystemdUnitInfo;
|
|
19
32
|
//# sourceMappingURL=systemd.d.ts.map
|
|
@@ -69,4 +69,23 @@ export function systemdIsActive(unitName, run) {
|
|
|
69
69
|
const r = run("systemctl", ["--user", "is-active", "--quiet", unitName]);
|
|
70
70
|
return r.status === 0;
|
|
71
71
|
}
|
|
72
|
+
/** Ask systemd about one unit; `scope: "user"` adds `--user`. */
|
|
73
|
+
export function systemdUnitInfo(unitName, run, scope = "user") {
|
|
74
|
+
const prefix = scope === "user" ? ["--user"] : [];
|
|
75
|
+
const word = (args) => {
|
|
76
|
+
const r = run("systemctl", [...prefix, ...args, unitName]);
|
|
77
|
+
return (r.stdout.trim().split("\n")[0] ?? "").trim() || "unknown";
|
|
78
|
+
};
|
|
79
|
+
const enabledState = word(["is-enabled"]);
|
|
80
|
+
const activeState = word(["is-active"]);
|
|
81
|
+
const show = run("systemctl", [...prefix, "show", "-p", "MainPID", "--value", unitName]);
|
|
82
|
+
const pid = Number(show.stdout.trim());
|
|
83
|
+
return {
|
|
84
|
+
enabled: /^(enabled|enabled-runtime|linked|linked-runtime|alias|static|indirect|generated)$/.test(enabledState),
|
|
85
|
+
active: /^(active|activating|reloading)$/.test(activeState),
|
|
86
|
+
enabledState,
|
|
87
|
+
activeState,
|
|
88
|
+
...(show.status === 0 && Number.isInteger(pid) && pid > 0 ? { pid } : {}),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
72
91
|
//# sourceMappingURL=systemd.js.map
|
|
@@ -21,6 +21,14 @@ export interface ServiceSpec {
|
|
|
21
21
|
restartSec: number;
|
|
22
22
|
/** systemd unit file name; defaults to `hq-agent-<name>.service`. */
|
|
23
23
|
unitName?: string;
|
|
24
|
+
/**
|
|
25
|
+
* macOS: run the job directly under node, without the HQ.app launcher and
|
|
26
|
+
* without AssociatedBundleIdentifiers, so launchd loads it at login whether
|
|
27
|
+
* or not the app is installed, enabled or allowed. The daemon sets this:
|
|
28
|
+
* on 2026-11-03 launchd skipped both app-associated agents at login and the
|
|
29
|
+
* daemon stayed down for 16 hours.
|
|
30
|
+
*/
|
|
31
|
+
independentOfApp?: boolean;
|
|
24
32
|
}
|
|
25
33
|
export interface ServiceHostPaths {
|
|
26
34
|
home: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type StoredPriorSignal } from "../prior-signals.js";
|
|
2
2
|
import { type BotRuntimeId } from "../../bot/config.js";
|
|
3
|
-
import type { SignalsStep, StepContext, StepResult } from "./types.js";
|
|
3
|
+
import type { SignalsStep, SpawnSyncFn, StepContext, StepResult } from "./types.js";
|
|
4
4
|
export declare const EXTRACT_SIGNAL_TYPES: readonly ["action_item", "commitment", "decision", "risk", "question", "key_point", "participant_contribution", "summary"];
|
|
5
5
|
export declare const DEFAULT_WORKLIST_LIMIT = 20;
|
|
6
6
|
export declare const DEFAULT_TURN_TIMEOUT_MS: number;
|
|
@@ -13,6 +13,20 @@ export declare const WORKLIST_ITEM_TIMEOUT_MS = 10000;
|
|
|
13
13
|
export declare function worklistTimeoutMs(limit: number): number;
|
|
14
14
|
export declare const CODEX_PERMISSION_PROFILE = "hq_signals_extract";
|
|
15
15
|
export declare const CANDIDATE_WRAPPER_NAME = "write-candidate.sh";
|
|
16
|
+
/** The sandbox profile the step writes to <turn dir>/.grok/sandbox.toml for a grok turn. */
|
|
17
|
+
export declare const GROK_SANDBOX_PROFILE = "hq_signals_extract";
|
|
18
|
+
export declare const GROK_SANDBOX_FILE: string;
|
|
19
|
+
/** The oldest grok whose `--help` lists every flag in GROK_EXTRACT_FLAGS. */
|
|
20
|
+
export declare const GROK_MIN_VERSION = "1.0.70";
|
|
21
|
+
/** Flags a grok turn needs; one missing from `--help` means the installed grok cannot be locked down. */
|
|
22
|
+
export declare const GROK_EXTRACT_FLAGS: readonly ["--permission-mode", "--tools", "--disallowed-tools", "--allow", "--deny", "--sandbox", "--disable-web-search", "--no-subagents", "--max-turns", "--cwd", "--output-format"];
|
|
23
|
+
/**
|
|
24
|
+
* grok's built-in read-only shell list (its permissions guide, "Read-Only
|
|
25
|
+
* Shell Commands"): auto-approved even under dontAsk, so each is denied by
|
|
26
|
+
* name. A Bash rule without `*` is a prefix match, so `Bash(cat)` also covers
|
|
27
|
+
* `cat` with any arguments.
|
|
28
|
+
*/
|
|
29
|
+
export declare const GROK_READ_ONLY_COMMANDS: readonly ["ls", "cat", "pwd", "date", "whoami", "hostname", "uptime", "ps", "head", "tail", "wc", "sort", "uniq", "tr", "cut", "mkdir", "touch", "git", "grep", "rg", "kubectl"];
|
|
16
30
|
/** The only tools a claude turn may use: Read inside the channel folder and the pinned wrapper. */
|
|
17
31
|
export declare function claudeAllowedTools(channelDir: string, wrapperPath: string): string[];
|
|
18
32
|
export interface PriorSignal {
|
|
@@ -36,10 +50,13 @@ export interface ExtractStepOptions {
|
|
|
36
50
|
resolveBotRuntime?: () => BotRuntimeId | null;
|
|
37
51
|
claudeBin?: string;
|
|
38
52
|
codexBin?: string;
|
|
53
|
+
grokBin?: string;
|
|
39
54
|
worklistLimit?: number;
|
|
40
55
|
turnTimeoutMs?: number;
|
|
41
56
|
/** Run channels whose source.yaml says run: cloud too (passes --force). */
|
|
42
57
|
force?: boolean;
|
|
58
|
+
/** OS used to decide whether grok's child-process network block exists. Default: this process. */
|
|
59
|
+
platform?: NodeJS.Platform;
|
|
43
60
|
}
|
|
44
61
|
interface SourceChannel {
|
|
45
62
|
channel: string;
|
|
@@ -77,8 +94,11 @@ export declare function buildExtractPrompt(input: {
|
|
|
77
94
|
item: WorkItem;
|
|
78
95
|
itemPath: string;
|
|
79
96
|
wrapperPath: string;
|
|
80
|
-
/**
|
|
81
|
-
|
|
97
|
+
/**
|
|
98
|
+
* codex has no Read tool; it reads with shell commands inside its sandbox.
|
|
99
|
+
* grok reads with read_file and returns candidate commands as JSON data.
|
|
100
|
+
*/
|
|
101
|
+
runtime?: "claude" | "codex" | "grok";
|
|
82
102
|
}): string;
|
|
83
103
|
/**
|
|
84
104
|
* The per-item candidate command. It pins --company, --audience and
|
|
@@ -91,7 +111,16 @@ export declare function buildCandidateWrapper(input: {
|
|
|
91
111
|
audience: string;
|
|
92
112
|
sourceRef: string;
|
|
93
113
|
}): string;
|
|
94
|
-
/**
|
|
114
|
+
/**
|
|
115
|
+
* Parse a stream-json transcript (claude), a codex --json event stream, or a
|
|
116
|
+
* grok streaming-json event stream.
|
|
117
|
+
*
|
|
118
|
+
* grok writes `{"type":"text","data":…}` deltas and one terminal
|
|
119
|
+
* `{"type":"end","stopReason":…,"num_turns":n,"total_cost_usd":x}`; a refused
|
|
120
|
+
* tool call ends the turn with stopReason "cancelled", and an API failure is
|
|
121
|
+
* `{"type":"error","message":…}`. Both are mapped to an error result so the
|
|
122
|
+
* item fails and is retried; only "end_turn" is success.
|
|
123
|
+
*/
|
|
95
124
|
export declare function parseTurnOutput(stdout: string): {
|
|
96
125
|
result: Record<string, unknown> | null;
|
|
97
126
|
turns: number;
|
|
@@ -134,9 +163,86 @@ export interface TurnScope {
|
|
|
134
163
|
wrapperPath: string;
|
|
135
164
|
/** companies/{co}/{signals,ontology}/_candidates, realpath. */
|
|
136
165
|
candidateDirs: string[];
|
|
166
|
+
/**
|
|
167
|
+
* Text of the pinned writer, when this turn generated one. A grok turn
|
|
168
|
+
* compares the file to this immediately before executing it.
|
|
169
|
+
*/
|
|
170
|
+
wrapperBody?: string;
|
|
137
171
|
}
|
|
138
172
|
/** TOML inline table for the codex permission profile's filesystem entries. */
|
|
139
173
|
export declare function codexFilesystemProfile(scope: TurnScope, codexBin: string, envPath: string | undefined): string;
|
|
174
|
+
/**
|
|
175
|
+
* The sandbox profile for a grok turn, written to <turn dir>/.grok/sandbox.toml.
|
|
176
|
+
* grok's `strict` already covers the turn dir (cwd), system paths and ~/.grok;
|
|
177
|
+
* the grants add bash's install prefix (Homebrew bash loads from it) and the
|
|
178
|
+
* read-only HQ paths of hqTurnGrants. Candidate folders are not granted:
|
|
179
|
+
* the host writes them after the turn, outside this sandbox.
|
|
180
|
+
*/
|
|
181
|
+
export declare function grokSandboxProfile(scope: TurnScope, envPath: string | undefined): string;
|
|
182
|
+
/** Grok can read the source; candidate writing belongs to the host. */
|
|
183
|
+
export declare function grokAllowRules(channelDir: string): string[];
|
|
184
|
+
/** Parse literal arguments only. No shell ever interprets model output. */
|
|
185
|
+
export declare function grokCandidateArgv(command: string, wrapperPath: string): string[] | null;
|
|
186
|
+
/**
|
|
187
|
+
* Validate the whole batch before writing. Only the host executes the pinned
|
|
188
|
+
* writer. When `expectedWrapper` is set, the file is compared to that text
|
|
189
|
+
* immediately before each exec. Strict can write the turn directory, so a
|
|
190
|
+
* writer that no longer matches is not run.
|
|
191
|
+
*/
|
|
192
|
+
export declare function executeGrokCandidates(text: string, wrapperPath: string, spawn: SpawnSyncFn, opts: Parameters<SpawnSyncFn>[2], expectedWrapper?: string): string | undefined;
|
|
193
|
+
/**
|
|
194
|
+
* Read denies for the grok login. Grok matches a `~` tool path literally and
|
|
195
|
+
* expands `~` only after that check, so the absolute temp home is not enough.
|
|
196
|
+
* `realHomes` is every home the original login might live under (the process
|
|
197
|
+
* HOME we copied from, and os.homedir() when that differs).
|
|
198
|
+
*/
|
|
199
|
+
export declare function grokAuthReadDenies(tempHome: string, realHomes: readonly string[]): string[];
|
|
200
|
+
/** Deny rules of a grok turn: every other tool class, the login file, and the read-only shell list. */
|
|
201
|
+
export declare function grokDenyRules(tempHome: string, realHomes: readonly string[]): string[];
|
|
202
|
+
export declare function grokTurnArgs(prompt: string, scope: TurnScope, maxTurns: number, tempHome: string, realHomes: readonly string[]): string[];
|
|
203
|
+
/** Parent env keys copied into a grok turn. Everything else, including credentials, stays out. */
|
|
204
|
+
export declare const GROK_ENV_ALLOW: readonly ["PATH", "HOME", "TMPDIR", "TMP", "TEMP", "LANG", "LC_ALL", "LC_CTYPE", "USER", "LOGNAME", "SHELL", "SystemRoot", "COMSPEC", "PATHEXT", "WINDIR", "TERM", "TZ", "HQ_ROOT"];
|
|
205
|
+
/** The turn's environment for grok: an allowlist, no sandbox or config overrides, no Claude or Cursor configuration scans. */
|
|
206
|
+
export declare function grokTurnEnv(base: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
207
|
+
export type GrokExtractSupport = {
|
|
208
|
+
ok: true;
|
|
209
|
+
} | {
|
|
210
|
+
ok: false;
|
|
211
|
+
missing: string[];
|
|
212
|
+
};
|
|
213
|
+
/** Whether `grok --help` lists every flag a locked-down turn needs, and streaming-json output. */
|
|
214
|
+
export declare function grokExtractSupportFromHelp(helpText: string): GrokExtractSupport;
|
|
215
|
+
export declare function resetGrokExtractProbeCache(): void;
|
|
216
|
+
/** Probe `grok --help` once per grok binary path (empty or failed output counts as every flag missing). */
|
|
217
|
+
export declare function probeGrokExtractSupport(grokBin: string, readHelp: () => string): GrokExtractSupport;
|
|
218
|
+
export type GrokAvailability = {
|
|
219
|
+
installed: false;
|
|
220
|
+
} | {
|
|
221
|
+
installed: true;
|
|
222
|
+
support: GrokExtractSupport;
|
|
223
|
+
};
|
|
224
|
+
export interface ToolAvailability {
|
|
225
|
+
installed: boolean;
|
|
226
|
+
/** The tool's own answer; null when it did not say (then it is not used as a fallback). */
|
|
227
|
+
signedIn: boolean | null;
|
|
228
|
+
}
|
|
229
|
+
export type ExtractRuntimeChoice = {
|
|
230
|
+
runtime: BotRuntimeId;
|
|
231
|
+
fallback?: string;
|
|
232
|
+
} | {
|
|
233
|
+
error: string;
|
|
234
|
+
};
|
|
235
|
+
/**
|
|
236
|
+
* Which runtime runs the turns. The main bot's runtime, except that a grok
|
|
237
|
+
* that is missing or cannot be locked down is never run: the step uses
|
|
238
|
+
* claude, then codex, when that tool is installed and signed in, and
|
|
239
|
+
* otherwise fails. The claude and codex checks run only when needed.
|
|
240
|
+
*/
|
|
241
|
+
export declare function chooseExtractRuntime(main: BotRuntimeId, avail: {
|
|
242
|
+
grok: () => GrokAvailability;
|
|
243
|
+
claude: () => ToolAvailability;
|
|
244
|
+
codex: () => ToolAvailability;
|
|
245
|
+
}, platform?: NodeJS.Platform): ExtractRuntimeChoice;
|
|
140
246
|
export declare function claudeTurnArgs(prompt: string, scope: TurnScope, maxTurns: number, budgetUsd: number): string[];
|
|
141
247
|
export declare function codexTurnArgs(prompt: string, scope: TurnScope, codexBin: string, envPath: string | undefined): string[];
|
|
142
248
|
export declare function extractPartialDocument(ctx: StepContext, doc: PartialDocument, prior: readonly PriorSignal[]): Promise<StepResult>;
|