@kody-ade/kody-engine-lite 0.1.104 → 0.1.106

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.
Files changed (73) hide show
  1. package/dist/bin/cli.js +626 -171
  2. package/kody.config.schema.json +5 -0
  3. package/package.json +1 -1
  4. package/prompts/taskify.md +0 -5
  5. package/templates/kody.yml +30 -103
  6. package/dist/agent-runner.d.ts +0 -4
  7. package/dist/agent-runner.js +0 -122
  8. package/dist/ci/parse-inputs.d.ts +0 -6
  9. package/dist/ci/parse-inputs.js +0 -76
  10. package/dist/ci/parse-safety.d.ts +0 -6
  11. package/dist/ci/parse-safety.js +0 -22
  12. package/dist/cli/args.d.ts +0 -13
  13. package/dist/cli/args.js +0 -42
  14. package/dist/cli/litellm.d.ts +0 -2
  15. package/dist/cli/litellm.js +0 -85
  16. package/dist/cli/task-resolution.d.ts +0 -2
  17. package/dist/cli/task-resolution.js +0 -41
  18. package/dist/config.d.ts +0 -49
  19. package/dist/config.js +0 -72
  20. package/dist/context.d.ts +0 -4
  21. package/dist/context.js +0 -83
  22. package/dist/definitions.d.ts +0 -3
  23. package/dist/definitions.js +0 -59
  24. package/dist/entry.d.ts +0 -1
  25. package/dist/entry.js +0 -236
  26. package/dist/git-utils.d.ts +0 -13
  27. package/dist/git-utils.js +0 -174
  28. package/dist/github-api.d.ts +0 -14
  29. package/dist/github-api.js +0 -114
  30. package/dist/kody-utils.d.ts +0 -1
  31. package/dist/kody-utils.js +0 -9
  32. package/dist/learning/auto-learn.d.ts +0 -2
  33. package/dist/learning/auto-learn.js +0 -169
  34. package/dist/logger.d.ts +0 -14
  35. package/dist/logger.js +0 -51
  36. package/dist/memory.d.ts +0 -1
  37. package/dist/memory.js +0 -20
  38. package/dist/observer.d.ts +0 -9
  39. package/dist/observer.js +0 -80
  40. package/dist/pipeline/complexity.d.ts +0 -3
  41. package/dist/pipeline/complexity.js +0 -12
  42. package/dist/pipeline/executor-registry.d.ts +0 -3
  43. package/dist/pipeline/executor-registry.js +0 -20
  44. package/dist/pipeline/hooks.d.ts +0 -17
  45. package/dist/pipeline/hooks.js +0 -110
  46. package/dist/pipeline/questions.d.ts +0 -2
  47. package/dist/pipeline/questions.js +0 -44
  48. package/dist/pipeline/runner-selection.d.ts +0 -2
  49. package/dist/pipeline/runner-selection.js +0 -13
  50. package/dist/pipeline/state.d.ts +0 -4
  51. package/dist/pipeline/state.js +0 -37
  52. package/dist/pipeline.d.ts +0 -3
  53. package/dist/pipeline.js +0 -213
  54. package/dist/preflight.d.ts +0 -1
  55. package/dist/preflight.js +0 -69
  56. package/dist/retrospective.d.ts +0 -26
  57. package/dist/retrospective.js +0 -211
  58. package/dist/stages/agent.d.ts +0 -2
  59. package/dist/stages/agent.js +0 -94
  60. package/dist/stages/gate.d.ts +0 -2
  61. package/dist/stages/gate.js +0 -32
  62. package/dist/stages/review.d.ts +0 -2
  63. package/dist/stages/review.js +0 -32
  64. package/dist/stages/ship.d.ts +0 -3
  65. package/dist/stages/ship.js +0 -154
  66. package/dist/stages/verify.d.ts +0 -2
  67. package/dist/stages/verify.js +0 -94
  68. package/dist/types.d.ts +0 -61
  69. package/dist/types.js +0 -1
  70. package/dist/validators.d.ts +0 -8
  71. package/dist/validators.js +0 -42
  72. package/dist/verify-runner.d.ts +0 -11
  73. package/dist/verify-runner.js +0 -110
@@ -201,6 +201,11 @@
201
201
  "readyTimeout": {
202
202
  "type": "number",
203
203
  "description": "Seconds to wait for the server to be ready. Default: 30"
204
+ },
205
+ "env": {
206
+ "type": "array",
207
+ "items": { "type": "string" },
208
+ "description": "List of GitHub secret names to forward as environment variables for the dev server process (e.g., ['BLOB_READ_WRITE_TOKEN', 'DATABASE_URL']). These are injected into the workflow env block during 'kody init'."
204
209
  }
205
210
  },
206
211
  "required": ["command", "url"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine-lite",
3
- "version": "0.1.104",
3
+ "version": "0.1.106",
4
4
  "description": "Autonomous SDLC pipeline: Kody orchestration + Claude Code + LiteLLM",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -26,15 +26,10 @@ Required JSON format:
26
26
  "description": "Clear description of what the task requires",
27
27
  "scope": ["list", "of", "exact/file/paths", "affected"],
28
28
  "risk_level": "low | medium | high",
29
- "hasUI": true,
30
29
  "existing_patterns": ["list of existing patterns found that the implementation should reuse"],
31
30
  "questions": []
32
31
  }
33
32
 
34
- hasUI heuristics:
35
- - true: task touches frontend files (.tsx, .jsx, .vue, .svelte, .css, .scss, .html), UI components, pages, layouts, or styles
36
- - false: task is purely backend, CLI, API, database, config, docs, or infrastructure
37
-
38
33
  Risk level heuristics:
39
34
  - low: single file change, no breaking changes, docs, config, isolated scripts, test additions, style changes
40
35
  - medium: multiple files, possible side effects, API changes, new dependencies, refactoring existing logic
@@ -103,107 +103,11 @@ jobs:
103
103
  if: steps.safety.outputs.valid == 'true'
104
104
  id: parse
105
105
  env:
106
- BODY: ${{ github.event.comment.body }}
107
- run: |
108
- # Strip carriage returns — GitHub comments may contain \r\n line endings
109
- BODY=$(printf '%s' "$BODY" | tr -d '\r')
110
-
111
- # Extract: @kody [mode] [task-id] [--from stage]
112
- KODY_ARGS=$(echo "$BODY" | grep -oP '(?:@kody|/kody)\s+\K.*' || echo "")
113
- MODE=$(echo "$KODY_ARGS" | awk '{print $1}')
114
- RAW_TASK_ID=$(echo "$KODY_ARGS" | awk '{print $2}')
115
- # Don't treat flags (--from, --feedback) as task IDs
116
- case "$RAW_TASK_ID" in
117
- --*) TASK_ID="" ;;
118
- *) TASK_ID="$RAW_TASK_ID" ;;
119
- esac
120
- FROM_STAGE=$(echo "$KODY_ARGS" | grep -oP '(?<=--from )\S+' || echo "")
121
- FEEDBACK=$(echo "$KODY_ARGS" | grep -oP '(?<=--feedback ")[^"]*' || echo "")
122
- COMPLEXITY=""
123
- if echo "$KODY_ARGS" | grep -q -- '--complexity'; then
124
- COMPLEXITY=$(echo "$KODY_ARGS" | tr ' ' '\n' | grep -A1 -- '--complexity' | tail -1)
125
- fi
126
- DRY_RUN="false"
127
- if echo "$KODY_ARGS" | grep -q -- '--dry-run'; then
128
- DRY_RUN="true"
129
- fi
130
-
131
- # Validate mode
132
- case "$MODE" in
133
- full|rerun|fix|fix-ci|status|approve|review|resolve|bootstrap) ;;
134
- *)
135
- # If first arg isn't a mode, it might be a task-id or nothing
136
- if [ -n "$MODE" ] && [ "$MODE" != "" ]; then
137
- TASK_ID="$MODE"
138
- fi
139
- MODE="full"
140
- ;;
141
- esac
142
-
143
- ISSUE_NUM="${{ github.event.issue.number }}"
144
-
145
- # For approve mode: extract answer body and convert to rerun
146
- # Must run BEFORE task-id generation so we don't create a new task
147
- # approve: extract answers, convert to rerun
148
- if [ "$MODE" = "approve" ]; then
149
- APPROVE_BODY=$(echo "$BODY" | sed -n '/\(@kody\|\/kody\)\s*approve/,$p' | tail -n +2)
150
- FEEDBACK="$APPROVE_BODY"
151
- MODE="rerun"
152
- fi
153
-
154
- # fix: extract description as feedback, convert to fix command
155
- if [ "$MODE" = "fix" ]; then
156
- FIX_BODY=$(echo "$BODY" | sed -n '/\(@kody\|\/kody\)\s*fix/,$p' | tail -n +2)
157
- if [ -n "$FIX_BODY" ]; then
158
- FEEDBACK="$FIX_BODY"
159
- fi
160
- # Leave TASK_ID empty — entry.ts finds latest task for issue
161
- fi
162
-
163
- # fix-ci: extract body as feedback + CI run ID
164
- if [ "$MODE" = "fix-ci" ]; then
165
- FIX_CI_BODY=$(echo "$BODY" | sed -n '/\(@kody\|\/kody\)\s*fix-ci/,$p' | tail -n +2)
166
- if [ -n "$FIX_CI_BODY" ]; then
167
- FEEDBACK="$FIX_CI_BODY"
168
- fi
169
- CI_RUN_ID=$(echo "$FIX_CI_BODY" | grep -oP 'Run ID:\s*\K\d+' || echo "")
170
- fi
171
-
172
- # Bootstrap mode: set task-id and skip normal pipeline
173
- if [ "$MODE" = "bootstrap" ]; then
174
- TASK_ID="bootstrap-$(date +%y%m%d-%H%M%S)"
175
- fi
176
-
177
- # Auto-generate task-id if not provided (only for full mode)
178
- if [ -z "$TASK_ID" ] && [ "$MODE" = "full" ]; then
179
- TASK_ID="${ISSUE_NUM}-$(date +%y%m%d-%H%M%S)"
180
- fi
181
-
182
- # Detect if this comment is on a PR (PRs are issues in GitHub API)
183
- PR_NUMBER=""
184
- if [ -n "${{ github.event.issue.pull_request }}" ]; then
185
- PR_NUMBER="$ISSUE_NUM"
186
- fi
187
-
188
- # For review mode on a PR comment: use the PR number directly
189
- if [ "$MODE" = "review" ] && [ -n "$PR_NUMBER" ]; then
190
- TASK_ID="review-pr-${PR_NUMBER}-$(date +%y%m%d-%H%M%S)"
191
- fi
192
-
193
- echo "task_id=$TASK_ID" >> $GITHUB_OUTPUT
194
- echo "mode=$MODE" >> $GITHUB_OUTPUT
195
- echo "from_stage=$FROM_STAGE" >> $GITHUB_OUTPUT
196
- echo "issue_number=$ISSUE_NUM" >> $GITHUB_OUTPUT
197
- echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
198
- {
199
- echo "feedback<<KODY_EOF"
200
- echo "$FEEDBACK"
201
- echo "KODY_EOF"
202
- } >> $GITHUB_OUTPUT
203
- echo "complexity=$COMPLEXITY" >> $GITHUB_OUTPUT
204
- echo "ci_run_id=${CI_RUN_ID:-}" >> $GITHUB_OUTPUT
205
- echo "dry_run=$DRY_RUN" >> $GITHUB_OUTPUT
206
- echo "valid=true" >> $GITHUB_OUTPUT
106
+ COMMENT_BODY: ${{ github.event.comment.body }}
107
+ ISSUE_NUMBER: ${{ github.event.issue.number }}
108
+ ISSUE_IS_PR: ${{ github.event.issue.pull_request }}
109
+ TRIGGER_TYPE: comment
110
+ run: kody-engine-lite ci-parse
207
111
 
208
112
  # ─── Orchestrate ─────────────────────────────────────────────────────────────
209
113
  orchestrate:
@@ -262,10 +166,21 @@ jobs:
262
166
  git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
263
167
  git config user.name "github-actions[bot]"
264
168
 
169
+ - name: Export project secrets
170
+ env:
171
+ ALL_SECRETS: ${{ toJSON(secrets) }}
172
+ run: |
173
+ echo "$ALL_SECRETS" | jq -r 'to_entries[] | select(.key | test("^(GITHUB_TOKEN)$") | not) | @json' | while IFS= read -r entry; do
174
+ KEY=$(echo "$entry" | jq -r '.key')
175
+ VALUE=$(echo "$entry" | jq -r '.value')
176
+ DELIM="KODY_EOF_${KEY}"
177
+ echo "${KEY}<<${DELIM}" >> $GITHUB_ENV
178
+ echo "${VALUE}" >> $GITHUB_ENV
179
+ echo "${DELIM}" >> $GITHUB_ENV
180
+ done
181
+
265
182
  - name: Run Kody pipeline
266
183
  env:
267
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
268
- ANTHROPIC_COMPATIBLE_API_KEY: ${{ secrets.ANTHROPIC_COMPATIBLE_API_KEY }}
269
184
  GH_TOKEN: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }}
270
185
  TASK_ID: ${{ github.event.inputs.task_id || needs.parse.outputs.task_id }}
271
186
  MODE: ${{ github.event.inputs.mode || needs.parse.outputs.mode || 'full' }}
@@ -342,10 +257,22 @@ jobs:
342
257
  github.event_name == 'pull_request' &&
343
258
  github.event.pull_request.merged == true
344
259
  runs-on: ubuntu-latest
260
+ permissions:
261
+ issues: write
345
262
  steps:
263
+ - name: Generate App token
264
+ id: app-token
265
+ if: vars.KODY_APP_ID != ''
266
+ uses: actions/create-github-app-token@v1
267
+ with:
268
+ app-id: ${{ vars.KODY_APP_ID }}
269
+ private-key: ${{ secrets.APP_PRIVATE_KEY }}
270
+ repositories: ${{ github.event.repository.name }}
271
+
346
272
  - name: Close linked issue
347
273
  uses: actions/github-script@v7
348
274
  with:
275
+ github-token: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }}
349
276
  script: |
350
277
  // Extract issue number from branch name (e.g. "42--feature-name")
351
278
  const branch = context.payload.pull_request.head.ref;
@@ -1,4 +0,0 @@
1
- import type { AgentRunner } from "./types.js";
2
- import type { KodyConfig } from "./config.js";
3
- export declare function createClaudeCodeRunner(): AgentRunner;
4
- export declare function createRunners(config: KodyConfig): Record<string, AgentRunner>;
@@ -1,122 +0,0 @@
1
- import { spawn, execFileSync } from "child_process";
2
- const SIGKILL_GRACE_MS = 5000;
3
- const STDERR_TAIL_CHARS = 500;
4
- function writeStdin(child, prompt) {
5
- return new Promise((resolve, reject) => {
6
- if (!child.stdin) {
7
- resolve();
8
- return;
9
- }
10
- child.stdin.write(prompt, (err) => {
11
- if (err)
12
- reject(err);
13
- else {
14
- child.stdin.end();
15
- resolve();
16
- }
17
- });
18
- });
19
- }
20
- function waitForProcess(child, timeout) {
21
- return new Promise((resolve) => {
22
- const stdoutChunks = [];
23
- const stderrChunks = [];
24
- child.stdout?.on("data", (chunk) => stdoutChunks.push(chunk));
25
- child.stderr?.on("data", (chunk) => stderrChunks.push(chunk));
26
- const timer = setTimeout(() => {
27
- child.kill("SIGTERM");
28
- setTimeout(() => {
29
- if (!child.killed)
30
- child.kill("SIGKILL");
31
- }, SIGKILL_GRACE_MS);
32
- }, timeout);
33
- child.on("exit", (code) => {
34
- clearTimeout(timer);
35
- resolve({
36
- code,
37
- stdout: Buffer.concat(stdoutChunks).toString(),
38
- stderr: Buffer.concat(stderrChunks).toString(),
39
- });
40
- });
41
- child.on("error", (err) => {
42
- clearTimeout(timer);
43
- resolve({ code: -1, stdout: "", stderr: err.message });
44
- });
45
- });
46
- }
47
- async function runSubprocess(command, args, prompt, timeout, options) {
48
- const child = spawn(command, args, {
49
- cwd: options?.cwd ?? process.cwd(),
50
- env: {
51
- ...process.env,
52
- SKIP_BUILD: "1",
53
- SKIP_HOOKS: "1",
54
- ...options?.env,
55
- },
56
- stdio: ["pipe", "pipe", "pipe"],
57
- });
58
- try {
59
- await writeStdin(child, prompt);
60
- }
61
- catch (err) {
62
- return {
63
- outcome: "failed",
64
- error: `Failed to send prompt: ${err instanceof Error ? err.message : String(err)}`,
65
- };
66
- }
67
- const { code, stdout, stderr } = await waitForProcess(child, timeout);
68
- if (code === 0) {
69
- return { outcome: "completed", output: stdout };
70
- }
71
- return {
72
- outcome: code === null ? "timed_out" : "failed",
73
- error: `Exit code ${code}\n${stderr.slice(-STDERR_TAIL_CHARS)}`,
74
- };
75
- }
76
- function checkCommand(command, args) {
77
- try {
78
- execFileSync(command, args, { timeout: 10_000, stdio: "pipe" });
79
- return true;
80
- }
81
- catch {
82
- return false;
83
- }
84
- }
85
- // ─── Claude Code Runner ──────────────────────────────────────────────────────
86
- export function createClaudeCodeRunner() {
87
- return {
88
- async run(_stageName, prompt, model, timeout, _taskDir, options) {
89
- return runSubprocess("claude", [
90
- "--print",
91
- "--model", model,
92
- "--dangerously-skip-permissions",
93
- "--allowedTools", "Bash,Edit,Read,Write,Glob,Grep",
94
- ], prompt, timeout, options);
95
- },
96
- async healthCheck() {
97
- return checkCommand("claude", ["--version"]);
98
- },
99
- };
100
- }
101
- // ─── Runner Factory ──────────────────────────────────────────────────────────
102
- const RUNNER_FACTORIES = {
103
- "claude-code": createClaudeCodeRunner,
104
- };
105
- export function createRunners(config) {
106
- // New multi-runner config
107
- if (config.agent.runners && Object.keys(config.agent.runners).length > 0) {
108
- const runners = {};
109
- for (const [name, runnerConfig] of Object.entries(config.agent.runners)) {
110
- const factory = RUNNER_FACTORIES[runnerConfig.type];
111
- if (factory) {
112
- runners[name] = factory();
113
- }
114
- }
115
- return runners;
116
- }
117
- // Legacy single-runner fallback
118
- const runnerType = config.agent.runner ?? "claude-code";
119
- const factory = RUNNER_FACTORIES[runnerType];
120
- const defaultName = config.agent.defaultRunner ?? "claude";
121
- return { [defaultName]: factory ? factory() : createClaudeCodeRunner() };
122
- }
@@ -1,6 +0,0 @@
1
- /**
2
- * Parses @kody / /kody comment body into structured inputs.
3
- * Run by the parse job in GitHub Actions.
4
- * Reads from env, writes to $GITHUB_OUTPUT.
5
- */
6
- export {};
@@ -1,76 +0,0 @@
1
- /**
2
- * Parses @kody / /kody comment body into structured inputs.
3
- * Run by the parse job in GitHub Actions.
4
- * Reads from env, writes to $GITHUB_OUTPUT.
5
- */
6
- import * as fs from "fs";
7
- const outputFile = process.env.GITHUB_OUTPUT;
8
- const triggerType = process.env.TRIGGER_TYPE ?? "dispatch";
9
- function output(key, value) {
10
- if (outputFile) {
11
- fs.appendFileSync(outputFile, `${key}=${value}\n`);
12
- }
13
- console.log(`${key}=${value}`);
14
- }
15
- // For workflow_dispatch, pass through inputs
16
- if (triggerType === "dispatch") {
17
- output("task_id", process.env.INPUT_TASK_ID ?? "");
18
- output("mode", process.env.INPUT_MODE ?? "full");
19
- output("from_stage", process.env.INPUT_FROM_STAGE ?? "");
20
- output("issue_number", process.env.INPUT_ISSUE_NUMBER ?? "");
21
- output("feedback", process.env.INPUT_FEEDBACK ?? "");
22
- output("valid", process.env.INPUT_TASK_ID ? "true" : "false");
23
- output("trigger_type", "dispatch");
24
- process.exit(0);
25
- }
26
- // For issue_comment, parse the comment body
27
- const commentBody = process.env.COMMENT_BODY ?? "";
28
- const issueNumber = process.env.ISSUE_NUMBER ?? "";
29
- // Match: @kody [mode] [task-id] [--from stage] [--feedback "text"]
30
- const kodyMatch = commentBody.match(/(?:@kody|\/kody)\s*(.*)/i);
31
- if (!kodyMatch) {
32
- output("valid", "false");
33
- output("trigger_type", "comment");
34
- process.exit(0);
35
- }
36
- const parts = kodyMatch[1].trim().split(/\s+/);
37
- const validModes = ["full", "rerun", "status"];
38
- let mode = "full";
39
- let taskId = "";
40
- let fromStage = "";
41
- let feedback = "";
42
- let i = 0;
43
- // First arg: mode or task-id
44
- if (parts[i] && validModes.includes(parts[i])) {
45
- mode = parts[i];
46
- i++;
47
- }
48
- // Second arg: task-id
49
- if (parts[i] && !parts[i].startsWith("--")) {
50
- taskId = parts[i];
51
- i++;
52
- }
53
- // Named args
54
- while (i < parts.length) {
55
- if (parts[i] === "--from" && parts[i + 1]) {
56
- fromStage = parts[i + 1];
57
- i += 2;
58
- }
59
- else if (parts[i] === "--feedback" && parts[i + 1]) {
60
- // Collect quoted feedback
61
- const rest = parts.slice(i + 1).join(" ");
62
- const quoted = rest.match(/^"([^"]*)"/);
63
- feedback = quoted ? quoted[1] : parts[i + 1];
64
- break;
65
- }
66
- else {
67
- i++;
68
- }
69
- }
70
- output("task_id", taskId);
71
- output("mode", mode);
72
- output("from_stage", fromStage);
73
- output("issue_number", issueNumber);
74
- output("feedback", feedback);
75
- output("valid", taskId ? "true" : "false");
76
- output("trigger_type", "comment");
@@ -1,6 +0,0 @@
1
- /**
2
- * Validates that a comment trigger is safe to execute.
3
- * Run by the parse job in GitHub Actions.
4
- * Reads from env, writes to $GITHUB_OUTPUT.
5
- */
6
- export {};
@@ -1,22 +0,0 @@
1
- /**
2
- * Validates that a comment trigger is safe to execute.
3
- * Run by the parse job in GitHub Actions.
4
- * Reads from env, writes to $GITHUB_OUTPUT.
5
- */
6
- import * as fs from "fs";
7
- const ALLOWED_ASSOCIATIONS = ["COLLABORATOR", "MEMBER", "OWNER"];
8
- const association = process.env.COMMENT_AUTHOR_ASSOCIATION ?? "";
9
- const outputFile = process.env.GITHUB_OUTPUT;
10
- function output(key, value) {
11
- if (outputFile) {
12
- fs.appendFileSync(outputFile, `${key}=${value}\n`);
13
- }
14
- console.log(`${key}=${value}`);
15
- }
16
- if (!ALLOWED_ASSOCIATIONS.includes(association)) {
17
- output("valid", "false");
18
- output("reason", `Author association '${association}' not in allowlist: ${ALLOWED_ASSOCIATIONS.join(", ")}`);
19
- process.exit(0);
20
- }
21
- output("valid", "true");
22
- output("reason", "");
@@ -1,13 +0,0 @@
1
- export interface CliInput {
2
- command: "run" | "rerun" | "fix" | "status";
3
- taskId?: string;
4
- task?: string;
5
- fromStage?: string;
6
- dryRun?: boolean;
7
- cwd?: string;
8
- issueNumber?: number;
9
- feedback?: string;
10
- local?: boolean;
11
- complexity?: "low" | "medium" | "high";
12
- }
13
- export declare function parseArgs(): CliInput;
package/dist/cli/args.js DELETED
@@ -1,42 +0,0 @@
1
- const isCI = !!process.env.GITHUB_ACTIONS;
2
- function getArg(args, flag) {
3
- const idx = args.indexOf(flag);
4
- if (idx !== -1 && args[idx + 1] && !args[idx + 1].startsWith("--")) {
5
- return args[idx + 1];
6
- }
7
- return undefined;
8
- }
9
- function hasFlag(args, flag) {
10
- return args.includes(flag);
11
- }
12
- export function parseArgs() {
13
- const args = process.argv.slice(2);
14
- if (hasFlag(args, "--help") || hasFlag(args, "-h") || args.length === 0) {
15
- console.log(`Usage:
16
- kody run --task-id <id> [--task "<desc>"] [--cwd <path>] [--issue-number <n>] [--complexity low|medium|high] [--feedback "<text>"] [--local] [--dry-run]
17
- kody rerun --task-id <id> --from <stage> [--cwd <path>] [--issue-number <n>]
18
- kody fix --task-id <id> [--cwd <path>] [--issue-number <n>] [--feedback "<text>"]
19
- kody status --task-id <id> [--cwd <path>]
20
- kody --help`);
21
- process.exit(0);
22
- }
23
- const command = args[0];
24
- if (!["run", "rerun", "fix", "status"].includes(command)) {
25
- console.error(`Unknown command: ${command}`);
26
- process.exit(1);
27
- }
28
- const issueStr = getArg(args, "--issue-number") ?? process.env.ISSUE_NUMBER;
29
- const localFlag = hasFlag(args, "--local");
30
- return {
31
- command,
32
- taskId: getArg(args, "--task-id") ?? process.env.TASK_ID,
33
- task: getArg(args, "--task"),
34
- fromStage: getArg(args, "--from") ?? process.env.FROM_STAGE,
35
- dryRun: hasFlag(args, "--dry-run") || process.env.DRY_RUN === "true",
36
- cwd: getArg(args, "--cwd"),
37
- issueNumber: issueStr ? parseInt(issueStr, 10) : undefined,
38
- feedback: getArg(args, "--feedback") ?? process.env.FEEDBACK,
39
- local: localFlag || (!isCI && !hasFlag(args, "--no-local")),
40
- complexity: (getArg(args, "--complexity") ?? process.env.COMPLEXITY),
41
- };
42
- }
@@ -1,2 +0,0 @@
1
- export declare function checkLitellmHealth(url: string): Promise<boolean>;
2
- export declare function tryStartLitellm(url: string, projectDir: string): Promise<ReturnType<typeof import("child_process").spawn> | null>;
@@ -1,85 +0,0 @@
1
- import * as fs from "fs";
2
- import * as path from "path";
3
- import { execFileSync } from "child_process";
4
- import { logger } from "../logger.js";
5
- export async function checkLitellmHealth(url) {
6
- try {
7
- const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3000) });
8
- return response.ok;
9
- }
10
- catch {
11
- return false;
12
- }
13
- }
14
- export async function tryStartLitellm(url, projectDir) {
15
- const configPath = path.join(projectDir, "litellm-config.yaml");
16
- if (!fs.existsSync(configPath)) {
17
- logger.warn("litellm-config.yaml not found — cannot start proxy");
18
- return null;
19
- }
20
- // Extract port from URL
21
- const portMatch = url.match(/:(\d+)/);
22
- const port = portMatch ? portMatch[1] : "4000";
23
- // Check if litellm is installed
24
- try {
25
- execFileSync("litellm", ["--version"], { timeout: 5000, stdio: "pipe" });
26
- }
27
- catch {
28
- try {
29
- execFileSync("python3", ["-m", "litellm", "--version"], { timeout: 5000, stdio: "pipe" });
30
- }
31
- catch {
32
- logger.warn("litellm not installed (pip install 'litellm[proxy]')");
33
- return null;
34
- }
35
- }
36
- logger.info(`Starting LiteLLM proxy on port ${port}...`);
37
- // Determine command
38
- let cmd;
39
- let args;
40
- try {
41
- execFileSync("litellm", ["--version"], { timeout: 5000, stdio: "pipe" });
42
- cmd = "litellm";
43
- args = ["--config", configPath, "--port", port];
44
- }
45
- catch {
46
- cmd = "python3";
47
- args = ["-m", "litellm", "--config", configPath, "--port", port];
48
- }
49
- // Load API key env vars from project .env (only *_API_KEY patterns)
50
- const dotenvPath = path.join(projectDir, ".env");
51
- const dotenvVars = {};
52
- if (fs.existsSync(dotenvPath)) {
53
- for (const line of fs.readFileSync(dotenvPath, "utf-8").split("\n")) {
54
- const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
55
- if (match)
56
- dotenvVars[match[1]] = match[2];
57
- }
58
- if (Object.keys(dotenvVars).length > 0) {
59
- logger.info(` Loaded API keys: ${Object.keys(dotenvVars).join(", ")}`);
60
- }
61
- }
62
- const { spawn } = await import("child_process");
63
- const child = spawn(cmd, args, {
64
- stdio: ["ignore", "pipe", "pipe"],
65
- detached: true,
66
- env: { ...process.env, ...dotenvVars },
67
- });
68
- // Capture stderr for debugging
69
- let proxyStderr = "";
70
- child.stderr?.on("data", (chunk) => { proxyStderr += chunk.toString(); });
71
- // Wait for health
72
- for (let i = 0; i < 30; i++) {
73
- await new Promise((r) => setTimeout(r, 2000));
74
- if (await checkLitellmHealth(url)) {
75
- logger.info(`LiteLLM proxy ready at ${url}`);
76
- return child;
77
- }
78
- }
79
- if (proxyStderr) {
80
- logger.warn(`LiteLLM stderr: ${proxyStderr.slice(-1000)}`);
81
- }
82
- logger.warn("LiteLLM proxy failed to start within 60s");
83
- child.kill();
84
- return null;
85
- }
@@ -1,2 +0,0 @@
1
- export declare function findLatestTaskForIssue(issueNumber: number, projectDir: string): string | null;
2
- export declare function generateTaskId(): string;
@@ -1,41 +0,0 @@
1
- import * as fs from "fs";
2
- import * as path from "path";
3
- import { execFileSync } from "child_process";
4
- export function findLatestTaskForIssue(issueNumber, projectDir) {
5
- const tasksDir = path.join(projectDir, ".tasks");
6
- if (!fs.existsSync(tasksDir))
7
- return null;
8
- // Only consider directories (not files)
9
- const allDirs = fs.readdirSync(tasksDir, { withFileTypes: true })
10
- .filter((d) => d.isDirectory())
11
- .map((d) => d.name)
12
- .sort()
13
- .reverse();
14
- // Direct match: tasks starting with issue number
15
- const prefix = `${issueNumber}-`;
16
- const direct = allDirs.find((d) => d.startsWith(prefix));
17
- if (direct)
18
- return direct;
19
- // Fallback for PR comments: extract issue number from current git branch
20
- // Branch format: <issueNum>--<slug> (e.g., 1031--security-8x-route)
21
- try {
22
- const branch = execFileSync("git", ["branch", "--show-current"], {
23
- encoding: "utf-8", cwd: projectDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
24
- }).trim();
25
- const branchIssueMatch = branch.match(/^(\d+)-/);
26
- if (branchIssueMatch) {
27
- const branchIssueNum = branchIssueMatch[1];
28
- const branchPrefix = `${branchIssueNum}-`;
29
- const fromBranch = allDirs.find((d) => d.startsWith(branchPrefix));
30
- if (fromBranch)
31
- return fromBranch;
32
- }
33
- }
34
- catch { /* ignore */ }
35
- return null;
36
- }
37
- export function generateTaskId() {
38
- const now = new Date();
39
- const pad = (n) => String(n).padStart(2, "0");
40
- return `${String(now.getFullYear()).slice(2)}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
41
- }