@yu_robotics/remote-cli 1.6.34 → 1.6.36

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 (35) hide show
  1. package/README.md +5 -5
  2. package/dist/client/MessageHandler.d.ts +3 -1
  3. package/dist/client/MessageHandler.d.ts.map +1 -1
  4. package/dist/client/MessageHandler.js +34 -18
  5. package/dist/client/MessageHandler.js.map +1 -1
  6. package/dist/commands/start.d.ts.map +1 -1
  7. package/dist/commands/start.js +9 -2
  8. package/dist/commands/start.js.map +1 -1
  9. package/dist/executor/PiExecutor.d.ts +80 -0
  10. package/dist/executor/PiExecutor.d.ts.map +1 -0
  11. package/dist/executor/PiExecutor.js +893 -0
  12. package/dist/executor/PiExecutor.js.map +1 -0
  13. package/dist/executor/index.d.ts +2 -1
  14. package/dist/executor/index.d.ts.map +1 -1
  15. package/dist/executor/index.js +16 -2
  16. package/dist/executor/index.js.map +1 -1
  17. package/dist/executor/pi/PiClient.d.ts +39 -0
  18. package/dist/executor/pi/PiClient.d.ts.map +1 -0
  19. package/dist/executor/pi/PiClient.js +243 -0
  20. package/dist/executor/pi/PiClient.js.map +1 -0
  21. package/dist/executor/pi/PiTypes.d.ts +59 -0
  22. package/dist/executor/pi/PiTypes.d.ts.map +1 -0
  23. package/dist/executor/pi/PiTypes.js +72 -0
  24. package/dist/executor/pi/PiTypes.js.map +1 -0
  25. package/dist/thread/ThreadExecutorPool.d.ts +1 -1
  26. package/dist/thread/ThreadExecutorPool.d.ts.map +1 -1
  27. package/dist/thread/ThreadExecutorPool.js +9 -2
  28. package/dist/thread/ThreadExecutorPool.js.map +1 -1
  29. package/dist/thread/types.d.ts +2 -0
  30. package/dist/thread/types.d.ts.map +1 -1
  31. package/dist/types/config.d.ts +14 -2
  32. package/dist/types/config.d.ts.map +1 -1
  33. package/dist/types/config.js +2 -0
  34. package/dist/types/config.js.map +1 -1
  35. package/package.json +1 -1
@@ -0,0 +1,893 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.PiExecutor = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const os = __importStar(require("os"));
39
+ const path = __importStar(require("path"));
40
+ const PiClient_1 = require("./pi/PiClient");
41
+ const PiTypes_1 = require("./pi/PiTypes");
42
+ const DEFAULT_TURN_TIMEOUT_MS = 10 * 60 * 1000;
43
+ const COMPACT_TIMEOUT_MS = 10 * 60 * 1000;
44
+ /** Grace period to distinguish immediate slash/extension commands from a real agent turn. */
45
+ const IMMEDIATE_SETTLE_MS = 750;
46
+ const INSTALL_COMMAND = 'npm install -g --ignore-scripts @earendil-works/pi-coding-agent';
47
+ function mapPiTool(toolName, args) {
48
+ const input = args && typeof args === 'object' ? args : {};
49
+ switch (toolName) {
50
+ case 'bash':
51
+ return { name: 'Bash', input: { command: input.command ?? input.cmd ?? '' } };
52
+ case 'read':
53
+ return { name: 'Read', input: { file_path: input.path ?? input.file_path ?? '' } };
54
+ case 'edit':
55
+ return { name: 'Edit', input: { file_path: input.path ?? input.file_path ?? '' } };
56
+ case 'write':
57
+ return { name: 'Write', input: { file_path: input.path ?? input.file_path ?? '' } };
58
+ case 'grep':
59
+ return { name: 'Grep', input };
60
+ case 'find':
61
+ return { name: 'Glob', input };
62
+ default:
63
+ return { name: toolName, input };
64
+ }
65
+ }
66
+ function extractMessageText(message) {
67
+ if (!message)
68
+ return '';
69
+ if (typeof message.content === 'string')
70
+ return message.content;
71
+ if (Array.isArray(message.content)) {
72
+ return message.content.map((block) => {
73
+ if (typeof block === 'string')
74
+ return block;
75
+ if (block?.type === 'text')
76
+ return String(block.text ?? '');
77
+ return '';
78
+ }).join('');
79
+ }
80
+ return '';
81
+ }
82
+ function extractAgentError(event) {
83
+ if (typeof event.finalError === 'string' && event.finalError.trim()) {
84
+ return event.finalError.trim();
85
+ }
86
+ const messages = Array.isArray(event.messages) ? event.messages : [];
87
+ for (const message of messages) {
88
+ if (message?.role !== 'assistant')
89
+ continue;
90
+ if (message.stopReason !== 'error' && typeof message.errorMessage !== 'string')
91
+ continue;
92
+ const text = typeof message.errorMessage === 'string' && message.errorMessage.trim()
93
+ ? message.errorMessage.trim()
94
+ : extractMessageText(message).trim();
95
+ return text || 'Pi agent ended with an error';
96
+ }
97
+ return undefined;
98
+ }
99
+ function defaultPiThinkingLevel(levels) {
100
+ const available = Array.isArray(levels) ? levels.map((level) => String(level)) : [];
101
+ if (available.includes('medium'))
102
+ return 'medium';
103
+ const preferred = available.find((level) => level !== 'off' && (0, PiTypes_1.isPiThinkingLevel)(level));
104
+ if (preferred)
105
+ return preferred;
106
+ if (available[0] && (0, PiTypes_1.isPiThinkingLevel)(available[0]))
107
+ return available[0];
108
+ return 'medium';
109
+ }
110
+ function toolResultText(result) {
111
+ if (!result)
112
+ return '';
113
+ if (typeof result === 'string')
114
+ return result;
115
+ const content = result.content;
116
+ if (typeof content === 'string')
117
+ return content;
118
+ if (Array.isArray(content)) {
119
+ return content.map((block) => {
120
+ if (typeof block === 'string')
121
+ return block;
122
+ if (block?.type === 'text')
123
+ return String(block.text ?? '');
124
+ return JSON.stringify(block);
125
+ }).join('');
126
+ }
127
+ return JSON.stringify(result);
128
+ }
129
+ /**
130
+ * Persistent Pi executor backed by `pi --mode rpc`.
131
+ * One instance is owned by one remote-cli thread.
132
+ */
133
+ class PiExecutor {
134
+ directoryGuard;
135
+ currentWorkingDirectory;
136
+ model;
137
+ effort;
138
+ provider;
139
+ autoApprove;
140
+ threadId;
141
+ sessionFilePath;
142
+ sessionStoreDir;
143
+ piCommand;
144
+ createClient;
145
+ client;
146
+ unsubscribe = null;
147
+ sessionId = null;
148
+ sessionFile = null;
149
+ activeTurn = null;
150
+ pendingUi = null;
151
+ destroyed = false;
152
+ constructor(directoryGuard, options = {}) {
153
+ this.directoryGuard = directoryGuard;
154
+ this.model = options.model;
155
+ this.effort = options.effort && options.effort !== 'auto' ? options.effort : undefined;
156
+ this.provider = options.provider;
157
+ this.autoApprove = options.autoApprove ?? true;
158
+ this.threadId = options.threadId;
159
+ this.piCommand = options.piCommand;
160
+ if (options.initialWorkingDirectory) {
161
+ try {
162
+ this.currentWorkingDirectory = directoryGuard.resolveWorkingDirectory(options.initialWorkingDirectory);
163
+ }
164
+ catch (error) {
165
+ console.warn(`[PiExecutor] Failed to use initial working directory: ${options.initialWorkingDirectory}`, error);
166
+ this.currentWorkingDirectory = process.cwd();
167
+ }
168
+ }
169
+ else {
170
+ this.currentWorkingDirectory = process.cwd();
171
+ }
172
+ const baseDir = options.sessionBaseDir
173
+ ?? path.join(process.env.HOME || os.homedir(), '.remote-cli', 'pi-sessions');
174
+ fs.mkdirSync(baseDir, { recursive: true });
175
+ this.sessionStoreDir = path.join(baseDir, 'store');
176
+ fs.mkdirSync(this.sessionStoreDir, { recursive: true });
177
+ this.sessionFilePath = this.threadId
178
+ ? path.join(baseDir, `${this.threadId}.json`)
179
+ : path.join(this.currentWorkingDirectory, '.pi-session');
180
+ this.loadPointer();
181
+ this.createClient = options.clientFactory ?? ((launch) => new PiClient_1.PiClient(launch));
182
+ this.client = this.createClient(this.buildLaunch());
183
+ this.unsubscribe = this.client.onEvent((event) => this.handleEvent(event));
184
+ }
185
+ async execute(prompt, options = {}) {
186
+ if (this.destroyed)
187
+ throw new Error('Executor has been destroyed');
188
+ if (this.activeTurn) {
189
+ return { success: false, error: 'Pi thread is already running a command' };
190
+ }
191
+ const trimmed = prompt.trim();
192
+ if (trimmed === '/skills' || trimmed === '/skill') {
193
+ return this.listSkills();
194
+ }
195
+ if (trimmed.startsWith('/')) {
196
+ const validationError = await this.validateSlashCommand(trimmed);
197
+ if (validationError)
198
+ return { success: false, error: validationError };
199
+ }
200
+ try {
201
+ await this.ensureClient();
202
+ }
203
+ catch (error) {
204
+ return { success: false, error: this.friendlyError(error) };
205
+ }
206
+ return new Promise((resolve) => {
207
+ const active = {
208
+ options,
209
+ resolve,
210
+ output: [],
211
+ sawAgentStart: false,
212
+ };
213
+ if (options.timeout && options.timeout > 0) {
214
+ active.timeoutTimer = setTimeout(() => {
215
+ void this.failActive(`Command timed out after ${options.timeout}ms`);
216
+ }, options.timeout);
217
+ }
218
+ else {
219
+ active.timeoutTimer = setTimeout(() => {
220
+ void this.failActive(`Command timed out after ${DEFAULT_TURN_TIMEOUT_MS}ms`);
221
+ }, DEFAULT_TURN_TIMEOUT_MS);
222
+ }
223
+ this.activeTurn = active;
224
+ const settled = new Promise((settle) => {
225
+ active.settle = settle;
226
+ });
227
+ const images = this.toImages(options.attachments);
228
+ void this.client.request({
229
+ type: 'prompt',
230
+ message: prompt,
231
+ ...(images.length > 0 ? { images } : {}),
232
+ }).then(async (response) => {
233
+ if (this.activeTurn !== active)
234
+ return;
235
+ if (!response.success) {
236
+ this.completeActive({ success: false, error: response.error || 'Pi rejected the prompt' });
237
+ return;
238
+ }
239
+ const outcome = await Promise.race([
240
+ settled.then(() => 'settled'),
241
+ sleep(IMMEDIATE_SETTLE_MS).then(() => 'grace'),
242
+ ]);
243
+ if (this.activeTurn !== active)
244
+ return;
245
+ if (outcome === 'grace' && !active.sawAgentStart && !active.error) {
246
+ const streaming = await this.isSessionStreaming();
247
+ if (this.activeTurn !== active)
248
+ return;
249
+ if (!streaming && !active.sawAgentStart) {
250
+ const text = active.output.join('') || await this.lastAssistantText();
251
+ this.completeActive({ success: true, output: text, sessionAbbr: this.sessionAbbr() });
252
+ return;
253
+ }
254
+ }
255
+ await settled;
256
+ if (this.activeTurn !== active)
257
+ return;
258
+ let output = active.output.join('');
259
+ if (!output && !active.error) {
260
+ output = await this.lastAssistantText();
261
+ }
262
+ if (!output && !active.error) {
263
+ active.error = active.lastRetryError || 'Pi completed the turn without a text response';
264
+ }
265
+ this.completeActive({
266
+ success: !active.error,
267
+ output,
268
+ error: active.error,
269
+ sessionAbbr: this.sessionAbbr(),
270
+ });
271
+ }).catch((error) => {
272
+ if (this.activeTurn === active) {
273
+ this.completeActive({ success: false, error: this.friendlyError(error) });
274
+ }
275
+ });
276
+ });
277
+ }
278
+ getCurrentWorkingDirectory() {
279
+ return this.currentWorkingDirectory;
280
+ }
281
+ async setWorkingDirectory(targetPath) {
282
+ const resolved = this.directoryGuard.resolveWorkingDirectory(targetPath, this.currentWorkingDirectory);
283
+ if (resolved === this.currentWorkingDirectory)
284
+ return;
285
+ this.currentWorkingDirectory = resolved;
286
+ // A Pi session owns the cwd recorded in its session header. Reopening the
287
+ // previous session file would silently keep tools in the old directory.
288
+ this.clearPointer();
289
+ this.client.updateLaunch?.(this.buildLaunch());
290
+ await this.recycleClient();
291
+ }
292
+ resetContext() {
293
+ if (this.destroyed)
294
+ return;
295
+ this.cancelPendingUi(true);
296
+ if (this.activeTurn) {
297
+ this.completeActive({
298
+ success: false,
299
+ error: 'Conversation cleared by user',
300
+ sessionAbbr: this.sessionAbbr(),
301
+ });
302
+ }
303
+ this.forgetSession();
304
+ this.replaceClient();
305
+ }
306
+ async abort() {
307
+ if (!this.client.isRunning())
308
+ return false;
309
+ try {
310
+ await this.client.request({ type: 'clear_queue' });
311
+ }
312
+ catch {
313
+ // Queue may already be empty.
314
+ }
315
+ try {
316
+ await this.client.request({ type: 'abort' });
317
+ }
318
+ catch {
319
+ await this.recycleClient();
320
+ }
321
+ if (this.activeTurn) {
322
+ this.completeActive({ success: false, error: 'Aborted', sessionAbbr: this.sessionAbbr() });
323
+ }
324
+ this.cancelPendingUi(true);
325
+ return true;
326
+ }
327
+ async destroy() {
328
+ this.destroyed = true;
329
+ this.cancelPendingUi(true);
330
+ if (this.activeTurn) {
331
+ this.completeActive({ success: false, error: 'Executor destroyed' });
332
+ }
333
+ this.unsubscribe?.();
334
+ this.unsubscribe = null;
335
+ await this.client.stop();
336
+ }
337
+ isWaitingInput() {
338
+ return this.pendingUi !== null;
339
+ }
340
+ sendInput(input) {
341
+ if (!this.pendingUi)
342
+ return false;
343
+ const pending = this.pendingUi;
344
+ const trimmed = input.trim();
345
+ if (!trimmed)
346
+ return false;
347
+ this.pendingUi = null;
348
+ if (pending.method === 'confirm') {
349
+ if (/^(n|no|cancel|false)$/i.test(trimmed)) {
350
+ this.client.send({ type: 'extension_ui_response', id: pending.id, cancelled: true });
351
+ }
352
+ else {
353
+ this.client.send({ type: 'extension_ui_response', id: pending.id, confirmed: true });
354
+ }
355
+ return true;
356
+ }
357
+ if (/^(skip|cancel)$/i.test(trimmed)) {
358
+ this.client.send({ type: 'extension_ui_response', id: pending.id, cancelled: true });
359
+ return true;
360
+ }
361
+ const options = pending.options ?? [];
362
+ const index = Number.parseInt(trimmed, 10);
363
+ if (!Number.isNaN(index) && index >= 1 && index <= options.length) {
364
+ this.client.send({ type: 'extension_ui_response', id: pending.id, value: options[index - 1] });
365
+ return true;
366
+ }
367
+ const match = options.find((option) => option.toLowerCase() === trimmed.toLowerCase());
368
+ this.client.send({ type: 'extension_ui_response', id: pending.id, value: match ?? trimmed });
369
+ return true;
370
+ }
371
+ async compact(onStream) {
372
+ return this.compactWhenFull(onStream);
373
+ }
374
+ async compactWhenFull(onStream) {
375
+ if (this.activeTurn) {
376
+ return { success: false, error: 'Cannot compact while a Pi command is running' };
377
+ }
378
+ try {
379
+ await this.ensureClient();
380
+ onStream?.('Compressing Pi conversation...\n');
381
+ const response = await this.client.request({ type: 'compact' }, COMPACT_TIMEOUT_MS);
382
+ if (!response.success) {
383
+ return { success: false, error: response.error || 'Pi compaction failed' };
384
+ }
385
+ const summary = typeof response.data?.summary === 'string' ? response.data.summary : '';
386
+ if (summary)
387
+ onStream?.(summary);
388
+ await this.refreshSessionPointer();
389
+ return { success: true, output: summary, sessionAbbr: this.sessionAbbr() };
390
+ }
391
+ catch (error) {
392
+ return { success: false, error: this.friendlyError(error) };
393
+ }
394
+ }
395
+ async setModel(model) {
396
+ const trimmed = model.trim();
397
+ if (!trimmed)
398
+ return { success: false, error: 'Model name is required' };
399
+ try {
400
+ await this.ensureClient();
401
+ const resolved = await this.resolveModel(trimmed);
402
+ if (!resolved) {
403
+ return { success: false, error: `Unknown Pi model: ${trimmed}` };
404
+ }
405
+ const response = await this.client.request({
406
+ type: 'set_model',
407
+ provider: resolved.provider,
408
+ modelId: resolved.modelId,
409
+ });
410
+ if (!response.success) {
411
+ return { success: false, error: response.error || `Failed to set Pi model: ${trimmed}` };
412
+ }
413
+ this.model = (0, PiTypes_1.formatPiModelRef)({ id: resolved.modelId, provider: resolved.provider });
414
+ this.client.updateLaunch?.({ model: this.model, provider: resolved.provider });
415
+ return { success: true, output: `Model set to ${this.model}.` };
416
+ }
417
+ catch (error) {
418
+ return { success: false, error: this.friendlyError(error) };
419
+ }
420
+ }
421
+ async clearModel() {
422
+ this.model = undefined;
423
+ this.client.updateLaunch?.({ model: undefined, provider: this.provider });
424
+ if (!this.client.isRunning())
425
+ return;
426
+ try {
427
+ const models = await this.listModels();
428
+ const fallback = models.find((model) => model.isDefault) ?? models[0];
429
+ if (!fallback) {
430
+ await this.recycleClient();
431
+ return;
432
+ }
433
+ const parsed = (0, PiTypes_1.parsePiModelRef)(fallback.id);
434
+ const response = await this.client.request({
435
+ type: 'set_model',
436
+ provider: parsed.provider,
437
+ modelId: parsed.modelId,
438
+ });
439
+ if (!response.success) {
440
+ await this.recycleClient();
441
+ }
442
+ }
443
+ catch {
444
+ await this.recycleClient();
445
+ }
446
+ }
447
+ async listModels() {
448
+ await this.ensureClient();
449
+ const response = await this.client.request({ type: 'get_available_models' });
450
+ if (!response.success) {
451
+ throw new Error(response.error || 'Failed to list Pi models');
452
+ }
453
+ const models = Array.isArray(response.data?.models) ? response.data.models : [];
454
+ return models.map((model, index) => ({
455
+ id: (0, PiTypes_1.formatPiModelRef)(model),
456
+ displayName: model.name || model.id,
457
+ isDefault: index === 0,
458
+ supportedReasoningEfforts: model.reasoning ? [...PiTypes_1.PI_THINKING_LEVELS] : ['off'],
459
+ inputModalities: model.input,
460
+ }));
461
+ }
462
+ async setEffort(effort) {
463
+ const trimmed = effort.trim().toLowerCase();
464
+ if (trimmed === 'auto') {
465
+ this.effort = undefined;
466
+ this.client.updateLaunch?.({ thinking: undefined });
467
+ if (this.client.isRunning()) {
468
+ try {
469
+ const available = await this.client.request({ type: 'get_available_thinking_levels' });
470
+ const level = defaultPiThinkingLevel(available.data?.levels);
471
+ const response = await this.client.request({ type: 'set_thinking_level', level });
472
+ if (!response.success) {
473
+ return { success: false, error: response.error || 'Failed to restore Pi thinking level' };
474
+ }
475
+ }
476
+ catch (error) {
477
+ return { success: false, error: this.friendlyError(error) };
478
+ }
479
+ }
480
+ return { success: true, output: 'Reasoning effort set to auto.' };
481
+ }
482
+ if (!(0, PiTypes_1.isPiThinkingLevel)(trimmed)) {
483
+ return {
484
+ success: false,
485
+ error: `Unsupported Pi thinking level: ${effort}. Use auto, ${PiTypes_1.PI_THINKING_LEVELS.join(', ')}.`,
486
+ };
487
+ }
488
+ try {
489
+ await this.ensureClient();
490
+ const response = await this.client.request({ type: 'set_thinking_level', level: trimmed });
491
+ if (!response.success) {
492
+ return { success: false, error: response.error || `Failed to set thinking level: ${trimmed}` };
493
+ }
494
+ this.effort = trimmed;
495
+ this.client.updateLaunch?.({ thinking: trimmed });
496
+ return { success: true, output: `Reasoning effort set to ${trimmed}.` };
497
+ }
498
+ catch (error) {
499
+ return { success: false, error: this.friendlyError(error) };
500
+ }
501
+ }
502
+ isProcessRunning() {
503
+ return this.client.isRunning();
504
+ }
505
+ getSessionId() {
506
+ return this.sessionId;
507
+ }
508
+ async deleteThreadData(threadId) {
509
+ const pointerPath = path.join(path.dirname(this.sessionFilePath), `${threadId}.json`);
510
+ let sessionFile = this.sessionFile;
511
+ try {
512
+ const stored = JSON.parse(fs.readFileSync(pointerPath, 'utf8'));
513
+ sessionFile = stored.sessionFile ?? sessionFile;
514
+ }
515
+ catch {
516
+ // Pointer may already be gone.
517
+ }
518
+ try {
519
+ fs.unlinkSync(pointerPath);
520
+ }
521
+ catch { /* ignore */ }
522
+ if (sessionFile) {
523
+ try {
524
+ fs.unlinkSync(sessionFile);
525
+ }
526
+ catch { /* ignore */ }
527
+ }
528
+ if (threadId === this.threadId) {
529
+ this.sessionId = null;
530
+ this.sessionFile = null;
531
+ }
532
+ }
533
+ async listSkills() {
534
+ try {
535
+ await this.ensureClient();
536
+ const response = await this.client.request({ type: 'get_commands' });
537
+ if (!response.success) {
538
+ return { success: false, error: response.error || 'Failed to list Pi commands' };
539
+ }
540
+ const commands = Array.isArray(response.data?.commands) ? response.data.commands : [];
541
+ const skills = commands.filter((command) => command?.source === 'skill');
542
+ if (skills.length === 0) {
543
+ return { success: true, output: '🧩 No Pi skills were discovered.\n\nSkills live under ~/.pi/agent/skills, ~/.agents/skills, .pi/skills, and .agents/skills. Invoke them as /skill:name.' };
544
+ }
545
+ const lines = skills.map((skill) => `- /${skill.name}${skill.description ? ` — ${skill.description}` : ''}`);
546
+ return { success: true, output: `🧩 Available Pi skills:\n${lines.join('\n')}\n\nInvoke with /skill:name` };
547
+ }
548
+ catch (error) {
549
+ return { success: false, error: this.friendlyError(error) };
550
+ }
551
+ }
552
+ async validateSlashCommand(prompt) {
553
+ try {
554
+ await this.ensureClient();
555
+ const response = await this.client.request({ type: 'get_commands' });
556
+ if (!response.success)
557
+ return response.error || 'Failed to query Pi commands';
558
+ const commandName = prompt.slice(1).split(/\s+/, 1)[0];
559
+ const commands = Array.isArray(response.data?.commands) ? response.data.commands : [];
560
+ if (commands.some((command) => command?.name === commandName))
561
+ return undefined;
562
+ return `Pi RPC does not expose /${commandName}. Only extension commands, prompt templates, and skills returned by /skills can be forwarded.`;
563
+ }
564
+ catch (error) {
565
+ return this.friendlyError(error);
566
+ }
567
+ }
568
+ async ensureClient() {
569
+ if (!this.client.isRunning()) {
570
+ this.client.updateLaunch?.(this.buildLaunch());
571
+ await this.client.start();
572
+ await this.refreshSessionPointer();
573
+ }
574
+ }
575
+ async recycleClient() {
576
+ await this.client.stop();
577
+ }
578
+ replaceClient() {
579
+ this.unsubscribe?.();
580
+ this.unsubscribe = null;
581
+ const previous = this.client;
582
+ this.client = this.createClient(this.buildLaunch());
583
+ this.unsubscribe = this.client.onEvent((event) => this.handleEvent(event));
584
+ void previous.stop().catch((error) => {
585
+ console.warn('[PiExecutor] Failed to stop previous Pi RPC process', error);
586
+ });
587
+ }
588
+ forgetSession() {
589
+ const sessionFile = this.sessionFile;
590
+ this.clearPointer();
591
+ if (sessionFile) {
592
+ try {
593
+ fs.unlinkSync(sessionFile);
594
+ }
595
+ catch { /* ignore */ }
596
+ }
597
+ }
598
+ buildLaunch() {
599
+ const pointerFile = this.sessionFile && fs.existsSync(this.sessionFile) ? this.sessionFile : undefined;
600
+ return {
601
+ command: this.piCommand,
602
+ cwd: this.currentWorkingDirectory,
603
+ approveProject: this.autoApprove,
604
+ sessionDir: this.sessionStoreDir,
605
+ sessionId: this.sessionId ?? this.threadId,
606
+ sessionFile: pointerFile,
607
+ sessionName: this.threadId ? `remote-cli-${this.threadId}` : undefined,
608
+ provider: this.provider,
609
+ model: this.model,
610
+ thinking: this.effort,
611
+ };
612
+ }
613
+ handleEvent(event) {
614
+ if (event.type === 'client_disconnected') {
615
+ if (this.activeTurn) {
616
+ this.completeActive({ success: false, error: event.error || 'Pi RPC process disconnected' });
617
+ }
618
+ return;
619
+ }
620
+ if (event.type === 'extension_ui_request') {
621
+ void this.handleUiRequest(event);
622
+ return;
623
+ }
624
+ const active = this.activeTurn;
625
+ if (!active)
626
+ return;
627
+ if (event.type === 'agent_start') {
628
+ active.sawAgentStart = true;
629
+ return;
630
+ }
631
+ if (event.type === 'agent_end') {
632
+ const error = extractAgentError(event);
633
+ if (error) {
634
+ if (event.willRetry)
635
+ active.lastRetryError = error;
636
+ else
637
+ active.error = error;
638
+ }
639
+ if (!error && active.output.join('') === '') {
640
+ const text = (Array.isArray(event.messages) ? event.messages : [])
641
+ .filter((message) => message?.role === 'assistant')
642
+ .map(extractMessageText)
643
+ .join('');
644
+ if (text) {
645
+ active.output.push(text);
646
+ active.options.onStream?.(text);
647
+ }
648
+ }
649
+ return;
650
+ }
651
+ if (event.type === 'agent_settled') {
652
+ active.settle?.();
653
+ return;
654
+ }
655
+ if (event.type === 'message_update') {
656
+ const delta = event.assistantMessageEvent;
657
+ if (delta?.type === 'text_delta' && typeof delta.delta === 'string') {
658
+ active.output.push(delta.delta);
659
+ active.options.onStream?.(delta.delta);
660
+ }
661
+ return;
662
+ }
663
+ if (event.type === 'message_end' && event.message?.role === 'assistant') {
664
+ const text = extractMessageText(event.message);
665
+ if (text && active.output.join('') === '') {
666
+ active.output.push(text);
667
+ active.options.onStream?.(text);
668
+ }
669
+ return;
670
+ }
671
+ if (event.type === 'turn_end') {
672
+ const text = extractMessageText(event.message);
673
+ if (text && active.output.join('') === '') {
674
+ active.output.push(text);
675
+ active.options.onStream?.(text);
676
+ }
677
+ return;
678
+ }
679
+ if (event.type === 'tool_execution_start') {
680
+ const mapped = mapPiTool(String(event.toolName ?? 'tool'), event.args);
681
+ active.options.onToolUse?.({
682
+ id: String(event.toolCallId ?? mapped.name),
683
+ name: mapped.name,
684
+ input: mapped.input,
685
+ });
686
+ return;
687
+ }
688
+ if (event.type === 'tool_execution_end') {
689
+ active.options.onToolResult?.({
690
+ tool_use_id: String(event.toolCallId ?? ''),
691
+ content: toolResultText(event.result),
692
+ is_error: Boolean(event.isError),
693
+ });
694
+ return;
695
+ }
696
+ if (event.type === 'extension_error') {
697
+ const message = typeof event.error === 'string' && event.error.trim()
698
+ ? event.error.trim()
699
+ : 'Pi extension failed';
700
+ active.error = message;
701
+ return;
702
+ }
703
+ if (event.type === 'auto_retry_start' && event.errorMessage) {
704
+ active.lastRetryError = String(event.errorMessage);
705
+ return;
706
+ }
707
+ if (event.type === 'auto_retry_end') {
708
+ if (event.success === false) {
709
+ active.error = String(event.finalError || active.lastRetryError || 'Pi retry failed');
710
+ }
711
+ else {
712
+ active.lastRetryError = undefined;
713
+ }
714
+ }
715
+ }
716
+ async handleUiRequest(event) {
717
+ const id = String(event.id ?? '');
718
+ const method = String(event.method ?? '');
719
+ if (!id || !method)
720
+ return;
721
+ const fireAndForget = ['notify', 'setStatus', 'setWidget', 'setTitle', 'set_editor_text'];
722
+ if (fireAndForget.includes(method)) {
723
+ if (method === 'notify' && event.message) {
724
+ this.activeTurn?.options.onStream?.(`\n${event.message}\n`);
725
+ }
726
+ return;
727
+ }
728
+ const options = Array.isArray(event.options) ? event.options.map((option) => String(option)) : [];
729
+ const title = String(event.title || event.message || 'Pi needs input');
730
+ // Pi extension dialogs are application-defined. A confirm request may be
731
+ // destructive (for example, clearing a session), so project trust must not
732
+ // be treated as permission to accept arbitrary extension UI requests.
733
+ if (!this.activeTurn) {
734
+ this.client.send({ type: 'extension_ui_response', id, cancelled: true });
735
+ return;
736
+ }
737
+ this.cancelPendingUi(true);
738
+ let lines;
739
+ if (method === 'confirm') {
740
+ lines = [`\n${title}`, 'Reply yes or no.\n'];
741
+ }
742
+ else if (method === 'select') {
743
+ lines = [`\n${title}`, ...options.map((option, index) => `${index + 1}. ${option}`), 'Reply with an option number, option name, skip, or cancel.\n'];
744
+ }
745
+ else {
746
+ lines = [`\n${title}`, 'Reply with a value, or send skip or cancel.\n'];
747
+ }
748
+ this.activeTurn?.options.onStream?.(lines.join('\n'));
749
+ this.pendingUi = { id, method, title, options };
750
+ }
751
+ async resolveModel(value) {
752
+ const parsed = (0, PiTypes_1.parsePiModelRef)(value);
753
+ try {
754
+ const models = await this.listModels();
755
+ const exact = models.find((model) => model.id === value || model.id === (0, PiTypes_1.formatPiModelRef)({ id: parsed.modelId, provider: parsed.provider }));
756
+ if (exact)
757
+ return (0, PiTypes_1.parsePiModelRef)(exact.id);
758
+ const byId = models.find((model) => (0, PiTypes_1.parsePiModelRef)(model.id).modelId === parsed.modelId);
759
+ if (byId)
760
+ return (0, PiTypes_1.parsePiModelRef)(byId.id);
761
+ }
762
+ catch {
763
+ // Fall through to the caller-supplied ref when the catalog is unavailable.
764
+ }
765
+ return parsed.modelId ? parsed : null;
766
+ }
767
+ async isSessionStreaming() {
768
+ try {
769
+ const response = await this.client.request({ type: 'get_state' });
770
+ return Boolean(response.data?.isStreaming);
771
+ }
772
+ catch {
773
+ return false;
774
+ }
775
+ }
776
+ async lastAssistantText() {
777
+ try {
778
+ const response = await this.client.request({ type: 'get_last_assistant_text' });
779
+ return typeof response.data?.text === 'string' ? response.data.text : '';
780
+ }
781
+ catch {
782
+ return '';
783
+ }
784
+ }
785
+ async refreshSessionPointer() {
786
+ try {
787
+ const response = await this.client.request({ type: 'get_state' });
788
+ if (response.success && response.data) {
789
+ if (typeof response.data.sessionId === 'string')
790
+ this.sessionId = response.data.sessionId;
791
+ if (typeof response.data.sessionFile === 'string')
792
+ this.sessionFile = response.data.sessionFile;
793
+ this.savePointer();
794
+ }
795
+ }
796
+ catch (error) {
797
+ console.warn('[PiExecutor] Failed to refresh Pi session pointer', error);
798
+ }
799
+ }
800
+ loadPointer() {
801
+ try {
802
+ const stored = JSON.parse(fs.readFileSync(this.sessionFilePath, 'utf8'));
803
+ if (stored.id)
804
+ this.sessionId = stored.id;
805
+ if (stored.sessionFile)
806
+ this.sessionFile = stored.sessionFile;
807
+ }
808
+ catch {
809
+ // First run for this thread.
810
+ }
811
+ }
812
+ savePointer() {
813
+ const pointer = {
814
+ id: this.sessionId ?? undefined,
815
+ sessionFile: this.sessionFile ?? undefined,
816
+ cwd: this.currentWorkingDirectory,
817
+ };
818
+ const temporaryPath = `${this.sessionFilePath}.${process.pid}.tmp`;
819
+ try {
820
+ fs.writeFileSync(temporaryPath, JSON.stringify(pointer), { encoding: 'utf8', mode: 0o600 });
821
+ fs.renameSync(temporaryPath, this.sessionFilePath);
822
+ }
823
+ catch (error) {
824
+ try {
825
+ fs.unlinkSync(temporaryPath);
826
+ }
827
+ catch { /* ignore */ }
828
+ console.warn('[PiExecutor] Failed to save Pi session pointer', error);
829
+ }
830
+ }
831
+ clearPointer() {
832
+ this.sessionId = null;
833
+ this.sessionFile = null;
834
+ try {
835
+ fs.unlinkSync(this.sessionFilePath);
836
+ }
837
+ catch { /* ignore */ }
838
+ }
839
+ completeActive(result) {
840
+ const active = this.activeTurn;
841
+ if (!active)
842
+ return;
843
+ this.activeTurn = null;
844
+ if (active.timeoutTimer)
845
+ clearTimeout(active.timeoutTimer);
846
+ active.settle?.();
847
+ this.cancelPendingUi(true);
848
+ active.resolve(result);
849
+ }
850
+ async failActive(error) {
851
+ if (!this.activeTurn)
852
+ return;
853
+ try {
854
+ await this.client.request({ type: 'abort' });
855
+ }
856
+ catch {
857
+ await this.recycleClient();
858
+ }
859
+ this.completeActive({ success: false, error, sessionAbbr: this.sessionAbbr() });
860
+ }
861
+ cancelPendingUi(cancel = false) {
862
+ const pending = this.pendingUi;
863
+ this.pendingUi = null;
864
+ if (pending && cancel) {
865
+ try {
866
+ this.client.send({ type: 'extension_ui_response', id: pending.id, cancelled: true });
867
+ }
868
+ catch {
869
+ // Process may already be gone.
870
+ }
871
+ }
872
+ }
873
+ toImages(attachments) {
874
+ return (attachments ?? [])
875
+ .filter((attachment) => attachment.type === 'image')
876
+ .map((attachment) => ({ type: 'image', data: attachment.data, mimeType: attachment.mimeType }));
877
+ }
878
+ sessionAbbr() {
879
+ return this.sessionId ? this.sessionId.slice(0, 8) : undefined;
880
+ }
881
+ friendlyError(error) {
882
+ const message = error instanceof Error ? error.message : String(error);
883
+ if (/ENOENT|not found on PATH|not installed/i.test(message)) {
884
+ return `Pi CLI is not installed or not found on PATH. Install it with \`${INSTALL_COMMAND}\`, authenticate with \`pi\`, or use /backend to switch backends.`;
885
+ }
886
+ return message;
887
+ }
888
+ }
889
+ exports.PiExecutor = PiExecutor;
890
+ function sleep(ms) {
891
+ return new Promise((resolve) => setTimeout(resolve, ms));
892
+ }
893
+ //# sourceMappingURL=PiExecutor.js.map