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,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RAG / hybrid search commands
|
|
3
|
+
* chainlesschain search <query> [--mode vector|bm25|hybrid] [--top-k <n>]
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import ora from "ora";
|
|
8
|
+
import { logger } from "../lib/logger.js";
|
|
9
|
+
import { bootstrap, shutdown } from "../runtime/bootstrap.js";
|
|
10
|
+
import { BM25Search } from "../lib/bm25-search.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Ensure the notes table exists (same as note.js)
|
|
14
|
+
*/
|
|
15
|
+
function ensureNotesTable(db) {
|
|
16
|
+
db.exec(`
|
|
17
|
+
CREATE TABLE IF NOT EXISTS notes (
|
|
18
|
+
id TEXT PRIMARY KEY,
|
|
19
|
+
title TEXT NOT NULL,
|
|
20
|
+
content TEXT DEFAULT '',
|
|
21
|
+
tags TEXT DEFAULT '[]',
|
|
22
|
+
category TEXT DEFAULT 'general',
|
|
23
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
24
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
25
|
+
deleted_at TEXT DEFAULT NULL
|
|
26
|
+
)
|
|
27
|
+
`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Load all notes from DB for indexing
|
|
32
|
+
*/
|
|
33
|
+
function loadNotes(db) {
|
|
34
|
+
ensureNotesTable(db);
|
|
35
|
+
return db
|
|
36
|
+
.prepare(
|
|
37
|
+
"SELECT id, title, content, tags, category, created_at FROM notes WHERE deleted_at IS NULL",
|
|
38
|
+
)
|
|
39
|
+
.all();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Simple vector embedding using character/word frequency (fallback when no LLM)
|
|
44
|
+
* Produces a 128-dim feature vector
|
|
45
|
+
*/
|
|
46
|
+
function simpleEmbed(text) {
|
|
47
|
+
if (!text) return new Array(128).fill(0);
|
|
48
|
+
|
|
49
|
+
const normalized = text.toLowerCase();
|
|
50
|
+
const vec = new Array(128).fill(0);
|
|
51
|
+
|
|
52
|
+
// Character frequency features (first 64 dims)
|
|
53
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
54
|
+
const code = normalized.charCodeAt(i);
|
|
55
|
+
vec[code % 64] += 1;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Word-level features (next 64 dims)
|
|
59
|
+
const words = normalized.split(/\s+/);
|
|
60
|
+
for (const word of words) {
|
|
61
|
+
let hash = 0;
|
|
62
|
+
for (let i = 0; i < word.length; i++) {
|
|
63
|
+
hash = (hash * 31 + word.charCodeAt(i)) & 0x7fffffff;
|
|
64
|
+
}
|
|
65
|
+
vec[64 + (hash % 64)] += 1;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Normalize
|
|
69
|
+
const mag = Math.sqrt(vec.reduce((s, v) => s + v * v, 0)) || 1;
|
|
70
|
+
return vec.map((v) => v / mag);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function cosineSimilarity(a, b) {
|
|
74
|
+
let dot = 0;
|
|
75
|
+
let magA = 0;
|
|
76
|
+
let magB = 0;
|
|
77
|
+
for (let i = 0; i < a.length; i++) {
|
|
78
|
+
dot += a[i] * b[i];
|
|
79
|
+
magA += a[i] * a[i];
|
|
80
|
+
magB += b[i] * b[i];
|
|
81
|
+
}
|
|
82
|
+
return dot / (Math.sqrt(magA) * Math.sqrt(magB) || 1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Vector search using simple embeddings
|
|
87
|
+
*/
|
|
88
|
+
function vectorSearch(query, notes, topK) {
|
|
89
|
+
const queryVec = simpleEmbed(query);
|
|
90
|
+
const scored = notes.map((note) => {
|
|
91
|
+
const text = [note.title || "", note.content || ""].join(" ");
|
|
92
|
+
const noteVec = simpleEmbed(text);
|
|
93
|
+
return {
|
|
94
|
+
id: note.id,
|
|
95
|
+
score: cosineSimilarity(queryVec, noteVec),
|
|
96
|
+
doc: note,
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
scored.sort((a, b) => b.score - a.score);
|
|
100
|
+
return scored.slice(0, topK);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Reciprocal Rank Fusion to combine results from multiple methods
|
|
105
|
+
*/
|
|
106
|
+
function rrfFusion(resultSets, k = 60) {
|
|
107
|
+
const scores = new Map();
|
|
108
|
+
|
|
109
|
+
for (const results of resultSets) {
|
|
110
|
+
for (let rank = 0; rank < results.length; rank++) {
|
|
111
|
+
const id = results[rank].id;
|
|
112
|
+
const rrfScore = 1 / (k + rank + 1);
|
|
113
|
+
scores.set(id, (scores.get(id) || 0) + rrfScore);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Get original docs
|
|
118
|
+
const allDocs = new Map();
|
|
119
|
+
for (const results of resultSets) {
|
|
120
|
+
for (const r of results) {
|
|
121
|
+
if (!allDocs.has(r.id)) {
|
|
122
|
+
allDocs.set(r.id, r.doc);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const fused = Array.from(scores.entries()).map(([id, score]) => ({
|
|
128
|
+
id,
|
|
129
|
+
score,
|
|
130
|
+
doc: allDocs.get(id),
|
|
131
|
+
}));
|
|
132
|
+
|
|
133
|
+
fused.sort((a, b) => b.score - a.score);
|
|
134
|
+
return fused;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function registerSearchCommand(program) {
|
|
138
|
+
program
|
|
139
|
+
.command("search")
|
|
140
|
+
.description("Search knowledge base (BM25 + vector hybrid)")
|
|
141
|
+
.argument("<query>", "Search query")
|
|
142
|
+
.option("--mode <mode>", "Search mode: bm25, vector, hybrid", "hybrid")
|
|
143
|
+
.option("--top-k <n>", "Number of results", "10")
|
|
144
|
+
.option("--threshold <n>", "Minimum score threshold", "0")
|
|
145
|
+
.option("--json", "Output as JSON")
|
|
146
|
+
.action(async (query, options) => {
|
|
147
|
+
const spinner = ora("Searching...").start();
|
|
148
|
+
try {
|
|
149
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
150
|
+
if (!ctx.db) {
|
|
151
|
+
spinner.fail("Database not available");
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const db = ctx.db.getDatabase();
|
|
156
|
+
const notes = loadNotes(db);
|
|
157
|
+
|
|
158
|
+
if (notes.length === 0) {
|
|
159
|
+
spinner.info("No notes in knowledge base. Use 'note add' first.");
|
|
160
|
+
await shutdown();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const topK = Math.max(1, parseInt(options.topK) || 10);
|
|
165
|
+
const threshold = parseFloat(options.threshold) || 0;
|
|
166
|
+
const mode = options.mode;
|
|
167
|
+
let results = [];
|
|
168
|
+
|
|
169
|
+
if (mode === "bm25") {
|
|
170
|
+
const bm25 = new BM25Search();
|
|
171
|
+
bm25.indexDocuments(notes);
|
|
172
|
+
results = bm25.search(query, { topK, threshold });
|
|
173
|
+
} else if (mode === "vector") {
|
|
174
|
+
results = vectorSearch(query, notes, topK);
|
|
175
|
+
if (threshold > 0) {
|
|
176
|
+
results = results.filter((r) => r.score >= threshold);
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
// Hybrid: BM25 + vector → RRF fusion
|
|
180
|
+
const bm25 = new BM25Search();
|
|
181
|
+
bm25.indexDocuments(notes);
|
|
182
|
+
const bm25Results = bm25.search(query, { topK: topK * 2 });
|
|
183
|
+
const vecResults = vectorSearch(query, notes, topK * 2);
|
|
184
|
+
results = rrfFusion([bm25Results, vecResults]).slice(0, topK);
|
|
185
|
+
if (threshold > 0) {
|
|
186
|
+
results = results.filter((r) => r.score >= threshold);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
spinner.stop();
|
|
191
|
+
|
|
192
|
+
if (options.json) {
|
|
193
|
+
console.log(
|
|
194
|
+
JSON.stringify(
|
|
195
|
+
results.map((r) => ({
|
|
196
|
+
id: r.id,
|
|
197
|
+
score: r.score,
|
|
198
|
+
title: r.doc.title,
|
|
199
|
+
category: r.doc.category,
|
|
200
|
+
created_at: r.doc.created_at,
|
|
201
|
+
snippet: (r.doc.content || "").substring(0, 200),
|
|
202
|
+
})),
|
|
203
|
+
null,
|
|
204
|
+
2,
|
|
205
|
+
),
|
|
206
|
+
);
|
|
207
|
+
} else if (results.length === 0) {
|
|
208
|
+
logger.info(`No results for "${query}"`);
|
|
209
|
+
} else {
|
|
210
|
+
logger.log(
|
|
211
|
+
chalk.bold(
|
|
212
|
+
`Search results for "${query}" (${results.length}, mode: ${mode}):\n`,
|
|
213
|
+
),
|
|
214
|
+
);
|
|
215
|
+
for (const r of results) {
|
|
216
|
+
const tags = JSON.parse(r.doc.tags || "[]");
|
|
217
|
+
const tagStr =
|
|
218
|
+
tags.length > 0 ? chalk.gray(` [${tags.join(", ")}]`) : "";
|
|
219
|
+
const snippet = (r.doc.content || "")
|
|
220
|
+
.substring(0, 120)
|
|
221
|
+
.replace(/\n/g, " ");
|
|
222
|
+
logger.log(
|
|
223
|
+
` ${chalk.yellow(r.score.toFixed(4))} ${chalk.gray(r.id.slice(0, 8))} ${chalk.white(r.doc.title)}${tagStr}`,
|
|
224
|
+
);
|
|
225
|
+
if (snippet) {
|
|
226
|
+
logger.log(` ${chalk.gray(snippet)}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
await shutdown();
|
|
232
|
+
} catch (err) {
|
|
233
|
+
spinner.fail(`Search failed: ${err.message}`);
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session management commands
|
|
3
|
+
* chainlesschain session list|show|resume|export|delete
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
import { logger } from "../lib/logger.js";
|
|
9
|
+
import { bootstrap, shutdown } from "../runtime/bootstrap.js";
|
|
10
|
+
import {
|
|
11
|
+
listSessions,
|
|
12
|
+
getSession,
|
|
13
|
+
deleteSession,
|
|
14
|
+
exportSessionMarkdown,
|
|
15
|
+
} from "../lib/session-manager.js";
|
|
16
|
+
|
|
17
|
+
export function registerSessionCommand(program) {
|
|
18
|
+
const session = program
|
|
19
|
+
.command("session")
|
|
20
|
+
.description("Conversation session management");
|
|
21
|
+
|
|
22
|
+
// session list
|
|
23
|
+
session
|
|
24
|
+
.command("list", { isDefault: true })
|
|
25
|
+
.description("List saved sessions")
|
|
26
|
+
.option("-n, --limit <n>", "Max sessions", "20")
|
|
27
|
+
.option("--json", "Output as JSON")
|
|
28
|
+
.action(async (options) => {
|
|
29
|
+
try {
|
|
30
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
31
|
+
if (!ctx.db) {
|
|
32
|
+
logger.error("Database not available");
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
const db = ctx.db.getDatabase();
|
|
36
|
+
const sessions = listSessions(db, {
|
|
37
|
+
limit: Math.max(1, parseInt(options.limit) || 20),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (options.json) {
|
|
41
|
+
console.log(JSON.stringify(sessions, null, 2));
|
|
42
|
+
} else if (sessions.length === 0) {
|
|
43
|
+
logger.info(
|
|
44
|
+
"No saved sessions. Use 'chat' or 'agent' to create one.",
|
|
45
|
+
);
|
|
46
|
+
} else {
|
|
47
|
+
logger.log(chalk.bold(`Sessions (${sessions.length}):\n`));
|
|
48
|
+
for (const s of sessions) {
|
|
49
|
+
logger.log(
|
|
50
|
+
` ${chalk.gray(s.id.slice(0, 16))} ${chalk.white(s.title)} ${chalk.cyan(s.message_count + " msgs")} ${chalk.gray(s.updated_at)}`,
|
|
51
|
+
);
|
|
52
|
+
if (s.summary) {
|
|
53
|
+
logger.log(` ${chalk.gray(s.summary.substring(0, 100))}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
await shutdown();
|
|
59
|
+
} catch (err) {
|
|
60
|
+
logger.error(`Failed: ${err.message}`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// session show
|
|
66
|
+
session
|
|
67
|
+
.command("show")
|
|
68
|
+
.description("Show a session's messages")
|
|
69
|
+
.argument("<id>", "Session ID (or prefix)")
|
|
70
|
+
.option("-n, --limit <n>", "Max messages to show")
|
|
71
|
+
.option("--json", "Output as JSON")
|
|
72
|
+
.action(async (id, options) => {
|
|
73
|
+
try {
|
|
74
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
75
|
+
if (!ctx.db) {
|
|
76
|
+
logger.error("Database not available");
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
const db = ctx.db.getDatabase();
|
|
80
|
+
const sess = getSession(db, id);
|
|
81
|
+
|
|
82
|
+
if (!sess) {
|
|
83
|
+
logger.error(`Session not found: ${id}`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (options.json) {
|
|
88
|
+
console.log(JSON.stringify(sess, null, 2));
|
|
89
|
+
} else {
|
|
90
|
+
logger.log(chalk.bold(sess.title));
|
|
91
|
+
logger.log(
|
|
92
|
+
chalk.gray(
|
|
93
|
+
`ID: ${sess.id} Provider: ${sess.provider} Model: ${sess.model} Messages: ${sess.message_count}`,
|
|
94
|
+
),
|
|
95
|
+
);
|
|
96
|
+
logger.log("");
|
|
97
|
+
|
|
98
|
+
let messages = sess.messages;
|
|
99
|
+
if (options.limit) {
|
|
100
|
+
messages = messages.slice(-parseInt(options.limit));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
for (const msg of messages) {
|
|
104
|
+
if (msg.role === "system") continue;
|
|
105
|
+
const label =
|
|
106
|
+
msg.role === "user" ? chalk.green("you> ") : chalk.blue("ai> ");
|
|
107
|
+
const content = (msg.content || "").substring(0, 500);
|
|
108
|
+
logger.log(`${label}${content}`);
|
|
109
|
+
logger.log("");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await shutdown();
|
|
114
|
+
} catch (err) {
|
|
115
|
+
logger.error(`Failed: ${err.message}`);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// session resume
|
|
121
|
+
session
|
|
122
|
+
.command("resume")
|
|
123
|
+
.description("Resume a session in chat mode")
|
|
124
|
+
.argument("<id>", "Session ID (or prefix)")
|
|
125
|
+
.option("--model <model>", "Model name")
|
|
126
|
+
.option("--provider <provider>", "LLM provider")
|
|
127
|
+
.action(async (id, options) => {
|
|
128
|
+
try {
|
|
129
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
130
|
+
if (!ctx.db) {
|
|
131
|
+
logger.error("Database not available");
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
const db = ctx.db.getDatabase();
|
|
135
|
+
const sess = getSession(db, id);
|
|
136
|
+
|
|
137
|
+
if (!sess) {
|
|
138
|
+
logger.error(`Session not found: ${id}`);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
logger.info(
|
|
143
|
+
`Resuming session: ${chalk.cyan(sess.title)} (${sess.message_count} messages)`,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// Import and start chat REPL with restored messages
|
|
147
|
+
const { startChatRepl } = await import("../repl/chat-repl.js");
|
|
148
|
+
await startChatRepl({
|
|
149
|
+
model: options.model || sess.model || "qwen2:7b",
|
|
150
|
+
provider: options.provider || sess.provider || "ollama",
|
|
151
|
+
baseUrl: options.baseUrl || "http://localhost:11434",
|
|
152
|
+
resumeMessages: sess.messages,
|
|
153
|
+
sessionId: sess.id,
|
|
154
|
+
});
|
|
155
|
+
} catch (err) {
|
|
156
|
+
logger.error(`Failed: ${err.message}`);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// session export
|
|
162
|
+
session
|
|
163
|
+
.command("export")
|
|
164
|
+
.description("Export a session as Markdown")
|
|
165
|
+
.argument("<id>", "Session ID (or prefix)")
|
|
166
|
+
.option("-o, --output <file>", "Output file path")
|
|
167
|
+
.action(async (id, options) => {
|
|
168
|
+
try {
|
|
169
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
170
|
+
if (!ctx.db) {
|
|
171
|
+
logger.error("Database not available");
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
const db = ctx.db.getDatabase();
|
|
175
|
+
const sess = getSession(db, id);
|
|
176
|
+
|
|
177
|
+
if (!sess) {
|
|
178
|
+
logger.error(`Session not found: ${id}`);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const markdown = exportSessionMarkdown(sess);
|
|
183
|
+
|
|
184
|
+
if (options.output) {
|
|
185
|
+
fs.writeFileSync(options.output, markdown, "utf8");
|
|
186
|
+
logger.success(`Exported to ${chalk.cyan(options.output)}`);
|
|
187
|
+
} else {
|
|
188
|
+
console.log(markdown);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
await shutdown();
|
|
192
|
+
} catch (err) {
|
|
193
|
+
logger.error(`Failed: ${err.message}`);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// session delete
|
|
199
|
+
session
|
|
200
|
+
.command("delete")
|
|
201
|
+
.description("Delete a session")
|
|
202
|
+
.argument("<id>", "Session ID")
|
|
203
|
+
.option("--force", "Skip confirmation")
|
|
204
|
+
.action(async (id, options) => {
|
|
205
|
+
try {
|
|
206
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
207
|
+
if (!ctx.db) {
|
|
208
|
+
logger.error("Database not available");
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
const db = ctx.db.getDatabase();
|
|
212
|
+
|
|
213
|
+
if (!options.force) {
|
|
214
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
215
|
+
const ok = await confirm({
|
|
216
|
+
message: `Delete session "${id}"?`,
|
|
217
|
+
});
|
|
218
|
+
if (!ok) {
|
|
219
|
+
logger.info("Cancelled");
|
|
220
|
+
await shutdown();
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const ok = deleteSession(db, id);
|
|
226
|
+
if (ok) {
|
|
227
|
+
logger.success("Session deleted");
|
|
228
|
+
} else {
|
|
229
|
+
logger.error(`Session not found: ${id}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
await shutdown();
|
|
233
|
+
} catch (err) {
|
|
234
|
+
logger.error(`Failed: ${err.message}`);
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|