@sylphx/flow 1.8.0 → 1.8.1
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/CHANGELOG.md +52 -0
- package/assets/output-styles/silent.md +141 -8
- package/assets/rules/core.md +19 -2
- package/package.json +2 -12
- package/src/commands/flow/execute.ts +470 -0
- package/src/commands/flow/index.ts +11 -0
- package/src/commands/flow/prompt.ts +35 -0
- package/src/commands/flow/setup.ts +312 -0
- package/src/commands/flow/targets.ts +18 -0
- package/src/commands/flow/types.ts +47 -0
- package/src/commands/flow-command.ts +18 -967
- package/src/commands/flow-orchestrator.ts +14 -5
- package/src/commands/hook-command.ts +1 -1
- package/src/commands/init-core.ts +12 -3
- package/src/commands/run-command.ts +1 -1
- package/src/config/rules.ts +1 -1
- package/src/core/error-handling.ts +1 -1
- package/src/core/loop-controller.ts +1 -1
- package/src/core/state-detector.ts +1 -1
- package/src/core/target-manager.ts +1 -1
- package/src/index.ts +1 -1
- package/src/shared/files/index.ts +1 -1
- package/src/shared/processing/index.ts +1 -1
- package/src/targets/claude-code.ts +3 -3
- package/src/targets/opencode.ts +3 -3
- package/src/utils/agent-enhancer.ts +2 -2
- package/src/utils/{mcp-config.ts → config/mcp-config.ts} +4 -4
- package/src/utils/{paths.ts → config/paths.ts} +1 -1
- package/src/utils/{settings.ts → config/settings.ts} +1 -1
- package/src/utils/{target-config.ts → config/target-config.ts} +5 -5
- package/src/utils/{target-utils.ts → config/target-utils.ts} +3 -3
- package/src/utils/display/banner.ts +25 -0
- package/src/utils/display/status.ts +55 -0
- package/src/utils/{file-operations.ts → files/file-operations.ts} +2 -2
- package/src/utils/files/jsonc.ts +36 -0
- package/src/utils/{sync-utils.ts → files/sync-utils.ts} +3 -3
- package/src/utils/index.ts +42 -61
- package/src/utils/version.ts +47 -0
- package/src/components/benchmark-monitor.tsx +0 -331
- package/src/components/reindex-progress.tsx +0 -261
- package/src/composables/functional/index.ts +0 -14
- package/src/composables/functional/useEnvironment.ts +0 -171
- package/src/composables/functional/useFileSystem.ts +0 -139
- package/src/composables/index.ts +0 -4
- package/src/composables/useEnv.ts +0 -13
- package/src/composables/useRuntimeConfig.ts +0 -27
- package/src/core/ai-sdk.ts +0 -603
- package/src/core/app-factory.ts +0 -381
- package/src/core/builtin-agents.ts +0 -9
- package/src/core/command-system.ts +0 -550
- package/src/core/config-system.ts +0 -550
- package/src/core/connection-pool.ts +0 -390
- package/src/core/di-container.ts +0 -155
- package/src/core/headless-display.ts +0 -96
- package/src/core/interfaces/index.ts +0 -22
- package/src/core/interfaces/repository.interface.ts +0 -91
- package/src/core/interfaces/service.interface.ts +0 -133
- package/src/core/interfaces.ts +0 -96
- package/src/core/result.ts +0 -351
- package/src/core/service-config.ts +0 -252
- package/src/core/session-service.ts +0 -121
- package/src/core/storage-factory.ts +0 -115
- package/src/core/stream-handler.ts +0 -288
- package/src/core/type-utils.ts +0 -427
- package/src/core/unified-storage.ts +0 -456
- package/src/core/validation/limit.ts +0 -46
- package/src/core/validation/query.ts +0 -20
- package/src/db/auto-migrate.ts +0 -322
- package/src/db/base-database-client.ts +0 -144
- package/src/db/cache-db.ts +0 -218
- package/src/db/cache-schema.ts +0 -75
- package/src/db/database.ts +0 -70
- package/src/db/index.ts +0 -252
- package/src/db/memory-db.ts +0 -153
- package/src/db/memory-schema.ts +0 -29
- package/src/db/schema.ts +0 -289
- package/src/db/session-repository.ts +0 -733
- package/src/domains/index.ts +0 -6
- package/src/domains/utilities/index.ts +0 -6
- package/src/domains/utilities/time/index.ts +0 -5
- package/src/domains/utilities/time/tools.ts +0 -291
- package/src/services/agent-service.ts +0 -273
- package/src/services/evaluation-service.ts +0 -271
- package/src/services/functional/evaluation-logic.ts +0 -296
- package/src/services/functional/file-processor.ts +0 -273
- package/src/services/functional/index.ts +0 -12
- package/src/services/memory.service.ts +0 -476
- package/src/types/api/batch.ts +0 -108
- package/src/types/api/errors.ts +0 -118
- package/src/types/api/index.ts +0 -55
- package/src/types/api/requests.ts +0 -76
- package/src/types/api/responses.ts +0 -180
- package/src/types/api/websockets.ts +0 -85
- package/src/types/benchmark.ts +0 -49
- package/src/types/database.types.ts +0 -510
- package/src/types/memory-types.ts +0 -63
- package/src/utils/advanced-tokenizer.ts +0 -191
- package/src/utils/ai-model-fetcher.ts +0 -19
- package/src/utils/async-file-operations.ts +0 -516
- package/src/utils/audio-player.ts +0 -345
- package/src/utils/codebase-helpers.ts +0 -211
- package/src/utils/console-ui.ts +0 -79
- package/src/utils/database-errors.ts +0 -140
- package/src/utils/debug-logger.ts +0 -49
- package/src/utils/file-scanner.ts +0 -259
- package/src/utils/help.ts +0 -20
- package/src/utils/immutable-cache.ts +0 -106
- package/src/utils/jsonc.ts +0 -158
- package/src/utils/memory-tui.ts +0 -414
- package/src/utils/models-dev.ts +0 -91
- package/src/utils/parallel-operations.ts +0 -487
- package/src/utils/process-manager.ts +0 -155
- package/src/utils/prompts.ts +0 -120
- package/src/utils/search-tool-builder.ts +0 -214
- package/src/utils/session-manager.ts +0 -168
- package/src/utils/session-title.ts +0 -87
- package/src/utils/simplified-errors.ts +0 -410
- package/src/utils/template-engine.ts +0 -94
- package/src/utils/test-audio.ts +0 -71
- package/src/utils/todo-context.ts +0 -46
- package/src/utils/token-counter.ts +0 -288
- /package/src/utils/{cli-output.ts → display/cli-output.ts} +0 -0
- /package/src/utils/{logger.ts → display/logger.ts} +0 -0
- /package/src/utils/{notifications.ts → display/notifications.ts} +0 -0
- /package/src/utils/{secret-utils.ts → security/secret-utils.ts} +0 -0
- /package/src/utils/{security.ts → security/security.ts} +0 -0
package/src/utils/prompts.ts
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Modern CLI prompts with progressive output
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { createInterface } from 'node:readline';
|
|
6
|
-
import chalk from 'chalk';
|
|
7
|
-
|
|
8
|
-
export async function ask(question: string, defaultValue?: string): Promise<string> {
|
|
9
|
-
const rl = createInterface({
|
|
10
|
-
input: process.stdin,
|
|
11
|
-
output: process.stdout,
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
const prompt = defaultValue
|
|
15
|
-
? `${chalk.cyan('❯')} ${question} ${chalk.gray(`(${defaultValue})`)}: `
|
|
16
|
-
: `${chalk.cyan('❯')} ${question}: `;
|
|
17
|
-
|
|
18
|
-
return new Promise((resolve) => {
|
|
19
|
-
rl.question(prompt, (answer) => {
|
|
20
|
-
rl.close();
|
|
21
|
-
resolve(answer.trim() || defaultValue || '');
|
|
22
|
-
});
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export async function askSecret(question: string): Promise<string> {
|
|
27
|
-
const rl = createInterface({
|
|
28
|
-
input: process.stdin,
|
|
29
|
-
output: process.stdout,
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
const prompt = `${chalk.cyan('❯')} ${question}: `;
|
|
33
|
-
|
|
34
|
-
return new Promise((resolve) => {
|
|
35
|
-
// Hide input for secrets
|
|
36
|
-
const stdin = process.stdin;
|
|
37
|
-
const _onData = (char: Buffer) => {
|
|
38
|
-
const str = char.toString('utf8');
|
|
39
|
-
if (str === '\n' || str === '\r' || str === '\r\n') {
|
|
40
|
-
(stdin as any).removeListener('data', _onData);
|
|
41
|
-
process.stdout.write('\n');
|
|
42
|
-
rl.close();
|
|
43
|
-
} else {
|
|
44
|
-
process.stdout.write('•');
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
process.stdout.write(prompt);
|
|
49
|
-
let input = '';
|
|
50
|
-
stdin.on('data', (char) => {
|
|
51
|
-
const str = char.toString('utf8');
|
|
52
|
-
if (str === '\n' || str === '\r' || str === '\r\n') {
|
|
53
|
-
process.stdout.write('\n');
|
|
54
|
-
rl.close();
|
|
55
|
-
resolve(input);
|
|
56
|
-
} else if (str === '\x7f' || str === '\b') {
|
|
57
|
-
// Backspace
|
|
58
|
-
if (input.length > 0) {
|
|
59
|
-
input = input.slice(0, -1);
|
|
60
|
-
process.stdout.write('\b \b');
|
|
61
|
-
}
|
|
62
|
-
} else {
|
|
63
|
-
input += str;
|
|
64
|
-
process.stdout.write('•');
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
stdin.setRawMode(true);
|
|
68
|
-
stdin.resume();
|
|
69
|
-
}).finally(() => {
|
|
70
|
-
const stdin = process.stdin;
|
|
71
|
-
stdin.setRawMode(false);
|
|
72
|
-
stdin.pause();
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export async function select<T extends string>(question: string, choices: T[]): Promise<T> {
|
|
77
|
-
console.log(`${chalk.cyan('❯')} ${question}`);
|
|
78
|
-
choices.forEach((choice, index) => {
|
|
79
|
-
console.log(chalk.gray(` ${index + 1}. ${choice}`));
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
const rl = createInterface({
|
|
83
|
-
input: process.stdin,
|
|
84
|
-
output: process.stdout,
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
return new Promise((resolve) => {
|
|
88
|
-
rl.question(chalk.cyan(` Select (1-${choices.length}): `), (answer) => {
|
|
89
|
-
rl.close();
|
|
90
|
-
const index = Number.parseInt(answer.trim(), 10) - 1;
|
|
91
|
-
if (index >= 0 && index < choices.length) {
|
|
92
|
-
resolve(choices[index]);
|
|
93
|
-
} else {
|
|
94
|
-
resolve(choices[0]);
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export async function confirm(question: string, defaultValue = true): Promise<boolean> {
|
|
101
|
-
const rl = createInterface({
|
|
102
|
-
input: process.stdin,
|
|
103
|
-
output: process.stdout,
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
const defaultText = defaultValue ? 'Y/n' : 'y/N';
|
|
107
|
-
const prompt = `${chalk.cyan('❯')} ${question} ${chalk.gray(`(${defaultText})`)}: `;
|
|
108
|
-
|
|
109
|
-
return new Promise((resolve) => {
|
|
110
|
-
rl.question(prompt, (answer) => {
|
|
111
|
-
rl.close();
|
|
112
|
-
const input = answer.trim().toLowerCase();
|
|
113
|
-
if (input) {
|
|
114
|
-
resolve(input === 'y' || input === 'yes');
|
|
115
|
-
} else {
|
|
116
|
-
resolve(defaultValue);
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
}
|
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unified search tool builder
|
|
3
|
-
* Creates consistent search and status tools for indexers
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
7
|
-
import { z } from 'zod';
|
|
8
|
-
import type { BaseIndexer } from '../services/search/base-indexer.js';
|
|
9
|
-
import { searchDocuments } from '../services/search/tfidf.js';
|
|
10
|
-
|
|
11
|
-
export interface SearchToolConfig {
|
|
12
|
-
indexer: BaseIndexer;
|
|
13
|
-
toolName: string; // e.g., 'search_knowledge', 'search_codebase'
|
|
14
|
-
statusToolName: string; // e.g., 'get_knowledge_status', 'get_indexing_status'
|
|
15
|
-
description: string;
|
|
16
|
-
searchDescription: string;
|
|
17
|
-
examples: string[];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Build search tool with consistent pattern
|
|
22
|
-
*/
|
|
23
|
-
export function buildSearchTool(server: McpServer, config: SearchToolConfig) {
|
|
24
|
-
const { indexer, toolName, statusToolName, description, searchDescription } = config;
|
|
25
|
-
|
|
26
|
-
// Register search tool
|
|
27
|
-
server.registerTool(
|
|
28
|
-
toolName,
|
|
29
|
-
{
|
|
30
|
-
description: `${description}
|
|
31
|
-
|
|
32
|
-
${searchDescription}
|
|
33
|
-
|
|
34
|
-
**Performance:**
|
|
35
|
-
- First search: ~1-5s (indexing time)
|
|
36
|
-
- Subsequent searches: <100ms (cached)
|
|
37
|
-
- Background indexing: Starts automatically on server startup
|
|
38
|
-
|
|
39
|
-
**Status:**
|
|
40
|
-
- Use \`${statusToolName}\` to check indexing progress
|
|
41
|
-
- If indexing in progress, returns progress message`,
|
|
42
|
-
inputSchema: {
|
|
43
|
-
query: z.string().describe('Search query'),
|
|
44
|
-
limit: z.number().optional().describe('Maximum results (default: 5, max: 20)'),
|
|
45
|
-
categories: z.array(z.string()).optional().describe('Filter by categories (optional)'),
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
async (args) => {
|
|
49
|
-
try {
|
|
50
|
-
const query = args.query as string;
|
|
51
|
-
const limit = Math.min((args.limit as number) || 5, 20);
|
|
52
|
-
const categories = args.categories as string[] | undefined;
|
|
53
|
-
|
|
54
|
-
// Check if indexing is in progress
|
|
55
|
-
const status = indexer.getStatus();
|
|
56
|
-
if (status.isIndexing) {
|
|
57
|
-
const elapsed = Math.round((Date.now() - status.startTime) / 1000);
|
|
58
|
-
return {
|
|
59
|
-
content: [
|
|
60
|
-
{
|
|
61
|
-
type: 'text',
|
|
62
|
-
text: `⏳ Indexing in progress...\n\n**Status:**\n- Progress: ${status.progress}%\n- Items indexed: ${status.indexedItems}/${status.totalItems}\n- Elapsed time: ${elapsed}s\n\n*Please wait and try again. Use \`${statusToolName}\` to check progress.*`,
|
|
63
|
-
},
|
|
64
|
-
],
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Check for errors
|
|
69
|
-
if (status.error) {
|
|
70
|
-
return {
|
|
71
|
-
content: [
|
|
72
|
-
{
|
|
73
|
-
type: 'text',
|
|
74
|
-
text: `✗ Indexing failed: ${status.error}\n\nPlease check the error and try again.`,
|
|
75
|
-
},
|
|
76
|
-
],
|
|
77
|
-
isError: true,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Perform search
|
|
82
|
-
const startTime = Date.now();
|
|
83
|
-
const index = await indexer.loadIndex();
|
|
84
|
-
const indexTime = Date.now() - startTime;
|
|
85
|
-
|
|
86
|
-
const searchStartTime = Date.now();
|
|
87
|
-
const results = searchDocuments(query, index, {
|
|
88
|
-
limit: limit * 2,
|
|
89
|
-
minScore: 0.01,
|
|
90
|
-
});
|
|
91
|
-
const searchTime = Date.now() - searchStartTime;
|
|
92
|
-
|
|
93
|
-
// Filter by categories if specified
|
|
94
|
-
let filtered = results;
|
|
95
|
-
if (categories && categories.length > 0) {
|
|
96
|
-
filtered = results.filter((result) => {
|
|
97
|
-
// Extract scheme/protocol from URI (e.g., 'knowledge' from 'knowledge://path')
|
|
98
|
-
const category = result.uri.split('://')[0];
|
|
99
|
-
return categories.includes(category);
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const finalResults = filtered.slice(0, limit);
|
|
104
|
-
|
|
105
|
-
if (finalResults.length === 0) {
|
|
106
|
-
return {
|
|
107
|
-
content: [
|
|
108
|
-
{
|
|
109
|
-
type: 'text',
|
|
110
|
-
text: `No results found for query: "${query}"\n\nTry:\n- Broader search terms\n- Different keywords\n- Check available categories`,
|
|
111
|
-
},
|
|
112
|
-
],
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Build response
|
|
117
|
-
const resultTexts = finalResults.map((item, index) => {
|
|
118
|
-
const filePath = item.uri.replace(/^(knowledge|file):\/\//, '');
|
|
119
|
-
let text = `## ${index + 1}. ${filePath}\n`;
|
|
120
|
-
text += `**Relevance**: ${(item.score * 100).toFixed(0)}%\n`;
|
|
121
|
-
text += `**Matched terms**: ${item.matchedTerms.join(', ')}\n\n`;
|
|
122
|
-
return text;
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
const summary = `Found ${finalResults.length} result(s) for "${query}":\n\n`;
|
|
126
|
-
const stats = `\n---\n\n**Stats:**\n- Total items: ${index.totalDocuments}\n- Index time: ${indexTime}ms\n- Search time: ${searchTime}ms\n`;
|
|
127
|
-
|
|
128
|
-
return {
|
|
129
|
-
content: [
|
|
130
|
-
{
|
|
131
|
-
type: 'text',
|
|
132
|
-
text: summary + resultTexts.join('\n---\n\n') + stats,
|
|
133
|
-
},
|
|
134
|
-
],
|
|
135
|
-
};
|
|
136
|
-
} catch (error: unknown) {
|
|
137
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
138
|
-
console.error(`[ERROR] ${toolName} failed:`, error);
|
|
139
|
-
return {
|
|
140
|
-
content: [
|
|
141
|
-
{
|
|
142
|
-
type: 'text',
|
|
143
|
-
text: `✗ Search error: ${errorMessage}`,
|
|
144
|
-
},
|
|
145
|
-
],
|
|
146
|
-
isError: true,
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
// Register status tool
|
|
153
|
-
server.registerTool(
|
|
154
|
-
statusToolName,
|
|
155
|
-
{
|
|
156
|
-
description: `Get indexing status for ${toolName}.
|
|
157
|
-
|
|
158
|
-
Shows:
|
|
159
|
-
- Whether indexing is in progress
|
|
160
|
-
- Progress percentage
|
|
161
|
-
- Number of items indexed
|
|
162
|
-
- Any errors`,
|
|
163
|
-
inputSchema: {},
|
|
164
|
-
},
|
|
165
|
-
async () => {
|
|
166
|
-
const status = indexer.getStatus();
|
|
167
|
-
|
|
168
|
-
if (status.isIndexing) {
|
|
169
|
-
const elapsed = Math.round((Date.now() - status.startTime) / 1000);
|
|
170
|
-
return {
|
|
171
|
-
content: [
|
|
172
|
-
{
|
|
173
|
-
type: 'text',
|
|
174
|
-
text: `⏳ **Indexing in Progress**\n\n- Progress: ${status.progress}%\n- Items indexed: ${status.indexedItems}/${status.totalItems}\n- Elapsed time: ${elapsed}s`,
|
|
175
|
-
},
|
|
176
|
-
],
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (status.error) {
|
|
181
|
-
return {
|
|
182
|
-
content: [
|
|
183
|
-
{
|
|
184
|
-
type: 'text',
|
|
185
|
-
text: `✗ **Indexing Failed**\n\nError: ${status.error}`,
|
|
186
|
-
},
|
|
187
|
-
],
|
|
188
|
-
isError: true,
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (indexer.isReady()) {
|
|
193
|
-
const stats = await indexer.getStats();
|
|
194
|
-
return {
|
|
195
|
-
content: [
|
|
196
|
-
{
|
|
197
|
-
type: 'text',
|
|
198
|
-
text: `✓ **Index Ready**\n\n- Total items: ${stats?.totalDocuments || 0}\n- Unique terms: ${stats?.uniqueTerms || 0}\n- Status: Ready for search`,
|
|
199
|
-
},
|
|
200
|
-
],
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return {
|
|
205
|
-
content: [
|
|
206
|
-
{
|
|
207
|
-
type: 'text',
|
|
208
|
-
text: '⚠️ **Not Indexed**\n\nIndexing will start automatically on first search.',
|
|
209
|
-
},
|
|
210
|
-
],
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
);
|
|
214
|
-
}
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session Manager
|
|
3
|
-
* Manage chat sessions for headless mode
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
7
|
-
import { join } from 'node:path';
|
|
8
|
-
import { homedir } from 'node:os';
|
|
9
|
-
import type { ProviderId } from '../config/ai-config.js';
|
|
10
|
-
import type { Session } from '../types/session.types.js';
|
|
11
|
-
|
|
12
|
-
export type { Session } from '../types/session.types.js';
|
|
13
|
-
|
|
14
|
-
const SESSION_DIR = join(homedir(), '.sylphx', 'sessions');
|
|
15
|
-
const LAST_SESSION_FILE = join(SESSION_DIR, '.last-session');
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Ensure session directory exists
|
|
19
|
-
*/
|
|
20
|
-
async function ensureSessionDir(): Promise<void> {
|
|
21
|
-
await mkdir(SESSION_DIR, { recursive: true });
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Get session file path
|
|
26
|
-
*/
|
|
27
|
-
function getSessionPath(sessionId: string): string {
|
|
28
|
-
return join(SESSION_DIR, `${sessionId}.json`);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Create new session
|
|
33
|
-
*/
|
|
34
|
-
export async function createSession(provider: ProviderId, model: string): Promise<Session> {
|
|
35
|
-
await ensureSessionDir();
|
|
36
|
-
|
|
37
|
-
const session: Session = {
|
|
38
|
-
id: `session-${Date.now()}`,
|
|
39
|
-
provider,
|
|
40
|
-
model,
|
|
41
|
-
messages: [],
|
|
42
|
-
todos: [], // Initialize empty todos
|
|
43
|
-
nextTodoId: 1, // Start from 1
|
|
44
|
-
created: Date.now(),
|
|
45
|
-
updated: Date.now(),
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
await saveSession(session);
|
|
49
|
-
await setLastSession(session.id);
|
|
50
|
-
|
|
51
|
-
return session;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Save session to file
|
|
56
|
-
*/
|
|
57
|
-
export async function saveSession(session: Session): Promise<void> {
|
|
58
|
-
await ensureSessionDir();
|
|
59
|
-
// Create a new object with updated timestamp (don't mutate readonly session from Zustand)
|
|
60
|
-
const sessionToSave = {
|
|
61
|
-
...session,
|
|
62
|
-
updated: Date.now(),
|
|
63
|
-
};
|
|
64
|
-
const path = getSessionPath(session.id);
|
|
65
|
-
// Use compact JSON format for faster serialization and smaller file size
|
|
66
|
-
await writeFile(path, JSON.stringify(sessionToSave), 'utf8');
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Load session from file with migration support
|
|
71
|
-
* Automatically adds missing fields from newer schema versions
|
|
72
|
-
*/
|
|
73
|
-
export async function loadSession(sessionId: string): Promise<Session | null> {
|
|
74
|
-
try {
|
|
75
|
-
const path = getSessionPath(sessionId);
|
|
76
|
-
const content = await readFile(path, 'utf8');
|
|
77
|
-
const rawSession = JSON.parse(content) as any;
|
|
78
|
-
|
|
79
|
-
// Migration: Add todos/nextTodoId if missing
|
|
80
|
-
if (!rawSession.todos) {
|
|
81
|
-
rawSession.todos = [];
|
|
82
|
-
}
|
|
83
|
-
if (typeof rawSession.nextTodoId !== 'number') {
|
|
84
|
-
rawSession.nextTodoId = 1;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Migration: Normalize message content format
|
|
88
|
-
// Old: { content: string }
|
|
89
|
-
// New: { content: MessagePart[] }
|
|
90
|
-
if (Array.isArray(rawSession.messages)) {
|
|
91
|
-
rawSession.messages = rawSession.messages.map((msg: any) => {
|
|
92
|
-
if (typeof msg.content === 'string') {
|
|
93
|
-
return {
|
|
94
|
-
...msg,
|
|
95
|
-
content: [{ type: 'text', content: msg.content }],
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
return msg;
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return rawSession as Session;
|
|
103
|
-
} catch {
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Get last session ID
|
|
110
|
-
*/
|
|
111
|
-
export async function getLastSessionId(): Promise<string | null> {
|
|
112
|
-
try {
|
|
113
|
-
const content = await readFile(LAST_SESSION_FILE, 'utf8');
|
|
114
|
-
return content.trim();
|
|
115
|
-
} catch {
|
|
116
|
-
return null;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Set last session ID
|
|
122
|
-
*/
|
|
123
|
-
export async function setLastSession(sessionId: string): Promise<void> {
|
|
124
|
-
await ensureSessionDir();
|
|
125
|
-
await writeFile(LAST_SESSION_FILE, sessionId, 'utf8');
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Load last session
|
|
130
|
-
*/
|
|
131
|
-
export async function loadLastSession(): Promise<Session | null> {
|
|
132
|
-
const sessionId = await getLastSessionId();
|
|
133
|
-
if (!sessionId) return null;
|
|
134
|
-
return loadSession(sessionId);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Add message to session (in-memory helper for headless mode)
|
|
139
|
-
* Converts string content to MessagePart[] format
|
|
140
|
-
*/
|
|
141
|
-
export function addMessage(
|
|
142
|
-
session: Session,
|
|
143
|
-
role: 'user' | 'assistant',
|
|
144
|
-
content: string
|
|
145
|
-
): Session {
|
|
146
|
-
return {
|
|
147
|
-
...session,
|
|
148
|
-
messages: [
|
|
149
|
-
...session.messages,
|
|
150
|
-
{
|
|
151
|
-
role,
|
|
152
|
-
content: [{ type: 'text', content }], // Convert to MessagePart[]
|
|
153
|
-
timestamp: Date.now(),
|
|
154
|
-
},
|
|
155
|
-
],
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Clear session messages but keep metadata
|
|
161
|
-
*/
|
|
162
|
-
export function clearSessionMessages(session: Session): Session {
|
|
163
|
-
return {
|
|
164
|
-
...session,
|
|
165
|
-
messages: [],
|
|
166
|
-
updated: Date.now(),
|
|
167
|
-
};
|
|
168
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session Title Generation Utility
|
|
3
|
-
* Re-exports pure functions from feature and adds streaming functionality
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { createAIStream } from '../core/ai-sdk.js';
|
|
7
|
-
import type { ProviderId } from '../types/config.types.js';
|
|
8
|
-
|
|
9
|
-
// Re-export pure functions from feature
|
|
10
|
-
export {
|
|
11
|
-
generateSessionTitle,
|
|
12
|
-
formatSessionDisplay,
|
|
13
|
-
formatRelativeTime,
|
|
14
|
-
cleanTitle,
|
|
15
|
-
truncateTitle,
|
|
16
|
-
} from '../features/session/utils/title.js';
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Generate a session title using LLM with streaming
|
|
20
|
-
*/
|
|
21
|
-
export async function generateSessionTitleWithStreaming(
|
|
22
|
-
firstMessage: string,
|
|
23
|
-
provider: ProviderId,
|
|
24
|
-
modelName: string,
|
|
25
|
-
providerConfig: any,
|
|
26
|
-
onChunk: (chunk: string) => void
|
|
27
|
-
): Promise<string> {
|
|
28
|
-
if (!firstMessage || firstMessage.trim().length === 0) {
|
|
29
|
-
return 'New Chat';
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
try {
|
|
33
|
-
// Get the provider instance and create the model
|
|
34
|
-
const { getProvider } = await import('../providers/index.js');
|
|
35
|
-
const providerInstance = getProvider(provider);
|
|
36
|
-
const model = providerInstance.createClient(providerConfig, modelName);
|
|
37
|
-
|
|
38
|
-
const streamGenerator = createAIStream({
|
|
39
|
-
model,
|
|
40
|
-
messages: [
|
|
41
|
-
{
|
|
42
|
-
role: 'user',
|
|
43
|
-
content: `You need to generate a SHORT, DESCRIPTIVE title (maximum 50 characters) for a chat conversation.
|
|
44
|
-
|
|
45
|
-
User's first message: "${firstMessage}"
|
|
46
|
-
|
|
47
|
-
Requirements:
|
|
48
|
-
- Summarize the TOPIC or INTENT, don't just copy the message
|
|
49
|
-
- Be concise and descriptive
|
|
50
|
-
- Maximum 50 characters
|
|
51
|
-
- Output ONLY the title, nothing else
|
|
52
|
-
|
|
53
|
-
Examples:
|
|
54
|
-
- Message: "How do I implement authentication?" → Title: "Authentication Implementation"
|
|
55
|
-
- Message: "你好,请帮我修复这个 bug" → Title: "Bug 修复请求"
|
|
56
|
-
- Message: "Can you help me with React hooks?" → Title: "React Hooks Help"
|
|
57
|
-
|
|
58
|
-
Now generate the title:`,
|
|
59
|
-
},
|
|
60
|
-
],
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
let fullTitle = '';
|
|
64
|
-
|
|
65
|
-
// Iterate the async generator and stream to UI
|
|
66
|
-
for await (const chunk of streamGenerator) {
|
|
67
|
-
if (chunk.type === 'text-delta' && chunk.textDelta) {
|
|
68
|
-
fullTitle += chunk.textDelta;
|
|
69
|
-
onChunk(chunk.textDelta);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Clean up title
|
|
74
|
-
let cleaned = fullTitle.trim();
|
|
75
|
-
cleaned = cleaned.replace(/^["'「『]+|["'」』]+$/g, ''); // Remove quotes
|
|
76
|
-
cleaned = cleaned.replace(/^(Title:|标题:)\s*/i, ''); // Remove "Title:" prefix
|
|
77
|
-
cleaned = cleaned.replace(/\n+/g, ' '); // Replace newlines with spaces
|
|
78
|
-
cleaned = cleaned.trim();
|
|
79
|
-
|
|
80
|
-
// Return truncated if needed
|
|
81
|
-
return cleaned.length > 50 ? cleaned.substring(0, 50) + '...' : cleaned;
|
|
82
|
-
} catch (error) {
|
|
83
|
-
// Fallback to simple title generation on any error
|
|
84
|
-
return generateSessionTitle(firstMessage);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|