@geminixiang/mama 0.2.0-beta.0 → 0.2.0-beta.2

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 (147) hide show
  1. package/README.md +94 -27
  2. package/dist/adapter.d.ts +9 -5
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/adapter.js.map +1 -1
  5. package/dist/adapters/discord/bot.d.ts.map +1 -1
  6. package/dist/adapters/discord/bot.js +9 -6
  7. package/dist/adapters/discord/bot.js.map +1 -1
  8. package/dist/adapters/discord/context.d.ts.map +1 -1
  9. package/dist/adapters/discord/context.js +16 -13
  10. package/dist/adapters/discord/context.js.map +1 -1
  11. package/dist/adapters/slack/bot.d.ts +10 -2
  12. package/dist/adapters/slack/bot.d.ts.map +1 -1
  13. package/dist/adapters/slack/bot.js +196 -32
  14. package/dist/adapters/slack/bot.js.map +1 -1
  15. package/dist/adapters/slack/context.d.ts.map +1 -1
  16. package/dist/adapters/slack/context.js +24 -17
  17. package/dist/adapters/slack/context.js.map +1 -1
  18. package/dist/adapters/telegram/bot.d.ts +2 -0
  19. package/dist/adapters/telegram/bot.d.ts.map +1 -1
  20. package/dist/adapters/telegram/bot.js +109 -29
  21. package/dist/adapters/telegram/bot.js.map +1 -1
  22. package/dist/adapters/telegram/context.d.ts.map +1 -1
  23. package/dist/adapters/telegram/context.js +8 -43
  24. package/dist/adapters/telegram/context.js.map +1 -1
  25. package/dist/adapters/telegram/html.d.ts +3 -0
  26. package/dist/adapters/telegram/html.d.ts.map +1 -0
  27. package/dist/adapters/telegram/html.js +98 -0
  28. package/dist/adapters/telegram/html.js.map +1 -0
  29. package/dist/agent.d.ts +4 -9
  30. package/dist/agent.d.ts.map +1 -1
  31. package/dist/agent.js +141 -92
  32. package/dist/agent.js.map +1 -1
  33. package/dist/bindings.d.ts +44 -0
  34. package/dist/bindings.d.ts.map +1 -0
  35. package/dist/bindings.js +74 -0
  36. package/dist/bindings.js.map +1 -0
  37. package/dist/config.d.ts +7 -0
  38. package/dist/config.d.ts.map +1 -1
  39. package/dist/config.js +53 -12
  40. package/dist/config.js.map +1 -1
  41. package/dist/context.d.ts +7 -7
  42. package/dist/context.d.ts.map +1 -1
  43. package/dist/context.js +9 -9
  44. package/dist/context.js.map +1 -1
  45. package/dist/events.d.ts +14 -5
  46. package/dist/events.d.ts.map +1 -1
  47. package/dist/events.js +45 -10
  48. package/dist/events.js.map +1 -1
  49. package/dist/execution-resolver.d.ts +20 -0
  50. package/dist/execution-resolver.d.ts.map +1 -0
  51. package/dist/execution-resolver.js +49 -0
  52. package/dist/execution-resolver.js.map +1 -0
  53. package/dist/instrument.d.ts.map +1 -1
  54. package/dist/instrument.js +2 -1
  55. package/dist/instrument.js.map +1 -1
  56. package/dist/link-server.d.ts +17 -0
  57. package/dist/link-server.d.ts.map +1 -0
  58. package/dist/link-server.js +899 -0
  59. package/dist/link-server.js.map +1 -0
  60. package/dist/link-token.d.ts +32 -0
  61. package/dist/link-token.d.ts.map +1 -0
  62. package/dist/link-token.js +68 -0
  63. package/dist/link-token.js.map +1 -0
  64. package/dist/log.d.ts +2 -2
  65. package/dist/log.d.ts.map +1 -1
  66. package/dist/log.js +7 -7
  67. package/dist/log.js.map +1 -1
  68. package/dist/login.d.ts +29 -0
  69. package/dist/login.d.ts.map +1 -0
  70. package/dist/login.js +164 -0
  71. package/dist/login.js.map +1 -0
  72. package/dist/main.d.ts.map +1 -1
  73. package/dist/main.js +226 -55
  74. package/dist/main.js.map +1 -1
  75. package/dist/provisioner.d.ts +52 -0
  76. package/dist/provisioner.d.ts.map +1 -0
  77. package/dist/provisioner.js +291 -0
  78. package/dist/provisioner.js.map +1 -0
  79. package/dist/sandbox/container.d.ts +15 -0
  80. package/dist/sandbox/container.d.ts.map +1 -0
  81. package/dist/sandbox/container.js +122 -0
  82. package/dist/sandbox/container.js.map +1 -0
  83. package/dist/sandbox/errors.d.ts +6 -0
  84. package/dist/sandbox/errors.d.ts.map +1 -0
  85. package/dist/sandbox/errors.js +11 -0
  86. package/dist/sandbox/errors.js.map +1 -0
  87. package/dist/sandbox/firecracker.d.ts +16 -0
  88. package/dist/sandbox/firecracker.d.ts.map +1 -0
  89. package/dist/sandbox/firecracker.js +206 -0
  90. package/dist/sandbox/firecracker.js.map +1 -0
  91. package/dist/sandbox/host.d.ts +10 -0
  92. package/dist/sandbox/host.d.ts.map +1 -0
  93. package/dist/sandbox/host.js +85 -0
  94. package/dist/sandbox/host.js.map +1 -0
  95. package/dist/sandbox/image.d.ts +5 -0
  96. package/dist/sandbox/image.d.ts.map +1 -0
  97. package/dist/sandbox/image.js +30 -0
  98. package/dist/sandbox/image.js.map +1 -0
  99. package/dist/sandbox/index.d.ts +20 -0
  100. package/dist/sandbox/index.d.ts.map +1 -0
  101. package/dist/sandbox/index.js +51 -0
  102. package/dist/sandbox/index.js.map +1 -0
  103. package/dist/sandbox/types.d.ts +51 -0
  104. package/dist/sandbox/types.d.ts.map +1 -0
  105. package/dist/sandbox/types.js +2 -0
  106. package/dist/sandbox/types.js.map +1 -0
  107. package/dist/sandbox/utils.d.ts +4 -0
  108. package/dist/sandbox/utils.d.ts.map +1 -0
  109. package/dist/sandbox/utils.js +51 -0
  110. package/dist/sandbox/utils.js.map +1 -0
  111. package/dist/sandbox.d.ts +1 -39
  112. package/dist/sandbox.d.ts.map +1 -1
  113. package/dist/sandbox.js +1 -286
  114. package/dist/sandbox.js.map +1 -1
  115. package/dist/sentry.d.ts +1 -1
  116. package/dist/sentry.d.ts.map +1 -1
  117. package/dist/sentry.js +4 -2
  118. package/dist/sentry.js.map +1 -1
  119. package/dist/session-store.d.ts +2 -6
  120. package/dist/session-store.d.ts.map +1 -1
  121. package/dist/session-store.js +3 -10
  122. package/dist/session-store.js.map +1 -1
  123. package/dist/store.d.ts +1 -1
  124. package/dist/store.d.ts.map +1 -1
  125. package/dist/store.js +8 -8
  126. package/dist/store.js.map +1 -1
  127. package/dist/tools/event.d.ts +22 -0
  128. package/dist/tools/event.d.ts.map +1 -0
  129. package/dist/tools/event.js +104 -0
  130. package/dist/tools/event.js.map +1 -0
  131. package/dist/tools/index.d.ts +7 -1
  132. package/dist/tools/index.d.ts.map +1 -1
  133. package/dist/tools/index.js +5 -1
  134. package/dist/tools/index.js.map +1 -1
  135. package/dist/ui-copy.d.ts +12 -0
  136. package/dist/ui-copy.d.ts.map +1 -0
  137. package/dist/ui-copy.js +36 -0
  138. package/dist/ui-copy.js.map +1 -0
  139. package/dist/vault-routing.d.ts +9 -0
  140. package/dist/vault-routing.d.ts.map +1 -0
  141. package/dist/vault-routing.js +52 -0
  142. package/dist/vault-routing.js.map +1 -0
  143. package/dist/vault.d.ts +106 -0
  144. package/dist/vault.d.ts.map +1 -0
  145. package/dist/vault.js +389 -0
  146. package/dist/vault.js.map +1 -0
  147. package/package.json +12 -11
@@ -4,6 +4,7 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "fs";
4
4
  import { readFile } from "fs/promises";
5
5
  import { basename, join } from "path";
6
6
  import * as log from "../../log.js";
7
+ import { PRODUCT_NAME, formatForceStopped, formatNothingRunning } from "../../ui-copy.js";
7
8
  import { createSlackAdapters } from "./context.js";
8
9
  // ============================================================================
9
10
  // Exponential backoff utility for Slack API calls
@@ -125,6 +126,21 @@ export class SlackBot {
125
126
  return result.ts;
126
127
  });
127
128
  }
129
+ async postEphemeral(channel, user, text) {
130
+ return withRetry(async () => {
131
+ await this.webClient.chat.postEphemeral({ channel, user, text });
132
+ });
133
+ }
134
+ async openDirectMessage(userId) {
135
+ return withRetry(async () => {
136
+ const result = await this.webClient.conversations.open({ users: userId });
137
+ const channelId = result.channel?.id;
138
+ if (!channelId) {
139
+ throw new Error(`Failed to open DM for user ${userId}`);
140
+ }
141
+ return channelId;
142
+ });
143
+ }
128
144
  async updateMessage(channel, ts, text) {
129
145
  return withRetry(async () => {
130
146
  await this.webClient.chat.update({ channel, ts, text });
@@ -236,14 +252,30 @@ export class SlackBot {
236
252
  * Returns true if enqueued, false if queue is full (max 5).
237
253
  */
238
254
  enqueueEvent(event) {
239
- const queue = this.getQueue(event.channel);
255
+ const conversationId = event.conversationId;
256
+ const queue = this.getQueue(conversationId);
240
257
  if (queue.size() >= 5) {
241
- log.logWarning(`Event queue full for ${event.channel}, discarding: ${event.text.substring(0, 50)}`);
258
+ log.logWarning(`Event queue full for ${conversationId}, discarding: ${event.text.substring(0, 50)}`);
242
259
  return false;
243
260
  }
244
- log.logInfo(`Enqueueing event for ${event.channel}: ${event.text.substring(0, 50)}`);
261
+ log.logInfo(`Enqueueing event for ${conversationId}: ${event.text.substring(0, 50)}`);
245
262
  queue.enqueue(() => {
246
- const adapters = createSlackAdapters(event, this, true);
263
+ const slackEvent = {
264
+ type: event.type,
265
+ conversationId,
266
+ conversationKind: event.conversationKind,
267
+ channel: conversationId,
268
+ ts: event.ts,
269
+ thread_ts: event.thread_ts,
270
+ user: event.user,
271
+ text: event.text,
272
+ attachments: event.attachments?.map((attachment) => ({
273
+ original: attachment.name,
274
+ localPath: attachment.localPath,
275
+ })),
276
+ sessionKey: event.sessionKey,
277
+ };
278
+ const adapters = createSlackAdapters(slackEvent, this, true);
247
279
  return this.handler.handleEvent(event, this, adapters, true);
248
280
  });
249
281
  return true;
@@ -267,12 +299,12 @@ export class SlackBot {
267
299
  type: "section",
268
300
  text: {
269
301
  type: "mrkdwn",
270
- text: "*Pi Agent*\nWelcome back! Start a new task or check on running work.",
302
+ text: `*${PRODUCT_NAME}*\nStart a new task or check on running work.`,
271
303
  },
272
304
  accessory: {
273
305
  type: "image",
274
306
  image_url: "https://media1.tenor.com/m/lfDATg4Bhc0AAAAC/happy-cat.gif",
275
- alt_text: "Pi Agent",
307
+ alt_text: PRODUCT_NAME,
276
308
  },
277
309
  },
278
310
  ];
@@ -364,11 +396,11 @@ export class SlackBot {
364
396
  for (const ev of periodicEvents) {
365
397
  const channelLabel = ev.platform === "slack"
366
398
  ? (() => {
367
- const channel = this.channels.get(ev.channelId);
368
- const channelName = channel ? `#${channel.name}` : ev.channelId;
399
+ const channel = this.channels.get(ev.conversationId);
400
+ const channelName = channel ? `#${channel.name}` : ev.conversationId;
369
401
  return `${ev.platform}:${channelName}`;
370
402
  })()
371
- : `${ev.platform}:${ev.channelId}`;
403
+ : `${ev.platform}:${ev.conversationId}`;
372
404
  const nextStr = ev.nextRun
373
405
  ? new Date(ev.nextRun).toLocaleString("en-US", {
374
406
  month: "short",
@@ -413,6 +445,115 @@ export class SlackBot {
413
445
  }
414
446
  return this.handler.isRunning(channelId) ? channelId : null;
415
447
  }
448
+ createDirectCommandAdapters(conversationId, userId, userName, text, ts) {
449
+ const message = {
450
+ id: ts,
451
+ sessionKey: conversationId,
452
+ conversationKind: "direct",
453
+ userId,
454
+ userName,
455
+ text,
456
+ attachments: [],
457
+ };
458
+ const responseCtx = {
459
+ respond: async (responseText) => {
460
+ const messageTs = await this.postMessage(conversationId, responseText);
461
+ this.logBotResponse(conversationId, responseText, messageTs);
462
+ },
463
+ replaceResponse: async (responseText) => {
464
+ const messageTs = await this.postMessage(conversationId, responseText);
465
+ this.logBotResponse(conversationId, responseText, messageTs);
466
+ },
467
+ respondInThread: async (responseText) => {
468
+ const messageTs = await this.postMessage(conversationId, responseText);
469
+ this.logBotResponse(conversationId, responseText, messageTs);
470
+ },
471
+ setTyping: async () => { },
472
+ setWorking: async () => { },
473
+ uploadFile: async (filePath, title) => {
474
+ await this.uploadFile(conversationId, filePath, title);
475
+ },
476
+ deleteResponse: async () => { },
477
+ };
478
+ return {
479
+ message,
480
+ responseCtx,
481
+ platform: this.getPlatformInfo(),
482
+ };
483
+ }
484
+ createSlashCommandBot(conversationId, threadTs) {
485
+ return {
486
+ start: async () => { },
487
+ postMessage: async (_channel, text) => {
488
+ if (threadTs) {
489
+ return this.postInThread(conversationId, threadTs, text);
490
+ }
491
+ return this.postMessage(conversationId, text);
492
+ },
493
+ updateMessage: async (channel, ts, text) => {
494
+ await this.updateMessage(channel, ts, text);
495
+ },
496
+ enqueueEvent: (event) => this.enqueueEvent(event),
497
+ getPlatformInfo: () => this.getPlatformInfo(),
498
+ };
499
+ }
500
+ async routeSlashLoginCommand(payload) {
501
+ const commandSuffix = payload.text?.trim();
502
+ const commandText = commandSuffix ? `${payload.command} ${commandSuffix}` : payload.command;
503
+ const createdAt = new Date();
504
+ const eventTs = (createdAt.getTime() / 1000).toFixed(6);
505
+ const sourceChannelId = payload.channel_id;
506
+ const isDirectMessage = sourceChannelId.startsWith("D");
507
+ const targetChannelId = isDirectMessage
508
+ ? sourceChannelId
509
+ : await this.openDirectMessage(payload.user_id);
510
+ const userName = payload.user_name ?? this.getUser(payload.user_id)?.userName;
511
+ this.logToFile(targetChannelId, {
512
+ date: createdAt.toISOString(),
513
+ ts: eventTs,
514
+ user: payload.user_id,
515
+ userName,
516
+ text: commandText,
517
+ attachments: [],
518
+ isBot: false,
519
+ });
520
+ if (!isDirectMessage) {
521
+ await this.postEphemeral(sourceChannelId, payload.user_id, `我已私訊你 ${PRODUCT_NAME} 的登入連結,請到私訊完成設定。`);
522
+ }
523
+ const event = {
524
+ type: "dm",
525
+ conversationId: targetChannelId,
526
+ conversationKind: "direct",
527
+ ts: eventTs,
528
+ user: payload.user_id,
529
+ text: commandText,
530
+ attachments: [],
531
+ sessionKey: targetChannelId,
532
+ };
533
+ const adapters = this.createDirectCommandAdapters(targetChannelId, payload.user_id, userName, commandText, eventTs);
534
+ await this.handler.handleEvent(event, this, adapters, false);
535
+ }
536
+ async routeSlashNewCommand(payload) {
537
+ const conversationId = payload.channel_id;
538
+ if (!conversationId.startsWith("D")) {
539
+ await this.postEphemeral(conversationId, payload.user_id, `為了避免誤清除共享上下文,${payload.command} 目前只能在與 ${PRODUCT_NAME} 的私訊中使用。`);
540
+ return;
541
+ }
542
+ const createdAt = new Date();
543
+ const eventTs = (createdAt.getTime() / 1000).toFixed(6);
544
+ const userName = payload.user_name ?? this.getUser(payload.user_id)?.userName;
545
+ this.logToFile(conversationId, {
546
+ date: createdAt.toISOString(),
547
+ ts: eventTs,
548
+ user: payload.user_id,
549
+ userName,
550
+ text: payload.command,
551
+ attachments: [],
552
+ isBot: false,
553
+ });
554
+ const commandBot = this.createSlashCommandBot(conversationId);
555
+ await this.handler.handleNew(conversationId, conversationId, commandBot);
556
+ }
416
557
  setupEventHandlers() {
417
558
  // Channel @mentions
418
559
  this.socketClient.on("app_mention", ({ event, ack }) => {
@@ -427,6 +568,8 @@ export class SlackBot {
427
568
  const sessionKey = e.thread_ts ? `${e.channel}:${e.thread_ts}` : e.channel;
428
569
  const slackEvent = {
429
570
  type: "mention",
571
+ conversationId: e.channel,
572
+ conversationKind: "shared",
430
573
  channel: e.channel,
431
574
  ts: e.ts,
432
575
  thread_ts: e.thread_ts,
@@ -451,21 +594,15 @@ export class SlackBot {
451
594
  this.handler.handleStop(stopTarget, e.channel, this);
452
595
  }
453
596
  else {
454
- this.postMessage(e.channel, "_Nothing running_");
597
+ this.postMessage(e.channel, formatNothingRunning("slack"));
455
598
  }
456
599
  ack();
457
600
  return;
458
601
  }
459
- // SYNC: Check if busy (per-thread)
460
- if (this.handler.isRunning(sessionKey)) {
461
- this.postMessage(e.channel, "_Already working in this thread. Say `@mama stop` to cancel._");
462
- }
463
- else {
464
- this.getQueue(sessionKey).enqueue(() => {
465
- const adapters = createSlackAdapters(slackEvent, this, false);
466
- return this.handler.handleEvent(slackEvent, this, adapters, false);
467
- });
468
- }
602
+ this.getQueue(sessionKey).enqueue(() => {
603
+ const adapters = createSlackAdapters(slackEvent, this, false);
604
+ return this.handler.handleEvent(slackEvent, this, adapters, false);
605
+ });
469
606
  ack();
470
607
  });
471
608
  // All messages (for logging) + DMs (for triggering)
@@ -485,6 +622,7 @@ export class SlackBot {
485
622
  return;
486
623
  }
487
624
  const isDM = e.channel_type === "im";
625
+ const conversationKind = isDM ? "direct" : "shared";
488
626
  const isBotMention = e.text?.includes(`<@${this.botUserId}>`);
489
627
  // Skip channel @mentions - already handled by app_mention event
490
628
  if (!isDM && isBotMention) {
@@ -493,6 +631,8 @@ export class SlackBot {
493
631
  }
494
632
  const slackEvent = {
495
633
  type: isDM ? "dm" : "mention",
634
+ conversationId: e.channel,
635
+ conversationKind,
496
636
  channel: e.channel,
497
637
  ts: e.ts,
498
638
  thread_ts: e.thread_ts,
@@ -518,7 +658,7 @@ export class SlackBot {
518
658
  this.handler.handleStop(stopTarget, e.channel, this);
519
659
  }
520
660
  else {
521
- this.postMessage(e.channel, "_Nothing running_");
661
+ this.postMessage(e.channel, formatNothingRunning("slack"));
522
662
  }
523
663
  ack();
524
664
  return;
@@ -532,23 +672,47 @@ export class SlackBot {
532
672
  this.handler.handleStop(dmSessionKey, e.channel, this); // Don't await, don't queue
533
673
  }
534
674
  else {
535
- this.postMessage(e.channel, "_Nothing running_");
675
+ this.postMessage(e.channel, formatNothingRunning("slack"));
536
676
  }
537
677
  ack();
538
678
  return;
539
679
  }
540
- if (this.handler.isRunning(dmSessionKey)) {
541
- this.postMessage(e.channel, "_Already working. Say `stop` to cancel._");
542
- }
543
- else {
544
- this.getQueue(dmSessionKey).enqueue(() => {
545
- const adapters = createSlackAdapters(slackEvent, this, false);
546
- return this.handler.handleEvent(slackEvent, this, adapters, false);
547
- });
548
- }
680
+ this.getQueue(dmSessionKey).enqueue(() => {
681
+ const adapters = createSlackAdapters(slackEvent, this, false);
682
+ return this.handler.handleEvent(slackEvent, this, adapters, false);
683
+ });
549
684
  }
550
685
  ack();
551
686
  });
687
+ this.socketClient.on("slash_commands", async ({ body, ack }) => {
688
+ const payload = body;
689
+ await ack();
690
+ if (!payload.command || !payload.channel_id || !payload.user_id) {
691
+ return;
692
+ }
693
+ const handlerPromise = payload.command === "/pi-login"
694
+ ? this.routeSlashLoginCommand({
695
+ command: payload.command,
696
+ text: payload.text,
697
+ channel_id: payload.channel_id,
698
+ user_id: payload.user_id,
699
+ user_name: payload.user_name,
700
+ })
701
+ : payload.command === "/pi-new"
702
+ ? this.routeSlashNewCommand({
703
+ command: payload.command,
704
+ channel_id: payload.channel_id,
705
+ user_id: payload.user_id,
706
+ user_name: payload.user_name,
707
+ })
708
+ : null;
709
+ if (!handlerPromise) {
710
+ return;
711
+ }
712
+ handlerPromise.catch((err) => {
713
+ log.logWarning("Slack slash command error", err instanceof Error ? err.message : String(err));
714
+ });
715
+ });
552
716
  // App Home tab
553
717
  this.socketClient.on("app_home_opened", ({ event, ack }) => {
554
718
  const e = event;
@@ -579,7 +743,7 @@ export class SlackBot {
579
743
  // Use handler's forceStop method
580
744
  this.handler.forceStop(sessionKey);
581
745
  // Notify in channel
582
- await this.postMessage(channelId, `_🔴 Force stopped by ${userId}_`);
746
+ await this.postMessage(channelId, formatForceStopped("slack", userId ?? "unknown"));
583
747
  // Refresh home tab
584
748
  if (userId) {
585
749
  this.webClient.views