@indigoai-us/hq-cli 5.345.119 → 5.345.121
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 +14 -0
- package/assets/scaffold/core/scripts/session-journal.sh +67 -5
- package/dist/command-catalog.generated.d.ts +169 -0
- package/dist/command-catalog.generated.js +199 -0
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/capture.d.ts +25 -0
- package/dist/commands/capture.js +95 -0
- package/dist/commands/core-session-journal-native.js +142 -12
- package/dist/commands/dm.js +23 -3
- package/dist/utils/vault-api.d.ts +10 -0
- package/dist/utils/vault-api.js +27 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,11 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.121] — 2026-10-13
|
|
6
|
+
- `hq capture` ships in this release. It was listed under 5.345.120, but that tag was cut before the change merged, so 5.345.120 does not include it. `hq capture` drives HQ Capture, the hq-capture pack's screen recorder: `doctor`, `build`, `start [--record]`, `record`, `status`, `wait`, `stop`, `run` and `resume` run the pack's launcher from the HQ root with the arguments unchanged and pass its JSON and exit code through. `hq capture record` starts a recording without anyone pressing Start (pack 0.1.3 or later).
|
|
7
|
+
|
|
8
|
+
## [5.345.120] — 2026-10-13
|
|
9
|
+
- Add `hq capture` to drive HQ Capture, the hq-capture pack's screen recorder: `doctor`, `build`, `start [--record]`, `record`, `status`, `wait`, `stop`, `run` and `resume` run the pack's launcher from the HQ root with the arguments unchanged and pass its JSON and exit code through. `hq capture record` starts a recording without anyone pressing Start (pack 0.1.3 or later).
|
|
10
|
+
|
|
11
|
+
- `hq dm forward --company <slug|cmp_id>` names the company a person or agent
|
|
12
|
+
destination belongs to. The CLI sends it as `companyUid`, so the server runs
|
|
13
|
+
its destination-side cross-company check. Without it the destination is
|
|
14
|
+
treated as personal. `--company` is refused for channel destinations, which
|
|
15
|
+
use the channel's own company.
|
|
16
|
+
|
|
5
17
|
## [5.345.119] — 2026-10-13
|
|
6
18
|
- The checkpoint sibling no longer falls back to Codex. `--backend auto` now picks only Claude or grok. Codex runs only with `--backend codex` or the new `HQ_CHECKPOINT_BACKEND=codex` setting (process environment or the HQ settings file env block). A sibling run that left no report stops counting as a crash after 6 hours, so one transient failure no longer pins the choice. When no allowed backend is available the sibling is skipped with a log line.
|
|
7
19
|
- Add native TypeScript hq core pipeline conductor and driver commands with the bundled shell companions as the flag-off fallback.
|
|
8
20
|
- Fix native policy retirement reports for partly stale references, accept shell-written newline-terminated trigger cooldown stamps, and resolve derive-trigger-facts session metadata from `HQ_ROOT` when the caller cwd differs, including repo detection for Windows path separators.
|
|
9
21
|
|
|
22
|
+
- `hq core session-journal tool-counter` now skips updates when its bounded lock retries are exhausted and reclaims stale counter locks like the shell implementation.
|
|
23
|
+
|
|
10
24
|
## [5.345.118] — 2026-10-13
|
|
11
25
|
|
|
12
26
|
- Remove the bundled worker-skill shell fallbacks. `hq core worker lint` and
|
|
@@ -128,12 +128,57 @@ resolve_session_key() {
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
# Read-modify-write on the counter must not interleave across concurrent tasks.
|
|
131
|
+
counter_lock_is_aged() {
|
|
132
|
+
local lockdir="$1" lock_mtime now
|
|
133
|
+
lock_mtime="$(stat -c %Y "$lockdir" 2>/dev/null)" || lock_mtime="$(stat -f %m "$lockdir" 2>/dev/null)" || return 1
|
|
134
|
+
now="$(date -u +%s 2>/dev/null)" || return 1
|
|
135
|
+
case "$lock_mtime:$now" in *[!0-9:]*|:*|*:) return 1 ;; esac
|
|
136
|
+
[ "$now" -ge "$lock_mtime" ] && [ "$((now - lock_mtime))" -ge 60 ]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
counter_windows_pid_is_alive() {
|
|
140
|
+
local pid="$1" output
|
|
141
|
+
case "${OSTYPE:-}" in msys*|cygwin*|win32*) ;; *) return 1 ;; esac
|
|
142
|
+
case "$pid" in ''|*[!0-9]*) return 1 ;; esac
|
|
143
|
+
output="$(MSYS_NO_PATHCONV=1 tasklist.exe /FO CSV /NH /FI "PID eq $pid" 2>/dev/null)" || return 1
|
|
144
|
+
printf '%s\n' "$output" | tr -d '\r"' | cut -d, -f2 | grep -Fxq "$pid"
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
counter_lock_owner_is_alive() {
|
|
148
|
+
local lockdir="$1" owner_pid="$2" windows_pid
|
|
149
|
+
kill -0 "$owner_pid" 2>/dev/null && return 0
|
|
150
|
+
windows_pid="$(cat "$lockdir/winpid" 2>/dev/null || true)"
|
|
151
|
+
if [ -n "$windows_pid" ]; then
|
|
152
|
+
counter_windows_pid_is_alive "$windows_pid" && return 0
|
|
153
|
+
elif counter_windows_pid_is_alive "$owner_pid"; then
|
|
154
|
+
# Old native clients wrote only a Win32 process.pid to pid.
|
|
155
|
+
return 0
|
|
156
|
+
fi
|
|
157
|
+
return 1
|
|
158
|
+
}
|
|
159
|
+
|
|
131
160
|
reclaim_stale_counter_lock() {
|
|
132
|
-
local lockdir="$1" owner_pid current_owner claim stale_dir suffix=0
|
|
161
|
+
local lockdir="$1" owner_pid current_owner current_windows_pid windows_pid claim stale_dir suffix=0 pidless=0
|
|
133
162
|
owner_pid="$(cat "$lockdir/pid" 2>/dev/null || true)"
|
|
134
|
-
case "$owner_pid" in
|
|
135
|
-
|
|
136
|
-
|
|
163
|
+
case "$owner_pid" in
|
|
164
|
+
'')
|
|
165
|
+
windows_pid="$(cat "$lockdir/winpid" 2>/dev/null || true)"
|
|
166
|
+
if [ -n "$windows_pid" ]; then
|
|
167
|
+
counter_windows_pid_is_alive "$windows_pid" && return 1
|
|
168
|
+
fi
|
|
169
|
+
# The complete counter retry window is 5 seconds (250 x 20 ms). A
|
|
170
|
+
# PID-less lock is reclaimable after one minute, twelve retry windows,
|
|
171
|
+
# allowing for a creator paused between mkdir and writing pid. The stat
|
|
172
|
+
# fallback keeps this elapsed-time threshold consistent on GNU/BSD find.
|
|
173
|
+
counter_lock_is_aged "$lockdir" || return 1
|
|
174
|
+
pidless=1
|
|
175
|
+
;;
|
|
176
|
+
*[!0-9]*) return 1 ;;
|
|
177
|
+
*)
|
|
178
|
+
[ "$owner_pid" -gt 1 ] 2>/dev/null || return 1
|
|
179
|
+
counter_lock_owner_is_alive "$lockdir" "$owner_pid" && return 1
|
|
180
|
+
;;
|
|
181
|
+
esac
|
|
137
182
|
|
|
138
183
|
claim="${lockdir}.reclaim"
|
|
139
184
|
mkdir "$claim" 2>/dev/null || return 1
|
|
@@ -143,7 +188,19 @@ reclaim_stale_counter_lock() {
|
|
|
143
188
|
fi
|
|
144
189
|
|
|
145
190
|
current_owner="$(cat "$lockdir/pid" 2>/dev/null || true)"
|
|
146
|
-
|
|
191
|
+
current_windows_pid="$(cat "$lockdir/winpid" 2>/dev/null || true)"
|
|
192
|
+
if [ "$pidless" -eq 1 ]; then
|
|
193
|
+
# Recheck both identity and age after winning the claim, before renaming.
|
|
194
|
+
if [ -z "$current_owner" ] && [ "$current_windows_pid" = "$windows_pid" ] \
|
|
195
|
+
&& { [ -z "$current_windows_pid" ] || ! counter_windows_pid_is_alive "$current_windows_pid"; } \
|
|
196
|
+
&& counter_lock_is_aged "$lockdir"; then
|
|
197
|
+
owner_pid=""
|
|
198
|
+
else
|
|
199
|
+
owner_pid="changed"
|
|
200
|
+
fi
|
|
201
|
+
fi
|
|
202
|
+
if [ "$current_owner" = "$owner_pid" ] && [ "$current_windows_pid" = "${windows_pid:-$(cat "$lockdir/winpid" 2>/dev/null || true)}" ] \
|
|
203
|
+
&& { [ "$pidless" -eq 1 ] || ! counter_lock_owner_is_alive "$lockdir" "$owner_pid"; }; then
|
|
147
204
|
stale_dir="${lockdir}.stale.$$.$RANDOM"
|
|
148
205
|
while [ -e "$stale_dir" ]; do
|
|
149
206
|
suffix=$((suffix + 1))
|
|
@@ -167,6 +224,11 @@ with_counter_lock() {
|
|
|
167
224
|
while [ "$i" -lt "$tries" ]; do
|
|
168
225
|
if mkdir "$lockdir" 2>/dev/null; then
|
|
169
226
|
acquired=1
|
|
227
|
+
if [[ "${OSTYPE:-}" == msys* ]] || [[ "${OSTYPE:-}" == cygwin* ]]; then
|
|
228
|
+
local windows_pid
|
|
229
|
+
windows_pid="$(cat "/proc/$$/winpid" 2>/dev/null || true)"
|
|
230
|
+
case "$windows_pid" in ''|*[!0-9]*) ;; *) printf '%s\n' "$windows_pid" > "$lockdir/winpid" || { rm -rf "$lockdir" 2>/dev/null || true; return 0; } ;; esac
|
|
231
|
+
fi
|
|
170
232
|
if ! printf '%s\n' "$$" > "$lockdir/pid" 2>/dev/null; then
|
|
171
233
|
rm -rf "$lockdir" 2>/dev/null || true
|
|
172
234
|
return 0
|
|
@@ -1352,6 +1352,171 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
1352
1352
|
readonly hidden: false;
|
|
1353
1353
|
}];
|
|
1354
1354
|
readonly subcommands: readonly [];
|
|
1355
|
+
}, {
|
|
1356
|
+
readonly name: "capture";
|
|
1357
|
+
readonly description: "Record your screen with HQ Capture: start, record, stop, status and wait";
|
|
1358
|
+
readonly aliases: readonly [];
|
|
1359
|
+
readonly hidden: false;
|
|
1360
|
+
readonly helpOption: true;
|
|
1361
|
+
readonly helpCommand: true;
|
|
1362
|
+
readonly usage: "[options] [command]";
|
|
1363
|
+
readonly helpText: readonly [];
|
|
1364
|
+
readonly arguments: readonly [];
|
|
1365
|
+
readonly options: readonly [];
|
|
1366
|
+
readonly subcommands: readonly [{
|
|
1367
|
+
readonly name: "doctor";
|
|
1368
|
+
readonly description: "Check this Mac can build and run HQ Capture";
|
|
1369
|
+
readonly aliases: readonly [];
|
|
1370
|
+
readonly hidden: false;
|
|
1371
|
+
readonly helpOption: false;
|
|
1372
|
+
readonly helpCommand: false;
|
|
1373
|
+
readonly usage: "[args...]";
|
|
1374
|
+
readonly helpText: readonly [];
|
|
1375
|
+
readonly arguments: readonly [{
|
|
1376
|
+
readonly name: "args";
|
|
1377
|
+
readonly required: false;
|
|
1378
|
+
readonly variadic: true;
|
|
1379
|
+
readonly description: "";
|
|
1380
|
+
}];
|
|
1381
|
+
readonly options: readonly [];
|
|
1382
|
+
readonly subcommands: readonly [];
|
|
1383
|
+
}, {
|
|
1384
|
+
readonly name: "build";
|
|
1385
|
+
readonly description: "Build HQ Capture if missing or out of date ([--force])";
|
|
1386
|
+
readonly aliases: readonly [];
|
|
1387
|
+
readonly hidden: false;
|
|
1388
|
+
readonly helpOption: false;
|
|
1389
|
+
readonly helpCommand: false;
|
|
1390
|
+
readonly usage: "[args...]";
|
|
1391
|
+
readonly helpText: readonly [];
|
|
1392
|
+
readonly arguments: readonly [{
|
|
1393
|
+
readonly name: "args";
|
|
1394
|
+
readonly required: false;
|
|
1395
|
+
readonly variadic: true;
|
|
1396
|
+
readonly description: "";
|
|
1397
|
+
}];
|
|
1398
|
+
readonly options: readonly [];
|
|
1399
|
+
readonly subcommands: readonly [];
|
|
1400
|
+
}, {
|
|
1401
|
+
readonly name: "start";
|
|
1402
|
+
readonly description: "Open HQ Capture for the session's company ([--company <slug>] [--record])";
|
|
1403
|
+
readonly aliases: readonly [];
|
|
1404
|
+
readonly hidden: false;
|
|
1405
|
+
readonly helpOption: false;
|
|
1406
|
+
readonly helpCommand: false;
|
|
1407
|
+
readonly usage: "[args...]";
|
|
1408
|
+
readonly helpText: readonly [];
|
|
1409
|
+
readonly arguments: readonly [{
|
|
1410
|
+
readonly name: "args";
|
|
1411
|
+
readonly required: false;
|
|
1412
|
+
readonly variadic: true;
|
|
1413
|
+
readonly description: "";
|
|
1414
|
+
}];
|
|
1415
|
+
readonly options: readonly [];
|
|
1416
|
+
readonly subcommands: readonly [];
|
|
1417
|
+
}, {
|
|
1418
|
+
readonly name: "record";
|
|
1419
|
+
readonly description: "Start recording in the open HQ Capture, as if Start was pressed";
|
|
1420
|
+
readonly aliases: readonly [];
|
|
1421
|
+
readonly hidden: false;
|
|
1422
|
+
readonly helpOption: false;
|
|
1423
|
+
readonly helpCommand: false;
|
|
1424
|
+
readonly usage: "[args...]";
|
|
1425
|
+
readonly helpText: readonly [];
|
|
1426
|
+
readonly arguments: readonly [{
|
|
1427
|
+
readonly name: "args";
|
|
1428
|
+
readonly required: false;
|
|
1429
|
+
readonly variadic: true;
|
|
1430
|
+
readonly description: "";
|
|
1431
|
+
}];
|
|
1432
|
+
readonly options: readonly [];
|
|
1433
|
+
readonly subcommands: readonly [];
|
|
1434
|
+
}, {
|
|
1435
|
+
readonly name: "status";
|
|
1436
|
+
readonly description: "Print HQ Capture's current status as JSON";
|
|
1437
|
+
readonly aliases: readonly [];
|
|
1438
|
+
readonly hidden: false;
|
|
1439
|
+
readonly helpOption: false;
|
|
1440
|
+
readonly helpCommand: false;
|
|
1441
|
+
readonly usage: "[args...]";
|
|
1442
|
+
readonly helpText: readonly [];
|
|
1443
|
+
readonly arguments: readonly [{
|
|
1444
|
+
readonly name: "args";
|
|
1445
|
+
readonly required: false;
|
|
1446
|
+
readonly variadic: true;
|
|
1447
|
+
readonly description: "";
|
|
1448
|
+
}];
|
|
1449
|
+
readonly options: readonly [];
|
|
1450
|
+
readonly subcommands: readonly [];
|
|
1451
|
+
}, {
|
|
1452
|
+
readonly name: "wait";
|
|
1453
|
+
readonly description: "Wait until the recording is saved, failed or closed ([--timeout <seconds>])";
|
|
1454
|
+
readonly aliases: readonly [];
|
|
1455
|
+
readonly hidden: false;
|
|
1456
|
+
readonly helpOption: false;
|
|
1457
|
+
readonly helpCommand: false;
|
|
1458
|
+
readonly usage: "[args...]";
|
|
1459
|
+
readonly helpText: readonly [];
|
|
1460
|
+
readonly arguments: readonly [{
|
|
1461
|
+
readonly name: "args";
|
|
1462
|
+
readonly required: false;
|
|
1463
|
+
readonly variadic: true;
|
|
1464
|
+
readonly description: "";
|
|
1465
|
+
}];
|
|
1466
|
+
readonly options: readonly [];
|
|
1467
|
+
readonly subcommands: readonly [];
|
|
1468
|
+
}, {
|
|
1469
|
+
readonly name: "stop";
|
|
1470
|
+
readonly description: "Stop HQ Capture, saving a recording in progress";
|
|
1471
|
+
readonly aliases: readonly [];
|
|
1472
|
+
readonly hidden: false;
|
|
1473
|
+
readonly helpOption: false;
|
|
1474
|
+
readonly helpCommand: false;
|
|
1475
|
+
readonly usage: "[args...]";
|
|
1476
|
+
readonly helpText: readonly [];
|
|
1477
|
+
readonly arguments: readonly [{
|
|
1478
|
+
readonly name: "args";
|
|
1479
|
+
readonly required: false;
|
|
1480
|
+
readonly variadic: true;
|
|
1481
|
+
readonly description: "";
|
|
1482
|
+
}];
|
|
1483
|
+
readonly options: readonly [];
|
|
1484
|
+
readonly subcommands: readonly [];
|
|
1485
|
+
}, {
|
|
1486
|
+
readonly name: "run";
|
|
1487
|
+
readonly description: "Run an agent command on a saved recording (<mode> <package> [args])";
|
|
1488
|
+
readonly aliases: readonly [];
|
|
1489
|
+
readonly hidden: false;
|
|
1490
|
+
readonly helpOption: false;
|
|
1491
|
+
readonly helpCommand: false;
|
|
1492
|
+
readonly usage: "[args...]";
|
|
1493
|
+
readonly helpText: readonly [];
|
|
1494
|
+
readonly arguments: readonly [{
|
|
1495
|
+
readonly name: "args";
|
|
1496
|
+
readonly required: false;
|
|
1497
|
+
readonly variadic: true;
|
|
1498
|
+
readonly description: "";
|
|
1499
|
+
}];
|
|
1500
|
+
readonly options: readonly [];
|
|
1501
|
+
readonly subcommands: readonly [];
|
|
1502
|
+
}, {
|
|
1503
|
+
readonly name: "resume";
|
|
1504
|
+
readonly description: "Check a pasted resume prompt and print its package (<prompt>)";
|
|
1505
|
+
readonly aliases: readonly [];
|
|
1506
|
+
readonly hidden: false;
|
|
1507
|
+
readonly helpOption: false;
|
|
1508
|
+
readonly helpCommand: false;
|
|
1509
|
+
readonly usage: "[args...]";
|
|
1510
|
+
readonly helpText: readonly [];
|
|
1511
|
+
readonly arguments: readonly [{
|
|
1512
|
+
readonly name: "args";
|
|
1513
|
+
readonly required: false;
|
|
1514
|
+
readonly variadic: true;
|
|
1515
|
+
readonly description: "";
|
|
1516
|
+
}];
|
|
1517
|
+
readonly options: readonly [];
|
|
1518
|
+
readonly subcommands: readonly [];
|
|
1519
|
+
}];
|
|
1355
1520
|
}, {
|
|
1356
1521
|
readonly name: "auth";
|
|
1357
1522
|
readonly description: "Manage the local HQ Cognito session";
|
|
@@ -3611,6 +3776,10 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
3611
3776
|
readonly flags: "--cross-company";
|
|
3612
3777
|
readonly description: "Confirm forwarding across company boundaries";
|
|
3613
3778
|
readonly hidden: false;
|
|
3779
|
+
}, {
|
|
3780
|
+
readonly flags: "--company <slug|cmp_id>";
|
|
3781
|
+
readonly description: "Company the person or agent destination belongs to, so cross-company rules apply";
|
|
3782
|
+
readonly hidden: false;
|
|
3614
3783
|
}];
|
|
3615
3784
|
readonly subcommands: readonly [];
|
|
3616
3785
|
}, {
|
|
@@ -1621,6 +1621,200 @@ export const COMMAND_CATALOG = [
|
|
|
1621
1621
|
],
|
|
1622
1622
|
"subcommands": []
|
|
1623
1623
|
},
|
|
1624
|
+
{
|
|
1625
|
+
"name": "capture",
|
|
1626
|
+
"description": "Record your screen with HQ Capture: start, record, stop, status and wait",
|
|
1627
|
+
"aliases": [],
|
|
1628
|
+
"hidden": false,
|
|
1629
|
+
"helpOption": true,
|
|
1630
|
+
"helpCommand": true,
|
|
1631
|
+
"usage": "[options] [command]",
|
|
1632
|
+
"helpText": [],
|
|
1633
|
+
"arguments": [],
|
|
1634
|
+
"options": [],
|
|
1635
|
+
"subcommands": [
|
|
1636
|
+
{
|
|
1637
|
+
"name": "doctor",
|
|
1638
|
+
"description": "Check this Mac can build and run HQ Capture",
|
|
1639
|
+
"aliases": [],
|
|
1640
|
+
"hidden": false,
|
|
1641
|
+
"helpOption": false,
|
|
1642
|
+
"helpCommand": false,
|
|
1643
|
+
"usage": "[args...]",
|
|
1644
|
+
"helpText": [],
|
|
1645
|
+
"arguments": [
|
|
1646
|
+
{
|
|
1647
|
+
"name": "args",
|
|
1648
|
+
"required": false,
|
|
1649
|
+
"variadic": true,
|
|
1650
|
+
"description": ""
|
|
1651
|
+
}
|
|
1652
|
+
],
|
|
1653
|
+
"options": [],
|
|
1654
|
+
"subcommands": []
|
|
1655
|
+
},
|
|
1656
|
+
{
|
|
1657
|
+
"name": "build",
|
|
1658
|
+
"description": "Build HQ Capture if missing or out of date ([--force])",
|
|
1659
|
+
"aliases": [],
|
|
1660
|
+
"hidden": false,
|
|
1661
|
+
"helpOption": false,
|
|
1662
|
+
"helpCommand": false,
|
|
1663
|
+
"usage": "[args...]",
|
|
1664
|
+
"helpText": [],
|
|
1665
|
+
"arguments": [
|
|
1666
|
+
{
|
|
1667
|
+
"name": "args",
|
|
1668
|
+
"required": false,
|
|
1669
|
+
"variadic": true,
|
|
1670
|
+
"description": ""
|
|
1671
|
+
}
|
|
1672
|
+
],
|
|
1673
|
+
"options": [],
|
|
1674
|
+
"subcommands": []
|
|
1675
|
+
},
|
|
1676
|
+
{
|
|
1677
|
+
"name": "start",
|
|
1678
|
+
"description": "Open HQ Capture for the session's company ([--company <slug>] [--record])",
|
|
1679
|
+
"aliases": [],
|
|
1680
|
+
"hidden": false,
|
|
1681
|
+
"helpOption": false,
|
|
1682
|
+
"helpCommand": false,
|
|
1683
|
+
"usage": "[args...]",
|
|
1684
|
+
"helpText": [],
|
|
1685
|
+
"arguments": [
|
|
1686
|
+
{
|
|
1687
|
+
"name": "args",
|
|
1688
|
+
"required": false,
|
|
1689
|
+
"variadic": true,
|
|
1690
|
+
"description": ""
|
|
1691
|
+
}
|
|
1692
|
+
],
|
|
1693
|
+
"options": [],
|
|
1694
|
+
"subcommands": []
|
|
1695
|
+
},
|
|
1696
|
+
{
|
|
1697
|
+
"name": "record",
|
|
1698
|
+
"description": "Start recording in the open HQ Capture, as if Start was pressed",
|
|
1699
|
+
"aliases": [],
|
|
1700
|
+
"hidden": false,
|
|
1701
|
+
"helpOption": false,
|
|
1702
|
+
"helpCommand": false,
|
|
1703
|
+
"usage": "[args...]",
|
|
1704
|
+
"helpText": [],
|
|
1705
|
+
"arguments": [
|
|
1706
|
+
{
|
|
1707
|
+
"name": "args",
|
|
1708
|
+
"required": false,
|
|
1709
|
+
"variadic": true,
|
|
1710
|
+
"description": ""
|
|
1711
|
+
}
|
|
1712
|
+
],
|
|
1713
|
+
"options": [],
|
|
1714
|
+
"subcommands": []
|
|
1715
|
+
},
|
|
1716
|
+
{
|
|
1717
|
+
"name": "status",
|
|
1718
|
+
"description": "Print HQ Capture's current status as JSON",
|
|
1719
|
+
"aliases": [],
|
|
1720
|
+
"hidden": false,
|
|
1721
|
+
"helpOption": false,
|
|
1722
|
+
"helpCommand": false,
|
|
1723
|
+
"usage": "[args...]",
|
|
1724
|
+
"helpText": [],
|
|
1725
|
+
"arguments": [
|
|
1726
|
+
{
|
|
1727
|
+
"name": "args",
|
|
1728
|
+
"required": false,
|
|
1729
|
+
"variadic": true,
|
|
1730
|
+
"description": ""
|
|
1731
|
+
}
|
|
1732
|
+
],
|
|
1733
|
+
"options": [],
|
|
1734
|
+
"subcommands": []
|
|
1735
|
+
},
|
|
1736
|
+
{
|
|
1737
|
+
"name": "wait",
|
|
1738
|
+
"description": "Wait until the recording is saved, failed or closed ([--timeout <seconds>])",
|
|
1739
|
+
"aliases": [],
|
|
1740
|
+
"hidden": false,
|
|
1741
|
+
"helpOption": false,
|
|
1742
|
+
"helpCommand": false,
|
|
1743
|
+
"usage": "[args...]",
|
|
1744
|
+
"helpText": [],
|
|
1745
|
+
"arguments": [
|
|
1746
|
+
{
|
|
1747
|
+
"name": "args",
|
|
1748
|
+
"required": false,
|
|
1749
|
+
"variadic": true,
|
|
1750
|
+
"description": ""
|
|
1751
|
+
}
|
|
1752
|
+
],
|
|
1753
|
+
"options": [],
|
|
1754
|
+
"subcommands": []
|
|
1755
|
+
},
|
|
1756
|
+
{
|
|
1757
|
+
"name": "stop",
|
|
1758
|
+
"description": "Stop HQ Capture, saving a recording in progress",
|
|
1759
|
+
"aliases": [],
|
|
1760
|
+
"hidden": false,
|
|
1761
|
+
"helpOption": false,
|
|
1762
|
+
"helpCommand": false,
|
|
1763
|
+
"usage": "[args...]",
|
|
1764
|
+
"helpText": [],
|
|
1765
|
+
"arguments": [
|
|
1766
|
+
{
|
|
1767
|
+
"name": "args",
|
|
1768
|
+
"required": false,
|
|
1769
|
+
"variadic": true,
|
|
1770
|
+
"description": ""
|
|
1771
|
+
}
|
|
1772
|
+
],
|
|
1773
|
+
"options": [],
|
|
1774
|
+
"subcommands": []
|
|
1775
|
+
},
|
|
1776
|
+
{
|
|
1777
|
+
"name": "run",
|
|
1778
|
+
"description": "Run an agent command on a saved recording (<mode> <package> [args])",
|
|
1779
|
+
"aliases": [],
|
|
1780
|
+
"hidden": false,
|
|
1781
|
+
"helpOption": false,
|
|
1782
|
+
"helpCommand": false,
|
|
1783
|
+
"usage": "[args...]",
|
|
1784
|
+
"helpText": [],
|
|
1785
|
+
"arguments": [
|
|
1786
|
+
{
|
|
1787
|
+
"name": "args",
|
|
1788
|
+
"required": false,
|
|
1789
|
+
"variadic": true,
|
|
1790
|
+
"description": ""
|
|
1791
|
+
}
|
|
1792
|
+
],
|
|
1793
|
+
"options": [],
|
|
1794
|
+
"subcommands": []
|
|
1795
|
+
},
|
|
1796
|
+
{
|
|
1797
|
+
"name": "resume",
|
|
1798
|
+
"description": "Check a pasted resume prompt and print its package (<prompt>)",
|
|
1799
|
+
"aliases": [],
|
|
1800
|
+
"hidden": false,
|
|
1801
|
+
"helpOption": false,
|
|
1802
|
+
"helpCommand": false,
|
|
1803
|
+
"usage": "[args...]",
|
|
1804
|
+
"helpText": [],
|
|
1805
|
+
"arguments": [
|
|
1806
|
+
{
|
|
1807
|
+
"name": "args",
|
|
1808
|
+
"required": false,
|
|
1809
|
+
"variadic": true,
|
|
1810
|
+
"description": ""
|
|
1811
|
+
}
|
|
1812
|
+
],
|
|
1813
|
+
"options": [],
|
|
1814
|
+
"subcommands": []
|
|
1815
|
+
}
|
|
1816
|
+
]
|
|
1817
|
+
},
|
|
1624
1818
|
{
|
|
1625
1819
|
"name": "auth",
|
|
1626
1820
|
"description": "Manage the local HQ Cognito session",
|
|
@@ -4370,6 +4564,11 @@ export const COMMAND_CATALOG = [
|
|
|
4370
4564
|
"flags": "--cross-company",
|
|
4371
4565
|
"description": "Confirm forwarding across company boundaries",
|
|
4372
4566
|
"hidden": false
|
|
4567
|
+
},
|
|
4568
|
+
{
|
|
4569
|
+
"flags": "--company <slug|cmp_id>",
|
|
4570
|
+
"description": "Company the person or agent destination belongs to, so cross-company rules apply",
|
|
4571
|
+
"hidden": false
|
|
4373
4572
|
}
|
|
4374
4573
|
],
|
|
4375
4574
|
"subcommands": []
|
|
@@ -213,6 +213,12 @@ export declare const REGISTRATION_PLAN: readonly [{
|
|
|
213
213
|
readonly parent: "program";
|
|
214
214
|
readonly module: "./commands/version.js";
|
|
215
215
|
readonly exportName: "registerVersionCommand";
|
|
216
|
+
}, {
|
|
217
|
+
readonly type: "registrar";
|
|
218
|
+
readonly root: "capture";
|
|
219
|
+
readonly parent: "program";
|
|
220
|
+
readonly module: "./commands/capture.js";
|
|
221
|
+
readonly exportName: "registerCaptureCommand";
|
|
216
222
|
}, {
|
|
217
223
|
readonly type: "registrar";
|
|
218
224
|
readonly root: "auth";
|
|
@@ -38,6 +38,7 @@ export const REGISTRATION_PLAN = [
|
|
|
38
38
|
{ type: "registrar", root: "resolve-company", parent: "program", module: "./commands/link.js", exportName: "registerResolveCompanyCommand" },
|
|
39
39
|
{ type: "registrar", root: "provider", parent: "program", module: "./commands/provider.js", exportName: "registerProviderCommand" },
|
|
40
40
|
{ type: "registrar", root: "version", parent: "program", module: "./commands/version.js", exportName: "registerVersionCommand" },
|
|
41
|
+
{ type: "registrar", root: "capture", parent: "program", module: "./commands/capture.js", exportName: "registerCaptureCommand" },
|
|
41
42
|
{ type: "registrar", root: "auth", parent: "program", module: "./commands/auth.js", exportName: "registerAuthCommands" },
|
|
42
43
|
{ type: "registrar", root: "secrets", parent: "program", module: "./commands/secrets.js", exportName: "registerSecretsCommand" },
|
|
43
44
|
{ type: "registrar", root: "db", parent: "program", module: "./commands/db.js", exportName: "registerDbCommand" },
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hq capture — drive HQ Capture (the hq-capture pack's screen recorder) from the CLI.
|
|
3
|
+
*
|
|
4
|
+
* Every subcommand runs the pack's launcher, `hq-record-process.sh`, from the HQ root with
|
|
5
|
+
* the arguments unchanged. The launcher owns company binding, the app's control folder and
|
|
6
|
+
* its JSON output; this command adds nothing to them, so the pack stays the one source of
|
|
7
|
+
* truth for what each subcommand does. The launcher's exit code becomes the CLI's (3 means
|
|
8
|
+
* `wait` timed out with HQ Capture still open).
|
|
9
|
+
*/
|
|
10
|
+
import { type StdioOptions } from "node:child_process";
|
|
11
|
+
import type { Command } from "commander";
|
|
12
|
+
export declare const CAPTURE_LAUNCHER = "core/packages/hq-pack-hq-capture/scripts/hq-record-process.sh";
|
|
13
|
+
export declare const CAPTURE_SUBCOMMANDS: readonly ["doctor", "build", "start", "record", "status", "wait", "stop", "run", "resume"];
|
|
14
|
+
export interface CaptureRunOptions {
|
|
15
|
+
root: string;
|
|
16
|
+
stdio?: StdioOptions;
|
|
17
|
+
}
|
|
18
|
+
/** Runs the launcher with `args` and returns its exit code. */
|
|
19
|
+
export declare function runCapture(args: readonly string[], opts: CaptureRunOptions): number;
|
|
20
|
+
export interface CaptureRegisterOptions {
|
|
21
|
+
root?: () => string;
|
|
22
|
+
stdio?: StdioOptions;
|
|
23
|
+
}
|
|
24
|
+
export declare function registerCaptureCommand(program: Command, opts?: CaptureRegisterOptions): Command;
|
|
25
|
+
//# sourceMappingURL=capture.d.ts.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hq capture — drive HQ Capture (the hq-capture pack's screen recorder) from the CLI.
|
|
3
|
+
*
|
|
4
|
+
* Every subcommand runs the pack's launcher, `hq-record-process.sh`, from the HQ root with
|
|
5
|
+
* the arguments unchanged. The launcher owns company binding, the app's control folder and
|
|
6
|
+
* its JSON output; this command adds nothing to them, so the pack stays the one source of
|
|
7
|
+
* truth for what each subcommand does. The launcher's exit code becomes the CLI's (3 means
|
|
8
|
+
* `wait` timed out with HQ Capture still open).
|
|
9
|
+
*/
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import { resolveDefaultHqRoot } from "../utils/hq-root-resolver.js";
|
|
14
|
+
export const CAPTURE_LAUNCHER = "core/packages/hq-pack-hq-capture/scripts/hq-record-process.sh";
|
|
15
|
+
export const CAPTURE_SUBCOMMANDS = [
|
|
16
|
+
"doctor",
|
|
17
|
+
"build",
|
|
18
|
+
"start",
|
|
19
|
+
"record",
|
|
20
|
+
"status",
|
|
21
|
+
"wait",
|
|
22
|
+
"stop",
|
|
23
|
+
"run",
|
|
24
|
+
"resume",
|
|
25
|
+
];
|
|
26
|
+
const DESCRIPTIONS = {
|
|
27
|
+
doctor: "Check this Mac can build and run HQ Capture",
|
|
28
|
+
build: "Build HQ Capture if missing or out of date ([--force])",
|
|
29
|
+
start: "Open HQ Capture for the session's company ([--company <slug>] [--record])",
|
|
30
|
+
record: "Start recording in the open HQ Capture, as if Start was pressed",
|
|
31
|
+
status: "Print HQ Capture's current status as JSON",
|
|
32
|
+
wait: "Wait until the recording is saved, failed or closed ([--timeout <seconds>])",
|
|
33
|
+
stop: "Stop HQ Capture, saving a recording in progress",
|
|
34
|
+
run: "Run an agent command on a saved recording (<mode> <package> [args])",
|
|
35
|
+
resume: "Check a pasted resume prompt and print its package (<prompt>)",
|
|
36
|
+
};
|
|
37
|
+
/** Runs the launcher with `args` and returns its exit code. */
|
|
38
|
+
export function runCapture(args, opts) {
|
|
39
|
+
const launcher = path.join(opts.root, CAPTURE_LAUNCHER);
|
|
40
|
+
if (!fs.existsSync(launcher)) {
|
|
41
|
+
process.stderr.write("HQ Capture is not installed in this HQ. Install it with: hq install hq-capture\n");
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
const result = spawnSync("bash", [launcher, ...args], {
|
|
45
|
+
cwd: opts.root,
|
|
46
|
+
stdio: opts.stdio ?? "inherit",
|
|
47
|
+
env: process.env,
|
|
48
|
+
});
|
|
49
|
+
if (result.error) {
|
|
50
|
+
process.stderr.write(`Could not run HQ Capture: ${result.error.message}\n`);
|
|
51
|
+
return 1;
|
|
52
|
+
}
|
|
53
|
+
if (result.signal)
|
|
54
|
+
return 1;
|
|
55
|
+
return result.status ?? 1;
|
|
56
|
+
}
|
|
57
|
+
export function registerCaptureCommand(program, opts = {}) {
|
|
58
|
+
const capture = program
|
|
59
|
+
.command("capture")
|
|
60
|
+
.description("Record your screen with HQ Capture: start, record, stop, status and wait");
|
|
61
|
+
for (const name of CAPTURE_SUBCOMMANDS) {
|
|
62
|
+
capture
|
|
63
|
+
.command(name)
|
|
64
|
+
.description(DESCRIPTIONS[name])
|
|
65
|
+
.argument("[args...]")
|
|
66
|
+
.allowUnknownOption(true)
|
|
67
|
+
.allowExcessArguments(true)
|
|
68
|
+
.helpOption(false)
|
|
69
|
+
.action((_args, _options, command) => {
|
|
70
|
+
const root = opts.root ? opts.root() : resolveDefaultHqRoot({ onMissing: "throw" });
|
|
71
|
+
const forwarded = rawArgsAfter(command, name) ?? command.args;
|
|
72
|
+
process.exitCode = runCapture([name, ...forwarded], { root, stdio: opts.stdio });
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return capture;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The arguments typed after `capture <name>`, exactly as given. Commander drops a `--`
|
|
79
|
+
* separator from `command.args`, which would change what the launcher receives, so the
|
|
80
|
+
* slice comes from the root command's raw argv. Returns undefined when it can't be found.
|
|
81
|
+
*/
|
|
82
|
+
function rawArgsAfter(command, name) {
|
|
83
|
+
let root = command;
|
|
84
|
+
while (root.parent)
|
|
85
|
+
root = root.parent;
|
|
86
|
+
const raw = root.rawArgs;
|
|
87
|
+
if (!Array.isArray(raw))
|
|
88
|
+
return undefined;
|
|
89
|
+
for (let i = 0; i + 1 < raw.length; i++) {
|
|
90
|
+
if (raw[i] === "capture" && raw[i + 1] === name)
|
|
91
|
+
return raw.slice(i + 2);
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=capture.js.map
|
|
@@ -89,6 +89,18 @@ function processIsAlive(pid) {
|
|
|
89
89
|
return error.code !== "ESRCH";
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
+
function counterLockOwnerIsAlive(lock, pid) {
|
|
93
|
+
if (processIsAlive(pid))
|
|
94
|
+
return true;
|
|
95
|
+
if (process.platform !== "win32")
|
|
96
|
+
return false;
|
|
97
|
+
let windowsPid = 0;
|
|
98
|
+
try {
|
|
99
|
+
windowsPid = Number.parseInt(fs.readFileSync(path.join(lock, "winpid"), "utf8"), 10) || 0;
|
|
100
|
+
}
|
|
101
|
+
catch { /* older shell locks did not record the Win32 PID */ }
|
|
102
|
+
return windowsPid > 1 && processIsAlive(windowsPid);
|
|
103
|
+
}
|
|
92
104
|
function acquireJournalWriteLock(dir) {
|
|
93
105
|
const lock = path.join(dir, ".session-journal-write.lock");
|
|
94
106
|
const ownerFile = path.join(lock, "pid");
|
|
@@ -142,26 +154,144 @@ function releaseJournalWriteLock(lock) {
|
|
|
142
154
|
}
|
|
143
155
|
catch { /* shell lock cleanup is best-effort */ }
|
|
144
156
|
}
|
|
145
|
-
|
|
157
|
+
const COUNTER_LOCK_PIDLESS_STALE_MS = 60_000;
|
|
158
|
+
function isAgedPidlessCounterLock(lock, now) {
|
|
159
|
+
try {
|
|
160
|
+
return now().getTime() - fs.statSync(lock).mtimeMs >= COUNTER_LOCK_PIDLESS_STALE_MS;
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function reclaimStaleCounterLock(lock, now) {
|
|
167
|
+
let rawOwner;
|
|
168
|
+
try {
|
|
169
|
+
rawOwner = fs.readFileSync(path.join(lock, "pid"), "utf8").trim();
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (error.code !== "ENOENT" || !isAgedPidlessCounterLock(lock, now))
|
|
173
|
+
return false;
|
|
174
|
+
rawOwner = "";
|
|
175
|
+
}
|
|
176
|
+
if (rawOwner !== "" && !/^\d+$/u.test(rawOwner))
|
|
177
|
+
return false;
|
|
178
|
+
let rawWindowsPid = "";
|
|
179
|
+
try {
|
|
180
|
+
rawWindowsPid = fs.readFileSync(path.join(lock, "winpid"), "utf8").trim();
|
|
181
|
+
}
|
|
182
|
+
catch { /* older shell locks did not record the Win32 PID */ }
|
|
183
|
+
if (rawWindowsPid !== "" && !/^\d+$/u.test(rawWindowsPid))
|
|
184
|
+
return false;
|
|
185
|
+
const owner = rawOwner ? Number.parseInt(rawOwner, 10) : 0;
|
|
186
|
+
const windowsOwner = rawWindowsPid ? Number.parseInt(rawWindowsPid, 10) : 0;
|
|
187
|
+
if (rawOwner && (owner <= 1 || counterLockOwnerIsAlive(lock, owner)))
|
|
188
|
+
return false;
|
|
189
|
+
if (!rawOwner && windowsOwner > 1 && processIsAlive(windowsOwner))
|
|
190
|
+
return false;
|
|
191
|
+
const claim = `${lock}.reclaim`;
|
|
192
|
+
try {
|
|
193
|
+
fs.mkdirSync(claim);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
fs.writeFileSync(path.join(claim, "pid"), `${process.pid}\n`, { flag: "wx" });
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
try {
|
|
203
|
+
fs.rmSync(claim, { recursive: true, force: true });
|
|
204
|
+
}
|
|
205
|
+
catch { /* best effort */ }
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
let currentRawOwner;
|
|
209
|
+
try {
|
|
210
|
+
currentRawOwner = fs.readFileSync(path.join(lock, "pid"), "utf8").trim();
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
currentRawOwner = "";
|
|
214
|
+
}
|
|
215
|
+
let currentRawWindowsPid;
|
|
216
|
+
try {
|
|
217
|
+
currentRawWindowsPid = fs.readFileSync(path.join(lock, "winpid"), "utf8").trim();
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
currentRawWindowsPid = "";
|
|
221
|
+
}
|
|
222
|
+
const sameOwner = currentRawOwner === rawOwner && currentRawWindowsPid === rawWindowsPid;
|
|
223
|
+
const stillStale = rawOwner
|
|
224
|
+
? sameOwner && !counterLockOwnerIsAlive(lock, owner)
|
|
225
|
+
: sameOwner && !(windowsOwner > 1 && processIsAlive(windowsOwner)) && isAgedPidlessCounterLock(lock, now);
|
|
226
|
+
if (stillStale) {
|
|
227
|
+
const stale = `${lock}.stale-${process.pid}-${Math.random().toString(16).slice(2)}`;
|
|
228
|
+
try {
|
|
229
|
+
fs.renameSync(lock, stale);
|
|
230
|
+
fs.rmSync(stale, { recursive: true, force: true });
|
|
231
|
+
fs.rmSync(claim, { recursive: true, force: true });
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
catch { /* another waiter reclaimed it */ }
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
fs.rmSync(claim, { recursive: true, force: true });
|
|
238
|
+
}
|
|
239
|
+
catch { /* best effort */ }
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
function withCounterLock(lock, env, writers, now, action) {
|
|
146
243
|
let i = 0;
|
|
147
244
|
let acquired = false;
|
|
148
|
-
|
|
245
|
+
let tries = env.HQ_JOURNAL_COUNTER_LOCK_TRIES ?? "250";
|
|
246
|
+
if (!/^\d+$/u.test(tries))
|
|
247
|
+
tries = "250";
|
|
248
|
+
const retryLimit = Number.parseInt(tries, 10);
|
|
249
|
+
while (i < retryLimit) {
|
|
149
250
|
try {
|
|
150
251
|
fs.mkdirSync(lock);
|
|
151
252
|
acquired = true;
|
|
253
|
+
try {
|
|
254
|
+
if (process.platform === "win32")
|
|
255
|
+
fs.writeFileSync(path.join(lock, "winpid"), `${process.pid}\n`, { flag: "wx" });
|
|
256
|
+
fs.writeFileSync(path.join(lock, "pid"), `${process.pid}\n`, { flag: "wx" });
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
try {
|
|
260
|
+
fs.rmSync(lock, { recursive: true, force: true });
|
|
261
|
+
}
|
|
262
|
+
catch { /* best effort */ }
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
break;
|
|
152
266
|
}
|
|
153
267
|
catch {
|
|
268
|
+
if (reclaimStaleCounterLock(lock, now))
|
|
269
|
+
continue;
|
|
154
270
|
i++;
|
|
155
|
-
if (i
|
|
156
|
-
|
|
157
|
-
sleepSync(20);
|
|
271
|
+
if (i < retryLimit)
|
|
272
|
+
sleepSync(20);
|
|
158
273
|
}
|
|
159
274
|
}
|
|
160
|
-
|
|
275
|
+
if (!acquired) {
|
|
276
|
+
writers.stderr("session-journal: tool-counter: lock remained busy; skipped update\n");
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
161
279
|
try {
|
|
162
|
-
|
|
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
|
+
}
|
|
163
294
|
}
|
|
164
|
-
catch { /* shell rmdir is best-effort */ }
|
|
165
295
|
}
|
|
166
296
|
function nextSequence(dir) {
|
|
167
297
|
let names = [];
|
|
@@ -205,12 +335,12 @@ function runToolCounter(root, action, sessionArg, env, now, writers) {
|
|
|
205
335
|
writers.stderr(`${sessionJournalAsset}: line ${line}: ${printableCounter}: Is a directory\n`);
|
|
206
336
|
};
|
|
207
337
|
if (action === "increment") {
|
|
208
|
-
withCounterLock(lock, () => {
|
|
338
|
+
withCounterLock(lock, env, writers, now, () => {
|
|
209
339
|
try {
|
|
210
340
|
fs.writeFileSync(counter, `${readCounter(counter) + 1}\n`);
|
|
211
341
|
}
|
|
212
342
|
catch (error) {
|
|
213
|
-
reportDirectoryWriteFailure(error,
|
|
343
|
+
reportDirectoryWriteFailure(error, 457);
|
|
214
344
|
}
|
|
215
345
|
});
|
|
216
346
|
}
|
|
@@ -218,12 +348,12 @@ function runToolCounter(root, action, sessionArg, env, now, writers) {
|
|
|
218
348
|
writers.stdout(`${readCounter(counter)}\n`);
|
|
219
349
|
}
|
|
220
350
|
else if (action === "reset") {
|
|
221
|
-
withCounterLock(lock, () => {
|
|
351
|
+
withCounterLock(lock, env, writers, now, () => {
|
|
222
352
|
try {
|
|
223
353
|
fs.writeFileSync(counter, "0\n");
|
|
224
354
|
}
|
|
225
355
|
catch (error) {
|
|
226
|
-
reportDirectoryWriteFailure(error,
|
|
356
|
+
reportDirectoryWriteFailure(error, 465);
|
|
227
357
|
}
|
|
228
358
|
});
|
|
229
359
|
}
|
package/dist/commands/dm.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
4
|
-
import { vaultApiFetch } from "../utils/vault-api.js";
|
|
4
|
+
import { resolveDestinationCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
|
|
5
5
|
import { isMachineIdentity } from "@indigoai-us/hq-cloud";
|
|
6
6
|
import { peekIdToken } from "../utils/id-token.js";
|
|
7
7
|
import { EMAIL_PATTERN, RECIPIENT_UID_PATTERN } from "../lib/notify-recipient.js";
|
|
@@ -830,12 +830,20 @@ async function runDmForward(eventId, opts) {
|
|
|
830
830
|
console.error(chalk.red("--to accepts one recipient or channel, not a group recipient list."));
|
|
831
831
|
process.exit(1);
|
|
832
832
|
}
|
|
833
|
+
const destinationChannelName = parseChannelName(opts.to);
|
|
834
|
+
if (opts.company !== undefined && destinationChannelName !== null) {
|
|
835
|
+
console.error(chalk.red("--company applies to person or agent destinations; a channel uses its own company."));
|
|
836
|
+
process.exit(1);
|
|
837
|
+
}
|
|
838
|
+
if (opts.company !== undefined && !opts.company.trim()) {
|
|
839
|
+
console.error(chalk.red("--company requires a company slug or cmp_ ID."));
|
|
840
|
+
process.exit(1);
|
|
841
|
+
}
|
|
833
842
|
const note = opts.noteFile !== undefined
|
|
834
843
|
? readFileSync(opts.noteFile, "utf8")
|
|
835
844
|
: opts.note;
|
|
836
845
|
const token = await ensureCognitoToken();
|
|
837
846
|
const source = await resolveForwardSourceConversation(token, opts.from);
|
|
838
|
-
const destinationChannelName = parseChannelName(opts.to);
|
|
839
847
|
const fileAccess = opts.grantFiles ? "grant" : opts.omitFiles ? "omit" : undefined;
|
|
840
848
|
const shared = {
|
|
841
849
|
...(note !== undefined ? { body: note } : {}),
|
|
@@ -869,8 +877,19 @@ async function runDmForward(eventId, opts) {
|
|
|
869
877
|
console.error(chalk.red("--to must be a person email, person/agent UID, or channel name/ID."));
|
|
870
878
|
process.exit(1);
|
|
871
879
|
}
|
|
880
|
+
// The server runs the destination-side cross-company check only when the
|
|
881
|
+
// request names the destination company; without it the destination is
|
|
882
|
+
// treated as personal. Not defaulted from the source, which would make a
|
|
883
|
+
// forward into another company look same-company.
|
|
884
|
+
const destinationCompanyUid = opts.company !== undefined
|
|
885
|
+
? await resolveDestinationCompanyUid(token, opts.company.trim())
|
|
886
|
+
: undefined;
|
|
872
887
|
path = "/v1/notify/dm";
|
|
873
|
-
payload = {
|
|
888
|
+
payload = {
|
|
889
|
+
...recipient,
|
|
890
|
+
...shared,
|
|
891
|
+
...(destinationCompanyUid ? { companyUid: destinationCompanyUid } : {}),
|
|
892
|
+
};
|
|
874
893
|
destinationLabel = opts.to;
|
|
875
894
|
}
|
|
876
895
|
const res = await vaultApiFetch({ token, path, method: "POST", body: payload });
|
|
@@ -1529,6 +1548,7 @@ export function registerDmCommand(program) {
|
|
|
1529
1548
|
.option("--grant-files", "Grant the recipient access to files they cannot currently open")
|
|
1530
1549
|
.option("--omit-files", "Omit files the recipient cannot currently open")
|
|
1531
1550
|
.option("--cross-company", "Confirm forwarding across company boundaries")
|
|
1551
|
+
.option("--company <slug|cmp_id>", "Company the person or agent destination belongs to, so cross-company rules apply")
|
|
1532
1552
|
.action(async (eventId, opts) => {
|
|
1533
1553
|
await runDmForward(eventId, opts);
|
|
1534
1554
|
});
|
|
@@ -68,6 +68,16 @@ export declare function vaultApiFetchPublic(opts: {
|
|
|
68
68
|
}): Promise<Response>;
|
|
69
69
|
export declare function looksLikeCompanyUid(ref: string): boolean;
|
|
70
70
|
export declare function looksLikePersonVaultUid(ref: string): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Resolve a company that may not be the caller's own — the destination of a
|
|
73
|
+
* cross-company DM forward. `getCompanyUid` prefers the caller's namespace, so
|
|
74
|
+
* when the destination company shares a slug with one of the caller's
|
|
75
|
+
* companies it would silently return the caller's company. Here a `cmp_` uid
|
|
76
|
+
* resolves directly; a slug goes to the global lookup first, and a slug that
|
|
77
|
+
* matches more than one company is refused with the candidate uids instead of
|
|
78
|
+
* guessing. Any other global failure falls back to `getCompanyUid`.
|
|
79
|
+
*/
|
|
80
|
+
export declare function resolveDestinationCompanyUid(token: string, ref: string, baseUrl?: string): Promise<string>;
|
|
71
81
|
export declare function getCompanyUid(token: string, companySlug: string | undefined,
|
|
72
82
|
/**
|
|
73
83
|
* Control-plane origin for the lookups. Defaults to DEFAULT_VAULT_API_URL;
|
package/dist/utils/vault-api.js
CHANGED
|
@@ -609,6 +609,33 @@ async function resolveCompanyUid(token, ref, baseUrl) {
|
|
|
609
609
|
const data = (await res.json());
|
|
610
610
|
return data.entity.uid;
|
|
611
611
|
}
|
|
612
|
+
/**
|
|
613
|
+
* Resolve a company that may not be the caller's own — the destination of a
|
|
614
|
+
* cross-company DM forward. `getCompanyUid` prefers the caller's namespace, so
|
|
615
|
+
* when the destination company shares a slug with one of the caller's
|
|
616
|
+
* companies it would silently return the caller's company. Here a `cmp_` uid
|
|
617
|
+
* resolves directly; a slug goes to the global lookup first, and a slug that
|
|
618
|
+
* matches more than one company is refused with the candidate uids instead of
|
|
619
|
+
* guessing. Any other global failure falls back to `getCompanyUid`.
|
|
620
|
+
*/
|
|
621
|
+
export async function resolveDestinationCompanyUid(token, ref, baseUrl) {
|
|
622
|
+
if (looksLikeCompanyUid(ref)) {
|
|
623
|
+
return resolveCompanyByUid(token, ref, baseUrl);
|
|
624
|
+
}
|
|
625
|
+
const res = await fetchGlobalCompanyBySlug(token, ref, baseUrl);
|
|
626
|
+
if (res.ok) {
|
|
627
|
+
const data = (await res.json());
|
|
628
|
+
return data.entity.uid;
|
|
629
|
+
}
|
|
630
|
+
raiseIfUnauthorized(res);
|
|
631
|
+
const body = (await res.json().catch(() => ({})));
|
|
632
|
+
if (res.status === 409 && Array.isArray(body.uids) && body.uids.length > 0) {
|
|
633
|
+
throw new CompanySelectionError(`Company slug '${ref}' matches ${body.uids.length} companies. ` +
|
|
634
|
+
`Re-run with --company <cmp_id> to name the destination company:\n` +
|
|
635
|
+
body.uids.map((u) => ` --company ${u}`).join('\n'));
|
|
636
|
+
}
|
|
637
|
+
return getCompanyUid(token, ref, baseUrl);
|
|
638
|
+
}
|
|
612
639
|
async function resolveCompanyFromMemberships(token, baseUrl) {
|
|
613
640
|
const res = await vaultApiFetch({
|
|
614
641
|
token,
|