@indigoai-us/hq-cli 5.300.0 → 5.301.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/assets/scaffold/core/scripts/token-usage-report.sh +9 -0
  3. package/dist/command-catalog.generated.d.ts +28 -2
  4. package/dist/command-catalog.generated.js +34 -2
  5. package/dist/commands/__fixtures__/access-vault.d.ts +2 -0
  6. package/dist/commands/__fixtures__/access-vault.js +1 -1
  7. package/dist/commands/access.d.ts +2 -1
  8. package/dist/commands/access.js +58 -49
  9. package/dist/commands/cloud-provision.js +51 -2
  10. package/dist/commands/core-company-index-scope-flag.d.ts +3 -0
  11. package/dist/commands/core-company-index-scope-flag.js +6 -0
  12. package/dist/commands/core-job-native-utils.d.ts +6 -0
  13. package/dist/commands/core-job-native-utils.js +80 -0
  14. package/dist/commands/core-job-native.d.ts +2 -4
  15. package/dist/commands/core-job-native.js +6 -10
  16. package/dist/commands/core-job-notify-native.d.ts +36 -0
  17. package/dist/commands/core-job-notify-native.js +359 -0
  18. package/dist/commands/core-job-remediate-native.d.ts +26 -0
  19. package/dist/commands/core-job-remediate-native.js +374 -0
  20. package/dist/commands/core.js +26 -8
  21. package/dist/commands/files.js +31 -14
  22. package/dist/commands/integrations-api.d.ts +6 -6
  23. package/dist/commands/integrations-api.js +4 -13
  24. package/dist/commands/integrations-manage.js +1 -4
  25. package/dist/commands/lanes-message.js +10 -2
  26. package/dist/commands/lanes-shared.d.ts +2 -0
  27. package/dist/commands/lanes.d.ts +2 -0
  28. package/dist/commands/lanes.js +1 -0
  29. package/dist/commands/meetings.js +35 -16
  30. package/dist/commands/safe-extract.d.ts +4 -4
  31. package/dist/commands/safe-extract.js +6 -7
  32. package/dist/lib/core-utils/token-usage-report.js +17 -3
  33. package/dist/lib/index-render/companies.d.ts +4 -1
  34. package/dist/lib/index-render/companies.js +66 -3
  35. package/dist/lib/index-render/index.d.ts +1 -0
  36. package/dist/lib/index-render/index.js +2 -1
  37. package/dist/lib/index-render/shared.d.ts +1 -1
  38. package/dist/lib/index-render/shared.js +4 -2
  39. package/dist/lib/mesh/live/daemon/mqtt-events.d.ts +7 -0
  40. package/dist/lib/mesh/live/daemon/mqtt-events.js +2 -0
  41. package/dist/lib/mesh/live/daemon/mqtt-events.test-d.d.ts +2 -0
  42. package/dist/lib/mesh/live/daemon/mqtt-events.test-d.js +10 -0
  43. package/dist/lib/mesh/live/daemon/presence.d.ts +2 -2
  44. package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +2 -2
  45. package/dist/utils/self-update.js +51 -4
  46. package/dist/utils/staged-npm-install.d.ts +16 -0
  47. package/dist/utils/staged-npm-install.js +264 -0
  48. package/dist/utils/update-command-supervisor.cjs +53 -1
  49. package/dist/utils/version-gate.d.ts +8 -0
  50. package/dist/utils/version-gate.js +82 -15
  51. package/package.json +2 -2
  52. package/dist/lib/install-path-compatibility-flag.d.ts +0 -9
  53. package/dist/lib/install-path-compatibility-flag.js +0 -12
@@ -0,0 +1,359 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+ import { load } from "js-yaml";
6
+ import { serializeJqJson } from "../lib/core-utils/jq-json-serializer.js";
7
+ import { registerCoreJobLeaf } from "./core-job-native-utils.js";
8
+ const ASSET = "core/scripts/hq-job-notify.sh";
9
+ const USAGE = [
10
+ "hq-job-notify.sh — dm-only remediation alerts for Outpost scheduled jobs (US-006).",
11
+ "",
12
+ 'Delivers via schedule-alerts profile with v1 channel=dm only (`hq dm` to owner).',
13
+ "If profile channel is slack|email|outpost-session, still deliver dm and log",
14
+ "channel_unimplemented. Never targets the original /schedule create session.",
15
+ "",
16
+ "Escalation collapse:",
17
+ " - first failure → DM immediately",
18
+ " - consecutive failures → at most one failure DM per 24h per job",
19
+ " - recovery after failures → recovered DM",
20
+ " - success after success → ok DM (every successful run)",
21
+ "",
22
+ "Notify delivery failure is logged and never fails the caller (exit 0).",
23
+ "",
24
+ "Usage:",
25
+ " core/scripts/hq-job-notify.sh \\",
26
+ " --job-id <id> --job-name <name> --outcome <ok|failed|blocked> \\",
27
+ " --summary <one-line> --duration <sec> --log-path <path> \\",
28
+ " [--failure-class <class>] [--next-action <text>] \\",
29
+ " [--owner <email|prs_*>] [--notify <dm|none|profile>] \\",
30
+ " [--hq-root <dir>]",
31
+ "",
32
+ "Env / overrides (tests):",
33
+ " HQ_ROOT, HOME, HQ_JOB_ALERT_DIR, HQ_JOB_NOTIFY_DM_BIN, HQ_JOB_NOTIFY_NOW_EPOCH",
34
+ " HQ_JOB_NOTIFY_COLLAPSE_SEC (default 86400)",
35
+ "",
36
+ "Exit: 0 always (delivery errors logged only); 2 usage.",
37
+ "",
38
+ "set -euo pipefail",
39
+ "",
40
+ "",
41
+ ].join("\n");
42
+ function defaultCommandExists(command, env) {
43
+ if (command.includes(path.sep)) {
44
+ try {
45
+ fs.accessSync(command, fs.constants.X_OK);
46
+ return true;
47
+ }
48
+ catch {
49
+ return false;
50
+ }
51
+ }
52
+ for (const directory of (env.PATH ?? "").split(path.delimiter)) {
53
+ if (!directory)
54
+ continue;
55
+ try {
56
+ fs.accessSync(path.join(directory, command), fs.constants.X_OK);
57
+ return true;
58
+ }
59
+ catch { /* Continue PATH lookup. */ }
60
+ }
61
+ return false;
62
+ }
63
+ function defaultRun(command, args, options) {
64
+ let logFd;
65
+ try {
66
+ if (options.logPath) {
67
+ try {
68
+ logFd = fs.openSync(options.logPath, "a");
69
+ }
70
+ catch {
71
+ // The shell treats failed output redirection as a failed delivery and
72
+ // continues the caller successfully; runJobNotify records that result.
73
+ return { code: 1, stdout: "" };
74
+ }
75
+ }
76
+ const result = spawnSync(command, args, {
77
+ cwd: options.cwd,
78
+ env: options.env,
79
+ encoding: "utf8",
80
+ stdio: options.captureStdout ? ["ignore", "pipe", "ignore"] : ["ignore", logFd ?? "ignore", logFd ?? "ignore"],
81
+ });
82
+ if (result.error) {
83
+ if (logFd !== undefined)
84
+ fs.writeSync(logFd, `${result.error.message}\n`);
85
+ return { code: 127, stdout: "" };
86
+ }
87
+ const signalNumber = result.signal ? os.constants.signals[result.signal] : undefined;
88
+ return {
89
+ code: result.status ?? (signalNumber ? 128 + signalNumber : 1),
90
+ stdout: result.stdout ?? "",
91
+ };
92
+ }
93
+ finally {
94
+ if (logFd !== undefined)
95
+ fs.closeSync(logFd);
96
+ }
97
+ }
98
+ export function defaultJobNotifyDependencies(env = process.env, cwd = process.cwd()) {
99
+ return {
100
+ env,
101
+ cwd,
102
+ now: () => new Date(),
103
+ exists: (file) => fs.existsSync(file),
104
+ readFile: (file) => fs.readFileSync(file, "utf8"),
105
+ mkdir: (directory) => fs.mkdirSync(directory, { recursive: true }),
106
+ writeFile: (file, content) => fs.writeFileSync(file, content),
107
+ appendFile: (file, content) => fs.appendFileSync(file, content),
108
+ chmod: (file, mode) => fs.chmodSync(file, mode),
109
+ commandExists: (command) => defaultCommandExists(command, env),
110
+ run: defaultRun,
111
+ stdout: (text) => { process.stdout.write(text); },
112
+ stderr: (text) => { process.stderr.write(text); },
113
+ };
114
+ }
115
+ function parseArguments(argv, deps) {
116
+ const values = {
117
+ hqRoot: deps.env.HQ_ROOT ?? "",
118
+ jobId: "", jobName: "", outcome: "", summary: "", duration: "", logPath: "",
119
+ failureClass: "", nextAction: "", owner: "", notifyMode: "",
120
+ };
121
+ const aliases = {
122
+ "--hq-root": "hqRoot", "--job-id": "jobId", "--job-name": "jobName",
123
+ "--outcome": "outcome", "--summary": "summary", "--duration": "duration",
124
+ "--log-path": "logPath", "--failure-class": "failureClass",
125
+ "--next-action": "nextAction", "--owner": "owner", "--notify": "notifyMode",
126
+ };
127
+ for (let index = 0; index < argv.length;) {
128
+ const flag = argv[index];
129
+ if (flag === "-h" || flag === "--help") {
130
+ deps.stdout(USAGE);
131
+ return 0;
132
+ }
133
+ if (flag === "--")
134
+ break;
135
+ const key = aliases[flag];
136
+ if (key) {
137
+ const value = argv[index + 1] ?? "";
138
+ if ((key === "hqRoot" || key === "jobId" || key === "outcome") && !value) {
139
+ deps.stderr(`hq-job-notify: --${key === "hqRoot" ? "hq-root" : key === "jobId" ? "job-id" : "outcome"} requires ${key === "hqRoot" ? "a directory" : "a value"}\n`);
140
+ return 2;
141
+ }
142
+ values[key] = value;
143
+ index += 2;
144
+ continue;
145
+ }
146
+ if (flag.startsWith("-")) {
147
+ deps.stderr(`hq-job-notify: unknown option: ${flag}\n`);
148
+ return 2;
149
+ }
150
+ deps.stderr(`hq-job-notify: unexpected argument: ${flag}\n`);
151
+ return 2;
152
+ }
153
+ if (!values.jobId) {
154
+ deps.stderr("hq-job-notify: usage: hq-job-notify.sh --job-id <id> --outcome <ok|failed|blocked> ...\n");
155
+ return 2;
156
+ }
157
+ if (!["ok", "failed", "blocked"].includes(values.outcome)) {
158
+ deps.stderr("hq-job-notify: --outcome must be ok|failed|blocked\n");
159
+ return 2;
160
+ }
161
+ if (!values.jobName)
162
+ values.jobName = values.jobId;
163
+ if (!values.summary)
164
+ values.summary = values.outcome;
165
+ if (!values.duration)
166
+ values.duration = "0";
167
+ return values;
168
+ }
169
+ function canUseLiveRootForArguments(argv) {
170
+ const parsed = parseArguments(argv, {
171
+ env: {},
172
+ stdout: () => undefined,
173
+ stderr: () => undefined,
174
+ });
175
+ return typeof parsed !== "number";
176
+ }
177
+ function profileChannel(profilePath, deps) {
178
+ if (!deps.exists(profilePath) || !deps.commandExists("yq"))
179
+ return "dm";
180
+ try {
181
+ const loaded = load(deps.readFile(profilePath));
182
+ const channel = loaded?.channel;
183
+ return channel === undefined || channel === null || channel === false || channel === "" ? "dm" : String(channel);
184
+ }
185
+ catch {
186
+ // The shell suppresses yq parse errors and falls back through `|| echo dm`.
187
+ return "dm";
188
+ }
189
+ }
190
+ function emailFromWhoami(output) {
191
+ for (const line of output.split(/\r?\n/)) {
192
+ if (!line.startsWith("email:"))
193
+ continue;
194
+ const separator = line.indexOf(": ");
195
+ return (separator < 0 ? line : line.slice(separator + 2)).trimEnd();
196
+ }
197
+ return "";
198
+ }
199
+ function normalizedEpoch(value) {
200
+ if (!/^-?\d+$/.test(value))
201
+ throw new Error(`invalid HQ_JOB_NOTIFY_NOW_EPOCH: ${value}`);
202
+ const result = Number(value);
203
+ if (!Number.isSafeInteger(result))
204
+ throw new Error(`invalid HQ_JOB_NOTIFY_NOW_EPOCH: ${value}`);
205
+ return result;
206
+ }
207
+ export function runJobNotify(argv, options = {}) {
208
+ const deps = {
209
+ ...defaultJobNotifyDependencies(options.dependencies?.env ?? process.env, options.dependencies?.cwd ?? process.cwd()),
210
+ ...options.dependencies,
211
+ };
212
+ if (options.hqRoot)
213
+ deps.env = { ...deps.env, HQ_ROOT: options.hqRoot };
214
+ const parsed = parseArguments(argv, deps);
215
+ if (typeof parsed === "number")
216
+ return parsed;
217
+ const hqRoot = parsed.hqRoot || options.hqRoot || deps.env.HQ_ROOT || "";
218
+ if (hqRoot)
219
+ deps.env = { ...deps.env, HQ_ROOT: hqRoot };
220
+ const nowEpoch = deps.env.HQ_JOB_NOTIFY_NOW_EPOCH
221
+ ? normalizedEpoch(deps.env.HQ_JOB_NOTIFY_NOW_EPOCH)
222
+ : Math.floor(deps.now().getTime() / 1000);
223
+ const collapseSecText = deps.env.HQ_JOB_NOTIFY_COLLAPSE_SEC || "86400";
224
+ if (!/^-?\d+$/.test(collapseSecText))
225
+ throw new Error(`invalid HQ_JOB_NOTIFY_COLLAPSE_SEC: ${collapseSecText}`);
226
+ const collapseSec = Number(collapseSecText);
227
+ const alertBase = deps.env.HQ_JOB_ALERT_DIR || path.join(deps.env.HOME || path.sep, ".hq", "jobs", "alerts");
228
+ const stateDir = path.join(alertBase, parsed.jobId);
229
+ deps.mkdir(stateDir);
230
+ const stateFile = path.join(stateDir, "state.json");
231
+ const deliveryLog = path.join(stateDir, "delivery.log");
232
+ const log = (message) => deps.stderr(`hq-job-notify: ${message}\n`);
233
+ let notifyMode = parsed.notifyMode;
234
+ if (!notifyMode || notifyMode === "null")
235
+ notifyMode = "profile";
236
+ if (notifyMode === "none") {
237
+ log(`notify=none — skipping delivery for job ${parsed.jobId}`);
238
+ deps.stdout('{"delivered":false,"reason":"notify_none"}\n');
239
+ return 0;
240
+ }
241
+ const selectedChannel = notifyMode === "profile"
242
+ ? profileChannel(path.join(hqRoot, "personal/settings/schedule-alerts.yaml"), deps)
243
+ : "dm";
244
+ if (notifyMode !== "profile" && notifyMode !== "dm")
245
+ log(`unknown notify mode '${notifyMode}' — falling back to dm`);
246
+ let channelUnimplemented = 0;
247
+ if (selectedChannel !== "dm" && selectedChannel !== "") {
248
+ channelUnimplemented = 1;
249
+ log(`channel_unimplemented=${selectedChannel} — delivering dm fallback`);
250
+ }
251
+ let owner = parsed.owner;
252
+ if (!owner || owner === "null") {
253
+ owner = "";
254
+ if (deps.commandExists("hq")) {
255
+ owner = emailFromWhoami(deps.run("hq", ["whoami"], {
256
+ cwd: deps.cwd, env: deps.env, captureStdout: true,
257
+ }).stdout);
258
+ }
259
+ }
260
+ if (!owner) {
261
+ log("no owner recipient — cannot dm (logged only); never fails the job");
262
+ deps.appendFile(deliveryLog, '{"delivered":false,"reason":"missing_owner"}\n');
263
+ }
264
+ let previous = {};
265
+ if (deps.exists(stateFile))
266
+ previous = JSON.parse(deps.readFile(stateFile));
267
+ const previousOutcome = previous.last_outcome == null ? "" : String(previous.last_outcome);
268
+ const previousFailureText = String(previous.last_failure_dm_epoch ?? 0);
269
+ const previousFailureEpoch = /^\d+$/.test(previousFailureText) ? Number(previousFailureText) : 0;
270
+ const kind = parsed.outcome === "ok"
271
+ ? previousOutcome === "failed" || previousOutcome === "blocked" ? "recovered" : "ok"
272
+ : previousOutcome === "failed" || previousOutcome === "blocked"
273
+ ? nowEpoch - previousFailureEpoch < collapseSec && previousFailureEpoch > 0 ? "suppressed" : "failed"
274
+ : "failed";
275
+ if (kind === "suppressed") {
276
+ log(`collapse: suppressing failure dm for job ${parsed.jobId} (within ${collapseSecText}s window)`);
277
+ const updatedAt = deps.now().toISOString().replace(/\.\d{3}Z$/, "Z");
278
+ deps.writeFile(stateFile, serializeJqJson({
279
+ last_outcome: parsed.outcome,
280
+ last_failure_dm_epoch: previousFailureEpoch,
281
+ last_updated_at: updatedAt,
282
+ last_failure_class: parsed.failureClass,
283
+ last_delivery: "suppressed",
284
+ }, "compact"));
285
+ try {
286
+ deps.chmod(stateFile, 0o600);
287
+ }
288
+ catch { /* chmod failure is ignored by the shell's `|| true`. */ }
289
+ deps.stdout(serializeJqJson({ delivered: false, reason: "collapsed_24h", channel_unimplemented: channelUnimplemented === 1 }, "compact"));
290
+ return 0;
291
+ }
292
+ const statusWord = kind === "recovered" ? "recovered" : parsed.outcome;
293
+ const classPart = parsed.failureClass && parsed.failureClass !== "null" ? ` class=${parsed.failureClass}` : "";
294
+ const actionPart = parsed.nextAction ? ` next: ${parsed.nextAction}` : "";
295
+ let headline;
296
+ if (kind === "ok" || kind === "recovered")
297
+ headline = `Scheduled job '${parsed.jobName}' ${kind} (${parsed.duration}s)`;
298
+ else
299
+ headline = `Scheduled job '${parsed.jobName}' ${statusWord}${classPart} (${parsed.duration}s)`;
300
+ const details = `job=${parsed.jobId}\nstatus=${statusWord}\nclassification=${parsed.failureClass || "none"}\nsummary=${parsed.summary}\nduration_seconds=${parsed.duration}\nlog=${parsed.logPath || "none"}${actionPart}`;
301
+ let delivered = 0;
302
+ let deliverRc;
303
+ let dmBin = deps.env.HQ_JOB_NOTIFY_DM_BIN || "";
304
+ if (!dmBin && deps.commandExists("hq"))
305
+ dmBin = "hq";
306
+ if (!owner) {
307
+ log("skip dm send — missing owner");
308
+ deliverRc = 1;
309
+ }
310
+ else if (!dmBin) {
311
+ log("skip dm send — hq CLI not found");
312
+ deliverRc = 1;
313
+ }
314
+ else {
315
+ const args = dmBin === "hq"
316
+ ? ["dm", owner, headline, "--details", details]
317
+ : [owner, headline, details];
318
+ deliverRc = deps.run(dmBin, args, { cwd: deps.cwd, env: deps.env, captureStdout: false, logPath: deliveryLog }).code;
319
+ if (deliverRc === 0) {
320
+ delivered = 1;
321
+ log(`dm delivered to ${owner} kind=${kind} job=${parsed.jobId}`);
322
+ }
323
+ else
324
+ log(`dm delivery failed rc=${deliverRc} job=${parsed.jobId} (logged; not failing run)`);
325
+ }
326
+ let failureDmEpoch = previousFailureEpoch;
327
+ if (kind === "failed" && delivered === 1)
328
+ failureDmEpoch = nowEpoch;
329
+ else if (kind === "ok" || kind === "recovered")
330
+ failureDmEpoch = 0;
331
+ const updatedAt = deps.now().toISOString().replace(/\.\d{3}Z$/, "Z");
332
+ deps.writeFile(stateFile, serializeJqJson({
333
+ last_outcome: parsed.outcome,
334
+ last_failure_dm_epoch: failureDmEpoch,
335
+ last_updated_at: updatedAt,
336
+ last_failure_class: parsed.failureClass,
337
+ last_delivery: kind,
338
+ last_delivered: delivered === 1,
339
+ }, "compact"));
340
+ try {
341
+ deps.chmod(stateFile, 0o600);
342
+ }
343
+ catch { /* chmod failure is ignored by the shell's `|| true`. */ }
344
+ deps.stdout(serializeJqJson({
345
+ delivered: delivered === 1,
346
+ kind,
347
+ owner,
348
+ channel_unimplemented: channelUnimplemented === 1,
349
+ deliver_rc: deliverRc,
350
+ }, "compact"));
351
+ return 0;
352
+ }
353
+ export function registerCoreJobNotifyNativeCommand(group, core) {
354
+ registerCoreJobLeaf(group, core, "notify", ASSET, "Send a scheduled-job status alert to its owner", (argv, hqRoot) => runJobNotify(argv, { hqRoot }), canUseLiveRootForArguments, [
355
+ "--hq-root", "--job-id", "--job-name", "--outcome", "--summary", "--duration", "--log-path",
356
+ "--failure-class", "--next-action", "--owner", "--notify",
357
+ ]);
358
+ }
359
+ //# sourceMappingURL=core-job-notify-native.js.map
@@ -0,0 +1,26 @@
1
+ /** Native TypeScript port of hq-job-remediate.sh. */
2
+ import { Command } from "commander";
3
+ type Environment = Record<string, string | undefined>;
4
+ export type JobRemediateRunOptions = {
5
+ cwd: string;
6
+ env: Environment;
7
+ logPath: string;
8
+ };
9
+ export type JobRemediateDependencies = {
10
+ env: Environment;
11
+ cwd: string;
12
+ /** Guard seam: this command must never inspect credential file contents. */
13
+ readFile: (file: string) => string;
14
+ commandExists: (command: string) => boolean;
15
+ run: (command: string, args: string[], options: JobRemediateRunOptions) => number;
16
+ stdout: (text: string) => void;
17
+ stderr: (text: string) => void;
18
+ };
19
+ export declare function defaultJobRemediateDependencies(env?: Environment, cwd?: string): JobRemediateDependencies;
20
+ export declare function runJobRemediate(argv: string[], options?: {
21
+ hqRoot?: string;
22
+ dependencies?: Partial<JobRemediateDependencies>;
23
+ }): number;
24
+ export declare function registerCoreJobRemediateNativeCommand(group: Command, core: Command): void;
25
+ export {};
26
+ //# sourceMappingURL=core-job-remediate-native.d.ts.map