@kody-ade/kody-engine 0.2.32 → 0.2.34
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/bin/kody2.js +422 -375
- package/package.json +1 -1
package/dist/bin/kody2.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// package.json
|
|
4
4
|
var package_default = {
|
|
5
5
|
name: "@kody-ade/kody-engine",
|
|
6
|
-
version: "0.2.
|
|
6
|
+
version: "0.2.34",
|
|
7
7
|
description: "kody2 \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
|
|
8
8
|
license: "MIT",
|
|
9
9
|
type: "module",
|
|
@@ -50,81 +50,115 @@ var package_default = {
|
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
// src/chat-cli.ts
|
|
53
|
-
import
|
|
54
|
-
import * as fs19 from "fs";
|
|
55
|
-
import * as path16 from "path";
|
|
53
|
+
import * as path14 from "path";
|
|
56
54
|
|
|
57
|
-
// src/chat/
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
// src/chat/pull.ts
|
|
56
|
+
var PullError = class extends Error {
|
|
57
|
+
constructor(message, status) {
|
|
58
|
+
super(message);
|
|
59
|
+
this.status = status;
|
|
60
|
+
this.name = "PullError";
|
|
61
|
+
}
|
|
62
|
+
status;
|
|
63
|
+
};
|
|
64
|
+
function createPullClient(opts) {
|
|
65
|
+
const fetchFn = opts.fetchFn ?? fetch;
|
|
66
|
+
const parsed = parseUrl(opts.baseUrl);
|
|
67
|
+
const token = opts.token ?? parsed.token;
|
|
68
|
+
if (!token) {
|
|
69
|
+
throw new PullError("session token not provided (expected inline ?token= in dashboardUrl)");
|
|
70
|
+
}
|
|
71
|
+
return async function pull(since, timeoutMs) {
|
|
72
|
+
const url = new URL(parsed.origin);
|
|
73
|
+
url.pathname = "/api/kody/chat/pull";
|
|
74
|
+
url.searchParams.set("sessionId", opts.sessionId);
|
|
75
|
+
url.searchParams.set("since", String(since));
|
|
76
|
+
url.searchParams.set("timeoutMs", String(timeoutMs));
|
|
77
|
+
url.searchParams.set("token", token);
|
|
78
|
+
const abort = AbortSignal.timeout(timeoutMs + 1e4);
|
|
79
|
+
const res = await fetchFn(url.toString(), {
|
|
80
|
+
method: "GET",
|
|
81
|
+
headers: { authorization: `Bearer ${token}` },
|
|
82
|
+
signal: abort
|
|
83
|
+
});
|
|
84
|
+
if (!res.ok) {
|
|
85
|
+
const body = await res.text().catch(() => "");
|
|
86
|
+
throw new PullError(`pull ${url.pathname} \u2192 ${res.status}: ${body.slice(0, 200)}`, res.status);
|
|
87
|
+
}
|
|
88
|
+
const data = await res.json();
|
|
89
|
+
return data;
|
|
90
|
+
};
|
|
62
91
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
`);
|
|
92
|
+
function parseUrl(baseUrl) {
|
|
93
|
+
try {
|
|
94
|
+
const u = new URL(baseUrl);
|
|
95
|
+
const token = u.searchParams.get("token");
|
|
96
|
+
const origin = `${u.protocol}//${u.host}${u.pathname !== "/" ? u.pathname : ""}`.replace(/\/$/, "");
|
|
97
|
+
return { origin: origin || u.origin, token };
|
|
98
|
+
} catch {
|
|
99
|
+
return { origin: baseUrl, token: null };
|
|
72
100
|
}
|
|
73
|
-
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/chat/events.ts
|
|
74
104
|
var HttpSink = class {
|
|
75
|
-
constructor(baseUrl, sessionId, logger = {
|
|
105
|
+
constructor(baseUrl, sessionId, token, fetchFn = fetch, logger = {
|
|
76
106
|
warn: (m) => process.stderr.write(`[kody2:chat] ${m}
|
|
77
107
|
`)
|
|
78
108
|
}) {
|
|
79
|
-
this.baseUrl = baseUrl;
|
|
80
109
|
this.sessionId = sessionId;
|
|
110
|
+
this.fetchFn = fetchFn;
|
|
81
111
|
this.logger = logger;
|
|
112
|
+
const parsed = parseUrl(baseUrl);
|
|
113
|
+
this.origin = parsed.origin;
|
|
114
|
+
const resolved = token ?? parsed.token;
|
|
115
|
+
if (!resolved) {
|
|
116
|
+
throw new Error("HttpSink: session token not provided (expected inline ?token= in baseUrl)");
|
|
117
|
+
}
|
|
118
|
+
this.token = resolved;
|
|
82
119
|
}
|
|
83
|
-
baseUrl;
|
|
84
120
|
sessionId;
|
|
121
|
+
fetchFn;
|
|
85
122
|
logger;
|
|
123
|
+
origin;
|
|
124
|
+
token;
|
|
86
125
|
async emit(event) {
|
|
87
|
-
const url =
|
|
126
|
+
const url = new URL(this.origin);
|
|
127
|
+
url.pathname = "/api/kody/events/ingest";
|
|
128
|
+
url.searchParams.set("sessionId", this.sessionId);
|
|
129
|
+
url.searchParams.set("token", this.token);
|
|
88
130
|
try {
|
|
89
|
-
const res = await
|
|
131
|
+
const res = await this.fetchFn(url.toString(), {
|
|
90
132
|
method: "POST",
|
|
91
|
-
headers: {
|
|
133
|
+
headers: {
|
|
134
|
+
"content-type": "application/json",
|
|
135
|
+
authorization: `Bearer ${this.token}`
|
|
136
|
+
},
|
|
92
137
|
body: JSON.stringify(event),
|
|
93
138
|
signal: AbortSignal.timeout(5e3)
|
|
94
139
|
});
|
|
95
140
|
if (!res.ok) {
|
|
96
|
-
this.logger.warn(`HttpSink POST ${url} \u2192 ${res.status}`);
|
|
141
|
+
this.logger.warn(`HttpSink POST ${url.pathname} \u2192 ${res.status}`);
|
|
97
142
|
}
|
|
98
143
|
} catch (err) {
|
|
99
|
-
this.logger.warn(
|
|
144
|
+
this.logger.warn(
|
|
145
|
+
`HttpSink POST ${url.pathname} failed: ${err instanceof Error ? err.message : String(err)}`
|
|
146
|
+
);
|
|
100
147
|
}
|
|
101
148
|
}
|
|
102
149
|
};
|
|
103
|
-
var TeeSink = class {
|
|
104
|
-
constructor(sinks) {
|
|
105
|
-
this.sinks = sinks;
|
|
106
|
-
}
|
|
107
|
-
sinks;
|
|
108
|
-
async emit(event) {
|
|
109
|
-
await Promise.all(this.sinks.map((s) => s.emit(event)));
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
function withSessionParam(baseUrl, sessionId) {
|
|
113
|
-
const joiner = baseUrl.includes("?") ? "&" : "?";
|
|
114
|
-
return `${baseUrl}${joiner}sessionId=${encodeURIComponent(sessionId)}`;
|
|
115
|
-
}
|
|
116
150
|
function makeRunId(sessionId, suffix) {
|
|
117
151
|
return `chat-${sessionId}-${suffix}`;
|
|
118
152
|
}
|
|
119
153
|
|
|
120
154
|
// src/agent.ts
|
|
121
|
-
import * as
|
|
122
|
-
import * as
|
|
155
|
+
import * as fs2 from "fs";
|
|
156
|
+
import * as path2 from "path";
|
|
123
157
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
124
158
|
|
|
125
159
|
// src/config.ts
|
|
126
|
-
import * as
|
|
127
|
-
import * as
|
|
160
|
+
import * as fs from "fs";
|
|
161
|
+
import * as path from "path";
|
|
128
162
|
var LITELLM_DEFAULT_PORT = 4e3;
|
|
129
163
|
var LITELLM_DEFAULT_URL = `http://localhost:${LITELLM_DEFAULT_PORT}`;
|
|
130
164
|
function parseProviderModel(s) {
|
|
@@ -142,13 +176,13 @@ function needsLitellmProxy(model) {
|
|
|
142
176
|
return model.provider !== "claude" && model.provider !== "anthropic";
|
|
143
177
|
}
|
|
144
178
|
function loadConfig(projectDir = process.cwd()) {
|
|
145
|
-
const configPath =
|
|
146
|
-
if (!
|
|
179
|
+
const configPath = path.join(projectDir, "kody.config.json");
|
|
180
|
+
if (!fs.existsSync(configPath)) {
|
|
147
181
|
throw new Error(`kody.config.json not found at ${configPath}`);
|
|
148
182
|
}
|
|
149
183
|
let raw;
|
|
150
184
|
try {
|
|
151
|
-
raw = JSON.parse(
|
|
185
|
+
raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
152
186
|
} catch (err) {
|
|
153
187
|
const msg = err instanceof Error ? err.message : String(err);
|
|
154
188
|
throw new Error(`kody.config.json is invalid JSON: ${msg}`);
|
|
@@ -325,10 +359,10 @@ function formatBytes(bytes) {
|
|
|
325
359
|
// src/agent.ts
|
|
326
360
|
var DEFAULT_ALLOWED_TOOLS = ["Bash", "Edit", "Read", "Write", "Glob", "Grep"];
|
|
327
361
|
async function runAgent(opts) {
|
|
328
|
-
const ndjsonDir = opts.ndjsonDir ??
|
|
329
|
-
|
|
330
|
-
const ndjsonPath =
|
|
331
|
-
const fullLog =
|
|
362
|
+
const ndjsonDir = opts.ndjsonDir ?? path2.join(opts.cwd, ".kody2");
|
|
363
|
+
fs2.mkdirSync(ndjsonDir, { recursive: true });
|
|
364
|
+
const ndjsonPath = path2.join(ndjsonDir, "last-run.jsonl");
|
|
365
|
+
const fullLog = fs2.createWriteStream(ndjsonPath, { flags: "w" });
|
|
332
366
|
const env = {
|
|
333
367
|
...process.env,
|
|
334
368
|
SKIP_HOOKS: "1",
|
|
@@ -400,53 +434,6 @@ async function runAgent(opts) {
|
|
|
400
434
|
return { outcome, finalText, error: errorMessage, ndjsonPath };
|
|
401
435
|
}
|
|
402
436
|
|
|
403
|
-
// src/chat/session.ts
|
|
404
|
-
import * as fs4 from "fs";
|
|
405
|
-
import * as path4 from "path";
|
|
406
|
-
function sessionFilePath(cwd, sessionId) {
|
|
407
|
-
return path4.join(cwd, ".kody", "sessions", `${sessionId}.jsonl`);
|
|
408
|
-
}
|
|
409
|
-
function readSession(file) {
|
|
410
|
-
if (!fs4.existsSync(file)) return [];
|
|
411
|
-
const raw = fs4.readFileSync(file, "utf-8").trim();
|
|
412
|
-
if (!raw) return [];
|
|
413
|
-
const turns = [];
|
|
414
|
-
for (const line of raw.split("\n")) {
|
|
415
|
-
if (!line.trim()) continue;
|
|
416
|
-
try {
|
|
417
|
-
const parsed = JSON.parse(line);
|
|
418
|
-
if (parsed.role !== "user" && parsed.role !== "assistant") continue;
|
|
419
|
-
if (typeof parsed.content !== "string") continue;
|
|
420
|
-
turns.push(parsed);
|
|
421
|
-
} catch {
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
return turns;
|
|
425
|
-
}
|
|
426
|
-
function appendTurn(file, turn) {
|
|
427
|
-
fs4.mkdirSync(path4.dirname(file), { recursive: true });
|
|
428
|
-
const line = JSON.stringify({
|
|
429
|
-
role: turn.role,
|
|
430
|
-
content: turn.content,
|
|
431
|
-
timestamp: turn.timestamp,
|
|
432
|
-
toolCalls: turn.toolCalls ?? []
|
|
433
|
-
});
|
|
434
|
-
fs4.appendFileSync(file, `${line}
|
|
435
|
-
`);
|
|
436
|
-
}
|
|
437
|
-
function seedInitialMessage(file, message) {
|
|
438
|
-
if (!message.trim()) return false;
|
|
439
|
-
const turns = readSession(file);
|
|
440
|
-
const lastUser = [...turns].reverse().find((t) => t.role === "user");
|
|
441
|
-
if (lastUser && lastUser.content === message) return false;
|
|
442
|
-
appendTurn(file, {
|
|
443
|
-
role: "user",
|
|
444
|
-
content: message,
|
|
445
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
446
|
-
});
|
|
447
|
-
return true;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
437
|
// src/chat/loop.ts
|
|
451
438
|
var CHAT_SYSTEM_PROMPT = [
|
|
452
439
|
"You are Kody, an AI assistant for the Kody Operations Dashboard. Reply to the user's",
|
|
@@ -455,20 +442,15 @@ var CHAT_SYSTEM_PROMPT = [
|
|
|
455
442
|
"read repository code or execute small checks when it helps you answer \u2014 otherwise",
|
|
456
443
|
"reply directly. Do not invent file paths, commit SHAs, or command output."
|
|
457
444
|
].join("\n");
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
const error = "last turn is not a user message \u2014 assistant already replied";
|
|
468
|
-
await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
|
|
469
|
-
return { exitCode: 64, error };
|
|
470
|
-
}
|
|
471
|
-
const prompt = buildPrompt(turns, opts.systemPrompt ?? CHAT_SYSTEM_PROMPT);
|
|
445
|
+
var DEFAULT_IDLE_TIMEOUT_MS = 3 * 60 * 1e3;
|
|
446
|
+
var DEFAULT_HARD_TIMEOUT_MS = 5 * 60 * 60 * 1e3;
|
|
447
|
+
var DEFAULT_PULL_TIMEOUT_MS = 25e3;
|
|
448
|
+
async function runChatSession(opts) {
|
|
449
|
+
const now = opts.now ?? (() => Date.now());
|
|
450
|
+
const idleTimeoutMs = opts.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
|
|
451
|
+
const hardTimeoutMs = opts.hardTimeoutMs ?? DEFAULT_HARD_TIMEOUT_MS;
|
|
452
|
+
const pullTimeoutMs = opts.pullTimeoutMs ?? DEFAULT_PULL_TIMEOUT_MS;
|
|
453
|
+
const systemPrompt = opts.systemPrompt ?? CHAT_SYSTEM_PROMPT;
|
|
472
454
|
const invoke = opts.invokeAgent ?? ((p) => runAgent({
|
|
473
455
|
prompt: p,
|
|
474
456
|
model: opts.model,
|
|
@@ -477,34 +459,68 @@ async function runChatTurn(opts) {
|
|
|
477
459
|
verbose: opts.verbose,
|
|
478
460
|
quiet: opts.quiet
|
|
479
461
|
}));
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
462
|
+
const history = [];
|
|
463
|
+
let since = 0;
|
|
464
|
+
let lastActivityAt = now();
|
|
465
|
+
const startedAt = now();
|
|
466
|
+
let turnsProcessed = 0;
|
|
467
|
+
while (true) {
|
|
468
|
+
if (now() - startedAt > hardTimeoutMs) {
|
|
469
|
+
await emit(opts.sink, "chat.done", opts.sessionId, "done", {
|
|
470
|
+
sessionId: opts.sessionId,
|
|
471
|
+
reason: "hard-timeout"
|
|
472
|
+
});
|
|
473
|
+
return { exitCode: 0, turnsProcessed, reason: "hard-timeout" };
|
|
474
|
+
}
|
|
475
|
+
let response;
|
|
476
|
+
try {
|
|
477
|
+
response = await opts.pull(since, pullTimeoutMs);
|
|
478
|
+
} catch (err) {
|
|
479
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
480
|
+
await emit(opts.sink, "chat.error", opts.sessionId, "error", { error: `pull failed: ${msg}` });
|
|
481
|
+
return { exitCode: 99, turnsProcessed, reason: `pull failed: ${msg}` };
|
|
482
|
+
}
|
|
483
|
+
if (response.turns.length === 0) {
|
|
484
|
+
if (now() - lastActivityAt > idleTimeoutMs) {
|
|
485
|
+
await emit(opts.sink, "chat.done", opts.sessionId, "done", {
|
|
486
|
+
sessionId: opts.sessionId,
|
|
487
|
+
reason: "idle-timeout"
|
|
488
|
+
});
|
|
489
|
+
return { exitCode: 0, turnsProcessed, reason: "idle-timeout" };
|
|
490
|
+
}
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
const newUserTurns = response.turns.filter((t) => t.role === "user");
|
|
494
|
+
for (const t of newUserTurns) history.push(t);
|
|
495
|
+
since = response.nextSince;
|
|
496
|
+
if (newUserTurns.length === 0) continue;
|
|
497
|
+
lastActivityAt = now();
|
|
498
|
+
const prompt = buildPrompt(history, systemPrompt);
|
|
499
|
+
let result;
|
|
500
|
+
try {
|
|
501
|
+
result = await invoke(prompt);
|
|
502
|
+
} catch (err) {
|
|
503
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
504
|
+
await emit(opts.sink, "chat.error", opts.sessionId, "error", { error: msg });
|
|
505
|
+
return { exitCode: 99, turnsProcessed, reason: msg };
|
|
506
|
+
}
|
|
507
|
+
if (result.outcome !== "completed") {
|
|
508
|
+
const error = result.error ?? "agent did not complete";
|
|
509
|
+
await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
|
|
510
|
+
return { exitCode: 99, turnsProcessed, reason: error };
|
|
511
|
+
}
|
|
512
|
+
const reply = result.finalText.trim();
|
|
513
|
+
const replyTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
514
|
+
history.push({ role: "assistant", content: reply, timestamp: replyTimestamp });
|
|
515
|
+
turnsProcessed++;
|
|
516
|
+
lastActivityAt = now();
|
|
517
|
+
await emit(opts.sink, "chat.message", opts.sessionId, `message-${turnsProcessed}`, {
|
|
518
|
+
sessionId: opts.sessionId,
|
|
519
|
+
role: "assistant",
|
|
520
|
+
content: reply,
|
|
521
|
+
timestamp: replyTimestamp
|
|
522
|
+
});
|
|
492
523
|
}
|
|
493
|
-
const reply = result.finalText.trim();
|
|
494
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
495
|
-
appendTurn(opts.sessionFile, {
|
|
496
|
-
role: "assistant",
|
|
497
|
-
content: reply,
|
|
498
|
-
timestamp: now
|
|
499
|
-
});
|
|
500
|
-
await emit(opts.sink, "chat.message", opts.sessionId, "message", {
|
|
501
|
-
sessionId: opts.sessionId,
|
|
502
|
-
role: "assistant",
|
|
503
|
-
content: reply,
|
|
504
|
-
timestamp: now
|
|
505
|
-
});
|
|
506
|
-
await emit(opts.sink, "chat.done", opts.sessionId, "done", { sessionId: opts.sessionId });
|
|
507
|
-
return { exitCode: 0, reply };
|
|
508
524
|
}
|
|
509
525
|
function buildPrompt(turns, systemPrompt) {
|
|
510
526
|
const header = `System: ${systemPrompt}`;
|
|
@@ -526,11 +542,11 @@ async function emit(sink, type, sessionId, suffix, payload) {
|
|
|
526
542
|
|
|
527
543
|
// src/kody2-cli.ts
|
|
528
544
|
import { execFileSync as execFileSync15 } from "child_process";
|
|
529
|
-
import * as
|
|
530
|
-
import * as
|
|
545
|
+
import * as fs16 from "fs";
|
|
546
|
+
import * as path13 from "path";
|
|
531
547
|
|
|
532
548
|
// src/dispatch.ts
|
|
533
|
-
import * as
|
|
549
|
+
import * as fs3 from "fs";
|
|
534
550
|
function autoDispatch(opts) {
|
|
535
551
|
const explicit = opts?.explicit;
|
|
536
552
|
if (explicit?.issueNumber && explicit.issueNumber > 0) {
|
|
@@ -542,10 +558,10 @@ function autoDispatch(opts) {
|
|
|
542
558
|
}
|
|
543
559
|
const eventName = process.env.GITHUB_EVENT_NAME;
|
|
544
560
|
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
545
|
-
if (!eventName || !eventPath || !
|
|
561
|
+
if (!eventName || !eventPath || !fs3.existsSync(eventPath)) return null;
|
|
546
562
|
let event = {};
|
|
547
563
|
try {
|
|
548
|
-
event = JSON.parse(
|
|
564
|
+
event = JSON.parse(fs3.readFileSync(eventPath, "utf-8"));
|
|
549
565
|
} catch {
|
|
550
566
|
return null;
|
|
551
567
|
}
|
|
@@ -614,14 +630,14 @@ function extractFeedback(afterTag) {
|
|
|
614
630
|
}
|
|
615
631
|
|
|
616
632
|
// src/executor.ts
|
|
617
|
-
import * as
|
|
618
|
-
import * as
|
|
633
|
+
import * as fs15 from "fs";
|
|
634
|
+
import * as path12 from "path";
|
|
619
635
|
|
|
620
636
|
// src/litellm.ts
|
|
621
637
|
import { execFileSync, spawn } from "child_process";
|
|
622
|
-
import * as
|
|
638
|
+
import * as fs4 from "fs";
|
|
623
639
|
import * as os from "os";
|
|
624
|
-
import * as
|
|
640
|
+
import * as path3 from "path";
|
|
625
641
|
async function checkLitellmHealth(url) {
|
|
626
642
|
try {
|
|
627
643
|
const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
|
|
@@ -661,20 +677,20 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
|
|
|
661
677
|
throw new Error("litellm not installed \u2014 run: pip install 'litellm[proxy]'");
|
|
662
678
|
}
|
|
663
679
|
}
|
|
664
|
-
const configPath =
|
|
665
|
-
|
|
680
|
+
const configPath = path3.join(os.tmpdir(), `kody2-litellm-${Date.now()}.yaml`);
|
|
681
|
+
fs4.writeFileSync(configPath, generateLitellmConfigYaml(model));
|
|
666
682
|
const portMatch = url.match(/:(\d+)/);
|
|
667
683
|
const port = portMatch ? portMatch[1] : "4000";
|
|
668
684
|
const args = cmd === "litellm" ? ["--config", configPath, "--port", port] : ["-m", "litellm", "--config", configPath, "--port", port];
|
|
669
685
|
const dotenvVars = readDotenvApiKeys(projectDir);
|
|
670
|
-
const logPath =
|
|
671
|
-
const outFd =
|
|
686
|
+
const logPath = path3.join(os.tmpdir(), `kody2-litellm-${Date.now()}.log`);
|
|
687
|
+
const outFd = fs4.openSync(logPath, "w");
|
|
672
688
|
const child = spawn(cmd, args, {
|
|
673
689
|
stdio: ["ignore", outFd, outFd],
|
|
674
690
|
detached: true,
|
|
675
691
|
env: stripBlockingEnv({ ...process.env, ...dotenvVars })
|
|
676
692
|
});
|
|
677
|
-
|
|
693
|
+
fs4.closeSync(outFd);
|
|
678
694
|
for (let i = 0; i < 30; i++) {
|
|
679
695
|
await new Promise((r) => setTimeout(r, 2e3));
|
|
680
696
|
if (await checkLitellmHealth(url)) {
|
|
@@ -691,7 +707,7 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
|
|
|
691
707
|
}
|
|
692
708
|
let logTail = "";
|
|
693
709
|
try {
|
|
694
|
-
logTail =
|
|
710
|
+
logTail = fs4.readFileSync(logPath, "utf-8").slice(-2e3);
|
|
695
711
|
} catch {
|
|
696
712
|
}
|
|
697
713
|
try {
|
|
@@ -702,10 +718,10 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
|
|
|
702
718
|
${logTail}`);
|
|
703
719
|
}
|
|
704
720
|
function readDotenvApiKeys(projectDir) {
|
|
705
|
-
const dotenvPath =
|
|
706
|
-
if (!
|
|
721
|
+
const dotenvPath = path3.join(projectDir, ".env");
|
|
722
|
+
if (!fs4.existsSync(dotenvPath)) return {};
|
|
707
723
|
const result = {};
|
|
708
|
-
for (const rawLine of
|
|
724
|
+
for (const rawLine of fs4.readFileSync(dotenvPath, "utf-8").split("\n")) {
|
|
709
725
|
const line = rawLine.trim();
|
|
710
726
|
if (!line || line.startsWith("#")) continue;
|
|
711
727
|
const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
|
|
@@ -728,8 +744,8 @@ function stripBlockingEnv(env) {
|
|
|
728
744
|
}
|
|
729
745
|
|
|
730
746
|
// src/profile.ts
|
|
731
|
-
import * as
|
|
732
|
-
import * as
|
|
747
|
+
import * as fs5 from "fs";
|
|
748
|
+
import * as path4 from "path";
|
|
733
749
|
var VALID_INPUT_TYPES = /* @__PURE__ */ new Set(["int", "string", "bool", "enum"]);
|
|
734
750
|
var VALID_PERMISSION_MODES = /* @__PURE__ */ new Set(["default", "acceptEdits", "plan", "bypassPermissions"]);
|
|
735
751
|
var ProfileError = class extends Error {
|
|
@@ -742,12 +758,12 @@ var ProfileError = class extends Error {
|
|
|
742
758
|
profilePath;
|
|
743
759
|
};
|
|
744
760
|
function loadProfile(profilePath) {
|
|
745
|
-
if (!
|
|
761
|
+
if (!fs5.existsSync(profilePath)) {
|
|
746
762
|
throw new ProfileError(profilePath, "file not found");
|
|
747
763
|
}
|
|
748
764
|
let raw;
|
|
749
765
|
try {
|
|
750
|
-
raw = JSON.parse(
|
|
766
|
+
raw = JSON.parse(fs5.readFileSync(profilePath, "utf-8"));
|
|
751
767
|
} catch (err) {
|
|
752
768
|
throw new ProfileError(profilePath, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
753
769
|
}
|
|
@@ -771,7 +787,7 @@ function loadProfile(profilePath) {
|
|
|
771
787
|
outputContract: r.outputContract,
|
|
772
788
|
inputArtifacts: parseInputArtifacts(profilePath, r.input),
|
|
773
789
|
outputArtifacts: parseOutputArtifacts(profilePath, r.output),
|
|
774
|
-
dir:
|
|
790
|
+
dir: path4.dirname(profilePath)
|
|
775
791
|
};
|
|
776
792
|
return profile;
|
|
777
793
|
}
|
|
@@ -950,21 +966,21 @@ function parseScriptList(p, key, raw) {
|
|
|
950
966
|
}
|
|
951
967
|
|
|
952
968
|
// src/scripts/buildSyntheticPlugin.ts
|
|
953
|
-
import * as
|
|
969
|
+
import * as fs6 from "fs";
|
|
954
970
|
import * as os2 from "os";
|
|
955
|
-
import * as
|
|
971
|
+
import * as path5 from "path";
|
|
956
972
|
function getPluginsCatalogRoot() {
|
|
957
|
-
const here =
|
|
973
|
+
const here = path5.dirname(new URL(import.meta.url).pathname);
|
|
958
974
|
const candidates = [
|
|
959
|
-
|
|
975
|
+
path5.join(here, "..", "plugins"),
|
|
960
976
|
// dev: src/scripts → src/plugins
|
|
961
|
-
|
|
977
|
+
path5.join(here, "..", "..", "plugins"),
|
|
962
978
|
// built: dist/scripts → dist/plugins
|
|
963
|
-
|
|
979
|
+
path5.join(here, "..", "..", "src", "plugins")
|
|
964
980
|
// fallback
|
|
965
981
|
];
|
|
966
982
|
for (const c of candidates) {
|
|
967
|
-
if (
|
|
983
|
+
if (fs6.existsSync(c) && fs6.statSync(c).isDirectory()) return c;
|
|
968
984
|
}
|
|
969
985
|
return candidates[0];
|
|
970
986
|
}
|
|
@@ -974,50 +990,50 @@ var buildSyntheticPlugin = async (ctx, profile) => {
|
|
|
974
990
|
if (!needsSynthetic) return;
|
|
975
991
|
const catalog = getPluginsCatalogRoot();
|
|
976
992
|
const runId = `${profile.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
977
|
-
const root =
|
|
978
|
-
|
|
993
|
+
const root = path5.join(os2.tmpdir(), `kody2-synth-${runId}`);
|
|
994
|
+
fs6.mkdirSync(path5.join(root, ".claude-plugin"), { recursive: true });
|
|
979
995
|
if (cc.skills.length > 0) {
|
|
980
|
-
const dst =
|
|
981
|
-
|
|
996
|
+
const dst = path5.join(root, "skills");
|
|
997
|
+
fs6.mkdirSync(dst, { recursive: true });
|
|
982
998
|
for (const name of cc.skills) {
|
|
983
|
-
const src =
|
|
984
|
-
if (!
|
|
985
|
-
copyDir(src,
|
|
999
|
+
const src = path5.join(catalog, "skills", name);
|
|
1000
|
+
if (!fs6.existsSync(src)) throw new Error(`buildSyntheticPlugin: skill not found in catalog: ${name}`);
|
|
1001
|
+
copyDir(src, path5.join(dst, name));
|
|
986
1002
|
}
|
|
987
1003
|
}
|
|
988
1004
|
if (cc.commands.length > 0) {
|
|
989
|
-
const dst =
|
|
990
|
-
|
|
1005
|
+
const dst = path5.join(root, "commands");
|
|
1006
|
+
fs6.mkdirSync(dst, { recursive: true });
|
|
991
1007
|
for (const name of cc.commands) {
|
|
992
|
-
const src =
|
|
993
|
-
if (!
|
|
994
|
-
|
|
1008
|
+
const src = path5.join(catalog, "commands", `${name}.md`);
|
|
1009
|
+
if (!fs6.existsSync(src)) throw new Error(`buildSyntheticPlugin: command not found in catalog: ${name}`);
|
|
1010
|
+
fs6.copyFileSync(src, path5.join(dst, `${name}.md`));
|
|
995
1011
|
}
|
|
996
1012
|
}
|
|
997
1013
|
if (cc.subagents.length > 0) {
|
|
998
|
-
const dst =
|
|
999
|
-
|
|
1014
|
+
const dst = path5.join(root, "agents");
|
|
1015
|
+
fs6.mkdirSync(dst, { recursive: true });
|
|
1000
1016
|
for (const name of cc.subagents) {
|
|
1001
|
-
const src =
|
|
1002
|
-
if (!
|
|
1003
|
-
|
|
1017
|
+
const src = path5.join(catalog, "agents", `${name}.md`);
|
|
1018
|
+
if (!fs6.existsSync(src)) throw new Error(`buildSyntheticPlugin: subagent not found in catalog: ${name}`);
|
|
1019
|
+
fs6.copyFileSync(src, path5.join(dst, `${name}.md`));
|
|
1004
1020
|
}
|
|
1005
1021
|
}
|
|
1006
1022
|
if (cc.hooks.length > 0) {
|
|
1007
|
-
const dst =
|
|
1008
|
-
|
|
1023
|
+
const dst = path5.join(root, "hooks");
|
|
1024
|
+
fs6.mkdirSync(dst, { recursive: true });
|
|
1009
1025
|
const merged = { hooks: {} };
|
|
1010
1026
|
for (const name of cc.hooks) {
|
|
1011
|
-
const src =
|
|
1012
|
-
if (!
|
|
1013
|
-
const parsed = JSON.parse(
|
|
1027
|
+
const src = path5.join(catalog, "hooks", `${name}.json`);
|
|
1028
|
+
if (!fs6.existsSync(src)) throw new Error(`buildSyntheticPlugin: hook not found in catalog: ${name}`);
|
|
1029
|
+
const parsed = JSON.parse(fs6.readFileSync(src, "utf-8"));
|
|
1014
1030
|
for (const [event, entries] of Object.entries(parsed.hooks ?? {})) {
|
|
1015
1031
|
if (!Array.isArray(entries)) continue;
|
|
1016
1032
|
if (!merged.hooks[event]) merged.hooks[event] = [];
|
|
1017
1033
|
merged.hooks[event].push(...entries);
|
|
1018
1034
|
}
|
|
1019
1035
|
}
|
|
1020
|
-
|
|
1036
|
+
fs6.writeFileSync(path5.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
|
|
1021
1037
|
`);
|
|
1022
1038
|
}
|
|
1023
1039
|
const manifest = {
|
|
@@ -1028,17 +1044,17 @@ var buildSyntheticPlugin = async (ctx, profile) => {
|
|
|
1028
1044
|
if (cc.skills.length > 0) manifest.skills = ["./skills/"];
|
|
1029
1045
|
if (cc.commands.length > 0) manifest.commands = ["./commands/"];
|
|
1030
1046
|
if (cc.subagents.length > 0) manifest.agents = cc.subagents.map((n) => `./agents/${n}.md`);
|
|
1031
|
-
|
|
1047
|
+
fs6.writeFileSync(path5.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
|
|
1032
1048
|
`);
|
|
1033
1049
|
ctx.data.syntheticPluginPath = root;
|
|
1034
1050
|
};
|
|
1035
1051
|
function copyDir(src, dst) {
|
|
1036
|
-
|
|
1037
|
-
for (const ent of
|
|
1038
|
-
const s =
|
|
1039
|
-
const d =
|
|
1052
|
+
fs6.mkdirSync(dst, { recursive: true });
|
|
1053
|
+
for (const ent of fs6.readdirSync(src, { withFileTypes: true })) {
|
|
1054
|
+
const s = path5.join(src, ent.name);
|
|
1055
|
+
const d = path5.join(dst, ent.name);
|
|
1040
1056
|
if (ent.isDirectory()) copyDir(s, d);
|
|
1041
|
-
else if (ent.isFile())
|
|
1057
|
+
else if (ent.isFile()) fs6.copyFileSync(s, d);
|
|
1042
1058
|
}
|
|
1043
1059
|
}
|
|
1044
1060
|
|
|
@@ -1104,18 +1120,18 @@ function formatMissesForFeedback(misses) {
|
|
|
1104
1120
|
}
|
|
1105
1121
|
|
|
1106
1122
|
// src/prompt.ts
|
|
1107
|
-
import * as
|
|
1108
|
-
import * as
|
|
1123
|
+
import * as fs7 from "fs";
|
|
1124
|
+
import * as path6 from "path";
|
|
1109
1125
|
var CONVENTIONS_PER_FILE_MAX_BYTES = 3e4;
|
|
1110
1126
|
var CONVENTION_FILES = ["CLAUDE.md", "AGENTS.md"];
|
|
1111
1127
|
function loadProjectConventions(projectDir) {
|
|
1112
1128
|
const out = [];
|
|
1113
1129
|
for (const rel of CONVENTION_FILES) {
|
|
1114
|
-
const abs =
|
|
1115
|
-
if (!
|
|
1130
|
+
const abs = path6.join(projectDir, rel);
|
|
1131
|
+
if (!fs7.existsSync(abs)) continue;
|
|
1116
1132
|
let content;
|
|
1117
1133
|
try {
|
|
1118
|
-
content =
|
|
1134
|
+
content = fs7.readFileSync(abs, "utf-8");
|
|
1119
1135
|
} catch {
|
|
1120
1136
|
continue;
|
|
1121
1137
|
}
|
|
@@ -1236,8 +1252,8 @@ import { execFileSync as execFileSync4 } from "child_process";
|
|
|
1236
1252
|
|
|
1237
1253
|
// src/commit.ts
|
|
1238
1254
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
1239
|
-
import * as
|
|
1240
|
-
import * as
|
|
1255
|
+
import * as fs8 from "fs";
|
|
1256
|
+
import * as path7 from "path";
|
|
1241
1257
|
var FORBIDDEN_PATH_PREFIXES = [
|
|
1242
1258
|
".kody/",
|
|
1243
1259
|
".kody-engine/",
|
|
@@ -1292,18 +1308,18 @@ function tryGit(args, cwd) {
|
|
|
1292
1308
|
}
|
|
1293
1309
|
function abortUnfinishedGitOps(cwd) {
|
|
1294
1310
|
const aborted = [];
|
|
1295
|
-
const gitDir =
|
|
1296
|
-
if (!
|
|
1297
|
-
if (
|
|
1311
|
+
const gitDir = path7.join(cwd ?? process.cwd(), ".git");
|
|
1312
|
+
if (!fs8.existsSync(gitDir)) return aborted;
|
|
1313
|
+
if (fs8.existsSync(path7.join(gitDir, "MERGE_HEAD"))) {
|
|
1298
1314
|
if (tryGit(["merge", "--abort"], cwd)) aborted.push("merge");
|
|
1299
1315
|
}
|
|
1300
|
-
if (
|
|
1316
|
+
if (fs8.existsSync(path7.join(gitDir, "CHERRY_PICK_HEAD"))) {
|
|
1301
1317
|
if (tryGit(["cherry-pick", "--abort"], cwd)) aborted.push("cherry-pick");
|
|
1302
1318
|
}
|
|
1303
|
-
if (
|
|
1319
|
+
if (fs8.existsSync(path7.join(gitDir, "REVERT_HEAD"))) {
|
|
1304
1320
|
if (tryGit(["revert", "--abort"], cwd)) aborted.push("revert");
|
|
1305
1321
|
}
|
|
1306
|
-
if (
|
|
1322
|
+
if (fs8.existsSync(path7.join(gitDir, "rebase-merge")) || fs8.existsSync(path7.join(gitDir, "rebase-apply"))) {
|
|
1307
1323
|
if (tryGit(["rebase", "--abort"], cwd)) aborted.push("rebase");
|
|
1308
1324
|
}
|
|
1309
1325
|
try {
|
|
@@ -1345,7 +1361,7 @@ function normalizeCommitMessage(raw) {
|
|
|
1345
1361
|
function commitAndPush(branch, agentMessage, cwd) {
|
|
1346
1362
|
const allChanged = listChangedFiles(cwd);
|
|
1347
1363
|
const allowedFiles = allChanged.filter((f) => !isForbiddenPath(f));
|
|
1348
|
-
const mergeHeadExists =
|
|
1364
|
+
const mergeHeadExists = fs8.existsSync(path7.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
|
|
1349
1365
|
if (allowedFiles.length === 0 && !mergeHeadExists) {
|
|
1350
1366
|
return { committed: false, pushed: false, sha: "", message: "" };
|
|
1351
1367
|
}
|
|
@@ -1440,20 +1456,20 @@ function defaultCommitMessage(mode, data) {
|
|
|
1440
1456
|
}
|
|
1441
1457
|
|
|
1442
1458
|
// src/scripts/composePrompt.ts
|
|
1443
|
-
import * as
|
|
1444
|
-
import * as
|
|
1459
|
+
import * as fs9 from "fs";
|
|
1460
|
+
import * as path8 from "path";
|
|
1445
1461
|
var MUSTACHE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
|
|
1446
1462
|
var composePrompt = async (ctx, profile) => {
|
|
1447
1463
|
const explicit = ctx.data.promptTemplate;
|
|
1448
1464
|
const mode = ctx.args.mode;
|
|
1449
1465
|
const candidates = [
|
|
1450
|
-
explicit ?
|
|
1451
|
-
mode ?
|
|
1452
|
-
|
|
1466
|
+
explicit ? path8.join(profile.dir, explicit) : null,
|
|
1467
|
+
mode ? path8.join(profile.dir, "prompts", `${mode}.md`) : null,
|
|
1468
|
+
path8.join(profile.dir, "prompt.md")
|
|
1453
1469
|
].filter(Boolean);
|
|
1454
1470
|
let templatePath = "";
|
|
1455
1471
|
for (const c of candidates) {
|
|
1456
|
-
if (
|
|
1472
|
+
if (fs9.existsSync(c)) {
|
|
1457
1473
|
templatePath = c;
|
|
1458
1474
|
break;
|
|
1459
1475
|
}
|
|
@@ -1461,7 +1477,7 @@ var composePrompt = async (ctx, profile) => {
|
|
|
1461
1477
|
if (!templatePath) {
|
|
1462
1478
|
throw new Error(`profile at ${profile.dir}: no prompt template found (tried ${candidates.join(", ")})`);
|
|
1463
1479
|
}
|
|
1464
|
-
const template =
|
|
1480
|
+
const template = fs9.readFileSync(templatePath, "utf-8");
|
|
1465
1481
|
const tokens = {
|
|
1466
1482
|
...stringifyAll(ctx.args, "args."),
|
|
1467
1483
|
...stringifyAll(ctx.data, ""),
|
|
@@ -1936,7 +1952,7 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd) {
|
|
|
1936
1952
|
|
|
1937
1953
|
// src/gha.ts
|
|
1938
1954
|
import { execFileSync as execFileSync7 } from "child_process";
|
|
1939
|
-
import * as
|
|
1955
|
+
import * as fs10 from "fs";
|
|
1940
1956
|
function getRunUrl() {
|
|
1941
1957
|
const server = process.env.GITHUB_SERVER_URL;
|
|
1942
1958
|
const repo = process.env.GITHUB_REPOSITORY;
|
|
@@ -1947,10 +1963,10 @@ function getRunUrl() {
|
|
|
1947
1963
|
function reactToTriggerComment(cwd) {
|
|
1948
1964
|
if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
|
|
1949
1965
|
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
1950
|
-
if (!eventPath || !
|
|
1966
|
+
if (!eventPath || !fs10.existsSync(eventPath)) return;
|
|
1951
1967
|
let event = null;
|
|
1952
1968
|
try {
|
|
1953
|
-
event = JSON.parse(
|
|
1969
|
+
event = JSON.parse(fs10.readFileSync(eventPath, "utf-8"));
|
|
1954
1970
|
} catch {
|
|
1955
1971
|
return;
|
|
1956
1972
|
}
|
|
@@ -2176,35 +2192,35 @@ function tryPostPr2(prNumber, body, cwd) {
|
|
|
2176
2192
|
|
|
2177
2193
|
// src/scripts/initFlow.ts
|
|
2178
2194
|
import { execFileSync as execFileSync9 } from "child_process";
|
|
2179
|
-
import * as
|
|
2180
|
-
import * as
|
|
2195
|
+
import * as fs12 from "fs";
|
|
2196
|
+
import * as path10 from "path";
|
|
2181
2197
|
|
|
2182
2198
|
// src/registry.ts
|
|
2183
|
-
import * as
|
|
2184
|
-
import * as
|
|
2199
|
+
import * as fs11 from "fs";
|
|
2200
|
+
import * as path9 from "path";
|
|
2185
2201
|
function getExecutablesRoot() {
|
|
2186
|
-
const here =
|
|
2202
|
+
const here = path9.dirname(new URL(import.meta.url).pathname);
|
|
2187
2203
|
const candidates = [
|
|
2188
|
-
|
|
2204
|
+
path9.join(here, "executables"),
|
|
2189
2205
|
// dev: src/
|
|
2190
|
-
|
|
2206
|
+
path9.join(here, "..", "executables"),
|
|
2191
2207
|
// built: dist/bin → dist/executables
|
|
2192
|
-
|
|
2208
|
+
path9.join(here, "..", "src", "executables")
|
|
2193
2209
|
// fallback
|
|
2194
2210
|
];
|
|
2195
2211
|
for (const c of candidates) {
|
|
2196
|
-
if (
|
|
2212
|
+
if (fs11.existsSync(c) && fs11.statSync(c).isDirectory()) return c;
|
|
2197
2213
|
}
|
|
2198
2214
|
return candidates[0];
|
|
2199
2215
|
}
|
|
2200
2216
|
function listExecutables(root = getExecutablesRoot()) {
|
|
2201
|
-
if (!
|
|
2202
|
-
const entries =
|
|
2217
|
+
if (!fs11.existsSync(root)) return [];
|
|
2218
|
+
const entries = fs11.readdirSync(root, { withFileTypes: true });
|
|
2203
2219
|
const out = [];
|
|
2204
2220
|
for (const ent of entries) {
|
|
2205
2221
|
if (!ent.isDirectory()) continue;
|
|
2206
|
-
const profilePath =
|
|
2207
|
-
if (
|
|
2222
|
+
const profilePath = path9.join(root, ent.name, "profile.json");
|
|
2223
|
+
if (fs11.existsSync(profilePath) && fs11.statSync(profilePath).isFile()) {
|
|
2208
2224
|
out.push({ name: ent.name, profilePath });
|
|
2209
2225
|
}
|
|
2210
2226
|
}
|
|
@@ -2212,8 +2228,8 @@ function listExecutables(root = getExecutablesRoot()) {
|
|
|
2212
2228
|
}
|
|
2213
2229
|
function hasExecutable(name, root = getExecutablesRoot()) {
|
|
2214
2230
|
if (!isSafeName(name)) return false;
|
|
2215
|
-
const profilePath =
|
|
2216
|
-
return
|
|
2231
|
+
const profilePath = path9.join(root, name, "profile.json");
|
|
2232
|
+
return fs11.existsSync(profilePath) && fs11.statSync(profilePath).isFile();
|
|
2217
2233
|
}
|
|
2218
2234
|
function isSafeName(name) {
|
|
2219
2235
|
return /^[a-z][a-z0-9-]*$/.test(name) && !name.includes("..");
|
|
@@ -2242,9 +2258,9 @@ function parseGenericFlags(argv) {
|
|
|
2242
2258
|
|
|
2243
2259
|
// src/scripts/initFlow.ts
|
|
2244
2260
|
function detectPackageManager(cwd) {
|
|
2245
|
-
if (
|
|
2246
|
-
if (
|
|
2247
|
-
if (
|
|
2261
|
+
if (fs12.existsSync(path10.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
2262
|
+
if (fs12.existsSync(path10.join(cwd, "yarn.lock"))) return "yarn";
|
|
2263
|
+
if (fs12.existsSync(path10.join(cwd, "bun.lockb"))) return "bun";
|
|
2248
2264
|
return "npm";
|
|
2249
2265
|
}
|
|
2250
2266
|
function qualityCommandsFor(pm) {
|
|
@@ -2365,22 +2381,22 @@ function performInit(cwd, force) {
|
|
|
2365
2381
|
const pm = detectPackageManager(cwd);
|
|
2366
2382
|
const ownerRepo = detectOwnerRepo(cwd);
|
|
2367
2383
|
const defaultBranch = defaultBranchFromGit(cwd);
|
|
2368
|
-
const configPath =
|
|
2369
|
-
if (
|
|
2384
|
+
const configPath = path10.join(cwd, "kody.config.json");
|
|
2385
|
+
if (fs12.existsSync(configPath) && !force) {
|
|
2370
2386
|
skipped.push("kody.config.json");
|
|
2371
2387
|
} else {
|
|
2372
2388
|
const cfg = makeConfig(pm, ownerRepo, defaultBranch);
|
|
2373
|
-
|
|
2389
|
+
fs12.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
|
|
2374
2390
|
`);
|
|
2375
2391
|
wrote.push("kody.config.json");
|
|
2376
2392
|
}
|
|
2377
|
-
const workflowDir =
|
|
2378
|
-
const workflowPath =
|
|
2379
|
-
if (
|
|
2393
|
+
const workflowDir = path10.join(cwd, ".github", "workflows");
|
|
2394
|
+
const workflowPath = path10.join(workflowDir, "kody2.yml");
|
|
2395
|
+
if (fs12.existsSync(workflowPath) && !force) {
|
|
2380
2396
|
skipped.push(".github/workflows/kody2.yml");
|
|
2381
2397
|
} else {
|
|
2382
|
-
|
|
2383
|
-
|
|
2398
|
+
fs12.mkdirSync(workflowDir, { recursive: true });
|
|
2399
|
+
fs12.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
|
|
2384
2400
|
wrote.push(".github/workflows/kody2.yml");
|
|
2385
2401
|
}
|
|
2386
2402
|
for (const exe of listExecutables()) {
|
|
@@ -2391,12 +2407,12 @@ function performInit(cwd, force) {
|
|
|
2391
2407
|
continue;
|
|
2392
2408
|
}
|
|
2393
2409
|
if (profile.kind !== "scheduled" || !profile.schedule) continue;
|
|
2394
|
-
const target =
|
|
2395
|
-
if (
|
|
2410
|
+
const target = path10.join(workflowDir, `kody2-${exe.name}.yml`);
|
|
2411
|
+
if (fs12.existsSync(target) && !force) {
|
|
2396
2412
|
skipped.push(`.github/workflows/kody2-${exe.name}.yml`);
|
|
2397
2413
|
continue;
|
|
2398
2414
|
}
|
|
2399
|
-
|
|
2415
|
+
fs12.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
|
|
2400
2416
|
wrote.push(`.github/workflows/kody2-${exe.name}.yml`);
|
|
2401
2417
|
}
|
|
2402
2418
|
return { wrote, skipped };
|
|
@@ -2903,8 +2919,8 @@ REVIEW_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.
|
|
|
2903
2919
|
|
|
2904
2920
|
// src/scripts/releaseFlow.ts
|
|
2905
2921
|
import { execFileSync as execFileSync11, spawnSync } from "child_process";
|
|
2906
|
-
import * as
|
|
2907
|
-
import * as
|
|
2922
|
+
import * as fs13 from "fs";
|
|
2923
|
+
import * as path11 from "path";
|
|
2908
2924
|
function bumpVersion(current, bump) {
|
|
2909
2925
|
const m = current.match(/^(\d+)\.(\d+)\.(\d+)(.*)$/);
|
|
2910
2926
|
if (!m) throw new Error(`cannot parse version '${current}' (expected x.y.z[-suffix])`);
|
|
@@ -2920,12 +2936,12 @@ function bumpVersion(current, bump) {
|
|
|
2920
2936
|
return `${major}.${minor}.${patch}`;
|
|
2921
2937
|
}
|
|
2922
2938
|
function updateVersionInFile(file, newVersion, cwd) {
|
|
2923
|
-
const abs =
|
|
2924
|
-
if (!
|
|
2925
|
-
const content =
|
|
2939
|
+
const abs = path11.join(cwd, file);
|
|
2940
|
+
if (!fs13.existsSync(abs)) return false;
|
|
2941
|
+
const content = fs13.readFileSync(abs, "utf-8");
|
|
2926
2942
|
const updated = content.replace(/"version"\s*:\s*"[^"]+"/, `"version": "${newVersion}"`);
|
|
2927
2943
|
if (updated === content) return false;
|
|
2928
|
-
|
|
2944
|
+
fs13.writeFileSync(abs, updated);
|
|
2929
2945
|
return true;
|
|
2930
2946
|
}
|
|
2931
2947
|
function generateChangelog(cwd, newVersion, lastTag) {
|
|
@@ -2973,19 +2989,19 @@ function generateChangelog(cwd, newVersion, lastTag) {
|
|
|
2973
2989
|
return parts.join("\n");
|
|
2974
2990
|
}
|
|
2975
2991
|
function prependChangelog(cwd, entry) {
|
|
2976
|
-
const p =
|
|
2992
|
+
const p = path11.join(cwd, "CHANGELOG.md");
|
|
2977
2993
|
const header = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n";
|
|
2978
|
-
if (
|
|
2979
|
-
const prior =
|
|
2994
|
+
if (fs13.existsSync(p)) {
|
|
2995
|
+
const prior = fs13.readFileSync(p, "utf-8");
|
|
2980
2996
|
if (/^#\s*Changelog\b/m.test(prior)) {
|
|
2981
2997
|
const idx = prior.indexOf("\n", prior.indexOf("# Changelog"));
|
|
2982
|
-
|
|
2998
|
+
fs13.writeFileSync(p, `${prior.slice(0, idx + 1)}
|
|
2983
2999
|
${entry}${prior.slice(idx + 1)}`);
|
|
2984
3000
|
} else {
|
|
2985
|
-
|
|
3001
|
+
fs13.writeFileSync(p, `${header}${entry}${prior}`);
|
|
2986
3002
|
}
|
|
2987
3003
|
} else {
|
|
2988
|
-
|
|
3004
|
+
fs13.writeFileSync(p, `${header}${entry}`);
|
|
2989
3005
|
}
|
|
2990
3006
|
}
|
|
2991
3007
|
function git3(args, cwd, timeout = 6e4) {
|
|
@@ -3036,13 +3052,13 @@ var releaseFlow = async (ctx) => {
|
|
|
3036
3052
|
};
|
|
3037
3053
|
async function runPrepare(args) {
|
|
3038
3054
|
const { cwd, bump, dryRun, versionFiles, ctx } = args;
|
|
3039
|
-
const pkgPath =
|
|
3040
|
-
if (!
|
|
3055
|
+
const pkgPath = path11.join(cwd, "package.json");
|
|
3056
|
+
if (!fs13.existsSync(pkgPath)) {
|
|
3041
3057
|
ctx.output.exitCode = 99;
|
|
3042
3058
|
ctx.output.reason = "release prepare: package.json not found";
|
|
3043
3059
|
return;
|
|
3044
3060
|
}
|
|
3045
|
-
const pkg = JSON.parse(
|
|
3061
|
+
const pkg = JSON.parse(fs13.readFileSync(pkgPath, "utf-8"));
|
|
3046
3062
|
if (typeof pkg.version !== "string") {
|
|
3047
3063
|
ctx.output.exitCode = 99;
|
|
3048
3064
|
ctx.output.reason = "release prepare: package.json has no version";
|
|
@@ -3113,8 +3129,8 @@ Merge this and then run \`kody2 release --mode finalize\`.`;
|
|
|
3113
3129
|
}
|
|
3114
3130
|
async function runFinalize(args) {
|
|
3115
3131
|
const { cwd, dryRun, timeoutMs, releaseCfg, ctx } = args;
|
|
3116
|
-
const pkgPath =
|
|
3117
|
-
const pkg = JSON.parse(
|
|
3132
|
+
const pkgPath = path11.join(cwd, "package.json");
|
|
3133
|
+
const pkg = JSON.parse(fs13.readFileSync(pkgPath, "utf-8"));
|
|
3118
3134
|
if (typeof pkg.version !== "string") {
|
|
3119
3135
|
ctx.output.exitCode = 99;
|
|
3120
3136
|
ctx.output.reason = "release finalize: package.json has no version";
|
|
@@ -3199,31 +3215,17 @@ ${truncate2(r.stderr, 2e3)}
|
|
|
3199
3215
|
|
|
3200
3216
|
// src/scripts/requireFeedbackActions.ts
|
|
3201
3217
|
var MIN_ITEMS = 1;
|
|
3202
|
-
var ACTIONABLE_HEADINGS = /^#{1,6}\s+(Concerns|Suggestions|Bugs)\b/i;
|
|
3203
|
-
var NEXT_HEADING = /^#{1,6}\s+/;
|
|
3204
3218
|
var requireFeedbackActions = async (ctx, profile) => {
|
|
3205
3219
|
if (!ctx.data.agentDone) return;
|
|
3206
3220
|
const actions = String(ctx.data.feedbackActions ?? "").trim();
|
|
3207
3221
|
const items = countActionItems(actions);
|
|
3222
|
+
ctx.data.feedbackAgentItemCount = items;
|
|
3208
3223
|
if (items < MIN_ITEMS) {
|
|
3209
3224
|
fail(
|
|
3210
3225
|
ctx,
|
|
3211
3226
|
profile,
|
|
3212
3227
|
actions.length === 0 ? "agent omitted required FEEDBACK_ACTIONS block \u2014 cannot verify that review feedback was addressed" : "agent FEEDBACK_ACTIONS block listed no items \u2014 cannot verify that review feedback was addressed"
|
|
3213
3228
|
);
|
|
3214
|
-
return;
|
|
3215
|
-
}
|
|
3216
|
-
const reviewBody = String(ctx.data.feedback ?? "");
|
|
3217
|
-
const expectedItems = countActionableReviewBullets(reviewBody);
|
|
3218
|
-
ctx.data.feedbackReviewItemCount = expectedItems;
|
|
3219
|
-
ctx.data.feedbackAgentItemCount = items;
|
|
3220
|
-
if (expectedItems > 0 && items < expectedItems) {
|
|
3221
|
-
fail(
|
|
3222
|
-
ctx,
|
|
3223
|
-
profile,
|
|
3224
|
-
`agent FEEDBACK_ACTIONS listed ${items} item(s) but the review has ${expectedItems} actionable bullet(s) under ### Concerns / ### Suggestions / ### Bugs \u2014 every review item must be accounted for`
|
|
3225
|
-
);
|
|
3226
|
-
return;
|
|
3227
3229
|
}
|
|
3228
3230
|
};
|
|
3229
3231
|
function fail(ctx, profile, reason) {
|
|
@@ -3245,25 +3247,6 @@ function countActionItems(block) {
|
|
|
3245
3247
|
}
|
|
3246
3248
|
return count;
|
|
3247
3249
|
}
|
|
3248
|
-
function countActionableReviewBullets(reviewBody) {
|
|
3249
|
-
if (!reviewBody.trim()) return 0;
|
|
3250
|
-
const lines = reviewBody.split("\n");
|
|
3251
|
-
let count = 0;
|
|
3252
|
-
let insideActionable = false;
|
|
3253
|
-
for (const raw of lines) {
|
|
3254
|
-
if (ACTIONABLE_HEADINGS.test(raw)) {
|
|
3255
|
-
insideActionable = true;
|
|
3256
|
-
continue;
|
|
3257
|
-
}
|
|
3258
|
-
if (insideActionable && NEXT_HEADING.test(raw)) {
|
|
3259
|
-
insideActionable = false;
|
|
3260
|
-
continue;
|
|
3261
|
-
}
|
|
3262
|
-
if (!insideActionable) continue;
|
|
3263
|
-
if (/^[-*]\s+\S/.test(raw)) count++;
|
|
3264
|
-
}
|
|
3265
|
-
return count;
|
|
3266
|
-
}
|
|
3267
3250
|
|
|
3268
3251
|
// src/scripts/requirePlanDeviations.ts
|
|
3269
3252
|
var requirePlanDeviations = async (ctx, profile) => {
|
|
@@ -3683,6 +3666,44 @@ function summarizeFeedbackActions(block) {
|
|
|
3683
3666
|
}
|
|
3684
3667
|
return summary;
|
|
3685
3668
|
}
|
|
3669
|
+
function extractReviewFileRefs(reviewBody) {
|
|
3670
|
+
if (!reviewBody) return [];
|
|
3671
|
+
const found = /* @__PURE__ */ new Set();
|
|
3672
|
+
const backtick = /`([^`\s]+\.[a-zA-Z]{1,5})(?::\d+(?:-\d+)?)?`/g;
|
|
3673
|
+
let m;
|
|
3674
|
+
while ((m = backtick.exec(reviewBody)) !== null) {
|
|
3675
|
+
const raw = m[1];
|
|
3676
|
+
if (isPlausibleSourcePath(raw)) found.add(raw);
|
|
3677
|
+
}
|
|
3678
|
+
const bare = /(?<![A-Za-z0-9/_.-])((?:[A-Za-z0-9_./-]+\/)+[A-Za-z0-9_.-]+\.[a-zA-Z]{1,5})(?::\d+(?:-\d+)?)?/g;
|
|
3679
|
+
while ((m = bare.exec(reviewBody)) !== null) {
|
|
3680
|
+
const raw = m[1];
|
|
3681
|
+
if (isPlausibleSourcePath(raw)) found.add(raw);
|
|
3682
|
+
}
|
|
3683
|
+
return Array.from(found);
|
|
3684
|
+
}
|
|
3685
|
+
function isPlausibleSourcePath(p) {
|
|
3686
|
+
if (p.startsWith("http://") || p.startsWith("https://")) return false;
|
|
3687
|
+
if (p.startsWith("//")) return false;
|
|
3688
|
+
if (p.startsWith("/")) return false;
|
|
3689
|
+
if (!p.includes("/")) return false;
|
|
3690
|
+
if (/\.(md|rst|txt|png|jpg|jpeg|gif|svg|pdf)$/i.test(p)) return false;
|
|
3691
|
+
const firstSeg = p.slice(0, p.indexOf("/"));
|
|
3692
|
+
if (firstSeg.includes(".")) return false;
|
|
3693
|
+
return true;
|
|
3694
|
+
}
|
|
3695
|
+
function declinedFileRefs(feedbackActions, refs) {
|
|
3696
|
+
if (!feedbackActions.trim() || refs.length === 0) return /* @__PURE__ */ new Set();
|
|
3697
|
+
const declined = /* @__PURE__ */ new Set();
|
|
3698
|
+
for (const raw of feedbackActions.split("\n")) {
|
|
3699
|
+
if (!/^\s*[-*]\s+/.test(raw)) continue;
|
|
3700
|
+
if (!/\bdeclined\s*:/i.test(raw)) continue;
|
|
3701
|
+
for (const ref of refs) {
|
|
3702
|
+
if (raw.includes(ref)) declined.add(ref);
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
return declined;
|
|
3706
|
+
}
|
|
3686
3707
|
function makeAction2(type, payload) {
|
|
3687
3708
|
return { type, payload, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3688
3709
|
}
|
|
@@ -3695,24 +3716,32 @@ var verifyFixAlignment = async (ctx, profile) => {
|
|
|
3695
3716
|
ctx.data.feedbackActionsSummary = summary;
|
|
3696
3717
|
const committed = Boolean(ctx.data.commitResult?.committed);
|
|
3697
3718
|
if (summary.totalItems === 0) {
|
|
3698
|
-
ctx
|
|
3699
|
-
ctx.output.reason = "fix produced no FEEDBACK_ACTIONS items";
|
|
3700
|
-
ctx.data.agentDone = false;
|
|
3701
|
-
ctx.data.action = makeAction2("FIX_FAILED", {
|
|
3702
|
-
reason: ctx.output.reason,
|
|
3703
|
-
feedbackActionsSummary: summary
|
|
3704
|
-
});
|
|
3705
|
-
return;
|
|
3719
|
+
return failOnce(ctx, "FIX_FAILED", "fix produced no FEEDBACK_ACTIONS items", summary);
|
|
3706
3720
|
}
|
|
3707
3721
|
if (summary.fixedItems > 0 && !committed) {
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3722
|
+
return failOnce(
|
|
3723
|
+
ctx,
|
|
3724
|
+
"FIX_FAILED",
|
|
3725
|
+
`fix claimed ${summary.fixedItems} fixed item(s) but produced no commit`,
|
|
3726
|
+
summary
|
|
3727
|
+
);
|
|
3728
|
+
}
|
|
3729
|
+
const reviewBody = ctx.data.feedback ?? "";
|
|
3730
|
+
const refs = extractReviewFileRefs(reviewBody);
|
|
3731
|
+
const changedFiles = (ctx.data.changedFiles ?? []).map((f) => f.trim()).filter(Boolean);
|
|
3732
|
+
ctx.data.reviewFileRefs = refs;
|
|
3733
|
+
if (refs.length > 0 && committed) {
|
|
3734
|
+
const declined = declinedFileRefs(feedbackActions, refs);
|
|
3735
|
+
const missing = refs.filter((r) => !declined.has(r) && !changedFiles.some((f) => filesMatch(f, r)));
|
|
3736
|
+
if (missing.length > 0) {
|
|
3737
|
+
return failOnce(
|
|
3738
|
+
ctx,
|
|
3739
|
+
"FIX_FAILED",
|
|
3740
|
+
`fix did not touch review-named file(s): ${missing.join(", ")} \u2014 address them or mark declined with a reason`,
|
|
3741
|
+
summary,
|
|
3742
|
+
{ missingFiles: missing, declinedFiles: Array.from(declined), changedFiles }
|
|
3743
|
+
);
|
|
3744
|
+
}
|
|
3716
3745
|
}
|
|
3717
3746
|
if (summary.fixedItems === 0 && summary.declinedItems > 0 && !committed) {
|
|
3718
3747
|
ctx.data.action = makeAction2("FIX_DECLINED", {
|
|
@@ -3721,6 +3750,25 @@ var verifyFixAlignment = async (ctx, profile) => {
|
|
|
3721
3750
|
});
|
|
3722
3751
|
}
|
|
3723
3752
|
};
|
|
3753
|
+
function failOnce(ctx, type, reason, summary, extra) {
|
|
3754
|
+
ctx.output.exitCode = 1;
|
|
3755
|
+
ctx.output.reason = reason;
|
|
3756
|
+
ctx.data.agentDone = false;
|
|
3757
|
+
ctx.data.action = makeAction2(type, {
|
|
3758
|
+
reason,
|
|
3759
|
+
feedbackActionsSummary: summary,
|
|
3760
|
+
...extra ?? {}
|
|
3761
|
+
});
|
|
3762
|
+
}
|
|
3763
|
+
function filesMatch(changedPath, reviewRef) {
|
|
3764
|
+
if (changedPath === reviewRef) return true;
|
|
3765
|
+
if (changedPath.endsWith("/" + reviewRef)) return true;
|
|
3766
|
+
if (reviewRef.endsWith("/" + changedPath)) return true;
|
|
3767
|
+
const a = changedPath.split("/");
|
|
3768
|
+
const b = reviewRef.split("/");
|
|
3769
|
+
if (a[a.length - 1] !== b[b.length - 1]) return false;
|
|
3770
|
+
return a.length >= 2 && b.length >= 2 && a[a.length - 2] === b[b.length - 2];
|
|
3771
|
+
}
|
|
3724
3772
|
|
|
3725
3773
|
// src/scripts/watchStalePrsFlow.ts
|
|
3726
3774
|
function readWatchConfig(ctx) {
|
|
@@ -3792,7 +3840,7 @@ var watchStalePrsFlow = async (ctx) => {
|
|
|
3792
3840
|
};
|
|
3793
3841
|
|
|
3794
3842
|
// src/scripts/writeRunSummary.ts
|
|
3795
|
-
import * as
|
|
3843
|
+
import * as fs14 from "fs";
|
|
3796
3844
|
var writeRunSummary = async (ctx, profile) => {
|
|
3797
3845
|
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
3798
3846
|
if (!summaryPath) return;
|
|
@@ -3814,7 +3862,7 @@ var writeRunSummary = async (ctx, profile) => {
|
|
|
3814
3862
|
if (reason) lines.push(`- **Reason:** ${reason}`);
|
|
3815
3863
|
lines.push("");
|
|
3816
3864
|
try {
|
|
3817
|
-
|
|
3865
|
+
fs14.appendFileSync(summaryPath, `${lines.join("\n")}
|
|
3818
3866
|
`);
|
|
3819
3867
|
} catch {
|
|
3820
3868
|
}
|
|
@@ -3962,9 +4010,9 @@ async function runExecutable(profileName, input) {
|
|
|
3962
4010
|
data: {},
|
|
3963
4011
|
output: { exitCode: 0 }
|
|
3964
4012
|
};
|
|
3965
|
-
const ndjsonDir =
|
|
4013
|
+
const ndjsonDir = path12.join(input.cwd, ".kody2");
|
|
3966
4014
|
const invokeAgent = async (prompt) => {
|
|
3967
|
-
const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) =>
|
|
4015
|
+
const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path12.isAbsolute(p) ? p : path12.resolve(profile.dir, p)).filter((p) => p.length > 0);
|
|
3968
4016
|
const syntheticPath = ctx.data.syntheticPluginPath;
|
|
3969
4017
|
const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
|
|
3970
4018
|
return runAgent({
|
|
@@ -4030,17 +4078,17 @@ async function runExecutable(profileName, input) {
|
|
|
4030
4078
|
}
|
|
4031
4079
|
}
|
|
4032
4080
|
function resolveProfilePath(profileName) {
|
|
4033
|
-
const here =
|
|
4081
|
+
const here = path12.dirname(new URL(import.meta.url).pathname);
|
|
4034
4082
|
const candidates = [
|
|
4035
|
-
|
|
4083
|
+
path12.join(here, "executables", profileName, "profile.json"),
|
|
4036
4084
|
// same-dir sibling (dev)
|
|
4037
|
-
|
|
4085
|
+
path12.join(here, "..", "executables", profileName, "profile.json"),
|
|
4038
4086
|
// up one (prod: dist/bin → dist/executables)
|
|
4039
|
-
|
|
4087
|
+
path12.join(here, "..", "src", "executables", profileName, "profile.json")
|
|
4040
4088
|
// fallback
|
|
4041
4089
|
];
|
|
4042
4090
|
for (const c of candidates) {
|
|
4043
|
-
if (
|
|
4091
|
+
if (fs15.existsSync(c)) return c;
|
|
4044
4092
|
}
|
|
4045
4093
|
return candidates[0];
|
|
4046
4094
|
}
|
|
@@ -4216,9 +4264,9 @@ function resolveAuthToken(env = process.env) {
|
|
|
4216
4264
|
return token;
|
|
4217
4265
|
}
|
|
4218
4266
|
function detectPackageManager2(cwd) {
|
|
4219
|
-
if (
|
|
4220
|
-
if (
|
|
4221
|
-
if (
|
|
4267
|
+
if (fs16.existsSync(path13.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
4268
|
+
if (fs16.existsSync(path13.join(cwd, "yarn.lock"))) return "yarn";
|
|
4269
|
+
if (fs16.existsSync(path13.join(cwd, "bun.lockb"))) return "bun";
|
|
4222
4270
|
return "npm";
|
|
4223
4271
|
}
|
|
4224
4272
|
function shellOut(cmd, args, cwd, stream = true) {
|
|
@@ -4298,11 +4346,11 @@ function configureGitIdentity(cwd) {
|
|
|
4298
4346
|
}
|
|
4299
4347
|
function postFailureTail(issueNumber, cwd, reason) {
|
|
4300
4348
|
if (!issueNumber) return;
|
|
4301
|
-
const logPath =
|
|
4349
|
+
const logPath = path13.join(cwd, ".kody2", "last-run.jsonl");
|
|
4302
4350
|
let tail = "";
|
|
4303
4351
|
try {
|
|
4304
|
-
if (
|
|
4305
|
-
const content =
|
|
4352
|
+
if (fs16.existsSync(logPath)) {
|
|
4353
|
+
const content = fs16.readFileSync(logPath, "utf-8");
|
|
4306
4354
|
tail = content.slice(-3e3);
|
|
4307
4355
|
}
|
|
4308
4356
|
} catch {
|
|
@@ -4327,7 +4375,7 @@ async function runCi(argv) {
|
|
|
4327
4375
|
return 0;
|
|
4328
4376
|
}
|
|
4329
4377
|
const args = parseCiArgs(argv);
|
|
4330
|
-
const cwd = args.cwd ?
|
|
4378
|
+
const cwd = args.cwd ? path13.resolve(args.cwd) : process.cwd();
|
|
4331
4379
|
let earlyConfig;
|
|
4332
4380
|
try {
|
|
4333
4381
|
earlyConfig = loadConfig(cwd);
|
|
@@ -4420,23 +4468,23 @@ var DEFAULT_MODEL = "claude/claude-haiku-4-5-20251001";
|
|
|
4420
4468
|
var CHAT_HELP = `kody2 chat \u2014 dashboard-driven chat session
|
|
4421
4469
|
|
|
4422
4470
|
Usage:
|
|
4423
|
-
kody2 chat [--session <id>] [--
|
|
4471
|
+
kody2 chat [--session <id>] [--model <provider/model>]
|
|
4424
4472
|
[--dashboard-url <url>] [--cwd <path>] [--verbose|--quiet]
|
|
4425
4473
|
|
|
4426
|
-
All inputs may also come from env: SESSION_ID,
|
|
4427
|
-
CLI flags take precedence over env. SESSION_ID
|
|
4474
|
+
All inputs may also come from env: SESSION_ID, MODEL, DASHBOARD_URL.
|
|
4475
|
+
CLI flags take precedence over env. SESSION_ID and DASHBOARD_URL are required
|
|
4476
|
+
(the runner long-polls the dashboard for user turns and pushes events back).
|
|
4428
4477
|
|
|
4429
4478
|
Exit codes:
|
|
4430
|
-
0
|
|
4431
|
-
64 bad inputs
|
|
4432
|
-
99 runtime failure (agent crash, LiteLLM failure)
|
|
4479
|
+
0 session exited cleanly (idle or hard timeout)
|
|
4480
|
+
64 bad inputs
|
|
4481
|
+
99 runtime failure (agent crash, pull failure, LiteLLM failure)
|
|
4433
4482
|
`;
|
|
4434
4483
|
function parseChatArgs(argv, env = process.env) {
|
|
4435
4484
|
const result = { errors: [] };
|
|
4436
4485
|
for (let i = 0; i < argv.length; i++) {
|
|
4437
4486
|
const arg = argv[i];
|
|
4438
4487
|
if (arg === "--session") result.sessionId = argv[++i];
|
|
4439
|
-
else if (arg === "--message") result.initMessage = argv[++i];
|
|
4440
4488
|
else if (arg === "--model") result.model = argv[++i];
|
|
4441
4489
|
else if (arg === "--dashboard-url") result.dashboardUrl = argv[++i];
|
|
4442
4490
|
else if (arg === "--cwd") result.cwd = argv[++i];
|
|
@@ -4447,34 +4495,18 @@ function parseChatArgs(argv, env = process.env) {
|
|
|
4447
4495
|
else if (arg) result.errors.push(`unexpected positional: ${arg}`);
|
|
4448
4496
|
}
|
|
4449
4497
|
result.sessionId = result.sessionId ?? env.SESSION_ID ?? void 0;
|
|
4450
|
-
result.initMessage = result.initMessage ?? env.INIT_MESSAGE ?? void 0;
|
|
4451
4498
|
result.model = result.model ?? env.MODEL ?? void 0;
|
|
4452
4499
|
result.dashboardUrl = result.dashboardUrl ?? env.DASHBOARD_URL ?? void 0;
|
|
4453
|
-
for (const key of ["sessionId", "
|
|
4500
|
+
for (const key of ["sessionId", "model", "dashboardUrl"]) {
|
|
4454
4501
|
const v = result[key];
|
|
4455
4502
|
if (typeof v === "string" && v.trim() === "") result[key] = void 0;
|
|
4456
4503
|
}
|
|
4457
|
-
if (!result.
|
|
4458
|
-
result.errors.push("--session <id> (or SESSION_ID env) is required");
|
|
4504
|
+
if (!result.errors.includes("__HELP__")) {
|
|
4505
|
+
if (!result.sessionId) result.errors.push("--session <id> (or SESSION_ID env) is required");
|
|
4506
|
+
if (!result.dashboardUrl) result.errors.push("--dashboard-url <url> (or DASHBOARD_URL env) is required");
|
|
4459
4507
|
}
|
|
4460
4508
|
return result;
|
|
4461
4509
|
}
|
|
4462
|
-
function commitChatFiles(cwd, sessionId, verbose) {
|
|
4463
|
-
const sessionFile = path16.relative(cwd, sessionFilePath(cwd, sessionId));
|
|
4464
|
-
const eventsFile = path16.relative(cwd, eventsFilePath(cwd, sessionId));
|
|
4465
|
-
const paths = [sessionFile, eventsFile].filter((p) => fs19.existsSync(path16.join(cwd, p)));
|
|
4466
|
-
if (paths.length === 0) return;
|
|
4467
|
-
const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
|
|
4468
|
-
try {
|
|
4469
|
-
execFileSync16("git", ["add", ...paths], opts);
|
|
4470
|
-
execFileSync16("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
|
|
4471
|
-
execFileSync16("git", ["push", "--quiet", "origin", "HEAD"], opts);
|
|
4472
|
-
} catch (err) {
|
|
4473
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
4474
|
-
process.stderr.write(`[kody2:chat] commit/push skipped: ${msg}
|
|
4475
|
-
`);
|
|
4476
|
-
}
|
|
4477
|
-
}
|
|
4478
4510
|
function tryLoadConfig(cwd) {
|
|
4479
4511
|
try {
|
|
4480
4512
|
return loadConfig(cwd);
|
|
@@ -4482,11 +4514,6 @@ function tryLoadConfig(cwd) {
|
|
|
4482
4514
|
return null;
|
|
4483
4515
|
}
|
|
4484
4516
|
}
|
|
4485
|
-
function buildSink(cwd, sessionId, dashboardUrl) {
|
|
4486
|
-
const sinks = [new FileSink(eventsFilePath(cwd, sessionId))];
|
|
4487
|
-
if (dashboardUrl) sinks.push(new HttpSink(dashboardUrl, sessionId));
|
|
4488
|
-
return new TeeSink(sinks);
|
|
4489
|
-
}
|
|
4490
4517
|
async function runChat(argv) {
|
|
4491
4518
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
4492
4519
|
process.stdout.write(CHAT_HELP);
|
|
@@ -4500,8 +4527,9 @@ async function runChat(argv) {
|
|
|
4500
4527
|
${CHAT_HELP}`);
|
|
4501
4528
|
return 64;
|
|
4502
4529
|
}
|
|
4503
|
-
const cwd = args.cwd ?
|
|
4530
|
+
const cwd = args.cwd ? path14.resolve(args.cwd) : process.cwd();
|
|
4504
4531
|
const sessionId = args.sessionId;
|
|
4532
|
+
const dashboardUrl = args.dashboardUrl;
|
|
4505
4533
|
const unpackedSecrets = unpackAllSecrets();
|
|
4506
4534
|
if (unpackedSecrets > 0) {
|
|
4507
4535
|
process.stdout.write(`\u2192 kody2: unpacked ${unpackedSecrets} secret(s) from ALL_SECRETS
|
|
@@ -4527,13 +4555,20 @@ ${CHAT_HELP}`);
|
|
|
4527
4555
|
return 99;
|
|
4528
4556
|
}
|
|
4529
4557
|
}
|
|
4558
|
+
let sink;
|
|
4559
|
+
try {
|
|
4560
|
+
sink = new HttpSink(dashboardUrl, sessionId);
|
|
4561
|
+
} catch (err) {
|
|
4562
|
+
process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}
|
|
4563
|
+
`);
|
|
4564
|
+
return 64;
|
|
4565
|
+
}
|
|
4530
4566
|
let litellm = null;
|
|
4531
4567
|
try {
|
|
4532
4568
|
litellm = await startLitellmIfNeeded(model, cwd);
|
|
4533
4569
|
} catch (err) {
|
|
4534
4570
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4535
|
-
|
|
4536
|
-
await sink2.emit({
|
|
4571
|
+
await sink.emit({
|
|
4537
4572
|
event: "chat.error",
|
|
4538
4573
|
payload: { sessionId, error: `litellm startup failed: ${msg}` },
|
|
4539
4574
|
runId: makeRunId(sessionId, "error"),
|
|
@@ -4541,21 +4576,33 @@ ${CHAT_HELP}`);
|
|
|
4541
4576
|
});
|
|
4542
4577
|
return 99;
|
|
4543
4578
|
}
|
|
4544
|
-
|
|
4545
|
-
if (args.initMessage) seedInitialMessage(sessionFile, args.initMessage);
|
|
4546
|
-
const sink = buildSink(cwd, sessionId, args.dashboardUrl);
|
|
4579
|
+
let pull;
|
|
4547
4580
|
try {
|
|
4548
|
-
|
|
4581
|
+
pull = createPullClient({ baseUrl: dashboardUrl, sessionId });
|
|
4582
|
+
} catch (err) {
|
|
4583
|
+
process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}
|
|
4584
|
+
`);
|
|
4585
|
+
try {
|
|
4586
|
+
litellm?.kill();
|
|
4587
|
+
} catch {
|
|
4588
|
+
}
|
|
4589
|
+
return 64;
|
|
4590
|
+
}
|
|
4591
|
+
process.stdout.write(`\u2192 kody2 chat: session ${sessionId}, model ${model.provider}/${model.model}
|
|
4592
|
+
`);
|
|
4593
|
+
try {
|
|
4594
|
+
const result = await runChatSession({
|
|
4549
4595
|
sessionId,
|
|
4550
|
-
sessionFile,
|
|
4551
4596
|
cwd,
|
|
4552
4597
|
model,
|
|
4553
4598
|
litellmUrl: litellm?.url ?? null,
|
|
4554
4599
|
sink,
|
|
4600
|
+
pull,
|
|
4555
4601
|
verbose: args.verbose,
|
|
4556
4602
|
quiet: args.quiet
|
|
4557
4603
|
});
|
|
4558
|
-
|
|
4604
|
+
process.stdout.write(`\u2192 kody2 chat: exited (${result.reason ?? "ok"}) after ${result.turnsProcessed} turn(s)
|
|
4605
|
+
`);
|
|
4559
4606
|
return result.exitCode;
|
|
4560
4607
|
} finally {
|
|
4561
4608
|
try {
|