pikiclaw 0.2.52 → 0.2.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # pikiclaw
4
4
 
5
- **Put the world's smartest AI agents in your pocket. Command local Claude & Gemini via best IM.**
5
+ **Put the world's smartest AI agents in your pocket. Command local Claude, Codex & Gemini via best IM.**
6
6
 
7
7
  *让最好用的 IM 变成你电脑上的顶级 Agent 控制台*
8
8
 
@@ -199,8 +199,10 @@ export function buildHumanLoopPromptMarkdown(prompt) {
199
199
  // ---------------------------------------------------------------------------
200
200
  // LivePreview renderer — produces Markdown for Feishu card elements
201
201
  // ---------------------------------------------------------------------------
202
- export function buildInitialPreviewMarkdown(agent, model, effort) {
202
+ export function buildInitialPreviewMarkdown(agent, model, effort, waiting = false) {
203
203
  const parts = [];
204
+ if (waiting)
205
+ parts.push('Waiting in queue...');
204
206
  if (model)
205
207
  parts.push(model);
206
208
  else
@@ -28,6 +28,7 @@ const SHUTDOWN_EXIT_CODE = {
28
28
  SIGINT: 130,
29
29
  SIGTERM: 143,
30
30
  };
31
+ const FEISHU_FILE_STAGE_REACTION = 'Get';
31
32
  function describeError(err) {
32
33
  if (!(err instanceof Error))
33
34
  return String(err ?? 'unknown error');
@@ -464,6 +465,17 @@ export class FeishuBot extends Bot {
464
465
  return active.result;
465
466
  };
466
467
  }
468
+ async safeSetMessageReaction(chatId, messageId, reactions) {
469
+ if (!supportsChannelCapability(this.channel, 'messageReactions'))
470
+ return;
471
+ const setReaction = this.channel?.setMessageReaction;
472
+ if (typeof setReaction !== 'function')
473
+ return;
474
+ try {
475
+ await setReaction.call(this.channel, chatId, messageId, reactions);
476
+ }
477
+ catch { }
478
+ }
467
479
  // ---- streaming bridge -----------------------------------------------------
468
480
  async handleMessage(msg, ctx) {
469
481
  const text = msg.text.trim();
@@ -507,6 +519,7 @@ export class FeishuBot extends Bot {
507
519
  throw new Error('no files persisted');
508
520
  this.log(`[handleMessage] staged files chat=${ctx.chatId} session=${staged.sessionId} files=${staged.importedFiles.length}`);
509
521
  this.registerSessionMessage(ctx.chatId, ctx.messageId, session);
522
+ await this.safeSetMessageReaction(ctx.chatId, ctx.messageId, [FEISHU_FILE_STAGE_REACTION]);
510
523
  }
511
524
  catch (e) {
512
525
  this.log(`[handleMessage] stage files failed: ${e?.message || e}`);
@@ -525,6 +538,7 @@ export class FeishuBot extends Bot {
525
538
  const start = Date.now();
526
539
  this.log(`[handleMessage] start chat=${ctx.chatId} agent=${session.agent} session=${session.sessionId || '(new)'} ` +
527
540
  `files=${files.length} prompt="${prompt.slice(0, 100)}"`);
541
+ const waiting = this.sessionHasPendingWork(session);
528
542
  const taskId = this.createTaskId(session);
529
543
  this.beginTask({
530
544
  taskId,
@@ -538,7 +552,7 @@ export class FeishuBot extends Bot {
538
552
  const stopKeyboard = this.buildStopKeyboard(this.actionIdForTask(taskId));
539
553
  const model = session.modelId || this.modelForAgent(session.agent);
540
554
  const effort = this.effortForAgent(session.agent);
541
- const placeholderId = await this.channel.sendStreamingCard(ctx.chatId, buildInitialPreviewMarkdown(session.agent, model, effort), {
555
+ const placeholderId = await this.channel.sendStreamingCard(ctx.chatId, buildInitialPreviewMarkdown(session.agent, model, effort, waiting), {
542
556
  replyTo: ctx.messageId || undefined,
543
557
  keyboard: stopKeyboard,
544
558
  });
@@ -327,8 +327,10 @@ function trimActivityForPreview(text, maxChars = 900) {
327
327
  return text.slice(0, Math.max(0, maxChars - 3)).trimEnd() + '...';
328
328
  return [...head, '...', ...tail].join('\n');
329
329
  }
330
- export function buildInitialPreviewHtml(agent) {
331
- return formatPreviewFooterHtml(agent, 0);
330
+ export function buildInitialPreviewHtml(agent, waiting = false) {
331
+ return waiting
332
+ ? `<i>Waiting in queue...</i>\n\n${formatPreviewFooterHtml(agent, 0)}`
333
+ : formatPreviewFooterHtml(agent, 0);
332
334
  }
333
335
  export function buildStreamPreviewHtml(input) {
334
336
  const maxBody = 2400;
@@ -502,9 +502,10 @@ export class TelegramBot extends Bot {
502
502
  sourceMessageId: ctx.messageId,
503
503
  });
504
504
  const stopKeyboard = this.buildStopKeyboard(this.actionIdForTask(taskId));
505
+ const waiting = this.sessionHasPendingWork(session);
505
506
  let phId = null;
506
507
  if (canEditMessages) {
507
- const placeholderId = await ctx.reply(buildInitialPreviewHtml(session.agent), { parseMode: 'HTML', messageThreadId, keyboard: stopKeyboard });
508
+ const placeholderId = await ctx.reply(buildInitialPreviewHtml(session.agent, waiting), { parseMode: 'HTML', messageThreadId, keyboard: stopKeyboard });
508
509
  phId = typeof placeholderId === 'number' ? placeholderId : null;
509
510
  if (phId != null) {
510
511
  this.registerSessionMessage(ctx.chatId, phId, session);
@@ -217,7 +217,7 @@ class FeishuChannel extends Channel {
217
217
  typingIndicators: false,
218
218
  commandMenu: true,
219
219
  callbackActions: true,
220
- messageReactions: false,
220
+ messageReactions: true,
221
221
  fileUpload: true,
222
222
  fileDownload: true,
223
223
  threads: false,
@@ -710,6 +710,19 @@ class FeishuChannel extends Channel {
710
710
  async sendTyping(_chatId) {
711
711
  // Feishu has no typing indicator API — no-op
712
712
  }
713
+ async setMessageReaction(_chatId, msgId, reactions) {
714
+ const messageId = String(msgId || '').trim();
715
+ const emojiTypes = [...new Set(reactions.map(reaction => String(reaction || '').trim()).filter(Boolean))];
716
+ if (!messageId || !emojiTypes.length)
717
+ return;
718
+ this._logOutgoing('setReaction', `msg_id=${messageId} reactions=${emojiTypes.join(',')}`);
719
+ for (const emojiType of emojiTypes) {
720
+ await this.client.im.messageReaction.create({
721
+ path: { message_id: messageId },
722
+ data: { reaction_type: { emoji_type: emojiType } },
723
+ }).catch(() => { });
724
+ }
725
+ }
713
726
  // ========================================================================
714
727
  // Streaming cards (CardKit v1) — typewriter effect
715
728
  // ========================================================================
@@ -97,6 +97,36 @@ export function shortValue(value, max = 90) {
97
97
  return text;
98
98
  return `${text.slice(0, Math.max(0, max - 3)).trimEnd()}...`;
99
99
  }
100
+ export function normalizeErrorMessage(value) {
101
+ if (typeof value === 'string')
102
+ return value.trim();
103
+ if (value instanceof Error)
104
+ return value.message.trim();
105
+ if (Array.isArray(value)) {
106
+ return value.map(item => normalizeErrorMessage(item)).filter(Boolean).join('; ').trim();
107
+ }
108
+ if (value && typeof value === 'object') {
109
+ const record = value;
110
+ const preferred = normalizeErrorMessage(record.message)
111
+ || normalizeErrorMessage(record.error)
112
+ || normalizeErrorMessage(record.detail)
113
+ || normalizeErrorMessage(record.type)
114
+ || normalizeErrorMessage(record.code)
115
+ || normalizeErrorMessage(record.status);
116
+ if (preferred)
117
+ return preferred;
118
+ try {
119
+ return JSON.stringify(value).trim();
120
+ }
121
+ catch { }
122
+ }
123
+ return value == null ? '' : String(value).trim();
124
+ }
125
+ export function joinErrorMessages(errors) {
126
+ if (!errors?.length)
127
+ return '';
128
+ return errors.map(error => normalizeErrorMessage(error)).filter(Boolean).join('; ').trim();
129
+ }
100
130
  export function appendSystemPrompt(base, extra) {
101
131
  const lhs = String(base || '').trim();
102
132
  const rhs = String(extra || '').trim();
@@ -655,6 +685,7 @@ export async function run(cmd, opts, parseLine) {
655
685
  recentActivity: [],
656
686
  claudeToolsById: new Map(),
657
687
  seenClaudeToolIds: new Set(),
688
+ geminiToolsById: new Map(),
658
689
  };
659
690
  const shellCmd = cmd.map(Q).join(' ');
660
691
  agentLog(`[spawn] full command: cd ${Q(opts.workdir)} && ${shellCmd}`);
@@ -729,16 +760,17 @@ export async function run(cmd, opts, parseLine) {
729
760
  s.text = s.msgs.join('\n\n');
730
761
  if (!s.thinking.trim() && s.thinkParts.length)
731
762
  s.thinking = s.thinkParts.join('\n\n');
763
+ const errorText = joinErrorMessages(s.errors);
732
764
  const ok = procOk && !s.errors && !timedOut && !interrupted;
733
- const error = s.errors?.map(e => e.trim()).filter(Boolean).join('; ').trim()
765
+ const error = errorText
734
766
  || (interrupted ? 'Interrupted by user.' : null)
735
767
  || (timedOut ? `Timed out after ${opts.timeout}s before the agent reported completion.` : null)
736
768
  || (!procOk ? (stderr.trim() || `Failed (exit=${code}).`) : null);
737
769
  const incomplete = !ok || s.stopReason === 'max_tokens' || s.stopReason === 'timeout';
738
770
  const elapsed = ((Date.now() - start) / 1000).toFixed(1);
739
771
  agentLog(`[result] ok=${ok && !s.errors} elapsed=${elapsed}s text=${s.text.length}chars thinking=${s.thinking.length}chars session=${s.sessionId || '?'}`);
740
- if (s.errors)
741
- agentLog(`[result] errors: ${s.errors.join('; ')}`);
772
+ if (errorText)
773
+ agentLog(`[result] errors: ${errorText}`);
742
774
  if (s.stopReason)
743
775
  agentLog(`[result] stop_reason=${s.stopReason}`);
744
776
  if (stderr.trim() && !procOk)
@@ -746,7 +778,7 @@ export async function run(cmd, opts, parseLine) {
746
778
  return {
747
779
  ok, sessionId: s.sessionId, workspacePath: null,
748
780
  model: s.model, thinkingEffort: s.thinkingEffort,
749
- message: s.text.trim() || s.errors?.join('; ') || (procOk ? '(no textual response)' : `Failed (exit=${code}).\n\n${stderr.trim() || '(no output)'}`),
781
+ message: s.text.trim() || errorText || (procOk ? '(no textual response)' : `Failed (exit=${code}).\n\n${stderr.trim() || '(no output)'}`),
750
782
  thinking: s.thinking.trim() || null,
751
783
  elapsedS: (Date.now() - start) / 1000,
752
784
  inputTokens: s.inputTokens, outputTokens: s.outputTokens, cachedInputTokens: s.cachedInputTokens,