helix-assistant-cli 0.0.0-stage → 0.2.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.
Files changed (51) hide show
  1. package/bin/helix.js +3 -0
  2. package/dist/client/agent-client.d.ts +60 -0
  3. package/dist/client/agent-client.js +322 -0
  4. package/dist/client/agent-launcher.d.ts +30 -0
  5. package/dist/client/agent-launcher.js +339 -0
  6. package/dist/client/types.d.ts +88 -0
  7. package/dist/client/types.js +2 -0
  8. package/dist/commands/apikey.d.ts +3 -0
  9. package/dist/commands/apikey.js +123 -0
  10. package/dist/commands/browser.d.ts +2 -0
  11. package/dist/commands/browser.js +35 -0
  12. package/dist/commands/mcp.d.ts +3 -0
  13. package/dist/commands/mcp.js +102 -0
  14. package/dist/commands/mic.d.ts +2 -0
  15. package/dist/commands/mic.js +57 -0
  16. package/dist/commands/skills.d.ts +3 -0
  17. package/dist/commands/skills.js +105 -0
  18. package/dist/config/env.d.ts +6 -0
  19. package/dist/config/env.js +71 -0
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.js +126 -0
  22. package/dist/ui/autocomplete.d.ts +25 -0
  23. package/dist/ui/autocomplete.js +142 -0
  24. package/dist/ui/banner.d.ts +24 -0
  25. package/dist/ui/banner.js +207 -0
  26. package/dist/ui/chat.d.ts +1 -0
  27. package/dist/ui/chat.js +5 -0
  28. package/dist/ui/colors.d.ts +34 -0
  29. package/dist/ui/colors.js +69 -0
  30. package/dist/ui/repl.d.ts +31 -0
  31. package/dist/ui/repl.js +402 -0
  32. package/package.json +31 -4
  33. package/src/client/agent-client.ts +387 -0
  34. package/src/client/agent-launcher.ts +374 -0
  35. package/src/client/types.ts +95 -0
  36. package/src/commands/apikey.ts +129 -0
  37. package/src/commands/browser.ts +39 -0
  38. package/src/commands/mcp.ts +105 -0
  39. package/src/commands/mic.ts +57 -0
  40. package/src/commands/skills.ts +109 -0
  41. package/src/config/env.ts +63 -0
  42. package/src/index.ts +139 -0
  43. package/src/ui/autocomplete.ts +153 -0
  44. package/src/ui/banner.ts +240 -0
  45. package/src/ui/chat.ts +1 -0
  46. package/src/ui/colors.ts +70 -0
  47. package/src/ui/repl.ts +473 -0
  48. package/tests/cli.test.ts +141 -0
  49. package/tsconfig.json +18 -0
  50. package/tsconfig.test.json +7 -0
  51. package/README.md +0 -3
package/bin/helix.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ require('../dist/index.js');
@@ -0,0 +1,60 @@
1
+ import { EventEmitter } from 'events';
2
+ import type { HelixSettings, McpServerConfig, ModelSummary, ProviderSummary, SkillSummary, TaskSummary } from './types';
3
+ export interface AgentClientOptions {
4
+ wsUrl?: string;
5
+ wsToken?: string;
6
+ autoReconnect?: boolean;
7
+ reconnectIntervalMs?: number;
8
+ }
9
+ export declare class AgentClient extends EventEmitter {
10
+ private ws;
11
+ private wsUrl;
12
+ private wsToken?;
13
+ private autoReconnect;
14
+ private reconnectIntervalMs;
15
+ private reconnecting;
16
+ private isExplicitlyClosed;
17
+ private pendingRequests;
18
+ private requestTimeouts;
19
+ private isConnected;
20
+ constructor(options?: AgentClientOptions);
21
+ get connected(): boolean;
22
+ get url(): string;
23
+ private failedAttempts;
24
+ private autoSpawnAttempted;
25
+ private _isAgentStarting;
26
+ get isStarting(): boolean;
27
+ connect(): Promise<void>;
28
+ private _scheduleReconnect;
29
+ private _handleMessage;
30
+ request<T = unknown>(type: string, payload?: unknown, timeoutMs?: number): Promise<T>;
31
+ send(type: string, payload?: unknown): boolean;
32
+ sendUserText(text: string, conversationId: string, history?: Array<{
33
+ role: 'user' | 'assistant';
34
+ content: string;
35
+ }>, attachments?: Array<{
36
+ name: string;
37
+ mime_type: string;
38
+ size?: number;
39
+ data_base64: string;
40
+ }>): boolean;
41
+ getTasks(): Promise<TaskSummary[]>;
42
+ getProviders(): Promise<ProviderSummary[]>;
43
+ getModels(requirements?: Record<string, unknown>): Promise<ModelSummary[]>;
44
+ getSettings(): Promise<HelixSettings>;
45
+ saveSettings(settings: Partial<HelixSettings>): Promise<HelixSettings>;
46
+ validateKey(provider: string, slot: number, key: string): Promise<string>;
47
+ getSkills(): Promise<SkillSummary[]>;
48
+ saveSkill(skill: Record<string, unknown>): Promise<SkillSummary>;
49
+ deleteSkill(name: string): Promise<{
50
+ deleted: boolean;
51
+ }>;
52
+ getMcpServers(): Promise<McpServerConfig[]>;
53
+ getMemoryStatus(): Promise<Record<string, unknown>>;
54
+ clearMemory(): Promise<void>;
55
+ cancelTask(taskId: string): void;
56
+ emergencyStop(): void;
57
+ confirmAction(requestId: string): void;
58
+ rejectAction(requestId: string): void;
59
+ close(): void;
60
+ }
@@ -0,0 +1,322 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.AgentClient = void 0;
7
+ const ws_1 = __importDefault(require("ws"));
8
+ const events_1 = require("events");
9
+ class AgentClient extends events_1.EventEmitter {
10
+ ws = null;
11
+ wsUrl;
12
+ wsToken;
13
+ autoReconnect;
14
+ reconnectIntervalMs;
15
+ reconnecting = false;
16
+ isExplicitlyClosed = false;
17
+ pendingRequests = new Map();
18
+ requestTimeouts = new Map();
19
+ isConnected = false;
20
+ constructor(options = {}) {
21
+ super();
22
+ const { getOrGenerateWsToken } = require('./agent-launcher');
23
+ this.wsUrl = options.wsUrl || 'ws://127.0.0.1:8765';
24
+ this.wsToken = options.wsToken || getOrGenerateWsToken();
25
+ this.autoReconnect = options.autoReconnect ?? true;
26
+ this.reconnectIntervalMs = options.reconnectIntervalMs ?? 3000;
27
+ }
28
+ get connected() {
29
+ return this.isConnected && this.ws !== null && this.ws.readyState === ws_1.default.OPEN;
30
+ }
31
+ get url() {
32
+ return this.wsUrl;
33
+ }
34
+ failedAttempts = 0;
35
+ autoSpawnAttempted = false;
36
+ _isAgentStarting = false;
37
+ get isStarting() {
38
+ return this._isAgentStarting;
39
+ }
40
+ connect() {
41
+ this.isExplicitlyClosed = false;
42
+ return new Promise((resolve, reject) => {
43
+ let settled = false;
44
+ try {
45
+ const headers = {};
46
+ if (this.wsToken) {
47
+ headers['Authorization'] = `Bearer ${this.wsToken}`;
48
+ }
49
+ const ws = new ws_1.default(this.wsUrl, { headers });
50
+ this.ws = ws;
51
+ ws.on('open', () => {
52
+ this.isConnected = true;
53
+ this.reconnecting = false;
54
+ settled = true;
55
+ this.emit('connected', this.wsUrl);
56
+ resolve();
57
+ });
58
+ ws.on('message', (data) => {
59
+ try {
60
+ const raw = data.toString();
61
+ const message = JSON.parse(raw);
62
+ this._handleMessage(message);
63
+ }
64
+ catch (err) {
65
+ this.emit('parse_error', err);
66
+ }
67
+ });
68
+ ws.on('error', (err) => {
69
+ this.emit('client_error', err);
70
+ if (!settled) {
71
+ settled = true;
72
+ reject(err);
73
+ }
74
+ });
75
+ ws.on('close', (code, reason) => {
76
+ this.isConnected = false;
77
+ if (this.ws === ws) {
78
+ this.ws = null;
79
+ }
80
+ if (!this._isAgentStarting) {
81
+ this.emit('disconnected', { code, reason: reason.toString() });
82
+ }
83
+ if (!this.isExplicitlyClosed && this.autoReconnect && !this._isAgentStarting) {
84
+ this._scheduleReconnect();
85
+ }
86
+ if (!settled) {
87
+ settled = true;
88
+ reject(new Error(`WebSocket closed prematurely (${code}): ${reason.toString()}`));
89
+ }
90
+ });
91
+ }
92
+ catch (err) {
93
+ if (!settled) {
94
+ settled = true;
95
+ reject(err);
96
+ }
97
+ }
98
+ });
99
+ }
100
+ _scheduleReconnect() {
101
+ if (this.reconnecting || this.isExplicitlyClosed || this._isAgentStarting)
102
+ return;
103
+ this.reconnecting = true;
104
+ this.failedAttempts++;
105
+ // If 3 consecutive failures occur, try to auto-launch the local Python agent
106
+ if (this.failedAttempts >= 3 && !this.autoSpawnAttempted) {
107
+ this.autoSpawnAttempted = true;
108
+ this._isAgentStarting = true;
109
+ this.emit('auto_spawning_agent');
110
+ (async () => {
111
+ try {
112
+ const { spawnAgentDaemon, waitForAgentReady, getOrGenerateWsToken } = require('./agent-launcher');
113
+ const token = getOrGenerateWsToken();
114
+ if (!this.wsToken) {
115
+ this.wsToken = token;
116
+ }
117
+ const spawnResult = await spawnAgentDaemon();
118
+ if (!spawnResult.success) {
119
+ this._isAgentStarting = false;
120
+ this.reconnecting = false;
121
+ this.emit('agent_start_failed', { error: spawnResult.error });
122
+ return;
123
+ }
124
+ // Lock retries: poll quietly until background service finishes starting up AND auth succeeds
125
+ const ready = await waitForAgentReady(8765, this.wsToken, 45000, 1000, (elapsedSec) => {
126
+ this.emit('agent_starting_progress', { elapsedSec });
127
+ });
128
+ this._isAgentStarting = false;
129
+ if (ready) {
130
+ this.failedAttempts = 0;
131
+ this.autoSpawnAttempted = false;
132
+ this.reconnecting = false;
133
+ this.emit('agent_ready');
134
+ try {
135
+ await this.connect();
136
+ }
137
+ catch {
138
+ this._scheduleReconnect();
139
+ }
140
+ }
141
+ else {
142
+ this.reconnecting = false;
143
+ this.emit('agent_start_failed', {
144
+ error: 'El servicio del agente no respondió en el puerto 8765 dentro de 45s.',
145
+ });
146
+ if (!this.isExplicitlyClosed && this.autoReconnect) {
147
+ this._scheduleReconnect();
148
+ }
149
+ }
150
+ }
151
+ catch (e) {
152
+ this._isAgentStarting = false;
153
+ this.reconnecting = false;
154
+ this.emit('agent_start_failed', { error: e.message });
155
+ if (!this.isExplicitlyClosed && this.autoReconnect) {
156
+ this._scheduleReconnect();
157
+ }
158
+ }
159
+ })();
160
+ return;
161
+ }
162
+ this.emit('reconnecting', {
163
+ intervalMs: this.reconnectIntervalMs,
164
+ attempt: this.failedAttempts,
165
+ });
166
+ setTimeout(async () => {
167
+ if (this.isExplicitlyClosed || this._isAgentStarting) {
168
+ this.reconnecting = false;
169
+ return;
170
+ }
171
+ try {
172
+ await this.connect();
173
+ this.failedAttempts = 0;
174
+ this.autoSpawnAttempted = false;
175
+ this.reconnecting = false;
176
+ }
177
+ catch {
178
+ this.reconnecting = false;
179
+ this._scheduleReconnect();
180
+ }
181
+ }, this.reconnectIntervalMs);
182
+ }
183
+ _handleMessage(message) {
184
+ // 1. Check if it's a response to a pending request
185
+ if (message.request_id && this.pendingRequests.has(message.request_id)) {
186
+ const resolver = this.pendingRequests.get(message.request_id);
187
+ const timeout = this.requestTimeouts.get(message.request_id);
188
+ if (timeout)
189
+ clearTimeout(timeout);
190
+ this.requestTimeouts.delete(message.request_id);
191
+ this.pendingRequests.delete(message.request_id);
192
+ resolver(message.payload, message.error);
193
+ return;
194
+ }
195
+ // 2. Emit corresponding typed events (dispatch once per type)
196
+ const type = message.type || 'unknown';
197
+ this.emit('message', message);
198
+ this.emit(type, message.payload);
199
+ }
200
+ request(type, payload = {}, timeoutMs = 15000) {
201
+ return new Promise((resolve, reject) => {
202
+ if (!this.connected) {
203
+ reject(new Error('Agent WebSocket is not connected'));
204
+ return;
205
+ }
206
+ const requestId = `cli_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
207
+ const message = {
208
+ type,
209
+ payload,
210
+ request_id: requestId,
211
+ timestamp: new Date().toISOString(),
212
+ };
213
+ const timeout = setTimeout(() => {
214
+ this.pendingRequests.delete(requestId);
215
+ this.requestTimeouts.delete(requestId);
216
+ reject(new Error(`Agent request timeout after ${timeoutMs}ms for ${type}`));
217
+ }, timeoutMs);
218
+ this.requestTimeouts.set(requestId, timeout);
219
+ this.pendingRequests.set(requestId, (data, error) => {
220
+ if (error) {
221
+ reject(new Error(error));
222
+ }
223
+ else {
224
+ resolve(data);
225
+ }
226
+ });
227
+ this.ws?.send(JSON.stringify(message));
228
+ });
229
+ }
230
+ send(type, payload = {}) {
231
+ if (!this.connected)
232
+ return false;
233
+ const message = {
234
+ type,
235
+ payload,
236
+ timestamp: new Date().toISOString(),
237
+ };
238
+ try {
239
+ this.ws?.send(JSON.stringify(message));
240
+ return true;
241
+ }
242
+ catch {
243
+ return false;
244
+ }
245
+ }
246
+ // --- High level client operations ---
247
+ sendUserText(text, conversationId, history = [], attachments) {
248
+ const payload = {
249
+ text,
250
+ conversation_id: conversationId,
251
+ conversation_history: history,
252
+ attachments,
253
+ };
254
+ return this.send('USER_TEXT', payload);
255
+ }
256
+ async getTasks() {
257
+ return this.request('GET_TASKS', {});
258
+ }
259
+ async getProviders() {
260
+ return this.request('GET_PROVIDERS', {});
261
+ }
262
+ async getModels(requirements = {}) {
263
+ return this.request('GET_MODELS', { requirements });
264
+ }
265
+ async getSettings() {
266
+ return this.request('GET_SETTINGS', {});
267
+ }
268
+ async saveSettings(settings) {
269
+ return this.request('SAVE_SETTINGS', { settings });
270
+ }
271
+ async validateKey(provider, slot, key) {
272
+ return this.request('VALIDATE_KEY', { provider, slot, key });
273
+ }
274
+ async getSkills() {
275
+ return this.request('GET_SKILLS', {});
276
+ }
277
+ async saveSkill(skill) {
278
+ return this.request('SAVE_SKILL', { skill });
279
+ }
280
+ async deleteSkill(name) {
281
+ return this.request('DELETE_SKILL', { name });
282
+ }
283
+ async getMcpServers() {
284
+ return this.request('GET_MCP_SERVERS', {});
285
+ }
286
+ async getMemoryStatus() {
287
+ return this.request('GET_MEMORY_STATUS', {});
288
+ }
289
+ async clearMemory() {
290
+ return this.request('CLEAR_MEMORY', {});
291
+ }
292
+ cancelTask(taskId) {
293
+ this.send('TASK_CANCEL', { task_id: taskId });
294
+ }
295
+ emergencyStop() {
296
+ this.send('TASK_CANCEL_ALL', {});
297
+ }
298
+ confirmAction(requestId) {
299
+ this.send('CONFIRMATION_GRANTED', { request_id: requestId });
300
+ }
301
+ rejectAction(requestId) {
302
+ this.send('CONFIRMATION_REJECTED', { request_id: requestId });
303
+ }
304
+ close() {
305
+ this.isExplicitlyClosed = true;
306
+ for (const timeout of this.requestTimeouts.values()) {
307
+ clearTimeout(timeout);
308
+ }
309
+ this.requestTimeouts.clear();
310
+ for (const resolver of this.pendingRequests.values()) {
311
+ resolver(undefined, 'Client disconnected');
312
+ }
313
+ this.pendingRequests.clear();
314
+ if (this.ws) {
315
+ this.ws.removeAllListeners();
316
+ this.ws.close();
317
+ this.ws = null;
318
+ }
319
+ this.isConnected = false;
320
+ }
321
+ }
322
+ exports.AgentClient = AgentClient;
@@ -0,0 +1,30 @@
1
+ export declare function getTokenFilePath(): string;
2
+ export declare function getOrGenerateWsToken(): string;
3
+ export declare function isAgentProcessRunning(): boolean;
4
+ export declare function isAgentStarting(): boolean;
5
+ export declare function findPythonAgentScript(): string | null;
6
+ export declare function findPythonExecutable(repoRoot: string): string;
7
+ /**
8
+ * Probes a TCP port quietly without initiating a WebSocket handshake.
9
+ */
10
+ export declare function probePort(port: number, host?: string, timeoutMs?: number): Promise<boolean>;
11
+ /**
12
+ * Probes the WebSocket endpoint with Authorization header.
13
+ * Returns:
14
+ * 'ready': 101 Switching Protocols (authenticated and connected)
15
+ * 'unauthorized': 401 Unauthorized (wrong token / rogue process on port)
16
+ * 'closed': Connection refused / port closed
17
+ */
18
+ export declare function probeWebSocketAuth(port?: number, token?: string, host?: string, timeoutMs?: number): Promise<'ready' | 'unauthorized' | 'closed'>;
19
+ /**
20
+ * Terminates any rogue or orphaned process occupying the specified port.
21
+ */
22
+ export declare function killProcessOnPort(port?: number): void;
23
+ /**
24
+ * Waits until the agent service has completed startup and successfully accepts WebSocket authentication.
25
+ */
26
+ export declare function waitForAgentReady(port?: number, token?: string, maxWaitMs?: number, pollIntervalMs?: number, onProgress?: (elapsedSec: number) => void): Promise<boolean>;
27
+ export declare function spawnAgentDaemon(port?: number): Promise<{
28
+ success: boolean;
29
+ error?: string;
30
+ }>;