@indigoai-us/hq-cli 5.305.0 → 5.307.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.
- package/CHANGELOG.md +13 -0
- package/assets/scaffold/core/scripts/audit-log.sh +384 -0
- package/assets/scaffold/core/scripts/git-pack-extension-check.sh +164 -0
- package/assets/scaffold/core/scripts/hq-pin.sh +118 -0
- package/assets/scaffold/core/scripts/lib/portable.sh +211 -0
- package/assets/scaffold/core/scripts/refresh-vault-access.sh +188 -0
- package/dist/command-catalog.generated.d.ts +54 -2
- package/dist/command-catalog.generated.js +66 -2
- package/dist/commands/core-audit-log-native.d.ts +9 -0
- package/dist/commands/core-audit-log-native.js +257 -0
- package/dist/commands/core-git-pack-extension-check-native.d.ts +22 -0
- package/dist/commands/core-git-pack-extension-check-native.js +190 -0
- package/dist/commands/core-native-utilities-flag.d.ts +4 -0
- package/dist/commands/core-native-utilities-flag.js +7 -0
- package/dist/commands/core-native-utilities.d.ts +28 -0
- package/dist/commands/core-native-utilities.js +52 -0
- package/dist/commands/core-pin-native.d.ts +14 -0
- package/dist/commands/core-pin-native.js +236 -0
- package/dist/commands/core-refresh-vault-access-native.d.ts +20 -0
- package/dist/commands/core-refresh-vault-access-native.js +229 -0
- package/dist/commands/core.js +15 -0
- package/dist/commands/daemon.js +24 -23
- package/dist/commands/dm.js +6 -1
- package/dist/commands/lanes.js +43 -1
- package/dist/lib/agent-kit/fallback.d.ts +0 -7
- package/dist/lib/agent-kit/fallback.js +4 -4
- package/dist/lib/agent-kit/inbox-state.d.ts +0 -2
- package/dist/lib/agent-kit/inbox-state.js +2 -2
- package/dist/lib/agent-kit/kit-config.d.ts +0 -5
- package/dist/lib/agent-kit/kit-config.js +5 -5
- package/dist/lib/agent-kit/log.d.ts +0 -1
- package/dist/lib/agent-kit/log.js +1 -1
- package/dist/lib/agent-kit/mcp/jsonrpc.d.ts +3 -12
- package/dist/lib/agent-kit/mcp/jsonrpc.js +4 -4
- package/dist/lib/agent-kit/mcp/tools.d.ts +0 -2
- package/dist/lib/agent-kit/mcp/tools.js +2 -2
- package/dist/lib/agent-kit/services.d.ts +0 -1
- package/dist/lib/agent-kit/services.js +1 -1
- package/dist/lib/checkpoint-ontology/apply.d.ts +0 -49
- package/dist/lib/checkpoint-ontology/apply.js +5 -5
- package/dist/lib/checkpoint-ontology/identity.d.ts +2 -1
- package/dist/lib/checkpoint-ontology/identity.js +1 -1
- package/dist/lib/checkpoint-ontology/multi-writer.d.ts +4 -9
- package/dist/lib/checkpoint-ontology/multi-writer.js +6 -6
- package/dist/lib/checkpoint-ontology/plan.d.ts +4 -17
- package/dist/lib/checkpoint-ontology/plan.js +5 -5
- package/dist/lib/checkpoint-ontology/queued-touches.d.ts +0 -3
- package/dist/lib/checkpoint-ontology/queued-touches.js +3 -3
- package/dist/lib/daemon/catalog.js +6 -2
- package/dist/lib/daemon/remote-control.d.ts +19 -8
- package/dist/lib/daemon/remote-control.js +62 -12
- package/dist/lib/daemon/supervisor.d.ts +2 -0
- package/dist/lib/daemon/supervisor.js +11 -0
- package/dist/lib/db/control-plane.js +6 -0
- package/dist/lib/deploy-apps.js +5 -0
- package/dist/startup-registration.js +9 -3
- package/dist/utils/install-tree-torn.d.ts +13 -6
- package/dist/utils/install-tree-torn.js +42 -16
- package/dist/utils/sentry-fingerprint.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/** Native per-session pin implementation matching core/scripts/hq-pin.sh. */
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
6
|
+
import { bundledShellUnboundValue, registerNativeUtility, utilityError } from "./core-native-utilities.js";
|
|
7
|
+
export const PIN_ASSET = "core/scripts/hq-pin.sh";
|
|
8
|
+
export const PIN_HELP = `# hq-pin.sh — anchor the current session to one goal (the "pin").
|
|
9
|
+
#
|
|
10
|
+
# A pin is the message, goal, or target the session keeps working toward
|
|
11
|
+
# across loops, wakeups, compactions, and hand-offs. It is stored per session
|
|
12
|
+
# and re-read at every checkpoint so the session cannot drift.
|
|
13
|
+
#
|
|
14
|
+
# Usage:
|
|
15
|
+
# core/scripts/hq-pin.sh set "<goal>" [--done "<criterion>"]... [--owner <name>] [--channel <dm-channel>]
|
|
16
|
+
# core/scripts/hq-pin.sh show # print the pin (goal, done criteria, owner, channel, set-at)
|
|
17
|
+
# core/scripts/hq-pin.sh check # print a one-line reminder for loop ticks; exit 3 if no pin
|
|
18
|
+
# core/scripts/hq-pin.sh note "<progress>" # append a dated progress line under the pin
|
|
19
|
+
# core/scripts/hq-pin.sh clear # remove the pin (the goal is met or withdrawn)
|
|
20
|
+
#
|
|
21
|
+
# Storage: workspace/sessions/<sid>/pin.md plus \`pin: <goal>\` in the session's
|
|
22
|
+
`;
|
|
23
|
+
const SESSION_ENV_KEYS = ["HQ_SESSION_ID", "CLAUDE_CODE_SESSION_ID", "CLAUDE_SESSION_ID", "CODEX_SESSION_ID", "CODEX_THREAD_ID", "GROK_SESSION_ID"];
|
|
24
|
+
const stamp = (date) => date.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
25
|
+
function normalizeId(value) { return value.replace(/[\t\n\v\f\r ]/g, ""); }
|
|
26
|
+
function validId(value) { return value !== "" && value !== "." && value !== ".." && /^[A-Za-z0-9._-]+$/.test(value); }
|
|
27
|
+
export function resolvePinSessionId(root, env) {
|
|
28
|
+
for (const key of SESSION_ENV_KEYS) {
|
|
29
|
+
const value = normalizeId(env[key] ?? "");
|
|
30
|
+
if (validId(value))
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const value = normalizeId(fs.readFileSync(path.join(root, "workspace/sessions/.current"), "utf8"));
|
|
35
|
+
return validId(value) ? value : "";
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function pinPath(deps, sid) { return path.join(deps.root, "workspace/sessions", sid, "pin.md"); }
|
|
42
|
+
function fail(message) { return utilityError(`hq-pin: ${message}`, 2); }
|
|
43
|
+
function sessionMetaPath(deps, sid) { return path.join(deps.root, "workspace/sessions", sid, "meta.yaml"); }
|
|
44
|
+
function writeSessionPin(deps, sid, goal) {
|
|
45
|
+
const meta = sessionMetaPath(deps, sid);
|
|
46
|
+
fs.mkdirSync(path.dirname(meta), { recursive: true });
|
|
47
|
+
let contents;
|
|
48
|
+
try {
|
|
49
|
+
contents = fs.readFileSync(meta, "utf8");
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (error.code !== "ENOENT")
|
|
53
|
+
throw error;
|
|
54
|
+
contents = `session_id: ${sid}\nstarted_at: "${stamp(deps.now())}"\nsenior: user\n`;
|
|
55
|
+
}
|
|
56
|
+
const lines = contents ? contents.replace(/\n$/, "").split("\n") : [];
|
|
57
|
+
let found = false;
|
|
58
|
+
const next = lines.map((line) => {
|
|
59
|
+
if (line.trimStart().split(/[\t ]/)[0] === "pin:") {
|
|
60
|
+
found = true;
|
|
61
|
+
return `pin: ${goal}`;
|
|
62
|
+
}
|
|
63
|
+
return line;
|
|
64
|
+
});
|
|
65
|
+
if (!found)
|
|
66
|
+
next.push(`pin: ${goal}`);
|
|
67
|
+
const text = `${next.join("\n")}\n`;
|
|
68
|
+
const temp = path.join(path.dirname(meta), `.meta.yaml.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`);
|
|
69
|
+
fs.writeFileSync(temp, text, { mode: 0o600 });
|
|
70
|
+
fs.renameSync(temp, meta);
|
|
71
|
+
}
|
|
72
|
+
function createPinDirectory(deps) {
|
|
73
|
+
const sid = resolvePinSessionId(deps.root, deps.env);
|
|
74
|
+
if (!sid)
|
|
75
|
+
return { error: fail("no current session (hq-session.sh current is empty)") };
|
|
76
|
+
const file = pinPath(deps, sid);
|
|
77
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
78
|
+
return { sid, file };
|
|
79
|
+
}
|
|
80
|
+
function setPin(args, deps) {
|
|
81
|
+
const goal = args[0] ?? "";
|
|
82
|
+
if (!goal)
|
|
83
|
+
return fail("set needs the goal text");
|
|
84
|
+
let owner = "";
|
|
85
|
+
let channel = "";
|
|
86
|
+
const done = [];
|
|
87
|
+
for (let i = 1; i < args.length;) {
|
|
88
|
+
const arg = args[i];
|
|
89
|
+
if (arg === "--done") {
|
|
90
|
+
if (i + 1 >= args.length)
|
|
91
|
+
return bundledShellUnboundValue(PIN_ASSET, 36);
|
|
92
|
+
done.push(args[i + 1]);
|
|
93
|
+
i += 2;
|
|
94
|
+
}
|
|
95
|
+
else if (arg === "--owner") {
|
|
96
|
+
if (i + 1 >= args.length)
|
|
97
|
+
return bundledShellUnboundValue(PIN_ASSET, 37);
|
|
98
|
+
owner = args[i + 1];
|
|
99
|
+
i += 2;
|
|
100
|
+
}
|
|
101
|
+
else if (arg === "--channel") {
|
|
102
|
+
if (i + 1 >= args.length)
|
|
103
|
+
return bundledShellUnboundValue(PIN_ASSET, 38);
|
|
104
|
+
channel = args[i + 1];
|
|
105
|
+
i += 2;
|
|
106
|
+
}
|
|
107
|
+
else
|
|
108
|
+
return fail(`set: unknown arg ${arg}`);
|
|
109
|
+
}
|
|
110
|
+
const current = createPinDirectory(deps);
|
|
111
|
+
if (current.error)
|
|
112
|
+
return current.error;
|
|
113
|
+
const lines = ["# Pin", "", `Goal: ${goal}`, "", `Set: ${stamp(deps.now())}`];
|
|
114
|
+
if (owner)
|
|
115
|
+
lines.push(`Owner: ${owner}`);
|
|
116
|
+
if (channel)
|
|
117
|
+
lines.push(`Channel: ${channel}`);
|
|
118
|
+
if (done.length > 0)
|
|
119
|
+
lines.push("", "## Done when", ...done.map((criterion) => `- [ ] ${criterion}`));
|
|
120
|
+
lines.push("", "## Progress");
|
|
121
|
+
fs.writeFileSync(current.file, `${lines.join("\n")}\n`, { encoding: "utf8", mode: 0o666 });
|
|
122
|
+
writeSessionPin(deps, current.sid, goal);
|
|
123
|
+
return { code: 0, stdout: `pinned session ${current.sid}: ${goal}\n`, stderr: "" };
|
|
124
|
+
}
|
|
125
|
+
function showPin(deps) {
|
|
126
|
+
const current = createPinDirectory(deps);
|
|
127
|
+
if (current.error)
|
|
128
|
+
return current.error;
|
|
129
|
+
if (!fs.existsSync(current.file))
|
|
130
|
+
return { code: 3, stdout: `no pin for session ${current.sid}\n`, stderr: "" };
|
|
131
|
+
return { code: 0, stdout: fs.readFileSync(current.file, "utf8"), stderr: "" };
|
|
132
|
+
}
|
|
133
|
+
function checkPin(deps) {
|
|
134
|
+
const current = createPinDirectory(deps);
|
|
135
|
+
if (current.error)
|
|
136
|
+
return current.error;
|
|
137
|
+
if (!fs.existsSync(current.file))
|
|
138
|
+
return { code: 3, stdout: "no pin\n", stderr: "" };
|
|
139
|
+
const text = fs.readFileSync(current.file, "utf8");
|
|
140
|
+
const goal = text.split(/\r?\n/).find((line) => line.startsWith("Goal: "))?.slice(6) ?? "";
|
|
141
|
+
const open = (text.match(/^- \[ \]/gm) ?? []).length;
|
|
142
|
+
const closed = (text.match(/^- \[x\]/gm) ?? []).length;
|
|
143
|
+
return { code: 0, stdout: `PIN: ${goal} (done criteria: ${closed} met, ${open} open)\n`, stderr: "" };
|
|
144
|
+
}
|
|
145
|
+
function notePin(args, deps) {
|
|
146
|
+
const text = args[0] ?? "";
|
|
147
|
+
if (!text)
|
|
148
|
+
return fail("note needs text");
|
|
149
|
+
const current = createPinDirectory(deps);
|
|
150
|
+
if (current.error)
|
|
151
|
+
return current.error;
|
|
152
|
+
if (!fs.existsSync(current.file))
|
|
153
|
+
return fail("no pin to note against");
|
|
154
|
+
const time = deps.now().toISOString().slice(11, 16);
|
|
155
|
+
fs.appendFileSync(current.file, `- ${time}Z ${text}\n`);
|
|
156
|
+
return { code: 0, stdout: "noted\n", stderr: "" };
|
|
157
|
+
}
|
|
158
|
+
function donePin(args, deps) {
|
|
159
|
+
const needle = args[0] ?? "";
|
|
160
|
+
if (!needle)
|
|
161
|
+
return fail("done needs a criterion substring");
|
|
162
|
+
const current = createPinDirectory(deps);
|
|
163
|
+
if (current.error)
|
|
164
|
+
return current.error;
|
|
165
|
+
if (!fs.existsSync(current.file))
|
|
166
|
+
return fail("no pin");
|
|
167
|
+
const text = fs.readFileSync(current.file, "utf8");
|
|
168
|
+
const lines = text.split("\n");
|
|
169
|
+
// Bash read ignores a final line without a newline. The shell script then
|
|
170
|
+
// echoes every line it did read, adding a newline to each one.
|
|
171
|
+
lines.pop();
|
|
172
|
+
let hit = false;
|
|
173
|
+
const next = lines.map((line) => {
|
|
174
|
+
if (line.startsWith("- [ ] ") && line.includes(needle)) {
|
|
175
|
+
hit = true;
|
|
176
|
+
// Preserve the bundled shell's parameter-expansion result: it keeps the
|
|
177
|
+
// matched source line after adding the checked marker.
|
|
178
|
+
return `- [x] ${line}`;
|
|
179
|
+
}
|
|
180
|
+
return line;
|
|
181
|
+
});
|
|
182
|
+
const output = next.length > 0 ? `${next.join("\n")}\n` : "";
|
|
183
|
+
const tempDir = fs.mkdtempSync(path.join(deps.tmpDir ?? os.tmpdir(), "hq-pin-"));
|
|
184
|
+
const tempFile = path.join(tempDir, "pin");
|
|
185
|
+
try {
|
|
186
|
+
fs.writeFileSync(tempFile, output, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
187
|
+
try {
|
|
188
|
+
fs.renameSync(tempFile, current.file);
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
if (error.code !== "EXDEV")
|
|
192
|
+
throw error;
|
|
193
|
+
fs.copyFileSync(tempFile, current.file);
|
|
194
|
+
fs.chmodSync(current.file, 0o600);
|
|
195
|
+
fs.unlinkSync(tempFile);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
200
|
+
}
|
|
201
|
+
return hit
|
|
202
|
+
? { code: 0, stdout: `marked done: ${needle}\n`, stderr: "" }
|
|
203
|
+
: fail(`no open criterion matches: ${needle}`);
|
|
204
|
+
}
|
|
205
|
+
function clearPin(deps) {
|
|
206
|
+
const current = createPinDirectory(deps);
|
|
207
|
+
if (current.error)
|
|
208
|
+
return current.error;
|
|
209
|
+
fs.rmSync(current.file, { force: true });
|
|
210
|
+
writeSessionPin(deps, current.sid, "");
|
|
211
|
+
return { code: 0, stdout: "pin cleared\n", stderr: "" };
|
|
212
|
+
}
|
|
213
|
+
export function runPin(args, deps) {
|
|
214
|
+
const [command = "", ...rest] = args;
|
|
215
|
+
if (command === "set")
|
|
216
|
+
return setPin(rest, deps);
|
|
217
|
+
if (command === "show")
|
|
218
|
+
return showPin(deps);
|
|
219
|
+
if (command === "check")
|
|
220
|
+
return checkPin(deps);
|
|
221
|
+
if (command === "note")
|
|
222
|
+
return notePin(rest, deps);
|
|
223
|
+
if (command === "done")
|
|
224
|
+
return donePin(rest, deps);
|
|
225
|
+
if (command === "clear")
|
|
226
|
+
return clearPin(deps);
|
|
227
|
+
return { code: 2, stdout: PIN_HELP, stderr: "" };
|
|
228
|
+
}
|
|
229
|
+
export function registerCorePinNative(core) {
|
|
230
|
+
registerNativeUtility(core, {
|
|
231
|
+
name: "pin", asset: PIN_ASSET, root: "live", summary: "Pin the current session to one goal",
|
|
232
|
+
resolveRoot: (_cwd, hqRoot) => resolveLiveRoot({ hqRoot }),
|
|
233
|
+
run: (args, context) => runPin(args, context),
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
//# sourceMappingURL=core-pin-native.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { type UtilityOutput } from "./core-native-utilities.js";
|
|
3
|
+
export declare const REFRESH_VAULT_ACCESS_ASSET = "core/scripts/refresh-vault-access.sh";
|
|
4
|
+
export declare const REFRESH_VAULT_ACCESS_HELP: string;
|
|
5
|
+
export type HqResult = {
|
|
6
|
+
code: number;
|
|
7
|
+
stdout: string;
|
|
8
|
+
stderr: string;
|
|
9
|
+
missing?: boolean;
|
|
10
|
+
};
|
|
11
|
+
export type RefreshVaultAccessDependencies = {
|
|
12
|
+
cwd: string;
|
|
13
|
+
env: NodeJS.ProcessEnv;
|
|
14
|
+
now: () => Date;
|
|
15
|
+
hqAvailable: boolean;
|
|
16
|
+
runHq?: (args: string[], cwd: string) => HqResult;
|
|
17
|
+
};
|
|
18
|
+
export declare function runRefreshVaultAccess(args: string[], deps: RefreshVaultAccessDependencies): UtilityOutput;
|
|
19
|
+
export declare function registerCoreRefreshVaultAccessNative(core: Command): void;
|
|
20
|
+
//# sourceMappingURL=core-refresh-vault-access-native.d.ts.map
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/** Native local manifest refresh for core/scripts/refresh-vault-access.sh. */
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { serializeJqJson } from "../lib/core-utils/jq-json-serializer.js";
|
|
6
|
+
import { registerNativeUtility } from "./core-native-utilities.js";
|
|
7
|
+
export const REFRESH_VAULT_ACCESS_ASSET = "core/scripts/refresh-vault-access.sh";
|
|
8
|
+
export const REFRESH_VAULT_ACCESS_HELP = `# hq-core: public
|
|
9
|
+
# refresh-vault-access.sh — build/refresh the local vault-access manifest that
|
|
10
|
+
# enforce-vault-write-access.sh (PreToolUse hook) reads.
|
|
11
|
+
#
|
|
12
|
+
# Output: <hqRoot>/.hq/vault-access.json
|
|
13
|
+
#
|
|
14
|
+
# {
|
|
15
|
+
# "version": 1,
|
|
16
|
+
# "generatedAt": "2026-01-01T00:00:00Z",
|
|
17
|
+
# "companies": {
|
|
18
|
+
# "<slug>": {
|
|
19
|
+
# "role": "owner|admin|member|guest|unknown",
|
|
20
|
+
# "grants": [ { "path": "reports/*", "permission": "read|write" } ]
|
|
21
|
+
# }
|
|
22
|
+
# }
|
|
23
|
+
# }
|
|
24
|
+
#
|
|
25
|
+
# Sources (best-effort, all via the hq CLI):
|
|
26
|
+
# - grants: \`hq files shared-with-me\` (explicit per-prefix grants)
|
|
27
|
+
# - role: \`hq members --company <slug> list\` matched against the caller's
|
|
28
|
+
# email (from \`hq whoami\`, override with HQ_VAULT_ACCESS_EMAIL)
|
|
29
|
+
#
|
|
30
|
+
# Fail-open by design: anything this script cannot determine is recorded as
|
|
31
|
+
# role "unknown" (the hook does not enforce unknown roles), and a company it
|
|
32
|
+
# cannot query at all is left out of the manifest entirely (the hook allows
|
|
33
|
+
# companies absent from the manifest). The server-side STS/ACL layer remains
|
|
34
|
+
# the authoritative security boundary — this manifest only powers the local
|
|
35
|
+
# early-warning block for read-only shares.
|
|
36
|
+
#
|
|
37
|
+
# Usage:
|
|
38
|
+
# bash core/scripts/refresh-vault-access.sh [--company <slug>]... [--root <hqRoot>]
|
|
39
|
+
#
|
|
40
|
+
`.replace(/^# ?/gm, "");
|
|
41
|
+
function defaultHq(args, cwd, env) {
|
|
42
|
+
const result = spawnSync("hq", args, { cwd, env, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
43
|
+
if (result.error)
|
|
44
|
+
return { code: 127, stdout: "", stderr: result.error.message, missing: result.error.code === "ENOENT" };
|
|
45
|
+
return { code: result.status ?? 128, stdout: String(result.stdout ?? ""), stderr: String(result.stderr ?? "") };
|
|
46
|
+
}
|
|
47
|
+
function executableOnPath(name, value) {
|
|
48
|
+
if (!value)
|
|
49
|
+
return false;
|
|
50
|
+
for (const directory of value.split(path.delimiter)) {
|
|
51
|
+
if (!directory)
|
|
52
|
+
continue;
|
|
53
|
+
const candidate = path.join(directory, name);
|
|
54
|
+
try {
|
|
55
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
catch { /* keep checking the PATH */ }
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
function isHqRoot(root) {
|
|
63
|
+
const candidate = root.endsWith(path.sep) ? root.slice(0, -1) : root;
|
|
64
|
+
if (!candidate)
|
|
65
|
+
return false;
|
|
66
|
+
try {
|
|
67
|
+
return fs.statSync(path.join(candidate, "core")).isDirectory() && fs.statSync(path.join(candidate, ".claude")).isDirectory();
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function parseOptions(args) {
|
|
74
|
+
let root = "";
|
|
75
|
+
const slugs = [];
|
|
76
|
+
for (let i = 0; i < args.length;) {
|
|
77
|
+
const arg = args[i];
|
|
78
|
+
if (arg === "--root") {
|
|
79
|
+
if (i + 1 >= args.length)
|
|
80
|
+
return { root, slugs, help: false, error: "--root requires a value" };
|
|
81
|
+
root = args[i + 1];
|
|
82
|
+
i += 2;
|
|
83
|
+
}
|
|
84
|
+
else if (arg === "--company") {
|
|
85
|
+
if (i + 1 >= args.length)
|
|
86
|
+
return { root, slugs, help: false, error: "--company requires a value" };
|
|
87
|
+
slugs.push(args[i + 1]);
|
|
88
|
+
i += 2;
|
|
89
|
+
}
|
|
90
|
+
else if (arg === "-h" || arg === "--help")
|
|
91
|
+
return { root, slugs, help: true };
|
|
92
|
+
else
|
|
93
|
+
return { root, slugs, help: false, error: `unknown argument: ${arg}` };
|
|
94
|
+
}
|
|
95
|
+
return { root, slugs, help: false };
|
|
96
|
+
}
|
|
97
|
+
function resolveRoot(parsedRoot, deps) {
|
|
98
|
+
if (parsedRoot)
|
|
99
|
+
return parsedRoot;
|
|
100
|
+
if (isHqRoot(deps.env.CLAUDE_PROJECT_DIR ?? ""))
|
|
101
|
+
return deps.env.CLAUDE_PROJECT_DIR;
|
|
102
|
+
let current = path.resolve(deps.cwd);
|
|
103
|
+
while (current !== path.parse(current).root) {
|
|
104
|
+
if (isHqRoot(current))
|
|
105
|
+
return current;
|
|
106
|
+
current = path.dirname(current);
|
|
107
|
+
}
|
|
108
|
+
return "";
|
|
109
|
+
}
|
|
110
|
+
// eslint-disable-next-line no-control-regex -- strip ANSI SGR sequences printed by hq.
|
|
111
|
+
function cleanAnsi(value) { return value.replace(/\u001b\[[0-9;]*m/g, ""); }
|
|
112
|
+
function log(message) { return `refresh-vault-access: ${message}\n`; }
|
|
113
|
+
function getEmail(output) {
|
|
114
|
+
const matches = cleanAnsi(output).match(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g) ?? [];
|
|
115
|
+
return matches.find((email) => !email.includes("@outposts.")) ?? "";
|
|
116
|
+
}
|
|
117
|
+
function parseGrantTable(output) {
|
|
118
|
+
const grants = new Map();
|
|
119
|
+
for (let line of cleanAnsi(output).split(/\r?\n/)) {
|
|
120
|
+
line = line.replace(" (private folder)", "");
|
|
121
|
+
if (/^COMPANY\s/.test(line) || /^─/.test(line) || /^\s*$/.test(line) || /^Nothing is explicitly shared/.test(line))
|
|
122
|
+
continue;
|
|
123
|
+
const fields = line.trim().split(/\s+/);
|
|
124
|
+
if (fields.length < 3)
|
|
125
|
+
continue;
|
|
126
|
+
const permission = ["read", "write", "admin"].includes(fields[2]) ? fields[2]
|
|
127
|
+
: fields.length >= 4 && ["read", "write", "admin"].includes(fields[3]) ? fields[3] : "";
|
|
128
|
+
if (!permission)
|
|
129
|
+
continue;
|
|
130
|
+
const rows = grants.get(fields[0]) ?? [];
|
|
131
|
+
rows.push({ path: fields[1], permission });
|
|
132
|
+
grants.set(fields[0], rows);
|
|
133
|
+
}
|
|
134
|
+
return grants;
|
|
135
|
+
}
|
|
136
|
+
function roleFromTable(output, email) {
|
|
137
|
+
if (!email)
|
|
138
|
+
return "unknown";
|
|
139
|
+
for (const line of cleanAnsi(output).split(/\r?\n/)) {
|
|
140
|
+
const [candidate, role] = line.trim().split(/\s+/);
|
|
141
|
+
if (candidate === email && ["owner", "admin", "member", "guest"].includes(role ?? ""))
|
|
142
|
+
return role;
|
|
143
|
+
if (candidate === email)
|
|
144
|
+
return "unknown";
|
|
145
|
+
}
|
|
146
|
+
return "unknown";
|
|
147
|
+
}
|
|
148
|
+
function resolveEmail(deps, runHq) {
|
|
149
|
+
const override = deps.env.HQ_VAULT_ACCESS_EMAIL ?? "";
|
|
150
|
+
if (override)
|
|
151
|
+
return { email: override, stderr: "" };
|
|
152
|
+
const result = runHq(["whoami"], deps.cwd);
|
|
153
|
+
const email = getEmail(result.stdout);
|
|
154
|
+
return { email, stderr: email ? "" : log("could not determine caller email — roles will be 'unknown' (hook will not enforce)") };
|
|
155
|
+
}
|
|
156
|
+
export function runRefreshVaultAccess(args, deps) {
|
|
157
|
+
const parsed = parseOptions(args);
|
|
158
|
+
if (parsed.help)
|
|
159
|
+
return { code: 0, stdout: REFRESH_VAULT_ACCESS_HELP, stderr: "" };
|
|
160
|
+
if (parsed.error)
|
|
161
|
+
return { code: 1, stdout: "", stderr: log(parsed.error) };
|
|
162
|
+
const root = resolveRoot(parsed.root, deps);
|
|
163
|
+
if (!isHqRoot(root))
|
|
164
|
+
return { code: 1, stdout: "", stderr: log("could not resolve the HQ root (pass --root <hqRoot>)") };
|
|
165
|
+
if (!deps.hqAvailable)
|
|
166
|
+
return { code: 0, stdout: "", stderr: log("hq CLI not found — nothing refreshed (the write-access hook stays fail-open without a manifest)") };
|
|
167
|
+
const runHq = deps.runHq ?? ((hqArgs, cwd) => defaultHq(hqArgs, cwd, deps.env));
|
|
168
|
+
const identity = resolveEmail(deps, runHq);
|
|
169
|
+
let stderr = identity.stderr;
|
|
170
|
+
const explicit = parsed.slugs;
|
|
171
|
+
const allGrants = new Map();
|
|
172
|
+
if (explicit.length > 0) {
|
|
173
|
+
for (const slug of explicit) {
|
|
174
|
+
const result = runHq(["files", "shared-with-me", "--company", slug], deps.cwd);
|
|
175
|
+
if (result.code !== 0) {
|
|
176
|
+
stderr += log(`skipping ${slug} — shared-with-me query failed`);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const rows = parseGrantTable(result.stdout);
|
|
180
|
+
for (const [company, grants] of rows)
|
|
181
|
+
allGrants.set(company, [...(allGrants.get(company) ?? []), ...grants]);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
const result = runHq(["files", "shared-with-me"], deps.cwd);
|
|
186
|
+
if (result.code !== 0)
|
|
187
|
+
return { code: 0, stdout: "", stderr: `${stderr}${log("shared-with-me roll-up failed — nothing refreshed")}` };
|
|
188
|
+
for (const [company, grants] of parseGrantTable(result.stdout))
|
|
189
|
+
allGrants.set(company, grants);
|
|
190
|
+
}
|
|
191
|
+
const companies = new Set(allGrants.keys());
|
|
192
|
+
for (const slug of explicit)
|
|
193
|
+
companies.add(slug);
|
|
194
|
+
const sortedSlugs = [...companies].sort();
|
|
195
|
+
if (sortedSlugs.length === 0)
|
|
196
|
+
stderr += log("no explicit grants found — writing an empty manifest (hook stays fail-open everywhere)");
|
|
197
|
+
const companyData = {};
|
|
198
|
+
for (const slug of sortedSlugs) {
|
|
199
|
+
let role = "unknown";
|
|
200
|
+
if (identity.email) {
|
|
201
|
+
const result = runHq(["members", "--company", slug, "list"], deps.cwd);
|
|
202
|
+
role = result.code === 0 ? roleFromTable(result.stdout, identity.email) : "unknown";
|
|
203
|
+
}
|
|
204
|
+
const grants = (allGrants.get(slug) ?? []).map((grant) => ({ path: grant.path, permission: grant.permission }));
|
|
205
|
+
companyData[slug] = { role, grants };
|
|
206
|
+
}
|
|
207
|
+
const manifest = { version: 1, generatedAt: deps.now().toISOString().replace(/\.\d{3}Z$/, "Z"), companies: companyData };
|
|
208
|
+
const directory = path.join(root, ".hq");
|
|
209
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
210
|
+
const target = path.join(directory, "vault-access.json");
|
|
211
|
+
const temp = `${target}.${process.pid}.${Math.random().toString(16).slice(2)}`;
|
|
212
|
+
fs.writeFileSync(temp, serializeJqJson(manifest, "pretty"), { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
213
|
+
fs.chmodSync(temp, 0o600);
|
|
214
|
+
fs.renameSync(temp, target);
|
|
215
|
+
stderr += log(`wrote .hq/vault-access.json (${sortedSlugs.length} companies, identity: ${identity.email || "unknown"})`);
|
|
216
|
+
return { code: 0, stdout: "", stderr };
|
|
217
|
+
}
|
|
218
|
+
export function registerCoreRefreshVaultAccessNative(core) {
|
|
219
|
+
registerNativeUtility(core, {
|
|
220
|
+
name: "refresh-vault-access", asset: REFRESH_VAULT_ACCESS_ASSET, root: "cwd", summary: "Refresh the local vault access manifest",
|
|
221
|
+
resolveRoot: (cwd) => cwd,
|
|
222
|
+
run: (args, context) => runRefreshVaultAccess(args, {
|
|
223
|
+
cwd: context.cwd, env: context.env, now: context.now,
|
|
224
|
+
hqAvailable: executableOnPath("hq", context.env.PATH),
|
|
225
|
+
runHq: (args, cwd) => defaultHq(args, cwd, context.env),
|
|
226
|
+
}),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
//# sourceMappingURL=core-refresh-vault-access-native.js.map
|
package/dist/commands/core.js
CHANGED
|
@@ -38,6 +38,10 @@ import { registerCoreKnowledgePrefsCommand } from "./core-knowledge-prefs.js";
|
|
|
38
38
|
import { registerCorePolicyCommand } from "./core-policy.js";
|
|
39
39
|
import { registerCoreJobsNativeCommand } from "./core-job-native.js";
|
|
40
40
|
import { registerCoreCheckpointGateSettingCommand } from "./core-checkpoint-gate-setting.js";
|
|
41
|
+
import { registerCoreAuditLogNative } from "./core-audit-log-native.js";
|
|
42
|
+
import { registerCoreGitPackExtensionCheckNative } from "./core-git-pack-extension-check-native.js";
|
|
43
|
+
import { registerCorePinNative } from "./core-pin-native.js";
|
|
44
|
+
import { registerCoreRefreshVaultAccessNative } from "./core-refresh-vault-access-native.js";
|
|
41
45
|
import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
42
46
|
import { interpreterFor, runBundledScript } from "../utils/run-bundled-script.js";
|
|
43
47
|
import { CompanyIndexUserError, renderIndexTarget } from "../lib/index-render/index.js";
|
|
@@ -271,6 +275,13 @@ export const SCAFFOLD_ONLY_ASSETS = [
|
|
|
271
275
|
{ asset: "core/scripts/handoff-post.sh", root: "live", summary: "Rollback shell for native handoff post" },
|
|
272
276
|
{ asset: "core/scripts/handoff-open-steps.sh", root: "live", summary: "Rollback shell for native handoff open-steps" },
|
|
273
277
|
{ asset: "core/scripts/qmd-reindex-bg.sh", root: "live", summary: "Rollback handoff reindex forwarder" },
|
|
278
|
+
// Wave 5 native utility rollback companions, sourced verbatim from staging
|
|
279
|
+
// because these assets were not present in the hq-cli bundle at launch.
|
|
280
|
+
{ asset: "core/scripts/audit-log.sh", root: "live", summary: "Rollback shell for native audit-log" },
|
|
281
|
+
{ asset: "core/scripts/git-pack-extension-check.sh", root: "cwd", summary: "Rollback shell for native git-pack-extension-check" },
|
|
282
|
+
{ asset: "core/scripts/hq-pin.sh", root: "live", summary: "Rollback shell for native pin" },
|
|
283
|
+
{ asset: "core/scripts/refresh-vault-access.sh", root: "cwd", summary: "Rollback shell for native refresh-vault-access" },
|
|
284
|
+
{ asset: "core/scripts/lib/portable.sh", root: "live", summary: "Shared portable helpers sourced by the audit-log rollback shell" },
|
|
274
285
|
...NATIVE_UTILITY_COMMANDS.map(({ asset, root, summary }) => ({
|
|
275
286
|
asset,
|
|
276
287
|
root,
|
|
@@ -354,6 +365,10 @@ export function registerCoreCommands(program) {
|
|
|
354
365
|
registerCoreKnowledgePrefsCommand(core);
|
|
355
366
|
registerCorePolicyCommand(core);
|
|
356
367
|
registerCoreJobsNativeCommand(core);
|
|
368
|
+
registerCoreAuditLogNative(core);
|
|
369
|
+
registerCoreGitPackExtensionCheckNative(core);
|
|
370
|
+
registerCorePinNative(core);
|
|
371
|
+
registerCoreRefreshVaultAccessNative(core);
|
|
357
372
|
// Wave 2 handoff/worktree commands retain their byte-identical shell assets
|
|
358
373
|
// as a default-off rollback path for one release. Keep the gate helper small
|
|
359
374
|
// so sibling Wave 2 work can reuse it without editing this command table.
|
package/dist/commands/daemon.js
CHANGED
|
@@ -627,25 +627,9 @@ async function runServiceSelfUpdate(d) {
|
|
|
627
627
|
if (!result.ok)
|
|
628
628
|
process.exitCode = 1;
|
|
629
629
|
}
|
|
630
|
-
/** Size, mtime and real path of a binary: changes whenever a new version is installed over it. */
|
|
631
|
-
function binaryFingerprint(bin) {
|
|
632
|
-
try {
|
|
633
|
-
const real = fs.realpathSync(bin);
|
|
634
|
-
const st = fs.statSync(real);
|
|
635
|
-
return `${real}:${st.size}:${st.mtimeMs}`;
|
|
636
|
-
}
|
|
637
|
-
catch {
|
|
638
|
-
return null;
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
630
|
async function runTmuxService(d, opts) {
|
|
642
|
-
const {
|
|
631
|
+
const { runRemoteControlHost, toolFingerprint } = await import("../lib/daemon/remote-control.js");
|
|
643
632
|
const { findOnPath } = await import("../lib/daemon/run.js");
|
|
644
|
-
const { resolveRunningInstall } = await import("../utils/version-gate.js");
|
|
645
|
-
// The daemon keeps this host running across a CLI update, so a new hq-cli
|
|
646
|
-
// install also counts as an update and restarts the session once it is quiet.
|
|
647
|
-
const cliRoot = resolveRunningInstall().packageRoot;
|
|
648
|
-
const watched = cliRoot ? [opts.watch, path.join(cliRoot, "package.json")] : [opts.watch];
|
|
649
633
|
const hqRoot = d.env.HQ_ROOT?.trim();
|
|
650
634
|
if (!hqRoot)
|
|
651
635
|
throw new DaemonCommandError(`${opts.label} needs HQ_ROOT (the daemon sets it from the saved HQ folder)`);
|
|
@@ -663,7 +647,9 @@ async function runTmuxService(d, opts) {
|
|
|
663
647
|
cwd: hqRoot,
|
|
664
648
|
argv: opts.argv,
|
|
665
649
|
label: opts.label,
|
|
666
|
-
|
|
650
|
+
// Only a new version of the tool restarts the session: not a same-version
|
|
651
|
+
// reinstall, and not an hq-cli release (the host's own code rarely matters).
|
|
652
|
+
binaryFingerprint: () => toolFingerprint(opts.watch),
|
|
667
653
|
now: () => Date.now(),
|
|
668
654
|
run: d.run,
|
|
669
655
|
pollMs: 5_000,
|
|
@@ -683,22 +669,37 @@ async function runTmuxService(d, opts) {
|
|
|
683
669
|
process.off("SIGINT", stop);
|
|
684
670
|
}
|
|
685
671
|
}
|
|
672
|
+
/** The claude or codex a remote-control host runs: --claude/--codex, else the one installed now. */
|
|
673
|
+
async function hostTool(d, name, given) {
|
|
674
|
+
const [{ serviceTool }, { findUserBinary }] = await Promise.all([
|
|
675
|
+
import("../lib/daemon/remote-control.js"),
|
|
676
|
+
import("../lib/daemon/run.js"),
|
|
677
|
+
]);
|
|
678
|
+
try {
|
|
679
|
+
return serviceTool(name, given, (tool) => findUserBinary(tool, d.env, d.home));
|
|
680
|
+
}
|
|
681
|
+
catch (err) {
|
|
682
|
+
throw new DaemonCommandError(err.message);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
686
685
|
async function runServiceRemoteControl(d, opts) {
|
|
687
686
|
const { TMUX_SESSION, remoteControlArgs } = await import("../lib/daemon/remote-control.js");
|
|
687
|
+
const claude = await hostTool(d, "claude", opts.claude);
|
|
688
688
|
await runTmuxService(d, {
|
|
689
689
|
session: TMUX_SESSION,
|
|
690
|
-
argv: [
|
|
690
|
+
argv: [claude, ...remoteControlArgs({ name: opts.name, permissionMode: opts.permissionMode })],
|
|
691
691
|
label: "claude remote-control",
|
|
692
|
-
watch:
|
|
692
|
+
watch: claude,
|
|
693
693
|
});
|
|
694
694
|
}
|
|
695
695
|
async function runServiceCodexRemoteControl(d, opts) {
|
|
696
696
|
const { CODEX_TMUX_SESSION } = await import("../lib/daemon/remote-control.js");
|
|
697
|
+
const codex = await hostTool(d, "codex", opts.codex);
|
|
697
698
|
await runTmuxService(d, {
|
|
698
699
|
session: CODEX_TMUX_SESSION,
|
|
699
700
|
argv: ["bash", opts.script],
|
|
700
701
|
label: "codex remote-control",
|
|
701
|
-
watch:
|
|
702
|
+
watch: codex,
|
|
702
703
|
});
|
|
703
704
|
}
|
|
704
705
|
export function registerDaemonCommand(program, over = {}) {
|
|
@@ -839,7 +840,7 @@ export function registerDaemonCommand(program, over = {}) {
|
|
|
839
840
|
service
|
|
840
841
|
.command("remote-control")
|
|
841
842
|
.description("Keep `claude remote-control` running in a tmux session in the HQ folder")
|
|
842
|
-
.
|
|
843
|
+
.option("--claude <path>", "claude binary (default: the one installed for this user)")
|
|
843
844
|
.requiredOption("--name <name>", "remote-control session name")
|
|
844
845
|
.option("--permission-mode <mode>", "claude --permission-mode")
|
|
845
846
|
.action(wrap((d, opts) => runServiceRemoteControl(d, opts)));
|
|
@@ -847,7 +848,7 @@ export function registerDaemonCommand(program, over = {}) {
|
|
|
847
848
|
.command("codex-remote-control")
|
|
848
849
|
.description("Keep the box's Codex remote-control runner going in a tmux session, restarting it after a Codex update")
|
|
849
850
|
.requiredOption("--script <path>", "the box's Codex runner script")
|
|
850
|
-
.
|
|
851
|
+
.option("--codex <path>", "codex binary, watched for updates (default: the one installed for this user)")
|
|
851
852
|
.action(wrap((d, opts) => runServiceCodexRemoteControl(d, opts)));
|
|
852
853
|
return daemon;
|
|
853
854
|
}
|
package/dist/commands/dm.js
CHANGED
|
@@ -407,7 +407,12 @@ async function runConnectionAction(action, identifier) {
|
|
|
407
407
|
const token = await ensureCognitoToken();
|
|
408
408
|
// Resolve the pairKey from the caller's pending requests when possible so
|
|
409
409
|
// accept/decline/block work against a friendly email or personUid.
|
|
410
|
-
const requests = await fetchRequests(token).catch(() =>
|
|
410
|
+
const requests = await fetchRequests(token).catch((err) => {
|
|
411
|
+
console.error("hq dm: pending connection request lookup failed", {
|
|
412
|
+
errorName: err instanceof Error ? err.name : typeof err,
|
|
413
|
+
});
|
|
414
|
+
return [];
|
|
415
|
+
});
|
|
411
416
|
const matched = matchRequest(requests, identifier);
|
|
412
417
|
const body = buildConnectionActionBody(identifier, matched);
|
|
413
418
|
const res = await vaultApiFetch({
|