@kody-ade/kody-engine 0.2.33 → 0.2.35
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 +407 -390
- 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.35",
|
|
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,115 +50,81 @@ var package_default = {
|
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
// src/chat-cli.ts
|
|
53
|
-
import
|
|
53
|
+
import { execFileSync as execFileSync16 } from "child_process";
|
|
54
|
+
import * as fs19 from "fs";
|
|
55
|
+
import * as path16 from "path";
|
|
54
56
|
|
|
55
|
-
// src/chat/
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
};
|
|
57
|
+
// src/chat/events.ts
|
|
58
|
+
import * as fs from "fs";
|
|
59
|
+
import * as path from "path";
|
|
60
|
+
function eventsFilePath(cwd, sessionId) {
|
|
61
|
+
return path.join(cwd, ".kody", "events", `${sessionId}.jsonl`);
|
|
91
62
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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 };
|
|
63
|
+
var FileSink = class {
|
|
64
|
+
constructor(file) {
|
|
65
|
+
this.file = file;
|
|
100
66
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
67
|
+
file;
|
|
68
|
+
async emit(event) {
|
|
69
|
+
fs.mkdirSync(path.dirname(this.file), { recursive: true });
|
|
70
|
+
fs.appendFileSync(this.file, `${JSON.stringify(event)}
|
|
71
|
+
`);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
104
74
|
var HttpSink = class {
|
|
105
|
-
constructor(baseUrl, sessionId,
|
|
75
|
+
constructor(baseUrl, sessionId, logger = {
|
|
106
76
|
warn: (m) => process.stderr.write(`[kody2:chat] ${m}
|
|
107
77
|
`)
|
|
108
78
|
}) {
|
|
79
|
+
this.baseUrl = baseUrl;
|
|
109
80
|
this.sessionId = sessionId;
|
|
110
|
-
this.fetchFn = fetchFn;
|
|
111
81
|
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;
|
|
119
82
|
}
|
|
83
|
+
baseUrl;
|
|
120
84
|
sessionId;
|
|
121
|
-
fetchFn;
|
|
122
85
|
logger;
|
|
123
|
-
origin;
|
|
124
|
-
token;
|
|
125
86
|
async emit(event) {
|
|
126
|
-
const url =
|
|
127
|
-
url.pathname = "/api/kody/events/ingest";
|
|
128
|
-
url.searchParams.set("sessionId", this.sessionId);
|
|
129
|
-
url.searchParams.set("token", this.token);
|
|
87
|
+
const url = withSessionParam(this.baseUrl, this.sessionId);
|
|
130
88
|
try {
|
|
131
|
-
const res = await
|
|
89
|
+
const res = await fetch(url, {
|
|
132
90
|
method: "POST",
|
|
133
|
-
headers: {
|
|
134
|
-
"content-type": "application/json",
|
|
135
|
-
authorization: `Bearer ${this.token}`
|
|
136
|
-
},
|
|
91
|
+
headers: { "content-type": "application/json" },
|
|
137
92
|
body: JSON.stringify(event),
|
|
138
93
|
signal: AbortSignal.timeout(5e3)
|
|
139
94
|
});
|
|
140
95
|
if (!res.ok) {
|
|
141
|
-
this.logger.warn(`HttpSink POST ${url
|
|
96
|
+
this.logger.warn(`HttpSink POST ${url} \u2192 ${res.status}`);
|
|
142
97
|
}
|
|
143
98
|
} catch (err) {
|
|
144
|
-
this.logger.warn(
|
|
145
|
-
`HttpSink POST ${url.pathname} failed: ${err instanceof Error ? err.message : String(err)}`
|
|
146
|
-
);
|
|
99
|
+
this.logger.warn(`HttpSink POST ${url} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
147
100
|
}
|
|
148
101
|
}
|
|
149
102
|
};
|
|
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
|
+
}
|
|
150
116
|
function makeRunId(sessionId, suffix) {
|
|
151
117
|
return `chat-${sessionId}-${suffix}`;
|
|
152
118
|
}
|
|
153
119
|
|
|
154
120
|
// src/agent.ts
|
|
155
|
-
import * as
|
|
156
|
-
import * as
|
|
121
|
+
import * as fs3 from "fs";
|
|
122
|
+
import * as path3 from "path";
|
|
157
123
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
158
124
|
|
|
159
125
|
// src/config.ts
|
|
160
|
-
import * as
|
|
161
|
-
import * as
|
|
126
|
+
import * as fs2 from "fs";
|
|
127
|
+
import * as path2 from "path";
|
|
162
128
|
var LITELLM_DEFAULT_PORT = 4e3;
|
|
163
129
|
var LITELLM_DEFAULT_URL = `http://localhost:${LITELLM_DEFAULT_PORT}`;
|
|
164
130
|
function parseProviderModel(s) {
|
|
@@ -176,13 +142,13 @@ function needsLitellmProxy(model) {
|
|
|
176
142
|
return model.provider !== "claude" && model.provider !== "anthropic";
|
|
177
143
|
}
|
|
178
144
|
function loadConfig(projectDir = process.cwd()) {
|
|
179
|
-
const configPath =
|
|
180
|
-
if (!
|
|
145
|
+
const configPath = path2.join(projectDir, "kody.config.json");
|
|
146
|
+
if (!fs2.existsSync(configPath)) {
|
|
181
147
|
throw new Error(`kody.config.json not found at ${configPath}`);
|
|
182
148
|
}
|
|
183
149
|
let raw;
|
|
184
150
|
try {
|
|
185
|
-
raw = JSON.parse(
|
|
151
|
+
raw = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
|
|
186
152
|
} catch (err) {
|
|
187
153
|
const msg = err instanceof Error ? err.message : String(err);
|
|
188
154
|
throw new Error(`kody.config.json is invalid JSON: ${msg}`);
|
|
@@ -359,10 +325,10 @@ function formatBytes(bytes) {
|
|
|
359
325
|
// src/agent.ts
|
|
360
326
|
var DEFAULT_ALLOWED_TOOLS = ["Bash", "Edit", "Read", "Write", "Glob", "Grep"];
|
|
361
327
|
async function runAgent(opts) {
|
|
362
|
-
const ndjsonDir = opts.ndjsonDir ??
|
|
363
|
-
|
|
364
|
-
const ndjsonPath =
|
|
365
|
-
const fullLog =
|
|
328
|
+
const ndjsonDir = opts.ndjsonDir ?? path3.join(opts.cwd, ".kody2");
|
|
329
|
+
fs3.mkdirSync(ndjsonDir, { recursive: true });
|
|
330
|
+
const ndjsonPath = path3.join(ndjsonDir, "last-run.jsonl");
|
|
331
|
+
const fullLog = fs3.createWriteStream(ndjsonPath, { flags: "w" });
|
|
366
332
|
const env = {
|
|
367
333
|
...process.env,
|
|
368
334
|
SKIP_HOOKS: "1",
|
|
@@ -437,6 +403,53 @@ async function runAgent(opts) {
|
|
|
437
403
|
return { outcome, finalText, error: errorMessage, ndjsonPath };
|
|
438
404
|
}
|
|
439
405
|
|
|
406
|
+
// src/chat/session.ts
|
|
407
|
+
import * as fs4 from "fs";
|
|
408
|
+
import * as path4 from "path";
|
|
409
|
+
function sessionFilePath(cwd, sessionId) {
|
|
410
|
+
return path4.join(cwd, ".kody", "sessions", `${sessionId}.jsonl`);
|
|
411
|
+
}
|
|
412
|
+
function readSession(file) {
|
|
413
|
+
if (!fs4.existsSync(file)) return [];
|
|
414
|
+
const raw = fs4.readFileSync(file, "utf-8").trim();
|
|
415
|
+
if (!raw) return [];
|
|
416
|
+
const turns = [];
|
|
417
|
+
for (const line of raw.split("\n")) {
|
|
418
|
+
if (!line.trim()) continue;
|
|
419
|
+
try {
|
|
420
|
+
const parsed = JSON.parse(line);
|
|
421
|
+
if (parsed.role !== "user" && parsed.role !== "assistant") continue;
|
|
422
|
+
if (typeof parsed.content !== "string") continue;
|
|
423
|
+
turns.push(parsed);
|
|
424
|
+
} catch {
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return turns;
|
|
428
|
+
}
|
|
429
|
+
function appendTurn(file, turn) {
|
|
430
|
+
fs4.mkdirSync(path4.dirname(file), { recursive: true });
|
|
431
|
+
const line = JSON.stringify({
|
|
432
|
+
role: turn.role,
|
|
433
|
+
content: turn.content,
|
|
434
|
+
timestamp: turn.timestamp,
|
|
435
|
+
toolCalls: turn.toolCalls ?? []
|
|
436
|
+
});
|
|
437
|
+
fs4.appendFileSync(file, `${line}
|
|
438
|
+
`);
|
|
439
|
+
}
|
|
440
|
+
function seedInitialMessage(file, message) {
|
|
441
|
+
if (!message.trim()) return false;
|
|
442
|
+
const turns = readSession(file);
|
|
443
|
+
const lastUser = [...turns].reverse().find((t) => t.role === "user");
|
|
444
|
+
if (lastUser && lastUser.content === message) return false;
|
|
445
|
+
appendTurn(file, {
|
|
446
|
+
role: "user",
|
|
447
|
+
content: message,
|
|
448
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
449
|
+
});
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
|
|
440
453
|
// src/chat/loop.ts
|
|
441
454
|
var CHAT_SYSTEM_PROMPT = [
|
|
442
455
|
"You are Kody, an AI assistant for the Kody Operations Dashboard. Reply to the user's",
|
|
@@ -445,15 +458,20 @@ var CHAT_SYSTEM_PROMPT = [
|
|
|
445
458
|
"read repository code or execute small checks when it helps you answer \u2014 otherwise",
|
|
446
459
|
"reply directly. Do not invent file paths, commit SHAs, or command output."
|
|
447
460
|
].join("\n");
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const
|
|
456
|
-
|
|
461
|
+
async function runChatTurn(opts) {
|
|
462
|
+
const turns = readSession(opts.sessionFile);
|
|
463
|
+
if (turns.length === 0) {
|
|
464
|
+
const error = "session file is empty \u2014 nothing to reply to";
|
|
465
|
+
await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
|
|
466
|
+
return { exitCode: 64, error };
|
|
467
|
+
}
|
|
468
|
+
const lastTurn = turns[turns.length - 1];
|
|
469
|
+
if (lastTurn.role !== "user") {
|
|
470
|
+
const error = "last turn is not a user message \u2014 assistant already replied";
|
|
471
|
+
await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
|
|
472
|
+
return { exitCode: 64, error };
|
|
473
|
+
}
|
|
474
|
+
const prompt = buildPrompt(turns, opts.systemPrompt ?? CHAT_SYSTEM_PROMPT);
|
|
457
475
|
const invoke = opts.invokeAgent ?? ((p) => runAgent({
|
|
458
476
|
prompt: p,
|
|
459
477
|
model: opts.model,
|
|
@@ -462,68 +480,34 @@ async function runChatSession(opts) {
|
|
|
462
480
|
verbose: opts.verbose,
|
|
463
481
|
quiet: opts.quiet
|
|
464
482
|
}));
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}
|
|
478
|
-
let response;
|
|
479
|
-
try {
|
|
480
|
-
response = await opts.pull(since, pullTimeoutMs);
|
|
481
|
-
} catch (err) {
|
|
482
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
483
|
-
await emit(opts.sink, "chat.error", opts.sessionId, "error", { error: `pull failed: ${msg}` });
|
|
484
|
-
return { exitCode: 99, turnsProcessed, reason: `pull failed: ${msg}` };
|
|
485
|
-
}
|
|
486
|
-
if (response.turns.length === 0) {
|
|
487
|
-
if (now() - lastActivityAt > idleTimeoutMs) {
|
|
488
|
-
await emit(opts.sink, "chat.done", opts.sessionId, "done", {
|
|
489
|
-
sessionId: opts.sessionId,
|
|
490
|
-
reason: "idle-timeout"
|
|
491
|
-
});
|
|
492
|
-
return { exitCode: 0, turnsProcessed, reason: "idle-timeout" };
|
|
493
|
-
}
|
|
494
|
-
continue;
|
|
495
|
-
}
|
|
496
|
-
const newUserTurns = response.turns.filter((t) => t.role === "user");
|
|
497
|
-
for (const t of newUserTurns) history.push(t);
|
|
498
|
-
since = response.nextSince;
|
|
499
|
-
if (newUserTurns.length === 0) continue;
|
|
500
|
-
lastActivityAt = now();
|
|
501
|
-
const prompt = buildPrompt(history, systemPrompt);
|
|
502
|
-
let result;
|
|
503
|
-
try {
|
|
504
|
-
result = await invoke(prompt);
|
|
505
|
-
} catch (err) {
|
|
506
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
507
|
-
await emit(opts.sink, "chat.error", opts.sessionId, "error", { error: msg });
|
|
508
|
-
return { exitCode: 99, turnsProcessed, reason: msg };
|
|
509
|
-
}
|
|
510
|
-
if (result.outcome !== "completed") {
|
|
511
|
-
const error = result.error ?? "agent did not complete";
|
|
512
|
-
await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
|
|
513
|
-
return { exitCode: 99, turnsProcessed, reason: error };
|
|
514
|
-
}
|
|
515
|
-
const reply = result.finalText.trim();
|
|
516
|
-
const replyTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
517
|
-
history.push({ role: "assistant", content: reply, timestamp: replyTimestamp });
|
|
518
|
-
turnsProcessed++;
|
|
519
|
-
lastActivityAt = now();
|
|
520
|
-
await emit(opts.sink, "chat.message", opts.sessionId, `message-${turnsProcessed}`, {
|
|
521
|
-
sessionId: opts.sessionId,
|
|
522
|
-
role: "assistant",
|
|
523
|
-
content: reply,
|
|
524
|
-
timestamp: replyTimestamp
|
|
525
|
-
});
|
|
483
|
+
let result;
|
|
484
|
+
try {
|
|
485
|
+
result = await invoke(prompt);
|
|
486
|
+
} catch (err) {
|
|
487
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
488
|
+
await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
|
|
489
|
+
return { exitCode: 99, error };
|
|
490
|
+
}
|
|
491
|
+
if (result.outcome !== "completed") {
|
|
492
|
+
const error = result.error ?? "agent did not complete";
|
|
493
|
+
await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
|
|
494
|
+
return { exitCode: 99, error };
|
|
526
495
|
}
|
|
496
|
+
const reply = result.finalText.trim();
|
|
497
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
498
|
+
appendTurn(opts.sessionFile, {
|
|
499
|
+
role: "assistant",
|
|
500
|
+
content: reply,
|
|
501
|
+
timestamp: now
|
|
502
|
+
});
|
|
503
|
+
await emit(opts.sink, "chat.message", opts.sessionId, "message", {
|
|
504
|
+
sessionId: opts.sessionId,
|
|
505
|
+
role: "assistant",
|
|
506
|
+
content: reply,
|
|
507
|
+
timestamp: now
|
|
508
|
+
});
|
|
509
|
+
await emit(opts.sink, "chat.done", opts.sessionId, "done", { sessionId: opts.sessionId });
|
|
510
|
+
return { exitCode: 0, reply };
|
|
527
511
|
}
|
|
528
512
|
function buildPrompt(turns, systemPrompt) {
|
|
529
513
|
const header = `System: ${systemPrompt}`;
|
|
@@ -545,11 +529,11 @@ async function emit(sink, type, sessionId, suffix, payload) {
|
|
|
545
529
|
|
|
546
530
|
// src/kody2-cli.ts
|
|
547
531
|
import { execFileSync as execFileSync15 } from "child_process";
|
|
548
|
-
import * as
|
|
549
|
-
import * as
|
|
532
|
+
import * as fs18 from "fs";
|
|
533
|
+
import * as path15 from "path";
|
|
550
534
|
|
|
551
535
|
// src/dispatch.ts
|
|
552
|
-
import * as
|
|
536
|
+
import * as fs5 from "fs";
|
|
553
537
|
function autoDispatch(opts) {
|
|
554
538
|
const explicit = opts?.explicit;
|
|
555
539
|
if (explicit?.issueNumber && explicit.issueNumber > 0) {
|
|
@@ -561,10 +545,10 @@ function autoDispatch(opts) {
|
|
|
561
545
|
}
|
|
562
546
|
const eventName = process.env.GITHUB_EVENT_NAME;
|
|
563
547
|
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
564
|
-
if (!eventName || !eventPath || !
|
|
548
|
+
if (!eventName || !eventPath || !fs5.existsSync(eventPath)) return null;
|
|
565
549
|
let event = {};
|
|
566
550
|
try {
|
|
567
|
-
event = JSON.parse(
|
|
551
|
+
event = JSON.parse(fs5.readFileSync(eventPath, "utf-8"));
|
|
568
552
|
} catch {
|
|
569
553
|
return null;
|
|
570
554
|
}
|
|
@@ -633,14 +617,14 @@ function extractFeedback(afterTag) {
|
|
|
633
617
|
}
|
|
634
618
|
|
|
635
619
|
// src/executor.ts
|
|
636
|
-
import * as
|
|
637
|
-
import * as
|
|
620
|
+
import * as fs17 from "fs";
|
|
621
|
+
import * as path14 from "path";
|
|
638
622
|
|
|
639
623
|
// src/litellm.ts
|
|
640
624
|
import { execFileSync, spawn } from "child_process";
|
|
641
|
-
import * as
|
|
625
|
+
import * as fs6 from "fs";
|
|
642
626
|
import * as os from "os";
|
|
643
|
-
import * as
|
|
627
|
+
import * as path5 from "path";
|
|
644
628
|
async function checkLitellmHealth(url) {
|
|
645
629
|
try {
|
|
646
630
|
const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
|
|
@@ -680,20 +664,20 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
|
|
|
680
664
|
throw new Error("litellm not installed \u2014 run: pip install 'litellm[proxy]'");
|
|
681
665
|
}
|
|
682
666
|
}
|
|
683
|
-
const configPath =
|
|
684
|
-
|
|
667
|
+
const configPath = path5.join(os.tmpdir(), `kody2-litellm-${Date.now()}.yaml`);
|
|
668
|
+
fs6.writeFileSync(configPath, generateLitellmConfigYaml(model));
|
|
685
669
|
const portMatch = url.match(/:(\d+)/);
|
|
686
670
|
const port = portMatch ? portMatch[1] : "4000";
|
|
687
671
|
const args = cmd === "litellm" ? ["--config", configPath, "--port", port] : ["-m", "litellm", "--config", configPath, "--port", port];
|
|
688
672
|
const dotenvVars = readDotenvApiKeys(projectDir);
|
|
689
|
-
const logPath =
|
|
690
|
-
const outFd =
|
|
673
|
+
const logPath = path5.join(os.tmpdir(), `kody2-litellm-${Date.now()}.log`);
|
|
674
|
+
const outFd = fs6.openSync(logPath, "w");
|
|
691
675
|
const child = spawn(cmd, args, {
|
|
692
676
|
stdio: ["ignore", outFd, outFd],
|
|
693
677
|
detached: true,
|
|
694
678
|
env: stripBlockingEnv({ ...process.env, ...dotenvVars })
|
|
695
679
|
});
|
|
696
|
-
|
|
680
|
+
fs6.closeSync(outFd);
|
|
697
681
|
for (let i = 0; i < 30; i++) {
|
|
698
682
|
await new Promise((r) => setTimeout(r, 2e3));
|
|
699
683
|
if (await checkLitellmHealth(url)) {
|
|
@@ -710,7 +694,7 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
|
|
|
710
694
|
}
|
|
711
695
|
let logTail = "";
|
|
712
696
|
try {
|
|
713
|
-
logTail =
|
|
697
|
+
logTail = fs6.readFileSync(logPath, "utf-8").slice(-2e3);
|
|
714
698
|
} catch {
|
|
715
699
|
}
|
|
716
700
|
try {
|
|
@@ -721,10 +705,10 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
|
|
|
721
705
|
${logTail}`);
|
|
722
706
|
}
|
|
723
707
|
function readDotenvApiKeys(projectDir) {
|
|
724
|
-
const dotenvPath =
|
|
725
|
-
if (!
|
|
708
|
+
const dotenvPath = path5.join(projectDir, ".env");
|
|
709
|
+
if (!fs6.existsSync(dotenvPath)) return {};
|
|
726
710
|
const result = {};
|
|
727
|
-
for (const rawLine of
|
|
711
|
+
for (const rawLine of fs6.readFileSync(dotenvPath, "utf-8").split("\n")) {
|
|
728
712
|
const line = rawLine.trim();
|
|
729
713
|
if (!line || line.startsWith("#")) continue;
|
|
730
714
|
const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
|
|
@@ -747,8 +731,8 @@ function stripBlockingEnv(env) {
|
|
|
747
731
|
}
|
|
748
732
|
|
|
749
733
|
// src/profile.ts
|
|
750
|
-
import * as
|
|
751
|
-
import * as
|
|
734
|
+
import * as fs7 from "fs";
|
|
735
|
+
import * as path6 from "path";
|
|
752
736
|
var VALID_INPUT_TYPES = /* @__PURE__ */ new Set(["int", "string", "bool", "enum"]);
|
|
753
737
|
var VALID_PERMISSION_MODES = /* @__PURE__ */ new Set(["default", "acceptEdits", "plan", "bypassPermissions"]);
|
|
754
738
|
var ProfileError = class extends Error {
|
|
@@ -761,12 +745,12 @@ var ProfileError = class extends Error {
|
|
|
761
745
|
profilePath;
|
|
762
746
|
};
|
|
763
747
|
function loadProfile(profilePath) {
|
|
764
|
-
if (!
|
|
748
|
+
if (!fs7.existsSync(profilePath)) {
|
|
765
749
|
throw new ProfileError(profilePath, "file not found");
|
|
766
750
|
}
|
|
767
751
|
let raw;
|
|
768
752
|
try {
|
|
769
|
-
raw = JSON.parse(
|
|
753
|
+
raw = JSON.parse(fs7.readFileSync(profilePath, "utf-8"));
|
|
770
754
|
} catch (err) {
|
|
771
755
|
throw new ProfileError(profilePath, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
772
756
|
}
|
|
@@ -790,7 +774,7 @@ function loadProfile(profilePath) {
|
|
|
790
774
|
outputContract: r.outputContract,
|
|
791
775
|
inputArtifacts: parseInputArtifacts(profilePath, r.input),
|
|
792
776
|
outputArtifacts: parseOutputArtifacts(profilePath, r.output),
|
|
793
|
-
dir:
|
|
777
|
+
dir: path6.dirname(profilePath)
|
|
794
778
|
};
|
|
795
779
|
return profile;
|
|
796
780
|
}
|
|
@@ -970,21 +954,21 @@ function parseScriptList(p, key, raw) {
|
|
|
970
954
|
}
|
|
971
955
|
|
|
972
956
|
// src/scripts/buildSyntheticPlugin.ts
|
|
973
|
-
import * as
|
|
957
|
+
import * as fs8 from "fs";
|
|
974
958
|
import * as os2 from "os";
|
|
975
|
-
import * as
|
|
959
|
+
import * as path7 from "path";
|
|
976
960
|
function getPluginsCatalogRoot() {
|
|
977
|
-
const here =
|
|
961
|
+
const here = path7.dirname(new URL(import.meta.url).pathname);
|
|
978
962
|
const candidates = [
|
|
979
|
-
|
|
963
|
+
path7.join(here, "..", "plugins"),
|
|
980
964
|
// dev: src/scripts → src/plugins
|
|
981
|
-
|
|
965
|
+
path7.join(here, "..", "..", "plugins"),
|
|
982
966
|
// built: dist/scripts → dist/plugins
|
|
983
|
-
|
|
967
|
+
path7.join(here, "..", "..", "src", "plugins")
|
|
984
968
|
// fallback
|
|
985
969
|
];
|
|
986
970
|
for (const c of candidates) {
|
|
987
|
-
if (
|
|
971
|
+
if (fs8.existsSync(c) && fs8.statSync(c).isDirectory()) return c;
|
|
988
972
|
}
|
|
989
973
|
return candidates[0];
|
|
990
974
|
}
|
|
@@ -994,50 +978,50 @@ var buildSyntheticPlugin = async (ctx, profile) => {
|
|
|
994
978
|
if (!needsSynthetic) return;
|
|
995
979
|
const catalog = getPluginsCatalogRoot();
|
|
996
980
|
const runId = `${profile.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
997
|
-
const root =
|
|
998
|
-
|
|
981
|
+
const root = path7.join(os2.tmpdir(), `kody2-synth-${runId}`);
|
|
982
|
+
fs8.mkdirSync(path7.join(root, ".claude-plugin"), { recursive: true });
|
|
999
983
|
if (cc.skills.length > 0) {
|
|
1000
|
-
const dst =
|
|
1001
|
-
|
|
984
|
+
const dst = path7.join(root, "skills");
|
|
985
|
+
fs8.mkdirSync(dst, { recursive: true });
|
|
1002
986
|
for (const name of cc.skills) {
|
|
1003
|
-
const src =
|
|
1004
|
-
if (!
|
|
1005
|
-
copyDir(src,
|
|
987
|
+
const src = path7.join(catalog, "skills", name);
|
|
988
|
+
if (!fs8.existsSync(src)) throw new Error(`buildSyntheticPlugin: skill not found in catalog: ${name}`);
|
|
989
|
+
copyDir(src, path7.join(dst, name));
|
|
1006
990
|
}
|
|
1007
991
|
}
|
|
1008
992
|
if (cc.commands.length > 0) {
|
|
1009
|
-
const dst =
|
|
1010
|
-
|
|
993
|
+
const dst = path7.join(root, "commands");
|
|
994
|
+
fs8.mkdirSync(dst, { recursive: true });
|
|
1011
995
|
for (const name of cc.commands) {
|
|
1012
|
-
const src =
|
|
1013
|
-
if (!
|
|
1014
|
-
|
|
996
|
+
const src = path7.join(catalog, "commands", `${name}.md`);
|
|
997
|
+
if (!fs8.existsSync(src)) throw new Error(`buildSyntheticPlugin: command not found in catalog: ${name}`);
|
|
998
|
+
fs8.copyFileSync(src, path7.join(dst, `${name}.md`));
|
|
1015
999
|
}
|
|
1016
1000
|
}
|
|
1017
1001
|
if (cc.subagents.length > 0) {
|
|
1018
|
-
const dst =
|
|
1019
|
-
|
|
1002
|
+
const dst = path7.join(root, "agents");
|
|
1003
|
+
fs8.mkdirSync(dst, { recursive: true });
|
|
1020
1004
|
for (const name of cc.subagents) {
|
|
1021
|
-
const src =
|
|
1022
|
-
if (!
|
|
1023
|
-
|
|
1005
|
+
const src = path7.join(catalog, "agents", `${name}.md`);
|
|
1006
|
+
if (!fs8.existsSync(src)) throw new Error(`buildSyntheticPlugin: subagent not found in catalog: ${name}`);
|
|
1007
|
+
fs8.copyFileSync(src, path7.join(dst, `${name}.md`));
|
|
1024
1008
|
}
|
|
1025
1009
|
}
|
|
1026
1010
|
if (cc.hooks.length > 0) {
|
|
1027
|
-
const dst =
|
|
1028
|
-
|
|
1011
|
+
const dst = path7.join(root, "hooks");
|
|
1012
|
+
fs8.mkdirSync(dst, { recursive: true });
|
|
1029
1013
|
const merged = { hooks: {} };
|
|
1030
1014
|
for (const name of cc.hooks) {
|
|
1031
|
-
const src =
|
|
1032
|
-
if (!
|
|
1033
|
-
const parsed = JSON.parse(
|
|
1015
|
+
const src = path7.join(catalog, "hooks", `${name}.json`);
|
|
1016
|
+
if (!fs8.existsSync(src)) throw new Error(`buildSyntheticPlugin: hook not found in catalog: ${name}`);
|
|
1017
|
+
const parsed = JSON.parse(fs8.readFileSync(src, "utf-8"));
|
|
1034
1018
|
for (const [event, entries] of Object.entries(parsed.hooks ?? {})) {
|
|
1035
1019
|
if (!Array.isArray(entries)) continue;
|
|
1036
1020
|
if (!merged.hooks[event]) merged.hooks[event] = [];
|
|
1037
1021
|
merged.hooks[event].push(...entries);
|
|
1038
1022
|
}
|
|
1039
1023
|
}
|
|
1040
|
-
|
|
1024
|
+
fs8.writeFileSync(path7.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
|
|
1041
1025
|
`);
|
|
1042
1026
|
}
|
|
1043
1027
|
const manifest = {
|
|
@@ -1048,17 +1032,17 @@ var buildSyntheticPlugin = async (ctx, profile) => {
|
|
|
1048
1032
|
if (cc.skills.length > 0) manifest.skills = ["./skills/"];
|
|
1049
1033
|
if (cc.commands.length > 0) manifest.commands = ["./commands/"];
|
|
1050
1034
|
if (cc.subagents.length > 0) manifest.agents = cc.subagents.map((n) => `./agents/${n}.md`);
|
|
1051
|
-
|
|
1035
|
+
fs8.writeFileSync(path7.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
|
|
1052
1036
|
`);
|
|
1053
1037
|
ctx.data.syntheticPluginPath = root;
|
|
1054
1038
|
};
|
|
1055
1039
|
function copyDir(src, dst) {
|
|
1056
|
-
|
|
1057
|
-
for (const ent of
|
|
1058
|
-
const s =
|
|
1059
|
-
const d =
|
|
1040
|
+
fs8.mkdirSync(dst, { recursive: true });
|
|
1041
|
+
for (const ent of fs8.readdirSync(src, { withFileTypes: true })) {
|
|
1042
|
+
const s = path7.join(src, ent.name);
|
|
1043
|
+
const d = path7.join(dst, ent.name);
|
|
1060
1044
|
if (ent.isDirectory()) copyDir(s, d);
|
|
1061
|
-
else if (ent.isFile())
|
|
1045
|
+
else if (ent.isFile()) fs8.copyFileSync(s, d);
|
|
1062
1046
|
}
|
|
1063
1047
|
}
|
|
1064
1048
|
|
|
@@ -1124,18 +1108,18 @@ function formatMissesForFeedback(misses) {
|
|
|
1124
1108
|
}
|
|
1125
1109
|
|
|
1126
1110
|
// src/prompt.ts
|
|
1127
|
-
import * as
|
|
1128
|
-
import * as
|
|
1111
|
+
import * as fs9 from "fs";
|
|
1112
|
+
import * as path8 from "path";
|
|
1129
1113
|
var CONVENTIONS_PER_FILE_MAX_BYTES = 3e4;
|
|
1130
1114
|
var CONVENTION_FILES = ["CLAUDE.md", "AGENTS.md"];
|
|
1131
1115
|
function loadProjectConventions(projectDir) {
|
|
1132
1116
|
const out = [];
|
|
1133
1117
|
for (const rel of CONVENTION_FILES) {
|
|
1134
|
-
const abs =
|
|
1135
|
-
if (!
|
|
1118
|
+
const abs = path8.join(projectDir, rel);
|
|
1119
|
+
if (!fs9.existsSync(abs)) continue;
|
|
1136
1120
|
let content;
|
|
1137
1121
|
try {
|
|
1138
|
-
content =
|
|
1122
|
+
content = fs9.readFileSync(abs, "utf-8");
|
|
1139
1123
|
} catch {
|
|
1140
1124
|
continue;
|
|
1141
1125
|
}
|
|
@@ -1256,8 +1240,8 @@ import { execFileSync as execFileSync4 } from "child_process";
|
|
|
1256
1240
|
|
|
1257
1241
|
// src/commit.ts
|
|
1258
1242
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
1259
|
-
import * as
|
|
1260
|
-
import * as
|
|
1243
|
+
import * as fs10 from "fs";
|
|
1244
|
+
import * as path9 from "path";
|
|
1261
1245
|
var FORBIDDEN_PATH_PREFIXES = [
|
|
1262
1246
|
".kody/",
|
|
1263
1247
|
".kody-engine/",
|
|
@@ -1312,18 +1296,18 @@ function tryGit(args, cwd) {
|
|
|
1312
1296
|
}
|
|
1313
1297
|
function abortUnfinishedGitOps(cwd) {
|
|
1314
1298
|
const aborted = [];
|
|
1315
|
-
const gitDir =
|
|
1316
|
-
if (!
|
|
1317
|
-
if (
|
|
1299
|
+
const gitDir = path9.join(cwd ?? process.cwd(), ".git");
|
|
1300
|
+
if (!fs10.existsSync(gitDir)) return aborted;
|
|
1301
|
+
if (fs10.existsSync(path9.join(gitDir, "MERGE_HEAD"))) {
|
|
1318
1302
|
if (tryGit(["merge", "--abort"], cwd)) aborted.push("merge");
|
|
1319
1303
|
}
|
|
1320
|
-
if (
|
|
1304
|
+
if (fs10.existsSync(path9.join(gitDir, "CHERRY_PICK_HEAD"))) {
|
|
1321
1305
|
if (tryGit(["cherry-pick", "--abort"], cwd)) aborted.push("cherry-pick");
|
|
1322
1306
|
}
|
|
1323
|
-
if (
|
|
1307
|
+
if (fs10.existsSync(path9.join(gitDir, "REVERT_HEAD"))) {
|
|
1324
1308
|
if (tryGit(["revert", "--abort"], cwd)) aborted.push("revert");
|
|
1325
1309
|
}
|
|
1326
|
-
if (
|
|
1310
|
+
if (fs10.existsSync(path9.join(gitDir, "rebase-merge")) || fs10.existsSync(path9.join(gitDir, "rebase-apply"))) {
|
|
1327
1311
|
if (tryGit(["rebase", "--abort"], cwd)) aborted.push("rebase");
|
|
1328
1312
|
}
|
|
1329
1313
|
try {
|
|
@@ -1365,7 +1349,7 @@ function normalizeCommitMessage(raw) {
|
|
|
1365
1349
|
function commitAndPush(branch, agentMessage, cwd) {
|
|
1366
1350
|
const allChanged = listChangedFiles(cwd);
|
|
1367
1351
|
const allowedFiles = allChanged.filter((f) => !isForbiddenPath(f));
|
|
1368
|
-
const mergeHeadExists =
|
|
1352
|
+
const mergeHeadExists = fs10.existsSync(path9.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
|
|
1369
1353
|
if (allowedFiles.length === 0 && !mergeHeadExists) {
|
|
1370
1354
|
return { committed: false, pushed: false, sha: "", message: "" };
|
|
1371
1355
|
}
|
|
@@ -1460,20 +1444,20 @@ function defaultCommitMessage(mode, data) {
|
|
|
1460
1444
|
}
|
|
1461
1445
|
|
|
1462
1446
|
// src/scripts/composePrompt.ts
|
|
1463
|
-
import * as
|
|
1464
|
-
import * as
|
|
1447
|
+
import * as fs11 from "fs";
|
|
1448
|
+
import * as path10 from "path";
|
|
1465
1449
|
var MUSTACHE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
|
|
1466
1450
|
var composePrompt = async (ctx, profile) => {
|
|
1467
1451
|
const explicit = ctx.data.promptTemplate;
|
|
1468
1452
|
const mode = ctx.args.mode;
|
|
1469
1453
|
const candidates = [
|
|
1470
|
-
explicit ?
|
|
1471
|
-
mode ?
|
|
1472
|
-
|
|
1454
|
+
explicit ? path10.join(profile.dir, explicit) : null,
|
|
1455
|
+
mode ? path10.join(profile.dir, "prompts", `${mode}.md`) : null,
|
|
1456
|
+
path10.join(profile.dir, "prompt.md")
|
|
1473
1457
|
].filter(Boolean);
|
|
1474
1458
|
let templatePath = "";
|
|
1475
1459
|
for (const c of candidates) {
|
|
1476
|
-
if (
|
|
1460
|
+
if (fs11.existsSync(c)) {
|
|
1477
1461
|
templatePath = c;
|
|
1478
1462
|
break;
|
|
1479
1463
|
}
|
|
@@ -1481,7 +1465,7 @@ var composePrompt = async (ctx, profile) => {
|
|
|
1481
1465
|
if (!templatePath) {
|
|
1482
1466
|
throw new Error(`profile at ${profile.dir}: no prompt template found (tried ${candidates.join(", ")})`);
|
|
1483
1467
|
}
|
|
1484
|
-
const template =
|
|
1468
|
+
const template = fs11.readFileSync(templatePath, "utf-8");
|
|
1485
1469
|
const tokens = {
|
|
1486
1470
|
...stringifyAll(ctx.args, "args."),
|
|
1487
1471
|
...stringifyAll(ctx.data, ""),
|
|
@@ -1956,7 +1940,7 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd) {
|
|
|
1956
1940
|
|
|
1957
1941
|
// src/gha.ts
|
|
1958
1942
|
import { execFileSync as execFileSync7 } from "child_process";
|
|
1959
|
-
import * as
|
|
1943
|
+
import * as fs12 from "fs";
|
|
1960
1944
|
function getRunUrl() {
|
|
1961
1945
|
const server = process.env.GITHUB_SERVER_URL;
|
|
1962
1946
|
const repo = process.env.GITHUB_REPOSITORY;
|
|
@@ -1967,10 +1951,10 @@ function getRunUrl() {
|
|
|
1967
1951
|
function reactToTriggerComment(cwd) {
|
|
1968
1952
|
if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
|
|
1969
1953
|
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
1970
|
-
if (!eventPath || !
|
|
1954
|
+
if (!eventPath || !fs12.existsSync(eventPath)) return;
|
|
1971
1955
|
let event = null;
|
|
1972
1956
|
try {
|
|
1973
|
-
event = JSON.parse(
|
|
1957
|
+
event = JSON.parse(fs12.readFileSync(eventPath, "utf-8"));
|
|
1974
1958
|
} catch {
|
|
1975
1959
|
return;
|
|
1976
1960
|
}
|
|
@@ -2196,35 +2180,35 @@ function tryPostPr2(prNumber, body, cwd) {
|
|
|
2196
2180
|
|
|
2197
2181
|
// src/scripts/initFlow.ts
|
|
2198
2182
|
import { execFileSync as execFileSync9 } from "child_process";
|
|
2199
|
-
import * as
|
|
2200
|
-
import * as
|
|
2183
|
+
import * as fs14 from "fs";
|
|
2184
|
+
import * as path12 from "path";
|
|
2201
2185
|
|
|
2202
2186
|
// src/registry.ts
|
|
2203
|
-
import * as
|
|
2204
|
-
import * as
|
|
2187
|
+
import * as fs13 from "fs";
|
|
2188
|
+
import * as path11 from "path";
|
|
2205
2189
|
function getExecutablesRoot() {
|
|
2206
|
-
const here =
|
|
2190
|
+
const here = path11.dirname(new URL(import.meta.url).pathname);
|
|
2207
2191
|
const candidates = [
|
|
2208
|
-
|
|
2192
|
+
path11.join(here, "executables"),
|
|
2209
2193
|
// dev: src/
|
|
2210
|
-
|
|
2194
|
+
path11.join(here, "..", "executables"),
|
|
2211
2195
|
// built: dist/bin → dist/executables
|
|
2212
|
-
|
|
2196
|
+
path11.join(here, "..", "src", "executables")
|
|
2213
2197
|
// fallback
|
|
2214
2198
|
];
|
|
2215
2199
|
for (const c of candidates) {
|
|
2216
|
-
if (
|
|
2200
|
+
if (fs13.existsSync(c) && fs13.statSync(c).isDirectory()) return c;
|
|
2217
2201
|
}
|
|
2218
2202
|
return candidates[0];
|
|
2219
2203
|
}
|
|
2220
2204
|
function listExecutables(root = getExecutablesRoot()) {
|
|
2221
|
-
if (!
|
|
2222
|
-
const entries =
|
|
2205
|
+
if (!fs13.existsSync(root)) return [];
|
|
2206
|
+
const entries = fs13.readdirSync(root, { withFileTypes: true });
|
|
2223
2207
|
const out = [];
|
|
2224
2208
|
for (const ent of entries) {
|
|
2225
2209
|
if (!ent.isDirectory()) continue;
|
|
2226
|
-
const profilePath =
|
|
2227
|
-
if (
|
|
2210
|
+
const profilePath = path11.join(root, ent.name, "profile.json");
|
|
2211
|
+
if (fs13.existsSync(profilePath) && fs13.statSync(profilePath).isFile()) {
|
|
2228
2212
|
out.push({ name: ent.name, profilePath });
|
|
2229
2213
|
}
|
|
2230
2214
|
}
|
|
@@ -2232,8 +2216,8 @@ function listExecutables(root = getExecutablesRoot()) {
|
|
|
2232
2216
|
}
|
|
2233
2217
|
function hasExecutable(name, root = getExecutablesRoot()) {
|
|
2234
2218
|
if (!isSafeName(name)) return false;
|
|
2235
|
-
const profilePath =
|
|
2236
|
-
return
|
|
2219
|
+
const profilePath = path11.join(root, name, "profile.json");
|
|
2220
|
+
return fs13.existsSync(profilePath) && fs13.statSync(profilePath).isFile();
|
|
2237
2221
|
}
|
|
2238
2222
|
function isSafeName(name) {
|
|
2239
2223
|
return /^[a-z][a-z0-9-]*$/.test(name) && !name.includes("..");
|
|
@@ -2262,9 +2246,9 @@ function parseGenericFlags(argv) {
|
|
|
2262
2246
|
|
|
2263
2247
|
// src/scripts/initFlow.ts
|
|
2264
2248
|
function detectPackageManager(cwd) {
|
|
2265
|
-
if (
|
|
2266
|
-
if (
|
|
2267
|
-
if (
|
|
2249
|
+
if (fs14.existsSync(path12.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
2250
|
+
if (fs14.existsSync(path12.join(cwd, "yarn.lock"))) return "yarn";
|
|
2251
|
+
if (fs14.existsSync(path12.join(cwd, "bun.lockb"))) return "bun";
|
|
2268
2252
|
return "npm";
|
|
2269
2253
|
}
|
|
2270
2254
|
function qualityCommandsFor(pm) {
|
|
@@ -2385,22 +2369,22 @@ function performInit(cwd, force) {
|
|
|
2385
2369
|
const pm = detectPackageManager(cwd);
|
|
2386
2370
|
const ownerRepo = detectOwnerRepo(cwd);
|
|
2387
2371
|
const defaultBranch = defaultBranchFromGit(cwd);
|
|
2388
|
-
const configPath =
|
|
2389
|
-
if (
|
|
2372
|
+
const configPath = path12.join(cwd, "kody.config.json");
|
|
2373
|
+
if (fs14.existsSync(configPath) && !force) {
|
|
2390
2374
|
skipped.push("kody.config.json");
|
|
2391
2375
|
} else {
|
|
2392
2376
|
const cfg = makeConfig(pm, ownerRepo, defaultBranch);
|
|
2393
|
-
|
|
2377
|
+
fs14.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
|
|
2394
2378
|
`);
|
|
2395
2379
|
wrote.push("kody.config.json");
|
|
2396
2380
|
}
|
|
2397
|
-
const workflowDir =
|
|
2398
|
-
const workflowPath =
|
|
2399
|
-
if (
|
|
2381
|
+
const workflowDir = path12.join(cwd, ".github", "workflows");
|
|
2382
|
+
const workflowPath = path12.join(workflowDir, "kody2.yml");
|
|
2383
|
+
if (fs14.existsSync(workflowPath) && !force) {
|
|
2400
2384
|
skipped.push(".github/workflows/kody2.yml");
|
|
2401
2385
|
} else {
|
|
2402
|
-
|
|
2403
|
-
|
|
2386
|
+
fs14.mkdirSync(workflowDir, { recursive: true });
|
|
2387
|
+
fs14.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
|
|
2404
2388
|
wrote.push(".github/workflows/kody2.yml");
|
|
2405
2389
|
}
|
|
2406
2390
|
for (const exe of listExecutables()) {
|
|
@@ -2411,12 +2395,12 @@ function performInit(cwd, force) {
|
|
|
2411
2395
|
continue;
|
|
2412
2396
|
}
|
|
2413
2397
|
if (profile.kind !== "scheduled" || !profile.schedule) continue;
|
|
2414
|
-
const target =
|
|
2415
|
-
if (
|
|
2398
|
+
const target = path12.join(workflowDir, `kody2-${exe.name}.yml`);
|
|
2399
|
+
if (fs14.existsSync(target) && !force) {
|
|
2416
2400
|
skipped.push(`.github/workflows/kody2-${exe.name}.yml`);
|
|
2417
2401
|
continue;
|
|
2418
2402
|
}
|
|
2419
|
-
|
|
2403
|
+
fs14.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
|
|
2420
2404
|
wrote.push(`.github/workflows/kody2-${exe.name}.yml`);
|
|
2421
2405
|
}
|
|
2422
2406
|
return { wrote, skipped };
|
|
@@ -2923,8 +2907,8 @@ REVIEW_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.
|
|
|
2923
2907
|
|
|
2924
2908
|
// src/scripts/releaseFlow.ts
|
|
2925
2909
|
import { execFileSync as execFileSync11, spawnSync } from "child_process";
|
|
2926
|
-
import * as
|
|
2927
|
-
import * as
|
|
2910
|
+
import * as fs15 from "fs";
|
|
2911
|
+
import * as path13 from "path";
|
|
2928
2912
|
function bumpVersion(current, bump) {
|
|
2929
2913
|
const m = current.match(/^(\d+)\.(\d+)\.(\d+)(.*)$/);
|
|
2930
2914
|
if (!m) throw new Error(`cannot parse version '${current}' (expected x.y.z[-suffix])`);
|
|
@@ -2940,12 +2924,12 @@ function bumpVersion(current, bump) {
|
|
|
2940
2924
|
return `${major}.${minor}.${patch}`;
|
|
2941
2925
|
}
|
|
2942
2926
|
function updateVersionInFile(file, newVersion, cwd) {
|
|
2943
|
-
const abs =
|
|
2944
|
-
if (!
|
|
2945
|
-
const content =
|
|
2927
|
+
const abs = path13.join(cwd, file);
|
|
2928
|
+
if (!fs15.existsSync(abs)) return false;
|
|
2929
|
+
const content = fs15.readFileSync(abs, "utf-8");
|
|
2946
2930
|
const updated = content.replace(/"version"\s*:\s*"[^"]+"/, `"version": "${newVersion}"`);
|
|
2947
2931
|
if (updated === content) return false;
|
|
2948
|
-
|
|
2932
|
+
fs15.writeFileSync(abs, updated);
|
|
2949
2933
|
return true;
|
|
2950
2934
|
}
|
|
2951
2935
|
function generateChangelog(cwd, newVersion, lastTag) {
|
|
@@ -2993,19 +2977,19 @@ function generateChangelog(cwd, newVersion, lastTag) {
|
|
|
2993
2977
|
return parts.join("\n");
|
|
2994
2978
|
}
|
|
2995
2979
|
function prependChangelog(cwd, entry) {
|
|
2996
|
-
const p =
|
|
2980
|
+
const p = path13.join(cwd, "CHANGELOG.md");
|
|
2997
2981
|
const header = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n";
|
|
2998
|
-
if (
|
|
2999
|
-
const prior =
|
|
2982
|
+
if (fs15.existsSync(p)) {
|
|
2983
|
+
const prior = fs15.readFileSync(p, "utf-8");
|
|
3000
2984
|
if (/^#\s*Changelog\b/m.test(prior)) {
|
|
3001
2985
|
const idx = prior.indexOf("\n", prior.indexOf("# Changelog"));
|
|
3002
|
-
|
|
2986
|
+
fs15.writeFileSync(p, `${prior.slice(0, idx + 1)}
|
|
3003
2987
|
${entry}${prior.slice(idx + 1)}`);
|
|
3004
2988
|
} else {
|
|
3005
|
-
|
|
2989
|
+
fs15.writeFileSync(p, `${header}${entry}${prior}`);
|
|
3006
2990
|
}
|
|
3007
2991
|
} else {
|
|
3008
|
-
|
|
2992
|
+
fs15.writeFileSync(p, `${header}${entry}`);
|
|
3009
2993
|
}
|
|
3010
2994
|
}
|
|
3011
2995
|
function git3(args, cwd, timeout = 6e4) {
|
|
@@ -3056,13 +3040,13 @@ var releaseFlow = async (ctx) => {
|
|
|
3056
3040
|
};
|
|
3057
3041
|
async function runPrepare(args) {
|
|
3058
3042
|
const { cwd, bump, dryRun, versionFiles, ctx } = args;
|
|
3059
|
-
const pkgPath =
|
|
3060
|
-
if (!
|
|
3043
|
+
const pkgPath = path13.join(cwd, "package.json");
|
|
3044
|
+
if (!fs15.existsSync(pkgPath)) {
|
|
3061
3045
|
ctx.output.exitCode = 99;
|
|
3062
3046
|
ctx.output.reason = "release prepare: package.json not found";
|
|
3063
3047
|
return;
|
|
3064
3048
|
}
|
|
3065
|
-
const pkg = JSON.parse(
|
|
3049
|
+
const pkg = JSON.parse(fs15.readFileSync(pkgPath, "utf-8"));
|
|
3066
3050
|
if (typeof pkg.version !== "string") {
|
|
3067
3051
|
ctx.output.exitCode = 99;
|
|
3068
3052
|
ctx.output.reason = "release prepare: package.json has no version";
|
|
@@ -3133,8 +3117,8 @@ Merge this and then run \`kody2 release --mode finalize\`.`;
|
|
|
3133
3117
|
}
|
|
3134
3118
|
async function runFinalize(args) {
|
|
3135
3119
|
const { cwd, dryRun, timeoutMs, releaseCfg, ctx } = args;
|
|
3136
|
-
const pkgPath =
|
|
3137
|
-
const pkg = JSON.parse(
|
|
3120
|
+
const pkgPath = path13.join(cwd, "package.json");
|
|
3121
|
+
const pkg = JSON.parse(fs15.readFileSync(pkgPath, "utf-8"));
|
|
3138
3122
|
if (typeof pkg.version !== "string") {
|
|
3139
3123
|
ctx.output.exitCode = 99;
|
|
3140
3124
|
ctx.output.reason = "release finalize: package.json has no version";
|
|
@@ -3219,31 +3203,17 @@ ${truncate2(r.stderr, 2e3)}
|
|
|
3219
3203
|
|
|
3220
3204
|
// src/scripts/requireFeedbackActions.ts
|
|
3221
3205
|
var MIN_ITEMS = 1;
|
|
3222
|
-
var ACTIONABLE_HEADINGS = /^#{1,6}\s+(Concerns|Suggestions|Bugs)\b/i;
|
|
3223
|
-
var NEXT_HEADING = /^#{1,6}\s+/;
|
|
3224
3206
|
var requireFeedbackActions = async (ctx, profile) => {
|
|
3225
3207
|
if (!ctx.data.agentDone) return;
|
|
3226
3208
|
const actions = String(ctx.data.feedbackActions ?? "").trim();
|
|
3227
3209
|
const items = countActionItems(actions);
|
|
3210
|
+
ctx.data.feedbackAgentItemCount = items;
|
|
3228
3211
|
if (items < MIN_ITEMS) {
|
|
3229
3212
|
fail(
|
|
3230
3213
|
ctx,
|
|
3231
3214
|
profile,
|
|
3232
3215
|
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"
|
|
3233
3216
|
);
|
|
3234
|
-
return;
|
|
3235
|
-
}
|
|
3236
|
-
const reviewBody = String(ctx.data.feedback ?? "");
|
|
3237
|
-
const expectedItems = countActionableReviewBullets(reviewBody);
|
|
3238
|
-
ctx.data.feedbackReviewItemCount = expectedItems;
|
|
3239
|
-
ctx.data.feedbackAgentItemCount = items;
|
|
3240
|
-
if (expectedItems > 0 && items < expectedItems) {
|
|
3241
|
-
fail(
|
|
3242
|
-
ctx,
|
|
3243
|
-
profile,
|
|
3244
|
-
`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`
|
|
3245
|
-
);
|
|
3246
|
-
return;
|
|
3247
3217
|
}
|
|
3248
3218
|
};
|
|
3249
3219
|
function fail(ctx, profile, reason) {
|
|
@@ -3265,25 +3235,6 @@ function countActionItems(block) {
|
|
|
3265
3235
|
}
|
|
3266
3236
|
return count;
|
|
3267
3237
|
}
|
|
3268
|
-
function countActionableReviewBullets(reviewBody) {
|
|
3269
|
-
if (!reviewBody.trim()) return 0;
|
|
3270
|
-
const lines = reviewBody.split("\n");
|
|
3271
|
-
let count = 0;
|
|
3272
|
-
let insideActionable = false;
|
|
3273
|
-
for (const raw of lines) {
|
|
3274
|
-
if (ACTIONABLE_HEADINGS.test(raw)) {
|
|
3275
|
-
insideActionable = true;
|
|
3276
|
-
continue;
|
|
3277
|
-
}
|
|
3278
|
-
if (insideActionable && NEXT_HEADING.test(raw)) {
|
|
3279
|
-
insideActionable = false;
|
|
3280
|
-
continue;
|
|
3281
|
-
}
|
|
3282
|
-
if (!insideActionable) continue;
|
|
3283
|
-
if (/^[-*]\s+\S/.test(raw)) count++;
|
|
3284
|
-
}
|
|
3285
|
-
return count;
|
|
3286
|
-
}
|
|
3287
3238
|
|
|
3288
3239
|
// src/scripts/requirePlanDeviations.ts
|
|
3289
3240
|
var requirePlanDeviations = async (ctx, profile) => {
|
|
@@ -3703,6 +3654,44 @@ function summarizeFeedbackActions(block) {
|
|
|
3703
3654
|
}
|
|
3704
3655
|
return summary;
|
|
3705
3656
|
}
|
|
3657
|
+
function extractReviewFileRefs(reviewBody) {
|
|
3658
|
+
if (!reviewBody) return [];
|
|
3659
|
+
const found = /* @__PURE__ */ new Set();
|
|
3660
|
+
const backtick = /`([^`\s]+\.[a-zA-Z]{1,5})(?::\d+(?:-\d+)?)?`/g;
|
|
3661
|
+
let m;
|
|
3662
|
+
while ((m = backtick.exec(reviewBody)) !== null) {
|
|
3663
|
+
const raw = m[1];
|
|
3664
|
+
if (isPlausibleSourcePath(raw)) found.add(raw);
|
|
3665
|
+
}
|
|
3666
|
+
const bare = /(?<![A-Za-z0-9/_.-])((?:[A-Za-z0-9_./-]+\/)+[A-Za-z0-9_.-]+\.[a-zA-Z]{1,5})(?::\d+(?:-\d+)?)?/g;
|
|
3667
|
+
while ((m = bare.exec(reviewBody)) !== null) {
|
|
3668
|
+
const raw = m[1];
|
|
3669
|
+
if (isPlausibleSourcePath(raw)) found.add(raw);
|
|
3670
|
+
}
|
|
3671
|
+
return Array.from(found);
|
|
3672
|
+
}
|
|
3673
|
+
function isPlausibleSourcePath(p) {
|
|
3674
|
+
if (p.startsWith("http://") || p.startsWith("https://")) return false;
|
|
3675
|
+
if (p.startsWith("//")) return false;
|
|
3676
|
+
if (p.startsWith("/")) return false;
|
|
3677
|
+
if (!p.includes("/")) return false;
|
|
3678
|
+
if (/\.(md|rst|txt|png|jpg|jpeg|gif|svg|pdf)$/i.test(p)) return false;
|
|
3679
|
+
const firstSeg = p.slice(0, p.indexOf("/"));
|
|
3680
|
+
if (firstSeg.includes(".")) return false;
|
|
3681
|
+
return true;
|
|
3682
|
+
}
|
|
3683
|
+
function declinedFileRefs(feedbackActions, refs) {
|
|
3684
|
+
if (!feedbackActions.trim() || refs.length === 0) return /* @__PURE__ */ new Set();
|
|
3685
|
+
const declined = /* @__PURE__ */ new Set();
|
|
3686
|
+
for (const raw of feedbackActions.split("\n")) {
|
|
3687
|
+
if (!/^\s*[-*]\s+/.test(raw)) continue;
|
|
3688
|
+
if (!/\bdeclined\s*:/i.test(raw)) continue;
|
|
3689
|
+
for (const ref of refs) {
|
|
3690
|
+
if (raw.includes(ref)) declined.add(ref);
|
|
3691
|
+
}
|
|
3692
|
+
}
|
|
3693
|
+
return declined;
|
|
3694
|
+
}
|
|
3706
3695
|
function makeAction2(type, payload) {
|
|
3707
3696
|
return { type, payload, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3708
3697
|
}
|
|
@@ -3715,24 +3704,32 @@ var verifyFixAlignment = async (ctx, profile) => {
|
|
|
3715
3704
|
ctx.data.feedbackActionsSummary = summary;
|
|
3716
3705
|
const committed = Boolean(ctx.data.commitResult?.committed);
|
|
3717
3706
|
if (summary.totalItems === 0) {
|
|
3718
|
-
ctx
|
|
3719
|
-
ctx.output.reason = "fix produced no FEEDBACK_ACTIONS items";
|
|
3720
|
-
ctx.data.agentDone = false;
|
|
3721
|
-
ctx.data.action = makeAction2("FIX_FAILED", {
|
|
3722
|
-
reason: ctx.output.reason,
|
|
3723
|
-
feedbackActionsSummary: summary
|
|
3724
|
-
});
|
|
3725
|
-
return;
|
|
3707
|
+
return failOnce(ctx, "FIX_FAILED", "fix produced no FEEDBACK_ACTIONS items", summary);
|
|
3726
3708
|
}
|
|
3727
3709
|
if (summary.fixedItems > 0 && !committed) {
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3710
|
+
return failOnce(
|
|
3711
|
+
ctx,
|
|
3712
|
+
"FIX_FAILED",
|
|
3713
|
+
`fix claimed ${summary.fixedItems} fixed item(s) but produced no commit`,
|
|
3714
|
+
summary
|
|
3715
|
+
);
|
|
3716
|
+
}
|
|
3717
|
+
const reviewBody = ctx.data.feedback ?? "";
|
|
3718
|
+
const refs = extractReviewFileRefs(reviewBody);
|
|
3719
|
+
const changedFiles = (ctx.data.changedFiles ?? []).map((f) => f.trim()).filter(Boolean);
|
|
3720
|
+
ctx.data.reviewFileRefs = refs;
|
|
3721
|
+
if (refs.length > 0 && committed) {
|
|
3722
|
+
const declined = declinedFileRefs(feedbackActions, refs);
|
|
3723
|
+
const missing = refs.filter((r) => !declined.has(r) && !changedFiles.some((f) => filesMatch(f, r)));
|
|
3724
|
+
if (missing.length > 0) {
|
|
3725
|
+
return failOnce(
|
|
3726
|
+
ctx,
|
|
3727
|
+
"FIX_FAILED",
|
|
3728
|
+
`fix did not touch review-named file(s): ${missing.join(", ")} \u2014 address them or mark declined with a reason`,
|
|
3729
|
+
summary,
|
|
3730
|
+
{ missingFiles: missing, declinedFiles: Array.from(declined), changedFiles }
|
|
3731
|
+
);
|
|
3732
|
+
}
|
|
3736
3733
|
}
|
|
3737
3734
|
if (summary.fixedItems === 0 && summary.declinedItems > 0 && !committed) {
|
|
3738
3735
|
ctx.data.action = makeAction2("FIX_DECLINED", {
|
|
@@ -3741,6 +3738,25 @@ var verifyFixAlignment = async (ctx, profile) => {
|
|
|
3741
3738
|
});
|
|
3742
3739
|
}
|
|
3743
3740
|
};
|
|
3741
|
+
function failOnce(ctx, type, reason, summary, extra) {
|
|
3742
|
+
ctx.output.exitCode = 1;
|
|
3743
|
+
ctx.output.reason = reason;
|
|
3744
|
+
ctx.data.agentDone = false;
|
|
3745
|
+
ctx.data.action = makeAction2(type, {
|
|
3746
|
+
reason,
|
|
3747
|
+
feedbackActionsSummary: summary,
|
|
3748
|
+
...extra ?? {}
|
|
3749
|
+
});
|
|
3750
|
+
}
|
|
3751
|
+
function filesMatch(changedPath, reviewRef) {
|
|
3752
|
+
if (changedPath === reviewRef) return true;
|
|
3753
|
+
if (changedPath.endsWith("/" + reviewRef)) return true;
|
|
3754
|
+
if (reviewRef.endsWith("/" + changedPath)) return true;
|
|
3755
|
+
const a = changedPath.split("/");
|
|
3756
|
+
const b = reviewRef.split("/");
|
|
3757
|
+
if (a[a.length - 1] !== b[b.length - 1]) return false;
|
|
3758
|
+
return a.length >= 2 && b.length >= 2 && a[a.length - 2] === b[b.length - 2];
|
|
3759
|
+
}
|
|
3744
3760
|
|
|
3745
3761
|
// src/scripts/watchStalePrsFlow.ts
|
|
3746
3762
|
function readWatchConfig(ctx) {
|
|
@@ -3812,7 +3828,7 @@ var watchStalePrsFlow = async (ctx) => {
|
|
|
3812
3828
|
};
|
|
3813
3829
|
|
|
3814
3830
|
// src/scripts/writeRunSummary.ts
|
|
3815
|
-
import * as
|
|
3831
|
+
import * as fs16 from "fs";
|
|
3816
3832
|
var writeRunSummary = async (ctx, profile) => {
|
|
3817
3833
|
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
3818
3834
|
if (!summaryPath) return;
|
|
@@ -3834,7 +3850,7 @@ var writeRunSummary = async (ctx, profile) => {
|
|
|
3834
3850
|
if (reason) lines.push(`- **Reason:** ${reason}`);
|
|
3835
3851
|
lines.push("");
|
|
3836
3852
|
try {
|
|
3837
|
-
|
|
3853
|
+
fs16.appendFileSync(summaryPath, `${lines.join("\n")}
|
|
3838
3854
|
`);
|
|
3839
3855
|
} catch {
|
|
3840
3856
|
}
|
|
@@ -3982,9 +3998,9 @@ async function runExecutable(profileName, input) {
|
|
|
3982
3998
|
data: {},
|
|
3983
3999
|
output: { exitCode: 0 }
|
|
3984
4000
|
};
|
|
3985
|
-
const ndjsonDir =
|
|
4001
|
+
const ndjsonDir = path14.join(input.cwd, ".kody2");
|
|
3986
4002
|
const invokeAgent = async (prompt) => {
|
|
3987
|
-
const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) =>
|
|
4003
|
+
const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path14.isAbsolute(p) ? p : path14.resolve(profile.dir, p)).filter((p) => p.length > 0);
|
|
3988
4004
|
const syntheticPath = ctx.data.syntheticPluginPath;
|
|
3989
4005
|
const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
|
|
3990
4006
|
return runAgent({
|
|
@@ -4051,17 +4067,17 @@ async function runExecutable(profileName, input) {
|
|
|
4051
4067
|
}
|
|
4052
4068
|
}
|
|
4053
4069
|
function resolveProfilePath(profileName) {
|
|
4054
|
-
const here =
|
|
4070
|
+
const here = path14.dirname(new URL(import.meta.url).pathname);
|
|
4055
4071
|
const candidates = [
|
|
4056
|
-
|
|
4072
|
+
path14.join(here, "executables", profileName, "profile.json"),
|
|
4057
4073
|
// same-dir sibling (dev)
|
|
4058
|
-
|
|
4074
|
+
path14.join(here, "..", "executables", profileName, "profile.json"),
|
|
4059
4075
|
// up one (prod: dist/bin → dist/executables)
|
|
4060
|
-
|
|
4076
|
+
path14.join(here, "..", "src", "executables", profileName, "profile.json")
|
|
4061
4077
|
// fallback
|
|
4062
4078
|
];
|
|
4063
4079
|
for (const c of candidates) {
|
|
4064
|
-
if (
|
|
4080
|
+
if (fs17.existsSync(c)) return c;
|
|
4065
4081
|
}
|
|
4066
4082
|
return candidates[0];
|
|
4067
4083
|
}
|
|
@@ -4237,9 +4253,9 @@ function resolveAuthToken(env = process.env) {
|
|
|
4237
4253
|
return token;
|
|
4238
4254
|
}
|
|
4239
4255
|
function detectPackageManager2(cwd) {
|
|
4240
|
-
if (
|
|
4241
|
-
if (
|
|
4242
|
-
if (
|
|
4256
|
+
if (fs18.existsSync(path15.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
4257
|
+
if (fs18.existsSync(path15.join(cwd, "yarn.lock"))) return "yarn";
|
|
4258
|
+
if (fs18.existsSync(path15.join(cwd, "bun.lockb"))) return "bun";
|
|
4243
4259
|
return "npm";
|
|
4244
4260
|
}
|
|
4245
4261
|
function shellOut(cmd, args, cwd, stream = true) {
|
|
@@ -4319,11 +4335,11 @@ function configureGitIdentity(cwd) {
|
|
|
4319
4335
|
}
|
|
4320
4336
|
function postFailureTail(issueNumber, cwd, reason) {
|
|
4321
4337
|
if (!issueNumber) return;
|
|
4322
|
-
const logPath =
|
|
4338
|
+
const logPath = path15.join(cwd, ".kody2", "last-run.jsonl");
|
|
4323
4339
|
let tail = "";
|
|
4324
4340
|
try {
|
|
4325
|
-
if (
|
|
4326
|
-
const content =
|
|
4341
|
+
if (fs18.existsSync(logPath)) {
|
|
4342
|
+
const content = fs18.readFileSync(logPath, "utf-8");
|
|
4327
4343
|
tail = content.slice(-3e3);
|
|
4328
4344
|
}
|
|
4329
4345
|
} catch {
|
|
@@ -4348,7 +4364,7 @@ async function runCi(argv) {
|
|
|
4348
4364
|
return 0;
|
|
4349
4365
|
}
|
|
4350
4366
|
const args = parseCiArgs(argv);
|
|
4351
|
-
const cwd = args.cwd ?
|
|
4367
|
+
const cwd = args.cwd ? path15.resolve(args.cwd) : process.cwd();
|
|
4352
4368
|
let earlyConfig;
|
|
4353
4369
|
try {
|
|
4354
4370
|
earlyConfig = loadConfig(cwd);
|
|
@@ -4441,23 +4457,23 @@ var DEFAULT_MODEL = "claude/claude-haiku-4-5-20251001";
|
|
|
4441
4457
|
var CHAT_HELP = `kody2 chat \u2014 dashboard-driven chat session
|
|
4442
4458
|
|
|
4443
4459
|
Usage:
|
|
4444
|
-
kody2 chat [--session <id>] [--model <provider/model>]
|
|
4460
|
+
kody2 chat [--session <id>] [--message <text>] [--model <provider/model>]
|
|
4445
4461
|
[--dashboard-url <url>] [--cwd <path>] [--verbose|--quiet]
|
|
4446
4462
|
|
|
4447
|
-
All inputs may also come from env: SESSION_ID, MODEL, DASHBOARD_URL.
|
|
4448
|
-
CLI flags take precedence over env. SESSION_ID
|
|
4449
|
-
(the runner long-polls the dashboard for user turns and pushes events back).
|
|
4463
|
+
All inputs may also come from env: SESSION_ID, INIT_MESSAGE, MODEL, DASHBOARD_URL.
|
|
4464
|
+
CLI flags take precedence over env. SESSION_ID is required.
|
|
4450
4465
|
|
|
4451
4466
|
Exit codes:
|
|
4452
|
-
0
|
|
4453
|
-
64 bad inputs
|
|
4454
|
-
99 runtime failure (agent crash,
|
|
4467
|
+
0 reply emitted successfully
|
|
4468
|
+
64 bad inputs (missing session, empty history)
|
|
4469
|
+
99 runtime failure (agent crash, LiteLLM failure)
|
|
4455
4470
|
`;
|
|
4456
4471
|
function parseChatArgs(argv, env = process.env) {
|
|
4457
4472
|
const result = { errors: [] };
|
|
4458
4473
|
for (let i = 0; i < argv.length; i++) {
|
|
4459
4474
|
const arg = argv[i];
|
|
4460
4475
|
if (arg === "--session") result.sessionId = argv[++i];
|
|
4476
|
+
else if (arg === "--message") result.initMessage = argv[++i];
|
|
4461
4477
|
else if (arg === "--model") result.model = argv[++i];
|
|
4462
4478
|
else if (arg === "--dashboard-url") result.dashboardUrl = argv[++i];
|
|
4463
4479
|
else if (arg === "--cwd") result.cwd = argv[++i];
|
|
@@ -4468,18 +4484,34 @@ function parseChatArgs(argv, env = process.env) {
|
|
|
4468
4484
|
else if (arg) result.errors.push(`unexpected positional: ${arg}`);
|
|
4469
4485
|
}
|
|
4470
4486
|
result.sessionId = result.sessionId ?? env.SESSION_ID ?? void 0;
|
|
4487
|
+
result.initMessage = result.initMessage ?? env.INIT_MESSAGE ?? void 0;
|
|
4471
4488
|
result.model = result.model ?? env.MODEL ?? void 0;
|
|
4472
4489
|
result.dashboardUrl = result.dashboardUrl ?? env.DASHBOARD_URL ?? void 0;
|
|
4473
|
-
for (const key of ["sessionId", "model", "dashboardUrl"]) {
|
|
4490
|
+
for (const key of ["sessionId", "initMessage", "model", "dashboardUrl"]) {
|
|
4474
4491
|
const v = result[key];
|
|
4475
4492
|
if (typeof v === "string" && v.trim() === "") result[key] = void 0;
|
|
4476
4493
|
}
|
|
4477
|
-
if (!result.errors.includes("__HELP__")) {
|
|
4478
|
-
|
|
4479
|
-
if (!result.dashboardUrl) result.errors.push("--dashboard-url <url> (or DASHBOARD_URL env) is required");
|
|
4494
|
+
if (!result.sessionId && !result.errors.includes("__HELP__")) {
|
|
4495
|
+
result.errors.push("--session <id> (or SESSION_ID env) is required");
|
|
4480
4496
|
}
|
|
4481
4497
|
return result;
|
|
4482
4498
|
}
|
|
4499
|
+
function commitChatFiles(cwd, sessionId, verbose) {
|
|
4500
|
+
const sessionFile = path16.relative(cwd, sessionFilePath(cwd, sessionId));
|
|
4501
|
+
const eventsFile = path16.relative(cwd, eventsFilePath(cwd, sessionId));
|
|
4502
|
+
const paths = [sessionFile, eventsFile].filter((p) => fs19.existsSync(path16.join(cwd, p)));
|
|
4503
|
+
if (paths.length === 0) return;
|
|
4504
|
+
const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
|
|
4505
|
+
try {
|
|
4506
|
+
execFileSync16("git", ["add", ...paths], opts);
|
|
4507
|
+
execFileSync16("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
|
|
4508
|
+
execFileSync16("git", ["push", "--quiet", "origin", "HEAD"], opts);
|
|
4509
|
+
} catch (err) {
|
|
4510
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4511
|
+
process.stderr.write(`[kody2:chat] commit/push skipped: ${msg}
|
|
4512
|
+
`);
|
|
4513
|
+
}
|
|
4514
|
+
}
|
|
4483
4515
|
function tryLoadConfig(cwd) {
|
|
4484
4516
|
try {
|
|
4485
4517
|
return loadConfig(cwd);
|
|
@@ -4487,6 +4519,11 @@ function tryLoadConfig(cwd) {
|
|
|
4487
4519
|
return null;
|
|
4488
4520
|
}
|
|
4489
4521
|
}
|
|
4522
|
+
function buildSink(cwd, sessionId, dashboardUrl) {
|
|
4523
|
+
const sinks = [new FileSink(eventsFilePath(cwd, sessionId))];
|
|
4524
|
+
if (dashboardUrl) sinks.push(new HttpSink(dashboardUrl, sessionId));
|
|
4525
|
+
return new TeeSink(sinks);
|
|
4526
|
+
}
|
|
4490
4527
|
async function runChat(argv) {
|
|
4491
4528
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
4492
4529
|
process.stdout.write(CHAT_HELP);
|
|
@@ -4500,9 +4537,8 @@ async function runChat(argv) {
|
|
|
4500
4537
|
${CHAT_HELP}`);
|
|
4501
4538
|
return 64;
|
|
4502
4539
|
}
|
|
4503
|
-
const cwd = args.cwd ?
|
|
4540
|
+
const cwd = args.cwd ? path16.resolve(args.cwd) : process.cwd();
|
|
4504
4541
|
const sessionId = args.sessionId;
|
|
4505
|
-
const dashboardUrl = args.dashboardUrl;
|
|
4506
4542
|
const unpackedSecrets = unpackAllSecrets();
|
|
4507
4543
|
if (unpackedSecrets > 0) {
|
|
4508
4544
|
process.stdout.write(`\u2192 kody2: unpacked ${unpackedSecrets} secret(s) from ALL_SECRETS
|
|
@@ -4528,20 +4564,13 @@ ${CHAT_HELP}`);
|
|
|
4528
4564
|
return 99;
|
|
4529
4565
|
}
|
|
4530
4566
|
}
|
|
4531
|
-
let sink;
|
|
4532
|
-
try {
|
|
4533
|
-
sink = new HttpSink(dashboardUrl, sessionId);
|
|
4534
|
-
} catch (err) {
|
|
4535
|
-
process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}
|
|
4536
|
-
`);
|
|
4537
|
-
return 64;
|
|
4538
|
-
}
|
|
4539
4567
|
let litellm = null;
|
|
4540
4568
|
try {
|
|
4541
4569
|
litellm = await startLitellmIfNeeded(model, cwd);
|
|
4542
4570
|
} catch (err) {
|
|
4543
4571
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4544
|
-
|
|
4572
|
+
const sink2 = buildSink(cwd, sessionId, args.dashboardUrl);
|
|
4573
|
+
await sink2.emit({
|
|
4545
4574
|
event: "chat.error",
|
|
4546
4575
|
payload: { sessionId, error: `litellm startup failed: ${msg}` },
|
|
4547
4576
|
runId: makeRunId(sessionId, "error"),
|
|
@@ -4549,33 +4578,21 @@ ${CHAT_HELP}`);
|
|
|
4549
4578
|
});
|
|
4550
4579
|
return 99;
|
|
4551
4580
|
}
|
|
4552
|
-
|
|
4581
|
+
const sessionFile = sessionFilePath(cwd, sessionId);
|
|
4582
|
+
if (args.initMessage) seedInitialMessage(sessionFile, args.initMessage);
|
|
4583
|
+
const sink = buildSink(cwd, sessionId, args.dashboardUrl);
|
|
4553
4584
|
try {
|
|
4554
|
-
|
|
4555
|
-
} catch (err) {
|
|
4556
|
-
process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}
|
|
4557
|
-
`);
|
|
4558
|
-
try {
|
|
4559
|
-
litellm?.kill();
|
|
4560
|
-
} catch {
|
|
4561
|
-
}
|
|
4562
|
-
return 64;
|
|
4563
|
-
}
|
|
4564
|
-
process.stdout.write(`\u2192 kody2 chat: session ${sessionId}, model ${model.provider}/${model.model}
|
|
4565
|
-
`);
|
|
4566
|
-
try {
|
|
4567
|
-
const result = await runChatSession({
|
|
4585
|
+
const result = await runChatTurn({
|
|
4568
4586
|
sessionId,
|
|
4587
|
+
sessionFile,
|
|
4569
4588
|
cwd,
|
|
4570
4589
|
model,
|
|
4571
4590
|
litellmUrl: litellm?.url ?? null,
|
|
4572
4591
|
sink,
|
|
4573
|
-
pull,
|
|
4574
4592
|
verbose: args.verbose,
|
|
4575
4593
|
quiet: args.quiet
|
|
4576
4594
|
});
|
|
4577
|
-
|
|
4578
|
-
`);
|
|
4595
|
+
commitChatFiles(cwd, sessionId, args.verbose ?? false);
|
|
4579
4596
|
return result.exitCode;
|
|
4580
4597
|
} finally {
|
|
4581
4598
|
try {
|