@geminixiang/mama 0.2.0-beta.5 → 0.2.0-beta.7

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 (94) hide show
  1. package/README.md +81 -18
  2. package/dist/adapter.d.ts +3 -0
  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 +1 -1
  6. package/dist/adapters/discord/bot.d.ts.map +1 -1
  7. package/dist/adapters/discord/bot.js +84 -17
  8. package/dist/adapters/discord/bot.js.map +1 -1
  9. package/dist/adapters/slack/bot.d.ts +12 -0
  10. package/dist/adapters/slack/bot.d.ts.map +1 -1
  11. package/dist/adapters/slack/bot.js +219 -15
  12. package/dist/adapters/slack/bot.js.map +1 -1
  13. package/dist/adapters/slack/context.d.ts.map +1 -1
  14. package/dist/adapters/slack/context.js +5 -0
  15. package/dist/adapters/slack/context.js.map +1 -1
  16. package/dist/adapters/slack/tools/attach.d.ts +1 -1
  17. package/dist/adapters/slack/tools/attach.d.ts.map +1 -1
  18. package/dist/adapters/slack/tools/attach.js.map +1 -1
  19. package/dist/adapters/telegram/bot.d.ts.map +1 -1
  20. package/dist/adapters/telegram/bot.js +32 -35
  21. package/dist/adapters/telegram/bot.js.map +1 -1
  22. package/dist/agent.d.ts.map +1 -1
  23. package/dist/agent.js +42 -26
  24. package/dist/agent.js.map +1 -1
  25. package/dist/commands/index.d.ts.map +1 -1
  26. package/dist/commands/index.js +10 -1
  27. package/dist/commands/index.js.map +1 -1
  28. package/dist/commands/model.d.ts +14 -0
  29. package/dist/commands/model.d.ts.map +1 -0
  30. package/dist/commands/model.js +112 -0
  31. package/dist/commands/model.js.map +1 -0
  32. package/dist/commands/new.d.ts +9 -0
  33. package/dist/commands/new.d.ts.map +1 -0
  34. package/dist/commands/new.js +28 -0
  35. package/dist/commands/new.js.map +1 -0
  36. package/dist/commands/sandbox.d.ts +10 -0
  37. package/dist/commands/sandbox.d.ts.map +1 -0
  38. package/dist/commands/sandbox.js +65 -0
  39. package/dist/commands/sandbox.js.map +1 -0
  40. package/dist/commands/session-view.d.ts.map +1 -1
  41. package/dist/commands/session-view.js +29 -9
  42. package/dist/commands/session-view.js.map +1 -1
  43. package/dist/commands/types.d.ts +2 -0
  44. package/dist/commands/types.d.ts.map +1 -1
  45. package/dist/commands/types.js.map +1 -1
  46. package/dist/commands/utils.d.ts +3 -0
  47. package/dist/commands/utils.d.ts.map +1 -1
  48. package/dist/commands/utils.js +5 -0
  49. package/dist/commands/utils.js.map +1 -1
  50. package/dist/config.d.ts +13 -4
  51. package/dist/config.d.ts.map +1 -1
  52. package/dist/config.js +177 -31
  53. package/dist/config.js.map +1 -1
  54. package/dist/context.d.ts +1 -1
  55. package/dist/context.d.ts.map +1 -1
  56. package/dist/context.js +50 -35
  57. package/dist/context.js.map +1 -1
  58. package/dist/main.d.ts.map +1 -1
  59. package/dist/main.js +53 -4
  60. package/dist/main.js.map +1 -1
  61. package/dist/provisioner.d.ts +12 -0
  62. package/dist/provisioner.d.ts.map +1 -1
  63. package/dist/provisioner.js +41 -10
  64. package/dist/provisioner.js.map +1 -1
  65. package/dist/runtime/session-runtime.d.ts +1 -0
  66. package/dist/runtime/session-runtime.d.ts.map +1 -1
  67. package/dist/runtime/session-runtime.js +18 -0
  68. package/dist/runtime/session-runtime.js.map +1 -1
  69. package/dist/session-store.d.ts +1 -1
  70. package/dist/session-store.d.ts.map +1 -1
  71. package/dist/session-store.js +1 -1
  72. package/dist/session-store.js.map +1 -1
  73. package/dist/session-view/service.d.ts.map +1 -1
  74. package/dist/session-view/service.js +1 -1
  75. package/dist/session-view/service.js.map +1 -1
  76. package/dist/tools/bash.d.ts +1 -1
  77. package/dist/tools/bash.d.ts.map +1 -1
  78. package/dist/tools/bash.js.map +1 -1
  79. package/dist/tools/edit.d.ts +1 -1
  80. package/dist/tools/edit.d.ts.map +1 -1
  81. package/dist/tools/edit.js.map +1 -1
  82. package/dist/tools/event.d.ts +1 -1
  83. package/dist/tools/event.d.ts.map +1 -1
  84. package/dist/tools/event.js.map +1 -1
  85. package/dist/tools/index.d.ts +1 -1
  86. package/dist/tools/index.d.ts.map +1 -1
  87. package/dist/tools/index.js.map +1 -1
  88. package/dist/tools/read.d.ts +1 -1
  89. package/dist/tools/read.d.ts.map +1 -1
  90. package/dist/tools/read.js.map +1 -1
  91. package/dist/tools/write.d.ts +1 -1
  92. package/dist/tools/write.d.ts.map +1 -1
  93. package/dist/tools/write.js.map +1 -1
  94. package/package.json +4 -4
@@ -15,6 +15,38 @@ function slackIsRateLimited(err) {
15
15
  return data?.error === "rate_limited" || data?.response?.status === 429;
16
16
  }
17
17
  const slackRetry = (fn) => withRetry(fn, { isRateLimited: slackIsRateLimited });
18
+ function collectSlackText(value, parts) {
19
+ if (value === null || value === undefined)
20
+ return;
21
+ if (typeof value === "string") {
22
+ const trimmed = value.trim();
23
+ if (trimmed)
24
+ parts.push(trimmed);
25
+ return;
26
+ }
27
+ if (Array.isArray(value)) {
28
+ for (const item of value)
29
+ collectSlackText(item, parts);
30
+ return;
31
+ }
32
+ if (typeof value !== "object")
33
+ return;
34
+ const obj = value;
35
+ for (const key of ["text", "fallback", "title", "value"]) {
36
+ collectSlackText(obj[key], parts);
37
+ }
38
+ collectSlackText(obj.fields, parts);
39
+ collectSlackText(obj.elements, parts);
40
+ collectSlackText(obj.blocks, parts);
41
+ }
42
+ function buildSlackAppMessageText(event) {
43
+ const parts = [];
44
+ collectSlackText(event.text, parts);
45
+ collectSlackText(event.blocks, parts);
46
+ collectSlackText(event.attachments, parts);
47
+ const deduped = parts.filter((part, index) => parts.indexOf(part) === index);
48
+ return deduped.join("\n");
49
+ }
18
50
  import { createSlackAdapters } from "./context.js";
19
51
  import { hasMaterializedSlackBranchSession } from "./branch-manager.js";
20
52
  import { resolveSlackSessionKey } from "./session.js";
@@ -24,6 +56,8 @@ import { resolveSlackSessionKey } from "./session.js";
24
56
  export class SlackBot {
25
57
  constructor(handler, config) {
26
58
  this.botUserId = null;
59
+ this.botId = null;
60
+ this.ownMentionRegex = null;
27
61
  this.startupTs = null; // Messages older than this are just logged, not processed
28
62
  this.users = new Map();
29
63
  this.channels = new Map();
@@ -44,6 +78,7 @@ export class SlackBot {
44
78
  async start() {
45
79
  const auth = await this.webClient.auth.test();
46
80
  this.botUserId = auth.user_id;
81
+ this.botId = typeof auth.bot_id === "string" ? auth.bot_id : null;
47
82
  await Promise.all([this.fetchUsers(), this.fetchChannels()]);
48
83
  log.logInfo(`Loaded ${this.channels.size} channels, ${this.users.size} users`);
49
84
  await this.backfillAllChannels();
@@ -65,6 +100,15 @@ export class SlackBot {
65
100
  getAllChannels() {
66
101
  return Array.from(this.channels.values());
67
102
  }
103
+ stripOwnMention(text) {
104
+ const source = text ?? "";
105
+ if (!this.botUserId)
106
+ return source.trim();
107
+ if (!this.ownMentionRegex || !this.ownMentionRegex.source.includes(this.botUserId)) {
108
+ this.ownMentionRegex = new RegExp(`<@${this.botUserId}>`, "gi");
109
+ }
110
+ return source.replace(this.ownMentionRegex, "").trim();
111
+ }
68
112
  async postMessage(channel, text) {
69
113
  return slackRetry(async () => {
70
114
  const result = await this.webClient.chat.postMessage({ channel, text });
@@ -76,9 +120,37 @@ export class SlackBot {
76
120
  await this.webClient.chat.postEphemeral({ channel, user, text });
77
121
  });
78
122
  }
123
+ async postEphemeralBlocks(channel, user, text, blocks) {
124
+ return slackRetry(async () => {
125
+ await this.webClient.chat.postEphemeral({ channel, user, text, blocks: blocks });
126
+ });
127
+ }
128
+ async postMessageBlocks(channel, text, blocks) {
129
+ return slackRetry(async () => {
130
+ const result = await this.webClient.chat.postMessage({
131
+ channel,
132
+ text,
133
+ blocks: blocks,
134
+ });
135
+ return result.ts;
136
+ });
137
+ }
79
138
  async postPrivate(conversationId, userId, text) {
80
139
  await this.postEphemeral(conversationId, userId, text);
81
140
  }
141
+ async postPrivateDiagnostic(conversationId, userId, text, options) {
142
+ if (options?.style !== "muted") {
143
+ await this.postPrivate(conversationId, userId, options?.style === "error" ? `_${text}_` : text);
144
+ return;
145
+ }
146
+ const CONTEXT_TEXT_LIMIT = 3000;
147
+ const blockText = text.length > CONTEXT_TEXT_LIMIT
148
+ ? text.substring(0, CONTEXT_TEXT_LIMIT - 20) + "\n_(truncated)_"
149
+ : text;
150
+ await this.postEphemeralBlocks(conversationId, userId, text, [
151
+ { type: "context", elements: [{ type: "mrkdwn", text: blockText }] },
152
+ ]);
153
+ }
82
154
  async openDirectMessage(userId) {
83
155
  return slackRetry(async () => {
84
156
  const result = await this.webClient.conversations.open({ users: userId });
@@ -387,6 +459,10 @@ export class SlackBot {
387
459
  return null;
388
460
  return resolveOnlyScopedStopTarget(this.handler, channelId);
389
461
  }
462
+ isStopText(text) {
463
+ const normalized = text.trim().toLowerCase();
464
+ return normalized === "stop" || normalized === "/stop";
465
+ }
390
466
  createCommandAdapters(conversationId, userId, userName, text, ts, options = {}) {
391
467
  const message = {
392
468
  id: ts,
@@ -405,10 +481,29 @@ export class SlackBot {
405
481
  const messageTs = await this.postMessage(conversationId, responseText);
406
482
  this.logBotResponse(conversationId, responseText, messageTs);
407
483
  };
484
+ const respondMuted = async (responseText) => {
485
+ const CONTEXT_TEXT_LIMIT = 3000;
486
+ const blockText = responseText.length > CONTEXT_TEXT_LIMIT
487
+ ? responseText.substring(0, CONTEXT_TEXT_LIMIT - 20) + "\n_(truncated)_"
488
+ : responseText;
489
+ const blocks = [{ type: "context", elements: [{ type: "mrkdwn", text: blockText }] }];
490
+ if (options.ephemeralChannelId) {
491
+ await this.postEphemeralBlocks(options.ephemeralChannelId, userId, responseText, blocks);
492
+ return;
493
+ }
494
+ const messageTs = await this.postMessageBlocks(conversationId, responseText, blocks);
495
+ this.logBotResponse(conversationId, responseText, messageTs);
496
+ };
408
497
  const responseCtx = {
409
498
  respond,
410
499
  replaceResponse: respond,
411
- respondDiagnostic: respond,
500
+ respondDiagnostic: async (responseText, options) => {
501
+ if (options?.style === "muted") {
502
+ await respondMuted(responseText);
503
+ return;
504
+ }
505
+ await respond(options?.style === "error" ? `_${responseText}_` : responseText);
506
+ },
412
507
  respondToolResult: async (result) => {
413
508
  const duration = (result.durationMs / 1000).toFixed(1);
414
509
  await respond(`${result.isError ? "Error" : "Done"} ${result.toolName} (${duration}s)\n${result.result}`);
@@ -500,6 +595,40 @@ export class SlackBot {
500
595
  const commandBot = this.createSlashCommandBot(conversationId);
501
596
  await this.handler.handleNew(conversationId, conversationId, commandBot);
502
597
  }
598
+ async routeSlashModelCommand(payload) {
599
+ const conversationId = payload.channel_id;
600
+ const isDirectMessage = conversationId.startsWith("D");
601
+ const createdAt = new Date();
602
+ const eventTs = (createdAt.getTime() / 1000).toFixed(6);
603
+ const userName = payload.user_name ?? this.getUser(payload.user_id)?.userName;
604
+ const commandSuffix = payload.text?.trim();
605
+ const commandText = commandSuffix ? `${payload.command} ${commandSuffix}` : payload.command;
606
+ this.logToFile(conversationId, {
607
+ date: createdAt.toISOString(),
608
+ ts: eventTs,
609
+ user: payload.user_id,
610
+ userName,
611
+ text: commandText,
612
+ attachments: [],
613
+ isBot: false,
614
+ });
615
+ const sessionKey = conversationId;
616
+ const event = {
617
+ type: "dm",
618
+ conversationId,
619
+ conversationKind: isDirectMessage ? "direct" : "shared",
620
+ ts: eventTs,
621
+ user: payload.user_id,
622
+ text: commandText,
623
+ attachments: [],
624
+ sessionKey,
625
+ };
626
+ const adapters = this.createCommandAdapters(conversationId, payload.user_id, userName, commandText, eventTs, isDirectMessage ? {} : { ephemeralChannelId: conversationId });
627
+ await this.handler.handleEvent(event, this, adapters, false);
628
+ }
629
+ async routeSlashSandboxCommand(payload) {
630
+ await this.routeSlashModelCommand(payload);
631
+ }
503
632
  async routeSlashSessionCommand(payload) {
504
633
  const conversationId = payload.channel_id;
505
634
  const isDirectMessage = conversationId.startsWith("D");
@@ -550,7 +679,7 @@ export class SlackBot {
550
679
  ts: e.ts,
551
680
  thread_ts: e.thread_ts,
552
681
  user: e.user,
553
- text: e.text.replace(/<@[A-Z0-9]+>/gi, "").trim(),
682
+ text: this.stripOwnMention(e.text),
554
683
  files: e.files,
555
684
  sessionKey,
556
685
  };
@@ -565,7 +694,7 @@ export class SlackBot {
565
694
  return;
566
695
  }
567
696
  // Check for stop command - execute immediately, don't queue!
568
- if (slackEvent.text.toLowerCase().trim() === "stop") {
697
+ if (this.isStopText(slackEvent.text)) {
569
698
  const stopTarget = this.resolveStopTarget(e.channel, e.thread_ts);
570
699
  if (stopTarget) {
571
700
  this.handler.handleStop(stopTarget, e.channel, this);
@@ -589,8 +718,30 @@ export class SlackBot {
589
718
  // All messages (for logging) + DMs (for triggering)
590
719
  this.socketClient.on("message", ({ event, ack }) => {
591
720
  const e = event;
592
- // Skip bot messages, edits, etc.
593
- if (e.bot_id || !e.user || e.user === this.botUserId) {
721
+ const hasFiles = !!e.files && e.files.length > 0;
722
+ const hasSlackContent = !!e.text || hasFiles || !!e.blocks?.length || !!e.attachments?.length;
723
+ const isOwnBotMessage = (!!e.user && e.user === this.botUserId) || (!!this.botId && e.bot_id === this.botId);
724
+ if (isOwnBotMessage) {
725
+ ack();
726
+ return;
727
+ }
728
+ const isExternalBotMessage = !!e.bot_id || e.subtype === "bot_message";
729
+ if (isExternalBotMessage) {
730
+ if (e.subtype !== undefined && e.subtype !== "bot_message" && e.subtype !== "file_share") {
731
+ ack();
732
+ return;
733
+ }
734
+ if (!hasSlackContent) {
735
+ ack();
736
+ return;
737
+ }
738
+ void this.logExternalBotMessage(e).catch((err) => {
739
+ log.logWarning("Failed to log Slack bot message", String(err));
740
+ });
741
+ ack();
742
+ return;
743
+ }
744
+ if (!e.user) {
594
745
  ack();
595
746
  return;
596
747
  }
@@ -598,7 +749,7 @@ export class SlackBot {
598
749
  ack();
599
750
  return;
600
751
  }
601
- if (!e.text && (!e.files || e.files.length === 0)) {
752
+ if (!hasSlackContent) {
602
753
  ack();
603
754
  return;
604
755
  }
@@ -620,7 +771,7 @@ export class SlackBot {
620
771
  ts: e.ts,
621
772
  thread_ts: e.thread_ts,
622
773
  user: e.user,
623
- text: (e.text || "").replace(/<@[A-Z0-9]+>/gi, "").trim(),
774
+ text: this.stripOwnMention(e.text),
624
775
  files: e.files,
625
776
  sessionKey,
626
777
  };
@@ -636,7 +787,7 @@ export class SlackBot {
636
787
  }
637
788
  // Check for stop command in channel threads (without @mention)
638
789
  // app_mention handles "@mama stop", but bare "stop" in a thread comes here
639
- if (!isDM && e.thread_ts && slackEvent.text.toLowerCase().trim() === "stop") {
790
+ if (!isDM && e.thread_ts && this.isStopText(slackEvent.text)) {
640
791
  const stopTarget = this.resolveStopTarget(e.channel, e.thread_ts);
641
792
  if (stopTarget) {
642
793
  this.handler.handleStop(stopTarget, e.channel, this);
@@ -654,7 +805,7 @@ export class SlackBot {
654
805
  if (isDM || isSharedThreadReply) {
655
806
  const activeSessionKey = slackEvent.sessionKey ?? resolveSlackSessionKey(e.channel, e.thread_ts);
656
807
  // Check for stop command - execute immediately, don't queue!
657
- if (slackEvent.text.toLowerCase().trim() === "stop") {
808
+ if (this.isStopText(slackEvent.text)) {
658
809
  const stopTarget = this.resolveStopTarget(e.channel, e.thread_ts);
659
810
  if (stopTarget) {
660
811
  this.handler.handleStop(stopTarget, e.channel, this); // Don't await, don't queue
@@ -709,7 +860,23 @@ export class SlackBot {
709
860
  user_id: payload.user_id,
710
861
  user_name: payload.user_name,
711
862
  })
712
- : null;
863
+ : payload.command === "/pi-model"
864
+ ? this.routeSlashModelCommand({
865
+ command: payload.command,
866
+ text: payload.text,
867
+ channel_id: payload.channel_id,
868
+ user_id: payload.user_id,
869
+ user_name: payload.user_name,
870
+ })
871
+ : payload.command === "/pi-sandbox"
872
+ ? this.routeSlashSandboxCommand({
873
+ command: payload.command,
874
+ text: payload.text,
875
+ channel_id: payload.channel_id,
876
+ user_id: payload.user_id,
877
+ user_name: payload.user_name,
878
+ })
879
+ : null;
713
880
  if (!handlerPromise) {
714
881
  return;
715
882
  }
@@ -782,6 +949,27 @@ export class SlackBot {
782
949
  });
783
950
  return attachments;
784
951
  }
952
+ async logExternalBotMessage(event) {
953
+ const attachments = event.files
954
+ ? await this.store.processAttachments(event.channel, event.files, event.ts)
955
+ : [];
956
+ const botName = event.username ?? event.bot_profile?.name ?? event.bot_profile?.real_name ?? event.bot_id;
957
+ this.logToFile(event.channel, {
958
+ date: new Date(parseFloat(event.ts) * 1000).toISOString(),
959
+ ts: event.ts,
960
+ threadTs: event.thread_ts,
961
+ user: event.bot_id ? `bot:${event.bot_id}` : "external-bot",
962
+ userName: botName,
963
+ displayName: botName,
964
+ text: buildSlackAppMessageText(event),
965
+ attachments,
966
+ isBot: true,
967
+ botId: event.bot_id,
968
+ appId: event.app_id ?? event.bot_profile?.app_id,
969
+ subtype: event.subtype,
970
+ });
971
+ return attachments;
972
+ }
785
973
  // ==========================================================================
786
974
  // Private - Backfill
787
975
  // ==========================================================================
@@ -830,14 +1018,26 @@ export class SlackBot {
830
1018
  cursor = result.response_metadata?.next_cursor;
831
1019
  pageCount++;
832
1020
  } while (cursor && pageCount < maxPages);
833
- // Filter: include mama's messages, exclude other bots, skip already logged
1021
+ // Filter: include mama's messages, external app/bot messages, and user messages.
834
1022
  const relevantMessages = allMessages.filter((msg) => {
835
1023
  if (!msg.ts || existingTs.has(msg.ts))
836
1024
  return false; // Skip duplicates
837
1025
  if (msg.user === this.botUserId)
838
1026
  return true;
839
- if (msg.bot_id)
840
- return false;
1027
+ const isExternalBotMessage = !!msg.bot_id || msg.subtype === "bot_message";
1028
+ if (isExternalBotMessage) {
1029
+ if (this.botId && msg.bot_id === this.botId)
1030
+ return false;
1031
+ if (msg.subtype !== undefined &&
1032
+ msg.subtype !== "bot_message" &&
1033
+ msg.subtype !== "file_share") {
1034
+ return false;
1035
+ }
1036
+ return (!!msg.text ||
1037
+ !!(msg.files && msg.files.length > 0) ||
1038
+ !!msg.blocks?.length ||
1039
+ !!msg.attachments?.length);
1040
+ }
841
1041
  if (msg.subtype !== undefined && msg.subtype !== "file_share")
842
1042
  return false;
843
1043
  if (!msg.user)
@@ -851,9 +1051,13 @@ export class SlackBot {
851
1051
  // Log each message to log.jsonl
852
1052
  for (const msg of relevantMessages) {
853
1053
  const isMamaMessage = msg.user === this.botUserId;
1054
+ const isExternalBotMessage = !isMamaMessage && (!!msg.bot_id || msg.subtype === "bot_message");
1055
+ if (isExternalBotMessage) {
1056
+ await this.logExternalBotMessage({ ...msg, channel: channelId, ts: msg.ts });
1057
+ continue;
1058
+ }
854
1059
  const user = this.users.get(msg.user);
855
- // Strip @mentions from text (same as live messages)
856
- const text = (msg.text || "").replace(/<@[A-Z0-9]+>/gi, "").trim();
1060
+ const text = this.stripOwnMention(msg.text);
857
1061
  const attachments = msg.files
858
1062
  ? await this.store.processAttachments(channelId, msg.files, msg.ts)
859
1063
  : [];