@indigoai-us/hq-cli 5.345.105 → 5.345.106

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 CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.106] — 2026-10-12
6
+ - On Windows, `hq core` converts Git Bash paths for commands that declare path operands. Direct calls use the same path rules as the former script wrappers.
7
+ - `hq logout` now ends your session on the HQ server as well as on this
8
+ device, so a copied token file stops working. If the server can't be
9
+ reached, logout still signs you out locally and says the old session will
10
+ expire on its own.
11
+
5
12
  ## [5.345.105] — 2026-10-12
6
13
  - Session-start health no longer files leftover company journals that this
7
14
  repair pass did not refresh, including older hooks that only read
@@ -1,6 +1,11 @@
1
1
  /**
2
- * hq logout — clears cached Cognito session
2
+ * hq logout — ends the Cognito session on the server, then clears the cached
3
+ * session on this device.
4
+ *
5
+ * The refresh token is revoked first so a copied token file stops working.
6
+ * Local tokens are always deleted, even when the server can't be reached.
3
7
  */
4
8
  import { Command } from 'commander';
9
+ export declare const REVOKE_FAILED_MESSAGE = "Signed out on this device. Couldn't reach HQ to end the session everywhere; it will expire on its own.";
5
10
  export declare function registerLogoutCommand(program: Command): void;
6
11
  //# sourceMappingURL=logout.d.ts.map
@@ -1,10 +1,17 @@
1
1
  /**
2
- * hq logout — clears cached Cognito session
2
+ * hq logout — ends the Cognito session on the server, then clears the cached
3
+ * session on this device.
4
+ *
5
+ * The refresh token is revoked first so a copied token file stops working.
6
+ * Local tokens are always deleted, even when the server can't be reached.
3
7
  */
4
8
  import chalk from 'chalk';
5
9
  import { clearCachedTokens, loadCachedTokens } from '@indigoai-us/hq-cloud';
6
10
  import { retainUsageAttributionForPerson } from '../lib/work-context/config.js';
7
11
  import { workContextRoot } from '../lib/work-context/paths.js';
12
+ import { DEFAULT_COGNITO } from '../utils/cognito-config.js';
13
+ import { revokeRefreshToken } from '../utils/cognito-revoke.js';
14
+ export const REVOKE_FAILED_MESSAGE = "Signed out on this device. Couldn't reach HQ to end the session everywhere; it will expire on its own.";
8
15
  export function registerLogoutCommand(program) {
9
16
  program
10
17
  .command('logout')
@@ -26,8 +33,17 @@ export function registerLogoutCommand(program) {
26
33
  console.log(chalk.yellow('No cached HQ session'));
27
34
  return;
28
35
  }
36
+ // Machine-minted sessions carry no refresh token; nothing to revoke.
37
+ const revoked = existing.refreshToken
38
+ ? (await revokeRefreshToken(existing.refreshToken, DEFAULT_COGNITO)).ok
39
+ : true;
29
40
  clearCachedTokens();
30
- console.log(chalk.green('Logged out successfully'));
41
+ if (revoked) {
42
+ console.log(chalk.green('Logged out successfully'));
43
+ }
44
+ else {
45
+ console.log(chalk.yellow(REVOKE_FAILED_MESSAGE));
46
+ }
31
47
  }
32
48
  catch (error) {
33
49
  console.error(chalk.red('Logout failed:'), error instanceof Error ? error.message : 'Unknown error');
@@ -49,6 +49,19 @@ export type FastScaffoldScript = {
49
49
  /** Whether the bundled fast-route module closure needs Node's network timeout patch. */
50
50
  requiresNodeNetworkCompatibility?: boolean;
51
51
  };
52
+ /**
53
+ * CLI core-command registry metadata for MSYS path operands. The exact rules
54
+ * were read from hq-core-staging core/scripts/cli-hosted.yaml and
55
+ * lib/forwarder.sh at d8c3e3f0f6f50db72b81970f7868f7beb4c88d0c.
56
+ * Kept in this already-loaded registry module so hook fast-route graphs stay
57
+ * unchanged.
58
+ */
59
+ export type CorePathOperandRule = "all-nonoptions" | "first-nonoption" | `position:${number}` | `options:${string}` | `comma:${string}`;
60
+ export declare const CORE_COMMAND_PATH_OPERANDS: Readonly<Record<string, CorePathOperandRule>>;
61
+ /** True when argv names a command that declares path operands. */
62
+ export declare function coreCommandHasPathOperands(argv: readonly string[]): boolean;
63
+ /** Apply the manifest's operand rule to a copied process.argv vector. */
64
+ export declare function normalizeCorePathOperands(argv: readonly string[], platform?: NodeJS.Platform, convertPath?: (value: string) => string): string[];
52
65
  /** Family kill switches whose owner-approved default is enabled. */
53
66
  export declare const CORE_NATIVE_DEFAULT_ON_FAMILY_FLAGS: readonly ["core-native-session", "core-native-hook-helpers", "core-native-policy", "core-native-utilities", "core-native-jobs", "core-native-ontology", "core-native-handoff", "core-native-delegate"];
54
67
  export declare function isDefaultOnFastFamily(flagKey: string): boolean;
@@ -9,6 +9,142 @@
9
9
  * on every turn's Stop. The entrypoint routes eligible commands before loading
10
10
  * the graph, and this module holds the parser + table that routing needs.
11
11
  */
12
+ export const CORE_COMMAND_PATH_OPERANDS = {
13
+ "audit-log": "comma:--files",
14
+ "codex-skill-bridge": "options:--root,--old-root",
15
+ "git-pack-extension-check": "options:--root",
16
+ "job notify": "options:--hq-root,--log-path",
17
+ "hq-status-summary": "options:--porcelain-file,--session-files-file",
18
+ "ontology-readme-drift": "options:--root",
19
+ "policy age-report": "options:--dir",
20
+ "policy retire": "options:--dir,--report-dir",
21
+ "policy frontmatter": "all-nonoptions",
22
+ "qmd-reindex-after-sync": "first-nonoption",
23
+ "refresh-vault-access": "options:--root",
24
+ "resize-screenshot": "position:0",
25
+ "token-usage-report": "options:--project-dir",
26
+ worktree: "options:--source",
27
+ };
28
+ /** True when argv names a command that declares path operands. */
29
+ export function coreCommandHasPathOperands(argv) {
30
+ if (argv[2] !== "core")
31
+ return false;
32
+ const coreArgs = argv.slice(3);
33
+ if (coreArgs.some((value, index) => (value === "--hq-root" && coreArgs[index + 1]?.startsWith("/")) ||
34
+ (value.startsWith("--hq-root=") && value.slice("--hq-root=".length).startsWith("/")))) {
35
+ return true;
36
+ }
37
+ let commandStart = 3;
38
+ if (argv[commandStart] === "--hq-root")
39
+ commandStart += 2;
40
+ else if (argv[commandStart]?.startsWith("--hq-root="))
41
+ commandStart += 1;
42
+ return Object.keys(CORE_COMMAND_PATH_OPERANDS).some((name) => name.split(" ").every((part, index) => argv[commandStart + index] === part));
43
+ }
44
+ /** Apply the manifest's operand rule to a copied process.argv vector. */
45
+ export function normalizeCorePathOperands(argv, platform = process.platform, convertPath = (value) => value) {
46
+ const normalized = [...argv];
47
+ if (platform !== "win32" || normalized[2] !== "core")
48
+ return normalized;
49
+ let commandStart = 3;
50
+ if (normalized[commandStart] === "--hq-root") {
51
+ const root = normalized[commandStart + 1];
52
+ if (root?.startsWith("/"))
53
+ normalized[commandStart + 1] = convertPath(root);
54
+ commandStart += 2;
55
+ }
56
+ else if (normalized[commandStart]?.startsWith("--hq-root=")) {
57
+ const value = normalized[commandStart].slice("--hq-root=".length);
58
+ if (value.startsWith("/"))
59
+ normalized[commandStart] = `--hq-root=${convertPath(value)}`;
60
+ commandStart += 1;
61
+ }
62
+ // Commander accepts the core group's root override after the subcommand too.
63
+ // Fast parsing defers that form, but normalize it before either route sees argv.
64
+ for (let index = commandStart; index < normalized.length; index += 1) {
65
+ const value = normalized[index];
66
+ if (value === "--")
67
+ break;
68
+ if (value === "--hq-root") {
69
+ index += 1;
70
+ if (normalized[index]?.startsWith("/"))
71
+ normalized[index] = convertPath(normalized[index]);
72
+ }
73
+ else if (value.startsWith("--hq-root=")) {
74
+ const root = value.slice("--hq-root=".length);
75
+ if (root.startsWith("/"))
76
+ normalized[index] = `--hq-root=${convertPath(root)}`;
77
+ }
78
+ }
79
+ const commandName = Object.keys(CORE_COMMAND_PATH_OPERANDS)
80
+ .sort((left, right) => right.split(" ").length - left.split(" ").length)
81
+ .find((name) => name.split(" ").every((part, index) => normalized[commandStart + index] === part));
82
+ if (!commandName)
83
+ return normalized;
84
+ const argsStart = commandStart + commandName.split(" ").length;
85
+ const args = normalized.slice(argsStart);
86
+ const convert = (index) => {
87
+ const value = args[index];
88
+ if (value?.startsWith("/"))
89
+ args[index] = convertPath(value);
90
+ };
91
+ const rule = CORE_COMMAND_PATH_OPERANDS[commandName];
92
+ if (rule === "all-nonoptions") {
93
+ args.forEach((value, index) => { if (!value.startsWith("--"))
94
+ convert(index); });
95
+ }
96
+ else if (rule === "first-nonoption") {
97
+ const index = args.findIndex((value) => !value.startsWith("--"));
98
+ if (index >= 0)
99
+ convert(index);
100
+ }
101
+ else if (rule.startsWith("position:")) {
102
+ convert(Number(rule.slice("position:".length)));
103
+ }
104
+ else if (rule.startsWith("options:")) {
105
+ const options = new Set(rule.slice("options:".length).split(","));
106
+ for (let index = 0; index < args.length; index += 1) {
107
+ const value = args[index];
108
+ if (options.has(value)) {
109
+ index += 1;
110
+ if (index < args.length)
111
+ convert(index);
112
+ }
113
+ else {
114
+ const equalsAt = value.indexOf("=");
115
+ if (equalsAt >= 0 && options.has(value.slice(0, equalsAt)) && value.slice(equalsAt + 1).startsWith("/")) {
116
+ args[index] = `${value.slice(0, equalsAt + 1)}${convertPath(value.slice(equalsAt + 1))}`;
117
+ }
118
+ }
119
+ }
120
+ }
121
+ else if (rule.startsWith("comma:")) {
122
+ const option = rule.slice("comma:".length);
123
+ for (let index = 0; index < args.length; index += 1) {
124
+ if (args[index] === option && index + 1 < args.length) {
125
+ index += 1;
126
+ args[index] = convertCommaPaths(args[index]);
127
+ break;
128
+ }
129
+ const prefix = `${option}=`;
130
+ if (args[index].startsWith(prefix)) {
131
+ const value = args[index].slice(prefix.length);
132
+ if (value.startsWith("/")) {
133
+ args[index] = `${prefix}${convertCommaPaths(value)}`;
134
+ }
135
+ break;
136
+ }
137
+ }
138
+ }
139
+ normalized.splice(argsStart, args.length, ...args);
140
+ return normalized;
141
+ function convertCommaPaths(value) {
142
+ const parts = value.split(",");
143
+ while (parts.at(-1) === "")
144
+ parts.pop();
145
+ return parts.map((part) => part.startsWith("/") ? convertPath(part) : part).join(",");
146
+ }
147
+ }
12
148
  /** Family kill switches whose owner-approved default is enabled. */
13
149
  export const CORE_NATIVE_DEFAULT_ON_FAMILY_FLAGS = [
14
150
  "core-native-session",
package/dist/index.js CHANGED
@@ -6,13 +6,23 @@ import "./node-network-compat-fast.js";
6
6
  import { isVersionRequest, wantsLabelledVersionReport } from "./version-request.js";
7
7
  // Dependency-light: a pure parser + table, no command graph. Safe to load on
8
8
  // every path (including --version) without reintroducing the heavy startup.
9
- import { isCommandHelpRequest, isFastCoreRequest, parseFastCore, parseSecretsEnvFastRequest, } from "./fast-scaffold.js";
9
+ import { isCommandHelpRequest, isFastCoreRequest, parseFastCore, coreCommandHasPathOperands, normalizeCorePathOperands, parseSecretsEnvFastRequest, } from "./fast-scaffold.js";
10
10
  import { CLI_COMMAND_HELP_CATALOG_FLAG } from "./lib/cli-flag-keys.js";
11
11
  // Also dependency-light: registers a process-level rejection boundary; its only
12
12
  // heavy import (main.js) is lazy and reached only when a rejection fires.
13
13
  import { installProcessRejectionBoundary } from "./unhandled-rejection-boundary.js";
14
14
  import { installFastPathStreamErrorHandlers } from "./utils/fast-stream-errors.js";
15
15
  import { fileURLToPath } from "node:url";
16
+ import { createRequire } from "node:module";
17
+ function cygpathMixedPath(value) {
18
+ // Require only a Node builtin: the package supports Node releases before
19
+ // require(ESM) became available, and this runs in the synchronous dispatcher.
20
+ const { spawnSync } = createRequire(import.meta.url)("node:child_process");
21
+ const result = spawnSync("cygpath", ["-m", value], { encoding: "utf8" });
22
+ if (result.error?.code === "ENOENT")
23
+ return value;
24
+ return String(result.stdout ?? "").replace(/\n+$/u, "");
25
+ }
16
26
  // Install BEFORE any dispatch so a floated rejection on ANY path (the command
17
27
  // graph or the fast-core forwarder) becomes a non-zero exit. @sentry/node's
18
28
  // onUnhandledRejection integration defaults to 'warn' — it captures but does
@@ -134,6 +144,10 @@ async function runNativeFastRouteWithRecovery(parsed) {
134
144
  }
135
145
  }
136
146
  function dispatchCliEntry() {
147
+ if (process.platform === "win32" && coreCommandHasPathOperands(process.argv)) {
148
+ // Keep cygpath off non-Windows and non-path command routes.
149
+ process.argv = normalizeCorePathOperands(process.argv, "win32", cygpathMixedPath);
150
+ }
137
151
  const versionRequest = isVersionRequest(process.argv);
138
152
  const helpRequest = !versionRequest && isCommandHelpRequest(process.argv);
139
153
  const parsedFastCore = versionRequest || helpRequest ? null : parseFastCore(process.argv);
@@ -0,0 +1,5 @@
1
+ import type { CognitoAuthConfig } from "@indigoai-us/hq-cloud";
2
+ /** Lightweight Cognito and Vault defaults shared by startup-safe modules. */
3
+ export declare const DEFAULT_COGNITO: CognitoAuthConfig;
4
+ export declare const DEFAULT_VAULT_API_URL: string;
5
+ //# sourceMappingURL=cognito-config.d.ts.map
@@ -0,0 +1,14 @@
1
+ /** Lightweight Cognito and Vault defaults shared by startup-safe modules. */
2
+ export const DEFAULT_COGNITO = {
3
+ region: process.env.AWS_REGION ?? "us-east-1",
4
+ userPoolDomain: process.env.HQ_COGNITO_DOMAIN ?? "vault-indigo-hq-prod",
5
+ clientId: process.env.HQ_COGNITO_CLIENT_ID ?? "7acei2c8v870enheptb1j5foln",
6
+ port: process.env.HQ_COGNITO_CALLBACK_PORT
7
+ ? Number(process.env.HQ_COGNITO_CALLBACK_PORT)
8
+ : 8765,
9
+ identityProvider: process.env.HQ_COGNITO_IDENTITY_PROVIDER !== undefined
10
+ ? process.env.HQ_COGNITO_IDENTITY_PROVIDER || undefined
11
+ : "Google",
12
+ };
13
+ export const DEFAULT_VAULT_API_URL = process.env.HQ_VAULT_API_URL ?? "https://hqapi.hq.computer";
14
+ //# sourceMappingURL=cognito-config.js.map
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Server-side revocation of a Cognito refresh token.
3
+ *
4
+ * `hq logout` calls this before it deletes the local token file so that a
5
+ * copied token file stops working too. Cognito's `/oauth2/revoke` endpoint on
6
+ * the hosted domain (the same domain the CLI already uses for `/oauth2/token`)
7
+ * revokes the refresh token and every access/ID token issued from it. The CLI
8
+ * app client is public (no client secret), so the body carries only the token
9
+ * and the client id.
10
+ *
11
+ * This never throws and never includes the token in its result: callers get a
12
+ * plain outcome they can print.
13
+ */
14
+ export interface CognitoRevokeConfig {
15
+ region: string;
16
+ userPoolDomain: string;
17
+ clientId: string;
18
+ }
19
+ export type RevokeOutcome = {
20
+ ok: true;
21
+ } | {
22
+ ok: false;
23
+ reason: "network" | "timeout" | "rejected";
24
+ status?: number;
25
+ };
26
+ /** Upper bound on the revoke request so logout never hangs on a dead network. */
27
+ export declare const REVOKE_TIMEOUT_MS = 5000;
28
+ export declare function cognitoRevokeEndpoint(config: CognitoRevokeConfig): string;
29
+ export declare function revokeRefreshToken(refreshToken: string, config: CognitoRevokeConfig, opts?: {
30
+ timeoutMs?: number;
31
+ fetchImpl?: typeof fetch;
32
+ }): Promise<RevokeOutcome>;
33
+ //# sourceMappingURL=cognito-revoke.d.ts.map
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Server-side revocation of a Cognito refresh token.
3
+ *
4
+ * `hq logout` calls this before it deletes the local token file so that a
5
+ * copied token file stops working too. Cognito's `/oauth2/revoke` endpoint on
6
+ * the hosted domain (the same domain the CLI already uses for `/oauth2/token`)
7
+ * revokes the refresh token and every access/ID token issued from it. The CLI
8
+ * app client is public (no client secret), so the body carries only the token
9
+ * and the client id.
10
+ *
11
+ * This never throws and never includes the token in its result: callers get a
12
+ * plain outcome they can print.
13
+ */
14
+ /** Upper bound on the revoke request so logout never hangs on a dead network. */
15
+ export const REVOKE_TIMEOUT_MS = 5_000;
16
+ export function cognitoRevokeEndpoint(config) {
17
+ return `https://${config.userPoolDomain}.auth.${config.region}.amazoncognito.com/oauth2/revoke`;
18
+ }
19
+ export async function revokeRefreshToken(refreshToken, config, opts = {}) {
20
+ const fetchImpl = opts.fetchImpl ?? fetch;
21
+ const body = new URLSearchParams({ token: refreshToken, client_id: config.clientId });
22
+ try {
23
+ const response = await fetchImpl(cognitoRevokeEndpoint(config), {
24
+ method: "POST",
25
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
26
+ body: body.toString(),
27
+ signal: AbortSignal.timeout(opts.timeoutMs ?? REVOKE_TIMEOUT_MS),
28
+ });
29
+ // Drain the body so the socket is released; its content is not needed.
30
+ await response.text().catch(() => undefined);
31
+ if (response.ok)
32
+ return { ok: true };
33
+ return { ok: false, reason: "rejected", status: response.status };
34
+ }
35
+ catch (error) {
36
+ const name = error instanceof Error ? error.name : "";
37
+ if (name === "TimeoutError" || name === "AbortError")
38
+ return { ok: false, reason: "timeout" };
39
+ return { ok: false, reason: "network" };
40
+ }
41
+ }
42
+ //# sourceMappingURL=cognito-revoke.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.105",
3
+ "version": "5.345.106",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {