@indigoai-us/hq-cli 5.345.106 → 5.345.107

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/assets/scaffold/core/scripts/hq-task.mjs +259 -0
  3. package/assets/scaffold/core/scripts/hq-task.sh +31 -0
  4. package/assets/scaffold/core/scripts/pipeline-worker-table.sh +285 -0
  5. package/dist/command-catalog.generated.d.ts +135 -5
  6. package/dist/command-catalog.generated.js +161 -5
  7. package/dist/commands/auth.js +42 -0
  8. package/dist/commands/core-hq-task-fast.d.ts +4 -0
  9. package/dist/commands/core-hq-task-fast.js +11 -0
  10. package/dist/commands/core-hq-task-native.d.ts +20 -0
  11. package/dist/commands/core-hq-task-native.js +275 -0
  12. package/dist/commands/core-native-bucket-c-flag.d.ts +3 -0
  13. package/dist/commands/core-native-bucket-c-flag.js +6 -0
  14. package/dist/commands/core-native-family-flag.d.ts +0 -2
  15. package/dist/commands/core-native-family-flag.js +0 -4
  16. package/dist/commands/core-native-pipeline-flag.d.ts +3 -0
  17. package/dist/commands/core-native-pipeline-flag.js +6 -0
  18. package/dist/commands/core-ontology-native.js +24 -30
  19. package/dist/commands/core-pipeline-workers-command.d.ts +3 -0
  20. package/dist/commands/core-pipeline-workers-command.js +37 -0
  21. package/dist/commands/core-pipeline-workers-native.d.ts +13 -0
  22. package/dist/commands/core-pipeline-workers-native.js +361 -0
  23. package/dist/commands/core.js +12 -10
  24. package/dist/commands/lanes.js +32 -7
  25. package/dist/commands/outposts.d.ts +5 -0
  26. package/dist/commands/outposts.js +5 -0
  27. package/dist/fast-scaffold.d.ts +1 -1
  28. package/dist/fast-scaffold.js +13 -5
  29. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +56937 -64550
  30. package/dist/hq-cloud-sync-runner/package.json +1 -1
  31. package/dist/lib/daemon/remote-control-pools.d.ts +2 -2
  32. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  33. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  34. package/dist/lib/lanes/reconcile.js +4 -0
  35. package/dist/lib/lanes/remote-control-lane.d.ts +33 -0
  36. package/dist/lib/lanes/remote-control-lane.js +276 -0
  37. package/dist/lib/lanes/spawn.d.ts +5 -0
  38. package/dist/lib/lanes/spawn.js +65 -14
  39. package/dist/lib/lanes/store.js +7 -0
  40. package/dist/lib/lanes/types.d.ts +2 -0
  41. package/dist/lib/lanes/types.js +1 -1
  42. package/dist/lib/lanes/watcher.js +8 -1
  43. package/dist/utils/cognito-session.d.ts +12 -4
  44. package/dist/utils/cognito-session.js +42 -9
  45. package/dist/utils/token-keychain.d.ts +96 -0
  46. package/dist/utils/token-keychain.js +218 -0
  47. package/package.json +2 -2
  48. package/assets/scaffold/core/scripts/ontology-audience-grant.sh +0 -81
  49. package/assets/scaffold/core/scripts/ontology-brief.mjs +0 -93
  50. package/assets/scaffold/core/scripts/ontology-candidate.sh +0 -100
  51. package/assets/scaffold/core/scripts/ontology-garden.mjs +0 -327
  52. package/assets/scaffold/core/scripts/ontology-source-worklist.sh +0 -226
  53. package/assets/scaffold/core/scripts/source-yaml-validate.sh +0 -36
@@ -25,15 +25,16 @@ import * as os from "os";
25
25
  import * as path from "path";
26
26
  import * as yaml from "js-yaml";
27
27
  import chalk from "chalk";
28
- import { loadCachedTokens, isExpiring, refreshTokens, saveCachedTokens, browserLogin, detectHqCoreVersion, isMachineIdentity, getValidMachineTokens, CognitoRefreshError, accessTokenFingerprint, invalidateCachedTokensByFingerprint, } from "@indigoai-us/hq-cloud";
28
+ import { loadCachedTokens as loadCachedTokensFromFile, isExpiring, refreshTokens, saveCachedTokens, browserLogin, detectHqCoreVersion, isMachineIdentity, getValidMachineTokens, CognitoRefreshError, accessTokenFingerprint, invalidateCachedTokensByFingerprint, } from "@indigoai-us/hq-cloud";
29
29
  import { CLI_NAME, CLI_VERSION } from "../cli-version.js";
30
30
  import { daemonDir } from "../lib/mesh/live/daemon/paths.js";
31
31
  import { workMeshRoot } from "../lib/mesh/live/paths.js";
32
32
  import { acquireLockFileExclusively } from "./exclusive-lock.js";
33
33
  import { reportNoninteractivePersonRefreshRejection } from "./cognito-refresh-sentry.js";
34
+ import { pickNewerSession, readKeychainTokens } from "./token-keychain.js";
34
35
  // Re-export the cached-session primitives for callers that must deliberately
35
36
  // avoid the refresh/login behavior in the higher-level helpers below.
36
- export { isExpiring, isMachineIdentity, loadCachedTokens };
37
+ export { isExpiring, isMachineIdentity };
37
38
  /**
38
39
  * Override for where machine-minted Cognito tokens are cached.
39
40
  * Default: the work-mesh daemon state dir (`~/.hq/work-mesh/daemon`), so a
@@ -313,10 +314,9 @@ export function personTokenCacheFile(home = os.homedir(), env = process.env) {
313
314
  export function hasPersonCachedSession(env = process.env) {
314
315
  try {
315
316
  const file = personTokenCacheFile(os.homedir(), env);
316
- if (!fs.existsSync(file))
317
- return false;
318
- const raw = JSON.parse(fs.readFileSync(file, "utf8"));
319
- if (typeof raw.accessToken !== "string" ||
317
+ const raw = readPersonTokenFile(os.homedir(), env);
318
+ if (!raw ||
319
+ typeof raw.accessToken !== "string" ||
320
320
  raw.accessToken.length === 0 ||
321
321
  typeof raw.idToken !== "string" ||
322
322
  raw.idToken.length === 0) {
@@ -474,12 +474,45 @@ function readTokenFile(file) {
474
474
  }
475
475
  }
476
476
  /**
477
- * Person cache as stored on disk for `env`. Does not consult hq-cloud's
477
+ * The person session from the shared macOS Keychain item (see
478
+ * ./token-keychain.ts), or null. Not read when the token file is missing:
479
+ * no file means signed out. Machine-identity and invalidated tokens are
480
+ * refused, the same as the file loader.
481
+ */
482
+ function personKeychainTokens(file, home, env, keychain = {}) {
483
+ if (!fs.existsSync(file))
484
+ return null;
485
+ const tokens = readKeychainTokens({ env, home, ...keychain });
486
+ if (!tokens)
487
+ return null;
488
+ const claims = decodeJwtPayload(tokens.idToken);
489
+ const entityType = claims?.["custom:entityType"];
490
+ if (typeof entityType === "string" && entityType.trim().length > 0)
491
+ return null;
492
+ if (fs.existsSync(`${file}.invalid.${accessTokenFingerprint(tokens.accessToken)}`))
493
+ return null;
494
+ return tokens;
495
+ }
496
+ /**
497
+ * Person session for `env`: the Keychain item and the token file, the newer
498
+ * by `expiresAt` (see ./token-keychain.ts). Does not consult hq-cloud's
478
499
  * in-memory fallback, which can surface a different session when the file
479
500
  * is missing or unreadable.
480
501
  */
481
- export function readPersonTokenFile(home = os.homedir(), env = process.env) {
482
- return readTokenFile(personTokenCacheFile(home, env));
502
+ export function readPersonTokenFile(home = os.homedir(), env = process.env, keychain = {}) {
503
+ const file = personTokenCacheFile(home, env);
504
+ return pickNewerSession(personKeychainTokens(file, home, env, keychain), readTokenFile(file));
505
+ }
506
+ /**
507
+ * The person's cached session: the shared Keychain item and hq-cloud's file
508
+ * loader, the newer by `expiresAt`. Writes still go only to the file in this
509
+ * step.
510
+ */
511
+ export function loadCachedTokens(env = process.env, keychain = {}) {
512
+ const home = os.homedir();
513
+ const file = personTokenCacheFile(home, env);
514
+ const fromFile = loadCachedTokensFromFile();
515
+ return pickNewerSession(personKeychainTokens(file, home, env, keychain), fromFile);
483
516
  }
484
517
  function restoreTokenFile(file, before) {
485
518
  if (before) {
@@ -0,0 +1,96 @@
1
+ /**
2
+ * macOS Keychain read of the person Cognito session (step 1 of moving the
3
+ * session off `~/.hq/cognito-tokens.json`).
4
+ *
5
+ * Shared item (must match hq-desktop-app `token_keychain.rs`, desktop #1586,
6
+ * and hq-cloud `src/token-keychain.ts`):
7
+ * - class: generic password, login keychain
8
+ * - service: `ai.indigo.hq.cognito-tokens`
9
+ * - account: `person`
10
+ * - value: the same camelCase JSON as the token file
11
+ * - signer: Team ID HWFZ2QDTMR (login-keychain partition `teamid:HWFZ2QDTMR`)
12
+ *
13
+ * The read goes through `/usr/bin/security`, so there is no native module.
14
+ * macOS asks the user once whether `security` may read the item; HQ accepts
15
+ * that prompt (owner decision) and explains it first:
16
+ * - Before the first read, a one-line note is printed to stderr. It is only
17
+ * shown, and the first read only happens, in an interactive terminal, so
18
+ * a background process never raises the prompt unexplained.
19
+ * - A denied prompt, a timeout or any other failure falls back to the file
20
+ * quietly and is remembered in `~/.hq/keychain-access.json`, so HQ does
21
+ * not ask again on every run. `hq auth keychain --retry` clears it.
22
+ * - "Item not found" is a plain miss and is not remembered as a denial.
23
+ *
24
+ * Choosing between the Keychain and the file (writes still go to the file):
25
+ * - No file means signed out; the Keychain is not read. Older tools sign
26
+ * out by deleting only the file, and the desktop clears the stale item.
27
+ * - Both valid: the newer session by `expiresAt` wins; a tie keeps the
28
+ * Keychain value. This matches the installer's rule.
29
+ * - Only one valid: that one.
30
+ *
31
+ * Non-macOS, an `HQ_STATE_DIR` override and `HQ_TOKEN_KEYCHAIN=0` read the
32
+ * file only. Token values are never printed or logged.
33
+ */
34
+ export declare const KEYCHAIN_SERVICE = "ai.indigo.hq.cognito-tokens";
35
+ export declare const KEYCHAIN_ACCOUNT = "person";
36
+ export declare const KEYCHAIN_TEAM_ID = "HWFZ2QDTMR";
37
+ /** Read timeout once access has been allowed (no prompt expected). */
38
+ export declare const KEYCHAIN_READ_TIMEOUT_MS = 3000;
39
+ /** First read: long enough for a person to answer the macOS prompt. */
40
+ export declare const KEYCHAIN_FIRST_READ_TIMEOUT_MS = 60000;
41
+ export declare const KEYCHAIN_FIRST_READ_NOTE = "macOS will ask once whether HQ can read your sign-in from the Keychain. Choose Always Allow.";
42
+ export interface KeychainTokenShape {
43
+ accessToken: string;
44
+ idToken: string;
45
+ refreshToken?: string;
46
+ expiresAt?: string | number;
47
+ }
48
+ export interface SecurityResult {
49
+ status: number | null;
50
+ stdout: string;
51
+ timedOut: boolean;
52
+ }
53
+ export type SecurityRunner = (args: string[], timeoutMs: number) => SecurityResult;
54
+ export interface KeychainAccessState {
55
+ noteShownAt?: string;
56
+ allowedAt?: string;
57
+ deniedAt?: string;
58
+ }
59
+ export interface KeychainReadOptions {
60
+ platform?: NodeJS.Platform;
61
+ env?: NodeJS.ProcessEnv;
62
+ home?: string;
63
+ runSecurity?: SecurityRunner;
64
+ /** Defaults to whether stderr is a terminal. */
65
+ interactive?: boolean;
66
+ /** Where the first-read note goes. Defaults to stderr. */
67
+ notify?: (line: string) => void;
68
+ /** Ignore a remembered denial and the read cache (`hq auth keychain --retry`). */
69
+ retry?: boolean;
70
+ now?: () => Date;
71
+ }
72
+ export type KeychainReadOutcome = "hit" | "miss" | "denied" | "skipped-denied" | "skipped-not-interactive" | "disabled";
73
+ export declare function keychainTokenReadEnabled(opts?: KeychainReadOptions): boolean;
74
+ export declare function keychainAccessStateFile(home?: string): string;
75
+ export declare function readKeychainAccessState(home?: string): KeychainAccessState;
76
+ export declare function parseTokenJson(raw: string): KeychainTokenShape | null;
77
+ /** Test seam: forget the in-process read cache. */
78
+ export declare function resetKeychainReadCacheForTests(): void;
79
+ /**
80
+ * Read the shared item, applying the first-read note, the remembered denial
81
+ * and the timeouts described above. Never throws.
82
+ */
83
+ export declare function readKeychainTokensDetailed(opts?: KeychainReadOptions): {
84
+ tokens: KeychainTokenShape | null;
85
+ outcome: KeychainReadOutcome;
86
+ };
87
+ export declare function readKeychainTokens(opts?: KeychainReadOptions): KeychainTokenShape | null;
88
+ /** Newer session by `expiresAt`; a tie keeps the Keychain value. */
89
+ export declare function pickNewerSession<T extends KeychainTokenShape>(fromKeychain: T | null, fromFile: T | null): T | null;
90
+ export type PersonTokenSource = "file" | "keychain" | "none";
91
+ /** The person session from the Keychain and the file, by the rules above. */
92
+ export declare function readPersonTokensWithKeychain(file?: string, opts?: KeychainReadOptions): {
93
+ tokens: KeychainTokenShape | null;
94
+ source: PersonTokenSource;
95
+ };
96
+ //# sourceMappingURL=token-keychain.d.ts.map
@@ -0,0 +1,218 @@
1
+ /**
2
+ * macOS Keychain read of the person Cognito session (step 1 of moving the
3
+ * session off `~/.hq/cognito-tokens.json`).
4
+ *
5
+ * Shared item (must match hq-desktop-app `token_keychain.rs`, desktop #1586,
6
+ * and hq-cloud `src/token-keychain.ts`):
7
+ * - class: generic password, login keychain
8
+ * - service: `ai.indigo.hq.cognito-tokens`
9
+ * - account: `person`
10
+ * - value: the same camelCase JSON as the token file
11
+ * - signer: Team ID HWFZ2QDTMR (login-keychain partition `teamid:HWFZ2QDTMR`)
12
+ *
13
+ * The read goes through `/usr/bin/security`, so there is no native module.
14
+ * macOS asks the user once whether `security` may read the item; HQ accepts
15
+ * that prompt (owner decision) and explains it first:
16
+ * - Before the first read, a one-line note is printed to stderr. It is only
17
+ * shown, and the first read only happens, in an interactive terminal, so
18
+ * a background process never raises the prompt unexplained.
19
+ * - A denied prompt, a timeout or any other failure falls back to the file
20
+ * quietly and is remembered in `~/.hq/keychain-access.json`, so HQ does
21
+ * not ask again on every run. `hq auth keychain --retry` clears it.
22
+ * - "Item not found" is a plain miss and is not remembered as a denial.
23
+ *
24
+ * Choosing between the Keychain and the file (writes still go to the file):
25
+ * - No file means signed out; the Keychain is not read. Older tools sign
26
+ * out by deleting only the file, and the desktop clears the stale item.
27
+ * - Both valid: the newer session by `expiresAt` wins; a tie keeps the
28
+ * Keychain value. This matches the installer's rule.
29
+ * - Only one valid: that one.
30
+ *
31
+ * Non-macOS, an `HQ_STATE_DIR` override and `HQ_TOKEN_KEYCHAIN=0` read the
32
+ * file only. Token values are never printed or logged.
33
+ */
34
+ import { spawnSync } from "node:child_process";
35
+ import * as fs from "node:fs";
36
+ import * as os from "node:os";
37
+ import * as path from "node:path";
38
+ export const KEYCHAIN_SERVICE = "ai.indigo.hq.cognito-tokens";
39
+ export const KEYCHAIN_ACCOUNT = "person";
40
+ export const KEYCHAIN_TEAM_ID = "HWFZ2QDTMR";
41
+ /** Read timeout once access has been allowed (no prompt expected). */
42
+ export const KEYCHAIN_READ_TIMEOUT_MS = 3_000;
43
+ /** First read: long enough for a person to answer the macOS prompt. */
44
+ export const KEYCHAIN_FIRST_READ_TIMEOUT_MS = 60_000;
45
+ /** `security` exit status for errSecItemNotFound. */
46
+ const SECURITY_ITEM_NOT_FOUND = 44;
47
+ /** Reuse a read within one process for this long (avoids a spawn per request). */
48
+ const READ_CACHE_MS = 30_000;
49
+ export const KEYCHAIN_FIRST_READ_NOTE = "macOS will ask once whether HQ can read your sign-in from the Keychain. Choose Always Allow.";
50
+ const defaultRunner = (args, timeoutMs) => {
51
+ try {
52
+ const result = spawnSync("/usr/bin/security", args, {
53
+ encoding: "utf8",
54
+ timeout: timeoutMs,
55
+ stdio: ["ignore", "pipe", "ignore"],
56
+ });
57
+ const timedOut = result.error?.code === "ETIMEDOUT";
58
+ return { status: result.status, stdout: typeof result.stdout === "string" ? result.stdout : "", timedOut };
59
+ }
60
+ catch {
61
+ return { status: null, stdout: "", timedOut: false };
62
+ }
63
+ };
64
+ export function keychainTokenReadEnabled(opts = {}) {
65
+ const platform = opts.platform ?? process.platform;
66
+ const env = opts.env ?? process.env;
67
+ if (platform !== "darwin")
68
+ return false;
69
+ const flag = env.HQ_TOKEN_KEYCHAIN?.trim().toLowerCase();
70
+ if (flag === "0" || flag === "false" || flag === "off")
71
+ return false;
72
+ // Only the default ~/.hq cache is mirrored into the shared item.
73
+ if (env.HQ_STATE_DIR?.trim())
74
+ return false;
75
+ return true;
76
+ }
77
+ export function keychainAccessStateFile(home = os.homedir()) {
78
+ return path.join(home, ".hq", "keychain-access.json");
79
+ }
80
+ export function readKeychainAccessState(home = os.homedir()) {
81
+ try {
82
+ const parsed = JSON.parse(fs.readFileSync(keychainAccessStateFile(home), "utf8"));
83
+ return parsed && typeof parsed === "object" ? parsed : {};
84
+ }
85
+ catch {
86
+ return {};
87
+ }
88
+ }
89
+ function writeKeychainAccessState(home, state) {
90
+ try {
91
+ const file = keychainAccessStateFile(home);
92
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
93
+ const tmp = `${file}.tmp.${process.pid}`;
94
+ fs.writeFileSync(tmp, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
95
+ fs.renameSync(tmp, file);
96
+ }
97
+ catch {
98
+ // Best effort: without the marker HQ may ask again, which is safe.
99
+ }
100
+ }
101
+ /** `security -w` prints hex when the secret has non-printable bytes (newlines). */
102
+ function decodeSecurityOutput(stdout) {
103
+ const trimmed = stdout.replace(/\r?\n$/, "");
104
+ if (!trimmed.startsWith("{") && /^(?:[0-9a-fA-F]{2})+$/.test(trimmed)) {
105
+ return Buffer.from(trimmed, "hex").toString("utf8");
106
+ }
107
+ return trimmed;
108
+ }
109
+ export function parseTokenJson(raw) {
110
+ try {
111
+ const value = JSON.parse(raw);
112
+ if (!value ||
113
+ typeof value.accessToken !== "string" ||
114
+ value.accessToken.length === 0 ||
115
+ typeof value.idToken !== "string" ||
116
+ value.idToken.length === 0) {
117
+ return null;
118
+ }
119
+ return value;
120
+ }
121
+ catch {
122
+ return null;
123
+ }
124
+ }
125
+ let readCache = null;
126
+ /** Test seam: forget the in-process read cache. */
127
+ export function resetKeychainReadCacheForTests() {
128
+ readCache = null;
129
+ }
130
+ /**
131
+ * Read the shared item, applying the first-read note, the remembered denial
132
+ * and the timeouts described above. Never throws.
133
+ */
134
+ export function readKeychainTokensDetailed(opts = {}) {
135
+ if (!keychainTokenReadEnabled(opts))
136
+ return { tokens: null, outcome: "disabled" };
137
+ const home = opts.home ?? os.homedir();
138
+ const now = opts.now ?? (() => new Date());
139
+ if (!opts.retry && readCache && readCache.home === home && now().getTime() - readCache.at < READ_CACHE_MS) {
140
+ return { tokens: readCache.tokens, outcome: readCache.outcome };
141
+ }
142
+ const remember = (tokens, outcome) => {
143
+ readCache = { at: now().getTime(), home, tokens, outcome };
144
+ return { tokens, outcome };
145
+ };
146
+ let state = readKeychainAccessState(home);
147
+ if (opts.retry && state.deniedAt) {
148
+ const { deniedAt: _cleared, ...rest } = state;
149
+ state = rest;
150
+ writeKeychainAccessState(home, state);
151
+ }
152
+ if (state.deniedAt)
153
+ return remember(null, "skipped-denied");
154
+ // Until a read has succeeded, a read may raise the macOS prompt, so it only
155
+ // happens in a terminal, after the note (shown once).
156
+ if (!state.allowedAt) {
157
+ const interactive = opts.interactive ?? Boolean(process.stderr.isTTY);
158
+ if (!interactive)
159
+ return remember(null, "skipped-not-interactive");
160
+ if (!state.noteShownAt) {
161
+ (opts.notify ?? ((line) => process.stderr.write(`${line}\n`)))(KEYCHAIN_FIRST_READ_NOTE);
162
+ state = { ...state, noteShownAt: now().toISOString() };
163
+ writeKeychainAccessState(home, state);
164
+ }
165
+ }
166
+ const run = opts.runSecurity ?? defaultRunner;
167
+ const result = run(["find-generic-password", "-s", KEYCHAIN_SERVICE, "-a", KEYCHAIN_ACCOUNT, "-w"], state.allowedAt ? KEYCHAIN_READ_TIMEOUT_MS : KEYCHAIN_FIRST_READ_TIMEOUT_MS);
168
+ if (result.status === 0 && !result.timedOut) {
169
+ if (!state.allowedAt)
170
+ writeKeychainAccessState(home, { ...state, allowedAt: now().toISOString() });
171
+ return remember(parseTokenJson(decodeSecurityOutput(result.stdout)), "hit");
172
+ }
173
+ if (result.status === SECURITY_ITEM_NOT_FOUND && !result.timedOut)
174
+ return remember(null, "miss");
175
+ // Denied, timed out, or failed: fall back quietly and don't ask again.
176
+ writeKeychainAccessState(home, { ...state, deniedAt: now().toISOString() });
177
+ return remember(null, "denied");
178
+ }
179
+ export function readKeychainTokens(opts = {}) {
180
+ return readKeychainTokensDetailed(opts).tokens;
181
+ }
182
+ function expiresAtMs(tokens) {
183
+ const value = tokens.expiresAt;
184
+ if (typeof value === "number" && Number.isFinite(value))
185
+ return value;
186
+ if (typeof value === "string") {
187
+ const parsed = Date.parse(value);
188
+ if (Number.isFinite(parsed))
189
+ return parsed;
190
+ }
191
+ return Number.NEGATIVE_INFINITY;
192
+ }
193
+ /** Newer session by `expiresAt`; a tie keeps the Keychain value. */
194
+ export function pickNewerSession(fromKeychain, fromFile) {
195
+ if (!fromKeychain)
196
+ return fromFile;
197
+ if (!fromFile)
198
+ return fromKeychain;
199
+ return expiresAtMs(fromFile) > expiresAtMs(fromKeychain) ? fromFile : fromKeychain;
200
+ }
201
+ /** The person session from the Keychain and the file, by the rules above. */
202
+ export function readPersonTokensWithKeychain(file = path.join(os.homedir(), ".hq", "cognito-tokens.json"), opts = {}) {
203
+ if (!fs.existsSync(file))
204
+ return { tokens: null, source: "none" };
205
+ let fromFile;
206
+ try {
207
+ fromFile = parseTokenJson(fs.readFileSync(file, "utf8"));
208
+ }
209
+ catch {
210
+ fromFile = null;
211
+ }
212
+ const fromKeychain = readKeychainTokens(opts);
213
+ const chosen = pickNewerSession(fromKeychain, fromFile);
214
+ if (!chosen)
215
+ return { tokens: null, source: "none" };
216
+ return { tokens: chosen, source: chosen === fromKeychain ? "keychain" : "file" };
217
+ }
218
+ //# sourceMappingURL=token-keychain.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.106",
3
+ "version": "5.345.107",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -48,7 +48,7 @@
48
48
  "@aws-sdk/client-iot-data-plane": "^3.1096.0",
49
49
  "@aws-sdk/client-s3": "^3.1049.0",
50
50
  "@aws-sdk/dsql-signer": "^3.1146.0",
51
- "@indigoai-us/hq-cloud": "~6.18.61",
51
+ "@indigoai-us/hq-cloud": "~6.18.66",
52
52
  "@indigoai-us/hq-flags-client": "^0.1.2",
53
53
  "@modelcontextprotocol/sdk": "1.29.0",
54
54
  "@sentry/node": "^10.49.0",
@@ -1,81 +0,0 @@
1
- #!/usr/bin/env bash
2
- # ontology-audience-grant.sh — grant each scoped audience folder to exactly the
3
- # people in its audience, and prove every grant by readback.
4
- # Spec: core/knowledge/public/hq-core/ontology-local-spec.md
5
- #
6
- # Usage: ontology-audience-grant.sh --company <co> [--dry-run]
7
- #
8
- # For every signals/@{key}/_audience.yaml under companies/<co>/:
9
- # - grants read on signals/@{key}/, ontology/facts/@{key}/, and each
10
- # sources/{channel}/@{key}/ that exists, to each principal that is an
11
- # active company member (non-members are skipped with a warning);
12
- # - one prefix grant per principal per folder, never per file;
13
- # - each grant is the recursive shared glob `<folder>/*` (a bare `<folder>/`
14
- # is a private create-only folder since hq-pro #3662);
15
- # - reads every grant back with `hq files acl --json`; a missing grant exits 3.
16
- # Company-audience paths get @all read on signals/{type}/ and
17
- # ontology/facts/@company/ only. Nothing else under ontology/, signals/,
18
- # sources/ is granted to @all.
19
- # Local-only companies (company.yaml cloud: false or absent cloud_uid) exit 0
20
- # with "local-only, no grants".
21
- set -uo pipefail
22
- HQ="${HQ_BIN:-hq}"
23
- root="${HQ_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}"
24
- co="" dry=0
25
- while [ $# -gt 0 ]; do
26
- case "$1" in --company) co="$2"; shift 2 ;; --dry-run) dry=1; shift ;; *) echo "unknown flag $1" >&2; exit 2 ;; esac
27
- done
28
- [ -n "$co" ] || { echo "usage: ontology-audience-grant.sh --company <co> [--dry-run]" >&2; exit 2; }
29
- C="$root/companies/$co"; [ -d "$C" ] || { echo "no company $co" >&2; exit 2; }
30
-
31
- cloud=no
32
- grep -Eq '^cloud:[[:space:]]*true' "$C/company.yaml" 2>/dev/null && cloud=yes
33
- if [ "$cloud" = no ] && [ -f "$root/companies/manifest.yaml" ]; then
34
- # manifest: companies:\n <co>:\n cloud_uid: ... (two-space company keys)
35
- awk -v co="$co" '/^ [^ #][^:]*:/{inco=($1==co":")} inco && /^ cloud_uid:[[:space:]]*[^[:space:]]/{f=1} END{exit !f}' "$root/companies/manifest.yaml" && cloud=yes
36
- fi
37
- [ "$cloud" = yes ] || { echo "local-only, no grants"; exit 0; }
38
-
39
- members="$("$HQ" members --company "$co" list 2>/dev/null | awk 'NR>1{print tolower($1)}')"
40
- [ -n "$members" ] || { echo "could not list members of $co" >&2; exit 3; }
41
-
42
- granted=0 skipped=0 failed=0
43
- grant() { # <folder/> <principal>
44
- # Grant the recursive shared glob `<folder>/*`, never the bare `<folder>/`:
45
- # since hq-pro #3662 a bare trailing slash is a private create-only folder
46
- # (read on it exposes no children) and ACL reads echo the pattern verbatim.
47
- local p="${1%/}/*" who="$2"
48
- if [ "$dry" = 1 ]; then echo "would grant read $p -> $who"; return 0; fi
49
- "$HQ" files --company "$co" share "$p" --with "$who" --permission read >/dev/null 2>&1 || true
50
- if "$HQ" files --company "$co" acl "$p" --json 2>/dev/null | jq -e --arg w "$who" \
51
- '.direct[]? | select((.granteeId==$w) or ($w=="@all" and .granteeType=="company-wide")) | select(.permission=="read" or .permission=="write" or .permission=="admin")' >/dev/null; then
52
- granted=$((granted+1)); echo "granted read $p -> $who"
53
- else
54
- failed=$((failed+1)); echo "READBACK FAILED: $p -> $who" >&2
55
- fi
56
- }
57
-
58
- for aud in "$C"/signals/@*/_audience.yaml; do
59
- [ -f "$aud" ] || continue
60
- key="$(basename "$(dirname "$aud")")"; key="${key#@}"
61
- principals="$(sed -n 's/^principals:[[:space:]]*\[\(.*\)\]/\1/p' "$aud" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | awk '/@/ { print tolower($0); next } { print }' | grep -v '^$')"
62
- prefixes="signals/@$key/"
63
- [ -d "$C/ontology/facts/@$key" ] && prefixes="$prefixes ontology/facts/@$key/"
64
- for d in "$C"/sources/*/@"$key"; do [ -d "$d" ] && prefixes="$prefixes sources/$(basename "$(dirname "$d")")/@$key/"; done
65
- for who in $principals; do
66
- # prs_/agt_ principals come from the source's own vault ACL (e.g. meeting
67
- # attendees), so they are already company identities; emails must be members.
68
- case "$who" in
69
- prs_*|agt_*) ;;
70
- *) if ! grep -qxF "$who" <<<"$members"; then echo "skip $who (not a member of $co)" >&2; skipped=$((skipped+1)); continue; fi ;;
71
- esac
72
- for p in $prefixes; do grant "$p" "$who"; done
73
- done
74
- done
75
- [ -d "$C/ontology/facts/@company" ] && grant "ontology/facts/@company/" "@all"
76
- for t in action_item commitment decision risk question key_point participant_contribution summary; do
77
- [ -d "$C/signals/$t" ] && grant "signals/$t/" "@all"
78
- done
79
-
80
- echo "audience grants for $co: $granted granted, $skipped skipped, $failed failed"
81
- [ "$failed" -eq 0 ] || exit 3
@@ -1,93 +0,0 @@
1
- #!/usr/bin/env node
2
- // ontology-brief.mjs — render a company brief from the reader's LOCAL tree.
3
- // Spec: core/knowledge/public/hq-core/ontology-local-spec.md
4
- //
5
- // Usage: node core/scripts/ontology-brief.mjs --company <co> [--hq-root <p>] [--days N] [--top N] [--json]
6
- //
7
- // Reads ontology/entities/, every ontology/facts/@*/ and signals/{type}/ +
8
- // signals/@*/{type}/ present locally. Sync pull only delivers what the reader
9
- // may read, so the brief shows exactly their visible facts and signals.
10
- // Writes nothing. Prints markdown (default) or JSON. Exit 0 with an empty brief
11
- // ("no local ontology") when nothing is there.
12
- import fs from "node:fs";
13
- import path from "node:path";
14
-
15
- const TYPES = ["decision", "commitment", "risk", "question", "action_item", "key_point", "participant_contribution", "summary"];
16
- const args = process.argv.slice(2);
17
- const opt = { days: 14, top: 10, json: false };
18
- for (let i = 0; i < args.length; i++) {
19
- const k = args[i];
20
- if (k === "--company") opt.company = args[++i];
21
- else if (k === "--hq-root") opt.root = args[++i];
22
- else if (k === "--days") opt.days = Number(args[++i]);
23
- else if (k === "--top") opt.top = Number(args[++i]);
24
- else if (k === "--json") opt.json = true;
25
- else { console.error(`ontology-brief: unknown flag ${k}`); process.exit(2); }
26
- }
27
- if (!opt.company) { console.error("usage: ontology-brief.mjs --company <co> [--days N] [--top N] [--json]"); process.exit(2); }
28
- opt.root = opt.root || process.env.HQ_ROOT || path.resolve(path.dirname(new URL(import.meta.url).pathname), "../..");
29
- const C = path.join(opt.root, "companies", opt.company);
30
- if (!fs.existsSync(C)) { console.error(`ontology-brief: no company ${opt.company}`); process.exit(2); }
31
-
32
- function fm(file) {
33
- const t = fs.readFileSync(file, "utf8");
34
- const m = t.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
35
- const out = { _body: (m ? m[2] : t).trim() };
36
- if (m) for (const line of m[1].split("\n")) {
37
- const mm = line.match(/^([A-Za-z_]+):\s*(.*)$/);
38
- if (mm) out[mm[1]] = mm[2].trim().replace(/^"(.*)"$/, "$1");
39
- }
40
- return out;
41
- }
42
- const walk = (dir, pred) => {
43
- if (!fs.existsSync(dir)) return [];
44
- const out = [];
45
- for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
46
- const p = path.join(dir, e.name);
47
- if (e.isDirectory()) { if (!e.name.startsWith("_")) out.push(...walk(p, pred)); }
48
- else if (pred(p)) out.push(p);
49
- }
50
- return out;
51
- };
52
-
53
- const entities = walk(path.join(C, "ontology", "entities"), p => p.endsWith(".md")).map(f => fm(f));
54
- const cutoff = Date.now() - opt.days * 86400e3;
55
- const signals = walk(path.join(C, "signals"), p => p.endsWith(".md") && !p.includes(`${path.sep}_`))
56
- .map(f => ({ ...fm(f), _scope: f.includes(`${path.sep}signals${path.sep}@`) ? "scoped" : "company" }))
57
- .filter(s => TYPES.includes(s.type));
58
- const recent = signals.filter(s => !s.created_at || Date.parse(s.created_at) >= cutoff)
59
- .sort((a, b) => (b.created_at || "").localeCompare(a.created_at || ""));
60
- const facts = new Map(); // slug -> [{scope, line}]
61
- for (const f of walk(path.join(C, "ontology", "facts"), p => p.endsWith(".md"))) {
62
- const d = fm(f); const scope = f.includes(`${path.sep}@company${path.sep}`) ? "company" : "scoped";
63
- const slug = d.entity || path.basename(f, ".md");
64
- for (const line of d._body.split("\n").filter(l => l.startsWith("- "))) {
65
- if (!facts.has(slug)) facts.set(slug, []);
66
- facts.get(slug).push({ scope, line: line.slice(2) });
67
- }
68
- }
69
- const hot = entities.map(e => ({ ...e, _facts: facts.get(e.slug) || [] }))
70
- .sort((a, b) => (b._facts.length - a._facts.length) || ((+b.signal_count || 0) - (+a.signal_count || 0)))
71
- .slice(0, opt.top);
72
-
73
- if (opt.json) {
74
- console.log(JSON.stringify({ company: opt.company, entities: entities.length, signals: signals.length,
75
- recent: recent.slice(0, opt.top * 2).map(s => ({ type: s.type, scope: s._scope, text: s.canonical_content || s._body })),
76
- hot: hot.map(e => ({ name: e.canonical_name, type: e.type, facts: e._facts })) }));
77
- process.exit(0);
78
- }
79
- if (!entities.length && !signals.length) { console.log(`# ${opt.company} — no local ontology yet\n\nNothing under ontology/ or signals/ that you can see.`); process.exit(0); }
80
- const out = [`# ${opt.company} — company brief (local)`, "", `${entities.length} entities · ${signals.length} signals you can see · last ${opt.days} days`, "", "## Recent Signals"];
81
- for (const t of TYPES) {
82
- const rows = recent.filter(s => s.type === t).slice(0, 5);
83
- if (!rows.length) continue;
84
- out.push("", `### ${t.replace("_", " ")}`);
85
- for (const s of rows) out.push(`- ${s.canonical_content || s._body} _(${s._scope})_`);
86
- }
87
- out.push("", "## Entities");
88
- for (const e of hot) {
89
- out.push("", `### ${e.canonical_name} — ${e.type}`);
90
- if (!e._facts.length) out.push("- no facts you can see");
91
- for (const f of e._facts.slice(0, 6)) out.push(`- ${f.line} _(${f.scope})_`);
92
- }
93
- console.log(out.join("\n"));