chainlesschain 0.37.9 → 0.37.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +309 -19
  2. package/bin/chainlesschain.js +4 -0
  3. package/package.json +1 -1
  4. package/src/commands/a2a.js +374 -0
  5. package/src/commands/audit.js +286 -0
  6. package/src/commands/auth.js +387 -0
  7. package/src/commands/bi.js +240 -0
  8. package/src/commands/browse.js +184 -0
  9. package/src/commands/cowork.js +317 -0
  10. package/src/commands/did.js +376 -0
  11. package/src/commands/economy.js +375 -0
  12. package/src/commands/encrypt.js +233 -0
  13. package/src/commands/evolution.js +398 -0
  14. package/src/commands/export.js +125 -0
  15. package/src/commands/git.js +215 -0
  16. package/src/commands/hmemory.js +273 -0
  17. package/src/commands/hook.js +260 -0
  18. package/src/commands/import.js +259 -0
  19. package/src/commands/init.js +184 -0
  20. package/src/commands/instinct.js +202 -0
  21. package/src/commands/llm.js +155 -4
  22. package/src/commands/lowcode.js +320 -0
  23. package/src/commands/mcp.js +302 -0
  24. package/src/commands/memory.js +282 -0
  25. package/src/commands/note.js +187 -0
  26. package/src/commands/org.js +505 -0
  27. package/src/commands/p2p.js +274 -0
  28. package/src/commands/plugin.js +451 -0
  29. package/src/commands/sandbox.js +366 -0
  30. package/src/commands/search.js +237 -0
  31. package/src/commands/session.js +238 -0
  32. package/src/commands/skill.js +254 -201
  33. package/src/commands/sync.js +249 -0
  34. package/src/commands/tokens.js +214 -0
  35. package/src/commands/wallet.js +416 -0
  36. package/src/commands/workflow.js +359 -0
  37. package/src/commands/zkp.js +277 -0
  38. package/src/index.js +93 -1
  39. package/src/lib/a2a-protocol.js +371 -0
  40. package/src/lib/agent-coordinator.js +273 -0
  41. package/src/lib/agent-economy.js +369 -0
  42. package/src/lib/app-builder.js +377 -0
  43. package/src/lib/audit-logger.js +364 -0
  44. package/src/lib/bi-engine.js +299 -0
  45. package/src/lib/bm25-search.js +322 -0
  46. package/src/lib/browser-automation.js +216 -0
  47. package/src/lib/cowork/ab-comparator-cli.js +180 -0
  48. package/src/lib/cowork/code-knowledge-graph-cli.js +232 -0
  49. package/src/lib/cowork/debate-review-cli.js +144 -0
  50. package/src/lib/cowork/decision-kb-cli.js +153 -0
  51. package/src/lib/cowork/project-style-analyzer-cli.js +168 -0
  52. package/src/lib/cowork-adapter.js +106 -0
  53. package/src/lib/crypto-manager.js +246 -0
  54. package/src/lib/did-manager.js +270 -0
  55. package/src/lib/ensure-utf8.js +59 -0
  56. package/src/lib/evolution-system.js +508 -0
  57. package/src/lib/git-integration.js +220 -0
  58. package/src/lib/hierarchical-memory.js +471 -0
  59. package/src/lib/hook-manager.js +387 -0
  60. package/src/lib/instinct-manager.js +190 -0
  61. package/src/lib/knowledge-exporter.js +302 -0
  62. package/src/lib/knowledge-importer.js +293 -0
  63. package/src/lib/llm-providers.js +325 -0
  64. package/src/lib/mcp-client.js +413 -0
  65. package/src/lib/memory-manager.js +211 -0
  66. package/src/lib/note-versioning.js +244 -0
  67. package/src/lib/org-manager.js +424 -0
  68. package/src/lib/p2p-manager.js +317 -0
  69. package/src/lib/pdf-parser.js +96 -0
  70. package/src/lib/permission-engine.js +374 -0
  71. package/src/lib/plan-mode.js +333 -0
  72. package/src/lib/plugin-manager.js +430 -0
  73. package/src/lib/project-detector.js +53 -0
  74. package/src/lib/response-cache.js +156 -0
  75. package/src/lib/sandbox-v2.js +503 -0
  76. package/src/lib/service-container.js +183 -0
  77. package/src/lib/session-manager.js +189 -0
  78. package/src/lib/skill-loader.js +274 -0
  79. package/src/lib/sync-manager.js +347 -0
  80. package/src/lib/token-tracker.js +200 -0
  81. package/src/lib/wallet-manager.js +348 -0
  82. package/src/lib/workflow-engine.js +503 -0
  83. package/src/lib/zkp-engine.js +241 -0
  84. package/src/repl/agent-repl.js +259 -124
@@ -0,0 +1,322 @@
1
+ /**
2
+ * BM25 search engine for CLI
3
+ *
4
+ * Implements Okapi BM25 ranking algorithm for keyword-based search.
5
+ * Lightweight port of desktop-app-vue/src/main/rag/bm25-search.js
6
+ * No external dependencies — uses simple tokenization.
7
+ */
8
+
9
+ /**
10
+ * Simple tokenizer for text
11
+ * Handles both Chinese and English text
12
+ */
13
+ function tokenize(text, language = "auto") {
14
+ if (!text || typeof text !== "string") return [];
15
+
16
+ const normalized = text.toLowerCase().trim();
17
+
18
+ // Detect language
19
+ const hasChinese = /[\u4e00-\u9fff]/.test(normalized);
20
+ const lang = language === "auto" ? (hasChinese ? "zh" : "en") : language;
21
+
22
+ if (lang === "zh") {
23
+ // Chinese: character-level + word-level bigrams
24
+ const chars = normalized.match(/[\u4e00-\u9fff]/g) || [];
25
+ const words = normalized.match(/[a-z0-9]+/g) || [];
26
+ const bigrams = [];
27
+ for (let i = 0; i < chars.length - 1; i++) {
28
+ bigrams.push(chars[i] + chars[i + 1]);
29
+ }
30
+ return [...chars, ...bigrams, ...words];
31
+ }
32
+
33
+ // English: word tokenization with stop word removal
34
+ const STOP_WORDS = new Set([
35
+ "a",
36
+ "an",
37
+ "the",
38
+ "is",
39
+ "are",
40
+ "was",
41
+ "were",
42
+ "be",
43
+ "been",
44
+ "being",
45
+ "have",
46
+ "has",
47
+ "had",
48
+ "do",
49
+ "does",
50
+ "did",
51
+ "will",
52
+ "would",
53
+ "could",
54
+ "should",
55
+ "may",
56
+ "might",
57
+ "shall",
58
+ "can",
59
+ "to",
60
+ "of",
61
+ "in",
62
+ "for",
63
+ "on",
64
+ "with",
65
+ "at",
66
+ "by",
67
+ "from",
68
+ "as",
69
+ "into",
70
+ "through",
71
+ "during",
72
+ "before",
73
+ "after",
74
+ "above",
75
+ "below",
76
+ "between",
77
+ "but",
78
+ "and",
79
+ "or",
80
+ "not",
81
+ "no",
82
+ "nor",
83
+ "so",
84
+ "if",
85
+ "then",
86
+ "than",
87
+ "too",
88
+ "very",
89
+ "just",
90
+ "about",
91
+ "up",
92
+ "out",
93
+ "it",
94
+ "its",
95
+ "this",
96
+ "that",
97
+ "these",
98
+ "those",
99
+ "i",
100
+ "me",
101
+ "my",
102
+ "we",
103
+ "our",
104
+ "you",
105
+ "your",
106
+ "he",
107
+ "him",
108
+ "his",
109
+ "she",
110
+ "her",
111
+ "they",
112
+ "them",
113
+ "their",
114
+ "what",
115
+ "which",
116
+ ]);
117
+
118
+ return normalized
119
+ .split(/[^a-z0-9\u4e00-\u9fff]+/)
120
+ .filter((w) => w.length > 1 && !STOP_WORDS.has(w));
121
+ }
122
+
123
+ /**
124
+ * BM25 Search Engine
125
+ */
126
+ export class BM25Search {
127
+ /**
128
+ * @param {object} options
129
+ * @param {number} [options.k1=1.5] - Term frequency saturation parameter
130
+ * @param {number} [options.b=0.75] - Length normalization parameter
131
+ * @param {string} [options.language="auto"] - Language for tokenization
132
+ */
133
+ constructor(options = {}) {
134
+ this.k1 = options.k1 || 1.5;
135
+ this.b = options.b || 0.75;
136
+ this.language = options.language || "auto";
137
+
138
+ // Index state
139
+ this.documents = []; // Array of { id, tokens, originalDoc }
140
+ this.df = new Map(); // document frequency per term
141
+ this.avgDl = 0; // average document length
142
+ this.totalDocs = 0;
143
+ }
144
+
145
+ /**
146
+ * Index a batch of documents
147
+ * @param {Array<{id: string, title?: string, content?: string}>} documents
148
+ */
149
+ indexDocuments(documents) {
150
+ this.documents = [];
151
+ this.df = new Map();
152
+
153
+ for (const doc of documents) {
154
+ const text = [doc.title || "", doc.content || ""].join(" ");
155
+ const tokens = tokenize(text, this.language);
156
+
157
+ this.documents.push({
158
+ id: doc.id,
159
+ tokens,
160
+ originalDoc: doc,
161
+ });
162
+
163
+ // Count document frequency
164
+ const seen = new Set();
165
+ for (const token of tokens) {
166
+ if (!seen.has(token)) {
167
+ seen.add(token);
168
+ this.df.set(token, (this.df.get(token) || 0) + 1);
169
+ }
170
+ }
171
+ }
172
+
173
+ this.totalDocs = this.documents.length;
174
+ this.avgDl =
175
+ this.totalDocs > 0
176
+ ? this.documents.reduce((sum, d) => sum + d.tokens.length, 0) /
177
+ this.totalDocs
178
+ : 0;
179
+ }
180
+
181
+ /**
182
+ * Add a single document to the index
183
+ */
184
+ addDocument(doc) {
185
+ const text = [doc.title || "", doc.content || ""].join(" ");
186
+ const tokens = tokenize(text, this.language);
187
+
188
+ this.documents.push({
189
+ id: doc.id,
190
+ tokens,
191
+ originalDoc: doc,
192
+ });
193
+
194
+ const seen = new Set();
195
+ for (const token of tokens) {
196
+ if (!seen.has(token)) {
197
+ seen.add(token);
198
+ this.df.set(token, (this.df.get(token) || 0) + 1);
199
+ }
200
+ }
201
+
202
+ this.totalDocs = this.documents.length;
203
+ this.avgDl =
204
+ this.documents.reduce((sum, d) => sum + d.tokens.length, 0) /
205
+ this.totalDocs;
206
+ }
207
+
208
+ /**
209
+ * Remove a document from the index
210
+ */
211
+ removeDocument(docId) {
212
+ const idx = this.documents.findIndex((d) => d.id === docId);
213
+ if (idx === -1) return false;
214
+
215
+ const doc = this.documents[idx];
216
+ const seen = new Set();
217
+ for (const token of doc.tokens) {
218
+ if (!seen.has(token)) {
219
+ seen.add(token);
220
+ const count = this.df.get(token) || 0;
221
+ if (count <= 1) {
222
+ this.df.delete(token);
223
+ } else {
224
+ this.df.set(token, count - 1);
225
+ }
226
+ }
227
+ }
228
+
229
+ this.documents.splice(idx, 1);
230
+ this.totalDocs = this.documents.length;
231
+ this.avgDl =
232
+ this.totalDocs > 0
233
+ ? this.documents.reduce((sum, d) => sum + d.tokens.length, 0) /
234
+ this.totalDocs
235
+ : 0;
236
+
237
+ return true;
238
+ }
239
+
240
+ /**
241
+ * Search for documents matching a query
242
+ * @param {string} query
243
+ * @param {object} [options]
244
+ * @param {number} [options.topK=10]
245
+ * @param {number} [options.threshold=0]
246
+ * @returns {Array<{id: string, score: number, doc: object}>}
247
+ */
248
+ search(query, options = {}) {
249
+ const topK = options.topK || 10;
250
+ const threshold = options.threshold || 0;
251
+
252
+ if (this.totalDocs === 0) return [];
253
+
254
+ const queryTokens = tokenize(query, this.language);
255
+ if (queryTokens.length === 0) return [];
256
+
257
+ const scores = [];
258
+
259
+ for (let i = 0; i < this.documents.length; i++) {
260
+ const score = this._calculateBM25(queryTokens, i);
261
+ if (score > threshold) {
262
+ scores.push({
263
+ id: this.documents[i].id,
264
+ score,
265
+ doc: this.documents[i].originalDoc,
266
+ });
267
+ }
268
+ }
269
+
270
+ scores.sort((a, b) => b.score - a.score);
271
+ return scores.slice(0, topK);
272
+ }
273
+
274
+ /**
275
+ * Calculate BM25 score for a document
276
+ */
277
+ _calculateBM25(queryTokens, docIdx) {
278
+ const doc = this.documents[docIdx];
279
+ const dl = doc.tokens.length;
280
+ let score = 0;
281
+
282
+ // Build term frequency map for this document
283
+ const tf = new Map();
284
+ for (const token of doc.tokens) {
285
+ tf.set(token, (tf.get(token) || 0) + 1);
286
+ }
287
+
288
+ for (const term of queryTokens) {
289
+ const termFreq = tf.get(term) || 0;
290
+ if (termFreq === 0) continue;
291
+
292
+ const docFreq = this.df.get(term) || 0;
293
+ if (docFreq === 0) continue;
294
+
295
+ // IDF component
296
+ const idf = Math.log(
297
+ (this.totalDocs - docFreq + 0.5) / (docFreq + 0.5) + 1,
298
+ );
299
+
300
+ // TF component with length normalization
301
+ const avgDl = this.avgDl || 1;
302
+ const tfNorm =
303
+ (termFreq * (this.k1 + 1)) /
304
+ (termFreq + this.k1 * (1 - this.b + this.b * (dl / avgDl)));
305
+
306
+ score += idf * tfNorm;
307
+ }
308
+
309
+ return score;
310
+ }
311
+
312
+ /**
313
+ * Get index statistics
314
+ */
315
+ getStats() {
316
+ return {
317
+ totalDocuments: this.totalDocs,
318
+ uniqueTerms: this.df.size,
319
+ avgDocumentLength: Math.round(this.avgDl),
320
+ };
321
+ }
322
+ }
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Headless browser automation — fetch, scrape, and extract content from web pages.
3
+ * Uses built-in fetch for basic operations, optional playwright for screenshots.
4
+ */
5
+
6
+ /**
7
+ * Fetch a URL and return the raw HTML.
8
+ */
9
+ export async function fetchPage(url, options = {}) {
10
+ const timeout = options.timeout || 30000;
11
+ const controller = new AbortController();
12
+ const timer = setTimeout(() => controller.abort(), timeout);
13
+
14
+ try {
15
+ const headers = {
16
+ "User-Agent": "ChainlessChain-CLI/0.37.9",
17
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
18
+ ...(options.headers || {}),
19
+ };
20
+
21
+ const response = await fetch(url, {
22
+ headers,
23
+ signal: controller.signal,
24
+ redirect: "follow",
25
+ });
26
+
27
+ if (!response.ok) {
28
+ throw new Error(`HTTP ${response.status} ${response.statusText}`);
29
+ }
30
+
31
+ const contentType = response.headers.get("content-type") || "";
32
+ const html = await response.text();
33
+
34
+ return {
35
+ url: response.url,
36
+ status: response.status,
37
+ contentType,
38
+ html,
39
+ size: html.length,
40
+ };
41
+ } finally {
42
+ clearTimeout(timer);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Extract text content from HTML, stripping tags.
48
+ */
49
+ export function extractText(html) {
50
+ return html
51
+ .replace(/<script[\s\S]*?<\/script>/gi, "")
52
+ .replace(/<style[\s\S]*?<\/style>/gi, "")
53
+ .replace(/<nav[\s\S]*?<\/nav>/gi, "")
54
+ .replace(/<footer[\s\S]*?<\/footer>/gi, "")
55
+ .replace(/<!--[\s\S]*?-->/g, "")
56
+ .replace(/<br\s*\/?>/gi, "\n")
57
+ .replace(/<\/p>/gi, "\n\n")
58
+ .replace(/<\/div>/gi, "\n")
59
+ .replace(/<\/h[1-6]>/gi, "\n\n")
60
+ .replace(/<\/li>/gi, "\n")
61
+ .replace(/<[^>]+>/g, "")
62
+ .replace(/&nbsp;/g, " ")
63
+ .replace(/&amp;/g, "&")
64
+ .replace(/&lt;/g, "<")
65
+ .replace(/&gt;/g, ">")
66
+ .replace(/&quot;/g, '"')
67
+ .replace(/&#(\d+);/g, (_, n) => String.fromCharCode(parseInt(n)))
68
+ .replace(/\n{3,}/g, "\n\n")
69
+ .replace(/[ \t]+/g, " ")
70
+ .split("\n")
71
+ .map((l) => l.trim())
72
+ .join("\n")
73
+ .trim();
74
+ }
75
+
76
+ /**
77
+ * Extract page title from HTML.
78
+ */
79
+ export function extractTitle(html) {
80
+ const match = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
81
+ return match ? match[1].trim().replace(/\s+/g, " ") : "";
82
+ }
83
+
84
+ /**
85
+ * Extract meta description from HTML.
86
+ */
87
+ export function extractMeta(html) {
88
+ const descMatch =
89
+ html.match(
90
+ /<meta\s+name=["']description["']\s+content=["']([^"']*?)["']/i,
91
+ ) ||
92
+ html.match(/<meta\s+content=["']([^"']*?)["']\s+name=["']description["']/i);
93
+ return descMatch ? descMatch[1] : "";
94
+ }
95
+
96
+ /**
97
+ * Extract elements matching a simple CSS selector.
98
+ * Supports: tag, .class, #id, tag.class, tag#id
99
+ */
100
+ export function querySelectorAll(html, selector) {
101
+ const results = [];
102
+
103
+ // Parse selector
104
+ const tagMatch = selector.match(/^(\w+)/);
105
+ const classMatch = selector.match(/\.([a-zA-Z0-9_-]+)/);
106
+ const idMatch = selector.match(/#([a-zA-Z0-9_-]+)/);
107
+
108
+ const tag = tagMatch ? tagMatch[1] : null;
109
+ const className = classMatch ? classMatch[1] : null;
110
+ const id = idMatch ? idMatch[1] : null;
111
+
112
+ // Build regex pattern
113
+ let pattern;
114
+ if (tag) {
115
+ pattern = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, "gi");
116
+ } else if (className || id) {
117
+ pattern = new RegExp(`<\\w+[^>]*>([\\s\\S]*?)<\\/\\w+>`, "gi");
118
+ } else {
119
+ return results;
120
+ }
121
+
122
+ let match;
123
+ while ((match = pattern.exec(html)) !== null) {
124
+ const fullTag = match[0];
125
+
126
+ // Check class filter
127
+ if (className) {
128
+ const classAttr = fullTag.match(/class=["']([^"']*?)["']/i);
129
+ if (!classAttr || !classAttr[1].split(/\s+/).includes(className))
130
+ continue;
131
+ }
132
+
133
+ // Check id filter
134
+ if (id) {
135
+ const idAttr = fullTag.match(/id=["']([^"']*?)["']/i);
136
+ if (!idAttr || idAttr[1] !== id) continue;
137
+ }
138
+
139
+ results.push({
140
+ html: fullTag,
141
+ text: extractText(fullTag),
142
+ });
143
+ }
144
+
145
+ return results;
146
+ }
147
+
148
+ /**
149
+ * Extract all links from HTML.
150
+ */
151
+ export function extractLinks(html, baseUrl) {
152
+ const links = [];
153
+ const linkRegex = /<a\s+[^>]*href=["']([^"'#]+)["'][^>]*>([\s\S]*?)<\/a>/gi;
154
+ let match;
155
+
156
+ while ((match = linkRegex.exec(html)) !== null) {
157
+ let href = match[1];
158
+ const text = extractText(match[2]).trim();
159
+
160
+ // Resolve relative URLs
161
+ if (baseUrl && !href.startsWith("http")) {
162
+ try {
163
+ href = new URL(href, baseUrl).href;
164
+ } catch {
165
+ continue;
166
+ }
167
+ }
168
+
169
+ if (text && href.startsWith("http")) {
170
+ links.push({ href, text });
171
+ }
172
+ }
173
+
174
+ return links;
175
+ }
176
+
177
+ /**
178
+ * Take a screenshot using playwright (optional dependency).
179
+ * Returns null if playwright is not installed.
180
+ */
181
+ export async function takeScreenshot(url, outputPath, options = {}) {
182
+ try {
183
+ const { chromium } = await import("playwright");
184
+ const browser = await chromium.launch({ headless: true });
185
+ const page = await browser.newPage({
186
+ viewport: {
187
+ width: options.width || 1280,
188
+ height: options.height || 720,
189
+ },
190
+ });
191
+
192
+ await page.goto(url, {
193
+ waitUntil: options.waitUntil || "networkidle",
194
+ timeout: options.timeout || 30000,
195
+ });
196
+
197
+ await page.screenshot({
198
+ path: outputPath,
199
+ fullPage: options.fullPage || false,
200
+ });
201
+
202
+ await browser.close();
203
+ return { success: true, path: outputPath };
204
+ } catch (err) {
205
+ if (
206
+ err.code === "ERR_MODULE_NOT_FOUND" ||
207
+ err.message?.includes("Cannot find")
208
+ ) {
209
+ return {
210
+ success: false,
211
+ error: "playwright not installed. Run: npm install -g playwright",
212
+ };
213
+ }
214
+ throw err;
215
+ }
216
+ }
@@ -0,0 +1,180 @@
1
+ /**
2
+ * A/B Comparator for CLI
3
+ *
4
+ * Generates N solution variants for a prompt using different agent configurations,
5
+ * then scores and ranks them against specified criteria.
6
+ */
7
+
8
+ import { createChatFn } from "../cowork-adapter.js";
9
+
10
+ const DEFAULT_CRITERIA = ["quality", "performance", "readability"];
11
+
12
+ const VARIANT_PROFILES = [
13
+ {
14
+ name: "conservative",
15
+ system:
16
+ "You are a conservative software engineer who favors proven patterns, stability, and minimal dependencies. Prefer simple, well-tested approaches over cutting-edge solutions.",
17
+ },
18
+ {
19
+ name: "innovative",
20
+ system:
21
+ "You are an innovative software engineer who favors modern patterns, new APIs, and elegant abstractions. Prioritize developer experience and future-proofing.",
22
+ },
23
+ {
24
+ name: "pragmatic",
25
+ system:
26
+ "You are a pragmatic software engineer who balances simplicity with capability. Choose the approach that ships fastest while maintaining acceptable quality.",
27
+ },
28
+ {
29
+ name: "performance-focused",
30
+ system:
31
+ "You are a performance-oriented engineer. Optimize for speed, memory efficiency, and minimal overhead. Accept complexity if it yields measurable performance gains.",
32
+ },
33
+ ];
34
+
35
+ /**
36
+ * Generate and compare N solution variants
37
+ *
38
+ * @param {object} params
39
+ * @param {string} params.prompt - The task/problem description
40
+ * @param {number} [params.variants=3] - Number of variants to generate
41
+ * @param {string[]} [params.criteria] - Scoring criteria
42
+ * @param {object} [params.llmOptions] - LLM provider options
43
+ * @returns {Promise<object>} Comparison result with ranked variants
44
+ */
45
+ export async function compare({
46
+ prompt,
47
+ variants = 3,
48
+ criteria = DEFAULT_CRITERIA,
49
+ llmOptions = {},
50
+ }) {
51
+ const chat = createChatFn(llmOptions);
52
+ const numVariants = Math.min(variants, VARIANT_PROFILES.length);
53
+ const profiles = VARIANT_PROFILES.slice(0, numVariants);
54
+ const generatedVariants = [];
55
+
56
+ // Phase 1: Generate variants
57
+ for (const profile of profiles) {
58
+ const messages = [
59
+ { role: "system", content: profile.system },
60
+ {
61
+ role: "user",
62
+ content: `Provide a solution for the following task. Include code if applicable.\n\nTask: ${prompt}\n\nProvide your solution with:\n1. Approach summary (1-2 sentences)\n2. Implementation (code or detailed steps)\n3. Trade-offs (pros and cons of this approach)`,
63
+ },
64
+ ];
65
+
66
+ try {
67
+ const response = await chat(messages, { maxTokens: 2000 });
68
+ generatedVariants.push({
69
+ name: profile.name,
70
+ profile: profile.system,
71
+ solution: response,
72
+ });
73
+ } catch (err) {
74
+ generatedVariants.push({
75
+ name: profile.name,
76
+ profile: profile.system,
77
+ solution: `Error generating variant: ${err.message}`,
78
+ });
79
+ }
80
+ }
81
+
82
+ // Phase 2: Score each variant against criteria
83
+ const scoringPrompt = `You are an impartial judge evaluating ${numVariants} solution variants against these criteria: ${criteria.join(", ")}.
84
+
85
+ For each variant, assign a score from 1-10 for each criterion. Then provide an overall ranking.
86
+
87
+ ${generatedVariants
88
+ .map((v, i) => `### Variant ${i + 1}: ${v.name}\n${v.solution}`)
89
+ .join("\n\n---\n\n")}
90
+
91
+ Respond in this exact format for each variant:
92
+ SCORES:
93
+ ${generatedVariants.map((v, i) => `Variant ${i + 1} (${v.name}): ${criteria.map((c) => `${c}=X`).join(", ")}`).join("\n")}
94
+
95
+ RANKING: (best to worst, comma-separated variant names)
96
+ WINNER: (name of the best variant)
97
+ REASON: (1-2 sentence justification)`;
98
+
99
+ let scores = [];
100
+ let ranking = [];
101
+ let winner = "";
102
+ let reason = "";
103
+
104
+ try {
105
+ const judgement = await chat(
106
+ [
107
+ {
108
+ role: "system",
109
+ content:
110
+ "You are an impartial technical evaluator. Score solutions objectively based on the given criteria.",
111
+ },
112
+ { role: "user", content: scoringPrompt },
113
+ ],
114
+ { maxTokens: 1500 },
115
+ );
116
+
117
+ // Parse scores
118
+ scores = parseScores(judgement, generatedVariants, criteria);
119
+ ranking = parseRanking(judgement, generatedVariants);
120
+ winner = parseWinner(judgement) || generatedVariants[0]?.name || "unknown";
121
+ reason = parseReason(judgement) || "See detailed scores above.";
122
+ } catch (err) {
123
+ reason = `Scoring error: ${err.message}`;
124
+ }
125
+
126
+ return {
127
+ prompt,
128
+ criteria,
129
+ variants: generatedVariants.map((v, i) => ({
130
+ ...v,
131
+ scores: scores[i] || {},
132
+ totalScore: scores[i]
133
+ ? Object.values(scores[i]).reduce((a, b) => a + b, 0)
134
+ : 0,
135
+ })),
136
+ ranking,
137
+ winner,
138
+ reason,
139
+ };
140
+ }
141
+
142
+ function parseScores(text, variants, criteria) {
143
+ const scores = [];
144
+ for (let i = 0; i < variants.length; i++) {
145
+ const variantScores = {};
146
+ for (const c of criteria) {
147
+ const pattern = new RegExp(
148
+ `variant\\s*${i + 1}[^\\n]*${c}\\s*=\\s*(\\d+)`,
149
+ "i",
150
+ );
151
+ const match = text.match(pattern);
152
+ variantScores[c] = match ? parseInt(match[1], 10) : 5;
153
+ }
154
+ scores.push(variantScores);
155
+ }
156
+ return scores;
157
+ }
158
+
159
+ function parseRanking(text, variants) {
160
+ const match = text.match(/RANKING:\s*(.+)/i);
161
+ if (match) {
162
+ return match[1]
163
+ .split(",")
164
+ .map((s) => s.trim())
165
+ .filter(Boolean);
166
+ }
167
+ return variants.map((v) => v.name);
168
+ }
169
+
170
+ function parseWinner(text) {
171
+ const match = text.match(/WINNER:\s*(.+)/i);
172
+ return match ? match[1].trim() : null;
173
+ }
174
+
175
+ function parseReason(text) {
176
+ const match = text.match(/REASON:\s*(.+)/i);
177
+ return match ? match[1].trim() : null;
178
+ }
179
+
180
+ export { DEFAULT_CRITERIA, VARIANT_PROFILES };