@indigoai-us/hq-cli 5.345.159 → 5.345.161
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 +48 -0
- package/assets/scaffold/core/scripts/session-journal.sh +116 -24
- package/dist/command-catalog.generated.d.ts +222 -9
- package/dist/command-catalog.generated.js +273 -9
- package/dist/commands/access.d.ts +6 -0
- package/dist/commands/access.js +18 -1
- package/dist/commands/acl-lock.d.ts +41 -0
- package/dist/commands/acl-lock.js +28 -0
- package/dist/commands/agent-enroll.d.ts +2 -0
- package/dist/commands/agent-enroll.js +9 -4
- package/dist/commands/agents.js +5 -11
- package/dist/commands/auth.js +6 -5
- package/dist/commands/billing-sponsor.d.ts +4 -4
- package/dist/commands/billing-sponsor.js +11 -11
- package/dist/commands/billing-usage.d.ts +22 -8
- package/dist/commands/billing-usage.js +57 -22
- package/dist/commands/billing.d.ts +7 -7
- package/dist/commands/billing.js +13 -13
- package/dist/commands/cloud-provision.js +2 -2
- package/dist/commands/cloud.js +7 -7
- 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/db-remote-deps.js +12 -1
- 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/files.js +19 -7
- 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-session-start.d.ts +12 -0
- package/dist/commands/lanes-session-start.js +93 -12
- package/dist/commands/meetings.d.ts +15 -4
- package/dist/commands/meetings.js +58 -19
- package/dist/commands/skill.js +6 -6
- package/dist/commands/whoami.js +1 -1
- package/dist/hook-engine-path.js +13 -2
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +3236 -1988
- 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/billing/plan-limit-refusals.d.ts +13 -3
- package/dist/lib/billing/plan-limit-refusals.js +26 -3
- package/dist/lib/billing/plan-limit-upgrade-url.d.ts +8 -0
- package/dist/lib/billing/plan-limit-upgrade-url.js +18 -0
- package/dist/lib/billing/plan-lock.d.ts +22 -6
- package/dist/lib/billing/plan-lock.js +58 -30
- package/dist/lib/billing/usage-cost.d.ts +24 -2
- package/dist/lib/billing/usage-cost.js +103 -3
- package/dist/lib/billing/usage-only.d.ts +37 -1
- package/dist/lib/billing/usage-only.js +89 -4
- package/dist/lib/billing/usage-pause.d.ts +27 -0
- package/dist/lib/billing/usage-pause.js +50 -0
- 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/db/control-plane.d.ts +5 -0
- package/dist/lib/db/control-plane.js +10 -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/hook-engine/resolve.d.ts +14 -0
- package/dist/lib/hook-engine/resolve.js +55 -1
- package/dist/lib/lanes/links.d.ts +2 -1
- package/dist/lib/lanes/links.js +235 -17
- 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/plan-limit-nag.d.ts +0 -16
- package/dist/lib/plan-limit-nag.js +17 -18
- package/dist/lib/windows/ps1-shims.d.ts +60 -0
- package/dist/lib/windows/ps1-shims.js +152 -0
- package/dist/main.d.ts +0 -2
- package/dist/main.js +19 -57
- package/dist/utils/plan-gate-error.d.ts +30 -14
- package/dist/utils/plan-gate-error.js +60 -46
- 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/team-upgrade.d.ts +19 -6
- package/dist/utils/team-upgrade.js +37 -39
- 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);
|
|
@@ -74,7 +74,18 @@ export function describeRemoteError(error) {
|
|
|
74
74
|
return "Remote databases are not available yet.";
|
|
75
75
|
}
|
|
76
76
|
if (status === 402 || code === "PLAN_REQUIRED") {
|
|
77
|
-
|
|
77
|
+
// US-024: the server's usage pause message, else its own sentence. The
|
|
78
|
+
// CLI names no plan: the flat plans are closed.
|
|
79
|
+
const pause = error.usagePause;
|
|
80
|
+
if (pause && typeof pause.message === "string" && pause.message) {
|
|
81
|
+
return typeof pause.actionUrl === "string" && pause.actionUrl
|
|
82
|
+
? `${pause.message}\nBilling: ${pause.actionUrl}`
|
|
83
|
+
: pause.message;
|
|
84
|
+
}
|
|
85
|
+
const serverMessage = error.serverMessage;
|
|
86
|
+
if (typeof serverMessage === "string" && serverMessage)
|
|
87
|
+
return serverMessage;
|
|
88
|
+
return "Remote databases are not available on this company's current billing. Run `hq billing status` to check it.";
|
|
78
89
|
}
|
|
79
90
|
if (status === 403)
|
|
80
91
|
return "You are not a member of this company, or this app belongs to another company.";
|
|
@@ -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
|
}
|
package/dist/commands/files.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
+
import { aclLockedBy, describeAclLock } from "./acl-lock.js";
|
|
2
3
|
import open from "open";
|
|
3
4
|
import * as readline from "node:readline";
|
|
4
5
|
import { resolveVaultCredential } from "../utils/resolve-vault-credential.js";
|
|
@@ -489,17 +490,21 @@ export function registerFilesCommand(program) {
|
|
|
489
490
|
throw new Error('Invalid ACL tree response');
|
|
490
491
|
}
|
|
491
492
|
const row = tree.directRow;
|
|
493
|
+
// The most-specific row has no grants, so broader grants do not apply.
|
|
494
|
+
const lockedBy = aclLockedBy(tree);
|
|
492
495
|
if (opts.json) {
|
|
493
|
-
console.log(JSON.stringify({ ...tree, schemaVersion: 1, companyUid, exists: row !== null }, null, 2));
|
|
496
|
+
console.log(JSON.stringify({ ...tree, schemaVersion: 1, companyUid, exists: row !== null, lockedBy }, null, 2));
|
|
494
497
|
return;
|
|
495
498
|
}
|
|
496
499
|
// No own row AND nothing inherited or granted below — preserve the
|
|
497
500
|
// original "no ACL record" exit path.
|
|
498
|
-
if (!row && tree.inherited.length === 0 && tree.children.length === 0) {
|
|
501
|
+
if (!row && !lockedBy && tree.inherited.length === 0 && tree.children.length === 0) {
|
|
499
502
|
console.error(chalk.red(`No ACL record exists for '${canonicalPrefix}'`));
|
|
500
503
|
process.exit(1);
|
|
501
504
|
}
|
|
502
|
-
const aclStatus =
|
|
505
|
+
const aclStatus = lockedBy
|
|
506
|
+
? lockedBy.readUnknown ? "locked unless open" : "locked"
|
|
507
|
+
: row?.open ? "open" : "restricted";
|
|
503
508
|
console.log(chalk.green(`ACL for ${displayAclPattern(tree.prefix)} (${aclStatus})`));
|
|
504
509
|
if (isPrivateFolderPattern(tree.prefix)) {
|
|
505
510
|
console.log(chalk.gray(privateFolderExplanation(tree.prefix)));
|
|
@@ -507,9 +512,12 @@ export function registerFilesCommand(program) {
|
|
|
507
512
|
if (row) {
|
|
508
513
|
console.log(`Creator: ${row.creatorUid}`);
|
|
509
514
|
}
|
|
510
|
-
else {
|
|
515
|
+
else if (!lockedBy) {
|
|
511
516
|
console.log(chalk.gray("No direct ACL row — access flows from the inherited/descendant grants below."));
|
|
512
517
|
}
|
|
518
|
+
if (lockedBy) {
|
|
519
|
+
console.log(chalk.yellow(describeAclLock(lockedBy, displayAclPattern(lockedBy.path))));
|
|
520
|
+
}
|
|
513
521
|
if (tree.effectivePermission) {
|
|
514
522
|
console.log(`Your effective permission: ${tree.effectivePermission}`);
|
|
515
523
|
}
|
|
@@ -560,9 +568,9 @@ export function registerFilesCommand(program) {
|
|
|
560
568
|
const directEntries = tree.direct;
|
|
561
569
|
if (directEntries.length === 0) {
|
|
562
570
|
if (row?.open) {
|
|
563
|
-
console.log(chalk.gray("
|
|
571
|
+
console.log(chalk.gray("No grants on this row (open: broader grants still apply). All active members have at least read access."));
|
|
564
572
|
}
|
|
565
|
-
else if (row) {
|
|
573
|
+
else if (row && !lockedBy) {
|
|
566
574
|
console.log(chalk.gray("No explicit grants on this prefix — only creator has access."));
|
|
567
575
|
}
|
|
568
576
|
}
|
|
@@ -571,7 +579,11 @@ export function registerFilesCommand(program) {
|
|
|
571
579
|
printEntryTable(directEntries, false);
|
|
572
580
|
}
|
|
573
581
|
if (tree.inherited.length > 0) {
|
|
574
|
-
console.log(
|
|
582
|
+
console.log(lockedBy
|
|
583
|
+
? lockedBy.readUnknown
|
|
584
|
+
? `\nGranted on ancestor prefixes (apply only if ${displayAclPattern(lockedBy.path)} is open):`
|
|
585
|
+
: `\nGranted on ancestor prefixes (do not apply: locked by ${displayAclPattern(lockedBy.path)}):`
|
|
586
|
+
: "\nInherited (granted on an ancestor prefix):");
|
|
575
587
|
printEntryTable(tree.inherited, true);
|
|
576
588
|
}
|
|
577
589
|
if (tree.children.length > 0) {
|
|
@@ -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;
|