@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.
- package/README.md +26 -12
- package/dist/index.js +775 -631
- 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
|
|
1220
|
-
if (file) return `${file}${
|
|
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
|
-
//
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
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
|
-
|
|
1812
|
-
return true;
|
|
1936
|
+
raw = readFileSync(authPath(), "utf8");
|
|
1813
1937
|
} catch {
|
|
1814
|
-
|
|
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
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
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
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
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
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
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
|
-
|
|
1846
|
-
|
|
1847
|
-
claude: "claude",
|
|
1848
|
-
codex: "codex",
|
|
1849
|
-
gemini: "agy",
|
|
1850
|
-
grok: "grok"
|
|
2193
|
+
init_connection();
|
|
2194
|
+
ChatGptAuthError = class extends Error {
|
|
1851
2195
|
};
|
|
1852
|
-
|
|
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
|
|
2272
|
-
import
|
|
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 =
|
|
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
|
|
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 ??
|
|
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 ||
|
|
2377
|
-
const worktreeRoot = flags.worktreeRoot || env.WORKTREE_ROOT ||
|
|
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(
|
|
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" ?
|
|
2407
|
-
const logPath =
|
|
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(
|
|
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
|
|
4540
|
-
const { error } = await ctx.db.storage.from(TICKET_IMAGES_BUCKET).upload(
|
|
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:
|
|
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([
|
|
4852
|
+
await ctx.db.storage.from(TICKET_IMAGES_BUCKET).remove([path8]);
|
|
4552
4853
|
fail(rowError.message);
|
|
4553
4854
|
}
|
|
4554
|
-
uploaded.push(
|
|
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
|
|
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.
|
|
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
|
-
|
|
5103
|
-
import {
|
|
5104
|
-
import {
|
|
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
|
|
5110
|
-
import {
|
|
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
|
-
//
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
function
|
|
5158
|
-
|
|
5159
|
-
const
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
}
|
|
5174
|
-
|
|
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
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
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
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
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
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
"
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
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
|
-
|
|
5339
|
-
|
|
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
|
-
|
|
5342
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
5586
|
+
const root = path6.join(homedir3(), ".cache", "higherdev", "architect");
|
|
5470
5587
|
await mkdir2(root, { recursive: true });
|
|
5471
|
-
const dir =
|
|
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
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
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
|
|
5610
|
+
function architectInstructions(opts) {
|
|
5496
5611
|
const { ctx } = opts;
|
|
5497
5612
|
return [
|
|
5498
|
-
"You are the HigherDEV architect
|
|
5499
|
-
"whether the platform is configured to build it.
|
|
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
|
-
"
|
|
5506
|
-
"
|
|
5507
|
-
"
|
|
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
|
-
"
|
|
5510
|
-
"- Prefer
|
|
5511
|
-
"
|
|
5512
|
-
"-
|
|
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
|
-
"
|
|
5517
|
-
"conventions, budgets, pausing. The orchestrator has no action for any of it.
|
|
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
|
|
5521
|
-
"Never use an em dash
|
|
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
|
|
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
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
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 =
|
|
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("--
|
|
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
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
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
|
|
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
|
|
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(
|
|
5686
|
-
|
|
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(
|
|
5820
|
+
out(c.dim("Left the architect."));
|
|
5691
5821
|
return;
|
|
5692
5822
|
}
|
|
5693
|
-
|
|
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
|
|
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
|
|
5861
|
+
const expiry = await tokenExpiry();
|
|
5733
5862
|
emit(
|
|
5734
|
-
{ codex: connection,
|
|
5863
|
+
{ codex: connection, token_expires_in_hours: expiry },
|
|
5735
5864
|
() => [
|
|
5736
|
-
`${c.dim("
|
|
5737
|
-
`${c.dim("
|
|
5738
|
-
connection.state === "connected" ? c.dim("
|
|
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
|
|
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 =
|
|
6004
|
+
const rl = createInterface3({ input: process.stdin, output: process.stderr });
|
|
5861
6005
|
let answer = "";
|
|
5862
6006
|
try {
|
|
5863
6007
|
answer = (await rl.question(
|