@indigoai-us/hq-cli 5.306.0 → 5.308.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 +10 -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/assets/scaffold/core/scripts/resolve-company.sh +199 -0
- package/dist/command-catalog.generated.d.ts +65 -0
- package/dist/command-catalog.generated.js +80 -0
- 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-resolve-company-command.d.ts +4 -0
- package/dist/commands/core-resolve-company-command.js +42 -0
- package/dist/commands/core-resolve-company-native.d.ts +28 -0
- package/dist/commands/core-resolve-company-native.js +408 -0
- package/dist/commands/core.js +18 -0
- package/dist/commands/dm.js +6 -1
- package/dist/commands/lanes.js +43 -1
- package/dist/fast-scaffold.js +12 -0
- 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/db/control-plane.js +6 -0
- package/dist/lib/deploy-apps.js +5 -0
- package/package.json +1 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { type UtilityOutput } from "./core-native-utilities.js";
|
|
3
|
+
export declare const PIN_ASSET = "core/scripts/hq-pin.sh";
|
|
4
|
+
export declare const PIN_HELP = "# hq-pin.sh \u2014 anchor the current session to one goal (the \"pin\").\n#\n# A pin is the message, goal, or target the session keeps working toward\n# across loops, wakeups, compactions, and hand-offs. It is stored per session\n# and re-read at every checkpoint so the session cannot drift.\n#\n# Usage:\n# core/scripts/hq-pin.sh set \"<goal>\" [--done \"<criterion>\"]... [--owner <name>] [--channel <dm-channel>]\n# core/scripts/hq-pin.sh show # print the pin (goal, done criteria, owner, channel, set-at)\n# core/scripts/hq-pin.sh check # print a one-line reminder for loop ticks; exit 3 if no pin\n# core/scripts/hq-pin.sh note \"<progress>\" # append a dated progress line under the pin\n# core/scripts/hq-pin.sh clear # remove the pin (the goal is met or withdrawn)\n#\n# Storage: workspace/sessions/<sid>/pin.md plus `pin: <goal>` in the session's\n";
|
|
5
|
+
export type PinDependencies = {
|
|
6
|
+
root: string;
|
|
7
|
+
env: NodeJS.ProcessEnv;
|
|
8
|
+
now: () => Date;
|
|
9
|
+
tmpDir?: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function resolvePinSessionId(root: string, env: NodeJS.ProcessEnv): string;
|
|
12
|
+
export declare function runPin(args: string[], deps: PinDependencies): UtilityOutput;
|
|
13
|
+
export declare function registerCorePinNative(core: Command): void;
|
|
14
|
+
//# sourceMappingURL=core-pin-native.d.ts.map
|
|
@@ -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
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
/** Commander registration stays in the full graph; the native resolver is fast-path safe. */
|
|
3
|
+
export declare function registerCoreResolveCompanyNativeCommand(core: Command): void;
|
|
4
|
+
//# sourceMappingURL=core-resolve-company-command.d.ts.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
2
|
+
import { runBundledScript } from "../utils/run-bundled-script.js";
|
|
3
|
+
import { describeCoreCommand } from "./core-command-catalog.js";
|
|
4
|
+
import { coreNativeUtilitiesEnabled } from "./core-native-utilities-flag.js";
|
|
5
|
+
import { RESOLVE_COMPANY_ASSET, resolveCompany } from "./core-resolve-company-native.js";
|
|
6
|
+
/** Commander registration stays in the full graph; the native resolver is fast-path safe. */
|
|
7
|
+
export function registerCoreResolveCompanyNativeCommand(core) {
|
|
8
|
+
const command = core.command("resolve-company")
|
|
9
|
+
.description("Resolve the active company for skills and routing")
|
|
10
|
+
.allowUnknownOption()
|
|
11
|
+
.allowExcessArguments()
|
|
12
|
+
.helpOption(false)
|
|
13
|
+
.argument("[args...]")
|
|
14
|
+
.action(async (args = [], _options, cmd) => {
|
|
15
|
+
const operands = cmd.args.length > 0 ? cmd.args : args;
|
|
16
|
+
if (!await coreNativeUtilitiesEnabled()) {
|
|
17
|
+
const root = resolveLiveRoot({ hqRoot: core.opts().hqRoot });
|
|
18
|
+
const { code } = runBundledScript({
|
|
19
|
+
asset: RESOLVE_COMPANY_ASSET,
|
|
20
|
+
args: operands,
|
|
21
|
+
cwd: root,
|
|
22
|
+
hqRoot: root,
|
|
23
|
+
});
|
|
24
|
+
if (code !== 0)
|
|
25
|
+
process.exit(code);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const root = resolveLiveRoot({ hqRoot: core.opts().hqRoot });
|
|
29
|
+
const result = await resolveCompany(operands, {
|
|
30
|
+
cwd: process.cwd(),
|
|
31
|
+
hqRoot: root,
|
|
32
|
+
stdinIsTty: process.stdin.isTTY === true,
|
|
33
|
+
});
|
|
34
|
+
process.stdout.write(result.stdout);
|
|
35
|
+
if (result.stderr.length)
|
|
36
|
+
process.stderr.write(result.stderr);
|
|
37
|
+
if (result.code !== 0)
|
|
38
|
+
process.exit(result.code);
|
|
39
|
+
});
|
|
40
|
+
describeCoreCommand(command, { execution: "native", asset: RESOLVE_COMPANY_ASSET, root: "live" });
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=core-resolve-company-command.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
export declare const RESOLVE_COMPANY_ASSET = "core/scripts/resolve-company.sh";
|
|
3
|
+
export declare const RESOLVE_COMPANY_HELP: string;
|
|
4
|
+
export type ResolveCompanyResult = {
|
|
5
|
+
code: number;
|
|
6
|
+
stdout: Buffer;
|
|
7
|
+
stderr: Buffer;
|
|
8
|
+
};
|
|
9
|
+
type HqResult = {
|
|
10
|
+
code: number;
|
|
11
|
+
stdout: string;
|
|
12
|
+
};
|
|
13
|
+
type ResolveCompanyFs = Pick<typeof fs, "readFileSync" | "statSync" | "accessSync">;
|
|
14
|
+
export type ResolveCompanyDependencies = {
|
|
15
|
+
cwd: string;
|
|
16
|
+
hqRoot?: string;
|
|
17
|
+
env?: NodeJS.ProcessEnv;
|
|
18
|
+
fs?: ResolveCompanyFs;
|
|
19
|
+
stdin?: string;
|
|
20
|
+
stdinIsTty?: boolean;
|
|
21
|
+
spawnHq?: (args: string[], env: NodeJS.ProcessEnv, timeoutMs: number, cwd?: string) => Promise<HqResult>;
|
|
22
|
+
pid?: number;
|
|
23
|
+
random?: () => number;
|
|
24
|
+
};
|
|
25
|
+
export declare function resolveCompany(args: readonly string[], dependencies: ResolveCompanyDependencies): Promise<ResolveCompanyResult>;
|
|
26
|
+
export declare function runResolveCompanyFast(args: readonly string[], stdin: string, cwd: string, hqRoot: string | undefined, _commandName?: string, _hqRootArg?: string, stdinReader?: () => Promise<string>): Promise<number>;
|
|
27
|
+
export {};
|
|
28
|
+
//# sourceMappingURL=core-resolve-company-native.d.ts.map
|