@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.
- package/README.md +41 -15
- package/dist/index.js +919 -687
- 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((
|
|
1197
|
-
const row = asRecord(
|
|
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
|
|
1214
|
-
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}`;
|
|
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
|
-
//
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
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
|
-
|
|
1758
|
-
return true;
|
|
1936
|
+
raw = readFileSync(authPath(), "utf8");
|
|
1759
1937
|
} catch {
|
|
1760
|
-
|
|
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
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
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;
|
|
1767
1957
|
}
|
|
1768
|
-
return found;
|
|
1769
1958
|
}
|
|
1770
|
-
|
|
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
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
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
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
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
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
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
|
-
|
|
1792
|
-
|
|
1793
|
-
claude: "claude",
|
|
1794
|
-
codex: "codex",
|
|
1795
|
-
gemini: "agy",
|
|
1796
|
-
grok: "grok"
|
|
2193
|
+
init_connection();
|
|
2194
|
+
ChatGptAuthError = class extends Error {
|
|
1797
2195
|
};
|
|
1798
|
-
|
|
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
|
|
2218
|
-
import
|
|
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 =
|
|
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
|
|
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 ??
|
|
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 ||
|
|
2323
|
-
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");
|
|
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(
|
|
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" ?
|
|
2353
|
-
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");
|
|
2354
2807
|
const plist = launchdPlist({ repoRoot, envFile, logPath });
|
|
2355
|
-
await ensureDir(
|
|
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
|
|
2738
|
-
|
|
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
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
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
|
|
2758
|
-
const
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
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
|
|
4486
|
-
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 });
|
|
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:
|
|
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([
|
|
4852
|
+
await ctx.db.storage.from(TICKET_IMAGES_BUCKET).remove([path8]);
|
|
4498
4853
|
fail(rowError.message);
|
|
4499
4854
|
}
|
|
4500
|
-
uploaded.push(
|
|
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
|
|
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.
|
|
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
|
-
|
|
5049
|
-
import {
|
|
5050
|
-
import {
|
|
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
|
|
5056
|
-
import {
|
|
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
|
-
//
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
function
|
|
5104
|
-
|
|
5105
|
-
const
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
}
|
|
5120
|
-
|
|
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
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
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
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
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
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
"
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
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
|
-
|
|
5285
|
-
|
|
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
|
-
|
|
5288
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
5586
|
+
const root = path6.join(homedir3(), ".cache", "higherdev", "architect");
|
|
5416
5587
|
await mkdir2(root, { recursive: true });
|
|
5417
|
-
const dir =
|
|
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
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
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
|
|
5610
|
+
function architectInstructions(opts) {
|
|
5442
5611
|
const { ctx } = opts;
|
|
5443
5612
|
return [
|
|
5444
|
-
"You are the HigherDEV architect
|
|
5445
|
-
"whether the platform is configured to build it.
|
|
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
|
-
"
|
|
5452
|
-
"
|
|
5453
|
-
"
|
|
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
|
-
"
|
|
5456
|
-
"- Prefer
|
|
5457
|
-
"
|
|
5458
|
-
"-
|
|
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
|
-
"
|
|
5463
|
-
"conventions, budgets, pausing. The orchestrator has no action for any of it.
|
|
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
|
|
5467
|
-
"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.",
|
|
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
|
|
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
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
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("--
|
|
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
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5601
|
-
|
|
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
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
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
|
|
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
|
|
5861
|
+
const expiry = await tokenExpiry();
|
|
5645
5862
|
emit(
|
|
5646
|
-
{ codex: connection,
|
|
5863
|
+
{ codex: connection, token_expires_in_hours: expiry },
|
|
5647
5864
|
() => [
|
|
5648
|
-
`${c.dim("
|
|
5649
|
-
`${c.dim("
|
|
5650
|
-
connection.state === "connected" ? c.dim("
|
|
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
|
|
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 =
|
|
6004
|
+
const rl = createInterface3({ input: process.stdin, output: process.stderr });
|
|
5773
6005
|
let answer = "";
|
|
5774
6006
|
try {
|
|
5775
6007
|
answer = (await rl.question(
|