@higherdev/cli 0.1.1 → 0.1.3
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 +30 -14
- package/dist/index.js +1014 -697
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -11,9 +11,10 @@ var __export = (target, all) => {
|
|
|
11
11
|
|
|
12
12
|
// ../../packages/db/src/enums.ts
|
|
13
13
|
function asEffort(value) {
|
|
14
|
-
|
|
14
|
+
const raw = typeof value === "string" ? value.trim() : "";
|
|
15
|
+
return efforts.includes(raw) ? raw : DEFAULT_EFFORT;
|
|
15
16
|
}
|
|
16
|
-
var ticketStatuses, epicStatuses, runStatuses, runKinds, runEventTypes, agentRoles, providers, efforts, messageRoles, messageToRoles, deliveries, createdBy;
|
|
17
|
+
var ticketStatuses, epicStatuses, runStatuses, runKinds, runEventTypes, agentRoles, providers, efforts, messageRoles, messageToRoles, deliveries, createdBy, DEFAULT_EFFORT;
|
|
17
18
|
var init_enums = __esm({
|
|
18
19
|
"../../packages/db/src/enums.ts"() {
|
|
19
20
|
"use strict";
|
|
@@ -43,13 +44,14 @@ var init_enums = __esm({
|
|
|
43
44
|
"error",
|
|
44
45
|
"status"
|
|
45
46
|
];
|
|
46
|
-
agentRoles = ["orchestrator", "reviewer", "builder"
|
|
47
|
+
agentRoles = ["orchestrator", "reviewer", "builder"];
|
|
47
48
|
providers = ["claude", "codex", "gemini", "grok"];
|
|
48
49
|
efforts = ["low", "medium", "high"];
|
|
49
50
|
messageRoles = ["human", "operator", "orchestrator", "reviewer", "builder", "architect"];
|
|
50
51
|
messageToRoles = [...messageRoles, "all"];
|
|
51
52
|
deliveries = ["queue", "interrupt"];
|
|
52
53
|
createdBy = ["human", "orchestrator", "api"];
|
|
54
|
+
DEFAULT_EFFORT = "high";
|
|
53
55
|
}
|
|
54
56
|
});
|
|
55
57
|
|
|
@@ -183,6 +185,8 @@ function auditSummary(actor, action, payload = {}) {
|
|
|
183
185
|
return key2 ? `${actor} took over ${key2}` : `${actor} took over a ticket`;
|
|
184
186
|
case "hand_back":
|
|
185
187
|
return key2 ? `${actor} handed back ${key2}` : `${actor} handed back a ticket`;
|
|
188
|
+
case "merge_override":
|
|
189
|
+
return key2 ? `${actor} authorized an optional-check merge for ${key2}` : `${actor} authorized an optional-check merge`;
|
|
186
190
|
case "create":
|
|
187
191
|
return key2 ? `${actor} created ${key2}` : `${actor} created ${subject(payload)}`;
|
|
188
192
|
case "update":
|
|
@@ -456,6 +460,36 @@ var init_cost = __esm({
|
|
|
456
460
|
}
|
|
457
461
|
});
|
|
458
462
|
|
|
463
|
+
// ../../packages/db/src/commands/lifecycle.ts
|
|
464
|
+
var lifecycleCommandRejectionCodes, lifecycleCommandResultCodes, TERMINAL_TICKET_STATUSES, OPTIONAL_CHECK_OVERRIDE_STATUSES, ticketStatusSet, terminalStatusSet, optionalCheckOverrideStatusSet;
|
|
465
|
+
var init_lifecycle = __esm({
|
|
466
|
+
"../../packages/db/src/commands/lifecycle.ts"() {
|
|
467
|
+
"use strict";
|
|
468
|
+
init_enums();
|
|
469
|
+
lifecycleCommandRejectionCodes = [
|
|
470
|
+
"illegal_transition",
|
|
471
|
+
"terminal_target",
|
|
472
|
+
"stale_state",
|
|
473
|
+
"malformed_intent",
|
|
474
|
+
"duplicate_command_identity"
|
|
475
|
+
];
|
|
476
|
+
lifecycleCommandResultCodes = ["accepted", ...lifecycleCommandRejectionCodes];
|
|
477
|
+
TERMINAL_TICKET_STATUSES = ["merged", "cancelled"];
|
|
478
|
+
OPTIONAL_CHECK_OVERRIDE_STATUSES = ["approved", "needs_decision"];
|
|
479
|
+
ticketStatusSet = new Set(ticketStatuses);
|
|
480
|
+
terminalStatusSet = new Set(TERMINAL_TICKET_STATUSES);
|
|
481
|
+
optionalCheckOverrideStatusSet = new Set(OPTIONAL_CHECK_OVERRIDE_STATUSES);
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// ../../packages/db/src/commands/index.ts
|
|
486
|
+
var init_commands = __esm({
|
|
487
|
+
"../../packages/db/src/commands/index.ts"() {
|
|
488
|
+
"use strict";
|
|
489
|
+
init_lifecycle();
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
|
|
459
493
|
// ../../packages/db/src/defaults.ts
|
|
460
494
|
function slugify(name) {
|
|
461
495
|
const slug = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -467,9 +501,19 @@ function hostAllowsRepo(allowedRepoOwners, repo) {
|
|
|
467
501
|
if (!owner) return false;
|
|
468
502
|
return allowedRepoOwners.some((item) => item.toLowerCase() === owner);
|
|
469
503
|
}
|
|
504
|
+
var DEFAULT_AGENTS;
|
|
470
505
|
var init_defaults = __esm({
|
|
471
506
|
"../../packages/db/src/defaults.ts"() {
|
|
472
507
|
"use strict";
|
|
508
|
+
init_enums();
|
|
509
|
+
DEFAULT_AGENTS = [
|
|
510
|
+
{ role: "orchestrator", provider: "claude", model: "claude-fable-5", display_name: "Orchestrator", effort: DEFAULT_EFFORT },
|
|
511
|
+
{ role: "reviewer", provider: "claude", model: "claude-opus-5", display_name: "Reviewer", effort: DEFAULT_EFFORT },
|
|
512
|
+
{ role: "builder", provider: "claude", model: "claude-sonnet-5", display_name: "Claude", effort: DEFAULT_EFFORT },
|
|
513
|
+
{ role: "builder", provider: "codex", model: "gpt-5.6-sol", display_name: "Codex", effort: DEFAULT_EFFORT },
|
|
514
|
+
{ role: "builder", provider: "gemini", model: "gemini-3.7-flash-high", display_name: "Gemini", effort: DEFAULT_EFFORT },
|
|
515
|
+
{ role: "builder", provider: "grok", model: "grok-4.6", display_name: "Grok", effort: DEFAULT_EFFORT }
|
|
516
|
+
];
|
|
473
517
|
}
|
|
474
518
|
});
|
|
475
519
|
|
|
@@ -691,7 +735,7 @@ var init_schemas = __esm({
|
|
|
691
735
|
role: z.enum(agentRoles),
|
|
692
736
|
provider: z.enum(providers),
|
|
693
737
|
model: z.string().min(1),
|
|
694
|
-
effort: z.enum(efforts).default(
|
|
738
|
+
effort: z.enum(efforts).default(DEFAULT_EFFORT),
|
|
695
739
|
display_name: z.string().min(1),
|
|
696
740
|
enabled: z.boolean(),
|
|
697
741
|
routing_notes: z.string().default(""),
|
|
@@ -746,6 +790,7 @@ var init_schemas = __esm({
|
|
|
746
790
|
next_review_at: isoDateSchema.nullable(),
|
|
747
791
|
created_by: z.enum(createdBy),
|
|
748
792
|
last_error: z.string().nullable(),
|
|
793
|
+
merge_override_optional_checks: z.boolean().default(false),
|
|
749
794
|
created_at: isoDateSchema,
|
|
750
795
|
updated_at: isoDateSchema
|
|
751
796
|
});
|
|
@@ -835,7 +880,8 @@ var init_schemas = __esm({
|
|
|
835
880
|
"dismiss",
|
|
836
881
|
"reroute",
|
|
837
882
|
"takeover",
|
|
838
|
-
"hand_back"
|
|
883
|
+
"hand_back",
|
|
884
|
+
"merge_override"
|
|
839
885
|
]);
|
|
840
886
|
auditEventSchema = z.object({
|
|
841
887
|
id: uuidSchema,
|
|
@@ -1036,6 +1082,32 @@ var init_templates = __esm({
|
|
|
1036
1082
|
}
|
|
1037
1083
|
});
|
|
1038
1084
|
|
|
1085
|
+
// ../../packages/db/src/tickets.ts
|
|
1086
|
+
function uncertainCreateError(error) {
|
|
1087
|
+
return Boolean(error && (!error.code || /^PGRST00[0-3]$/.test(error.code)));
|
|
1088
|
+
}
|
|
1089
|
+
async function createTicketRecord(db, workspaceId, fields, ticketId) {
|
|
1090
|
+
const requestId = ticketId ?? globalThis.crypto.randomUUID();
|
|
1091
|
+
const args = {
|
|
1092
|
+
p_ticket_id: requestId,
|
|
1093
|
+
p_workspace_id: workspaceId,
|
|
1094
|
+
p_fields: fields
|
|
1095
|
+
};
|
|
1096
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
1097
|
+
try {
|
|
1098
|
+
const result = await db.rpc("create_ticket", args);
|
|
1099
|
+
if (!result.error || attempt === 1 || !uncertainCreateError(result.error)) return result;
|
|
1100
|
+
} catch (error) {
|
|
1101
|
+
if (attempt === 1) throw error;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
var init_tickets = __esm({
|
|
1106
|
+
"../../packages/db/src/tickets.ts"() {
|
|
1107
|
+
"use strict";
|
|
1108
|
+
}
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1039
1111
|
// ../../packages/db/src/ticket-writes.ts
|
|
1040
1112
|
async function workspaceBySlug(db, slug) {
|
|
1041
1113
|
const { data, error } = await db.from("workspaces").select("*").eq("slug", slug).maybeSingle();
|
|
@@ -1134,7 +1206,13 @@ function buildTimeline(input) {
|
|
|
1134
1206
|
id: `run:${run5.id}`,
|
|
1135
1207
|
at,
|
|
1136
1208
|
kind,
|
|
1137
|
-
title: [
|
|
1209
|
+
title: [
|
|
1210
|
+
run5.kind,
|
|
1211
|
+
run5.provider,
|
|
1212
|
+
run5.status,
|
|
1213
|
+
run5.reviewed_sha ? `@ ${run5.reviewed_sha.slice(0, 12)}` : "",
|
|
1214
|
+
span
|
|
1215
|
+
].filter(Boolean).join(" "),
|
|
1138
1216
|
detail: run5.summary ? clip(run5.summary) : void 0,
|
|
1139
1217
|
durationMs: runDurationMs(run5, nowIso),
|
|
1140
1218
|
endedAt: run5.ended_at
|
|
@@ -1216,8 +1294,8 @@ function toolDetail(payload) {
|
|
|
1216
1294
|
const command = str(input.command || input.cmd);
|
|
1217
1295
|
const additions = Number(input.additions ?? input.added);
|
|
1218
1296
|
const deletions = Number(input.deletions ?? input.removed);
|
|
1219
|
-
const
|
|
1220
|
-
if (file) return `${file}${
|
|
1297
|
+
const stat2 = Number.isFinite(additions) || Number.isFinite(deletions) ? ` +${Number.isFinite(additions) ? additions : 0}/-${Number.isFinite(deletions) ? deletions : 0}` : "";
|
|
1298
|
+
if (file) return `${file}${stat2}`;
|
|
1221
1299
|
if (command) return command.length > 80 ? `${command.slice(0, 77)}...` : command;
|
|
1222
1300
|
return "";
|
|
1223
1301
|
}
|
|
@@ -1388,6 +1466,7 @@ var init_src = __esm({
|
|
|
1388
1466
|
init_blocked();
|
|
1389
1467
|
init_budgets();
|
|
1390
1468
|
init_cost();
|
|
1469
|
+
init_commands();
|
|
1391
1470
|
init_defaults();
|
|
1392
1471
|
init_enums();
|
|
1393
1472
|
init_epic_progress();
|
|
@@ -1399,6 +1478,7 @@ var init_src = __esm({
|
|
|
1399
1478
|
init_status();
|
|
1400
1479
|
init_stuck();
|
|
1401
1480
|
init_templates();
|
|
1481
|
+
init_tickets();
|
|
1402
1482
|
init_ticket_writes();
|
|
1403
1483
|
init_timeline();
|
|
1404
1484
|
init_transcript();
|
|
@@ -1519,6 +1599,119 @@ var init_theme = __esm({
|
|
|
1519
1599
|
}
|
|
1520
1600
|
});
|
|
1521
1601
|
|
|
1602
|
+
// src/architect/connection.ts
|
|
1603
|
+
var connection_exports = {};
|
|
1604
|
+
__export(connection_exports, {
|
|
1605
|
+
classifyAuthFile: () => classifyAuthFile,
|
|
1606
|
+
codexConnection: () => codexConnection,
|
|
1607
|
+
codexHome: () => codexHome,
|
|
1608
|
+
connectCodex: () => connectCodex,
|
|
1609
|
+
connectionSummary: () => connectionSummary,
|
|
1610
|
+
howToFix: () => howToFix,
|
|
1611
|
+
onPath: () => onPath
|
|
1612
|
+
});
|
|
1613
|
+
import { execFile, spawn } from "child_process";
|
|
1614
|
+
import { access, readFile as readFile2 } from "fs/promises";
|
|
1615
|
+
import { constants } from "fs";
|
|
1616
|
+
import { homedir as homedir2 } from "os";
|
|
1617
|
+
import path2 from "path";
|
|
1618
|
+
import { promisify } from "util";
|
|
1619
|
+
function codexHome(env = process.env) {
|
|
1620
|
+
return env.CODEX_HOME ?? path2.join(homedir2(), ".codex");
|
|
1621
|
+
}
|
|
1622
|
+
function classifyAuthFile(auth) {
|
|
1623
|
+
if (!auth) return null;
|
|
1624
|
+
const apiKey = typeof auth.OPENAI_API_KEY === "string" ? auth.OPENAI_API_KEY.trim() : "";
|
|
1625
|
+
if (apiKey) {
|
|
1626
|
+
return {
|
|
1627
|
+
state: "api_key",
|
|
1628
|
+
detail: "Codex is signed in with an API key, which bills per token."
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
const refresh = auth.tokens?.refresh_token;
|
|
1632
|
+
if (auth.auth_mode === "chatgpt" && typeof refresh === "string" && refresh.length > 0) {
|
|
1633
|
+
const accountId = typeof auth.tokens?.account_id === "string" ? auth.tokens.account_id : null;
|
|
1634
|
+
return { state: "connected", accountId, detail: "Codex is connected to a ChatGPT subscription." };
|
|
1635
|
+
}
|
|
1636
|
+
return { state: "signed_out", detail: "Codex is installed but not signed in." };
|
|
1637
|
+
}
|
|
1638
|
+
async function onPath(bin, env = process.env) {
|
|
1639
|
+
const dirs = (env.PATH ?? "").split(path2.delimiter).filter(Boolean);
|
|
1640
|
+
for (const dir of dirs) {
|
|
1641
|
+
try {
|
|
1642
|
+
await access(path2.join(dir, bin), constants.X_OK);
|
|
1643
|
+
return true;
|
|
1644
|
+
} catch {
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
return false;
|
|
1648
|
+
}
|
|
1649
|
+
async function codexConnection(env = process.env) {
|
|
1650
|
+
if (!await onPath("codex")) {
|
|
1651
|
+
return {
|
|
1652
|
+
state: "not_installed",
|
|
1653
|
+
detail: "The Codex CLI is not on PATH. Install it, then run `hd connect`."
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
let auth = null;
|
|
1657
|
+
try {
|
|
1658
|
+
auth = JSON.parse(await readFile2(path2.join(codexHome(env), "auth.json"), "utf8"));
|
|
1659
|
+
} catch {
|
|
1660
|
+
auth = null;
|
|
1661
|
+
}
|
|
1662
|
+
const fromFile = classifyAuthFile(auth);
|
|
1663
|
+
if (fromFile) return fromFile;
|
|
1664
|
+
try {
|
|
1665
|
+
const { stdout } = await run("codex", ["login", "status"], { timeout: 2e4 });
|
|
1666
|
+
const text = stdout.trim();
|
|
1667
|
+
if (/chatgpt/i.test(text)) {
|
|
1668
|
+
return { state: "connected", accountId: null, detail: text };
|
|
1669
|
+
}
|
|
1670
|
+
if (/api key/i.test(text)) {
|
|
1671
|
+
return { state: "api_key", detail: text };
|
|
1672
|
+
}
|
|
1673
|
+
return { state: "signed_out", detail: text || "Codex is not signed in." };
|
|
1674
|
+
} catch {
|
|
1675
|
+
return { state: "signed_out", detail: "Codex is installed but not signed in." };
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
function connectionSummary(connection) {
|
|
1679
|
+
switch (connection.state) {
|
|
1680
|
+
case "connected":
|
|
1681
|
+
return connection.accountId ? `${connection.detail} Account ${connection.accountId}.` : connection.detail;
|
|
1682
|
+
default:
|
|
1683
|
+
return connection.detail;
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
function howToFix(connection) {
|
|
1687
|
+
switch (connection.state) {
|
|
1688
|
+
case "connected":
|
|
1689
|
+
return null;
|
|
1690
|
+
case "not_installed":
|
|
1691
|
+
return "Install the Codex CLI, then run `hd connect`.";
|
|
1692
|
+
case "api_key":
|
|
1693
|
+
return "Run `codex logout` then `hd connect` to sign in with the subscription instead.";
|
|
1694
|
+
default:
|
|
1695
|
+
return "Run `hd connect` to sign in through ChatGPT.";
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
async function connectCodex() {
|
|
1699
|
+
const code = await new Promise((resolve, reject) => {
|
|
1700
|
+
const child = spawn("codex", ["login"], { stdio: "inherit" });
|
|
1701
|
+
child.on("error", reject);
|
|
1702
|
+
child.on("exit", (status) => resolve(status ?? 1));
|
|
1703
|
+
});
|
|
1704
|
+
if (code !== 0) throw new Error(`codex login exited ${code}`);
|
|
1705
|
+
return codexConnection();
|
|
1706
|
+
}
|
|
1707
|
+
var run;
|
|
1708
|
+
var init_connection = __esm({
|
|
1709
|
+
"src/architect/connection.ts"() {
|
|
1710
|
+
"use strict";
|
|
1711
|
+
run = promisify(execFile);
|
|
1712
|
+
}
|
|
1713
|
+
});
|
|
1714
|
+
|
|
1522
1715
|
// src/read/queries.ts
|
|
1523
1716
|
function unwrap(result) {
|
|
1524
1717
|
if (result.error) fail(result.error.message);
|
|
@@ -1803,53 +1996,289 @@ var init_subscribe = __esm({
|
|
|
1803
1996
|
}
|
|
1804
1997
|
});
|
|
1805
1998
|
|
|
1806
|
-
//
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1999
|
+
// src/architect/chatgpt.ts
|
|
2000
|
+
var chatgpt_exports = {};
|
|
2001
|
+
__export(chatgpt_exports, {
|
|
2002
|
+
ChatGptAuthError: () => ChatGptAuthError,
|
|
2003
|
+
MAX_STEPS: () => MAX_STEPS,
|
|
2004
|
+
getAuth: () => getAuth,
|
|
2005
|
+
mergeRefreshed: () => mergeRefreshed,
|
|
2006
|
+
msUntilExpiry: () => msUntilExpiry,
|
|
2007
|
+
newSessionId: () => newSessionId,
|
|
2008
|
+
runAgent: () => runAgent
|
|
2009
|
+
});
|
|
2010
|
+
import { readFileSync, renameSync, writeFileSync } from "fs";
|
|
2011
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
2012
|
+
import path4 from "path";
|
|
2013
|
+
function readAuthFile() {
|
|
2014
|
+
let raw;
|
|
1810
2015
|
try {
|
|
1811
|
-
|
|
1812
|
-
return true;
|
|
2016
|
+
raw = readFileSync(authPath(), "utf8");
|
|
1813
2017
|
} catch {
|
|
1814
|
-
|
|
2018
|
+
throw new ChatGptAuthError(
|
|
2019
|
+
"No ChatGPT session found. Install the Codex CLI (npm i -g @openai/codex), then run `hd connect`."
|
|
2020
|
+
);
|
|
2021
|
+
}
|
|
2022
|
+
try {
|
|
2023
|
+
return JSON.parse(raw);
|
|
2024
|
+
} catch {
|
|
2025
|
+
throw new ChatGptAuthError(`${authPath()} is unreadable. Run \`hd connect\`.`);
|
|
1815
2026
|
}
|
|
1816
2027
|
}
|
|
1817
|
-
function
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
if (
|
|
2028
|
+
function msUntilExpiry(jwt, now = Date.now()) {
|
|
2029
|
+
try {
|
|
2030
|
+
const payload = jwt.split(".")[1];
|
|
2031
|
+
if (!payload) return Number.POSITIVE_INFINITY;
|
|
2032
|
+
const claims = JSON.parse(Buffer.from(payload, "base64").toString("utf8"));
|
|
2033
|
+
if (typeof claims.exp !== "number") return Number.POSITIVE_INFINITY;
|
|
2034
|
+
return claims.exp * 1e3 - now;
|
|
2035
|
+
} catch {
|
|
2036
|
+
return Number.POSITIVE_INFINITY;
|
|
1821
2037
|
}
|
|
1822
|
-
return found;
|
|
1823
2038
|
}
|
|
1824
|
-
|
|
2039
|
+
function mergeRefreshed(file, body, at = /* @__PURE__ */ new Date()) {
|
|
2040
|
+
return {
|
|
2041
|
+
...file,
|
|
2042
|
+
tokens: {
|
|
2043
|
+
...file.tokens,
|
|
2044
|
+
access_token: body.access_token,
|
|
2045
|
+
// Refreshing rotates this. Dropping the new value would leave both this
|
|
2046
|
+
// CLI and `codex login` holding a token the server has retired.
|
|
2047
|
+
...body.refresh_token ? { refresh_token: body.refresh_token } : {},
|
|
2048
|
+
...body.id_token ? { id_token: body.id_token } : {}
|
|
2049
|
+
},
|
|
2050
|
+
last_refresh: at.toISOString()
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
async function refreshSession(file) {
|
|
2054
|
+
const refresh = file.tokens?.refresh_token;
|
|
2055
|
+
if (!refresh) throw new ChatGptAuthError("Your ChatGPT session has expired. Run `hd connect`.");
|
|
2056
|
+
let res;
|
|
1825
2057
|
try {
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
2058
|
+
res = await fetch(TOKEN_ENDPOINT, {
|
|
2059
|
+
method: "POST",
|
|
2060
|
+
headers: { "Content-Type": "application/json" },
|
|
2061
|
+
body: JSON.stringify({
|
|
2062
|
+
grant_type: "refresh_token",
|
|
2063
|
+
refresh_token: refresh,
|
|
2064
|
+
client_id: CLIENT_ID,
|
|
2065
|
+
scope: "openid profile email"
|
|
2066
|
+
})
|
|
1830
2067
|
});
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
2068
|
+
} catch {
|
|
2069
|
+
throw new ChatGptAuthError("Could not reach OpenAI to renew your ChatGPT session.");
|
|
2070
|
+
}
|
|
2071
|
+
if (!res.ok) throw new ChatGptAuthError("Your ChatGPT session could not be renewed. Run `hd connect`.");
|
|
2072
|
+
const body = await res.json();
|
|
2073
|
+
if (!body.access_token) {
|
|
2074
|
+
throw new ChatGptAuthError("Your ChatGPT session could not be renewed. Run `hd connect`.");
|
|
2075
|
+
}
|
|
2076
|
+
const merged = mergeRefreshed(file, body);
|
|
2077
|
+
const target = authPath();
|
|
2078
|
+
const tmp = `${target}.hd.tmp`;
|
|
2079
|
+
try {
|
|
2080
|
+
writeFileSync(tmp, `${JSON.stringify(merged, null, 2)}
|
|
2081
|
+
`, { mode: 384 });
|
|
2082
|
+
renameSync(tmp, target);
|
|
2083
|
+
} catch {
|
|
2084
|
+
}
|
|
2085
|
+
return merged;
|
|
2086
|
+
}
|
|
2087
|
+
async function getAuth(force = false) {
|
|
2088
|
+
let file = readAuthFile();
|
|
2089
|
+
const accessToken = file.tokens?.access_token;
|
|
2090
|
+
const accountId = file.tokens?.account_id;
|
|
2091
|
+
if (!accessToken || !accountId) {
|
|
2092
|
+
throw new ChatGptAuthError("No ChatGPT subscription token found. Run `hd connect`.");
|
|
2093
|
+
}
|
|
2094
|
+
if (force || msUntilExpiry(accessToken) < RENEW_BEFORE_MS) {
|
|
2095
|
+
file = await refreshSession(file);
|
|
2096
|
+
}
|
|
2097
|
+
return {
|
|
2098
|
+
accessToken: file.tokens?.access_token ?? accessToken,
|
|
2099
|
+
accountId: file.tokens?.account_id ?? accountId
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
async function streamTurn(opts) {
|
|
2103
|
+
const res = await fetch(ENDPOINT, {
|
|
2104
|
+
method: "POST",
|
|
2105
|
+
signal: opts.signal,
|
|
2106
|
+
headers: {
|
|
2107
|
+
"Content-Type": "application/json",
|
|
2108
|
+
Authorization: `Bearer ${opts.auth.accessToken}`,
|
|
2109
|
+
"chatgpt-account-id": opts.auth.accountId,
|
|
2110
|
+
"OpenAI-Beta": "responses=experimental",
|
|
2111
|
+
originator: "codex_cli_rs",
|
|
2112
|
+
session_id: opts.sessionId,
|
|
2113
|
+
Accept: "text/event-stream"
|
|
2114
|
+
},
|
|
2115
|
+
body: JSON.stringify({
|
|
2116
|
+
model: opts.model,
|
|
2117
|
+
instructions: opts.instructions,
|
|
2118
|
+
input: opts.input,
|
|
2119
|
+
tools: opts.tools.map((tool) => ({
|
|
2120
|
+
type: "function",
|
|
2121
|
+
name: tool.name,
|
|
2122
|
+
description: tool.description,
|
|
2123
|
+
strict: false,
|
|
2124
|
+
parameters: tool.parameters ?? { type: "object", properties: {} }
|
|
2125
|
+
})),
|
|
2126
|
+
tool_choice: "auto",
|
|
2127
|
+
parallel_tool_calls: false,
|
|
2128
|
+
store: false,
|
|
2129
|
+
stream: true
|
|
2130
|
+
})
|
|
2131
|
+
});
|
|
2132
|
+
if (res.status === 401 || res.status === 403) {
|
|
2133
|
+
throw new ChatGptAuthError("ChatGPT rejected the session. Run `hd connect`.");
|
|
2134
|
+
}
|
|
2135
|
+
if (!res.ok) {
|
|
2136
|
+
const body = (await res.text().catch(() => "")).slice(0, 300);
|
|
2137
|
+
throw new Error(`ChatGPT request failed (${res.status}): ${body || res.statusText}`);
|
|
2138
|
+
}
|
|
2139
|
+
if (!res.body) throw new Error("ChatGPT returned no body.");
|
|
2140
|
+
const out2 = { text: "", calls: [], usage: null };
|
|
2141
|
+
const reader = res.body.getReader();
|
|
2142
|
+
const decoder = new TextDecoder();
|
|
2143
|
+
let buf = "";
|
|
2144
|
+
for (; ; ) {
|
|
2145
|
+
const { done, value } = await reader.read();
|
|
2146
|
+
if (done) break;
|
|
2147
|
+
buf += decoder.decode(value, { stream: true });
|
|
2148
|
+
let nl;
|
|
2149
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
2150
|
+
const line = buf.slice(0, nl).trim();
|
|
2151
|
+
buf = buf.slice(nl + 1);
|
|
2152
|
+
if (!line.startsWith("data:")) continue;
|
|
2153
|
+
const payload = line.slice(5).trim();
|
|
2154
|
+
if (!payload || payload === "[DONE]") continue;
|
|
2155
|
+
let event;
|
|
2156
|
+
try {
|
|
2157
|
+
event = JSON.parse(payload);
|
|
2158
|
+
} catch {
|
|
2159
|
+
continue;
|
|
2160
|
+
}
|
|
2161
|
+
if (event.type === "response.output_text.delta" && event.delta) {
|
|
2162
|
+
out2.text += event.delta;
|
|
2163
|
+
opts.onText?.(event.delta);
|
|
2164
|
+
} else if (event.type === "response.output_item.done" && event.item?.type === "function_call") {
|
|
2165
|
+
out2.calls.push({
|
|
2166
|
+
name: String(event.item.name ?? ""),
|
|
2167
|
+
arguments: String(event.item.arguments ?? "{}"),
|
|
2168
|
+
call_id: String(event.item.call_id ?? "")
|
|
2169
|
+
});
|
|
2170
|
+
} else if (event.type === "response.completed" && event.response?.usage) {
|
|
2171
|
+
out2.usage = event.response.usage;
|
|
2172
|
+
} else if (event.type === "response.failed") {
|
|
2173
|
+
throw new Error(event.response?.error?.message ?? "ChatGPT reported a failed response.");
|
|
2174
|
+
}
|
|
1837
2175
|
}
|
|
1838
|
-
return "";
|
|
1839
2176
|
}
|
|
2177
|
+
return out2;
|
|
1840
2178
|
}
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
2179
|
+
async function runAgent(opts) {
|
|
2180
|
+
let auth = await getAuth();
|
|
2181
|
+
const byName = new Map(opts.tools.map((tool) => [tool.name, tool]));
|
|
2182
|
+
const specs = opts.tools.map((tool) => ({
|
|
2183
|
+
name: tool.name,
|
|
2184
|
+
description: tool.description,
|
|
2185
|
+
parameters: tool.parameters
|
|
2186
|
+
}));
|
|
2187
|
+
const input = [];
|
|
2188
|
+
for (const message of opts.history.slice(-20)) {
|
|
2189
|
+
input.push({
|
|
2190
|
+
type: "message",
|
|
2191
|
+
role: message.role,
|
|
2192
|
+
content: [
|
|
2193
|
+
{ type: message.role === "user" ? "input_text" : "output_text", text: message.content }
|
|
2194
|
+
]
|
|
2195
|
+
});
|
|
2196
|
+
}
|
|
2197
|
+
input.push({ type: "message", role: "user", content: [{ type: "input_text", text: opts.prompt }] });
|
|
2198
|
+
let text = "";
|
|
2199
|
+
let usage = null;
|
|
2200
|
+
let renewed = false;
|
|
2201
|
+
let toolCalls = 0;
|
|
2202
|
+
let step = 0;
|
|
2203
|
+
const maxSteps = opts.maxSteps ?? MAX_STEPS;
|
|
2204
|
+
for (; step < maxSteps; step += 1) {
|
|
2205
|
+
let turn;
|
|
2206
|
+
const send = () => streamTurn({
|
|
2207
|
+
auth,
|
|
2208
|
+
sessionId: opts.sessionId,
|
|
2209
|
+
model: opts.model,
|
|
2210
|
+
instructions: opts.instructions,
|
|
2211
|
+
input,
|
|
2212
|
+
tools: specs,
|
|
2213
|
+
onText: (delta) => opts.onEvent?.({ kind: "text", delta }),
|
|
2214
|
+
signal: opts.signal
|
|
2215
|
+
});
|
|
2216
|
+
try {
|
|
2217
|
+
turn = await send();
|
|
2218
|
+
} catch (error) {
|
|
2219
|
+
if (error instanceof ChatGptAuthError && !renewed) {
|
|
2220
|
+
renewed = true;
|
|
2221
|
+
auth = await getAuth(true);
|
|
2222
|
+
turn = await send();
|
|
2223
|
+
} else {
|
|
2224
|
+
throw error;
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
if (turn.usage) usage = turn.usage;
|
|
2228
|
+
if (turn.text) text = turn.text;
|
|
2229
|
+
if (turn.calls.length === 0) break;
|
|
2230
|
+
for (const call of turn.calls) {
|
|
2231
|
+
toolCalls += 1;
|
|
2232
|
+
let args = {};
|
|
2233
|
+
try {
|
|
2234
|
+
args = JSON.parse(call.arguments || "{}");
|
|
2235
|
+
} catch {
|
|
2236
|
+
}
|
|
2237
|
+
opts.onEvent?.({ kind: "tool", name: call.name, args });
|
|
2238
|
+
const tool = byName.get(call.name);
|
|
2239
|
+
let output;
|
|
2240
|
+
let ok2 = true;
|
|
2241
|
+
if (!tool) {
|
|
2242
|
+
ok2 = false;
|
|
2243
|
+
output = `Error: no tool named ${call.name}.`;
|
|
2244
|
+
} else {
|
|
2245
|
+
try {
|
|
2246
|
+
const result = await tool.run(args);
|
|
2247
|
+
output = typeof result === "string" ? result : JSON.stringify(result ?? null);
|
|
2248
|
+
if (!output) output = "(no result)";
|
|
2249
|
+
} catch (error) {
|
|
2250
|
+
ok2 = false;
|
|
2251
|
+
output = `Error: ${error instanceof Error ? error.message : String(error)}`;
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
opts.onEvent?.({ kind: "tool_result", name: call.name, ok: ok2, detail: output.slice(0, 200) });
|
|
2255
|
+
input.push({
|
|
2256
|
+
type: "function_call",
|
|
2257
|
+
name: call.name,
|
|
2258
|
+
arguments: call.arguments,
|
|
2259
|
+
call_id: call.call_id
|
|
2260
|
+
});
|
|
2261
|
+
input.push({ type: "function_call_output", call_id: call.call_id, output });
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
return { text: text.trim(), usage, steps: step + 1, toolCalls };
|
|
2265
|
+
}
|
|
2266
|
+
function newSessionId() {
|
|
2267
|
+
return randomUUID2();
|
|
2268
|
+
}
|
|
2269
|
+
var ChatGptAuthError, ENDPOINT, TOKEN_ENDPOINT, CLIENT_ID, RENEW_BEFORE_MS, authPath, MAX_STEPS;
|
|
2270
|
+
var init_chatgpt = __esm({
|
|
2271
|
+
"src/architect/chatgpt.ts"() {
|
|
1844
2272
|
"use strict";
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
claude: "claude",
|
|
1848
|
-
codex: "codex",
|
|
1849
|
-
gemini: "agy",
|
|
1850
|
-
grok: "grok"
|
|
2273
|
+
init_connection();
|
|
2274
|
+
ChatGptAuthError = class extends Error {
|
|
1851
2275
|
};
|
|
1852
|
-
|
|
2276
|
+
ENDPOINT = "https://chatgpt.com/backend-api/codex/responses";
|
|
2277
|
+
TOKEN_ENDPOINT = "https://auth.openai.com/oauth/token";
|
|
2278
|
+
CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
2279
|
+
RENEW_BEFORE_MS = 60 * 60 * 1e3;
|
|
2280
|
+
authPath = () => path4.join(codexHome(), "auth.json");
|
|
2281
|
+
MAX_STEPS = 16;
|
|
1853
2282
|
}
|
|
1854
2283
|
});
|
|
1855
2284
|
|
|
@@ -2061,6 +2490,56 @@ var init_App = __esm({
|
|
|
2061
2490
|
}
|
|
2062
2491
|
});
|
|
2063
2492
|
|
|
2493
|
+
// ../runner/src/providers.ts
|
|
2494
|
+
import { execFile as execFile3, execFileSync } from "child_process";
|
|
2495
|
+
import { promisify as promisify3 } from "util";
|
|
2496
|
+
function isOnPath(bin) {
|
|
2497
|
+
try {
|
|
2498
|
+
execFileSync("which", [bin], { stdio: "ignore" });
|
|
2499
|
+
return true;
|
|
2500
|
+
} catch {
|
|
2501
|
+
return false;
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
function detectProviders() {
|
|
2505
|
+
const found = [];
|
|
2506
|
+
for (const [provider, bin] of Object.entries(PROVIDER_BINS)) {
|
|
2507
|
+
if (isOnPath(bin)) found.push(provider);
|
|
2508
|
+
}
|
|
2509
|
+
return found;
|
|
2510
|
+
}
|
|
2511
|
+
async function captureCli(bin, args, timeoutMs = MODEL_DISCOVER_TIMEOUT_MS) {
|
|
2512
|
+
try {
|
|
2513
|
+
const { stdout, stderr } = await execFileAsync(bin, args, {
|
|
2514
|
+
encoding: "utf8",
|
|
2515
|
+
timeout: timeoutMs,
|
|
2516
|
+
maxBuffer: 512 * 1024
|
|
2517
|
+
});
|
|
2518
|
+
return [stdout, stderr].filter(Boolean).join("\n");
|
|
2519
|
+
} catch (error) {
|
|
2520
|
+
if (error && typeof error === "object") {
|
|
2521
|
+
const err = error;
|
|
2522
|
+
const text = [err.stdout, err.stderr].filter(Boolean).join("\n").trim();
|
|
2523
|
+
if (text) return text;
|
|
2524
|
+
}
|
|
2525
|
+
return "";
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
var execFileAsync, PROVIDER_BINS, MODEL_DISCOVER_TIMEOUT_MS;
|
|
2529
|
+
var init_providers = __esm({
|
|
2530
|
+
"../runner/src/providers.ts"() {
|
|
2531
|
+
"use strict";
|
|
2532
|
+
execFileAsync = promisify3(execFile3);
|
|
2533
|
+
PROVIDER_BINS = {
|
|
2534
|
+
claude: "claude",
|
|
2535
|
+
codex: "codex",
|
|
2536
|
+
gemini: "agy",
|
|
2537
|
+
grok: "grok"
|
|
2538
|
+
};
|
|
2539
|
+
MODEL_DISCOVER_TIMEOUT_MS = 2e3;
|
|
2540
|
+
}
|
|
2541
|
+
});
|
|
2542
|
+
|
|
2064
2543
|
// ../runner/src/auth-check.ts
|
|
2065
2544
|
function firstLine(text) {
|
|
2066
2545
|
const line = text.split(/\r?\n/).map((row) => row.trim()).find(Boolean);
|
|
@@ -2268,8 +2747,8 @@ __export(init_exports, {
|
|
|
2268
2747
|
parseInitArgs: () => parseInitArgs
|
|
2269
2748
|
});
|
|
2270
2749
|
import { mkdir as mkdir3, readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
|
|
2271
|
-
import { homedir as
|
|
2272
|
-
import
|
|
2750
|
+
import { homedir as homedir4, hostname, tmpdir as tmpdir2 } from "os";
|
|
2751
|
+
import path7 from "path";
|
|
2273
2752
|
import { fileURLToPath } from "url";
|
|
2274
2753
|
import { execFile as execFile4 } from "child_process";
|
|
2275
2754
|
import { promisify as promisify4 } from "util";
|
|
@@ -2334,7 +2813,7 @@ function xmlEscape(value) {
|
|
|
2334
2813
|
}
|
|
2335
2814
|
function launchdPlist(opts) {
|
|
2336
2815
|
const label = opts.label ?? LAUNCHD_LABEL;
|
|
2337
|
-
const program =
|
|
2816
|
+
const program = path7.join(opts.repoRoot, "deploy/run-runner.sh");
|
|
2338
2817
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
2339
2818
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2340
2819
|
<plist version="1.0">
|
|
@@ -2365,16 +2844,16 @@ function launchdPlist(opts) {
|
|
|
2365
2844
|
`;
|
|
2366
2845
|
}
|
|
2367
2846
|
function defaultRepoRoot(fromFile = fileURLToPath(import.meta.url)) {
|
|
2368
|
-
return
|
|
2847
|
+
return path7.resolve(path7.dirname(fromFile), "../../..");
|
|
2369
2848
|
}
|
|
2370
2849
|
async function initHost(flags, deps = {}) {
|
|
2371
2850
|
const env = deps.env ?? process.env;
|
|
2372
2851
|
const platform = deps.platform ?? process.platform;
|
|
2373
|
-
const home = (deps.homedir ??
|
|
2852
|
+
const home = (deps.homedir ?? homedir4)();
|
|
2374
2853
|
const repoRoot = defaultRepoRoot();
|
|
2375
2854
|
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 ||
|
|
2855
|
+
const envFile = flags.envFile || path7.join(repoRoot, "apps/runner/.env");
|
|
2856
|
+
const worktreeRoot = flags.worktreeRoot || env.WORKTREE_ROOT || path7.join(home, "higherdev-worktrees");
|
|
2378
2857
|
const steps = [
|
|
2379
2858
|
"Write the runner env file",
|
|
2380
2859
|
"Register the host",
|
|
@@ -2400,13 +2879,13 @@ async function initHost(flags, deps = {}) {
|
|
|
2400
2879
|
RUNNER_STOP_TIMEOUT_MS: existing.RUNNER_STOP_TIMEOUT_MS || "90000"
|
|
2401
2880
|
});
|
|
2402
2881
|
if (!flags.dryRun) {
|
|
2403
|
-
await ensureDir(
|
|
2882
|
+
await ensureDir(path7.dirname(envFile), { recursive: true });
|
|
2404
2883
|
await write(envFile, formatEnvFile(merged), "utf8");
|
|
2405
2884
|
}
|
|
2406
|
-
const plistPath = flags.plistOut || (flags.installLaunchd && !flags.dryRun && platform === "darwin" ?
|
|
2407
|
-
const logPath =
|
|
2885
|
+
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"));
|
|
2886
|
+
const logPath = path7.join(home, "Library/Logs/higherdev-runner.log");
|
|
2408
2887
|
const plist = launchdPlist({ repoRoot, envFile, logPath });
|
|
2409
|
-
await ensureDir(
|
|
2888
|
+
await ensureDir(path7.dirname(plistPath), { recursive: true });
|
|
2410
2889
|
await write(plistPath, plist, "utf8");
|
|
2411
2890
|
let launchdInstalled = false;
|
|
2412
2891
|
if (flags.installLaunchd && !flags.dryRun && platform === "darwin") {
|
|
@@ -2671,154 +3150,56 @@ async function currentEmail(db) {
|
|
|
2671
3150
|
function normalizeEmail(email) {
|
|
2672
3151
|
return email.trim().toLowerCase();
|
|
2673
3152
|
}
|
|
2674
|
-
async function serverAllows(db) {
|
|
2675
|
-
const { data, error } = await db.rpc("is_allowlisted");
|
|
2676
|
-
if (error) return false;
|
|
2677
|
-
return data === true;
|
|
2678
|
-
}
|
|
2679
|
-
async function audit(db, email, workspaceId, action, payload) {
|
|
2680
|
-
const { error } = await db.from("audit_events").insert(auditEventRow({ workspace_id: workspaceId, actor: email, action, payload }));
|
|
2681
|
-
if (!error) return;
|
|
2682
|
-
process.stderr.write(
|
|
2683
|
-
`warning: the change was made but was not written to the audit log (${error.message}).
|
|
2684
|
-
Apply the pending migrations in supabase/migrations.
|
|
2685
|
-
`
|
|
2686
|
-
);
|
|
2687
|
-
}
|
|
2688
|
-
async function requireCtx() {
|
|
2689
|
-
const db = await createDb();
|
|
2690
|
-
const email = await currentEmail(db);
|
|
2691
|
-
if (!email) fail("Not signed in. Run `hd login`.");
|
|
2692
|
-
if (!await serverAllows(db)) {
|
|
2693
|
-
fail(`${email} is not allowed on this HigherDEV. Ask its owner to add you.`);
|
|
2694
|
-
}
|
|
2695
|
-
return { db, email, audit: (workspaceId, action, payload) => audit(db, email, workspaceId, action, payload) };
|
|
2696
|
-
}
|
|
2697
|
-
async function requireWorkspace(slugOverride) {
|
|
2698
|
-
const ctx = await requireCtx();
|
|
2699
|
-
const config = await loadConfig();
|
|
2700
|
-
const slug = slugOverride ?? process.env.HIGHERDEV_WORKSPACE ?? config.workspace;
|
|
2701
|
-
if (slug) {
|
|
2702
|
-
const workspace = await workspaceBySlug(ctx.db, slug);
|
|
2703
|
-
if (!workspace) fail(`No workspace named ${slug}. Run \`hd workspace ls\`.`);
|
|
2704
|
-
return { ...ctx, workspace };
|
|
2705
|
-
}
|
|
2706
|
-
const { data, error } = await ctx.db.from("workspaces").select("*").order("created_at");
|
|
2707
|
-
if (error) fail(error.message);
|
|
2708
|
-
const rows = data ?? [];
|
|
2709
|
-
if (rows.length === 1) {
|
|
2710
|
-
await saveConfig({ workspace: rows[0].slug });
|
|
2711
|
-
return { ...ctx, workspace: rows[0] };
|
|
2712
|
-
}
|
|
2713
|
-
if (rows.length === 0) fail("No workspaces yet. Run `hd workspace new <name> <owner/repo>`.");
|
|
2714
|
-
fail(
|
|
2715
|
-
`Pick a workspace: ${rows.map((row) => row.slug).join(", ")}.
|
|
2716
|
-
Run \`hd use <slug>\` or pass --workspace.`
|
|
2717
|
-
);
|
|
2718
|
-
}
|
|
2719
|
-
|
|
2720
|
-
// src/auth.ts
|
|
2721
|
-
init_theme();
|
|
2722
|
-
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
|
-
}
|
|
3153
|
+
async function serverAllows(db) {
|
|
3154
|
+
const { data, error } = await db.rpc("is_allowlisted");
|
|
3155
|
+
if (error) return false;
|
|
3156
|
+
return data === true;
|
|
2790
3157
|
}
|
|
2791
|
-
function
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
3158
|
+
async function audit(db, email, workspaceId, action, payload) {
|
|
3159
|
+
const { error } = await db.from("audit_events").insert(auditEventRow({ workspace_id: workspaceId, actor: email, action, payload }));
|
|
3160
|
+
if (!error) return;
|
|
3161
|
+
process.stderr.write(
|
|
3162
|
+
`warning: the change was made but was not written to the audit log (${error.message}).
|
|
3163
|
+
Apply the pending migrations in supabase/migrations.
|
|
3164
|
+
`
|
|
3165
|
+
);
|
|
2798
3166
|
}
|
|
2799
|
-
function
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
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.";
|
|
3167
|
+
async function requireCtx() {
|
|
3168
|
+
const db = await createDb();
|
|
3169
|
+
const email = await currentEmail(db);
|
|
3170
|
+
if (!email) fail("Not signed in. Run `hd login`.");
|
|
3171
|
+
if (!await serverAllows(db)) {
|
|
3172
|
+
fail(`${email} is not allowed on this HigherDEV. Ask its owner to add you.`);
|
|
2809
3173
|
}
|
|
3174
|
+
return { db, email, audit: (workspaceId, action, payload) => audit(db, email, workspaceId, action, payload) };
|
|
2810
3175
|
}
|
|
2811
|
-
async function
|
|
2812
|
-
const
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
3176
|
+
async function requireWorkspace(slugOverride) {
|
|
3177
|
+
const ctx = await requireCtx();
|
|
3178
|
+
const config = await loadConfig();
|
|
3179
|
+
const slug = slugOverride ?? process.env.HIGHERDEV_WORKSPACE ?? config.workspace;
|
|
3180
|
+
if (slug) {
|
|
3181
|
+
const workspace = await workspaceBySlug(ctx.db, slug);
|
|
3182
|
+
if (!workspace) fail(`No workspace named ${slug}. Run \`hd workspace ls\`.`);
|
|
3183
|
+
return { ...ctx, workspace };
|
|
3184
|
+
}
|
|
3185
|
+
const { data, error } = await ctx.db.from("workspaces").select("*").order("created_at");
|
|
3186
|
+
if (error) fail(error.message);
|
|
3187
|
+
const rows = data ?? [];
|
|
3188
|
+
if (rows.length === 1) {
|
|
3189
|
+
await saveConfig({ workspace: rows[0].slug });
|
|
3190
|
+
return { ...ctx, workspace: rows[0] };
|
|
3191
|
+
}
|
|
3192
|
+
if (rows.length === 0) fail("No workspaces yet. Run `hd workspace new <name> <owner/repo>`.");
|
|
3193
|
+
fail(
|
|
3194
|
+
`Pick a workspace: ${rows.map((row) => row.slug).join(", ")}.
|
|
3195
|
+
Run \`hd use <slug>\` or pass --workspace.`
|
|
3196
|
+
);
|
|
2819
3197
|
}
|
|
2820
3198
|
|
|
2821
3199
|
// src/auth.ts
|
|
3200
|
+
init_theme();
|
|
3201
|
+
init_json();
|
|
3202
|
+
init_connection();
|
|
2822
3203
|
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
2823
3204
|
var CODE_RE = /^\d{6}$/;
|
|
2824
3205
|
async function prompt(question) {
|
|
@@ -3736,10 +4117,8 @@ async function createTicket(ctx, input) {
|
|
|
3736
4117
|
assigned: Boolean(assigned.agent),
|
|
3737
4118
|
blockerStatuses: blockers.statuses
|
|
3738
4119
|
});
|
|
3739
|
-
const { data, error } = await ctx.db.
|
|
3740
|
-
workspace_id: ctx.workspace.id,
|
|
4120
|
+
const { data, error } = await createTicketRecord(ctx.db, ctx.workspace.id, {
|
|
3741
4121
|
epic_id: input.epic ?? null,
|
|
3742
|
-
key: "",
|
|
3743
4122
|
title,
|
|
3744
4123
|
body_md: input.body ?? "",
|
|
3745
4124
|
acceptance_md: acceptance,
|
|
@@ -3752,7 +4131,7 @@ async function createTicket(ctx, input) {
|
|
|
3752
4131
|
provider_pinned: Boolean(assigned.agent),
|
|
3753
4132
|
blocked_by: blockers.ids,
|
|
3754
4133
|
created_by: "human"
|
|
3755
|
-
})
|
|
4134
|
+
});
|
|
3756
4135
|
if (error) fail(error.message);
|
|
3757
4136
|
await ctx.audit(ctx.workspace.id, "create", { ticket_key: data.key, subject: "a ticket" });
|
|
3758
4137
|
return data;
|
|
@@ -3840,7 +4219,12 @@ async function handBack(ctx, ticket) {
|
|
|
3840
4219
|
return data;
|
|
3841
4220
|
}
|
|
3842
4221
|
async function killRun(ctx, runId, ticketKey) {
|
|
3843
|
-
const { data, error } = await ctx.db.
|
|
4222
|
+
const { data, error } = await ctx.db.rpc("request_run_cancellation", {
|
|
4223
|
+
p_run_id: runId,
|
|
4224
|
+
p_workspace_id: ctx.workspace.id,
|
|
4225
|
+
p_requested_by: ctx.email,
|
|
4226
|
+
p_reason: `Cancellation requested by ${ctx.email}.`
|
|
4227
|
+
});
|
|
3844
4228
|
if (error) fail(error.message);
|
|
3845
4229
|
if (!data) fail(`No run ${runId} in ${ctx.workspace.slug}.`);
|
|
3846
4230
|
await ctx.audit(ctx.workspace.id, "kill", { ticket_key: ticketKey, run_id: runId });
|
|
@@ -3972,14 +4356,14 @@ function registerWriteCommands(program) {
|
|
|
3972
4356
|
const live = runs2.find((run6) => run6.status === "running" || run6.status === "queued");
|
|
3973
4357
|
if (!live) fail(`${ticket.key} has no live run.`);
|
|
3974
4358
|
const killed2 = await killRun(ctx, live.id, ticket.key);
|
|
3975
|
-
ok(`
|
|
4359
|
+
ok(`Cancellation requested for ${c.dim(killed2.id.slice(0, 8))} on ${c.bold(ticket.key)}.`, { run: killed2 });
|
|
3976
4360
|
return;
|
|
3977
4361
|
}
|
|
3978
4362
|
const runs = await loadRuns(ctx, { limit: 200 });
|
|
3979
4363
|
const run5 = runs.find((row) => row.id.startsWith(target));
|
|
3980
4364
|
if (!run5) fail(`No run matching ${target}.`);
|
|
3981
4365
|
const killed = await killRun(ctx, run5.id);
|
|
3982
|
-
ok(`
|
|
4366
|
+
ok(`Cancellation requested for ${c.dim(killed.id.slice(0, 8))}.`, { run: killed });
|
|
3983
4367
|
});
|
|
3984
4368
|
const epic = group(program, "epic", "epics and their dependency waves");
|
|
3985
4369
|
epic.command("new").argument("<title>", "epic title").argument("[spec]", "path to a markdown spec").description("create an epic; the orchestrator decomposes it").action(async function(title, spec) {
|
|
@@ -4290,7 +4674,7 @@ function registerAgentCommands(program) {
|
|
|
4290
4674
|
row.role,
|
|
4291
4675
|
row.provider,
|
|
4292
4676
|
c.dim(row.model),
|
|
4293
|
-
row.effort,
|
|
4677
|
+
asEffort(row.effort),
|
|
4294
4678
|
row.enabled ? c.green("yes") : c.dim("no"),
|
|
4295
4679
|
truncate(row.routing_notes ?? "", 44)
|
|
4296
4680
|
])
|
|
@@ -4304,7 +4688,7 @@ function registerAgentCommands(program) {
|
|
|
4304
4688
|
found,
|
|
4305
4689
|
() => [
|
|
4306
4690
|
`${c.bold(found.display_name)} ${c.dim(found.role)}`,
|
|
4307
|
-
`${c.dim("model")} ${found.provider}/${found.model} ${c.dim(`effort ${found.effort}`)}`,
|
|
4691
|
+
`${c.dim("model")} ${found.provider}/${found.model} ${c.dim(`effort ${asEffort(found.effort)}`)}`,
|
|
4308
4692
|
`${c.dim("enabled")} ${found.enabled ? "yes" : "no"}`,
|
|
4309
4693
|
`${c.dim("caps")} ${found.runs_per_hour ?? "unlimited"} runs/hour, ${found.daily_spend_usd ?? "unlimited"} usd/day`,
|
|
4310
4694
|
found.routing_notes ? `
|
|
@@ -4359,7 +4743,7 @@ ${found.prompt_addendum}` : ""
|
|
|
4359
4743
|
await ctx.audit(ctx.workspace.id, "configure", { subject: `agent ${data.display_name}` });
|
|
4360
4744
|
ok(`${c.bold(data.display_name)} updated.`, { agent: data });
|
|
4361
4745
|
});
|
|
4362
|
-
agent.command("new").argument("<name>", "display name").description("add a builder").requiredOption("--provider <provider>", "claude, codex, gemini, or grok").requiredOption("--model <model>", "model id").option("--effort <level>", "low, medium, or high",
|
|
4746
|
+
agent.command("new").argument("<name>", "display name").description("add a builder").requiredOption("--provider <provider>", "claude, codex, gemini, or grok").requiredOption("--model <model>", "model id").option("--effort <level>", "low, medium, or high", DEFAULT_EFFORT).option("--notes <text>", "routing notes").action(async function(name) {
|
|
4363
4747
|
const opts = this.opts();
|
|
4364
4748
|
const ctx = await requireWorkspace(slugOf5(this));
|
|
4365
4749
|
if (!providers.includes(opts.provider)) {
|
|
@@ -4444,8 +4828,13 @@ function registerWorkspaceCommands(program) {
|
|
|
4444
4828
|
default_host: opts.host
|
|
4445
4829
|
}).select("*").single();
|
|
4446
4830
|
if (error) fail(error.message);
|
|
4831
|
+
const { error: agentsError } = await ctx.db.from("agents").insert(DEFAULT_AGENTS.map((agent) => ({ ...agent, workspace_id: data.id })));
|
|
4832
|
+
if (agentsError) fail(`Workspace created, but seeding its agents failed: ${agentsError.message}`);
|
|
4447
4833
|
await ctx.audit(data.id, "create", { subject: `workspace ${data.slug}` });
|
|
4448
|
-
ok(
|
|
4834
|
+
ok(
|
|
4835
|
+
`${c.bold(data.slug)} created with ${DEFAULT_AGENTS.length} agents. Run \`hd use ${data.slug}\`.`,
|
|
4836
|
+
{ workspace: data }
|
|
4837
|
+
);
|
|
4449
4838
|
});
|
|
4450
4839
|
workspace.command("set").description("change workspace settings").option("--branch <name>", "default branch").option("--host <id>", "default host").option("--auto-merge", "merge approved PRs automatically").option("--no-auto-merge", "wait for a human to merge").option("--cap <provider=n...>", "set a provider cap, e.g. --cap claude=2").action(async function() {
|
|
4451
4840
|
const opts = this.opts();
|
|
@@ -4536,22 +4925,22 @@ function registerAttachCommands(program) {
|
|
|
4536
4925
|
if (!contentType) fail(`${name} is not a jpg, png, gif, or webp.`);
|
|
4537
4926
|
const ext = extensionForContentType(contentType);
|
|
4538
4927
|
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(
|
|
4928
|
+
const path8 = attachmentObjectPath(ctx.workspace.id, ticket.id, randomUUID(), ext);
|
|
4929
|
+
const { error } = await ctx.db.storage.from(TICKET_IMAGES_BUCKET).upload(path8, bytes, { contentType, upsert: false });
|
|
4541
4930
|
if (error) fail(`${name}: ${error.message}`);
|
|
4542
4931
|
const { error: rowError } = await ctx.db.from("attachments").insert({
|
|
4543
4932
|
workspace_id: ctx.workspace.id,
|
|
4544
4933
|
ticket_id: ticket.id,
|
|
4545
4934
|
message_id: null,
|
|
4546
|
-
path:
|
|
4935
|
+
path: path8,
|
|
4547
4936
|
content_type: contentType,
|
|
4548
4937
|
created_by: "human"
|
|
4549
4938
|
});
|
|
4550
4939
|
if (rowError) {
|
|
4551
|
-
await ctx.db.storage.from(TICKET_IMAGES_BUCKET).remove([
|
|
4940
|
+
await ctx.db.storage.from(TICKET_IMAGES_BUCKET).remove([path8]);
|
|
4552
4941
|
fail(rowError.message);
|
|
4553
4942
|
}
|
|
4554
|
-
uploaded.push(
|
|
4943
|
+
uploaded.push(path8);
|
|
4555
4944
|
}
|
|
4556
4945
|
await ctx.audit(ctx.workspace.id, "create", { ticket_key: ticket.key, subject: "attachments" });
|
|
4557
4946
|
ok(`Attached ${uploaded.length} file${uploaded.length === 1 ? "" : "s"} to ${c.bold(ticket.key)}.`, {
|
|
@@ -4587,7 +4976,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
4587
4976
|
import { z as z3 } from "zod";
|
|
4588
4977
|
|
|
4589
4978
|
// src/version.ts
|
|
4590
|
-
var VERSION = "0.1.
|
|
4979
|
+
var VERSION = "0.1.3";
|
|
4591
4980
|
|
|
4592
4981
|
// src/architect/tools.ts
|
|
4593
4982
|
init_src();
|
|
@@ -5019,6 +5408,13 @@ var TOOLS = [
|
|
|
5019
5408
|
function toolsFor(access2) {
|
|
5020
5409
|
return access2 === "read" ? TOOLS.filter((tool) => tool.access === "read") : TOOLS;
|
|
5021
5410
|
}
|
|
5411
|
+
function toolJsonSchema(tool) {
|
|
5412
|
+
const schema = z2.toJSONSchema(tool.schema);
|
|
5413
|
+
delete schema.$schema;
|
|
5414
|
+
if (!schema.type) schema.type = "object";
|
|
5415
|
+
if (!schema.properties) schema.properties = {};
|
|
5416
|
+
return schema;
|
|
5417
|
+
}
|
|
5022
5418
|
async function toolContext(slug) {
|
|
5023
5419
|
return requireWorkspace(slug);
|
|
5024
5420
|
}
|
|
@@ -5090,385 +5486,262 @@ async function serveMcp(opts) {
|
|
|
5090
5486
|
}
|
|
5091
5487
|
|
|
5092
5488
|
// 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
5489
|
init_json();
|
|
5100
5490
|
init_theme();
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
import {
|
|
5104
|
-
import { createInterface as createInterface2 } from "readline";
|
|
5105
|
-
|
|
5106
|
-
// ../runner/src/adapters/codex.ts
|
|
5107
|
-
init_providers();
|
|
5491
|
+
init_chatgpt();
|
|
5492
|
+
import { execFile as execFile2 } from "child_process";
|
|
5493
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
5108
5494
|
import { homedir as homedir3 } from "os";
|
|
5109
|
-
import
|
|
5110
|
-
import {
|
|
5495
|
+
import path6 from "path";
|
|
5496
|
+
import { promisify as promisify2 } from "util";
|
|
5111
5497
|
|
|
5112
|
-
//
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
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
|
-
}
|
|
5498
|
+
// src/architect/brain.ts
|
|
5499
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
5500
|
+
init_json();
|
|
5501
|
+
init_theme();
|
|
5502
|
+
init_connection();
|
|
5503
|
+
var BRAINS = [
|
|
5504
|
+
{
|
|
5505
|
+
id: "chatgpt",
|
|
5506
|
+
label: "ChatGPT subscription",
|
|
5507
|
+
billing: "included in your ChatGPT plan, nothing per token",
|
|
5508
|
+
// gpt-5.6, without a suffix, is refused by this endpoint for ChatGPT accounts.
|
|
5509
|
+
defaultModel: "gpt-5.6-sol",
|
|
5510
|
+
models: ["gpt-5.6-sol", "gpt-5.5"]
|
|
5132
5511
|
}
|
|
5133
|
-
|
|
5512
|
+
];
|
|
5513
|
+
var DEFAULT_BRAIN = {
|
|
5514
|
+
id: "chatgpt",
|
|
5515
|
+
model: "gpt-5.6-sol",
|
|
5516
|
+
effort: "high"
|
|
5517
|
+
};
|
|
5518
|
+
function brainById(id) {
|
|
5519
|
+
return BRAINS.find((brain) => brain.id === id) ?? null;
|
|
5134
5520
|
}
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
5139
|
-
|
|
5140
|
-
// ../runner/src/adapters/cost.ts
|
|
5141
|
-
function costFromPayload(payload) {
|
|
5142
|
-
if (!payload) return null;
|
|
5143
|
-
const values = [payload.total_cost_usd, payload.cost_usd, payload.totalCostUsd, payload.costUsd];
|
|
5144
|
-
pushUsageCosts(asRecord2(payload.usage), values);
|
|
5145
|
-
pushUsageCosts(asRecord2(payload.info), values);
|
|
5146
|
-
const result = asRecord2(payload.result);
|
|
5147
|
-
if (result) {
|
|
5148
|
-
values.push(result.total_cost_usd, result.cost_usd);
|
|
5149
|
-
pushUsageCosts(asRecord2(result.usage), values);
|
|
5150
|
-
}
|
|
5151
|
-
for (const value of values) {
|
|
5152
|
-
const n = toNonNegativeNumber(value);
|
|
5153
|
-
if (n != null) return n;
|
|
5154
|
-
}
|
|
5155
|
-
return null;
|
|
5521
|
+
function describeBrain(config) {
|
|
5522
|
+
const brain = brainById(config.id);
|
|
5523
|
+
return `${brain?.label ?? config.id} (${config.model}, effort ${config.effort})`;
|
|
5156
5524
|
}
|
|
5157
|
-
function
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
for (const value of values) {
|
|
5165
|
-
const n = toNonNegativeInt(value);
|
|
5166
|
-
if (n != null) return n;
|
|
5525
|
+
async function confirm(question) {
|
|
5526
|
+
const rl = createInterface2({ input: process.stdin, output: process.stderr });
|
|
5527
|
+
try {
|
|
5528
|
+
const answer = (await rl.question(question)).trim().toLowerCase();
|
|
5529
|
+
return answer === "" || answer === "y" || answer === "yes";
|
|
5530
|
+
} finally {
|
|
5531
|
+
rl.close();
|
|
5167
5532
|
}
|
|
5168
|
-
return null;
|
|
5169
5533
|
}
|
|
5170
|
-
function
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
}
|
|
5177
|
-
|
|
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;
|
|
5534
|
+
async function requireBrain(opts = {}) {
|
|
5535
|
+
const config = await loadConfig();
|
|
5536
|
+
const chosen = config.brain ?? DEFAULT_BRAIN;
|
|
5537
|
+
const interactive = opts.interactive ?? (process.stdin.isTTY && process.stdout.isTTY);
|
|
5538
|
+
const connection = await codexConnection();
|
|
5539
|
+
if (connection.state === "connected") {
|
|
5540
|
+
if (!config.brain) await saveConfig({ brain: chosen });
|
|
5541
|
+
return chosen;
|
|
5182
5542
|
}
|
|
5183
|
-
|
|
5543
|
+
if (!interactive) {
|
|
5544
|
+
fail(`${connectionSummary(connection)}
|
|
5545
|
+
${howToFix(connection) ?? ""}`.trim());
|
|
5546
|
+
}
|
|
5547
|
+
if (connection.state === "not_installed") {
|
|
5548
|
+
fail(`${connectionSummary(connection)}
|
|
5549
|
+
${howToFix(connection) ?? ""}`.trim());
|
|
5550
|
+
}
|
|
5551
|
+
const brain = brainById(chosen.id);
|
|
5552
|
+
out("");
|
|
5553
|
+
out(`${c.bold("HigherDEV needs your ChatGPT subscription to answer.")}`);
|
|
5554
|
+
out(c.dim(`${brain?.billing ?? ""}. The sign-in happens in your browser; hd never sees the token.`));
|
|
5555
|
+
if (!await confirm(c.dim("Press enter to sign in, or n to cancel: "))) {
|
|
5556
|
+
fail("Not connected. Run `hd brain` when you want to.");
|
|
5557
|
+
}
|
|
5558
|
+
const connected = await connectCodex();
|
|
5559
|
+
if (connected.state !== "connected") {
|
|
5560
|
+
fail(`${connectionSummary(connected)}
|
|
5561
|
+
${howToFix(connected) ?? ""}`.trim());
|
|
5562
|
+
}
|
|
5563
|
+
out(c.green(connectionSummary(connected)));
|
|
5564
|
+
await saveConfig({ brain: chosen });
|
|
5565
|
+
return chosen;
|
|
5184
5566
|
}
|
|
5185
|
-
function
|
|
5186
|
-
const
|
|
5187
|
-
|
|
5567
|
+
async function setBrain(patch) {
|
|
5568
|
+
const config = await loadConfig();
|
|
5569
|
+
const next = { ...config.brain ?? DEFAULT_BRAIN, ...patch };
|
|
5570
|
+
const brain = brainById(next.id);
|
|
5571
|
+
if (!brain) fail(`Unknown brain ${next.id}. One of: ${BRAINS.map((row) => row.id).join(", ")}.`);
|
|
5572
|
+
if (patch.model && !brain.models.includes(patch.model)) {
|
|
5573
|
+
out(c.yellow(`${patch.model} is not one of the models known to work here (${brain.models.join(", ")}).`));
|
|
5574
|
+
}
|
|
5575
|
+
await saveConfig({ brain: next });
|
|
5576
|
+
return next;
|
|
5188
5577
|
}
|
|
5189
5578
|
|
|
5190
|
-
//
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
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
|
-
}
|
|
5579
|
+
// src/architect/repo.ts
|
|
5580
|
+
import { readFile as readFile6, readdir, stat } from "fs/promises";
|
|
5581
|
+
import path5 from "path";
|
|
5582
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
5583
|
+
".git",
|
|
5584
|
+
"node_modules",
|
|
5585
|
+
".next",
|
|
5586
|
+
"dist",
|
|
5587
|
+
"build",
|
|
5588
|
+
"coverage",
|
|
5589
|
+
".turbo",
|
|
5590
|
+
".vercel",
|
|
5591
|
+
".pnpm-store"
|
|
5592
|
+
]);
|
|
5593
|
+
var MAX_FILE_BYTES = 2e5;
|
|
5594
|
+
var MAX_LIST = 400;
|
|
5595
|
+
var MAX_MATCHES = 60;
|
|
5596
|
+
function resolveInside(root, candidate) {
|
|
5597
|
+
const full = path5.resolve(root, candidate ?? ".");
|
|
5598
|
+
const rel = path5.relative(root, full);
|
|
5599
|
+
if (rel.startsWith("..") || path5.isAbsolute(rel)) {
|
|
5600
|
+
throw new Error(`${candidate} is outside the checkout.`);
|
|
5601
|
+
}
|
|
5602
|
+
return full;
|
|
5603
|
+
}
|
|
5604
|
+
function isSkipped(name) {
|
|
5605
|
+
return SKIP_DIRS.has(name) || name.startsWith(".DS_Store");
|
|
5606
|
+
}
|
|
5607
|
+
async function walk(root, dir, out2, depth) {
|
|
5608
|
+
if (out2.length >= MAX_LIST || depth > 12) return;
|
|
5609
|
+
let entries;
|
|
5610
|
+
try {
|
|
5611
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
5612
|
+
} catch {
|
|
5613
|
+
return;
|
|
5614
|
+
}
|
|
5615
|
+
for (const entry of entries) {
|
|
5616
|
+
if (out2.length >= MAX_LIST) return;
|
|
5617
|
+
if (isSkipped(entry.name)) continue;
|
|
5618
|
+
const full = path5.join(dir, entry.name);
|
|
5619
|
+
if (entry.isDirectory()) {
|
|
5620
|
+
await walk(root, full, out2, depth + 1);
|
|
5621
|
+
} else if (entry.isFile()) {
|
|
5622
|
+
out2.push(path5.relative(root, full));
|
|
5246
5623
|
}
|
|
5247
5624
|
}
|
|
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
5625
|
}
|
|
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
|
-
});
|
|
5626
|
+
function matchesGlob(rel, glob) {
|
|
5627
|
+
if (!glob) return true;
|
|
5628
|
+
const pattern = glob.split("").map((ch) => {
|
|
5629
|
+
if (ch === "*") return "\0";
|
|
5630
|
+
return /[.+^${}()|[\]\\]/.test(ch) ? `\\${ch}` : ch;
|
|
5631
|
+
}).join("").replace(//g, ".*").replace(//g, "[^/]*");
|
|
5632
|
+
return new RegExp(`^${pattern}$`).test(rel);
|
|
5285
5633
|
}
|
|
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 };
|
|
5634
|
+
function buildRepoTools(root) {
|
|
5635
|
+
if (!root) return [];
|
|
5636
|
+
return [
|
|
5637
|
+
{
|
|
5638
|
+
name: "list_files",
|
|
5639
|
+
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.",
|
|
5640
|
+
parameters: {
|
|
5641
|
+
type: "object",
|
|
5642
|
+
properties: {
|
|
5643
|
+
dir: { type: "string", description: "Directory relative to the repo root. Defaults to the root." },
|
|
5644
|
+
glob: { type: "string", description: "Optional glob filter, e.g. **/*.ts" }
|
|
5645
|
+
}
|
|
5646
|
+
},
|
|
5647
|
+
async run(args) {
|
|
5648
|
+
const dir = resolveInside(root, String(args.dir ?? "."));
|
|
5649
|
+
const found = [];
|
|
5650
|
+
await walk(root, dir, found, 0);
|
|
5651
|
+
const glob = args.glob ? String(args.glob) : void 0;
|
|
5652
|
+
const filtered = found.filter((rel) => matchesGlob(rel, glob)).sort();
|
|
5653
|
+
return {
|
|
5654
|
+
root: path5.basename(root),
|
|
5655
|
+
count: filtered.length,
|
|
5656
|
+
truncated: found.length >= MAX_LIST,
|
|
5657
|
+
files: filtered
|
|
5658
|
+
};
|
|
5337
5659
|
}
|
|
5338
|
-
|
|
5339
|
-
|
|
5660
|
+
},
|
|
5661
|
+
{
|
|
5662
|
+
name: "read_file",
|
|
5663
|
+
description: "Read a file from the workspace checkout. Large files come back truncated.",
|
|
5664
|
+
parameters: {
|
|
5665
|
+
type: "object",
|
|
5666
|
+
properties: {
|
|
5667
|
+
path: { type: "string", description: "File path relative to the repo root." }
|
|
5668
|
+
},
|
|
5669
|
+
required: ["path"]
|
|
5670
|
+
},
|
|
5671
|
+
async run(args) {
|
|
5672
|
+
const file = resolveInside(root, String(args.path ?? ""));
|
|
5673
|
+
const info = await stat(file).catch(() => null);
|
|
5674
|
+
if (!info?.isFile()) throw new Error(`${args.path} is not a file in the checkout.`);
|
|
5675
|
+
const raw = await readFile6(file, "utf8");
|
|
5676
|
+
const truncated = raw.length > MAX_FILE_BYTES;
|
|
5677
|
+
return {
|
|
5678
|
+
path: path5.relative(root, file),
|
|
5679
|
+
bytes: info.size,
|
|
5680
|
+
truncated,
|
|
5681
|
+
content: truncated ? raw.slice(0, MAX_FILE_BYTES) : raw
|
|
5682
|
+
};
|
|
5340
5683
|
}
|
|
5341
|
-
|
|
5342
|
-
|
|
5684
|
+
},
|
|
5685
|
+
{
|
|
5686
|
+
name: "search_code",
|
|
5687
|
+
description: "Search the workspace checkout for a string or regular expression, returning matching lines with their file and line number.",
|
|
5688
|
+
parameters: {
|
|
5689
|
+
type: "object",
|
|
5690
|
+
properties: {
|
|
5691
|
+
query: { type: "string", description: "Text or regular expression to find." },
|
|
5692
|
+
glob: { type: "string", description: "Optional glob to narrow the search, e.g. **/*.ts" }
|
|
5693
|
+
},
|
|
5694
|
+
required: ["query"]
|
|
5695
|
+
},
|
|
5696
|
+
async run(args) {
|
|
5697
|
+
const query = String(args.query ?? "");
|
|
5698
|
+
if (!query) throw new Error("search_code needs a query.");
|
|
5699
|
+
let re;
|
|
5700
|
+
try {
|
|
5701
|
+
re = new RegExp(query, "i");
|
|
5702
|
+
} catch {
|
|
5703
|
+
re = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
|
|
5704
|
+
}
|
|
5705
|
+
const files = [];
|
|
5706
|
+
await walk(root, root, files, 0);
|
|
5707
|
+
const glob = args.glob ? String(args.glob) : void 0;
|
|
5708
|
+
const matches = [];
|
|
5709
|
+
for (const rel of files) {
|
|
5710
|
+
if (matches.length >= MAX_MATCHES) break;
|
|
5711
|
+
if (!matchesGlob(rel, glob)) continue;
|
|
5712
|
+
let content;
|
|
5713
|
+
try {
|
|
5714
|
+
content = await readFile6(path5.join(root, rel), "utf8");
|
|
5715
|
+
} catch {
|
|
5716
|
+
continue;
|
|
5717
|
+
}
|
|
5718
|
+
if (content.length > MAX_FILE_BYTES) content = content.slice(0, MAX_FILE_BYTES);
|
|
5719
|
+
const lines = content.split("\n");
|
|
5720
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
5721
|
+
if (matches.length >= MAX_MATCHES) break;
|
|
5722
|
+
if (re.test(lines[i])) {
|
|
5723
|
+
matches.push({ path: rel, line: i + 1, text: lines[i].trim().slice(0, 200) });
|
|
5724
|
+
}
|
|
5725
|
+
}
|
|
5726
|
+
}
|
|
5727
|
+
return { count: matches.length, truncated: matches.length >= MAX_MATCHES, matches };
|
|
5343
5728
|
}
|
|
5344
|
-
return { type: "status", payload: event };
|
|
5345
|
-
} catch {
|
|
5346
|
-
return { type: "text", payload: { text: trimmed } };
|
|
5347
5729
|
}
|
|
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
5730
|
];
|
|
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
5731
|
}
|
|
5445
5732
|
|
|
5446
5733
|
// src/architect/plan.ts
|
|
5447
|
-
var run2 =
|
|
5448
|
-
var ARCHITECT_ROLE = "architect";
|
|
5449
|
-
async function architectAgent(ctx) {
|
|
5450
|
-
const { data, error } = await ctx.db.from("agents").select("*").eq("workspace_id", ctx.workspace.id).eq("role", ARCHITECT_ROLE).maybeSingle();
|
|
5451
|
-
if (error) fail(error.message);
|
|
5452
|
-
if (!data) {
|
|
5453
|
-
fail(
|
|
5454
|
-
"This workspace has no architect agent. Apply the migrations, or add one with:\n hd agent new Architect --provider codex --model gpt-5.6-sol"
|
|
5455
|
-
);
|
|
5456
|
-
}
|
|
5457
|
-
if (!data.enabled) fail(`${data.display_name} is disabled. Enable it with \`hd agent set ${data.display_name} --enable\`.`);
|
|
5458
|
-
return data;
|
|
5459
|
-
}
|
|
5734
|
+
var run2 = promisify2(execFile2);
|
|
5460
5735
|
async function repoCheckout(ctx) {
|
|
5461
5736
|
const here = process.cwd();
|
|
5462
5737
|
try {
|
|
5463
5738
|
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;
|
|
5739
|
+
if (stdout.trim().toLowerCase().includes(ctx.workspace.repo.toLowerCase())) return here;
|
|
5467
5740
|
} catch {
|
|
5468
5741
|
}
|
|
5469
|
-
const root =
|
|
5742
|
+
const root = path6.join(homedir3(), ".cache", "higherdev", "architect");
|
|
5470
5743
|
await mkdir2(root, { recursive: true });
|
|
5471
|
-
const dir =
|
|
5744
|
+
const dir = path6.join(root, ctx.workspace.slug);
|
|
5472
5745
|
try {
|
|
5473
5746
|
await run2("git", ["-C", dir, "fetch", "--depth", "1", "origin", ctx.workspace.default_branch], {
|
|
5474
5747
|
timeout: 12e4
|
|
@@ -5476,6 +5749,7 @@ async function repoCheckout(ctx) {
|
|
|
5476
5749
|
await run2("git", ["-C", dir, "reset", "--hard", `origin/${ctx.workspace.default_branch}`], {
|
|
5477
5750
|
timeout: 6e4
|
|
5478
5751
|
});
|
|
5752
|
+
return dir;
|
|
5479
5753
|
} catch {
|
|
5480
5754
|
try {
|
|
5481
5755
|
await run2(
|
|
@@ -5483,139 +5757,133 @@ async function repoCheckout(ctx) {
|
|
|
5483
5757
|
["repo", "clone", ctx.workspace.repo, dir, "--", "--depth", "1", "--branch", ctx.workspace.default_branch],
|
|
5484
5758
|
{ timeout: 3e5 }
|
|
5485
5759
|
);
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
Run \`hd plan\` from inside the repo, or check \`gh auth status\`.`
|
|
5490
|
-
);
|
|
5760
|
+
return dir;
|
|
5761
|
+
} catch {
|
|
5762
|
+
return null;
|
|
5491
5763
|
}
|
|
5492
5764
|
}
|
|
5493
|
-
return dir;
|
|
5494
5765
|
}
|
|
5495
|
-
function
|
|
5766
|
+
function architectInstructions(opts) {
|
|
5496
5767
|
const { ctx } = opts;
|
|
5497
5768
|
return [
|
|
5498
|
-
"You are the HigherDEV architect
|
|
5499
|
-
"whether the platform is configured to build it.
|
|
5500
|
-
"orchestrator dispatches it to builders.",
|
|
5769
|
+
"You are the HigherDEV architect, running in the owner's terminal.",
|
|
5770
|
+
"You decide what should be built and whether the platform is configured to build it.",
|
|
5771
|
+
"You do not write code: you file the work, and the orchestrator dispatches it to builders.",
|
|
5501
5772
|
"",
|
|
5502
|
-
`Workspace: ${ctx.workspace.slug} (${ctx.workspace.repo}, branch ${ctx.workspace.default_branch})
|
|
5773
|
+
`Workspace: ${ctx.workspace.slug} (${ctx.workspace.repo}, branch ${ctx.workspace.default_branch}).`,
|
|
5503
5774
|
`You are acting for ${ctx.email}.`,
|
|
5504
5775
|
"",
|
|
5505
|
-
"
|
|
5506
|
-
"
|
|
5507
|
-
"
|
|
5776
|
+
"Your tools are the only source of truth about the board. Call status before you assume anything,",
|
|
5777
|
+
"and list_agents before you assume who can build what. Never invent tickets, agents, or state.",
|
|
5778
|
+
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
5779
|
"",
|
|
5509
|
-
"
|
|
5510
|
-
"- Prefer
|
|
5511
|
-
"
|
|
5512
|
-
"-
|
|
5780
|
+
"Handing work over:",
|
|
5781
|
+
"- Prefer one epic with a well-written spec. create_epic triggers the orchestrator, which",
|
|
5782
|
+
" decomposes the spec into tickets itself and assigns them. That is the handoff.",
|
|
5783
|
+
"- Use create_ticket only for work that needs no decomposition.",
|
|
5513
5784
|
"- A ticket is one PR, one area, under about 400 changed lines, with acceptance criteria and a",
|
|
5514
5785
|
" test expectation. Two live tickets must not share an area; chain them with blocked_by.",
|
|
5515
5786
|
"",
|
|
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.",
|
|
5519
|
-
"",
|
|
5520
|
-
opts.readOnly ? "This run is read-only. Propose the plan in your reply; do not expect any write tool to exist." : "When you are done, summarise what you filed and what the owner should look at.",
|
|
5521
|
-
"Never use an em dash in anything you write.",
|
|
5522
|
-
opts.addendum ? `
|
|
5523
|
-
${opts.addendum}` : "",
|
|
5524
|
-
"",
|
|
5525
|
-
"---",
|
|
5787
|
+
"Only you can change agent configuration: model, effort, routing notes, prompt addenda,",
|
|
5788
|
+
"conventions, budgets, pausing. The orchestrator has no action for any of it.",
|
|
5526
5789
|
"",
|
|
5527
|
-
opts.
|
|
5528
|
-
|
|
5790
|
+
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.",
|
|
5791
|
+
"Answer conversationally and briefly. Lead with the answer. Never use an em dash."
|
|
5792
|
+
].filter(Boolean).join("\n");
|
|
5529
5793
|
}
|
|
5530
|
-
async function
|
|
5794
|
+
async function openArchitect(opts) {
|
|
5531
5795
|
const { ctx } = opts;
|
|
5532
|
-
const
|
|
5533
|
-
const
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5796
|
+
const brain = await requireBrain();
|
|
5797
|
+
const repo = await repoCheckout(ctx);
|
|
5798
|
+
const platformTools = toolsFor(opts.readOnly ? "read" : "all").map((tool) => ({
|
|
5799
|
+
name: tool.name,
|
|
5800
|
+
description: tool.description,
|
|
5801
|
+
parameters: toolJsonSchema(tool),
|
|
5802
|
+
run: (args) => tool.run(ctx, args)
|
|
5803
|
+
}));
|
|
5804
|
+
return {
|
|
5805
|
+
brain,
|
|
5806
|
+
instructions: architectInstructions({
|
|
5807
|
+
ctx,
|
|
5808
|
+
readOnly: opts.readOnly,
|
|
5809
|
+
hasRepo: Boolean(repo)
|
|
5810
|
+
}),
|
|
5811
|
+
tools: [...platformTools, ...buildRepoTools(repo)],
|
|
5812
|
+
sessionId: newSessionId(),
|
|
5813
|
+
history: [],
|
|
5814
|
+
repo
|
|
5815
|
+
};
|
|
5816
|
+
}
|
|
5817
|
+
async function architectTurn(opts) {
|
|
5818
|
+
const { ctx, session } = opts;
|
|
5553
5819
|
const { data: runRow, error: runError } = await ctx.db.from("runs").insert({
|
|
5554
5820
|
workspace_id: ctx.workspace.id,
|
|
5555
5821
|
ticket_id: null,
|
|
5556
|
-
|
|
5822
|
+
// No agent: the brain is the operator's own model, not a worker in this
|
|
5823
|
+
// workspace. The run exists so what it spends is still visible.
|
|
5824
|
+
agent_id: null,
|
|
5557
5825
|
kind: "architect",
|
|
5558
|
-
provider:
|
|
5559
|
-
model:
|
|
5826
|
+
provider: "codex",
|
|
5827
|
+
model: session.brain.model,
|
|
5560
5828
|
status: "running",
|
|
5561
5829
|
started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5830
|
+
session_id: session.sessionId,
|
|
5562
5831
|
host: null
|
|
5563
5832
|
}).select("id").single();
|
|
5564
5833
|
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
5834
|
let seq = 0;
|
|
5580
|
-
const
|
|
5835
|
+
const events = [];
|
|
5836
|
+
const record = (type, payload) => events.push({ run_id: runRow.id, seq: seq++, type, payload });
|
|
5581
5837
|
const flush = async () => {
|
|
5582
|
-
if (
|
|
5583
|
-
const batch = pending.splice(0, pending.length);
|
|
5584
|
-
await ctx.db.from("run_events").insert(batch);
|
|
5838
|
+
if (events.length) await ctx.db.from("run_events").insert(events);
|
|
5585
5839
|
};
|
|
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
|
-
|
|
5840
|
+
try {
|
|
5841
|
+
const result = await runAgent({
|
|
5842
|
+
prompt: opts.request,
|
|
5843
|
+
instructions: session.instructions,
|
|
5844
|
+
history: session.history,
|
|
5845
|
+
tools: session.tools,
|
|
5846
|
+
model: session.brain.model,
|
|
5847
|
+
sessionId: session.sessionId,
|
|
5848
|
+
signal: opts.signal,
|
|
5849
|
+
onEvent: (event) => {
|
|
5850
|
+
if (event.kind === "tool") record("tool_use", { name: event.name, input: event.args });
|
|
5851
|
+
else if (event.kind === "tool_result") {
|
|
5852
|
+
record("tool_result", { name: event.name, ok: event.ok, content: event.detail });
|
|
5853
|
+
}
|
|
5854
|
+
opts.onEvent(event);
|
|
5855
|
+
}
|
|
5856
|
+
});
|
|
5857
|
+
if (result.text) record("text", { text: result.text });
|
|
5858
|
+
session.history.push({ role: "user", content: opts.request });
|
|
5859
|
+
session.history.push({ role: "assistant", content: result.text });
|
|
5860
|
+
await flush();
|
|
5861
|
+
await ctx.db.from("runs").update({
|
|
5862
|
+
status: "succeeded",
|
|
5863
|
+
ended_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5864
|
+
exit_code: 0,
|
|
5865
|
+
turns: result.steps,
|
|
5866
|
+
summary: result.text.slice(0, 500) || "Architect turn"
|
|
5867
|
+
}).eq("id", runRow.id);
|
|
5868
|
+
await ctx.audit(ctx.workspace.id, "architect", { subject: describeBrain(session.brain) });
|
|
5869
|
+
return { text: result.text, runId: runRow.id, toolCalls: result.toolCalls };
|
|
5870
|
+
} catch (error) {
|
|
5871
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5872
|
+
record("error", { error: message });
|
|
5873
|
+
await flush();
|
|
5874
|
+
await ctx.db.from("runs").update({
|
|
5875
|
+
status: "failed",
|
|
5876
|
+
ended_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5877
|
+
exit_code: 1,
|
|
5878
|
+
summary: message.slice(0, 500)
|
|
5879
|
+
}).eq("id", runRow.id);
|
|
5880
|
+
if (error instanceof ChatGptAuthError) fail(message);
|
|
5881
|
+
throw error;
|
|
5882
|
+
}
|
|
5616
5883
|
}
|
|
5617
5884
|
|
|
5618
5885
|
// src/architect/commands.ts
|
|
5886
|
+
init_connection();
|
|
5619
5887
|
function slugOf6(command) {
|
|
5620
5888
|
return command.optsWithGlobals().workspace;
|
|
5621
5889
|
}
|
|
@@ -5641,25 +5909,38 @@ ${howToFix(connection) ?? ""}`.trim()
|
|
|
5641
5909
|
);
|
|
5642
5910
|
}
|
|
5643
5911
|
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("--
|
|
5912
|
+
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").action(async function(request) {
|
|
5645
5913
|
const opts = this.opts();
|
|
5646
5914
|
const ctx = await requireWorkspace(slugOf6(this));
|
|
5647
5915
|
const first = request.join(" ").trim();
|
|
5648
5916
|
const conversational = !first && !opts.once && process.stdin.isTTY && !isJsonMode();
|
|
5649
|
-
const
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5917
|
+
const session = await openArchitect({ ctx, readOnly: Boolean(opts.readOnly) });
|
|
5918
|
+
let wroteText = false;
|
|
5919
|
+
const onEvent = (event) => {
|
|
5920
|
+
if (isJsonMode()) return;
|
|
5921
|
+
if (event.kind === "text") {
|
|
5922
|
+
process.stdout.write(event.delta);
|
|
5923
|
+
wroteText = true;
|
|
5924
|
+
return;
|
|
5925
|
+
}
|
|
5926
|
+
if (wroteText) {
|
|
5927
|
+
process.stdout.write("\n");
|
|
5928
|
+
wroteText = false;
|
|
5929
|
+
}
|
|
5930
|
+
if (event.kind === "tool") out(c.dim(` ${event.name}`));
|
|
5931
|
+
else if (!event.ok) out(c.red(` ${event.name} failed: ${event.detail}`));
|
|
5932
|
+
};
|
|
5933
|
+
const turn = async (ask) => {
|
|
5934
|
+
const result = await architectTurn({ ctx, session, request: ask, onEvent });
|
|
5935
|
+
if (!isJsonMode()) {
|
|
5936
|
+
if (wroteText) {
|
|
5937
|
+
process.stdout.write("\n");
|
|
5938
|
+
wroteText = false;
|
|
5939
|
+
} else if (!result.text) {
|
|
5940
|
+
out(c.dim("(no reply)"));
|
|
5660
5941
|
}
|
|
5661
|
-
}
|
|
5662
|
-
return
|
|
5942
|
+
}
|
|
5943
|
+
return result;
|
|
5663
5944
|
};
|
|
5664
5945
|
if (!conversational) {
|
|
5665
5946
|
let ask = first;
|
|
@@ -5668,30 +5949,24 @@ function registerArchitectCommands(program) {
|
|
|
5668
5949
|
ask = await askLine("plan> ");
|
|
5669
5950
|
if (!ask) fail("Nothing asked.");
|
|
5670
5951
|
}
|
|
5671
|
-
const
|
|
5952
|
+
const result = await turn(ask);
|
|
5672
5953
|
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)}.`));
|
|
5954
|
+
emit({ ok: true, run_id: result.runId, text: result.text, tool_calls: result.toolCalls }, () => "");
|
|
5682
5955
|
}
|
|
5683
5956
|
return;
|
|
5684
5957
|
}
|
|
5685
|
-
out(
|
|
5686
|
-
|
|
5958
|
+
out(
|
|
5959
|
+
c.dim(
|
|
5960
|
+
`${describeBrain(session.brain)}${session.repo ? ", repo readable" : ""}. Blank line or /exit leaves.`
|
|
5961
|
+
)
|
|
5962
|
+
);
|
|
5687
5963
|
for (; ; ) {
|
|
5688
5964
|
const ask = await askLine("\nplan> ");
|
|
5689
5965
|
if (!ask || ask === "/exit" || ask === "/quit") {
|
|
5690
|
-
out(c.dim(
|
|
5966
|
+
out(c.dim("Left the architect."));
|
|
5691
5967
|
return;
|
|
5692
5968
|
}
|
|
5693
|
-
|
|
5694
|
-
if (result.sessionId) resume = true;
|
|
5969
|
+
await turn(ask);
|
|
5695
5970
|
}
|
|
5696
5971
|
});
|
|
5697
5972
|
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() {
|
|
@@ -5707,6 +5982,33 @@ Continue with \`hd plan --resume "..."\`. Run ${result.runId.slice(0, 8)}.`));
|
|
|
5707
5982
|
}
|
|
5708
5983
|
await serveMcp({ readOnly, workspace: slugOf6(this) });
|
|
5709
5984
|
});
|
|
5985
|
+
program.command("brain").description("the model you talk to, and the subscription behind it").option("--model <model>", "which model to use").option("--effort <level>", "low, medium, or high").action(async function() {
|
|
5986
|
+
const opts = this.opts();
|
|
5987
|
+
if (opts.model || opts.effort) {
|
|
5988
|
+
const next = await setBrain({
|
|
5989
|
+
...opts.model ? { model: String(opts.model) } : {},
|
|
5990
|
+
...opts.effort ? { effort: String(opts.effort) } : {}
|
|
5991
|
+
});
|
|
5992
|
+
ok(describeBrain(next), { brain: next });
|
|
5993
|
+
return;
|
|
5994
|
+
}
|
|
5995
|
+
const config = await loadConfig();
|
|
5996
|
+
const chosen = config.brain ?? DEFAULT_BRAIN;
|
|
5997
|
+
const connection = await codexConnection();
|
|
5998
|
+
emit(
|
|
5999
|
+
{ brain: chosen, connected: connection.state === "connected", codex: connection },
|
|
6000
|
+
() => [
|
|
6001
|
+
`${c.bold(describeBrain(chosen))}`,
|
|
6002
|
+
...BRAINS.map(
|
|
6003
|
+
(brain) => ` ${brain.id === chosen.id ? c.blue("*") : " "} ${brain.label.padEnd(24)} ${c.dim(brain.billing)}`
|
|
6004
|
+
),
|
|
6005
|
+
"",
|
|
6006
|
+
connection.state === "connected" ? c.green(connectionSummary(connection)) : `${c.yellow(connectionSummary(connection))}
|
|
6007
|
+
${howToFix(connection) ?? ""}`,
|
|
6008
|
+
c.dim("Models: " + (brainById(chosen.id)?.models.join(", ") ?? ""))
|
|
6009
|
+
].join("\n")
|
|
6010
|
+
);
|
|
6011
|
+
});
|
|
5710
6012
|
program.command("connect").description("connect Codex to your ChatGPT subscription, so the architect costs nothing per token").option("--check", "report the state without starting a login").action(async function() {
|
|
5711
6013
|
let connection = await codexConnection();
|
|
5712
6014
|
if (connection.state === "connected" || this.opts().check) {
|
|
@@ -5727,19 +6029,34 @@ Run \`codex login\` in a terminal.`);
|
|
|
5727
6029
|
report(connection);
|
|
5728
6030
|
if (connection.state !== "connected") process.exitCode = 1;
|
|
5729
6031
|
});
|
|
5730
|
-
program.command("doctor").description("check the architect's
|
|
6032
|
+
program.command("doctor").description("check the architect's ChatGPT connection and how a turn would bill").action(async function() {
|
|
5731
6033
|
const connection = await codexConnection();
|
|
5732
|
-
const
|
|
6034
|
+
const expiry = await tokenExpiry();
|
|
5733
6035
|
emit(
|
|
5734
|
-
{ codex: connection,
|
|
6036
|
+
{ codex: connection, token_expires_in_hours: expiry },
|
|
5735
6037
|
() => [
|
|
5736
|
-
`${c.dim("
|
|
5737
|
-
`${c.dim("
|
|
5738
|
-
connection.state === "connected" ? c.dim("
|
|
5739
|
-
].join("\n")
|
|
6038
|
+
`${c.dim("chatgpt")} ${connection.state === "connected" ? c.green(connectionSummary(connection)) : c.yellow(connectionSummary(connection))}`,
|
|
6039
|
+
expiry == null ? "" : `${c.dim("session")} renews automatically, valid for ${expiry}h`,
|
|
6040
|
+
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) ?? "")
|
|
6041
|
+
].filter(Boolean).join("\n")
|
|
5740
6042
|
);
|
|
5741
6043
|
});
|
|
5742
6044
|
}
|
|
6045
|
+
async function tokenExpiry() {
|
|
6046
|
+
try {
|
|
6047
|
+
const { readFile: readFile8 } = await import("fs/promises");
|
|
6048
|
+
const path8 = await import("path");
|
|
6049
|
+
const { codexHome: codexHome2 } = await Promise.resolve().then(() => (init_connection(), connection_exports));
|
|
6050
|
+
const { msUntilExpiry: msUntilExpiry2 } = await Promise.resolve().then(() => (init_chatgpt(), chatgpt_exports));
|
|
6051
|
+
const raw = await readFile8(path8.join(codexHome2(), "auth.json"), "utf8");
|
|
6052
|
+
const token = JSON.parse(raw).tokens?.access_token;
|
|
6053
|
+
if (!token) return null;
|
|
6054
|
+
const ms = msUntilExpiry2(token);
|
|
6055
|
+
return Number.isFinite(ms) ? Math.round(ms / 36e5) : null;
|
|
6056
|
+
} catch {
|
|
6057
|
+
return null;
|
|
6058
|
+
}
|
|
6059
|
+
}
|
|
5743
6060
|
|
|
5744
6061
|
// src/tui/commands.ts
|
|
5745
6062
|
init_json();
|