kie-ai-cli 1.0.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/dist/code.js ADDED
@@ -0,0 +1,937 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ const DEFAULT_EXCLUDED_DIRS = new Set([
4
+ '.git',
5
+ 'node_modules',
6
+ 'dist',
7
+ '.cursor',
8
+ '.vscode',
9
+ ]);
10
+ function isTextLike(content) {
11
+ return !content.includes('\u0000');
12
+ }
13
+ async function safeReadText(filePath, maxChars) {
14
+ try {
15
+ const raw = await fs.promises.readFile(filePath, 'utf-8');
16
+ if (!isTextLike(raw))
17
+ return '[binary file skipped]';
18
+ if (raw.length <= maxChars)
19
+ return raw;
20
+ return `${raw.slice(0, maxChars)}\n\n... [truncated: ${raw.length - maxChars} chars omitted]`;
21
+ }
22
+ catch (err) {
23
+ const msg = err instanceof Error ? err.message : String(err);
24
+ return `[read failed: ${msg}]`;
25
+ }
26
+ }
27
+ async function walkTree(root, options) {
28
+ const lines = [];
29
+ const excluded = options.excludedDirs ?? DEFAULT_EXCLUDED_DIRS;
30
+ const visit = async (current, relative) => {
31
+ if (lines.length >= options.maxEntries)
32
+ return;
33
+ let entries = [];
34
+ try {
35
+ entries = await fs.promises.readdir(current, { withFileTypes: true });
36
+ }
37
+ catch {
38
+ return;
39
+ }
40
+ entries.sort((a, b) => a.name.localeCompare(b.name));
41
+ for (const entry of entries) {
42
+ if (lines.length >= options.maxEntries)
43
+ return;
44
+ const rel = relative ? path.posix.join(relative, entry.name) : entry.name;
45
+ if (entry.isDirectory()) {
46
+ if (excluded.has(entry.name))
47
+ continue;
48
+ lines.push(`${rel}/`);
49
+ await visit(path.join(current, entry.name), rel);
50
+ }
51
+ else {
52
+ lines.push(rel);
53
+ }
54
+ }
55
+ };
56
+ await visit(root, '');
57
+ return lines;
58
+ }
59
+ async function walkFiles(root, maxEntries) {
60
+ const files = [];
61
+ const visit = async (current, relative) => {
62
+ if (files.length >= maxEntries)
63
+ return;
64
+ let entries = [];
65
+ try {
66
+ entries = await fs.promises.readdir(current, { withFileTypes: true });
67
+ }
68
+ catch {
69
+ return;
70
+ }
71
+ entries.sort((a, b) => a.name.localeCompare(b.name));
72
+ for (const entry of entries) {
73
+ if (files.length >= maxEntries)
74
+ return;
75
+ const rel = relative ? path.posix.join(relative, entry.name) : entry.name;
76
+ if (entry.isDirectory()) {
77
+ if (DEFAULT_EXCLUDED_DIRS.has(entry.name))
78
+ continue;
79
+ await visit(path.join(current, entry.name), rel);
80
+ }
81
+ else {
82
+ files.push(rel);
83
+ }
84
+ }
85
+ };
86
+ await visit(root, '');
87
+ return files;
88
+ }
89
+ export async function buildCodeContext(options) {
90
+ const files = await Promise.all(options.includeFiles.map(async (requestedPath) => {
91
+ const resolvedPath = path.resolve(options.cwd, requestedPath);
92
+ const relPath = path.relative(options.cwd, resolvedPath) || requestedPath;
93
+ const content = await safeReadText(resolvedPath, options.maxFileChars);
94
+ return {
95
+ requestedPath,
96
+ resolvedPath: relPath,
97
+ content,
98
+ };
99
+ }));
100
+ const tree = options.includeTree
101
+ ? (await walkTree(options.cwd, { maxEntries: options.treeMaxEntries })).join('\n')
102
+ : null;
103
+ return { tree, files };
104
+ }
105
+ export function getCodeWorkspaceConfigPath(cwd) {
106
+ return path.join(cwd, '.kie', 'code.json');
107
+ }
108
+ function safeWeight(value, fallback = 1) {
109
+ const n = typeof value === 'number' ? value : Number(value);
110
+ if (!Number.isFinite(n) || n <= 0)
111
+ return fallback;
112
+ return Math.max(0.05, Number(n.toFixed(3)));
113
+ }
114
+ export function buildDefaultRoutingConfig(defaultModel = 'claude-opus-4-7') {
115
+ return {
116
+ strategy: 'weighted-random',
117
+ phases: {
118
+ plan: [{ model: defaultModel, weight: 1 }],
119
+ execute: [{ model: defaultModel, weight: 1 }],
120
+ fix: [{ model: defaultModel, weight: 1 }],
121
+ },
122
+ };
123
+ }
124
+ function normalizeRoutingConfig(input, fallbackModel) {
125
+ const defaultConfig = buildDefaultRoutingConfig(fallbackModel);
126
+ const strategy = input?.strategy === 'argmax' || input?.strategy === 'weighted-random'
127
+ ? input.strategy
128
+ : defaultConfig.strategy;
129
+ const normalizePhase = (phase) => {
130
+ const raw = input?.phases?.[phase];
131
+ if (!Array.isArray(raw) || raw.length === 0)
132
+ return defaultConfig.phases[phase];
133
+ const cleaned = raw
134
+ .map((item) => ({
135
+ model: typeof item?.model === 'string' ? item.model.trim() : '',
136
+ weight: safeWeight(item?.weight, 1),
137
+ }))
138
+ .filter((item) => item.model.length > 0);
139
+ return cleaned.length > 0 ? cleaned : defaultConfig.phases[phase];
140
+ };
141
+ return {
142
+ strategy,
143
+ phases: {
144
+ plan: normalizePhase('plan'),
145
+ execute: normalizePhase('execute'),
146
+ fix: normalizePhase('fix'),
147
+ },
148
+ };
149
+ }
150
+ export async function initCodeWorkspace(cwd, defaults) {
151
+ const configPath = getCodeWorkspaceConfigPath(cwd);
152
+ const dir = path.dirname(configPath);
153
+ const config = {
154
+ version: 1,
155
+ createdAt: new Date().toISOString(),
156
+ root: cwd,
157
+ includeTree: defaults?.includeTree ?? true,
158
+ treeLimit: defaults?.treeLimit ?? 120,
159
+ fileMaxChars: defaults?.fileMaxChars ?? 8000,
160
+ defaultModel: defaults?.defaultModel,
161
+ defaultRoute: defaults?.defaultRoute ?? 'auto',
162
+ routing: normalizeRoutingConfig(defaults?.routing, defaults?.defaultModel || 'claude-opus-4-7'),
163
+ };
164
+ const exists = await fs.promises.stat(configPath).then(() => true).catch(() => false);
165
+ if (exists) {
166
+ const loaded = await loadCodeWorkspaceConfig(cwd);
167
+ return { created: false, configPath, config: loaded ?? config };
168
+ }
169
+ await fs.promises.mkdir(dir, { recursive: true });
170
+ await fs.promises.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
171
+ return { created: true, configPath, config };
172
+ }
173
+ export async function loadCodeWorkspaceConfig(cwd) {
174
+ const configPath = getCodeWorkspaceConfigPath(cwd);
175
+ try {
176
+ const raw = await fs.promises.readFile(configPath, 'utf-8');
177
+ const parsed = JSON.parse(raw);
178
+ if (!parsed || typeof parsed !== 'object')
179
+ return null;
180
+ return {
181
+ version: 1,
182
+ createdAt: typeof parsed.createdAt === 'string' ? parsed.createdAt : new Date().toISOString(),
183
+ root: typeof parsed.root === 'string' ? parsed.root : cwd,
184
+ includeTree: typeof parsed.includeTree === 'boolean' ? parsed.includeTree : true,
185
+ treeLimit: typeof parsed.treeLimit === 'number' && parsed.treeLimit > 0 ? parsed.treeLimit : 120,
186
+ fileMaxChars: typeof parsed.fileMaxChars === 'number' && parsed.fileMaxChars > 0
187
+ ? parsed.fileMaxChars
188
+ : 8000,
189
+ defaultModel: typeof parsed.defaultModel === 'string' ? parsed.defaultModel : undefined,
190
+ defaultRoute: resolveCodeRoute(parsed.defaultRoute),
191
+ routing: normalizeRoutingConfig(parsed.routing, typeof parsed.defaultModel === 'string' ? parsed.defaultModel : 'claude-opus-4-7'),
192
+ };
193
+ }
194
+ catch {
195
+ return null;
196
+ }
197
+ }
198
+ export async function saveCodeWorkspaceConfig(cwd, config) {
199
+ const filePath = getCodeWorkspaceConfigPath(cwd);
200
+ await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
201
+ const next = {
202
+ ...config,
203
+ defaultRoute: resolveCodeRoute(config.defaultRoute),
204
+ routing: normalizeRoutingConfig(config.routing, config.defaultModel || 'claude-opus-4-7'),
205
+ };
206
+ await fs.promises.writeFile(filePath, JSON.stringify(next, null, 2), 'utf-8');
207
+ }
208
+ export function resolveCodeRoute(input) {
209
+ const normalized = (input || '').trim().toLowerCase();
210
+ if (normalized === 'quality' || normalized === 'balanced' || normalized === 'fast') {
211
+ return normalized;
212
+ }
213
+ return 'auto';
214
+ }
215
+ function chooseExistingModel(candidates, fallback) {
216
+ for (const model of candidates) {
217
+ if (model)
218
+ return model;
219
+ }
220
+ return fallback;
221
+ }
222
+ export function pickModelByRoute(route, fallbackModel, phase) {
223
+ if (route === 'quality') {
224
+ return chooseExistingModel(['claude-opus-4-7', 'claude-opus-4-6', fallbackModel], fallbackModel);
225
+ }
226
+ if (route === 'balanced') {
227
+ return chooseExistingModel(['claude-sonnet-4-6', 'claude-sonnet-4-5', fallbackModel], fallbackModel);
228
+ }
229
+ if (route === 'fast') {
230
+ return chooseExistingModel(['claude-haiku-4-5', fallbackModel], fallbackModel);
231
+ }
232
+ if (phase === 'fix') {
233
+ return chooseExistingModel(['claude-sonnet-4-6', 'claude-sonnet-4-5', fallbackModel], fallbackModel);
234
+ }
235
+ if (phase === 'execute') {
236
+ return chooseExistingModel(['claude-opus-4-7', 'claude-sonnet-4-6', fallbackModel], fallbackModel);
237
+ }
238
+ return chooseExistingModel(['claude-opus-4-7', fallbackModel], fallbackModel);
239
+ }
240
+ export function getCodeModelMetricsPath(cwd) {
241
+ return path.join(cwd, '.kie', 'model-metrics.json');
242
+ }
243
+ export async function loadCodeModelMetrics(cwd) {
244
+ const filePath = getCodeModelMetricsPath(cwd);
245
+ try {
246
+ const raw = await fs.promises.readFile(filePath, 'utf-8');
247
+ const parsed = JSON.parse(raw);
248
+ if (!parsed || typeof parsed !== 'object' || !parsed.metrics) {
249
+ return { version: 1, updatedAt: new Date().toISOString(), metrics: {} };
250
+ }
251
+ return {
252
+ version: 1,
253
+ updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : new Date().toISOString(),
254
+ metrics: parsed.metrics,
255
+ };
256
+ }
257
+ catch {
258
+ return { version: 1, updatedAt: new Date().toISOString(), metrics: {} };
259
+ }
260
+ }
261
+ export async function saveCodeModelMetrics(cwd, data) {
262
+ const filePath = getCodeModelMetricsPath(cwd);
263
+ await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
264
+ await fs.promises.writeFile(filePath, JSON.stringify({ ...data, updatedAt: new Date().toISOString() }, null, 2), 'utf-8');
265
+ }
266
+ export function recordCodeModelMetric(data, model, success, latencyMs) {
267
+ const current = data.metrics[model] || {
268
+ model,
269
+ runs: 0,
270
+ success: 0,
271
+ fail: 0,
272
+ avgLatencyMs: 0,
273
+ };
274
+ current.runs += 1;
275
+ if (success)
276
+ current.success += 1;
277
+ else
278
+ current.fail += 1;
279
+ const boundedLatency = Math.max(1, Math.round(latencyMs));
280
+ current.avgLatencyMs =
281
+ current.runs === 1
282
+ ? boundedLatency
283
+ : Math.round((current.avgLatencyMs * (current.runs - 1) + boundedLatency) / current.runs);
284
+ data.metrics[model] = current;
285
+ data.updatedAt = new Date().toISOString();
286
+ }
287
+ function computeModelScore(entry, metric) {
288
+ const baseWeight = safeWeight(entry.weight, 1);
289
+ if (!metric || metric.runs === 0)
290
+ return baseWeight;
291
+ const successRate = metric.success / Math.max(1, metric.runs);
292
+ const successFactor = 0.7 + successRate * 0.6;
293
+ const latencyFactor = metric.avgLatencyMs <= 2500 ? 1 : Math.max(0.6, 2500 / metric.avgLatencyMs);
294
+ return Number((baseWeight * successFactor * latencyFactor).toFixed(4));
295
+ }
296
+ function weightedPick(items) {
297
+ const total = items.reduce((sum, item) => sum + Math.max(0, item.score), 0);
298
+ if (total <= 0)
299
+ return items[0]?.model || '';
300
+ const target = Math.random() * total;
301
+ let acc = 0;
302
+ for (const item of items) {
303
+ acc += Math.max(0, item.score);
304
+ if (target <= acc)
305
+ return item.model;
306
+ }
307
+ return items[items.length - 1]?.model || '';
308
+ }
309
+ export function selectModelFromRouting(route, fallbackModel, phase, routing, metrics) {
310
+ if (route !== 'auto') {
311
+ const picked = pickModelByRoute(route, fallbackModel, phase);
312
+ return { model: picked, scored: [{ model: picked, score: 1, baseWeight: 1 }] };
313
+ }
314
+ const pool = routing.phases[phase] || [];
315
+ const scored = pool
316
+ .map((entry) => ({
317
+ model: entry.model,
318
+ score: computeModelScore(entry, metrics.metrics[entry.model]),
319
+ baseWeight: safeWeight(entry.weight, 1),
320
+ }))
321
+ .filter((item) => item.model);
322
+ if (scored.length === 0) {
323
+ return { model: fallbackModel, scored: [{ model: fallbackModel, score: 1, baseWeight: 1 }] };
324
+ }
325
+ const model = routing.strategy === 'argmax'
326
+ ? [...scored].sort((a, b) => b.score - a.score)[0].model
327
+ : weightedPick(scored);
328
+ return { model, scored };
329
+ }
330
+ export function upsertWeightedModel(routing, phase, model, weight) {
331
+ const trimmedModel = model.trim();
332
+ const next = normalizeRoutingConfig(routing, trimmedModel || 'claude-opus-4-7');
333
+ const list = next.phases[phase].filter((item) => item.model !== trimmedModel);
334
+ list.push({ model: trimmedModel, weight: safeWeight(weight, 1) });
335
+ next.phases[phase] = list;
336
+ return next;
337
+ }
338
+ export function removeWeightedModel(routing, phase, model) {
339
+ const next = normalizeRoutingConfig(routing, 'claude-opus-4-7');
340
+ next.phases[phase] = next.phases[phase].filter((item) => item.model !== model.trim());
341
+ if (next.phases[phase].length === 0) {
342
+ next.phases[phase] = [{ model: 'claude-opus-4-7', weight: 1 }];
343
+ }
344
+ return next;
345
+ }
346
+ export function getCodeWorkspaceProfilePath(cwd) {
347
+ return path.join(cwd, '.kie', 'workspace.json');
348
+ }
349
+ async function detectPackageManager(cwd) {
350
+ const tryStat = async (p) => { try {
351
+ await fs.promises.stat(p);
352
+ return true;
353
+ }
354
+ catch {
355
+ return false;
356
+ } };
357
+ if (await tryStat(path.join(cwd, 'pnpm-lock.yaml')))
358
+ return 'pnpm';
359
+ if (await tryStat(path.join(cwd, 'yarn.lock')))
360
+ return 'yarn';
361
+ if (await tryStat(path.join(cwd, 'bun.lockb')) || await tryStat(path.join(cwd, 'bun.lock')))
362
+ return 'bun';
363
+ if (await tryStat(path.join(cwd, 'package-lock.json')))
364
+ return 'npm';
365
+ return 'unknown';
366
+ }
367
+ async function readPackageJsonInfo(cwd) {
368
+ try {
369
+ const raw = await fs.promises.readFile(path.join(cwd, 'package.json'), 'utf-8');
370
+ const p = JSON.parse(raw);
371
+ const deps = Object.keys(p.dependencies || {});
372
+ const devDeps = Object.keys(p.devDependencies || {});
373
+ return {
374
+ name: p.name || null,
375
+ description: p.description || null,
376
+ scripts: Object.keys(p.scripts || {}),
377
+ scriptDetails: (p.scripts || {}),
378
+ dependencies: deps,
379
+ devDependencies: devDeps,
380
+ allDeps: new Set([...deps, ...devDeps]),
381
+ main: p.main || null,
382
+ bin: typeof p.bin === 'string' ? [p.bin] : Object.values(p.bin || {}),
383
+ engines: (p.engines || {}),
384
+ };
385
+ }
386
+ catch {
387
+ return { name: null, description: null, scripts: [], scriptDetails: {}, dependencies: [], devDependencies: [], allDeps: new Set(), main: null, bin: [], engines: {} };
388
+ }
389
+ }
390
+ function detectFrameworks(allDeps, topFiles) {
391
+ const r = [];
392
+ const check = (dep, label) => { if (allDeps.has(dep))
393
+ r.push(label); };
394
+ check('next', 'Next.js');
395
+ check('nuxt', 'Nuxt');
396
+ check('react', 'React');
397
+ check('vue', 'Vue');
398
+ check('@angular/core', 'Angular');
399
+ check('svelte', 'Svelte');
400
+ check('express', 'Express');
401
+ check('fastify', 'Fastify');
402
+ check('koa', 'Koa');
403
+ check('@nestjs/core', 'NestJS');
404
+ check('electron', 'Electron');
405
+ check('prisma', 'Prisma');
406
+ check('@prisma/client', 'Prisma');
407
+ check('mongoose', 'Mongoose');
408
+ check('typeorm', 'TypeORM');
409
+ check('sequelize', 'Sequelize');
410
+ check('tailwindcss', 'Tailwind CSS');
411
+ check('styled-components', 'Styled Components');
412
+ check('redux', 'Redux');
413
+ check('@reduxjs/toolkit', 'Redux Toolkit');
414
+ check('zustand', 'Zustand');
415
+ check('mobx', 'MobX');
416
+ check('socket.io', 'Socket.IO');
417
+ check('graphql', 'GraphQL');
418
+ check('trpc', 'tRPC');
419
+ check('@trpc/server', 'tRPC');
420
+ check('commander', 'Commander');
421
+ check('inquirer', 'Inquirer');
422
+ check('three', 'Three.js');
423
+ check('d3', 'D3.js');
424
+ if (topFiles.has('manage.py'))
425
+ r.push('Django');
426
+ if (topFiles.has('go.mod'))
427
+ r.push('Go Module');
428
+ if (topFiles.has('Cargo.toml'))
429
+ r.push('Rust/Cargo');
430
+ if (topFiles.has('pyproject.toml') || topFiles.has('setup.py') || topFiles.has('requirements.txt'))
431
+ r.push('Python');
432
+ return [...new Set(r)];
433
+ }
434
+ function detectTestFramework(allDeps, scriptDetails) {
435
+ if (allDeps.has('vitest'))
436
+ return 'Vitest';
437
+ if (allDeps.has('jest'))
438
+ return 'Jest';
439
+ if (allDeps.has('mocha'))
440
+ return 'Mocha';
441
+ if (allDeps.has('@playwright/test') || allDeps.has('playwright'))
442
+ return 'Playwright';
443
+ if (allDeps.has('cypress'))
444
+ return 'Cypress';
445
+ if (allDeps.has('ava'))
446
+ return 'Ava';
447
+ const ts = scriptDetails['test'] || '';
448
+ if (ts.includes('vitest'))
449
+ return 'Vitest';
450
+ if (ts.includes('jest'))
451
+ return 'Jest';
452
+ if (ts.includes('mocha'))
453
+ return 'Mocha';
454
+ if (ts.includes('pytest'))
455
+ return 'Pytest';
456
+ return null;
457
+ }
458
+ function detectBuildTool(allDeps, topFiles) {
459
+ if (allDeps.has('vite'))
460
+ return 'Vite';
461
+ if (allDeps.has('webpack'))
462
+ return 'Webpack';
463
+ if (allDeps.has('esbuild'))
464
+ return 'esbuild';
465
+ if (allDeps.has('rollup'))
466
+ return 'Rollup';
467
+ if (allDeps.has('parcel'))
468
+ return 'Parcel';
469
+ if (topFiles.has('turbo.json'))
470
+ return 'Turborepo';
471
+ if (topFiles.has('tsconfig.json') && allDeps.has('typescript'))
472
+ return 'tsc';
473
+ if (topFiles.has('Makefile'))
474
+ return 'Make';
475
+ return null;
476
+ }
477
+ const KNOWN_CONFIG_FILES = new Set([
478
+ 'tsconfig.json', 'jsconfig.json',
479
+ '.eslintrc', '.eslintrc.js', '.eslintrc.json', '.eslintrc.yml', 'eslint.config.js', 'eslint.config.mjs',
480
+ '.prettierrc', '.prettierrc.js', '.prettierrc.json', 'prettier.config.js',
481
+ 'vite.config.ts', 'vite.config.js', 'webpack.config.js', 'webpack.config.ts',
482
+ 'next.config.js', 'next.config.mjs', 'next.config.ts',
483
+ 'tailwind.config.js', 'tailwind.config.ts', 'postcss.config.js',
484
+ '.babelrc', 'babel.config.js',
485
+ 'docker-compose.yml', 'docker-compose.yaml', 'Dockerfile',
486
+ '.env', '.env.local', '.env.example',
487
+ 'jest.config.js', 'jest.config.ts', 'vitest.config.ts', 'vitest.config.js',
488
+ 'turbo.json', '.editorconfig', '.nvmrc', '.node-version',
489
+ 'Makefile', 'Procfile', 'vercel.json', 'netlify.toml',
490
+ 'go.mod', 'Cargo.toml', 'pyproject.toml', 'requirements.txt',
491
+ ]);
492
+ const KNOWN_KEY_FILES = new Set([
493
+ 'README.md', 'readme.md', 'README.txt', 'CHANGELOG.md',
494
+ 'CONTRIBUTING.md', 'LICENSE', 'LICENSE.md',
495
+ ]);
496
+ function detectConfigFiles(entries) {
497
+ return entries.filter((e) => !e.isDirectory() && KNOWN_CONFIG_FILES.has(e.name)).map((e) => e.name).sort();
498
+ }
499
+ function detectKeyFiles(entries) {
500
+ return entries.filter((e) => !e.isDirectory() && KNOWN_KEY_FILES.has(e.name)).map((e) => e.name);
501
+ }
502
+ function detectEntryPoints(pkg, topFileSet) {
503
+ const entries = [];
504
+ if (pkg.main)
505
+ entries.push(pkg.main);
506
+ entries.push(...pkg.bin);
507
+ for (const f of ['src/index.ts', 'src/index.js', 'src/main.ts', 'src/main.js', 'src/app.ts', 'src/app.js', 'index.ts', 'index.js', 'app.ts', 'app.js', 'server.ts', 'server.js', 'main.go', 'main.py', 'app.py']) {
508
+ if (topFileSet.has(f))
509
+ entries.push(f);
510
+ }
511
+ return [...new Set(entries)].slice(0, 8);
512
+ }
513
+ async function detectGitBranch(cwd) {
514
+ try {
515
+ const head = await fs.promises.readFile(path.join(cwd, '.git', 'HEAD'), 'utf-8');
516
+ const match = head.trim().match(/^ref:\s*refs\/heads\/(.+)$/);
517
+ return match ? match[1] : head.trim().slice(0, 12);
518
+ }
519
+ catch {
520
+ return null;
521
+ }
522
+ }
523
+ async function detectNodeVersion(cwd) {
524
+ for (const f of ['.nvmrc', '.node-version']) {
525
+ try {
526
+ const v = (await fs.promises.readFile(path.join(cwd, f), 'utf-8')).trim();
527
+ if (v)
528
+ return v;
529
+ }
530
+ catch { }
531
+ }
532
+ return null;
533
+ }
534
+ function summarizeLanguages(files) {
535
+ const counters = new Map();
536
+ for (const file of files) {
537
+ const ext = path.extname(file).toLowerCase() || '(none)';
538
+ counters.set(ext, (counters.get(ext) || 0) + 1);
539
+ }
540
+ return [...counters.entries()]
541
+ .sort((a, b) => b[1] - a[1])
542
+ .slice(0, 10)
543
+ .map(([ext, count]) => ({ ext, count }));
544
+ }
545
+ export async function scanWorkspaceProfile(cwd, maxEntries = 800) {
546
+ let entries = [];
547
+ try {
548
+ entries = await fs.promises.readdir(cwd, { withFileTypes: true });
549
+ }
550
+ catch { }
551
+ const topLevelDirs = entries
552
+ .filter((entry) => entry.isDirectory() && !DEFAULT_EXCLUDED_DIRS.has(entry.name))
553
+ .map((entry) => entry.name)
554
+ .sort()
555
+ .slice(0, 30);
556
+ const topFileSet = new Set(entries.filter((e) => !e.isDirectory()).map((e) => e.name));
557
+ // Also check src/ for entry points
558
+ try {
559
+ const srcEntries = await fs.promises.readdir(path.join(cwd, 'src'), { withFileTypes: true });
560
+ for (const e of srcEntries) {
561
+ if (!e.isDirectory())
562
+ topFileSet.add(`src/${e.name}`);
563
+ }
564
+ }
565
+ catch { }
566
+ const files = await walkFiles(cwd, maxEntries);
567
+ const pkg = await readPackageJsonInfo(cwd);
568
+ const frameworks = detectFrameworks(pkg.allDeps, topFileSet);
569
+ return {
570
+ version: 2,
571
+ createdAt: new Date().toISOString(),
572
+ root: cwd,
573
+ packageManager: await detectPackageManager(cwd),
574
+ scripts: pkg.scripts,
575
+ topLevelDirs,
576
+ languageStats: summarizeLanguages(files),
577
+ projectName: pkg.name,
578
+ projectDescription: pkg.description,
579
+ dependencies: pkg.dependencies.slice(0, 30),
580
+ devDependencies: pkg.devDependencies.slice(0, 20),
581
+ frameworks,
582
+ configFiles: detectConfigFiles(entries),
583
+ testFramework: detectTestFramework(pkg.allDeps, pkg.scriptDetails),
584
+ buildTool: detectBuildTool(pkg.allDeps, topFileSet),
585
+ entryPoints: detectEntryPoints(pkg, topFileSet),
586
+ gitBranch: await detectGitBranch(cwd),
587
+ totalFiles: files.length,
588
+ keyFiles: detectKeyFiles(entries),
589
+ nodeVersion: pkg.engines['node'] || await detectNodeVersion(cwd),
590
+ };
591
+ }
592
+ export async function initCodeWorkspaceProfile(cwd, maxEntries = 500) {
593
+ const profilePath = getCodeWorkspaceProfilePath(cwd);
594
+ await fs.promises.mkdir(path.dirname(profilePath), { recursive: true });
595
+ const profile = await scanWorkspaceProfile(cwd, maxEntries);
596
+ await fs.promises.writeFile(profilePath, JSON.stringify(profile, null, 2), 'utf-8');
597
+ return { profilePath, profile };
598
+ }
599
+ export async function loadCodeWorkspaceProfile(cwd) {
600
+ const profilePath = getCodeWorkspaceProfilePath(cwd);
601
+ try {
602
+ const raw = await fs.promises.readFile(profilePath, 'utf-8');
603
+ const parsed = JSON.parse(raw);
604
+ if (!parsed || typeof parsed !== 'object')
605
+ return null;
606
+ return parsed;
607
+ }
608
+ catch {
609
+ return null;
610
+ }
611
+ }
612
+ export function buildCodeSystemPrompt(profile) {
613
+ const base = [
614
+ '你是 Kie — 一个驻留在终端中的顶级 AI 软件工程师。',
615
+ '你精通全栈开发、系统架构和 DevOps,能独立完成从需求分析到生产部署的完整工程链路。',
616
+ '',
617
+ '【核心原则】',
618
+ '1. 精准理解意图:用户的需求可能模糊,你必须推断最合理的解读,而不是追问显而易见的细节。',
619
+ '2. 最小改动原则:只修改完成任务必须改动的部分,不要进行未被要求的重构、美化或"顺便"优化。',
620
+ '3. 防御性编码:所有代码必须处理边界情况和异常,类型安全优先。',
621
+ '4. 输出极度精简:不要复述需求、不要输出"好的我来帮你"之类的废话。直接给方案和代码。',
622
+ '5. 中文交流:全程使用专业中文,技术术语保留英文原词(如 async、middleware、hook)。',
623
+ ];
624
+ if (profile) {
625
+ base.push('', '【当前工作区信息】');
626
+ if (profile.projectName)
627
+ base.push(`- 项目: ${profile.projectName}${profile.projectDescription ? ` — ${profile.projectDescription}` : ''}`);
628
+ base.push(`- 包管理器: ${profile.packageManager}`);
629
+ if (profile.nodeVersion)
630
+ base.push(`- Node 版本: ${profile.nodeVersion}`);
631
+ if (profile.gitBranch)
632
+ base.push(`- Git 分支: ${profile.gitBranch}`);
633
+ if (profile.frameworks?.length)
634
+ base.push(`- 技术栈: ${profile.frameworks.join(', ')}`);
635
+ if (profile.buildTool)
636
+ base.push(`- 构建工具: ${profile.buildTool}`);
637
+ if (profile.testFramework)
638
+ base.push(`- 测试框架: ${profile.testFramework}`);
639
+ base.push(`- 可用 scripts: ${profile.scripts.join(', ') || '(无)'}`);
640
+ base.push(`- 主要语言: ${profile.languageStats.slice(0, 5).map(l => `${l.ext}(${l.count})`).join(', ') || '(未知)'}`);
641
+ base.push(`- 顶层目录: ${profile.topLevelDirs.join(', ') || '(未知)'}`);
642
+ if (profile.entryPoints?.length)
643
+ base.push(`- 入口文件: ${profile.entryPoints.join(', ')}`);
644
+ if (profile.configFiles?.length)
645
+ base.push(`- 配置文件: ${profile.configFiles.join(', ')}`);
646
+ if (profile.dependencies?.length)
647
+ base.push(`- 核心依赖: ${profile.dependencies.slice(0, 15).join(', ')}${(profile.dependencies.length || 0) > 15 ? ` (+${profile.dependencies.length - 15})` : ''}`);
648
+ if (profile.totalFiles)
649
+ base.push(`- 文件总数: ${profile.totalFiles}`);
650
+ }
651
+ return base.join('\n');
652
+ }
653
+ export function buildCodeAgentSystemPrompt(profile) {
654
+ return [
655
+ buildCodeSystemPrompt(profile),
656
+ '',
657
+ '═══════════════════════════════════════════',
658
+ ' AGENT 自治执行模式',
659
+ '═══════════════════════════════════════════',
660
+ '',
661
+ '你被授权在用户的 Windows 机器上自主执行命令。',
662
+ '你的工作方式是:探查 → 规划 → 执行 → 验证,循环直至任务完成。',
663
+ '',
664
+ '【决策框架】',
665
+ '第一轮:必须先探查项目现状(dir / type 目标文件),绝对禁止在未读取文件的情况下直接修改代码。',
666
+ '后续轮:根据上一轮命令的实际输出来决定下一步,而不是臆测结果。',
667
+ '最终轮:执行验证命令(构建/测试),确认无误后标记 done=true。',
668
+ '',
669
+ '【命令规范 — Windows 环境】',
670
+ '✅ 允许: dir, type, node, npm, npx, pnpm, git, powershell 内置命令',
671
+ '❌ 禁止: cat, ls, grep, rm, touch, sed, awk(这些不是 Windows 命令)',
672
+ '⚠️ 路径用反斜杠 \\\\,引号用双引号 ""',
673
+ '',
674
+ '【文件编辑工具箱】',
675
+ '修改文件时绝对禁止输出完整文件内容!请使用以下方法之一:',
676
+ '',
677
+ '方法A — Node.js 精确替换(推荐):',
678
+ 'node -e "let f=require(\'fs\');let s=f.readFileSync(\'文件路径\',\'utf8\');s=s.replace(\'旧内容\',\'新内容\');f.writeFileSync(\'文件路径\',s)"',
679
+ '',
680
+ '方法B — Node.js 行级插入/替换:',
681
+ 'node -e "let f=require(\'fs\');let lines=f.readFileSync(\'文件路径\',\'utf8\').split(\'\\n\');lines.splice(行号,删除行数,\'新行内容\');f.writeFileSync(\'文件路径\',lines.join(\'\\n\'))"',
682
+ '',
683
+ '方法C — 创建新文件(仅小文件 <30 行时可用):',
684
+ 'node -e "require(\'fs\').writeFileSync(\'文件路径\', `完整内容`)"',
685
+ '',
686
+ '【错误自愈协议】',
687
+ '当命令执行失败时:',
688
+ '1. 先分析错误信息的根因(不要猜测)',
689
+ '2. 用 type 或 dir 确认文件/目录的实际状态',
690
+ '3. 针对性修复,一次只改一处',
691
+ '4. 再次运行验证命令确认修复成功',
692
+ '',
693
+ '【输出截断声明】',
694
+ '注意:超长的命令输出(如 type 大文件)会被 CLI 自动截断,你看到的可能只有头部和尾部各一段。',
695
+ '如果需要查看文件特定区域,请使用: node -e "console.log(require(\'fs\').readFileSync(\'文件\',\'utf8\').split(\'\\n\').slice(起始行,结束行).join(\'\\n\'))"',
696
+ '',
697
+ '【强制输出格式】',
698
+ '你的每一次回复必须严格使用以下 XML 结构(标签外不允许有任何文字):',
699
+ '',
700
+ '<assistant_reply>',
701
+ '1-3 句话,简要说明你在这一轮做了什么/要做什么。',
702
+ '</assistant_reply>',
703
+ '<commands>',
704
+ '每行一条命令(最多 3 条)',
705
+ '如果任务已完成或仅需回答问题,此处留空',
706
+ '</commands>',
707
+ '<done>false</done>',
708
+ '',
709
+ '【done 判定标准】',
710
+ 'false = 任务尚未完成,或需要更多验证',
711
+ 'true = 用户要求已 100% 实现 + 验证通过 + 无残留问题',
712
+ ].join('\n');
713
+ }
714
+ export function buildCodeUserPrompt(userTask, context, profile) {
715
+ const sections = [];
716
+ sections.push(`【用户任务】\n${userTask.trim()}`);
717
+ if (profile) {
718
+ const lines = [];
719
+ if (profile.projectName)
720
+ lines.push(`项目: ${profile.projectName}${profile.projectDescription ? ` — ${profile.projectDescription}` : ''}`);
721
+ lines.push(`packageManager: ${profile.packageManager}`);
722
+ if (profile.gitBranch)
723
+ lines.push(`branch: ${profile.gitBranch}`);
724
+ if (profile.frameworks?.length)
725
+ lines.push(`技术栈: ${profile.frameworks.join(', ')}`);
726
+ if (profile.buildTool)
727
+ lines.push(`构建: ${profile.buildTool}`);
728
+ if (profile.testFramework)
729
+ lines.push(`测试: ${profile.testFramework}`);
730
+ lines.push(`scripts: ${profile.scripts.join(', ') || '(none)'}`);
731
+ lines.push(`topLevelDirs: ${profile.topLevelDirs.join(', ') || '(none)'}`);
732
+ lines.push(`languages: ${profile.languageStats.map((l) => `${l.ext}:${l.count}`).join(', ') || '(none)'}`);
733
+ if (profile.entryPoints?.length)
734
+ lines.push(`入口: ${profile.entryPoints.join(', ')}`);
735
+ if (profile.configFiles?.length)
736
+ lines.push(`配置: ${profile.configFiles.join(', ')}`);
737
+ if (profile.dependencies?.length)
738
+ lines.push(`dependencies: ${profile.dependencies.join(', ')}`);
739
+ if (profile.devDependencies?.length)
740
+ lines.push(`devDependencies: ${profile.devDependencies.join(', ')}`);
741
+ if (profile.totalFiles)
742
+ lines.push(`文件总数: ${profile.totalFiles}`);
743
+ sections.push(`【工作区画像】\n${lines.join('\n')}`);
744
+ }
745
+ if (context.tree) {
746
+ sections.push(`【项目结构(截断)】\n${context.tree}`);
747
+ }
748
+ if (context.files.length > 0) {
749
+ const filesText = context.files
750
+ .map((file) => `### ${file.resolvedPath}\n\`\`\`\n${file.content}\n\`\`\``)
751
+ .join('\n\n');
752
+ sections.push(`【用户指定文件内容】\n${filesText}`);
753
+ }
754
+ sections.push([
755
+ '【输出要求】',
756
+ '- 先给出“实现方案(MVP)”',
757
+ '- 再给出“建议修改文件清单”',
758
+ '- 最后给出“验证命令与预期结果”',
759
+ ].join('\n'));
760
+ return sections.join('\n\n');
761
+ }
762
+ function readTag(text, tag) {
763
+ const re = new RegExp(`<${tag}>[\\s\\S]*?<\\/${tag}>`, 'i');
764
+ const matched = text.match(re);
765
+ if (!matched)
766
+ return '';
767
+ return matched[0]
768
+ .replace(new RegExp(`^<${tag}>`, 'i'), '')
769
+ .replace(new RegExp(`<\\/${tag}>$`, 'i'), '')
770
+ .trim();
771
+ }
772
+ function extractCommandsFromFencedBlocks(text) {
773
+ const commands = [];
774
+ const blockRe = /```(?:bash|sh|shell|powershell|pwsh|cmd)?\s*([\s\S]*?)```/gi;
775
+ let match = blockRe.exec(text);
776
+ while (match) {
777
+ const lines = match[1]
778
+ .split('\n')
779
+ .map((line) => line.trim())
780
+ .map((line) => line.replace(/^\$\s*/, '').replace(/^PS>\s*/i, '').trim())
781
+ .filter((line) => line.length > 0 && !line.startsWith('#'));
782
+ commands.push(...lines);
783
+ match = blockRe.exec(text);
784
+ }
785
+ return commands;
786
+ }
787
+ function extractCommandsFromLooseLines(text) {
788
+ const likely = /^(?:\$|PS>|>)?\s*(npm|pnpm|yarn|npx|node|git|ls|dir|type|cat|python|pytest|go|cargo|dotnet|mvn|gradle|bun|tsc|powershell|mkdir|echo|copy|pip|poetry|make|cmake)\b/i;
789
+ const numbered = /^(?:\d+[.)\s]\s*|[-*]\s+)(?:\$|PS>|>)?\s*(npm|pnpm|yarn|npx|node|git|dir|type|tsc|powershell|mkdir|echo)\b/i;
790
+ return text
791
+ .split('\n')
792
+ .map((line) => line.trim())
793
+ .map((line) => line.replace(/^\d+[.)\s]\s*/, '').replace(/^[-*]\s+/, '').replace(/^\$\s*/, '').replace(/^PS>\s*/i, '').replace(/^>\s*/, '').trim())
794
+ .filter((line) => likely.test(line) || numbered.test(line));
795
+ }
796
+ export function parseAgentResponse(text) {
797
+ const thought = readTag(text, 'thought');
798
+ const reply = readTag(text, 'assistant_reply') || text.trim();
799
+ const commandsBlock = readTag(text, 'commands');
800
+ const doneRaw = readTag(text, 'done').toLowerCase();
801
+ let commands = commandsBlock
802
+ .split('\n')
803
+ .map((line) => line.trim())
804
+ .map((line) => line.replace(/^[-*]\s*/, '').replace(/^\$\s*/, '').trim())
805
+ .filter(Boolean);
806
+ if (commands.length === 0) {
807
+ commands = extractCommandsFromFencedBlocks(text);
808
+ }
809
+ if (commands.length === 0) {
810
+ commands = extractCommandsFromLooseLines(text);
811
+ }
812
+ commands = commands.slice(0, 3);
813
+ let done = doneRaw === 'true';
814
+ if (!doneRaw) {
815
+ const normalized = text.toLowerCase();
816
+ if (commands.length === 0 && (normalized.includes('已完成') || normalized.includes('done'))) {
817
+ done = true;
818
+ }
819
+ }
820
+ return { thought, reply, commands, done };
821
+ }
822
+ /**
823
+ * 裁剪对话历史,防止 token 爆炸。
824
+ * 保留第一条用户消息(原始意图)和最近 keepRecent 条消息,
825
+ * 中间部分用一条摘要消息替代。仅用于构建 API 请求,不修改原始数组。
826
+ */
827
+ export function trimConversation(conversation, maxMessages = 24, keepRecent = 10) {
828
+ if (conversation.length <= maxMessages)
829
+ return conversation;
830
+ const firstUserIdx = conversation.findIndex((m) => m.role === 'user');
831
+ const firstUser = firstUserIdx >= 0 ? conversation[firstUserIdx] : null;
832
+ const recent = conversation.slice(-keepRecent);
833
+ const trimmedCount = conversation.length - keepRecent - (firstUser ? 1 : 0);
834
+ const contextNote = {
835
+ role: 'user',
836
+ content: `[System] 为控制 token 用量,已省略中间 ${trimmedCount} 条对话记录。请基于最近的上下文继续推进任务。`,
837
+ };
838
+ const result = [];
839
+ if (firstUser)
840
+ result.push(firstUser);
841
+ result.push(contextNote);
842
+ result.push(...recent);
843
+ return result;
844
+ }
845
+ /** 截断超长命令输出,保留头尾各 headLines/tailLines 行 */
846
+ export function truncateCommandOutput(output, maxLines = 80, headLines = 40, tailLines = 30) {
847
+ const lines = output.split('\n');
848
+ if (lines.length <= maxLines)
849
+ return output;
850
+ const omitted = lines.length - headLines - tailLines;
851
+ return [
852
+ ...lines.slice(0, headLines),
853
+ `\n... [已省略中间 ${omitted} 行,共 ${lines.length} 行] ...\n`,
854
+ ...lines.slice(-tailLines),
855
+ ].join('\n');
856
+ }
857
+ const DANGEROUS_PATTERNS = [
858
+ /\brm\s+-rf\s+\/\b/i,
859
+ /\brm\s+-rf\s+\*\b/i,
860
+ /\bshutdown\b/i,
861
+ /\breboot\b/i,
862
+ /\bmkfs\b/i,
863
+ /\bformat\b/i,
864
+ /\bdel\s+\/f\s+\/s\s+\/q\b/i,
865
+ /\bpoweroff\b/i,
866
+ ];
867
+ export function isDangerousCommand(command) {
868
+ const trimmed = command.trim();
869
+ if (!trimmed)
870
+ return true;
871
+ return DANGEROUS_PATTERNS.some((pattern) => pattern.test(trimmed));
872
+ }
873
+ async function getCodeSessionsDir(cwd) {
874
+ const dir = path.join(cwd, '.kie', 'sessions');
875
+ await fs.promises.mkdir(dir, { recursive: true });
876
+ return dir;
877
+ }
878
+ async function getCodeSessionPath(cwd, sessionId) {
879
+ const safe = sessionId.replace(/[^a-zA-Z0-9_-]/g, '_');
880
+ return path.join(await getCodeSessionsDir(cwd), `${safe}.json`);
881
+ }
882
+ async function getSessionIndexPath(cwd) {
883
+ return path.join(await getCodeSessionsDir(cwd), 'sessions-index.json');
884
+ }
885
+ export async function loadSessionIndex(cwd) {
886
+ const indexPath = await getSessionIndexPath(cwd);
887
+ try {
888
+ const raw = await fs.promises.readFile(indexPath, 'utf-8');
889
+ return JSON.parse(raw);
890
+ }
891
+ catch {
892
+ return {};
893
+ }
894
+ }
895
+ export async function saveSessionIndex(cwd, index) {
896
+ const indexPath = await getSessionIndexPath(cwd);
897
+ await fs.promises.writeFile(indexPath, JSON.stringify(index, null, 2), 'utf-8');
898
+ }
899
+ export function createCodeSessionId() {
900
+ return `code-${Date.now()}`;
901
+ }
902
+ export async function saveCodeSession(cwd, state) {
903
+ const filePath = await getCodeSessionPath(cwd, state.id);
904
+ await fs.promises.writeFile(filePath, JSON.stringify(state, null, 2), 'utf-8');
905
+ const index = await loadSessionIndex(cwd);
906
+ index[state.id] = {
907
+ id: state.id,
908
+ updatedAt: state.updatedAt || new Date().toISOString(),
909
+ messageCount: Array.isArray(state.conversation) ? state.conversation.length : 0,
910
+ };
911
+ await saveSessionIndex(cwd, index);
912
+ }
913
+ export async function loadCodeSession(cwd, sessionId) {
914
+ const filePath = await getCodeSessionPath(cwd, sessionId);
915
+ try {
916
+ const raw = await fs.promises.readFile(filePath, 'utf-8');
917
+ const parsed = JSON.parse(raw);
918
+ if (!parsed || typeof parsed !== 'object')
919
+ return null;
920
+ return {
921
+ id: parsed.id || sessionId,
922
+ updatedAt: parsed.updatedAt || new Date().toISOString(),
923
+ conversation: Array.isArray(parsed.conversation) ? parsed.conversation : [],
924
+ extraFiles: Array.isArray(parsed.extraFiles) ? parsed.extraFiles : [],
925
+ model: typeof parsed.model === 'string' ? parsed.model : 'claude-opus-4-7',
926
+ route: resolveCodeRoute(parsed.route),
927
+ autoApprove: Boolean(parsed.autoApprove),
928
+ };
929
+ }
930
+ catch {
931
+ return null;
932
+ }
933
+ }
934
+ export async function listCodeSessions(cwd) {
935
+ const index = await loadSessionIndex(cwd);
936
+ return Object.values(index).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
937
+ }