agent-state-machine 2.1.8 → 2.1.9
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/bin/cli.js +1 -140
- package/lib/config-utils.js +259 -0
- package/lib/llm.js +23 -5
- package/lib/runtime/model-resolution.js +128 -0
- package/package.json +1 -1
- package/templates/project-builder/agents/assumptions-clarifier.md +0 -8
- package/templates/project-builder/agents/code-reviewer.md +0 -8
- package/templates/project-builder/agents/code-writer.md +0 -10
- package/templates/project-builder/agents/requirements-clarifier.md +0 -7
- package/templates/project-builder/agents/roadmap-generator.md +0 -10
- package/templates/project-builder/agents/scope-clarifier.md +0 -6
- package/templates/project-builder/agents/security-clarifier.md +0 -9
- package/templates/project-builder/agents/security-reviewer.md +0 -12
- package/templates/project-builder/agents/task-planner.md +0 -10
- package/templates/project-builder/agents/test-planner.md +0 -9
- package/templates/project-builder/config.js +1 -0
- package/vercel-server/public/remote/assets/{index-Cunx4VJE.js → index-BOKpYANC.js} +32 -27
- package/vercel-server/public/remote/assets/index-DHL_iHQW.css +1 -0
- package/vercel-server/public/remote/index.html +2 -2
- package/vercel-server/ui/src/components/ContentCard.jsx +6 -6
- package/vercel-server/public/remote/assets/index-D02x57pS.css +0 -1
package/bin/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import { pathToFileURL, fileURLToPath } from 'url';
|
|
|
7
7
|
import { WorkflowRuntime } from '../lib/index.js';
|
|
8
8
|
import { setup } from '../lib/setup.js';
|
|
9
9
|
import { generateSessionToken } from '../lib/remote/client.js';
|
|
10
|
+
import { readRemotePathFromConfig, writeRemotePathToConfig } from '../lib/config-utils.js';
|
|
10
11
|
|
|
11
12
|
import { startLocalServer } from '../vercel-server/local-server.js';
|
|
12
13
|
|
|
@@ -119,146 +120,6 @@ function resolveWorkflowEntry(workflowDir) {
|
|
|
119
120
|
return null;
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
function findConfigObjectRange(source) {
|
|
123
|
-
const match = source.match(/export\s+const\s+config\s*=/);
|
|
124
|
-
if (!match) return null;
|
|
125
|
-
const startSearch = match.index + match[0].length;
|
|
126
|
-
const braceStart = source.indexOf('{', startSearch);
|
|
127
|
-
if (braceStart === -1) return null;
|
|
128
|
-
|
|
129
|
-
let depth = 0;
|
|
130
|
-
let inString = null;
|
|
131
|
-
let inLineComment = false;
|
|
132
|
-
let inBlockComment = false;
|
|
133
|
-
let escape = false;
|
|
134
|
-
|
|
135
|
-
for (let i = braceStart; i < source.length; i += 1) {
|
|
136
|
-
const ch = source[i];
|
|
137
|
-
const next = source[i + 1];
|
|
138
|
-
|
|
139
|
-
if (inLineComment) {
|
|
140
|
-
if (ch === '\n') inLineComment = false;
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (inBlockComment) {
|
|
145
|
-
if (ch === '*' && next === '/') {
|
|
146
|
-
inBlockComment = false;
|
|
147
|
-
i += 1;
|
|
148
|
-
}
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (inString) {
|
|
153
|
-
if (escape) {
|
|
154
|
-
escape = false;
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
if (ch === '\\') {
|
|
158
|
-
escape = true;
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
if (ch === inString) {
|
|
162
|
-
inString = null;
|
|
163
|
-
}
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (ch === '"' || ch === '\'' || ch === '`') {
|
|
168
|
-
inString = ch;
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (ch === '/' && next === '/') {
|
|
173
|
-
inLineComment = true;
|
|
174
|
-
i += 1;
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (ch === '/' && next === '*') {
|
|
179
|
-
inBlockComment = true;
|
|
180
|
-
i += 1;
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (ch === '{') {
|
|
185
|
-
depth += 1;
|
|
186
|
-
} else if (ch === '}') {
|
|
187
|
-
depth -= 1;
|
|
188
|
-
if (depth === 0) {
|
|
189
|
-
return { start: braceStart, end: i };
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function readRemotePathFromConfig(configFile) {
|
|
198
|
-
const source = fs.readFileSync(configFile, 'utf-8');
|
|
199
|
-
const range = findConfigObjectRange(source);
|
|
200
|
-
if (!range) return null;
|
|
201
|
-
const configSource = source.slice(range.start, range.end + 1);
|
|
202
|
-
const match = configSource.match(/\bremotePath\s*:\s*(['"`])([^'"`]+)\1/);
|
|
203
|
-
return match ? match[2] : null;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function writeRemotePathToConfig(configFile, remotePath) {
|
|
207
|
-
const source = fs.readFileSync(configFile, 'utf-8');
|
|
208
|
-
const range = findConfigObjectRange(source);
|
|
209
|
-
const remoteLine = `remotePath: "${remotePath}"`;
|
|
210
|
-
|
|
211
|
-
if (!range) {
|
|
212
|
-
const hasConfigExport = /export\s+const\s+config\s*=/.test(source);
|
|
213
|
-
if (hasConfigExport) {
|
|
214
|
-
throw new Error('Config export is not an object literal; add remotePath manually.');
|
|
215
|
-
}
|
|
216
|
-
const trimmed = source.replace(/\s*$/, '');
|
|
217
|
-
const appended = `${trimmed}\n\nexport const config = {\n ${remoteLine}\n};\n`;
|
|
218
|
-
fs.writeFileSync(configFile, appended);
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const configSource = source.slice(range.start, range.end + 1);
|
|
223
|
-
const remoteRegex = /\bremotePath\s*:\s*(['"`])([^'"`]*?)\1/;
|
|
224
|
-
let updatedConfigSource;
|
|
225
|
-
|
|
226
|
-
if (remoteRegex.test(configSource)) {
|
|
227
|
-
updatedConfigSource = configSource.replace(remoteRegex, remoteLine);
|
|
228
|
-
} else {
|
|
229
|
-
const inner = configSource.slice(1, -1);
|
|
230
|
-
const indentMatch = inner.match(/\n([ \t]+)\S/);
|
|
231
|
-
const indent = indentMatch ? indentMatch[1] : ' ';
|
|
232
|
-
const trimmedInner = inner.replace(/\s*$/, '');
|
|
233
|
-
const hasContent = trimmedInner.trim().length > 0;
|
|
234
|
-
let updatedInner = trimmedInner;
|
|
235
|
-
|
|
236
|
-
if (hasContent) {
|
|
237
|
-
for (let i = updatedInner.length - 1; i >= 0; i -= 1) {
|
|
238
|
-
const ch = updatedInner[i];
|
|
239
|
-
if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') continue;
|
|
240
|
-
if (ch !== ',') {
|
|
241
|
-
updatedInner += ',';
|
|
242
|
-
}
|
|
243
|
-
break;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const needsNewline = updatedInner && !updatedInner.endsWith('\n');
|
|
248
|
-
const insert = `${indent}${remoteLine},\n`;
|
|
249
|
-
const newInner = hasContent
|
|
250
|
-
? `${updatedInner}${needsNewline ? '\n' : ''}${insert}`
|
|
251
|
-
: `\n${insert}`;
|
|
252
|
-
updatedConfigSource = `{${newInner}}`;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const updatedSource =
|
|
256
|
-
source.slice(0, range.start) +
|
|
257
|
-
updatedConfigSource +
|
|
258
|
-
source.slice(range.end + 1);
|
|
259
|
-
fs.writeFileSync(configFile, updatedSource);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
123
|
function ensureRemotePath(configFile, { forceNew = false } = {}) {
|
|
263
124
|
const existing = readRemotePathFromConfig(configFile);
|
|
264
125
|
if (existing && !forceNew) return existing;
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: /lib/config-utils.js
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
|
|
7
|
+
export function findConfigObjectRange(source) {
|
|
8
|
+
const match = source.match(/export\s+const\s+config\s*=/);
|
|
9
|
+
if (!match) return null;
|
|
10
|
+
const startSearch = match.index + match[0].length;
|
|
11
|
+
const braceStart = source.indexOf('{', startSearch);
|
|
12
|
+
if (braceStart === -1) return null;
|
|
13
|
+
|
|
14
|
+
return findObjectLiteralRange(source, braceStart);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function findObjectLiteralRange(source, braceStart) {
|
|
18
|
+
let depth = 0;
|
|
19
|
+
let inString = null;
|
|
20
|
+
let inLineComment = false;
|
|
21
|
+
let inBlockComment = false;
|
|
22
|
+
let escape = false;
|
|
23
|
+
|
|
24
|
+
for (let i = braceStart; i < source.length; i += 1) {
|
|
25
|
+
const ch = source[i];
|
|
26
|
+
const next = source[i + 1];
|
|
27
|
+
|
|
28
|
+
if (inLineComment) {
|
|
29
|
+
if (ch === '\n') inLineComment = false;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (inBlockComment) {
|
|
34
|
+
if (ch === '*' && next === '/') {
|
|
35
|
+
inBlockComment = false;
|
|
36
|
+
i += 1;
|
|
37
|
+
}
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (inString) {
|
|
42
|
+
if (escape) {
|
|
43
|
+
escape = false;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (ch === '\\') {
|
|
47
|
+
escape = true;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (ch === inString) {
|
|
51
|
+
inString = null;
|
|
52
|
+
}
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (ch === '"' || ch === '\'' || ch === '`') {
|
|
57
|
+
inString = ch;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (ch === '/' && next === '/') {
|
|
62
|
+
inLineComment = true;
|
|
63
|
+
i += 1;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (ch === '/' && next === '*') {
|
|
68
|
+
inBlockComment = true;
|
|
69
|
+
i += 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (ch === '{') {
|
|
74
|
+
depth += 1;
|
|
75
|
+
} else if (ch === '}') {
|
|
76
|
+
depth -= 1;
|
|
77
|
+
if (depth === 0) {
|
|
78
|
+
return { start: braceStart, end: i };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function detectIndent(inner) {
|
|
87
|
+
const indentMatch = inner.match(/\n([ \t]+)\S/);
|
|
88
|
+
return indentMatch ? indentMatch[1] : ' ';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function escapeRegExp(value) {
|
|
92
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isValidIdentifier(value) {
|
|
96
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function formatObjectKey(value) {
|
|
100
|
+
if (isValidIdentifier(value)) return value;
|
|
101
|
+
return JSON.stringify(value);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function insertPropertyIntoObjectSource(objectSource, propertySource, indent) {
|
|
105
|
+
const inner = objectSource.slice(1, -1);
|
|
106
|
+
const trimmedInner = inner.replace(/\s*$/, '');
|
|
107
|
+
const hasContent = trimmedInner.trim().length > 0;
|
|
108
|
+
let updatedInner = trimmedInner;
|
|
109
|
+
|
|
110
|
+
if (hasContent) {
|
|
111
|
+
for (let i = updatedInner.length - 1; i >= 0; i -= 1) {
|
|
112
|
+
const ch = updatedInner[i];
|
|
113
|
+
if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') continue;
|
|
114
|
+
if (ch !== ',') {
|
|
115
|
+
updatedInner += ',';
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const needsNewline = updatedInner && !updatedInner.endsWith('\n');
|
|
122
|
+
const insert = `${indent}${propertySource},\n`;
|
|
123
|
+
const newInner = hasContent
|
|
124
|
+
? `${updatedInner}${needsNewline ? '\n' : ''}${insert}`
|
|
125
|
+
: `\n${insert}`;
|
|
126
|
+
|
|
127
|
+
return `{${newInner}}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function readRemotePathFromConfig(configFile) {
|
|
131
|
+
const source = fs.readFileSync(configFile, 'utf-8');
|
|
132
|
+
const range = findConfigObjectRange(source);
|
|
133
|
+
if (!range) return null;
|
|
134
|
+
const configSource = source.slice(range.start, range.end + 1);
|
|
135
|
+
const match = configSource.match(/\bremotePath\s*:\s*(['"`])([^'"`]+)\1/);
|
|
136
|
+
return match ? match[2] : null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function writeRemotePathToConfig(configFile, remotePath) {
|
|
140
|
+
const source = fs.readFileSync(configFile, 'utf-8');
|
|
141
|
+
const range = findConfigObjectRange(source);
|
|
142
|
+
const remoteLine = `remotePath: ${JSON.stringify(remotePath)}`;
|
|
143
|
+
|
|
144
|
+
if (!range) {
|
|
145
|
+
const hasConfigExport = /export\s+const\s+config\s*=/.test(source);
|
|
146
|
+
if (hasConfigExport) {
|
|
147
|
+
throw new Error('Config export is not an object literal; add remotePath manually.');
|
|
148
|
+
}
|
|
149
|
+
const trimmed = source.replace(/\s*$/, '');
|
|
150
|
+
const appended = `${trimmed}\n\nexport const config = {\n ${remoteLine}\n};\n`;
|
|
151
|
+
fs.writeFileSync(configFile, appended);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const configSource = source.slice(range.start, range.end + 1);
|
|
156
|
+
const remoteRegex = /\bremotePath\s*:\s*(['"`])([^'"`]*?)\1/;
|
|
157
|
+
let updatedConfigSource;
|
|
158
|
+
|
|
159
|
+
if (remoteRegex.test(configSource)) {
|
|
160
|
+
updatedConfigSource = configSource.replace(remoteRegex, remoteLine);
|
|
161
|
+
} else {
|
|
162
|
+
const inner = configSource.slice(1, -1);
|
|
163
|
+
const indent = detectIndent(inner);
|
|
164
|
+
updatedConfigSource = insertPropertyIntoObjectSource(configSource, remoteLine, indent);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const updatedSource =
|
|
168
|
+
source.slice(0, range.start) +
|
|
169
|
+
updatedConfigSource +
|
|
170
|
+
source.slice(range.end + 1);
|
|
171
|
+
fs.writeFileSync(configFile, updatedSource);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function findModelsObjectRange(configSource) {
|
|
175
|
+
const regex = /\bmodels\s*:\s*{/g;
|
|
176
|
+
const match = regex.exec(configSource);
|
|
177
|
+
if (!match) return null;
|
|
178
|
+
const braceStart = configSource.indexOf('{', match.index + match[0].length - 1);
|
|
179
|
+
if (braceStart === -1) return null;
|
|
180
|
+
return findObjectLiteralRange(configSource, braceStart);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function buildModelKeyRegex(modelKey) {
|
|
184
|
+
const escaped = escapeRegExp(modelKey);
|
|
185
|
+
const quotedKey = `['"\\\`]${escaped}['"\\\`]`;
|
|
186
|
+
const unquotedKey = isValidIdentifier(modelKey) ? escaped : null;
|
|
187
|
+
const keyPattern = unquotedKey ? `(?:${unquotedKey}|${quotedKey})` : quotedKey;
|
|
188
|
+
return new RegExp(`(^|[,{\\s])(${keyPattern})\\s*:\\s*(['"\\\`])([^'"\\\\\`]*?)\\3`, 'm');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function readModelFromConfig(configFile, modelKey) {
|
|
192
|
+
const source = fs.readFileSync(configFile, 'utf-8');
|
|
193
|
+
const range = findConfigObjectRange(source);
|
|
194
|
+
if (!range) return null;
|
|
195
|
+
const configSource = source.slice(range.start, range.end + 1);
|
|
196
|
+
const modelsRange = findModelsObjectRange(configSource);
|
|
197
|
+
if (!modelsRange) return null;
|
|
198
|
+
const modelsSource = configSource.slice(modelsRange.start, modelsRange.end + 1);
|
|
199
|
+
const keyRegex = buildModelKeyRegex(modelKey);
|
|
200
|
+
const match = modelsSource.match(keyRegex);
|
|
201
|
+
return match ? match[4] : null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function writeModelToConfig(configFile, modelKey, modelValue) {
|
|
205
|
+
const source = fs.readFileSync(configFile, 'utf-8');
|
|
206
|
+
const range = findConfigObjectRange(source);
|
|
207
|
+
const formattedKey = formatObjectKey(modelKey);
|
|
208
|
+
const serializedValue = JSON.stringify(modelValue);
|
|
209
|
+
|
|
210
|
+
if (!range) {
|
|
211
|
+
const hasConfigExport = /export\s+const\s+config\s*=/.test(source);
|
|
212
|
+
if (hasConfigExport) {
|
|
213
|
+
throw new Error('Config export is not an object literal; add models mapping manually.');
|
|
214
|
+
}
|
|
215
|
+
const trimmed = source.replace(/\s*$/, '');
|
|
216
|
+
const appended = `${trimmed}\n\nexport const config = {\n models: {\n ${formattedKey}: ${serializedValue}\n }\n};\n`;
|
|
217
|
+
fs.writeFileSync(configFile, appended);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const configSource = source.slice(range.start, range.end + 1);
|
|
222
|
+
const modelsRange = findModelsObjectRange(configSource);
|
|
223
|
+
let updatedConfigSource = configSource;
|
|
224
|
+
|
|
225
|
+
if (!modelsRange) {
|
|
226
|
+
const inner = configSource.slice(1, -1);
|
|
227
|
+
const indent = detectIndent(inner);
|
|
228
|
+
const nestedIndent = `${indent} `;
|
|
229
|
+
const modelsBlock = `models: {\n${nestedIndent}${formattedKey}: ${serializedValue},\n${indent}}`;
|
|
230
|
+
updatedConfigSource = insertPropertyIntoObjectSource(configSource, modelsBlock, indent);
|
|
231
|
+
} else {
|
|
232
|
+
const modelsSource = configSource.slice(modelsRange.start, modelsRange.end + 1);
|
|
233
|
+
const keyRegex = buildModelKeyRegex(modelKey);
|
|
234
|
+
let updatedModelsSource;
|
|
235
|
+
|
|
236
|
+
if (keyRegex.test(modelsSource)) {
|
|
237
|
+
updatedModelsSource = modelsSource.replace(
|
|
238
|
+
keyRegex,
|
|
239
|
+
(_match, prefix, keyToken) => `${prefix}${keyToken}: ${serializedValue}`
|
|
240
|
+
);
|
|
241
|
+
} else {
|
|
242
|
+
const inner = modelsSource.slice(1, -1);
|
|
243
|
+
const indent = detectIndent(inner);
|
|
244
|
+
const modelLine = `${formattedKey}: ${serializedValue}`;
|
|
245
|
+
updatedModelsSource = insertPropertyIntoObjectSource(modelsSource, modelLine, indent);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
updatedConfigSource =
|
|
249
|
+
configSource.slice(0, modelsRange.start) +
|
|
250
|
+
updatedModelsSource +
|
|
251
|
+
configSource.slice(modelsRange.end + 1);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const updatedSource =
|
|
255
|
+
source.slice(0, range.start) +
|
|
256
|
+
updatedConfigSource +
|
|
257
|
+
source.slice(range.end + 1);
|
|
258
|
+
fs.writeFileSync(configFile, updatedSource);
|
|
259
|
+
}
|
package/lib/llm.js
CHANGED
|
@@ -7,6 +7,8 @@ import path from 'path';
|
|
|
7
7
|
import os from 'os';
|
|
8
8
|
import { spawn, execSync } from 'child_process';
|
|
9
9
|
import { createRequire } from 'module';
|
|
10
|
+
import { getCurrentRuntime } from './runtime/runtime.js';
|
|
11
|
+
import { resolveUnknownModel } from './runtime/model-resolution.js';
|
|
10
12
|
|
|
11
13
|
const require = createRequire(import.meta.url);
|
|
12
14
|
|
|
@@ -419,13 +421,29 @@ export async function llm(context, options) {
|
|
|
419
421
|
const apiKeys = config.apiKeys || {};
|
|
420
422
|
|
|
421
423
|
// Look up the model command/config
|
|
422
|
-
|
|
424
|
+
let modelConfig = models[options.model];
|
|
423
425
|
|
|
424
426
|
if (!modelConfig) {
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
427
|
+
const runtime = getCurrentRuntime();
|
|
428
|
+
if (runtime) {
|
|
429
|
+
const workflowDir = options.workflowDir || config.workflowDir || runtime.workflowDir;
|
|
430
|
+
if (!workflowDir) {
|
|
431
|
+
throw new Error(`Unknown model key: "${options.model}". Workflow directory is missing.`);
|
|
432
|
+
}
|
|
433
|
+
modelConfig = await resolveUnknownModel(options.model, config, workflowDir, {
|
|
434
|
+
availableCLIs: detectAvailableCLIs()
|
|
435
|
+
});
|
|
436
|
+
if (!config.models) {
|
|
437
|
+
config.models = models;
|
|
438
|
+
}
|
|
439
|
+
config.models[options.model] = modelConfig;
|
|
440
|
+
runtime.workflowConfig.models[options.model] = modelConfig;
|
|
441
|
+
} else {
|
|
442
|
+
const available = Object.keys(models).join(', ');
|
|
443
|
+
throw new Error(
|
|
444
|
+
`Unknown model key: "${options.model}". Available models: ${available || 'none defined'}`
|
|
445
|
+
);
|
|
446
|
+
}
|
|
429
447
|
}
|
|
430
448
|
|
|
431
449
|
// Build the full prompt
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: /lib/runtime/model-resolution.js
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { askHuman } from './prompt.js';
|
|
8
|
+
import { createInteraction, parseInteractionResponse } from './interaction.js';
|
|
9
|
+
import { readModelFromConfig, writeModelToConfig } from '../config-utils.js';
|
|
10
|
+
|
|
11
|
+
function sanitizeSlug(value) {
|
|
12
|
+
return String(value)
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
15
|
+
.replace(/^-+|-+$/g, '')
|
|
16
|
+
.substring(0, 40) || 'model';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function buildModelSuggestions(availableCLIs = {}, existingModels = {}) {
|
|
20
|
+
const options = [];
|
|
21
|
+
const lookup = {};
|
|
22
|
+
|
|
23
|
+
const addOption = (key, label, description, value) => {
|
|
24
|
+
options.push({ key, label, description });
|
|
25
|
+
lookup[key] = value;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
Object.entries(existingModels).forEach(([key, value]) => {
|
|
29
|
+
const optionKey = `existing:${key}`;
|
|
30
|
+
const description = value ? `Maps to "${value}"` : 'Existing model mapping';
|
|
31
|
+
addOption(optionKey, `Use existing model: ${key}`, description, value);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (availableCLIs.claude) {
|
|
35
|
+
addOption('cli:claude', 'claude -p', 'Claude CLI (print mode)', 'claude -p');
|
|
36
|
+
}
|
|
37
|
+
if (availableCLIs.gemini) {
|
|
38
|
+
addOption('cli:gemini', 'gemini', 'Gemini CLI', 'gemini');
|
|
39
|
+
}
|
|
40
|
+
if (availableCLIs.codex) {
|
|
41
|
+
addOption('cli:codex', 'codex', 'Codex CLI (exec)', 'codex');
|
|
42
|
+
}
|
|
43
|
+
if (availableCLIs.ollama) {
|
|
44
|
+
addOption('cli:ollama', 'ollama run llama3.1', 'Ollama CLI (example model)', 'ollama run llama3.1');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
addOption(
|
|
48
|
+
'api:openai',
|
|
49
|
+
'api:openai:gpt-4.1-mini',
|
|
50
|
+
'OpenAI API (example model)',
|
|
51
|
+
'api:openai:gpt-4.1-mini'
|
|
52
|
+
);
|
|
53
|
+
addOption(
|
|
54
|
+
'api:anthropic',
|
|
55
|
+
'api:anthropic:claude-3-5-sonnet-20241022',
|
|
56
|
+
'Anthropic API (example model)',
|
|
57
|
+
'api:anthropic:claude-3-5-sonnet-20241022'
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return { options, lookup };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function promptForModelConfig(modelKey, existingModels = {}, availableCLIs = {}) {
|
|
64
|
+
const existingKeys = Object.keys(existingModels);
|
|
65
|
+
const existingSummary = existingKeys.length > 0
|
|
66
|
+
? `Existing models: ${existingKeys.join(', ')}`
|
|
67
|
+
: 'No models configured yet.';
|
|
68
|
+
|
|
69
|
+
const { options, lookup } = buildModelSuggestions(availableCLIs, existingModels);
|
|
70
|
+
const prompt = `Unknown model key: "${modelKey}"\n\nHow would you like to configure this model?\n${existingSummary}`;
|
|
71
|
+
const slug = `model-${sanitizeSlug(modelKey)}`;
|
|
72
|
+
const interaction = createInteraction('choice', slug, {
|
|
73
|
+
prompt,
|
|
74
|
+
options,
|
|
75
|
+
allowCustom: true,
|
|
76
|
+
multiSelect: false
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const answer = await askHuman(prompt, { slug, interaction });
|
|
80
|
+
const parsed = await parseInteractionResponse(interaction, answer);
|
|
81
|
+
|
|
82
|
+
if (parsed.isCustom && parsed.customText) {
|
|
83
|
+
return parsed.customText.trim();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (parsed.selectedKey) {
|
|
87
|
+
const mapped = lookup[parsed.selectedKey];
|
|
88
|
+
if (mapped) return mapped;
|
|
89
|
+
return String(parsed.selectedKey).trim();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (typeof parsed.text === 'string' && parsed.text.trim()) {
|
|
93
|
+
return parsed.text.trim();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (typeof parsed.raw === 'string' && parsed.raw.trim()) {
|
|
97
|
+
return parsed.raw.trim();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
throw new Error('No model configuration provided.');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function resolveUnknownModel(modelKey, config, workflowDir, options = {}) {
|
|
104
|
+
if (!workflowDir) {
|
|
105
|
+
throw new Error('Cannot resolve model without a workflow directory.');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const configFile = path.join(workflowDir, 'config.js');
|
|
109
|
+
if (!fs.existsSync(configFile)) {
|
|
110
|
+
throw new Error(`config.js not found in ${workflowDir}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const existing = readModelFromConfig(configFile, modelKey);
|
|
114
|
+
if (existing) {
|
|
115
|
+
return existing;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const existingModels = config?.models || {};
|
|
119
|
+
const availableCLIs = options.availableCLIs || {};
|
|
120
|
+
const modelValue = await promptForModelConfig(modelKey, existingModels, availableCLIs);
|
|
121
|
+
|
|
122
|
+
if (!modelValue) {
|
|
123
|
+
throw new Error('Model configuration cannot be empty.');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
writeModelToConfig(configFile, modelKey, modelValue);
|
|
127
|
+
return modelValue;
|
|
128
|
+
}
|
package/package.json
CHANGED
|
@@ -9,14 +9,6 @@ response: choice
|
|
|
9
9
|
|
|
10
10
|
You are an assumptions and constraints analyst. Your job is to identify and validate assumptions before development.
|
|
11
11
|
|
|
12
|
-
## Context
|
|
13
|
-
Project Description: {{projectDescription}}
|
|
14
|
-
Scope: {{scope}}
|
|
15
|
-
Requirements: {{requirements}}
|
|
16
|
-
{{#if previousResponse}}
|
|
17
|
-
User's Previous Response: {{previousResponse}}
|
|
18
|
-
{{/if}}
|
|
19
|
-
|
|
20
12
|
## Instructions
|
|
21
13
|
|
|
22
14
|
Identify implicit assumptions that could impact the project. Consider:
|
|
@@ -7,14 +7,6 @@ format: json
|
|
|
7
7
|
|
|
8
8
|
You are a senior code reviewer. Review implementations for quality, correctness, and best practices.
|
|
9
9
|
|
|
10
|
-
## Context
|
|
11
|
-
Task: {{task}}
|
|
12
|
-
Implementation: {{implementation}}
|
|
13
|
-
Test Plan: {{testPlan}}
|
|
14
|
-
{{#if feedback}}
|
|
15
|
-
Previous Feedback: {{feedback}}
|
|
16
|
-
{{/if}}
|
|
17
|
-
|
|
18
10
|
## Instructions
|
|
19
11
|
|
|
20
12
|
Perform a thorough code review covering:
|
|
@@ -7,16 +7,6 @@ format: json
|
|
|
7
7
|
|
|
8
8
|
You are a senior software developer. Implement the task according to specifications.
|
|
9
9
|
|
|
10
|
-
## Context
|
|
11
|
-
Task: {{task}}
|
|
12
|
-
Phase: {{phase}}
|
|
13
|
-
Requirements: {{requirements}}
|
|
14
|
-
Test Plan: {{testPlan}}
|
|
15
|
-
Security Considerations: {{securityConsiderations}}
|
|
16
|
-
{{#if feedback}}
|
|
17
|
-
Previous Feedback (IMPORTANT - address these issues): {{feedback}}
|
|
18
|
-
{{/if}}
|
|
19
|
-
|
|
20
10
|
## Instructions
|
|
21
11
|
|
|
22
12
|
Implement the task following these principles:
|
|
@@ -9,13 +9,6 @@ response: choice
|
|
|
9
9
|
|
|
10
10
|
You are a requirements analysis specialist. Your job is to gather and clarify functional and non-functional requirements.
|
|
11
11
|
|
|
12
|
-
## Context
|
|
13
|
-
Project Description: {{projectDescription}}
|
|
14
|
-
Scope: {{scope}}
|
|
15
|
-
{{#if previousResponse}}
|
|
16
|
-
User's Previous Response: {{previousResponse}}
|
|
17
|
-
{{/if}}
|
|
18
|
-
|
|
19
12
|
## Instructions
|
|
20
13
|
|
|
21
14
|
Based on the project description and scope, identify requirements that need clarification. Consider:
|
|
@@ -7,16 +7,6 @@ format: json
|
|
|
7
7
|
|
|
8
8
|
You are a project planning specialist. Generate a phased development roadmap as structured JSON.
|
|
9
9
|
|
|
10
|
-
## Context
|
|
11
|
-
Project Description: {{projectDescription}}
|
|
12
|
-
Scope: {{scope}}
|
|
13
|
-
Requirements: {{requirements}}
|
|
14
|
-
Assumptions: {{assumptions}}
|
|
15
|
-
Security: {{security}}
|
|
16
|
-
{{#if feedback}}
|
|
17
|
-
User Feedback: {{feedback}}
|
|
18
|
-
{{/if}}
|
|
19
|
-
|
|
20
10
|
## Instructions
|
|
21
11
|
|
|
22
12
|
Create a phased roadmap as a JSON object. Each phase should:
|
|
@@ -9,12 +9,6 @@ response: choice
|
|
|
9
9
|
|
|
10
10
|
You are a project scope clarification specialist. Your job is to ensure the project scope is well-defined before development begins.
|
|
11
11
|
|
|
12
|
-
## Context
|
|
13
|
-
Project Description: {{projectDescription}}
|
|
14
|
-
{{#if previousResponse}}
|
|
15
|
-
User's Previous Response: {{previousResponse}}
|
|
16
|
-
{{/if}}
|
|
17
|
-
|
|
18
12
|
## Instructions
|
|
19
13
|
|
|
20
14
|
Analyze the project description and determine if the scope is clear. Consider:
|