@indigoai-us/hq-cli 5.345.170 → 5.345.172
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 +84 -0
- package/assets/scaffold/core/scripts/hq-delegate-send.sh +1 -1
- package/dist/command-catalog.generated.d.ts +206 -7
- package/dist/command-catalog.generated.js +252 -8
- package/dist/command-registration-plan.d.ts +7 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/access.d.ts +32 -1
- package/dist/commands/access.js +112 -3
- package/dist/commands/agent-kit.d.ts +1 -1
- package/dist/commands/agent-kit.js +3 -1
- package/dist/commands/agents.js +2 -1
- package/dist/commands/bot.d.ts +3 -0
- package/dist/commands/bot.js +179 -18
- package/dist/commands/cloud.d.ts +58 -1
- package/dist/commands/cloud.js +228 -10
- package/dist/commands/core-company-index-scope-flag.d.ts +8 -1
- package/dist/commands/core-company-index-scope-flag.js +9 -2
- package/dist/commands/core-delegate-w1b.js +5 -2
- package/dist/commands/core-dm-bind-native.js +19 -4
- package/dist/commands/core-rebuild-index-fast.js +24 -6
- package/dist/commands/core.js +18 -6
- package/dist/commands/daemon.js +16 -1
- package/dist/commands/dm.d.ts +17 -3
- package/dist/commands/dm.js +121 -26
- package/dist/commands/files.d.ts +1 -0
- package/dist/commands/files.js +22 -4
- package/dist/commands/integrations-api.d.ts +4 -0
- package/dist/commands/integrations-api.js +18 -2
- package/dist/commands/integrations-connect.d.ts +10 -3
- package/dist/commands/integrations-connect.js +70 -6
- package/dist/commands/integrations-core.d.ts +23 -0
- package/dist/commands/integrations-core.js +100 -0
- package/dist/commands/integrations-manage.js +8 -1
- package/dist/commands/integrations.js +49 -5
- package/dist/commands/lanes-loop.d.ts +20 -0
- package/dist/commands/lanes-loop.js +69 -11
- package/dist/commands/lanes.js +9 -1
- package/dist/commands/rescue.d.ts +9 -0
- package/dist/commands/rescue.js +31 -0
- package/dist/commands/run.js +115 -40
- package/dist/commands/scan.d.ts +5 -1
- package/dist/commands/scan.js +16 -4
- package/dist/commands/secrets.js +50 -9
- package/dist/commands/sync-core.d.ts +22 -0
- package/dist/commands/sync-core.js +124 -0
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2447 -2135
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/agent-kit/inbox-state.d.ts +17 -0
- package/dist/lib/agent-kit/inbox-state.js +41 -20
- package/dist/lib/agent-kit/kit-config.d.ts +2 -0
- package/dist/lib/agent-kit/kit-config.js +6 -0
- package/dist/lib/agent-kit/mcp/tools.js +12 -5
- package/dist/lib/agent-kit/run/heartbeat.d.ts +6 -3
- package/dist/lib/agent-kit/run/heartbeat.js +6 -3
- package/dist/lib/agent-kit/run/inbox.d.ts +8 -3
- package/dist/lib/agent-kit/run/inbox.js +42 -10
- package/dist/lib/agent-kit/run/request-timeout.d.ts +26 -0
- package/dist/lib/agent-kit/run/request-timeout.js +51 -0
- package/dist/lib/bot/api.d.ts +1 -1
- package/dist/lib/bot/config.d.ts +10 -0
- package/dist/lib/bot/config.js +17 -1
- package/dist/lib/bot/fleet-local.d.ts +142 -0
- package/dist/lib/bot/fleet-local.js +689 -0
- package/dist/lib/bot/fleet-python.d.ts +47 -0
- package/dist/lib/bot/fleet-python.js +218 -0
- package/dist/lib/bot/index.d.ts +2 -0
- package/dist/lib/bot/index.js +2 -0
- package/dist/lib/bot/local-config.d.ts +3 -2
- package/dist/lib/bot/local-config.js +5 -2
- package/dist/lib/bot/promote.d.ts +10 -0
- package/dist/lib/bot/promote.js +3 -2
- package/dist/lib/bot/prompt.d.ts +6 -1
- package/dist/lib/bot/prompt.js +8 -3
- package/dist/lib/bot/runtime/index.js +8 -4
- package/dist/lib/bot/runtime/scratch-dir.d.ts +24 -0
- package/dist/lib/bot/runtime/scratch-dir.js +41 -0
- package/dist/lib/caller-principal.d.ts +4 -0
- package/dist/lib/caller-principal.js +40 -0
- package/dist/lib/capacity/collectors/grok.js +8 -2
- package/dist/lib/cli-flag-keys.d.ts +2 -0
- package/dist/lib/cli-flag-keys.js +2 -0
- package/dist/lib/daemon/catalog.d.ts +12 -4
- package/dist/lib/daemon/catalog.js +70 -24
- package/dist/lib/daemon/config.d.ts +0 -7
- package/dist/lib/daemon/config.js +0 -32
- package/dist/lib/daemon/run.d.ts +5 -1
- package/dist/lib/daemon/run.js +48 -11
- package/dist/lib/daemon/supervisor.js +3 -1
- package/dist/lib/doctor/checks/release-links.d.ts +62 -1
- package/dist/lib/doctor/checks/release-links.js +188 -2
- package/dist/lib/doctor/fix/apply.js +54 -0
- package/dist/lib/doctor/fix/remediation.d.ts +8 -1
- package/dist/lib/doctor/fix/remediation.js +22 -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/index-render/index.d.ts +9 -0
- package/dist/lib/index-render/index.js +65 -5
- package/dist/lib/index-render/session-scope.d.ts +69 -0
- package/dist/lib/index-render/session-scope.js +279 -0
- package/dist/lib/lanes/admission.js +7 -20
- package/dist/lib/lanes/reconcile.js +13 -15
- package/dist/lib/lanes/stale-lock.d.ts +50 -0
- package/dist/lib/lanes/stale-lock.js +105 -0
- package/dist/lib/lanes/store.js +11 -76
- package/dist/lib/notify-recipient.d.ts +10 -4
- package/dist/lib/notify-recipient.js +16 -4
- package/dist/lib/secret-input-link.d.ts +42 -0
- package/dist/lib/secret-input-link.js +93 -0
- package/dist/lib/secret-scan/range.d.ts +57 -0
- package/dist/lib/secret-scan/range.js +266 -0
- package/dist/lib/secret-scan/repo-config.d.ts +18 -3
- package/dist/lib/secret-scan/repo-config.js +135 -2
- package/dist/lib/sync-core/engine-home.d.ts +44 -0
- package/dist/lib/sync-core/engine-home.js +193 -0
- package/dist/lib/sync-core/flag.d.ts +17 -0
- package/dist/lib/sync-core/flag.js +47 -0
- package/dist/lib/sync-core/reconcile.d.ts +31 -0
- package/dist/lib/sync-core/reconcile.js +196 -0
- package/dist/lib/sync-core/release.d.ts +32 -0
- package/dist/lib/sync-core/release.js +140 -0
- package/dist/lib/sync-core/rollout-state.d.ts +24 -0
- package/dist/lib/sync-core/rollout-state.js +94 -0
- package/dist/lib/vault-sync-switches.d.ts +5 -0
- package/dist/lib/vault-sync-switches.js +43 -0
- package/dist/run/script-bound-schema.d.ts +27 -0
- package/dist/run/script-bound-schema.js +97 -0
- package/dist/secrets-env-action.d.ts +2 -0
- package/dist/secrets-env-action.js +7 -2
- package/dist/utils/cognito-session.d.ts +3 -30
- package/dist/utils/cognito-session.js +2 -100
- package/dist/utils/machine-principal-state.d.ts +38 -0
- package/dist/utils/machine-principal-state.js +110 -0
- package/dist/utils/remote-list-not-converging.d.ts +21 -0
- package/dist/utils/remote-list-not-converging.js +40 -0
- package/dist/utils/safe-link.js +9 -0
- package/dist/utils/script-bound-exec.d.ts +158 -0
- package/dist/utils/script-bound-exec.js +568 -0
- package/dist/utils/sync-watchdog.d.ts +92 -0
- package/dist/utils/sync-watchdog.js +250 -0
- package/package.json +8 -8
- package/dist/lib/daemon/sync-core-canary.d.ts +0 -9
- package/dist/lib/daemon/sync-core-canary.js +0 -56
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
export const DEFAULT_LIVE_OWNER_MAX_MS = 10 * 60_000;
|
|
5
|
+
/** HQ_LANES_LOCK_LIVE_OWNER_MAX_MS overrides the live-owner ceiling. */
|
|
6
|
+
export function liveOwnerMaxMs(env = process.env) {
|
|
7
|
+
const raw = env.HQ_LANES_LOCK_LIVE_OWNER_MAX_MS;
|
|
8
|
+
const parsed = raw === undefined ? NaN : Number(raw);
|
|
9
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : DEFAULT_LIVE_OWNER_MAX_MS;
|
|
10
|
+
}
|
|
11
|
+
export function lockOwnerPidAlive(pid) {
|
|
12
|
+
try {
|
|
13
|
+
process.kill(pid, 0);
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
return err.code !== "ESRCH";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Contents to write into a freshly created lock file. */
|
|
21
|
+
export function lockFileContents(hostname = os.hostname()) {
|
|
22
|
+
return `${process.pid}\n${Date.now()}\n${hostname}\n`;
|
|
23
|
+
}
|
|
24
|
+
function sameFile(a, b) {
|
|
25
|
+
return a.ino === b.ino && a.dev === b.dev;
|
|
26
|
+
}
|
|
27
|
+
/** Decide whether the lock content/age describes an abandoned lock. */
|
|
28
|
+
export function lockIsStale(raw, mtimeMs, now, hostname, isAlive, options) {
|
|
29
|
+
const reclaimByAge = options.reclaimByAge ?? true;
|
|
30
|
+
const [pidLine, tsLine, hostLine] = raw.split("\n");
|
|
31
|
+
const pid = pidLine ? Number(pidLine) : NaN;
|
|
32
|
+
const ts = tsLine ? Number(tsLine) : NaN;
|
|
33
|
+
const pidKnown = Number.isSafeInteger(pid) && pid > 0;
|
|
34
|
+
// A pid is only meaningful on the host that wrote it.
|
|
35
|
+
const sameHost = hostLine !== undefined && hostLine.trim() !== "" && hostLine.trim() === hostname;
|
|
36
|
+
const mtimeAge = now - mtimeMs;
|
|
37
|
+
const age = Number.isFinite(ts) ? Math.max(now - ts, mtimeAge) : mtimeAge;
|
|
38
|
+
if (pidKnown && sameHost) {
|
|
39
|
+
if (!isAlive(pid))
|
|
40
|
+
return true;
|
|
41
|
+
return reclaimByAge && age > Math.max(options.liveOwnerMaxMs, options.staleMs);
|
|
42
|
+
}
|
|
43
|
+
// A legacy lock (no host line, written by an older CLI) whose pid is alive
|
|
44
|
+
// here may be a live local holder: give it the live-owner ceiling too.
|
|
45
|
+
const legacyNoHost = hostLine === undefined || hostLine.trim() === "";
|
|
46
|
+
if (pidKnown && legacyNoHost && isAlive(pid)) {
|
|
47
|
+
return reclaimByAge && age > Math.max(options.liveOwnerMaxMs, options.staleMs);
|
|
48
|
+
}
|
|
49
|
+
return reclaimByAge && age > options.staleMs;
|
|
50
|
+
}
|
|
51
|
+
/** Remove an abandoned lock atomically. Returns true when this call reclaimed it. */
|
|
52
|
+
export function reclaimStaleLock(lockPath, options, io = {}) {
|
|
53
|
+
const statSync = io.statSync ?? ((p) => fs.statSync(p));
|
|
54
|
+
const readFileSync = io.readFileSync ?? ((p, enc) => fs.readFileSync(p, enc));
|
|
55
|
+
const renameSync = io.renameSync ?? fs.renameSync.bind(fs);
|
|
56
|
+
const unlinkSync = io.unlinkSync ?? fs.unlinkSync.bind(fs);
|
|
57
|
+
const linkSync = io.linkSync ?? fs.linkSync.bind(fs);
|
|
58
|
+
const hostname = io.hostname ?? os.hostname;
|
|
59
|
+
const isAlive = io.isAlive ?? lockOwnerPidAlive;
|
|
60
|
+
const before = statSync(lockPath);
|
|
61
|
+
const raw = readFileSync(lockPath, "utf8");
|
|
62
|
+
if (!lockIsStale(raw, before.mtimeMs, Date.now(), hostname(), isAlive, options))
|
|
63
|
+
return false;
|
|
64
|
+
const parked = `${lockPath}.stale.${process.pid}.${randomBytes(6).toString("hex")}`;
|
|
65
|
+
try {
|
|
66
|
+
renameSync(lockPath, parked);
|
|
67
|
+
}
|
|
68
|
+
catch (renameErr) {
|
|
69
|
+
// Another reclaimer won the rename, or the holder released it.
|
|
70
|
+
if (renameErr.code === "ENOENT")
|
|
71
|
+
return false;
|
|
72
|
+
throw renameErr;
|
|
73
|
+
}
|
|
74
|
+
const removeParked = () => {
|
|
75
|
+
try {
|
|
76
|
+
unlinkSync(parked);
|
|
77
|
+
}
|
|
78
|
+
catch (unlinkErr) {
|
|
79
|
+
if (unlinkErr.code !== "ENOENT")
|
|
80
|
+
throw unlinkErr;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
let reclaimed;
|
|
84
|
+
try {
|
|
85
|
+
reclaimed = sameFile(statSync(parked), before) && readFileSync(parked, "utf8") === raw;
|
|
86
|
+
if (!reclaimed) {
|
|
87
|
+
// The parked file is a live holder's fresh lock. Put it back.
|
|
88
|
+
try {
|
|
89
|
+
linkSync(parked, lockPath);
|
|
90
|
+
}
|
|
91
|
+
catch (linkErr) {
|
|
92
|
+
if (linkErr.code !== "EEXIST")
|
|
93
|
+
throw linkErr;
|
|
94
|
+
process.stderr.write(`${options.label ?? "lanes store"}: could not restore displaced lock ${lockPath}; a newer lock already exists\n`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
removeParked();
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
removeParked();
|
|
103
|
+
return reclaimed;
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=stale-lock.js.map
|
package/dist/lib/lanes/store.js
CHANGED
|
@@ -20,6 +20,7 @@ import { LaneNotFoundError, LaneSchemaError, LaneVersionConflict, LanesError, is
|
|
|
20
20
|
import { HISTORY_CAP, HOST_KINDS, LANE_STORY_STATUSES, LANE_SCHEMA_VERSION, QUESTION_SCHEMA_VERSION, SENIOR_KINDS, isHostKind, isLaneProvider, isQueuedReason, isLaneState, isQuestionStatus, isSeniorKind, isTerminalLaneState, parseMajorSchema, REQUIRED_LANE_RECORD_FIELDS, LANE_FIELD_WRITERS, LANE_RUNNERS, LANE_WRITERS, } from "./types.js";
|
|
21
21
|
import { laneStories } from "./stories.js";
|
|
22
22
|
import { laneStoryLedgerEnabled, syncLaneToProjectStoryLedger, validateProjectStories, } from "./story-ledger.js";
|
|
23
|
+
import { liveOwnerMaxMs, reclaimStaleLock } from "./stale-lock.js";
|
|
23
24
|
export const LANES_STORE_REL = path.join("workspace", "lanes");
|
|
24
25
|
export const LANE_RECORDS_DIR = "lanes";
|
|
25
26
|
export const QUESTION_RECORDS_DIR = "questions";
|
|
@@ -159,89 +160,23 @@ function wait(ms) {
|
|
|
159
160
|
function lockPathFor(recordPath) {
|
|
160
161
|
return `${recordPath}.lock`;
|
|
161
162
|
}
|
|
162
|
-
function lockOwnerAlive(pid) {
|
|
163
|
-
try {
|
|
164
|
-
process.kill(pid, 0);
|
|
165
|
-
return true;
|
|
166
|
-
}
|
|
167
|
-
catch (err) {
|
|
168
|
-
return err.code !== "ESRCH";
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
163
|
function sameFile(a, b) {
|
|
172
164
|
return a.ino === b.ino && a.dev === b.dev;
|
|
173
165
|
}
|
|
174
166
|
/**
|
|
175
167
|
* Remove a record lock that no live process holds, and report whether it was
|
|
176
|
-
* removed.
|
|
177
|
-
*
|
|
178
|
-
* larger of the timestamp age and the file mtime age, so a future or
|
|
179
|
-
* unparseable timestamp still ages out through mtime.
|
|
180
|
-
*
|
|
181
|
-
* Removal is atomic: the lock is renamed to a unique name, which only one
|
|
182
|
-
* reclaimer can win. If the renamed file is not the lock that was inspected
|
|
183
|
-
* (another process reclaimed it and took a fresh lock in between), the fresh
|
|
184
|
-
* lock is linked back into place and nothing is reclaimed.
|
|
168
|
+
* removed. See stale-lock.ts for the rules; a live same-host holder is kept
|
|
169
|
+
* until the live-owner ceiling, never by the ordinary stale window.
|
|
185
170
|
*/
|
|
186
171
|
function recordLockIsStale(lockPath, deps, reclaimByAge = true) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
// A pid is only meaningful on the host that wrote it. A lock with a foreign
|
|
196
|
-
// or missing host falls back to the age rule.
|
|
197
|
-
const sameHost = hostLine !== undefined && hostLine.trim() !== "" && hostLine.trim() === deps.hostname();
|
|
198
|
-
const now = Date.now();
|
|
199
|
-
const mtimeAge = now - before.mtimeMs;
|
|
200
|
-
const age = Number.isFinite(ts) ? Math.max(now - ts, mtimeAge) : mtimeAge;
|
|
201
|
-
const stale = (pidKnown && sameHost && pid !== process.pid && !lockOwnerAlive(pid)) ||
|
|
202
|
-
(reclaimByAge && age > lockStaleMs());
|
|
203
|
-
if (!stale)
|
|
204
|
-
return false;
|
|
205
|
-
const parked = `${lockPath}.stale.${process.pid}.${randomBytes(6).toString("hex")}`;
|
|
206
|
-
try {
|
|
207
|
-
deps.renameSync(lockPath, parked);
|
|
208
|
-
}
|
|
209
|
-
catch (renameErr) {
|
|
210
|
-
// Another reclaimer won the rename, or the holder released it.
|
|
211
|
-
if (renameErr.code === "ENOENT")
|
|
212
|
-
return false;
|
|
213
|
-
throw renameErr;
|
|
214
|
-
}
|
|
215
|
-
const removeParked = () => {
|
|
216
|
-
try {
|
|
217
|
-
deps.unlinkSync(parked);
|
|
218
|
-
}
|
|
219
|
-
catch (unlinkErr) {
|
|
220
|
-
if (unlinkErr.code !== "ENOENT")
|
|
221
|
-
throw unlinkErr;
|
|
222
|
-
}
|
|
223
|
-
};
|
|
224
|
-
let reclaimed;
|
|
225
|
-
try {
|
|
226
|
-
reclaimed = sameFile(statSync(parked), before) && deps.readFileSync(parked, "utf8") === raw;
|
|
227
|
-
if (!reclaimed) {
|
|
228
|
-
// The parked file is a live holder's fresh lock. Put it back.
|
|
229
|
-
try {
|
|
230
|
-
linkSync(parked, lockPath);
|
|
231
|
-
}
|
|
232
|
-
catch (linkErr) {
|
|
233
|
-
if (linkErr.code !== "EEXIST")
|
|
234
|
-
throw linkErr;
|
|
235
|
-
process.stderr.write(`lanes store: could not restore displaced lock ${lockPath}; a newer lock already exists\n`);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
catch (err) {
|
|
240
|
-
removeParked();
|
|
241
|
-
throw err;
|
|
242
|
-
}
|
|
243
|
-
removeParked();
|
|
244
|
-
return reclaimed;
|
|
172
|
+
return reclaimStaleLock(lockPath, { staleMs: lockStaleMs(), liveOwnerMaxMs: liveOwnerMaxMs(), reclaimByAge }, {
|
|
173
|
+
statSync: deps.statSync ?? ((p) => fs.statSync(p)),
|
|
174
|
+
readFileSync: (p, enc) => String(deps.readFileSync(p, enc)),
|
|
175
|
+
renameSync: deps.renameSync,
|
|
176
|
+
unlinkSync: deps.unlinkSync,
|
|
177
|
+
linkSync: deps.linkSync ?? fs.linkSync.bind(fs),
|
|
178
|
+
hostname: deps.hostname,
|
|
179
|
+
});
|
|
245
180
|
}
|
|
246
181
|
function withRecordLock(recordPath, deps, fn, timeoutMs = LOCK_TIMEOUT_MS, reclaimByAge = true) {
|
|
247
182
|
const lockPath = lockPathFor(recordPath);
|
|
@@ -14,9 +14,15 @@ export type DmRecipientKind = "person" | "agent" | "channel" | "group";
|
|
|
14
14
|
*/
|
|
15
15
|
export declare function recipientKindOf(recipient: string): "person" | "agent";
|
|
16
16
|
/**
|
|
17
|
-
* Default audience by recipient
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* Default audience by recipient and caller. A person login always defaults to
|
|
18
|
+
* `human`: people never send agent-tagged messages. A machine caller (a bot,
|
|
19
|
+
* an agent or an outpost) sends `human` to a person and `agent` to an agent,
|
|
20
|
+
* a channel or a group, so those are folded away from people by default.
|
|
20
21
|
*/
|
|
21
|
-
export declare function defaultAudienceFor(kind: DmRecipientKind): DmAudience;
|
|
22
|
+
export declare function defaultAudienceFor(kind: DmRecipientKind, machineCaller: boolean): DmAudience;
|
|
23
|
+
/** Server error code for an agent-tagged send from a person login. */
|
|
24
|
+
export declare const PERSON_CANNOT_SEND_AGENT_CODE = "person_cannot_send_agent_message";
|
|
25
|
+
export declare const PERSON_AGENT_AUDIENCE_MESSAGE = "People can only send human messages. Agent messages are sent by agents and bots.";
|
|
26
|
+
/** True when this caller may tag a message with `audience`. */
|
|
27
|
+
export declare function callerMayUseAudience(audience: string | undefined, machineCaller: boolean): boolean;
|
|
22
28
|
//# sourceMappingURL=notify-recipient.d.ts.map
|
|
@@ -16,11 +16,23 @@ export function recipientKindOf(recipient) {
|
|
|
16
16
|
return /^agt_/.test(recipient.trim()) ? "agent" : "person";
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
|
-
* Default audience by recipient
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* Default audience by recipient and caller. A person login always defaults to
|
|
20
|
+
* `human`: people never send agent-tagged messages. A machine caller (a bot,
|
|
21
|
+
* an agent or an outpost) sends `human` to a person and `agent` to an agent,
|
|
22
|
+
* a channel or a group, so those are folded away from people by default.
|
|
22
23
|
*/
|
|
23
|
-
export function defaultAudienceFor(kind) {
|
|
24
|
+
export function defaultAudienceFor(kind, machineCaller) {
|
|
25
|
+
if (!machineCaller)
|
|
26
|
+
return "human";
|
|
24
27
|
return kind === "person" ? "human" : "agent";
|
|
25
28
|
}
|
|
29
|
+
/** Server error code for an agent-tagged send from a person login. */
|
|
30
|
+
export const PERSON_CANNOT_SEND_AGENT_CODE = "person_cannot_send_agent_message";
|
|
31
|
+
export const PERSON_AGENT_AUDIENCE_MESSAGE = "People can only send human messages. Agent messages are sent by agents and bots.";
|
|
32
|
+
/** True when this caller may tag a message with `audience`. */
|
|
33
|
+
export function callerMayUseAudience(audience, machineCaller) {
|
|
34
|
+
if (machineCaller || audience === undefined || audience === "")
|
|
35
|
+
return true;
|
|
36
|
+
return audience === "human";
|
|
37
|
+
}
|
|
26
38
|
//# sourceMappingURL=notify-recipient.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rendering for `hq secrets generate-link` output.
|
|
3
|
+
*
|
|
4
|
+
* The URL is a single-use capability, so every mode prints it exactly once
|
|
5
|
+
* and never puts the token in a visible label.
|
|
6
|
+
*
|
|
7
|
+
* - `text` (default) on a TTY: the URL is wrapped in an OSC-8 hyperlink when
|
|
8
|
+
* the terminal supports it, and printed plain otherwise.
|
|
9
|
+
* - `text` when stdout is not a TTY: unchanged plain output (no escapes), so
|
|
10
|
+
* pipes and scripts that grep the URL keep working.
|
|
11
|
+
* - `markdown`: stdout gets one line, a Markdown inline link; the metadata
|
|
12
|
+
* goes to stderr so `| pbcopy` or `$(...)` captures only the link.
|
|
13
|
+
* - `json`: stdout gets one JSON object with the API fields, nothing else.
|
|
14
|
+
*/
|
|
15
|
+
export type SecretLinkFormat = "text" | "markdown" | "json";
|
|
16
|
+
export declare const SECRET_LINK_FORMATS: readonly SecretLinkFormat[];
|
|
17
|
+
export interface SecretInputLinkData {
|
|
18
|
+
url: string;
|
|
19
|
+
expiresAt: string;
|
|
20
|
+
secretName: string;
|
|
21
|
+
}
|
|
22
|
+
export interface RenderedSecretLink {
|
|
23
|
+
stdout: string[];
|
|
24
|
+
stderr: string[];
|
|
25
|
+
}
|
|
26
|
+
export declare function osc8(url: string, label: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Best-effort OSC-8 support detection, consulted only when stdout is a TTY.
|
|
29
|
+
* FORCE_HYPERLINK=1/0 overrides detection on a TTY; it never adds escapes to
|
|
30
|
+
* non-TTY output.
|
|
31
|
+
* Unknown terminals get the plain URL, which most terminals auto-link anyway.
|
|
32
|
+
*/
|
|
33
|
+
export declare function supportsHyperlinks(env: NodeJS.ProcessEnv): boolean;
|
|
34
|
+
export declare function stripControlChars(value: string): string;
|
|
35
|
+
export declare function sanitizeSecretInputLinkData(data: SecretInputLinkData): SecretInputLinkData;
|
|
36
|
+
export declare function markdownSecretLink(raw: SecretInputLinkData): string;
|
|
37
|
+
export declare function renderSecretInputLink(raw: SecretInputLinkData, opts: {
|
|
38
|
+
format: SecretLinkFormat;
|
|
39
|
+
isTTY: boolean;
|
|
40
|
+
env: NodeJS.ProcessEnv;
|
|
41
|
+
}): RenderedSecretLink;
|
|
42
|
+
//# sourceMappingURL=secret-input-link.d.ts.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rendering for `hq secrets generate-link` output.
|
|
3
|
+
*
|
|
4
|
+
* The URL is a single-use capability, so every mode prints it exactly once
|
|
5
|
+
* and never puts the token in a visible label.
|
|
6
|
+
*
|
|
7
|
+
* - `text` (default) on a TTY: the URL is wrapped in an OSC-8 hyperlink when
|
|
8
|
+
* the terminal supports it, and printed plain otherwise.
|
|
9
|
+
* - `text` when stdout is not a TTY: unchanged plain output (no escapes), so
|
|
10
|
+
* pipes and scripts that grep the URL keep working.
|
|
11
|
+
* - `markdown`: stdout gets one line, a Markdown inline link; the metadata
|
|
12
|
+
* goes to stderr so `| pbcopy` or `$(...)` captures only the link.
|
|
13
|
+
* - `json`: stdout gets one JSON object with the API fields, nothing else.
|
|
14
|
+
*/
|
|
15
|
+
export const SECRET_LINK_FORMATS = ["text", "markdown", "json"];
|
|
16
|
+
const OSC = "\u001b]";
|
|
17
|
+
const ST = "\u001b\\";
|
|
18
|
+
export function osc8(url, label) {
|
|
19
|
+
return `${OSC}8;;${url}${ST}${label}${OSC}8;;${ST}`;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Best-effort OSC-8 support detection, consulted only when stdout is a TTY.
|
|
23
|
+
* FORCE_HYPERLINK=1/0 overrides detection on a TTY; it never adds escapes to
|
|
24
|
+
* non-TTY output.
|
|
25
|
+
* Unknown terminals get the plain URL, which most terminals auto-link anyway.
|
|
26
|
+
*/
|
|
27
|
+
export function supportsHyperlinks(env) {
|
|
28
|
+
const force = env.FORCE_HYPERLINK;
|
|
29
|
+
if (force !== undefined && force !== "")
|
|
30
|
+
return force !== "0" && force !== "false";
|
|
31
|
+
if (env.TERM === "dumb" || env.CI)
|
|
32
|
+
return false;
|
|
33
|
+
const program = env.TERM_PROGRAM ?? "";
|
|
34
|
+
if (["iTerm.app", "WezTerm", "vscode", "ghostty", "Hyper", "Tabby", "rio"].includes(program)) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
if (env.WT_SESSION || env.KITTY_WINDOW_ID || env.KONSOLE_VERSION || env.DOMTERM)
|
|
38
|
+
return true;
|
|
39
|
+
const vte = Number.parseInt(env.VTE_VERSION ?? "", 10);
|
|
40
|
+
if (Number.isFinite(vte) && vte >= 5000)
|
|
41
|
+
return true;
|
|
42
|
+
const term = env.TERM ?? "";
|
|
43
|
+
return /kitty|alacritty|foot|wezterm|ghostty/.test(term);
|
|
44
|
+
}
|
|
45
|
+
/** Percent-encode the characters that would end a Markdown link target early. */
|
|
46
|
+
function markdownTarget(url) {
|
|
47
|
+
return url.replace(/[()<>\s]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`);
|
|
48
|
+
}
|
|
49
|
+
// C0 (U+0000-U+001F), DEL and C1 (U+0080-U+009F). Server-supplied fields must
|
|
50
|
+
// not be able to inject terminal escapes (ESC, BEL, OSC) or extra lines.
|
|
51
|
+
// eslint-disable-next-line no-control-regex
|
|
52
|
+
const CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
53
|
+
export function stripControlChars(value) {
|
|
54
|
+
return value.replace(CONTROL_CHARS, "");
|
|
55
|
+
}
|
|
56
|
+
export function sanitizeSecretInputLinkData(data) {
|
|
57
|
+
return {
|
|
58
|
+
url: stripControlChars(String(data.url ?? "")),
|
|
59
|
+
expiresAt: stripControlChars(String(data.expiresAt ?? "")),
|
|
60
|
+
secretName: stripControlChars(String(data.secretName ?? "")),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function markdownLabelText(value) {
|
|
64
|
+
return value.replace(/[[\]\\]/g, (c) => `\\${c}`);
|
|
65
|
+
}
|
|
66
|
+
export function markdownSecretLink(raw) {
|
|
67
|
+
const data = sanitizeSecretInputLinkData(raw);
|
|
68
|
+
const label = `Submit ${markdownLabelText(data.secretName)} (expires ${markdownLabelText(data.expiresAt)}) ›`;
|
|
69
|
+
return `[${label}](${markdownTarget(data.url)})`;
|
|
70
|
+
}
|
|
71
|
+
export function renderSecretInputLink(raw, opts) {
|
|
72
|
+
const data = sanitizeSecretInputLinkData(raw);
|
|
73
|
+
const meta = [
|
|
74
|
+
` Secret: ${data.secretName}`,
|
|
75
|
+
` Expires: ${data.expiresAt}`,
|
|
76
|
+
" One-time use — link is invalidated after first submission.",
|
|
77
|
+
];
|
|
78
|
+
if (opts.format === "json") {
|
|
79
|
+
return {
|
|
80
|
+
stdout: [JSON.stringify({ url: data.url, expiresAt: data.expiresAt, secretName: data.secretName })],
|
|
81
|
+
stderr: [],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (opts.format === "markdown") {
|
|
85
|
+
return { stdout: [markdownSecretLink(data)], stderr: meta };
|
|
86
|
+
}
|
|
87
|
+
const shown = opts.isTTY && supportsHyperlinks(opts.env) ? osc8(data.url, data.url) : data.url;
|
|
88
|
+
return {
|
|
89
|
+
stdout: ["Secret input link generated:", `\n ${shown}\n`, ...meta],
|
|
90
|
+
stderr: [],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=secret-input-link.js.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Range scan: the lines a branch added between <base> and <head>, as
|
|
3
|
+
* `commit-secret-scan.sh pr-check` needs them.
|
|
4
|
+
*
|
|
5
|
+
* What is scanned:
|
|
6
|
+
* - The added lines of every non-merge commit in base..head, each against
|
|
7
|
+
* its own parent. A secret committed in one commit and deleted in a later
|
|
8
|
+
* one is still reported: it stays in the branch history.
|
|
9
|
+
* - For every merge commit in base..head, the lines that differ from all of
|
|
10
|
+
* its parents (git's combined diff, "++" lines): content the merge itself
|
|
11
|
+
* introduced, such as a conflict resolution. Lines a merge only carries
|
|
12
|
+
* over from one parent are either base content or scanned in that
|
|
13
|
+
* parent's own commits.
|
|
14
|
+
* - The added lines of the net diff merge-base(base, head)..head.
|
|
15
|
+
* - Binary files those diffs add or change: path rules, plus their bytes
|
|
16
|
+
* scanned as text up to MAX_BINARY_BYTES (larger ones are warned about).
|
|
17
|
+
* Lines that already existed on the base, and earlier versions of lines the
|
|
18
|
+
* branch did not touch, are not scanned.
|
|
19
|
+
*
|
|
20
|
+
* Config comes from the repo's .gitleaks.toml:
|
|
21
|
+
* - Allowlists: the exact-value exemptions committed on the base (the same
|
|
22
|
+
* contract as `scan secrets --repo --allowlist-rev`). A branch cannot
|
|
23
|
+
* exempt its own commits by adding an allowlist.
|
|
24
|
+
* - Rules: [[rules]] committed on the base, added to the built-in rules.
|
|
25
|
+
* Rules the branch adds or changes are not compiled or run: a branch-added
|
|
26
|
+
* regex could be written to backtrack for minutes (ReDoS), and the base
|
|
27
|
+
* copy has been reviewed.
|
|
28
|
+
* A rejected allowlist or rules section is warned about and dropped; the scan
|
|
29
|
+
* still runs with the built-in rules (stricter, never looser).
|
|
30
|
+
*
|
|
31
|
+
* Range errors: an unknown rev, a reversed range (head behind base) and a
|
|
32
|
+
* merge-base failure other than "no common history" exit 2. base == head is an
|
|
33
|
+
* empty range: a warning and no findings.
|
|
34
|
+
*/
|
|
35
|
+
import { type Fragment } from "./detect.js";
|
|
36
|
+
import type { SecretFinding } from "./types.js";
|
|
37
|
+
export declare const RULES_IGNORED_WARNING = "HQ secret scan: warning: the repo .gitleaks.toml [[rules]] were rejected; scanning with the built-in rules only";
|
|
38
|
+
export declare const EMPTY_RANGE_WARNING = "HQ secret scan: warning: the range is empty (base and head are the same commit); nothing to scan";
|
|
39
|
+
/** Binary blobs larger than this are not read; the scan warns instead. */
|
|
40
|
+
export declare const MAX_BINARY_BYTES: number;
|
|
41
|
+
/** `<base>..<head>` or `<base>` (head = HEAD). A three-dot range is rejected: the net diff is always taken from the merge base. */
|
|
42
|
+
export declare function parseRange(range: string): {
|
|
43
|
+
base: string;
|
|
44
|
+
head: string;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Lines a merge introduced, from `git diff-tree --cc` output: a line counts
|
|
48
|
+
* only when every parent column is "+", i.e. it is in none of the parents.
|
|
49
|
+
*/
|
|
50
|
+
export declare function parseCombinedDiff(diff: string): Fragment[];
|
|
51
|
+
/**
|
|
52
|
+
* Findings for the lines added on <head> since <base>. A finding from a
|
|
53
|
+
* commit that the net diff no longer shows is named `<file>@<commit12>`, so
|
|
54
|
+
* the reader can find the version that held it.
|
|
55
|
+
*/
|
|
56
|
+
export declare function scanRange(cwd: string, range: string): SecretFinding[];
|
|
57
|
+
//# sourceMappingURL=range.d.ts.map
|