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.
- package/README.md +309 -19
- package/bin/chainlesschain.js +4 -0
- package/package.json +1 -1
- package/src/commands/a2a.js +374 -0
- package/src/commands/audit.js +286 -0
- package/src/commands/auth.js +387 -0
- package/src/commands/bi.js +240 -0
- package/src/commands/browse.js +184 -0
- package/src/commands/cowork.js +317 -0
- package/src/commands/did.js +376 -0
- package/src/commands/economy.js +375 -0
- package/src/commands/encrypt.js +233 -0
- package/src/commands/evolution.js +398 -0
- package/src/commands/export.js +125 -0
- package/src/commands/git.js +215 -0
- package/src/commands/hmemory.js +273 -0
- package/src/commands/hook.js +260 -0
- package/src/commands/import.js +259 -0
- package/src/commands/init.js +184 -0
- package/src/commands/instinct.js +202 -0
- package/src/commands/llm.js +155 -4
- package/src/commands/lowcode.js +320 -0
- package/src/commands/mcp.js +302 -0
- package/src/commands/memory.js +282 -0
- package/src/commands/note.js +187 -0
- package/src/commands/org.js +505 -0
- package/src/commands/p2p.js +274 -0
- package/src/commands/plugin.js +451 -0
- package/src/commands/sandbox.js +366 -0
- package/src/commands/search.js +237 -0
- package/src/commands/session.js +238 -0
- package/src/commands/skill.js +254 -201
- package/src/commands/sync.js +249 -0
- package/src/commands/tokens.js +214 -0
- package/src/commands/wallet.js +416 -0
- package/src/commands/workflow.js +359 -0
- package/src/commands/zkp.js +277 -0
- package/src/index.js +93 -1
- package/src/lib/a2a-protocol.js +371 -0
- package/src/lib/agent-coordinator.js +273 -0
- package/src/lib/agent-economy.js +369 -0
- package/src/lib/app-builder.js +377 -0
- package/src/lib/audit-logger.js +364 -0
- package/src/lib/bi-engine.js +299 -0
- package/src/lib/bm25-search.js +322 -0
- package/src/lib/browser-automation.js +216 -0
- package/src/lib/cowork/ab-comparator-cli.js +180 -0
- package/src/lib/cowork/code-knowledge-graph-cli.js +232 -0
- package/src/lib/cowork/debate-review-cli.js +144 -0
- package/src/lib/cowork/decision-kb-cli.js +153 -0
- package/src/lib/cowork/project-style-analyzer-cli.js +168 -0
- package/src/lib/cowork-adapter.js +106 -0
- package/src/lib/crypto-manager.js +246 -0
- package/src/lib/did-manager.js +270 -0
- package/src/lib/ensure-utf8.js +59 -0
- package/src/lib/evolution-system.js +508 -0
- package/src/lib/git-integration.js +220 -0
- package/src/lib/hierarchical-memory.js +471 -0
- package/src/lib/hook-manager.js +387 -0
- package/src/lib/instinct-manager.js +190 -0
- package/src/lib/knowledge-exporter.js +302 -0
- package/src/lib/knowledge-importer.js +293 -0
- package/src/lib/llm-providers.js +325 -0
- package/src/lib/mcp-client.js +413 -0
- package/src/lib/memory-manager.js +211 -0
- package/src/lib/note-versioning.js +244 -0
- package/src/lib/org-manager.js +424 -0
- package/src/lib/p2p-manager.js +317 -0
- package/src/lib/pdf-parser.js +96 -0
- package/src/lib/permission-engine.js +374 -0
- package/src/lib/plan-mode.js +333 -0
- package/src/lib/plugin-manager.js +430 -0
- package/src/lib/project-detector.js +53 -0
- package/src/lib/response-cache.js +156 -0
- package/src/lib/sandbox-v2.js +503 -0
- package/src/lib/service-container.js +183 -0
- package/src/lib/session-manager.js +189 -0
- package/src/lib/skill-loader.js +274 -0
- package/src/lib/sync-manager.js +347 -0
- package/src/lib/token-tracker.js +200 -0
- package/src/lib/wallet-manager.js +348 -0
- package/src/lib/workflow-engine.js +503 -0
- package/src/lib/zkp-engine.js +241 -0
- 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(/ /g, " ")
|
|
63
|
+
.replace(/&/g, "&")
|
|
64
|
+
.replace(/</g, "<")
|
|
65
|
+
.replace(/>/g, ">")
|
|
66
|
+
.replace(/"/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 };
|