vigthoria-cli 1.6.20 → 1.6.22

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.
@@ -0,0 +1,278 @@
1
+ "use strict";
2
+ /**
3
+ * Vigthoria CLI → DevTools Bridge Telemetry Client
4
+ *
5
+ * Connects the local CLI to the remote bridge server in "commando" mode,
6
+ * streaming real-time activity (commands, tool calls, model responses,
7
+ * file edits, errors) and receiving admin-issued commands.
8
+ *
9
+ * Design principles:
10
+ * - Fire-and-forget: never blocks the CLI main flow
11
+ * - Auto-reconnects with exponential back-off
12
+ * - Opt-in via --bridge <url> flag
13
+ * - Sensitive data (API keys, tokens) is never transmitted
14
+ */
15
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
+ desc = { enumerable: true, get: function() { return m[k]; } };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }) : (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ o[k2] = m[k];
25
+ }));
26
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
28
+ }) : function(o, v) {
29
+ o["default"] = v;
30
+ });
31
+ var __importStar = (this && this.__importStar) || (function () {
32
+ var ownKeys = function(o) {
33
+ ownKeys = Object.getOwnPropertyNames || function (o) {
34
+ var ar = [];
35
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
36
+ return ar;
37
+ };
38
+ return ownKeys(o);
39
+ };
40
+ return function (mod) {
41
+ if (mod && mod.__esModule) return mod;
42
+ var result = {};
43
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
44
+ __setModuleDefault(result, mod);
45
+ return result;
46
+ };
47
+ })();
48
+ var __importDefault = (this && this.__importDefault) || function (mod) {
49
+ return (mod && mod.__esModule) ? mod : { "default": mod };
50
+ };
51
+ Object.defineProperty(exports, "__esModule", { value: true });
52
+ exports.BridgeClient = void 0;
53
+ exports.getBridgeClient = getBridgeClient;
54
+ const ws_1 = __importDefault(require("ws"));
55
+ const os = __importStar(require("os"));
56
+ // ── Singleton accessor ───────────────────────────────────────────────
57
+ let _instance = null;
58
+ /** Get the active bridge client (may be null if --bridge was not used). */
59
+ function getBridgeClient() {
60
+ return _instance;
61
+ }
62
+ // ── BridgeClient ─────────────────────────────────────────────────────
63
+ class BridgeClient {
64
+ ws = null;
65
+ url;
66
+ apiKey;
67
+ machineLabel;
68
+ clientId;
69
+ connected = false;
70
+ reconnectTimer = null;
71
+ heartbeatTimer = null;
72
+ queue = []; // buffered events while disconnected
73
+ maxQueueSize = 500;
74
+ reconnectDelay = 2000; // ms, doubles on failure up to 30 s
75
+ destroyed = false;
76
+ onAdminCommand;
77
+ constructor(opts) {
78
+ this.url = opts.bridgeUrl.replace(/\/$/, '');
79
+ if (!this.url.includes('/ws')) {
80
+ this.url = this.url.replace(/^http/, 'ws') + '/ws';
81
+ }
82
+ this.apiKey = opts.apiKey;
83
+ this.machineLabel = opts.machineLabel || os.hostname();
84
+ this.clientId = `cli-${this.machineLabel}-${Date.now().toString(36)}`;
85
+ this.onAdminCommand = opts.onAdminCommand;
86
+ _instance = this;
87
+ }
88
+ // ── Lifecycle ────────────────────────────────────────────────────
89
+ async connect() {
90
+ if (this.destroyed)
91
+ return;
92
+ return new Promise((resolve) => {
93
+ try {
94
+ this.ws = new ws_1.default(this.url, { handshakeTimeout: 8000 });
95
+ this.ws.on('open', () => {
96
+ this.connected = true;
97
+ this.reconnectDelay = 2000;
98
+ // Authenticate as CLI commando client
99
+ this.sendRaw({
100
+ type: 'auth',
101
+ payload: {
102
+ source: 'cli-commando',
103
+ clientId: this.clientId,
104
+ machineLabel: this.machineLabel,
105
+ hostname: os.hostname(),
106
+ platform: os.platform(),
107
+ arch: os.arch(),
108
+ nodeVersion: process.version,
109
+ auth: this.apiKey ? { apiKey: this.apiKey } : undefined,
110
+ },
111
+ });
112
+ // Flush buffered events
113
+ this.flushQueue();
114
+ this.startHeartbeat();
115
+ resolve();
116
+ });
117
+ this.ws.on('message', (raw) => {
118
+ try {
119
+ const msg = JSON.parse(raw.toString());
120
+ if (msg.type === 'admin:command' && this.onAdminCommand) {
121
+ this.onAdminCommand(msg);
122
+ }
123
+ }
124
+ catch {
125
+ // ignore unparseable messages
126
+ }
127
+ });
128
+ this.ws.on('close', () => {
129
+ this.connected = false;
130
+ this.stopHeartbeat();
131
+ this.scheduleReconnect();
132
+ });
133
+ this.ws.on('error', () => {
134
+ this.connected = false;
135
+ this.stopHeartbeat();
136
+ this.scheduleReconnect();
137
+ resolve(); // resolve even on failure – must never block CLI
138
+ });
139
+ }
140
+ catch {
141
+ resolve(); // swallow – bridge is optional
142
+ }
143
+ });
144
+ }
145
+ destroy() {
146
+ this.destroyed = true;
147
+ this.stopHeartbeat();
148
+ if (this.reconnectTimer)
149
+ clearTimeout(this.reconnectTimer);
150
+ if (this.ws) {
151
+ try {
152
+ this.ws.close();
153
+ }
154
+ catch { /* ignore */ }
155
+ }
156
+ this.ws = null;
157
+ this.connected = false;
158
+ if (_instance === this)
159
+ _instance = null;
160
+ }
161
+ get isConnected() {
162
+ return this.connected;
163
+ }
164
+ // ── Telemetry emitters (public API used by CLI code) ─────────────
165
+ /** CLI session started (command, flags, cwd). */
166
+ emitStart(data) {
167
+ this.emit('cli:start', data);
168
+ }
169
+ /** User entered a prompt / message. */
170
+ emitPrompt(data) {
171
+ this.emit('cli:prompt', data);
172
+ }
173
+ /** Model response received (summary only, not full content). */
174
+ emitModelResponse(data) {
175
+ this.emit('cli:model-response', data);
176
+ }
177
+ /** Tool is being called. */
178
+ emitToolCall(data) {
179
+ // Redact sensitive arg values
180
+ const safeArgs = {};
181
+ for (const [k, v] of Object.entries(data.args)) {
182
+ if (/key|token|password|secret|auth/i.test(k)) {
183
+ safeArgs[k] = '***';
184
+ }
185
+ else if (typeof v === 'string' && v.length > 2000) {
186
+ safeArgs[k] = v.slice(0, 200) + `...[${v.length} chars]`;
187
+ }
188
+ else {
189
+ safeArgs[k] = v;
190
+ }
191
+ }
192
+ this.emit('cli:tool-call', { tool: data.tool, args: safeArgs });
193
+ }
194
+ /** Tool finished executing. */
195
+ emitToolResult(data) {
196
+ this.emit('cli:tool-result', data);
197
+ }
198
+ /** File was written or edited. */
199
+ emitFileEdit(data) {
200
+ this.emit('cli:file-edit', data);
201
+ }
202
+ /** Error occurred. */
203
+ emitError(data) {
204
+ this.emit('cli:error', data);
205
+ }
206
+ /** Mode changed (agent / operator / chat). */
207
+ emitModeChange(data) {
208
+ this.emit('cli:mode-change', data);
209
+ }
210
+ /** Session ended. */
211
+ emitEnd(data) {
212
+ this.emit('cli:end', data);
213
+ }
214
+ // ── Internals ────────────────────────────────────────────────────
215
+ emit(type, payload) {
216
+ const event = {
217
+ type,
218
+ payload,
219
+ ts: Date.now(),
220
+ clientId: this.clientId,
221
+ };
222
+ const json = JSON.stringify(event);
223
+ if (this.connected && this.ws?.readyState === ws_1.default.OPEN) {
224
+ try {
225
+ this.ws.send(json);
226
+ }
227
+ catch {
228
+ this.bufferEvent(json);
229
+ }
230
+ }
231
+ else {
232
+ this.bufferEvent(json);
233
+ }
234
+ }
235
+ sendRaw(obj) {
236
+ if (this.ws?.readyState === ws_1.default.OPEN) {
237
+ this.ws.send(JSON.stringify(obj));
238
+ }
239
+ }
240
+ bufferEvent(json) {
241
+ if (this.queue.length >= this.maxQueueSize) {
242
+ this.queue.shift(); // drop oldest
243
+ }
244
+ this.queue.push(json);
245
+ }
246
+ flushQueue() {
247
+ while (this.queue.length > 0 && this.ws?.readyState === ws_1.default.OPEN) {
248
+ const msg = this.queue.shift();
249
+ try {
250
+ this.ws.send(msg);
251
+ }
252
+ catch {
253
+ break;
254
+ }
255
+ }
256
+ }
257
+ scheduleReconnect() {
258
+ if (this.destroyed || this.reconnectTimer)
259
+ return;
260
+ this.reconnectTimer = setTimeout(() => {
261
+ this.reconnectTimer = null;
262
+ this.connect();
263
+ }, this.reconnectDelay);
264
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000);
265
+ }
266
+ startHeartbeat() {
267
+ this.heartbeatTimer = setInterval(() => {
268
+ this.emit('cli:heartbeat', { uptime: process.uptime() });
269
+ }, 30000);
270
+ }
271
+ stopHeartbeat() {
272
+ if (this.heartbeatTimer) {
273
+ clearInterval(this.heartbeatTimer);
274
+ this.heartbeatTimer = null;
275
+ }
276
+ }
277
+ }
278
+ exports.BridgeClient = BridgeClient;
@@ -5,7 +5,15 @@ import { type Options as OraOptions, type Ora } from 'ora';
5
5
  export type { Ora };
6
6
  /**
7
7
  * Create an ora spinner that writes to stderr so it never
8
- * pollutes stdout JSON output or triggers PowerShell error styling.
8
+ * pollutes stdout JSON output.
9
+ *
10
+ * IMPORTANT: On Windows PowerShell, any stderr output triggers
11
+ * NativeCommandError styling. The spinner animation itself is
12
+ * tolerable, but `spinner.fail(msg)` writes the message to stderr
13
+ * which produces ugly red PowerShell errors.
14
+ *
15
+ * Prefer: spinner.stop() then Logger.error(msg) — which writes to
16
+ * stdout — instead of spinner.fail(msg).
9
17
  */
10
18
  export declare function createSpinner(textOrOpts: string | OraOptions): Ora;
11
19
  export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'success';
@@ -12,7 +12,15 @@ const chalk_1 = __importDefault(require("chalk"));
12
12
  const ora_1 = __importDefault(require("ora"));
13
13
  /**
14
14
  * Create an ora spinner that writes to stderr so it never
15
- * pollutes stdout JSON output or triggers PowerShell error styling.
15
+ * pollutes stdout JSON output.
16
+ *
17
+ * IMPORTANT: On Windows PowerShell, any stderr output triggers
18
+ * NativeCommandError styling. The spinner animation itself is
19
+ * tolerable, but `spinner.fail(msg)` writes the message to stderr
20
+ * which produces ugly red PowerShell errors.
21
+ *
22
+ * Prefer: spinner.stop() then Logger.error(msg) — which writes to
23
+ * stdout — instead of spinner.fail(msg).
16
24
  */
17
25
  function createSpinner(textOrOpts) {
18
26
  const opts = typeof textOrOpts === 'string' ? { text: textOrOpts } : textOrOpts;
@@ -35,7 +43,10 @@ class Logger {
35
43
  console.log(chalk_1.default.yellow('⚠'), ...args);
36
44
  }
37
45
  error(...args) {
38
- console.error(chalk_1.default.red('✗'), ...args);
46
+ // Write error messages to stdout (not stderr) to avoid triggering
47
+ // PowerShell NativeCommandError styling. The red ✗ prefix already
48
+ // signals an error visually; stderr redirection is unnecessary.
49
+ console.log(chalk_1.default.red('✗'), ...args);
39
50
  }
40
51
  success(...args) {
41
52
  console.log(chalk_1.default.green('✓'), ...args);
@@ -356,6 +356,10 @@ class AgenticTools {
356
356
  * Execute a tool call with enhanced error handling and retry logic
357
357
  */
358
358
  async execute(call) {
359
+ // Guard against malformed tool calls with undefined or empty tool names
360
+ if (!call.tool || typeof call.tool !== 'string' || !call.tool.trim()) {
361
+ return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Invalid tool call: tool name is ${call.tool === undefined ? 'undefined' : 'empty'}`, `Provide a valid tool name. Available tools: ${AgenticTools.getToolDefinitions().map(t => t.name).join(', ')}`);
362
+ }
359
363
  const normalizedCall = this.normalizeToolCall(call);
360
364
  const tool = AgenticTools.getToolDefinitions().find(t => t.name === normalizedCall.tool);
361
365
  if (!tool) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigthoria-cli",
3
- "version": "1.6.20",
3
+ "version": "1.6.22",
4
4
  "description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
5
5
  "main": "dist/index.js",
6
6
  "files": [
@@ -21,6 +21,7 @@
21
21
  "dev": "ts-node src/index.ts",
22
22
  "test": "npm run test:cli",
23
23
  "test:cli": "npm run build && node scripts/test-cli-suite.js",
24
+ "test:regression": "npm run build && node scripts/test-regression-1.6.22.js",
24
25
  "test:agent:smoke": "npm run build && node scripts/test-agent-smoke.js",
25
26
  "test:agent:routing": "npm run build && node scripts/test-agent-routing-policy.js",
26
27
  "test:agent:context": "npm run build && node scripts/test-agent-context-trace-e2e.js",