icopilot 2.2.0
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/CHANGELOG.md +250 -0
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/bin/icopilot.js +6 -0
- package/dist/acp/router.js +123 -0
- package/dist/acp/schema.js +53 -0
- package/dist/agents/aggregator.js +187 -0
- package/dist/agents/custom-agents.js +97 -0
- package/dist/agents/goal-driven.js +411 -0
- package/dist/agents/multi-repo.js +350 -0
- package/dist/agents/parallel-runner.js +181 -0
- package/dist/agents/router.js +144 -0
- package/dist/agents/self-heal.js +481 -0
- package/dist/agents/tdd-agent.js +278 -0
- package/dist/api/github-models.js +158 -0
- package/dist/bridge/ide-bridge.js +479 -0
- package/dist/cloud/routine-executor.js +34 -0
- package/dist/cloud/routine-scheduler.js +67 -0
- package/dist/cloud/routine-storage.js +297 -0
- package/dist/commands/acp-cmd.js +143 -0
- package/dist/commands/actions-cmd.js +624 -0
- package/dist/commands/agent-cmd.js +144 -0
- package/dist/commands/alias-cmd.js +132 -0
- package/dist/commands/bookmark-cmd.js +77 -0
- package/dist/commands/changelog-cmd.js +99 -0
- package/dist/commands/changes-cmd.js +120 -0
- package/dist/commands/clipboard-cmd.js +217 -0
- package/dist/commands/cloud-routine-cmd.js +265 -0
- package/dist/commands/codegen-cmd.js +544 -0
- package/dist/commands/compare-cmd.js +116 -0
- package/dist/commands/context-cmd.js +247 -0
- package/dist/commands/context-viz-cmd.js +43 -0
- package/dist/commands/conventions-cmd.js +116 -0
- package/dist/commands/cost-cmd.js +51 -0
- package/dist/commands/deps-cmd.js +294 -0
- package/dist/commands/diagram-cmd.js +658 -0
- package/dist/commands/diff-review-cmd.js +92 -0
- package/dist/commands/doc-cmd.js +412 -0
- package/dist/commands/doctor-cmd.js +152 -0
- package/dist/commands/editor-cmd.js +49 -0
- package/dist/commands/env-cmd.js +86 -0
- package/dist/commands/explain-cmd.js +78 -0
- package/dist/commands/explain-shell-cmd.js +22 -0
- package/dist/commands/explore-cmd.js +231 -0
- package/dist/commands/feedback-cmd.js +98 -0
- package/dist/commands/fix-cmd.js +17 -0
- package/dist/commands/generate-cmd.js +38 -0
- package/dist/commands/git-extra.js +197 -0
- package/dist/commands/git-log-cmd.js +98 -0
- package/dist/commands/git-undo-cmd.js +137 -0
- package/dist/commands/git.js +155 -0
- package/dist/commands/history-cmd.js +122 -0
- package/dist/commands/index-cmd.js +65 -0
- package/dist/commands/init-cmd.js +73 -0
- package/dist/commands/lint-cmd.js +133 -0
- package/dist/commands/memory-cmd.js +98 -0
- package/dist/commands/metrics-cmd.js +97 -0
- package/dist/commands/mode-prefix.js +30 -0
- package/dist/commands/multi-cmd.js +44 -0
- package/dist/commands/notify-cmd.js +204 -0
- package/dist/commands/profile-cmd.js +101 -0
- package/dist/commands/prompts.js +17 -0
- package/dist/commands/rag-cmd.js +60 -0
- package/dist/commands/readme-cmd.js +564 -0
- package/dist/commands/reasoning-cmd.js +34 -0
- package/dist/commands/refactor-cmd.js +96 -0
- package/dist/commands/release-cmd.js +450 -0
- package/dist/commands/repo-cmd.js +195 -0
- package/dist/commands/route-cmd.js +21 -0
- package/dist/commands/schedule-cmd.js +109 -0
- package/dist/commands/search-cmd.js +47 -0
- package/dist/commands/security-cmd.js +156 -0
- package/dist/commands/settings-cmd.js +238 -0
- package/dist/commands/skill-cmd.js +338 -0
- package/dist/commands/slash.js +2721 -0
- package/dist/commands/snippets-cmd.js +83 -0
- package/dist/commands/space-cmd.js +92 -0
- package/dist/commands/stash-cmd.js +156 -0
- package/dist/commands/stats-cmd.js +36 -0
- package/dist/commands/style-cmd.js +85 -0
- package/dist/commands/suggest-cmd.js +40 -0
- package/dist/commands/summary-cmd.js +138 -0
- package/dist/commands/task-cmd.js +58 -0
- package/dist/commands/team-memory-cmd.js +97 -0
- package/dist/commands/template-cmd.js +475 -0
- package/dist/commands/test-cmd.js +146 -0
- package/dist/commands/todo-cmd.js +172 -0
- package/dist/commands/tokens-cmd.js +277 -0
- package/dist/commands/trigger-cmd.js +147 -0
- package/dist/commands/undo-cmd.js +18 -0
- package/dist/commands/voice-cmd.js +89 -0
- package/dist/commands/watch-cmd.js +110 -0
- package/dist/commands/web-cmd.js +183 -0
- package/dist/commands/worktree-cmd.js +119 -0
- package/dist/config-profile.js +66 -0
- package/dist/config.js +288 -0
- package/dist/context/compactor.js +53 -0
- package/dist/context/dep-context.js +329 -0
- package/dist/context/file-refs.js +54 -0
- package/dist/context/git-context.js +229 -0
- package/dist/context/image-input.js +66 -0
- package/dist/context/memory.js +55 -0
- package/dist/context/persistent-memory.js +104 -0
- package/dist/context/pinned.js +96 -0
- package/dist/context/priority.js +150 -0
- package/dist/context/read-only.js +48 -0
- package/dist/context/smart-files.js +286 -0
- package/dist/context/team-memory.js +156 -0
- package/dist/extensions/loader.js +149 -0
- package/dist/extensions/marketplace.js +49 -0
- package/dist/extensions/slack-provider.js +181 -0
- package/dist/extensions/team.js +56 -0
- package/dist/extensions/teams-provider.js +222 -0
- package/dist/extensions/voice.js +18 -0
- package/dist/hooks/lifecycle.js +215 -0
- package/dist/hooks/precommit.js +463 -0
- package/dist/index/embeddings.js +23 -0
- package/dist/index/indexer.js +86 -0
- package/dist/index/retrieve.js +20 -0
- package/dist/index/store.js +95 -0
- package/dist/index.js +286 -0
- package/dist/intelligence/dead-code.js +457 -0
- package/dist/intelligence/error-watch.js +263 -0
- package/dist/intelligence/navigation.js +141 -0
- package/dist/intelligence/stack-trace.js +210 -0
- package/dist/intelligence/symbol-index.js +410 -0
- package/dist/knowledge/auto-memory.js +412 -0
- package/dist/knowledge/conventions.js +475 -0
- package/dist/knowledge/corrections.js +213 -0
- package/dist/knowledge/rag.js +450 -0
- package/dist/knowledge/style-learner.js +324 -0
- package/dist/logger.js +35 -0
- package/dist/mcp/client.js +144 -0
- package/dist/mcp/config.js +24 -0
- package/dist/mcp/index.js +89 -0
- package/dist/modes/auto-compact.js +20 -0
- package/dist/modes/autopilot.js +157 -0
- package/dist/modes/background.js +82 -0
- package/dist/modes/interactive.js +187 -0
- package/dist/modes/oneshot.js +36 -0
- package/dist/modes/tui.js +265 -0
- package/dist/modes/turn.js +342 -0
- package/dist/notifications/manager.js +107 -0
- package/dist/plugins/marketplace.js +244 -0
- package/dist/providers/custom-provider.js +298 -0
- package/dist/providers/local-model.js +121 -0
- package/dist/routing/profiles.js +44 -0
- package/dist/routing/router.js +18 -0
- package/dist/sandbox/container.js +151 -0
- package/dist/security/audit.js +237 -0
- package/dist/security/content-filter.js +449 -0
- package/dist/security/proxy.js +301 -0
- package/dist/security/retention.js +281 -0
- package/dist/security/roles.js +252 -0
- package/dist/server/api-server.js +679 -0
- package/dist/session/bookmarks.js +72 -0
- package/dist/session/cloud-session.js +291 -0
- package/dist/session/handoff.js +405 -0
- package/dist/session/manager.js +35 -0
- package/dist/session/session.js +296 -0
- package/dist/session/share.js +313 -0
- package/dist/session/undo-journal.js +91 -0
- package/dist/snippets/store.js +60 -0
- package/dist/spaces/space-config.js +156 -0
- package/dist/spaces/space.js +220 -0
- package/dist/stats/store.js +101 -0
- package/dist/tools/apply-patch.js +134 -0
- package/dist/tools/auto-check.js +218 -0
- package/dist/tools/diff-edit.js +150 -0
- package/dist/tools/diff-prompt.js +36 -0
- package/dist/tools/edit-file.js +66 -0
- package/dist/tools/file-ops.js +205 -0
- package/dist/tools/glob.js +17 -0
- package/dist/tools/grep.js +56 -0
- package/dist/tools/image.js +194 -0
- package/dist/tools/list-directory.js +228 -0
- package/dist/tools/memory.js +17 -0
- package/dist/tools/multi-edit.js +299 -0
- package/dist/tools/policy.js +95 -0
- package/dist/tools/registry.js +484 -0
- package/dist/tools/retry.js +74 -0
- package/dist/tools/run-in-terminal.js +162 -0
- package/dist/tools/safety.js +64 -0
- package/dist/tools/sandbox.js +15 -0
- package/dist/tools/search-symbols.js +212 -0
- package/dist/tools/shell.js +118 -0
- package/dist/tools/web.js +167 -0
- package/dist/ui/prompt.js +37 -0
- package/dist/ui/render.js +96 -0
- package/dist/ui/screen.js +13 -0
- package/dist/ui/theme.js +56 -0
- package/dist/util/browser.js +34 -0
- package/dist/util/completion.js +350 -0
- package/dist/util/cost.js +28 -0
- package/dist/util/keybindings.js +113 -0
- package/dist/util/lazy.js +26 -0
- package/dist/util/perf.js +25 -0
- package/dist/util/token-worker.js +11 -0
- package/dist/util/tokens.js +50 -0
- package/dist/workflows/builtins.js +128 -0
- package/dist/workflows/engine.js +496 -0
- package/dist/workflows/file-trigger.js +197 -0
- package/package.json +79 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
const FRAMEWORKS = {
|
|
5
|
+
vitest: {
|
|
6
|
+
name: 'vitest',
|
|
7
|
+
argsForFile: (testFile, cwd) => ['vitest', 'run', path.relative(cwd, testFile)],
|
|
8
|
+
},
|
|
9
|
+
jest: {
|
|
10
|
+
name: 'jest',
|
|
11
|
+
argsForFile: (testFile, cwd) => ['jest', '--runInBand', path.relative(cwd, testFile)],
|
|
12
|
+
},
|
|
13
|
+
mocha: {
|
|
14
|
+
name: 'mocha',
|
|
15
|
+
argsForFile: (testFile, cwd) => ['mocha', path.relative(cwd, testFile)],
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
export class TDDAgent {
|
|
19
|
+
cwd;
|
|
20
|
+
constructor(cwd = process.cwd()) {
|
|
21
|
+
this.cwd = cwd;
|
|
22
|
+
}
|
|
23
|
+
generateTests(spec) {
|
|
24
|
+
const normalized = normalizeSpec(spec);
|
|
25
|
+
const files = this.planFiles(normalized);
|
|
26
|
+
const framework = this.detectFramework();
|
|
27
|
+
const importPath = toImportPath(files.testFile, files.sourceFile);
|
|
28
|
+
const specLiteral = JSON.stringify(normalized, null, 2);
|
|
29
|
+
const suiteName = `${files.slug} tdd cycle`;
|
|
30
|
+
if (framework === 'jest') {
|
|
31
|
+
return [
|
|
32
|
+
"import { describe, it, expect } from '@jest/globals';",
|
|
33
|
+
`import { buildArtifact } from '${importPath}';`,
|
|
34
|
+
'',
|
|
35
|
+
`const spec = ${specLiteral} as const;`,
|
|
36
|
+
'',
|
|
37
|
+
`describe('${suiteName}', () => {`,
|
|
38
|
+
" it('captures the original description', () => {",
|
|
39
|
+
' expect(buildArtifact().description).toBe(spec.description);',
|
|
40
|
+
' });',
|
|
41
|
+
'',
|
|
42
|
+
" it('retains sample inputs', () => {",
|
|
43
|
+
' expect(buildArtifact().inputExamples).toEqual(spec.inputExamples ?? []);',
|
|
44
|
+
' });',
|
|
45
|
+
'',
|
|
46
|
+
' for (const behavior of spec.expectedBehaviors) {',
|
|
47
|
+
' it(`tracks behavior: ${behavior}`, () => {',
|
|
48
|
+
' expect(buildArtifact().expectedBehaviors).toContain(behavior);',
|
|
49
|
+
' });',
|
|
50
|
+
' }',
|
|
51
|
+
'});',
|
|
52
|
+
'',
|
|
53
|
+
renderSpecComment(normalized),
|
|
54
|
+
].join('\n');
|
|
55
|
+
}
|
|
56
|
+
if (framework === 'mocha') {
|
|
57
|
+
return [
|
|
58
|
+
"import assert from 'node:assert/strict';",
|
|
59
|
+
"import { describe, it } from 'mocha';",
|
|
60
|
+
`import { buildArtifact } from '${importPath}';`,
|
|
61
|
+
'',
|
|
62
|
+
`const spec = ${specLiteral} as const;`,
|
|
63
|
+
'',
|
|
64
|
+
`describe('${suiteName}', () => {`,
|
|
65
|
+
" it('captures the original description', () => {",
|
|
66
|
+
' assert.equal(buildArtifact().description, spec.description);',
|
|
67
|
+
' });',
|
|
68
|
+
'',
|
|
69
|
+
" it('retains sample inputs', () => {",
|
|
70
|
+
' assert.deepEqual(buildArtifact().inputExamples, spec.inputExamples ?? []);',
|
|
71
|
+
' });',
|
|
72
|
+
'',
|
|
73
|
+
' for (const behavior of spec.expectedBehaviors) {',
|
|
74
|
+
' it(`tracks behavior: ${behavior}`, () => {',
|
|
75
|
+
' assert.equal(buildArtifact().expectedBehaviors.includes(behavior), true);',
|
|
76
|
+
' });',
|
|
77
|
+
' }',
|
|
78
|
+
'});',
|
|
79
|
+
'',
|
|
80
|
+
renderSpecComment(normalized),
|
|
81
|
+
].join('\n');
|
|
82
|
+
}
|
|
83
|
+
return [
|
|
84
|
+
"import { describe, it, expect } from 'vitest';",
|
|
85
|
+
`import { buildArtifact } from '${importPath}';`,
|
|
86
|
+
'',
|
|
87
|
+
`const spec = ${specLiteral} as const;`,
|
|
88
|
+
'',
|
|
89
|
+
`describe('${suiteName}', () => {`,
|
|
90
|
+
" it('captures the original description', () => {",
|
|
91
|
+
' expect(buildArtifact().description).toBe(spec.description);',
|
|
92
|
+
' });',
|
|
93
|
+
'',
|
|
94
|
+
" it('retains sample inputs', () => {",
|
|
95
|
+
' expect(buildArtifact().inputExamples).toEqual(spec.inputExamples ?? []);',
|
|
96
|
+
' });',
|
|
97
|
+
'',
|
|
98
|
+
' for (const behavior of spec.expectedBehaviors) {',
|
|
99
|
+
' it(`tracks behavior: ${behavior}`, () => {',
|
|
100
|
+
' expect(buildArtifact().expectedBehaviors).toContain(behavior);',
|
|
101
|
+
' });',
|
|
102
|
+
' }',
|
|
103
|
+
'});',
|
|
104
|
+
'',
|
|
105
|
+
renderSpecComment(normalized),
|
|
106
|
+
].join('\n');
|
|
107
|
+
}
|
|
108
|
+
implement(testFile) {
|
|
109
|
+
const spec = this.readEmbeddedSpec(testFile);
|
|
110
|
+
const specLiteral = JSON.stringify(spec, null, 2);
|
|
111
|
+
return [
|
|
112
|
+
'export interface GeneratedTDDArtifact {',
|
|
113
|
+
' description: string;',
|
|
114
|
+
' inputExamples: string[];',
|
|
115
|
+
' expectedBehaviors: string[];',
|
|
116
|
+
'}',
|
|
117
|
+
'',
|
|
118
|
+
`const artifact = ${specLiteral} as const;`,
|
|
119
|
+
'',
|
|
120
|
+
'export function buildArtifact(): GeneratedTDDArtifact {',
|
|
121
|
+
' return {',
|
|
122
|
+
' description: artifact.description,',
|
|
123
|
+
' inputExamples: [...(artifact.inputExamples ?? [])],',
|
|
124
|
+
' expectedBehaviors: [...artifact.expectedBehaviors],',
|
|
125
|
+
' };',
|
|
126
|
+
'}',
|
|
127
|
+
'',
|
|
128
|
+
'export default buildArtifact;',
|
|
129
|
+
'',
|
|
130
|
+
].join('\n');
|
|
131
|
+
}
|
|
132
|
+
runTests(testFile) {
|
|
133
|
+
const framework = FRAMEWORKS[this.detectFramework()];
|
|
134
|
+
const result = spawnSync('npx', framework.argsForFile(testFile, this.cwd), {
|
|
135
|
+
cwd: this.cwd,
|
|
136
|
+
encoding: 'utf8',
|
|
137
|
+
shell: process.platform === 'win32',
|
|
138
|
+
});
|
|
139
|
+
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`.trim();
|
|
140
|
+
const passed = lastNumberMatch(output, /\b(\d+)\s+passed\b/gi);
|
|
141
|
+
const failed = lastNumberMatch(output, /\b(\d+)\s+failed\b/gi);
|
|
142
|
+
const total = lastNumberMatch(output, /\b(\d+)\s+total\b/gi) ||
|
|
143
|
+
lastNumberMatch(output, /Tests?\s+.*\((\d+)\)/gi) ||
|
|
144
|
+
passed + failed;
|
|
145
|
+
return {
|
|
146
|
+
passed: passed || (result.status === 0 ? total : 0),
|
|
147
|
+
failed: failed || (result.status === 0 ? 0 : total > 0 ? Math.max(total - passed, 1) : 1),
|
|
148
|
+
total: total || passed + failed || (result.status === 0 ? 1 : 1),
|
|
149
|
+
output,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
refactor(src, _test) {
|
|
153
|
+
return `${src
|
|
154
|
+
.split(/\r?\n/)
|
|
155
|
+
.map((line) => line.trimEnd())
|
|
156
|
+
.join('\n')
|
|
157
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
158
|
+
.trimEnd()}\n`;
|
|
159
|
+
}
|
|
160
|
+
fullCycle(spec) {
|
|
161
|
+
const normalized = normalizeSpec(spec);
|
|
162
|
+
const files = this.planFiles(normalized);
|
|
163
|
+
fs.mkdirSync(path.dirname(files.testFile), { recursive: true });
|
|
164
|
+
fs.mkdirSync(path.dirname(files.sourceFile), { recursive: true });
|
|
165
|
+
const testSource = this.generateTests(normalized);
|
|
166
|
+
fs.writeFileSync(files.testFile, testSource, 'utf8');
|
|
167
|
+
let cycles = 1;
|
|
168
|
+
const firstRun = this.runTests(files.testFile);
|
|
169
|
+
if (firstRun.failed > 0 || !fs.existsSync(files.sourceFile)) {
|
|
170
|
+
fs.writeFileSync(files.sourceFile, this.implement(files.testFile), 'utf8');
|
|
171
|
+
cycles += 1;
|
|
172
|
+
}
|
|
173
|
+
let finalRun = this.runTests(files.testFile);
|
|
174
|
+
if (finalRun.failed > 0) {
|
|
175
|
+
const currentSource = fs.readFileSync(files.sourceFile, 'utf8');
|
|
176
|
+
fs.writeFileSync(files.sourceFile, this.refactor(currentSource, testSource), 'utf8');
|
|
177
|
+
cycles += 1;
|
|
178
|
+
finalRun = this.runTests(files.testFile);
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
spec: normalized,
|
|
182
|
+
testFile: files.testFile,
|
|
183
|
+
sourceFile: files.sourceFile,
|
|
184
|
+
cycles,
|
|
185
|
+
finalStatus: finalRun.failed === 0 ? 'green' : 'red',
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
detectFramework() {
|
|
189
|
+
const pkg = this.readPackageJson();
|
|
190
|
+
const testScript = String(pkg?.scripts?.test ?? '').toLowerCase();
|
|
191
|
+
if (testScript.includes('vitest') || this.hasConfigPrefix('vitest.config.'))
|
|
192
|
+
return 'vitest';
|
|
193
|
+
if (testScript.includes('jest') || this.hasConfigPrefix('jest.config.'))
|
|
194
|
+
return 'jest';
|
|
195
|
+
if (testScript.includes('mocha') || this.hasDependency(pkg, 'mocha'))
|
|
196
|
+
return 'mocha';
|
|
197
|
+
if (this.hasDependency(pkg, 'vitest'))
|
|
198
|
+
return 'vitest';
|
|
199
|
+
if (this.hasDependency(pkg, 'jest'))
|
|
200
|
+
return 'jest';
|
|
201
|
+
return 'vitest';
|
|
202
|
+
}
|
|
203
|
+
planFiles(spec) {
|
|
204
|
+
const slug = slugify(spec.description) || 'tdd-cycle';
|
|
205
|
+
return {
|
|
206
|
+
slug,
|
|
207
|
+
testFile: path.join(this.cwd, 'tests', 'tdd', `${slug}.test.ts`),
|
|
208
|
+
sourceFile: path.join(this.cwd, 'src', 'tdd', `${slug}.ts`),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
readEmbeddedSpec(testFile) {
|
|
212
|
+
const testSource = fs.readFileSync(testFile, 'utf8');
|
|
213
|
+
const match = /\/\*\s*TDD_SPEC\s*([\s\S]*?)\*\//.exec(testSource);
|
|
214
|
+
if (!match?.[1]) {
|
|
215
|
+
throw new Error(`Missing embedded TDD spec in ${testFile}`);
|
|
216
|
+
}
|
|
217
|
+
return normalizeSpec(JSON.parse(match[1]));
|
|
218
|
+
}
|
|
219
|
+
readPackageJson() {
|
|
220
|
+
const packagePath = path.join(this.cwd, 'package.json');
|
|
221
|
+
if (!fs.existsSync(packagePath))
|
|
222
|
+
return undefined;
|
|
223
|
+
try {
|
|
224
|
+
return JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
hasDependency(pkg, name) {
|
|
231
|
+
return Boolean(pkg?.devDependencies?.[name] ?? pkg?.dependencies?.[name]);
|
|
232
|
+
}
|
|
233
|
+
hasConfigPrefix(prefix) {
|
|
234
|
+
try {
|
|
235
|
+
return fs.readdirSync(this.cwd).some((entry) => entry.startsWith(prefix));
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function normalizeSpec(spec) {
|
|
243
|
+
const description = spec.description.trim();
|
|
244
|
+
const inputExamples = [
|
|
245
|
+
...new Set((spec.inputExamples ?? []).map((item) => item.trim()).filter(Boolean)),
|
|
246
|
+
];
|
|
247
|
+
const expectedBehaviors = [
|
|
248
|
+
...new Set(spec.expectedBehaviors.map((item) => item.trim()).filter(Boolean)),
|
|
249
|
+
];
|
|
250
|
+
return {
|
|
251
|
+
description,
|
|
252
|
+
...(inputExamples.length > 0 ? { inputExamples } : {}),
|
|
253
|
+
expectedBehaviors: expectedBehaviors.length > 0 ? expectedBehaviors : ['captures the original description'],
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function renderSpecComment(spec) {
|
|
257
|
+
return `/* TDD_SPEC\n${JSON.stringify(spec, null, 2)}\n*/`;
|
|
258
|
+
}
|
|
259
|
+
function toImportPath(fromFile, targetFile) {
|
|
260
|
+
const relative = path.relative(path.dirname(fromFile), targetFile).replace(/\\/g, '/');
|
|
261
|
+
const prefixed = relative.startsWith('.') ? relative : `./${relative}`;
|
|
262
|
+
return prefixed.replace(/\.(?:ts|tsx|js|jsx)$/i, '.js');
|
|
263
|
+
}
|
|
264
|
+
function slugify(value) {
|
|
265
|
+
return value
|
|
266
|
+
.trim()
|
|
267
|
+
.toLowerCase()
|
|
268
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
269
|
+
.replace(/^-+|-+$/g, '');
|
|
270
|
+
}
|
|
271
|
+
function lastNumberMatch(text, pattern) {
|
|
272
|
+
let match = null;
|
|
273
|
+
let last = 0;
|
|
274
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
275
|
+
last = Number(match[1] ?? 0);
|
|
276
|
+
}
|
|
277
|
+
return Number.isFinite(last) ? last : 0;
|
|
278
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import { config } from '../config.js';
|
|
3
|
+
import { providerRegistry, resolveProviderApiKey, } from '../providers/custom-provider.js';
|
|
4
|
+
import { ProxyManager } from '../security/proxy.js';
|
|
5
|
+
import { theme } from '../ui/theme.js';
|
|
6
|
+
let _client = null;
|
|
7
|
+
let _clientCacheKey = null;
|
|
8
|
+
export function client() {
|
|
9
|
+
const provider = activeProvider();
|
|
10
|
+
const proxyConfig = ProxyManager.shared().loadConfig();
|
|
11
|
+
const cacheKey = JSON.stringify({
|
|
12
|
+
provider: config.provider,
|
|
13
|
+
endpoint: config.endpoint,
|
|
14
|
+
token: config.token || resolveProviderApiKey(provider) || 'not-needed',
|
|
15
|
+
headers: provider?.headers || null,
|
|
16
|
+
proxy: proxyConfig,
|
|
17
|
+
});
|
|
18
|
+
if (_client && _clientCacheKey === cacheKey)
|
|
19
|
+
return _client;
|
|
20
|
+
_client = new OpenAI({
|
|
21
|
+
apiKey: config.token || resolveProviderApiKey(provider) || 'not-needed',
|
|
22
|
+
baseURL: config.endpoint || provider?.baseUrl,
|
|
23
|
+
defaultHeaders: provider?.headers,
|
|
24
|
+
httpAgent: ProxyManager.shared().getAgent(),
|
|
25
|
+
});
|
|
26
|
+
_clientCacheKey = cacheKey;
|
|
27
|
+
return _client;
|
|
28
|
+
}
|
|
29
|
+
export function activeProvider() {
|
|
30
|
+
return providerRegistry.get(config.provider) || providerRegistry.getActive();
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Stream a chat completion from GitHub Models, with exponential backoff
|
|
34
|
+
* on HTTP 429. Supports tool/function calling.
|
|
35
|
+
*/
|
|
36
|
+
export async function streamChat(opts) {
|
|
37
|
+
const maxAttempts = 5;
|
|
38
|
+
let attempt = 0;
|
|
39
|
+
let lastErr;
|
|
40
|
+
while (attempt < maxAttempts) {
|
|
41
|
+
try {
|
|
42
|
+
return await runOnce(opts);
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
lastErr = err;
|
|
46
|
+
const status = err?.status ?? err?.response?.status;
|
|
47
|
+
if (err?.name === 'AbortError' || opts.signal?.aborted)
|
|
48
|
+
throw err;
|
|
49
|
+
if (status === 429) {
|
|
50
|
+
const retryAfter = Number(err?.headers?.get?.('retry-after') || err?.response?.headers?.['retry-after'] || 0);
|
|
51
|
+
const wait = retryAfter > 0 ? retryAfter * 1000 : 2 ** attempt * 1500;
|
|
52
|
+
process.stderr.write(theme.warn(`\n⚠ Rate limit (429). Cooling down ${Math.ceil(wait / 1000)}s ` +
|
|
53
|
+
`(attempt ${attempt + 1}/${maxAttempts})…\n`));
|
|
54
|
+
await sleep(wait, opts.signal);
|
|
55
|
+
attempt++;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (status && status >= 500 && status < 600 && attempt < maxAttempts - 1) {
|
|
59
|
+
const wait = 2 ** attempt * 1000;
|
|
60
|
+
process.stderr.write(theme.warn(`\n⚠ Upstream ${status}; retrying in ${wait}ms…\n`));
|
|
61
|
+
await sleep(wait, opts.signal);
|
|
62
|
+
attempt++;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
throw lastErr;
|
|
69
|
+
}
|
|
70
|
+
async function runOnce(opts) {
|
|
71
|
+
const provider = activeProvider();
|
|
72
|
+
const request = {
|
|
73
|
+
model: opts.model,
|
|
74
|
+
messages: opts.messages,
|
|
75
|
+
tools: opts.tools,
|
|
76
|
+
temperature: opts.temperature ?? 0.2,
|
|
77
|
+
...buildChatReasoningParams(opts.model, provider?.maxTokens),
|
|
78
|
+
stream: true,
|
|
79
|
+
};
|
|
80
|
+
const stream = (await client().chat.completions.create(request, {
|
|
81
|
+
signal: opts.signal,
|
|
82
|
+
}));
|
|
83
|
+
let content = '';
|
|
84
|
+
let finishReason = null;
|
|
85
|
+
const toolAcc = {};
|
|
86
|
+
for await (const chunk of stream) {
|
|
87
|
+
const choice = chunk.choices?.[0];
|
|
88
|
+
if (!choice)
|
|
89
|
+
continue;
|
|
90
|
+
const delta = choice.delta || {};
|
|
91
|
+
if (typeof delta.content === 'string' && delta.content.length) {
|
|
92
|
+
content += delta.content;
|
|
93
|
+
opts.onToken(delta.content);
|
|
94
|
+
}
|
|
95
|
+
if (Array.isArray(delta.tool_calls)) {
|
|
96
|
+
for (const tc of delta.tool_calls) {
|
|
97
|
+
const idx = tc.index ?? 0;
|
|
98
|
+
const cur = toolAcc[idx] || (toolAcc[idx] = { id: '', name: '', arguments: '' });
|
|
99
|
+
if (tc.id)
|
|
100
|
+
cur.id = tc.id;
|
|
101
|
+
if (tc.function?.name)
|
|
102
|
+
cur.name = tc.function.name;
|
|
103
|
+
if (tc.function?.arguments)
|
|
104
|
+
cur.arguments += tc.function.arguments;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (choice.finish_reason)
|
|
108
|
+
finishReason = choice.finish_reason;
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
content,
|
|
112
|
+
toolCalls: Object.values(toolAcc).filter((t) => t.name),
|
|
113
|
+
finishReason,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function buildChatReasoningParams(model, providerMaxTokens) {
|
|
117
|
+
const params = {};
|
|
118
|
+
if (supportsReasoningEffort(model) && config.reasoningEffort) {
|
|
119
|
+
params.reasoning_effort = config.reasoningEffort;
|
|
120
|
+
}
|
|
121
|
+
const completionBudget = resolveCompletionBudget(model, providerMaxTokens, config.thinkTokens);
|
|
122
|
+
if (completionBudget !== undefined) {
|
|
123
|
+
params.max_completion_tokens = completionBudget;
|
|
124
|
+
return params;
|
|
125
|
+
}
|
|
126
|
+
if (providerMaxTokens) {
|
|
127
|
+
params.max_tokens = providerMaxTokens;
|
|
128
|
+
}
|
|
129
|
+
return params;
|
|
130
|
+
}
|
|
131
|
+
function resolveCompletionBudget(model, providerMaxTokens, thinkTokens) {
|
|
132
|
+
if (!supportsReasoningTokenBudget(model))
|
|
133
|
+
return undefined;
|
|
134
|
+
const budgets = [providerMaxTokens, thinkTokens].filter((value) => typeof value === 'number' && Number.isFinite(value) && value >= 0);
|
|
135
|
+
if (!budgets.length)
|
|
136
|
+
return undefined;
|
|
137
|
+
return Math.min(...budgets.map((value) => Math.floor(value)));
|
|
138
|
+
}
|
|
139
|
+
function supportsReasoningEffort(model) {
|
|
140
|
+
return /^o\d/i.test(model.trim());
|
|
141
|
+
}
|
|
142
|
+
function supportsReasoningTokenBudget(model) {
|
|
143
|
+
const normalized = model.trim().toLowerCase();
|
|
144
|
+
return /^o\d/.test(normalized) || normalized.startsWith('gpt-5');
|
|
145
|
+
}
|
|
146
|
+
function sleep(ms, signal) {
|
|
147
|
+
return new Promise((resolve, reject) => {
|
|
148
|
+
const t = setTimeout(() => {
|
|
149
|
+
signal?.removeEventListener('abort', onAbort);
|
|
150
|
+
resolve();
|
|
151
|
+
}, ms);
|
|
152
|
+
const onAbort = () => {
|
|
153
|
+
clearTimeout(t);
|
|
154
|
+
reject(new DOMException('Aborted', 'AbortError'));
|
|
155
|
+
};
|
|
156
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
157
|
+
});
|
|
158
|
+
}
|