pikiclaw 0.3.17 → 0.3.18

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.
@@ -432,36 +432,43 @@ function overlayCodexManagedPreview(workdir, sessionId, richMessages) {
432
432
  };
433
433
  return merged;
434
434
  }
435
- function buildCodexInteractionRequest(method, params, requestId) {
435
+ function toAgentInteraction(method, params, requestId) {
436
436
  if (method === 'item/tool/requestUserInput') {
437
- const questions = Array.isArray(params?.questions) ? params.questions : [];
437
+ const raw = Array.isArray(params?.questions) ? params.questions : [];
438
+ const questions = raw
439
+ .map((q) => ({
440
+ id: String(q?.id || ''),
441
+ header: String(q?.header || '') || 'Question',
442
+ prompt: String(q?.question || ''),
443
+ options: Array.isArray(q?.options)
444
+ ? q.options.map((o) => ({
445
+ label: String(o?.label || ''),
446
+ description: String(o?.description || ''),
447
+ value: String(o?.label || ''),
448
+ }))
449
+ : null,
450
+ allowFreeform: !!q?.isOther || !Array.isArray(q?.options) || !q.options.length,
451
+ secret: !!q?.isSecret,
452
+ allowEmpty: true,
453
+ }))
454
+ .filter((q) => q.id && q.prompt);
438
455
  return {
439
- kind: 'requestUserInput',
440
- requestId,
441
- threadId: String(params?.threadId || ''),
442
- turnId: String(params?.turnId || ''),
443
- itemId: String(params?.itemId || ''),
444
- questions: questions.map((question) => ({
445
- id: String(question?.id || ''),
446
- header: String(question?.header || ''),
447
- question: String(question?.question || ''),
448
- isOther: !!question?.isOther,
449
- isSecret: !!question?.isSecret,
450
- options: Array.isArray(question?.options)
451
- ? question.options.map((option) => ({
452
- label: String(option?.label || ''),
453
- description: String(option?.description || ''),
454
- }))
455
- : null,
456
- })).filter((question) => question.id && question.question),
456
+ kind: 'user-input',
457
+ id: requestId,
458
+ title: 'User Input Required',
459
+ hint: 'Use the buttons when available. Reply with text when prompted.',
460
+ questions,
461
+ resolveWith: (answers) => ({
462
+ answers: Object.fromEntries(Object.entries(answers).map(([id, vals]) => [id, { answers: vals }])),
463
+ }),
457
464
  };
458
465
  }
459
466
  return null;
460
467
  }
461
- function defaultCodexInteractionResponse(request) {
468
+ function defaultAgentInteractionResponse(interaction) {
462
469
  const answers = {};
463
- for (const question of request.questions)
464
- answers[question.id] = { answers: [] };
470
+ for (const q of interaction.questions)
471
+ answers[q.id] = { answers: [] };
465
472
  return { answers };
466
473
  }
467
474
  function defaultCodexServerRequestResponse(method) {
@@ -836,22 +843,22 @@ function handleTurnPlanUpdated(params, s, emit) {
836
843
  // Stream request handler (extracted from doCodexStream)
837
844
  // ---------------------------------------------------------------------------
838
845
  async function handleCodexRequest(method, params, requestId, s, opts, emit) {
839
- const interaction = buildCodexInteractionRequest(method, params, requestId);
846
+ const interaction = toAgentInteraction(method, params, requestId);
840
847
  if (!interaction)
841
848
  return defaultCodexServerRequestResponse(method);
842
- pushRecentActivity(s.recentNarrative, interaction.kind === 'requestUserInput' ? 'Waiting for user input' : 'Waiting for approval');
849
+ pushRecentActivity(s.recentNarrative, interaction.kind === 'user-input' ? 'Waiting for user input' : 'Waiting for approval');
843
850
  emit();
844
851
  try {
845
- if (opts.onCodexInteractionRequest) {
846
- const response = await opts.onCodexInteractionRequest(interaction);
847
- return response ?? defaultCodexInteractionResponse(interaction);
852
+ if (opts.onInteraction) {
853
+ const response = await opts.onInteraction(interaction);
854
+ return response ?? defaultAgentInteractionResponse(interaction);
848
855
  }
849
856
  }
850
857
  catch (error) {
851
858
  pushRecentActivity(s.recentFailures, `Human input failed: ${shortValue(error?.message || error, 120)}`, 4);
852
859
  emit();
853
860
  }
854
- return defaultCodexInteractionResponse(interaction);
861
+ return defaultAgentInteractionResponse(interaction);
855
862
  }
856
863
  // ---------------------------------------------------------------------------
857
864
  // Stream via app-server
package/dist/bot/bot.js CHANGED
@@ -279,6 +279,26 @@ export class Bot {
279
279
  }
280
280
  break;
281
281
  }
282
+ case 'interaction': {
283
+ const snap = this.streamSnapshots.get(sessionKey);
284
+ if (snap) {
285
+ const list = snap.interactions || [];
286
+ list.push(event.interaction);
287
+ snap.interactions = list;
288
+ snap.updatedAt = now;
289
+ }
290
+ break;
291
+ }
292
+ case 'interaction-resolved': {
293
+ const snap = this.streamSnapshots.get(sessionKey);
294
+ if (snap?.interactions) {
295
+ snap.interactions = snap.interactions.filter(i => i.promptId !== event.promptId);
296
+ if (!snap.interactions.length)
297
+ delete snap.interactions;
298
+ snap.updatedAt = now;
299
+ }
300
+ break;
301
+ }
282
302
  }
283
303
  // Push to dashboard SSE — throttle text events, push everything else immediately
284
304
  try {
@@ -903,6 +923,7 @@ export class Bot {
903
923
  this.humanLoopPrompts.delete(promptId);
904
924
  this.removeHumanLoopPromptFromChat(prompt.chatId, promptId);
905
925
  prompt.resolve(buildHumanLoopResponse(prompt));
926
+ this.emitInteractionResolved(prompt.taskId, promptId);
906
927
  return prompt;
907
928
  }
908
929
  clearHumanLoopPrompt(promptId, error) {
@@ -913,8 +934,14 @@ export class Bot {
913
934
  this.removeHumanLoopPromptFromChat(prompt.chatId, promptId);
914
935
  if (error)
915
936
  prompt.reject(error);
937
+ this.emitInteractionResolved(prompt.taskId, promptId);
916
938
  return prompt;
917
939
  }
940
+ emitInteractionResolved(taskId, promptId) {
941
+ const task = this.activeTasks.get(taskId);
942
+ if (task)
943
+ this.emitStream(task.sessionKey, { type: 'interaction-resolved', promptId });
944
+ }
918
945
  humanLoopSelectOption(promptId, optionValue, opts = {}) {
919
946
  const prompt = this.humanLoopPrompts.get(promptId) || null;
920
947
  if (!prompt)
@@ -963,6 +990,79 @@ export class Bot {
963
990
  else
964
991
  this.humanLoopPromptIdsByChat.delete(chatKey);
965
992
  }
993
+ /**
994
+ * Create an interaction handler that bridges agent requests to the human-loop
995
+ * state machine and pushes SSE events to the dashboard.
996
+ *
997
+ * IM channel subclasses override `renderInteractionPrompt()` to render
998
+ * buttons/cards in their native UI. Dashboard clients receive the
999
+ * `interaction` SSE event and respond via REST.
1000
+ */
1001
+ createInteractionHandler(chatId, taskId, sessionKey) {
1002
+ return async (request) => {
1003
+ const active = this.beginHumanLoopPrompt({
1004
+ taskId,
1005
+ chatId,
1006
+ title: request.title,
1007
+ hint: request.hint,
1008
+ questions: request.questions,
1009
+ resolveWith: request.resolveWith,
1010
+ });
1011
+ const interactionSnapshot = {
1012
+ promptId: active.prompt.promptId,
1013
+ kind: request.kind,
1014
+ title: request.title,
1015
+ hint: request.hint,
1016
+ questions: request.questions,
1017
+ };
1018
+ this.emitStream(sessionKey, { type: 'interaction', taskId, interaction: interactionSnapshot });
1019
+ try {
1020
+ await this.renderInteractionPrompt(active.prompt, chatId);
1021
+ }
1022
+ catch (error) {
1023
+ this.humanLoopCancel(active.prompt.promptId, error?.message || 'Failed to send prompt.');
1024
+ throw error;
1025
+ }
1026
+ return active.result;
1027
+ };
1028
+ }
1029
+ /**
1030
+ * Render an interaction prompt in the IM channel.
1031
+ * Override in channel subclasses (Telegram, Feishu, etc.).
1032
+ * Dashboard-only sessions (chatId='dashboard') are a no-op by default.
1033
+ */
1034
+ async renderInteractionPrompt(_prompt, _chatId) {
1035
+ // Default: no-op (dashboard-only sessions use SSE events instead)
1036
+ }
1037
+ // ---- Public interaction API (used by dashboard routes) --------------------
1038
+ /** Respond to a pending interaction prompt with a selected option. */
1039
+ interactionSelectOption(promptId, optionValue, opts) {
1040
+ return this.humanLoopSelectOption(promptId, optionValue, opts);
1041
+ }
1042
+ /** Submit freeform text to a pending interaction prompt. */
1043
+ interactionSubmitText(promptId, text) {
1044
+ const prompt = this.humanLoopPrompt(promptId);
1045
+ if (!prompt)
1046
+ return null;
1047
+ if (!isHumanLoopAwaitingText(prompt))
1048
+ return null;
1049
+ const result = setHumanLoopText(prompt, text);
1050
+ if (result.completed)
1051
+ this.resolveHumanLoopPrompt(prompt.promptId);
1052
+ return { prompt, ...result };
1053
+ }
1054
+ /** Skip the current question in a pending interaction prompt. */
1055
+ interactionSkip(promptId) {
1056
+ return this.humanLoopSkip(promptId);
1057
+ }
1058
+ /** Cancel a pending interaction prompt. */
1059
+ interactionCancel(promptId, reason = 'Cancelled from dashboard.') {
1060
+ return this.humanLoopCancel(promptId, reason);
1061
+ }
1062
+ /** Get a specific interaction prompt by ID. */
1063
+ interactionPrompt(promptId) {
1064
+ return this.humanLoopPrompt(promptId);
1065
+ }
966
1066
  selectedSession(chatId) {
967
1067
  return this.getSelectedSession(this.chat(chatId));
968
1068
  }
@@ -1003,7 +1103,7 @@ export class Bot {
1003
1103
  const result = await this.runStream(prompt, session, attachments, (text, thinking, activity, meta, plan) => {
1004
1104
  opts.onText?.(text, thinking, activity, meta, plan);
1005
1105
  this.emitStream(currentSessionKey(), { type: 'text', text, thinking, activity, plan });
1006
- }, undefined, undefined, abortController.signal);
1106
+ }, undefined, undefined, abortController.signal, this.createInteractionHandler(opts.chatId ?? 'dashboard', taskId, currentSessionKey()));
1007
1107
  this.emitStream(currentSessionKey(), {
1008
1108
  type: 'done',
1009
1109
  taskId,
@@ -1272,7 +1372,7 @@ export class Bot {
1272
1372
  if (!opts.initial)
1273
1373
  this.onManagedConfigChange(config, opts);
1274
1374
  }
1275
- async runStream(prompt, cs, attachments, onText, systemPrompt, mcpSendFile, abortSignal, onCodexInteractionRequest, onSteerReady, onCodexTurnReady) {
1375
+ async runStream(prompt, cs, attachments, onText, systemPrompt, mcpSendFile, abortSignal, onInteraction, onSteerReady, onCodexTurnReady) {
1276
1376
  const resolvedModel = cs.modelId || this.modelForAgent(cs.agent);
1277
1377
  const agentConfig = this.agentConfigs[cs.agent] || {};
1278
1378
  const resolvedThinkingEffort = ('thinkingEffort' in cs && typeof cs.thinkingEffort === 'string' && cs.thinkingEffort.trim())
@@ -1359,7 +1459,7 @@ export class Bot {
1359
1459
  // MCP bridge
1360
1460
  mcpSendFile,
1361
1461
  abortSignal,
1362
- onCodexInteractionRequest,
1462
+ onInteraction,
1363
1463
  onSteerReady,
1364
1464
  onCodexTurnReady,
1365
1465
  };
@@ -17,9 +17,8 @@ import { SKILL_CMD_PREFIX, } from '../../bot/menu.js';
17
17
  import { getStartData, getSessionsPageData, getModelsListData, getSessionTurnPreviewData, getStatusDataAsync, getHostDataSync, resolveSkillPrompt, } from '../../bot/commands.js';
18
18
  import { buildAgentsCommandView, buildModelsCommandView, buildModeCommandView, buildSessionsCommandView, buildSkillsCommandView, decodeCommandAction, executeCommandAction, } from '../../bot/command-ui.js';
19
19
  import { LivePreview } from '../telegram/live-preview.js';
20
- import { formatActiveTaskRestartError, getActiveTaskCount, registerProcessRuntime, requestProcessRestart, } from '../../core/process-control.js';
20
+ import { registerProcessRuntime, requestProcessRestart, } from '../../core/process-control.js';
21
21
  import { feishuPreviewRenderer, buildInitialPreviewMarkdown, buildHumanLoopPromptMarkdown, buildFinalReplyRender, renderCommandNotice, renderCommandSelectionCard, renderSessionTurnMarkdown, renderStart, renderStatus, renderHost, buildSwitchWorkdirCard, resolveFeishuRegisteredPath, } from './render.js';
22
- import { buildCodexHumanLoopPrompt } from '../../bot/human-loop-codex.js';
23
22
  import { currentHumanLoopQuestion, humanLoopOptionSelected } from '../../bot/human-loop.js';
24
23
  import { FeishuChannel } from './channel.js';
25
24
  import { splitText, supportsChannelCapability } from '../base.js';
@@ -384,11 +383,6 @@ export class FeishuBot extends Bot {
384
383
  await ctx.channel.sendCard(ctx.chatId, view);
385
384
  }
386
385
  async cmdRestart(ctx) {
387
- const activeTasks = getActiveTaskCount();
388
- if (activeTasks > 0) {
389
- await ctx.reply(`⚠ ${formatActiveTaskRestartError(activeTasks)}`);
390
- return;
391
- }
392
386
  await ctx.reply('**Restarting pikiclaw...**\n\nPulling latest version. The bot will be back shortly.');
393
387
  void requestProcessRestart({ log: msg => this.log(msg) });
394
388
  }
@@ -474,27 +468,12 @@ export class FeishuBot extends Bot {
474
468
  keyboard: { rows: [] },
475
469
  }).catch(() => { });
476
470
  }
477
- createCodexHumanLoopHandler(ctx, taskId) {
478
- return async (request) => {
479
- const blueprint = buildCodexHumanLoopPrompt(request);
480
- const active = this.beginHumanLoopPrompt({
481
- taskId,
482
- chatId: ctx.chatId,
483
- ...blueprint,
484
- });
485
- try {
486
- const sent = await ctx.reply(buildHumanLoopPromptMarkdown(active.prompt), {
487
- keyboard: this.buildHumanLoopKeyboard(active.prompt.promptId),
488
- });
489
- if (sent)
490
- this.registerHumanLoopMessage(active.prompt.promptId, sent);
491
- }
492
- catch (error) {
493
- this.humanLoopCancel(active.prompt.promptId, error?.message || 'Failed to send prompt.');
494
- throw error;
495
- }
496
- return active.result;
497
- };
471
+ async renderInteractionPrompt(prompt, chatId) {
472
+ const sent = await this.channel.send(chatId, buildHumanLoopPromptMarkdown(prompt), {
473
+ keyboard: this.buildHumanLoopKeyboard(prompt.promptId),
474
+ });
475
+ if (sent)
476
+ this.registerHumanLoopMessage(prompt.promptId, sent);
498
477
  }
499
478
  async safeSetMessageReaction(chatId, messageId, reactions) {
500
479
  if (!supportsChannelCapability(this.channel, 'messageReactions'))
@@ -640,7 +619,7 @@ export class FeishuBot extends Bot {
640
619
  const mcpSendFile = this.createMcpSendFileCallback(ctx);
641
620
  const result = await this.runStream(prompt, session, files, (nextText, nextThinking, nextActivity = '', meta, plan) => {
642
621
  livePreview?.update(nextText, nextThinking, nextActivity, meta, plan);
643
- }, undefined, mcpSendFile, abortController.signal, this.createCodexHumanLoopHandler(ctx, taskId), (steer) => {
622
+ }, undefined, mcpSendFile, abortController.signal, this.createInteractionHandler(ctx.chatId, taskId, session.key), (steer) => {
644
623
  const currentTask = this.activeTasks.get(taskId);
645
624
  if (!currentTask || currentTask.cancelled || currentTask.status !== 'running')
646
625
  return;
@@ -18,9 +18,8 @@ import { getStartData, getStatusDataAsync, getHostDataSync, getSessionTurnPrevie
18
18
  import { buildAgentsCommandView, buildModelsCommandView, buildModeCommandView, buildSessionsCommandView, buildSkillsCommandView, decodeCommandAction, executeCommandAction, } from '../../bot/command-ui.js';
19
19
  import { buildSwitchWorkdirView, resolveRegisteredPath } from './directory.js';
20
20
  import { LivePreview } from './live-preview.js';
21
- import { formatActiveTaskRestartError, getActiveTaskCount, registerProcessRuntime, buildRestartCommand, requestProcessRestart, } from '../../core/process-control.js';
21
+ import { registerProcessRuntime, buildRestartCommand, requestProcessRestart, } from '../../core/process-control.js';
22
22
  import { buildInitialPreviewHtml, buildHumanLoopPromptHtml, buildStreamPreviewHtml, buildFinalReplyRender, escapeHtml, formatMenuLines, formatProviderUsageLines, renderCommandNoticeHtml, renderCommandSelectionHtml, renderCommandSelectionKeyboard, renderSessionTurnHtml, truncateMiddle, } from './render.js';
23
- import { buildCodexHumanLoopPrompt } from '../../bot/human-loop-codex.js';
24
23
  import { currentHumanLoopQuestion, humanLoopOptionSelected } from '../../bot/human-loop.js';
25
24
  import { TelegramChannel } from './channel.js';
26
25
  import { splitText, supportsChannelCapability } from '../base.js';
@@ -348,11 +347,6 @@ export class TelegramBot extends Bot {
348
347
  await this.sendCommandView(ctx, buildModeCommandView(this, ctx.chatId));
349
348
  }
350
349
  async cmdRestart(ctx) {
351
- const activeTasks = getActiveTaskCount();
352
- if (activeTasks > 0) {
353
- await ctx.reply(`⚠ ${formatActiveTaskRestartError(activeTasks)}`, { parseMode: 'HTML' });
354
- return;
355
- }
356
350
  await ctx.reply(`<b>Restarting pikiclaw...</b>\n\n` +
357
351
  `The bot will be back shortly.`, { parseMode: 'HTML' });
358
352
  void requestProcessRestart({ log: msg => this.log(msg) });
@@ -418,30 +412,18 @@ export class TelegramBot extends Bot {
418
412
  keyboard: { inline_keyboard: [] },
419
413
  }).catch(() => { });
420
414
  }
421
- createCodexHumanLoopHandler(ctx, taskId, messageThreadId) {
422
- return async (request) => {
423
- const blueprint = buildCodexHumanLoopPrompt(request);
424
- const active = this.beginHumanLoopPrompt({
425
- taskId,
426
- chatId: ctx.chatId,
427
- ...blueprint,
428
- });
429
- try {
430
- const sent = await ctx.reply(buildHumanLoopPromptHtml(active.prompt), {
431
- parseMode: 'HTML',
432
- messageThreadId,
433
- keyboard: this.buildHumanLoopKeyboard(active.prompt.promptId),
434
- });
435
- if (typeof sent === 'number')
436
- this.registerHumanLoopMessage(active.prompt.promptId, sent);
437
- }
438
- catch (error) {
439
- this.humanLoopCancel(active.prompt.promptId, error?.message || 'Failed to send prompt.');
440
- throw error;
441
- }
442
- return active.result;
443
- };
415
+ async renderInteractionPrompt(prompt, chatId) {
416
+ const messageThreadId = this.interactionThreadIds.get(prompt.taskId);
417
+ const sent = await this.channel.send(chatId, buildHumanLoopPromptHtml(prompt), {
418
+ parseMode: 'HTML',
419
+ messageThreadId,
420
+ keyboard: this.buildHumanLoopKeyboard(prompt.promptId),
421
+ });
422
+ if (typeof sent === 'number')
423
+ this.registerHumanLoopMessage(prompt.promptId, sent);
444
424
  }
425
+ /** Cache the messageThreadId per task so renderInteractionPrompt can use it. */
426
+ interactionThreadIds = new Map();
445
427
  // ---- streaming bridge -----------------------------------------------------
446
428
  async handleMessage(msg, ctx) {
447
429
  const text = msg.text.trim();
@@ -581,9 +563,10 @@ export class TelegramBot extends Bot {
581
563
  }
582
564
  // MCP sendFile callback: sends files to IM in real-time during the stream
583
565
  const mcpSendFile = this.createMcpSendFileCallback(ctx, messageThreadId);
566
+ this.interactionThreadIds.set(taskId, messageThreadId);
584
567
  const result = await this.runStream(prompt, session, files, (nextText, nextThinking, nextActivity = '', meta, plan) => {
585
568
  livePreview?.update(nextText, nextThinking, nextActivity, meta, plan);
586
- }, undefined, mcpSendFile, abortController.signal, this.createCodexHumanLoopHandler(ctx, taskId, messageThreadId), (steer) => {
569
+ }, undefined, mcpSendFile, abortController.signal, this.createInteractionHandler(ctx.chatId, taskId, session.key), (steer) => {
587
570
  const currentTask = this.activeTasks.get(taskId);
588
571
  if (!currentTask || currentTask.cancelled || currentTask.status !== 'running')
589
572
  return;
@@ -631,6 +614,7 @@ export class TelegramBot extends Bot {
631
614
  }
632
615
  finally {
633
616
  livePreview?.dispose();
617
+ this.interactionThreadIds.delete(taskId);
634
618
  this.finishTask(taskId);
635
619
  this.syncSelectedChats(session);
636
620
  }
@@ -7,7 +7,7 @@ import path from 'node:path';
7
7
  import { Bot, buildPrompt, fmtUptime, fmtBytes, normalizeAgent, parseAllowedChatIds, } from '../../bot/bot.js';
8
8
  import { BOT_SHUTDOWN_FORCE_EXIT_MS, buildSessionTaskId } from '../../bot/orchestration.js';
9
9
  import { shutdownAllDrivers } from '../../agent/driver.js';
10
- import { formatActiveTaskRestartError, getActiveTaskCount, registerProcessRuntime, requestProcessRestart, } from '../../core/process-control.js';
10
+ import { registerProcessRuntime, requestProcessRestart, } from '../../core/process-control.js';
11
11
  import { getStatusDataAsync, getHostDataSync, getAgentsListData, getSkillsListData, getModelsListData, getSessionsPageData, getStartData, } from '../../bot/commands.js';
12
12
  import { WeixinChannel } from './channel.js';
13
13
  import { getActiveUserConfig } from '../../core/config/user-config.js';
@@ -386,11 +386,6 @@ export class WeixinBot extends Bot {
386
386
  await ctx.reply(`Stopped: ${parts.join(', ')}.`);
387
387
  }
388
388
  async cmdRestart(ctx) {
389
- const activeTasks = getActiveTaskCount();
390
- if (activeTasks > 0) {
391
- await ctx.reply(formatActiveTaskRestartError(activeTasks));
392
- return;
393
- }
394
389
  await ctx.reply('Restarting pikiclaw...');
395
390
  void requestProcessRestart({ log: msg => this.log(msg) });
396
391
  }
@@ -87,9 +87,6 @@ export function getActiveTaskCount() {
87
87
  }
88
88
  return total;
89
89
  }
90
- export function formatActiveTaskRestartError(activeTasks) {
91
- return `${activeTasks} task(s) still running. Wait for them to finish or try again.`;
92
- }
93
90
  export function createRestartStateFilePath(ownerPid = process.pid) {
94
91
  const dir = path.join(os.tmpdir(), 'pikiclaw');
95
92
  fs.mkdirSync(dir, { recursive: true });
@@ -188,15 +185,6 @@ function spawnReplacementProcess(bin, args, env, log) {
188
185
  return child;
189
186
  }
190
187
  export async function requestProcessRestart(opts = {}) {
191
- const activeTasks = getActiveTaskCount();
192
- if (activeTasks > 0) {
193
- return {
194
- ok: false,
195
- restarting: false,
196
- error: formatActiveTaskRestartError(activeTasks),
197
- activeTasks,
198
- };
199
- }
200
188
  if (restartInFlight) {
201
189
  return {
202
190
  ok: true,
@@ -12,7 +12,7 @@ import { validateFeishuConfig, validateTelegramConfig, validateWeixinConfig } fr
12
12
  import { resolveGuiIntegrationConfig } from '../../agent/mcp/bridge.js';
13
13
  import { normalizeWeixinBaseUrl, startWeixinQrLogin, waitForWeixinQrLogin, } from '../../channels/weixin/api.js';
14
14
  import { getManagedBrowserStatus, launchManagedBrowserSetup, } from '../../browser-profile.js';
15
- import { formatActiveTaskRestartError, getActiveTaskCount, requestProcessRestart, } from '../../core/process-control.js';
15
+ import { requestProcessRestart, } from '../../core/process-control.js';
16
16
  import { checkPermissions, detectHostTerminalApp, installAppium, isAppiumInstalled, isManagedAppiumRunning, isValidPermissionKey, requestPermission, startManagedAppium, stopManagedAppium, } from '../platform.js';
17
17
  import { VERSION } from '../../core/version.js';
18
18
  import { runtime } from '../runtime.js';
@@ -258,10 +258,6 @@ app.post('/api/open-preferences', async (c) => {
258
258
  });
259
259
  // Restart process
260
260
  app.post('/api/restart', (c) => {
261
- const activeTasks = getActiveTaskCount();
262
- if (activeTasks > 0) {
263
- return c.json({ ok: false, error: formatActiveTaskRestartError(activeTasks) }, 409);
264
- }
265
261
  setTimeout(() => {
266
262
  void requestProcessRestart({ log: message => runtime.log(message) });
267
263
  }, 50);
@@ -8,7 +8,7 @@ import path from 'node:path';
8
8
  import { loadUserConfig } from '../../core/config/user-config.js';
9
9
  import { listAgents, listSkills } from '../../agent/index.js';
10
10
  import { getSessionStatusForBot } from '../../bot/session-status.js';
11
- import { cancelSessionTask, getSessionStreamState, queueDashboardSessionTask, steerSessionTask, } from '../session-control.js';
11
+ import { cancelSessionTask, getSessionStreamState, queueDashboardSessionTask, steerSessionTask, interactionSelectOption, interactionSubmitText, interactionSkip, interactionCancel, getInteractionPrompt, } from '../session-control.js';
12
12
  import { querySessions, querySessionTail, querySessionMessages, getWorkspaceOverviews, updateSession, linkSessions, buildMigrationContext, exportSession, importSession, loadWorkspaces, addWorkspace, removeWorkspace, updateWorkspace, } from '../../bot/session-hub.js';
13
13
  import { DASHBOARD_PAGINATION } from '../../core/constants.js';
14
14
  import { runtime } from '../runtime.js';
@@ -505,4 +505,67 @@ app.post('/api/session-hub/session/steer', async (c) => {
505
505
  return c.json({ ok: false, error: e.message }, 500);
506
506
  }
507
507
  });
508
+ // ==========================================================================
509
+ // Interaction prompts (human-in-the-loop)
510
+ // ==========================================================================
511
+ /** GET /api/interaction/:promptId — Get interaction prompt state. */
512
+ app.get('/api/interaction/:promptId', (c) => {
513
+ const { promptId } = c.req.param();
514
+ const result = getInteractionPrompt(promptId);
515
+ return c.json(result, result.ok ? 200 : 503);
516
+ });
517
+ /** POST /api/interaction/:promptId/select — Select an option. */
518
+ app.post('/api/interaction/:promptId/select', async (c) => {
519
+ try {
520
+ const { promptId } = c.req.param();
521
+ const body = await c.req.json();
522
+ const { value, requestFreeform } = body || {};
523
+ if (!value && !requestFreeform) {
524
+ return c.json({ ok: false, error: 'value is required' }, 400);
525
+ }
526
+ const result = interactionSelectOption(promptId, value || '__other__', { requestFreeform: !!requestFreeform });
527
+ return c.json(result, result.ok ? 200 : (result.error === 'Bot is not running' ? 503 : 404));
528
+ }
529
+ catch (e) {
530
+ return c.json({ ok: false, error: e.message }, 500);
531
+ }
532
+ });
533
+ /** POST /api/interaction/:promptId/text — Submit freeform text. */
534
+ app.post('/api/interaction/:promptId/text', async (c) => {
535
+ try {
536
+ const { promptId } = c.req.param();
537
+ const body = await c.req.json();
538
+ const { text } = body || {};
539
+ if (typeof text !== 'string') {
540
+ return c.json({ ok: false, error: 'text is required' }, 400);
541
+ }
542
+ const result = interactionSubmitText(promptId, text);
543
+ return c.json(result, result.ok ? 200 : (result.error === 'Bot is not running' ? 503 : 404));
544
+ }
545
+ catch (e) {
546
+ return c.json({ ok: false, error: e.message }, 500);
547
+ }
548
+ });
549
+ /** POST /api/interaction/:promptId/skip — Skip current question. */
550
+ app.post('/api/interaction/:promptId/skip', async (c) => {
551
+ try {
552
+ const { promptId } = c.req.param();
553
+ const result = interactionSkip(promptId);
554
+ return c.json(result, result.ok ? 200 : (result.error === 'Bot is not running' ? 503 : 404));
555
+ }
556
+ catch (e) {
557
+ return c.json({ ok: false, error: e.message }, 500);
558
+ }
559
+ });
560
+ /** POST /api/interaction/:promptId/cancel — Cancel interaction prompt. */
561
+ app.post('/api/interaction/:promptId/cancel', async (c) => {
562
+ try {
563
+ const { promptId } = c.req.param();
564
+ const result = interactionCancel(promptId);
565
+ return c.json(result, result.ok ? 200 : (result.error === 'Bot is not running' ? 503 : 404));
566
+ }
567
+ catch (e) {
568
+ return c.json({ ok: false, error: e.message }, 500);
569
+ }
570
+ });
508
571
  export default app;
@@ -104,3 +104,62 @@ export async function steerSessionTask(taskId) {
104
104
  const result = await bot.steerTask(taskId);
105
105
  return { ok: true, steered: result.steered };
106
106
  }
107
+ // ---------------------------------------------------------------------------
108
+ // Interaction prompt control (human-in-the-loop)
109
+ // ---------------------------------------------------------------------------
110
+ export function interactionSelectOption(promptId, optionValue, opts) {
111
+ const bot = runtime.getBotRef();
112
+ if (!bot)
113
+ return { ok: false, error: 'Bot is not running' };
114
+ const result = bot.interactionSelectOption(promptId, optionValue, opts);
115
+ if (!result)
116
+ return { ok: false, error: 'Prompt not found or no longer active' };
117
+ return { ok: true, completed: result.completed, advanced: result.advanced };
118
+ }
119
+ export function interactionSubmitText(promptId, text) {
120
+ const bot = runtime.getBotRef();
121
+ if (!bot)
122
+ return { ok: false, error: 'Bot is not running' };
123
+ const result = bot.interactionSubmitText(promptId, text);
124
+ if (!result)
125
+ return { ok: false, error: 'Prompt not found or not awaiting text' };
126
+ return { ok: true, completed: result.completed, advanced: result.advanced };
127
+ }
128
+ export function interactionSkip(promptId) {
129
+ const bot = runtime.getBotRef();
130
+ if (!bot)
131
+ return { ok: false, error: 'Bot is not running' };
132
+ const result = bot.interactionSkip(promptId);
133
+ if (!result)
134
+ return { ok: false, error: 'Prompt not found or no longer active' };
135
+ return { ok: true, completed: result.completed, advanced: result.advanced };
136
+ }
137
+ export function interactionCancel(promptId) {
138
+ const bot = runtime.getBotRef();
139
+ if (!bot)
140
+ return { ok: false, error: 'Bot is not running' };
141
+ const result = bot.interactionCancel(promptId);
142
+ if (!result)
143
+ return { ok: false, error: 'Prompt not found or no longer active' };
144
+ return { ok: true };
145
+ }
146
+ export function getInteractionPrompt(promptId) {
147
+ const bot = runtime.getBotRef();
148
+ if (!bot)
149
+ return { ok: false, error: 'Bot is not running' };
150
+ const prompt = bot.interactionPrompt(promptId);
151
+ if (!prompt)
152
+ return { ok: true, prompt: null };
153
+ return {
154
+ ok: true,
155
+ prompt: {
156
+ promptId: prompt.promptId,
157
+ taskId: prompt.taskId,
158
+ title: prompt.title,
159
+ hint: prompt.hint,
160
+ questions: prompt.questions,
161
+ currentIndex: prompt.currentIndex,
162
+ answers: prompt.answers,
163
+ },
164
+ };
165
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pikiclaw",
3
- "version": "0.3.17",
3
+ "version": "0.3.18",
4
4
  "description": "Put the world's smartest AI agents in your pocket. Command local Claude & Gemini via IM. | 让最好用的 IM 变成你电脑上的顶级 Agent 控制台",
5
5
  "type": "module",
6
6
  "bin": {
@@ -37,7 +37,7 @@
37
37
  "test:watch": "vitest"
38
38
  },
39
39
  "engines": {
40
- "node": ">=18.0.0"
40
+ "node": ">=20.0.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@tailwindcss/vite": "^4.2.1",
@@ -1,26 +0,0 @@
1
- /**
2
- * Maps Codex user-input requests into IM human-loop prompts.
3
- */
4
- export function buildCodexHumanLoopPrompt(request) {
5
- return {
6
- title: 'User Input Required',
7
- detail: 'codex',
8
- hint: 'Use the buttons when available. Reply with text when prompted.',
9
- questions: request.questions.map(question => ({
10
- id: question.id,
11
- header: question.header || 'Question',
12
- prompt: question.question,
13
- options: question.options?.map(option => ({
14
- label: option.label,
15
- description: option.description,
16
- value: option.label,
17
- })) || null,
18
- allowFreeform: question.isOther || !question.options?.length,
19
- secret: question.isSecret,
20
- allowEmpty: true,
21
- })),
22
- resolveWith: answers => ({
23
- answers: Object.fromEntries(Object.entries(answers).map(([id, values]) => [id, { answers: values }])),
24
- }),
25
- };
26
- }