pulse-coder-cli 0.0.1-alpha.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/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "pulse-coder-cli",
3
+ "version": "0.0.1-alpha.1",
4
+ "description": "CLI interface for Pulse Coder",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "bin": {
8
+ "pulse-coder": "./dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsup",
12
+ "dev": "tsup --watch",
13
+ "test": "vitest",
14
+ "start": "node dist/index.js"
15
+ },
16
+ "dependencies": {
17
+ "@pulse-coder/engine": "workspace:*"
18
+ },
19
+ "devDependencies": {
20
+ "typescript": "^5.0.0",
21
+ "tsup": "^8.0.0",
22
+ "vitest": "^1.0.0",
23
+ "@types/node": "^25.0.10"
24
+ }
25
+ }
package/src/index.ts ADDED
@@ -0,0 +1,321 @@
1
+ import { PulseAgent } from '@pulse-coder/engine';
2
+ import * as readline from 'readline';
3
+ import type { Context } from '@pulse-coder/engine';
4
+ import { SessionCommands } from './session-commands.js';
5
+ import { InputManager } from './input-manager.js';
6
+
7
+ class CoderCLI {
8
+ private agent: PulseAgent;
9
+ private context: Context;
10
+ private sessionCommands: SessionCommands;
11
+ private inputManager: InputManager;
12
+
13
+ constructor() {
14
+ // šŸŽÆ ēŽ°åœØå¼•ę“Žč‡ŖåŠØåŒ…å«å†…ē½®ę’ä»¶ļ¼Œę— éœ€ę˜¾å¼é…ē½®ļ¼
15
+ this.agent = new PulseAgent({
16
+ enginePlugins: {
17
+ // åŖé…ē½®ę‰©å±•ę’ä»¶ē›®å½•ļ¼Œå†…ē½®ę’ä»¶ä¼šč‡ŖåŠØåŠ č½½
18
+ dirs: ['.pulse-coder/engine-plugins', '.coder/engine-plugins', '~/.pulse-coder/engine-plugins', '~/.coder/engine-plugins'],
19
+ scan: true
20
+ },
21
+ userConfigPlugins: {
22
+ dirs: ['.pulse-coder/config', '.coder/config', '~/.pulse-coder/config', '~/.coder/config'],
23
+ scan: true
24
+ }
25
+ // ę³Øę„ļ¼šäøå†éœ€č¦ plugins: [...] é…ē½®
26
+ });
27
+ this.context = { messages: [] };
28
+ this.sessionCommands = new SessionCommands();
29
+ this.inputManager = new InputManager();
30
+ }
31
+
32
+ private async handleCommand(command: string, args: string[]): Promise<void> {
33
+ try {
34
+ switch (command.toLowerCase()) {
35
+ case 'help':
36
+ console.log('\nšŸ“‹ Available commands:');
37
+ console.log('/help - Show this help message');
38
+ console.log('/new [title] - Create a new session');
39
+ console.log('/resume <id> - Resume a saved session');
40
+ console.log('/sessions - List all saved sessions');
41
+ console.log('/search <query> - Search in saved sessions');
42
+ console.log('/rename <id> <new-title> - Rename a session');
43
+ console.log('/delete <id> - Delete a session');
44
+ console.log('/clear - Clear current conversation');
45
+ console.log('/status - Show current session status');
46
+ console.log('/save - Save current session explicitly');
47
+ console.log('/exit - Exit the application');
48
+ break;
49
+
50
+ case 'new':
51
+ const newTitle = args.join(' ') || undefined;
52
+ await this.sessionCommands.createSession(newTitle);
53
+ this.context.messages = [];
54
+ break;
55
+
56
+ case 'resume':
57
+ if (args.length === 0) {
58
+ console.log('\nāŒ Please provide a session ID');
59
+ console.log('Usage: /resume <session-id>');
60
+ break;
61
+ }
62
+ const sessionId = args[0];
63
+ const success = await this.sessionCommands.resumeSession(sessionId);
64
+ if (success) {
65
+ await this.sessionCommands.loadContext(this.context);
66
+ }
67
+ break;
68
+
69
+ case 'sessions':
70
+ await this.sessionCommands.listSessions();
71
+ break;
72
+
73
+ case 'search':
74
+ if (args.length === 0) {
75
+ console.log('\nāŒ Please provide a search query');
76
+ console.log('Usage: /search <query>');
77
+ break;
78
+ }
79
+ const query = args.join(' ');
80
+ await this.sessionCommands.searchSessions(query);
81
+ break;
82
+
83
+ case 'rename':
84
+ if (args.length < 2) {
85
+ console.log('\nāŒ Please provide session ID and new title');
86
+ console.log('Usage: /rename <session-id> <new-title>');
87
+ break;
88
+ }
89
+ const renameId = args[0];
90
+ const newName = args.slice(1).join(' ');
91
+ await this.sessionCommands.renameSession(renameId, newName);
92
+ break;
93
+
94
+ case 'delete':
95
+ if (args.length === 0) {
96
+ console.log('\nāŒ Please provide a session ID');
97
+ console.log('Usage: /delete <session-id>');
98
+ break;
99
+ }
100
+ const deleteId = args[0];
101
+ await this.sessionCommands.deleteSession(deleteId);
102
+ break;
103
+
104
+ case 'clear':
105
+ this.context.messages = [];
106
+ console.log('\n🧹 Current conversation cleared!');
107
+ break;
108
+
109
+ case 'status':
110
+ const currentId = this.sessionCommands.getCurrentSessionId();
111
+ console.log(`\nšŸ“Š Session Status:`);
112
+ console.log(`Current Session: ${currentId || 'None (new session)'}`);
113
+ console.log(`Messages: ${this.context.messages.length}`);
114
+ if (currentId) {
115
+ console.log(`To save this session, use: /save`);
116
+ }
117
+ break;
118
+
119
+ case 'save':
120
+ if (this.sessionCommands.getCurrentSessionId()) {
121
+ await this.sessionCommands.saveContext(this.context);
122
+ console.log('\nšŸ’¾ Current session saved!');
123
+ } else {
124
+ console.log('\nāŒ No active session. Create one with /new');
125
+ }
126
+ break;
127
+
128
+ case 'exit':
129
+ console.log('šŸ’¾ Saving current session...');
130
+ await this.sessionCommands.saveContext(this.context);
131
+ console.log('Goodbye!');
132
+ process.exit(0);
133
+ break;
134
+
135
+ default:
136
+ console.log(`\nāš ļø Unknown command: ${command}`);
137
+ console.log('Type /help to see available commands');
138
+ }
139
+ } catch (error) {
140
+ console.error('\nāŒ Error executing command:', error);
141
+ }
142
+ }
143
+
144
+ async start() {
145
+ console.log('šŸš€ Pulse Coder CLI is running...');
146
+ console.log('Type your messages and press Enter. Type "exit" to quit.');
147
+ console.log('Commands starting with "/" will trigger command mode.\n');
148
+
149
+ await this.sessionCommands.initialize();
150
+ await this.agent.initialize();
151
+
152
+ // ę˜¾ē¤ŗę’ä»¶ēŠ¶ę€
153
+ const pluginStatus = this.agent.getPluginStatus();
154
+ console.log(`āœ… Built-in plugins loaded: ${pluginStatus.enginePlugins.length} plugins`);
155
+
156
+ // Auto-create a new session
157
+ await this.sessionCommands.createSession();
158
+
159
+ const rl = readline.createInterface({
160
+ input: process.stdin,
161
+ output: process.stdout,
162
+ prompt: '> '
163
+ });
164
+
165
+ let currentAbortController: AbortController | null = null;
166
+ let isProcessing = false;
167
+
168
+ // Handle SIGINT gracefully
169
+ process.on('SIGINT', () => {
170
+ if (isProcessing && currentAbortController && !currentAbortController.signal.aborted) {
171
+ currentAbortController.abort();
172
+ console.log('\n[Abort] Request cancelled.');
173
+ return;
174
+ }
175
+
176
+ // Cancel any pending clarification request
177
+ if (this.inputManager.hasPendingRequest()) {
178
+ this.inputManager.cancel('User interrupted with Ctrl+C');
179
+ console.log('\n[Abort] Clarification cancelled.');
180
+ rl.prompt();
181
+ return;
182
+ }
183
+
184
+ console.log('\nšŸ’¾ Saving current session...');
185
+ this.sessionCommands.saveContext(this.context).then(() => {
186
+ console.log('šŸ‘‹ Goodbye!');
187
+ process.exit(0);
188
+ });
189
+ });
190
+
191
+ // Main input handler
192
+ const handleInput = async (input: string) => {
193
+ const trimmedInput = input.trim();
194
+
195
+ // Handle clarification requests first
196
+ if (this.inputManager.handleUserInput(trimmedInput)) {
197
+ return;
198
+ }
199
+
200
+ if (trimmedInput.toLowerCase() === 'exit') {
201
+ console.log('šŸ’¾ Saving current session...');
202
+ await this.sessionCommands.saveContext(this.context);
203
+ console.log('šŸ‘‹ Goodbye!');
204
+ rl.close();
205
+ return;
206
+ }
207
+
208
+ if (!trimmedInput) {
209
+ rl.prompt();
210
+ return;
211
+ }
212
+
213
+ // Handle commands
214
+ if (trimmedInput.startsWith('/')) {
215
+ const commandLine = trimmedInput.substring(1);
216
+ const parts = commandLine.split(/\s+/).filter(part => part.length > 0);
217
+
218
+ if (parts.length === 0) {
219
+ console.log('\nāš ļø Please provide a command after "/"');
220
+ rl.prompt();
221
+ return;
222
+ }
223
+
224
+ const command = parts[0];
225
+ const args = parts.slice(1);
226
+
227
+ await this.handleCommand(command, args);
228
+ rl.prompt();
229
+ return;
230
+ }
231
+
232
+ // Regular message processing
233
+ this.context.messages.push({
234
+ role: 'user',
235
+ content: trimmedInput,
236
+ });
237
+
238
+ console.log('\nšŸ”„ Processing...\n');
239
+
240
+ const ac = new AbortController();
241
+ currentAbortController = ac;
242
+ isProcessing = true;
243
+
244
+ let sawText = false;
245
+
246
+ try {
247
+ const result = await this.agent.run(this.context, {
248
+ abortSignal: ac.signal,
249
+ onText: (delta) => {
250
+ sawText = true;
251
+ process.stdout.write(delta);
252
+ },
253
+ onToolCall: (toolCall) => {
254
+ const input = 'input' in toolCall ? toolCall.input : undefined;
255
+ const inputText = input === undefined ? '' : `(${JSON.stringify(input)})`;
256
+ process.stdout.write(`\nšŸ”§ ${toolCall.toolName}${inputText}\n`);
257
+ },
258
+ onToolResult: (toolResult) => {
259
+ process.stdout.write(`\nāœ… ${toolResult.toolName}\n`);
260
+ },
261
+ onStepFinish: (step) => {
262
+ process.stdout.write(`\nšŸ“‹ Step finished: ${step.finishReason}\n`);
263
+ },
264
+ onClarificationRequest: async (request) => {
265
+ return await this.inputManager.requestInput(request);
266
+ },
267
+ onCompacted: (newMessages) => {
268
+ this.context.messages = newMessages;
269
+ },
270
+ onResponse: (messages) => {
271
+ this.context.messages.push(...messages);
272
+ },
273
+ });
274
+
275
+ if (result) {
276
+ if (!sawText) {
277
+ console.log(result);
278
+ } else {
279
+ console.log();
280
+ }
281
+
282
+ this.context.messages.push({
283
+ role: 'assistant',
284
+ content: result,
285
+ });
286
+
287
+ await this.sessionCommands.saveContext(this.context);
288
+ }
289
+ } catch (error) {
290
+ if (error.name === 'AbortError') {
291
+ console.log('\n[Abort] Operation cancelled.');
292
+ } else {
293
+ console.error('\nāŒ Error:', error.message);
294
+ }
295
+ } finally {
296
+ isProcessing = false;
297
+ currentAbortController = null;
298
+ rl.prompt();
299
+ }
300
+ };
301
+
302
+ // Start the CLI
303
+ rl.prompt();
304
+ rl.on('line', handleInput);
305
+
306
+ // Handle terminal close
307
+ rl.on('close', async () => {
308
+ console.log('\nšŸ’¾ Saving current session...');
309
+ await this.sessionCommands.saveContext(this.context);
310
+ console.log('šŸ‘‹ Goodbye!');
311
+ process.exit(0);
312
+ });
313
+ }
314
+ }
315
+
316
+ // Always start the CLI when executed directly
317
+ const cli = new CoderCLI();
318
+ cli.start().catch(error => {
319
+ console.error('Failed to start CLI:', error);
320
+ process.exit(1);
321
+ });
@@ -0,0 +1,113 @@
1
+ import type { ClarificationRequest } from '@pulse-coder/engine';
2
+
3
+ interface PendingRequest {
4
+ request: ClarificationRequest;
5
+ resolve: (answer: string) => void;
6
+ reject: (error: Error) => void;
7
+ timeoutId?: NodeJS.Timeout;
8
+ }
9
+
10
+ /**
11
+ * InputManager handles asynchronous user input for clarification requests.
12
+ * It manages pending clarification requests and coordinates with the readline interface.
13
+ */
14
+ export class InputManager {
15
+ private pendingRequest: PendingRequest | null = null;
16
+
17
+ /**
18
+ * Request user input for a clarification
19
+ * @param request The clarification request details
20
+ * @returns Promise that resolves with the user's answer
21
+ */
22
+ async requestInput(request: ClarificationRequest): Promise<string> {
23
+ return new Promise<string>((resolve, reject) => {
24
+ // If there's already a pending request, reject this one
25
+ if (this.pendingRequest) {
26
+ reject(new Error('Another clarification request is already pending'));
27
+ return;
28
+ }
29
+
30
+ // Display the question to the user
31
+ console.log(`\nā“ ${request.question}`);
32
+ if (request.context) {
33
+ console.log(` ${request.context}`);
34
+ }
35
+ if (request.defaultAnswer) {
36
+ console.log(` (Default: ${request.defaultAnswer})`);
37
+ }
38
+ console.log(''); // Empty line for spacing
39
+
40
+ // Set up timeout if specified
41
+ let timeoutId: NodeJS.Timeout | undefined;
42
+ if (request.timeout > 0) {
43
+ timeoutId = setTimeout(() => {
44
+ if (this.pendingRequest?.request.id === request.id) {
45
+ const error = new Error(`Clarification request timed out after ${request.timeout}ms`);
46
+ this.pendingRequest.reject(error);
47
+ this.pendingRequest = null;
48
+ }
49
+ }, request.timeout);
50
+ }
51
+
52
+ // Store the pending request
53
+ this.pendingRequest = {
54
+ request,
55
+ resolve,
56
+ reject,
57
+ timeoutId
58
+ };
59
+ });
60
+ }
61
+
62
+ /**
63
+ * Handle user input - checks if there's a pending clarification request
64
+ * @param input The user's input
65
+ * @returns true if input was consumed by a pending clarification, false otherwise
66
+ */
67
+ handleUserInput(input: string): boolean {
68
+ if (!this.pendingRequest) {
69
+ return false;
70
+ }
71
+
72
+ // Clear the timeout if it exists
73
+ if (this.pendingRequest.timeoutId) {
74
+ clearTimeout(this.pendingRequest.timeoutId);
75
+ }
76
+
77
+ // Resolve the pending request with the user's input
78
+ const trimmedInput = input.trim();
79
+ this.pendingRequest.resolve(trimmedInput);
80
+ this.pendingRequest = null;
81
+
82
+ return true;
83
+ }
84
+
85
+ /**
86
+ * Cancel any pending clarification request
87
+ * @param reason Optional cancellation reason
88
+ */
89
+ cancel(reason?: string): void {
90
+ if (this.pendingRequest) {
91
+ if (this.pendingRequest.timeoutId) {
92
+ clearTimeout(this.pendingRequest.timeoutId);
93
+ }
94
+
95
+ this.pendingRequest.reject(new Error(reason || 'Clarification request cancelled'));
96
+ this.pendingRequest = null;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Check if there's a pending clarification request
102
+ */
103
+ hasPendingRequest(): boolean {
104
+ return this.pendingRequest !== null;
105
+ }
106
+
107
+ /**
108
+ * Get the current pending request (for debugging/testing)
109
+ */
110
+ getPendingRequest(): ClarificationRequest | null {
111
+ return this.pendingRequest?.request || null;
112
+ }
113
+ }
@@ -0,0 +1,142 @@
1
+ import { SessionManager } from './session.js';
2
+ import type { Context } from '@pulse-coder/engine';
3
+
4
+ export class SessionCommands {
5
+ private sessionManager: SessionManager;
6
+ private currentSessionId: string | null = null;
7
+
8
+ constructor() {
9
+ this.sessionManager = new SessionManager();
10
+ }
11
+
12
+ async initialize(): Promise<void> {
13
+ await this.sessionManager.initialize();
14
+ }
15
+
16
+ getCurrentSessionId(): string | null {
17
+ return this.currentSessionId;
18
+ }
19
+
20
+ async createSession(title?: string): Promise<string> {
21
+ const session = await this.sessionManager.createSession(title);
22
+ this.currentSessionId = session.id;
23
+ console.log(`\nāœ… New session created: ${session.title} (ID: ${session.id})`);
24
+ return session.id;
25
+ }
26
+
27
+ async resumeSession(id: string): Promise<boolean> {
28
+ const session = await this.sessionManager.loadSession(id);
29
+ if (!session) {
30
+ console.log(`\nāŒ Session not found: ${id}`);
31
+ return false;
32
+ }
33
+
34
+ this.currentSessionId = session.id;
35
+ console.log(`\nāœ… Resumed session: ${session.title} (ID: ${session.id})`);
36
+ console.log(`šŸ“Š Loaded ${session.messages.length} messages`);
37
+
38
+ // Show last few messages as context
39
+ const recentMessages = session.messages.slice(-5);
40
+ if (recentMessages.length > 0) {
41
+ console.log('\nšŸ’¬ Recent conversation:');
42
+ recentMessages.forEach((msg, index) => {
43
+ const role = msg.role === 'user' ? 'šŸ‘¤ You' : 'šŸ¤– Assistant';
44
+ const contentStr = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
45
+ const preview = contentStr.substring(0, 100) + (contentStr.length > 100 ? '...' : '');
46
+ console.log(`${index + 1}. ${role}: ${preview}`);
47
+ });
48
+ }
49
+
50
+ return true;
51
+ }
52
+
53
+ async listSessions(): Promise<void> {
54
+ const sessions = await this.sessionManager.listSessions();
55
+
56
+ if (sessions.length === 0) {
57
+ console.log('\nšŸ“­ No saved sessions found.');
58
+ return;
59
+ }
60
+
61
+ console.log('\nšŸ“‹ Saved sessions:');
62
+ console.log('='.repeat(80));
63
+
64
+ sessions.forEach((session, index) => {
65
+ const isActive = session.id === this.currentSessionId ? 'āœ…' : ' ';
66
+ const date = new Date(session.updatedAt).toLocaleString();
67
+ console.log(`${index + 1}. ${isActive} ${session.title}`);
68
+ console.log(` ID: ${session.id}`);
69
+ console.log(` Messages: ${session.messageCount} | Updated: ${date}`);
70
+ console.log(` Preview: ${session.preview}`);
71
+ console.log();
72
+ });
73
+ }
74
+
75
+ async saveContext(context: Context): Promise<void> {
76
+ if (!this.currentSessionId) return;
77
+
78
+ const session = await this.sessionManager.loadSession(this.currentSessionId);
79
+ if (!session) return;
80
+
81
+ // Sync messages from context
82
+ session.messages = context.messages.map(msg => ({
83
+ role: msg.role as 'user' | 'assistant' | 'system',
84
+ content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
85
+ timestamp: Date.now(),
86
+ }));
87
+
88
+ await this.sessionManager.saveSession(session);
89
+ }
90
+
91
+ async loadContext(context: Context): Promise<void> {
92
+ if (!this.currentSessionId) return;
93
+
94
+ const session = await this.sessionManager.loadSession(this.currentSessionId);
95
+ if (!session) return;
96
+
97
+ // Load messages into context
98
+ context.messages = session.messages.map(msg => ({
99
+ role: msg.role,
100
+ content: msg.content,
101
+ }));
102
+ }
103
+
104
+ async searchSessions(query: string): Promise<void> {
105
+ const sessions = await this.sessionManager.searchSessions(query);
106
+
107
+ if (sessions.length === 0) {
108
+ console.log(`\nšŸ” No sessions found matching "${query}"`);
109
+ return;
110
+ }
111
+
112
+ console.log(`\nšŸ” Search results for "${query}":`);
113
+ sessions.forEach((session, index) => {
114
+ console.log(`${index + 1}. ${session.title} (${session.id}) - ${session.messageCount} messages`);
115
+ console.log(` Updated: ${new Date(session.updatedAt).toLocaleString()}`);
116
+ console.log(` Preview: ${session.preview}`);
117
+ });
118
+ }
119
+
120
+ async deleteSession(id: string): Promise<boolean> {
121
+ const success = await this.sessionManager.deleteSession(id);
122
+ if (success) {
123
+ console.log(`\nšŸ—‘ļø Session ${id} deleted`);
124
+ if (this.currentSessionId === id) {
125
+ this.currentSessionId = null;
126
+ }
127
+ } else {
128
+ console.log(`\nāŒ Failed to delete session ${id}`);
129
+ }
130
+ return success;
131
+ }
132
+
133
+ async renameSession(id: string, newTitle: string): Promise<boolean> {
134
+ const success = await this.sessionManager.updateSessionTitle(id, newTitle);
135
+ if (success) {
136
+ console.log(`\nāœ… Session ${id} renamed to "${newTitle}"`);
137
+ } else {
138
+ console.log(`\nāŒ Failed to rename session ${id}`);
139
+ }
140
+ return success;
141
+ }
142
+ }