@kody-ade/kody-engine 0.4.612 → 0.4.617

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -115,6 +115,12 @@ downstream workflows or modify `.github/workflows/*`.
115
115
 
116
116
  The consumer workflow listens on `issue_comment` for `@kody ...` dispatch and `workflow_dispatch` for manual runs and chat mode. Convex owns scheduled Loop wakeups.
117
117
 
118
+ Live chat transcripts remain in Kody's canonical backend. In GitHub Actions,
119
+ the Engine uses the workflow's short-lived OIDC identity to call Dashboard's
120
+ repository-scoped backend endpoint; consumer repositories must not receive
121
+ `CONVEX_URL` or `KODY_SERVICE_KEY`. Local and server runtimes may connect
122
+ directly with those two variables when no GitHub Actions identity exists.
123
+
118
124
  ## Commands
119
125
 
120
126
  ```
package/dist/bin/kody.js CHANGED
@@ -15,9 +15,13 @@ var init_package = __esm({
15
15
  "package.json"() {
16
16
  package_default = {
17
17
  name: "@kody-ade/kody-engine",
18
- version: "0.4.612",
18
+ version: "0.4.617",
19
19
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative implementation profiles.",
20
20
  license: "MIT",
21
+ repository: {
22
+ type: "git",
23
+ url: "git+https://github.com/aharonyaircohen/kody-engine.git"
24
+ },
21
25
  type: "module",
22
26
  bin: {
23
27
  "kody-engine": "dist/bin/kody.js"
@@ -72,10 +76,6 @@ var init_package = __esm({
72
76
  engines: {
73
77
  node: ">=22"
74
78
  },
75
- repository: {
76
- type: "git",
77
- url: "git+https://github.com/aharonyaircohen/kody-engine.git"
78
- },
79
79
  homepage: "https://github.com/aharonyaircohen/kody-engine",
80
80
  bugs: "https://github.com/aharonyaircohen/kody-engine/issues"
81
81
  };
@@ -2322,7 +2322,9 @@ function parseDeliveryConfigAllowlist(raw) {
2322
2322
  if (filePath !== "kody.config.json" || !Array.isArray(paths) || paths.length === 0 || paths.length > 32) {
2323
2323
  throw new Error("contract.json deliveryConfigAllowlist contains an unsupported config file or path list");
2324
2324
  }
2325
- if (!paths.every((value) => typeof value === "string" && /^[A-Za-z][A-Za-z0-9]*(?:\.[A-Za-z][A-Za-z0-9]*)*$/.test(value))) {
2325
+ if (!paths.every(
2326
+ (value) => typeof value === "string" && /^[A-Za-z][A-Za-z0-9]*(?:\.[A-Za-z][A-Za-z0-9]*)*$/.test(value)
2327
+ )) {
2326
2328
  throw new Error("contract.json deliveryConfigAllowlist must contain safe dotted config paths");
2327
2329
  }
2328
2330
  parsed[filePath] = [...new Set(paths)];
@@ -2367,9 +2369,7 @@ function parseCapabilityRequirements(raw) {
2367
2369
  if (raw.githubTestToken !== void 0 && typeof raw.githubTestToken !== "boolean") {
2368
2370
  throw new Error("contract.json requirements.githubTestToken must be boolean");
2369
2371
  }
2370
- if (raw.qaAccountCredentials !== void 0 && (!Array.isArray(raw.qaAccountCredentials) || raw.qaAccountCredentials.length === 0 || !raw.qaAccountCredentials.every(
2371
- (name) => typeof name === "string" && /^[A-Z][A-Z0-9_]{0,127}$/.test(name)
2372
- ))) {
2372
+ if (raw.qaAccountCredentials !== void 0 && (!Array.isArray(raw.qaAccountCredentials) || raw.qaAccountCredentials.length === 0 || !raw.qaAccountCredentials.every((name) => typeof name === "string" && /^[A-Z][A-Z0-9_]{0,127}$/.test(name)))) {
2373
2373
  throw new Error("contract.json requirements.qaAccountCredentials must contain valid credential names");
2374
2374
  }
2375
2375
  if (raw.qaAccountModelSettings !== void 0 && !isPlainObject(raw.qaAccountModelSettings)) {
@@ -6110,6 +6110,7 @@ function loadProfile(profilePath) {
6110
6110
  const STATE_LOADERS = [
6111
6111
  "loadCapabilityState",
6112
6112
  "loadJobFromFile",
6113
+ "loadLiveAgent",
6113
6114
  "runTickScript",
6114
6115
  "runScheduledImplementationTick",
6115
6116
  "runScheduledExecutableTick"
@@ -8909,9 +8910,7 @@ function abortUnfinishedGitOps(cwd) {
8909
8910
  return aborted;
8910
8911
  }
8911
8912
  function isExplicitlyAllowed(p, allowlist) {
8912
- return allowlist.some(
8913
- (entry) => entry.endsWith("/**") ? p.startsWith(entry.slice(0, -2)) : p === entry
8914
- );
8913
+ return allowlist.some((entry) => entry.endsWith("/**") ? p.startsWith(entry.slice(0, -2)) : p === entry);
8915
8914
  }
8916
8915
  function isForbiddenPath(p, deliveryPathAllowlist = []) {
8917
8916
  if (FORBIDDEN_PATH_EXACT.has(p)) return true;
@@ -12713,18 +12712,10 @@ var init_commitAndPush = __esm({
12713
12712
  const deliveryPathAllowlist = Array.isArray(ctx.data.deliveryPathAllowlist) ? ctx.data.deliveryPathAllowlist : [];
12714
12713
  const deliveryConfigAllowlist = ctx.data.deliveryConfigAllowlist && typeof ctx.data.deliveryConfigAllowlist === "object" ? ctx.data.deliveryConfigAllowlist : {};
12715
12714
  try {
12716
- const result2 = commitAndPush(
12717
- branch,
12718
- message,
12719
- ctx.cwd,
12720
- deliveryPathAllowlist,
12721
- deliveryConfigAllowlist
12722
- );
12715
+ const result2 = commitAndPush(branch, message, ctx.cwd, deliveryPathAllowlist, deliveryConfigAllowlist);
12723
12716
  ctx.data.commitResult = result2;
12724
12717
  const postCommitFiles = result2.committed ? listFilesInCommit("HEAD", ctx.cwd) : listChangedFiles(ctx.cwd);
12725
- ctx.data.changedFiles = postCommitFiles.filter(
12726
- (f) => !isForbiddenPath(f, deliveryPathAllowlist)
12727
- );
12718
+ ctx.data.changedFiles = postCommitFiles.filter((f) => !isForbiddenPath(f, deliveryPathAllowlist));
12728
12719
  if (result2.committed && !result2.pushed) {
12729
12720
  const reason = result2.pushError ?? "push failed (no error detail)";
12730
12721
  ctx.data.commitCrash = reason;
@@ -16552,8 +16543,8 @@ var loadLiveAgent;
16552
16543
  var init_loadLiveAgent = __esm({
16553
16544
  "src/scripts/loadLiveAgent.ts"() {
16554
16545
  "use strict";
16555
- init_definition_paths();
16556
16546
  init_agents();
16547
+ init_definition_paths();
16557
16548
  init_state_backend();
16558
16549
  loadLiveAgent = async (ctx, profile) => {
16559
16550
  const agent = String(ctx.args.agent ?? ctx.data.jobAgent ?? "").trim();
@@ -18593,10 +18584,7 @@ function parseSetCookie(value, hostname) {
18593
18584
  const attributes = /* @__PURE__ */ new Map();
18594
18585
  for (const part of parts) {
18595
18586
  const index = part.indexOf("=");
18596
- attributes.set(
18597
- (index < 0 ? part : part.slice(0, index)).toLowerCase(),
18598
- index < 0 ? "" : part.slice(index + 1)
18599
- );
18587
+ attributes.set((index < 0 ? part : part.slice(0, index)).toLowerCase(), index < 0 ? "" : part.slice(index + 1));
18600
18588
  }
18601
18589
  const sameSite = attributes.get("samesite")?.toLowerCase();
18602
18590
  return {
@@ -18721,11 +18709,9 @@ function mergeStorageStates(existingPath, nextPath) {
18721
18709
  }
18722
18710
  origins.set(entry.origin, { origin: entry.origin, localStorage: [...localStorage.values()] });
18723
18711
  }
18724
- fs49.writeFileSync(
18725
- nextPath,
18726
- JSON.stringify({ cookies: [...cookies.values()], origins: [...origins.values()] }),
18727
- { mode: 384 }
18728
- );
18712
+ fs49.writeFileSync(nextPath, JSON.stringify({ cookies: [...cookies.values()], origins: [...origins.values()] }), {
18713
+ mode: 384
18714
+ });
18729
18715
  }
18730
18716
  function configurePlaywright(profile, storageStatePath) {
18731
18717
  const playwright = profile.claudeCode.mcpServers.find((server) => server.name === "playwright");
@@ -20741,12 +20727,19 @@ function buildTickChildEnv(parent, force, loaded) {
20741
20727
  "GH_PAT",
20742
20728
  "GITHUB_TOKEN",
20743
20729
  "GITHUB_REPOSITORY",
20730
+ "GITHUB_REPOSITORY_ID",
20731
+ "GITHUB_REPOSITORY_OWNER_ID",
20744
20732
  "GITHUB_REF",
20745
20733
  "GITHUB_SHA",
20746
20734
  "GITHUB_RUN_ID",
20747
20735
  "GITHUB_RUN_ATTEMPT",
20736
+ "GITHUB_SERVER_URL",
20737
+ "GITHUB_EVENT_NAME",
20748
20738
  "GITHUB_WORKFLOW",
20739
+ "GITHUB_WORKFLOW_REF",
20749
20740
  "GITHUB_ACTIONS",
20741
+ "ACTIONS_ID_TOKEN_REQUEST_URL",
20742
+ "ACTIONS_ID_TOKEN_REQUEST_TOKEN",
20750
20743
  "CI",
20751
20744
  "KODY_DRY_RUN",
20752
20745
  "KODY_NO_COMMIT"
@@ -20980,25 +20973,6 @@ var init_runTickScript = __esm({
20980
20973
  }
20981
20974
  });
20982
20975
 
20983
- // src/scripts/saveManagedGoalState.ts
20984
- var saveManagedGoalState;
20985
- var init_saveManagedGoalState = __esm({
20986
- "src/scripts/saveManagedGoalState.ts"() {
20987
- "use strict";
20988
- init_state2();
20989
- saveManagedGoalState = async (ctx) => {
20990
- ctx.skipAgent = true;
20991
- const goal = ctx.data.goal;
20992
- if (!goal?.raw) return;
20993
- const original = typeof ctx.data.goalOriginalStateText === "string" ? ctx.data.goalOriginalStateText : "";
20994
- const changedBeforeTimestamp = original !== serializeGoalState(goal.raw);
20995
- const updated = changedBeforeTimestamp ? { ...goal.raw, updatedAt: nowIso() } : goal.raw;
20996
- ctx.data.goalPersistState = updated;
20997
- ctx.data.goalPersistChanged = original !== serializeGoalState(updated);
20998
- };
20999
- }
21000
- });
21001
-
21002
20976
  // src/scripts/saveLiveAgentState.ts
21003
20977
  var saveLiveAgentState;
21004
20978
  var init_saveLiveAgentState = __esm({
@@ -21034,6 +21008,25 @@ var init_saveLiveAgentState = __esm({
21034
21008
  }
21035
21009
  });
21036
21010
 
21011
+ // src/scripts/saveManagedGoalState.ts
21012
+ var saveManagedGoalState;
21013
+ var init_saveManagedGoalState = __esm({
21014
+ "src/scripts/saveManagedGoalState.ts"() {
21015
+ "use strict";
21016
+ init_state2();
21017
+ saveManagedGoalState = async (ctx) => {
21018
+ ctx.skipAgent = true;
21019
+ const goal = ctx.data.goal;
21020
+ if (!goal?.raw) return;
21021
+ const original = typeof ctx.data.goalOriginalStateText === "string" ? ctx.data.goalOriginalStateText : "";
21022
+ const changedBeforeTimestamp = original !== serializeGoalState(goal.raw);
21023
+ const updated = changedBeforeTimestamp ? { ...goal.raw, updatedAt: nowIso() } : goal.raw;
21024
+ ctx.data.goalPersistState = updated;
21025
+ ctx.data.goalPersistChanged = original !== serializeGoalState(updated);
21026
+ };
21027
+ }
21028
+ });
21029
+
21037
21030
  // src/scripts/setCommentTarget.ts
21038
21031
  var setCommentTarget;
21039
21032
  var init_setCommentTarget = __esm({
@@ -22382,8 +22375,8 @@ var init_scripts = __esm({
22382
22375
  init_runScheduledImplementationTick();
22383
22376
  init_runSimpleCapabilityScript();
22384
22377
  init_runTickScript();
22385
- init_saveManagedGoalState();
22386
22378
  init_saveLiveAgentState();
22379
+ init_saveManagedGoalState();
22387
22380
  init_saveTaskState();
22388
22381
  init_setCommentTarget();
22389
22382
  init_setLifecycleLabel();
@@ -24229,9 +24222,10 @@ function enforceCapabilityOutputContract(capability, result) {
24229
24222
  if (result.exitCode !== 0 || !schema) {
24230
24223
  return result;
24231
24224
  }
24225
+ const contractOutput = result.capabilityOutput === void 0 && typeof result.prUrl === "string" && result.prUrl.trim().length > 0 ? { prUrl: result.prUrl } : result.capabilityOutput;
24232
24226
  try {
24233
- validateCapabilityContractOutput(schema, result.capabilityOutput);
24234
- return result;
24227
+ validateCapabilityContractOutput(schema, contractOutput);
24228
+ return contractOutput === result.capabilityOutput ? result : { ...result, capabilityOutput: contractOutput };
24235
24229
  } catch (error) {
24236
24230
  const reason = error instanceof Error ? error.message : String(error);
24237
24231
  const blocked = {
@@ -25585,6 +25579,7 @@ function makeRunId(sessionId, suffix) {
25585
25579
  }
25586
25580
 
25587
25581
  // src/chat/session-store.ts
25582
+ init_kody_api_client();
25588
25583
  init_convex_client();
25589
25584
  import { anyApi as anyApi2 } from "convex/server";
25590
25585
  function currentEpochTurns(result) {
@@ -25612,11 +25607,12 @@ function createSessionStore(opts) {
25612
25607
  warn: (message) => process.stderr.write(`[kody:chat:store] ${message}
25613
25608
  `)
25614
25609
  };
25615
- const client = opts.client !== void 0 ? opts.client : createConvexClientFromEnv();
25616
- const tenantId2 = opts.tenantId ?? process.env.GITHUB_REPOSITORY ?? "";
25610
+ const env = opts.env ?? process.env;
25611
+ const client = opts.client !== void 0 ? opts.client : hasGitHubActionsIdentity(env) ? createKodyApiBackendClient(env) : createConvexClientFromEnv(env);
25612
+ const tenantId2 = opts.tenantId ?? env.GITHUB_REPOSITORY ?? "";
25617
25613
  if (!client || !tenantId2) {
25618
25614
  throw new Error(
25619
- "Canonical Convex conversation storage is required (CONVEX_URL, KODY_SERVICE_KEY, and GITHUB_REPOSITORY)"
25615
+ "Canonical conversation storage requires GitHub Actions identity or direct Kody backend credentials"
25620
25616
  );
25621
25617
  }
25622
25618
  logger.info(`conversation ${opts.sessionId}: using canonical Convex store (tenant ${tenantId2})`);
@@ -27194,6 +27190,10 @@ function ensurePackageManagerInstalled(pm, cwd) {
27194
27190
  return shellOut("npm", ["install", "-g", spec], cwd);
27195
27191
  }
27196
27192
  function installDeps(pm, cwd) {
27193
+ if (!fs56.existsSync(path53.join(cwd, "package.json"))) {
27194
+ process.stdout.write("\u2192 kody: no package.json found \u2014 skipping consumer dependency install\n");
27195
+ return 0;
27196
+ }
27197
27197
  const ensureCode = ensurePackageManagerInstalled(pm, cwd);
27198
27198
  if (ensureCode !== 0) return ensureCode;
27199
27199
  const args = {
@@ -29287,8 +29287,137 @@ init_repoWorkspace();
29287
29287
  import * as path58 from "path";
29288
29288
  import { createInterface as createInterface2 } from "readline";
29289
29289
 
29290
+ // src/terminal/brain-terminal-adapters.ts
29291
+ import { spawn as spawn9 } from "child_process";
29292
+ import { createHash as createHash9, randomBytes as randomBytes2 } from "crypto";
29293
+ import { mkdir, readFile, rename, writeFile as writeFile2 } from "fs/promises";
29294
+ import * as path57 from "path";
29295
+ function runTerminalCommand(command, args, input) {
29296
+ return new Promise((resolve24, reject) => {
29297
+ const child = spawn9(command, args, { stdio: ["pipe", "pipe", "pipe"] });
29298
+ const stdout = [];
29299
+ const stderr = [];
29300
+ child.stdout.on("data", (chunk) => stdout.push(chunk));
29301
+ child.stderr.on("data", (chunk) => stderr.push(chunk));
29302
+ child.on("error", reject);
29303
+ child.on("close", (code) => {
29304
+ resolve24({
29305
+ code: code ?? 1,
29306
+ stdout: Buffer.concat(stdout).toString("utf8"),
29307
+ stderr: Buffer.concat(stderr).toString("utf8")
29308
+ });
29309
+ });
29310
+ if (input !== void 0) child.stdin.end(input);
29311
+ else child.stdin.end();
29312
+ });
29313
+ }
29314
+ function storeKey(id) {
29315
+ return createHash9("sha256").update(id).digest("hex");
29316
+ }
29317
+ function isStoredSession(value) {
29318
+ if (!value || typeof value !== "object") return false;
29319
+ const session = value;
29320
+ return session.version === 1 && typeof session.id === "string" && typeof session.sessionName === "string" && typeof session.cwd === "string" && Number.isInteger(session.generation) && Number.isInteger(session.revision) && typeof session.output === "string" && typeof session.scope?.owner === "string" && typeof session.scope?.repo === "string" && typeof session.scope?.conversationId === "string";
29321
+ }
29322
+ var FileBrainTerminalMetadataStore = class {
29323
+ constructor(root) {
29324
+ this.root = root;
29325
+ }
29326
+ root;
29327
+ file(id) {
29328
+ return path57.join(this.root, `${storeKey(id)}.json`);
29329
+ }
29330
+ async read(id) {
29331
+ try {
29332
+ const parsed = JSON.parse(await readFile(this.file(id), "utf8"));
29333
+ if (!isStoredSession(parsed) || parsed.id !== id) {
29334
+ throw new Error("stored terminal session is invalid");
29335
+ }
29336
+ return parsed;
29337
+ } catch (error) {
29338
+ if (error.code === "ENOENT") return null;
29339
+ throw error;
29340
+ }
29341
+ }
29342
+ async write(session) {
29343
+ await mkdir(this.root, { recursive: true, mode: 448 });
29344
+ const target = this.file(session.id);
29345
+ const temporary = `${target}.${process.pid}.${randomBytes2(6).toString("hex")}.tmp`;
29346
+ await writeFile2(temporary, `${JSON.stringify(session)}
29347
+ `, { mode: 384 });
29348
+ await rename(temporary, target);
29349
+ }
29350
+ };
29351
+ function commandError(action, result) {
29352
+ const detail = result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`;
29353
+ return new Error(`tmux ${action} failed: ${detail.slice(0, 500)}`);
29354
+ }
29355
+ var TmuxBrainTerminalRuntime = class {
29356
+ constructor(run = runTerminalCommand) {
29357
+ this.run = run;
29358
+ }
29359
+ run;
29360
+ async tmux(action, args, input) {
29361
+ const result = await this.run("tmux", args, input);
29362
+ if (result.code !== 0) throw commandError(action, result);
29363
+ return result;
29364
+ }
29365
+ async start(sessionName2, cwd, cols, rows) {
29366
+ await this.tmux("start", [
29367
+ "new-session",
29368
+ "-d",
29369
+ "-s",
29370
+ sessionName2,
29371
+ "-x",
29372
+ String(cols),
29373
+ "-y",
29374
+ String(rows),
29375
+ "-c",
29376
+ cwd,
29377
+ "/bin/bash",
29378
+ "-l"
29379
+ ]);
29380
+ await this.tmux("configure", ["set-option", "-t", sessionName2, "status", "off"]);
29381
+ await this.tmux("configure", ["set-option", "-t", sessionName2, "history-limit", "50000"]);
29382
+ await this.tmux("configure", ["set-option", "-w", "-t", sessionName2, "remain-on-exit", "on"]);
29383
+ const inspected = await this.inspect(sessionName2);
29384
+ if (!inspected.alive || inspected.processId === null) throw new Error("tmux terminal did not start");
29385
+ return { processId: inspected.processId };
29386
+ }
29387
+ async inspect(sessionName2) {
29388
+ const result = await this.run("tmux", ["list-panes", "-t", sessionName2, "-F", "#{pane_dead}:#{pane_pid}"]);
29389
+ if (result.code !== 0) return { alive: false, processId: null };
29390
+ const [dead, pid] = result.stdout.trim().split(":");
29391
+ const processId = Number(pid);
29392
+ return {
29393
+ alive: dead === "0" && Number.isInteger(processId) && processId > 0,
29394
+ processId: Number.isInteger(processId) && processId > 0 ? processId : null
29395
+ };
29396
+ }
29397
+ async capture(sessionName2) {
29398
+ const alternate = await this.run("tmux", ["display-message", "-p", "-t", sessionName2, "#{alternate_on}"]);
29399
+ if (alternate.code !== 0) throw commandError("inspect screen", alternate);
29400
+ const args = alternate.stdout.trim() === "1" ? ["capture-pane", "-p", "-e", "-t", sessionName2] : ["capture-pane", "-p", "-e", "-J", "-S", "-50000", "-t", sessionName2];
29401
+ return (await this.tmux("capture", args)).stdout;
29402
+ }
29403
+ async input(sessionName2, data) {
29404
+ const bufferName = `kody_${randomBytes2(8).toString("hex")}`;
29405
+ await this.tmux("load input", ["load-buffer", "-b", bufferName, "-"], data);
29406
+ await this.tmux("paste input", ["paste-buffer", "-d", "-b", bufferName, "-t", sessionName2]);
29407
+ }
29408
+ async resize(sessionName2, cols, rows) {
29409
+ await this.tmux("resize", ["resize-window", "-t", sessionName2, "-x", String(cols), "-y", String(rows)]);
29410
+ }
29411
+ async stop(sessionName2) {
29412
+ const result = await this.run("tmux", ["kill-session", "-t", sessionName2]);
29413
+ if (result.code !== 0 && !/can't find session|no server running/i.test(result.stderr)) {
29414
+ throw commandError("stop", result);
29415
+ }
29416
+ }
29417
+ };
29418
+
29290
29419
  // src/terminal/brain-terminal-session.ts
29291
- import { createHash as createHash9 } from "crypto";
29420
+ import { createHash as createHash10 } from "crypto";
29292
29421
  var MAX_CAPTURE_CHARS = 2e5;
29293
29422
  function requiredIdentifier(value, name, max = 240) {
29294
29423
  if (typeof value !== "string" || !value.trim() || value.length > max) {
@@ -29369,7 +29498,7 @@ function parseBrainTerminalCommand(value) {
29369
29498
  }
29370
29499
  }
29371
29500
  function sessionName(id) {
29372
- return `kody_${createHash9("sha256").update(id).digest("hex").slice(0, 32)}`;
29501
+ return `kody_${createHash10("sha256").update(id).digest("hex").slice(0, 32)}`;
29373
29502
  }
29374
29503
  function stateEvent(session) {
29375
29504
  return {
@@ -29431,12 +29560,7 @@ var BrainTerminalSessionAgent = class {
29431
29560
  await this.persist(session);
29432
29561
  let started;
29433
29562
  try {
29434
- started = await this.dependencies.runtime.start(
29435
- session.sessionName,
29436
- session.cwd,
29437
- session.cols,
29438
- session.rows
29439
- );
29563
+ started = await this.dependencies.runtime.start(session.sessionName, session.cwd, session.cols, session.rows);
29440
29564
  } catch (cause) {
29441
29565
  session = { ...session, state: "failed", updatedAt: this.now() };
29442
29566
  await this.persist(session);
@@ -29595,155 +29719,6 @@ var BrainTerminalSessionAgent = class {
29595
29719
  }
29596
29720
  };
29597
29721
 
29598
- // src/terminal/brain-terminal-adapters.ts
29599
- import { createHash as createHash10, randomBytes as randomBytes2 } from "crypto";
29600
- import { mkdir, readFile, rename, writeFile as writeFile2 } from "fs/promises";
29601
- import * as path57 from "path";
29602
- import { spawn as spawn9 } from "child_process";
29603
- function runTerminalCommand(command, args, input) {
29604
- return new Promise((resolve24, reject) => {
29605
- const child = spawn9(command, args, { stdio: ["pipe", "pipe", "pipe"] });
29606
- const stdout = [];
29607
- const stderr = [];
29608
- child.stdout.on("data", (chunk) => stdout.push(chunk));
29609
- child.stderr.on("data", (chunk) => stderr.push(chunk));
29610
- child.on("error", reject);
29611
- child.on("close", (code) => {
29612
- resolve24({
29613
- code: code ?? 1,
29614
- stdout: Buffer.concat(stdout).toString("utf8"),
29615
- stderr: Buffer.concat(stderr).toString("utf8")
29616
- });
29617
- });
29618
- if (input !== void 0) child.stdin.end(input);
29619
- else child.stdin.end();
29620
- });
29621
- }
29622
- function storeKey(id) {
29623
- return createHash10("sha256").update(id).digest("hex");
29624
- }
29625
- function isStoredSession(value) {
29626
- if (!value || typeof value !== "object") return false;
29627
- const session = value;
29628
- return session.version === 1 && typeof session.id === "string" && typeof session.sessionName === "string" && typeof session.cwd === "string" && Number.isInteger(session.generation) && Number.isInteger(session.revision) && typeof session.output === "string" && typeof session.scope?.owner === "string" && typeof session.scope?.repo === "string" && typeof session.scope?.conversationId === "string";
29629
- }
29630
- var FileBrainTerminalMetadataStore = class {
29631
- constructor(root) {
29632
- this.root = root;
29633
- }
29634
- root;
29635
- file(id) {
29636
- return path57.join(this.root, `${storeKey(id)}.json`);
29637
- }
29638
- async read(id) {
29639
- try {
29640
- const parsed = JSON.parse(await readFile(this.file(id), "utf8"));
29641
- if (!isStoredSession(parsed) || parsed.id !== id) {
29642
- throw new Error("stored terminal session is invalid");
29643
- }
29644
- return parsed;
29645
- } catch (error) {
29646
- if (error.code === "ENOENT") return null;
29647
- throw error;
29648
- }
29649
- }
29650
- async write(session) {
29651
- await mkdir(this.root, { recursive: true, mode: 448 });
29652
- const target = this.file(session.id);
29653
- const temporary = `${target}.${process.pid}.${randomBytes2(6).toString("hex")}.tmp`;
29654
- await writeFile2(temporary, `${JSON.stringify(session)}
29655
- `, { mode: 384 });
29656
- await rename(temporary, target);
29657
- }
29658
- };
29659
- function commandError(action, result) {
29660
- const detail = result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`;
29661
- return new Error(`tmux ${action} failed: ${detail.slice(0, 500)}`);
29662
- }
29663
- var TmuxBrainTerminalRuntime = class {
29664
- constructor(run = runTerminalCommand) {
29665
- this.run = run;
29666
- }
29667
- run;
29668
- async tmux(action, args, input) {
29669
- const result = await this.run("tmux", args, input);
29670
- if (result.code !== 0) throw commandError(action, result);
29671
- return result;
29672
- }
29673
- async start(sessionName2, cwd, cols, rows) {
29674
- await this.tmux("start", [
29675
- "new-session",
29676
- "-d",
29677
- "-s",
29678
- sessionName2,
29679
- "-x",
29680
- String(cols),
29681
- "-y",
29682
- String(rows),
29683
- "-c",
29684
- cwd,
29685
- "/bin/bash",
29686
- "-l"
29687
- ]);
29688
- await this.tmux("configure", ["set-option", "-t", sessionName2, "status", "off"]);
29689
- await this.tmux("configure", ["set-option", "-t", sessionName2, "history-limit", "50000"]);
29690
- await this.tmux("configure", ["set-option", "-w", "-t", sessionName2, "remain-on-exit", "on"]);
29691
- const inspected = await this.inspect(sessionName2);
29692
- if (!inspected.alive || inspected.processId === null) throw new Error("tmux terminal did not start");
29693
- return { processId: inspected.processId };
29694
- }
29695
- async inspect(sessionName2) {
29696
- const result = await this.run("tmux", [
29697
- "list-panes",
29698
- "-t",
29699
- sessionName2,
29700
- "-F",
29701
- "#{pane_dead}:#{pane_pid}"
29702
- ]);
29703
- if (result.code !== 0) return { alive: false, processId: null };
29704
- const [dead, pid] = result.stdout.trim().split(":");
29705
- const processId = Number(pid);
29706
- return {
29707
- alive: dead === "0" && Number.isInteger(processId) && processId > 0,
29708
- processId: Number.isInteger(processId) && processId > 0 ? processId : null
29709
- };
29710
- }
29711
- async capture(sessionName2) {
29712
- const alternate = await this.run("tmux", [
29713
- "display-message",
29714
- "-p",
29715
- "-t",
29716
- sessionName2,
29717
- "#{alternate_on}"
29718
- ]);
29719
- if (alternate.code !== 0) throw commandError("inspect screen", alternate);
29720
- const args = alternate.stdout.trim() === "1" ? ["capture-pane", "-p", "-e", "-t", sessionName2] : ["capture-pane", "-p", "-e", "-J", "-S", "-50000", "-t", sessionName2];
29721
- return (await this.tmux("capture", args)).stdout;
29722
- }
29723
- async input(sessionName2, data) {
29724
- const bufferName = `kody_${randomBytes2(8).toString("hex")}`;
29725
- await this.tmux("load input", ["load-buffer", "-b", bufferName, "-"], data);
29726
- await this.tmux("paste input", ["paste-buffer", "-d", "-b", bufferName, "-t", sessionName2]);
29727
- }
29728
- async resize(sessionName2, cols, rows) {
29729
- await this.tmux("resize", [
29730
- "resize-window",
29731
- "-t",
29732
- sessionName2,
29733
- "-x",
29734
- String(cols),
29735
- "-y",
29736
- String(rows)
29737
- ]);
29738
- }
29739
- async stop(sessionName2) {
29740
- const result = await this.run("tmux", ["kill-session", "-t", sessionName2]);
29741
- if (result.code !== 0 && !/can't find session|no server running/i.test(result.stderr)) {
29742
- throw commandError("stop", result);
29743
- }
29744
- }
29745
- };
29746
-
29747
29722
  // src/servers/brain-terminal-agent.ts
29748
29723
  var DEFAULT_POLL_INTERVAL_MS = 150;
29749
29724
  function writeEvent(output, event) {
@@ -22,6 +22,10 @@
22
22
  "outputSchema": {
23
23
  "type": "object",
24
24
  "properties": {
25
+ "prUrl": {
26
+ "type": "string",
27
+ "description": "Canonical pull request URL produced by the implementation."
28
+ },
25
29
  "reason": {
26
30
  "type": "string"
27
31
  },
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.612",
4
- "description": "kody autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative implementation profiles.",
3
+ "version": "0.4.617",
4
+ "description": "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative implementation profiles.",
5
5
  "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/aharonyaircohen/kody-engine.git"
9
+ },
6
10
  "type": "module",
7
11
  "bin": {
8
12
  "kody-engine": "dist/bin/kody.js"
@@ -57,10 +61,6 @@
57
61
  "engines": {
58
62
  "node": ">=22"
59
63
  },
60
- "repository": {
61
- "type": "git",
62
- "url": "git+https://github.com/aharonyaircohen/kody-engine.git"
63
- },
64
64
  "homepage": "https://github.com/aharonyaircohen/kody-engine",
65
65
  "bugs": "https://github.com/aharonyaircohen/kody-engine/issues"
66
66
  }