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,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Knowledge Graph for CLI
|
|
3
|
+
*
|
|
4
|
+
* Builds a lightweight entity-relationship graph from source code:
|
|
5
|
+
* - Files, classes, functions, imports, exports
|
|
6
|
+
* - Dependency relationships
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
|
|
12
|
+
const CODE_EXTENSIONS = new Set([
|
|
13
|
+
".js",
|
|
14
|
+
".ts",
|
|
15
|
+
".jsx",
|
|
16
|
+
".tsx",
|
|
17
|
+
".vue",
|
|
18
|
+
".py",
|
|
19
|
+
".java",
|
|
20
|
+
".go",
|
|
21
|
+
".rs",
|
|
22
|
+
".rb",
|
|
23
|
+
".kt",
|
|
24
|
+
".swift",
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build a knowledge graph from a file or directory
|
|
29
|
+
*
|
|
30
|
+
* @param {object} params
|
|
31
|
+
* @param {string} params.targetPath - File or directory to analyze
|
|
32
|
+
* @param {number} [params.maxFiles=50] - Max files to process
|
|
33
|
+
* @returns {Promise<object>} Knowledge graph
|
|
34
|
+
*/
|
|
35
|
+
export async function buildKnowledgeGraph({ targetPath, maxFiles = 50 }) {
|
|
36
|
+
const entities = [];
|
|
37
|
+
const relationships = [];
|
|
38
|
+
|
|
39
|
+
const stat = fs.statSync(targetPath);
|
|
40
|
+
const files = stat.isDirectory()
|
|
41
|
+
? collectFiles(targetPath, maxFiles)
|
|
42
|
+
: [targetPath];
|
|
43
|
+
|
|
44
|
+
for (const filePath of files) {
|
|
45
|
+
const ext = path.extname(filePath);
|
|
46
|
+
if (!CODE_EXTENSIONS.has(ext)) continue;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
50
|
+
const relativePath = path.relative(process.cwd(), filePath);
|
|
51
|
+
|
|
52
|
+
entities.push({
|
|
53
|
+
type: "file",
|
|
54
|
+
name: relativePath,
|
|
55
|
+
language: ext.slice(1),
|
|
56
|
+
lines: content.split("\n").length,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Extract imports
|
|
60
|
+
const imports = extractImports(content, ext);
|
|
61
|
+
for (const imp of imports) {
|
|
62
|
+
relationships.push({
|
|
63
|
+
from: relativePath,
|
|
64
|
+
to: imp,
|
|
65
|
+
type: "imports",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Extract exports / public APIs
|
|
70
|
+
const exports = extractExports(content, ext);
|
|
71
|
+
for (const exp of exports) {
|
|
72
|
+
entities.push({
|
|
73
|
+
type: "export",
|
|
74
|
+
name: exp.name,
|
|
75
|
+
kind: exp.kind,
|
|
76
|
+
file: relativePath,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Extract classes/functions
|
|
81
|
+
const definitions = extractDefinitions(content, ext);
|
|
82
|
+
for (const def of definitions) {
|
|
83
|
+
entities.push({
|
|
84
|
+
type: def.kind,
|
|
85
|
+
name: def.name,
|
|
86
|
+
file: relativePath,
|
|
87
|
+
line: def.line,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// Skip unreadable files
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Build summary
|
|
96
|
+
const fileCount = entities.filter((e) => e.type === "file").length;
|
|
97
|
+
const exportCount = entities.filter((e) => e.type === "export").length;
|
|
98
|
+
const defCount = entities.filter(
|
|
99
|
+
(e) => e.type === "class" || e.type === "function",
|
|
100
|
+
).length;
|
|
101
|
+
|
|
102
|
+
const summary = [
|
|
103
|
+
`Code Knowledge Graph for: ${targetPath}`,
|
|
104
|
+
` Files analyzed: ${fileCount}`,
|
|
105
|
+
` Entities: ${entities.length} (${defCount} definitions, ${exportCount} exports)`,
|
|
106
|
+
` Relationships: ${relationships.length}`,
|
|
107
|
+
"",
|
|
108
|
+
"Top imports:",
|
|
109
|
+
...getTopImports(relationships).map((r) => ` ${r.to} (${r.count} refs)`),
|
|
110
|
+
].join("\n");
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
entities,
|
|
114
|
+
relationships,
|
|
115
|
+
stats: {
|
|
116
|
+
fileCount,
|
|
117
|
+
exportCount,
|
|
118
|
+
defCount,
|
|
119
|
+
relationshipCount: relationships.length,
|
|
120
|
+
},
|
|
121
|
+
summary,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function collectFiles(dir, maxFiles) {
|
|
126
|
+
const files = [];
|
|
127
|
+
const queue = [dir];
|
|
128
|
+
|
|
129
|
+
while (queue.length > 0 && files.length < maxFiles) {
|
|
130
|
+
const current = queue.shift();
|
|
131
|
+
try {
|
|
132
|
+
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
133
|
+
for (const entry of entries) {
|
|
134
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules")
|
|
135
|
+
continue;
|
|
136
|
+
const fullPath = path.join(current, entry.name);
|
|
137
|
+
if (entry.isDirectory()) {
|
|
138
|
+
queue.push(fullPath);
|
|
139
|
+
} else if (CODE_EXTENSIONS.has(path.extname(entry.name))) {
|
|
140
|
+
files.push(fullPath);
|
|
141
|
+
if (files.length >= maxFiles) break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
// Skip unreadable dirs
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return files;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function extractImports(content, ext) {
|
|
153
|
+
const imports = [];
|
|
154
|
+
|
|
155
|
+
// ES6 imports
|
|
156
|
+
const esImportRe = /import\s+(?:.*?\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
157
|
+
let match;
|
|
158
|
+
while ((match = esImportRe.exec(content))) {
|
|
159
|
+
imports.push(match[1]);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// CommonJS require
|
|
163
|
+
const cjsRe = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
164
|
+
while ((match = cjsRe.exec(content))) {
|
|
165
|
+
imports.push(match[1]);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Python imports
|
|
169
|
+
if (ext === ".py") {
|
|
170
|
+
const pyRe = /(?:from|import)\s+([\w.]+)/g;
|
|
171
|
+
while ((match = pyRe.exec(content))) {
|
|
172
|
+
imports.push(match[1]);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return [...new Set(imports)];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function extractExports(content, ext) {
|
|
180
|
+
const exports = [];
|
|
181
|
+
|
|
182
|
+
// ES6 exports
|
|
183
|
+
const esExportRe =
|
|
184
|
+
/export\s+(?:default\s+)?(?:(?:async\s+)?function|class|const|let|var)\s+(\w+)/g;
|
|
185
|
+
let match;
|
|
186
|
+
while ((match = esExportRe.exec(content))) {
|
|
187
|
+
exports.push({ name: match[1], kind: "named" });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// module.exports
|
|
191
|
+
if (/module\.exports\s*=/.test(content)) {
|
|
192
|
+
exports.push({ name: "default", kind: "cjs" });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return exports;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function extractDefinitions(content, ext) {
|
|
199
|
+
const defs = [];
|
|
200
|
+
const lines = content.split("\n");
|
|
201
|
+
|
|
202
|
+
for (let i = 0; i < lines.length; i++) {
|
|
203
|
+
const line = lines[i];
|
|
204
|
+
|
|
205
|
+
// Class definitions
|
|
206
|
+
const classMatch = line.match(/(?:export\s+)?class\s+(\w+)/);
|
|
207
|
+
if (classMatch) {
|
|
208
|
+
defs.push({ kind: "class", name: classMatch[1], line: i + 1 });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Function definitions
|
|
212
|
+
const funcMatch = line.match(/(?:export\s+)?(?:async\s+)?function\s+(\w+)/);
|
|
213
|
+
if (funcMatch) {
|
|
214
|
+
defs.push({ kind: "function", name: funcMatch[1], line: i + 1 });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return defs;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function getTopImports(relationships) {
|
|
222
|
+
const counts = {};
|
|
223
|
+
for (const r of relationships) {
|
|
224
|
+
if (r.type === "imports") {
|
|
225
|
+
counts[r.to] = (counts[r.to] || 0) + 1;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return Object.entries(counts)
|
|
229
|
+
.sort((a, b) => b[1] - a[1])
|
|
230
|
+
.slice(0, 10)
|
|
231
|
+
.map(([to, count]) => ({ to, count }));
|
|
232
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-perspective debate review for CLI
|
|
3
|
+
*
|
|
4
|
+
* Spawns multiple reviewer agents (performance, security, maintainability, etc.)
|
|
5
|
+
* Each reviews independently via LLM, then a moderator synthesizes a verdict.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createChatFn } from "../cowork-adapter.js";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_PERSPECTIVES = ["performance", "security", "maintainability"];
|
|
11
|
+
|
|
12
|
+
const PERSPECTIVE_PROMPTS = {
|
|
13
|
+
performance: {
|
|
14
|
+
role: "Performance Reviewer",
|
|
15
|
+
system:
|
|
16
|
+
"You are a performance-focused code reviewer. Analyze code for time complexity, memory usage, unnecessary allocations, blocking operations, N+1 queries, and scalability issues. Be specific and cite line numbers when possible.",
|
|
17
|
+
},
|
|
18
|
+
security: {
|
|
19
|
+
role: "Security Reviewer",
|
|
20
|
+
system:
|
|
21
|
+
"You are a security-focused code reviewer. Analyze code for injection vulnerabilities, authentication/authorization issues, data exposure, insecure defaults, OWASP top 10, and supply chain risks. Be specific and cite line numbers when possible.",
|
|
22
|
+
},
|
|
23
|
+
maintainability: {
|
|
24
|
+
role: "Maintainability Reviewer",
|
|
25
|
+
system:
|
|
26
|
+
"You are a maintainability-focused code reviewer. Analyze code for readability, naming conventions, coupling, cohesion, DRY violations, missing error handling, test coverage gaps, and documentation needs. Be specific and cite line numbers when possible.",
|
|
27
|
+
},
|
|
28
|
+
correctness: {
|
|
29
|
+
role: "Correctness Reviewer",
|
|
30
|
+
system:
|
|
31
|
+
"You are a correctness-focused code reviewer. Analyze code for logic errors, off-by-one bugs, race conditions, null/undefined handling, edge cases, type mismatches, and incorrect assumptions. Be specific and cite line numbers when possible.",
|
|
32
|
+
},
|
|
33
|
+
architecture: {
|
|
34
|
+
role: "Architecture Reviewer",
|
|
35
|
+
system:
|
|
36
|
+
"You are an architecture-focused code reviewer. Analyze code for separation of concerns, dependency management, design pattern usage, extensibility, and alignment with established project patterns. Be specific.",
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Run a multi-perspective debate review
|
|
42
|
+
*
|
|
43
|
+
* @param {object} params
|
|
44
|
+
* @param {string} params.target - Description of what's being reviewed
|
|
45
|
+
* @param {string} params.code - The code to review
|
|
46
|
+
* @param {string[]} [params.perspectives] - Perspectives to use
|
|
47
|
+
* @param {object} [params.llmOptions] - LLM provider options
|
|
48
|
+
* @returns {Promise<object>} Review result with verdict and reviews
|
|
49
|
+
*/
|
|
50
|
+
export async function startDebate({
|
|
51
|
+
target,
|
|
52
|
+
code,
|
|
53
|
+
perspectives = DEFAULT_PERSPECTIVES,
|
|
54
|
+
llmOptions = {},
|
|
55
|
+
}) {
|
|
56
|
+
const chat = createChatFn(llmOptions);
|
|
57
|
+
const reviews = [];
|
|
58
|
+
|
|
59
|
+
// Phase 1: Independent reviews from each perspective
|
|
60
|
+
for (const perspective of perspectives) {
|
|
61
|
+
const config = PERSPECTIVE_PROMPTS[perspective] || {
|
|
62
|
+
role: `${perspective} Reviewer`,
|
|
63
|
+
system: `You are a ${perspective}-focused code reviewer. Provide specific, actionable feedback.`,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const messages = [
|
|
67
|
+
{ role: "system", content: config.system },
|
|
68
|
+
{
|
|
69
|
+
role: "user",
|
|
70
|
+
content: `Review the following code/content.\n\nTarget: ${target}\n\n\`\`\`\n${code}\n\`\`\`\n\nProvide your review as a ${config.role}. Format your response as:\n\n## Issues Found\n- List each issue with severity (HIGH/MEDIUM/LOW)\n\n## Recommendations\n- List specific improvements\n\n## Verdict\nAPPROVE, NEEDS_WORK, or REJECT with a brief reason.`,
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const response = await chat(messages, { maxTokens: 1500 });
|
|
76
|
+
const verdict = extractVerdict(response);
|
|
77
|
+
reviews.push({
|
|
78
|
+
perspective,
|
|
79
|
+
role: config.role,
|
|
80
|
+
review: response,
|
|
81
|
+
verdict,
|
|
82
|
+
});
|
|
83
|
+
} catch (err) {
|
|
84
|
+
reviews.push({
|
|
85
|
+
perspective,
|
|
86
|
+
role: config.role,
|
|
87
|
+
review: `Error: ${err.message}`,
|
|
88
|
+
verdict: "ERROR",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Phase 2: Moderator synthesizes final verdict
|
|
94
|
+
const moderatorMessages = [
|
|
95
|
+
{
|
|
96
|
+
role: "system",
|
|
97
|
+
content:
|
|
98
|
+
"You are a senior engineering lead moderating a code review. Synthesize the perspectives below into a final verdict. Weight security and correctness issues higher than style concerns.",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
role: "user",
|
|
102
|
+
content: `Multiple reviewers analyzed this code. Synthesize their findings into a final verdict.\n\nTarget: ${target}\n\n${reviews
|
|
103
|
+
.map((r) => `### ${r.role} (${r.verdict})\n${r.review}`)
|
|
104
|
+
.join(
|
|
105
|
+
"\n\n---\n\n",
|
|
106
|
+
)}\n\nProvide:\n1. Final Verdict: APPROVE / NEEDS_WORK / REJECT\n2. Consensus Score: 0-100 (how much the reviewers agree)\n3. Summary of key findings across all perspectives\n4. Priority action items (if any)`,
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
let finalVerdict = "NEEDS_WORK";
|
|
111
|
+
let consensusScore = 50;
|
|
112
|
+
let summary = "";
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
summary = await chat(moderatorMessages, { maxTokens: 1500 });
|
|
116
|
+
finalVerdict = extractVerdict(summary);
|
|
117
|
+
consensusScore = extractConsensusScore(summary);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
summary = `Moderator error: ${err.message}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
target,
|
|
124
|
+
perspectives,
|
|
125
|
+
reviews,
|
|
126
|
+
verdict: finalVerdict,
|
|
127
|
+
consensusScore,
|
|
128
|
+
summary,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function extractVerdict(text) {
|
|
133
|
+
if (/\bREJECT\b/i.test(text)) return "REJECT";
|
|
134
|
+
if (/\bAPPROVE\b/i.test(text)) return "APPROVE";
|
|
135
|
+
return "NEEDS_WORK";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function extractConsensusScore(text) {
|
|
139
|
+
const match = text.match(/consensus\s*score[:\s]*(\d+)/i);
|
|
140
|
+
if (match) return parseInt(match[1], 10);
|
|
141
|
+
return 50;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export { DEFAULT_PERSPECTIVES, PERSPECTIVE_PROMPTS };
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision Knowledge Base for CLI
|
|
3
|
+
*
|
|
4
|
+
* Extracts architectural decisions from code and documentation using LLM analysis.
|
|
5
|
+
* Outputs structured ADR-like records.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { createChatFn } from "../cowork-adapter.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extract architectural decisions from a file or directory
|
|
14
|
+
*
|
|
15
|
+
* @param {object} params
|
|
16
|
+
* @param {string} params.targetPath - File or directory to analyze
|
|
17
|
+
* @param {object} [params.llmOptions] - LLM provider options
|
|
18
|
+
* @returns {Promise<object>} Extracted decisions
|
|
19
|
+
*/
|
|
20
|
+
export async function extractDecisions({ targetPath, llmOptions = {} }) {
|
|
21
|
+
const chat = createChatFn(llmOptions);
|
|
22
|
+
const stat = fs.statSync(targetPath);
|
|
23
|
+
|
|
24
|
+
let content;
|
|
25
|
+
if (stat.isDirectory()) {
|
|
26
|
+
// Read key files that typically contain decisions
|
|
27
|
+
const candidates = [
|
|
28
|
+
"CLAUDE.md",
|
|
29
|
+
"CLAUDE-decisions.md",
|
|
30
|
+
"README.md",
|
|
31
|
+
"ARCHITECTURE.md",
|
|
32
|
+
"ADR",
|
|
33
|
+
"docs/adr",
|
|
34
|
+
"docs/decisions",
|
|
35
|
+
"package.json",
|
|
36
|
+
];
|
|
37
|
+
const parts = [];
|
|
38
|
+
for (const candidate of candidates) {
|
|
39
|
+
const fullPath = path.join(targetPath, candidate);
|
|
40
|
+
if (fs.existsSync(fullPath)) {
|
|
41
|
+
const stat2 = fs.statSync(fullPath);
|
|
42
|
+
if (stat2.isFile()) {
|
|
43
|
+
try {
|
|
44
|
+
const fileContent = fs.readFileSync(fullPath, "utf-8");
|
|
45
|
+
parts.push(
|
|
46
|
+
`--- ${candidate} ---\n${fileContent.substring(0, 5000)}`,
|
|
47
|
+
);
|
|
48
|
+
} catch {
|
|
49
|
+
// Skip
|
|
50
|
+
}
|
|
51
|
+
} else if (stat2.isDirectory()) {
|
|
52
|
+
// Read first few files in ADR directory
|
|
53
|
+
try {
|
|
54
|
+
const files = fs.readdirSync(fullPath).slice(0, 10);
|
|
55
|
+
for (const f of files) {
|
|
56
|
+
const fp = path.join(fullPath, f);
|
|
57
|
+
if (fs.statSync(fp).isFile()) {
|
|
58
|
+
const fc = fs.readFileSync(fp, "utf-8");
|
|
59
|
+
parts.push(
|
|
60
|
+
`--- ${candidate}/${f} ---\n${fc.substring(0, 3000)}`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// Skip
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
content = parts.join("\n\n");
|
|
71
|
+
} else {
|
|
72
|
+
content = fs.readFileSync(targetPath, "utf-8");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!content || content.trim().length === 0) {
|
|
76
|
+
return {
|
|
77
|
+
decisions: [],
|
|
78
|
+
summary: "No relevant content found for decision extraction.",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Truncate if too long
|
|
83
|
+
if (content.length > 20000) {
|
|
84
|
+
content = content.substring(0, 20000) + "\n... (truncated)";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const messages = [
|
|
88
|
+
{
|
|
89
|
+
role: "system",
|
|
90
|
+
content: `You are an experienced software architect. Extract architectural decisions from the provided content. For each decision, identify:
|
|
91
|
+
1. Title — short name for the decision
|
|
92
|
+
2. Status — (accepted, proposed, deprecated, superseded)
|
|
93
|
+
3. Context — why was this decision needed?
|
|
94
|
+
4. Decision — what was decided?
|
|
95
|
+
5. Consequences — what are the trade-offs?
|
|
96
|
+
|
|
97
|
+
Format each decision as:
|
|
98
|
+
### Decision: <title>
|
|
99
|
+
- **Status**: <status>
|
|
100
|
+
- **Context**: <context>
|
|
101
|
+
- **Decision**: <what was decided>
|
|
102
|
+
- **Consequences**: <trade-offs>
|
|
103
|
+
|
|
104
|
+
List all decisions you can find. If no explicit decisions are documented, infer them from technology choices, patterns, and configuration.`,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
role: "user",
|
|
108
|
+
content: `Extract architectural decisions from this content:\n\n${content}`,
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const response = await chat(messages, { maxTokens: 2000 });
|
|
114
|
+
const decisions = parseDecisions(response);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
decisions,
|
|
118
|
+
raw: response,
|
|
119
|
+
summary: `Found ${decisions.length} architectural decisions in ${targetPath}\n\n${decisions.map((d) => ` - ${d.title} (${d.status})`).join("\n")}`,
|
|
120
|
+
};
|
|
121
|
+
} catch (err) {
|
|
122
|
+
return {
|
|
123
|
+
decisions: [],
|
|
124
|
+
summary: `Decision extraction failed: ${err.message}`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function parseDecisions(text) {
|
|
130
|
+
const decisions = [];
|
|
131
|
+
const sections = text.split(/###\s*Decision:\s*/i);
|
|
132
|
+
|
|
133
|
+
for (let i = 1; i < sections.length; i++) {
|
|
134
|
+
const section = sections[i];
|
|
135
|
+
const lines = section.split("\n");
|
|
136
|
+
const title = lines[0].trim();
|
|
137
|
+
|
|
138
|
+
const statusMatch = section.match(/\*\*Status\*\*:\s*(.+)/i);
|
|
139
|
+
const contextMatch = section.match(/\*\*Context\*\*:\s*(.+)/i);
|
|
140
|
+
const decisionMatch = section.match(/\*\*Decision\*\*:\s*(.+)/i);
|
|
141
|
+
const consequencesMatch = section.match(/\*\*Consequences?\*\*:\s*(.+)/i);
|
|
142
|
+
|
|
143
|
+
decisions.push({
|
|
144
|
+
title,
|
|
145
|
+
status: statusMatch ? statusMatch[1].trim() : "accepted",
|
|
146
|
+
context: contextMatch ? contextMatch[1].trim() : "",
|
|
147
|
+
decision: decisionMatch ? decisionMatch[1].trim() : "",
|
|
148
|
+
consequences: consequencesMatch ? consequencesMatch[1].trim() : "",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return decisions;
|
|
153
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Style Analyzer for CLI
|
|
3
|
+
*
|
|
4
|
+
* Analyzes coding style, conventions, and patterns in a codebase using LLM.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { createChatFn } from "../cowork-adapter.js";
|
|
10
|
+
|
|
11
|
+
const CODE_EXTENSIONS = new Set([
|
|
12
|
+
".js",
|
|
13
|
+
".ts",
|
|
14
|
+
".jsx",
|
|
15
|
+
".tsx",
|
|
16
|
+
".vue",
|
|
17
|
+
".py",
|
|
18
|
+
".java",
|
|
19
|
+
".go",
|
|
20
|
+
".rs",
|
|
21
|
+
".kt",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Analyze project coding style and conventions
|
|
26
|
+
*
|
|
27
|
+
* @param {object} params
|
|
28
|
+
* @param {string} params.targetPath - File or directory to analyze
|
|
29
|
+
* @param {object} [params.llmOptions] - LLM provider options
|
|
30
|
+
* @returns {Promise<object>} Style analysis result
|
|
31
|
+
*/
|
|
32
|
+
export async function analyzeProjectStyle({ targetPath, llmOptions = {} }) {
|
|
33
|
+
const chat = createChatFn(llmOptions);
|
|
34
|
+
const stat = fs.statSync(targetPath);
|
|
35
|
+
|
|
36
|
+
// Collect sample files
|
|
37
|
+
const samples = [];
|
|
38
|
+
if (stat.isDirectory()) {
|
|
39
|
+
const files = collectSampleFiles(targetPath, 8);
|
|
40
|
+
for (const file of files) {
|
|
41
|
+
try {
|
|
42
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
43
|
+
const relative = path.relative(process.cwd(), file);
|
|
44
|
+
samples.push({
|
|
45
|
+
path: relative,
|
|
46
|
+
content: content.substring(0, 3000),
|
|
47
|
+
ext: path.extname(file),
|
|
48
|
+
});
|
|
49
|
+
} catch {
|
|
50
|
+
// Skip
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
const content = fs.readFileSync(targetPath, "utf-8");
|
|
55
|
+
samples.push({
|
|
56
|
+
path: path.relative(process.cwd(), targetPath),
|
|
57
|
+
content: content.substring(0, 5000),
|
|
58
|
+
ext: path.extname(targetPath),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (samples.length === 0) {
|
|
63
|
+
return { patterns: [], summary: "No code files found to analyze." };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check for config files
|
|
67
|
+
const configFiles = [];
|
|
68
|
+
const configCandidates = [
|
|
69
|
+
".eslintrc.js",
|
|
70
|
+
".eslintrc.json",
|
|
71
|
+
".prettierrc",
|
|
72
|
+
"tsconfig.json",
|
|
73
|
+
"pyproject.toml",
|
|
74
|
+
".editorconfig",
|
|
75
|
+
"biome.json",
|
|
76
|
+
];
|
|
77
|
+
const baseDir = stat.isDirectory() ? targetPath : path.dirname(targetPath);
|
|
78
|
+
for (const cf of configCandidates) {
|
|
79
|
+
const cfPath = path.join(baseDir, cf);
|
|
80
|
+
if (fs.existsSync(cfPath)) {
|
|
81
|
+
try {
|
|
82
|
+
configFiles.push({
|
|
83
|
+
name: cf,
|
|
84
|
+
content: fs.readFileSync(cfPath, "utf-8").substring(0, 2000),
|
|
85
|
+
});
|
|
86
|
+
} catch {
|
|
87
|
+
// Skip
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const codeContext = samples
|
|
93
|
+
.map((s) => `--- ${s.path} ---\n${s.content}`)
|
|
94
|
+
.join("\n\n");
|
|
95
|
+
|
|
96
|
+
const configContext =
|
|
97
|
+
configFiles.length > 0
|
|
98
|
+
? `\n\nConfiguration files:\n${configFiles.map((c) => `--- ${c.name} ---\n${c.content}`).join("\n\n")}`
|
|
99
|
+
: "";
|
|
100
|
+
|
|
101
|
+
const messages = [
|
|
102
|
+
{
|
|
103
|
+
role: "system",
|
|
104
|
+
content: `You are a senior code reviewer analyzing project conventions. Identify:
|
|
105
|
+
1. Naming conventions (variables, functions, files)
|
|
106
|
+
2. Code organization patterns
|
|
107
|
+
3. Import/export style
|
|
108
|
+
4. Error handling patterns
|
|
109
|
+
5. Comment/documentation style
|
|
110
|
+
6. Testing patterns (if visible)
|
|
111
|
+
7. Formatting preferences (indentation, quotes, semicolons)
|
|
112
|
+
|
|
113
|
+
Be specific with examples from the code samples provided.`,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
role: "user",
|
|
117
|
+
content: `Analyze the coding style and conventions in these ${samples.length} code samples:\n\n${codeContext}${configContext}\n\nProvide a structured analysis of the project's coding conventions and patterns.`,
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const response = await chat(messages, { maxTokens: 2000 });
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
samplesAnalyzed: samples.length,
|
|
126
|
+
configFilesFound: configFiles.map((c) => c.name),
|
|
127
|
+
analysis: response,
|
|
128
|
+
summary: `Style Analysis for: ${targetPath}\n Samples: ${samples.length} files\n Config: ${configFiles.map((c) => c.name).join(", ") || "none found"}\n\n${response}`,
|
|
129
|
+
};
|
|
130
|
+
} catch (err) {
|
|
131
|
+
return {
|
|
132
|
+
samplesAnalyzed: samples.length,
|
|
133
|
+
summary: `Style analysis failed: ${err.message}`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function collectSampleFiles(dir, maxFiles) {
|
|
139
|
+
const files = [];
|
|
140
|
+
const queue = [dir];
|
|
141
|
+
|
|
142
|
+
while (queue.length > 0 && files.length < maxFiles) {
|
|
143
|
+
const current = queue.shift();
|
|
144
|
+
try {
|
|
145
|
+
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
146
|
+
for (const entry of entries) {
|
|
147
|
+
if (
|
|
148
|
+
entry.name.startsWith(".") ||
|
|
149
|
+
entry.name === "node_modules" ||
|
|
150
|
+
entry.name === "dist"
|
|
151
|
+
) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const fullPath = path.join(current, entry.name);
|
|
155
|
+
if (entry.isDirectory()) {
|
|
156
|
+
queue.push(fullPath);
|
|
157
|
+
} else if (CODE_EXTENSIONS.has(path.extname(entry.name))) {
|
|
158
|
+
files.push(fullPath);
|
|
159
|
+
if (files.length >= maxFiles) break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
// Skip
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return files;
|
|
168
|
+
}
|