ccrecall 0.0.4 → 0.0.5
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/package.json +1 -1
- package/src/cli.ts +329 -0
- package/src/db.ts +162 -0
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -92,6 +92,331 @@ Database: ${db_path}
|
|
|
92
92
|
},
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
+
export const query = defineCommand({
|
|
96
|
+
meta: {
|
|
97
|
+
name: 'query',
|
|
98
|
+
description: 'Execute raw SQL against the database',
|
|
99
|
+
},
|
|
100
|
+
args: {
|
|
101
|
+
...sharedArgs,
|
|
102
|
+
sql: {
|
|
103
|
+
type: 'positional' as const,
|
|
104
|
+
description: 'SQL query to execute',
|
|
105
|
+
required: true,
|
|
106
|
+
},
|
|
107
|
+
format: {
|
|
108
|
+
type: 'string',
|
|
109
|
+
alias: 'f',
|
|
110
|
+
description: 'Output format: table, json, csv (default: table)',
|
|
111
|
+
},
|
|
112
|
+
limit: {
|
|
113
|
+
type: 'string',
|
|
114
|
+
alias: 'l',
|
|
115
|
+
description: 'Limit rows (appends LIMIT clause if not present)',
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
async run({ args }) {
|
|
119
|
+
const { Database: BunDB } = await import('bun:sqlite');
|
|
120
|
+
const { existsSync } = await import('fs');
|
|
121
|
+
|
|
122
|
+
const db_path = args.db ?? DEFAULT_DB_PATH;
|
|
123
|
+
if (!existsSync(db_path)) {
|
|
124
|
+
console.error(`Database not found: ${db_path}`);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const db = new BunDB(db_path, { readonly: true });
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
let sql = args.sql;
|
|
132
|
+
const format = args.format ?? 'table';
|
|
133
|
+
|
|
134
|
+
// Add LIMIT if specified and not already present
|
|
135
|
+
if (args.limit && !/\bLIMIT\b/i.test(sql)) {
|
|
136
|
+
sql = `${sql.replace(/;?\s*$/, '')} LIMIT ${parseInt(args.limit, 10)}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const rows = db.prepare(sql).all() as Record<string, unknown>[];
|
|
140
|
+
|
|
141
|
+
if (rows.length === 0) {
|
|
142
|
+
console.log('No results.');
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const columns = Object.keys(rows[0]);
|
|
147
|
+
|
|
148
|
+
if (format === 'json') {
|
|
149
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
150
|
+
} else if (format === 'csv') {
|
|
151
|
+
console.log(columns.join(','));
|
|
152
|
+
for (const row of rows) {
|
|
153
|
+
const values = columns.map((c) => {
|
|
154
|
+
const v = row[c];
|
|
155
|
+
if (v === null) return '';
|
|
156
|
+
const s = String(v);
|
|
157
|
+
return s.includes(',') ||
|
|
158
|
+
s.includes('"') ||
|
|
159
|
+
s.includes('\n')
|
|
160
|
+
? `"${s.replace(/"/g, '""')}"`
|
|
161
|
+
: s;
|
|
162
|
+
});
|
|
163
|
+
console.log(values.join(','));
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
// table format
|
|
167
|
+
const widths = columns.map((c) =>
|
|
168
|
+
Math.max(
|
|
169
|
+
c.length,
|
|
170
|
+
...rows.map(
|
|
171
|
+
(r) => String(r[c] ?? '').slice(0, 50).length,
|
|
172
|
+
),
|
|
173
|
+
),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const header = columns
|
|
177
|
+
.map((c, i) => c.padEnd(widths[i]))
|
|
178
|
+
.join(' | ');
|
|
179
|
+
const sep = widths.map((w) => '-'.repeat(w)).join('-+-');
|
|
180
|
+
|
|
181
|
+
console.log(header);
|
|
182
|
+
console.log(sep);
|
|
183
|
+
for (const row of rows) {
|
|
184
|
+
const line = columns
|
|
185
|
+
.map((c, i) =>
|
|
186
|
+
String(row[c] ?? '')
|
|
187
|
+
.slice(0, 50)
|
|
188
|
+
.padEnd(widths[i]),
|
|
189
|
+
)
|
|
190
|
+
.join(' | ');
|
|
191
|
+
console.log(line);
|
|
192
|
+
}
|
|
193
|
+
console.log(`\n${rows.length} row(s)`);
|
|
194
|
+
}
|
|
195
|
+
} catch (err) {
|
|
196
|
+
console.error('SQL error:', (err as Error).message);
|
|
197
|
+
process.exit(1);
|
|
198
|
+
} finally {
|
|
199
|
+
db.close();
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
export const tools = defineCommand({
|
|
205
|
+
meta: {
|
|
206
|
+
name: 'tools',
|
|
207
|
+
description: 'Show most-used tools',
|
|
208
|
+
},
|
|
209
|
+
args: {
|
|
210
|
+
...sharedArgs,
|
|
211
|
+
top: {
|
|
212
|
+
type: 'string',
|
|
213
|
+
alias: 't',
|
|
214
|
+
description: 'Number of tools to show (default: 10)',
|
|
215
|
+
},
|
|
216
|
+
project: {
|
|
217
|
+
type: 'string',
|
|
218
|
+
alias: 'p',
|
|
219
|
+
description: 'Filter by project path',
|
|
220
|
+
},
|
|
221
|
+
format: {
|
|
222
|
+
type: 'string',
|
|
223
|
+
alias: 'f',
|
|
224
|
+
description: 'Output format: table, json (default: table)',
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
async run({ args }) {
|
|
228
|
+
const { Database } = await import('./db.ts');
|
|
229
|
+
|
|
230
|
+
const db_path = args.db ?? DEFAULT_DB_PATH;
|
|
231
|
+
const db = new Database(db_path);
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const results = db.get_tool_stats({
|
|
235
|
+
limit: args.top ? parseInt(args.top, 10) : undefined,
|
|
236
|
+
project: args.project,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
if (results.length === 0) {
|
|
240
|
+
console.log('No tool usage data found.');
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (args.format === 'json') {
|
|
245
|
+
console.log(JSON.stringify(results, null, 2));
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const maxNameLen = Math.max(
|
|
250
|
+
4,
|
|
251
|
+
...results.map((r) => r.tool_name.length),
|
|
252
|
+
);
|
|
253
|
+
const maxCountLen = Math.max(
|
|
254
|
+
5,
|
|
255
|
+
...results.map((r) => r.count.toString().length),
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
console.log(
|
|
259
|
+
`${'Tool'.padEnd(maxNameLen)} ${'Count'.padStart(maxCountLen)} %`,
|
|
260
|
+
);
|
|
261
|
+
console.log(
|
|
262
|
+
`${'-'.repeat(maxNameLen)} ${'-'.repeat(maxCountLen)} ------`,
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
for (const r of results) {
|
|
266
|
+
console.log(
|
|
267
|
+
`${r.tool_name.padEnd(maxNameLen)} ${r.count.toString().padStart(maxCountLen)} ${r.percentage.toFixed(1).padStart(5)}%`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
} finally {
|
|
271
|
+
db.close();
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
export const search = defineCommand({
|
|
277
|
+
meta: {
|
|
278
|
+
name: 'search',
|
|
279
|
+
description: 'Full-text search across messages',
|
|
280
|
+
},
|
|
281
|
+
args: {
|
|
282
|
+
...sharedArgs,
|
|
283
|
+
term: {
|
|
284
|
+
type: 'positional' as const,
|
|
285
|
+
description:
|
|
286
|
+
'Search term (supports FTS5 syntax: AND, OR, NOT, "phrase", prefix*)',
|
|
287
|
+
required: true,
|
|
288
|
+
},
|
|
289
|
+
limit: {
|
|
290
|
+
type: 'string',
|
|
291
|
+
alias: 'l',
|
|
292
|
+
description: 'Maximum results (default: 20)',
|
|
293
|
+
},
|
|
294
|
+
project: {
|
|
295
|
+
type: 'string',
|
|
296
|
+
alias: 'p',
|
|
297
|
+
description: 'Filter by project path',
|
|
298
|
+
},
|
|
299
|
+
rebuild: {
|
|
300
|
+
type: 'boolean',
|
|
301
|
+
description: 'Rebuild FTS index before searching',
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
async run({ args }) {
|
|
305
|
+
const { Database } = await import('./db.ts');
|
|
306
|
+
|
|
307
|
+
const db_path = args.db ?? DEFAULT_DB_PATH;
|
|
308
|
+
const db = new Database(db_path);
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
if (args.rebuild) {
|
|
312
|
+
console.log('Rebuilding FTS index...');
|
|
313
|
+
db.rebuild_fts();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const results = db.search(args.term, {
|
|
317
|
+
limit: args.limit ? parseInt(args.limit, 10) : undefined,
|
|
318
|
+
project: args.project,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
if (results.length === 0) {
|
|
322
|
+
console.log('No matches found.');
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
console.log(`Found ${results.length} matches:\n`);
|
|
327
|
+
|
|
328
|
+
for (const r of results) {
|
|
329
|
+
const date = new Date(r.timestamp)
|
|
330
|
+
.toISOString()
|
|
331
|
+
.split('T')[0];
|
|
332
|
+
const project = r.project_path.split('/').slice(-2).join('/');
|
|
333
|
+
console.log(`[${date}] ${project}`);
|
|
334
|
+
console.log(` ${r.snippet.replace(/\n/g, ' ')}`);
|
|
335
|
+
console.log(` session: ${r.session_id.slice(0, 8)}...\n`);
|
|
336
|
+
}
|
|
337
|
+
} finally {
|
|
338
|
+
db.close();
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
export const sessions = defineCommand({
|
|
344
|
+
meta: {
|
|
345
|
+
name: 'sessions',
|
|
346
|
+
description: 'List recent sessions',
|
|
347
|
+
},
|
|
348
|
+
args: {
|
|
349
|
+
...sharedArgs,
|
|
350
|
+
limit: {
|
|
351
|
+
type: 'string',
|
|
352
|
+
alias: 'l',
|
|
353
|
+
description: 'Maximum sessions to show (default: 10)',
|
|
354
|
+
},
|
|
355
|
+
project: {
|
|
356
|
+
type: 'string',
|
|
357
|
+
alias: 'p',
|
|
358
|
+
description: 'Filter by project path',
|
|
359
|
+
},
|
|
360
|
+
format: {
|
|
361
|
+
type: 'string',
|
|
362
|
+
alias: 'f',
|
|
363
|
+
description: 'Output format: table or json (default: table)',
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
async run({ args }) {
|
|
367
|
+
const { Database } = await import('./db.ts');
|
|
368
|
+
|
|
369
|
+
const db_path = args.db ?? DEFAULT_DB_PATH;
|
|
370
|
+
const db = new Database(db_path);
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
const results = db.get_sessions({
|
|
374
|
+
limit: args.limit ? parseInt(args.limit, 10) : undefined,
|
|
375
|
+
project: args.project,
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
if (results.length === 0) {
|
|
379
|
+
console.log('No sessions found.');
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (args.format === 'json') {
|
|
384
|
+
console.log(JSON.stringify(results, null, 2));
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Table format
|
|
389
|
+
console.log(
|
|
390
|
+
'Date | Project | Msgs | Tokens | Duration',
|
|
391
|
+
);
|
|
392
|
+
console.log(
|
|
393
|
+
'-----------|----------------------------------|------|-----------|----------',
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
for (const s of results) {
|
|
397
|
+
const date = new Date(s.first_timestamp)
|
|
398
|
+
.toISOString()
|
|
399
|
+
.split('T')[0];
|
|
400
|
+
const project = s.project_path
|
|
401
|
+
.split('/')
|
|
402
|
+
.slice(-2)
|
|
403
|
+
.join('/')
|
|
404
|
+
.padEnd(32)
|
|
405
|
+
.slice(0, 32);
|
|
406
|
+
const msgs = String(s.message_count).padStart(4);
|
|
407
|
+
const tokens = s.total_tokens.toLocaleString().padStart(9);
|
|
408
|
+
const duration =
|
|
409
|
+
s.duration_mins > 0 ? `${s.duration_mins}m` : '<1m';
|
|
410
|
+
console.log(
|
|
411
|
+
`${date} | ${project} | ${msgs} | ${tokens} | ${duration.padStart(8)}`,
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
} finally {
|
|
415
|
+
db.close();
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
|
|
95
420
|
export const main = defineCommand({
|
|
96
421
|
meta: {
|
|
97
422
|
name: 'ccrecall',
|
|
@@ -109,5 +434,9 @@ export const main = defineCommand({
|
|
|
109
434
|
subCommands: {
|
|
110
435
|
sync,
|
|
111
436
|
stats,
|
|
437
|
+
search,
|
|
438
|
+
sessions,
|
|
439
|
+
query,
|
|
440
|
+
tools,
|
|
112
441
|
},
|
|
113
442
|
});
|
package/src/db.ts
CHANGED
|
@@ -119,6 +119,27 @@ CREATE INDEX IF NOT EXISTS idx_teams_lead_session ON teams(lead_session_id);
|
|
|
119
119
|
CREATE INDEX IF NOT EXISTS idx_team_members_team ON team_members(team_id);
|
|
120
120
|
CREATE INDEX IF NOT EXISTS idx_team_tasks_team ON team_tasks(team_id);
|
|
121
121
|
CREATE INDEX IF NOT EXISTS idx_team_tasks_status ON team_tasks(status);
|
|
122
|
+
|
|
123
|
+
-- FTS5 full-text search index for messages
|
|
124
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
|
|
125
|
+
content_text,
|
|
126
|
+
content='messages',
|
|
127
|
+
content_rowid='rowid'
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
-- Triggers to keep FTS index in sync with messages table
|
|
131
|
+
CREATE TRIGGER IF NOT EXISTS messages_fts_insert AFTER INSERT ON messages BEGIN
|
|
132
|
+
INSERT INTO messages_fts(rowid, content_text) VALUES (new.rowid, new.content_text);
|
|
133
|
+
END;
|
|
134
|
+
|
|
135
|
+
CREATE TRIGGER IF NOT EXISTS messages_fts_delete AFTER DELETE ON messages BEGIN
|
|
136
|
+
INSERT INTO messages_fts(messages_fts, rowid, content_text) VALUES('delete', old.rowid, old.content_text);
|
|
137
|
+
END;
|
|
138
|
+
|
|
139
|
+
CREATE TRIGGER IF NOT EXISTS messages_fts_update AFTER UPDATE ON messages BEGIN
|
|
140
|
+
INSERT INTO messages_fts(messages_fts, rowid, content_text) VALUES('delete', old.rowid, old.content_text);
|
|
141
|
+
INSERT INTO messages_fts(rowid, content_text) VALUES (new.rowid, new.content_text);
|
|
142
|
+
END;
|
|
122
143
|
`;
|
|
123
144
|
|
|
124
145
|
export class Database {
|
|
@@ -444,6 +465,147 @@ export class Database {
|
|
|
444
465
|
this.db.run('DELETE FROM sync_state');
|
|
445
466
|
}
|
|
446
467
|
|
|
468
|
+
search(
|
|
469
|
+
term: string,
|
|
470
|
+
options: { limit?: number; project?: string } = {},
|
|
471
|
+
): Array<{
|
|
472
|
+
uuid: string;
|
|
473
|
+
session_id: string;
|
|
474
|
+
project_path: string;
|
|
475
|
+
content_text: string;
|
|
476
|
+
timestamp: number;
|
|
477
|
+
snippet: string;
|
|
478
|
+
}> {
|
|
479
|
+
const limit = options.limit ?? 20;
|
|
480
|
+
let query = `
|
|
481
|
+
SELECT
|
|
482
|
+
m.uuid,
|
|
483
|
+
m.session_id,
|
|
484
|
+
s.project_path,
|
|
485
|
+
m.content_text,
|
|
486
|
+
m.timestamp,
|
|
487
|
+
snippet(messages_fts, 0, '>>>', '<<<', '...', 32) as snippet
|
|
488
|
+
FROM messages_fts
|
|
489
|
+
JOIN messages m ON m.rowid = messages_fts.rowid
|
|
490
|
+
JOIN sessions s ON s.id = m.session_id
|
|
491
|
+
WHERE messages_fts MATCH ?
|
|
492
|
+
`;
|
|
493
|
+
const params: (string | number)[] = [term];
|
|
494
|
+
|
|
495
|
+
if (options.project) {
|
|
496
|
+
query += ` AND s.project_path LIKE ?`;
|
|
497
|
+
params.push(`%${options.project}%`);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
query += ` ORDER BY rank LIMIT ?`;
|
|
501
|
+
params.push(limit);
|
|
502
|
+
|
|
503
|
+
return this.db.prepare(query).all(...params) as Array<{
|
|
504
|
+
uuid: string;
|
|
505
|
+
session_id: string;
|
|
506
|
+
project_path: string;
|
|
507
|
+
content_text: string;
|
|
508
|
+
timestamp: number;
|
|
509
|
+
snippet: string;
|
|
510
|
+
}>;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
rebuild_fts() {
|
|
514
|
+
this.db.run(
|
|
515
|
+
`INSERT INTO messages_fts(messages_fts) VALUES('rebuild')`,
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
get_sessions(
|
|
520
|
+
options: { limit?: number; project?: string } = {},
|
|
521
|
+
): Array<{
|
|
522
|
+
id: string;
|
|
523
|
+
project_path: string;
|
|
524
|
+
first_timestamp: number;
|
|
525
|
+
last_timestamp: number;
|
|
526
|
+
message_count: number;
|
|
527
|
+
total_tokens: number;
|
|
528
|
+
duration_mins: number;
|
|
529
|
+
}> {
|
|
530
|
+
const limit = options.limit ?? 10;
|
|
531
|
+
let query = `
|
|
532
|
+
SELECT
|
|
533
|
+
s.id,
|
|
534
|
+
s.project_path,
|
|
535
|
+
s.first_timestamp,
|
|
536
|
+
s.last_timestamp,
|
|
537
|
+
COUNT(m.uuid) as message_count,
|
|
538
|
+
COALESCE(SUM(m.input_tokens + m.output_tokens), 0) as total_tokens,
|
|
539
|
+
CAST((s.last_timestamp - s.first_timestamp) / 60000.0 AS INTEGER) as duration_mins
|
|
540
|
+
FROM sessions s
|
|
541
|
+
LEFT JOIN messages m ON m.session_id = s.id
|
|
542
|
+
`;
|
|
543
|
+
const params: (string | number)[] = [];
|
|
544
|
+
|
|
545
|
+
if (options.project) {
|
|
546
|
+
query += ` WHERE s.project_path LIKE ?`;
|
|
547
|
+
params.push(`%${options.project}%`);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
query += ` GROUP BY s.id ORDER BY s.last_timestamp DESC LIMIT ?`;
|
|
551
|
+
params.push(limit);
|
|
552
|
+
|
|
553
|
+
return this.db.prepare(query).all(...params) as Array<{
|
|
554
|
+
id: string;
|
|
555
|
+
project_path: string;
|
|
556
|
+
first_timestamp: number;
|
|
557
|
+
last_timestamp: number;
|
|
558
|
+
message_count: number;
|
|
559
|
+
total_tokens: number;
|
|
560
|
+
duration_mins: number;
|
|
561
|
+
}>;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
get_tool_stats(
|
|
565
|
+
options: { limit?: number; project?: string } = {},
|
|
566
|
+
): Array<{
|
|
567
|
+
tool_name: string;
|
|
568
|
+
count: number;
|
|
569
|
+
percentage: number;
|
|
570
|
+
}> {
|
|
571
|
+
const limit = options.limit ?? 10;
|
|
572
|
+
let query = `
|
|
573
|
+
SELECT
|
|
574
|
+
tc.tool_name,
|
|
575
|
+
COUNT(*) as count
|
|
576
|
+
FROM tool_calls tc
|
|
577
|
+
`;
|
|
578
|
+
const params: (string | number)[] = [];
|
|
579
|
+
|
|
580
|
+
if (options.project) {
|
|
581
|
+
query += `
|
|
582
|
+
JOIN sessions s ON s.id = tc.session_id
|
|
583
|
+
WHERE s.project_path LIKE ?
|
|
584
|
+
`;
|
|
585
|
+
params.push(`%${options.project}%`);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
query += `
|
|
589
|
+
GROUP BY tc.tool_name
|
|
590
|
+
ORDER BY count DESC
|
|
591
|
+
LIMIT ?
|
|
592
|
+
`;
|
|
593
|
+
params.push(limit);
|
|
594
|
+
|
|
595
|
+
const rows = this.db.prepare(query).all(...params) as Array<{
|
|
596
|
+
tool_name: string;
|
|
597
|
+
count: number;
|
|
598
|
+
}>;
|
|
599
|
+
|
|
600
|
+
const total = rows.reduce((sum, r) => sum + r.count, 0);
|
|
601
|
+
|
|
602
|
+
return rows.map((r) => ({
|
|
603
|
+
tool_name: r.tool_name,
|
|
604
|
+
count: r.count,
|
|
605
|
+
percentage: total > 0 ? (r.count / total) * 100 : 0,
|
|
606
|
+
}));
|
|
607
|
+
}
|
|
608
|
+
|
|
447
609
|
close() {
|
|
448
610
|
this.db.close();
|
|
449
611
|
}
|