@fieldwangai/agentflow 0.1.165 → 0.1.167
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/lib/agent-runners.mjs +131 -21
- package/bin/lib/ai-exploration.mjs +293 -0
- package/bin/lib/composer-agent.mjs +11 -0
- package/bin/lib/cursor-api-key-pool.mjs +246 -32
- package/bin/lib/cursor-model-catalog.mjs +152 -0
- package/bin/lib/repository-index-events.mjs +17 -0
- package/bin/lib/repository-index.mjs +522 -0
- package/bin/lib/ui-server.mjs +167 -0
- package/bin/lib/workspace-routes.mjs +947 -154
- package/bin/lib/workspace-server.mjs +3 -0
- package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-pVdrZ-Rl.js → WorkflowAssistantThread-B0i4F0Ab.js} +1 -1
- package/builtin/web-ui/dist/assets/index-BLTi7FF5.js +877 -0
- package/builtin/web-ui/dist/assets/index-yplDmRpj.css +1 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +2 -2
- package/skills/agentflow-ai-exploration/SKILL.md +127 -0
- package/skills/agentflow-ai-exploration/agents/openai.yaml +4 -0
- package/skills/agentflow-ai-exploration/references/protocol.md +120 -0
- package/skills/agentflow-ai-exploration/scripts/agentflow-ai-exploration.mjs +308 -0
- package/skills/agentflow-ai-exploration/scripts/auth-store.mjs +102 -0
- package/skills/agentflow-cli/runtime/bin/lib/skill-runtime.mjs +242 -137
- package/skills/agentflow-cli/runtime/package.json +1 -1
- package/builtin/web-ui/dist/assets/index-BQeq5tdj.css +0 -1
- package/builtin/web-ui/dist/assets/index-Czutb6ai.js +0 -873
|
@@ -1,71 +1,285 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
1
3
|
let nextCursorApiKeyCursor = 0;
|
|
2
|
-
const
|
|
4
|
+
const cursorApiKeyStates = new Map();
|
|
3
5
|
|
|
4
|
-
export function
|
|
6
|
+
export function parseCursorApiKeyRecords(value = "") {
|
|
5
7
|
const raw = String(value || "").trim();
|
|
6
8
|
if (!raw) return [];
|
|
9
|
+
const normalizeRecord = (item, index) => {
|
|
10
|
+
if (typeof item === "string") {
|
|
11
|
+
const key = item.trim();
|
|
12
|
+
return key ? { id: legacyKeyId(key), name: `Key ${index + 1}`, key } : null;
|
|
13
|
+
}
|
|
14
|
+
if (!item || typeof item !== "object") return null;
|
|
15
|
+
const key = String(item.key || "").trim();
|
|
16
|
+
if (!key) return null;
|
|
17
|
+
return {
|
|
18
|
+
id: String(item.id || "").trim() || legacyKeyId(key),
|
|
19
|
+
name: String(item.name || "").trim() || `Key ${index + 1}`,
|
|
20
|
+
key,
|
|
21
|
+
createdAt: String(item.createdAt || "").trim(),
|
|
22
|
+
};
|
|
23
|
+
};
|
|
7
24
|
try {
|
|
8
25
|
const parsed = JSON.parse(raw);
|
|
9
|
-
if (Array.isArray(parsed))
|
|
10
|
-
return parsed
|
|
11
|
-
.map((item) => {
|
|
12
|
-
if (typeof item === "string") return item.trim();
|
|
13
|
-
if (item && typeof item === "object") return String(item.key || "").trim();
|
|
14
|
-
return "";
|
|
15
|
-
})
|
|
16
|
-
.filter(Boolean);
|
|
17
|
-
}
|
|
26
|
+
if (Array.isArray(parsed)) return parsed.map(normalizeRecord).filter(Boolean);
|
|
18
27
|
} catch {
|
|
19
28
|
// Legacy comma-separated format.
|
|
20
29
|
}
|
|
21
|
-
return raw.split(",").map(
|
|
30
|
+
return raw.split(",").map(normalizeRecord).filter(Boolean);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function parseCursorApiKeyPool(value = "") {
|
|
34
|
+
return parseCursorApiKeyRecords(value).map((record) => record.key);
|
|
22
35
|
}
|
|
23
36
|
|
|
24
|
-
export function createCursorApiKeyAttempts(env = {}) {
|
|
25
|
-
const
|
|
26
|
-
if (
|
|
37
|
+
export function createCursorApiKeyAttempts(env = {}, now = Date.now()) {
|
|
38
|
+
const records = parseCursorApiKeyRecords(env.CURSOR_API_KEYS);
|
|
39
|
+
if (records.length === 0) return [undefined];
|
|
27
40
|
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
const selections = records.map((record, index) => ({
|
|
42
|
+
...record,
|
|
43
|
+
index,
|
|
44
|
+
total: records.length,
|
|
45
|
+
modelSelection: getCursorApiKeyModelSelection(record.id, now),
|
|
46
|
+
}));
|
|
47
|
+
const candidates = selections.filter((selection) => Boolean(selection.modelSelection));
|
|
48
|
+
// Preserve the old pool's last-resort behavior if every key is cooling down.
|
|
49
|
+
// Normal operation always uses candidates and therefore respects lane cooldowns.
|
|
50
|
+
const effective = candidates.length > 0
|
|
51
|
+
? candidates
|
|
52
|
+
: selections.map((selection) => ({
|
|
53
|
+
...selection,
|
|
54
|
+
modelSelection: { lane: "auto", modelId: "auto", modelName: "Auto" },
|
|
55
|
+
}));
|
|
33
56
|
const start = nextCursorApiKeyCursor % effective.length;
|
|
34
57
|
nextCursorApiKeyCursor += 1;
|
|
35
58
|
return [...effective.slice(start), ...effective.slice(0, start)];
|
|
36
59
|
}
|
|
37
60
|
|
|
38
|
-
export function
|
|
39
|
-
|
|
61
|
+
export function getCursorApiKeyModelSelection(keyOrSelection, now = Date.now()) {
|
|
62
|
+
const keyId = selectionId(keyOrSelection);
|
|
63
|
+
const keyState = cursorApiKeyStates.get(keyId);
|
|
64
|
+
const autoBlocked = (keyState?.auto?.blockedUntil || 0) > now;
|
|
65
|
+
if (!autoBlocked) return { lane: "auto", modelId: "auto", modelName: "Auto" };
|
|
66
|
+
|
|
67
|
+
if (keyState?.auto?.errorCategory !== "explicit_limit" || keyState.auto.fallbackEligible !== true) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
const fallbackModel = keyState.fallbackModel;
|
|
71
|
+
if (!fallbackModel || (keyState.fallback?.blockedUntil || 0) > now) return undefined;
|
|
72
|
+
return {
|
|
73
|
+
lane: "fallback",
|
|
74
|
+
modelId: fallbackModel.id,
|
|
75
|
+
modelName: fallbackModel.displayName,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function recordCursorApiKeyFallbackModel(keyOrSelection, model) {
|
|
80
|
+
if (!model?.id) return;
|
|
81
|
+
const keyId = selectionId(keyOrSelection);
|
|
82
|
+
const keyState = cursorApiKeyStates.get(keyId) || {};
|
|
83
|
+
keyState.fallbackModel = { ...model };
|
|
84
|
+
cursorApiKeyStates.set(keyId, keyState);
|
|
40
85
|
}
|
|
41
86
|
|
|
42
|
-
export function
|
|
43
|
-
|
|
87
|
+
export function markCursorApiKeyLaneBlocked(
|
|
88
|
+
keyOrSelection,
|
|
89
|
+
lane,
|
|
90
|
+
cooldownMinutes = 30,
|
|
91
|
+
errorText = "",
|
|
92
|
+
now = Date.now(),
|
|
93
|
+
evidence = {},
|
|
94
|
+
) {
|
|
95
|
+
const keyId = selectionId(keyOrSelection);
|
|
96
|
+
if (!keyId || !["auto", "fallback"].includes(lane)) return 0;
|
|
44
97
|
const minutes = Math.max(1, Number(cooldownMinutes) || 30);
|
|
45
|
-
|
|
98
|
+
const keyState = cursorApiKeyStates.get(keyId) || {};
|
|
99
|
+
const currentLane = keyState[lane] || {};
|
|
100
|
+
const blockedUntil = Math.max(currentLane.blockedUntil || 0, now + minutes * 60 * 1000);
|
|
101
|
+
const errorCategory = classifyCursorApiKeyLimitError(errorText);
|
|
102
|
+
const fallbackModel = keyState.fallbackModel;
|
|
103
|
+
const lastFailure = {
|
|
104
|
+
triggeredAt: new Date(now).toISOString(),
|
|
105
|
+
...(errorCategory ? { errorCategory } : {}),
|
|
106
|
+
errorPreview: sanitizeErrorPreview(errorText),
|
|
107
|
+
lane,
|
|
108
|
+
modelId: String(evidence?.modelId || (lane === "auto" ? "auto" : fallbackModel?.id || "fallback")),
|
|
109
|
+
modelName: String(evidence?.modelName || (lane === "auto" ? "Auto" : fallbackModel?.displayName || "降级模型")),
|
|
110
|
+
};
|
|
111
|
+
keyState[lane] = {
|
|
112
|
+
...currentLane,
|
|
113
|
+
blockedUntil,
|
|
114
|
+
...(errorCategory ? { errorCategory } : {}),
|
|
115
|
+
...(lane === "auto" ? { fallbackEligible: isCursorAutoFallbackEligible(errorText) } : {}),
|
|
116
|
+
lastFailure,
|
|
117
|
+
};
|
|
118
|
+
keyState.lastFailure = lastFailure;
|
|
119
|
+
cursorApiKeyStates.set(keyId, keyState);
|
|
120
|
+
return blockedUntil;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function clearCursorApiKeyLaneCooldown(keyOrSelection, lane) {
|
|
124
|
+
const keyState = cursorApiKeyStates.get(selectionId(keyOrSelection));
|
|
125
|
+
if (!keyState?.[lane]) return false;
|
|
126
|
+
keyState[lane].blockedUntil = 0;
|
|
127
|
+
delete keyState[lane].errorCategory;
|
|
128
|
+
delete keyState[lane].fallbackEligible;
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function clearCursorApiKeyCooldown(keyOrSelection) {
|
|
133
|
+
const keyState = cursorApiKeyStates.get(selectionId(keyOrSelection));
|
|
134
|
+
if (!keyState) return false;
|
|
135
|
+
let changed = false;
|
|
136
|
+
for (const lane of ["auto", "fallback"]) {
|
|
137
|
+
if (!keyState[lane] || (keyState[lane].blockedUntil || 0) <= 0) continue;
|
|
138
|
+
keyState[lane].blockedUntil = 0;
|
|
139
|
+
delete keyState[lane].errorCategory;
|
|
140
|
+
delete keyState[lane].fallbackEligible;
|
|
141
|
+
changed = true;
|
|
142
|
+
}
|
|
143
|
+
return changed;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function recordCursorApiKeyUsage(keyOrSelection, selection = {}, now = Date.now()) {
|
|
147
|
+
const keyId = selectionId(keyOrSelection);
|
|
148
|
+
if (!keyId || keyId === "default") return;
|
|
149
|
+
const keyState = cursorApiKeyStates.get(keyId) || {};
|
|
150
|
+
keyState.lastUsedAt = new Date(now).toISOString();
|
|
151
|
+
keyState.lastSelection = {
|
|
152
|
+
lane: selection?.lane === "fallback" ? "fallback" : "auto",
|
|
153
|
+
modelId: String(selection?.modelId || "auto"),
|
|
154
|
+
modelName: String(selection?.modelName || "Auto"),
|
|
155
|
+
};
|
|
156
|
+
cursorApiKeyStates.set(keyId, keyState);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function getCursorApiKeyPoolStatuses(records = [], now = Date.now()) {
|
|
160
|
+
return (Array.isArray(records) ? records : []).map((record) => {
|
|
161
|
+
const id = selectionId(record);
|
|
162
|
+
const keyState = cursorApiKeyStates.get(id);
|
|
163
|
+
const selection = getCursorApiKeyModelSelection(id, now);
|
|
164
|
+
const laneCooldowns = buildLaneCooldowns(keyState, now);
|
|
165
|
+
const common = {
|
|
166
|
+
id,
|
|
167
|
+
...(keyState?.lastUsedAt ? { lastUsedAt: keyState.lastUsedAt } : {}),
|
|
168
|
+
...(keyState?.fallbackModel ? { fallbackModel: keyState.fallbackModel } : {}),
|
|
169
|
+
laneCooldowns,
|
|
170
|
+
...(keyState?.lastFailure ? { lastFailure: keyState.lastFailure } : {}),
|
|
171
|
+
};
|
|
172
|
+
if (selection) {
|
|
173
|
+
return {
|
|
174
|
+
...common,
|
|
175
|
+
status: "available",
|
|
176
|
+
activeLane: selection.lane,
|
|
177
|
+
activeModelId: selection.modelId,
|
|
178
|
+
activeModelName: selection.modelName,
|
|
179
|
+
degraded: selection.lane === "fallback",
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const activeCooldowns = laneCooldowns.filter((item) => item.remainingSeconds > 0);
|
|
183
|
+
const earliest = activeCooldowns.reduce(
|
|
184
|
+
(result, item) => !result || item.remainingSeconds < result.remainingSeconds ? item : result,
|
|
185
|
+
undefined,
|
|
186
|
+
);
|
|
187
|
+
const autoState = keyState?.auto;
|
|
188
|
+
return {
|
|
189
|
+
...common,
|
|
190
|
+
status: "cooling_down",
|
|
191
|
+
...(autoState?.errorCategory ? { errorCategory: autoState.errorCategory } : {}),
|
|
192
|
+
blockedUntil: earliest?.blockedUntil || new Date(Math.max(now, autoState?.blockedUntil || now)).toISOString(),
|
|
193
|
+
remainingSeconds: earliest?.remainingSeconds || Math.max(0, Math.ceil(((autoState?.blockedUntil || now) - now) / 1000)),
|
|
194
|
+
};
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function cursorApiKeyEnv(selection) {
|
|
199
|
+
return selection?.key ? { CURSOR_API_KEY: selection.key } : {};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function markCursorApiKeyQuotaBlocked(selection, cooldownMinutes = 30, errorText = "") {
|
|
203
|
+
return markCursorApiKeyLaneBlocked(selection, "auto", cooldownMinutes, errorText);
|
|
46
204
|
}
|
|
47
205
|
|
|
48
206
|
export function cursorApiKeyLabel(selection) {
|
|
49
207
|
if (!selection) return "default";
|
|
50
|
-
return `${selection.index + 1}/${selection.total}`;
|
|
208
|
+
return selection.name ? `${selection.index + 1}/${selection.total} (${selection.name})` : `${selection.index + 1}/${selection.total}`;
|
|
51
209
|
}
|
|
52
210
|
|
|
53
|
-
export function
|
|
211
|
+
export function isCursorAutoFallbackEligible(error = "") {
|
|
212
|
+
const text = String(error || "");
|
|
213
|
+
return [/\bout\s+of\s+usage\b/i, /\busage\s+limit\b/i].some((pattern) => pattern.test(text));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function classifyCursorApiKeyLimitError(error = "") {
|
|
54
217
|
const text = String(error || "");
|
|
55
|
-
if (!text) return
|
|
56
|
-
|
|
218
|
+
if (!text) return undefined;
|
|
219
|
+
const isExplicitLimit = [
|
|
57
220
|
/\b429\b/i,
|
|
58
221
|
/rate[_\s-]*limit/i,
|
|
59
222
|
/too many requests/i,
|
|
60
223
|
/quota/i,
|
|
61
224
|
/usage\s+limit/i,
|
|
225
|
+
/out\s+of\s+usage/i,
|
|
62
226
|
/limit\s+(?:exceeded|reached)/i,
|
|
63
227
|
/exceeded\s+(?:your\s+)?limit/i,
|
|
64
|
-
/
|
|
65
|
-
/
|
|
228
|
+
/insufficient[_\s-]*quota/i,
|
|
229
|
+
/credit[s]?\s+(?:exhausted|limit)/i,
|
|
230
|
+
/request\s+limit/i,
|
|
66
231
|
].some((pattern) => pattern.test(text));
|
|
232
|
+
if (isExplicitLimit) return "explicit_limit";
|
|
233
|
+
if (/resource[_\s-]*exhausted/i.test(text)) return "resource_exhausted";
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function isCursorQuotaError(error = "") {
|
|
238
|
+
return classifyCursorApiKeyLimitError(error) !== undefined;
|
|
67
239
|
}
|
|
68
240
|
|
|
69
|
-
export function cursorApiKeyCooldownMinutes(env = {}) {
|
|
241
|
+
export function cursorApiKeyCooldownMinutes(env = {}, errorText = "") {
|
|
242
|
+
if (classifyCursorApiKeyLimitError(errorText) === "resource_exhausted") {
|
|
243
|
+
return Math.max(1, Number(env.AGENTFLOW_CURSOR_API_KEY_RESOURCE_EXHAUSTED_COOLDOWN_MINUTES || 3) || 3);
|
|
244
|
+
}
|
|
70
245
|
return Math.max(1, Number(env.AGENTFLOW_CURSOR_API_KEY_COOLDOWN_MINUTES || env.CURSOR_API_KEY_COOLDOWN_MINUTES || 30) || 30);
|
|
71
246
|
}
|
|
247
|
+
|
|
248
|
+
export function resetCursorApiKeyPoolForTests() {
|
|
249
|
+
nextCursorApiKeyCursor = 0;
|
|
250
|
+
cursorApiKeyStates.clear();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function selectionId(keyOrSelection) {
|
|
254
|
+
if (typeof keyOrSelection === "string") return keyOrSelection;
|
|
255
|
+
if (keyOrSelection?.id) return String(keyOrSelection.id);
|
|
256
|
+
if (keyOrSelection?.key) return legacyKeyId(String(keyOrSelection.key));
|
|
257
|
+
return "default";
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function legacyKeyId(key) {
|
|
261
|
+
return `legacy_${createHash("sha256").update(String(key || "")).digest("hex").slice(0, 16)}`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function buildLaneCooldowns(keyState, now) {
|
|
265
|
+
if (!keyState) return [];
|
|
266
|
+
const lanes = [];
|
|
267
|
+
for (const lane of ["auto", "fallback"]) {
|
|
268
|
+
const laneState = keyState[lane];
|
|
269
|
+
if (!laneState || (laneState.blockedUntil || 0) <= now) continue;
|
|
270
|
+
const fallbackModel = keyState.fallbackModel;
|
|
271
|
+
lanes.push({
|
|
272
|
+
lane,
|
|
273
|
+
modelId: lane === "auto" ? "auto" : fallbackModel?.id || "fallback",
|
|
274
|
+
modelName: lane === "auto" ? "Auto" : fallbackModel?.displayName || "降级模型",
|
|
275
|
+
...(laneState.errorCategory ? { errorCategory: laneState.errorCategory } : {}),
|
|
276
|
+
blockedUntil: new Date(laneState.blockedUntil).toISOString(),
|
|
277
|
+
remainingSeconds: Math.ceil((laneState.blockedUntil - now) / 1000),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
return lanes;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function sanitizeErrorPreview(errorText) {
|
|
284
|
+
return String(errorText || "").replace(/(?:sk|key)[-_][A-Za-z0-9_-]{8,}/gi, "[redacted]").trim().slice(0, 500);
|
|
285
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
const MODEL_CATALOG_TIMEOUT_MS = 15_000;
|
|
4
|
+
const MODEL_CATALOG_CACHE_MS = 15 * 60 * 1000;
|
|
5
|
+
const MAX_CATALOG_OUTPUT_LENGTH = 64 * 1024;
|
|
6
|
+
const ANSI_ESCAPE_PATTERN = /[\u001B\u009B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[-a-zA-Z\d/#&.:=?%@~_]+)*)?\u0007)|(?:(?:\d{1,4}(?:[;:]\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
|
|
7
|
+
|
|
8
|
+
const modelCatalogCache = new Map();
|
|
9
|
+
|
|
10
|
+
export async function discoverCursorModels({
|
|
11
|
+
keyId,
|
|
12
|
+
cwd,
|
|
13
|
+
env,
|
|
14
|
+
command = "agent",
|
|
15
|
+
forceRefresh = false,
|
|
16
|
+
} = {}) {
|
|
17
|
+
const now = Date.now();
|
|
18
|
+
const cacheKey = String(keyId || "default");
|
|
19
|
+
const cached = modelCatalogCache.get(cacheKey);
|
|
20
|
+
if (!forceRefresh && cached && cached.expiresAt > now) {
|
|
21
|
+
return buildCatalogResult(cached.models);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const commandResult = await runModelsCommand({ cwd, env, command });
|
|
25
|
+
if (!commandResult.success) return { models: [], error: commandResult.error };
|
|
26
|
+
|
|
27
|
+
const models = parseCursorModelsOutput(commandResult.output);
|
|
28
|
+
if (models.length === 0) {
|
|
29
|
+
return { models: [], error: "Cursor CLI did not return any available models." };
|
|
30
|
+
}
|
|
31
|
+
modelCatalogCache.set(cacheKey, {
|
|
32
|
+
expiresAt: now + MODEL_CATALOG_CACHE_MS,
|
|
33
|
+
models,
|
|
34
|
+
});
|
|
35
|
+
return buildCatalogResult(models);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function parseCursorModelsOutput(output = "") {
|
|
39
|
+
const lines = stripAnsi(String(output || ""))
|
|
40
|
+
.split(/\r?\n/)
|
|
41
|
+
.map((line) => line.trim())
|
|
42
|
+
.filter(Boolean);
|
|
43
|
+
const models = [];
|
|
44
|
+
let insideModelList = false;
|
|
45
|
+
for (const line of lines) {
|
|
46
|
+
if (/^available models$/i.test(line)) {
|
|
47
|
+
insideModelList = true;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (/^tip:/i.test(line)) break;
|
|
51
|
+
if (!insideModelList || /^no models available/i.test(line)) continue;
|
|
52
|
+
|
|
53
|
+
const flagsMatch = line.match(/\s+\(((?:current|default)(?:,\s*(?:current|default))*)\)$/i);
|
|
54
|
+
const flags = flagsMatch?.[1]?.toLowerCase() || "";
|
|
55
|
+
const value = flagsMatch ? line.slice(0, flagsMatch.index).trim() : line;
|
|
56
|
+
const separatorIndex = value.indexOf(" - ");
|
|
57
|
+
const id = (separatorIndex >= 0 ? value.slice(0, separatorIndex) : value).trim();
|
|
58
|
+
const displayName = (separatorIndex >= 0 ? value.slice(separatorIndex + 3) : id).trim();
|
|
59
|
+
if (!id || (separatorIndex < 0 && /\s/.test(id))) continue;
|
|
60
|
+
models.push({
|
|
61
|
+
id,
|
|
62
|
+
displayName: displayName || id,
|
|
63
|
+
isDefault: flags.split(/,\s*/).includes("default"),
|
|
64
|
+
isCurrent: flags.split(/,\s*/).includes("current"),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return dedupeModels(models);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function selectComposerFallbackModel(models = []) {
|
|
71
|
+
return models.find((model) => [model?.id, model?.displayName].some((value) =>
|
|
72
|
+
/(^|[^a-z0-9])composer(?=$|[^a-z0-9])/i.test(String(value || ""))
|
|
73
|
+
));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function clearCursorModelCatalogCache() {
|
|
77
|
+
modelCatalogCache.clear();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function buildCatalogResult(models) {
|
|
81
|
+
const fallback = selectComposerFallbackModel(models);
|
|
82
|
+
return {
|
|
83
|
+
models,
|
|
84
|
+
...(fallback ? {
|
|
85
|
+
fallbackModel: {
|
|
86
|
+
id: fallback.id,
|
|
87
|
+
displayName: formatComposerModelName(fallback),
|
|
88
|
+
discoveredAt: new Date().toISOString(),
|
|
89
|
+
},
|
|
90
|
+
} : {}),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function runModelsCommand({ cwd, env, command }) {
|
|
95
|
+
return new Promise((resolve) => {
|
|
96
|
+
let stdout = "";
|
|
97
|
+
let stderr = "";
|
|
98
|
+
let settled = false;
|
|
99
|
+
const child = spawn(command || "agent", ["models"], {
|
|
100
|
+
cwd,
|
|
101
|
+
shell: false,
|
|
102
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
103
|
+
env,
|
|
104
|
+
});
|
|
105
|
+
const finish = (result) => {
|
|
106
|
+
if (settled) return;
|
|
107
|
+
settled = true;
|
|
108
|
+
clearTimeout(timeoutId);
|
|
109
|
+
resolve(result);
|
|
110
|
+
};
|
|
111
|
+
const timeoutId = setTimeout(() => {
|
|
112
|
+
child.kill("SIGTERM");
|
|
113
|
+
finish({ success: false, error: "Cursor CLI model discovery timed out." });
|
|
114
|
+
}, MODEL_CATALOG_TIMEOUT_MS);
|
|
115
|
+
child.stdout?.on("data", (chunk) => {
|
|
116
|
+
if (stdout.length < MAX_CATALOG_OUTPUT_LENGTH) stdout += chunk.toString();
|
|
117
|
+
});
|
|
118
|
+
child.stderr?.on("data", (chunk) => {
|
|
119
|
+
if (stderr.length < MAX_CATALOG_OUTPUT_LENGTH) stderr += chunk.toString();
|
|
120
|
+
});
|
|
121
|
+
child.on("error", (error) => finish({ success: false, error: error.message }));
|
|
122
|
+
child.on("exit", (code) => {
|
|
123
|
+
if (code === 0) {
|
|
124
|
+
finish({ success: true, output: stdout.slice(0, MAX_CATALOG_OUTPUT_LENGTH) });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
finish({
|
|
128
|
+
success: false,
|
|
129
|
+
error: stripAnsi(stderr || stdout || `Cursor CLI models exited with code ${code}`).trim().slice(0, 1000),
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function stripAnsi(value) {
|
|
136
|
+
return String(value || "").replace(ANSI_ESCAPE_PATTERN, "");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function formatComposerModelName(model) {
|
|
140
|
+
if (/(^|[^a-z0-9])composer(?=$|[^a-z0-9])/i.test(model.displayName)) return model.displayName;
|
|
141
|
+
if (model.displayName && model.displayName !== model.id) return `${model.id} · ${model.displayName}`;
|
|
142
|
+
return model.id;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function dedupeModels(models) {
|
|
146
|
+
const seen = new Set();
|
|
147
|
+
return models.filter((model) => {
|
|
148
|
+
if (seen.has(model.id)) return false;
|
|
149
|
+
seen.add(model.id);
|
|
150
|
+
return true;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const runFinishedListeners = new Set();
|
|
2
|
+
|
|
3
|
+
export function onRepositoryRunFinished(listener) {
|
|
4
|
+
if (typeof listener !== "function") return () => {};
|
|
5
|
+
runFinishedListeners.add(listener);
|
|
6
|
+
return () => runFinishedListeners.delete(listener);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function emitRepositoryRunFinished(workspaceRoot, run, status) {
|
|
10
|
+
for (const listener of runFinishedListeners) {
|
|
11
|
+
try {
|
|
12
|
+
listener(workspaceRoot, run, status);
|
|
13
|
+
} catch {
|
|
14
|
+
// Derived repository updates must never break the authoritative run ledger.
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|