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.
Files changed (77) hide show
  1. package/README.md +281 -382
  2. package/dist/agent-system/timestamp-tracker.d.ts +159 -0
  3. package/dist/agent-system/timestamp-tracker.d.ts.map +1 -0
  4. package/dist/agent-system/timestamp-tracker.js +405 -0
  5. package/dist/agent-system/timestamp-tracker.js.map +1 -0
  6. package/dist/agent-system/todolist-manager.d.ts +244 -0
  7. package/dist/agent-system/todolist-manager.d.ts.map +1 -0
  8. package/dist/agent-system/todolist-manager.js +580 -0
  9. package/dist/agent-system/todolist-manager.js.map +1 -0
  10. package/dist/analyzer/intelligent-analyzer.d.ts +7 -0
  11. package/dist/analyzer/intelligent-analyzer.d.ts.map +1 -1
  12. package/dist/analyzer/intelligent-analyzer.js +46 -1
  13. package/dist/analyzer/intelligent-analyzer.js.map +1 -1
  14. package/dist/cli/commands/embeddings-refresh.d.ts.map +1 -1
  15. package/dist/cli/commands/embeddings-refresh.js +4 -1
  16. package/dist/cli/commands/embeddings-refresh.js.map +1 -1
  17. package/dist/cli/commands/migrate.d.ts.map +1 -1
  18. package/dist/cli/commands/migrate.js +8 -0
  19. package/dist/cli/commands/migrate.js.map +1 -1
  20. package/dist/cli/commands/snapshot.d.ts +28 -0
  21. package/dist/cli/commands/snapshot.d.ts.map +1 -0
  22. package/dist/cli/commands/snapshot.js +408 -0
  23. package/dist/cli/commands/snapshot.js.map +1 -0
  24. package/dist/cli/repl/init/wizard.d.ts.map +1 -1
  25. package/dist/cli/repl/init/wizard.js +12 -4
  26. package/dist/cli/repl/init/wizard.js.map +1 -1
  27. package/dist/cli/version/comparator.d.ts +1 -0
  28. package/dist/cli/version/comparator.d.ts.map +1 -1
  29. package/dist/cli/version/comparator.js +1 -0
  30. package/dist/cli/version/comparator.js.map +1 -1
  31. package/dist/db/client.d.ts +5 -0
  32. package/dist/db/client.d.ts.map +1 -1
  33. package/dist/db/client.js +7 -0
  34. package/dist/db/client.js.map +1 -1
  35. package/dist/db/schema.d.ts +1 -1
  36. package/dist/db/schema.js +1 -1
  37. package/dist/embeddings/openrouter.d.ts.map +1 -1
  38. package/dist/embeddings/openrouter.js +8 -3
  39. package/dist/embeddings/openrouter.js.map +1 -1
  40. package/dist/services/snapshot-manager.d.ts +251 -0
  41. package/dist/services/snapshot-manager.d.ts.map +1 -0
  42. package/dist/services/snapshot-manager.js +541 -0
  43. package/dist/services/snapshot-manager.js.map +1 -0
  44. package/dist/utils/chunking.d.ts +38 -0
  45. package/dist/utils/chunking.d.ts.map +1 -0
  46. package/dist/utils/chunking.js +133 -0
  47. package/dist/utils/chunking.js.map +1 -0
  48. package/dist/utils/encoding.d.ts +24 -0
  49. package/dist/utils/encoding.d.ts.map +1 -0
  50. package/dist/utils/encoding.js +32 -0
  51. package/dist/utils/encoding.js.map +1 -0
  52. package/dist/utils/index.d.ts +8 -0
  53. package/dist/utils/index.d.ts.map +1 -0
  54. package/dist/utils/index.js +8 -0
  55. package/dist/utils/index.js.map +1 -0
  56. package/docs/QUICKSTART.md +1 -1
  57. package/docs/TROUBLESHOOTING.md +51 -76
  58. package/docs/plans/2026-02-09-v3.7.0-database-fixes-and-improvements.md +900 -0
  59. package/docs/plans/2026-02-11-context-engineering-enhancement.md +1402 -0
  60. package/package.json +8 -2
  61. package/src/agent-system/timestamp-tracker.ts +520 -0
  62. package/src/agent-system/todolist-manager.ts +753 -0
  63. package/src/analyzer/intelligent-analyzer.ts +58 -1
  64. package/src/cli/commands/embeddings-refresh.ts +4 -1
  65. package/src/cli/commands/migrate.ts +8 -0
  66. package/src/cli/commands/snapshot.ts +471 -0
  67. package/src/cli/repl/init/wizard.ts +12 -4
  68. package/src/cli/version/comparator.ts +1 -0
  69. package/src/db/client.ts +8 -0
  70. package/src/db/migrations/0016_add_context_system_tables.sql +38 -0
  71. package/src/db/migrations/files/0015_add_sync_state_version_tracking.sql +18 -0
  72. package/src/db/schema.ts +1 -1
  73. package/src/embeddings/openrouter.ts +10 -4
  74. package/src/services/snapshot-manager.ts +719 -0
  75. package/src/utils/chunking.ts +152 -0
  76. package/src/utils/encoding.ts +33 -0
  77. 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
- return this.client.embed(text);
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
- console.log(chalk.dim(` Embedding: ${item.name}`));
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
- this.hasExistingKey = !!process.env.OPENROUTER_API_KEY;
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
- return process.env.OPENROUTER_API_KEY!;
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
- if (value.startsWith('sk-or-v1-')) return true;
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
- return apiKey || '';
184
+ // Strip BOM from user input before returning
185
+ return stripBOM(apiKey || '');
178
186
  }
179
187
 
180
188
  /**
@@ -104,3 +104,4 @@ export function formatUpdateType(type: UpdateType, from: string, to: string): st
104
104
  export function isValidVersion(version: string): boolean {
105
105
  return /^\d+\.\d+\.\d+/.test(version);
106
106
  }
107
+ export const version = '3.8.0';
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
+ );