@indigoai-us/hq-cli 5.345.114 → 5.345.116
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 +13 -0
- package/assets/scaffold/core/scripts/hq-delegate-send.sh +5 -1
- package/dist/commands/bot-jobs.d.ts +1 -11
- package/dist/commands/bot-jobs.js +13 -34
- package/dist/commands/core-audit-log-native.js +1 -1
- package/dist/commands/core-checkpoint-native.d.ts +27 -0
- package/dist/commands/core-checkpoint-native.js +85 -2
- package/dist/commands/core-dm-bind-native.js +1 -1
- package/dist/commands/core-git-pack-extension-check-native.js +1 -1
- package/dist/commands/core-job-native-utils.d.ts +2 -2
- package/dist/commands/core-job-native-utils.js +3 -11
- package/dist/commands/core-job-notify-native.js +1 -2
- package/dist/commands/core-job-probe-native.d.ts +2 -3
- package/dist/commands/core-job-probe-native.js +14 -22
- package/dist/commands/core-job-remediate-native.js +1 -2
- package/dist/commands/core-job-run-native.js +58 -54
- package/dist/commands/core-jobs-native.d.ts +1 -3
- package/dist/commands/core-jobs-native.js +16 -271
- package/dist/commands/core-jobs-schedule.d.ts +7 -0
- package/dist/commands/core-jobs-schedule.js +243 -0
- package/dist/commands/core-native-utilities.d.ts +2 -3
- package/dist/commands/core-native-utilities.js +2 -19
- package/dist/commands/core-pin-native.js +1 -1
- package/dist/commands/core-refresh-vault-access-native.js +1 -1
- package/dist/commands/core-resolve-company-command.js +2 -17
- package/dist/commands/core-work-mesh-bind-trusted-native.js +1 -1
- package/dist/commands/core.js +1 -17
- package/dist/fast-scaffold.d.ts +1 -1
- package/dist/fast-scaffold.js +0 -38
- package/dist/lib/daemon/catalog.d.ts +0 -2
- package/dist/lib/daemon/catalog.js +3 -6
- package/dist/lib/daemon/run.js +0 -7
- package/dist/lib/scan-packages/index.js +6 -0
- package/dist/lib/workers-registry/index.d.ts +16 -0
- package/dist/lib/workers-registry/index.js +1 -1
- package/dist/utils/resident-package-preload.js +2 -7
- package/package.json +1 -1
- package/assets/scaffold/core/scripts/audit-log.sh +0 -384
- package/assets/scaffold/core/scripts/git-pack-extension-check.sh +0 -164
- package/assets/scaffold/core/scripts/hq-dm-bind.sh +0 -590
- package/assets/scaffold/core/scripts/hq-job-notify.sh +0 -389
- package/assets/scaffold/core/scripts/hq-job-probe.sh +0 -865
- package/assets/scaffold/core/scripts/hq-job-remediate.sh +0 -359
- package/assets/scaffold/core/scripts/hq-job-run.sh +0 -1025
- package/assets/scaffold/core/scripts/hq-pin.sh +0 -118
- package/assets/scaffold/core/scripts/jobs-schedule-parse.sh +0 -310
- package/assets/scaffold/core/scripts/jobs-validate.sh +0 -482
- package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +0 -836
- package/assets/scaffold/core/scripts/refresh-vault-access.sh +0 -188
- package/assets/scaffold/core/scripts/resolve-company.sh +0 -331
- package/assets/scaffold/core/scripts/work-mesh-live-bind-trusted.sh +0 -184
- package/dist/commands/core-job-post-exit-assets.d.ts +0 -8
- package/dist/commands/core-job-post-exit-assets.js +0 -37
- package/dist/commands/core-native-jobs-flag.d.ts +0 -4
- package/dist/commands/core-native-jobs-flag.js +0 -7
- package/dist/commands/core-native-utilities-flag.d.ts +0 -4
- package/dist/commands/core-native-utilities-flag.js +0 -7
|
@@ -1,590 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# hq-dm-bind.sh — bind the current session to one HQ DM channel, post
|
|
3
|
-
# structured updates to it, and listen for replies (feedback / requests).
|
|
4
|
-
#
|
|
5
|
-
# Usage:
|
|
6
|
-
# core/scripts/hq-dm-bind.sh bind <channel> # bind + set the read cursor to "now"
|
|
7
|
-
# core/scripts/hq-dm-bind.sh status # show the binding and cursor
|
|
8
|
-
# 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]
|
|
9
|
-
# core/scripts/hq-dm-bind.sh post --title <t> - < body.md # body from stdin (already structured)
|
|
10
|
-
# core/scripts/hq-dm-bind.sh threads # list the channel's recent top-level threads (id, replies, author, text)
|
|
11
|
-
# core/scripts/hq-dm-bind.sh roster # print the channel members a post will @-mention
|
|
12
|
-
# core/scripts/hq-dm-bind.sh poll # print new messages since the cursor, advance it
|
|
13
|
-
# core/scripts/hq-dm-bind.sh listen [--interval 60] [--timeout 1800]
|
|
14
|
-
# # block until a new message arrives (or timeout)
|
|
15
|
-
# core/scripts/hq-dm-bind.sh unbind
|
|
16
|
-
#
|
|
17
|
-
# The binding lives in the session's meta.yaml (`dm_channel`) via hq-session.sh;
|
|
18
|
-
# the cursor is `workspace/sessions/<sid>/dm-bind.cursor` (the last seen sort
|
|
19
|
-
# key from `hq dm channel --json`). `listen` is meant to run detached
|
|
20
|
-
# (Bash run_in_background): it exits 0 and prints the new messages the moment
|
|
21
|
-
# one lands, exits 3 on timeout with nothing new.
|
|
22
|
-
#
|
|
23
|
-
# Every post opens with an @-mention of every other member of the channel, so
|
|
24
|
-
# the update reaches people (HQ mentions are structured: the `hq` CLI resolves
|
|
25
|
-
# the `@"Display Name"` tokens against the roster and sends the real mention
|
|
26
|
-
# list; an unresolvable name fails the post rather than posting text that looks
|
|
27
|
-
# addressed but is not). The CLI rewrites a resolved `@"Display Name"` token to
|
|
28
|
-
# `@Display Name`, so the room never shows the quotes.
|
|
29
|
-
#
|
|
30
|
-
# Posts thread by topic. The first post on a topic (`--topic`, else the title)
|
|
31
|
-
# is a top-level root; every later post on the same topic in this session goes
|
|
32
|
-
# out as a reply under it. `--under <id>` replies under an existing thread that
|
|
33
|
-
# someone else (or an earlier session) started — a top-level post is only for a
|
|
34
|
-
# subject the channel has no thread for yet. `--new-thread` starts a fresh root. The topic → root
|
|
35
|
-
# map is `workspace/sessions/<sid>/dm-bind.threads`.
|
|
36
|
-
#
|
|
37
|
-
# A post ALWAYS mentions someone. `--to <name>` (repeatable) narrows the line to
|
|
38
|
-
# the named members, for a reply to one person; without it the line names every
|
|
39
|
-
# other human member (bots are left out unless named with --to, because a tagged
|
|
40
|
-
# bot wakes and replies). There is no way to post unmentioned: an update nobody is
|
|
41
|
-
# notified about does not get read. `post` refuses when the roster cannot be
|
|
42
|
-
# read rather than sending a silent update.
|
|
43
|
-
#
|
|
44
|
-
# Post format (kept deliberately plain — see .claude/skills/dm-bind/SKILL.md):
|
|
45
|
-
# @"Ada Lovelace" @"Grace Hopper"
|
|
46
|
-
#
|
|
47
|
-
# <title> — <state>
|
|
48
|
-
#
|
|
49
|
-
# • line
|
|
50
|
-
# • line
|
|
51
|
-
#
|
|
52
|
-
# Next: line
|
|
53
|
-
# Need from you: line
|
|
54
|
-
set -euo pipefail
|
|
55
|
-
|
|
56
|
-
if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
|
|
57
|
-
sed -n '2,/^set -euo pipefail$/ { /^set -euo pipefail$/!p; }' "$0"
|
|
58
|
-
exit 0
|
|
59
|
-
fi
|
|
60
|
-
|
|
61
|
-
HQ_ROOT="${HQ_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
|
62
|
-
SESSION_SH="$HQ_ROOT/core/scripts/hq-session.sh"
|
|
63
|
-
|
|
64
|
-
die() { echo "hq-dm-bind: $*" >&2; exit 2; }
|
|
65
|
-
need() { command -v "$1" >/dev/null 2>&1 || die "missing dependency: $1"; }
|
|
66
|
-
need hq; need jq
|
|
67
|
-
|
|
68
|
-
session_id() { bash "$SESSION_SH" current 2>/dev/null || true; }
|
|
69
|
-
session_dir() {
|
|
70
|
-
local sid; sid="$(session_id)"
|
|
71
|
-
[ -n "$sid" ] || die "no current session (hq-session.sh current is empty)"
|
|
72
|
-
local d="$HQ_ROOT/workspace/sessions/$sid"; mkdir -p "$d"; echo "$d"
|
|
73
|
-
}
|
|
74
|
-
cursor_file() { echo "$(session_dir)/dm-bind.cursor"; }
|
|
75
|
-
channel() { bash "$SESSION_SH" get dm_channel 2>/dev/null | tr -d '"' || true; }
|
|
76
|
-
require_channel() {
|
|
77
|
-
local ch; ch="$(channel)"
|
|
78
|
-
[ -n "$ch" ] && [ "$ch" != "null" ] || die "session is not bound — run: core/scripts/hq-dm-bind.sh bind <channel>"
|
|
79
|
-
echo "$ch"
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
# Own address, used to skip our own posts when polling. Best effort.
|
|
83
|
-
self_email() {
|
|
84
|
-
hq whoami 2>/dev/null | grep -oE '[[:alnum:]._+-]+@[[:alnum:].-]+\.[[:alpha:]]+' | head -1 || true
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
CLI_MIN_VERSION="5.137.0"
|
|
88
|
-
|
|
89
|
-
# Human sessions have both legacy local Cognito files. Machine-principal
|
|
90
|
-
# sessions use the hq CLI's machine token and do not have those files.
|
|
91
|
-
has_cognito_session() {
|
|
92
|
-
[ -r "$HOME/.hq/config.json" ] && [ -r "$HOME/.hq/cognito-tokens.json" ]
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
uses_cli_auth() { ! has_cognito_session; }
|
|
96
|
-
|
|
97
|
-
cli_version() {
|
|
98
|
-
local output version
|
|
99
|
-
if ! output="$(hq --version 2>/dev/null)"; then
|
|
100
|
-
echo "hq-dm-bind: could not read the installed hq-cli version" >&2
|
|
101
|
-
return 1
|
|
102
|
-
fi
|
|
103
|
-
version="$(printf '%s\n' "$output" | sed -nE 's/^CLI[[:space:]]+([0-9]+(\.[0-9]+){2}).*$/\1/p' | head -1)"
|
|
104
|
-
if [ -z "$version" ]; then
|
|
105
|
-
version="$(printf '%s\n' "$output" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)"
|
|
106
|
-
fi
|
|
107
|
-
printf '%s' "$version"
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
version_at_least() {
|
|
111
|
-
local actual="$1" required="$2"
|
|
112
|
-
local a=0 b=0 c=0 ra=0 rb=0 rc=0
|
|
113
|
-
IFS=. read -r a b c <<< "$actual"
|
|
114
|
-
IFS=. read -r ra rb rc <<< "$required"
|
|
115
|
-
[ "${a:-0}" -gt "${ra:-0}" ] || {
|
|
116
|
-
[ "${a:-0}" -eq "${ra:-0}" ] && {
|
|
117
|
-
[ "${b:-0}" -gt "${rb:-0}" ] || {
|
|
118
|
-
[ "${b:-0}" -eq "${rb:-0}" ] && [ "${c:-0}" -ge "${rc:-0}" ]
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
require_cli_version() {
|
|
125
|
-
local actual
|
|
126
|
-
actual="$(cli_version)" || return 2
|
|
127
|
-
if [ -z "$actual" ] || ! version_at_least "$actual" "$CLI_MIN_VERSION"; then
|
|
128
|
-
echo "hq-dm-bind: hq-cli >= $CLI_MIN_VERSION is required for machine-principal DM binding (found ${actual:-unknown}); update hq-cli and retry." >&2
|
|
129
|
-
return 2
|
|
130
|
-
fi
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
cli_identity() {
|
|
134
|
-
local identity
|
|
135
|
-
if ! identity="$(hq whoami --json 2>/dev/null)"; then
|
|
136
|
-
echo "hq-dm-bind: could not read the hq-cli identity" >&2
|
|
137
|
-
return 1
|
|
138
|
-
fi
|
|
139
|
-
if ! printf '%s' "$identity" | jq -e 'type == "object"' >/dev/null 2>&1; then
|
|
140
|
-
echo "hq-dm-bind: hq-cli returned an invalid identity" >&2
|
|
141
|
-
return 1
|
|
142
|
-
fi
|
|
143
|
-
printf '%s' "$identity"
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
# Normalise `hq dm channel --json` to a JSON array of messages.
|
|
147
|
-
fetch_items() {
|
|
148
|
-
local ch="$1" limit="${2:-30}"
|
|
149
|
-
hq dm channel "$ch" --limit "$limit" --json 2>/dev/null \
|
|
150
|
-
| jq -c 'if type=="array" then . else (.messages // .items // []) end' 2>/dev/null \
|
|
151
|
-
|| echo '[]'
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
latest_sk() { fetch_items "$1" 5 | jq -r 'last // empty | .sk // empty'; }
|
|
155
|
-
roster_file() { echo "$(session_dir)/dm-bind.roster"; }
|
|
156
|
-
|
|
157
|
-
# The channel's members other than us, one display name<TAB>uid per line.
|
|
158
|
-
# Machine-principal sessions use the CLI, while human sessions retain the
|
|
159
|
-
# local-file path for compatibility. Fails soft after logging a CLI read error.
|
|
160
|
-
fetch_roster() {
|
|
161
|
-
local ch="$1" me; me="$(self_email)"
|
|
162
|
-
if uses_cli_auth; then
|
|
163
|
-
require_cli_version || return $?
|
|
164
|
-
if ! hq channels members "$ch"; then
|
|
165
|
-
echo "hq-dm-bind: could not read the channel roster through hq-cli" >&2
|
|
166
|
-
fi
|
|
167
|
-
return 0
|
|
168
|
-
fi
|
|
169
|
-
command -v node >/dev/null 2>&1 || return 0
|
|
170
|
-
node - roster "$ch" "$me" <<'NODE' 2>/dev/null || true
|
|
171
|
-
const fs = require("fs"), os = require("os"), path = require("path");
|
|
172
|
-
const [, ch, me] = process.argv.slice(2);
|
|
173
|
-
const home = os.homedir();
|
|
174
|
-
const slug = (n) => String(n).trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
175
|
-
(async () => {
|
|
176
|
-
const cfg = JSON.parse(fs.readFileSync(path.join(home, ".hq", "config.json"), "utf8"));
|
|
177
|
-
const tok = JSON.parse(fs.readFileSync(path.join(home, ".hq", "cognito-tokens.json"), "utf8")).idToken;
|
|
178
|
-
const base = cfg.vaultApiUrl; const h = { Authorization: "Bearer " + tok };
|
|
179
|
-
const chans = await (await fetch(base + "/v1/notify/channels", { headers: h })).json();
|
|
180
|
-
const rows = Array.isArray(chans) ? chans : (chans.channels || []);
|
|
181
|
-
const hit = rows.find((c) => c.channelId === ch || slug(c.name || "") === slug(ch) || c.slug === slug(ch));
|
|
182
|
-
if (!hit) return;
|
|
183
|
-
const mem = await (await fetch(base + "/v1/notify/channels/" + encodeURIComponent(hit.channelId) + "/members", { headers: h })).json();
|
|
184
|
-
const list = Array.isArray(mem) ? mem : (mem.members || []);
|
|
185
|
-
for (const m of list) {
|
|
186
|
-
const email = String(m.email || "").toLowerCase();
|
|
187
|
-
if (me && email === me.toLowerCase()) continue;
|
|
188
|
-
const name = String(m.displayName || "").trim();
|
|
189
|
-
const uid = String(m.personUid || m.participantUid || m.uid || "").trim();
|
|
190
|
-
if (name) process.stdout.write(name + "\t" + uid + "\n");
|
|
191
|
-
}
|
|
192
|
-
})().catch(() => {});
|
|
193
|
-
NODE
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
# Refresh the cached roster; fall back to the cache when the fetch fails.
|
|
197
|
-
roster() {
|
|
198
|
-
local ch="$1" rf; rf="$(roster_file)"
|
|
199
|
-
if uses_cli_auth; then require_cli_version || return $?; fi
|
|
200
|
-
local fresh; fresh="$(fetch_roster "$ch")"
|
|
201
|
-
if [ -n "$fresh" ]; then printf '%s\n' "$fresh" > "$rf"; fi
|
|
202
|
-
cut -f1 "$rf" 2>/dev/null || true
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
# The structured `mentions` array for a newline list of display names, built
|
|
206
|
-
# from the cached roster rows (name<TAB>uid). `agt_*` uids are agents, every
|
|
207
|
-
# other uid is a human — the server 400s when the type and the prefix disagree.
|
|
208
|
-
mentions_json() {
|
|
209
|
-
local rf; rf="$(roster_file)"
|
|
210
|
-
jq -Rn --rawfile rows "$rf" '
|
|
211
|
-
($rows | split("\n") | map(select(length>0) | split("\t")) | map({key:.[0], value:(.[1] // "")}) | from_entries) as $uid
|
|
212
|
-
| [inputs | select(length>0) | select(($uid[.] // "") != "")
|
|
213
|
-
| {participantUid:$uid[.], participantType:(if ($uid[.]|startswith("agt_")) then "agent" else "human" end), displayName:.}]'
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
threads_file() { echo "$(session_dir)/dm-bind.threads"; }
|
|
217
|
-
topic_slug() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+|-+$//g'; }
|
|
218
|
-
thread_root() { awk -F'\t' -v k="$1" '$1==k{v=$2} END{if(v!="")print v}' "$(threads_file)" 2>/dev/null || true; }
|
|
219
|
-
|
|
220
|
-
# POST one message to the channel through the legacy notify API. Reads the JSON
|
|
221
|
-
# payload from $1 and prints the new message's eventId. Machine-principal
|
|
222
|
-
# sessions use send_cli_channel below.
|
|
223
|
-
send_channel() {
|
|
224
|
-
local ch="$1" payload="$2"
|
|
225
|
-
node - send "$ch" "$payload" <<'NODE'
|
|
226
|
-
const fs = require("fs"), os = require("os"), path = require("path");
|
|
227
|
-
const [, ch, payload] = process.argv.slice(2);
|
|
228
|
-
const home = os.homedir();
|
|
229
|
-
const slug = (n) => String(n).trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
230
|
-
(async () => {
|
|
231
|
-
const cfg = JSON.parse(fs.readFileSync(path.join(home, ".hq", "config.json"), "utf8"));
|
|
232
|
-
const tok = JSON.parse(fs.readFileSync(path.join(home, ".hq", "cognito-tokens.json"), "utf8")).idToken;
|
|
233
|
-
const base = cfg.vaultApiUrl; const h = { Authorization: "Bearer " + tok, "Content-Type": "application/json" };
|
|
234
|
-
const chans = await (await fetch(base + "/v1/notify/channels", { headers: h })).json();
|
|
235
|
-
const rows = Array.isArray(chans) ? chans : (chans.channels || []);
|
|
236
|
-
const hit = rows.find((c) => c.channelId === ch || slug(c.name || "") === slug(ch) || c.slug === slug(ch));
|
|
237
|
-
if (!hit) throw new Error("channel not found: " + ch);
|
|
238
|
-
const res = await fetch(base + "/v1/notify/channels/" + encodeURIComponent(hit.channelId) + "/messages", { method: "POST", headers: h, body: payload });
|
|
239
|
-
const text = await res.text();
|
|
240
|
-
if (!res.ok) throw new Error("post failed: HTTP " + res.status + " " + text.slice(0, 300));
|
|
241
|
-
let j = {}; try { j = JSON.parse(text); } catch {}
|
|
242
|
-
const m = j.message || j.event || j;
|
|
243
|
-
const id = m.eventId || (typeof m.sk === "string" && m.sk.includes("#") ? m.sk.split("#").pop() : "");
|
|
244
|
-
process.stdout.write(String(id || ""));
|
|
245
|
-
})().catch((e) => { process.stderr.write("hq-dm-bind: " + e.message + "\n"); process.exit(1); });
|
|
246
|
-
NODE
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
# The CLI prints a human confirmation rather than the created event id. Read
|
|
250
|
-
# the channel back and use the newest message from this CLI identity so topic
|
|
251
|
-
# replies can keep using the existing thread map.
|
|
252
|
-
latest_cli_event_id() {
|
|
253
|
-
local ch="$1" identity agent username person email
|
|
254
|
-
identity="$(cli_identity)" || return 1
|
|
255
|
-
agent="$(printf '%s' "$identity" | jq -r '.agentUid // empty')"
|
|
256
|
-
username="$(printf '%s' "$identity" | jq -r '.username // empty')"
|
|
257
|
-
person="$(printf '%s' "$identity" | jq -r '.personUid // empty')"
|
|
258
|
-
email="$(printf '%s' "$identity" | jq -r '.email // empty')"
|
|
259
|
-
fetch_items "$ch" 50 | jq -r \
|
|
260
|
-
--arg agent "$agent" --arg username "$username" --arg person "$person" --arg email "$email" '
|
|
261
|
-
[ .[]
|
|
262
|
-
| select(
|
|
263
|
-
if ($agent != "" or $username != "") then
|
|
264
|
-
(((.senderMachineUid // .fromMachineUid // "") == $agent)
|
|
265
|
-
or ((.senderMachineUid // .fromMachineUid // "") == $username))
|
|
266
|
-
elif $person != "" then
|
|
267
|
-
((.fromPersonUid // "") == $person)
|
|
268
|
-
elif $email != "" then
|
|
269
|
-
((.fromEmail // "" | ascii_downcase) == ($email | ascii_downcase))
|
|
270
|
-
else false
|
|
271
|
-
end
|
|
272
|
-
)
|
|
273
|
-
| (.eventId // .messageId // "")
|
|
274
|
-
] | last // empty'
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
send_cli_channel() {
|
|
278
|
-
local ch="$1" body="$2" root="$3"
|
|
279
|
-
require_cli_version || return $?
|
|
280
|
-
local -a args=(dm "$ch")
|
|
281
|
-
if [ -n "$root" ]; then args+=(--thread "$root"); fi
|
|
282
|
-
args+=("$body")
|
|
283
|
-
if ! hq "${args[@]}" >/dev/null; then
|
|
284
|
-
echo "hq-dm-bind: hq dm failed — nothing was sent" >&2
|
|
285
|
-
return 1
|
|
286
|
-
fi
|
|
287
|
-
local event_id
|
|
288
|
-
event_id="$(latest_cli_event_id "$ch")" || return 1
|
|
289
|
-
if [ -z "$event_id" ]; then
|
|
290
|
-
echo "hq-dm-bind: post succeeded but its event id could not be read back" >&2
|
|
291
|
-
return 1
|
|
292
|
-
fi
|
|
293
|
-
printf '%s' "$event_id"
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
# The @-mention line: one quoted token per member so multi-word names resolve.
|
|
297
|
-
# The quotes are input syntax only — the `hq` CLI strips them once the name
|
|
298
|
-
# resolves, so the room reads `@Ada Lovelace`. With no extra args it names every
|
|
299
|
-
# other member; extra args narrow it to those members, matched case-insensitively
|
|
300
|
-
# against the roster (exact display name first, then a unique partial match).
|
|
301
|
-
# Exit 4: roster unreadable. Exit 5: a --to name matched nobody or several.
|
|
302
|
-
mention_line() {
|
|
303
|
-
local ch="$1"; shift
|
|
304
|
-
local names line="" n
|
|
305
|
-
names="$(roster "$ch")"
|
|
306
|
-
[ -n "$names" ] || return 4
|
|
307
|
-
if [ $# -eq 0 ]; then
|
|
308
|
-
# Default: the people, not the bots. A tagged bot wakes and answers, and a
|
|
309
|
-
# round of "no response needed" replies under every update is the clutter
|
|
310
|
-
# threads exist to prevent. Name a bot with --to when the post is for it.
|
|
311
|
-
local humans; humans="$(awk -F'\t' '$2 !~ /^agt_/ {print $1}' "$(roster_file)" 2>/dev/null || true)"
|
|
312
|
-
[ -n "$humans" ] || humans="$names"
|
|
313
|
-
while IFS= read -r n; do [ -n "$n" ] && line="$line @\"$n\""; done <<< "$humans"
|
|
314
|
-
else
|
|
315
|
-
local want hit count
|
|
316
|
-
for want in "$@"; do
|
|
317
|
-
want="${want#@}"
|
|
318
|
-
hit="$(printf '%s\n' "$names" | grep -ixF -- "$want" | head -1 || true)"
|
|
319
|
-
if [ -z "$hit" ]; then
|
|
320
|
-
count="$(printf '%s\n' "$names" | grep -icF -- "$want" || true)"
|
|
321
|
-
if [ "$count" = 1 ]; then hit="$(printf '%s\n' "$names" | grep -iF -- "$want")"; fi
|
|
322
|
-
fi
|
|
323
|
-
if [ -z "$hit" ]; then
|
|
324
|
-
echo "hq-dm-bind: --to '$want' does not match exactly one channel member. Members:" >&2
|
|
325
|
-
printf '%s\n' "$names" | sed 's/^/ /' >&2
|
|
326
|
-
return 5
|
|
327
|
-
fi
|
|
328
|
-
case "$line" in *"@\"$hit\""*) ;; *) line="$line @\"$hit\"" ;; esac
|
|
329
|
-
done
|
|
330
|
-
fi
|
|
331
|
-
printf '%s' "${line# }"
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
cmd_bind() {
|
|
335
|
-
local ch="${1:-}"; [ -n "$ch" ] || die "bind needs a channel name (see: hq channels)"
|
|
336
|
-
ch="${ch#\#}"
|
|
337
|
-
hq channels 2>/dev/null | grep -- "hq dm $ch " >/dev/null || die "no channel named '$ch' (see: hq channels)"
|
|
338
|
-
bash "$SESSION_SH" set dm_channel "$ch" >/dev/null
|
|
339
|
-
local sk; sk="$(latest_sk "$ch")"
|
|
340
|
-
printf '%s\n' "$sk" > "$(cursor_file)"
|
|
341
|
-
local names; names="$(roster "$ch")"
|
|
342
|
-
local n; n="$(printf '%s' "$names" | grep -c . || true)"
|
|
343
|
-
echo "bound session $(session_id) to #$ch (cursor: ${sk:-<empty>}; will @-mention $n member(s))"
|
|
344
|
-
[ "$n" -gt 0 ] || echo "hq-dm-bind: could not read the channel roster — posts will not @-mention anyone until it is readable" >&2
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
cmd_roster() {
|
|
348
|
-
local ch; ch="$(require_channel)"
|
|
349
|
-
roster "$ch"
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
# Recent top-level messages (thread roots) in the bound channel, newest last:
|
|
353
|
-
# "<id8> <replies> <author>: <first 90 chars>". Read this BEFORE starting a
|
|
354
|
-
# new topic — if a thread for the subject already exists, reply under it.
|
|
355
|
-
recent_roots() { fetch_items "$1" 100 | jq -c 'map(select((.rootEventId // "") == ""))'; }
|
|
356
|
-
cmd_threads() {
|
|
357
|
-
local ch; ch="$(require_channel)"
|
|
358
|
-
recent_roots "$ch" | jq -r '.[-25:] | .[] | [(.eventId[0:8]), ((.replyCount // 0)|tostring), ((.fromDisplayName // "?") + ": " + ((.body // "") | gsub("\\s+";" ") | .[0:90]))] | @tsv'
|
|
359
|
-
}
|
|
360
|
-
# Resolve --under: a full event id, or a unique id prefix of a recent root.
|
|
361
|
-
resolve_root() {
|
|
362
|
-
local ch="$1" want="$2" hits
|
|
363
|
-
hits="$(recent_roots "$ch" | jq -r --arg w "$want" '.[] | select(.eventId | startswith($w)) | .eventId')"
|
|
364
|
-
[ -n "$hits" ] || { echo "hq-dm-bind: --under '$want' matches no recent top-level message (see: hq-dm-bind.sh threads)" >&2; return 1; }
|
|
365
|
-
[ "$(printf '%s\n' "$hits" | wc -l | tr -d ' ')" = 1 ] || { echo "hq-dm-bind: --under '$want' matches several messages — use more of the id" >&2; return 1; }
|
|
366
|
-
printf '%s' "$hits"
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
cmd_unbind() {
|
|
370
|
-
bash "$SESSION_SH" set dm_channel "" >/dev/null
|
|
371
|
-
rm -f "$(cursor_file)" "$(roster_file)" "$(threads_file)"
|
|
372
|
-
echo "unbound"
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
cmd_status() {
|
|
376
|
-
local ch; ch="$(channel)"
|
|
377
|
-
echo "session: $(session_id)"
|
|
378
|
-
echo "channel: ${ch:-<none>}"
|
|
379
|
-
echo "cursor: $(cat "$(cursor_file)" 2>/dev/null || echo '<none>')"
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
cmd_post() {
|
|
383
|
-
local ch; ch="$(require_channel)"
|
|
384
|
-
local title="" state="" body_from_stdin=0 topic="" new_thread=0 under="" allow_long=0
|
|
385
|
-
local -a lines=() nexts=() asks=() tos=()
|
|
386
|
-
while [ $# -gt 0 ]; do
|
|
387
|
-
case "$1" in
|
|
388
|
-
--topic) topic="$2"; shift 2 ;;
|
|
389
|
-
--new-thread) new_thread=1; shift ;;
|
|
390
|
-
--under) [ -n "${2:-}" ] || die "post: --under needs a message id (see: threads)"; under="$2"; shift 2 ;;
|
|
391
|
-
--to) [ -n "${2:-}" ] || die "post: --to needs a member name"; tos+=("$2"); shift 2 ;;
|
|
392
|
-
--no-mention) die "post: --no-mention was removed — every post @-mentions someone. Use --to <name> to address specific members." ;;
|
|
393
|
-
--title) title="$2"; shift 2 ;;
|
|
394
|
-
--state) state="$2"; shift 2 ;;
|
|
395
|
-
--line) lines+=("$2"); shift 2 ;;
|
|
396
|
-
--next) nexts+=("$2"); shift 2 ;;
|
|
397
|
-
--ask) asks+=("$2"); shift 2 ;;
|
|
398
|
-
--allow-long) allow_long=1; shift ;;
|
|
399
|
-
-) body_from_stdin=1; shift ;;
|
|
400
|
-
*) die "post: unknown arg $1" ;;
|
|
401
|
-
esac
|
|
402
|
-
done
|
|
403
|
-
[ -n "$title" ] || die "post needs --title"
|
|
404
|
-
|
|
405
|
-
# Length guard — refuse before any network call so nothing partial is sent.
|
|
406
|
-
# Override with --allow-long only when the post genuinely must be longer.
|
|
407
|
-
if [ "$allow_long" != 1 ] && [ "$body_from_stdin" != 1 ]; then
|
|
408
|
-
local _guard_fail=0 _len _preview
|
|
409
|
-
if [ "${#lines[@]}" -gt 6 ]; then
|
|
410
|
-
echo "hq-dm-bind: too many --line bullets (${#lines[@]}, limit 6). Put extra detail in workspace/reports/<name>.md and link it in one bullet. Use --allow-long to override." >&2
|
|
411
|
-
_guard_fail=1
|
|
412
|
-
fi
|
|
413
|
-
if [ "${#nexts[@]}" -gt 2 ]; then
|
|
414
|
-
echo "hq-dm-bind: too many --next items (${#nexts[@]}, limit 2). Use --allow-long to override." >&2
|
|
415
|
-
_guard_fail=1
|
|
416
|
-
fi
|
|
417
|
-
if [ "${#asks[@]}" -gt 1 ]; then
|
|
418
|
-
echo "hq-dm-bind: too many --ask items (${#asks[@]}, limit 1). Use --allow-long to override." >&2
|
|
419
|
-
_guard_fail=1
|
|
420
|
-
fi
|
|
421
|
-
for _l in "${lines[@]}"; do
|
|
422
|
-
_len="$(printf '%s' "$_l" | wc -c | tr -d ' ')"
|
|
423
|
-
if [ "$_len" -gt 140 ]; then
|
|
424
|
-
_preview="$(printf '%s' "$_l" | cut -c1-60)"
|
|
425
|
-
echo "hq-dm-bind: --line too long ($_len chars, limit 140): \"$_preview\". Put the detail in workspace/reports/<name>.md and reference the path in one line. Use --allow-long to override." >&2
|
|
426
|
-
_guard_fail=1
|
|
427
|
-
fi
|
|
428
|
-
done
|
|
429
|
-
for _l in "${nexts[@]}"; do
|
|
430
|
-
_len="$(printf '%s' "$_l" | wc -c | tr -d ' ')"
|
|
431
|
-
if [ "$_len" -gt 140 ]; then
|
|
432
|
-
_preview="$(printf '%s' "$_l" | cut -c1-60)"
|
|
433
|
-
echo "hq-dm-bind: --next too long ($_len chars, limit 140): \"$_preview\". Put the detail in workspace/reports/<name>.md and reference the path in one line. Use --allow-long to override." >&2
|
|
434
|
-
_guard_fail=1
|
|
435
|
-
fi
|
|
436
|
-
done
|
|
437
|
-
for _l in "${asks[@]}"; do
|
|
438
|
-
_len="$(printf '%s' "$_l" | wc -c | tr -d ' ')"
|
|
439
|
-
if [ "$_len" -gt 140 ]; then
|
|
440
|
-
_preview="$(printf '%s' "$_l" | cut -c1-60)"
|
|
441
|
-
echo "hq-dm-bind: --ask too long ($_len chars, limit 140): \"$_preview\". Put the detail in workspace/reports/<name>.md and reference the path in one line. Use --allow-long to override." >&2
|
|
442
|
-
_guard_fail=1
|
|
443
|
-
fi
|
|
444
|
-
done
|
|
445
|
-
[ "$_guard_fail" = 0 ] || exit 2
|
|
446
|
-
fi
|
|
447
|
-
local msg="$title"; [ -n "$state" ] && msg="$title — $state"
|
|
448
|
-
if [ "$body_from_stdin" = 1 ]; then
|
|
449
|
-
msg="$msg"$'\n\n'"$(cat)"
|
|
450
|
-
else
|
|
451
|
-
if [ ${#lines[@]} -gt 0 ]; then
|
|
452
|
-
msg="$msg"$'\n'
|
|
453
|
-
for l in "${lines[@]}"; do msg="$msg"$'\n'"• $l"; done
|
|
454
|
-
fi
|
|
455
|
-
if [ ${#nexts[@]} -gt 0 ]; then
|
|
456
|
-
msg="$msg"$'\n'
|
|
457
|
-
for l in "${nexts[@]}"; do msg="$msg"$'\n'"Next: $l"; done
|
|
458
|
-
fi
|
|
459
|
-
if [ ${#asks[@]} -gt 0 ]; then
|
|
460
|
-
msg="$msg"$'\n'
|
|
461
|
-
for l in "${asks[@]}"; do msg="$msg"$'\n'"Need from you: $l"; done
|
|
462
|
-
fi
|
|
463
|
-
fi
|
|
464
|
-
local ml rc=0
|
|
465
|
-
if [ ${#tos[@]} -gt 0 ]; then ml="$(mention_line "$ch" "${tos[@]}")" || rc=$?
|
|
466
|
-
else ml="$(mention_line "$ch")" || rc=$?; fi
|
|
467
|
-
[ "$rc" != 5 ] || exit 2
|
|
468
|
-
[ "$rc" = 0 ] && [ -n "$ml" ] || die "could not read the channel roster — not posting an update that notifies nobody (check \`hq whoami\` and that node is on PATH, then retry)"
|
|
469
|
-
# The room shows `@Ada Lovelace`; the quotes only ever existed as CLI syntax
|
|
470
|
-
# for the hq CLI path. Keep the quoted form for CLI mention resolution.
|
|
471
|
-
local shown names_nl posted_msg cli_msg
|
|
472
|
-
shown="$(printf '%s' "$ml" | sed -E 's/@"([^"]+)"/@\1/g')"
|
|
473
|
-
names_nl="$(printf '%s' "$ml" | grep -oE '@"[^"]+"' | sed -E 's/^@"//; s/"$//')"
|
|
474
|
-
posted_msg="$shown"$'\n\n'"$msg"
|
|
475
|
-
cli_msg="$ml"$'\n\n'"$msg"
|
|
476
|
-
msg="$posted_msg"
|
|
477
|
-
|
|
478
|
-
# Total body length check (counts what the receipt prints — mention line included).
|
|
479
|
-
if [ "$allow_long" != 1 ]; then
|
|
480
|
-
local _total; _total="$(printf '%s' "$msg" | wc -c | tr -d ' ')"
|
|
481
|
-
if [ "$_total" -gt 600 ]; then
|
|
482
|
-
echo "hq-dm-bind: post body too long ($_total chars, limit 600). Put the detail in workspace/reports/<name>.md and reference the path in one line. Use --allow-long to override." >&2
|
|
483
|
-
exit 2
|
|
484
|
-
fi
|
|
485
|
-
fi
|
|
486
|
-
|
|
487
|
-
# One thread per topic: the first post on a topic is the root, every later
|
|
488
|
-
# post on it is a reply under that root, so the room stays scannable.
|
|
489
|
-
local key root=""
|
|
490
|
-
key="$(topic_slug "${topic:-$title}")"
|
|
491
|
-
if [ -n "$under" ]; then
|
|
492
|
-
# Reply under someone else's (or an earlier) thread, and keep this topic there.
|
|
493
|
-
root="$(resolve_root "$ch" "$under")" || exit 2
|
|
494
|
-
[ "$(thread_root "$key")" = "$root" ] || printf '%s\t%s\n' "$key" "$root" >> "$(threads_file)"
|
|
495
|
-
elif [ "$new_thread" != 1 ]; then
|
|
496
|
-
root="$(thread_root "$key")"
|
|
497
|
-
fi
|
|
498
|
-
local eid
|
|
499
|
-
if uses_cli_auth; then
|
|
500
|
-
eid="$(send_cli_channel "$ch" "$cli_msg" "$root")" || die "post failed — nothing was sent"
|
|
501
|
-
else
|
|
502
|
-
local payload
|
|
503
|
-
payload="$(printf '%s\n' "$names_nl" | mentions_json | jq -c --arg body "$msg" --arg root "$root" \
|
|
504
|
-
'{body:$body, mentions:.} + (if $root != "" then {rootEventId:$root} else {} end)')"
|
|
505
|
-
[ "$(printf '%s' "$payload" | jq '.mentions | length')" -gt 0 ] || die "no mention resolved to a channel member id — not posting an update that notifies nobody"
|
|
506
|
-
eid="$(send_channel "$ch" "$payload")" || die "post failed — nothing was sent"
|
|
507
|
-
fi
|
|
508
|
-
if [ -z "$root" ]; then
|
|
509
|
-
[ -n "$eid" ] && printf '%s\t%s\n' "$key" "$eid" >> "$(threads_file)"
|
|
510
|
-
echo "thread: new topic '$key'${eid:+ (root $eid)}"
|
|
511
|
-
else
|
|
512
|
-
echo "thread: reply under topic '$key'"
|
|
513
|
-
fi
|
|
514
|
-
# Our own post must not come back as "new" on the next poll.
|
|
515
|
-
latest_sk "$ch" > "$(cursor_file)"
|
|
516
|
-
echo "posted to #$ch ($(printf '%s' "$msg" | wc -c | tr -d ' ') chars)"
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
# Prints new messages (not ours) since the cursor as "<time> <name>: <body>",
|
|
520
|
-
# advances the cursor. Exit 0 if any printed, 3 if none.
|
|
521
|
-
cmd_poll() {
|
|
522
|
-
local ch; ch="$(require_channel)"
|
|
523
|
-
local cf; cf="$(cursor_file)"
|
|
524
|
-
local cursor; cursor="$(cat "$cf" 2>/dev/null || true)"
|
|
525
|
-
local items; items="$(fetch_items "$ch" 50)"
|
|
526
|
-
local last; last="$(printf '%s' "$items" | jq -r 'last // empty | .sk // empty')"
|
|
527
|
-
local msgs
|
|
528
|
-
if uses_cli_auth; then
|
|
529
|
-
local identity agent username person email
|
|
530
|
-
identity="$(cli_identity)" || die "could not read the hq-cli identity — not polling"
|
|
531
|
-
agent="$(printf '%s' "$identity" | jq -r '.agentUid // empty')"
|
|
532
|
-
username="$(printf '%s' "$identity" | jq -r '.username // empty')"
|
|
533
|
-
person="$(printf '%s' "$identity" | jq -r '.personUid // empty')"
|
|
534
|
-
email="$(printf '%s' "$identity" | jq -r '.email // empty')"
|
|
535
|
-
msgs="$(printf '%s' "$items" | jq -r \
|
|
536
|
-
--arg cur "$cursor" --arg agent "$agent" --arg username "$username" --arg person "$person" --arg email "$email" '
|
|
537
|
-
map(select((.sk // "") > $cur and
|
|
538
|
-
(if ($agent != "" or $username != "") then
|
|
539
|
-
(((.senderMachineUid // .fromMachineUid // "") != $agent)
|
|
540
|
-
and ((.senderMachineUid // .fromMachineUid // "") != $username))
|
|
541
|
-
elif $person != "" then
|
|
542
|
-
((.fromPersonUid // "") != $person)
|
|
543
|
-
elif $email != "" then
|
|
544
|
-
((.fromEmail // "" | ascii_downcase) != ($email | ascii_downcase))
|
|
545
|
-
else true
|
|
546
|
-
end)))
|
|
547
|
-
| .[]
|
|
548
|
-
| ((.sk // "")[0:16] | sub("T"; " ")) + "Z " + (.fromDisplayName // .fromEmail // "?") + ": " + ((.body // "") | gsub("^\\s+|\\s+$"; ""))')"
|
|
549
|
-
else
|
|
550
|
-
local me; me="$(self_email)"
|
|
551
|
-
msgs="$(printf '%s' "$items" | jq -r --arg cur "$cursor" --arg me "$me" '
|
|
552
|
-
map(select((.sk // "") > $cur and ($me == "" or .fromEmail != $me)))
|
|
553
|
-
| .[]
|
|
554
|
-
| ((.sk // "")[0:16] | sub("T"; " ")) + "Z " + (.fromDisplayName // .fromEmail // "?") + ": " + ((.body // "") | gsub("^\\s+|\\s+$"; ""))')"
|
|
555
|
-
fi
|
|
556
|
-
[ -n "$last" ] && printf '%s\n' "$last" > "$cf"
|
|
557
|
-
if [ -n "$(printf '%s' "$msgs" | tr -d '[:space:]')" ]; then printf '%s\n' "$msgs"; return 0; fi
|
|
558
|
-
return 3
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
cmd_listen() {
|
|
562
|
-
local interval=60 timeout=1800
|
|
563
|
-
while [ $# -gt 0 ]; do
|
|
564
|
-
case "$1" in
|
|
565
|
-
--interval) interval="$2"; shift 2 ;;
|
|
566
|
-
--timeout) timeout="$2"; shift 2 ;;
|
|
567
|
-
*) die "listen: unknown arg $1" ;;
|
|
568
|
-
esac
|
|
569
|
-
done
|
|
570
|
-
local ch; ch="$(require_channel)"
|
|
571
|
-
local deadline=$(( $(date +%s) + timeout ))
|
|
572
|
-
echo "listening on #$ch every ${interval}s (timeout ${timeout}s)"
|
|
573
|
-
while [ "$(date +%s)" -lt "$deadline" ]; do
|
|
574
|
-
if cmd_poll; then echo "--- new message(s) on #$ch; handle them, then re-run listen"; return 0; fi
|
|
575
|
-
sleep "$interval"
|
|
576
|
-
done
|
|
577
|
-
echo "no new messages on #$ch within ${timeout}s"; return 3
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
case "${1:-}" in
|
|
581
|
-
bind) shift; cmd_bind "$@" ;;
|
|
582
|
-
unbind) cmd_unbind ;;
|
|
583
|
-
status) cmd_status ;;
|
|
584
|
-
post) shift; cmd_post "$@" ;;
|
|
585
|
-
roster) cmd_roster ;;
|
|
586
|
-
threads) cmd_threads ;;
|
|
587
|
-
poll) cmd_poll ;;
|
|
588
|
-
listen) shift; cmd_listen "$@" ;;
|
|
589
|
-
*) sed -n '2,25p' "$0"; exit 2 ;;
|
|
590
|
-
esac
|