@indigoai-us/hq-cli 5.309.1 → 5.311.0

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 (99) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/assets/scaffold/core/scripts/hq-dm-bind.sh +585 -0
  3. package/assets/scaffold/core/scripts/jobs-validate.sh +1 -1
  4. package/assets/scaffold/core/scripts/work-mesh-live-bind-trusted.sh +152 -0
  5. package/dist/command-catalog.generated.d.ts +28 -2
  6. package/dist/command-catalog.generated.js +34 -2
  7. package/dist/commands/channels.d.ts +1 -9
  8. package/dist/commands/channels.js +2 -2
  9. package/dist/commands/company-transfer.d.ts +4 -28
  10. package/dist/commands/company-transfer.js +5 -49
  11. package/dist/commands/core-checkpoint-stop-gate-native.d.ts +34 -0
  12. package/dist/commands/core-checkpoint-stop-gate-native.js +713 -0
  13. package/dist/commands/core-checkpoint.d.ts +0 -24
  14. package/dist/commands/core-checkpoint.js +4 -4
  15. package/dist/commands/core-command-catalog.js +1 -1
  16. package/dist/commands/core-dm-bind-native.d.ts +32 -0
  17. package/dist/commands/core-dm-bind-native.js +668 -0
  18. package/dist/commands/core-hq-session-native.d.ts +39 -0
  19. package/dist/commands/core-hq-session-native.js +579 -0
  20. package/dist/commands/core-jobs-native.js +1 -4
  21. package/dist/commands/core-native-session-flag.d.ts +4 -0
  22. package/dist/commands/core-native-session-flag.js +7 -0
  23. package/dist/commands/core-native-utilities.d.ts +2 -1
  24. package/dist/commands/core-native-utilities.js +4 -2
  25. package/dist/commands/core-native-worker-skills-flag.d.ts +4 -0
  26. package/dist/commands/core-native-worker-skills-flag.js +7 -0
  27. package/dist/commands/core-work-mesh-bind-trusted-native.d.ts +32 -0
  28. package/dist/commands/core-work-mesh-bind-trusted-native.js +190 -0
  29. package/dist/commands/core-worker-lint-native.d.ts +4 -0
  30. package/dist/commands/core-worker-lint-native.js +160 -0
  31. package/dist/commands/core-worker-share-native.d.ts +5 -0
  32. package/dist/commands/core-worker-share-native.js +260 -0
  33. package/dist/commands/core-worker-skills-command.d.ts +8 -0
  34. package/dist/commands/core-worker-skills-command.js +33 -0
  35. package/dist/commands/core-worker-skills-runtime.d.ts +33 -0
  36. package/dist/commands/core-worker-skills-runtime.js +86 -0
  37. package/dist/commands/core.js +10 -2
  38. package/dist/commands/dm.d.ts +3 -7
  39. package/dist/commands/dm.js +3 -3
  40. package/dist/commands/integrations-import.d.ts +1 -1
  41. package/dist/commands/integrations-import.js +34 -19
  42. package/dist/commands/lanes-accounts.js +3 -1
  43. package/dist/commands/lanes-message.js +84 -10
  44. package/dist/commands/lanes-shared.d.ts +4 -1
  45. package/dist/commands/lanes.d.ts +4 -1
  46. package/dist/commands/lanes.js +48 -7
  47. package/dist/commands/personal-connector-settings-flag.d.ts +4 -0
  48. package/dist/commands/personal-connector-settings-flag.js +7 -0
  49. package/dist/commands/report.d.ts +1 -26
  50. package/dist/commands/report.js +6 -6
  51. package/dist/fast-core.js +15 -2
  52. package/dist/fast-scaffold.js +25 -2
  53. package/dist/index.js +8 -0
  54. package/dist/lib/bot/continuity.d.ts +2 -1
  55. package/dist/lib/bot/job-completions.d.ts +0 -2
  56. package/dist/lib/bot/job-completions.js +2 -2
  57. package/dist/lib/bot/local-config.d.ts +0 -10
  58. package/dist/lib/bot/local-config.js +3 -9
  59. package/dist/lib/bot/owner-context.d.ts +2 -6
  60. package/dist/lib/bot/owner-context.js +3 -3
  61. package/dist/lib/bot/runtime/windows.d.ts +0 -2
  62. package/dist/lib/bot/runtime/windows.js +1 -1
  63. package/dist/lib/bot/setup-explain.d.ts +0 -4
  64. package/dist/lib/bot/setup-explain.js +2 -2
  65. package/dist/lib/daemon/catalog.d.ts +3 -2
  66. package/dist/lib/daemon/hq-cloud.d.ts +2 -2
  67. package/dist/lib/daemon/hq-cloud.js +1 -1
  68. package/dist/lib/daemon/machine-type.d.ts +3 -4
  69. package/dist/lib/daemon/machine-type.js +2 -2
  70. package/dist/lib/daemon/remote-control.d.ts +0 -1
  71. package/dist/lib/daemon/remote-control.js +1 -1
  72. package/dist/lib/daemon/supervisor.d.ts +4 -9
  73. package/dist/lib/daemon/supervisor.js +5 -5
  74. package/dist/lib/lanes/admission-host.d.ts +3 -0
  75. package/dist/lib/lanes/admission-host.js +4 -0
  76. package/dist/lib/lanes/autopilot-registry.d.ts +62 -0
  77. package/dist/lib/lanes/autopilot-registry.js +192 -0
  78. package/dist/lib/lanes/autopilot.d.ts +2 -0
  79. package/dist/lib/lanes/autopilot.js +18 -5
  80. package/dist/lib/lanes/claude-balance.d.ts +6 -0
  81. package/dist/lib/lanes/claude-balance.js +17 -2
  82. package/dist/lib/lanes/keep-alive.d.ts +3 -0
  83. package/dist/lib/lanes/keep-alive.js +3 -0
  84. package/dist/lib/lanes/reconcile.js +14 -6
  85. package/dist/lib/lanes/spawn.js +2 -0
  86. package/dist/lib/mesh/live/daemon/credentials.d.ts +2 -17
  87. package/dist/lib/mesh/live/daemon/credentials.js +9 -9
  88. package/dist/lib/mesh/live/daemon/index.d.ts +5 -5
  89. package/dist/lib/mesh/live/daemon/index.js +2 -2
  90. package/dist/lib/mesh/live/daemon/state.d.ts +1 -1
  91. package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +4 -11
  92. package/dist/lib/mesh/live/daemon/transcript-watch.js +5 -5
  93. package/dist/lib/work-context/company.d.ts +2 -25
  94. package/dist/lib/work-context/company.js +3 -3
  95. package/dist/lib/work-context/config.d.ts +3 -8
  96. package/dist/lib/work-context/config.js +2 -13
  97. package/dist/lib/work-context/state.d.ts +4 -13
  98. package/dist/lib/work-context/state.js +2 -30
  99. package/package.json +1 -1
@@ -0,0 +1,668 @@
1
+ /** Native implementation of core/scripts/hq-dm-bind.sh. */
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+ import { resolveLiveRoot } from "../utils/hq-roots.js";
7
+ import { bundledShellUnboundValue, registerNativeUtility, utilityError } from "./core-native-utilities.js";
8
+ export const DM_BIND_ASSET = "core/scripts/hq-dm-bind.sh";
9
+ export const DM_BIND_HELP = `# hq-dm-bind.sh — bind the current session to one HQ DM channel, post
10
+ # structured updates to it, and listen for replies (feedback / requests).
11
+ #
12
+ # Usage:
13
+ # core/scripts/hq-dm-bind.sh bind <channel> # bind + set the read cursor to "now"
14
+ # core/scripts/hq-dm-bind.sh status # show the binding and cursor
15
+ # core/scripts/hq-dm-bind.sh post --title <t> [--state <s>] [--line <l>]... [--next <l>]... [--ask <l>]... [--to <name>]... [--topic <t>] [--under <id>] [--new-thread]
16
+ # core/scripts/hq-dm-bind.sh post --title <t> - < body.md # body from stdin (already structured)
17
+ # core/scripts/hq-dm-bind.sh threads # list the channel's recent top-level threads (id, replies, author, text)
18
+ # core/scripts/hq-dm-bind.sh roster # print the channel members a post will @-mention
19
+ # core/scripts/hq-dm-bind.sh poll # print new messages since the cursor, advance it
20
+ # core/scripts/hq-dm-bind.sh listen [--interval 60] [--timeout 1800]
21
+ # # block until a new message arrives (or timeout)
22
+ # core/scripts/hq-dm-bind.sh unbind
23
+ #
24
+ # The binding lives in the session's meta.yaml (\`dm_channel\`) via hq-session.sh;
25
+ # the cursor is \`workspace/sessions/<sid>/dm-bind.cursor\` (the last seen sort
26
+ # key from \`hq dm channel --json\`). \`listen\` is meant to run detached
27
+ # (Bash run_in_background): it exits 0 and prints the new messages the moment
28
+ # one lands, exits 3 on timeout with nothing new.
29
+ #
30
+ # Every post opens with an @-mention of every other member of the channel, so
31
+ # the update reaches people (HQ mentions are structured: the \`hq\` CLI resolves
32
+ # the \`@"Display Name"\` tokens against the roster and sends the real mention
33
+ `;
34
+ export const createDmBindDeps = (root, cwd, env, now, home = env.HOME || os.homedir()) => ({
35
+ root, cwd, env, home, now, fs,
36
+ runHq: (args, childEnv) => {
37
+ const result = spawnSync("hq", args, { cwd, env: { ...childEnv, HQ_ROOT: root }, encoding: "utf8" });
38
+ return { code: result.status ?? 127, stdout: result.stdout ?? "", stderr: result.stderr ?? (result.error ? `${result.error.message}\n` : "") };
39
+ },
40
+ runHqSession: (args, childEnv) => {
41
+ const result = spawnSync("bash", [path.join(root, "core/scripts/hq-session.sh"), ...args], { cwd, env: childEnv, encoding: "utf8" });
42
+ return { code: result.status ?? 127, stdout: result.stdout ?? "", stderr: result.stderr ?? (result.error ? `${result.error.message}\n` : "") };
43
+ },
44
+ fetch: globalThis.fetch,
45
+ readStdin: () => new Promise((resolve, reject) => {
46
+ let data = "";
47
+ process.stdin.setEncoding("utf8");
48
+ process.stdin.on("data", (chunk) => { data += chunk; });
49
+ process.stdin.on("end", () => resolve(data));
50
+ process.stdin.on("error", reject);
51
+ }),
52
+ sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
53
+ });
54
+ const fail = (message, code = 2) => utilityError(`hq-dm-bind: ${message}`, code);
55
+ const sessionDirectory = (d, sid) => path.join(d.root, "workspace/sessions", sid);
56
+ const filePath = (d, sid, name) => path.join(sessionDirectory(d, sid), name);
57
+ const sessionArgs = (d, args) => d.runHqSession(args, d.env);
58
+ const validSid = (value) => value !== "" && value !== "." && value !== ".." && /^[A-Za-z0-9._-]+$/.test(value);
59
+ export function resolveDmBindSessionId(d) {
60
+ for (const key of ["HQ_SESSION_ID", "CLAUDE_CODE_SESSION_ID", "CLAUDE_SESSION_ID", "CODEX_SESSION_ID", "CODEX_THREAD_ID", "GROK_SESSION_ID"]) {
61
+ const candidate = (d.env[key] ?? "").replace(/\s/g, "");
62
+ if (validSid(candidate))
63
+ return candidate;
64
+ }
65
+ try {
66
+ const candidate = d.fs.readFileSync(path.join(d.root, "workspace/sessions/.current"), "utf8").replace(/\s/g, "");
67
+ return validSid(candidate) ? candidate : "";
68
+ }
69
+ catch {
70
+ return "";
71
+ }
72
+ }
73
+ function sessionId(d) {
74
+ const result = sessionArgs(d, ["current"]);
75
+ return result.stdout.replace(/\n+$/g, "");
76
+ }
77
+ function channel(d) {
78
+ const result = sessionArgs(d, ["get", "dm_channel"]);
79
+ return result.code === 0 ? result.stdout.replace(/"/g, "").replace(/\n+$/g, "") : "";
80
+ }
81
+ function requireChannel(d) {
82
+ const value = channel(d);
83
+ return value && value !== "null" ? value : fail("session is not bound — run: core/scripts/hq-dm-bind.sh bind <channel>");
84
+ }
85
+ function ensureDir(d, sid) {
86
+ const directory = sessionDirectory(d, sid);
87
+ d.fs.mkdirSync(directory, { recursive: true });
88
+ return directory;
89
+ }
90
+ function hasHqOnPath(d) {
91
+ for (const directory of (d.env.PATH ?? "").split(path.delimiter)) {
92
+ if (!directory)
93
+ continue;
94
+ try {
95
+ const stat = d.fs.statSync(path.join(directory, "hq"));
96
+ if (stat.isFile() && (stat.mode & 0o111) !== 0)
97
+ return true;
98
+ }
99
+ catch { /* PATH entry is absent */ }
100
+ }
101
+ return false;
102
+ }
103
+ function textFile(d, file) {
104
+ try {
105
+ return d.fs.readFileSync(file, "utf8");
106
+ }
107
+ catch {
108
+ return "";
109
+ }
110
+ }
111
+ function jsonObject(value) {
112
+ try {
113
+ const parsed = JSON.parse(value);
114
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : undefined;
115
+ }
116
+ catch {
117
+ return undefined;
118
+ }
119
+ }
120
+ function itemsFromHq(d, ch, limit = 30) {
121
+ const result = d.runHq(["dm", "channel", ch, "--limit", String(limit), "--json"], { ...d.env, HQ_ROOT: d.root });
122
+ try {
123
+ const parsed = JSON.parse(result.stdout);
124
+ const object = parsed;
125
+ const rows = Array.isArray(parsed) ? parsed : (jqAlternative(jqAlternative(object?.messages, object?.items), []));
126
+ return Array.isArray(rows) ? rows.filter((row) => !!row && typeof row === "object" && !Array.isArray(row)) : [];
127
+ }
128
+ catch {
129
+ return [];
130
+ }
131
+ }
132
+ function jqAlternative(value, fallback) {
133
+ return value === null || value === undefined || value === false ? fallback : value;
134
+ }
135
+ function latestSk(d, ch, limit = 5) {
136
+ const rows = itemsFromHq(d, ch, limit);
137
+ return String(jqAlternative(rows.at(-1)?.sk, ""));
138
+ }
139
+ function cliVersion(d) {
140
+ const result = d.runHq(["--version"], { ...d.env, HQ_ROOT: d.root });
141
+ if (result.code !== 0)
142
+ throw new Error("hq-dm-bind: could not read the installed hq-cli version");
143
+ const first = result.stdout.match(/^CLI\s+([0-9]+(?:\.[0-9]+){2})/m)?.[1];
144
+ return first ?? result.stdout.match(/[0-9]+\.[0-9]+\.[0-9]+/)?.[0] ?? "";
145
+ }
146
+ function requireCliVersion(d) {
147
+ let actual;
148
+ try {
149
+ actual = cliVersion(d);
150
+ }
151
+ catch (error) {
152
+ return utilityError(String(error instanceof Error ? error.message : error), 2);
153
+ }
154
+ const nums = actual.split(".").map(Number);
155
+ const okay = nums.length === 3 && (nums[0] > 5 || (nums[0] === 5 && (nums[1] > 137 || (nums[1] === 137 && nums[2] >= 0))));
156
+ return okay ? undefined : utilityError(`hq-dm-bind: hq-cli >= 5.137.0 is required for machine-principal DM binding (found ${actual || "unknown"}); update hq-cli and retry.`, 2);
157
+ }
158
+ function usesCliAuth(d) {
159
+ try {
160
+ d.fs.accessSync(path.join(d.home, ".hq/config.json"), fs.constants.R_OK);
161
+ d.fs.accessSync(path.join(d.home, ".hq/cognito-tokens.json"), fs.constants.R_OK);
162
+ return false;
163
+ }
164
+ catch {
165
+ return true;
166
+ }
167
+ }
168
+ function whoamiText(d, json = false) {
169
+ const result = d.runHq(json ? ["whoami", "--json"] : ["whoami"], { ...d.env, HQ_ROOT: d.root });
170
+ return result.code === 0 ? result.stdout : "";
171
+ }
172
+ function selfEmail(d) { return whoamiText(d).match(/[A-Za-z0-9._+-]+@[A-Za-z0-9.-]+\.[A-Za-z]+/)?.[0] ?? ""; }
173
+ function slug(value) { return String(value ?? "").trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""); }
174
+ async function fetchRoster(d, ch) {
175
+ const me = selfEmail(d);
176
+ if (usesCliAuth(d)) {
177
+ const versionError = requireCliVersion(d);
178
+ if (versionError)
179
+ return { stdout: "", stderr: versionError.stderr };
180
+ const result = d.runHq(["channels", "members", ch], { ...d.env, HQ_ROOT: d.root });
181
+ return { stdout: result.stdout, stderr: result.code === 0 ? result.stderr : `${result.stderr}hq-dm-bind: could not read the channel roster through hq-cli\n` };
182
+ }
183
+ try {
184
+ const config = JSON.parse(d.fs.readFileSync(path.join(d.home, ".hq/config.json"), "utf8"));
185
+ const token = JSON.parse(d.fs.readFileSync(path.join(d.home, ".hq/cognito-tokens.json"), "utf8")).idToken;
186
+ const headers = { Authorization: `Bearer ${token}` };
187
+ const response = await d.fetch(`${config.vaultApiUrl}/v1/notify/channels`, { headers });
188
+ const channels = await response.json();
189
+ const rows = Array.isArray(channels) ? channels : channels.channels ?? [];
190
+ const hit = rows.find((entry) => entry.channelId === ch || slug(entry.name) === slug(ch) || entry.slug === slug(ch));
191
+ if (!hit?.channelId)
192
+ return { stdout: "", stderr: "" };
193
+ const membersResponse = await d.fetch(`${config.vaultApiUrl}/v1/notify/channels/${encodeURIComponent(String(hit.channelId))}/members`, { headers });
194
+ const members = await membersResponse.json();
195
+ const list = Array.isArray(members) ? members : members.members ?? [];
196
+ const lines = list.flatMap((member) => {
197
+ if (me && String(member.email ?? "").toLowerCase() === me.toLowerCase())
198
+ return [];
199
+ const name = String(member.displayName ?? "").trim();
200
+ const uid = String(member.personUid ?? member.participantUid ?? member.uid ?? "").trim();
201
+ return name ? [`${name}\t${uid}`] : [];
202
+ });
203
+ return { stdout: lines.length ? `${lines.join("\n")}\n` : "", stderr: "" };
204
+ }
205
+ catch {
206
+ return { stdout: "", stderr: "" };
207
+ }
208
+ }
209
+ async function roster(d, sid, ch) {
210
+ const file = filePath(d, sid, "dm-bind.roster");
211
+ if (usesCliAuth(d)) {
212
+ const versionError = requireCliVersion(d);
213
+ if (versionError)
214
+ return { names: [], stderr: versionError.stderr };
215
+ }
216
+ const fresh = await fetchRoster(d, ch);
217
+ let stderr = fresh.stderr;
218
+ if (fresh.stdout.trim())
219
+ d.fs.writeFileSync(file, `${fresh.stdout.replace(/\n+$/, "")}\n`);
220
+ const contents = textFile(d, file);
221
+ const names = contents.split(/\r?\n/).filter(Boolean).map((row) => row.split("\t")[0] ?? "");
222
+ return { names, stderr };
223
+ }
224
+ function rosterRows(d, sid) {
225
+ return textFile(d, filePath(d, sid, "dm-bind.roster")).split(/\r?\n/).filter(Boolean).map((row) => {
226
+ const [name = "", uid = ""] = row.split("\t");
227
+ return { name, uid };
228
+ });
229
+ }
230
+ function mentionsJson(d, sid, names) {
231
+ const uid = new Map(rosterRows(d, sid).map(({ name, uid: value }) => [name, value]));
232
+ return names.filter((name) => jqAlternative(uid.get(name), "") !== "").map((name) => {
233
+ const participantUid = uid.get(name);
234
+ return { participantUid, participantType: participantUid.startsWith("agt_") ? "agent" : "human", displayName: name };
235
+ });
236
+ }
237
+ function getTopicRoot(d, sid, topic) {
238
+ const rows = textFile(d, filePath(d, sid, "dm-bind.threads")).split(/\r?\n/);
239
+ let found = "";
240
+ for (const line of rows) {
241
+ const [key, root] = line.split("\t");
242
+ if (key === topic && root)
243
+ found = root;
244
+ }
245
+ return found;
246
+ }
247
+ function addTopicRoot(d, sid, topic, root) {
248
+ d.fs.appendFileSync(filePath(d, sid, "dm-bind.threads"), `${topic}\t${root}\n`);
249
+ }
250
+ function recentRoots(d, ch) { return itemsFromHq(d, ch, 100).filter((row) => !row.rootEventId); }
251
+ function resolveRoot(d, ch, prefix) {
252
+ const hits = recentRoots(d, ch).map((row) => String(jqAlternative(row.eventId, ""))).filter((id) => id.startsWith(prefix));
253
+ if (!hits.length)
254
+ return fail(`--under '${prefix}' matches no recent top-level message (see: hq-dm-bind.sh threads)`);
255
+ if (hits.length !== 1)
256
+ return fail(`--under '${prefix}' matches several messages — use more of the id`);
257
+ return hits[0];
258
+ }
259
+ function findMentions(d, sid, ch, tos) {
260
+ // Caller ensures the directory exists before using these per-session files.
261
+ const content = textFile(d, filePath(d, sid, "dm-bind.roster"));
262
+ const allNames = content.split(/\r?\n/).filter(Boolean).map((line) => line.split("\t")[0] ?? "");
263
+ const stderr = "";
264
+ if (!allNames.length)
265
+ return { names: [], stderr, error: fail("could not read the channel roster — not posting an update that notifies nobody (check `hq whoami` and that node is on PATH, then retry)") };
266
+ const rows = rosterRows(d, sid);
267
+ let selected;
268
+ if (!tos.length) {
269
+ selected = rows.filter((row) => !row.uid.startsWith("agt_")).map((row) => row.name);
270
+ if (!selected.length)
271
+ selected = allNames;
272
+ }
273
+ else {
274
+ selected = [];
275
+ for (let want of tos) {
276
+ want = want.replace(/^@/, "");
277
+ let hit = allNames.find((name) => name.toLowerCase() === want.toLowerCase()) ?? "";
278
+ if (!hit) {
279
+ const matches = allNames.filter((name) => name.toLowerCase().includes(want.toLowerCase()));
280
+ if (matches.length === 1)
281
+ hit = matches[0];
282
+ }
283
+ if (!hit) {
284
+ const members = allNames.map((name) => ` ${name}`).join("\n");
285
+ return { names: [], stderr: "", error: utilityError(`hq-dm-bind: --to '${want}' does not match exactly one channel member. Members:\n${members}`, 2) };
286
+ }
287
+ if (!selected.includes(hit))
288
+ selected.push(hit);
289
+ }
290
+ }
291
+ return { value: selected.map((name) => `@"${name}"`).join(" "), names: selected, stderr };
292
+ }
293
+ function identity(d) {
294
+ const raw = whoamiText(d, true);
295
+ const parsed = jsonObject(raw);
296
+ return parsed ?? utilityError("hq-dm-bind: hq-cli returned an invalid identity", 1);
297
+ }
298
+ function isUtilityOutput(value) {
299
+ return typeof value.code === "number" && typeof value.stdout === "string" && typeof value.stderr === "string";
300
+ }
301
+ function latestCliEventId(d, ch) {
302
+ const who = identity(d);
303
+ if (isUtilityOutput(who))
304
+ return who;
305
+ const agent = String(jqAlternative(who.agentUid, ""));
306
+ const username = String(jqAlternative(who.username, ""));
307
+ const person = String(jqAlternative(who.personUid, ""));
308
+ const email = String(jqAlternative(who.email, ""));
309
+ const rows = itemsFromHq(d, ch, 50).filter((row) => {
310
+ if (agent || username) {
311
+ const sender = String(jqAlternative(row.senderMachineUid, jqAlternative(row.fromMachineUid, "")));
312
+ return sender === agent || sender === username;
313
+ }
314
+ if (person)
315
+ return row.fromPersonUid === person;
316
+ if (email)
317
+ return String(jqAlternative(row.fromEmail, "")).toLowerCase() === email.toLowerCase();
318
+ return false;
319
+ });
320
+ return String(jqAlternative(rows.at(-1)?.eventId, jqAlternative(rows.at(-1)?.messageId, "")));
321
+ }
322
+ async function sendHuman(d, ch, payload) {
323
+ const config = JSON.parse(d.fs.readFileSync(path.join(d.home, ".hq/config.json"), "utf8"));
324
+ const token = JSON.parse(d.fs.readFileSync(path.join(d.home, ".hq/cognito-tokens.json"), "utf8")).idToken;
325
+ const headers = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
326
+ const channelsResponse = await d.fetch(`${config.vaultApiUrl}/v1/notify/channels`, { headers });
327
+ const channels = await channelsResponse.json();
328
+ const rows = Array.isArray(channels) ? channels : channels.channels ?? [];
329
+ const hit = rows.find((entry) => entry.channelId === ch || slug(entry.name) === slug(ch) || entry.slug === slug(ch));
330
+ if (!hit?.channelId)
331
+ throw new Error(`channel not found: ${ch}`);
332
+ const response = await d.fetch(`${config.vaultApiUrl}/v1/notify/channels/${encodeURIComponent(String(hit.channelId))}/messages`, {
333
+ method: "POST", headers, body: JSON.stringify(payload),
334
+ });
335
+ const text = await response.text();
336
+ if (!response.ok)
337
+ throw new Error(`post failed: HTTP ${response.status} ${text.slice(0, 300)}`);
338
+ let json = {};
339
+ try {
340
+ json = JSON.parse(text);
341
+ }
342
+ catch { /* jq shell ignores malformed receipt JSON */ }
343
+ const message = jqAlternative(json.message, jqAlternative(json.event, json));
344
+ const sk = typeof message.sk === "string" && message.sk.includes("#") ? message.sk.split("#").at(-1) : "";
345
+ return String(jqAlternative(message.eventId, jqAlternative(sk, "")));
346
+ }
347
+ const stringValue = (value, fallback = "") => typeof value === "string" ? value : fallback;
348
+ async function bind(d, arg) {
349
+ if (!arg)
350
+ return fail("bind needs a channel name (see: hq channels)");
351
+ const ch = arg.replace(/^#/, "");
352
+ const channels = d.runHq(["channels"], { ...d.env, HQ_ROOT: d.root });
353
+ if (channels.code !== 0 || !channels.stdout.includes(`hq dm ${ch} `))
354
+ return fail(`no channel named '${ch}' (see: hq channels)`);
355
+ const set = sessionArgs(d, ["set", "dm_channel", ch]);
356
+ if (set.code !== 0)
357
+ return { code: set.code, stdout: "", stderr: set.stderr };
358
+ const sk = latestSk(d, ch);
359
+ const sid = sessionId(d);
360
+ ensureDir(d, sid);
361
+ d.fs.writeFileSync(filePath(d, sid, "dm-bind.cursor"), `${sk}\n`);
362
+ const result = await roster(d, sid, ch);
363
+ const n = result.names.length;
364
+ return { code: 0, stdout: `bound session ${sid} to #${ch} (cursor: ${sk || "<empty>"}; will @-mention ${n} member(s))\n`, stderr: result.stderr + (n ? "" : "hq-dm-bind: could not read the channel roster — posts will not @-mention anyone until it is readable\n") };
365
+ }
366
+ function status(d) {
367
+ const sid = sessionId(d);
368
+ if (sid)
369
+ ensureDir(d, sid);
370
+ const ch = channel(d);
371
+ const cursor = sid ? textFile(d, filePath(d, sid, "dm-bind.cursor")).replace(/\n$/, "") || "<none>" : "<none>";
372
+ const stderr = sid ? "" : "hq-dm-bind: no current session (hq-session.sh current is empty)\n";
373
+ return { code: 0, stdout: `session: ${sid}\nchannel: ${ch || "<none>"}\ncursor: ${cursor}\n`, stderr };
374
+ }
375
+ function unbind(d) {
376
+ const set = sessionArgs(d, ["set", "dm_channel", ""]);
377
+ if (set.code !== 0)
378
+ return { code: set.code, stdout: "", stderr: set.stderr };
379
+ const sid = sessionId(d);
380
+ for (const name of ["dm-bind.cursor", "dm-bind.roster", "dm-bind.threads"])
381
+ d.fs.rmSync(filePath(d, sid, name), { force: true });
382
+ return { code: 0, stdout: "unbound\n", stderr: "" };
383
+ }
384
+ async function cmdRoster(d) {
385
+ const ch = requireChannel(d);
386
+ if (typeof ch !== "string")
387
+ return ch;
388
+ const sid = sessionId(d);
389
+ ensureDir(d, sid);
390
+ const result = await roster(d, sid, ch);
391
+ return { code: 0, stdout: result.names.length ? `${result.names.join("\n")}\n` : "", stderr: result.stderr };
392
+ }
393
+ async function cmdThreads(d) {
394
+ const ch = requireChannel(d);
395
+ if (typeof ch !== "string")
396
+ return ch;
397
+ const rows = recentRoots(d, ch).slice(-25).map((row) => {
398
+ const eventId = Array.from(String(jqAlternative(row.eventId, ""))).slice(0, 8).join("");
399
+ const body = Array.from(String(jqAlternative(row.body, "")).replace(/\s+/g, " ")).slice(0, 90).join("");
400
+ return `${eventId}\t${String(jqAlternative(row.replyCount, 0))}\t${String(jqAlternative(row.fromDisplayName, "?"))}: ${body}`;
401
+ });
402
+ return { code: 0, stdout: rows.length ? `${rows.join("\n")}\n` : "", stderr: "" };
403
+ }
404
+ function formatMessages(rows, cursor, own) {
405
+ return rows.filter((row) => String(jqAlternative(row.sk, "")) > cursor && !own(row)).map((row) => {
406
+ const sk = Array.from(String(jqAlternative(row.sk, ""))).slice(0, 16).join("").replace("T", " ");
407
+ const name = String(jqAlternative(row.fromDisplayName, jqAlternative(row.fromEmail, "?")));
408
+ const body = String(jqAlternative(row.body, "")).replace(/^\s+|\s+$/g, "");
409
+ return `${sk}Z ${name}: ${body}`;
410
+ }).join("\n");
411
+ }
412
+ async function poll(d) {
413
+ const ch = requireChannel(d);
414
+ if (typeof ch !== "string")
415
+ return ch;
416
+ const sid = sessionId(d);
417
+ ensureDir(d, sid);
418
+ const cursor = textFile(d, filePath(d, sid, "dm-bind.cursor"));
419
+ const rows = itemsFromHq(d, ch, 50);
420
+ const last = String(jqAlternative(rows.at(-1)?.sk, ""));
421
+ let own;
422
+ if (usesCliAuth(d)) {
423
+ const who = identity(d);
424
+ if ("code" in who)
425
+ return { code: 2, stdout: "", stderr: `${who.stderr}${fail("could not read the hq-cli identity — not polling").stderr}` };
426
+ const agent = String(jqAlternative(who.agentUid, ""));
427
+ const username = String(jqAlternative(who.username, ""));
428
+ const person = String(jqAlternative(who.personUid, ""));
429
+ const email = String(jqAlternative(who.email, ""));
430
+ own = (row) => {
431
+ if (agent || username) {
432
+ const sender = String(jqAlternative(row.senderMachineUid, jqAlternative(row.fromMachineUid, "")));
433
+ return sender === agent || sender === username;
434
+ }
435
+ if (person)
436
+ return String(jqAlternative(row.fromPersonUid, "")) === person;
437
+ if (email)
438
+ return String(jqAlternative(row.fromEmail, "")).toLowerCase() === email.toLowerCase();
439
+ return false;
440
+ };
441
+ }
442
+ else {
443
+ const me = selfEmail(d);
444
+ own = (row) => me !== "" && row.fromEmail === me;
445
+ }
446
+ const messages = formatMessages(rows, cursor, own);
447
+ if (last)
448
+ d.fs.writeFileSync(filePath(d, sid, "dm-bind.cursor"), `${last}\n`);
449
+ return messages.trim() ? { code: 0, stdout: `${messages}\n`, stderr: "" } : { code: 3, stdout: "", stderr: "" };
450
+ }
451
+ async function post(d, args) {
452
+ const ch = requireChannel(d);
453
+ if (typeof ch !== "string")
454
+ return ch;
455
+ let title = "", state = "", stdin = false, topic = "", newThread = false, under = "", allowLong = false;
456
+ const lines = [], nexts = [], asks = [], tos = [];
457
+ for (let i = 0; i < args.length;) {
458
+ const arg = args[i];
459
+ if (["--topic", "--under", "--to", "--title", "--state", "--line", "--next", "--ask"].includes(arg)) {
460
+ if (args[i + 1] === undefined) {
461
+ const sourceLines = { "--topic": 383, "--title": 388, "--state": 389, "--line": 390, "--next": 391, "--ask": 392 };
462
+ if (sourceLines[arg] !== undefined)
463
+ return bundledShellUnboundValue(DM_BIND_ASSET, sourceLines[arg]);
464
+ }
465
+ const value = args[i + 1] ?? "";
466
+ if (arg === "--under" && !value)
467
+ return fail("post: --under needs a message id (see: threads)");
468
+ if (arg === "--to" && !value)
469
+ return fail("post: --to needs a member name");
470
+ if (arg === "--topic")
471
+ topic = value;
472
+ else if (arg === "--under")
473
+ under = value;
474
+ else if (arg === "--to")
475
+ tos.push(value);
476
+ else if (arg === "--title")
477
+ title = value;
478
+ else if (arg === "--state")
479
+ state = value;
480
+ else if (arg === "--line")
481
+ lines.push(value);
482
+ else if (arg === "--next")
483
+ nexts.push(value);
484
+ else
485
+ asks.push(value);
486
+ i += 2;
487
+ }
488
+ else if (arg === "--new-thread") {
489
+ newThread = true;
490
+ i += 1;
491
+ }
492
+ else if (arg === "--no-mention")
493
+ return fail("post: --no-mention was removed — every post @-mentions someone. Use --to <name> to address specific members.");
494
+ else if (arg === "--allow-long") {
495
+ allowLong = true;
496
+ i += 1;
497
+ }
498
+ else if (arg === "-") {
499
+ stdin = true;
500
+ i += 1;
501
+ }
502
+ else
503
+ return fail(`post: unknown arg ${arg}`);
504
+ }
505
+ if (!title)
506
+ return fail("post needs --title");
507
+ const errorLines = [];
508
+ if (!allowLong && !stdin) {
509
+ if (lines.length > 6)
510
+ errorLines.push(`hq-dm-bind: too many --line bullets (${lines.length}, limit 6). Put extra detail in workspace/reports/<name>.md and link it in one bullet. Use --allow-long to override.`);
511
+ if (nexts.length > 2)
512
+ errorLines.push(`hq-dm-bind: too many --next items (${nexts.length}, limit 2). Use --allow-long to override.`);
513
+ if (asks.length > 1)
514
+ errorLines.push(`hq-dm-bind: too many --ask items (${asks.length}, limit 1). Use --allow-long to override.`);
515
+ for (const [kind, values] of [["line", lines], ["next", nexts], ["ask", asks]])
516
+ for (const value of values) {
517
+ const length = Buffer.byteLength(value);
518
+ if (length > 140)
519
+ errorLines.push(Buffer.concat([Buffer.from(`hq-dm-bind: --${kind} too long (${length} chars, limit 140): "`), Buffer.from(value).subarray(0, 60), Buffer.from('". Put the detail in workspace/reports/<name>.md and reference the path in one line. Use --allow-long to override.')]).toString("latin1"));
520
+ }
521
+ if (errorLines.length) {
522
+ const lines = errorLines.map((line) => Buffer.from(line, "latin1"));
523
+ return { code: 2, stdout: "", stderr: "", stderrBytes: Buffer.concat([...lines.flatMap((line, index) => index ? [Buffer.from("\n"), line] : [line]), Buffer.from("\n")]) };
524
+ }
525
+ }
526
+ let message = title + (state ? ` — ${state}` : "");
527
+ if (stdin)
528
+ message += `\n\n${(await d.readStdin()).replace(/\n+$/, "")}`;
529
+ else {
530
+ if (lines.length)
531
+ message += `\n${lines.map((line) => `\n• ${line}`).join("")}`;
532
+ if (nexts.length)
533
+ message += `\n${nexts.map((line) => `\nNext: ${line}`).join("")}`;
534
+ if (asks.length)
535
+ message += `\n${asks.map((line) => `\nNeed from you: ${line}`).join("")}`;
536
+ }
537
+ const sid = sessionId(d);
538
+ ensureDir(d, sid);
539
+ const fetched = await roster(d, sid, ch);
540
+ if (usesCliAuth(d) && fetched.names.length === 0 && fetched.stderr.startsWith("hq-dm-bind: hq-cli >= 5.137.0 is required")) {
541
+ return { code: 2, stdout: "", stderr: fetched.stderr + fail("could not read the channel roster — not posting an update that notifies nobody (check `hq whoami` and that node is on PATH, then retry)").stderr };
542
+ }
543
+ const mention = findMentions(d, sid, ch, tos);
544
+ if (mention.error)
545
+ return { ...mention.error, stderr: fetched.stderr + mention.error.stderr };
546
+ const mentionLine = mention.value ?? "";
547
+ if (!mentionLine)
548
+ return fail("could not read the channel roster — not posting an update that notifies nobody (check `hq whoami` and that node is on PATH, then retry)");
549
+ const shown = mentionLine.replace(/@"([^"]+)"/g, "@$1");
550
+ const names = mention.names;
551
+ const postedMessage = `${shown}\n\n${message}`;
552
+ const cliMessage = `${mentionLine}\n\n${message}`;
553
+ if (!allowLong && Buffer.byteLength(postedMessage) > 600)
554
+ return { code: 2, stdout: "", stderr: `hq-dm-bind: post body too long (${Buffer.byteLength(postedMessage)} chars, limit 600). Put the detail in workspace/reports/<name>.md and reference the path in one line. Use --allow-long to override.\n` };
555
+ const key = (topic || title).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
556
+ let root = "";
557
+ if (under) {
558
+ const resolved = resolveRoot(d, ch, under);
559
+ if (typeof resolved !== "string")
560
+ return resolved;
561
+ root = resolved;
562
+ if (getTopicRoot(d, sid, key) !== root)
563
+ addTopicRoot(d, sid, key, root);
564
+ }
565
+ else if (!newThread)
566
+ root = getTopicRoot(d, sid, key);
567
+ let eventId = "";
568
+ if (usesCliAuth(d)) {
569
+ const versionError = requireCliVersion(d);
570
+ if (versionError)
571
+ return versionError;
572
+ const call = ["dm", ch, ...(root ? ["--thread", root] : []), cliMessage];
573
+ const sent = d.runHq(call, { ...d.env, HQ_ROOT: d.root });
574
+ if (sent.code !== 0)
575
+ return utilityError("hq-dm-bind: hq dm failed — nothing was sent\n", 1);
576
+ const readback = latestCliEventId(d, ch);
577
+ if (typeof readback !== "string")
578
+ return readback;
579
+ eventId = readback;
580
+ if (!eventId)
581
+ return utilityError("hq-dm-bind: post succeeded but its event id could not be read back\n", 1);
582
+ }
583
+ else {
584
+ const mentions = mentionsJson(d, sid, names);
585
+ if (!mentions.length)
586
+ return fail("no mention resolved to a channel member id — not posting an update that notifies nobody");
587
+ const payload = { body: postedMessage, mentions };
588
+ if (root)
589
+ payload.rootEventId = root;
590
+ try {
591
+ eventId = await sendHuman(d, ch, payload);
592
+ }
593
+ catch (error) {
594
+ return utilityError(`hq-dm-bind: ${error.message}\n`, 1);
595
+ }
596
+ }
597
+ if (!root) {
598
+ if (eventId)
599
+ addTopicRoot(d, sid, key, eventId);
600
+ }
601
+ const threadOutput = root ? `thread: reply under topic '${key}'\n` : `thread: new topic '${key}'${eventId ? ` (root ${eventId})` : ""}\n`;
602
+ d.fs.writeFileSync(filePath(d, sid, "dm-bind.cursor"), `${latestSk(d, ch)}\n`);
603
+ return { code: 0, stdout: `${threadOutput}posted to #${ch} (${Buffer.byteLength(postedMessage)} chars)\n`, stderr: fetched.stderr };
604
+ }
605
+ async function listen(d, args) {
606
+ let interval = 60, timeout = 1800;
607
+ for (let i = 0; i < args.length;) {
608
+ const arg = args[i];
609
+ if (arg !== "--interval" && arg !== "--timeout")
610
+ return fail(`listen: unknown arg ${arg}`);
611
+ if (args[i + 1] === undefined)
612
+ return bundledShellUnboundValue(DM_BIND_ASSET, arg === "--interval" ? 560 : 561);
613
+ const value = args[i + 1];
614
+ const n = Number(value);
615
+ if (!Number.isFinite(n))
616
+ return fail(`listen: ${arg.slice(2)} needs a value`);
617
+ if (arg === "--interval")
618
+ interval = n;
619
+ else
620
+ timeout = n;
621
+ i += 2;
622
+ }
623
+ const ch = requireChannel(d);
624
+ if (typeof ch !== "string")
625
+ return ch;
626
+ const deadline = Math.floor(d.now().getTime() / 1000) + timeout;
627
+ let output = `listening on #${ch} every ${interval}s (timeout ${timeout}s)\n`;
628
+ while (Math.floor(d.now().getTime() / 1000) < deadline) {
629
+ const result = await poll(d);
630
+ if (result.code === 0)
631
+ return { code: 0, stdout: `${output}${result.stdout}--- new message(s) on #${ch}; handle them, then re-run listen\n`, stderr: result.stderr };
632
+ if (result.code !== 3)
633
+ return { code: result.code, stdout: `${output}${result.stdout}`, stderr: result.stderr, ...(result.stderrBytes ? { stderrBytes: result.stderrBytes } : {}) };
634
+ await d.sleep(interval * 1000);
635
+ }
636
+ output += `no new messages on #${ch} within ${timeout}s\n`;
637
+ return { code: 3, stdout: output, stderr: "" };
638
+ }
639
+ export async function runDmBind(args, deps) {
640
+ if (!hasHqOnPath(deps))
641
+ return fail("missing dependency: hq");
642
+ const [command = "", ...rest] = args;
643
+ if (command === "bind")
644
+ return bind(deps, rest[0]);
645
+ if (command === "status")
646
+ return status(deps);
647
+ if (command === "unbind")
648
+ return unbind(deps);
649
+ if (command === "roster")
650
+ return cmdRoster(deps);
651
+ if (command === "threads")
652
+ return cmdThreads(deps);
653
+ if (command === "poll")
654
+ return poll(deps);
655
+ if (command === "post")
656
+ return post(deps, rest);
657
+ if (command === "listen")
658
+ return listen(deps, rest);
659
+ return { code: 2, stdout: DM_BIND_HELP, stderr: "" };
660
+ }
661
+ export function registerCoreDmBindNative(core) {
662
+ registerNativeUtility(core, {
663
+ name: "dm-bind", asset: DM_BIND_ASSET, root: "live", summary: "Bind this session to one HQ DM channel",
664
+ resolveRoot: (_cwd, hqRoot) => resolveLiveRoot({ hqRoot }),
665
+ run: (args, context) => runDmBind(args, createDmBindDeps(context.root, context.cwd, context.env, context.now)),
666
+ });
667
+ }
668
+ //# sourceMappingURL=core-dm-bind-native.js.map