@indigoai-us/hq-cli 5.109.0 → 5.109.1
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 +7 -0
- package/dist/commands/access.js +24 -4
- package/dist/utils/access-outcomes.d.ts +23 -0
- package/dist/utils/access-outcomes.js +53 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.109.1] — 2026-09-08
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `hq access` records each outcome (outcome, company, timestamp only) to
|
|
10
|
+
`.hq/access-outcomes.jsonl` so the cause mix can be reviewed later.
|
|
11
|
+
|
|
5
12
|
## [5.109.0] — 2026-09-08
|
|
6
13
|
|
|
7
14
|
### Added
|
package/dist/commands/access.js
CHANGED
|
@@ -26,6 +26,7 @@ import { listActiveMembers } from "./members.js";
|
|
|
26
26
|
import { buildDmBody } from "./dm.js";
|
|
27
27
|
import { peekIdToken } from "../utils/id-token.js";
|
|
28
28
|
import { findRecentAccessRequest, formatTimeAgo, recordAccessRequest, } from "../utils/access-requests.js";
|
|
29
|
+
import { recordAccessOutcome } from "../utils/access-outcomes.js";
|
|
29
30
|
export function outcomeExitCode(outcome) {
|
|
30
31
|
if (outcome === "local" || outcome === "not-synced")
|
|
31
32
|
return 0;
|
|
@@ -146,7 +147,25 @@ function aclDenied(tree) {
|
|
|
146
147
|
const perm = tree.effectivePermission;
|
|
147
148
|
return perm === null || perm === "none" || perm === "deny";
|
|
148
149
|
}
|
|
149
|
-
|
|
150
|
+
/**
|
|
151
|
+
* Build the result and record one cause-mix telemetry line. Every exit path
|
|
152
|
+
* goes through here, so each run is recorded exactly once. The write is
|
|
153
|
+
* best-effort: a failure adds a step + one stderr warning and never changes
|
|
154
|
+
* the outcome or exit code. Only outcome, company, and timestamp are stored.
|
|
155
|
+
*/
|
|
156
|
+
function finish(outcome, fields, ctx) {
|
|
157
|
+
try {
|
|
158
|
+
recordAccessOutcome(ctx.hqRoot, {
|
|
159
|
+
outcome,
|
|
160
|
+
company: fields.company,
|
|
161
|
+
ts: new Date((ctx.now ?? Date.now)()).toISOString(),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
166
|
+
fields.steps.push(`telemetry: write failed — ${msg}`);
|
|
167
|
+
ctx.stderr(`warning: could not record access outcome telemetry: ${msg}`);
|
|
168
|
+
}
|
|
150
169
|
return { outcome, exitCode: outcomeExitCode(outcome), ...fields };
|
|
151
170
|
}
|
|
152
171
|
export function resolveGrantor(tree, members) {
|
|
@@ -194,6 +213,7 @@ export async function runAccess(input) {
|
|
|
194
213
|
const steps = [];
|
|
195
214
|
const relative = normalizeTarget(input.target, slug);
|
|
196
215
|
const anchored = (key) => `companies/${slug}/${key}`;
|
|
216
|
+
const finishCtx = { hqRoot: deps.hqRoot, now: deps.now, stderr };
|
|
197
217
|
let vendDenied = false;
|
|
198
218
|
try {
|
|
199
219
|
await deps.vaultClient.sts.vend({ companyUid: deps.companyUid });
|
|
@@ -252,7 +272,7 @@ export async function runAccess(input) {
|
|
|
252
272
|
company: slug,
|
|
253
273
|
exists: false,
|
|
254
274
|
steps,
|
|
255
|
-
});
|
|
275
|
+
}, finishCtx);
|
|
256
276
|
emit(input, result, log);
|
|
257
277
|
return result;
|
|
258
278
|
}
|
|
@@ -275,7 +295,7 @@ export async function runAccess(input) {
|
|
|
275
295
|
exists: false,
|
|
276
296
|
candidates,
|
|
277
297
|
steps,
|
|
278
|
-
});
|
|
298
|
+
}, finishCtx);
|
|
279
299
|
emit(input, result, log);
|
|
280
300
|
return result;
|
|
281
301
|
}
|
|
@@ -511,7 +531,7 @@ export async function runAccess(input) {
|
|
|
511
531
|
...(requestSentAt ? { requestSentAt } : {}),
|
|
512
532
|
...(alreadyAskedAt ? { alreadyAskedAt } : {}),
|
|
513
533
|
...(requestNote ? { requestNote } : {}),
|
|
514
|
-
});
|
|
534
|
+
}, finishCtx);
|
|
515
535
|
emit(input, result, log);
|
|
516
536
|
return result;
|
|
517
537
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AccessOutcome } from "../commands/access.js";
|
|
2
|
+
/**
|
|
3
|
+
* One line of the `hq access` cause-mix ledger. Deliberately narrow: no path,
|
|
4
|
+
* requester, or grantor — only enough to review which outcomes users hit.
|
|
5
|
+
*/
|
|
6
|
+
export interface AccessOutcomeRecord {
|
|
7
|
+
outcome: AccessOutcome;
|
|
8
|
+
company: string;
|
|
9
|
+
ts: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function accessOutcomesPath(hqRoot: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Append one JSONL line. Best-effort by contract: callers are expected to
|
|
14
|
+
* catch and downgrade any failure, since telemetry must never change an
|
|
15
|
+
* `hq access` outcome or exit code.
|
|
16
|
+
*/
|
|
17
|
+
export declare function recordAccessOutcome(hqRoot: string, rec: AccessOutcomeRecord): void;
|
|
18
|
+
/**
|
|
19
|
+
* Read the ledger. A missing file is the empty ledger; malformed lines are
|
|
20
|
+
* skipped so one bad append never hides the rest of the history.
|
|
21
|
+
*/
|
|
22
|
+
export declare function readAccessOutcomes(hqRoot: string): AccessOutcomeRecord[];
|
|
23
|
+
//# sourceMappingURL=access-outcomes.d.ts.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
export function accessOutcomesPath(hqRoot) {
|
|
4
|
+
return path.join(hqRoot, ".hq", "access-outcomes.jsonl");
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Append one JSONL line. Best-effort by contract: callers are expected to
|
|
8
|
+
* catch and downgrade any failure, since telemetry must never change an
|
|
9
|
+
* `hq access` outcome or exit code.
|
|
10
|
+
*/
|
|
11
|
+
export function recordAccessOutcome(hqRoot, rec) {
|
|
12
|
+
const file = accessOutcomesPath(hqRoot);
|
|
13
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
14
|
+
const line = JSON.stringify({ outcome: rec.outcome, company: rec.company, ts: rec.ts });
|
|
15
|
+
fs.appendFileSync(file, `${line}\n`, "utf-8");
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Read the ledger. A missing file is the empty ledger; malformed lines are
|
|
19
|
+
* skipped so one bad append never hides the rest of the history.
|
|
20
|
+
*/
|
|
21
|
+
export function readAccessOutcomes(hqRoot) {
|
|
22
|
+
const file = accessOutcomesPath(hqRoot);
|
|
23
|
+
let raw;
|
|
24
|
+
try {
|
|
25
|
+
raw = fs.readFileSync(file, "utf-8");
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
if (err.code === "ENOENT")
|
|
29
|
+
return [];
|
|
30
|
+
throw err;
|
|
31
|
+
}
|
|
32
|
+
const out = [];
|
|
33
|
+
for (const line of raw.split("\n")) {
|
|
34
|
+
const trimmed = line.trim();
|
|
35
|
+
if (!trimmed)
|
|
36
|
+
continue;
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(trimmed);
|
|
39
|
+
if (parsed &&
|
|
40
|
+
typeof parsed === "object" &&
|
|
41
|
+
typeof parsed.outcome === "string" &&
|
|
42
|
+
typeof parsed.company === "string" &&
|
|
43
|
+
typeof parsed.ts === "string") {
|
|
44
|
+
out.push(parsed);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// malformed line: skip
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=access-outcomes.js.map
|