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,430 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Manager — Plugin installation, management, and marketplace for CLI.
|
|
3
|
+
* Manages plugin lifecycle: install, enable, disable, remove, update.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import crypto from "crypto";
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { getElectronUserDataDir } from "./paths.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Ensure plugin tables exist.
|
|
13
|
+
*/
|
|
14
|
+
export function ensurePluginTables(db) {
|
|
15
|
+
db.exec(`
|
|
16
|
+
CREATE TABLE IF NOT EXISTS plugins (
|
|
17
|
+
id TEXT PRIMARY KEY,
|
|
18
|
+
name TEXT NOT NULL UNIQUE,
|
|
19
|
+
version TEXT NOT NULL,
|
|
20
|
+
description TEXT,
|
|
21
|
+
author TEXT,
|
|
22
|
+
homepage TEXT,
|
|
23
|
+
entry_point TEXT,
|
|
24
|
+
permissions TEXT,
|
|
25
|
+
status TEXT DEFAULT 'installed',
|
|
26
|
+
enabled INTEGER DEFAULT 1,
|
|
27
|
+
install_path TEXT,
|
|
28
|
+
installed_at TEXT DEFAULT (datetime('now')),
|
|
29
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
30
|
+
)
|
|
31
|
+
`);
|
|
32
|
+
db.exec(`
|
|
33
|
+
CREATE TABLE IF NOT EXISTS plugin_settings (
|
|
34
|
+
id TEXT PRIMARY KEY,
|
|
35
|
+
plugin_id TEXT NOT NULL,
|
|
36
|
+
key TEXT NOT NULL,
|
|
37
|
+
value TEXT
|
|
38
|
+
)
|
|
39
|
+
`);
|
|
40
|
+
db.exec(`
|
|
41
|
+
CREATE TABLE IF NOT EXISTS plugin_skills (
|
|
42
|
+
id TEXT PRIMARY KEY,
|
|
43
|
+
plugin_name TEXT NOT NULL,
|
|
44
|
+
skill_name TEXT NOT NULL,
|
|
45
|
+
skill_path TEXT NOT NULL,
|
|
46
|
+
installed_at TEXT DEFAULT (datetime('now'))
|
|
47
|
+
)
|
|
48
|
+
`);
|
|
49
|
+
db.exec(`
|
|
50
|
+
CREATE TABLE IF NOT EXISTS plugin_registry (
|
|
51
|
+
name TEXT PRIMARY KEY,
|
|
52
|
+
latest_version TEXT,
|
|
53
|
+
description TEXT,
|
|
54
|
+
author TEXT,
|
|
55
|
+
downloads INTEGER DEFAULT 0,
|
|
56
|
+
rating REAL DEFAULT 0,
|
|
57
|
+
tags TEXT,
|
|
58
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
59
|
+
)
|
|
60
|
+
`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Install a plugin (record in DB).
|
|
65
|
+
*/
|
|
66
|
+
export function installPlugin(db, pluginInfo) {
|
|
67
|
+
ensurePluginTables(db);
|
|
68
|
+
const {
|
|
69
|
+
name,
|
|
70
|
+
version,
|
|
71
|
+
description,
|
|
72
|
+
author,
|
|
73
|
+
homepage,
|
|
74
|
+
entryPoint,
|
|
75
|
+
permissions,
|
|
76
|
+
installPath,
|
|
77
|
+
} = pluginInfo;
|
|
78
|
+
|
|
79
|
+
if (!name || !version) {
|
|
80
|
+
throw new Error("Plugin name and version are required");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check if already installed
|
|
84
|
+
const existing = getPlugin(db, name);
|
|
85
|
+
if (existing) {
|
|
86
|
+
throw new Error(`Plugin already installed: ${name}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const id = `plugin-${crypto.randomBytes(8).toString("hex")}`;
|
|
90
|
+
|
|
91
|
+
db.prepare(
|
|
92
|
+
`INSERT INTO plugins (id, name, version, description, author, homepage, entry_point, permissions, status, enabled, install_path)
|
|
93
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
94
|
+
).run(
|
|
95
|
+
id,
|
|
96
|
+
name,
|
|
97
|
+
version,
|
|
98
|
+
description || null,
|
|
99
|
+
author || null,
|
|
100
|
+
homepage || null,
|
|
101
|
+
entryPoint || null,
|
|
102
|
+
permissions ? JSON.stringify(permissions) : null,
|
|
103
|
+
"installed",
|
|
104
|
+
1,
|
|
105
|
+
installPath || null,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
id,
|
|
110
|
+
name,
|
|
111
|
+
version,
|
|
112
|
+
description,
|
|
113
|
+
author,
|
|
114
|
+
status: "installed",
|
|
115
|
+
enabled: true,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get a plugin by name.
|
|
121
|
+
*/
|
|
122
|
+
export function getPlugin(db, name) {
|
|
123
|
+
ensurePluginTables(db);
|
|
124
|
+
return db.prepare("SELECT * FROM plugins WHERE name = ?").get(name);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get a plugin by ID.
|
|
129
|
+
*/
|
|
130
|
+
export function getPluginById(db, pluginId) {
|
|
131
|
+
ensurePluginTables(db);
|
|
132
|
+
return db.prepare("SELECT * FROM plugins WHERE id = ?").get(pluginId);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* List all installed plugins.
|
|
137
|
+
*/
|
|
138
|
+
export function listPlugins(db, options = {}) {
|
|
139
|
+
ensurePluginTables(db);
|
|
140
|
+
const { enabledOnly = false } = options;
|
|
141
|
+
|
|
142
|
+
if (enabledOnly) {
|
|
143
|
+
return db
|
|
144
|
+
.prepare("SELECT * FROM plugins WHERE enabled = 1 ORDER BY name ASC")
|
|
145
|
+
.all();
|
|
146
|
+
}
|
|
147
|
+
return db.prepare("SELECT * FROM plugins ORDER BY name ASC").all();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Enable a plugin.
|
|
152
|
+
*/
|
|
153
|
+
export function enablePlugin(db, name) {
|
|
154
|
+
ensurePluginTables(db);
|
|
155
|
+
const result = db
|
|
156
|
+
.prepare("UPDATE plugins SET enabled = ?, status = ? WHERE name = ?")
|
|
157
|
+
.run(1, "installed", name);
|
|
158
|
+
return result.changes > 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Disable a plugin.
|
|
163
|
+
*/
|
|
164
|
+
export function disablePlugin(db, name) {
|
|
165
|
+
ensurePluginTables(db);
|
|
166
|
+
const result = db
|
|
167
|
+
.prepare("UPDATE plugins SET enabled = ?, status = ? WHERE name = ?")
|
|
168
|
+
.run(0, "disabled", name);
|
|
169
|
+
return result.changes > 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Remove (uninstall) a plugin.
|
|
174
|
+
*/
|
|
175
|
+
export function removePlugin(db, name) {
|
|
176
|
+
ensurePluginTables(db);
|
|
177
|
+
// Remove settings first
|
|
178
|
+
const plugin = getPlugin(db, name);
|
|
179
|
+
if (!plugin) return false;
|
|
180
|
+
|
|
181
|
+
db.prepare("DELETE FROM plugin_settings WHERE plugin_id = ?").run(plugin.id);
|
|
182
|
+
const result = db.prepare("DELETE FROM plugins WHERE name = ?").run(name);
|
|
183
|
+
return result.changes > 0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Update a plugin version.
|
|
188
|
+
*/
|
|
189
|
+
export function updatePlugin(db, name, newVersion) {
|
|
190
|
+
ensurePluginTables(db);
|
|
191
|
+
const result = db
|
|
192
|
+
.prepare(
|
|
193
|
+
"UPDATE plugins SET version = ?, updated_at = datetime('now') WHERE name = ?",
|
|
194
|
+
)
|
|
195
|
+
.run(newVersion, name);
|
|
196
|
+
return result.changes > 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Set a plugin setting.
|
|
201
|
+
*/
|
|
202
|
+
export function setPluginSetting(db, pluginName, key, value) {
|
|
203
|
+
ensurePluginTables(db);
|
|
204
|
+
const plugin = getPlugin(db, pluginName);
|
|
205
|
+
if (!plugin) throw new Error(`Plugin not found: ${pluginName}`);
|
|
206
|
+
|
|
207
|
+
// Remove existing setting for this key
|
|
208
|
+
db.prepare("DELETE FROM plugin_settings WHERE plugin_id = ? AND key = ?").run(
|
|
209
|
+
plugin.id,
|
|
210
|
+
key,
|
|
211
|
+
);
|
|
212
|
+
const settingId = `ps-${crypto.randomBytes(4).toString("hex")}`;
|
|
213
|
+
db.prepare(
|
|
214
|
+
`INSERT INTO plugin_settings (id, plugin_id, key, value) VALUES (?, ?, ?, ?)`,
|
|
215
|
+
).run(
|
|
216
|
+
settingId,
|
|
217
|
+
plugin.id,
|
|
218
|
+
key,
|
|
219
|
+
typeof value === "string" ? value : JSON.stringify(value),
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Get a plugin setting.
|
|
227
|
+
*/
|
|
228
|
+
export function getPluginSetting(db, pluginName, key) {
|
|
229
|
+
ensurePluginTables(db);
|
|
230
|
+
const plugin = getPlugin(db, pluginName);
|
|
231
|
+
if (!plugin) return null;
|
|
232
|
+
|
|
233
|
+
const row = db
|
|
234
|
+
.prepare(
|
|
235
|
+
"SELECT value FROM plugin_settings WHERE plugin_id = ? AND key = ?",
|
|
236
|
+
)
|
|
237
|
+
.get(plugin.id, key);
|
|
238
|
+
return row ? row.value : null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Get all settings for a plugin.
|
|
243
|
+
*/
|
|
244
|
+
export function getPluginSettings(db, pluginName) {
|
|
245
|
+
ensurePluginTables(db);
|
|
246
|
+
const plugin = getPlugin(db, pluginName);
|
|
247
|
+
if (!plugin) return {};
|
|
248
|
+
|
|
249
|
+
const rows = db
|
|
250
|
+
.prepare("SELECT key, value FROM plugin_settings WHERE plugin_id = ?")
|
|
251
|
+
.all(plugin.id);
|
|
252
|
+
const settings = {};
|
|
253
|
+
for (const row of rows) {
|
|
254
|
+
settings[row.key] = row.value;
|
|
255
|
+
}
|
|
256
|
+
return settings;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ─── Registry / Marketplace ─────────────────────────────
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Add/update a plugin in the registry.
|
|
263
|
+
*/
|
|
264
|
+
export function registerInMarketplace(db, pluginInfo) {
|
|
265
|
+
ensurePluginTables(db);
|
|
266
|
+
const { name, latestVersion, description, author, tags } = pluginInfo;
|
|
267
|
+
|
|
268
|
+
db.prepare(
|
|
269
|
+
`INSERT OR REPLACE INTO plugin_registry (name, latest_version, description, author, tags)
|
|
270
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
271
|
+
).run(
|
|
272
|
+
name,
|
|
273
|
+
latestVersion,
|
|
274
|
+
description || null,
|
|
275
|
+
author || null,
|
|
276
|
+
tags ? JSON.stringify(tags) : null,
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
return { name, latestVersion, description, author };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Search plugins in registry.
|
|
284
|
+
*/
|
|
285
|
+
export function searchRegistry(db, query) {
|
|
286
|
+
ensurePluginTables(db);
|
|
287
|
+
return db
|
|
288
|
+
.prepare(
|
|
289
|
+
"SELECT * FROM plugin_registry WHERE name LIKE ? OR description LIKE ? ORDER BY downloads DESC",
|
|
290
|
+
)
|
|
291
|
+
.all(`%${query}%`, `%${query}%`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* List all registry plugins.
|
|
296
|
+
*/
|
|
297
|
+
export function listRegistry(db) {
|
|
298
|
+
ensurePluginTables(db);
|
|
299
|
+
return db
|
|
300
|
+
.prepare("SELECT * FROM plugin_registry ORDER BY downloads DESC")
|
|
301
|
+
.all();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Get plugin summary.
|
|
306
|
+
*/
|
|
307
|
+
export function getPluginSummary(db) {
|
|
308
|
+
ensurePluginTables(db);
|
|
309
|
+
const total = db.prepare("SELECT COUNT(*) as c FROM plugins").get();
|
|
310
|
+
const enabled = db
|
|
311
|
+
.prepare("SELECT COUNT(*) as c FROM plugins WHERE enabled = ?")
|
|
312
|
+
.get(1);
|
|
313
|
+
const registry = db
|
|
314
|
+
.prepare("SELECT COUNT(*) as c FROM plugin_registry")
|
|
315
|
+
.get();
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
installed: total?.c || 0,
|
|
319
|
+
enabled: enabled?.c || 0,
|
|
320
|
+
registryCount: registry?.c || 0,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ─── Plugin Skills ──────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Get the marketplace skills directory
|
|
328
|
+
*/
|
|
329
|
+
function getMarketplaceSkillsDir() {
|
|
330
|
+
return path.join(getElectronUserDataDir(), "marketplace", "skills");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Install skills from a plugin manifest.
|
|
335
|
+
* Copies skill directories to the marketplace/skills/ layer.
|
|
336
|
+
*
|
|
337
|
+
* @param {object} db - Database instance
|
|
338
|
+
* @param {string} pluginName - Plugin name
|
|
339
|
+
* @param {string} pluginPath - Root path of the plugin package
|
|
340
|
+
* @param {{ name: string, path: string }[]} skills - Skills declared in manifest
|
|
341
|
+
* @returns {{ installed: string[] }} Names of installed skills
|
|
342
|
+
*/
|
|
343
|
+
export function installPluginSkills(db, pluginName, pluginPath, skills) {
|
|
344
|
+
ensurePluginTables(db);
|
|
345
|
+
if (!skills || skills.length === 0) return { installed: [] };
|
|
346
|
+
|
|
347
|
+
const marketplaceDir = getMarketplaceSkillsDir();
|
|
348
|
+
const installed = [];
|
|
349
|
+
|
|
350
|
+
for (const skill of skills) {
|
|
351
|
+
const srcDir = path.resolve(pluginPath, skill.path);
|
|
352
|
+
if (!fs.existsSync(srcDir)) continue;
|
|
353
|
+
|
|
354
|
+
const destDir = path.join(marketplaceDir, skill.name);
|
|
355
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
356
|
+
|
|
357
|
+
// Copy skill files
|
|
358
|
+
_copyDirSync(srcDir, destDir);
|
|
359
|
+
|
|
360
|
+
// Record in DB
|
|
361
|
+
const id = `ps-${crypto.randomBytes(6).toString("hex")}`;
|
|
362
|
+
db.prepare(
|
|
363
|
+
`INSERT OR REPLACE INTO plugin_skills (id, plugin_name, skill_name, skill_path)
|
|
364
|
+
VALUES (?, ?, ?, ?)`,
|
|
365
|
+
).run(id, pluginName, skill.name, destDir);
|
|
366
|
+
|
|
367
|
+
installed.push(skill.name);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return { installed };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Remove all skills installed by a plugin.
|
|
375
|
+
*
|
|
376
|
+
* @param {object} db - Database instance
|
|
377
|
+
* @param {string} pluginName - Plugin name
|
|
378
|
+
* @returns {{ removed: string[] }} Names of removed skills
|
|
379
|
+
*/
|
|
380
|
+
export function removePluginSkills(db, pluginName) {
|
|
381
|
+
ensurePluginTables(db);
|
|
382
|
+
const rows = db
|
|
383
|
+
.prepare("SELECT * FROM plugin_skills WHERE plugin_name = ?")
|
|
384
|
+
.all(pluginName);
|
|
385
|
+
|
|
386
|
+
const removed = [];
|
|
387
|
+
for (const row of rows) {
|
|
388
|
+
// Remove the skill directory
|
|
389
|
+
if (fs.existsSync(row.skill_path)) {
|
|
390
|
+
fs.rmSync(row.skill_path, { recursive: true, force: true });
|
|
391
|
+
}
|
|
392
|
+
removed.push(row.skill_name);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
db.prepare("DELETE FROM plugin_skills WHERE plugin_name = ?").run(pluginName);
|
|
396
|
+
return { removed };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* List skills installed by a specific plugin.
|
|
401
|
+
*
|
|
402
|
+
* @param {object} db - Database instance
|
|
403
|
+
* @param {string} pluginName - Plugin name
|
|
404
|
+
* @returns {{ skill_name: string, skill_path: string }[]}
|
|
405
|
+
*/
|
|
406
|
+
export function getPluginSkills(db, pluginName) {
|
|
407
|
+
ensurePluginTables(db);
|
|
408
|
+
return db
|
|
409
|
+
.prepare(
|
|
410
|
+
"SELECT skill_name, skill_path FROM plugin_skills WHERE plugin_name = ?",
|
|
411
|
+
)
|
|
412
|
+
.all(pluginName);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Recursively copy a directory
|
|
417
|
+
*/
|
|
418
|
+
function _copyDirSync(src, dest) {
|
|
419
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
420
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
421
|
+
for (const entry of entries) {
|
|
422
|
+
const srcPath = path.join(src, entry.name);
|
|
423
|
+
const destPath = path.join(dest, entry.name);
|
|
424
|
+
if (entry.isDirectory()) {
|
|
425
|
+
_copyDirSync(srcPath, destPath);
|
|
426
|
+
} else {
|
|
427
|
+
fs.copyFileSync(srcPath, destPath);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project root detection utility
|
|
3
|
+
* Finds and loads .chainlesschain/ project configuration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Walk up from startDir looking for .chainlesschain/config.json
|
|
11
|
+
* @param {string} [startDir=process.cwd()] - Directory to start searching from
|
|
12
|
+
* @returns {string|null} Project root directory or null
|
|
13
|
+
*/
|
|
14
|
+
export function findProjectRoot(startDir) {
|
|
15
|
+
let dir = path.resolve(startDir || process.cwd());
|
|
16
|
+
const root = path.parse(dir).root;
|
|
17
|
+
|
|
18
|
+
while (dir !== root) {
|
|
19
|
+
const configPath = path.join(dir, ".chainlesschain", "config.json");
|
|
20
|
+
if (fs.existsSync(configPath)) {
|
|
21
|
+
return dir;
|
|
22
|
+
}
|
|
23
|
+
const parent = path.dirname(dir);
|
|
24
|
+
if (parent === dir) break;
|
|
25
|
+
dir = parent;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Load project configuration from .chainlesschain/config.json
|
|
33
|
+
* @param {string} projectRoot - Project root directory
|
|
34
|
+
* @returns {object|null} Parsed config or null on error
|
|
35
|
+
*/
|
|
36
|
+
export function loadProjectConfig(projectRoot) {
|
|
37
|
+
const configPath = path.join(projectRoot, ".chainlesschain", "config.json");
|
|
38
|
+
try {
|
|
39
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
40
|
+
return JSON.parse(content);
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Quick boolean check: are we inside a .chainlesschain project?
|
|
48
|
+
* @param {string} [startDir] - Directory to check from
|
|
49
|
+
* @returns {boolean}
|
|
50
|
+
*/
|
|
51
|
+
export function isInsideProject(startDir) {
|
|
52
|
+
return findProjectRoot(startDir) !== null;
|
|
53
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response cache for CLI
|
|
3
|
+
*
|
|
4
|
+
* Caches LLM responses to avoid redundant API calls.
|
|
5
|
+
* Lightweight port of desktop-app-vue/src/main/llm/response-cache.js
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createHash } from "crypto";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_TTL = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
11
|
+
const DEFAULT_MAX_SIZE = 500;
|
|
12
|
+
|
|
13
|
+
function ensureCacheTable(db) {
|
|
14
|
+
db.exec(`
|
|
15
|
+
CREATE TABLE IF NOT EXISTS llm_cache (
|
|
16
|
+
cache_key TEXT PRIMARY KEY,
|
|
17
|
+
provider TEXT NOT NULL,
|
|
18
|
+
model TEXT NOT NULL,
|
|
19
|
+
request_hash TEXT NOT NULL,
|
|
20
|
+
response_content TEXT NOT NULL,
|
|
21
|
+
response_tokens INTEGER DEFAULT 0,
|
|
22
|
+
hit_count INTEGER DEFAULT 0,
|
|
23
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
24
|
+
last_accessed_at TEXT DEFAULT (datetime('now')),
|
|
25
|
+
expires_at TEXT NOT NULL
|
|
26
|
+
)
|
|
27
|
+
`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Generate a cache key from request parameters
|
|
32
|
+
*/
|
|
33
|
+
function generateCacheKey(provider, model, messages) {
|
|
34
|
+
const payload = JSON.stringify({ provider, model, messages });
|
|
35
|
+
return createHash("sha256").update(payload).digest("hex");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Look up a cached response
|
|
40
|
+
* @returns {{ hit: boolean, response?: string, tokensSaved?: number, cacheAge?: number }}
|
|
41
|
+
*/
|
|
42
|
+
export function getCachedResponse(db, provider, model, messages) {
|
|
43
|
+
ensureCacheTable(db);
|
|
44
|
+
|
|
45
|
+
const key = generateCacheKey(provider, model, messages);
|
|
46
|
+
|
|
47
|
+
const row = db
|
|
48
|
+
.prepare(
|
|
49
|
+
`SELECT * FROM llm_cache WHERE cache_key = ? AND expires_at > datetime('now')`,
|
|
50
|
+
)
|
|
51
|
+
.get(key);
|
|
52
|
+
|
|
53
|
+
if (!row) {
|
|
54
|
+
return { hit: false };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Update access stats
|
|
58
|
+
db.prepare(
|
|
59
|
+
`UPDATE llm_cache SET hit_count = hit_count + 1, last_accessed_at = datetime('now') WHERE cache_key = ?`,
|
|
60
|
+
).run(key);
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
hit: true,
|
|
64
|
+
response: row.response_content,
|
|
65
|
+
tokensSaved: row.response_tokens,
|
|
66
|
+
cacheAge: Date.now() - new Date(row.created_at).getTime(),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Store a response in cache
|
|
72
|
+
*/
|
|
73
|
+
export function setCachedResponse(
|
|
74
|
+
db,
|
|
75
|
+
provider,
|
|
76
|
+
model,
|
|
77
|
+
messages,
|
|
78
|
+
response,
|
|
79
|
+
options = {},
|
|
80
|
+
) {
|
|
81
|
+
ensureCacheTable(db);
|
|
82
|
+
|
|
83
|
+
const key = generateCacheKey(provider, model, messages);
|
|
84
|
+
const requestHash = createHash("md5")
|
|
85
|
+
.update(JSON.stringify(messages))
|
|
86
|
+
.digest("hex");
|
|
87
|
+
const ttl = options.ttl || DEFAULT_TTL;
|
|
88
|
+
const maxSize = options.maxSize || DEFAULT_MAX_SIZE;
|
|
89
|
+
const expiresAt = new Date(Date.now() + ttl).toISOString();
|
|
90
|
+
const responseTokens = options.responseTokens || 0;
|
|
91
|
+
|
|
92
|
+
// LRU eviction if needed
|
|
93
|
+
const countRow = db.prepare("SELECT COUNT(*) as cnt FROM llm_cache").get();
|
|
94
|
+
const count = countRow?.cnt || 0;
|
|
95
|
+
if (count >= maxSize) {
|
|
96
|
+
db.prepare(
|
|
97
|
+
`DELETE FROM llm_cache WHERE cache_key IN (
|
|
98
|
+
SELECT cache_key FROM llm_cache ORDER BY last_accessed_at ASC, created_at ASC LIMIT ?
|
|
99
|
+
)`,
|
|
100
|
+
).run(Math.max(1, Math.ceil(maxSize * 0.1)));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
db.prepare(
|
|
104
|
+
`INSERT OR REPLACE INTO llm_cache (cache_key, provider, model, request_hash, response_content, response_tokens, expires_at)
|
|
105
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
106
|
+
).run(key, provider, model, requestHash, response, responseTokens, expiresAt);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Clear all cached responses
|
|
111
|
+
*/
|
|
112
|
+
export function clearCache(db) {
|
|
113
|
+
ensureCacheTable(db);
|
|
114
|
+
db.prepare("DELETE FROM llm_cache").run();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Remove expired entries
|
|
119
|
+
*/
|
|
120
|
+
export function clearExpired(db) {
|
|
121
|
+
ensureCacheTable(db);
|
|
122
|
+
const result = db
|
|
123
|
+
.prepare("DELETE FROM llm_cache WHERE expires_at <= datetime('now')")
|
|
124
|
+
.run();
|
|
125
|
+
return result.changes;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get cache statistics
|
|
130
|
+
*/
|
|
131
|
+
export function getCacheStats(db) {
|
|
132
|
+
ensureCacheTable(db);
|
|
133
|
+
|
|
134
|
+
const stats = db
|
|
135
|
+
.prepare(
|
|
136
|
+
`SELECT
|
|
137
|
+
COUNT(*) as total_entries,
|
|
138
|
+
COALESCE(SUM(hit_count), 0) as total_hits,
|
|
139
|
+
COALESCE(SUM(response_tokens), 0) as total_tokens_saved
|
|
140
|
+
FROM llm_cache`,
|
|
141
|
+
)
|
|
142
|
+
.get();
|
|
143
|
+
|
|
144
|
+
const expired = db
|
|
145
|
+
.prepare(
|
|
146
|
+
"SELECT COUNT(*) as cnt FROM llm_cache WHERE expires_at <= datetime('now')",
|
|
147
|
+
)
|
|
148
|
+
.get();
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
total_entries: stats?.total_entries || 0,
|
|
152
|
+
total_hits: stats?.total_hits || 0,
|
|
153
|
+
total_tokens_saved: stats?.total_tokens_saved || 0,
|
|
154
|
+
expired_entries: expired?.cnt || 0,
|
|
155
|
+
};
|
|
156
|
+
}
|