@higherdev/cli 0.1.0 → 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 +41 -15
  2. package/dist/index.js +919 -687
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1190,11 +1190,17 @@ function str(value) {
1190
1190
  }
1191
1191
  function assistantText(payload) {
1192
1192
  if (typeof payload.text === "string") return payload.text;
1193
+ const item = asRecord(payload.item);
1194
+ if (typeof item.text === "string") return item.text;
1195
+ if (Array.isArray(item.content)) {
1196
+ const joined = item.content.map((entry) => str(asRecord(entry).text)).filter(Boolean).join("\n");
1197
+ if (joined) return joined;
1198
+ }
1193
1199
  const message = asRecord(payload.message);
1194
1200
  const content = message.content;
1195
1201
  if (Array.isArray(content)) {
1196
- return content.map((item) => {
1197
- const row = asRecord(item);
1202
+ return content.map((item2) => {
1203
+ const row = asRecord(item2);
1198
1204
  return str(row.text);
1199
1205
  }).filter(Boolean).join("\n");
1200
1206
  }
@@ -1210,8 +1216,8 @@ function toolDetail(payload) {
1210
1216
  const command = str(input.command || input.cmd);
1211
1217
  const additions = Number(input.additions ?? input.added);
1212
1218
  const deletions = Number(input.deletions ?? input.removed);
1213
- const stat = Number.isFinite(additions) || Number.isFinite(deletions) ? ` +${Number.isFinite(additions) ? additions : 0}/-${Number.isFinite(deletions) ? deletions : 0}` : "";
1214
- 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}`;
1215
1221
  if (command) return command.length > 80 ? `${command.slice(0, 77)}...` : command;
1216
1222
  return "";
1217
1223
  }
@@ -1224,10 +1230,56 @@ function resultTail(payload) {
1224
1230
  ${tail}`;
1225
1231
  return prefix || tail;
1226
1232
  }
1233
+ function codexItemLine(event, item) {
1234
+ const kind = str(item.type);
1235
+ if (!kind || kind === "agent_message") return null;
1236
+ if (kind === "mcp_tool_call") {
1237
+ const server = str(item.server);
1238
+ const tool = str(item.tool);
1239
+ const status = str(item.status);
1240
+ if (status === "in_progress") return SKIP;
1241
+ const failed = status === "failed" || Boolean(item.error);
1242
+ const name = [server, tool].filter(Boolean).join("/") || "tool";
1243
+ const args = item.arguments && Object.keys(asRecord(item.arguments)).length > 0 ? ` ${JSON.stringify(item.arguments)}` : "";
1244
+ return {
1245
+ id: event.id,
1246
+ kind: failed ? "error" : "tool",
1247
+ title: `${name}${clip2(args, 120)}${status && status !== "completed" ? ` (${status})` : ""}`,
1248
+ body: failed ? clip2(str(asRecord(item.error).message ?? item.error), 400) || void 0 : void 0
1249
+ };
1250
+ }
1251
+ if (kind === "command_execution") {
1252
+ const command = clip2(str(item.command ?? item.cmd), 120);
1253
+ const code = item.exit_code ?? item.exitCode;
1254
+ return {
1255
+ id: event.id,
1256
+ kind: code != null && Number(code) !== 0 ? "error" : "tool",
1257
+ title: `${command || "command"}${code == null ? "" : ` (exit ${code})`}`
1258
+ };
1259
+ }
1260
+ if (kind === "file_change" || kind === "patch_apply") {
1261
+ const files = Array.isArray(item.changes) ? item.changes.map((change) => str(asRecord(change).path)).filter(Boolean) : [str(item.path)].filter(Boolean);
1262
+ return { id: event.id, kind: "tool", title: `edited ${files.join(", ") || "a file"}` };
1263
+ }
1264
+ if (kind === "reasoning" || kind === "todo_list") return SKIP;
1265
+ return { id: event.id, kind: "status", title: kind.replaceAll("_", " ") };
1266
+ }
1267
+ function clip2(text, max) {
1268
+ const flat = String(text ?? "").trim();
1269
+ return flat.length <= max ? flat : `${flat.slice(0, Math.max(0, max - 3))}...`;
1270
+ }
1227
1271
  function transcriptLines(events) {
1228
1272
  const lines = [];
1229
1273
  for (const event of events) {
1230
1274
  const payload = asRecord(event.payload);
1275
+ if (payload.item) {
1276
+ const line = codexItemLine(event, asRecord(payload.item));
1277
+ if (line === SKIP) continue;
1278
+ if (line) {
1279
+ lines.push(line);
1280
+ continue;
1281
+ }
1282
+ }
1231
1283
  if (event.type === "error") {
1232
1284
  lines.push({
1233
1285
  id: event.id,
@@ -1277,9 +1329,11 @@ function transcriptLines(events) {
1277
1329
  }
1278
1330
  return lines;
1279
1331
  }
1332
+ var SKIP;
1280
1333
  var init_transcript = __esm({
1281
1334
  "../../packages/db/src/transcript.ts"() {
1282
1335
  "use strict";
1336
+ SKIP = "skip";
1283
1337
  }
1284
1338
  });
1285
1339
 
@@ -1465,6 +1519,119 @@ var init_theme = __esm({
1465
1519
  }
1466
1520
  });
1467
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
+
1468
1635
  // src/read/queries.ts
1469
1636
  function unwrap(result) {
1470
1637
  if (result.error) fail(result.error.message);
@@ -1749,53 +1916,289 @@ var init_subscribe = __esm({
1749
1916
  }
1750
1917
  });
1751
1918
 
1752
- // ../runner/src/providers.ts
1753
- import { execFile as execFile2, execFileSync } from "child_process";
1754
- import { promisify as promisify2 } from "util";
1755
- 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;
1756
1935
  try {
1757
- execFileSync("which", [bin], { stdio: "ignore" });
1758
- return true;
1936
+ raw = readFileSync(authPath(), "utf8");
1759
1937
  } catch {
1760
- 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\`.`);
1761
1946
  }
1762
1947
  }
1763
- function detectProviders() {
1764
- const found = [];
1765
- for (const [provider, bin] of Object.entries(PROVIDER_BINS)) {
1766
- 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;
1767
1957
  }
1768
- return found;
1769
1958
  }
1770
- 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;
1771
1977
  try {
1772
- const { stdout, stderr } = await execFileAsync(bin, args, {
1773
- encoding: "utf8",
1774
- timeout: timeoutMs,
1775
- 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
+ })
1776
1987
  });
1777
- return [stdout, stderr].filter(Boolean).join("\n");
1778
- } catch (error) {
1779
- if (error && typeof error === "object") {
1780
- const err = error;
1781
- const text = [err.stdout, err.stderr].filter(Boolean).join("\n").trim();
1782
- 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
+ }
1783
2095
  }
1784
- return "";
1785
2096
  }
2097
+ return out2;
1786
2098
  }
1787
- var execFileAsync, PROVIDER_BINS, MODEL_DISCOVER_TIMEOUT_MS;
1788
- var init_providers = __esm({
1789
- "../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"() {
1790
2192
  "use strict";
1791
- execFileAsync = promisify2(execFile2);
1792
- PROVIDER_BINS = {
1793
- claude: "claude",
1794
- codex: "codex",
1795
- gemini: "agy",
1796
- grok: "grok"
2193
+ init_connection();
2194
+ ChatGptAuthError = class extends Error {
1797
2195
  };
1798
- 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;
1799
2202
  }
1800
2203
  });
1801
2204
 
@@ -2007,6 +2410,56 @@ var init_App = __esm({
2007
2410
  }
2008
2411
  });
2009
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
+
2010
2463
  // ../runner/src/auth-check.ts
2011
2464
  function firstLine(text) {
2012
2465
  const line = text.split(/\r?\n/).map((row) => row.trim()).find(Boolean);
@@ -2214,8 +2667,8 @@ __export(init_exports, {
2214
2667
  parseInitArgs: () => parseInitArgs
2215
2668
  });
2216
2669
  import { mkdir as mkdir3, readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
2217
- import { homedir as homedir5, hostname, tmpdir as tmpdir2 } from "os";
2218
- import path6 from "path";
2670
+ import { homedir as homedir4, hostname, tmpdir as tmpdir2 } from "os";
2671
+ import path7 from "path";
2219
2672
  import { fileURLToPath } from "url";
2220
2673
  import { execFile as execFile4 } from "child_process";
2221
2674
  import { promisify as promisify4 } from "util";
@@ -2280,7 +2733,7 @@ function xmlEscape(value) {
2280
2733
  }
2281
2734
  function launchdPlist(opts) {
2282
2735
  const label = opts.label ?? LAUNCHD_LABEL;
2283
- const program = path6.join(opts.repoRoot, "deploy/run-runner.sh");
2736
+ const program = path7.join(opts.repoRoot, "deploy/run-runner.sh");
2284
2737
  return `<?xml version="1.0" encoding="UTF-8"?>
2285
2738
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2286
2739
  <plist version="1.0">
@@ -2311,16 +2764,16 @@ function launchdPlist(opts) {
2311
2764
  `;
2312
2765
  }
2313
2766
  function defaultRepoRoot(fromFile = fileURLToPath(import.meta.url)) {
2314
- return path6.resolve(path6.dirname(fromFile), "../../..");
2767
+ return path7.resolve(path7.dirname(fromFile), "../../..");
2315
2768
  }
2316
2769
  async function initHost(flags, deps = {}) {
2317
2770
  const env = deps.env ?? process.env;
2318
2771
  const platform = deps.platform ?? process.platform;
2319
- const home = (deps.homedir ?? homedir5)();
2772
+ const home = (deps.homedir ?? homedir4)();
2320
2773
  const repoRoot = defaultRepoRoot();
2321
2774
  const host = flags.host || env.RUNNER_HOST || (deps.hostname ?? hostname)().replace(/\..*$/, "").toLowerCase();
2322
- const envFile = flags.envFile || path6.join(repoRoot, "apps/runner/.env");
2323
- 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");
2324
2777
  const steps = [
2325
2778
  "Write the runner env file",
2326
2779
  "Register the host",
@@ -2346,13 +2799,13 @@ async function initHost(flags, deps = {}) {
2346
2799
  RUNNER_STOP_TIMEOUT_MS: existing.RUNNER_STOP_TIMEOUT_MS || "90000"
2347
2800
  });
2348
2801
  if (!flags.dryRun) {
2349
- await ensureDir(path6.dirname(envFile), { recursive: true });
2802
+ await ensureDir(path7.dirname(envFile), { recursive: true });
2350
2803
  await write(envFile, formatEnvFile(merged), "utf8");
2351
2804
  }
2352
- 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"));
2353
- 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");
2354
2807
  const plist = launchdPlist({ repoRoot, envFile, logPath });
2355
- await ensureDir(path6.dirname(plistPath), { recursive: true });
2808
+ await ensureDir(path7.dirname(plistPath), { recursive: true });
2356
2809
  await write(plistPath, plist, "utf8");
2357
2810
  let launchdInstalled = false;
2358
2811
  if (flags.installLaunchd && !flags.dryRun && platform === "darwin") {
@@ -2599,172 +3052,74 @@ async function createDb() {
2599
3052
  storage,
2600
3053
  storageKey: "higherdev-cli"
2601
3054
  }
2602
- });
2603
- client.auth.onAuthStateChange((_event, session) => {
2604
- if (session?.access_token) void client?.realtime.setAuth(session.access_token);
2605
- });
2606
- return client;
2607
- }
2608
- async function flushSession() {
2609
- await storage?.settled();
2610
- }
2611
- async function currentEmail(db) {
2612
- const { data } = await db.auth.getUser();
2613
- return data.user?.email ?? null;
2614
- }
2615
-
2616
- // src/context.ts
2617
- function normalizeEmail(email) {
2618
- return email.trim().toLowerCase();
2619
- }
2620
- async function serverAllows(db) {
2621
- const { data, error } = await db.rpc("is_allowlisted");
2622
- if (error) return false;
2623
- return data === true;
2624
- }
2625
- async function audit(db, email, workspaceId, action, payload) {
2626
- const { error } = await db.from("audit_events").insert(auditEventRow({ workspace_id: workspaceId, actor: email, action, payload }));
2627
- if (!error) return;
2628
- process.stderr.write(
2629
- `warning: the change was made but was not written to the audit log (${error.message}).
2630
- Apply the pending migrations in supabase/migrations.
2631
- `
2632
- );
2633
- }
2634
- async function requireCtx() {
2635
- const db = await createDb();
2636
- const email = await currentEmail(db);
2637
- if (!email) fail("Not signed in. Run `hd login`.");
2638
- if (!await serverAllows(db)) {
2639
- fail(`${email} is not allowed on this HigherDEV. Ask its owner to add you.`);
2640
- }
2641
- return { db, email, audit: (workspaceId, action, payload) => audit(db, email, workspaceId, action, payload) };
2642
- }
2643
- async function requireWorkspace(slugOverride) {
2644
- const ctx = await requireCtx();
2645
- const config = await loadConfig();
2646
- const slug = slugOverride ?? process.env.HIGHERDEV_WORKSPACE ?? config.workspace;
2647
- if (slug) {
2648
- const workspace = await workspaceBySlug(ctx.db, slug);
2649
- if (!workspace) fail(`No workspace named ${slug}. Run \`hd workspace ls\`.`);
2650
- return { ...ctx, workspace };
2651
- }
2652
- const { data, error } = await ctx.db.from("workspaces").select("*").order("created_at");
2653
- if (error) fail(error.message);
2654
- const rows = data ?? [];
2655
- if (rows.length === 1) {
2656
- await saveConfig({ workspace: rows[0].slug });
2657
- return { ...ctx, workspace: rows[0] };
2658
- }
2659
- if (rows.length === 0) fail("No workspaces yet. Run `hd workspace new <name> <owner/repo>`.");
2660
- fail(
2661
- `Pick a workspace: ${rows.map((row) => row.slug).join(", ")}.
2662
- Run \`hd use <slug>\` or pass --workspace.`
2663
- );
2664
- }
2665
-
2666
- // src/auth.ts
2667
- init_theme();
2668
- init_json();
2669
-
2670
- // src/architect/connection.ts
2671
- import { execFile, spawn } from "child_process";
2672
- import { access, readFile as readFile2 } from "fs/promises";
2673
- import { constants } from "fs";
2674
- import { homedir as homedir2 } from "os";
2675
- import path2 from "path";
2676
- import { promisify } from "util";
2677
- var run = promisify(execFile);
2678
- function codexHome(env = process.env) {
2679
- return env.CODEX_HOME ?? path2.join(homedir2(), ".codex");
2680
- }
2681
- function classifyAuthFile(auth) {
2682
- if (!auth) return null;
2683
- const apiKey = typeof auth.OPENAI_API_KEY === "string" ? auth.OPENAI_API_KEY.trim() : "";
2684
- if (apiKey) {
2685
- return {
2686
- state: "api_key",
2687
- detail: "Codex is signed in with an API key, which bills per token."
2688
- };
2689
- }
2690
- const refresh = auth.tokens?.refresh_token;
2691
- if (auth.auth_mode === "chatgpt" && typeof refresh === "string" && refresh.length > 0) {
2692
- const accountId = typeof auth.tokens?.account_id === "string" ? auth.tokens.account_id : null;
2693
- return { state: "connected", accountId, detail: "Codex is connected to a ChatGPT subscription." };
2694
- }
2695
- return { state: "signed_out", detail: "Codex is installed but not signed in." };
2696
- }
2697
- async function onPath(bin, env = process.env) {
2698
- const dirs = (env.PATH ?? "").split(path2.delimiter).filter(Boolean);
2699
- for (const dir of dirs) {
2700
- try {
2701
- await access(path2.join(dir, bin), constants.X_OK);
2702
- return true;
2703
- } catch {
2704
- }
2705
- }
2706
- return false;
2707
- }
2708
- async function codexConnection(env = process.env) {
2709
- if (!await onPath("codex")) {
2710
- return {
2711
- state: "not_installed",
2712
- detail: "The Codex CLI is not on PATH. Install it, then run `hd connect`."
2713
- };
2714
- }
2715
- let auth = null;
2716
- try {
2717
- auth = JSON.parse(await readFile2(path2.join(codexHome(env), "auth.json"), "utf8"));
2718
- } catch {
2719
- auth = null;
2720
- }
2721
- const fromFile = classifyAuthFile(auth);
2722
- if (fromFile) return fromFile;
2723
- try {
2724
- const { stdout } = await run("codex", ["login", "status"], { timeout: 2e4 });
2725
- const text = stdout.trim();
2726
- if (/chatgpt/i.test(text)) {
2727
- return { state: "connected", accountId: null, detail: text };
2728
- }
2729
- if (/api key/i.test(text)) {
2730
- return { state: "api_key", detail: text };
2731
- }
2732
- return { state: "signed_out", detail: text || "Codex is not signed in." };
2733
- } catch {
2734
- return { state: "signed_out", detail: "Codex is installed but not signed in." };
2735
- }
3055
+ });
3056
+ client.auth.onAuthStateChange((_event, session) => {
3057
+ if (session?.access_token) void client?.realtime.setAuth(session.access_token);
3058
+ });
3059
+ return client;
2736
3060
  }
2737
- function connectionSummary(connection) {
2738
- switch (connection.state) {
2739
- case "connected":
2740
- return connection.accountId ? `${connection.detail} Account ${connection.accountId}.` : connection.detail;
2741
- default:
2742
- return connection.detail;
2743
- }
3061
+ async function flushSession() {
3062
+ await storage?.settled();
2744
3063
  }
2745
- function howToFix(connection) {
2746
- switch (connection.state) {
2747
- case "connected":
2748
- return null;
2749
- case "not_installed":
2750
- return "Install the Codex CLI, then run `hd connect`.";
2751
- case "api_key":
2752
- return "Run `codex logout` then `hd connect` to sign in with the subscription instead.";
2753
- default:
2754
- return "Run `hd connect` to sign in through ChatGPT.";
3064
+ async function currentEmail(db) {
3065
+ const { data } = await db.auth.getUser();
3066
+ return data.user?.email ?? null;
3067
+ }
3068
+
3069
+ // src/context.ts
3070
+ function normalizeEmail(email) {
3071
+ return email.trim().toLowerCase();
3072
+ }
3073
+ async function serverAllows(db) {
3074
+ const { data, error } = await db.rpc("is_allowlisted");
3075
+ if (error) return false;
3076
+ return data === true;
3077
+ }
3078
+ async function audit(db, email, workspaceId, action, payload) {
3079
+ const { error } = await db.from("audit_events").insert(auditEventRow({ workspace_id: workspaceId, actor: email, action, payload }));
3080
+ if (!error) return;
3081
+ process.stderr.write(
3082
+ `warning: the change was made but was not written to the audit log (${error.message}).
3083
+ Apply the pending migrations in supabase/migrations.
3084
+ `
3085
+ );
3086
+ }
3087
+ async function requireCtx() {
3088
+ const db = await createDb();
3089
+ const email = await currentEmail(db);
3090
+ if (!email) fail("Not signed in. Run `hd login`.");
3091
+ if (!await serverAllows(db)) {
3092
+ fail(`${email} is not allowed on this HigherDEV. Ask its owner to add you.`);
2755
3093
  }
3094
+ return { db, email, audit: (workspaceId, action, payload) => audit(db, email, workspaceId, action, payload) };
2756
3095
  }
2757
- async function connectCodex() {
2758
- const code = await new Promise((resolve, reject) => {
2759
- const child = spawn("codex", ["login"], { stdio: "inherit" });
2760
- child.on("error", reject);
2761
- child.on("exit", (status) => resolve(status ?? 1));
2762
- });
2763
- if (code !== 0) throw new Error(`codex login exited ${code}`);
2764
- return codexConnection();
3096
+ async function requireWorkspace(slugOverride) {
3097
+ const ctx = await requireCtx();
3098
+ const config = await loadConfig();
3099
+ const slug = slugOverride ?? process.env.HIGHERDEV_WORKSPACE ?? config.workspace;
3100
+ if (slug) {
3101
+ const workspace = await workspaceBySlug(ctx.db, slug);
3102
+ if (!workspace) fail(`No workspace named ${slug}. Run \`hd workspace ls\`.`);
3103
+ return { ...ctx, workspace };
3104
+ }
3105
+ const { data, error } = await ctx.db.from("workspaces").select("*").order("created_at");
3106
+ if (error) fail(error.message);
3107
+ const rows = data ?? [];
3108
+ if (rows.length === 1) {
3109
+ await saveConfig({ workspace: rows[0].slug });
3110
+ return { ...ctx, workspace: rows[0] };
3111
+ }
3112
+ if (rows.length === 0) fail("No workspaces yet. Run `hd workspace new <name> <owner/repo>`.");
3113
+ fail(
3114
+ `Pick a workspace: ${rows.map((row) => row.slug).join(", ")}.
3115
+ Run \`hd use <slug>\` or pass --workspace.`
3116
+ );
2765
3117
  }
2766
3118
 
2767
3119
  // src/auth.ts
3120
+ init_theme();
3121
+ init_json();
3122
+ init_connection();
2768
3123
  var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2769
3124
  var CODE_RE = /^\d{6}$/;
2770
3125
  async function prompt(question) {
@@ -4482,22 +4837,22 @@ function registerAttachCommands(program) {
4482
4837
  if (!contentType) fail(`${name} is not a jpg, png, gif, or webp.`);
4483
4838
  const ext = extensionForContentType(contentType);
4484
4839
  if (!ext) fail(`${name} is not a jpg, png, gif, or webp.`);
4485
- const path7 = attachmentObjectPath(ctx.workspace.id, ticket.id, randomUUID(), ext);
4486
- 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 });
4487
4842
  if (error) fail(`${name}: ${error.message}`);
4488
4843
  const { error: rowError } = await ctx.db.from("attachments").insert({
4489
4844
  workspace_id: ctx.workspace.id,
4490
4845
  ticket_id: ticket.id,
4491
4846
  message_id: null,
4492
- path: path7,
4847
+ path: path8,
4493
4848
  content_type: contentType,
4494
4849
  created_by: "human"
4495
4850
  });
4496
4851
  if (rowError) {
4497
- await ctx.db.storage.from(TICKET_IMAGES_BUCKET).remove([path7]);
4852
+ await ctx.db.storage.from(TICKET_IMAGES_BUCKET).remove([path8]);
4498
4853
  fail(rowError.message);
4499
4854
  }
4500
- uploaded.push(path7);
4855
+ uploaded.push(path8);
4501
4856
  }
4502
4857
  await ctx.audit(ctx.workspace.id, "create", { ticket_key: ticket.key, subject: "attachments" });
4503
4858
  ok(`Attached ${uploaded.length} file${uploaded.length === 1 ? "" : "s"} to ${c.bold(ticket.key)}.`, {
@@ -4523,7 +4878,7 @@ function registerAttachCommands(program) {
4523
4878
  }
4524
4879
 
4525
4880
  // src/architect/commands.ts
4526
- import { createInterface as createInterface3 } from "readline/promises";
4881
+ import { createInterface as createInterface2 } from "readline/promises";
4527
4882
  init_json();
4528
4883
  init_theme();
4529
4884
 
@@ -4533,7 +4888,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4533
4888
  import { z as z3 } from "zod";
4534
4889
 
4535
4890
  // src/version.ts
4536
- var VERSION = "0.1.0";
4891
+ var VERSION = "0.1.2";
4537
4892
 
4538
4893
  // src/architect/tools.ts
4539
4894
  init_src();
@@ -4965,6 +5320,13 @@ var TOOLS = [
4965
5320
  function toolsFor(access2) {
4966
5321
  return access2 === "read" ? TOOLS.filter((tool) => tool.access === "read") : TOOLS;
4967
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
+ }
4968
5330
  async function toolContext(slug) {
4969
5331
  return requireWorkspace(slug);
4970
5332
  }
@@ -5036,361 +5398,172 @@ async function serveMcp(opts) {
5036
5398
  }
5037
5399
 
5038
5400
  // src/architect/plan.ts
5039
- init_src();
5040
- import { execFile as execFile3 } from "child_process";
5041
- import { mkdir as mkdir2 } from "fs/promises";
5042
- import { homedir as homedir4 } from "os";
5043
- import path5 from "path";
5044
- import { promisify as promisify3 } from "util";
5045
5401
  init_json();
5046
5402
  init_theme();
5047
-
5048
- // src/architect/codex.ts
5049
- import { spawn as spawn4 } from "child_process";
5050
- import { createInterface as createInterface2 } from "readline";
5051
-
5052
- // ../runner/src/adapters/codex.ts
5053
- init_providers();
5403
+ init_chatgpt();
5404
+ init_connection();
5405
+ import { execFile as execFile2 } from "child_process";
5406
+ import { mkdir as mkdir2 } from "fs/promises";
5054
5407
  import { homedir as homedir3 } from "os";
5055
- import path4 from "path";
5056
- import { readFile as readFile6 } from "fs/promises";
5057
-
5058
- // ../runner/src/adapters/final.ts
5059
- var OUTCOMES = /* @__PURE__ */ new Set(["pr_opened", "pr_updated", "blocked", "failed"]);
5060
- function extractFinal(text) {
5061
- const trimmed = text.trim();
5062
- const candidates = [trimmed];
5063
- const fence = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/);
5064
- if (fence?.[1]) candidates.unshift(fence[1].trim());
5065
- const firstBrace = trimmed.indexOf("{");
5066
- const lastBrace = trimmed.lastIndexOf("}");
5067
- if (firstBrace >= 0 && lastBrace > firstBrace) {
5068
- candidates.unshift(trimmed.slice(firstBrace, lastBrace + 1));
5069
- }
5070
- for (const candidate of candidates) {
5071
- try {
5072
- const parsed = JSON.parse(candidate);
5073
- if (parsed && typeof parsed.summary === "string" && parsed.outcome && OUTCOMES.has(parsed.outcome)) {
5074
- return parsed;
5075
- }
5076
- } catch {
5077
- }
5078
- }
5079
- return null;
5080
- }
5081
-
5082
- // ../runner/src/adapters/spawn.ts
5083
- import { spawn as spawn3 } from "child_process";
5084
- import { randomUUID as randomUUID2 } from "crypto";
5408
+ import path6 from "path";
5409
+ import { promisify as promisify2 } from "util";
5085
5410
 
5086
- // ../runner/src/adapters/cost.ts
5087
- function costFromPayload(payload) {
5088
- if (!payload) return null;
5089
- const values = [payload.total_cost_usd, payload.cost_usd, payload.totalCostUsd, payload.costUsd];
5090
- pushUsageCosts(asRecord2(payload.usage), values);
5091
- pushUsageCosts(asRecord2(payload.info), values);
5092
- const result = asRecord2(payload.result);
5093
- if (result) {
5094
- values.push(result.total_cost_usd, result.cost_usd);
5095
- pushUsageCosts(asRecord2(result.usage), values);
5096
- }
5097
- for (const value of values) {
5098
- const n = toNonNegativeNumber(value);
5099
- if (n != null) return n;
5100
- }
5101
- return null;
5102
- }
5103
- function turnsFromPayload(payload) {
5104
- if (!payload) return null;
5105
- const values = [payload.num_turns, payload.numTurns, payload.turns];
5106
- const usage = asRecord2(payload.usage);
5107
- if (usage) values.push(usage.num_turns, usage.numTurns, usage.turns);
5108
- const result = asRecord2(payload.result);
5109
- if (result) values.push(result.num_turns, result.numTurns, result.turns);
5110
- for (const value of values) {
5111
- const n = toNonNegativeInt(value);
5112
- if (n != null) return n;
5113
- }
5114
- return null;
5115
- }
5116
- function pushUsageCosts(usage, values) {
5117
- if (!usage) return;
5118
- values.push(usage.total_cost_usd, usage.cost_usd, usage.totalCostUsd, usage.costUsd);
5119
- }
5120
- function asRecord2(value) {
5121
- return value && typeof value === "object" && !Array.isArray(value) ? value : null;
5122
- }
5123
- function toNonNegativeNumber(value) {
5124
- if (typeof value === "number") return Number.isFinite(value) && value >= 0 ? value : null;
5125
- if (typeof value === "string" && value.trim()) {
5126
- const n = Number(value);
5127
- 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;
5128
5446
  }
5129
- return null;
5130
- }
5131
- function toNonNegativeInt(value) {
5132
- const n = toNonNegativeNumber(value);
5133
- return n != null && Number.isInteger(n) ? n : null;
5134
- }
5135
-
5136
- // ../runner/src/adapters/spawn.ts
5137
- function spawnCli(opts) {
5138
- let sessionId = opts.sessionId ?? randomUUID2();
5139
- const child = spawn3(opts.bin, opts.args, {
5140
- cwd: opts.cwd,
5141
- env: process.env,
5142
- stdio: ["ignore", "pipe", "pipe"]
5143
- });
5144
- const queue = [];
5145
- let notify = null;
5146
- let closed = false;
5147
- let resultText = "";
5148
- let costUsd = null;
5149
- let turns = null;
5150
- const wait = collectLines(child, (line) => {
5151
- const event = opts.parse(line);
5152
- const sid = event?.payload?.session_id;
5153
- if (typeof sid === "string" && sid) sessionId = sid;
5154
- const thread = event?.payload?.thread_id;
5155
- if (typeof thread === "string" && thread) sessionId = thread;
5156
- const structured = event?.payload?.structured_output;
5157
- if (structured && typeof structured === "object") {
5158
- resultText = JSON.stringify(structured);
5159
- } else if (typeof event?.payload?.result === "string") {
5160
- resultText = event.payload.result;
5161
- }
5162
- const cost = costFromPayload(event?.payload);
5163
- if (cost != null) costUsd = cost;
5164
- const turnCount = turnsFromPayload(event?.payload);
5165
- if (turnCount != null) turns = turnCount;
5166
- queue.push(line);
5167
- notify?.();
5168
- }).then(({ stdout, stderr, exitCode, signal }) => {
5169
- closed = true;
5170
- notify?.();
5171
- return {
5172
- exitCode,
5173
- signal,
5174
- final: extractFinal(resultText || stdout),
5175
- rawText: resultText || stdout,
5176
- stderr,
5177
- costUsd,
5178
- turns
5179
- };
5180
- });
5181
- async function* stream() {
5182
- while (!closed || queue.length > 0) {
5183
- if (queue.length === 0) {
5184
- await new Promise((resolve) => {
5185
- notify = resolve;
5186
- });
5187
- notify = null;
5188
- }
5189
- while (queue.length > 0) {
5190
- yield queue.shift();
5191
- }
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));
5192
5455
  }
5193
5456
  }
5194
- return {
5195
- get sessionId() {
5196
- return sessionId;
5197
- },
5198
- stream: stream(),
5199
- kill() {
5200
- child.kill("SIGINT");
5201
- setTimeout(() => {
5202
- if (!child.killed) child.kill("SIGTERM");
5203
- }, 1e4).unref();
5204
- },
5205
- wait: () => wait
5206
- };
5207
5457
  }
5208
- function collectLines(child, onLine) {
5209
- return new Promise((resolve, reject) => {
5210
- let buffer = "";
5211
- let stdout = "";
5212
- let stderr = "";
5213
- child.stdout?.setEncoding("utf8");
5214
- child.stderr?.setEncoding("utf8");
5215
- child.stdout?.on("data", (chunk) => {
5216
- stdout += chunk;
5217
- buffer += chunk;
5218
- const lines = buffer.split("\n");
5219
- buffer = lines.pop() ?? "";
5220
- for (const line of lines) onLine(line);
5221
- });
5222
- child.stderr?.on("data", (chunk) => {
5223
- stderr += chunk;
5224
- });
5225
- child.on("error", reject);
5226
- child.on("close", (exitCode, signal) => {
5227
- if (buffer) onLine(buffer);
5228
- resolve({ stdout, stderr, exitCode, signal });
5229
- });
5230
- });
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);
5231
5465
  }
5232
-
5233
- // ../runner/src/adapters/codex.ts
5234
- function effortConfig(effort) {
5235
- const level = effort ?? "high";
5236
- return ["-c", `model_reasoning_effort="${level}"`];
5237
- }
5238
- var codexAdapter = {
5239
- name: "codex",
5240
- start({ prompt: prompt2, cwd, model, effort }) {
5241
- return spawnCli({
5242
- bin: "codex",
5243
- args: [
5244
- "exec",
5245
- "--json",
5246
- "--model",
5247
- model,
5248
- ...effortConfig(effort),
5249
- "--dangerously-bypass-approvals-and-sandbox",
5250
- "-C",
5251
- cwd,
5252
- prompt2
5253
- ],
5254
- cwd,
5255
- parse: codexAdapter.parse
5256
- });
5257
- },
5258
- resume({ sessionId, prompt: prompt2, cwd, effort }) {
5259
- return spawnCli({
5260
- bin: "codex",
5261
- args: ["exec", "resume", sessionId, "--json", ...effortConfig(effort), prompt2],
5262
- cwd,
5263
- parse: codexAdapter.parse,
5264
- sessionId
5265
- });
5266
- },
5267
- parse(line) {
5268
- const trimmed = line.trim();
5269
- if (!trimmed) return null;
5270
- if (!trimmed.startsWith("{")) return { type: "text", payload: { text: trimmed } };
5271
- try {
5272
- const event = JSON.parse(trimmed);
5273
- const type = String(event.type ?? event.kind ?? "");
5274
- if (type === "thread.started" || type === "session" || type === "thread_started") {
5275
- const sessionId = typeof event.thread_id === "string" && event.thread_id || typeof event.session_id === "string" && event.session_id || void 0;
5276
- return { type: "status", payload: { phase: "init", session_id: sessionId, thread_id: sessionId } };
5277
- }
5278
- if (type.includes("tool") && (type.includes("call") || type === "tool_use")) {
5279
- return { type: "tool_use", payload: event };
5280
- }
5281
- if (type.includes("tool") && type.includes("result")) {
5282
- 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
+ };
5283
5491
  }
5284
- if (type === "item.completed" || type === "agent_message" || type === "message") {
5285
- 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
+ };
5286
5515
  }
5287
- if (type === "error" || event.error) {
5288
- 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 };
5289
5560
  }
5290
- return { type: "status", payload: event };
5291
- } catch {
5292
- return { type: "text", payload: { text: trimmed } };
5293
5561
  }
5294
- },
5295
- extractFinal
5296
- };
5297
-
5298
- // src/architect/codex.ts
5299
- function mcpEndpoint(entry = process.argv[1], execPath = process.execPath) {
5300
- const mcpArgs = ["mcp"];
5301
- if (entry.endsWith(".ts") || entry.endsWith(".tsx")) {
5302
- return { command: "tsx", args: [entry, ...mcpArgs] };
5303
- }
5304
- return { command: execPath, args: [entry, ...mcpArgs] };
5305
- }
5306
- function mcpConfigArgs(endpoint, extraArgs = []) {
5307
- const args = [...endpoint.args, ...extraArgs];
5308
- return [
5309
- "-c",
5310
- `mcp_servers.hd.command=${JSON.stringify(endpoint.command)}`,
5311
- "-c",
5312
- `mcp_servers.hd.args=${JSON.stringify(args)}`
5313
- ];
5314
- }
5315
- function buildCodexArgs(opts) {
5316
- const base = [
5317
- "--json",
5318
- "-c",
5319
- `model_reasoning_effort="${opts.effort}"`,
5320
- "--sandbox",
5321
- "read-only",
5322
- "--skip-git-repo-check",
5323
- ...opts.mcp
5324
5562
  ];
5325
- if (opts.resumeSessionId) {
5326
- return ["exec", "resume", opts.resumeSessionId, ...base, opts.prompt];
5327
- }
5328
- return ["exec", "--model", opts.model, ...base, "-C", opts.cwd, opts.prompt];
5329
- }
5330
- function spawnArchitect(opts) {
5331
- const args = buildCodexArgs(opts);
5332
- const child = spawn4("codex", args, {
5333
- cwd: opts.cwd,
5334
- stdio: ["ignore", "pipe", "pipe"],
5335
- env: process.env
5336
- });
5337
- let sessionId = opts.resumeSessionId ?? null;
5338
- const chunks = [];
5339
- let stderr = "";
5340
- child.stderr.on("data", (data) => {
5341
- stderr += data.toString();
5342
- });
5343
- const queue = [];
5344
- let notify = null;
5345
- let finished = false;
5346
- const lines = createInterface2({ input: child.stdout });
5347
- lines.on("line", (line) => {
5348
- chunks.push(line);
5349
- const event = codexAdapter.parse(line);
5350
- if (!event) return;
5351
- const candidate = event.payload.session_id ?? event.payload.thread_id;
5352
- if (!sessionId && typeof candidate === "string") sessionId = candidate;
5353
- queue.push(event);
5354
- notify?.();
5355
- });
5356
- const done = new Promise(
5357
- (resolve) => {
5358
- child.on("close", (code) => {
5359
- finished = true;
5360
- notify?.();
5361
- resolve({
5362
- exitCode: code,
5363
- text: chunks.join("\n") + (stderr ? `
5364
- ${stderr}` : ""),
5365
- sessionId
5366
- });
5367
- });
5368
- child.on("error", () => {
5369
- finished = true;
5370
- notify?.();
5371
- resolve({ exitCode: 1, text: stderr || "codex failed to start", sessionId });
5372
- });
5373
- }
5374
- );
5375
- const events = {
5376
- async *[Symbol.asyncIterator]() {
5377
- while (true) {
5378
- while (queue.length > 0) yield queue.shift();
5379
- if (finished) return;
5380
- await new Promise((resolve) => {
5381
- notify = () => {
5382
- notify = null;
5383
- resolve();
5384
- };
5385
- });
5386
- }
5387
- }
5388
- };
5389
- return { args, events, kill: () => child.kill("SIGINT"), done };
5390
5563
  }
5391
5564
 
5392
5565
  // src/architect/plan.ts
5393
- var run2 = promisify3(execFile3);
5566
+ var run2 = promisify2(execFile2);
5394
5567
  var ARCHITECT_ROLE = "architect";
5395
5568
  async function architectAgent(ctx) {
5396
5569
  const { data, error } = await ctx.db.from("agents").select("*").eq("workspace_id", ctx.workspace.id).eq("role", ARCHITECT_ROLE).maybeSingle();
@@ -5407,14 +5580,12 @@ async function repoCheckout(ctx) {
5407
5580
  const here = process.cwd();
5408
5581
  try {
5409
5582
  const { stdout } = await run2("git", ["remote", "get-url", "origin"], { cwd: here });
5410
- const origin = stdout.trim().toLowerCase();
5411
- const repo = ctx.workspace.repo.toLowerCase();
5412
- if (origin.includes(repo)) return here;
5583
+ if (stdout.trim().toLowerCase().includes(ctx.workspace.repo.toLowerCase())) return here;
5413
5584
  } catch {
5414
5585
  }
5415
- const root = path5.join(homedir4(), ".cache", "higherdev", "architect");
5586
+ const root = path6.join(homedir3(), ".cache", "higherdev", "architect");
5416
5587
  await mkdir2(root, { recursive: true });
5417
- const dir = path5.join(root, ctx.workspace.slug);
5588
+ const dir = path6.join(root, ctx.workspace.slug);
5418
5589
  try {
5419
5590
  await run2("git", ["-C", dir, "fetch", "--depth", "1", "origin", ctx.workspace.default_branch], {
5420
5591
  timeout: 12e4
@@ -5422,6 +5593,7 @@ async function repoCheckout(ctx) {
5422
5593
  await run2("git", ["-C", dir, "reset", "--hard", `origin/${ctx.workspace.default_branch}`], {
5423
5594
  timeout: 6e4
5424
5595
  });
5596
+ return dir;
5425
5597
  } catch {
5426
5598
  try {
5427
5599
  await run2(
@@ -5429,51 +5601,43 @@ async function repoCheckout(ctx) {
5429
5601
  ["repo", "clone", ctx.workspace.repo, dir, "--", "--depth", "1", "--branch", ctx.workspace.default_branch],
5430
5602
  { timeout: 3e5 }
5431
5603
  );
5432
- } catch (error) {
5433
- fail(
5434
- `Could not get a checkout of ${ctx.workspace.repo} (${error instanceof Error ? error.message : String(error)}).
5435
- Run \`hd plan\` from inside the repo, or check \`gh auth status\`.`
5436
- );
5604
+ return dir;
5605
+ } catch {
5606
+ return null;
5437
5607
  }
5438
5608
  }
5439
- return dir;
5440
5609
  }
5441
- function architectPrompt(opts) {
5610
+ function architectInstructions(opts) {
5442
5611
  const { ctx } = opts;
5443
5612
  return [
5444
- "You are the HigherDEV architect for the workspace below. You decide what should be built and",
5445
- "whether the platform is configured to build it. You do not write code: you file the work and the",
5446
- "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.",
5447
5616
  "",
5448
- `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}).`,
5449
5618
  `You are acting for ${ctx.email}.`,
5450
5619
  "",
5451
- "Tools: the `hd` MCP server is attached and is your only way to reach the platform. Call `status`",
5452
- "first, and `list_agents` before you assume who can build what. The repository is checked out at",
5453
- "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.",
5454
5623
  "",
5455
- "How to hand work over:",
5456
- "- Prefer creating one epic with a well-written spec. `create_epic` triggers the orchestrator,",
5457
- " which decomposes the spec into tickets itself and assigns them. That is the handoff.",
5458
- "- 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.",
5459
5628
  "- A ticket is one PR, one area, under about 400 changed lines, with acceptance criteria and a",
5460
5629
  " test expectation. Two live tickets must not share an area; chain them with blocked_by.",
5461
5630
  "",
5462
- "What only you can do: agent configuration. Model, effort, routing notes, prompt addenda,",
5463
- "conventions, budgets, pausing. The orchestrator has no action for any of it. If the work you are",
5464
- "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.",
5465
5633
  "",
5466
- 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.",
5467
- "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.",
5468
5636
  opts.addendum ? `
5469
- ${opts.addendum}` : "",
5470
- "",
5471
- "---",
5472
- "",
5473
- opts.request
5474
- ].filter((line) => line !== null).join("\n");
5637
+ ${opts.addendum}` : ""
5638
+ ].filter(Boolean).join("\n");
5475
5639
  }
5476
- async function runArchitect(opts) {
5640
+ async function openArchitect(opts) {
5477
5641
  const { ctx } = opts;
5478
5642
  const agent = await architectAgent(ctx);
5479
5643
  const connection = await codexConnection();
@@ -5481,88 +5645,112 @@ async function runArchitect(opts) {
5481
5645
  fail(`${connectionSummary(connection)}
5482
5646
  ${howToFix(connection) ?? ""}`.trim());
5483
5647
  }
5484
- if (connection.state !== "connected") {
5485
- opts.onLine(c.yellow(`${connectionSummary(connection)} Continuing because --allow-api-billing was passed.`));
5486
- }
5487
- const cwd = await repoCheckout(ctx);
5488
- const config = await loadConfig();
5489
- const sessionKey = `architect_session_${ctx.workspace.slug}`;
5490
- const previous = config[sessionKey];
5491
- const resumeSessionId = opts.resume && typeof previous === "string" ? previous : null;
5492
- if (opts.resume && !resumeSessionId) fail("No architect session to resume in this workspace.");
5493
- const endpoint = mcpEndpoint();
5494
- const mcp = mcpConfigArgs(endpoint, [
5495
- ...opts.readOnly ? ["--read-only"] : [],
5496
- "--workspace",
5497
- ctx.workspace.slug
5498
- ]);
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;
5499
5671
  const { data: runRow, error: runError } = await ctx.db.from("runs").insert({
5500
5672
  workspace_id: ctx.workspace.id,
5501
5673
  ticket_id: null,
5502
- agent_id: agent.id,
5674
+ agent_id: session.agent.id,
5503
5675
  kind: "architect",
5504
- provider: agent.provider,
5505
- model: agent.model,
5676
+ provider: session.agent.provider,
5677
+ model: session.agent.model,
5506
5678
  status: "running",
5507
5679
  started_at: (/* @__PURE__ */ new Date()).toISOString(),
5680
+ session_id: session.sessionId,
5508
5681
  host: null
5509
5682
  }).select("id").single();
5510
5683
  if (runError) fail(runError.message);
5511
- const prompt2 = architectPrompt({
5512
- ctx,
5513
- request: opts.request,
5514
- addendum: agent.prompt_addendum,
5515
- readOnly: opts.readOnly
5516
- });
5517
- const spawned = spawnArchitect({
5518
- prompt: prompt2,
5519
- cwd,
5520
- model: agent.model,
5521
- effort: agent.effort ?? "high",
5522
- mcp,
5523
- resumeSessionId
5524
- });
5525
5684
  let seq = 0;
5526
- const pending = [];
5685
+ const events = [];
5686
+ const record = (type, payload) => events.push({ run_id: runRow.id, seq: seq++, type, payload });
5527
5687
  const flush = async () => {
5528
- if (pending.length === 0) return;
5529
- const batch = pending.splice(0, pending.length);
5530
- await ctx.db.from("run_events").insert(batch);
5688
+ if (events.length) await ctx.db.from("run_events").insert(events);
5531
5689
  };
5532
- const ticker = setInterval(() => void flush(), 2e3);
5533
- const interrupt = () => spawned.kill();
5534
- process.once("SIGINT", interrupt);
5535
- for await (const event of spawned.events) {
5536
- pending.push({ run_id: runRow.id, seq: seq++, type: event.type, payload: event.payload });
5537
- const [line] = transcriptLines([{ id: String(seq), type: event.type, payload: event.payload }]);
5538
- if (!line) continue;
5539
- if (line.kind === "text") opts.onLine(line.title);
5540
- else if (line.kind === "error") opts.onLine(c.red(line.title));
5541
- else opts.onLine(c.dim(` ${line.title}`));
5542
- }
5543
- clearInterval(ticker);
5544
- await flush();
5545
- process.off("SIGINT", interrupt);
5546
- const result = await spawned.done;
5547
- await ctx.db.from("runs").update({
5548
- status: result.exitCode === 0 ? "succeeded" : "failed",
5549
- ended_at: (/* @__PURE__ */ new Date()).toISOString(),
5550
- exit_code: result.exitCode,
5551
- session_id: result.sessionId,
5552
- summary: result.exitCode === 0 ? "Architect session" : `Codex exited ${result.exitCode}`
5553
- }).eq("id", runRow.id);
5554
- if (result.sessionId) await saveConfig({ [sessionKey]: result.sessionId });
5555
- await ctx.audit(ctx.workspace.id, "architect", { subject: agent.display_name });
5556
- if (result.exitCode !== 0 && !isJsonMode()) {
5557
- opts.onLine(c.red(`codex exited ${result.exitCode}`));
5558
- }
5559
- 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
+ }
5560
5733
  }
5561
5734
 
5562
5735
  // src/architect/commands.ts
5736
+ init_connection();
5563
5737
  function slugOf6(command) {
5564
5738
  return command.optsWithGlobals().workspace;
5565
5739
  }
5740
+ async function askLine(prompt2) {
5741
+ const rl = createInterface2({ input: process.stdin, output: process.stderr });
5742
+ try {
5743
+ return await new Promise((resolve) => {
5744
+ rl.once("close", () => resolve(""));
5745
+ rl.question(prompt2).then(
5746
+ (answer) => resolve(answer.trim()),
5747
+ () => resolve("")
5748
+ );
5749
+ });
5750
+ } finally {
5751
+ rl.close();
5752
+ }
5753
+ }
5566
5754
  function report(connection) {
5567
5755
  emit(
5568
5756
  { codex: connection },
@@ -5571,39 +5759,68 @@ ${howToFix(connection) ?? ""}`.trim()
5571
5759
  );
5572
5760
  }
5573
5761
  function registerArchitectCommands(program) {
5574
- program.command("plan").argument("[request...]", "what to plan, assess, or change").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("--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) {
5575
5763
  const opts = this.opts();
5576
5764
  const ctx = await requireWorkspace(slugOf6(this));
5577
- let ask = request.join(" ").trim();
5578
- if (!ask) {
5579
- if (!process.stdin.isTTY) fail("Give the architect something to do.");
5580
- const rl = createInterface3({ input: process.stdin, output: process.stderr });
5581
- try {
5582
- ask = (await rl.question(c.blue("plan> "))).trim();
5583
- } finally {
5584
- rl.close();
5585
- }
5586
- if (!ask) fail("Nothing asked.");
5587
- }
5588
- const lines = [];
5589
- const result = await runArchitect({
5765
+ const first = request.join(" ").trim();
5766
+ const conversational = !first && !opts.once && process.stdin.isTTY && !isJsonMode();
5767
+ const session = await openArchitect({
5590
5768
  ctx,
5591
- request: ask,
5592
5769
  readOnly: Boolean(opts.readOnly),
5593
- resume: Boolean(opts.resume),
5594
- allowApiBilling: Boolean(opts.allowApiBilling),
5595
- onLine: (text) => {
5596
- lines.push(text);
5597
- if (!isJsonMode()) out(text);
5598
- }
5770
+ allowApiBilling: Boolean(opts.allowApiBilling)
5599
5771
  });
5600
- if (isJsonMode()) {
5601
- emit({ ok: result.exitCode === 0, run_id: result.runId, session_id: result.sessionId, output: lines }, () => "");
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)"));
5795
+ }
5796
+ }
5797
+ return result;
5798
+ };
5799
+ if (!conversational) {
5800
+ let ask = first;
5801
+ if (!ask) {
5802
+ if (!process.stdin.isTTY) fail("Give the architect something to do.");
5803
+ ask = await askLine("plan> ");
5804
+ if (!ask) fail("Nothing asked.");
5805
+ }
5806
+ const result = await turn(ask);
5807
+ if (isJsonMode()) {
5808
+ emit({ ok: true, run_id: result.runId, text: result.text, tool_calls: result.toolCalls }, () => "");
5809
+ }
5602
5810
  return;
5603
5811
  }
5604
- if (result.sessionId) {
5605
- out(c.dim(`
5606
- Continue with \`hd plan --resume "..."\`. Run ${result.runId.slice(0, 8)}.`));
5812
+ out(
5813
+ c.dim(
5814
+ `Talking to the architect (${session.agent.model}${session.repo ? ", repo readable" : ""}). Blank line or /exit leaves.`
5815
+ )
5816
+ );
5817
+ for (; ; ) {
5818
+ const ask = await askLine("\nplan> ");
5819
+ if (!ask || ask === "/exit" || ask === "/quit") {
5820
+ out(c.dim("Left the architect."));
5821
+ return;
5822
+ }
5823
+ await turn(ask);
5607
5824
  }
5608
5825
  });
5609
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() {
@@ -5639,19 +5856,34 @@ Run \`codex login\` in a terminal.`);
5639
5856
  report(connection);
5640
5857
  if (connection.state !== "connected") process.exitCode = 1;
5641
5858
  });
5642
- 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() {
5643
5860
  const connection = await codexConnection();
5644
- const endpoint = mcpEndpoint();
5861
+ const expiry = await tokenExpiry();
5645
5862
  emit(
5646
- { codex: connection, mcp: endpoint, config_args: mcpConfigArgs(endpoint) },
5863
+ { codex: connection, token_expires_in_hours: expiry },
5647
5864
  () => [
5648
- `${c.dim("codex")} ${connection.state === "connected" ? c.green(connectionSummary(connection)) : c.yellow(connectionSummary(connection))}`,
5649
- `${c.dim("mcp server")} ${endpoint.command} ${endpoint.args.join(" ")}`,
5650
- connection.state === "connected" ? c.dim("Architect runs bill the ChatGPT subscription, not per token.") : c.yellow(howToFix(connection) ?? "")
5651
- ].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")
5652
5869
  );
5653
5870
  });
5654
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
+ }
5655
5887
 
5656
5888
  // src/tui/commands.ts
5657
5889
  init_json();
@@ -5720,7 +5952,7 @@ ${runner.HD_HELP}`);
5720
5952
 
5721
5953
  // src/host/upgrade.ts
5722
5954
  import { execFile as execFile5 } from "child_process";
5723
- import { createInterface as createInterface4 } from "readline/promises";
5955
+ import { createInterface as createInterface3 } from "readline/promises";
5724
5956
  import { promisify as promisify5 } from "util";
5725
5957
  init_json();
5726
5958
  init_theme();
@@ -5769,7 +6001,7 @@ async function offerUpdate(now = Date.now()) {
5769
6001
  await saveUpdateState({ checked_at: now });
5770
6002
  if (!latest || !isNewer(latest, VERSION)) return;
5771
6003
  if (state.skipped === latest) return;
5772
- const rl = createInterface4({ input: process.stdin, output: process.stderr });
6004
+ const rl = createInterface3({ input: process.stdin, output: process.stderr });
5773
6005
  let answer = "";
5774
6006
  try {
5775
6007
  answer = (await rl.question(