k0ntext 3.6.0 → 3.8.0
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 +281 -382
- package/dist/agent-system/timestamp-tracker.d.ts +159 -0
- package/dist/agent-system/timestamp-tracker.d.ts.map +1 -0
- package/dist/agent-system/timestamp-tracker.js +405 -0
- package/dist/agent-system/timestamp-tracker.js.map +1 -0
- package/dist/agent-system/todolist-manager.d.ts +244 -0
- package/dist/agent-system/todolist-manager.d.ts.map +1 -0
- package/dist/agent-system/todolist-manager.js +580 -0
- package/dist/agent-system/todolist-manager.js.map +1 -0
- package/dist/analyzer/intelligent-analyzer.d.ts +7 -0
- package/dist/analyzer/intelligent-analyzer.d.ts.map +1 -1
- package/dist/analyzer/intelligent-analyzer.js +46 -1
- package/dist/analyzer/intelligent-analyzer.js.map +1 -1
- package/dist/cli/commands/embeddings-refresh.d.ts.map +1 -1
- package/dist/cli/commands/embeddings-refresh.js +4 -1
- package/dist/cli/commands/embeddings-refresh.js.map +1 -1
- package/dist/cli/commands/migrate.d.ts.map +1 -1
- package/dist/cli/commands/migrate.js +8 -0
- package/dist/cli/commands/migrate.js.map +1 -1
- package/dist/cli/commands/snapshot.d.ts +28 -0
- package/dist/cli/commands/snapshot.d.ts.map +1 -0
- package/dist/cli/commands/snapshot.js +408 -0
- package/dist/cli/commands/snapshot.js.map +1 -0
- package/dist/cli/repl/init/wizard.d.ts.map +1 -1
- package/dist/cli/repl/init/wizard.js +12 -4
- package/dist/cli/repl/init/wizard.js.map +1 -1
- package/dist/cli/version/comparator.d.ts +1 -0
- package/dist/cli/version/comparator.d.ts.map +1 -1
- package/dist/cli/version/comparator.js +1 -0
- package/dist/cli/version/comparator.js.map +1 -1
- package/dist/db/client.d.ts +5 -0
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +7 -0
- package/dist/db/client.js.map +1 -1
- package/dist/db/schema.d.ts +1 -1
- package/dist/db/schema.js +1 -1
- package/dist/embeddings/openrouter.d.ts.map +1 -1
- package/dist/embeddings/openrouter.js +8 -3
- package/dist/embeddings/openrouter.js.map +1 -1
- package/dist/services/snapshot-manager.d.ts +251 -0
- package/dist/services/snapshot-manager.d.ts.map +1 -0
- package/dist/services/snapshot-manager.js +541 -0
- package/dist/services/snapshot-manager.js.map +1 -0
- package/dist/utils/chunking.d.ts +38 -0
- package/dist/utils/chunking.d.ts.map +1 -0
- package/dist/utils/chunking.js +133 -0
- package/dist/utils/chunking.js.map +1 -0
- package/dist/utils/encoding.d.ts +24 -0
- package/dist/utils/encoding.d.ts.map +1 -0
- package/dist/utils/encoding.js +32 -0
- package/dist/utils/encoding.js.map +1 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +8 -0
- package/dist/utils/index.js.map +1 -0
- package/docs/QUICKSTART.md +1 -1
- package/docs/TROUBLESHOOTING.md +51 -76
- package/docs/plans/2026-02-09-v3.7.0-database-fixes-and-improvements.md +900 -0
- package/docs/plans/2026-02-11-context-engineering-enhancement.md +1402 -0
- package/package.json +8 -2
- package/src/agent-system/timestamp-tracker.ts +520 -0
- package/src/agent-system/todolist-manager.ts +753 -0
- package/src/analyzer/intelligent-analyzer.ts +58 -1
- package/src/cli/commands/embeddings-refresh.ts +4 -1
- package/src/cli/commands/migrate.ts +8 -0
- package/src/cli/commands/snapshot.ts +471 -0
- package/src/cli/repl/init/wizard.ts +12 -4
- package/src/cli/version/comparator.ts +1 -0
- package/src/db/client.ts +8 -0
- package/src/db/migrations/0016_add_context_system_tables.sql +38 -0
- package/src/db/migrations/files/0015_add_sync_state_version_tracking.sql +18 -0
- package/src/db/schema.ts +1 -1
- package/src/embeddings/openrouter.ts +10 -4
- package/src/services/snapshot-manager.ts +719 -0
- package/src/utils/chunking.ts +152 -0
- package/src/utils/encoding.ts +33 -0
- package/src/utils/index.ts +8 -0
|
@@ -10,6 +10,7 @@ import path from 'path';
|
|
|
10
10
|
import { glob } from 'glob';
|
|
11
11
|
import { OpenRouterClient, createOpenRouterClient, hasOpenRouterKey } from '../embeddings/openrouter.js';
|
|
12
12
|
import { AI_TOOLS, AI_TOOL_FOLDERS, type AITool } from '../db/schema.js';
|
|
13
|
+
import { estimateTokens, chunkForEmbedding } from '../utils/chunking.js';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Discovery result for a file
|
|
@@ -588,12 +589,68 @@ Return ONLY valid JSON, no markdown formatting.
|
|
|
588
589
|
|
|
589
590
|
/**
|
|
590
591
|
* Generate embedding for a single text string (e.g., search query)
|
|
592
|
+
*
|
|
593
|
+
* Automatically chunks large texts (>8K tokens) to fit within API limits.
|
|
594
|
+
* For chunked texts, returns the average of all chunk embeddings.
|
|
591
595
|
*/
|
|
592
596
|
async embedText(text: string): Promise<number[]> {
|
|
593
597
|
if (!this.client) {
|
|
594
598
|
throw new Error('OpenRouter client not available for embeddings');
|
|
595
599
|
}
|
|
596
|
-
|
|
600
|
+
|
|
601
|
+
// Check if text needs chunking (8K token limit for OpenRouter)
|
|
602
|
+
const tokenEstimate = estimateTokens(text);
|
|
603
|
+
|
|
604
|
+
if (tokenEstimate <= 8000) {
|
|
605
|
+
// Text is small enough, embed directly
|
|
606
|
+
return this.client.embed(text);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Text is too large, chunk it and embed each chunk
|
|
610
|
+
const chunks = chunkForEmbedding(text);
|
|
611
|
+
|
|
612
|
+
if (chunks.length === 1) {
|
|
613
|
+
return this.client.embed(chunks[0]);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Embed all chunks
|
|
617
|
+
const embeddings: number[][] = [];
|
|
618
|
+
for (const chunk of chunks) {
|
|
619
|
+
const embedding = await this.client.embed(chunk);
|
|
620
|
+
embeddings.push(embedding);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Return the average embedding across all chunks
|
|
624
|
+
return this.averageEmbeddings(embeddings);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Average multiple embeddings into a single vector
|
|
629
|
+
*/
|
|
630
|
+
private averageEmbeddings(embeddings: number[][]): number[] {
|
|
631
|
+
if (embeddings.length === 0) {
|
|
632
|
+
throw new Error('Cannot average empty embeddings array');
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (embeddings.length === 1) {
|
|
636
|
+
return embeddings[0];
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const dimension = embeddings[0].length;
|
|
640
|
+
const averaged = new Array(dimension).fill(0);
|
|
641
|
+
|
|
642
|
+
for (const embedding of embeddings) {
|
|
643
|
+
for (let i = 0; i < dimension; i++) {
|
|
644
|
+
averaged[i] += embedding[i];
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Divide by count to get average
|
|
649
|
+
for (let i = 0; i < dimension; i++) {
|
|
650
|
+
averaged[i] /= embeddings.length;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return averaged;
|
|
597
654
|
}
|
|
598
655
|
|
|
599
656
|
/**
|
|
@@ -11,6 +11,7 @@ import { confirm } from '@inquirer/prompts';
|
|
|
11
11
|
import { createIntelligentAnalyzer } from '../../analyzer/intelligent-analyzer.js';
|
|
12
12
|
import { hasOpenRouterKey } from '../../embeddings/openrouter.js';
|
|
13
13
|
import { DatabaseClient } from '../../db/client.js';
|
|
14
|
+
import { estimateTokens } from '../../utils/chunking.js';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Embeddings refresh command
|
|
@@ -105,7 +106,9 @@ export const embeddingsRefreshCommand = new Command('embeddings:refresh')
|
|
|
105
106
|
for (const item of batch) {
|
|
106
107
|
if (options.verbose) {
|
|
107
108
|
spinner.stop();
|
|
108
|
-
|
|
109
|
+
const tokenEstimate = estimateTokens(item.content);
|
|
110
|
+
const chunkInfo = tokenEstimate > 8000 ? chalk.yellow(` (${Math.ceil(tokenEstimate / 8000)} chunks)`) : '';
|
|
111
|
+
console.log(chalk.dim(` Embedding: ${item.name}${chunkInfo}`));
|
|
109
112
|
spinner.start();
|
|
110
113
|
}
|
|
111
114
|
|
|
@@ -18,6 +18,14 @@ import { MigrationRunner } from '../../db/migrations/index.js';
|
|
|
18
18
|
*/
|
|
19
19
|
export const migrateCommand = new Command('migrate')
|
|
20
20
|
.description('Manage database schema migrations')
|
|
21
|
+
.action(() => {
|
|
22
|
+
// Default action: show help if no subcommand specified
|
|
23
|
+
console.log('\nAvailable subcommands:\n');
|
|
24
|
+
console.log(' k0ntext migrate status Show migration status');
|
|
25
|
+
console.log(' k0ntext migrate up Apply pending migrations');
|
|
26
|
+
console.log(' k0ntext migrate rollback Rollback to a previous backup\n');
|
|
27
|
+
console.log('Run "k0ntext migrate <subcommand> --help" for more information.\n');
|
|
28
|
+
})
|
|
21
29
|
|
|
22
30
|
// Status subcommand
|
|
23
31
|
.command('status')
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot Commands
|
|
3
|
+
*
|
|
4
|
+
* CLI commands for managing database snapshots.
|
|
5
|
+
* Supports create, restore, list, and diff operations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import ora from 'ora';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import fs from 'fs/promises';
|
|
13
|
+
import { confirm, input, select } from '@inquirer/prompts';
|
|
14
|
+
import type { DatabaseClient } from '../../db/client.js';
|
|
15
|
+
import { SnapshotManager } from '../../services/snapshot-manager.js';
|
|
16
|
+
import type { SnapshotMetadata, SnapshotListEntry, SnapshotDiffResult } from '../../services/snapshot-manager.js';
|
|
17
|
+
import { compareVersions, needsUpdate, getUpdateType } from '../version/comparator.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Format bytes for display
|
|
21
|
+
*/
|
|
22
|
+
function formatBytes(bytes: number): string {
|
|
23
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
24
|
+
let size = bytes;
|
|
25
|
+
let unitIndex = 0;
|
|
26
|
+
|
|
27
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
28
|
+
size /= 1024;
|
|
29
|
+
unitIndex++;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Format date for display
|
|
37
|
+
*/
|
|
38
|
+
function formatDate(isoString: string): string {
|
|
39
|
+
const date = new Date(isoString);
|
|
40
|
+
const now = new Date();
|
|
41
|
+
const diffMs = now.getTime() - date.getTime();
|
|
42
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
43
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
44
|
+
|
|
45
|
+
if (diffHours > 24) {
|
|
46
|
+
return `${date.toLocaleDateString()} (${Math.floor(diffHours / 24)}d ago)`;
|
|
47
|
+
} else if (diffHours > 0) {
|
|
48
|
+
return `${date.toLocaleDateString()} (${diffHours}h ago)`;
|
|
49
|
+
} else if (diffMins > 0) {
|
|
50
|
+
return `${date.toLocaleDateString()} (${diffMins}m ago)`;
|
|
51
|
+
} else {
|
|
52
|
+
return date.toLocaleTimeString();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Format snapshot metadata for display
|
|
58
|
+
*/
|
|
59
|
+
function formatSnapshot(snapshot: SnapshotMetadata): string {
|
|
60
|
+
const tags = snapshot.tags ? snapshot.tags.map((t: string) => chalk.cyan(`#${t}`)).join(' ') : '';
|
|
61
|
+
const tagStr = tags ? ` ${tags}` : '';
|
|
62
|
+
const auto = snapshot.automatic ? chalk.dim('[auto]') : '';
|
|
63
|
+
|
|
64
|
+
return `${chalk.bold(snapshot.name)} ${auto}${tagStr}
|
|
65
|
+
${chalk.dim(`ID: ${snapshot.id}`)}
|
|
66
|
+
${chalk.dim(`Created: ${formatDate(snapshot.createdAt)}`)}
|
|
67
|
+
${chalk.dim(`Size: ${formatBytes(snapshot.size)}`)}
|
|
68
|
+
${chalk.dim(`Items: ${snapshot.itemCount}`)}
|
|
69
|
+
${snapshot.gitCommit ? chalk.dim(`Git: ${snapshot.gitCommit.substring(0, 8)}`) : ''}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Snapshot create command
|
|
74
|
+
*/
|
|
75
|
+
export const snapshotCreateCommand = new Command('snapshot')
|
|
76
|
+
.alias('snap')
|
|
77
|
+
.description('Create a database snapshot')
|
|
78
|
+
.option('-n, --name <name>', 'Snapshot name')
|
|
79
|
+
.option('-d, --description <text>', 'Snapshot description')
|
|
80
|
+
.option('-t, --tags <tags>', 'Comma-separated tags')
|
|
81
|
+
.option('--no-compress', 'Do not compress snapshot')
|
|
82
|
+
.option('--dry-run', 'Show what would be saved without creating')
|
|
83
|
+
.option('--no-git', 'Do not include git commit in metadata')
|
|
84
|
+
.action(async (options) => {
|
|
85
|
+
const spinner = ora();
|
|
86
|
+
const projectRoot = process.cwd();
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
// Load database
|
|
90
|
+
const { DatabaseClient } = await import('../../db/client.js');
|
|
91
|
+
const versionModule = await import('../../cli/version/comparator.js');
|
|
92
|
+
const K0NTEXT_VERSION = versionModule.version;
|
|
93
|
+
const db = new DatabaseClient(projectRoot);
|
|
94
|
+
const manager = new SnapshotManager(db, projectRoot, K0NTEXT_VERSION);
|
|
95
|
+
|
|
96
|
+
// Get snapshot name if not provided
|
|
97
|
+
let name = options.name;
|
|
98
|
+
if (!name) {
|
|
99
|
+
const now = new Date();
|
|
100
|
+
name = `Snapshot ${now.toLocaleDateString()} ${now.toLocaleTimeString()}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Parse tags
|
|
104
|
+
const tags = options.tags ? options.tags.split(',').map((t: string) => t.trim()) : undefined;
|
|
105
|
+
|
|
106
|
+
spinner.start('Creating snapshot...');
|
|
107
|
+
|
|
108
|
+
// Create snapshot
|
|
109
|
+
const metadata = await manager.createSnapshot({
|
|
110
|
+
name,
|
|
111
|
+
description: options.description,
|
|
112
|
+
tags,
|
|
113
|
+
dryRun: !!options.dryRun,
|
|
114
|
+
includeGitCommit: options.git !== false,
|
|
115
|
+
compress: options.noCompress !== true
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
spinner.stop();
|
|
119
|
+
|
|
120
|
+
if (options.dryRun) {
|
|
121
|
+
console.log(chalk.bold('\nDry run - snapshot would be created:'));
|
|
122
|
+
console.log(formatSnapshot(metadata));
|
|
123
|
+
console.log(chalk.dim('\nRun without --dry-run to create the snapshot.'));
|
|
124
|
+
} else {
|
|
125
|
+
console.log(chalk.bold('\nSnapshot created successfully!'));
|
|
126
|
+
console.log(formatSnapshot(metadata));
|
|
127
|
+
console.log(chalk.dim(`\nRun 'k0ntext snapshot restore ${metadata.id}' to restore.`));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
db.close();
|
|
131
|
+
|
|
132
|
+
} catch (error) {
|
|
133
|
+
spinner.fail('Failed to create snapshot');
|
|
134
|
+
console.error(chalk.red(`\nError: ${error instanceof Error ? error.message : error}`));
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Snapshot restore command
|
|
141
|
+
*/
|
|
142
|
+
export const snapshotRestoreCommand = new Command('snapshot')
|
|
143
|
+
.alias('snap-restore')
|
|
144
|
+
.description('Restore a database snapshot')
|
|
145
|
+
.argument('[snapshot]', 'Snapshot ID or path to restore')
|
|
146
|
+
.option('-f, --force', 'Restore without confirmation')
|
|
147
|
+
.option('--no-backup', 'Do not backup current database')
|
|
148
|
+
.option('--no-verify', 'Skip database verification after restore')
|
|
149
|
+
.action(async (snapshotId, options) => {
|
|
150
|
+
const spinner = ora();
|
|
151
|
+
const projectRoot = process.cwd();
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
// Load database
|
|
155
|
+
const { DatabaseClient } = await import('../../db/client.js');
|
|
156
|
+
const { version: K0NTEXT_VERSION } = await import('../../cli/version/comparator.js');
|
|
157
|
+
const db = new DatabaseClient(projectRoot);
|
|
158
|
+
const manager = new SnapshotManager(db, projectRoot, K0NTEXT_VERSION);
|
|
159
|
+
|
|
160
|
+
// List snapshots if no ID provided
|
|
161
|
+
if (!snapshotId) {
|
|
162
|
+
spinner.start('Loading snapshots...');
|
|
163
|
+
const snapshots = await manager.listSnapshots();
|
|
164
|
+
spinner.stop();
|
|
165
|
+
|
|
166
|
+
if (snapshots.length === 0) {
|
|
167
|
+
console.log(chalk.yellow('\nNo snapshots found.'));
|
|
168
|
+
console.log(chalk.dim('Run "k0ntext snapshot create" to create one.\n'));
|
|
169
|
+
db.close();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
console.log(chalk.bold('\nAvailable Snapshots:'));
|
|
174
|
+
console.log(chalk.dim('─'.repeat(60)));
|
|
175
|
+
|
|
176
|
+
for (const snap of snapshots.slice(0, 20)) {
|
|
177
|
+
console.log(formatSnapshot({
|
|
178
|
+
id: snap.id,
|
|
179
|
+
name: snap.name,
|
|
180
|
+
createdAt: snap.createdAt,
|
|
181
|
+
size: snap.size,
|
|
182
|
+
itemCount: snap.itemCount,
|
|
183
|
+
k0ntextVersion: K0NTEXT_VERSION,
|
|
184
|
+
automatic: false,
|
|
185
|
+
tags: snap.tags
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (snapshots.length > 20) {
|
|
190
|
+
console.log(chalk.dim(`\n... and ${snapshots.length - 20} more`));
|
|
191
|
+
console.log(chalk.dim(`Run "k0ntext snapshot restore <id>" to restore a snapshot.`));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
db.close();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Resolve snapshot path
|
|
199
|
+
let snapshotPath = snapshotId;
|
|
200
|
+
if (!path.isAbsolute(snapshotId)) {
|
|
201
|
+
// Try to find snapshot by ID
|
|
202
|
+
const snapshots = await manager.listSnapshots();
|
|
203
|
+
const found = snapshots.find(s => s.id === snapshotId);
|
|
204
|
+
if (found) {
|
|
205
|
+
snapshotPath = found.path;
|
|
206
|
+
} else {
|
|
207
|
+
spinner.fail(chalk.red(`Snapshot not found: ${snapshotId}`));
|
|
208
|
+
console.log(chalk.dim('\nRun "k0ntext snapshot list" to see available snapshots.\n'));
|
|
209
|
+
db.close();
|
|
210
|
+
process.exit(1);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Confirm restore
|
|
216
|
+
if (!options.force) {
|
|
217
|
+
const confirmed = await confirm({
|
|
218
|
+
message: `Restore snapshot ${snapshotId}? This will replace your current database.`,
|
|
219
|
+
default: false
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
if (!confirmed) {
|
|
223
|
+
console.log(chalk.dim('\nRestore cancelled.\n'));
|
|
224
|
+
db.close();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
spinner.start('Restoring snapshot...');
|
|
230
|
+
|
|
231
|
+
await manager.restoreSnapshot({
|
|
232
|
+
snapshotPath,
|
|
233
|
+
force: true,
|
|
234
|
+
backupBeforeRestore: options.backup !== false,
|
|
235
|
+
verify: options.verify !== false
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
spinner.succeed(chalk.green('Snapshot restored successfully!'));
|
|
239
|
+
console.log(chalk.dim('\nDatabase has been restored to the selected snapshot state.'));
|
|
240
|
+
|
|
241
|
+
db.close();
|
|
242
|
+
|
|
243
|
+
} catch (error) {
|
|
244
|
+
spinner.fail('Failed to restore snapshot');
|
|
245
|
+
console.error(chalk.red(`\nError: ${error instanceof Error ? error.message : error}`));
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Snapshot list command
|
|
252
|
+
*/
|
|
253
|
+
export const snapshotListCommand = new Command('snapshot')
|
|
254
|
+
.alias('snapshots')
|
|
255
|
+
.description('List all database snapshots')
|
|
256
|
+
.option('-v, --verbose', 'Show detailed information')
|
|
257
|
+
.option('--json', 'Output in JSON format')
|
|
258
|
+
.action(async (options) => {
|
|
259
|
+
const spinner = ora();
|
|
260
|
+
const projectRoot = process.cwd();
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
// Load database
|
|
264
|
+
const { DatabaseClient } = await import('../../db/client.js');
|
|
265
|
+
const db = new DatabaseClient(projectRoot);
|
|
266
|
+
const manager = new SnapshotManager(db, projectRoot, 'unknown');
|
|
267
|
+
|
|
268
|
+
spinner.start('Loading snapshots...');
|
|
269
|
+
|
|
270
|
+
const snapshots = await manager.listSnapshots();
|
|
271
|
+
const usage = await manager.getStorageUsage();
|
|
272
|
+
|
|
273
|
+
spinner.stop();
|
|
274
|
+
|
|
275
|
+
if (snapshots.length === 0) {
|
|
276
|
+
console.log(chalk.yellow('\nNo snapshots found.'));
|
|
277
|
+
console.log(chalk.dim('Run "k0ntext snapshot create" to create one.\n'));
|
|
278
|
+
db.close();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// JSON output
|
|
283
|
+
if (options.json) {
|
|
284
|
+
console.log(JSON.stringify({
|
|
285
|
+
total: snapshots.length,
|
|
286
|
+
usage: {
|
|
287
|
+
totalSize: formatBytes(usage.totalSize),
|
|
288
|
+
oldest: usage.oldestSnapshot,
|
|
289
|
+
newest: usage.newestSnapshot
|
|
290
|
+
},
|
|
291
|
+
snapshots: snapshots.map(s => ({
|
|
292
|
+
id: s.id,
|
|
293
|
+
name: s.name,
|
|
294
|
+
createdAt: s.createdAt,
|
|
295
|
+
size: s.size,
|
|
296
|
+
itemCount: s.itemCount,
|
|
297
|
+
tags: s.tags
|
|
298
|
+
}))
|
|
299
|
+
}, null, 2));
|
|
300
|
+
db.close();
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Regular output
|
|
305
|
+
console.log(chalk.bold('\nSnapshots'));
|
|
306
|
+
console.log(chalk.dim('─'.repeat(60)));
|
|
307
|
+
|
|
308
|
+
console.log(`Total: ${chalk.cyan(snapshots.length.toString())} snapshots`);
|
|
309
|
+
console.log(`Storage: ${chalk.cyan(formatBytes(usage.totalSize))}`);
|
|
310
|
+
console.log(`Oldest: ${chalk.dim(usage.oldestSnapshot ? formatDate(usage.oldestSnapshot) : 'N/A')}`);
|
|
311
|
+
console.log(`Newest: ${chalk.dim(usage.newestSnapshot ? formatDate(usage.newestSnapshot) : 'N/A')}`);
|
|
312
|
+
|
|
313
|
+
console.log(chalk.dim('\n' + '─'.repeat(60) + '\n'));
|
|
314
|
+
|
|
315
|
+
for (const snap of snapshots) {
|
|
316
|
+
const tags = snap.tags ? snap.tags.map(t => chalk.cyan(`#${t}`)).join(' ') : '';
|
|
317
|
+
console.log(`${chalk.bold(snap.name)} ${chalk.dim(`(${snap.id})`)}`);
|
|
318
|
+
console.log(` Created: ${chalk.dim(formatDate(snap.createdAt))}`);
|
|
319
|
+
console.log(` Size: ${chalk.cyan(formatBytes(snap.size))}`);
|
|
320
|
+
console.log(` Items: ${chalk.cyan(snap.itemCount.toString())}`);
|
|
321
|
+
if (tags.length > 0) {
|
|
322
|
+
console.log(` Tags: ${tags}`);
|
|
323
|
+
}
|
|
324
|
+
if (options.verbose && snap.description) {
|
|
325
|
+
console.log(` Description: ${chalk.dim(snap.description)}`);
|
|
326
|
+
}
|
|
327
|
+
console.log('');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
db.close();
|
|
331
|
+
|
|
332
|
+
} catch (error) {
|
|
333
|
+
spinner.fail('Failed to list snapshots');
|
|
334
|
+
console.error(chalk.red(`\nError: ${error instanceof Error ? error.message : error}`));
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Snapshot diff command
|
|
341
|
+
*/
|
|
342
|
+
export const snapshotDiffCommand = new Command('snapshot')
|
|
343
|
+
.alias('snap-diff')
|
|
344
|
+
.description('Compare two snapshots')
|
|
345
|
+
.argument('<snapshot-a>', 'First snapshot ID or path')
|
|
346
|
+
.argument('<snapshot-b>', 'Second snapshot ID or path')
|
|
347
|
+
.option('-v, --verbose', 'Show detailed differences')
|
|
348
|
+
.option('--json', 'Output in JSON format')
|
|
349
|
+
.action(async (snapshotA, snapshotB, options) => {
|
|
350
|
+
const spinner = ora();
|
|
351
|
+
const projectRoot = process.cwd();
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
// Load database
|
|
355
|
+
const { DatabaseClient } = await import('../../db/client.js');
|
|
356
|
+
const db = new DatabaseClient(projectRoot);
|
|
357
|
+
const manager = new SnapshotManager(db, projectRoot, 'unknown');
|
|
358
|
+
|
|
359
|
+
spinner.start('Comparing snapshots...');
|
|
360
|
+
|
|
361
|
+
const diff = await manager.diffSnapshots(snapshotA, snapshotB);
|
|
362
|
+
|
|
363
|
+
spinner.stop();
|
|
364
|
+
|
|
365
|
+
// JSON output
|
|
366
|
+
if (options.json) {
|
|
367
|
+
console.log(JSON.stringify(diff, null, 2));
|
|
368
|
+
db.close();
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Regular output
|
|
373
|
+
console.log(chalk.bold('\nSnapshot Comparison'));
|
|
374
|
+
console.log(chalk.dim('─'.repeat(60)));
|
|
375
|
+
console.log(`Snapshot A: ${chalk.cyan(diff.snapshotA)}`);
|
|
376
|
+
console.log(`Snapshot B: ${chalk.cyan(diff.snapshotB)}`);
|
|
377
|
+
|
|
378
|
+
console.log(chalk.bold('\nSummary:'));
|
|
379
|
+
console.log(` Only in A: ${chalk.yellow(diff.onlyInA.length.toString())}`);
|
|
380
|
+
console.log(` Only in B: ${chalk.yellow(diff.onlyInB.length.toString())}`);
|
|
381
|
+
console.log(` Changed: ${chalk.yellow(diff.differences.filter(d => d.changeType !== 'same').length.toString())}`);
|
|
382
|
+
|
|
383
|
+
if (options.verbose || diff.differences.filter(d => d.changeType !== 'same').length <= 20) {
|
|
384
|
+
console.log(chalk.bold('\nChanges:'));
|
|
385
|
+
|
|
386
|
+
const changes = diff.differences.filter(d => d.changeType !== 'same');
|
|
387
|
+
|
|
388
|
+
for (const change of changes.slice(0, 50)) {
|
|
389
|
+
const icon = change.changeType === 'added' ? chalk.green('+') :
|
|
390
|
+
change.changeType === 'removed' ? chalk.red('-') :
|
|
391
|
+
change.changeType === 'modified' ? chalk.yellow('~') :
|
|
392
|
+
chalk.dim('=');
|
|
393
|
+
|
|
394
|
+
const typeLabel = change.changeType === 'added' ? chalk.green('added') :
|
|
395
|
+
change.changeType === 'removed' ? chalk.red('removed') :
|
|
396
|
+
change.changeType === 'modified' ? chalk.yellow('modified') :
|
|
397
|
+
'';
|
|
398
|
+
|
|
399
|
+
console.log(` ${icon} ${chalk.cyan(change.id)} ${chalk.dim(`[${change.type}]`)}: ${change.name}`);
|
|
400
|
+
if (typeLabel) {
|
|
401
|
+
console.log(` ${typeLabel}`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (changes.length > 50) {
|
|
406
|
+
console.log(chalk.dim(`\n... and ${changes.length - 50} more changes`));
|
|
407
|
+
}
|
|
408
|
+
} else {
|
|
409
|
+
console.log(chalk.dim('\nRun with --verbose to see detailed changes.'));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
db.close();
|
|
413
|
+
|
|
414
|
+
} catch (error) {
|
|
415
|
+
spinner.fail('Failed to compare snapshots');
|
|
416
|
+
console.error(chalk.red(`\nError: ${error instanceof Error ? error.message : error}`));
|
|
417
|
+
process.exit(1);
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Snapshot delete command
|
|
423
|
+
*/
|
|
424
|
+
export const snapshotDeleteCommand = new Command('snapshot')
|
|
425
|
+
.alias('snap-delete')
|
|
426
|
+
.description('Delete a snapshot')
|
|
427
|
+
.argument('<snapshot-id>', 'Snapshot ID to delete')
|
|
428
|
+
.option('-f, --force', 'Delete without confirmation')
|
|
429
|
+
.action(async (snapshotId, options) => {
|
|
430
|
+
const spinner = ora();
|
|
431
|
+
const projectRoot = process.cwd();
|
|
432
|
+
|
|
433
|
+
try {
|
|
434
|
+
// Load database
|
|
435
|
+
const { DatabaseClient } = await import('../../db/client.js');
|
|
436
|
+
const { version: K0NTEXT_VERSION } = await import('../../cli/version/comparator.js');
|
|
437
|
+
const db = new DatabaseClient(projectRoot);
|
|
438
|
+
const manager = new SnapshotManager(db, projectRoot, K0NTEXT_VERSION);
|
|
439
|
+
|
|
440
|
+
// Confirm deletion
|
|
441
|
+
if (!options.force) {
|
|
442
|
+
const confirmed = await confirm({
|
|
443
|
+
message: `Delete snapshot ${snapshotId}? This cannot be undone.`,
|
|
444
|
+
default: false
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
if (!confirmed) {
|
|
448
|
+
console.log(chalk.dim('\nDeletion cancelled.\n'));
|
|
449
|
+
db.close();
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
spinner.start('Deleting snapshot...');
|
|
455
|
+
|
|
456
|
+
const deleted = await manager.deleteSnapshot(snapshotId);
|
|
457
|
+
|
|
458
|
+
if (deleted) {
|
|
459
|
+
spinner.succeed(chalk.green('Snapshot deleted successfully!'));
|
|
460
|
+
} else {
|
|
461
|
+
spinner.fail(chalk.red('Snapshot not found'));
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
db.close();
|
|
465
|
+
|
|
466
|
+
} catch (error) {
|
|
467
|
+
spinner.fail('Failed to delete snapshot');
|
|
468
|
+
console.error(chalk.red(`\nError: ${error instanceof Error ? error.message : error}`));
|
|
469
|
+
process.exit(1);
|
|
470
|
+
}
|
|
471
|
+
});
|
|
@@ -8,6 +8,7 @@ import { input, confirm, select, checkbox } from '@inquirer/prompts';
|
|
|
8
8
|
import chalk from 'chalk';
|
|
9
9
|
import { ProjectType } from '../core/session.js';
|
|
10
10
|
import { K0NTEXT_THEME } from '../tui/theme.js';
|
|
11
|
+
import { stripBOM } from '../../../utils/encoding.js';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Wizard configuration result
|
|
@@ -66,7 +67,9 @@ export class InitWizard {
|
|
|
66
67
|
|
|
67
68
|
constructor(projectRoot: string) {
|
|
68
69
|
this.projectRoot = projectRoot;
|
|
69
|
-
|
|
70
|
+
// Strip UTF-8 BOM from env var if present (Windows editors sometimes add this)
|
|
71
|
+
const cleanKey = process.env.OPENROUTER_API_KEY ? stripBOM(process.env.OPENROUTER_API_KEY) : '';
|
|
72
|
+
this.hasExistingKey = cleanKey.length > 0;
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
/**
|
|
@@ -146,7 +149,9 @@ for your specific needs.
|
|
|
146
149
|
});
|
|
147
150
|
|
|
148
151
|
if (useExisting) {
|
|
149
|
-
|
|
152
|
+
// Strip UTF-8 BOM from env var if present (Windows editors sometimes add this)
|
|
153
|
+
const envKey = process.env.OPENROUTER_API_KEY || '';
|
|
154
|
+
return stripBOM(envKey);
|
|
150
155
|
}
|
|
151
156
|
}
|
|
152
157
|
|
|
@@ -158,7 +163,9 @@ for your specific needs.
|
|
|
158
163
|
message: 'Enter your OpenRouter API key (or press Enter to skip):',
|
|
159
164
|
validate: (value: string) => {
|
|
160
165
|
if (!value) return true; // Allow skipping
|
|
161
|
-
|
|
166
|
+
// Strip BOM before validation
|
|
167
|
+
const cleanValue = stripBOM(value);
|
|
168
|
+
if (cleanValue.startsWith('sk-or-v1-')) return true;
|
|
162
169
|
return 'Invalid API key format. Should start with "sk-or-v1-"';
|
|
163
170
|
}
|
|
164
171
|
});
|
|
@@ -174,7 +181,8 @@ for your specific needs.
|
|
|
174
181
|
}
|
|
175
182
|
}
|
|
176
183
|
|
|
177
|
-
|
|
184
|
+
// Strip BOM from user input before returning
|
|
185
|
+
return stripBOM(apiKey || '');
|
|
178
186
|
}
|
|
179
187
|
|
|
180
188
|
/**
|
package/src/db/client.ts
CHANGED
|
@@ -1210,6 +1210,14 @@ export class DatabaseClient {
|
|
|
1210
1210
|
}
|
|
1211
1211
|
}
|
|
1212
1212
|
|
|
1213
|
+
/**
|
|
1214
|
+
* Prepare a SQL statement
|
|
1215
|
+
* Exposes the underlying Database.prepare() method for external use
|
|
1216
|
+
*/
|
|
1217
|
+
prepare(sql: string): Database.Statement {
|
|
1218
|
+
return this.db.prepare(sql);
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1213
1221
|
/**
|
|
1214
1222
|
* Close database connection
|
|
1215
1223
|
*/
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
-- Migration 0016: Context System and TodoList Support
|
|
2
|
+
-- From: 1.5.0
|
|
3
|
+
-- To: 1.6.0
|
|
4
|
+
|
|
5
|
+
-- Todo sessions (for tracking across compactions)
|
|
6
|
+
CREATE TABLE IF NOT EXISTS todo_sessions (
|
|
7
|
+
id TEXT PRIMARY KEY,
|
|
8
|
+
name TEXT NOT NULL,
|
|
9
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
10
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
11
|
+
parent_session TEXT,
|
|
12
|
+
metadata JSON
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
-- Todo tasks
|
|
16
|
+
CREATE TABLE IF NOT EXISTS todo_tasks (
|
|
17
|
+
id TEXT PRIMARY KEY,
|
|
18
|
+
session_id TEXT NOT NULL,
|
|
19
|
+
subject TEXT NOT NULL,
|
|
20
|
+
description TEXT,
|
|
21
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
22
|
+
dependencies TEXT,
|
|
23
|
+
assigned_to TEXT,
|
|
24
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
25
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
26
|
+
completed_at TEXT,
|
|
27
|
+
FOREIGN KEY (session_id) REFERENCES todo_sessions(id) ON DELETE CASCADE
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
-- File timestamps for sync tracking
|
|
31
|
+
CREATE TABLE IF NOT EXISTS file_timestamps (
|
|
32
|
+
path TEXT PRIMARY KEY,
|
|
33
|
+
modified_time TEXT NOT NULL,
|
|
34
|
+
size INTEGER NOT NULL,
|
|
35
|
+
hash TEXT NOT NULL,
|
|
36
|
+
last_checked TEXT NOT NULL DEFAULT (datetime('now')),
|
|
37
|
+
git_commit TEXT
|
|
38
|
+
);
|