@tyvm/knowhow 0.0.54 → 0.0.56
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/docs/input-queue-manager.md +142 -0
- package/docs/multi-worker-management.md +142 -0
- package/package.json +1 -1
- package/scripts/README.md +119 -0
- package/scripts/restore_keys.sh +59 -0
- package/scripts/unset_keys.sh +60 -0
- package/src/agents/base/base.ts +2 -2
- package/src/agents/tools/askHuman.ts +2 -0
- package/src/agents/tools/startAgentTask.ts +2 -2
- package/src/ai.ts +3 -1
- package/src/chat/CliChatService.ts +2 -2
- package/src/chat/modules/AgentModule.ts +25 -2
- package/src/chat-old.ts +2 -2
- package/src/cli.ts +56 -3
- package/src/clients/anthropic.ts +7 -5
- package/src/clients/knowhow.ts +2 -2
- package/src/clients/openai.ts +5 -0
- package/src/index.ts +6 -6
- package/src/microphone.ts +12 -4
- package/src/services/DockerService.ts +473 -0
- package/src/services/KnowhowClient.ts +4 -1
- package/src/services/index.ts +5 -1
- package/src/types.ts +7 -0
- package/src/utils/InputQueueManager.ts +324 -0
- package/src/utils/index.ts +5 -152
- package/src/worker.ts +158 -9
- package/src/workerRegistry.ts +152 -0
- package/tests/clients/AIClient.test.ts +177 -92
- package/tests/manual/test-concurrent-ask.ts +43 -0
- package/tests/services/DockerService.test.ts +24 -0
- package/tests/unit/input-queue.test.ts +80 -0
- package/ts_build/package.json +1 -1
- package/ts_build/src/agents/base/base.js +2 -2
- package/ts_build/src/agents/tools/askHuman.d.ts +1 -1
- package/ts_build/src/agents/tools/askHuman.js.map +1 -1
- package/ts_build/src/agents/tools/startAgentTask.js +2 -1
- package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
- package/ts_build/src/ai.js +3 -1
- package/ts_build/src/ai.js.map +1 -1
- package/ts_build/src/chat/CliChatService.js +1 -1
- package/ts_build/src/chat/CliChatService.js.map +1 -1
- package/ts_build/src/chat/modules/AgentModule.js +11 -1
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/chat-old.js +1 -1
- package/ts_build/src/chat-old.js.map +1 -1
- package/ts_build/src/cli.js +46 -3
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/clients/anthropic.js +7 -5
- package/ts_build/src/clients/anthropic.js.map +1 -1
- package/ts_build/src/clients/knowhow.js +1 -1
- package/ts_build/src/clients/knowhow.js.map +1 -1
- package/ts_build/src/clients/openai.js +5 -0
- package/ts_build/src/clients/openai.js.map +1 -1
- package/ts_build/src/dockerWorker.d.ts +22 -0
- package/ts_build/src/dockerWorker.js +210 -0
- package/ts_build/src/dockerWorker.js.map +1 -0
- package/ts_build/src/index.js +4 -4
- package/ts_build/src/index.js.map +1 -1
- package/ts_build/src/microphone.js +8 -3
- package/ts_build/src/microphone.js.map +1 -1
- package/ts_build/src/services/DockerService.d.ts +26 -0
- package/ts_build/src/services/DockerService.js +363 -0
- package/ts_build/src/services/DockerService.js.map +1 -0
- package/ts_build/src/services/KnowhowClient.d.ts +1 -1
- package/ts_build/src/services/KnowhowClient.js +1 -1
- package/ts_build/src/services/KnowhowClient.js.map +1 -1
- package/ts_build/src/services/index.d.ts +3 -0
- package/ts_build/src/services/index.js +4 -1
- package/ts_build/src/services/index.js.map +1 -1
- package/ts_build/src/types.d.ts +5 -0
- package/ts_build/src/types.js +4 -0
- package/ts_build/src/types.js.map +1 -1
- package/ts_build/src/utils/InputQueueManager.d.ts +19 -0
- package/ts_build/src/utils/InputQueueManager.js +234 -0
- package/ts_build/src/utils/InputQueueManager.js.map +1 -0
- package/ts_build/src/utils/index.d.ts +1 -3
- package/ts_build/src/utils/index.js +4 -114
- package/ts_build/src/utils/index.js.map +1 -1
- package/ts_build/src/worker-entrypoint.d.ts +2 -0
- package/ts_build/src/worker-entrypoint.js +39 -0
- package/ts_build/src/worker-entrypoint.js.map +1 -0
- package/ts_build/src/worker.d.ts +7 -1
- package/ts_build/src/worker.js +117 -9
- package/ts_build/src/worker.js.map +1 -1
- package/ts_build/src/workerRegistry.d.ts +11 -0
- package/ts_build/src/workerRegistry.js +143 -0
- package/ts_build/src/workerRegistry.js.map +1 -0
- package/ts_build/tests/clients/AIClient.test.js +88 -42
- package/ts_build/tests/clients/AIClient.test.js.map +1 -1
- package/ts_build/tests/manual/test-concurrent-ask.d.ts +1 -0
- package/ts_build/tests/manual/test-concurrent-ask.js +22 -0
- package/ts_build/tests/manual/test-concurrent-ask.js.map +1 -0
- package/ts_build/tests/services/DockerService.test.d.ts +1 -0
- package/ts_build/tests/services/DockerService.test.js +22 -0
- package/ts_build/tests/services/DockerService.test.js.map +1 -0
- package/ts_build/tests/unit/input-queue.test.d.ts +1 -0
- package/ts_build/tests/unit/input-queue.test.js +32 -0
- package/ts_build/tests/unit/input-queue.test.js.map +1 -0
package/src/services/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { McpService } from "./Mcp";
|
|
|
9
9
|
import { S3Service } from "./S3";
|
|
10
10
|
import { ToolsService } from "./Tools";
|
|
11
11
|
import { PluginService } from "../plugins/plugins";
|
|
12
|
+
import { DockerService } from "./DockerService";
|
|
12
13
|
|
|
13
14
|
export * from "./AgentService";
|
|
14
15
|
export * from "./EventService";
|
|
@@ -18,6 +19,7 @@ export * from "./S3";
|
|
|
18
19
|
export * from "./Tools";
|
|
19
20
|
export * as MCP from "./Mcp";
|
|
20
21
|
export * from "./EmbeddingService";
|
|
22
|
+
export * from "./DockerService";
|
|
21
23
|
export { Clients } from "../clients";
|
|
22
24
|
|
|
23
25
|
let Singletons = {} as {
|
|
@@ -28,6 +30,7 @@ let Singletons = {} as {
|
|
|
28
30
|
GitHub: GitHubService;
|
|
29
31
|
Mcp: McpService;
|
|
30
32
|
AwsS3: S3Service;
|
|
33
|
+
Docker: DockerService;
|
|
31
34
|
knowhowApiClient: KnowhowSimpleClient;
|
|
32
35
|
Plugins: PluginService;
|
|
33
36
|
Clients: AIClient;
|
|
@@ -51,6 +54,7 @@ export const services = (): typeof Singletons => {
|
|
|
51
54
|
Agents,
|
|
52
55
|
AwsS3: new S3Service(),
|
|
53
56
|
Clients,
|
|
57
|
+
Docker: new DockerService(),
|
|
54
58
|
Downloader,
|
|
55
59
|
Events,
|
|
56
60
|
Flags: new FlagsService(),
|
|
@@ -58,7 +62,7 @@ export const services = (): typeof Singletons => {
|
|
|
58
62
|
Mcp: new McpService(),
|
|
59
63
|
Plugins,
|
|
60
64
|
Tools,
|
|
61
|
-
knowhowApiClient: new KnowhowSimpleClient(
|
|
65
|
+
knowhowApiClient: new KnowhowSimpleClient(),
|
|
62
66
|
};
|
|
63
67
|
|
|
64
68
|
Singletons.Tools.setContext({
|
package/src/types.ts
CHANGED
|
@@ -63,6 +63,9 @@ export type Config = {
|
|
|
63
63
|
|
|
64
64
|
worker?: {
|
|
65
65
|
allowedTools?: string[];
|
|
66
|
+
sandbox?: boolean;
|
|
67
|
+
volumes?: string[];
|
|
68
|
+
envFile?: string;
|
|
66
69
|
};
|
|
67
70
|
};
|
|
68
71
|
|
|
@@ -138,6 +141,7 @@ export type ChatInteraction = {
|
|
|
138
141
|
|
|
139
142
|
export const Models = {
|
|
140
143
|
anthropic: {
|
|
144
|
+
Opus4_5: "claude-opus-4-5-20251101",
|
|
141
145
|
Opus4: "claude-opus-4-20250514",
|
|
142
146
|
Opus4_1: "claude-opus-4-1-20250805",
|
|
143
147
|
Sonnet4_5: "claude-sonnet-4-5-20250929",
|
|
@@ -162,6 +166,7 @@ export const Models = {
|
|
|
162
166
|
Grok2Vision1212: "grok-2-vision-1212",
|
|
163
167
|
},
|
|
164
168
|
openai: {
|
|
169
|
+
GPT_5_2: "gpt-5.2",
|
|
165
170
|
GPT_5_1: "gpt-5.1",
|
|
166
171
|
GPT_5: "gpt-5",
|
|
167
172
|
GPT_5_Mini: "gpt-5-mini",
|
|
@@ -231,6 +236,8 @@ export const OpenAiReasoningModels = [
|
|
|
231
236
|
Models.openai.GPT_5,
|
|
232
237
|
Models.openai.GPT_5_Mini,
|
|
233
238
|
Models.openai.GPT_5_Nano,
|
|
239
|
+
Models.openai.GPT_5_1,
|
|
240
|
+
Models.openai.GPT_5_2,
|
|
234
241
|
];
|
|
235
242
|
|
|
236
243
|
export const OpenAiEmbeddingModels = [
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import readline from "node:readline";
|
|
2
|
+
|
|
3
|
+
export const askHistory: string[] = [];
|
|
4
|
+
|
|
5
|
+
type AskOptions = {
|
|
6
|
+
question: string;
|
|
7
|
+
options?: string[];
|
|
8
|
+
history?: string[];
|
|
9
|
+
resolve: (value: string) => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export class InputQueueManager {
|
|
13
|
+
private stack: AskOptions[] = [];
|
|
14
|
+
private rl: readline.Interface | null = null;
|
|
15
|
+
|
|
16
|
+
// We keep one “live” buffer shared across stacked questions
|
|
17
|
+
// (so typing is preserved when questions change)
|
|
18
|
+
private currentLine = "";
|
|
19
|
+
|
|
20
|
+
// History navigation state (custom: global askHistory + per-question history)
|
|
21
|
+
private historyIndex = -1;
|
|
22
|
+
private savedLineBeforeHistory = "";
|
|
23
|
+
|
|
24
|
+
private ensureRl(): readline.Interface {
|
|
25
|
+
if (this.rl) return this.rl;
|
|
26
|
+
|
|
27
|
+
this.rl = readline.createInterface({
|
|
28
|
+
input: process.stdin,
|
|
29
|
+
output: process.stdout,
|
|
30
|
+
terminal: true,
|
|
31
|
+
historySize: 500,
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Use readline's built-in completion system so Tab does NOT insert a literal tab.
|
|
35
|
+
*/
|
|
36
|
+
completer: (line: string) => {
|
|
37
|
+
const current = this.peek();
|
|
38
|
+
const opts = current?.options ?? [];
|
|
39
|
+
if (opts.length === 0) return [[], line];
|
|
40
|
+
|
|
41
|
+
// Identify the "word" at the end of the line that we want to complete
|
|
42
|
+
// (default readline behavior is word-based completion)
|
|
43
|
+
const lastSpace = Math.max(
|
|
44
|
+
line.lastIndexOf(" "),
|
|
45
|
+
line.lastIndexOf("\t")
|
|
46
|
+
);
|
|
47
|
+
const word = line.slice(lastSpace + 1); // the token to complete
|
|
48
|
+
|
|
49
|
+
const hits = opts.filter((c) => c.startsWith(word));
|
|
50
|
+
|
|
51
|
+
// Return [matches, wordToReplace]
|
|
52
|
+
// Readline will replace `word` with the selected match (or extend if unique)
|
|
53
|
+
return [hits, word];
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// When user presses Enter, resolve ONLY the top question
|
|
58
|
+
this.rl.on("line", (line) => {
|
|
59
|
+
const current = this.peek();
|
|
60
|
+
if (!current) return;
|
|
61
|
+
|
|
62
|
+
// IMPORTANT: do not allow embedded newlines in history / answers
|
|
63
|
+
const answer = this.sanitizeHistoryEntry(line);
|
|
64
|
+
|
|
65
|
+
// Pop & resolve current question
|
|
66
|
+
const resolved = this.stack.pop();
|
|
67
|
+
resolved?.resolve(answer);
|
|
68
|
+
|
|
69
|
+
// Add to global history
|
|
70
|
+
if (answer && !askHistory.includes(answer)) {
|
|
71
|
+
askHistory.push(answer);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Reset preserved buffer + history nav state for the next question
|
|
75
|
+
this.currentLine = "";
|
|
76
|
+
this.historyIndex = -1;
|
|
77
|
+
this.savedLineBeforeHistory = "";
|
|
78
|
+
|
|
79
|
+
// Update prompt for next stacked question (if any)
|
|
80
|
+
this.renderTopOrClose();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Handle Ctrl+C (readline SIGINT)
|
|
84
|
+
this.rl.on("SIGINT", () => {
|
|
85
|
+
// If there’s an active question, cancel it (like Esc)
|
|
86
|
+
if (this.stack.length > 0) {
|
|
87
|
+
const cancelled = this.stack.pop();
|
|
88
|
+
cancelled?.resolve("");
|
|
89
|
+
this.currentLine = "";
|
|
90
|
+
this.historyIndex = -1;
|
|
91
|
+
this.savedLineBeforeHistory = "";
|
|
92
|
+
this.renderTopOrClose();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
// Otherwise exit
|
|
96
|
+
this.close();
|
|
97
|
+
process.exit(0);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Capture keypresses for ESC + history nav while still using readline.
|
|
101
|
+
// Tab is handled by rl completer.
|
|
102
|
+
readline.emitKeypressEvents(process.stdin);
|
|
103
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
104
|
+
|
|
105
|
+
process.stdin.on("keypress", (_str, key) => {
|
|
106
|
+
// Handle Ctrl+C in raw mode (some terminals deliver this here instead of SIGINT)
|
|
107
|
+
if (key?.ctrl && key?.name === "c") {
|
|
108
|
+
if (this.stack.length > 0) {
|
|
109
|
+
const cancelled = this.stack.pop();
|
|
110
|
+
cancelled?.resolve("");
|
|
111
|
+
this.currentLine = "";
|
|
112
|
+
}
|
|
113
|
+
this.close();
|
|
114
|
+
process.exit(0);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// If RL is closed or nothing to ask, ignore
|
|
118
|
+
if (!this.rl || this.stack.length === 0) return;
|
|
119
|
+
|
|
120
|
+
// Keep our buffer in sync with readline’s live line
|
|
121
|
+
this.syncFromReadline();
|
|
122
|
+
|
|
123
|
+
// Any "real typing" should exit history mode
|
|
124
|
+
// (we'll treat left/right as not exiting; you can tweak)
|
|
125
|
+
const exitsHistoryMode =
|
|
126
|
+
key &&
|
|
127
|
+
(key.name === "return" ||
|
|
128
|
+
key.name === "enter" ||
|
|
129
|
+
key.name === "backspace" ||
|
|
130
|
+
(key.sequence &&
|
|
131
|
+
key.sequence.length === 1 &&
|
|
132
|
+
!key.ctrl &&
|
|
133
|
+
!key.meta));
|
|
134
|
+
if (exitsHistoryMode && this.historyIndex !== -1) {
|
|
135
|
+
this.historyIndex = -1;
|
|
136
|
+
this.savedLineBeforeHistory = "";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (key?.name === "escape") {
|
|
140
|
+
// Cancel only the current (top) question
|
|
141
|
+
const cancelled = this.stack.pop();
|
|
142
|
+
cancelled?.resolve("");
|
|
143
|
+
|
|
144
|
+
this.currentLine = "";
|
|
145
|
+
this.historyIndex = -1;
|
|
146
|
+
this.savedLineBeforeHistory = "";
|
|
147
|
+
|
|
148
|
+
// clear the current input in readline and redraw
|
|
149
|
+
this.replaceLine("");
|
|
150
|
+
this.renderTopOrClose();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Custom Up/Down history: global askHistory + per-question history
|
|
155
|
+
if (key?.name === "up") {
|
|
156
|
+
const fullHistory = this.getFullHistory();
|
|
157
|
+
if (fullHistory.length === 0) return;
|
|
158
|
+
|
|
159
|
+
if (this.historyIndex === -1) {
|
|
160
|
+
// entering history mode: remember current typed text
|
|
161
|
+
this.savedLineBeforeHistory = this.currentLine;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (this.historyIndex < fullHistory.length - 1) {
|
|
165
|
+
this.historyIndex++;
|
|
166
|
+
const next =
|
|
167
|
+
fullHistory[fullHistory.length - 1 - this.historyIndex] ?? "";
|
|
168
|
+
this.replaceLine(next);
|
|
169
|
+
this.currentLine = next;
|
|
170
|
+
}
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (key?.name === "down") {
|
|
175
|
+
const fullHistory = this.getFullHistory();
|
|
176
|
+
if (fullHistory.length === 0) return;
|
|
177
|
+
|
|
178
|
+
if (this.historyIndex > 0) {
|
|
179
|
+
this.historyIndex--;
|
|
180
|
+
const next =
|
|
181
|
+
fullHistory[fullHistory.length - 1 - this.historyIndex] ?? "";
|
|
182
|
+
this.replaceLine(next);
|
|
183
|
+
this.currentLine = next;
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (this.historyIndex === 0) {
|
|
188
|
+
// leave history mode, restore what user was typing
|
|
189
|
+
this.historyIndex = -1;
|
|
190
|
+
const restore = this.savedLineBeforeHistory ?? "";
|
|
191
|
+
this.savedLineBeforeHistory = "";
|
|
192
|
+
this.replaceLine(restore);
|
|
193
|
+
this.currentLine = restore;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
return this.rl;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async ask(question: string, options: string[] = [], history: string[] = []) {
|
|
205
|
+
return new Promise<string>((resolve) => {
|
|
206
|
+
this.stack.push({ question, options, history, resolve });
|
|
207
|
+
|
|
208
|
+
const rl = this.ensureRl();
|
|
209
|
+
|
|
210
|
+
// IMPORTANT: snapshot readline's current buffer before we redraw/switch prompts.
|
|
211
|
+
// This prevents us from clobbering tab-completed text with a stale currentLine.
|
|
212
|
+
this.syncFromReadline();
|
|
213
|
+
|
|
214
|
+
// Update prompt to top-of-stack
|
|
215
|
+
this.render();
|
|
216
|
+
|
|
217
|
+
// Preserve what user typed so far
|
|
218
|
+
this.replaceLine(this.currentLine);
|
|
219
|
+
|
|
220
|
+
rl.prompt(true);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private peek(): AskOptions | undefined {
|
|
225
|
+
return this.stack[this.stack.length - 1];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private syncFromReadline(): void {
|
|
229
|
+
if (!this.rl) return;
|
|
230
|
+
this.currentLine = (this.rl as any).line ?? "";
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private sanitizeHistoryEntry(value: string): string {
|
|
234
|
+
// Prevent embedded newlines from triggering readline's "line" event
|
|
235
|
+
return value.replace(/[\r\n]+/g, " ").trim();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private getFullHistory(): string[] {
|
|
239
|
+
const current = this.peek();
|
|
240
|
+
const local = current?.history ?? [];
|
|
241
|
+
|
|
242
|
+
// De-dup while preserving order preference (older -> newer)
|
|
243
|
+
const merged = [...askHistory, ...local];
|
|
244
|
+
const seen = new Set<string>();
|
|
245
|
+
const out: string[] = [];
|
|
246
|
+
|
|
247
|
+
for (const item of merged) {
|
|
248
|
+
const clean = this.sanitizeHistoryEntry(item);
|
|
249
|
+
if (!clean) continue;
|
|
250
|
+
if (seen.has(clean)) continue;
|
|
251
|
+
seen.add(clean);
|
|
252
|
+
out.push(clean);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private render(): void {
|
|
259
|
+
if (!this.rl) return;
|
|
260
|
+
const current = this.peek();
|
|
261
|
+
if (!current) return;
|
|
262
|
+
|
|
263
|
+
// Make prompt be the question (readline manages wrapping/cursor)
|
|
264
|
+
this.rl.setPrompt(current.question);
|
|
265
|
+
this.rl.prompt(true);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private renderTopOrClose(): void {
|
|
269
|
+
if (this.stack.length === 0) {
|
|
270
|
+
this.close();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// IMPORTANT: snapshot readline's current buffer before we redraw/switch prompts.
|
|
275
|
+
// This prevents us from clobbering tab-completed text with a stale currentLine.
|
|
276
|
+
this.syncFromReadline();
|
|
277
|
+
|
|
278
|
+
this.render();
|
|
279
|
+
this.replaceLine(this.currentLine);
|
|
280
|
+
this.rl?.prompt(true);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private replaceLine(next: string): void {
|
|
284
|
+
if (!this.rl) return;
|
|
285
|
+
|
|
286
|
+
const safe = this.sanitizeHistoryEntry(next);
|
|
287
|
+
|
|
288
|
+
// Clear current line and write next input without affecting terminal scrollback
|
|
289
|
+
this.rl.write(null, { ctrl: true, name: "u" }); // Ctrl+U clears the line
|
|
290
|
+
if (safe) this.rl.write(safe);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Returns the longest common prefix of all strings in the array.
|
|
295
|
+
*/
|
|
296
|
+
private longestCommonPrefix(items: string[]): string {
|
|
297
|
+
if (items.length === 0) return "";
|
|
298
|
+
let prefix = items[0];
|
|
299
|
+
|
|
300
|
+
for (let i = 1; i < items.length; i++) {
|
|
301
|
+
const s = items[i];
|
|
302
|
+
let j = 0;
|
|
303
|
+
while (j < prefix.length && j < s.length && prefix[j] === s[j]) j++;
|
|
304
|
+
prefix = prefix.slice(0, j);
|
|
305
|
+
if (!prefix) break;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return prefix;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private close(): void {
|
|
312
|
+
if (!this.rl) return;
|
|
313
|
+
this.rl.close();
|
|
314
|
+
this.rl = null;
|
|
315
|
+
|
|
316
|
+
if (process.stdin.isTTY) {
|
|
317
|
+
try {
|
|
318
|
+
process.stdin.setRawMode(false);
|
|
319
|
+
} catch {
|
|
320
|
+
// ignore
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { exec } from "child_process";
|
|
|
5
5
|
import * as fs from "fs";
|
|
6
6
|
import { marked } from "marked";
|
|
7
7
|
import { markedTerminal } from "marked-terminal";
|
|
8
|
+
import { InputQueueManager } from "./InputQueueManager";
|
|
8
9
|
|
|
9
10
|
marked.use(markedTerminal());
|
|
10
11
|
|
|
@@ -16,164 +17,16 @@ export const execAsync = util.promisify(exec);
|
|
|
16
17
|
export const fileStat = promisify(fs.stat);
|
|
17
18
|
export const wait = promisify(setTimeout);
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
|
|
21
|
+
// Create singleton instance
|
|
22
|
+
const inputQueue = new InputQueueManager();
|
|
20
23
|
|
|
21
24
|
export const ask = async (
|
|
22
25
|
question: string,
|
|
23
26
|
options: string[] = [],
|
|
24
27
|
history = []
|
|
25
|
-
) => {
|
|
26
|
-
const fullHistory = [...askHistory, ...history];
|
|
27
|
-
const readline = require("readline").createInterface({
|
|
28
|
-
input: process.stdin,
|
|
29
|
-
output: process.stdout,
|
|
30
|
-
history: fullHistory,
|
|
31
|
-
completer: (line) => {
|
|
32
|
-
const hits = options.filter((c) => c?.startsWith(line));
|
|
33
|
-
return [hits.length ? hits : options, line];
|
|
34
|
-
},
|
|
35
|
-
terminal: true,
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
const _ask = util.promisify(readline.question).bind(readline);
|
|
39
|
-
const answer = await _ask(question);
|
|
40
|
-
readline.close();
|
|
41
|
-
|
|
42
|
-
return answer;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Enhanced ask function that handles paste operations with newlines
|
|
47
|
-
* and provides a better multi-line input experience
|
|
48
|
-
*/
|
|
49
|
-
export const askWithPaste = async (
|
|
50
|
-
question: string,
|
|
51
|
-
options: string[] = [],
|
|
52
|
-
history: string[] = [],
|
|
53
|
-
submitKeys: string[] = ["ctrl+d", "ctrl+enter"]
|
|
54
28
|
): Promise<string> => {
|
|
55
|
-
|
|
56
|
-
const fullHistory = [...askHistory, ...history];
|
|
57
|
-
|
|
58
|
-
return new Promise((resolve) => {
|
|
59
|
-
const rl = readline.createInterface({
|
|
60
|
-
input: process.stdin,
|
|
61
|
-
output: process.stdout,
|
|
62
|
-
history: fullHistory,
|
|
63
|
-
completer: (line: string) => {
|
|
64
|
-
const hits = options.filter((c) => c?.startsWith(line));
|
|
65
|
-
return [hits.length ? hits : options, line];
|
|
66
|
-
},
|
|
67
|
-
terminal: true,
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
let buffer = "";
|
|
71
|
-
let currentLine = "";
|
|
72
|
-
let cursorPos = 0;
|
|
73
|
-
const historyIndex = -1;
|
|
74
|
-
|
|
75
|
-
// Display the question
|
|
76
|
-
process.stdout.write(question);
|
|
77
|
-
|
|
78
|
-
// Handle raw input to detect paste operations and special keys
|
|
79
|
-
process.stdin.setRawMode(true);
|
|
80
|
-
process.stdin.resume();
|
|
81
|
-
|
|
82
|
-
const cleanup = () => {
|
|
83
|
-
process.stdin.setRawMode(false);
|
|
84
|
-
process.stdin.pause();
|
|
85
|
-
rl.close();
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
process.stdin.on("data", (data) => {
|
|
89
|
-
const input = data.toString();
|
|
90
|
-
|
|
91
|
-
// Handle Ctrl+C
|
|
92
|
-
if (input === "\u0003") {
|
|
93
|
-
cleanup();
|
|
94
|
-
process.exit(0);
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Handle Ctrl+D (submit)
|
|
99
|
-
if (input === "\u0004") {
|
|
100
|
-
process.stdout.write("\n");
|
|
101
|
-
cleanup();
|
|
102
|
-
resolve(buffer.trim());
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Handle Ctrl+Enter (submit)
|
|
107
|
-
if (input === "\r\n" || input === "\n\r") {
|
|
108
|
-
process.stdout.write("\n");
|
|
109
|
-
cleanup();
|
|
110
|
-
resolve(buffer.trim());
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Handle regular Enter (add newline to buffer)
|
|
115
|
-
if (input === "\r" || input === "\n") {
|
|
116
|
-
buffer += currentLine + "\n";
|
|
117
|
-
currentLine = "";
|
|
118
|
-
cursorPos = 0;
|
|
119
|
-
process.stdout.write("\n");
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Handle Backspace
|
|
124
|
-
if (input === "\u007f" || input === "\b") {
|
|
125
|
-
if (cursorPos > 0) {
|
|
126
|
-
currentLine =
|
|
127
|
-
currentLine.slice(0, cursorPos - 1) + currentLine.slice(cursorPos);
|
|
128
|
-
cursorPos--;
|
|
129
|
-
// Redraw current line
|
|
130
|
-
process.stdout.write(
|
|
131
|
-
"\r" +
|
|
132
|
-
" ".repeat(question.length + currentLine.length + 1) +
|
|
133
|
-
"\r" +
|
|
134
|
-
question +
|
|
135
|
-
currentLine
|
|
136
|
-
);
|
|
137
|
-
process.stdout.write("\u001b[" + (question.length + cursorPos) + "G");
|
|
138
|
-
}
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Handle Tab (autocomplete)
|
|
143
|
-
if (input === "\t" && options.length > 0) {
|
|
144
|
-
const hits = options.filter((c) => c?.startsWith(currentLine));
|
|
145
|
-
if (hits.length === 1) {
|
|
146
|
-
currentLine = hits[0];
|
|
147
|
-
cursorPos = currentLine.length;
|
|
148
|
-
process.stdout.write("\r" + question + currentLine);
|
|
149
|
-
}
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Handle regular characters (including pasted content)
|
|
154
|
-
if (input.length > 1) {
|
|
155
|
-
// This is likely a paste operation
|
|
156
|
-
currentLine =
|
|
157
|
-
currentLine.slice(0, cursorPos) +
|
|
158
|
-
input +
|
|
159
|
-
currentLine.slice(cursorPos);
|
|
160
|
-
cursorPos += input.length;
|
|
161
|
-
} else if (input >= " ") {
|
|
162
|
-
// Single printable character
|
|
163
|
-
currentLine =
|
|
164
|
-
currentLine.slice(0, cursorPos) +
|
|
165
|
-
input +
|
|
166
|
-
currentLine.slice(cursorPos);
|
|
167
|
-
cursorPos++;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Redraw current line
|
|
171
|
-
process.stdout.write("\r" + question + currentLine);
|
|
172
|
-
if (cursorPos < currentLine.length) {
|
|
173
|
-
process.stdout.write("\u001b[" + (question.length + cursorPos) + "G");
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
});
|
|
29
|
+
return inputQueue.ask(question, options, history);
|
|
177
30
|
};
|
|
178
31
|
|
|
179
32
|
export const Marked = marked;
|