@sylphx/flow 1.0.1 → 1.0.3
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 +12 -0
- package/package.json +10 -9
- package/src/commands/codebase-command.ts +168 -0
- package/src/commands/flow-command.ts +1137 -0
- package/src/commands/flow-orchestrator.ts +296 -0
- package/src/commands/hook-command.ts +444 -0
- package/src/commands/init-command.ts +92 -0
- package/src/commands/init-core.ts +322 -0
- package/src/commands/knowledge-command.ts +161 -0
- package/src/commands/run-command.ts +120 -0
- package/src/components/benchmark-monitor.tsx +331 -0
- package/src/components/reindex-progress.tsx +261 -0
- package/src/composables/functional/index.ts +14 -0
- package/src/composables/functional/useEnvironment.ts +171 -0
- package/src/composables/functional/useFileSystem.ts +139 -0
- package/src/composables/index.ts +5 -0
- package/src/composables/useEnv.ts +13 -0
- package/src/composables/useRuntimeConfig.ts +27 -0
- package/src/composables/useTargetConfig.ts +45 -0
- package/src/config/ai-config.ts +376 -0
- package/src/config/constants.ts +35 -0
- package/src/config/index.ts +27 -0
- package/src/config/rules.ts +43 -0
- package/src/config/servers.ts +371 -0
- package/src/config/targets.ts +126 -0
- package/src/core/agent-loader.ts +141 -0
- package/src/core/agent-manager.ts +174 -0
- package/src/core/ai-sdk.ts +603 -0
- package/src/core/app-factory.ts +381 -0
- package/src/core/builtin-agents.ts +9 -0
- package/src/core/command-system.ts +550 -0
- package/src/core/config-system.ts +550 -0
- package/src/core/connection-pool.ts +390 -0
- package/src/core/di-container.ts +155 -0
- package/src/core/error-handling.ts +519 -0
- package/src/core/formatting/bytes.test.ts +115 -0
- package/src/core/formatting/bytes.ts +64 -0
- package/src/core/functional/async.ts +313 -0
- package/src/core/functional/either.ts +109 -0
- package/src/core/functional/error-handler.ts +135 -0
- package/src/core/functional/error-types.ts +311 -0
- package/src/core/functional/index.ts +19 -0
- package/src/core/functional/option.ts +142 -0
- package/src/core/functional/pipe.ts +189 -0
- package/src/core/functional/result.ts +204 -0
- package/src/core/functional/validation.ts +138 -0
- package/src/core/headless-display.ts +96 -0
- package/src/core/index.ts +6 -0
- package/src/core/installers/file-installer.ts +303 -0
- package/src/core/installers/mcp-installer.ts +213 -0
- package/src/core/interfaces/index.ts +22 -0
- package/src/core/interfaces/repository.interface.ts +91 -0
- package/src/core/interfaces/service.interface.ts +133 -0
- package/src/core/interfaces.ts +129 -0
- package/src/core/loop-controller.ts +200 -0
- package/src/core/result.ts +351 -0
- package/src/core/rule-loader.ts +147 -0
- package/src/core/rule-manager.ts +240 -0
- package/src/core/service-config.ts +252 -0
- package/src/core/session-service.ts +121 -0
- package/src/core/state-detector.ts +389 -0
- package/src/core/storage-factory.ts +115 -0
- package/src/core/stream-handler.ts +288 -0
- package/src/core/target-manager.ts +161 -0
- package/src/core/type-utils.ts +427 -0
- package/src/core/unified-storage.ts +456 -0
- package/src/core/upgrade-manager.ts +300 -0
- package/src/core/validation/limit.test.ts +155 -0
- package/src/core/validation/limit.ts +46 -0
- package/src/core/validation/query.test.ts +44 -0
- package/src/core/validation/query.ts +20 -0
- package/src/db/auto-migrate.ts +322 -0
- package/src/db/base-database-client.ts +144 -0
- package/src/db/cache-db.ts +218 -0
- package/src/db/cache-schema.ts +75 -0
- package/src/db/database.ts +70 -0
- package/src/db/index.ts +252 -0
- package/src/db/memory-db.ts +153 -0
- package/src/db/memory-schema.ts +29 -0
- package/src/db/schema.ts +289 -0
- package/src/db/session-repository.ts +733 -0
- package/src/domains/codebase/index.ts +5 -0
- package/src/domains/codebase/tools.ts +139 -0
- package/src/domains/index.ts +8 -0
- package/src/domains/knowledge/index.ts +10 -0
- package/src/domains/knowledge/resources.ts +537 -0
- package/src/domains/knowledge/tools.ts +174 -0
- package/src/domains/utilities/index.ts +6 -0
- package/src/domains/utilities/time/index.ts +5 -0
- package/src/domains/utilities/time/tools.ts +291 -0
- package/src/index.ts +211 -0
- package/src/services/agent-service.ts +273 -0
- package/src/services/claude-config-service.ts +252 -0
- package/src/services/config-service.ts +258 -0
- package/src/services/evaluation-service.ts +271 -0
- package/src/services/functional/evaluation-logic.ts +296 -0
- package/src/services/functional/file-processor.ts +273 -0
- package/src/services/functional/index.ts +12 -0
- package/src/services/index.ts +13 -0
- package/src/services/mcp-service.ts +432 -0
- package/src/services/memory.service.ts +476 -0
- package/src/services/search/base-indexer.ts +156 -0
- package/src/services/search/codebase-indexer-types.ts +38 -0
- package/src/services/search/codebase-indexer.ts +647 -0
- package/src/services/search/embeddings-provider.ts +455 -0
- package/src/services/search/embeddings.ts +316 -0
- package/src/services/search/functional-indexer.ts +323 -0
- package/src/services/search/index.ts +27 -0
- package/src/services/search/indexer.ts +380 -0
- package/src/services/search/knowledge-indexer.ts +422 -0
- package/src/services/search/semantic-search.ts +244 -0
- package/src/services/search/tfidf.ts +559 -0
- package/src/services/search/unified-search-service.ts +888 -0
- package/src/services/smart-config-service.ts +385 -0
- package/src/services/storage/cache-storage.ts +487 -0
- package/src/services/storage/drizzle-storage.ts +581 -0
- package/src/services/storage/index.ts +15 -0
- package/src/services/storage/lancedb-vector-storage.ts +494 -0
- package/src/services/storage/memory-storage.ts +268 -0
- package/src/services/storage/separated-storage.ts +467 -0
- package/src/services/storage/vector-storage.ts +13 -0
- package/src/shared/agents/index.ts +63 -0
- package/src/shared/files/index.ts +99 -0
- package/src/shared/index.ts +32 -0
- package/src/shared/logging/index.ts +24 -0
- package/src/shared/processing/index.ts +153 -0
- package/src/shared/types/index.ts +25 -0
- package/src/targets/claude-code.ts +574 -0
- package/src/targets/functional/claude-code-logic.ts +185 -0
- package/src/targets/functional/index.ts +6 -0
- package/src/targets/opencode.ts +529 -0
- package/src/types/agent.types.ts +32 -0
- package/src/types/api/batch.ts +108 -0
- package/src/types/api/errors.ts +118 -0
- package/src/types/api/index.ts +55 -0
- package/src/types/api/requests.ts +76 -0
- package/src/types/api/responses.ts +180 -0
- package/src/types/api/websockets.ts +85 -0
- package/src/types/api.types.ts +9 -0
- package/src/types/benchmark.ts +49 -0
- package/src/types/cli.types.ts +87 -0
- package/src/types/common.types.ts +35 -0
- package/src/types/database.types.ts +510 -0
- package/src/types/mcp-config.types.ts +448 -0
- package/src/types/mcp.types.ts +69 -0
- package/src/types/memory-types.ts +63 -0
- package/src/types/provider.types.ts +28 -0
- package/src/types/rule.types.ts +24 -0
- package/src/types/session.types.ts +214 -0
- package/src/types/target-config.types.ts +295 -0
- package/src/types/target.types.ts +140 -0
- package/src/types/todo.types.ts +25 -0
- package/src/types.ts +40 -0
- package/src/utils/advanced-tokenizer.ts +191 -0
- package/src/utils/agent-enhancer.ts +114 -0
- package/src/utils/ai-model-fetcher.ts +19 -0
- package/src/utils/async-file-operations.ts +516 -0
- package/src/utils/audio-player.ts +345 -0
- package/src/utils/cli-output.ts +266 -0
- package/src/utils/codebase-helpers.ts +211 -0
- package/src/utils/console-ui.ts +79 -0
- package/src/utils/database-errors.ts +140 -0
- package/src/utils/debug-logger.ts +49 -0
- package/src/utils/error-handler.ts +53 -0
- package/src/utils/file-operations.ts +310 -0
- package/src/utils/file-scanner.ts +259 -0
- package/src/utils/functional/array.ts +355 -0
- package/src/utils/functional/index.ts +15 -0
- package/src/utils/functional/object.ts +279 -0
- package/src/utils/functional/string.ts +281 -0
- package/src/utils/functional.ts +543 -0
- package/src/utils/help.ts +20 -0
- package/src/utils/immutable-cache.ts +106 -0
- package/src/utils/index.ts +78 -0
- package/src/utils/jsonc.ts +158 -0
- package/src/utils/logger.ts +396 -0
- package/src/utils/mcp-config.ts +249 -0
- package/src/utils/memory-tui.ts +414 -0
- package/src/utils/models-dev.ts +91 -0
- package/src/utils/notifications.ts +169 -0
- package/src/utils/object-utils.ts +51 -0
- package/src/utils/parallel-operations.ts +487 -0
- package/src/utils/paths.ts +143 -0
- package/src/utils/process-manager.ts +155 -0
- package/src/utils/prompts.ts +120 -0
- package/src/utils/search-tool-builder.ts +214 -0
- package/src/utils/secret-utils.ts +179 -0
- package/src/utils/security.ts +537 -0
- package/src/utils/session-manager.ts +168 -0
- package/src/utils/session-title.ts +87 -0
- package/src/utils/settings.ts +182 -0
- package/src/utils/simplified-errors.ts +410 -0
- package/src/utils/sync-utils.ts +159 -0
- package/src/utils/target-config.ts +570 -0
- package/src/utils/target-utils.ts +394 -0
- package/src/utils/template-engine.ts +94 -0
- package/src/utils/test-audio.ts +71 -0
- package/src/utils/todo-context.ts +46 -0
- package/src/utils/token-counter.ts +288 -0
- package/dist/index.d.ts +0 -10
- package/dist/index.js +0 -59554
- package/dist/lancedb.linux-x64-gnu-b7f0jgsz.node +0 -0
- package/dist/lancedb.linux-x64-musl-tgcv22rx.node +0 -0
- package/dist/shared/chunk-25dwp0dp.js +0 -89
- package/dist/shared/chunk-3pjb6063.js +0 -208
- package/dist/shared/chunk-4d6ydpw7.js +0 -2854
- package/dist/shared/chunk-4wjcadjk.js +0 -225
- package/dist/shared/chunk-5j4w74t6.js +0 -30
- package/dist/shared/chunk-5j8m3dh3.js +0 -58
- package/dist/shared/chunk-5thh3qem.js +0 -91
- package/dist/shared/chunk-6g9xy73m.js +0 -252
- package/dist/shared/chunk-7eq34c42.js +0 -23
- package/dist/shared/chunk-c2gwgx3r.js +0 -115
- package/dist/shared/chunk-cjd3mk4c.js +0 -1320
- package/dist/shared/chunk-g5cv6703.js +0 -368
- package/dist/shared/chunk-hpkhykhq.js +0 -574
- package/dist/shared/chunk-m2322pdk.js +0 -122
- package/dist/shared/chunk-nd5fdvaq.js +0 -26
- package/dist/shared/chunk-pgd3m6zf.js +0 -108
- package/dist/shared/chunk-qk8n91hw.js +0 -494
- package/dist/shared/chunk-rkkn8szp.js +0 -16855
- package/dist/shared/chunk-t16rfxh0.js +0 -61
- package/dist/shared/chunk-t4fbfa5v.js +0 -19
- package/dist/shared/chunk-t77h86w6.js +0 -276
- package/dist/shared/chunk-v0ez4aef.js +0 -71
- package/dist/shared/chunk-v29j2r3s.js +0 -32051
- package/dist/shared/chunk-vfbc6ew5.js +0 -765
- package/dist/shared/chunk-vmeqwm1c.js +0 -204
- package/dist/shared/chunk-x66eh37x.js +0 -137
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProcessManager interface for managing child processes
|
|
3
|
+
*/
|
|
4
|
+
export interface ProcessManager {
|
|
5
|
+
trackChildProcess(childProcess: any): void;
|
|
6
|
+
killAllProcesses(): Promise<void>;
|
|
7
|
+
// Internal for testing - exposed for tests to access state
|
|
8
|
+
readonly _state?: ProcessManagerState;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Internal state for ProcessManager
|
|
13
|
+
*/
|
|
14
|
+
interface ProcessManagerState {
|
|
15
|
+
readonly childProcesses: Set<any>;
|
|
16
|
+
isShuttingDown: boolean;
|
|
17
|
+
readonly signalHandlers: Map<string, (...args: any[]) => void>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create a ProcessManager instance
|
|
22
|
+
*/
|
|
23
|
+
export function createProcessManager(): ProcessManager {
|
|
24
|
+
const state: ProcessManagerState = {
|
|
25
|
+
childProcesses: new Set(),
|
|
26
|
+
isShuttingDown: false,
|
|
27
|
+
signalHandlers: new Map(),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Cleanup signal handlers and reset state (for testing)
|
|
32
|
+
*/
|
|
33
|
+
const cleanup = (): void => {
|
|
34
|
+
// Remove signal handlers
|
|
35
|
+
for (const [signal, handler] of state.signalHandlers.entries()) {
|
|
36
|
+
process.removeListener(signal as any, handler);
|
|
37
|
+
}
|
|
38
|
+
state.signalHandlers.clear();
|
|
39
|
+
|
|
40
|
+
// Clear child processes
|
|
41
|
+
state.childProcesses.clear();
|
|
42
|
+
state.isShuttingDown = false;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Kill all tracked child processes
|
|
47
|
+
*/
|
|
48
|
+
const killAllProcesses = async (): Promise<void> => {
|
|
49
|
+
const killPromises = Array.from(state.childProcesses).map(async (childProcess) => {
|
|
50
|
+
try {
|
|
51
|
+
if (childProcess && !childProcess.killed) {
|
|
52
|
+
childProcess.kill('SIGTERM');
|
|
53
|
+
|
|
54
|
+
// Force kill if it doesn't stop after 2 seconds
|
|
55
|
+
setTimeout(() => {
|
|
56
|
+
if (!childProcess.killed) {
|
|
57
|
+
childProcess.kill('SIGKILL');
|
|
58
|
+
}
|
|
59
|
+
}, 2000);
|
|
60
|
+
}
|
|
61
|
+
} catch (_error) {
|
|
62
|
+
// Silently handle kill errors
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await Promise.all(killPromises);
|
|
67
|
+
state.childProcesses.clear();
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Setup signal handlers for graceful shutdown
|
|
72
|
+
*/
|
|
73
|
+
const setupSignalHandlers = (): void => {
|
|
74
|
+
const shutdown = async (_signal: string) => {
|
|
75
|
+
if (state.isShuttingDown) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
state.isShuttingDown = true;
|
|
79
|
+
|
|
80
|
+
await killAllProcesses();
|
|
81
|
+
process.exit(0);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Create and store signal handlers
|
|
85
|
+
const sigintHandler = () => shutdown('SIGINT');
|
|
86
|
+
const sigtermHandler = () => shutdown('SIGTERM');
|
|
87
|
+
const sighupHandler = () => shutdown('SIGHUP');
|
|
88
|
+
|
|
89
|
+
state.signalHandlers.set('SIGINT', sigintHandler);
|
|
90
|
+
state.signalHandlers.set('SIGTERM', sigtermHandler);
|
|
91
|
+
state.signalHandlers.set('SIGHUP', sighupHandler);
|
|
92
|
+
|
|
93
|
+
// Handle termination signals
|
|
94
|
+
process.on('SIGINT', sigintHandler);
|
|
95
|
+
process.on('SIGTERM', sigtermHandler);
|
|
96
|
+
process.on('SIGHUP', sighupHandler);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Track a child process for cleanup on shutdown
|
|
101
|
+
*/
|
|
102
|
+
const trackChildProcess = (childProcess: any): void => {
|
|
103
|
+
state.childProcesses.add(childProcess);
|
|
104
|
+
|
|
105
|
+
// Remove from tracking when process exits
|
|
106
|
+
childProcess.on('exit', () => {
|
|
107
|
+
state.childProcesses.delete(childProcess);
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Setup signal handlers when instance is created
|
|
112
|
+
setupSignalHandlers();
|
|
113
|
+
|
|
114
|
+
const manager: ProcessManager & { _cleanup?: () => void; _state?: ProcessManagerState } = {
|
|
115
|
+
trackChildProcess,
|
|
116
|
+
killAllProcesses,
|
|
117
|
+
_state: state, // Expose state for testing
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// Expose cleanup for testing
|
|
121
|
+
(manager as any)._cleanup = cleanup;
|
|
122
|
+
|
|
123
|
+
return manager;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Singleton instance for backward compatibility
|
|
127
|
+
let _processManagerInstance: ProcessManager | null = null;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get the singleton ProcessManager instance
|
|
131
|
+
* @deprecated Use createProcessManager() for new code
|
|
132
|
+
*/
|
|
133
|
+
export class ProcessManager {
|
|
134
|
+
static getInstance(): ProcessManager {
|
|
135
|
+
if (!_processManagerInstance) {
|
|
136
|
+
_processManagerInstance = createProcessManager();
|
|
137
|
+
}
|
|
138
|
+
return _processManagerInstance;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Reset singleton instance (for testing only)
|
|
143
|
+
* @internal
|
|
144
|
+
*/
|
|
145
|
+
static resetInstance(): void {
|
|
146
|
+
if (_processManagerInstance) {
|
|
147
|
+
// Call cleanup if available
|
|
148
|
+
const cleanup = (_processManagerInstance as any)._cleanup;
|
|
149
|
+
if (cleanup) {
|
|
150
|
+
cleanup();
|
|
151
|
+
}
|
|
152
|
+
_processManagerInstance = null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Secret management utilities for handling {file:} syntax and .secrets directory
|
|
6
|
+
*/
|
|
7
|
+
export const secretUtils = {
|
|
8
|
+
/**
|
|
9
|
+
* Get the secrets directory path
|
|
10
|
+
*/
|
|
11
|
+
getSecretsDir(cwd: string): string {
|
|
12
|
+
return path.join(cwd, '.secrets');
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Ensure secrets directory exists
|
|
17
|
+
*/
|
|
18
|
+
async ensureSecretsDir(cwd: string): Promise<void> {
|
|
19
|
+
const secretsDir = secretUtils.getSecretsDir(cwd);
|
|
20
|
+
await fs.mkdir(secretsDir, { recursive: true });
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Write a secret to a file in .secrets directory
|
|
25
|
+
*/
|
|
26
|
+
async writeSecret(cwd: string, key: string, value: string): Promise<string> {
|
|
27
|
+
await secretUtils.ensureSecretsDir(cwd);
|
|
28
|
+
|
|
29
|
+
const secretFile = path.join('.secrets', key);
|
|
30
|
+
const secretPath = path.join(cwd, secretFile);
|
|
31
|
+
|
|
32
|
+
await fs.writeFile(secretPath, value.trim(), 'utf8');
|
|
33
|
+
return secretFile;
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Read a secret from a file
|
|
38
|
+
*/
|
|
39
|
+
async readSecret(cwd: string, secretFile: string): Promise<string> {
|
|
40
|
+
const secretPath = path.resolve(cwd, secretFile);
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
return await fs.readFile(secretPath, 'utf8');
|
|
44
|
+
} catch (error) {
|
|
45
|
+
throw new Error(`Failed to read secret file ${secretFile}: ${error}`);
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Convert a secret key to {file:} reference format
|
|
51
|
+
*/
|
|
52
|
+
toFileReference(key: string): string {
|
|
53
|
+
return `{file:.secrets/${key}}`;
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if a value is a file reference
|
|
58
|
+
*/
|
|
59
|
+
isFileReference(value: string): boolean {
|
|
60
|
+
return value.startsWith('{file:') && value.endsWith('}');
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Extract file path from {file:} reference
|
|
65
|
+
*/
|
|
66
|
+
extractFilePath(reference: string): string {
|
|
67
|
+
if (!secretUtils.isFileReference(reference)) {
|
|
68
|
+
throw new Error(`Invalid file reference: ${reference}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return reference.slice(6, -1); // Remove {file: prefix and } suffix
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolve file references in an object recursively
|
|
76
|
+
*/
|
|
77
|
+
async resolveFileReferences(cwd: string, obj: any): Promise<any> {
|
|
78
|
+
if (typeof obj === 'string' && secretUtils.isFileReference(obj)) {
|
|
79
|
+
const filePath = secretUtils.extractFilePath(obj);
|
|
80
|
+
return await secretUtils.readSecret(cwd, filePath);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (Array.isArray(obj)) {
|
|
84
|
+
return Promise.all(obj.map((item) => secretUtils.resolveFileReferences(cwd, item)));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (obj && typeof obj === 'object') {
|
|
88
|
+
const resolved: any = {};
|
|
89
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
90
|
+
resolved[key] = await secretUtils.resolveFileReferences(cwd, value);
|
|
91
|
+
}
|
|
92
|
+
return resolved;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return obj;
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Convert secret values to file references in environment variables
|
|
100
|
+
*/
|
|
101
|
+
async convertSecretsToFileReferences(
|
|
102
|
+
cwd: string,
|
|
103
|
+
envVars: Record<string, string>
|
|
104
|
+
): Promise<Record<string, string>> {
|
|
105
|
+
const result: Record<string, string> = {};
|
|
106
|
+
|
|
107
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
108
|
+
if (value && !secretUtils.isFileReference(value)) {
|
|
109
|
+
// Write the secret to a file and create file reference
|
|
110
|
+
const _secretFile = await secretUtils.writeSecret(cwd, key, value);
|
|
111
|
+
result[key] = secretUtils.toFileReference(key);
|
|
112
|
+
} else {
|
|
113
|
+
result[key] = value;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return result;
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Save multiple secrets to files
|
|
122
|
+
*/
|
|
123
|
+
async saveSecrets(cwd: string, secrets: Record<string, string>): Promise<void> {
|
|
124
|
+
for (const [key, value] of Object.entries(secrets)) {
|
|
125
|
+
await secretUtils.writeSecret(cwd, key, value);
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Load all secrets from .secrets directory
|
|
131
|
+
*/
|
|
132
|
+
async loadSecrets(cwd: string): Promise<Record<string, string>> {
|
|
133
|
+
const secretsDir = secretUtils.getSecretsDir(cwd);
|
|
134
|
+
const secrets: Record<string, string> = {};
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const files = await fs.readdir(secretsDir);
|
|
138
|
+
|
|
139
|
+
for (const file of files) {
|
|
140
|
+
const filePath = path.join(secretsDir, file);
|
|
141
|
+
const stat = await fs.stat(filePath);
|
|
142
|
+
|
|
143
|
+
if (stat.isFile()) {
|
|
144
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
145
|
+
secrets[file] = content.trim();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} catch (_error) {
|
|
149
|
+
// Directory doesn't exist or can't be read
|
|
150
|
+
// Return empty secrets object
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return secrets;
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Add .secrets to .gitignore if not already present
|
|
158
|
+
*/
|
|
159
|
+
async addToGitignore(cwd: string): Promise<void> {
|
|
160
|
+
const gitignorePath = path.join(cwd, '.gitignore');
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
let gitignoreContent = '';
|
|
164
|
+
try {
|
|
165
|
+
gitignoreContent = await fs.readFile(gitignorePath, 'utf8');
|
|
166
|
+
} catch {
|
|
167
|
+
// .gitignore doesn't exist, create it
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const lines = gitignoreContent.split('\n').map((line) => line.trim());
|
|
171
|
+
if (!lines.includes('.secrets') && !lines.includes('.secrets/')) {
|
|
172
|
+
gitignoreContent += `${gitignoreContent && !gitignoreContent.endsWith('\n') ? '\n' : ''}.secrets/\n`;
|
|
173
|
+
await fs.writeFile(gitignorePath, gitignoreContent, 'utf8');
|
|
174
|
+
}
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.warn('Warning: Could not update .gitignore:', error);
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
};
|