@indigoai-us/hq-cli 5.345.169 → 5.345.171

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/assets/scaffold/core/scripts/hq-delegate-send.sh +1 -1
  3. package/dist/command-catalog.generated.d.ts +181 -9
  4. package/dist/command-catalog.generated.js +224 -10
  5. package/dist/commands/access.d.ts +32 -1
  6. package/dist/commands/access.js +119 -5
  7. package/dist/commands/agent-kit.d.ts +1 -1
  8. package/dist/commands/agent-kit.js +3 -1
  9. package/dist/commands/agents.js +2 -1
  10. package/dist/commands/bot.d.ts +3 -0
  11. package/dist/commands/bot.js +141 -14
  12. package/dist/commands/cloud.d.ts +78 -1
  13. package/dist/commands/cloud.js +274 -9
  14. package/dist/commands/core-company-index-scope-flag.d.ts +8 -1
  15. package/dist/commands/core-company-index-scope-flag.js +9 -2
  16. package/dist/commands/core-delegate-w1b.js +5 -2
  17. package/dist/commands/core-dm-bind-native.js +19 -4
  18. package/dist/commands/core-rebuild-index-fast.js +24 -6
  19. package/dist/commands/core.js +18 -6
  20. package/dist/commands/dm.d.ts +17 -3
  21. package/dist/commands/dm.js +121 -26
  22. package/dist/commands/feedback-triage.d.ts +43 -1
  23. package/dist/commands/feedback-triage.js +123 -23
  24. package/dist/commands/feedback.d.ts +40 -1
  25. package/dist/commands/feedback.js +184 -4
  26. package/dist/commands/files.d.ts +42 -0
  27. package/dist/commands/files.js +223 -30
  28. package/dist/commands/integrations-api.d.ts +4 -0
  29. package/dist/commands/integrations-api.js +18 -2
  30. package/dist/commands/integrations-connect.d.ts +10 -3
  31. package/dist/commands/integrations-connect.js +70 -6
  32. package/dist/commands/integrations-core.d.ts +23 -0
  33. package/dist/commands/integrations-core.js +100 -0
  34. package/dist/commands/integrations-manage.js +8 -1
  35. package/dist/commands/integrations.js +49 -5
  36. package/dist/commands/lanes-loop.d.ts +20 -0
  37. package/dist/commands/lanes-loop.js +69 -11
  38. package/dist/commands/lanes.js +9 -1
  39. package/dist/commands/mesh.js +6 -0
  40. package/dist/commands/rescue.d.ts +9 -0
  41. package/dist/commands/rescue.js +31 -0
  42. package/dist/commands/run.js +115 -40
  43. package/dist/commands/scan.d.ts +5 -1
  44. package/dist/commands/scan.js +16 -4
  45. package/dist/commands/secrets.js +50 -9
  46. package/dist/commands/sync-manifest.d.ts +16 -0
  47. package/dist/commands/sync-manifest.js +17 -1
  48. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2367 -2109
  49. package/dist/hq-cloud-sync-runner/package.json +1 -1
  50. package/dist/lib/agent-kit/inbox-state.d.ts +17 -0
  51. package/dist/lib/agent-kit/inbox-state.js +41 -20
  52. package/dist/lib/agent-kit/kit-config.d.ts +2 -0
  53. package/dist/lib/agent-kit/kit-config.js +6 -0
  54. package/dist/lib/agent-kit/mcp/tools.js +12 -5
  55. package/dist/lib/agent-kit/run/heartbeat.d.ts +6 -3
  56. package/dist/lib/agent-kit/run/heartbeat.js +6 -3
  57. package/dist/lib/agent-kit/run/inbox.d.ts +8 -3
  58. package/dist/lib/agent-kit/run/inbox.js +42 -10
  59. package/dist/lib/agent-kit/run/request-timeout.d.ts +26 -0
  60. package/dist/lib/agent-kit/run/request-timeout.js +51 -0
  61. package/dist/lib/bot/api.d.ts +1 -1
  62. package/dist/lib/bot/config.d.ts +10 -0
  63. package/dist/lib/bot/config.js +17 -1
  64. package/dist/lib/bot/decisions.d.ts +62 -0
  65. package/dist/lib/bot/decisions.js +228 -0
  66. package/dist/lib/bot/fleet-local.d.ts +127 -0
  67. package/dist/lib/bot/fleet-local.js +638 -0
  68. package/dist/lib/bot/index.d.ts +2 -0
  69. package/dist/lib/bot/index.js +2 -0
  70. package/dist/lib/bot/local-config.d.ts +3 -2
  71. package/dist/lib/bot/local-config.js +5 -2
  72. package/dist/lib/bot/promote.d.ts +10 -0
  73. package/dist/lib/bot/promote.js +3 -2
  74. package/dist/lib/bot/run.d.ts +7 -0
  75. package/dist/lib/bot/run.js +104 -1
  76. package/dist/lib/bot/runtime/index.js +8 -4
  77. package/dist/lib/bot/runtime/scratch-dir.d.ts +24 -0
  78. package/dist/lib/bot/runtime/scratch-dir.js +41 -0
  79. package/dist/lib/caller-principal.d.ts +4 -0
  80. package/dist/lib/caller-principal.js +40 -0
  81. package/dist/lib/capacity/collectors/grok.js +8 -2
  82. package/dist/lib/cli-flag-keys.d.ts +2 -0
  83. package/dist/lib/cli-flag-keys.js +2 -0
  84. package/dist/lib/daemon/catalog.d.ts +4 -0
  85. package/dist/lib/daemon/catalog.js +63 -29
  86. package/dist/lib/daemon/config.d.ts +7 -0
  87. package/dist/lib/daemon/config.js +32 -0
  88. package/dist/lib/daemon/run.d.ts +3 -2
  89. package/dist/lib/daemon/run.js +23 -3
  90. package/dist/lib/daemon/supervisor.d.ts +1 -0
  91. package/dist/lib/daemon/supervisor.js +4 -0
  92. package/dist/lib/daemon/sync-core-canary.d.ts +12 -0
  93. package/dist/lib/daemon/sync-core-canary.js +61 -0
  94. package/dist/lib/doctor/checks/release-links.d.ts +62 -1
  95. package/dist/lib/doctor/checks/release-links.js +188 -2
  96. package/dist/lib/doctor/checks/sync-health.d.ts +32 -0
  97. package/dist/lib/doctor/checks/sync-health.js +54 -1
  98. package/dist/lib/doctor/fix/apply.d.ts +26 -4
  99. package/dist/lib/doctor/fix/apply.js +149 -11
  100. package/dist/lib/doctor/fix/remediation.d.ts +8 -1
  101. package/dist/lib/doctor/fix/remediation.js +22 -0
  102. package/dist/lib/doctor/types.d.ts +1 -1
  103. package/dist/lib/doctor/types.js +1 -0
  104. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  105. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  106. package/dist/lib/index-render/index.d.ts +9 -0
  107. package/dist/lib/index-render/index.js +65 -5
  108. package/dist/lib/index-render/session-scope.d.ts +69 -0
  109. package/dist/lib/index-render/session-scope.js +279 -0
  110. package/dist/lib/lanes/admission.js +7 -20
  111. package/dist/lib/lanes/reconcile.js +13 -15
  112. package/dist/lib/lanes/stale-lock.d.ts +50 -0
  113. package/dist/lib/lanes/stale-lock.js +105 -0
  114. package/dist/lib/lanes/store.js +11 -76
  115. package/dist/lib/mesh/live/daemon/work-subscribe.js +31 -0
  116. package/dist/lib/notify-recipient.d.ts +10 -4
  117. package/dist/lib/notify-recipient.js +16 -4
  118. package/dist/lib/secret-input-link.d.ts +42 -0
  119. package/dist/lib/secret-input-link.js +93 -0
  120. package/dist/lib/secret-scan/range.d.ts +57 -0
  121. package/dist/lib/secret-scan/range.js +266 -0
  122. package/dist/lib/secret-scan/repo-config.d.ts +18 -3
  123. package/dist/lib/secret-scan/repo-config.js +135 -2
  124. package/dist/lib/work-context/organize.d.ts +7 -0
  125. package/dist/lib/work-context/organize.js +21 -1
  126. package/dist/run/script-bound-schema.d.ts +27 -0
  127. package/dist/run/script-bound-schema.js +97 -0
  128. package/dist/secrets-env-action.d.ts +2 -0
  129. package/dist/secrets-env-action.js +7 -2
  130. package/dist/sentry-before-send.js +1 -0
  131. package/dist/utils/cognito-session.d.ts +3 -30
  132. package/dist/utils/cognito-session.js +2 -100
  133. package/dist/utils/machine-principal-state.d.ts +38 -0
  134. package/dist/utils/machine-principal-state.js +110 -0
  135. package/dist/utils/safe-link.js +9 -0
  136. package/dist/utils/script-bound-exec.d.ts +158 -0
  137. package/dist/utils/script-bound-exec.js +568 -0
  138. package/dist/utils/sync-watchdog.d.ts +92 -0
  139. package/dist/utils/sync-watchdog.js +250 -0
  140. package/package.json +8 -8
@@ -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
@@ -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. A lock is stale when it was written on this host and its owner pid
177
- * no longer exists, or when it is older than the stale window. Age is the
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
- const statSync = deps.statSync ?? ((p) => fs.statSync(p));
188
- const linkSync = deps.linkSync ?? fs.linkSync.bind(fs);
189
- const before = statSync(lockPath);
190
- const raw = deps.readFileSync(lockPath, "utf8");
191
- const [pidLine, tsLine, hostLine] = raw.split("\n");
192
- const pid = pidLine ? Number(pidLine) : NaN;
193
- const ts = tsLine ? Number(tsLine) : NaN;
194
- const pidKnown = Number.isSafeInteger(pid) && pid > 0;
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);
@@ -12,6 +12,33 @@ import { backoffDelayMs, RECONNECT_MIN_DELAY_MS } from "./presence.js";
12
12
  import { AGENT_INBOX_PUSH_STATE_BASENAME, SYNC_WAKE_STATE_BASENAME } from "../paths.js";
13
13
  export { AGENT_INBOX_PUSH_STATE_BASENAME, SYNC_WAKE_STATE_BASENAME } from "../paths.js";
14
14
  const INBOX_PUSH_HEARTBEAT_MS = 5_000;
15
+ const SYNC_WAKE_KEYS = new Set(["v", "type", "vault", "head", "rootId"]);
16
+ const SYNC_WAKE_VAULT = /^(?:prs|cmp|agt)_[0-9A-Za-z]+$/;
17
+ const SYNC_WAKE_ROOT = /^[0-9a-f]{64}$/i;
18
+ function isSyncWakeEnvelope(payload) {
19
+ let parsed;
20
+ try {
21
+ parsed = JSON.parse(Buffer.isBuffer(payload) ? payload.toString("utf8") : payload);
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
27
+ return false;
28
+ const message = parsed;
29
+ const keys = Object.keys(message);
30
+ if (keys.length !== SYNC_WAKE_KEYS.size || keys.some((key) => !SYNC_WAKE_KEYS.has(key))) {
31
+ return false;
32
+ }
33
+ return message.v === 1 &&
34
+ message.type === "vault.head" &&
35
+ typeof message.vault === "string" &&
36
+ SYNC_WAKE_VAULT.test(message.vault) &&
37
+ Number.isSafeInteger(message.head) &&
38
+ Number(message.head) > 0 &&
39
+ (message.rootId === null ||
40
+ (typeof message.rootId === "string" && SYNC_WAKE_ROOT.test(message.rootId)));
41
+ }
15
42
  function readWakeSequence(meshRoot, basename, label, onInfo) {
16
43
  const target = path.join(meshRoot, basename);
17
44
  try {
@@ -260,6 +287,10 @@ export async function startWorkCacheSubscriber(opts) {
260
287
  return;
261
288
  }
262
289
  if (syncTopic && _topic === syncTopic && syncSubscribed) {
290
+ if (!isSyncWakeEnvelope(payload)) {
291
+ opts.onInfo?.("sync wake ignored (invalid envelope)");
292
+ return;
293
+ }
263
294
  syncWakeSequence += 1;
264
295
  persistSyncState(true);
265
296
  return;
@@ -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: a 1:1 message to a person is for that
18
- * person (`human`); a message to an agent, a channel or a group is tagged
19
- * `agent` and folded away from people by default.
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: a 1:1 message to a person is for that
20
- * person (`human`); a message to an agent, a channel or a group is tagged
21
- * `agent` and folded away from people by default.
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