k0ntext 3.2.1 → 3.3.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/dist/cli/index.js +28 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/repl/core/parser.d.ts +84 -0
- package/dist/cli/repl/core/parser.d.ts.map +1 -0
- package/dist/cli/repl/core/parser.js +309 -0
- package/dist/cli/repl/core/parser.js.map +1 -0
- package/dist/cli/repl/core/session.d.ts +124 -0
- package/dist/cli/repl/core/session.d.ts.map +1 -0
- package/dist/cli/repl/core/session.js +196 -0
- package/dist/cli/repl/core/session.js.map +1 -0
- package/dist/cli/repl/index.d.ts +56 -0
- package/dist/cli/repl/index.d.ts.map +1 -0
- package/dist/cli/repl/index.js +468 -0
- package/dist/cli/repl/index.js.map +1 -0
- package/dist/cli/repl/init/wizard.d.ts +61 -0
- package/dist/cli/repl/init/wizard.d.ts.map +1 -0
- package/dist/cli/repl/init/wizard.js +245 -0
- package/dist/cli/repl/init/wizard.js.map +1 -0
- package/dist/cli/repl/tui/theme.d.ts +109 -0
- package/dist/cli/repl/tui/theme.d.ts.map +1 -0
- package/dist/cli/repl/tui/theme.js +225 -0
- package/dist/cli/repl/tui/theme.js.map +1 -0
- package/dist/cli/repl/update/checker.d.ts +64 -0
- package/dist/cli/repl/update/checker.d.ts.map +1 -0
- package/dist/cli/repl/update/checker.js +166 -0
- package/dist/cli/repl/update/checker.js.map +1 -0
- package/dist/db/client.d.ts +1 -0
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +2 -1
- package/dist/db/client.js.map +1 -1
- package/package.json +4 -1
- package/src/cli/index.ts +28 -2
- package/src/cli/repl/core/parser.ts +373 -0
- package/src/cli/repl/core/session.ts +269 -0
- package/src/cli/repl/index.ts +554 -0
- package/src/cli/repl/init/wizard.ts +300 -0
- package/src/cli/repl/tui/theme.ts +276 -0
- package/src/cli/repl/update/checker.ts +209 -0
- package/src/db/client.ts +9 -7
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REPL Command Parser
|
|
3
|
+
*
|
|
4
|
+
* Parses and executes commands in the REPL shell
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ProjectType } from './session.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Parsed command
|
|
11
|
+
*/
|
|
12
|
+
export interface ParsedCommand {
|
|
13
|
+
raw: string;
|
|
14
|
+
name: string;
|
|
15
|
+
args: string[];
|
|
16
|
+
flags: Record<string, boolean | string>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Command result
|
|
21
|
+
*/
|
|
22
|
+
export interface CommandResult {
|
|
23
|
+
success: boolean;
|
|
24
|
+
output?: string;
|
|
25
|
+
error?: string;
|
|
26
|
+
data?: unknown;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Command definition
|
|
31
|
+
*/
|
|
32
|
+
export interface CommandDefinition {
|
|
33
|
+
name: string;
|
|
34
|
+
aliases?: string[];
|
|
35
|
+
description: string;
|
|
36
|
+
usage: string;
|
|
37
|
+
examples: string[];
|
|
38
|
+
handler: (args: string[], flags: Record<string, boolean | string>) => Promise<CommandResult>;
|
|
39
|
+
completions?: (args: string[]) => string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* REPL Command Parser
|
|
44
|
+
*/
|
|
45
|
+
export class REPLCommandParser {
|
|
46
|
+
private commands: Map<string, CommandDefinition>;
|
|
47
|
+
private aliases: Map<string, string>;
|
|
48
|
+
|
|
49
|
+
constructor() {
|
|
50
|
+
this.commands = new Map();
|
|
51
|
+
this.aliases = new Map();
|
|
52
|
+
this.registerDefaultCommands();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Register a command
|
|
57
|
+
*/
|
|
58
|
+
registerCommand(def: CommandDefinition): void {
|
|
59
|
+
this.commands.set(def.name, def);
|
|
60
|
+
|
|
61
|
+
// Register aliases
|
|
62
|
+
if (def.aliases) {
|
|
63
|
+
for (const alias of def.aliases) {
|
|
64
|
+
this.aliases.set(alias, def.name);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Unregister a command
|
|
71
|
+
*/
|
|
72
|
+
unregisterCommand(name: string): void {
|
|
73
|
+
const def = this.commands.get(name);
|
|
74
|
+
if (def) {
|
|
75
|
+
this.commands.delete(name);
|
|
76
|
+
|
|
77
|
+
// Remove aliases
|
|
78
|
+
if (def.aliases) {
|
|
79
|
+
for (const alias of def.aliases) {
|
|
80
|
+
this.aliases.delete(alias);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Parse a command string
|
|
88
|
+
*/
|
|
89
|
+
parse(input: string): ParsedCommand | null {
|
|
90
|
+
const trimmed = input.trim();
|
|
91
|
+
if (!trimmed) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Split into parts, respecting quotes
|
|
96
|
+
const parts: string[] = [];
|
|
97
|
+
let current = '';
|
|
98
|
+
let inQuotes = false;
|
|
99
|
+
let quoteChar = '';
|
|
100
|
+
|
|
101
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
102
|
+
const char = trimmed[i];
|
|
103
|
+
|
|
104
|
+
if ((char === '"' || char === "'") && (i === 0 || trimmed[i - 1] !== '\\')) {
|
|
105
|
+
if (!inQuotes) {
|
|
106
|
+
inQuotes = true;
|
|
107
|
+
quoteChar = char;
|
|
108
|
+
} else if (char === quoteChar) {
|
|
109
|
+
inQuotes = false;
|
|
110
|
+
quoteChar = '';
|
|
111
|
+
}
|
|
112
|
+
} else if (char === ' ' && !inQuotes) {
|
|
113
|
+
if (current) {
|
|
114
|
+
parts.push(current);
|
|
115
|
+
current = '';
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
current += char;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (current) {
|
|
123
|
+
parts.push(current);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (parts.length === 0) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Parse flags
|
|
131
|
+
const args: string[] = [];
|
|
132
|
+
const flags: Record<string, boolean | string> = {};
|
|
133
|
+
|
|
134
|
+
for (const part of parts.slice(1)) {
|
|
135
|
+
if (part.startsWith('--')) {
|
|
136
|
+
const flagParts = part.slice(2).split('=');
|
|
137
|
+
const flagName = flagParts[0];
|
|
138
|
+
if (flagParts.length > 1) {
|
|
139
|
+
flags[flagName] = flagParts.slice(1).join('=');
|
|
140
|
+
} else {
|
|
141
|
+
flags[flagName] = true;
|
|
142
|
+
}
|
|
143
|
+
} else if (part.startsWith('-')) {
|
|
144
|
+
flags[part.slice(1)] = true;
|
|
145
|
+
} else {
|
|
146
|
+
args.push(part);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Resolve command name (handle aliases)
|
|
151
|
+
let name = parts[0].toLowerCase();
|
|
152
|
+
if (this.aliases.has(name)) {
|
|
153
|
+
name = this.aliases.get(name)!;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
raw: trimmed,
|
|
158
|
+
name,
|
|
159
|
+
args,
|
|
160
|
+
flags
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Execute a parsed command
|
|
166
|
+
*/
|
|
167
|
+
async execute(parsed: ParsedCommand): Promise<CommandResult> {
|
|
168
|
+
const def = this.commands.get(parsed.name);
|
|
169
|
+
|
|
170
|
+
if (!def) {
|
|
171
|
+
return {
|
|
172
|
+
success: false,
|
|
173
|
+
error: `Unknown command: ${parsed.name}. Type 'help' for available commands.`
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
return await def.handler(parsed.args, parsed.flags);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
return {
|
|
181
|
+
success: false,
|
|
182
|
+
error: error instanceof Error ? error.message : String(error)
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get command completions
|
|
189
|
+
*/
|
|
190
|
+
getCompletions(input: string, cursor: number): string[] {
|
|
191
|
+
const parsed = this.parse(input);
|
|
192
|
+
if (!parsed) {
|
|
193
|
+
return Array.from(this.commands.keys());
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const def = this.commands.get(parsed.name);
|
|
197
|
+
if (def && def.completions) {
|
|
198
|
+
return def.completions(parsed.args);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get all command names
|
|
206
|
+
*/
|
|
207
|
+
getCommandNames(): string[] {
|
|
208
|
+
return Array.from(this.commands.keys());
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get command definition
|
|
213
|
+
*/
|
|
214
|
+
getCommand(name: string): CommandDefinition | undefined {
|
|
215
|
+
// Resolve alias
|
|
216
|
+
const resolvedName = this.aliases.get(name) || name;
|
|
217
|
+
return this.commands.get(resolvedName);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Get help text for a command
|
|
222
|
+
*/
|
|
223
|
+
getHelp(name?: string): string {
|
|
224
|
+
if (name) {
|
|
225
|
+
const def = this.getCommand(name);
|
|
226
|
+
if (!def) {
|
|
227
|
+
return `Unknown command: ${name}`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return `
|
|
231
|
+
${def.name} - ${def.description}
|
|
232
|
+
|
|
233
|
+
Usage:
|
|
234
|
+
${def.usage}
|
|
235
|
+
|
|
236
|
+
Examples:
|
|
237
|
+
${def.examples.map(e => ` ${e}`).join('\n')}
|
|
238
|
+
`.trim();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Show all commands
|
|
242
|
+
const lines = [
|
|
243
|
+
'\nAvailable Commands:',
|
|
244
|
+
''
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
const categories = this.groupCommandsByCategory();
|
|
248
|
+
|
|
249
|
+
for (const [category, commands] of Object.entries(categories)) {
|
|
250
|
+
lines.push(` ${category}:`);
|
|
251
|
+
for (const cmd of commands) {
|
|
252
|
+
const def = this.getCommand(cmd);
|
|
253
|
+
if (def) {
|
|
254
|
+
lines.push(` ${cmd.padEnd(15)} - ${def.description}`);
|
|
255
|
+
} else {
|
|
256
|
+
lines.push(` ${cmd.padEnd(15)} - (no description)`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
lines.push('');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
lines.push(' Use "help <command>" for more information on a specific command.');
|
|
263
|
+
lines.push('');
|
|
264
|
+
|
|
265
|
+
return lines.join('\n');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Group commands by category
|
|
270
|
+
*/
|
|
271
|
+
private groupCommandsByCategory(): Record<string, string[]> {
|
|
272
|
+
const categories: Record<string, string[]> = {
|
|
273
|
+
'Core': ['help', 'exit', 'clear', 'config'],
|
|
274
|
+
'Database': ['stats', 'index', 'search'],
|
|
275
|
+
'Initialization': ['init', 'generate'],
|
|
276
|
+
'Monitoring': ['watch', 'drift'],
|
|
277
|
+
'MCP': ['mcp', 'sync']
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// Add all commands to categories
|
|
281
|
+
for (const name of this.getCommandNames()) {
|
|
282
|
+
let categorized = false;
|
|
283
|
+
for (const [_, cmds] of Object.entries(categories)) {
|
|
284
|
+
if (cmds.includes(name)) {
|
|
285
|
+
categorized = true;
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (!categorized) {
|
|
290
|
+
if (!categories['Other']) {
|
|
291
|
+
categories['Other'] = [];
|
|
292
|
+
}
|
|
293
|
+
categories['Other'].push(name);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return categories;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Register default commands
|
|
302
|
+
*/
|
|
303
|
+
private registerDefaultCommands(): void {
|
|
304
|
+
// Help command
|
|
305
|
+
this.registerCommand({
|
|
306
|
+
name: 'help',
|
|
307
|
+
aliases: ['?', 'h'],
|
|
308
|
+
description: 'Show help information',
|
|
309
|
+
usage: 'help [command]',
|
|
310
|
+
examples: [
|
|
311
|
+
'help',
|
|
312
|
+
'help index',
|
|
313
|
+
'help stats'
|
|
314
|
+
],
|
|
315
|
+
handler: async (args) => {
|
|
316
|
+
const commandName = args[0];
|
|
317
|
+
return {
|
|
318
|
+
success: true,
|
|
319
|
+
output: this.getHelp(commandName)
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Exit command
|
|
325
|
+
this.registerCommand({
|
|
326
|
+
name: 'exit',
|
|
327
|
+
aliases: ['quit', 'q'],
|
|
328
|
+
description: 'Exit the REPL shell',
|
|
329
|
+
usage: 'exit',
|
|
330
|
+
examples: ['exit', 'quit'],
|
|
331
|
+
handler: async () => {
|
|
332
|
+
return {
|
|
333
|
+
success: true,
|
|
334
|
+
output: 'Goodbye!'
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// Clear command
|
|
340
|
+
this.registerCommand({
|
|
341
|
+
name: 'clear',
|
|
342
|
+
aliases: ['cls'],
|
|
343
|
+
description: 'Clear the screen',
|
|
344
|
+
usage: 'clear',
|
|
345
|
+
examples: ['clear'],
|
|
346
|
+
handler: async () => {
|
|
347
|
+
console.clear();
|
|
348
|
+
return {
|
|
349
|
+
success: true,
|
|
350
|
+
output: ''
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// Config command
|
|
356
|
+
this.registerCommand({
|
|
357
|
+
name: 'config',
|
|
358
|
+
description: 'Manage configuration',
|
|
359
|
+
usage: 'config [get|set|list] [key] [value]',
|
|
360
|
+
examples: [
|
|
361
|
+
'config list',
|
|
362
|
+
'config get projectType',
|
|
363
|
+
'config set projectType webapp'
|
|
364
|
+
],
|
|
365
|
+
handler: async (args) => {
|
|
366
|
+
return {
|
|
367
|
+
success: true,
|
|
368
|
+
output: 'Config command - use get|set|list'
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REPL Session Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages REPL session lifecycle, state persistence, and activity tracking
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generate a simple UUID v4
|
|
12
|
+
*/
|
|
13
|
+
function generateUUID(): string {
|
|
14
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
15
|
+
const r = Math.random() * 16 | 0;
|
|
16
|
+
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
17
|
+
return v.toString(16);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Session state
|
|
23
|
+
*/
|
|
24
|
+
export interface SessionState {
|
|
25
|
+
sessionId: string;
|
|
26
|
+
startTime: string;
|
|
27
|
+
lastActivity: string;
|
|
28
|
+
projectRoot: string;
|
|
29
|
+
config: SessionConfig;
|
|
30
|
+
history: CommandHistoryEntry[];
|
|
31
|
+
stats: SessionStats;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Session configuration
|
|
36
|
+
*/
|
|
37
|
+
export interface SessionConfig {
|
|
38
|
+
apiKey?: string;
|
|
39
|
+
projectType?: ProjectType;
|
|
40
|
+
aiTools: string[];
|
|
41
|
+
features: string[];
|
|
42
|
+
autoUpdate: boolean;
|
|
43
|
+
theme: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Project type
|
|
48
|
+
*/
|
|
49
|
+
export type ProjectType = 'monorepo' | 'webapp' | 'library' | 'api' | 'cli' | 'unknown';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Command history entry
|
|
53
|
+
*/
|
|
54
|
+
export interface CommandHistoryEntry {
|
|
55
|
+
command: string;
|
|
56
|
+
timestamp: string;
|
|
57
|
+
result?: string;
|
|
58
|
+
duration?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Session statistics
|
|
63
|
+
*/
|
|
64
|
+
export interface SessionStats {
|
|
65
|
+
commandsExecuted: number;
|
|
66
|
+
searchesPerformed: number;
|
|
67
|
+
filesIndexed: number;
|
|
68
|
+
embeddingsGenerated: number;
|
|
69
|
+
errorsEncountered: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* REPL Session Manager
|
|
74
|
+
*/
|
|
75
|
+
export class REPLSessionManager {
|
|
76
|
+
private state: SessionState;
|
|
77
|
+
private statePath: string;
|
|
78
|
+
private projectRoot: string;
|
|
79
|
+
|
|
80
|
+
constructor(projectRoot: string) {
|
|
81
|
+
this.projectRoot = projectRoot;
|
|
82
|
+
const k0ntextDir = path.join(projectRoot, '.k0ntext');
|
|
83
|
+
this.statePath = path.join(k0ntextDir, 'session.json');
|
|
84
|
+
|
|
85
|
+
// Ensure .k0ntext directory exists
|
|
86
|
+
if (!fs.existsSync(k0ntextDir)) {
|
|
87
|
+
fs.mkdirSync(k0ntextDir, { recursive: true });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Try to load existing session or create new
|
|
91
|
+
this.state = this.load() || this.createNewSession();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Create a new session
|
|
96
|
+
*/
|
|
97
|
+
private createNewSession(): SessionState {
|
|
98
|
+
return {
|
|
99
|
+
sessionId: generateUUID(),
|
|
100
|
+
startTime: new Date().toISOString(),
|
|
101
|
+
lastActivity: new Date().toISOString(),
|
|
102
|
+
projectRoot: this.projectRoot,
|
|
103
|
+
config: {
|
|
104
|
+
aiTools: [],
|
|
105
|
+
autoUpdate: true,
|
|
106
|
+
theme: 'default',
|
|
107
|
+
features: []
|
|
108
|
+
},
|
|
109
|
+
history: [],
|
|
110
|
+
stats: {
|
|
111
|
+
commandsExecuted: 0,
|
|
112
|
+
searchesPerformed: 0,
|
|
113
|
+
filesIndexed: 0,
|
|
114
|
+
embeddingsGenerated: 0,
|
|
115
|
+
errorsEncountered: 0
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Load session from disk
|
|
122
|
+
*/
|
|
123
|
+
private load(): SessionState | null {
|
|
124
|
+
try {
|
|
125
|
+
if (fs.existsSync(this.statePath)) {
|
|
126
|
+
const content = fs.readFileSync(this.statePath, 'utf-8');
|
|
127
|
+
return JSON.parse(content) as SessionState;
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
// If load fails, return null to create new session
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Save session to disk
|
|
137
|
+
*/
|
|
138
|
+
save(): void {
|
|
139
|
+
try {
|
|
140
|
+
this.state.lastActivity = new Date().toISOString();
|
|
141
|
+
fs.writeFileSync(this.statePath, JSON.stringify(this.state, null, 2));
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error('Failed to save session:', error);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get session state
|
|
149
|
+
*/
|
|
150
|
+
getState(): SessionState {
|
|
151
|
+
return { ...this.state };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Update session config
|
|
156
|
+
*/
|
|
157
|
+
updateConfig(config: Partial<SessionConfig>): void {
|
|
158
|
+
this.state.config = { ...this.state.config, ...config };
|
|
159
|
+
this.save();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Add command to history
|
|
164
|
+
*/
|
|
165
|
+
addCommand(command: string, result?: string, duration?: number): void {
|
|
166
|
+
const entry: CommandHistoryEntry = {
|
|
167
|
+
command,
|
|
168
|
+
timestamp: new Date().toISOString(),
|
|
169
|
+
result,
|
|
170
|
+
duration
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
this.state.history.push(entry);
|
|
174
|
+
|
|
175
|
+
// Keep only last 100 commands
|
|
176
|
+
if (this.state.history.length > 100) {
|
|
177
|
+
this.state.history = this.state.history.slice(-100);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
this.state.stats.commandsExecuted++;
|
|
181
|
+
this.save();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get command history
|
|
186
|
+
*/
|
|
187
|
+
getHistory(limit?: number): CommandHistoryEntry[] {
|
|
188
|
+
const history = this.state.history.slice().reverse();
|
|
189
|
+
return limit ? history.slice(0, limit) : history;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Update session stats
|
|
194
|
+
*/
|
|
195
|
+
updateStats(stats: Partial<SessionStats>): void {
|
|
196
|
+
this.state.stats = { ...this.state.stats, ...stats };
|
|
197
|
+
this.save();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get session stats
|
|
202
|
+
*/
|
|
203
|
+
getStats(): SessionStats {
|
|
204
|
+
return { ...this.state.stats };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get session duration
|
|
209
|
+
*/
|
|
210
|
+
getDuration(): { ms: number; human: string } {
|
|
211
|
+
const start = new Date(this.state.startTime);
|
|
212
|
+
const now = new Date();
|
|
213
|
+
const diff = now.getTime() - start.getTime();
|
|
214
|
+
|
|
215
|
+
const seconds = Math.floor(diff / 1000);
|
|
216
|
+
const minutes = Math.floor(seconds / 60);
|
|
217
|
+
const hours = Math.floor(minutes / 60);
|
|
218
|
+
const days = Math.floor(hours / 24);
|
|
219
|
+
|
|
220
|
+
let human = '';
|
|
221
|
+
if (days > 0) human += `${days}d `;
|
|
222
|
+
if (hours % 24 > 0) human += `${hours % 24}h `;
|
|
223
|
+
if (minutes % 60 > 0) human += `${minutes % 60}m `;
|
|
224
|
+
if (seconds % 60 > 0 || human === '') human += `${seconds % 60}s`;
|
|
225
|
+
|
|
226
|
+
return { ms: diff, human: human.trim() };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Check if session is initialized (has API key and project type)
|
|
231
|
+
*/
|
|
232
|
+
isInitialized(): boolean {
|
|
233
|
+
return !!(
|
|
234
|
+
this.state.config.apiKey ||
|
|
235
|
+
process.env.OPENROUTER_API_KEY
|
|
236
|
+
) && !!this.state.config.projectType;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Set initialization status
|
|
241
|
+
*/
|
|
242
|
+
setInitialized(apiKey: string, projectType: ProjectType): void {
|
|
243
|
+
this.state.config.apiKey = apiKey;
|
|
244
|
+
this.state.config.projectType = projectType;
|
|
245
|
+
this.save();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get OpenRouter API key
|
|
250
|
+
*/
|
|
251
|
+
getApiKey(): string | undefined {
|
|
252
|
+
return this.state.config.apiKey || process.env.OPENROUTER_API_KEY;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Clear session (for reset/reinit)
|
|
257
|
+
*/
|
|
258
|
+
clear(): void {
|
|
259
|
+
this.state = this.createNewSession();
|
|
260
|
+
this.save();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* End session
|
|
265
|
+
*/
|
|
266
|
+
end(): void {
|
|
267
|
+
this.save();
|
|
268
|
+
}
|
|
269
|
+
}
|