@higherdev/cli 0.1.1 → 0.1.2

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 (3) hide show
  1. package/README.md +26 -12
  2. package/dist/index.js +775 -631
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1216,8 +1216,8 @@ function toolDetail(payload) {
1216
1216
  const command = str(input.command || input.cmd);
1217
1217
  const additions = Number(input.additions ?? input.added);
1218
1218
  const deletions = Number(input.deletions ?? input.removed);
1219
- const stat = Number.isFinite(additions) || Number.isFinite(deletions) ? ` +${Number.isFinite(additions) ? additions : 0}/-${Number.isFinite(deletions) ? deletions : 0}` : "";
1220
- if (file) return `${file}${stat}`;
1219
+ const stat2 = Number.isFinite(additions) || Number.isFinite(deletions) ? ` +${Number.isFinite(additions) ? additions : 0}/-${Number.isFinite(deletions) ? deletions : 0}` : "";
1220
+ if (file) return `${file}${stat2}`;
1221
1221
  if (command) return command.length > 80 ? `${command.slice(0, 77)}...` : command;
1222
1222
  return "";
1223
1223
  }
@@ -1519,6 +1519,119 @@ var init_theme = __esm({
1519
1519
  }
1520
1520
  });
1521
1521
 
1522
+ // src/architect/connection.ts
1523
+ var connection_exports = {};
1524
+ __export(connection_exports, {
1525
+ classifyAuthFile: () => classifyAuthFile,
1526
+ codexConnection: () => codexConnection,
1527
+ codexHome: () => codexHome,
1528
+ connectCodex: () => connectCodex,
1529
+ connectionSummary: () => connectionSummary,
1530
+ howToFix: () => howToFix,
1531
+ onPath: () => onPath
1532
+ });
1533
+ import { execFile, spawn } from "child_process";
1534
+ import { access, readFile as readFile2 } from "fs/promises";
1535
+ import { constants } from "fs";
1536
+ import { homedir as homedir2 } from "os";
1537
+ import path2 from "path";
1538
+ import { promisify } from "util";
1539
+ function codexHome(env = process.env) {
1540
+ return env.CODEX_HOME ?? path2.join(homedir2(), ".codex");
1541
+ }
1542
+ function classifyAuthFile(auth) {
1543
+ if (!auth) return null;
1544
+ const apiKey = typeof auth.OPENAI_API_KEY === "string" ? auth.OPENAI_API_KEY.trim() : "";
1545
+ if (apiKey) {
1546
+ return {
1547
+ state: "api_key",
1548
+ detail: "Codex is signed in with an API key, which bills per token."
1549
+ };
1550
+ }
1551
+ const refresh = auth.tokens?.refresh_token;
1552
+ if (auth.auth_mode === "chatgpt" && typeof refresh === "string" && refresh.length > 0) {
1553
+ const accountId = typeof auth.tokens?.account_id === "string" ? auth.tokens.account_id : null;
1554
+ return { state: "connected", accountId, detail: "Codex is connected to a ChatGPT subscription." };
1555
+ }
1556
+ return { state: "signed_out", detail: "Codex is installed but not signed in." };
1557
+ }
1558
+ async function onPath(bin, env = process.env) {
1559
+ const dirs = (env.PATH ?? "").split(path2.delimiter).filter(Boolean);
1560
+ for (const dir of dirs) {
1561
+ try {
1562
+ await access(path2.join(dir, bin), constants.X_OK);
1563
+ return true;
1564
+ } catch {
1565
+ }
1566
+ }
1567
+ return false;
1568
+ }
1569
+ async function codexConnection(env = process.env) {
1570
+ if (!await onPath("codex")) {
1571
+ return {
1572
+ state: "not_installed",
1573
+ detail: "The Codex CLI is not on PATH. Install it, then run `hd connect`."
1574
+ };
1575
+ }
1576
+ let auth = null;
1577
+ try {
1578
+ auth = JSON.parse(await readFile2(path2.join(codexHome(env), "auth.json"), "utf8"));
1579
+ } catch {
1580
+ auth = null;
1581
+ }
1582
+ const fromFile = classifyAuthFile(auth);
1583
+ if (fromFile) return fromFile;
1584
+ try {
1585
+ const { stdout } = await run("codex", ["login", "status"], { timeout: 2e4 });
1586
+ const text = stdout.trim();
1587
+ if (/chatgpt/i.test(text)) {
1588
+ return { state: "connected", accountId: null, detail: text };
1589
+ }
1590
+ if (/api key/i.test(text)) {
1591
+ return { state: "api_key", detail: text };
1592
+ }
1593
+ return { state: "signed_out", detail: text || "Codex is not signed in." };
1594
+ } catch {
1595
+ return { state: "signed_out", detail: "Codex is installed but not signed in." };
1596
+ }
1597
+ }
1598
+ function connectionSummary(connection) {
1599
+ switch (connection.state) {
1600
+ case "connected":
1601
+ return connection.accountId ? `${connection.detail} Account ${connection.accountId}.` : connection.detail;
1602
+ default:
1603
+ return connection.detail;
1604
+ }
1605
+ }
1606
+ function howToFix(connection) {
1607
+ switch (connection.state) {
1608
+ case "connected":
1609
+ return null;
1610
+ case "not_installed":
1611
+ return "Install the Codex CLI, then run `hd connect`.";
1612
+ case "api_key":
1613
+ return "Run `codex logout` then `hd connect` to sign in with the subscription instead.";
1614
+ default:
1615
+ return "Run `hd connect` to sign in through ChatGPT.";
1616
+ }
1617
+ }
1618
+ async function connectCodex() {
1619
+ const code = await new Promise((resolve, reject) => {
1620
+ const child = spawn("codex", ["login"], { stdio: "inherit" });
1621
+ child.on("error", reject);
1622
+ child.on("exit", (status) => resolve(status ?? 1));
1623
+ });
1624
+ if (code !== 0) throw new Error(`codex login exited ${code}`);
1625
+ return codexConnection();
1626
+ }
1627
+ var run;
1628
+ var init_connection = __esm({
1629
+ "src/architect/connection.ts"() {
1630
+ "use strict";
1631
+ run = promisify(execFile);
1632
+ }
1633
+ });
1634
+
1522
1635
  // src/read/queries.ts
1523
1636
  function unwrap(result) {
1524
1637
  if (result.error) fail(result.error.message);
@@ -1803,53 +1916,289 @@ var init_subscribe = __esm({
1803
1916
  }
1804
1917
  });
1805
1918
 
1806
- // ../runner/src/providers.ts
1807
- import { execFile as execFile2, execFileSync } from "child_process";
1808
- import { promisify as promisify2 } from "util";
1809
- function isOnPath(bin) {
1919
+ // src/architect/chatgpt.ts
1920
+ var chatgpt_exports = {};
1921
+ __export(chatgpt_exports, {
1922
+ ChatGptAuthError: () => ChatGptAuthError,
1923
+ MAX_STEPS: () => MAX_STEPS,
1924
+ getAuth: () => getAuth,
1925
+ mergeRefreshed: () => mergeRefreshed,
1926
+ msUntilExpiry: () => msUntilExpiry,
1927
+ newSessionId: () => newSessionId,
1928
+ runAgent: () => runAgent
1929
+ });
1930
+ import { readFileSync, renameSync, writeFileSync } from "fs";
1931
+ import { randomUUID as randomUUID2 } from "crypto";
1932
+ import path4 from "path";
1933
+ function readAuthFile() {
1934
+ let raw;
1810
1935
  try {
1811
- execFileSync("which", [bin], { stdio: "ignore" });
1812
- return true;
1936
+ raw = readFileSync(authPath(), "utf8");
1813
1937
  } catch {
1814
- return false;
1938
+ throw new ChatGptAuthError(
1939
+ "No ChatGPT session found. Install the Codex CLI (npm i -g @openai/codex), then run `hd connect`."
1940
+ );
1941
+ }
1942
+ try {
1943
+ return JSON.parse(raw);
1944
+ } catch {
1945
+ throw new ChatGptAuthError(`${authPath()} is unreadable. Run \`hd connect\`.`);
1815
1946
  }
1816
1947
  }
1817
- function detectProviders() {
1818
- const found = [];
1819
- for (const [provider, bin] of Object.entries(PROVIDER_BINS)) {
1820
- if (isOnPath(bin)) found.push(provider);
1948
+ function msUntilExpiry(jwt, now = Date.now()) {
1949
+ try {
1950
+ const payload = jwt.split(".")[1];
1951
+ if (!payload) return Number.POSITIVE_INFINITY;
1952
+ const claims = JSON.parse(Buffer.from(payload, "base64").toString("utf8"));
1953
+ if (typeof claims.exp !== "number") return Number.POSITIVE_INFINITY;
1954
+ return claims.exp * 1e3 - now;
1955
+ } catch {
1956
+ return Number.POSITIVE_INFINITY;
1821
1957
  }
1822
- return found;
1823
1958
  }
1824
- async function captureCli(bin, args, timeoutMs = MODEL_DISCOVER_TIMEOUT_MS) {
1959
+ function mergeRefreshed(file, body, at = /* @__PURE__ */ new Date()) {
1960
+ return {
1961
+ ...file,
1962
+ tokens: {
1963
+ ...file.tokens,
1964
+ access_token: body.access_token,
1965
+ // Refreshing rotates this. Dropping the new value would leave both this
1966
+ // CLI and `codex login` holding a token the server has retired.
1967
+ ...body.refresh_token ? { refresh_token: body.refresh_token } : {},
1968
+ ...body.id_token ? { id_token: body.id_token } : {}
1969
+ },
1970
+ last_refresh: at.toISOString()
1971
+ };
1972
+ }
1973
+ async function refreshSession(file) {
1974
+ const refresh = file.tokens?.refresh_token;
1975
+ if (!refresh) throw new ChatGptAuthError("Your ChatGPT session has expired. Run `hd connect`.");
1976
+ let res;
1825
1977
  try {
1826
- const { stdout, stderr } = await execFileAsync(bin, args, {
1827
- encoding: "utf8",
1828
- timeout: timeoutMs,
1829
- maxBuffer: 512 * 1024
1978
+ res = await fetch(TOKEN_ENDPOINT, {
1979
+ method: "POST",
1980
+ headers: { "Content-Type": "application/json" },
1981
+ body: JSON.stringify({
1982
+ grant_type: "refresh_token",
1983
+ refresh_token: refresh,
1984
+ client_id: CLIENT_ID,
1985
+ scope: "openid profile email"
1986
+ })
1830
1987
  });
1831
- return [stdout, stderr].filter(Boolean).join("\n");
1832
- } catch (error) {
1833
- if (error && typeof error === "object") {
1834
- const err = error;
1835
- const text = [err.stdout, err.stderr].filter(Boolean).join("\n").trim();
1836
- if (text) return text;
1988
+ } catch {
1989
+ throw new ChatGptAuthError("Could not reach OpenAI to renew your ChatGPT session.");
1990
+ }
1991
+ if (!res.ok) throw new ChatGptAuthError("Your ChatGPT session could not be renewed. Run `hd connect`.");
1992
+ const body = await res.json();
1993
+ if (!body.access_token) {
1994
+ throw new ChatGptAuthError("Your ChatGPT session could not be renewed. Run `hd connect`.");
1995
+ }
1996
+ const merged = mergeRefreshed(file, body);
1997
+ const target = authPath();
1998
+ const tmp = `${target}.hd.tmp`;
1999
+ try {
2000
+ writeFileSync(tmp, `${JSON.stringify(merged, null, 2)}
2001
+ `, { mode: 384 });
2002
+ renameSync(tmp, target);
2003
+ } catch {
2004
+ }
2005
+ return merged;
2006
+ }
2007
+ async function getAuth(force = false) {
2008
+ let file = readAuthFile();
2009
+ const accessToken = file.tokens?.access_token;
2010
+ const accountId = file.tokens?.account_id;
2011
+ if (!accessToken || !accountId) {
2012
+ throw new ChatGptAuthError("No ChatGPT subscription token found. Run `hd connect`.");
2013
+ }
2014
+ if (force || msUntilExpiry(accessToken) < RENEW_BEFORE_MS) {
2015
+ file = await refreshSession(file);
2016
+ }
2017
+ return {
2018
+ accessToken: file.tokens?.access_token ?? accessToken,
2019
+ accountId: file.tokens?.account_id ?? accountId
2020
+ };
2021
+ }
2022
+ async function streamTurn(opts) {
2023
+ const res = await fetch(ENDPOINT, {
2024
+ method: "POST",
2025
+ signal: opts.signal,
2026
+ headers: {
2027
+ "Content-Type": "application/json",
2028
+ Authorization: `Bearer ${opts.auth.accessToken}`,
2029
+ "chatgpt-account-id": opts.auth.accountId,
2030
+ "OpenAI-Beta": "responses=experimental",
2031
+ originator: "codex_cli_rs",
2032
+ session_id: opts.sessionId,
2033
+ Accept: "text/event-stream"
2034
+ },
2035
+ body: JSON.stringify({
2036
+ model: opts.model,
2037
+ instructions: opts.instructions,
2038
+ input: opts.input,
2039
+ tools: opts.tools.map((tool) => ({
2040
+ type: "function",
2041
+ name: tool.name,
2042
+ description: tool.description,
2043
+ strict: false,
2044
+ parameters: tool.parameters ?? { type: "object", properties: {} }
2045
+ })),
2046
+ tool_choice: "auto",
2047
+ parallel_tool_calls: false,
2048
+ store: false,
2049
+ stream: true
2050
+ })
2051
+ });
2052
+ if (res.status === 401 || res.status === 403) {
2053
+ throw new ChatGptAuthError("ChatGPT rejected the session. Run `hd connect`.");
2054
+ }
2055
+ if (!res.ok) {
2056
+ const body = (await res.text().catch(() => "")).slice(0, 300);
2057
+ throw new Error(`ChatGPT request failed (${res.status}): ${body || res.statusText}`);
2058
+ }
2059
+ if (!res.body) throw new Error("ChatGPT returned no body.");
2060
+ const out2 = { text: "", calls: [], usage: null };
2061
+ const reader = res.body.getReader();
2062
+ const decoder = new TextDecoder();
2063
+ let buf = "";
2064
+ for (; ; ) {
2065
+ const { done, value } = await reader.read();
2066
+ if (done) break;
2067
+ buf += decoder.decode(value, { stream: true });
2068
+ let nl;
2069
+ while ((nl = buf.indexOf("\n")) >= 0) {
2070
+ const line = buf.slice(0, nl).trim();
2071
+ buf = buf.slice(nl + 1);
2072
+ if (!line.startsWith("data:")) continue;
2073
+ const payload = line.slice(5).trim();
2074
+ if (!payload || payload === "[DONE]") continue;
2075
+ let event;
2076
+ try {
2077
+ event = JSON.parse(payload);
2078
+ } catch {
2079
+ continue;
2080
+ }
2081
+ if (event.type === "response.output_text.delta" && event.delta) {
2082
+ out2.text += event.delta;
2083
+ opts.onText?.(event.delta);
2084
+ } else if (event.type === "response.output_item.done" && event.item?.type === "function_call") {
2085
+ out2.calls.push({
2086
+ name: String(event.item.name ?? ""),
2087
+ arguments: String(event.item.arguments ?? "{}"),
2088
+ call_id: String(event.item.call_id ?? "")
2089
+ });
2090
+ } else if (event.type === "response.completed" && event.response?.usage) {
2091
+ out2.usage = event.response.usage;
2092
+ } else if (event.type === "response.failed") {
2093
+ throw new Error(event.response?.error?.message ?? "ChatGPT reported a failed response.");
2094
+ }
1837
2095
  }
1838
- return "";
1839
2096
  }
2097
+ return out2;
1840
2098
  }
1841
- var execFileAsync, PROVIDER_BINS, MODEL_DISCOVER_TIMEOUT_MS;
1842
- var init_providers = __esm({
1843
- "../runner/src/providers.ts"() {
2099
+ async function runAgent(opts) {
2100
+ let auth = await getAuth();
2101
+ const byName = new Map(opts.tools.map((tool) => [tool.name, tool]));
2102
+ const specs = opts.tools.map((tool) => ({
2103
+ name: tool.name,
2104
+ description: tool.description,
2105
+ parameters: tool.parameters
2106
+ }));
2107
+ const input = [];
2108
+ for (const message of opts.history.slice(-20)) {
2109
+ input.push({
2110
+ type: "message",
2111
+ role: message.role,
2112
+ content: [
2113
+ { type: message.role === "user" ? "input_text" : "output_text", text: message.content }
2114
+ ]
2115
+ });
2116
+ }
2117
+ input.push({ type: "message", role: "user", content: [{ type: "input_text", text: opts.prompt }] });
2118
+ let text = "";
2119
+ let usage = null;
2120
+ let renewed = false;
2121
+ let toolCalls = 0;
2122
+ let step = 0;
2123
+ const maxSteps = opts.maxSteps ?? MAX_STEPS;
2124
+ for (; step < maxSteps; step += 1) {
2125
+ let turn;
2126
+ const send = () => streamTurn({
2127
+ auth,
2128
+ sessionId: opts.sessionId,
2129
+ model: opts.model,
2130
+ instructions: opts.instructions,
2131
+ input,
2132
+ tools: specs,
2133
+ onText: (delta) => opts.onEvent?.({ kind: "text", delta }),
2134
+ signal: opts.signal
2135
+ });
2136
+ try {
2137
+ turn = await send();
2138
+ } catch (error) {
2139
+ if (error instanceof ChatGptAuthError && !renewed) {
2140
+ renewed = true;
2141
+ auth = await getAuth(true);
2142
+ turn = await send();
2143
+ } else {
2144
+ throw error;
2145
+ }
2146
+ }
2147
+ if (turn.usage) usage = turn.usage;
2148
+ if (turn.text) text = turn.text;
2149
+ if (turn.calls.length === 0) break;
2150
+ for (const call of turn.calls) {
2151
+ toolCalls += 1;
2152
+ let args = {};
2153
+ try {
2154
+ args = JSON.parse(call.arguments || "{}");
2155
+ } catch {
2156
+ }
2157
+ opts.onEvent?.({ kind: "tool", name: call.name, args });
2158
+ const tool = byName.get(call.name);
2159
+ let output;
2160
+ let ok2 = true;
2161
+ if (!tool) {
2162
+ ok2 = false;
2163
+ output = `Error: no tool named ${call.name}.`;
2164
+ } else {
2165
+ try {
2166
+ const result = await tool.run(args);
2167
+ output = typeof result === "string" ? result : JSON.stringify(result ?? null);
2168
+ if (!output) output = "(no result)";
2169
+ } catch (error) {
2170
+ ok2 = false;
2171
+ output = `Error: ${error instanceof Error ? error.message : String(error)}`;
2172
+ }
2173
+ }
2174
+ opts.onEvent?.({ kind: "tool_result", name: call.name, ok: ok2, detail: output.slice(0, 200) });
2175
+ input.push({
2176
+ type: "function_call",
2177
+ name: call.name,
2178
+ arguments: call.arguments,
2179
+ call_id: call.call_id
2180
+ });
2181
+ input.push({ type: "function_call_output", call_id: call.call_id, output });
2182
+ }
2183
+ }
2184
+ return { text: text.trim(), usage, steps: step + 1, toolCalls };
2185
+ }
2186
+ function newSessionId() {
2187
+ return randomUUID2();
2188
+ }
2189
+ var ChatGptAuthError, ENDPOINT, TOKEN_ENDPOINT, CLIENT_ID, RENEW_BEFORE_MS, authPath, MAX_STEPS;
2190
+ var init_chatgpt = __esm({
2191
+ "src/architect/chatgpt.ts"() {
1844
2192
  "use strict";
1845
- execFileAsync = promisify2(execFile2);
1846
- PROVIDER_BINS = {
1847
- claude: "claude",
1848
- codex: "codex",
1849
- gemini: "agy",
1850
- grok: "grok"
2193
+ init_connection();
2194
+ ChatGptAuthError = class extends Error {
1851
2195
  };
1852
- MODEL_DISCOVER_TIMEOUT_MS = 2e3;
2196
+ ENDPOINT = "https://chatgpt.com/backend-api/codex/responses";
2197
+ TOKEN_ENDPOINT = "https://auth.openai.com/oauth/token";
2198
+ CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
2199
+ RENEW_BEFORE_MS = 60 * 60 * 1e3;
2200
+ authPath = () => path4.join(codexHome(), "auth.json");
2201
+ MAX_STEPS = 16;
1853
2202
  }
1854
2203
  });
1855
2204
 
@@ -2061,6 +2410,56 @@ var init_App = __esm({
2061
2410
  }
2062
2411
  });
2063
2412
 
2413
+ // ../runner/src/providers.ts
2414
+ import { execFile as execFile3, execFileSync } from "child_process";
2415
+ import { promisify as promisify3 } from "util";
2416
+ function isOnPath(bin) {
2417
+ try {
2418
+ execFileSync("which", [bin], { stdio: "ignore" });
2419
+ return true;
2420
+ } catch {
2421
+ return false;
2422
+ }
2423
+ }
2424
+ function detectProviders() {
2425
+ const found = [];
2426
+ for (const [provider, bin] of Object.entries(PROVIDER_BINS)) {
2427
+ if (isOnPath(bin)) found.push(provider);
2428
+ }
2429
+ return found;
2430
+ }
2431
+ async function captureCli(bin, args, timeoutMs = MODEL_DISCOVER_TIMEOUT_MS) {
2432
+ try {
2433
+ const { stdout, stderr } = await execFileAsync(bin, args, {
2434
+ encoding: "utf8",
2435
+ timeout: timeoutMs,
2436
+ maxBuffer: 512 * 1024
2437
+ });
2438
+ return [stdout, stderr].filter(Boolean).join("\n");
2439
+ } catch (error) {
2440
+ if (error && typeof error === "object") {
2441
+ const err = error;
2442
+ const text = [err.stdout, err.stderr].filter(Boolean).join("\n").trim();
2443
+ if (text) return text;
2444
+ }
2445
+ return "";
2446
+ }
2447
+ }
2448
+ var execFileAsync, PROVIDER_BINS, MODEL_DISCOVER_TIMEOUT_MS;
2449
+ var init_providers = __esm({
2450
+ "../runner/src/providers.ts"() {
2451
+ "use strict";
2452
+ execFileAsync = promisify3(execFile3);
2453
+ PROVIDER_BINS = {
2454
+ claude: "claude",
2455
+ codex: "codex",
2456
+ gemini: "agy",
2457
+ grok: "grok"
2458
+ };
2459
+ MODEL_DISCOVER_TIMEOUT_MS = 2e3;
2460
+ }
2461
+ });
2462
+
2064
2463
  // ../runner/src/auth-check.ts
2065
2464
  function firstLine(text) {
2066
2465
  const line = text.split(/\r?\n/).map((row) => row.trim()).find(Boolean);
@@ -2268,8 +2667,8 @@ __export(init_exports, {
2268
2667
  parseInitArgs: () => parseInitArgs
2269
2668
  });
2270
2669
  import { mkdir as mkdir3, readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
2271
- import { homedir as homedir5, hostname, tmpdir as tmpdir2 } from "os";
2272
- import path6 from "path";
2670
+ import { homedir as homedir4, hostname, tmpdir as tmpdir2 } from "os";
2671
+ import path7 from "path";
2273
2672
  import { fileURLToPath } from "url";
2274
2673
  import { execFile as execFile4 } from "child_process";
2275
2674
  import { promisify as promisify4 } from "util";
@@ -2334,7 +2733,7 @@ function xmlEscape(value) {
2334
2733
  }
2335
2734
  function launchdPlist(opts) {
2336
2735
  const label = opts.label ?? LAUNCHD_LABEL;
2337
- const program = path6.join(opts.repoRoot, "deploy/run-runner.sh");
2736
+ const program = path7.join(opts.repoRoot, "deploy/run-runner.sh");
2338
2737
  return `<?xml version="1.0" encoding="UTF-8"?>
2339
2738
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2340
2739
  <plist version="1.0">
@@ -2365,16 +2764,16 @@ function launchdPlist(opts) {
2365
2764
  `;
2366
2765
  }
2367
2766
  function defaultRepoRoot(fromFile = fileURLToPath(import.meta.url)) {
2368
- return path6.resolve(path6.dirname(fromFile), "../../..");
2767
+ return path7.resolve(path7.dirname(fromFile), "../../..");
2369
2768
  }
2370
2769
  async function initHost(flags, deps = {}) {
2371
2770
  const env = deps.env ?? process.env;
2372
2771
  const platform = deps.platform ?? process.platform;
2373
- const home = (deps.homedir ?? homedir5)();
2772
+ const home = (deps.homedir ?? homedir4)();
2374
2773
  const repoRoot = defaultRepoRoot();
2375
2774
  const host = flags.host || env.RUNNER_HOST || (deps.hostname ?? hostname)().replace(/\..*$/, "").toLowerCase();
2376
- const envFile = flags.envFile || path6.join(repoRoot, "apps/runner/.env");
2377
- const worktreeRoot = flags.worktreeRoot || env.WORKTREE_ROOT || path6.join(home, "higherdev-worktrees");
2775
+ const envFile = flags.envFile || path7.join(repoRoot, "apps/runner/.env");
2776
+ const worktreeRoot = flags.worktreeRoot || env.WORKTREE_ROOT || path7.join(home, "higherdev-worktrees");
2378
2777
  const steps = [
2379
2778
  "Write the runner env file",
2380
2779
  "Register the host",
@@ -2400,13 +2799,13 @@ async function initHost(flags, deps = {}) {
2400
2799
  RUNNER_STOP_TIMEOUT_MS: existing.RUNNER_STOP_TIMEOUT_MS || "90000"
2401
2800
  });
2402
2801
  if (!flags.dryRun) {
2403
- await ensureDir(path6.dirname(envFile), { recursive: true });
2802
+ await ensureDir(path7.dirname(envFile), { recursive: true });
2404
2803
  await write(envFile, formatEnvFile(merged), "utf8");
2405
2804
  }
2406
- const plistPath = flags.plistOut || (flags.installLaunchd && !flags.dryRun && platform === "darwin" ? path6.join(home, "Library/LaunchAgents", `${LAUNCHD_LABEL}.plist`) : path6.join((deps.tmpdir ?? tmpdir2)(), "higherdev-runner.plist"));
2407
- const logPath = path6.join(home, "Library/Logs/higherdev-runner.log");
2805
+ const plistPath = flags.plistOut || (flags.installLaunchd && !flags.dryRun && platform === "darwin" ? path7.join(home, "Library/LaunchAgents", `${LAUNCHD_LABEL}.plist`) : path7.join((deps.tmpdir ?? tmpdir2)(), "higherdev-runner.plist"));
2806
+ const logPath = path7.join(home, "Library/Logs/higherdev-runner.log");
2408
2807
  const plist = launchdPlist({ repoRoot, envFile, logPath });
2409
- await ensureDir(path6.dirname(plistPath), { recursive: true });
2808
+ await ensureDir(path7.dirname(plistPath), { recursive: true });
2410
2809
  await write(plistPath, plist, "utf8");
2411
2810
  let launchdInstalled = false;
2412
2811
  if (flags.installLaunchd && !flags.dryRun && platform === "darwin") {
@@ -2720,105 +3119,7 @@ Run \`hd use <slug>\` or pass --workspace.`
2720
3119
  // src/auth.ts
2721
3120
  init_theme();
2722
3121
  init_json();
2723
-
2724
- // src/architect/connection.ts
2725
- import { execFile, spawn } from "child_process";
2726
- import { access, readFile as readFile2 } from "fs/promises";
2727
- import { constants } from "fs";
2728
- import { homedir as homedir2 } from "os";
2729
- import path2 from "path";
2730
- import { promisify } from "util";
2731
- var run = promisify(execFile);
2732
- function codexHome(env = process.env) {
2733
- return env.CODEX_HOME ?? path2.join(homedir2(), ".codex");
2734
- }
2735
- function classifyAuthFile(auth) {
2736
- if (!auth) return null;
2737
- const apiKey = typeof auth.OPENAI_API_KEY === "string" ? auth.OPENAI_API_KEY.trim() : "";
2738
- if (apiKey) {
2739
- return {
2740
- state: "api_key",
2741
- detail: "Codex is signed in with an API key, which bills per token."
2742
- };
2743
- }
2744
- const refresh = auth.tokens?.refresh_token;
2745
- if (auth.auth_mode === "chatgpt" && typeof refresh === "string" && refresh.length > 0) {
2746
- const accountId = typeof auth.tokens?.account_id === "string" ? auth.tokens.account_id : null;
2747
- return { state: "connected", accountId, detail: "Codex is connected to a ChatGPT subscription." };
2748
- }
2749
- return { state: "signed_out", detail: "Codex is installed but not signed in." };
2750
- }
2751
- async function onPath(bin, env = process.env) {
2752
- const dirs = (env.PATH ?? "").split(path2.delimiter).filter(Boolean);
2753
- for (const dir of dirs) {
2754
- try {
2755
- await access(path2.join(dir, bin), constants.X_OK);
2756
- return true;
2757
- } catch {
2758
- }
2759
- }
2760
- return false;
2761
- }
2762
- async function codexConnection(env = process.env) {
2763
- if (!await onPath("codex")) {
2764
- return {
2765
- state: "not_installed",
2766
- detail: "The Codex CLI is not on PATH. Install it, then run `hd connect`."
2767
- };
2768
- }
2769
- let auth = null;
2770
- try {
2771
- auth = JSON.parse(await readFile2(path2.join(codexHome(env), "auth.json"), "utf8"));
2772
- } catch {
2773
- auth = null;
2774
- }
2775
- const fromFile = classifyAuthFile(auth);
2776
- if (fromFile) return fromFile;
2777
- try {
2778
- const { stdout } = await run("codex", ["login", "status"], { timeout: 2e4 });
2779
- const text = stdout.trim();
2780
- if (/chatgpt/i.test(text)) {
2781
- return { state: "connected", accountId: null, detail: text };
2782
- }
2783
- if (/api key/i.test(text)) {
2784
- return { state: "api_key", detail: text };
2785
- }
2786
- return { state: "signed_out", detail: text || "Codex is not signed in." };
2787
- } catch {
2788
- return { state: "signed_out", detail: "Codex is installed but not signed in." };
2789
- }
2790
- }
2791
- function connectionSummary(connection) {
2792
- switch (connection.state) {
2793
- case "connected":
2794
- return connection.accountId ? `${connection.detail} Account ${connection.accountId}.` : connection.detail;
2795
- default:
2796
- return connection.detail;
2797
- }
2798
- }
2799
- function howToFix(connection) {
2800
- switch (connection.state) {
2801
- case "connected":
2802
- return null;
2803
- case "not_installed":
2804
- return "Install the Codex CLI, then run `hd connect`.";
2805
- case "api_key":
2806
- return "Run `codex logout` then `hd connect` to sign in with the subscription instead.";
2807
- default:
2808
- return "Run `hd connect` to sign in through ChatGPT.";
2809
- }
2810
- }
2811
- async function connectCodex() {
2812
- const code = await new Promise((resolve, reject) => {
2813
- const child = spawn("codex", ["login"], { stdio: "inherit" });
2814
- child.on("error", reject);
2815
- child.on("exit", (status) => resolve(status ?? 1));
2816
- });
2817
- if (code !== 0) throw new Error(`codex login exited ${code}`);
2818
- return codexConnection();
2819
- }
2820
-
2821
- // src/auth.ts
3122
+ init_connection();
2822
3123
  var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2823
3124
  var CODE_RE = /^\d{6}$/;
2824
3125
  async function prompt(question) {
@@ -4536,22 +4837,22 @@ function registerAttachCommands(program) {
4536
4837
  if (!contentType) fail(`${name} is not a jpg, png, gif, or webp.`);
4537
4838
  const ext = extensionForContentType(contentType);
4538
4839
  if (!ext) fail(`${name} is not a jpg, png, gif, or webp.`);
4539
- const path7 = attachmentObjectPath(ctx.workspace.id, ticket.id, randomUUID(), ext);
4540
- const { error } = await ctx.db.storage.from(TICKET_IMAGES_BUCKET).upload(path7, bytes, { contentType, upsert: false });
4840
+ const path8 = attachmentObjectPath(ctx.workspace.id, ticket.id, randomUUID(), ext);
4841
+ const { error } = await ctx.db.storage.from(TICKET_IMAGES_BUCKET).upload(path8, bytes, { contentType, upsert: false });
4541
4842
  if (error) fail(`${name}: ${error.message}`);
4542
4843
  const { error: rowError } = await ctx.db.from("attachments").insert({
4543
4844
  workspace_id: ctx.workspace.id,
4544
4845
  ticket_id: ticket.id,
4545
4846
  message_id: null,
4546
- path: path7,
4847
+ path: path8,
4547
4848
  content_type: contentType,
4548
4849
  created_by: "human"
4549
4850
  });
4550
4851
  if (rowError) {
4551
- await ctx.db.storage.from(TICKET_IMAGES_BUCKET).remove([path7]);
4852
+ await ctx.db.storage.from(TICKET_IMAGES_BUCKET).remove([path8]);
4552
4853
  fail(rowError.message);
4553
4854
  }
4554
- uploaded.push(path7);
4855
+ uploaded.push(path8);
4555
4856
  }
4556
4857
  await ctx.audit(ctx.workspace.id, "create", { ticket_key: ticket.key, subject: "attachments" });
4557
4858
  ok(`Attached ${uploaded.length} file${uploaded.length === 1 ? "" : "s"} to ${c.bold(ticket.key)}.`, {
@@ -4577,7 +4878,7 @@ function registerAttachCommands(program) {
4577
4878
  }
4578
4879
 
4579
4880
  // src/architect/commands.ts
4580
- import { createInterface as createInterface3 } from "readline/promises";
4881
+ import { createInterface as createInterface2 } from "readline/promises";
4581
4882
  init_json();
4582
4883
  init_theme();
4583
4884
 
@@ -4587,7 +4888,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4587
4888
  import { z as z3 } from "zod";
4588
4889
 
4589
4890
  // src/version.ts
4590
- var VERSION = "0.1.1";
4891
+ var VERSION = "0.1.2";
4591
4892
 
4592
4893
  // src/architect/tools.ts
4593
4894
  init_src();
@@ -5019,6 +5320,13 @@ var TOOLS = [
5019
5320
  function toolsFor(access2) {
5020
5321
  return access2 === "read" ? TOOLS.filter((tool) => tool.access === "read") : TOOLS;
5021
5322
  }
5323
+ function toolJsonSchema(tool) {
5324
+ const schema = z2.toJSONSchema(tool.schema);
5325
+ delete schema.$schema;
5326
+ if (!schema.type) schema.type = "object";
5327
+ if (!schema.properties) schema.properties = {};
5328
+ return schema;
5329
+ }
5022
5330
  async function toolContext(slug) {
5023
5331
  return requireWorkspace(slug);
5024
5332
  }
@@ -5090,361 +5398,172 @@ async function serveMcp(opts) {
5090
5398
  }
5091
5399
 
5092
5400
  // src/architect/plan.ts
5093
- init_src();
5094
- import { execFile as execFile3 } from "child_process";
5095
- import { mkdir as mkdir2 } from "fs/promises";
5096
- import { homedir as homedir4 } from "os";
5097
- import path5 from "path";
5098
- import { promisify as promisify3 } from "util";
5099
5401
  init_json();
5100
5402
  init_theme();
5101
-
5102
- // src/architect/codex.ts
5103
- import { spawn as spawn4 } from "child_process";
5104
- import { createInterface as createInterface2 } from "readline";
5105
-
5106
- // ../runner/src/adapters/codex.ts
5107
- init_providers();
5403
+ init_chatgpt();
5404
+ init_connection();
5405
+ import { execFile as execFile2 } from "child_process";
5406
+ import { mkdir as mkdir2 } from "fs/promises";
5108
5407
  import { homedir as homedir3 } from "os";
5109
- import path4 from "path";
5110
- import { readFile as readFile6 } from "fs/promises";
5111
-
5112
- // ../runner/src/adapters/final.ts
5113
- var OUTCOMES = /* @__PURE__ */ new Set(["pr_opened", "pr_updated", "blocked", "failed"]);
5114
- function extractFinal(text) {
5115
- const trimmed = text.trim();
5116
- const candidates = [trimmed];
5117
- const fence = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/);
5118
- if (fence?.[1]) candidates.unshift(fence[1].trim());
5119
- const firstBrace = trimmed.indexOf("{");
5120
- const lastBrace = trimmed.lastIndexOf("}");
5121
- if (firstBrace >= 0 && lastBrace > firstBrace) {
5122
- candidates.unshift(trimmed.slice(firstBrace, lastBrace + 1));
5123
- }
5124
- for (const candidate of candidates) {
5125
- try {
5126
- const parsed = JSON.parse(candidate);
5127
- if (parsed && typeof parsed.summary === "string" && parsed.outcome && OUTCOMES.has(parsed.outcome)) {
5128
- return parsed;
5129
- }
5130
- } catch {
5131
- }
5132
- }
5133
- return null;
5134
- }
5135
-
5136
- // ../runner/src/adapters/spawn.ts
5137
- import { spawn as spawn3 } from "child_process";
5138
- import { randomUUID as randomUUID2 } from "crypto";
5408
+ import path6 from "path";
5409
+ import { promisify as promisify2 } from "util";
5139
5410
 
5140
- // ../runner/src/adapters/cost.ts
5141
- function costFromPayload(payload) {
5142
- if (!payload) return null;
5143
- const values = [payload.total_cost_usd, payload.cost_usd, payload.totalCostUsd, payload.costUsd];
5144
- pushUsageCosts(asRecord2(payload.usage), values);
5145
- pushUsageCosts(asRecord2(payload.info), values);
5146
- const result = asRecord2(payload.result);
5147
- if (result) {
5148
- values.push(result.total_cost_usd, result.cost_usd);
5149
- pushUsageCosts(asRecord2(result.usage), values);
5150
- }
5151
- for (const value of values) {
5152
- const n = toNonNegativeNumber(value);
5153
- if (n != null) return n;
5154
- }
5155
- return null;
5156
- }
5157
- function turnsFromPayload(payload) {
5158
- if (!payload) return null;
5159
- const values = [payload.num_turns, payload.numTurns, payload.turns];
5160
- const usage = asRecord2(payload.usage);
5161
- if (usage) values.push(usage.num_turns, usage.numTurns, usage.turns);
5162
- const result = asRecord2(payload.result);
5163
- if (result) values.push(result.num_turns, result.numTurns, result.turns);
5164
- for (const value of values) {
5165
- const n = toNonNegativeInt(value);
5166
- if (n != null) return n;
5167
- }
5168
- return null;
5169
- }
5170
- function pushUsageCosts(usage, values) {
5171
- if (!usage) return;
5172
- values.push(usage.total_cost_usd, usage.cost_usd, usage.totalCostUsd, usage.costUsd);
5173
- }
5174
- function asRecord2(value) {
5175
- return value && typeof value === "object" && !Array.isArray(value) ? value : null;
5176
- }
5177
- function toNonNegativeNumber(value) {
5178
- if (typeof value === "number") return Number.isFinite(value) && value >= 0 ? value : null;
5179
- if (typeof value === "string" && value.trim()) {
5180
- const n = Number(value);
5181
- return Number.isFinite(n) && n >= 0 ? n : null;
5411
+ // src/architect/repo.ts
5412
+ import { readFile as readFile6, readdir, stat } from "fs/promises";
5413
+ import path5 from "path";
5414
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
5415
+ ".git",
5416
+ "node_modules",
5417
+ ".next",
5418
+ "dist",
5419
+ "build",
5420
+ "coverage",
5421
+ ".turbo",
5422
+ ".vercel",
5423
+ ".pnpm-store"
5424
+ ]);
5425
+ var MAX_FILE_BYTES = 2e5;
5426
+ var MAX_LIST = 400;
5427
+ var MAX_MATCHES = 60;
5428
+ function resolveInside(root, candidate) {
5429
+ const full = path5.resolve(root, candidate ?? ".");
5430
+ const rel = path5.relative(root, full);
5431
+ if (rel.startsWith("..") || path5.isAbsolute(rel)) {
5432
+ throw new Error(`${candidate} is outside the checkout.`);
5433
+ }
5434
+ return full;
5435
+ }
5436
+ function isSkipped(name) {
5437
+ return SKIP_DIRS.has(name) || name.startsWith(".DS_Store");
5438
+ }
5439
+ async function walk(root, dir, out2, depth) {
5440
+ if (out2.length >= MAX_LIST || depth > 12) return;
5441
+ let entries;
5442
+ try {
5443
+ entries = await readdir(dir, { withFileTypes: true });
5444
+ } catch {
5445
+ return;
5182
5446
  }
5183
- return null;
5184
- }
5185
- function toNonNegativeInt(value) {
5186
- const n = toNonNegativeNumber(value);
5187
- return n != null && Number.isInteger(n) ? n : null;
5188
- }
5189
-
5190
- // ../runner/src/adapters/spawn.ts
5191
- function spawnCli(opts) {
5192
- let sessionId = opts.sessionId ?? randomUUID2();
5193
- const child = spawn3(opts.bin, opts.args, {
5194
- cwd: opts.cwd,
5195
- env: process.env,
5196
- stdio: ["ignore", "pipe", "pipe"]
5197
- });
5198
- const queue = [];
5199
- let notify = null;
5200
- let closed = false;
5201
- let resultText = "";
5202
- let costUsd = null;
5203
- let turns = null;
5204
- const wait = collectLines(child, (line) => {
5205
- const event = opts.parse(line);
5206
- const sid = event?.payload?.session_id;
5207
- if (typeof sid === "string" && sid) sessionId = sid;
5208
- const thread = event?.payload?.thread_id;
5209
- if (typeof thread === "string" && thread) sessionId = thread;
5210
- const structured = event?.payload?.structured_output;
5211
- if (structured && typeof structured === "object") {
5212
- resultText = JSON.stringify(structured);
5213
- } else if (typeof event?.payload?.result === "string") {
5214
- resultText = event.payload.result;
5215
- }
5216
- const cost = costFromPayload(event?.payload);
5217
- if (cost != null) costUsd = cost;
5218
- const turnCount = turnsFromPayload(event?.payload);
5219
- if (turnCount != null) turns = turnCount;
5220
- queue.push(line);
5221
- notify?.();
5222
- }).then(({ stdout, stderr, exitCode, signal }) => {
5223
- closed = true;
5224
- notify?.();
5225
- return {
5226
- exitCode,
5227
- signal,
5228
- final: extractFinal(resultText || stdout),
5229
- rawText: resultText || stdout,
5230
- stderr,
5231
- costUsd,
5232
- turns
5233
- };
5234
- });
5235
- async function* stream() {
5236
- while (!closed || queue.length > 0) {
5237
- if (queue.length === 0) {
5238
- await new Promise((resolve) => {
5239
- notify = resolve;
5240
- });
5241
- notify = null;
5242
- }
5243
- while (queue.length > 0) {
5244
- yield queue.shift();
5245
- }
5447
+ for (const entry of entries) {
5448
+ if (out2.length >= MAX_LIST) return;
5449
+ if (isSkipped(entry.name)) continue;
5450
+ const full = path5.join(dir, entry.name);
5451
+ if (entry.isDirectory()) {
5452
+ await walk(root, full, out2, depth + 1);
5453
+ } else if (entry.isFile()) {
5454
+ out2.push(path5.relative(root, full));
5246
5455
  }
5247
5456
  }
5248
- return {
5249
- get sessionId() {
5250
- return sessionId;
5251
- },
5252
- stream: stream(),
5253
- kill() {
5254
- child.kill("SIGINT");
5255
- setTimeout(() => {
5256
- if (!child.killed) child.kill("SIGTERM");
5257
- }, 1e4).unref();
5258
- },
5259
- wait: () => wait
5260
- };
5261
5457
  }
5262
- function collectLines(child, onLine) {
5263
- return new Promise((resolve, reject) => {
5264
- let buffer = "";
5265
- let stdout = "";
5266
- let stderr = "";
5267
- child.stdout?.setEncoding("utf8");
5268
- child.stderr?.setEncoding("utf8");
5269
- child.stdout?.on("data", (chunk) => {
5270
- stdout += chunk;
5271
- buffer += chunk;
5272
- const lines = buffer.split("\n");
5273
- buffer = lines.pop() ?? "";
5274
- for (const line of lines) onLine(line);
5275
- });
5276
- child.stderr?.on("data", (chunk) => {
5277
- stderr += chunk;
5278
- });
5279
- child.on("error", reject);
5280
- child.on("close", (exitCode, signal) => {
5281
- if (buffer) onLine(buffer);
5282
- resolve({ stdout, stderr, exitCode, signal });
5283
- });
5284
- });
5458
+ function matchesGlob(rel, glob) {
5459
+ if (!glob) return true;
5460
+ const pattern = glob.split("").map((ch) => {
5461
+ if (ch === "*") return "\0";
5462
+ return /[.+^${}()|[\]\\]/.test(ch) ? `\\${ch}` : ch;
5463
+ }).join("").replace(//g, ".*").replace(//g, "[^/]*");
5464
+ return new RegExp(`^${pattern}$`).test(rel);
5285
5465
  }
5286
-
5287
- // ../runner/src/adapters/codex.ts
5288
- function effortConfig(effort) {
5289
- const level = effort ?? "high";
5290
- return ["-c", `model_reasoning_effort="${level}"`];
5291
- }
5292
- var codexAdapter = {
5293
- name: "codex",
5294
- start({ prompt: prompt2, cwd, model, effort }) {
5295
- return spawnCli({
5296
- bin: "codex",
5297
- args: [
5298
- "exec",
5299
- "--json",
5300
- "--model",
5301
- model,
5302
- ...effortConfig(effort),
5303
- "--dangerously-bypass-approvals-and-sandbox",
5304
- "-C",
5305
- cwd,
5306
- prompt2
5307
- ],
5308
- cwd,
5309
- parse: codexAdapter.parse
5310
- });
5311
- },
5312
- resume({ sessionId, prompt: prompt2, cwd, effort }) {
5313
- return spawnCli({
5314
- bin: "codex",
5315
- args: ["exec", "resume", sessionId, "--json", ...effortConfig(effort), prompt2],
5316
- cwd,
5317
- parse: codexAdapter.parse,
5318
- sessionId
5319
- });
5320
- },
5321
- parse(line) {
5322
- const trimmed = line.trim();
5323
- if (!trimmed) return null;
5324
- if (!trimmed.startsWith("{")) return { type: "text", payload: { text: trimmed } };
5325
- try {
5326
- const event = JSON.parse(trimmed);
5327
- const type = String(event.type ?? event.kind ?? "");
5328
- if (type === "thread.started" || type === "session" || type === "thread_started") {
5329
- const sessionId = typeof event.thread_id === "string" && event.thread_id || typeof event.session_id === "string" && event.session_id || void 0;
5330
- return { type: "status", payload: { phase: "init", session_id: sessionId, thread_id: sessionId } };
5331
- }
5332
- if (type.includes("tool") && (type.includes("call") || type === "tool_use")) {
5333
- return { type: "tool_use", payload: event };
5334
- }
5335
- if (type.includes("tool") && type.includes("result")) {
5336
- return { type: "tool_result", payload: event };
5466
+ function buildRepoTools(root) {
5467
+ if (!root) return [];
5468
+ return [
5469
+ {
5470
+ name: "list_files",
5471
+ description: "List files in the workspace checkout, so you can see the shape of the codebase before reading anything. Optionally filter with a glob such as apps/web/**/*.tsx.",
5472
+ parameters: {
5473
+ type: "object",
5474
+ properties: {
5475
+ dir: { type: "string", description: "Directory relative to the repo root. Defaults to the root." },
5476
+ glob: { type: "string", description: "Optional glob filter, e.g. **/*.ts" }
5477
+ }
5478
+ },
5479
+ async run(args) {
5480
+ const dir = resolveInside(root, String(args.dir ?? "."));
5481
+ const found = [];
5482
+ await walk(root, dir, found, 0);
5483
+ const glob = args.glob ? String(args.glob) : void 0;
5484
+ const filtered = found.filter((rel) => matchesGlob(rel, glob)).sort();
5485
+ return {
5486
+ root: path5.basename(root),
5487
+ count: filtered.length,
5488
+ truncated: found.length >= MAX_LIST,
5489
+ files: filtered
5490
+ };
5337
5491
  }
5338
- if (type === "item.completed" || type === "agent_message" || type === "message") {
5339
- return { type: "text", payload: event };
5492
+ },
5493
+ {
5494
+ name: "read_file",
5495
+ description: "Read a file from the workspace checkout. Large files come back truncated.",
5496
+ parameters: {
5497
+ type: "object",
5498
+ properties: {
5499
+ path: { type: "string", description: "File path relative to the repo root." }
5500
+ },
5501
+ required: ["path"]
5502
+ },
5503
+ async run(args) {
5504
+ const file = resolveInside(root, String(args.path ?? ""));
5505
+ const info = await stat(file).catch(() => null);
5506
+ if (!info?.isFile()) throw new Error(`${args.path} is not a file in the checkout.`);
5507
+ const raw = await readFile6(file, "utf8");
5508
+ const truncated = raw.length > MAX_FILE_BYTES;
5509
+ return {
5510
+ path: path5.relative(root, file),
5511
+ bytes: info.size,
5512
+ truncated,
5513
+ content: truncated ? raw.slice(0, MAX_FILE_BYTES) : raw
5514
+ };
5340
5515
  }
5341
- if (type === "error" || event.error) {
5342
- return { type: "error", payload: event };
5516
+ },
5517
+ {
5518
+ name: "search_code",
5519
+ description: "Search the workspace checkout for a string or regular expression, returning matching lines with their file and line number.",
5520
+ parameters: {
5521
+ type: "object",
5522
+ properties: {
5523
+ query: { type: "string", description: "Text or regular expression to find." },
5524
+ glob: { type: "string", description: "Optional glob to narrow the search, e.g. **/*.ts" }
5525
+ },
5526
+ required: ["query"]
5527
+ },
5528
+ async run(args) {
5529
+ const query = String(args.query ?? "");
5530
+ if (!query) throw new Error("search_code needs a query.");
5531
+ let re;
5532
+ try {
5533
+ re = new RegExp(query, "i");
5534
+ } catch {
5535
+ re = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
5536
+ }
5537
+ const files = [];
5538
+ await walk(root, root, files, 0);
5539
+ const glob = args.glob ? String(args.glob) : void 0;
5540
+ const matches = [];
5541
+ for (const rel of files) {
5542
+ if (matches.length >= MAX_MATCHES) break;
5543
+ if (!matchesGlob(rel, glob)) continue;
5544
+ let content;
5545
+ try {
5546
+ content = await readFile6(path5.join(root, rel), "utf8");
5547
+ } catch {
5548
+ continue;
5549
+ }
5550
+ if (content.length > MAX_FILE_BYTES) content = content.slice(0, MAX_FILE_BYTES);
5551
+ const lines = content.split("\n");
5552
+ for (let i = 0; i < lines.length; i += 1) {
5553
+ if (matches.length >= MAX_MATCHES) break;
5554
+ if (re.test(lines[i])) {
5555
+ matches.push({ path: rel, line: i + 1, text: lines[i].trim().slice(0, 200) });
5556
+ }
5557
+ }
5558
+ }
5559
+ return { count: matches.length, truncated: matches.length >= MAX_MATCHES, matches };
5343
5560
  }
5344
- return { type: "status", payload: event };
5345
- } catch {
5346
- return { type: "text", payload: { text: trimmed } };
5347
5561
  }
5348
- },
5349
- extractFinal
5350
- };
5351
-
5352
- // src/architect/codex.ts
5353
- function mcpEndpoint(entry = process.argv[1], execPath = process.execPath) {
5354
- const mcpArgs = ["mcp"];
5355
- if (entry.endsWith(".ts") || entry.endsWith(".tsx")) {
5356
- return { command: "tsx", args: [entry, ...mcpArgs] };
5357
- }
5358
- return { command: execPath, args: [entry, ...mcpArgs] };
5359
- }
5360
- function mcpConfigArgs(endpoint, extraArgs = []) {
5361
- const args = [...endpoint.args, ...extraArgs];
5362
- return [
5363
- "-c",
5364
- `mcp_servers.hd.command=${JSON.stringify(endpoint.command)}`,
5365
- "-c",
5366
- `mcp_servers.hd.args=${JSON.stringify(args)}`
5367
- ];
5368
- }
5369
- function buildCodexArgs(opts) {
5370
- const shared = [
5371
- "--json",
5372
- "-c",
5373
- `model_reasoning_effort="${opts.effort}"`,
5374
- "-c",
5375
- 'sandbox_mode="read-only"',
5376
- "--skip-git-repo-check",
5377
- ...opts.mcp
5378
5562
  ];
5379
- if (opts.resumeSessionId) {
5380
- return ["exec", "resume", opts.resumeSessionId, ...shared, opts.prompt];
5381
- }
5382
- return ["exec", "--model", opts.model, "--sandbox", "read-only", ...shared, "-C", opts.cwd, opts.prompt];
5383
- }
5384
- function spawnArchitect(opts) {
5385
- const args = buildCodexArgs(opts);
5386
- const child = spawn4("codex", args, {
5387
- cwd: opts.cwd,
5388
- stdio: ["ignore", "pipe", "pipe"],
5389
- env: process.env
5390
- });
5391
- let sessionId = opts.resumeSessionId ?? null;
5392
- const chunks = [];
5393
- let stderr = "";
5394
- child.stderr.on("data", (data) => {
5395
- stderr += data.toString();
5396
- });
5397
- const queue = [];
5398
- let notify = null;
5399
- let finished = false;
5400
- const lines = createInterface2({ input: child.stdout });
5401
- lines.on("line", (line) => {
5402
- chunks.push(line);
5403
- const event = codexAdapter.parse(line);
5404
- if (!event) return;
5405
- const candidate = event.payload.session_id ?? event.payload.thread_id;
5406
- if (!sessionId && typeof candidate === "string") sessionId = candidate;
5407
- queue.push(event);
5408
- notify?.();
5409
- });
5410
- const done = new Promise(
5411
- (resolve) => {
5412
- child.on("close", (code) => {
5413
- finished = true;
5414
- notify?.();
5415
- resolve({
5416
- exitCode: code,
5417
- text: chunks.join("\n") + (stderr ? `
5418
- ${stderr}` : ""),
5419
- sessionId
5420
- });
5421
- });
5422
- child.on("error", () => {
5423
- finished = true;
5424
- notify?.();
5425
- resolve({ exitCode: 1, text: stderr || "codex failed to start", sessionId });
5426
- });
5427
- }
5428
- );
5429
- const events = {
5430
- async *[Symbol.asyncIterator]() {
5431
- while (true) {
5432
- while (queue.length > 0) yield queue.shift();
5433
- if (finished) return;
5434
- await new Promise((resolve) => {
5435
- notify = () => {
5436
- notify = null;
5437
- resolve();
5438
- };
5439
- });
5440
- }
5441
- }
5442
- };
5443
- return { args, events, kill: () => child.kill("SIGINT"), done };
5444
5563
  }
5445
5564
 
5446
5565
  // src/architect/plan.ts
5447
- var run2 = promisify3(execFile3);
5566
+ var run2 = promisify2(execFile2);
5448
5567
  var ARCHITECT_ROLE = "architect";
5449
5568
  async function architectAgent(ctx) {
5450
5569
  const { data, error } = await ctx.db.from("agents").select("*").eq("workspace_id", ctx.workspace.id).eq("role", ARCHITECT_ROLE).maybeSingle();
@@ -5461,14 +5580,12 @@ async function repoCheckout(ctx) {
5461
5580
  const here = process.cwd();
5462
5581
  try {
5463
5582
  const { stdout } = await run2("git", ["remote", "get-url", "origin"], { cwd: here });
5464
- const origin = stdout.trim().toLowerCase();
5465
- const repo = ctx.workspace.repo.toLowerCase();
5466
- if (origin.includes(repo)) return here;
5583
+ if (stdout.trim().toLowerCase().includes(ctx.workspace.repo.toLowerCase())) return here;
5467
5584
  } catch {
5468
5585
  }
5469
- const root = path5.join(homedir4(), ".cache", "higherdev", "architect");
5586
+ const root = path6.join(homedir3(), ".cache", "higherdev", "architect");
5470
5587
  await mkdir2(root, { recursive: true });
5471
- const dir = path5.join(root, ctx.workspace.slug);
5588
+ const dir = path6.join(root, ctx.workspace.slug);
5472
5589
  try {
5473
5590
  await run2("git", ["-C", dir, "fetch", "--depth", "1", "origin", ctx.workspace.default_branch], {
5474
5591
  timeout: 12e4
@@ -5476,6 +5593,7 @@ async function repoCheckout(ctx) {
5476
5593
  await run2("git", ["-C", dir, "reset", "--hard", `origin/${ctx.workspace.default_branch}`], {
5477
5594
  timeout: 6e4
5478
5595
  });
5596
+ return dir;
5479
5597
  } catch {
5480
5598
  try {
5481
5599
  await run2(
@@ -5483,51 +5601,43 @@ async function repoCheckout(ctx) {
5483
5601
  ["repo", "clone", ctx.workspace.repo, dir, "--", "--depth", "1", "--branch", ctx.workspace.default_branch],
5484
5602
  { timeout: 3e5 }
5485
5603
  );
5486
- } catch (error) {
5487
- fail(
5488
- `Could not get a checkout of ${ctx.workspace.repo} (${error instanceof Error ? error.message : String(error)}).
5489
- Run \`hd plan\` from inside the repo, or check \`gh auth status\`.`
5490
- );
5604
+ return dir;
5605
+ } catch {
5606
+ return null;
5491
5607
  }
5492
5608
  }
5493
- return dir;
5494
5609
  }
5495
- function architectPrompt(opts) {
5610
+ function architectInstructions(opts) {
5496
5611
  const { ctx } = opts;
5497
5612
  return [
5498
- "You are the HigherDEV architect for the workspace below. You decide what should be built and",
5499
- "whether the platform is configured to build it. You do not write code: you file the work and the",
5500
- "orchestrator dispatches it to builders.",
5613
+ "You are the HigherDEV architect, running in the owner's terminal.",
5614
+ "You decide what should be built and whether the platform is configured to build it.",
5615
+ "You do not write code: you file the work, and the orchestrator dispatches it to builders.",
5501
5616
  "",
5502
- `Workspace: ${ctx.workspace.slug} (${ctx.workspace.repo}, branch ${ctx.workspace.default_branch})`,
5617
+ `Workspace: ${ctx.workspace.slug} (${ctx.workspace.repo}, branch ${ctx.workspace.default_branch}).`,
5503
5618
  `You are acting for ${ctx.email}.`,
5504
5619
  "",
5505
- "Tools: the `hd` MCP server is attached and is your only way to reach the platform. Call `status`",
5506
- "first, and `list_agents` before you assume who can build what. The repository is checked out at",
5507
- "your working directory, read only, so ground your assessment in the actual code.",
5620
+ "Your tools are the only source of truth about the board. Call status before you assume anything,",
5621
+ "and list_agents before you assume who can build what. Never invent tickets, agents, or state.",
5622
+ opts.hasRepo ? "A checkout of the repository is readable through list_files, read_file, and search_code. Ground any claim about the code in what you actually read." : "No checkout is available, so do not claim anything about the code you have not been told.",
5508
5623
  "",
5509
- "How to hand work over:",
5510
- "- Prefer creating one epic with a well-written spec. `create_epic` triggers the orchestrator,",
5511
- " which decomposes the spec into tickets itself and assigns them. That is the handoff.",
5512
- "- File individual tickets with `create_ticket` only for work that needs no decomposition.",
5624
+ "Handing work over:",
5625
+ "- Prefer one epic with a well-written spec. create_epic triggers the orchestrator, which",
5626
+ " decomposes the spec into tickets itself and assigns them. That is the handoff.",
5627
+ "- Use create_ticket only for work that needs no decomposition.",
5513
5628
  "- A ticket is one PR, one area, under about 400 changed lines, with acceptance criteria and a",
5514
5629
  " test expectation. Two live tickets must not share an area; chain them with blocked_by.",
5515
5630
  "",
5516
- "What only you can do: agent configuration. Model, effort, routing notes, prompt addenda,",
5517
- "conventions, budgets, pausing. The orchestrator has no action for any of it. If the work you are",
5518
- "planning needs a different builder lineup, change it with `update_agent` and say why.",
5631
+ "Only you can change agent configuration: model, effort, routing notes, prompt addenda,",
5632
+ "conventions, budgets, pausing. The orchestrator has no action for any of it.",
5519
5633
  "",
5520
- opts.readOnly ? "This run is read-only. Propose the plan in your reply; do not expect any write tool to exist." : "When you are done, summarise what you filed and what the owner should look at.",
5521
- "Never use an em dash in anything you write.",
5634
+ opts.readOnly ? "This session is read-only. Propose in your reply; the tools that change things are not loaded." : "Say what you filed and what the owner should look at.",
5635
+ "Answer conversationally and briefly. Lead with the answer. Never use an em dash.",
5522
5636
  opts.addendum ? `
5523
- ${opts.addendum}` : "",
5524
- "",
5525
- "---",
5526
- "",
5527
- opts.request
5528
- ].filter((line) => line !== null).join("\n");
5637
+ ${opts.addendum}` : ""
5638
+ ].filter(Boolean).join("\n");
5529
5639
  }
5530
- async function runArchitect(opts) {
5640
+ async function openArchitect(opts) {
5531
5641
  const { ctx } = opts;
5532
5642
  const agent = await architectAgent(ctx);
5533
5643
  const connection = await codexConnection();
@@ -5535,92 +5645,100 @@ async function runArchitect(opts) {
5535
5645
  fail(`${connectionSummary(connection)}
5536
5646
  ${howToFix(connection) ?? ""}`.trim());
5537
5647
  }
5538
- if (connection.state !== "connected") {
5539
- opts.onLine(c.yellow(`${connectionSummary(connection)} Continuing because --allow-api-billing was passed.`));
5540
- }
5541
- const cwd = await repoCheckout(ctx);
5542
- const config = await loadConfig();
5543
- const sessionKey = `architect_session_${ctx.workspace.slug}`;
5544
- const previous = config[sessionKey];
5545
- const resumeSessionId = opts.resume && typeof previous === "string" ? previous : null;
5546
- if (opts.resume && !resumeSessionId) fail("No architect session to resume in this workspace.");
5547
- const endpoint = mcpEndpoint();
5548
- const mcp = mcpConfigArgs(endpoint, [
5549
- ...opts.readOnly ? ["--read-only"] : [],
5550
- "--workspace",
5551
- ctx.workspace.slug
5552
- ]);
5648
+ const repo = await repoCheckout(ctx);
5649
+ const platformTools = toolsFor(opts.readOnly ? "read" : "all").map((tool) => ({
5650
+ name: tool.name,
5651
+ description: tool.description,
5652
+ parameters: toolJsonSchema(tool),
5653
+ run: (args) => tool.run(ctx, args)
5654
+ }));
5655
+ return {
5656
+ agent,
5657
+ instructions: architectInstructions({
5658
+ ctx,
5659
+ addendum: agent.prompt_addendum,
5660
+ readOnly: opts.readOnly,
5661
+ hasRepo: Boolean(repo)
5662
+ }),
5663
+ tools: [...platformTools, ...buildRepoTools(repo)],
5664
+ sessionId: newSessionId(),
5665
+ history: [],
5666
+ repo
5667
+ };
5668
+ }
5669
+ async function architectTurn(opts) {
5670
+ const { ctx, session } = opts;
5553
5671
  const { data: runRow, error: runError } = await ctx.db.from("runs").insert({
5554
5672
  workspace_id: ctx.workspace.id,
5555
5673
  ticket_id: null,
5556
- agent_id: agent.id,
5674
+ agent_id: session.agent.id,
5557
5675
  kind: "architect",
5558
- provider: agent.provider,
5559
- model: agent.model,
5676
+ provider: session.agent.provider,
5677
+ model: session.agent.model,
5560
5678
  status: "running",
5561
5679
  started_at: (/* @__PURE__ */ new Date()).toISOString(),
5680
+ session_id: session.sessionId,
5562
5681
  host: null
5563
5682
  }).select("id").single();
5564
5683
  if (runError) fail(runError.message);
5565
- const prompt2 = architectPrompt({
5566
- ctx,
5567
- request: opts.request,
5568
- addendum: agent.prompt_addendum,
5569
- readOnly: opts.readOnly
5570
- });
5571
- const spawned = spawnArchitect({
5572
- prompt: prompt2,
5573
- cwd,
5574
- model: agent.model,
5575
- effort: agent.effort ?? "high",
5576
- mcp,
5577
- resumeSessionId
5578
- });
5579
5684
  let seq = 0;
5580
- const pending = [];
5685
+ const events = [];
5686
+ const record = (type, payload) => events.push({ run_id: runRow.id, seq: seq++, type, payload });
5581
5687
  const flush = async () => {
5582
- if (pending.length === 0) return;
5583
- const batch = pending.splice(0, pending.length);
5584
- await ctx.db.from("run_events").insert(batch);
5688
+ if (events.length) await ctx.db.from("run_events").insert(events);
5585
5689
  };
5586
- const ticker = setInterval(() => void flush(), 2e3);
5587
- const interrupt = () => spawned.kill();
5588
- process.once("SIGINT", interrupt);
5589
- for await (const event of spawned.events) {
5590
- pending.push({ run_id: runRow.id, seq: seq++, type: event.type, payload: event.payload });
5591
- const [line] = transcriptLines([{ id: String(seq), type: event.type, payload: event.payload }]);
5592
- if (!line) continue;
5593
- if (line.kind === "status" && line.title === "init") continue;
5594
- if (line.kind === "text") opts.onLine(line.title);
5595
- else if (line.kind === "error") opts.onLine(c.red(line.title));
5596
- else opts.onLine(c.dim(` ${line.title}`));
5597
- if (line.body) opts.onLine(c.dim(` ${line.body}`));
5598
- }
5599
- clearInterval(ticker);
5600
- await flush();
5601
- process.off("SIGINT", interrupt);
5602
- const result = await spawned.done;
5603
- await ctx.db.from("runs").update({
5604
- status: result.exitCode === 0 ? "succeeded" : "failed",
5605
- ended_at: (/* @__PURE__ */ new Date()).toISOString(),
5606
- exit_code: result.exitCode,
5607
- session_id: result.sessionId,
5608
- summary: result.exitCode === 0 ? "Architect session" : `Codex exited ${result.exitCode}`
5609
- }).eq("id", runRow.id);
5610
- if (result.sessionId) await saveConfig({ [sessionKey]: result.sessionId });
5611
- await ctx.audit(ctx.workspace.id, "architect", { subject: agent.display_name });
5612
- if (result.exitCode !== 0 && !isJsonMode()) {
5613
- opts.onLine(c.red(`codex exited ${result.exitCode}`));
5614
- }
5615
- return { sessionId: result.sessionId, exitCode: result.exitCode, runId: runRow.id };
5690
+ try {
5691
+ const result = await runAgent({
5692
+ prompt: opts.request,
5693
+ instructions: session.instructions,
5694
+ history: session.history,
5695
+ tools: session.tools,
5696
+ model: session.agent.model,
5697
+ sessionId: session.sessionId,
5698
+ signal: opts.signal,
5699
+ onEvent: (event) => {
5700
+ if (event.kind === "tool") record("tool_use", { name: event.name, input: event.args });
5701
+ else if (event.kind === "tool_result") {
5702
+ record("tool_result", { name: event.name, ok: event.ok, content: event.detail });
5703
+ }
5704
+ opts.onEvent(event);
5705
+ }
5706
+ });
5707
+ if (result.text) record("text", { text: result.text });
5708
+ session.history.push({ role: "user", content: opts.request });
5709
+ session.history.push({ role: "assistant", content: result.text });
5710
+ await flush();
5711
+ await ctx.db.from("runs").update({
5712
+ status: "succeeded",
5713
+ ended_at: (/* @__PURE__ */ new Date()).toISOString(),
5714
+ exit_code: 0,
5715
+ turns: result.steps,
5716
+ summary: result.text.slice(0, 500) || "Architect turn"
5717
+ }).eq("id", runRow.id);
5718
+ await ctx.audit(ctx.workspace.id, "architect", { subject: session.agent.display_name });
5719
+ return { text: result.text, runId: runRow.id, toolCalls: result.toolCalls };
5720
+ } catch (error) {
5721
+ const message = error instanceof Error ? error.message : String(error);
5722
+ record("error", { error: message });
5723
+ await flush();
5724
+ await ctx.db.from("runs").update({
5725
+ status: "failed",
5726
+ ended_at: (/* @__PURE__ */ new Date()).toISOString(),
5727
+ exit_code: 1,
5728
+ summary: message.slice(0, 500)
5729
+ }).eq("id", runRow.id);
5730
+ if (error instanceof ChatGptAuthError) fail(message);
5731
+ throw error;
5732
+ }
5616
5733
  }
5617
5734
 
5618
5735
  // src/architect/commands.ts
5736
+ init_connection();
5619
5737
  function slugOf6(command) {
5620
5738
  return command.optsWithGlobals().workspace;
5621
5739
  }
5622
5740
  async function askLine(prompt2) {
5623
- const rl = createInterface3({ input: process.stdin, output: process.stderr });
5741
+ const rl = createInterface2({ input: process.stdin, output: process.stderr });
5624
5742
  try {
5625
5743
  return await new Promise((resolve) => {
5626
5744
  rl.once("close", () => resolve(""));
@@ -5641,25 +5759,42 @@ ${howToFix(connection) ?? ""}`.trim()
5641
5759
  );
5642
5760
  }
5643
5761
  function registerArchitectCommands(program) {
5644
- program.command("plan").argument("[request...]", "what to plan, assess, or change; omit for a conversation").description("the architect: assess the repo, shape the work, tune the platform").option("--read-only", "let it look and propose, but change nothing").option("--resume", "continue the last architect session in this workspace").option("--once", "run a single turn and exit, even without a request").option("--allow-api-billing", "run even when Codex would bill per token instead of the subscription").action(async function(request) {
5762
+ program.command("plan").argument("[request...]", "what to plan, assess, or change; omit for a conversation").description("the architect: assess the repo, shape the work, tune the platform").option("--read-only", "let it look and propose, but change nothing").option("--once", "run a single turn and exit, even without a request").option("--allow-api-billing", "run even when Codex would bill per token instead of the subscription").action(async function(request) {
5645
5763
  const opts = this.opts();
5646
5764
  const ctx = await requireWorkspace(slugOf6(this));
5647
5765
  const first = request.join(" ").trim();
5648
5766
  const conversational = !first && !opts.once && process.stdin.isTTY && !isJsonMode();
5649
- const turn = async (ask, resume2) => {
5650
- const lines = [];
5651
- const result = await runArchitect({
5652
- ctx,
5653
- request: ask,
5654
- readOnly: Boolean(opts.readOnly),
5655
- resume: resume2,
5656
- allowApiBilling: Boolean(opts.allowApiBilling),
5657
- onLine: (text) => {
5658
- lines.push(text);
5659
- if (!isJsonMode()) out(text);
5767
+ const session = await openArchitect({
5768
+ ctx,
5769
+ readOnly: Boolean(opts.readOnly),
5770
+ allowApiBilling: Boolean(opts.allowApiBilling)
5771
+ });
5772
+ let wroteText = false;
5773
+ const onEvent = (event) => {
5774
+ if (isJsonMode()) return;
5775
+ if (event.kind === "text") {
5776
+ process.stdout.write(event.delta);
5777
+ wroteText = true;
5778
+ return;
5779
+ }
5780
+ if (wroteText) {
5781
+ process.stdout.write("\n");
5782
+ wroteText = false;
5783
+ }
5784
+ if (event.kind === "tool") out(c.dim(` ${event.name}`));
5785
+ else if (!event.ok) out(c.red(` ${event.name} failed: ${event.detail}`));
5786
+ };
5787
+ const turn = async (ask) => {
5788
+ const result = await architectTurn({ ctx, session, request: ask, onEvent });
5789
+ if (!isJsonMode()) {
5790
+ if (wroteText) {
5791
+ process.stdout.write("\n");
5792
+ wroteText = false;
5793
+ } else if (!result.text) {
5794
+ out(c.dim("(no reply)"));
5660
5795
  }
5661
- });
5662
- return { result, lines };
5796
+ }
5797
+ return result;
5663
5798
  };
5664
5799
  if (!conversational) {
5665
5800
  let ask = first;
@@ -5668,30 +5803,24 @@ function registerArchitectCommands(program) {
5668
5803
  ask = await askLine("plan> ");
5669
5804
  if (!ask) fail("Nothing asked.");
5670
5805
  }
5671
- const { result, lines } = await turn(ask, Boolean(opts.resume));
5806
+ const result = await turn(ask);
5672
5807
  if (isJsonMode()) {
5673
- emit(
5674
- { ok: result.exitCode === 0, run_id: result.runId, session_id: result.sessionId, output: lines },
5675
- () => ""
5676
- );
5677
- return;
5678
- }
5679
- if (result.sessionId) {
5680
- out(c.dim(`
5681
- Continue with \`hd plan --resume "..."\`. Run ${result.runId.slice(0, 8)}.`));
5808
+ emit({ ok: true, run_id: result.runId, text: result.text, tool_calls: result.toolCalls }, () => "");
5682
5809
  }
5683
5810
  return;
5684
5811
  }
5685
- out(c.dim("Talking to the architect. Enter sends, blank line or /exit leaves, ctrl-c stops a turn."));
5686
- let resume = Boolean(opts.resume);
5812
+ out(
5813
+ c.dim(
5814
+ `Talking to the architect (${session.agent.model}${session.repo ? ", repo readable" : ""}). Blank line or /exit leaves.`
5815
+ )
5816
+ );
5687
5817
  for (; ; ) {
5688
5818
  const ask = await askLine("\nplan> ");
5689
5819
  if (!ask || ask === "/exit" || ask === "/quit") {
5690
- out(c.dim('Left the architect. `hd plan --resume "..."` picks the thread back up.'));
5820
+ out(c.dim("Left the architect."));
5691
5821
  return;
5692
5822
  }
5693
- const { result } = await turn(ask, resume);
5694
- if (result.sessionId) resume = true;
5823
+ await turn(ask);
5695
5824
  }
5696
5825
  });
5697
5826
  program.command("mcp").description("serve the platform as MCP tools on stdio, for Codex, Claude Code, or Mel").option("--read-only", "expose only the tools that change nothing").option("--list", "print the tools instead of serving them").action(async function() {
@@ -5727,19 +5856,34 @@ Run \`codex login\` in a terminal.`);
5727
5856
  report(connection);
5728
5857
  if (connection.state !== "connected") process.exitCode = 1;
5729
5858
  });
5730
- program.command("doctor").description("check the architect's Codex connection and how a run would bill").action(async function() {
5859
+ program.command("doctor").description("check the architect's ChatGPT connection and how a turn would bill").action(async function() {
5731
5860
  const connection = await codexConnection();
5732
- const endpoint = mcpEndpoint();
5861
+ const expiry = await tokenExpiry();
5733
5862
  emit(
5734
- { codex: connection, mcp: endpoint, config_args: mcpConfigArgs(endpoint) },
5863
+ { codex: connection, token_expires_in_hours: expiry },
5735
5864
  () => [
5736
- `${c.dim("codex")} ${connection.state === "connected" ? c.green(connectionSummary(connection)) : c.yellow(connectionSummary(connection))}`,
5737
- `${c.dim("mcp server")} ${endpoint.command} ${endpoint.args.join(" ")}`,
5738
- connection.state === "connected" ? c.dim("Architect runs bill the ChatGPT subscription, not per token.") : c.yellow(howToFix(connection) ?? "")
5739
- ].join("\n")
5865
+ `${c.dim("chatgpt")} ${connection.state === "connected" ? c.green(connectionSummary(connection)) : c.yellow(connectionSummary(connection))}`,
5866
+ expiry == null ? "" : `${c.dim("session")} renews automatically, valid for ${expiry}h`,
5867
+ connection.state === "connected" ? c.dim("The architect calls the ChatGPT backend directly on your subscription. No Codex process, no per-token billing.") : c.yellow(howToFix(connection) ?? "")
5868
+ ].filter(Boolean).join("\n")
5740
5869
  );
5741
5870
  });
5742
5871
  }
5872
+ async function tokenExpiry() {
5873
+ try {
5874
+ const { readFile: readFile8 } = await import("fs/promises");
5875
+ const path8 = await import("path");
5876
+ const { codexHome: codexHome2 } = await Promise.resolve().then(() => (init_connection(), connection_exports));
5877
+ const { msUntilExpiry: msUntilExpiry2 } = await Promise.resolve().then(() => (init_chatgpt(), chatgpt_exports));
5878
+ const raw = await readFile8(path8.join(codexHome2(), "auth.json"), "utf8");
5879
+ const token = JSON.parse(raw).tokens?.access_token;
5880
+ if (!token) return null;
5881
+ const ms = msUntilExpiry2(token);
5882
+ return Number.isFinite(ms) ? Math.round(ms / 36e5) : null;
5883
+ } catch {
5884
+ return null;
5885
+ }
5886
+ }
5743
5887
 
5744
5888
  // src/tui/commands.ts
5745
5889
  init_json();
@@ -5808,7 +5952,7 @@ ${runner.HD_HELP}`);
5808
5952
 
5809
5953
  // src/host/upgrade.ts
5810
5954
  import { execFile as execFile5 } from "child_process";
5811
- import { createInterface as createInterface4 } from "readline/promises";
5955
+ import { createInterface as createInterface3 } from "readline/promises";
5812
5956
  import { promisify as promisify5 } from "util";
5813
5957
  init_json();
5814
5958
  init_theme();
@@ -5857,7 +6001,7 @@ async function offerUpdate(now = Date.now()) {
5857
6001
  await saveUpdateState({ checked_at: now });
5858
6002
  if (!latest || !isNewer(latest, VERSION)) return;
5859
6003
  if (state.skipped === latest) return;
5860
- const rl = createInterface4({ input: process.stdin, output: process.stderr });
6004
+ const rl = createInterface3({ input: process.stdin, output: process.stderr });
5861
6005
  let answer = "";
5862
6006
  try {
5863
6007
  answer = (await rl.question(