@hizliemre/horse-code 0.2.0 → 0.3.1
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/dist/{app-2GPGCDX6.js → app-4WN37LZ3.js} +282 -57
- package/dist/{chunk-7JMWPTJ5.js → chunk-27F44PBD.js} +221 -1363
- package/dist/{chunk-23CLQ2KO.js → chunk-2WXG35EM.js} +19 -15
- package/dist/{chunk-LNW557IO.js → chunk-372X5HHU.js} +2 -2
- package/dist/{chunk-5ZV42XGJ.js → chunk-3ACDNDCG.js} +1 -1
- package/dist/{chunk-XEGQT5EN.js → chunk-4M6LXNG2.js} +1 -1
- package/dist/{chunk-6OSEQOYY.js → chunk-6S4WWQMN.js} +2 -2
- package/dist/{chunk-LLL7QWXB.js → chunk-BFIZMM4G.js} +6 -6
- package/dist/chunk-CYLPQWIF.js +214 -0
- package/dist/chunk-G45RWL7S.js +289 -0
- package/dist/{chunk-AE36LLL2.js → chunk-JLWQCA7B.js} +2 -209
- package/dist/{chunk-KKWZBZYK.js → chunk-JR2JLRE3.js} +26 -4
- package/dist/{run-P6ZYL5JL.js → chunk-QJYVZPLG.js} +133 -389
- package/dist/{chunk-UGESK765.js → chunk-UTHLEW5V.js} +1 -1
- package/dist/chunk-YULQ4URQ.js +1220 -0
- package/dist/{chunk-KAGKX2YT.js → chunk-ZPJP2VH5.js} +10 -1
- package/dist/cli.js +586 -104
- package/dist/{fix-JOIXQFVP.js → fix-QCL5AITT.js} +10 -8
- package/dist/{ongoing-WHYXPW24.js → ongoing-6NUSPSCV.js} +3 -2
- package/dist/{project-graph-5HNPRFQG.js → project-graph-OGIM2B33.js} +1 -1
- package/dist/run-V5ZLZ3LS.js +274 -0
- package/dist/{save-skills-X7U3KCPU.js → save-skills-NPKTYNAF.js} +2 -1
- package/dist/{trace-X6TU3AG6.js → trace-UVMZZRA5.js} +1 -1
- package/dist/{trace-adopt-URECQWJV.js → trace-adopt-7HWELJFE.js} +1 -1
- package/dist/{trace-run-7U4WJZ3V.js → trace-run-CZWEZ4R6.js} +8 -4
- package/dist/{triage-FCYHD2AQ.js → triage-IFCVL5MA.js} +7 -6
- package/dist/{verify-6SC4I77M.js → verify-HWZBTK5X.js} +16 -12
- package/package.json +1 -1
- package/dist/chunk-MRZVA5JB.js +0 -163
package/dist/cli.js
CHANGED
|
@@ -15,31 +15,25 @@ import {
|
|
|
15
15
|
restoreTerminal,
|
|
16
16
|
runJob,
|
|
17
17
|
sttySane
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-2WXG35EM.js";
|
|
19
19
|
import "./chunk-M2RKCIGV.js";
|
|
20
20
|
import {
|
|
21
|
-
CODE_TEAM,
|
|
22
21
|
CliProvider,
|
|
23
|
-
DEFAULT_COUNCIL,
|
|
24
|
-
DEFAULT_PROMPTS,
|
|
25
|
-
DEFAULT_ROLE_SKILLS,
|
|
26
|
-
PLAN_TEAM,
|
|
27
|
-
REQUIRED_ROLES,
|
|
28
22
|
RoleFitness,
|
|
29
|
-
RoleRegistry,
|
|
30
23
|
SHORT_CALL_MS,
|
|
31
|
-
SPEC_TEAM,
|
|
32
24
|
WorktreeManager,
|
|
33
25
|
buildCouncilRegistry,
|
|
34
26
|
buildTeamRegistry,
|
|
35
|
-
checkProfile,
|
|
36
|
-
cliCatalog,
|
|
37
27
|
mainWorktreeRoot,
|
|
38
|
-
runLogin,
|
|
39
28
|
toSlug
|
|
40
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-27F44PBD.js";
|
|
41
30
|
import "./chunk-QF4MP6BS.js";
|
|
42
|
-
import
|
|
31
|
+
import {
|
|
32
|
+
MAX_CHUNK_CHARS,
|
|
33
|
+
discover,
|
|
34
|
+
readFinding
|
|
35
|
+
} from "./chunk-QJYVZPLG.js";
|
|
36
|
+
import "./chunk-UTHLEW5V.js";
|
|
43
37
|
import "./chunk-ZSQ24YDJ.js";
|
|
44
38
|
import {
|
|
45
39
|
parseFrontmatter
|
|
@@ -48,53 +42,75 @@ import {
|
|
|
48
42
|
UNSET_MODEL,
|
|
49
43
|
loadConfig,
|
|
50
44
|
saveSkillSource
|
|
51
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-JR2JLRE3.js";
|
|
52
46
|
import "./chunk-H2FDGPVW.js";
|
|
53
47
|
import {
|
|
48
|
+
CODE_TEAM,
|
|
49
|
+
DEFAULT_COUNCIL,
|
|
50
|
+
DEFAULT_PROMPTS,
|
|
51
|
+
DEFAULT_ROLE_SKILLS,
|
|
52
|
+
PLAN_TEAM,
|
|
53
|
+
REQUIRED_ROLES,
|
|
54
|
+
RoleRegistry,
|
|
55
|
+
SPEC_TEAM,
|
|
56
|
+
ZAI_MODELS,
|
|
54
57
|
buildBrief,
|
|
58
|
+
cliCatalog,
|
|
55
59
|
describePlan,
|
|
60
|
+
modelsFor,
|
|
56
61
|
planFor,
|
|
57
62
|
runTraces,
|
|
58
63
|
traceableFiles
|
|
59
|
-
} from "./chunk-
|
|
64
|
+
} from "./chunk-YULQ4URQ.js";
|
|
65
|
+
import {
|
|
66
|
+
CLI_KINDS,
|
|
67
|
+
checkProfile,
|
|
68
|
+
runLogin
|
|
69
|
+
} from "./chunk-G45RWL7S.js";
|
|
60
70
|
import {
|
|
61
71
|
defaultGitRunner
|
|
62
72
|
} from "./chunk-LPQU436C.js";
|
|
63
73
|
import {
|
|
64
74
|
InjectionLog,
|
|
65
75
|
memoryNote
|
|
66
|
-
} from "./chunk-
|
|
76
|
+
} from "./chunk-BFIZMM4G.js";
|
|
67
77
|
import "./chunk-63E73TGI.js";
|
|
78
|
+
import {
|
|
79
|
+
ToolRegistry
|
|
80
|
+
} from "./chunk-CYLPQWIF.js";
|
|
81
|
+
import {
|
|
82
|
+
briefStatus
|
|
83
|
+
} from "./chunk-6S4WWQMN.js";
|
|
68
84
|
import {
|
|
69
85
|
Telemetry,
|
|
70
|
-
ToolRegistry,
|
|
71
86
|
clearPerfMarks,
|
|
72
87
|
sampleMemory,
|
|
73
88
|
setTelemetry,
|
|
74
89
|
stripThinking,
|
|
75
90
|
telemetry,
|
|
76
91
|
writeHeapSnapshot
|
|
77
|
-
} from "./chunk-
|
|
78
|
-
import {
|
|
79
|
-
briefStatus
|
|
80
|
-
} from "./chunk-6OSEQOYY.js";
|
|
92
|
+
} from "./chunk-JLWQCA7B.js";
|
|
81
93
|
import {
|
|
82
94
|
discoverTraceRoot,
|
|
83
|
-
|
|
84
|
-
|
|
95
|
+
ensureGitignore,
|
|
96
|
+
localOnly,
|
|
97
|
+
setTraceRoot,
|
|
98
|
+
sharedDerived,
|
|
99
|
+
traceRootRel
|
|
100
|
+
} from "./chunk-ZPJP2VH5.js";
|
|
85
101
|
import {
|
|
86
102
|
buildProjectGraph,
|
|
87
103
|
graphStatus,
|
|
88
104
|
graphifyPython
|
|
89
|
-
} from "./chunk-
|
|
105
|
+
} from "./chunk-4M6LXNG2.js";
|
|
90
106
|
import {
|
|
91
107
|
sessionBase
|
|
92
108
|
} from "./chunk-6W4UH2BQ.js";
|
|
93
109
|
|
|
94
110
|
// src/cli.ts
|
|
95
111
|
import { execFileSync } from "child_process";
|
|
96
|
-
import { readFileSync as
|
|
97
|
-
import { join as
|
|
112
|
+
import { readFileSync as readFileSync5, existsSync as existsSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, realpathSync, rmSync, statSync as statSync2 } from "fs";
|
|
113
|
+
import { join as join12, dirname as dirname5 } from "path";
|
|
98
114
|
import { pathToFileURL } from "url";
|
|
99
115
|
|
|
100
116
|
// src/agents/cli-accounts.ts
|
|
@@ -181,6 +197,26 @@ var AccountPool = class {
|
|
|
181
197
|
return kind ? this.accounts.filter((a) => a.kind === kind).length : this.accounts.length;
|
|
182
198
|
}
|
|
183
199
|
};
|
|
200
|
+
function modelsPanel(usage2, models, kinds, inUse = [], now = Date.now()) {
|
|
201
|
+
const used = new Set(inUse);
|
|
202
|
+
const rows = [];
|
|
203
|
+
for (const kind of kinds) {
|
|
204
|
+
const mine = usage2.filter((u) => u.account.kind === kind);
|
|
205
|
+
const who = mine.length ? mine.map(({ account, reading }) => {
|
|
206
|
+
const name = account.email ?? account.plan ?? account.name;
|
|
207
|
+
return `${name}${reading ? ` \xB7 ${Math.round(reading.spent * 100)}% used ${ageOf(reading.at, now)} ago` : ""}`;
|
|
208
|
+
}).join(" \xB7 ") : "_not connected \u2014 these will fail every call_";
|
|
209
|
+
rows.push(`**${kind}** \u2014 ${who}`);
|
|
210
|
+
rows.push(` ${models(kind).map((m) => `${used.has(m) ? "\u25CF" : "\xB7"} ${m}`).join(" ")}`);
|
|
211
|
+
}
|
|
212
|
+
const idle = kinds.filter((k) => usage2.some((u) => u.account.kind === k)).filter((k) => !models(k).some((m) => used.has(m)));
|
|
213
|
+
const note = idle.length ? `
|
|
214
|
+
|
|
215
|
+
_\u25CF = a role is running it. ${idle.join(" and ")} ${idle.length > 1 ? "are" : "is"} connected but in no chain \u2014 \`/roles adjust\` assigns them._` : "\n\n_\u25CF = a role is running it._";
|
|
216
|
+
return `**Models** \u2014 what your connected subscriptions serve
|
|
217
|
+
|
|
218
|
+
${rows.join("\n")}${note}`;
|
|
219
|
+
}
|
|
184
220
|
function ageOf(at, now) {
|
|
185
221
|
const mins = Math.max(0, Math.round((now - at) / 6e4));
|
|
186
222
|
if (mins < 60) return `${mins}m`;
|
|
@@ -207,9 +243,11 @@ function freshDir(kind, home, taken) {
|
|
|
207
243
|
}
|
|
208
244
|
}
|
|
209
245
|
function nameFor(kind, status, dir) {
|
|
210
|
-
|
|
246
|
+
const base = dir.split("/").pop() ?? "profile";
|
|
247
|
+
return status.email ?? (base.startsWith(`${kind}-`) ? base : `${kind}-${base}`);
|
|
211
248
|
}
|
|
212
249
|
function withAmbient(kind, existing, ambient) {
|
|
250
|
+
if (kind === "zai") return [...existing];
|
|
213
251
|
if (existing.some((a) => a.kind === kind)) return [...existing];
|
|
214
252
|
if (!ambient.loggedIn) return [...existing];
|
|
215
253
|
return [
|
|
@@ -228,11 +266,11 @@ function accountsIn(config) {
|
|
|
228
266
|
return raw.filter((a) => {
|
|
229
267
|
const e = a;
|
|
230
268
|
if (typeof a !== "object" || a === null) return false;
|
|
231
|
-
if (
|
|
269
|
+
if (!CLI_KINDS.includes(e.kind)) return false;
|
|
232
270
|
return e.configDir === void 0 || typeof e.configDir === "string";
|
|
233
271
|
});
|
|
234
272
|
}
|
|
235
|
-
function addProvider(kind, io) {
|
|
273
|
+
async function addProvider(kind, io) {
|
|
236
274
|
const config = io.readConfig();
|
|
237
275
|
const existing = accountsIn(config);
|
|
238
276
|
const accounts = withAmbient(kind, existing, io.check(kind));
|
|
@@ -242,12 +280,26 @@ function addProvider(kind, io) {
|
|
|
242
280
|
io.log(`It stays first in line, so runs keep spending it before anything added now.`);
|
|
243
281
|
}
|
|
244
282
|
const dir = freshDir(kind, io.home, accounts);
|
|
245
|
-
|
|
283
|
+
if (kind === "zai") {
|
|
284
|
+
io.log(`
|
|
285
|
+
z.ai has no sign-in \u2014 an account is an API key, and the profile at ${dir} will hold it.`);
|
|
286
|
+
io.log(`Take the key from your z.ai dashboard; it is written to that profile alone and nowhere else.
|
|
287
|
+
`);
|
|
288
|
+
} else {
|
|
289
|
+
io.log(`
|
|
246
290
|
Opening ${kind}'s own sign-in, for a NEW profile at ${dir}.`);
|
|
247
|
-
|
|
291
|
+
io.log(`Sign in with the account you want to add \u2014 not the one already connected.
|
|
248
292
|
`);
|
|
249
|
-
|
|
293
|
+
}
|
|
294
|
+
const r = await io.login(kind, dir);
|
|
250
295
|
if (!r.ok) {
|
|
296
|
+
if (kind === "zai") {
|
|
297
|
+
io.log(`
|
|
298
|
+
z.ai did not accept that key${r.error ? `: ${r.error}` : ""}. Nothing was changed.`);
|
|
299
|
+
io.log(`The key is checked by making one real call, because \`claude auth status\` reports any token as connected \u2014 valid or not.`);
|
|
300
|
+
io.log(`Nothing needs cleaning up: running this again reuses ${dir}.`);
|
|
301
|
+
return 1;
|
|
302
|
+
}
|
|
251
303
|
io.log(`
|
|
252
304
|
${kind} sign-in did not complete${r.error ? `: ${r.error}` : ""}. Nothing was changed.`);
|
|
253
305
|
io.log(`If a browser page opened, its address holds the reason \u2014 an \`error=\` there is the account being refused, not this command failing.`);
|
|
@@ -277,13 +329,203 @@ That is the account already connected as "${already.name}". Nothing was changed.
|
|
|
277
329
|
const mine = [...accounts, entry].filter((a) => a.kind === kind);
|
|
278
330
|
io.log(`
|
|
279
331
|
Connected ${entry.email ?? entry.name}${entry.plan ? ` (${entry.plan})` : ""}.`);
|
|
280
|
-
io.log(`${mine.length} ${kind} account${mine.length === 1 ? "" : "s"} now connected. Runs
|
|
332
|
+
io.log(`${mine.length} ${kind} account${mine.length === 1 ? "" : "s"} now connected. Runs take turns between them.`);
|
|
333
|
+
if (kind === "claude") io.log(`One nearly out of quota steps aside for the others.`);
|
|
334
|
+
else io.log(`${kind} reports no quota figures, so a spent account is discovered by being refused, not before.`);
|
|
335
|
+
if (kind === "zai" && mine.length > 1) {
|
|
336
|
+
io.log(`z.ai reports no account identity, so this cannot be checked: if that key is one already connected, remove the duplicate from ~/.horsecode/config.json.`);
|
|
337
|
+
}
|
|
281
338
|
return 0;
|
|
282
339
|
}
|
|
283
340
|
|
|
341
|
+
// src/agents/zai-profile.ts
|
|
342
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
343
|
+
import { join as join3 } from "path";
|
|
344
|
+
var ZAI_BASE_URL = "https://api.z.ai/api/anthropic";
|
|
345
|
+
function zaiSettings(token) {
|
|
346
|
+
return { env: { ANTHROPIC_BASE_URL: ZAI_BASE_URL, ANTHROPIC_AUTH_TOKEN: token } };
|
|
347
|
+
}
|
|
348
|
+
function settingsPath(dir) {
|
|
349
|
+
return join3(dir, "settings.json");
|
|
350
|
+
}
|
|
351
|
+
function writeZaiProfile(dir, token) {
|
|
352
|
+
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
353
|
+
writeFileSync2(settingsPath(dir), JSON.stringify(zaiSettings(token), null, 2) + "\n", { mode: 384 });
|
|
354
|
+
}
|
|
355
|
+
async function verifyZaiKey(token, model, timeoutMs = 3e4) {
|
|
356
|
+
const ctl = new AbortController();
|
|
357
|
+
const timer = setTimeout(() => ctl.abort(), timeoutMs);
|
|
358
|
+
try {
|
|
359
|
+
const res = await fetch(`${ZAI_BASE_URL}/v1/messages`, {
|
|
360
|
+
method: "POST",
|
|
361
|
+
signal: ctl.signal,
|
|
362
|
+
headers: {
|
|
363
|
+
"content-type": "application/json",
|
|
364
|
+
// Both spellings: z.ai accepts the bearer form, and `x-api-key` is what the Anthropic wire format
|
|
365
|
+
// names. Sending each costs nothing and removes a guess about which one this endpoint reads.
|
|
366
|
+
authorization: `Bearer ${token}`,
|
|
367
|
+
"x-api-key": token,
|
|
368
|
+
"anthropic-version": "2023-06-01"
|
|
369
|
+
},
|
|
370
|
+
body: JSON.stringify({ model, max_tokens: 16, messages: [{ role: "user", content: "Reply with: ok" }] })
|
|
371
|
+
});
|
|
372
|
+
const body = await res.json().catch(() => void 0);
|
|
373
|
+
if (!res.ok) return { ok: false, error: zaiErrorText(body) ?? `the endpoint answered HTTP ${res.status}` };
|
|
374
|
+
const served = body?.model;
|
|
375
|
+
return { ok: true, ...served ? { served } : {} };
|
|
376
|
+
} catch (e) {
|
|
377
|
+
const aborted = e instanceof Error && e.name === "AbortError";
|
|
378
|
+
return {
|
|
379
|
+
ok: false,
|
|
380
|
+
error: aborted ? `${ZAI_BASE_URL} did not answer within ${Math.round(timeoutMs / 1e3)}s` : e instanceof Error ? e.message : String(e)
|
|
381
|
+
};
|
|
382
|
+
} finally {
|
|
383
|
+
clearTimeout(timer);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function zaiErrorText(body) {
|
|
387
|
+
const msg = body?.error?.message;
|
|
388
|
+
return typeof msg === "string" && msg.trim() ? msg.trim() : void 0;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/agents/prompt.ts
|
|
392
|
+
import { spawnSync } from "child_process";
|
|
393
|
+
import { closeSync, openSync, readSync } from "fs";
|
|
394
|
+
function promptLine() {
|
|
395
|
+
return readTty(false);
|
|
396
|
+
}
|
|
397
|
+
function promptSecret() {
|
|
398
|
+
return readTty(true);
|
|
399
|
+
}
|
|
400
|
+
function confirm(question) {
|
|
401
|
+
process.stdout.write(`${question} [y/N] `);
|
|
402
|
+
const answer = promptLine();
|
|
403
|
+
process.stdout.write("\n");
|
|
404
|
+
return /^y(es)?$/i.test((answer ?? "").trim());
|
|
405
|
+
}
|
|
406
|
+
function chooseFrom(question, options) {
|
|
407
|
+
process.stdout.write(`${question}
|
|
408
|
+
`);
|
|
409
|
+
options.forEach((o, i) => process.stdout.write(` ${i + 1}) ${o}
|
|
410
|
+
`));
|
|
411
|
+
process.stdout.write(`Number (anything else cancels): `);
|
|
412
|
+
const line = promptLine();
|
|
413
|
+
process.stdout.write("\n");
|
|
414
|
+
const n = Number((line ?? "").trim());
|
|
415
|
+
return Number.isInteger(n) && n >= 1 && n <= options.length ? n - 1 : void 0;
|
|
416
|
+
}
|
|
417
|
+
function readTty(hidden) {
|
|
418
|
+
let fd;
|
|
419
|
+
try {
|
|
420
|
+
fd = openSync("/dev/tty", "r");
|
|
421
|
+
} catch {
|
|
422
|
+
return void 0;
|
|
423
|
+
}
|
|
424
|
+
const hushed = hidden && spawnSync("stty", ["-echo"], { stdio: ["inherit", "ignore", "ignore"] }).status === 0;
|
|
425
|
+
try {
|
|
426
|
+
const buf = Buffer.alloc(1);
|
|
427
|
+
let out = "";
|
|
428
|
+
for (; ; ) {
|
|
429
|
+
let n = 0;
|
|
430
|
+
try {
|
|
431
|
+
n = readSync(fd, buf, 0, 1, null);
|
|
432
|
+
} catch {
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
if (n === 0) break;
|
|
436
|
+
const ch = buf.toString("utf8");
|
|
437
|
+
if (ch === "\n" || ch === "\r") break;
|
|
438
|
+
out += ch;
|
|
439
|
+
}
|
|
440
|
+
return out.trim() || void 0;
|
|
441
|
+
} finally {
|
|
442
|
+
if (hushed) spawnSync("stty", ["echo"], { stdio: ["inherit", "ignore", "ignore"] });
|
|
443
|
+
closeSync(fd);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// src/agents/remove-provider.ts
|
|
448
|
+
function ownedProfile(home, dir) {
|
|
449
|
+
if (!dir) return false;
|
|
450
|
+
const root = `${home}/.horsecode/profiles/`;
|
|
451
|
+
return dir.startsWith(root) && dir.length > root.length && !dir.slice(root.length).includes("..");
|
|
452
|
+
}
|
|
453
|
+
function describeAccount(a) {
|
|
454
|
+
const who = a.email ?? a.name;
|
|
455
|
+
return `${a.kind} ${who}${a.plan ? ` (${a.plan})` : ""}${a.configDir ? "" : " \u2014 the signed-in default"}`;
|
|
456
|
+
}
|
|
457
|
+
function pickAccount(accounts, kind, name) {
|
|
458
|
+
const mine = accounts.filter((a) => a.kind === kind);
|
|
459
|
+
if (!mine.length) return { error: `no ${kind} account is connected` };
|
|
460
|
+
if (!name) return mine.length === 1 ? { account: mine[0] } : { choices: mine };
|
|
461
|
+
const found = mine.filter((a) => a.name === name || a.email === name);
|
|
462
|
+
if (!found.length) {
|
|
463
|
+
return {
|
|
464
|
+
error: `no ${kind} account called "${name}". Connected:
|
|
465
|
+
` + mine.map((a) => ` ${a.name}${a.email && a.email !== a.name ? ` (${a.email})` : ""}`).join("\n")
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
if (found.length > 1) return { error: `"${name}" matches ${found.length} ${kind} accounts \u2014 remove one by its exact name` };
|
|
469
|
+
return { account: found[0] };
|
|
470
|
+
}
|
|
471
|
+
function removeProvider(kind, name, io) {
|
|
472
|
+
const config = io.readConfig();
|
|
473
|
+
const accounts = accountsIn(config);
|
|
474
|
+
const picked = pickAccount(accounts, kind, name);
|
|
475
|
+
if ("error" in picked) {
|
|
476
|
+
io.log(picked.error);
|
|
477
|
+
const others = accounts.filter((a) => a.kind !== kind);
|
|
478
|
+
if (!accounts.filter((a) => a.kind === kind).length && others.length) {
|
|
479
|
+
io.log(`
|
|
480
|
+
Connected accounts:
|
|
481
|
+
${others.map((a) => ` ${describeAccount(a)}`).join("\n")}`);
|
|
482
|
+
}
|
|
483
|
+
return { code: 1 };
|
|
484
|
+
}
|
|
485
|
+
let account;
|
|
486
|
+
if ("account" in picked) {
|
|
487
|
+
account = picked.account;
|
|
488
|
+
} else {
|
|
489
|
+
const chosen = io.choose(`Which ${kind} account should be removed?`, picked.choices.map(describeAccount));
|
|
490
|
+
if (chosen === void 0) {
|
|
491
|
+
io.log("Nothing was removed.");
|
|
492
|
+
return { code: 1 };
|
|
493
|
+
}
|
|
494
|
+
account = picked.choices[chosen];
|
|
495
|
+
}
|
|
496
|
+
const kept = accounts.filter((a) => a !== account);
|
|
497
|
+
io.writeConfig({ ...config, accounts: kept });
|
|
498
|
+
if (io.usage) {
|
|
499
|
+
const readings = io.usage.load();
|
|
500
|
+
const { [readingKey(kind, account.name)]: _dropped, ...rest } = readings;
|
|
501
|
+
io.usage.save(rest);
|
|
502
|
+
}
|
|
503
|
+
io.log(`Removed ${describeAccount(account)}.`);
|
|
504
|
+
if (!account.configDir) {
|
|
505
|
+
io.log(`That was the account you are signed into ${kind} with, not a profile horse-code created.`);
|
|
506
|
+
io.log(`It is still signed in, and a run will find it again. To stop using it, sign out of ${kind} itself.`);
|
|
507
|
+
return { code: 0, removed: account };
|
|
508
|
+
}
|
|
509
|
+
if (!ownedProfile(io.home, account.configDir)) {
|
|
510
|
+
io.log(`Its profile at ${account.configDir} was not created by horse-code, so it is left alone.`);
|
|
511
|
+
return { code: 0, removed: account };
|
|
512
|
+
}
|
|
513
|
+
const holds = kind === "zai" ? "the API key" : `the ${kind} sign-in`;
|
|
514
|
+
if (!io.confirm(`Also delete ${account.configDir}, which holds ${holds}?`)) {
|
|
515
|
+
io.log(`Left ${account.configDir} in place. Delete it by hand to remove ${holds}.`);
|
|
516
|
+
return { code: 0, removed: account };
|
|
517
|
+
}
|
|
518
|
+
io.removeDir(account.configDir);
|
|
519
|
+
io.log(`Deleted ${account.configDir}.`);
|
|
520
|
+
return { code: 0, removed: account };
|
|
521
|
+
}
|
|
522
|
+
function removeUsage(given) {
|
|
523
|
+
return `remove-provider needs a CLI to disconnect: ${CLI_KINDS.map((k) => `\`hcode remove-provider ${k}\``).join(", ")}${given ? ` (got "${given}")` : ""}`;
|
|
524
|
+
}
|
|
525
|
+
|
|
284
526
|
// src/skills/registry.ts
|
|
285
527
|
import { readdir, readFile } from "fs/promises";
|
|
286
|
-
import { join as
|
|
528
|
+
import { join as join4 } from "path";
|
|
287
529
|
var SkillRegistry = class {
|
|
288
530
|
skills = /* @__PURE__ */ new Map();
|
|
289
531
|
register(skill) {
|
|
@@ -306,7 +548,7 @@ var SkillRegistry = class {
|
|
|
306
548
|
if (!e.isDirectory()) continue;
|
|
307
549
|
let raw;
|
|
308
550
|
try {
|
|
309
|
-
raw = await readFile(
|
|
551
|
+
raw = await readFile(join4(dir, e.name, "SKILL.md"), "utf8");
|
|
310
552
|
} catch {
|
|
311
553
|
continue;
|
|
312
554
|
}
|
|
@@ -314,14 +556,14 @@ var SkillRegistry = class {
|
|
|
314
556
|
if (!name || !description) {
|
|
315
557
|
throw new Error(`skill ${e.name}: missing frontmatter (name/description)`);
|
|
316
558
|
}
|
|
317
|
-
this.register({ name, description, content: body, dir:
|
|
559
|
+
this.register({ name, description, content: body, dir: join4(dir, e.name) });
|
|
318
560
|
}
|
|
319
561
|
}
|
|
320
562
|
};
|
|
321
563
|
|
|
322
564
|
// src/skills/builtin.ts
|
|
323
565
|
import { readdir as readdir2, readFile as readFile2 } from "fs/promises";
|
|
324
|
-
import { dirname as dirname2, join as
|
|
566
|
+
import { dirname as dirname2, join as join5, resolve } from "path";
|
|
325
567
|
import { fileURLToPath } from "url";
|
|
326
568
|
function candidates() {
|
|
327
569
|
const here = dirname2(fileURLToPath(import.meta.url));
|
|
@@ -342,14 +584,14 @@ async function registerBuiltinSkills(registry, dirs = candidates()) {
|
|
|
342
584
|
if (!e.isDirectory()) continue;
|
|
343
585
|
let raw;
|
|
344
586
|
try {
|
|
345
|
-
raw = await readFile2(
|
|
587
|
+
raw = await readFile2(join5(dir, e.name, "SKILL.md"), "utf8");
|
|
346
588
|
} catch {
|
|
347
589
|
continue;
|
|
348
590
|
}
|
|
349
591
|
const { parseFrontmatter: parseFrontmatter2 } = await import("./frontmatter-UNIPNLLO.js");
|
|
350
592
|
const { name, description, body } = parseFrontmatter2(raw);
|
|
351
593
|
if (!name || !description) continue;
|
|
352
|
-
found.push({ name, description, content: body, dir:
|
|
594
|
+
found.push({ name, description, content: body, dir: join5(dir, e.name) });
|
|
353
595
|
}
|
|
354
596
|
if (!found.length) continue;
|
|
355
597
|
for (const s of found) registry.register(s);
|
|
@@ -364,12 +606,12 @@ async function registerBuiltinSkills(registry, dirs = candidates()) {
|
|
|
364
606
|
// src/skills/external.ts
|
|
365
607
|
import { mkdir, rm, readFile as readFile3, writeFile, readdir as readdir3, cp } from "fs/promises";
|
|
366
608
|
import { existsSync } from "fs";
|
|
367
|
-
import { join as
|
|
609
|
+
import { join as join6 } from "path";
|
|
368
610
|
import { spawn } from "child_process";
|
|
369
611
|
import { tmpdir } from "os";
|
|
370
612
|
var PROVENANCE = ".horsecode-source.json";
|
|
371
613
|
function externalSkillsDir(home) {
|
|
372
|
-
return
|
|
614
|
+
return join6(home, ".horsecode", "skills");
|
|
373
615
|
}
|
|
374
616
|
function validName(name) {
|
|
375
617
|
return /^[A-Za-z0-9._-]+$/.test(name) && !name.includes("..");
|
|
@@ -413,7 +655,7 @@ async function resolveSha(src, deps) {
|
|
|
413
655
|
}
|
|
414
656
|
async function installedSource(home, name) {
|
|
415
657
|
try {
|
|
416
|
-
return JSON.parse(await readFile3(
|
|
658
|
+
return JSON.parse(await readFile3(join6(externalSkillsDir(home), name, PROVENANCE), "utf8"));
|
|
417
659
|
} catch {
|
|
418
660
|
return void 0;
|
|
419
661
|
}
|
|
@@ -423,27 +665,27 @@ async function installSkillSource(home, src, deps = {}) {
|
|
|
423
665
|
if (!validRepo(src.repo)) throw new Error(`skill source repo must be "owner/repo": ${src.repo}`);
|
|
424
666
|
if (src.path?.includes("..")) throw new Error(`skill source path may not contain "..": ${src.path}`);
|
|
425
667
|
const sha = await resolveSha(src, deps);
|
|
426
|
-
const dest =
|
|
668
|
+
const dest = join6(externalSkillsDir(home), src.name);
|
|
427
669
|
const current = await installedSource(home, src.name);
|
|
428
|
-
if (current?.sha === sha && existsSync(
|
|
670
|
+
if (current?.sha === sha && existsSync(join6(dest, "SKILL.md"))) {
|
|
429
671
|
return { name: src.name, sha, changed: false };
|
|
430
672
|
}
|
|
431
|
-
const staging =
|
|
673
|
+
const staging = join6(tmpdir(), `hc-skill-${src.name}-${sha.slice(0, 8)}`);
|
|
432
674
|
await rm(staging, { recursive: true, force: true });
|
|
433
675
|
await mkdir(staging, { recursive: true });
|
|
434
676
|
try {
|
|
435
677
|
const url = `https://codeload.github.com/${src.repo}/tar.gz/${sha}`;
|
|
436
678
|
const dl = await run("sh", ["-c", `curl -fsSL --max-time 120 ${JSON.stringify(url)} | tar xz -C ${JSON.stringify(staging)} --strip-components=1`]);
|
|
437
679
|
if (dl.code !== 0) throw new Error(`download failed for ${src.repo}@${sha.slice(0, 8)}: ${dl.err.trim().slice(0, 200)}`);
|
|
438
|
-
const from = src.path ?
|
|
439
|
-
if (!existsSync(
|
|
680
|
+
const from = src.path ? join6(staging, src.path) : staging;
|
|
681
|
+
if (!existsSync(join6(from, "SKILL.md"))) {
|
|
440
682
|
throw new Error(`${src.repo}${src.path ? `/${src.path}` : ""} has no SKILL.md`);
|
|
441
683
|
}
|
|
442
684
|
await rm(dest, { recursive: true, force: true });
|
|
443
685
|
await mkdir(dest, { recursive: true });
|
|
444
686
|
await cp(from, dest, { recursive: true });
|
|
445
687
|
const record = { ...src, sha, installedAt: (deps.now ?? Date.now)() };
|
|
446
|
-
await writeFile(
|
|
688
|
+
await writeFile(join6(dest, PROVENANCE), `${JSON.stringify(record, null, 2)}
|
|
447
689
|
`, "utf8");
|
|
448
690
|
return { name: src.name, sha, changed: true };
|
|
449
691
|
} finally {
|
|
@@ -463,6 +705,137 @@ async function syncSkillSources(home, sources, deps = {}) {
|
|
|
463
705
|
return { ok, failed };
|
|
464
706
|
}
|
|
465
707
|
|
|
708
|
+
// src/engine/init-project.ts
|
|
709
|
+
import { existsSync as existsSync2, statSync, readdirSync } from "fs";
|
|
710
|
+
import { join as join7 } from "path";
|
|
711
|
+
|
|
712
|
+
// src/migrate/project-docs.ts
|
|
713
|
+
import { basename } from "path";
|
|
714
|
+
var DERIVED = [
|
|
715
|
+
{ re: /^\.horsecode\//, why: "horse-code's own state \u2014 traces describe the code, so rules mined from them would be the code talking back to itself" },
|
|
716
|
+
{ re: /^graphify-out\//, why: "generated by the graph build" },
|
|
717
|
+
{ re: /(^|\/)(node_modules|vendor|third_party|\.git)\//, why: "not this project's writing" },
|
|
718
|
+
{ re: /(^|\/)\.claude\/worktrees\//, why: "a nested checkout of this repository" }
|
|
719
|
+
];
|
|
720
|
+
var HISTORY = [
|
|
721
|
+
{ re: /(^|\/)archive(d)?\//i, why: "an archive of finished work" },
|
|
722
|
+
{ re: /\d{4}-\d{2}-\d{2}/, why: "dated \u2014 a record of a moment, not a standing rule" },
|
|
723
|
+
{ re: /(^|\/)(HANDOFF|RESUME|CHANGELOG|RELEASE[-_]NOTES)[^/]*$/i, why: "a record of what happened" },
|
|
724
|
+
{ re: /(^|\/)chats?\//i, why: "a pasted conversation" },
|
|
725
|
+
{ re: /(^|\/)chat\d*\.md$/i, why: "a pasted conversation" }
|
|
726
|
+
];
|
|
727
|
+
function sortProjectDocs(files, known = []) {
|
|
728
|
+
const seen = new Set(known.map((k) => k.replace(/\\/g, "/")));
|
|
729
|
+
const candidates2 = [];
|
|
730
|
+
const skipped = [];
|
|
731
|
+
for (const f of files) {
|
|
732
|
+
const path = f.path.replace(/\\/g, "/");
|
|
733
|
+
if (!/\.mdx?$/i.test(path)) continue;
|
|
734
|
+
if (seen.has(path) || seen.has(basename(path))) continue;
|
|
735
|
+
const why = DERIVED.find((d) => d.re.test(path))?.why ?? HISTORY.find((h) => h.re.test(path))?.why;
|
|
736
|
+
if (why) skipped.push({ path, bytes: f.bytes, skipped: why });
|
|
737
|
+
else candidates2.push({ path, bytes: f.bytes });
|
|
738
|
+
}
|
|
739
|
+
const bySize = (a, b) => b.bytes - a.bytes;
|
|
740
|
+
return { candidates: candidates2.sort(bySize), skipped: skipped.sort(bySize) };
|
|
741
|
+
}
|
|
742
|
+
function totalBytes(docs) {
|
|
743
|
+
return docs.reduce((n, d) => n + d.bytes, 0);
|
|
744
|
+
}
|
|
745
|
+
function classifyCalls(bytes, chunk) {
|
|
746
|
+
return Math.max(1, Math.ceil(bytes / chunk));
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// src/engine/init-project.ts
|
|
750
|
+
var UNCLAIMED_FILE_FLOOR = 50;
|
|
751
|
+
function countFiles(dir, cap = UNCLAIMED_FILE_FLOOR * 4) {
|
|
752
|
+
let n = 0;
|
|
753
|
+
const walk = (d) => {
|
|
754
|
+
if (n >= cap) return;
|
|
755
|
+
let entries;
|
|
756
|
+
try {
|
|
757
|
+
entries = readdirSync(d, { withFileTypes: true });
|
|
758
|
+
} catch {
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
for (const e of entries) {
|
|
762
|
+
if (n >= cap) return;
|
|
763
|
+
if (e.isDirectory()) walk(join7(d, e.name));
|
|
764
|
+
else n++;
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
walk(dir);
|
|
768
|
+
return n;
|
|
769
|
+
}
|
|
770
|
+
async function initProject(cwd, isIgnored, untracked, knowledge) {
|
|
771
|
+
const changed = await ensureGitignore(cwd);
|
|
772
|
+
const unclaimed = [];
|
|
773
|
+
for (const rel of untracked()) {
|
|
774
|
+
const path = join7(cwd, rel);
|
|
775
|
+
let dir = false;
|
|
776
|
+
try {
|
|
777
|
+
dir = statSync(path).isDirectory();
|
|
778
|
+
} catch {
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
if (!dir || isIgnored(rel)) continue;
|
|
782
|
+
const files = countFiles(path);
|
|
783
|
+
if (files >= UNCLAIMED_FILE_FLOOR) unclaimed.push({ path: rel, files });
|
|
784
|
+
}
|
|
785
|
+
return {
|
|
786
|
+
changed,
|
|
787
|
+
kept: [`${traceRootRel().replace(/\\/g, "/")}/`, ...sharedDerived()].filter((p) => existsSync2(join7(cwd, p)) || p.endsWith("/")),
|
|
788
|
+
excluded: localOnly(),
|
|
789
|
+
unclaimed: unclaimed.sort((a, b) => b.files - a.files),
|
|
790
|
+
...knowledge ? { knowledge } : {}
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
function hasKnowledge(k) {
|
|
794
|
+
return !!k && (k.named.length > 0 || k.docs.length > 0);
|
|
795
|
+
}
|
|
796
|
+
function size(bytes) {
|
|
797
|
+
return bytes >= 1024 * 1024 ? `${(bytes / 1024 / 1024).toFixed(1)} MB` : `${Math.max(1, Math.round(bytes / 1024))} KB`;
|
|
798
|
+
}
|
|
799
|
+
function describeKnowledge(k, chunk = MAX_CHUNK_CHARS) {
|
|
800
|
+
const lines = ["", "**This project already says things about itself.**", ""];
|
|
801
|
+
if (k.named.length) {
|
|
802
|
+
lines.push(`Files a tool is known to write \u2014 ${size(totalBytes(k.named.map((n) => ({ path: n.label, bytes: n.bytes }))))}:`);
|
|
803
|
+
lines.push(...k.named.map((n) => ` \xB7 \`${n.label}\` (${size(n.bytes)})`));
|
|
804
|
+
}
|
|
805
|
+
if (k.docs.length) {
|
|
806
|
+
const bytes = totalBytes(k.docs);
|
|
807
|
+
lines.push("", `The project's other markdown \u2014 ${k.docs.length} file(s), ${size(bytes)}, about ${classifyCalls(bytes, chunk)} classification call(s):`);
|
|
808
|
+
lines.push(...k.docs.slice(0, 8).map((d) => ` \xB7 \`${d.path}\` (${size(d.bytes)})`));
|
|
809
|
+
if (k.docs.length > 8) lines.push(` \xB7 \u2026and ${k.docs.length - 8} more`);
|
|
810
|
+
}
|
|
811
|
+
if (k.skipped.length) {
|
|
812
|
+
const reasons = /* @__PURE__ */ new Map();
|
|
813
|
+
for (const sk of k.skipped) reasons.set(sk.skipped ?? "", (reasons.get(sk.skipped ?? "") ?? 0) + 1);
|
|
814
|
+
lines.push("", `Set aside \u2014 ${k.skipped.length} file(s), ${size(totalBytes(k.skipped))}:`);
|
|
815
|
+
for (const [why, n] of [...reasons].sort((a, b) => b[1] - a[1])) lines.push(` \xB7 ${n} \xD7 ${why}`);
|
|
816
|
+
}
|
|
817
|
+
return lines.join("\n");
|
|
818
|
+
}
|
|
819
|
+
function describeInit(report) {
|
|
820
|
+
const lines = [
|
|
821
|
+
report.changed ? "**Project set up for horse-code** \u2014 `.gitignore` updated." : "**Project is already set up** \u2014 `.gitignore` already says everything needed.",
|
|
822
|
+
"",
|
|
823
|
+
"Committed on purpose, because a clone would otherwise pay to produce it again:",
|
|
824
|
+
...report.kept.map((p) => ` \xB7 \`${p}\``),
|
|
825
|
+
"",
|
|
826
|
+
"Kept out on purpose \u2014 derived from the source, keyed to this checkout, or unmergeable:",
|
|
827
|
+
...report.excluded.map((p) => ` \xB7 \`${p}\``)
|
|
828
|
+
];
|
|
829
|
+
if (report.unclaimed.length) {
|
|
830
|
+
lines.push(
|
|
831
|
+
"",
|
|
832
|
+
"Untracked and covered by no rule \u2014 `git add -A` would commit these. Yours to decide:",
|
|
833
|
+
...report.unclaimed.map((u) => ` \xB7 \`${u.path}\` (${u.files >= UNCLAIMED_FILE_FLOOR * 4 ? `${u.files}+` : u.files} files)`)
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
return lines.join("\n");
|
|
837
|
+
}
|
|
838
|
+
|
|
466
839
|
// src/permission/rules.ts
|
|
467
840
|
import picomatch from "picomatch";
|
|
468
841
|
var DANGEROUS_PATTERNS = [
|
|
@@ -733,8 +1106,8 @@ var ProposalQueue = class {
|
|
|
733
1106
|
};
|
|
734
1107
|
|
|
735
1108
|
// src/speckit/templates.ts
|
|
736
|
-
import { existsSync as
|
|
737
|
-
import { join as
|
|
1109
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
1110
|
+
import { join as join8, dirname as dirname3 } from "path";
|
|
738
1111
|
var TEMPLATE_FILES = {
|
|
739
1112
|
spec: "spec-template.md",
|
|
740
1113
|
plan: "plan-template.md",
|
|
@@ -757,10 +1130,10 @@ async function loadSpecKit(opts) {
|
|
|
757
1130
|
);
|
|
758
1131
|
}
|
|
759
1132
|
const fetchFn = opts.fetch ?? globalThis.fetch;
|
|
760
|
-
const cacheDir =
|
|
1133
|
+
const cacheDir = join8(opts.home, ".horsecode", "spec-kit", opts.version, "templates");
|
|
761
1134
|
const get = async (relPath) => {
|
|
762
|
-
const cachePath =
|
|
763
|
-
if (
|
|
1135
|
+
const cachePath = join8(cacheDir, relPath);
|
|
1136
|
+
if (existsSync3(cachePath)) return readFileSync3(cachePath, "utf8");
|
|
764
1137
|
const url = `${RAW_BASE}/${opts.version}/templates/${relPath}`;
|
|
765
1138
|
const res = await fetchFn(url);
|
|
766
1139
|
if (!res.ok) {
|
|
@@ -770,8 +1143,8 @@ Check your network or set specKit.version to a valid tag.`
|
|
|
770
1143
|
);
|
|
771
1144
|
}
|
|
772
1145
|
const text = await res.text();
|
|
773
|
-
|
|
774
|
-
|
|
1146
|
+
mkdirSync3(dirname3(cachePath), { recursive: true });
|
|
1147
|
+
writeFileSync3(cachePath, text, "utf8");
|
|
775
1148
|
return text;
|
|
776
1149
|
};
|
|
777
1150
|
const templates = {};
|
|
@@ -787,7 +1160,7 @@ Check your network or set specKit.version to a valid tag.`
|
|
|
787
1160
|
|
|
788
1161
|
// src/wiring.ts
|
|
789
1162
|
import { homedir } from "os";
|
|
790
|
-
import { join as
|
|
1163
|
+
import { join as join9 } from "path";
|
|
791
1164
|
async function buildJobDeps(opts) {
|
|
792
1165
|
const { config } = opts;
|
|
793
1166
|
const roles = {};
|
|
@@ -801,7 +1174,12 @@ async function buildJobDeps(opts) {
|
|
|
801
1174
|
const roleRegistry = new RoleRegistry(roles, DEFAULT_PROMPTS, opts.skillRegistry);
|
|
802
1175
|
if (opts.accounts) {
|
|
803
1176
|
const pool = opts.accounts;
|
|
804
|
-
roleRegistry.setSourceWeights(() => ({
|
|
1177
|
+
roleRegistry.setSourceWeights(() => ({
|
|
1178
|
+
claude: pool.count("claude"),
|
|
1179
|
+
codex: pool.count("codex"),
|
|
1180
|
+
grok: pool.count("grok"),
|
|
1181
|
+
zai: pool.count("zai")
|
|
1182
|
+
}));
|
|
805
1183
|
}
|
|
806
1184
|
const fillModels = (r) => ({
|
|
807
1185
|
...r,
|
|
@@ -828,7 +1206,7 @@ async function buildJobDeps(opts) {
|
|
|
828
1206
|
const queue = new ProposalQueue();
|
|
829
1207
|
let kitPromise;
|
|
830
1208
|
const specKit = () => kitPromise ??= loadSpecKit({ version: config.specKit.version, home: opts.home, fetch: opts.fetch });
|
|
831
|
-
const fitness = new RoleFitness(
|
|
1209
|
+
const fitness = new RoleFitness(join9(opts.home ?? homedir(), ".horsecode", "model-fitness.json"));
|
|
832
1210
|
roleRegistry.setFitness(fitness);
|
|
833
1211
|
return {
|
|
834
1212
|
provider: opts.provider,
|
|
@@ -1020,10 +1398,10 @@ function redactRecord(record) {
|
|
|
1020
1398
|
}
|
|
1021
1399
|
|
|
1022
1400
|
// src/obs/sink.ts
|
|
1023
|
-
import { createWriteStream, mkdirSync as
|
|
1024
|
-
import { join as
|
|
1401
|
+
import { createWriteStream, mkdirSync as mkdirSync4 } from "fs";
|
|
1402
|
+
import { join as join10 } from "path";
|
|
1025
1403
|
function telemetryDir(home) {
|
|
1026
|
-
return
|
|
1404
|
+
return join10(home, ".horsecode", "telemetry");
|
|
1027
1405
|
}
|
|
1028
1406
|
var MAX_QUEUE = 1e4;
|
|
1029
1407
|
var FileSink = class {
|
|
@@ -1032,9 +1410,9 @@ var FileSink = class {
|
|
|
1032
1410
|
dropped = 0;
|
|
1033
1411
|
path;
|
|
1034
1412
|
constructor(home, runId) {
|
|
1035
|
-
this.path =
|
|
1413
|
+
this.path = join10(telemetryDir(home), `${runId}.jsonl`);
|
|
1036
1414
|
try {
|
|
1037
|
-
|
|
1415
|
+
mkdirSync4(telemetryDir(home), { recursive: true });
|
|
1038
1416
|
this.stream = createWriteStream(this.path, { flags: "a" });
|
|
1039
1417
|
this.stream.on("error", () => {
|
|
1040
1418
|
this.stream = void 0;
|
|
@@ -1091,13 +1469,13 @@ var FileSink = class {
|
|
|
1091
1469
|
};
|
|
1092
1470
|
|
|
1093
1471
|
// src/cli-info.ts
|
|
1094
|
-
import { readFileSync as
|
|
1095
|
-
import { dirname as dirname4, join as
|
|
1472
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
1473
|
+
import { dirname as dirname4, join as join11 } from "path";
|
|
1096
1474
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1097
1475
|
function hcodeVersion() {
|
|
1098
1476
|
try {
|
|
1099
1477
|
const here = dirname4(fileURLToPath2(import.meta.url));
|
|
1100
|
-
const pkg = JSON.parse(
|
|
1478
|
+
const pkg = JSON.parse(readFileSync4(join11(here, "..", "package.json"), "utf8"));
|
|
1101
1479
|
return pkg.version ?? "unknown";
|
|
1102
1480
|
} catch {
|
|
1103
1481
|
return "unknown";
|
|
@@ -1124,7 +1502,9 @@ function usage() {
|
|
|
1124
1502
|
" hcode start the REPL",
|
|
1125
1503
|
' hcode "<request>" run one request to completion, then report where the work landed',
|
|
1126
1504
|
" hcode init write .horsecode/ for this project and ask for the essentials",
|
|
1127
|
-
" hcode add-provider <cli> connect one more claude
|
|
1505
|
+
" hcode add-provider <cli> connect one more claude, codex, grok or zai subscription",
|
|
1506
|
+
" hcode remove-provider <cli> [name]",
|
|
1507
|
+
" disconnect one, and optionally delete the sign-in it holds",
|
|
1128
1508
|
"",
|
|
1129
1509
|
"OPTIONS",
|
|
1130
1510
|
" -b, --branch <name> branch the session's worktree from this ref (default: current branch)",
|
|
@@ -1152,6 +1532,18 @@ function nearestFlag(flag) {
|
|
|
1152
1532
|
|
|
1153
1533
|
// src/cli.ts
|
|
1154
1534
|
var HEAP_MB = 12288;
|
|
1535
|
+
async function connectZai(dir) {
|
|
1536
|
+
process.stdout.write("z.ai API key (not shown as you type): ");
|
|
1537
|
+
const token = promptSecret();
|
|
1538
|
+
process.stdout.write("\n");
|
|
1539
|
+
if (!token) return { ok: false, error: "no key was entered" };
|
|
1540
|
+
console.log(`Asking ${ZAI_MODELS[0]} one question, to see whether the key works\u2026`);
|
|
1541
|
+
const v = await verifyZaiKey(token, ZAI_MODELS[0]);
|
|
1542
|
+
if (!v.ok) return { ok: false, error: v.error };
|
|
1543
|
+
writeZaiProfile(dir, token);
|
|
1544
|
+
console.log(` answered by ${v.served ?? "the endpoint (which named no model)"}.`);
|
|
1545
|
+
return { ok: true };
|
|
1546
|
+
}
|
|
1155
1547
|
function parseArgs(argv) {
|
|
1156
1548
|
let fromBranch;
|
|
1157
1549
|
let jobName;
|
|
@@ -1310,14 +1702,14 @@ async function main(argv) {
|
|
|
1310
1702
|
read: read2,
|
|
1311
1703
|
readFile: (p) => {
|
|
1312
1704
|
try {
|
|
1313
|
-
return
|
|
1705
|
+
return readFileSync5(p, "utf8");
|
|
1314
1706
|
} catch {
|
|
1315
1707
|
return void 0;
|
|
1316
1708
|
}
|
|
1317
1709
|
},
|
|
1318
1710
|
writeFile: (p, c) => {
|
|
1319
|
-
|
|
1320
|
-
|
|
1711
|
+
mkdirSync5(dirname5(p), { recursive: true });
|
|
1712
|
+
writeFileSync4(p, c);
|
|
1321
1713
|
},
|
|
1322
1714
|
home: process.env.HOME ?? "",
|
|
1323
1715
|
log: (s) => console.log(s)
|
|
@@ -1329,32 +1721,74 @@ async function main(argv) {
|
|
|
1329
1721
|
}
|
|
1330
1722
|
if (argv[0] === "add-provider") {
|
|
1331
1723
|
const kind = argv[1];
|
|
1332
|
-
if (kind
|
|
1333
|
-
console.error(`add-provider needs a CLI to connect:
|
|
1724
|
+
if (!kind || !CLI_KINDS.includes(kind)) {
|
|
1725
|
+
console.error(`add-provider needs a CLI to connect: ${CLI_KINDS.map((k) => `\`hcode add-provider ${k}\``).join(", ")}${argv[1] ? ` (got "${argv[1]}")` : ""}`);
|
|
1334
1726
|
process.exitCode = 1;
|
|
1335
1727
|
return;
|
|
1336
1728
|
}
|
|
1337
1729
|
const path = `${process.env.HOME ?? ""}/.horsecode/config.json`;
|
|
1338
|
-
process.exitCode = addProvider(kind, {
|
|
1730
|
+
process.exitCode = await addProvider(kind, {
|
|
1339
1731
|
home: process.env.HOME ?? "",
|
|
1340
1732
|
readConfig: () => {
|
|
1341
1733
|
try {
|
|
1342
|
-
const parsed = JSON.parse(
|
|
1734
|
+
const parsed = JSON.parse(readFileSync5(path, "utf8"));
|
|
1343
1735
|
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
1344
1736
|
} catch {
|
|
1345
1737
|
return {};
|
|
1346
1738
|
}
|
|
1347
1739
|
},
|
|
1348
1740
|
writeConfig: (c) => {
|
|
1349
|
-
|
|
1350
|
-
|
|
1741
|
+
mkdirSync5(dirname5(path), { recursive: true });
|
|
1742
|
+
writeFileSync4(path, JSON.stringify(c, null, 2) + "\n");
|
|
1351
1743
|
},
|
|
1352
|
-
|
|
1744
|
+
/**
|
|
1745
|
+
* "Make this profile real" means two different things, and z.ai is the one that is not a sign-in.
|
|
1746
|
+
*
|
|
1747
|
+
* The other three hand the terminal to their own OAuth. z.ai has none — a profile is a settings file
|
|
1748
|
+
* holding an endpoint and a key — so here the step is: ask for the key, write it, and then SPEND one
|
|
1749
|
+
* call proving it works. That last part is not ceremony: `claude auth status` reports any token as
|
|
1750
|
+
* connected, valid or not, so without a real call a mistyped key would be filed as a working account
|
|
1751
|
+
* and fail every call of the next run.
|
|
1752
|
+
*/
|
|
1753
|
+
login: (k, dir) => k === "zai" ? connectZai(dir ?? "") : runLogin(k, dir),
|
|
1353
1754
|
check: checkProfile,
|
|
1354
1755
|
log: (line) => console.log(line)
|
|
1355
1756
|
});
|
|
1356
1757
|
return;
|
|
1357
1758
|
}
|
|
1759
|
+
if (argv[0] === "remove-provider") {
|
|
1760
|
+
const kind = argv[1];
|
|
1761
|
+
if (!kind || !CLI_KINDS.includes(kind)) {
|
|
1762
|
+
console.error(removeUsage(argv[1]));
|
|
1763
|
+
process.exitCode = 1;
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
const path = `${process.env.HOME ?? ""}/.horsecode/config.json`;
|
|
1767
|
+
const home2 = process.env.HOME ?? "";
|
|
1768
|
+
process.exitCode = removeProvider(kind, argv[2], {
|
|
1769
|
+
home: home2,
|
|
1770
|
+
readConfig: () => {
|
|
1771
|
+
try {
|
|
1772
|
+
const parsed = JSON.parse(readFileSync5(path, "utf8"));
|
|
1773
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
1774
|
+
} catch {
|
|
1775
|
+
return {};
|
|
1776
|
+
}
|
|
1777
|
+
},
|
|
1778
|
+
writeConfig: (c) => {
|
|
1779
|
+
mkdirSync5(dirname5(path), { recursive: true });
|
|
1780
|
+
writeFileSync4(path, JSON.stringify(c, null, 2) + "\n");
|
|
1781
|
+
},
|
|
1782
|
+
removeDir: (dir) => {
|
|
1783
|
+
rmSync(dir, { recursive: true, force: true });
|
|
1784
|
+
},
|
|
1785
|
+
confirm,
|
|
1786
|
+
choose: chooseFrom,
|
|
1787
|
+
usage: fileUsageStore(home2),
|
|
1788
|
+
log: (line) => console.log(line)
|
|
1789
|
+
}).code;
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1358
1792
|
const cwd = process.cwd();
|
|
1359
1793
|
const config = loadConfig({
|
|
1360
1794
|
cwd,
|
|
@@ -1362,7 +1796,7 @@ async function main(argv) {
|
|
|
1362
1796
|
env: process.env,
|
|
1363
1797
|
readFile: (p) => {
|
|
1364
1798
|
try {
|
|
1365
|
-
return
|
|
1799
|
+
return readFileSync5(p, "utf8");
|
|
1366
1800
|
} catch {
|
|
1367
1801
|
return void 0;
|
|
1368
1802
|
}
|
|
@@ -1370,13 +1804,13 @@ async function main(argv) {
|
|
|
1370
1804
|
});
|
|
1371
1805
|
const home = process.env.HOME ?? "";
|
|
1372
1806
|
if (!process.env.HC_HEAP_SET && !process.env.NODE_OPTIONS) {
|
|
1373
|
-
const { spawnSync } = await import("child_process");
|
|
1807
|
+
const { spawnSync: spawnSync2 } = await import("child_process");
|
|
1374
1808
|
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
1375
1809
|
const ignore = () => {
|
|
1376
1810
|
};
|
|
1377
1811
|
const SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
1378
1812
|
for (const sig of SIGNALS) process.on(sig, ignore);
|
|
1379
|
-
const r =
|
|
1813
|
+
const r = spawnSync2(process.execPath, [`--max-old-space-size=${HEAP_MB}`, ...process.argv.slice(1)], {
|
|
1380
1814
|
stdio: "inherit",
|
|
1381
1815
|
/**
|
|
1382
1816
|
* `NODE_ENV=production` is set here because it must be set BEFORE React is imported.
|
|
@@ -1412,7 +1846,7 @@ async function main(argv) {
|
|
|
1412
1846
|
process.once("SIGTERM", flush);
|
|
1413
1847
|
}
|
|
1414
1848
|
const telemetryNote = sink ? `\u{1F4C8} Telemetry \u2192 \`${sink.path}\` (one JSON object per line)` : void 0;
|
|
1415
|
-
const ambientLogins =
|
|
1849
|
+
const ambientLogins = CLI_KINDS.filter((k) => k !== "zai" && !config.accounts.some((a) => a.kind === k)).map((kind) => ({ kind, status: checkProfile(kind) }));
|
|
1416
1850
|
const accounts = new AccountPool(
|
|
1417
1851
|
[
|
|
1418
1852
|
...config.accounts,
|
|
@@ -1427,13 +1861,43 @@ async function main(argv) {
|
|
|
1427
1861
|
);
|
|
1428
1862
|
const notSignedIn = ambientLogins.filter((a) => !a.status.loggedIn).map((a) => a.kind);
|
|
1429
1863
|
const accountsNote = () => accountsLine(accounts.usage(), notSignedIn);
|
|
1864
|
+
const initProjectFn = async () => {
|
|
1865
|
+
const r = await defaultGitRunner(["ls-files", "--others", "--exclude-standard", "--directory"], cwd);
|
|
1866
|
+
const list = r.stdout.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
1867
|
+
const ignored = /* @__PURE__ */ new Set();
|
|
1868
|
+
for (const path of list) {
|
|
1869
|
+
if ((await defaultGitRunner(["check-ignore", "-q", path], cwd)).code === 0) ignored.add(path);
|
|
1870
|
+
}
|
|
1871
|
+
const all = await defaultGitRunner(["ls-files", "--cached", "--others", "--exclude-standard"], cwd);
|
|
1872
|
+
const md = all.stdout.split("\n").map((l) => l.trim()).filter((l) => /\.mdx?$/i.test(l));
|
|
1873
|
+
const withSize = md.map((path) => {
|
|
1874
|
+
try {
|
|
1875
|
+
return { path, bytes: statSync2(join12(cwd, path)).size };
|
|
1876
|
+
} catch {
|
|
1877
|
+
return { path, bytes: 0 };
|
|
1878
|
+
}
|
|
1879
|
+
});
|
|
1880
|
+
const found = await discover({ cwd, home });
|
|
1881
|
+
const named = found.filter((f) => f.kind === "rules" || f.kind === "mcp").map((f) => ({ label: f.label, bytes: f.bytes }));
|
|
1882
|
+
const { candidates: candidates2, skipped } = sortProjectDocs(withSize, found.map((f) => f.label));
|
|
1883
|
+
const knowledge = { named, docs: candidates2, skipped };
|
|
1884
|
+
const report = await initProject(cwd, (path) => ignored.has(path), () => list, knowledge);
|
|
1885
|
+
const canImport = hasKnowledge(knowledge);
|
|
1886
|
+
const extra = [];
|
|
1887
|
+
for (const d of candidates2) {
|
|
1888
|
+
const r2 = await readFinding(join12(cwd, d.path));
|
|
1889
|
+
if (r2?.text) extra.push({ kind: "rules", tool: "project document", path: join12(cwd, d.path), label: d.path, bytes: r2.bytes, own: true, text: r2.text });
|
|
1890
|
+
}
|
|
1891
|
+
return { text: describeInit(report) + (canImport ? describeKnowledge(knowledge) : ""), canImport, extra };
|
|
1892
|
+
};
|
|
1893
|
+
const modelsPanelNote = (inUse) => modelsPanel(accounts.usage(), modelsFor, CLI_KINDS, inUse);
|
|
1430
1894
|
const raw = new CliProvider({ readOnly: false, accounts });
|
|
1431
1895
|
const provider = config.telemetry ? telemetryProvider(raw, telemetry()) : raw;
|
|
1432
1896
|
const skillRegistry = new SkillRegistry();
|
|
1433
1897
|
await registerBuiltinSkills(skillRegistry);
|
|
1434
1898
|
await skillRegistry.loadFromDir(externalSkillsDir(home));
|
|
1435
|
-
const skillsDir =
|
|
1436
|
-
if (
|
|
1899
|
+
const skillsDir = join12(cwd, ".horsecode", "skills");
|
|
1900
|
+
if (existsSync4(skillsDir)) await skillRegistry.loadFromDir(skillsDir);
|
|
1437
1901
|
const worktreeHome = await mainWorktreeRoot(defaultGitRunner, cwd);
|
|
1438
1902
|
if (worktreeHome !== cwd) {
|
|
1439
1903
|
console.log(`\u2139\uFE0F Sessions will be opened in ${worktreeHome} \u2014 the repository's main checkout.`);
|
|
@@ -1465,7 +1929,7 @@ async function main(argv) {
|
|
|
1465
1929
|
const useTui = shouldUseTui(!!process.stdin.isTTY, !!process.stdout.isTTY, !!args.noTui);
|
|
1466
1930
|
if (!args.prompt) {
|
|
1467
1931
|
if (useTui) {
|
|
1468
|
-
const { runTuiRepl } = await import("./app-
|
|
1932
|
+
const { runTuiRepl } = await import("./app-4WN37LZ3.js");
|
|
1469
1933
|
const { fetchCatalog, makeProbe, discoverSources } = await import("./discover-G2Z6XC3O.js");
|
|
1470
1934
|
const { loadSourceCache, saveSourceCache } = await import("./source-cache-XEK5WN7I.js");
|
|
1471
1935
|
const manualSources = config.modelSources.length > 0;
|
|
@@ -1516,8 +1980,8 @@ _Expected something like \`https://github.com/owner/repo\` or a link to the dire
|
|
|
1516
1980
|
_Discoverable by every agent. \`/roles adjust\` assigns it to the roles it fits; \`/skills update\` re-installs it from upstream._`;
|
|
1517
1981
|
};
|
|
1518
1982
|
const reloadProjectSkills = async () => {
|
|
1519
|
-
const dir =
|
|
1520
|
-
if (
|
|
1983
|
+
const dir = join12(cwd, ".horsecode", "skills");
|
|
1984
|
+
if (existsSync4(dir)) await skillRegistry.loadFromDir(dir);
|
|
1521
1985
|
};
|
|
1522
1986
|
const updateSkills = async () => {
|
|
1523
1987
|
if (!config.skillSources.length) {
|
|
@@ -1536,7 +2000,7 @@ _Discoverable by every agent. \`/roles adjust\` assigns it to the roles it fits;
|
|
|
1536
2000
|
${lines.join("\n")}${tail}`;
|
|
1537
2001
|
};
|
|
1538
2002
|
const graphStatusText = async () => {
|
|
1539
|
-
const st = await graphStatus(
|
|
2003
|
+
const st = await graphStatus((await derivedWorkdir(false)).dir);
|
|
1540
2004
|
if (!st.built) {
|
|
1541
2005
|
const py = await graphifyPython();
|
|
1542
2006
|
return py ? "No code graph yet. `/graph build` \u2014 AST parsing only, no tokens, a few seconds.\n\n_Without it every agent works blind: it can grep for a name but cannot tell what calls it._" : "No code graph, and graphify is not installed.\n\n`uv tool install graphifyy` (or `pipx install graphifyy`), then `/graph build`.\n\n_MIT, pure tree-sitter AST parsing \u2014 no API key, no tokens._";
|
|
@@ -1555,7 +2019,7 @@ ${lines.join("\n")}${tail}`;
|
|
|
1555
2019
|
|
|
1556
2020
|
_Every agent can query it: \`graph_impact\` (blast radius), \`graph_trace\`, \`graph_find\`, \`graph_context\`, \`graph_overview\`._`;
|
|
1557
2021
|
};
|
|
1558
|
-
const buildGraphText = async () => (await buildProjectGraph(
|
|
2022
|
+
const buildGraphText = async () => (await buildProjectGraph((await derivedWorkdir(true)).dir)).message;
|
|
1559
2023
|
const cleanWorktreesText = async (apply, branch) => {
|
|
1560
2024
|
const { surveySessions, cleanSessions, describeSurvey, describeClean } = await import("./clean-YOQATBMZ.js");
|
|
1561
2025
|
const target = branch?.trim() || fromBranch;
|
|
@@ -1565,31 +2029,42 @@ _Every agent can query it: \`graph_impact\` (blast radius), \`graph_trace\`, \`g
|
|
|
1565
2029
|
return describeClean(await cleanSessions(defaultGitRunner, cwd, target), target);
|
|
1566
2030
|
};
|
|
1567
2031
|
const gitFiles = async () => (await defaultGitRunner(["ls-files", "--cached", "--others", "--exclude-standard"], cwd)).stdout.split("\n").filter(Boolean);
|
|
1568
|
-
const traceableDocs = async () => traceableFiles(
|
|
1569
|
-
const traceableFiles2 = async () => traceableFiles(
|
|
1570
|
-
const
|
|
2032
|
+
const traceableDocs = async (dir = cwd) => traceableFiles(dir, { code: false });
|
|
2033
|
+
const traceableFiles2 = async (dir = cwd) => traceableFiles(dir);
|
|
2034
|
+
const tracerChain = () => {
|
|
1571
2035
|
for (const role of ["tracer", "architect", "senior-coder", "judge"]) {
|
|
1572
|
-
const
|
|
1573
|
-
if (
|
|
2036
|
+
const chain = (config.roles[role]?.models ?? []).filter((m) => m && m !== "default");
|
|
2037
|
+
if (chain.length) return chain;
|
|
1574
2038
|
}
|
|
1575
|
-
return config.model;
|
|
2039
|
+
return [config.model];
|
|
2040
|
+
};
|
|
2041
|
+
const derivedWorkdir = async (create) => {
|
|
2042
|
+
const inSession = sessionBase(cwd);
|
|
2043
|
+
if (inSession) return { dir: inSession };
|
|
2044
|
+
const standing = join12(cwd, ".horsecode", "worktrees", "traces", "base");
|
|
2045
|
+
if (!create) return { dir: existsSync4(standing) ? standing : cwd };
|
|
2046
|
+
const session = await manager.openFixed(fromBranch, "traces");
|
|
2047
|
+
return { dir: session.baseWorktree, session };
|
|
1576
2048
|
};
|
|
1577
2049
|
const planTracesFn = async () => {
|
|
1578
|
-
const
|
|
1579
|
-
|
|
2050
|
+
const { dir } = await derivedWorkdir(false);
|
|
2051
|
+
const plan = await planFor(dir, await traceableFiles2(dir));
|
|
2052
|
+
return { summary: describePlan(plan, tracerChain()), jobs: plan.jobs.length };
|
|
1580
2053
|
};
|
|
1581
2054
|
const runTracesFn = async (onProgress, metered) => {
|
|
1582
|
-
const
|
|
1583
|
-
const
|
|
1584
|
-
const
|
|
2055
|
+
const { dir, session } = await derivedWorkdir(true);
|
|
2056
|
+
const files = await traceableFiles2(dir);
|
|
2057
|
+
const brief = await buildBrief({ cwd: dir, provider: metered ?? provider, models: tracerChain(), files: await traceableDocs(dir) });
|
|
2058
|
+
const plan = await planFor(dir, files);
|
|
1585
2059
|
const res = await runTraces({
|
|
1586
|
-
cwd,
|
|
2060
|
+
cwd: dir,
|
|
1587
2061
|
provider: metered ?? provider,
|
|
1588
|
-
|
|
2062
|
+
models: tracerChain(),
|
|
1589
2063
|
plan,
|
|
1590
2064
|
liveFiles: new Set(files),
|
|
1591
2065
|
...onProgress ? { onProgress } : {}
|
|
1592
2066
|
});
|
|
2067
|
+
if (session) await manager.preserveSession(session, `docs(traces): ${res.written} file(s) described`);
|
|
1593
2068
|
const bits = [`${brief.message}
|
|
1594
2069
|
|
|
1595
2070
|
**Traces written: ${res.written}**`];
|
|
@@ -1597,9 +2072,13 @@ _Every agent can query it: \`graph_impact\` (blast radius), \`graph_trace\`, \`g
|
|
|
1597
2072
|
if (res.pruned.length) bits.push(`${res.pruned.length} removed for deleted files`);
|
|
1598
2073
|
if (res.wroteGitignore) bits.push("\n\n_Added .gitignore rules: traces are committed, the AST cache is not._");
|
|
1599
2074
|
const failures = describeTraceFailures(res.failed);
|
|
2075
|
+
const landed = session ? `
|
|
2076
|
+
|
|
2077
|
+
_Written and committed on \`${session.baseBranch}\`, so your working tree is untouched._
|
|
2078
|
+
_Bring them in with \`git merge ${session.baseBranch}\`, or browse them at \`${session.baseWorktree}\`._` : "\n\n_Written in this session's worktree, on its own branch._";
|
|
1600
2079
|
return `${bits.join(" \xB7 ")}${failures ? `
|
|
1601
2080
|
|
|
1602
|
-
${failures}` : ""}
|
|
2081
|
+
${failures}` : ""}${landed}
|
|
1603
2082
|
|
|
1604
2083
|
_Committed with the repo, so every clone starts with them. Agents read one with \`graph_trace\`._`;
|
|
1605
2084
|
};
|
|
@@ -1607,11 +2086,14 @@ _Committed with the repo, so every clone starts with them. Agents read one with
|
|
|
1607
2086
|
buildDeps,
|
|
1608
2087
|
memStore,
|
|
1609
2088
|
accountsNote,
|
|
2089
|
+
modelsPanel: modelsPanelNote,
|
|
2090
|
+
initProject: initProjectFn,
|
|
1610
2091
|
listSkills,
|
|
1611
2092
|
updateSkills,
|
|
1612
2093
|
addSkill,
|
|
1613
2094
|
reloadProjectSkills,
|
|
1614
2095
|
graphStatus: graphStatusText,
|
|
2096
|
+
derivedDir: async () => (await derivedWorkdir(true)).dir,
|
|
1615
2097
|
buildGraph: buildGraphText,
|
|
1616
2098
|
cleanWorktrees: cleanWorktreesText,
|
|
1617
2099
|
planTraces: planTracesFn,
|
|
@@ -1644,7 +2126,7 @@ _Committed with the repo, so every clone starts with them. Agents read one with
|
|
|
1644
2126
|
...args.revisionRounds !== void 0 && { revisionRounds: args.revisionRounds }
|
|
1645
2127
|
};
|
|
1646
2128
|
if (useTui) {
|
|
1647
|
-
const { runTui } = await import("./app-
|
|
2129
|
+
const { runTui } = await import("./app-4WN37LZ3.js");
|
|
1648
2130
|
const res = await runTui({ buildDeps, job });
|
|
1649
2131
|
console.log(renderResult(res));
|
|
1650
2132
|
return;
|