@indigoai-us/hq-cli 5.345.160 → 5.345.162
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 +36 -0
- package/assets/scaffold/core/scripts/session-journal.sh +116 -24
- package/dist/command-catalog.generated.d.ts +262 -5
- package/dist/command-catalog.generated.js +322 -5
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/agent-inbox.js +6 -2
- package/dist/commands/auth.js +6 -5
- package/dist/commands/core-session-helper-runtime.d.ts +1 -0
- package/dist/commands/core-session-journal-native.d.ts +1 -0
- package/dist/commands/core-session-journal-native.js +158 -34
- package/dist/commands/feedback.d.ts +12 -0
- package/dist/commands/feedback.js +52 -0
- package/dist/commands/files-browse.d.ts +8 -0
- package/dist/commands/files-browse.js +83 -1
- package/dist/commands/install-global.d.ts +2 -0
- package/dist/commands/install-global.js +24 -1
- package/dist/commands/integrations-connect.js +1 -0
- package/dist/commands/integrations-manage.d.ts +2 -0
- package/dist/commands/integrations-manage.js +6 -4
- package/dist/commands/lanes-link.js +5 -3
- package/dist/commands/lanes-loop.js +7 -7
- package/dist/commands/lanes-questions.js +54 -4
- package/dist/commands/lanes-session-start.d.ts +12 -0
- package/dist/commands/lanes-session-start.js +93 -12
- package/dist/commands/lanes.js +2 -1
- package/dist/commands/meetings.d.ts +11 -0
- package/dist/commands/meetings.js +41 -11
- package/dist/commands/sync-held.d.ts +87 -0
- package/dist/commands/sync-held.js +191 -0
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +3830 -2812
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/index.js +21 -0
- package/dist/lanes-session-start-argv.d.ts +7 -0
- package/dist/lanes-session-start-argv.js +9 -0
- package/dist/lib/agent-kit/inbox-state.d.ts +5 -4
- package/dist/lib/agent-kit/inbox-state.js +24 -3
- package/dist/lib/bot/log.d.ts +3 -2
- package/dist/lib/bot/progress.d.ts +23 -1
- package/dist/lib/bot/progress.js +43 -5
- package/dist/lib/bot/run.js +2 -0
- package/dist/lib/bot/runtime/claude.d.ts +12 -0
- package/dist/lib/bot/runtime/claude.js +20 -0
- package/dist/lib/bot/runtime/index.d.ts +10 -1
- package/dist/lib/bot/runtime/index.js +14 -2
- package/dist/lib/bot/runtime/types.d.ts +8 -0
- package/dist/lib/doctor/checks/ps1-shims.d.ts +21 -0
- package/dist/lib/doctor/checks/ps1-shims.js +82 -0
- package/dist/lib/doctor/fix/apply.d.ts +13 -1
- package/dist/lib/doctor/fix/apply.js +38 -3
- package/dist/lib/doctor/fix/remediation.d.ts +9 -1
- package/dist/lib/doctor/fix/remediation.js +22 -0
- package/dist/lib/doctor/registry.js +2 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/lanes/links.d.ts +2 -1
- package/dist/lib/lanes/links.js +235 -17
- package/dist/lib/lanes/loop.d.ts +17 -0
- package/dist/lib/lanes/loop.js +235 -5
- package/dist/lib/lanes/questions.d.ts +1 -0
- package/dist/lib/lanes/registry-flag.d.ts +24 -0
- package/dist/lib/lanes/registry-flag.js +72 -6
- package/dist/lib/lanes/store.d.ts +4 -2
- package/dist/lib/lanes/store.js +8 -8
- package/dist/lib/windows/ps1-shims.d.ts +60 -0
- package/dist/lib/windows/ps1-shims.js +152 -0
- package/dist/utils/self-update.d.ts +2 -0
- package/dist/utils/self-update.js +8 -0
- package/dist/utils/staged-npm-install.js +13 -2
- package/dist/utils/token-keychain.d.ts +11 -8
- package/dist/utils/token-keychain.js +36 -15
- package/package.json +8 -8
- package/scripts/install-hook-engine.mjs +12 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
const USAGE = `session-journal.sh: usage:
|
|
5
6
|
session-journal.sh write "<title>" [--files f1,f2,...] [--body-file PATH] [--session KEY]
|
|
@@ -163,6 +164,58 @@ function isAgedPidlessCounterLock(lock, now) {
|
|
|
163
164
|
return false;
|
|
164
165
|
}
|
|
165
166
|
}
|
|
167
|
+
function reclaimStaleCounterTempDirs(lock, now) {
|
|
168
|
+
const parent = path.dirname(lock);
|
|
169
|
+
// Publishers create <lock>.tmp.XXXXXX, matching the shell helper. Older native
|
|
170
|
+
// builds used <lock>.tmp-XXXXXX, so reclaim aged orphans with either prefix.
|
|
171
|
+
const prefixes = [`${path.basename(lock)}.tmp.`, `${path.basename(lock)}.tmp-`];
|
|
172
|
+
let names;
|
|
173
|
+
try {
|
|
174
|
+
names = fs.readdirSync(parent);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
for (const name of names) {
|
|
180
|
+
if (!prefixes.some((prefix) => name.startsWith(prefix)))
|
|
181
|
+
continue;
|
|
182
|
+
const temp = path.join(parent, name);
|
|
183
|
+
if (!isAgedPidlessCounterLock(temp, now))
|
|
184
|
+
continue;
|
|
185
|
+
const read = (file) => {
|
|
186
|
+
try {
|
|
187
|
+
return fs.readFileSync(path.join(temp, file), "utf8").trim();
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return "";
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
const owner = read("pid");
|
|
194
|
+
const windowsOwner = read("winpid");
|
|
195
|
+
const token = read("owner-token");
|
|
196
|
+
if (owner && !/^\d+$/u.test(owner))
|
|
197
|
+
continue;
|
|
198
|
+
if (windowsOwner && !/^\d+$/u.test(windowsOwner))
|
|
199
|
+
continue;
|
|
200
|
+
if (owner && Number(owner) > 1 && counterLockOwnerIsAlive(temp, Number(owner)))
|
|
201
|
+
continue;
|
|
202
|
+
if (!owner && windowsOwner && Number(windowsOwner) > 1 && processIsAlive(Number(windowsOwner)))
|
|
203
|
+
continue;
|
|
204
|
+
if (read("pid") !== owner || read("winpid") !== windowsOwner || read("owner-token") !== token
|
|
205
|
+
|| !isAgedPidlessCounterLock(temp, now))
|
|
206
|
+
continue;
|
|
207
|
+
if (owner && counterLockOwnerIsAlive(temp, Number(owner)))
|
|
208
|
+
continue;
|
|
209
|
+
if (windowsOwner && Number(windowsOwner) > 1 && processIsAlive(Number(windowsOwner)))
|
|
210
|
+
continue;
|
|
211
|
+
const stale = `${temp}.stale-${process.pid}-${Math.random().toString(16).slice(2)}`;
|
|
212
|
+
try {
|
|
213
|
+
fs.renameSync(temp, stale);
|
|
214
|
+
fs.rmSync(stale, { recursive: true, force: true });
|
|
215
|
+
}
|
|
216
|
+
catch { /* another waiter cleaned it or the owner resumed */ }
|
|
217
|
+
}
|
|
218
|
+
}
|
|
166
219
|
function reclaimStaleCounterLock(lock, now) {
|
|
167
220
|
let rawOwner;
|
|
168
221
|
try {
|
|
@@ -184,6 +237,11 @@ function reclaimStaleCounterLock(lock, now) {
|
|
|
184
237
|
return false;
|
|
185
238
|
const owner = rawOwner ? Number.parseInt(rawOwner, 10) : 0;
|
|
186
239
|
const windowsOwner = rawWindowsPid ? Number.parseInt(rawWindowsPid, 10) : 0;
|
|
240
|
+
let observedToken = "";
|
|
241
|
+
try {
|
|
242
|
+
observedToken = fs.readFileSync(path.join(lock, "owner-token"), "utf8").trim();
|
|
243
|
+
}
|
|
244
|
+
catch { /* legacy lock */ }
|
|
187
245
|
if (rawOwner && (owner <= 1 || counterLockOwnerIsAlive(lock, owner)))
|
|
188
246
|
return false;
|
|
189
247
|
if (!rawOwner && windowsOwner > 1 && processIsAlive(windowsOwner))
|
|
@@ -219,7 +277,12 @@ function reclaimStaleCounterLock(lock, now) {
|
|
|
219
277
|
catch {
|
|
220
278
|
currentRawWindowsPid = "";
|
|
221
279
|
}
|
|
222
|
-
|
|
280
|
+
let currentToken = "";
|
|
281
|
+
try {
|
|
282
|
+
currentToken = fs.readFileSync(path.join(lock, "owner-token"), "utf8").trim();
|
|
283
|
+
}
|
|
284
|
+
catch { /* legacy lock */ }
|
|
285
|
+
const sameOwner = currentRawOwner === rawOwner && currentRawWindowsPid === rawWindowsPid && currentToken === observedToken;
|
|
223
286
|
const stillStale = rawOwner
|
|
224
287
|
? sameOwner && !counterLockOwnerIsAlive(lock, owner)
|
|
225
288
|
: sameOwner && !(windowsOwner > 1 && processIsAlive(windowsOwner)) && isAgedPidlessCounterLock(lock, now);
|
|
@@ -239,7 +302,15 @@ function reclaimStaleCounterLock(lock, now) {
|
|
|
239
302
|
catch { /* best effort */ }
|
|
240
303
|
return false;
|
|
241
304
|
}
|
|
242
|
-
function
|
|
305
|
+
function readLockToken(lock) {
|
|
306
|
+
try {
|
|
307
|
+
return fs.readFileSync(path.join(lock, "owner-token"), "utf8").trim();
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
return "";
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function withCounterLock(lock, env, writers, now, action, beforeCounterAction) {
|
|
243
314
|
let i = 0;
|
|
244
315
|
let acquired = false;
|
|
245
316
|
let tries = env.HQ_JOURNAL_COUNTER_LOCK_TRIES ?? "250";
|
|
@@ -247,22 +318,82 @@ function withCounterLock(lock, env, writers, now, action) {
|
|
|
247
318
|
tries = "250";
|
|
248
319
|
const retryLimit = Number.parseInt(tries, 10);
|
|
249
320
|
while (i < retryLimit) {
|
|
321
|
+
reclaimStaleCounterTempDirs(lock, now);
|
|
250
322
|
try {
|
|
251
|
-
fs.
|
|
252
|
-
|
|
323
|
+
const temp = fs.mkdtempSync(`${lock}.tmp.`);
|
|
324
|
+
const ownerToken = randomUUID();
|
|
253
325
|
try {
|
|
254
326
|
if (process.platform === "win32")
|
|
255
|
-
fs.writeFileSync(path.join(
|
|
256
|
-
fs.writeFileSync(path.join(
|
|
327
|
+
fs.writeFileSync(path.join(temp, "winpid"), `${process.pid}\n`, { flag: "wx" });
|
|
328
|
+
fs.writeFileSync(path.join(temp, "pid"), `${process.pid}\n`, { flag: "wx" });
|
|
329
|
+
fs.writeFileSync(path.join(temp, "owner-token"), `${ownerToken}\n`, { flag: "wx" });
|
|
330
|
+
const publishClaim = `${lock}.publish`;
|
|
331
|
+
try {
|
|
332
|
+
fs.mkdirSync(publishClaim);
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
try {
|
|
336
|
+
fs.rmSync(temp, { recursive: true, force: true });
|
|
337
|
+
}
|
|
338
|
+
catch { /* best effort */ }
|
|
339
|
+
if (error.code === "EEXIST" && reclaimStaleCounterLock(publishClaim, now))
|
|
340
|
+
continue;
|
|
341
|
+
throw error;
|
|
342
|
+
}
|
|
343
|
+
try {
|
|
344
|
+
fs.writeFileSync(path.join(publishClaim, "owner-token"), `${ownerToken}\n`, { flag: "wx" });
|
|
345
|
+
fs.writeFileSync(path.join(publishClaim, "pid"), `${process.pid}\n`, { flag: "wx" });
|
|
346
|
+
// renameSync can replace an existing empty PID-less directory on
|
|
347
|
+
// POSIX. Refuse a pre-existing lock while publishers are serialized.
|
|
348
|
+
if (readLockToken(publishClaim) !== ownerToken || fs.existsSync(lock))
|
|
349
|
+
throw new Error("counter lock already exists");
|
|
350
|
+
fs.renameSync(temp, lock);
|
|
351
|
+
}
|
|
352
|
+
finally {
|
|
353
|
+
if (readLockToken(publishClaim) === ownerToken)
|
|
354
|
+
fs.rmSync(publishClaim, { recursive: true, force: true });
|
|
355
|
+
}
|
|
356
|
+
acquired = true;
|
|
257
357
|
}
|
|
258
|
-
catch {
|
|
358
|
+
catch (error) {
|
|
359
|
+
try {
|
|
360
|
+
fs.rmSync(temp, { recursive: true, force: true });
|
|
361
|
+
}
|
|
362
|
+
catch { /* best effort */ }
|
|
363
|
+
throw error;
|
|
364
|
+
}
|
|
365
|
+
if (!acquired)
|
|
366
|
+
continue;
|
|
367
|
+
if (readLockToken(lock) !== ownerToken) {
|
|
368
|
+
acquired = false;
|
|
369
|
+
i++;
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
beforeCounterAction?.();
|
|
373
|
+
if (readLockToken(lock) !== ownerToken) {
|
|
374
|
+
acquired = false;
|
|
375
|
+
i++;
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
const completed = action(ownerToken);
|
|
379
|
+
if (!completed) {
|
|
380
|
+
if (readLockToken(lock) === ownerToken) {
|
|
381
|
+
try {
|
|
382
|
+
fs.rmSync(lock, { recursive: true, force: true });
|
|
383
|
+
}
|
|
384
|
+
catch { /* best effort */ }
|
|
385
|
+
}
|
|
386
|
+
acquired = false;
|
|
387
|
+
i++;
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (readLockToken(lock) === ownerToken) {
|
|
259
391
|
try {
|
|
260
392
|
fs.rmSync(lock, { recursive: true, force: true });
|
|
261
393
|
}
|
|
262
394
|
catch { /* best effort */ }
|
|
263
|
-
return;
|
|
264
395
|
}
|
|
265
|
-
|
|
396
|
+
return;
|
|
266
397
|
}
|
|
267
398
|
catch {
|
|
268
399
|
if (reclaimStaleCounterLock(lock, now))
|
|
@@ -276,22 +407,6 @@ function withCounterLock(lock, env, writers, now, action) {
|
|
|
276
407
|
writers.stderr("session-journal: tool-counter: lock remained busy; skipped update\n");
|
|
277
408
|
return;
|
|
278
409
|
}
|
|
279
|
-
try {
|
|
280
|
-
action();
|
|
281
|
-
}
|
|
282
|
-
finally {
|
|
283
|
-
let owner = 0;
|
|
284
|
-
try {
|
|
285
|
-
owner = Number.parseInt(fs.readFileSync(path.join(lock, "pid"), "utf8"), 10) || 0;
|
|
286
|
-
}
|
|
287
|
-
catch { /* lock disappeared */ }
|
|
288
|
-
if (owner === process.pid) {
|
|
289
|
-
try {
|
|
290
|
-
fs.rmSync(lock, { recursive: true, force: true });
|
|
291
|
-
}
|
|
292
|
-
catch { /* best effort */ }
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
410
|
}
|
|
296
411
|
function nextSequence(dir) {
|
|
297
412
|
let names = [];
|
|
@@ -313,7 +428,7 @@ function options(args, valueFlags) {
|
|
|
313
428
|
}
|
|
314
429
|
return values;
|
|
315
430
|
}
|
|
316
|
-
function runToolCounter(root, action, sessionArg, env, now, writers) {
|
|
431
|
+
function runToolCounter(root, action, sessionArg, env, now, writers, beforeCounterAction) {
|
|
317
432
|
const rawKey = sessionArg || env.HQ_JOURNAL_SESSION || env.HQ_HOOK_SESSION_ID || "";
|
|
318
433
|
const key = counterKey(rawKey);
|
|
319
434
|
const date = now().toISOString().slice(0, 10);
|
|
@@ -335,27 +450,36 @@ function runToolCounter(root, action, sessionArg, env, now, writers) {
|
|
|
335
450
|
writers.stderr(`${sessionJournalAsset}: line ${line}: ${printableCounter}: Is a directory\n`);
|
|
336
451
|
};
|
|
337
452
|
if (action === "increment") {
|
|
338
|
-
withCounterLock(lock, env, writers, now, () => {
|
|
453
|
+
withCounterLock(lock, env, writers, now, (ownerToken) => {
|
|
454
|
+
if (readLockToken(lock) !== ownerToken)
|
|
455
|
+
return false;
|
|
456
|
+
const next = readCounter(counter) + 1;
|
|
457
|
+
if (readLockToken(lock) !== ownerToken)
|
|
458
|
+
return false;
|
|
339
459
|
try {
|
|
340
|
-
fs.writeFileSync(counter, `${
|
|
460
|
+
fs.writeFileSync(counter, `${next}\n`);
|
|
341
461
|
}
|
|
342
462
|
catch (error) {
|
|
343
|
-
reportDirectoryWriteFailure(error,
|
|
463
|
+
reportDirectoryWriteFailure(error, 545);
|
|
344
464
|
}
|
|
345
|
-
|
|
465
|
+
return true;
|
|
466
|
+
}, beforeCounterAction);
|
|
346
467
|
}
|
|
347
468
|
else if (action === "read") {
|
|
348
469
|
writers.stdout(`${readCounter(counter)}\n`);
|
|
349
470
|
}
|
|
350
471
|
else if (action === "reset") {
|
|
351
|
-
withCounterLock(lock, env, writers, now, () => {
|
|
472
|
+
withCounterLock(lock, env, writers, now, (ownerToken) => {
|
|
473
|
+
if (readLockToken(lock) !== ownerToken)
|
|
474
|
+
return false;
|
|
352
475
|
try {
|
|
353
476
|
fs.writeFileSync(counter, "0\n");
|
|
354
477
|
}
|
|
355
478
|
catch (error) {
|
|
356
|
-
reportDirectoryWriteFailure(error,
|
|
479
|
+
reportDirectoryWriteFailure(error, 556);
|
|
357
480
|
}
|
|
358
|
-
|
|
481
|
+
return true;
|
|
482
|
+
}, beforeCounterAction);
|
|
359
483
|
}
|
|
360
484
|
else {
|
|
361
485
|
writers.stderr("session-journal: tool-counter: subcommand required (increment|read|reset)\n");
|
|
@@ -519,7 +643,7 @@ function executeSessionJournal(args, cwd, hqRoot, dependencies, writers) {
|
|
|
519
643
|
if (command === "tool-counter") {
|
|
520
644
|
const action = rest[0] ?? "";
|
|
521
645
|
const session = options(rest.slice(1), ["--session"]).get("--session") ?? "";
|
|
522
|
-
runToolCounter(root, action, session, runtime.env, runtime.now, writers);
|
|
646
|
+
runToolCounter(root, action, session, runtime.env, runtime.now, writers, dependencies.beforeCounterAction);
|
|
523
647
|
return 0;
|
|
524
648
|
}
|
|
525
649
|
writers.stderr(USAGE);
|
|
@@ -91,5 +91,17 @@ export declare function postFeedbackUpdate(opts: {
|
|
|
91
91
|
publicUrl: string | null;
|
|
92
92
|
slack: string | null;
|
|
93
93
|
}>;
|
|
94
|
+
/** Mirror of hq-pro normalizeAssignee: a prs_ uid as-is, an email lowercased. */
|
|
95
|
+
export declare function normalizeFeedbackAssignee(value: string): string;
|
|
96
|
+
/**
|
|
97
|
+
* Reassign a ticket. A person's own login may move a ticket that another
|
|
98
|
+
* person claimed; agent and operator service tokens get 409
|
|
99
|
+
* TICKET_CLAIMED_BY_HUMAN from the server for those tickets.
|
|
100
|
+
*/
|
|
101
|
+
export declare function assignFeedback(opts: {
|
|
102
|
+
id: string;
|
|
103
|
+
token: string;
|
|
104
|
+
assignee: string;
|
|
105
|
+
}): Promise<void>;
|
|
94
106
|
export declare function registerFeedbackCommand(program: Command): void;
|
|
95
107
|
//# sourceMappingURL=feedback.d.ts.map
|
|
@@ -285,6 +285,57 @@ export async function postFeedbackUpdate(opts) {
|
|
|
285
285
|
slack: typeof data.publicUpdateSlack === "string" ? data.publicUpdateSlack : null,
|
|
286
286
|
};
|
|
287
287
|
}
|
|
288
|
+
const ASSIGNEE_RE = /^(?:prs_[A-Za-z0-9]+|[^\s@]+@[^\s@]+\.[^\s@]+)$/;
|
|
289
|
+
/** Mirror of hq-pro normalizeAssignee: a prs_ uid as-is, an email lowercased. */
|
|
290
|
+
export function normalizeFeedbackAssignee(value) {
|
|
291
|
+
const trimmed = value.trim();
|
|
292
|
+
if (!ASSIGNEE_RE.test(trimmed)) {
|
|
293
|
+
throw new Error(`Invalid --to '${value}': expected an email address or a prs_ uid.`);
|
|
294
|
+
}
|
|
295
|
+
return trimmed.startsWith("prs_") ? trimmed : trimmed.toLowerCase();
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Reassign a ticket. A person's own login may move a ticket that another
|
|
299
|
+
* person claimed; agent and operator service tokens get 409
|
|
300
|
+
* TICKET_CLAIMED_BY_HUMAN from the server for those tickets.
|
|
301
|
+
*/
|
|
302
|
+
export async function assignFeedback(opts) {
|
|
303
|
+
const res = await vaultApiFetch({
|
|
304
|
+
token: opts.token,
|
|
305
|
+
path: `/v1/feedback/${opts.id}`,
|
|
306
|
+
method: "PATCH",
|
|
307
|
+
body: { assignee: opts.assignee },
|
|
308
|
+
});
|
|
309
|
+
if (res.ok)
|
|
310
|
+
return;
|
|
311
|
+
const data = (await res.json().catch(() => ({})));
|
|
312
|
+
const message = typeof data.error === "string" ? data.error : res.statusText;
|
|
313
|
+
if (data.code === "TICKET_CLAIMED_BY_HUMAN") {
|
|
314
|
+
throw new Error(`${message} Run hq feedback assign from your own hq login (hq login), not an agent or operator token.`);
|
|
315
|
+
}
|
|
316
|
+
throw new Error(`Failed to assign ${opts.id}: ${message}`);
|
|
317
|
+
}
|
|
318
|
+
function registerAssignSubcommand(feedbackCmd) {
|
|
319
|
+
feedbackCmd
|
|
320
|
+
.command("assign")
|
|
321
|
+
.description("Reassign a ticket to a person, including one another person has claimed")
|
|
322
|
+
.argument("<id>", "Feedback id (feedback_<uuid>)")
|
|
323
|
+
.requiredOption("--to <assignee>", "New assignee: an email address or a prs_ uid")
|
|
324
|
+
.action(async (id, opts) => {
|
|
325
|
+
try {
|
|
326
|
+
assertFeedbackId(id);
|
|
327
|
+
const assignee = normalizeFeedbackAssignee(opts.to);
|
|
328
|
+
const token = await resolveStaffToken();
|
|
329
|
+
assertVaultApiRequestSupported({ token, path: `/v1/feedback/${id}` });
|
|
330
|
+
await assignFeedback({ id, token, assignee });
|
|
331
|
+
console.log(`Assigned: ${id} -> ${assignee}`);
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
288
339
|
function registerLinkSubcommand(feedbackCmd) {
|
|
289
340
|
feedbackCmd
|
|
290
341
|
.command("link")
|
|
@@ -447,5 +498,6 @@ export function registerFeedbackCommand(program) {
|
|
|
447
498
|
registerLinkSubcommand(feedbackCmd);
|
|
448
499
|
registerUpdateSubcommand(feedbackCmd);
|
|
449
500
|
registerFeedbackShowCommand(feedbackCmd);
|
|
501
|
+
registerAssignSubcommand(feedbackCmd);
|
|
450
502
|
}
|
|
451
503
|
//# sourceMappingURL=feedback.js.map
|
|
@@ -459,6 +459,14 @@ export declare const STORAGE_DOWNLOAD_FAILED_CODE = "FILES_STORAGE_DOWNLOAD_FAIL
|
|
|
459
459
|
* firewall that does not allow the bucket host, and we say so.
|
|
460
460
|
*/
|
|
461
461
|
export declare function presignedDownloadError(key: string, url: string, dl: Response): Promise<Error>;
|
|
462
|
+
/**
|
|
463
|
+
* Turn a presigned download that never got an HTTP response (connection
|
|
464
|
+
* refused or reset, DNS failure, timeout, or a TLS failure) into an error that
|
|
465
|
+
* names the storage host and tells the operator to allow it. Returns
|
|
466
|
+
* undefined for any other error so the caller rethrows it unchanged. Only the
|
|
467
|
+
* URL host is used; the signed query string never reaches the message.
|
|
468
|
+
*/
|
|
469
|
+
export declare function presignedDownloadTransportError(key: string, url: string, err: unknown): Error | undefined;
|
|
462
470
|
export declare function createCompanyPresignClient(input: {
|
|
463
471
|
token: string;
|
|
464
472
|
companyUid: string;
|
|
@@ -760,6 +760,82 @@ export async function presignedDownloadError(key, url, dl) {
|
|
|
760
760
|
...(storageCode ? { storageCode } : {}),
|
|
761
761
|
});
|
|
762
762
|
}
|
|
763
|
+
/** Socket-level error codes that mean the storage host could not be reached. */
|
|
764
|
+
const TRANSPORT_BLOCK_CODES = new Set([
|
|
765
|
+
"ECONNREFUSED",
|
|
766
|
+
"ECONNRESET",
|
|
767
|
+
"ETIMEDOUT",
|
|
768
|
+
"EHOSTUNREACH",
|
|
769
|
+
"ENETUNREACH",
|
|
770
|
+
"EPIPE",
|
|
771
|
+
"ENOTFOUND",
|
|
772
|
+
"EAI_AGAIN",
|
|
773
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
774
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
775
|
+
"UND_ERR_SOCKET",
|
|
776
|
+
]);
|
|
777
|
+
/** TLS failures, usually a TLS-inspecting proxy whose certificate Node does not trust. */
|
|
778
|
+
const TLS_BLOCK_CODES = new Set([
|
|
779
|
+
"CERT_HAS_EXPIRED",
|
|
780
|
+
"DEPTH_ZERO_SELF_SIGNED_CERT",
|
|
781
|
+
"SELF_SIGNED_CERT_IN_CHAIN",
|
|
782
|
+
"UNABLE_TO_GET_ISSUER_CERT",
|
|
783
|
+
"UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
|
|
784
|
+
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
|
|
785
|
+
"ERR_TLS_CERT_ALTNAME_INVALID",
|
|
786
|
+
"ERR_SSL_WRONG_VERSION_NUMBER",
|
|
787
|
+
"EPROTO",
|
|
788
|
+
]);
|
|
789
|
+
function errorChainCode(err) {
|
|
790
|
+
let cur = err;
|
|
791
|
+
for (let depth = 0; depth < 5 && cur && typeof cur === "object"; depth++) {
|
|
792
|
+
const code = cur.code;
|
|
793
|
+
if (typeof code === "string" && code.length > 0)
|
|
794
|
+
return code;
|
|
795
|
+
cur = cur.cause;
|
|
796
|
+
}
|
|
797
|
+
return undefined;
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Turn a presigned download that never got an HTTP response (connection
|
|
801
|
+
* refused or reset, DNS failure, timeout, or a TLS failure) into an error that
|
|
802
|
+
* names the storage host and tells the operator to allow it. Returns
|
|
803
|
+
* undefined for any other error so the caller rethrows it unchanged. Only the
|
|
804
|
+
* URL host is used; the signed query string never reaches the message.
|
|
805
|
+
*/
|
|
806
|
+
export function presignedDownloadTransportError(key, url, err) {
|
|
807
|
+
const name = err?.name;
|
|
808
|
+
const timedOut = name === "TimeoutError";
|
|
809
|
+
const code = timedOut ? "TIMEOUT" : errorChainCode(err);
|
|
810
|
+
if (!code)
|
|
811
|
+
return undefined;
|
|
812
|
+
const tls = TLS_BLOCK_CODES.has(code) || code.startsWith("ERR_TLS_") || code.startsWith("ERR_SSL_");
|
|
813
|
+
if (!timedOut && !tls && !TRANSPORT_BLOCK_CODES.has(code))
|
|
814
|
+
return undefined;
|
|
815
|
+
let host = "the storage host";
|
|
816
|
+
try {
|
|
817
|
+
host = new URL(url).host;
|
|
818
|
+
}
|
|
819
|
+
catch {
|
|
820
|
+
// keep the generic label
|
|
821
|
+
}
|
|
822
|
+
const what = tls
|
|
823
|
+
? `the TLS connection to ${host} failed (${code}). A proxy that inspects HTTPS traffic is the usual cause`
|
|
824
|
+
: timedOut
|
|
825
|
+
? `the connection to ${host} timed out. A firewall or egress rule that drops traffic is the usual cause`
|
|
826
|
+
: `the connection to ${host} failed (${code}). A firewall, proxy or egress rule on this network is the usual cause`;
|
|
827
|
+
const message = `Failed to download '${key}': ${what}. Your file access was granted; the ` +
|
|
828
|
+
`network blocked the storage host. Ask your network operator to allow ` +
|
|
829
|
+
`outbound HTTPS (TCP 443) to ${host} (HQ vault files are served from ` +
|
|
830
|
+
`*.s3.us-east-1.amazonaws.com), then retry.`;
|
|
831
|
+
return Object.assign(new Error(message, { cause: err }), {
|
|
832
|
+
code: STORAGE_DOWNLOAD_FAILED_CODE,
|
|
833
|
+
key,
|
|
834
|
+
blockedHost: host,
|
|
835
|
+
transportCode: code,
|
|
836
|
+
fromStorage: false,
|
|
837
|
+
});
|
|
838
|
+
}
|
|
763
839
|
export function createCompanyPresignClient(input) {
|
|
764
840
|
const { token, companyUid, companySlug, baseUrl } = input;
|
|
765
841
|
const denied = (message, key, extra) => Object.assign(new Error(message), {
|
|
@@ -829,7 +905,13 @@ export function createCompanyPresignClient(input) {
|
|
|
829
905
|
}
|
|
830
906
|
throw new Error(message);
|
|
831
907
|
}
|
|
832
|
-
|
|
908
|
+
let dl;
|
|
909
|
+
try {
|
|
910
|
+
dl = await fetch(first.url, { signal: AbortSignal.timeout(120_000) });
|
|
911
|
+
}
|
|
912
|
+
catch (err) {
|
|
913
|
+
throw presignedDownloadTransportError(key, first.url, err) ?? err;
|
|
914
|
+
}
|
|
833
915
|
if (!dl.ok) {
|
|
834
916
|
throw await presignedDownloadError(key, first.url, dl);
|
|
835
917
|
}
|
|
@@ -83,6 +83,8 @@ export interface GlobalCommandOpts {
|
|
|
83
83
|
trustCodexHooks?: (cwd: string, keys: readonly string[]) => Promise<RuntimeHookTrustResult>;
|
|
84
84
|
/** Test seam: is the npm hq.cmd folder on the Windows PATH? Null off Windows. */
|
|
85
85
|
shimPathCheck?: () => ShimPathCheck | null;
|
|
86
|
+
/** Test seam: the Windows npm prefix holding hq.cmd. Null off Windows. */
|
|
87
|
+
ps1ShimDir?: () => string | null;
|
|
86
88
|
}
|
|
87
89
|
export declare function resolveInstallRoot(explicit: string | undefined, options?: {
|
|
88
90
|
env?: NodeJS.ProcessEnv;
|
|
@@ -27,7 +27,8 @@ import { readCompaniesManifestMap } from '../lib/work-context/repo-remote.js';
|
|
|
27
27
|
import { resolveDefaultHqRoot } from '../utils/hq-root-resolver.js';
|
|
28
28
|
import { trustCodexUserHooks } from '../utils/hook-trust.js';
|
|
29
29
|
import { assertProviderWrappersInstallable, installProviderWrappers, providerWrapperInstallPaths, removeProviderWrappers } from '../lib/install/provider-wrappers.js';
|
|
30
|
-
import { checkShimOnPath, ensureShimOnUserPath, shimPathWarning } from '../lib/windows/user-path.js';
|
|
30
|
+
import { checkShimOnPath, ensureShimOnUserPath, npmShimDir, shimPathWarning } from '../lib/windows/user-path.js';
|
|
31
|
+
import { removeHqCliPs1Shims } from '../lib/windows/ps1-shims.js';
|
|
31
32
|
import { addAgentsMdBlock, addCodexHooks, classifyLegacySkills, codexAdapterPath, codexCharterSource, codexHookTrustKeys, codexTargets, hasAllCodexHooks, HQ_OWNED_MARKER, LEGACY_BACKUP_NAME, LEGACY_SKILLS_NAME, removeAgentsMdBlock, removeCodexHooks, } from '../lib/install/codex-targets.js';
|
|
32
33
|
import { addClaudeMdBlock, addHqHooks, charterPath, claudeTargets, hasAllHqHooks, HQ_ANYWHERE_PACK, HQ_MCP_SERVER_NAME, hqSkillsSource, lineDiff, masterHookPath, shimPath, MCP_SUBPROCESS_PATH, removeClaudeMdBlock, removeHqHooks, SKILL_LINK_PREFIX, } from '../lib/install/claude-targets.js';
|
|
33
34
|
export { hookTargetFor, isDaemonInstalled } from '../lib/install/hook-target.js';
|
|
@@ -1156,6 +1157,24 @@ export function resolveInstallRoot(explicit, options = {}) {
|
|
|
1156
1157
|
throw Object.assign(new Error('Run from inside your HQ checkout, or pass --hq-root <dir>.'), { expected: true });
|
|
1157
1158
|
}
|
|
1158
1159
|
}
|
|
1160
|
+
function defaultPs1ShimDir() {
|
|
1161
|
+
return process.platform === 'win32' ? npmShimDir(process.argv[1], fs.existsSync) : null;
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* PowerShell picks npm's hq.ps1 over hq.cmd, and a Restricted execution policy
|
|
1165
|
+
* blocks it. Remove only npm's own hq-cli launchers; never change the policy.
|
|
1166
|
+
*/
|
|
1167
|
+
function removePs1ShimsReport(dir) {
|
|
1168
|
+
const result = removeHqCliPs1Shims(dir);
|
|
1169
|
+
const lines = [];
|
|
1170
|
+
if (result.removed.length > 0) {
|
|
1171
|
+
lines.push(` windows: removed ${result.removed.map((f) => path.win32.basename(f)).join(', ')} so PowerShell now runs hq.cmd.`);
|
|
1172
|
+
}
|
|
1173
|
+
for (const failure of result.failed) {
|
|
1174
|
+
lines.push(` windows: could not remove ${failure.file} (${failure.reason}). Run hq.cmd instead of hq in PowerShell.`);
|
|
1175
|
+
}
|
|
1176
|
+
return lines;
|
|
1177
|
+
}
|
|
1159
1178
|
/** Entry used by `hq install --global` (wired from pkg-install.ts). */
|
|
1160
1179
|
export async function runInstallGlobal(opts) {
|
|
1161
1180
|
const runtime = assertRuntime(opts.runtime);
|
|
@@ -1201,6 +1220,10 @@ export async function runInstallGlobal(opts) {
|
|
|
1201
1220
|
if (shimPath)
|
|
1202
1221
|
for (const line of shimPathWarning(shimPath))
|
|
1203
1222
|
(opts.log ?? console.log)(line);
|
|
1223
|
+
const ps1Dir = (opts.ps1ShimDir ?? defaultPs1ShimDir)();
|
|
1224
|
+
if (ps1Dir)
|
|
1225
|
+
for (const line of removePs1ShimsReport(ps1Dir))
|
|
1226
|
+
(opts.log ?? console.log)(line);
|
|
1204
1227
|
}
|
|
1205
1228
|
}
|
|
1206
1229
|
/**
|
|
@@ -1449,6 +1449,7 @@ export function registerConnectCommands(integrations) {
|
|
|
1449
1449
|
.option("--auth <mode>", "Force the auth mode: none, key, or oauth (default: detect)")
|
|
1450
1450
|
.option("--client-id <id>", "Client id of an OAuth app you registered with the provider yourself")
|
|
1451
1451
|
.option("--client-secret-stdin", "Read that app's client secret from stdin")
|
|
1452
|
+
.option("--scopes <list>", "Permissions to request, space- or comma-separated (default: the permissions the app was approved for last time)")
|
|
1452
1453
|
.option("--no-browser", "Print the sign-in URL instead of opening a browser")
|
|
1453
1454
|
.option("--timeout <seconds>", "How long to wait for a browser sign-in (default 300)")
|
|
1454
1455
|
.option("--connect", "For a revoked row, run `connect <domain>` to re-add and revive it")
|
|
@@ -26,6 +26,8 @@ interface Principal {
|
|
|
26
26
|
* guessing — a misparsed principal silently grants access to the wrong party.
|
|
27
27
|
*/
|
|
28
28
|
export declare function parsePrincipal(raw: string): Principal;
|
|
29
|
+
/** `--with` / `--to` / `--from` help. Groups resolve membership at call time. */
|
|
30
|
+
export declare const PRINCIPAL_HELP = "Email, uid, vault group (grp_\u2026), or `everyone`";
|
|
29
31
|
export declare function registerManageCommands(integrations: Command): void;
|
|
30
32
|
/** Order-insensitive identity of an allowlist, for change detection. */
|
|
31
33
|
export declare function allowlistFingerprint(grants: WriteAllowlistGrant[]): string;
|
|
@@ -76,6 +76,8 @@ export function parsePrincipal(raw) {
|
|
|
76
76
|
}
|
|
77
77
|
throw new IntegrationsCliError(`Could not tell what '${raw}' refers to. Use an email address, a uid (prs_… / agt_… / grp_…), \`everyone\`, or an explicit \`person:<id>\` / \`group:<id>\` form.`, { expected: true });
|
|
78
78
|
}
|
|
79
|
+
/** `--with` / `--to` / `--from` help. Groups resolve membership at call time. */
|
|
80
|
+
export const PRINCIPAL_HELP = "Email, uid, vault group (grp_…), or `everyone`";
|
|
79
81
|
/** How a principal reads back in output, without leaking a raw uid as a name. */
|
|
80
82
|
function describePrincipal(entry) {
|
|
81
83
|
if (entry.granteeType === "company-wide")
|
|
@@ -379,7 +381,7 @@ export function registerManageCommands(integrations) {
|
|
|
379
381
|
.option("--provider <slug>", "Connected app (e.g. linear)")
|
|
380
382
|
.option("--connection <id>", "Connection id (acct_…)")
|
|
381
383
|
.requiredOption("--tool <name>", "The tool to authorize")
|
|
382
|
-
.requiredOption("--to <principal>",
|
|
384
|
+
.requiredOption("--to <principal>", PRINCIPAL_HELP)
|
|
383
385
|
.option("--permission <level>", "read, write, or admin", "write")
|
|
384
386
|
.option("--json", "Machine-readable output")
|
|
385
387
|
.action(async (app, opts) => {
|
|
@@ -418,7 +420,7 @@ export function registerManageCommands(integrations) {
|
|
|
418
420
|
.option("--provider <slug>", "Connected app (e.g. linear)")
|
|
419
421
|
.option("--connection <id>", "Connection id (acct_…)")
|
|
420
422
|
.requiredOption("--tool <name>", "The tool to stop authorizing")
|
|
421
|
-
.requiredOption("--from <principal>",
|
|
423
|
+
.requiredOption("--from <principal>", PRINCIPAL_HELP)
|
|
422
424
|
.option("--json", "Machine-readable output")
|
|
423
425
|
.action(async (app, opts) => {
|
|
424
426
|
const principal = parsePrincipal(opts.from);
|
|
@@ -465,7 +467,7 @@ export function registerManageCommands(integrations) {
|
|
|
465
467
|
.option("--company <slug>", "Company slug, e.g. indigo")
|
|
466
468
|
.option("--provider <slug>", "Connected app (e.g. linear)")
|
|
467
469
|
.option("--connection <id>", "Connection id (acct_…)")
|
|
468
|
-
.requiredOption("--with <principal>",
|
|
470
|
+
.requiredOption("--with <principal>", PRINCIPAL_HELP)
|
|
469
471
|
.option("--permission <level>", "read, write, or admin", "write")
|
|
470
472
|
.option("--json", "Machine-readable output")
|
|
471
473
|
.action(async (app, opts) => {
|
|
@@ -495,7 +497,7 @@ export function registerManageCommands(integrations) {
|
|
|
495
497
|
.option("--company <slug>", "Company slug, e.g. indigo")
|
|
496
498
|
.option("--provider <slug>", "Connected app (e.g. linear)")
|
|
497
499
|
.option("--connection <id>", "Connection id (acct_…)")
|
|
498
|
-
.requiredOption("--from <principal>",
|
|
500
|
+
.requiredOption("--from <principal>", PRINCIPAL_HELP)
|
|
499
501
|
.option("--json", "Machine-readable output")
|
|
500
502
|
.action(async (app, opts) => {
|
|
501
503
|
const principal = parsePrincipal(opts.from);
|
|
@@ -14,6 +14,7 @@ function addLinkOptions(command) {
|
|
|
14
14
|
.option("--engine <engine>", "Session engine")
|
|
15
15
|
.option("--title <title>", "Session title")
|
|
16
16
|
.option("--host-session <id>", "Host session id")
|
|
17
|
+
.option("--claims <ids>", "Exact comma-separated recipient receipt IDs")
|
|
17
18
|
.option("--text <message>", "Message text")
|
|
18
19
|
.option("--text-file <path>", "Read message text from a file")
|
|
19
20
|
.option("--state <state>", "Child state")
|
|
@@ -35,7 +36,7 @@ function commandError(error) {
|
|
|
35
36
|
}
|
|
36
37
|
export function registerLanesLinkCommand(lanes) {
|
|
37
38
|
const link = lanes.command("link").description("Link live sessions to a conductor through session inboxes");
|
|
38
|
-
const commands = ["open", "join", "send", "report", "status", "inbox", "read", "wait", "list", "leave", "close"];
|
|
39
|
+
const commands = ["open", "join", "send", "ack", "capabilities", "report", "status", "inbox", "read", "wait", "list", "leave", "close"];
|
|
39
40
|
for (const subcommand of commands) {
|
|
40
41
|
addLinkOptions(link.command(subcommand)).action(async (options) => {
|
|
41
42
|
try {
|
|
@@ -56,7 +57,8 @@ export function registerLanesLinkCommand(lanes) {
|
|
|
56
57
|
}
|
|
57
58
|
const deliver = link.command("_deliver", { hidden: true })
|
|
58
59
|
.description("Internal linked-session hook delivery")
|
|
59
|
-
.requiredOption("--event <event>", "Hook event")
|
|
60
|
+
.requiredOption("--event <event>", "Hook event")
|
|
61
|
+
.option("--ack-mode <mode>", "Internal explicit recipient acknowledgement");
|
|
60
62
|
deliver.action(async (options) => {
|
|
61
63
|
// Hook commands must not update/re-exec the CLI inside the provider's time budget.
|
|
62
64
|
process.env.HQ_NO_UPDATE_CHECK = "1";
|
|
@@ -70,7 +72,7 @@ export function registerLanesLinkCommand(lanes) {
|
|
|
70
72
|
env: process.env,
|
|
71
73
|
defaultValue: true,
|
|
72
74
|
});
|
|
73
|
-
const result = await runLaneLinkWithFlag("_deliver", { hqRoot: root, env: process.env, event: options.event, input }, enabled);
|
|
75
|
+
const result = await runLaneLinkWithFlag("_deliver", { hqRoot: root, env: process.env, event: options.event, ackMode: options.ackMode, input }, enabled);
|
|
74
76
|
if (result.output)
|
|
75
77
|
writeJson(result.output);
|
|
76
78
|
}
|