chainlesschain 0.37.8 → 0.37.10
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 +403 -8
- package/bin/chainlesschain.js +4 -0
- package/package.json +7 -2
- package/src/commands/agent.js +30 -0
- package/src/commands/ask.js +114 -0
- package/src/commands/audit.js +286 -0
- package/src/commands/auth.js +387 -0
- package/src/commands/browse.js +184 -0
- package/src/commands/chat.js +35 -0
- package/src/commands/db.js +152 -0
- package/src/commands/did.js +376 -0
- package/src/commands/encrypt.js +233 -0
- package/src/commands/export.js +125 -0
- package/src/commands/git.js +215 -0
- package/src/commands/import.js +259 -0
- package/src/commands/instinct.js +202 -0
- package/src/commands/llm.js +288 -0
- package/src/commands/mcp.js +302 -0
- package/src/commands/memory.js +282 -0
- package/src/commands/note.js +489 -0
- package/src/commands/org.js +505 -0
- package/src/commands/p2p.js +274 -0
- package/src/commands/plugin.js +398 -0
- package/src/commands/search.js +237 -0
- package/src/commands/session.js +238 -0
- package/src/commands/skill.js +479 -0
- package/src/commands/sync.js +249 -0
- package/src/commands/tokens.js +214 -0
- package/src/commands/wallet.js +416 -0
- package/src/index.js +65 -0
- package/src/lib/audit-logger.js +364 -0
- package/src/lib/bm25-search.js +322 -0
- package/src/lib/browser-automation.js +216 -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/git-integration.js +220 -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/platform.js +15 -0
- package/src/lib/plugin-manager.js +312 -0
- package/src/lib/response-cache.js +156 -0
- package/src/lib/session-manager.js +189 -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/repl/agent-repl.js +912 -0
- package/src/repl/chat-repl.js +262 -0
- package/src/runtime/bootstrap.js +159 -0
|
@@ -0,0 +1,312 @@
|
|
|
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
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Ensure plugin tables exist.
|
|
12
|
+
*/
|
|
13
|
+
export function ensurePluginTables(db) {
|
|
14
|
+
db.exec(`
|
|
15
|
+
CREATE TABLE IF NOT EXISTS plugins (
|
|
16
|
+
id TEXT PRIMARY KEY,
|
|
17
|
+
name TEXT NOT NULL UNIQUE,
|
|
18
|
+
version TEXT NOT NULL,
|
|
19
|
+
description TEXT,
|
|
20
|
+
author TEXT,
|
|
21
|
+
homepage TEXT,
|
|
22
|
+
entry_point TEXT,
|
|
23
|
+
permissions TEXT,
|
|
24
|
+
status TEXT DEFAULT 'installed',
|
|
25
|
+
enabled INTEGER DEFAULT 1,
|
|
26
|
+
install_path TEXT,
|
|
27
|
+
installed_at TEXT DEFAULT (datetime('now')),
|
|
28
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
29
|
+
)
|
|
30
|
+
`);
|
|
31
|
+
db.exec(`
|
|
32
|
+
CREATE TABLE IF NOT EXISTS plugin_settings (
|
|
33
|
+
id TEXT PRIMARY KEY,
|
|
34
|
+
plugin_id TEXT NOT NULL,
|
|
35
|
+
key TEXT NOT NULL,
|
|
36
|
+
value TEXT
|
|
37
|
+
)
|
|
38
|
+
`);
|
|
39
|
+
db.exec(`
|
|
40
|
+
CREATE TABLE IF NOT EXISTS plugin_registry (
|
|
41
|
+
name TEXT PRIMARY KEY,
|
|
42
|
+
latest_version TEXT,
|
|
43
|
+
description TEXT,
|
|
44
|
+
author TEXT,
|
|
45
|
+
downloads INTEGER DEFAULT 0,
|
|
46
|
+
rating REAL DEFAULT 0,
|
|
47
|
+
tags TEXT,
|
|
48
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
49
|
+
)
|
|
50
|
+
`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Install a plugin (record in DB).
|
|
55
|
+
*/
|
|
56
|
+
export function installPlugin(db, pluginInfo) {
|
|
57
|
+
ensurePluginTables(db);
|
|
58
|
+
const {
|
|
59
|
+
name,
|
|
60
|
+
version,
|
|
61
|
+
description,
|
|
62
|
+
author,
|
|
63
|
+
homepage,
|
|
64
|
+
entryPoint,
|
|
65
|
+
permissions,
|
|
66
|
+
installPath,
|
|
67
|
+
} = pluginInfo;
|
|
68
|
+
|
|
69
|
+
if (!name || !version) {
|
|
70
|
+
throw new Error("Plugin name and version are required");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Check if already installed
|
|
74
|
+
const existing = getPlugin(db, name);
|
|
75
|
+
if (existing) {
|
|
76
|
+
throw new Error(`Plugin already installed: ${name}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const id = `plugin-${crypto.randomBytes(8).toString("hex")}`;
|
|
80
|
+
|
|
81
|
+
db.prepare(
|
|
82
|
+
`INSERT INTO plugins (id, name, version, description, author, homepage, entry_point, permissions, status, enabled, install_path)
|
|
83
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
84
|
+
).run(
|
|
85
|
+
id,
|
|
86
|
+
name,
|
|
87
|
+
version,
|
|
88
|
+
description || null,
|
|
89
|
+
author || null,
|
|
90
|
+
homepage || null,
|
|
91
|
+
entryPoint || null,
|
|
92
|
+
permissions ? JSON.stringify(permissions) : null,
|
|
93
|
+
"installed",
|
|
94
|
+
1,
|
|
95
|
+
installPath || null,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
id,
|
|
100
|
+
name,
|
|
101
|
+
version,
|
|
102
|
+
description,
|
|
103
|
+
author,
|
|
104
|
+
status: "installed",
|
|
105
|
+
enabled: true,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get a plugin by name.
|
|
111
|
+
*/
|
|
112
|
+
export function getPlugin(db, name) {
|
|
113
|
+
ensurePluginTables(db);
|
|
114
|
+
return db.prepare("SELECT * FROM plugins WHERE name = ?").get(name);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get a plugin by ID.
|
|
119
|
+
*/
|
|
120
|
+
export function getPluginById(db, pluginId) {
|
|
121
|
+
ensurePluginTables(db);
|
|
122
|
+
return db.prepare("SELECT * FROM plugins WHERE id = ?").get(pluginId);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* List all installed plugins.
|
|
127
|
+
*/
|
|
128
|
+
export function listPlugins(db, options = {}) {
|
|
129
|
+
ensurePluginTables(db);
|
|
130
|
+
const { enabledOnly = false } = options;
|
|
131
|
+
|
|
132
|
+
if (enabledOnly) {
|
|
133
|
+
return db
|
|
134
|
+
.prepare("SELECT * FROM plugins WHERE enabled = 1 ORDER BY name ASC")
|
|
135
|
+
.all();
|
|
136
|
+
}
|
|
137
|
+
return db.prepare("SELECT * FROM plugins ORDER BY name ASC").all();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Enable a plugin.
|
|
142
|
+
*/
|
|
143
|
+
export function enablePlugin(db, name) {
|
|
144
|
+
ensurePluginTables(db);
|
|
145
|
+
const result = db
|
|
146
|
+
.prepare("UPDATE plugins SET enabled = ?, status = ? WHERE name = ?")
|
|
147
|
+
.run(1, "installed", name);
|
|
148
|
+
return result.changes > 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Disable a plugin.
|
|
153
|
+
*/
|
|
154
|
+
export function disablePlugin(db, name) {
|
|
155
|
+
ensurePluginTables(db);
|
|
156
|
+
const result = db
|
|
157
|
+
.prepare("UPDATE plugins SET enabled = ?, status = ? WHERE name = ?")
|
|
158
|
+
.run(0, "disabled", name);
|
|
159
|
+
return result.changes > 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Remove (uninstall) a plugin.
|
|
164
|
+
*/
|
|
165
|
+
export function removePlugin(db, name) {
|
|
166
|
+
ensurePluginTables(db);
|
|
167
|
+
// Remove settings first
|
|
168
|
+
const plugin = getPlugin(db, name);
|
|
169
|
+
if (!plugin) return false;
|
|
170
|
+
|
|
171
|
+
db.prepare("DELETE FROM plugin_settings WHERE plugin_id = ?").run(plugin.id);
|
|
172
|
+
const result = db.prepare("DELETE FROM plugins WHERE name = ?").run(name);
|
|
173
|
+
return result.changes > 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Update a plugin version.
|
|
178
|
+
*/
|
|
179
|
+
export function updatePlugin(db, name, newVersion) {
|
|
180
|
+
ensurePluginTables(db);
|
|
181
|
+
const result = db
|
|
182
|
+
.prepare(
|
|
183
|
+
"UPDATE plugins SET version = ?, updated_at = datetime('now') WHERE name = ?",
|
|
184
|
+
)
|
|
185
|
+
.run(newVersion, name);
|
|
186
|
+
return result.changes > 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Set a plugin setting.
|
|
191
|
+
*/
|
|
192
|
+
export function setPluginSetting(db, pluginName, key, value) {
|
|
193
|
+
ensurePluginTables(db);
|
|
194
|
+
const plugin = getPlugin(db, pluginName);
|
|
195
|
+
if (!plugin) throw new Error(`Plugin not found: ${pluginName}`);
|
|
196
|
+
|
|
197
|
+
// Remove existing setting for this key
|
|
198
|
+
db.prepare("DELETE FROM plugin_settings WHERE plugin_id = ? AND key = ?").run(
|
|
199
|
+
plugin.id,
|
|
200
|
+
key,
|
|
201
|
+
);
|
|
202
|
+
const settingId = `ps-${crypto.randomBytes(4).toString("hex")}`;
|
|
203
|
+
db.prepare(
|
|
204
|
+
`INSERT INTO plugin_settings (id, plugin_id, key, value) VALUES (?, ?, ?, ?)`,
|
|
205
|
+
).run(
|
|
206
|
+
settingId,
|
|
207
|
+
plugin.id,
|
|
208
|
+
key,
|
|
209
|
+
typeof value === "string" ? value : JSON.stringify(value),
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Get a plugin setting.
|
|
217
|
+
*/
|
|
218
|
+
export function getPluginSetting(db, pluginName, key) {
|
|
219
|
+
ensurePluginTables(db);
|
|
220
|
+
const plugin = getPlugin(db, pluginName);
|
|
221
|
+
if (!plugin) return null;
|
|
222
|
+
|
|
223
|
+
const row = db
|
|
224
|
+
.prepare(
|
|
225
|
+
"SELECT value FROM plugin_settings WHERE plugin_id = ? AND key = ?",
|
|
226
|
+
)
|
|
227
|
+
.get(plugin.id, key);
|
|
228
|
+
return row ? row.value : null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get all settings for a plugin.
|
|
233
|
+
*/
|
|
234
|
+
export function getPluginSettings(db, pluginName) {
|
|
235
|
+
ensurePluginTables(db);
|
|
236
|
+
const plugin = getPlugin(db, pluginName);
|
|
237
|
+
if (!plugin) return {};
|
|
238
|
+
|
|
239
|
+
const rows = db
|
|
240
|
+
.prepare("SELECT key, value FROM plugin_settings WHERE plugin_id = ?")
|
|
241
|
+
.all(plugin.id);
|
|
242
|
+
const settings = {};
|
|
243
|
+
for (const row of rows) {
|
|
244
|
+
settings[row.key] = row.value;
|
|
245
|
+
}
|
|
246
|
+
return settings;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ─── Registry / Marketplace ─────────────────────────────
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Add/update a plugin in the registry.
|
|
253
|
+
*/
|
|
254
|
+
export function registerInMarketplace(db, pluginInfo) {
|
|
255
|
+
ensurePluginTables(db);
|
|
256
|
+
const { name, latestVersion, description, author, tags } = pluginInfo;
|
|
257
|
+
|
|
258
|
+
db.prepare(
|
|
259
|
+
`INSERT OR REPLACE INTO plugin_registry (name, latest_version, description, author, tags)
|
|
260
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
261
|
+
).run(
|
|
262
|
+
name,
|
|
263
|
+
latestVersion,
|
|
264
|
+
description || null,
|
|
265
|
+
author || null,
|
|
266
|
+
tags ? JSON.stringify(tags) : null,
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
return { name, latestVersion, description, author };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Search plugins in registry.
|
|
274
|
+
*/
|
|
275
|
+
export function searchRegistry(db, query) {
|
|
276
|
+
ensurePluginTables(db);
|
|
277
|
+
return db
|
|
278
|
+
.prepare(
|
|
279
|
+
"SELECT * FROM plugin_registry WHERE name LIKE ? OR description LIKE ? ORDER BY downloads DESC",
|
|
280
|
+
)
|
|
281
|
+
.all(`%${query}%`, `%${query}%`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* List all registry plugins.
|
|
286
|
+
*/
|
|
287
|
+
export function listRegistry(db) {
|
|
288
|
+
ensurePluginTables(db);
|
|
289
|
+
return db
|
|
290
|
+
.prepare("SELECT * FROM plugin_registry ORDER BY downloads DESC")
|
|
291
|
+
.all();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Get plugin summary.
|
|
296
|
+
*/
|
|
297
|
+
export function getPluginSummary(db) {
|
|
298
|
+
ensurePluginTables(db);
|
|
299
|
+
const total = db.prepare("SELECT COUNT(*) as c FROM plugins").get();
|
|
300
|
+
const enabled = db
|
|
301
|
+
.prepare("SELECT COUNT(*) as c FROM plugins WHERE enabled = ?")
|
|
302
|
+
.get(1);
|
|
303
|
+
const registry = db
|
|
304
|
+
.prepare("SELECT COUNT(*) as c FROM plugin_registry")
|
|
305
|
+
.get();
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
installed: total?.c || 0,
|
|
309
|
+
enabled: enabled?.c || 0,
|
|
310
|
+
registryCount: registry?.c || 0,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session manager for CLI
|
|
3
|
+
*
|
|
4
|
+
* Persists chat/agent conversations to DB for resume and export.
|
|
5
|
+
* Lightweight port of desktop-app-vue/src/main/llm/session-manager.js
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createHash } from "crypto";
|
|
9
|
+
|
|
10
|
+
function ensureSessionsTable(db) {
|
|
11
|
+
db.exec(`
|
|
12
|
+
CREATE TABLE IF NOT EXISTS llm_sessions (
|
|
13
|
+
id TEXT PRIMARY KEY,
|
|
14
|
+
title TEXT DEFAULT 'Untitled',
|
|
15
|
+
provider TEXT DEFAULT '',
|
|
16
|
+
model TEXT DEFAULT '',
|
|
17
|
+
message_count INTEGER DEFAULT 0,
|
|
18
|
+
messages TEXT DEFAULT '[]',
|
|
19
|
+
summary TEXT DEFAULT '',
|
|
20
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
21
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
22
|
+
)
|
|
23
|
+
`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create a new session
|
|
28
|
+
*/
|
|
29
|
+
export function createSession(db, options = {}) {
|
|
30
|
+
ensureSessionsTable(db);
|
|
31
|
+
|
|
32
|
+
const id =
|
|
33
|
+
options.id ||
|
|
34
|
+
`session-${Date.now()}-${createHash("sha256").update(Math.random().toString()).digest("hex").slice(0, 6)}`;
|
|
35
|
+
|
|
36
|
+
db.prepare(
|
|
37
|
+
`INSERT INTO llm_sessions (id, title, provider, model, messages) VALUES (?, ?, ?, ?, ?)`,
|
|
38
|
+
).run(
|
|
39
|
+
id,
|
|
40
|
+
options.title || "Untitled",
|
|
41
|
+
options.provider || "",
|
|
42
|
+
options.model || "",
|
|
43
|
+
JSON.stringify(options.messages || []),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return { id, title: options.title || "Untitled" };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Add a message to a session
|
|
51
|
+
*/
|
|
52
|
+
export function addMessage(db, sessionId, role, content) {
|
|
53
|
+
ensureSessionsTable(db);
|
|
54
|
+
|
|
55
|
+
const session = db
|
|
56
|
+
.prepare("SELECT messages, message_count FROM llm_sessions WHERE id = ?")
|
|
57
|
+
.get(sessionId);
|
|
58
|
+
|
|
59
|
+
if (!session) return null;
|
|
60
|
+
|
|
61
|
+
const messages = JSON.parse(session.messages || "[]");
|
|
62
|
+
messages.push({ role, content, timestamp: new Date().toISOString() });
|
|
63
|
+
|
|
64
|
+
db.prepare(
|
|
65
|
+
`UPDATE llm_sessions SET messages = ?, message_count = ?, updated_at = datetime('now') WHERE id = ?`,
|
|
66
|
+
).run(JSON.stringify(messages), messages.length, sessionId);
|
|
67
|
+
|
|
68
|
+
return { messageCount: messages.length };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Save all messages at once (batch update)
|
|
73
|
+
*/
|
|
74
|
+
export function saveMessages(db, sessionId, messages) {
|
|
75
|
+
ensureSessionsTable(db);
|
|
76
|
+
|
|
77
|
+
const result = db
|
|
78
|
+
.prepare(
|
|
79
|
+
`UPDATE llm_sessions SET messages = ?, message_count = ?, updated_at = datetime('now') WHERE id = ?`,
|
|
80
|
+
)
|
|
81
|
+
.run(JSON.stringify(messages), messages.length, sessionId);
|
|
82
|
+
|
|
83
|
+
return { messageCount: messages.length, updated: result.changes > 0 };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get a session by ID
|
|
88
|
+
*/
|
|
89
|
+
export function getSession(db, sessionId) {
|
|
90
|
+
ensureSessionsTable(db);
|
|
91
|
+
|
|
92
|
+
// Try exact match first, then prefix match
|
|
93
|
+
let session = db
|
|
94
|
+
.prepare("SELECT * FROM llm_sessions WHERE id = ?")
|
|
95
|
+
.get(sessionId);
|
|
96
|
+
|
|
97
|
+
if (!session) {
|
|
98
|
+
session = db
|
|
99
|
+
.prepare("SELECT * FROM llm_sessions WHERE id LIKE ? LIMIT 1")
|
|
100
|
+
.get(`${sessionId}%`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!session) return null;
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
...session,
|
|
107
|
+
messages: JSON.parse(session.messages || "[]"),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* List all sessions
|
|
113
|
+
*/
|
|
114
|
+
export function listSessions(db, options = {}) {
|
|
115
|
+
ensureSessionsTable(db);
|
|
116
|
+
|
|
117
|
+
const limit = options.limit || 20;
|
|
118
|
+
|
|
119
|
+
return db
|
|
120
|
+
.prepare(
|
|
121
|
+
`SELECT id, title, provider, model, message_count, summary, created_at, updated_at
|
|
122
|
+
FROM llm_sessions
|
|
123
|
+
ORDER BY updated_at DESC
|
|
124
|
+
LIMIT ?`,
|
|
125
|
+
)
|
|
126
|
+
.all(limit);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Update session title or summary
|
|
131
|
+
*/
|
|
132
|
+
export function updateSession(db, sessionId, updates) {
|
|
133
|
+
ensureSessionsTable(db);
|
|
134
|
+
|
|
135
|
+
if (updates.title) {
|
|
136
|
+
db.prepare(
|
|
137
|
+
"UPDATE llm_sessions SET title = ?, updated_at = datetime('now') WHERE id = ?",
|
|
138
|
+
).run(updates.title, sessionId);
|
|
139
|
+
}
|
|
140
|
+
if (updates.summary) {
|
|
141
|
+
db.prepare(
|
|
142
|
+
"UPDATE llm_sessions SET summary = ?, updated_at = datetime('now') WHERE id = ?",
|
|
143
|
+
).run(updates.summary, sessionId);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Delete a session
|
|
149
|
+
*/
|
|
150
|
+
export function deleteSession(db, sessionId) {
|
|
151
|
+
ensureSessionsTable(db);
|
|
152
|
+
const result = db
|
|
153
|
+
.prepare("DELETE FROM llm_sessions WHERE id = ?")
|
|
154
|
+
.run(sessionId);
|
|
155
|
+
return result.changes > 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Export session as markdown
|
|
160
|
+
*/
|
|
161
|
+
export function exportSessionMarkdown(session) {
|
|
162
|
+
const lines = [
|
|
163
|
+
`# ${session.title}`,
|
|
164
|
+
"",
|
|
165
|
+
`- **Created**: ${session.created_at}`,
|
|
166
|
+
`- **Provider**: ${session.provider || "unknown"}`,
|
|
167
|
+
`- **Model**: ${session.model || "unknown"}`,
|
|
168
|
+
`- **Messages**: ${session.message_count}`,
|
|
169
|
+
"",
|
|
170
|
+
"---",
|
|
171
|
+
"",
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
const messages =
|
|
175
|
+
typeof session.messages === "string"
|
|
176
|
+
? JSON.parse(session.messages)
|
|
177
|
+
: session.messages || [];
|
|
178
|
+
|
|
179
|
+
for (const msg of messages) {
|
|
180
|
+
if (msg.role === "system") continue;
|
|
181
|
+
const label = msg.role === "user" ? "**You**" : "**AI**";
|
|
182
|
+
lines.push(`### ${label}`);
|
|
183
|
+
lines.push("");
|
|
184
|
+
lines.push(msg.content || "");
|
|
185
|
+
lines.push("");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return lines.join("\n");
|
|
189
|
+
}
|