@indigoai-us/hq-cli 5.345.146 → 5.345.147

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.
@@ -1 +1 @@
1
- {"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.82","bundleSha256":"0bf6cd65150094c035cb4ee532c00bde78d030ad0d2132aa606c566f11997f41","formatVersion":1}
1
+ {"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.87","bundleSha256":"916fb28a230f0f0dd83cfaa87366670512898636a137c74a4c989a261301cf5e","formatVersion":1}
@@ -77,6 +77,8 @@ export function renderBotLaunchdPlist(p) {
77
77
  ["HOME", p.home],
78
78
  ["PATH", p.pathEntries.join(":")],
79
79
  ["HQ_NO_UPDATE_CHECK", "1"],
80
+ // Bots must never replace/relaunch the interactive desktop, including during setup.
81
+ ["HQ_NO_ENSURE_HQ_DESKTOP", "1"],
80
82
  ...(p.hqRoot ? [["HQ_ROOT", p.hqRoot]] : []),
81
83
  ]
82
84
  .map(([k, v]) => ` <key>${escapeXml(k)}</key>\n <string>${escapeXml(v)}</string>`)
@@ -135,6 +137,7 @@ RestartSec=5
135
137
  Environment=HOME=${p.home}
136
138
  Environment=PATH=${p.pathEntries.join(":")}
137
139
  Environment=HQ_NO_UPDATE_CHECK=1
140
+ Environment=HQ_NO_ENSURE_HQ_DESKTOP=1
138
141
  ${p.hqRoot ? `Environment=HQ_ROOT=${p.hqRoot}\n` : ""}StandardOutput=append:${p.logPath}
139
142
  StandardError=append:${p.logPath}
140
143
 
@@ -345,7 +345,9 @@ export async function runRuntimeTurn(runtime, input, deps = {}) {
345
345
  const recheck = memo !== undefined && (nowMs() - memo.at >= MODEL_FALLBACK_RECHECK_MS || memo.turns >= MODEL_FALLBACK_RECHECK_TURNS);
346
346
  const remembered = memo && !recheck ? memo : undefined;
347
347
  const args = runtime.buildArgs(remembered ? { ...turnInput, model: remembered.to } : turnInput, { outputFile, newSessionId });
348
- const env = input.env ?? deps.env ?? process.env;
348
+ // Enforce at the model boundary too: old plists and direct `bot run` callers
349
+ // may lack the guard. A bot prompt must never quit the user's desktop app.
350
+ const env = { ...(input.env ?? deps.env ?? process.env), HQ_NO_ENSURE_HQ_DESKTOP: "1" };
349
351
  const binary = runtime.resolveBinary?.(env, input.cwd, turnInput.platform) ?? runtime.binary;
350
352
  const stdin = runtime.stdinInput?.(turnInput);
351
353
  const protocol = runtime.spawnProtocol?.(turnInput);
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Release-shipped links (hq-core mode 120000). An update that junctions a
3
+ * directory at the staging clone, then deletes that clone, leaves a link that
4
+ * does not resolve (feedback_3e1a7445). `hq doctor` reports those and the
5
+ * repair step. A link that resolves somewhere else inside the HQ root is a
6
+ * user customization and is left alone.
7
+ */
8
+ import type { CheckContext, CheckFamily, CheckResult } from "../types.js";
9
+ export declare const RELEASE_LINKS_FAMILY_ID = "release-links";
10
+ export declare const RELEASE_LINKS_FAMILY_TITLE = "Release links";
11
+ export declare const RELEASE_LINKS_CHECK_ID = "release-links.resolve";
12
+ export declare const RELEASE_LINK_REMEDIATION = "Run `hq rescue -y` again. On Windows without Developer Mode, a directory link is written as a junction and a file link as a copy.";
13
+ interface ReleaseLink {
14
+ rel: string;
15
+ linkText: string;
16
+ kind: "dir" | "file";
17
+ }
18
+ /** Paths git records as symlinks in the hq-core release. */
19
+ export declare const RELEASE_LINKS: readonly ReleaseLink[];
20
+ export declare const releaseLinksFamily: CheckFamily;
21
+ export declare function checkReleaseLinks(context: CheckContext): CheckResult[];
22
+ export {};
23
+ //# sourceMappingURL=release-links.d.ts.map
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Release-shipped links (hq-core mode 120000). An update that junctions a
3
+ * directory at the staging clone, then deletes that clone, leaves a link that
4
+ * does not resolve (feedback_3e1a7445). `hq doctor` reports those and the
5
+ * repair step. A link that resolves somewhere else inside the HQ root is a
6
+ * user customization and is left alone.
7
+ */
8
+ import * as fs from "node:fs";
9
+ import * as path from "node:path";
10
+ export const RELEASE_LINKS_FAMILY_ID = "release-links";
11
+ export const RELEASE_LINKS_FAMILY_TITLE = "Release links";
12
+ export const RELEASE_LINKS_CHECK_ID = "release-links.resolve";
13
+ export const RELEASE_LINK_REMEDIATION = "Run `hq rescue -y` again. On Windows without Developer Mode, a directory link is written as a junction and a file link as a copy.";
14
+ const CYGWIN_SYMLINK_COOKIE = Buffer.from("!<symlink>", "latin1");
15
+ /** Paths git records as symlinks in the hq-core release. */
16
+ export const RELEASE_LINKS = [
17
+ { rel: ".agents/skills", linkText: "../.claude/skills", kind: "dir" },
18
+ { rel: ".codex/claude", linkText: "../.claude", kind: "dir" },
19
+ {
20
+ rel: ".codex/output-style.md",
21
+ linkText: "../.claude/output-styles/hq.md",
22
+ kind: "file",
23
+ },
24
+ { rel: "AGENTS.md", linkText: ".claude/CLAUDE.md", kind: "file" },
25
+ {
26
+ rel: "companies/_template/.obsidian",
27
+ linkText: "../../.obsidian",
28
+ kind: "dir",
29
+ },
30
+ ];
31
+ export const releaseLinksFamily = {
32
+ id: RELEASE_LINKS_FAMILY_ID,
33
+ title: RELEASE_LINKS_FAMILY_TITLE,
34
+ run: (context) => Promise.resolve(checkReleaseLinks(context)),
35
+ };
36
+ export function checkReleaseLinks(context) {
37
+ const present = RELEASE_LINKS.filter((entry) => lstatOrNull(path.join(context.hqRoot, entry.rel)) !== null);
38
+ if (present.length === 0) {
39
+ return [
40
+ {
41
+ status: "NA",
42
+ checkId: RELEASE_LINKS_CHECK_ID,
43
+ target: context.hqRoot,
44
+ message: "Skipped: this HQ root has none of the release-shipped links.",
45
+ },
46
+ ];
47
+ }
48
+ const failures = [];
49
+ for (const entry of present) {
50
+ const problem = releaseLinkProblem(context.hqRoot, entry);
51
+ if (!problem)
52
+ continue;
53
+ failures.push({
54
+ status: "FAIL",
55
+ checkId: RELEASE_LINKS_CHECK_ID,
56
+ target: entry.rel,
57
+ message: `${entry.rel} ${problem}`,
58
+ remediation: RELEASE_LINK_REMEDIATION,
59
+ });
60
+ }
61
+ if (failures.length > 0)
62
+ return failures;
63
+ return [
64
+ {
65
+ status: "PASS",
66
+ checkId: RELEASE_LINKS_CHECK_ID,
67
+ target: context.hqRoot,
68
+ message: "Release links resolve inside this HQ root.",
69
+ },
70
+ ];
71
+ }
72
+ function releaseLinkProblem(hqRoot, entry) {
73
+ const destination = path.join(hqRoot, entry.rel);
74
+ const lst = lstatOrNull(destination);
75
+ if (!lst)
76
+ return null;
77
+ if (lst.isSymbolicLink()) {
78
+ let resolved;
79
+ try {
80
+ resolved = fs.realpathSync.native(destination);
81
+ }
82
+ catch {
83
+ return "does not resolve. Its target is missing, often because an update linked it at a staging folder that was then deleted.";
84
+ }
85
+ if (!pathIsInside(hqRoot, resolved)) {
86
+ return "points outside this HQ root, so it does not resolve to the shipped target.";
87
+ }
88
+ return null;
89
+ }
90
+ if (lst.isFile()) {
91
+ const bytes = readFileOrNull(destination);
92
+ if (bytes && isSymlinkStub(bytes, entry.linkText)) {
93
+ return "is still a Git symlink stub, not a working link.";
94
+ }
95
+ if (entry.kind === "dir") {
96
+ return "is a file, not a directory link.";
97
+ }
98
+ const expected = containedTarget(hqRoot, entry);
99
+ if (expected) {
100
+ const target = readFileOrNull(expected);
101
+ if (target && bytes && bytes.equals(target))
102
+ return null;
103
+ }
104
+ // A regular file that is not the stub is a user edit or a Windows copy
105
+ // of the target. Either way the path is usable.
106
+ return null;
107
+ }
108
+ // A real directory in place of a directory link is a user replacement.
109
+ return null;
110
+ }
111
+ function containedTarget(hqRoot, entry) {
112
+ const destination = path.join(hqRoot, entry.rel);
113
+ const resolved = path.resolve(path.dirname(destination), entry.linkText);
114
+ return pathIsInside(hqRoot, resolved) ? resolved : null;
115
+ }
116
+ function isSymlinkStub(bytes, linkText) {
117
+ if (bytes.equals(Buffer.from(linkText, "utf8")))
118
+ return true;
119
+ return (bytes.length >= CYGWIN_SYMLINK_COOKIE.length &&
120
+ bytes.subarray(0, CYGWIN_SYMLINK_COOKIE.length).equals(CYGWIN_SYMLINK_COOKIE));
121
+ }
122
+ function pathIsInside(parent, child) {
123
+ const parentAbs = path.resolve(parent);
124
+ const childAbs = path.resolve(child);
125
+ if (childAbs === parentAbs || childAbs.startsWith(parentAbs + path.sep))
126
+ return true;
127
+ try {
128
+ const parentReal = fs.realpathSync.native(parentAbs);
129
+ const childReal = fs.realpathSync.native(childAbs);
130
+ return childReal === parentReal || childReal.startsWith(parentReal + path.sep);
131
+ }
132
+ catch {
133
+ return false;
134
+ }
135
+ }
136
+ function lstatOrNull(target) {
137
+ try {
138
+ return fs.lstatSync(target);
139
+ }
140
+ catch {
141
+ return null;
142
+ }
143
+ }
144
+ function readFileOrNull(target) {
145
+ try {
146
+ return fs.readFileSync(target);
147
+ }
148
+ catch {
149
+ return null;
150
+ }
151
+ }
152
+ //# sourceMappingURL=release-links.js.map
@@ -26,6 +26,7 @@ import { botsFamily } from "./checks/bots.js";
26
26
  import { anywhereFamily } from "./checks/anywhere.js";
27
27
  import { scaffoldCliFloorFamily } from "./checks/scaffold-cli-floor.js";
28
28
  import { strandedLanesFamily } from "./checks/stranded-lanes.js";
29
+ import { releaseLinksFamily } from "./checks/release-links.js";
29
30
  import { fixtureCoverageFamily } from "./fixtures/discover.js";
30
31
  import { checkClaudeWiring } from "./checks/claude-wiring.js";
31
32
  /**
@@ -270,6 +271,8 @@ export function createDefaultRegistry() {
270
271
  // Failed keep-alive lead lanes cannot recover themselves; make them visible
271
272
  // in the standard local doctor report with the senior recovery command.
272
273
  registry.register(strandedLanesFamily);
274
+ // Release symlinks left pointing at a deleted update staging folder.
275
+ registry.register(releaseLinksFamily);
273
276
  return registry;
274
277
  }
275
278
  //# sourceMappingURL=registry.js.map
@@ -23,6 +23,12 @@ export interface GenerateWorkersRegistryResult {
23
23
  * own, and because duplicates resolve by path order — a tenant path sorts before
24
24
  * both `core` and `personal` — the checkout wins. The operator's actual worker
25
25
  * then silently vanishes from the registry every skill reads.
26
+ *
27
+ * Claude Code worktrees (`.claude/worktrees/<name>/`) and Codex worktrees
28
+ * (`.codex/worktrees/<name>/`) are the same trap in a different folder: a
29
+ * nested HQ root under a company project. Those copies land in the registry as
30
+ * `companies/<slug>/…/.claude/worktrees/…` or `.codex/worktrees/…` paths, so
31
+ * `hq lanes create` then fails with `worker_tenancy_unresolved`.
26
32
  */
27
33
  export declare function isExcludedWorkerDirectory(relativeDirectory: string): boolean;
28
34
  /**
@@ -157,9 +157,17 @@ function withoutTimestamp(content) {
157
157
  * own, and because duplicates resolve by path order — a tenant path sorts before
158
158
  * both `core` and `personal` — the checkout wins. The operator's actual worker
159
159
  * then silently vanishes from the registry every skill reads.
160
+ *
161
+ * Claude Code worktrees (`.claude/worktrees/<name>/`) and Codex worktrees
162
+ * (`.codex/worktrees/<name>/`) are the same trap in a different folder: a
163
+ * nested HQ root under a company project. Those copies land in the registry as
164
+ * `companies/<slug>/…/.claude/worktrees/…` or `.codex/worktrees/…` paths, so
165
+ * `hq lanes create` then fails with `worker_tenancy_unresolved`.
160
166
  */
161
167
  export function isExcludedWorkerDirectory(relativeDirectory) {
162
168
  const normalized = relativeDirectory.replaceAll('\\', '/');
169
+ if (/(?:^|\/)\.(?:claude|codex)\/worktrees(?:\/|$)/.test(normalized))
170
+ return true;
163
171
  const [root, tenant, nested] = normalized.split('/');
164
172
  if (root !== 'companies')
165
173
  return /(?:^|\/)_overrides(?:\/|$)/.test(normalized);
@@ -0,0 +1,2 @@
1
+ export declare function isHeadless(): boolean;
2
+ //# sourceMappingURL=headless.d.ts.map
@@ -0,0 +1,8 @@
1
+ export function isHeadless() {
2
+ if (process.env.CI)
3
+ return true;
4
+ if (process.platform !== "linux")
5
+ return false;
6
+ return !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY;
7
+ }
8
+ //# sourceMappingURL=headless.js.map
@@ -0,0 +1,4 @@
1
+ import type { CognitoAuthConfig, CognitoTokens } from "@indigoai-us/hq-cloud";
2
+ /** Keep desktop login unchanged; manual login uses the same PKCE loopback flow. */
3
+ export declare function loginWithBrowserPreference(config: CognitoAuthConfig, browser?: boolean): Promise<CognitoTokens>;
4
+ //# sourceMappingURL=login-browser.d.ts.map
@@ -0,0 +1,122 @@
1
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
2
+ import { createServer } from "node:http";
3
+ import { browserLogin, saveCachedTokens, CognitoAuthError } from "@indigoai-us/hq-cloud";
4
+ import { isHeadless } from "./headless.js";
5
+ function assertHumanLoginAllowed() {
6
+ // Preserve hq-cloud's company-machine boundary on the manual URL path.
7
+ if (["1", "true", "yes"].includes(process.env.HQ_MACHINE_AUTH_REQUIRED?.trim().toLowerCase() ?? "")) {
8
+ throw new CognitoAuthError("Human authentication is unavailable in required company machine mode");
9
+ }
10
+ }
11
+ /** Keep desktop login unchanged; manual login uses the same PKCE loopback flow. */
12
+ export async function loginWithBrowserPreference(config, browser = true) {
13
+ if (browser && !isHeadless())
14
+ return browserLogin(config);
15
+ assertHumanLoginAllowed();
16
+ const port = config.port ?? 3000;
17
+ const redirectUri = `http://localhost:${port}/callback`;
18
+ const base = `https://${config.userPoolDomain}.auth.${config.region}.amazoncognito.com`;
19
+ const verifier = randomBytes(32).toString("base64url");
20
+ const state = randomBytes(16).toString("base64url");
21
+ const url = new URL(`${base}/oauth2/authorize`);
22
+ url.search = new URLSearchParams({
23
+ client_id: config.clientId,
24
+ response_type: "code",
25
+ scope: (config.scopes ?? ["openid", "email", "profile"]).join(" "),
26
+ redirect_uri: redirectUri,
27
+ code_challenge: createHash("sha256").update(verifier).digest("base64url"),
28
+ code_challenge_method: "S256",
29
+ state,
30
+ }).toString();
31
+ if (config.identityProvider)
32
+ url.searchParams.set("identity_provider", config.identityProvider);
33
+ if (config.prompt)
34
+ url.searchParams.set("prompt", config.prompt);
35
+ const code = await new Promise((resolve, reject) => {
36
+ let settled = false;
37
+ const handler = (req, res) => {
38
+ if (settled) {
39
+ res.writeHead(410).end("Sign-in already finished");
40
+ return;
41
+ }
42
+ const callback = new URL(req.url ?? "/", redirectUri);
43
+ if (callback.pathname !== "/callback") {
44
+ res.writeHead(404).end("Not found");
45
+ return;
46
+ }
47
+ const receivedState = Buffer.from(callback.searchParams.get("state") ?? "");
48
+ const expectedState = Buffer.from(state);
49
+ const validState = receivedState.length === expectedState.length
50
+ && timingSafeEqual(receivedState, expectedState);
51
+ const code = callback.searchParams.get("code");
52
+ if (!validState || !code || callback.searchParams.has("error")) {
53
+ res.writeHead(400).end("Sign-in failed. Return to your terminal and try again.");
54
+ finish(new CognitoAuthError(validState ? "Sign-in callback was denied or missing a code" : "Cognito state parameter mismatch"));
55
+ return;
56
+ }
57
+ res.writeHead(200, { "Content-Type": "text/plain" }).end("Sign-in response received. Return to your terminal to finish.");
58
+ finish();
59
+ resolve(code);
60
+ };
61
+ const servers = [createServer(handler), createServer(handler)];
62
+ const timer = setTimeout(() => finish(new CognitoAuthError("Login timed out after 15 minutes")), 15 * 60 * 1000);
63
+ function finish(error) {
64
+ settled = true;
65
+ clearTimeout(timer);
66
+ for (const server of servers)
67
+ server.close();
68
+ if (error)
69
+ reject(error);
70
+ }
71
+ let attempts = 0;
72
+ let listening = 0;
73
+ let lastError;
74
+ const ready = () => {
75
+ if (++attempts !== servers.length)
76
+ return;
77
+ if (!listening) {
78
+ finish(lastError ?? new Error("Cannot bind login callback port"));
79
+ return;
80
+ }
81
+ try {
82
+ assertHumanLoginAllowed();
83
+ console.log(`Open this URL to sign in:\n${url.toString()}`);
84
+ console.log(`Waiting for sign-in. For a remote host, forward local port ${port} to this host's port ${port} before opening the URL.`);
85
+ }
86
+ catch (error) {
87
+ finish(error instanceof Error ? error : new Error("Sign-in unavailable"));
88
+ }
89
+ };
90
+ servers.forEach((server, index) => {
91
+ server.once("error", (error) => { lastError = error; ready(); });
92
+ server.listen(port, index === 0 ? "127.0.0.1" : "::1", () => { listening++; ready(); });
93
+ });
94
+ });
95
+ assertHumanLoginAllowed();
96
+ const response = await fetch(`${base}/oauth2/token`, {
97
+ method: "POST",
98
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
99
+ body: new URLSearchParams({ grant_type: "authorization_code", client_id: config.clientId, code, code_verifier: verifier, redirect_uri: redirectUri }),
100
+ signal: AbortSignal.timeout(30_000),
101
+ });
102
+ // Never echo an OAuth response body, which can contain credentials.
103
+ if (!response.ok)
104
+ throw new CognitoAuthError(`Token exchange failed (${response.status})`);
105
+ const data = await response.json().catch(() => {
106
+ throw new CognitoAuthError("Cognito returned an invalid token response");
107
+ });
108
+ if (!data || typeof data.access_token !== "string" || !data.access_token
109
+ || typeof data.id_token !== "string" || !data.id_token
110
+ || typeof data.refresh_token !== "string" || !data.refresh_token
111
+ || typeof data.expires_in !== "number" || !Number.isFinite(data.expires_in) || data.expires_in <= 0) {
112
+ throw new CognitoAuthError("Cognito returned an incomplete token response");
113
+ }
114
+ const tokens = {
115
+ accessToken: data.access_token, idToken: data.id_token, refreshToken: data.refresh_token,
116
+ expiresAt: Date.now() + data.expires_in * 1000, tokenType: "Bearer",
117
+ };
118
+ assertHumanLoginAllowed();
119
+ saveCachedTokens(tokens);
120
+ return tokens;
121
+ }
122
+ //# sourceMappingURL=login-browser.js.map
@@ -1,15 +1,9 @@
1
+ import { isHeadless } from "./headless.js";
1
2
  import open from "open";
2
3
  import { ensureCognitoToken } from "./cognito-session.js";
3
4
  import { getCompanyUid, vaultApiFetch } from "./vault-api.js";
4
5
  const CHECKOUT_SUCCESS_URL = "https://hq.computer/billing?checkout=success";
5
6
  const CHECKOUT_CANCEL_URL = "https://hq.computer/billing?checkout=cancel";
6
- function isHeadless() {
7
- if (process.env.CI)
8
- return true;
9
- if (process.platform !== "linux")
10
- return false;
11
- return !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY;
12
- }
13
7
  function ownerUpgradeError() {
14
8
  return Object.assign(new Error("Only a company owner can upgrade. Ask a company owner to upgrade."), { expected: true });
15
9
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.146",
3
+ "version": "5.345.147",
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.82",
51
+ "@indigoai-us/hq-cloud": "~6.18.87",
52
52
  "@indigoai-us/hq-flags-client": "^0.1.2",
53
53
  "@modelcontextprotocol/sdk": "1.29.0",
54
54
  "@sentry/node": "^10.49.0",