@geminixiang/mama 0.2.0-beta.1 → 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 (102) hide show
  1. package/README.md +85 -58
  2. package/dist/adapter.d.ts +8 -6
  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 +2 -2
  6. package/dist/adapters/discord/bot.d.ts.map +1 -1
  7. package/dist/adapters/discord/bot.js +20 -29
  8. package/dist/adapters/discord/bot.js.map +1 -1
  9. package/dist/adapters/discord/context.d.ts.map +1 -1
  10. package/dist/adapters/discord/context.js +16 -20
  11. package/dist/adapters/discord/context.js.map +1 -1
  12. package/dist/adapters/slack/bot.d.ts +11 -4
  13. package/dist/adapters/slack/bot.d.ts.map +1 -1
  14. package/dist/adapters/slack/bot.js +199 -73
  15. package/dist/adapters/slack/bot.js.map +1 -1
  16. package/dist/adapters/slack/context.d.ts.map +1 -1
  17. package/dist/adapters/slack/context.js +27 -30
  18. package/dist/adapters/slack/context.js.map +1 -1
  19. package/dist/adapters/telegram/bot.d.ts +4 -2
  20. package/dist/adapters/telegram/bot.d.ts.map +1 -1
  21. package/dist/adapters/telegram/bot.js +130 -71
  22. package/dist/adapters/telegram/bot.js.map +1 -1
  23. package/dist/adapters/telegram/context.d.ts.map +1 -1
  24. package/dist/adapters/telegram/context.js +9 -95
  25. package/dist/adapters/telegram/context.js.map +1 -1
  26. package/dist/adapters/telegram/html.d.ts +3 -0
  27. package/dist/adapters/telegram/html.d.ts.map +1 -0
  28. package/dist/adapters/telegram/html.js +98 -0
  29. package/dist/adapters/telegram/html.js.map +1 -0
  30. package/dist/agent.d.ts +3 -11
  31. package/dist/agent.d.ts.map +1 -1
  32. package/dist/agent.js +63 -70
  33. package/dist/agent.js.map +1 -1
  34. package/dist/bindings.d.ts +1 -20
  35. package/dist/bindings.d.ts.map +1 -1
  36. package/dist/bindings.js +1 -21
  37. package/dist/bindings.js.map +1 -1
  38. package/dist/config.d.ts +7 -27
  39. package/dist/config.d.ts.map +1 -1
  40. package/dist/config.js +77 -63
  41. package/dist/config.js.map +1 -1
  42. package/dist/context.d.ts +2 -2
  43. package/dist/context.d.ts.map +1 -1
  44. package/dist/context.js +2 -2
  45. package/dist/context.js.map +1 -1
  46. package/dist/events.d.ts +11 -6
  47. package/dist/events.d.ts.map +1 -1
  48. package/dist/events.js +33 -13
  49. package/dist/events.js.map +1 -1
  50. package/dist/execution-resolver.d.ts.map +1 -1
  51. package/dist/execution-resolver.js +1 -3
  52. package/dist/execution-resolver.js.map +1 -1
  53. package/dist/instrument.d.ts.map +1 -1
  54. package/dist/instrument.js +5 -11
  55. package/dist/instrument.js.map +1 -1
  56. package/dist/link-server.d.ts +2 -1
  57. package/dist/link-server.d.ts.map +1 -1
  58. package/dist/link-server.js +62 -2
  59. package/dist/link-server.js.map +1 -1
  60. package/dist/login.d.ts +1 -1
  61. package/dist/login.d.ts.map +1 -1
  62. package/dist/login.js +1 -1
  63. package/dist/login.js.map +1 -1
  64. package/dist/main.d.ts.map +1 -1
  65. package/dist/main.js +96 -112
  66. package/dist/main.js.map +1 -1
  67. package/dist/provisioner.d.ts +0 -41
  68. package/dist/provisioner.d.ts.map +1 -1
  69. package/dist/provisioner.js +0 -45
  70. package/dist/provisioner.js.map +1 -1
  71. package/dist/sandbox/host.d.ts +0 -2
  72. package/dist/sandbox/host.d.ts.map +1 -1
  73. package/dist/sandbox/host.js +1 -5
  74. package/dist/sandbox/host.js.map +1 -1
  75. package/dist/sentry.d.ts.map +1 -1
  76. package/dist/sentry.js +2 -0
  77. package/dist/sentry.js.map +1 -1
  78. package/dist/session-store.d.ts +1 -1
  79. package/dist/session-store.d.ts.map +1 -1
  80. package/dist/session-store.js +5 -9
  81. package/dist/session-store.js.map +1 -1
  82. package/dist/tools/event.d.ts +1 -0
  83. package/dist/tools/event.d.ts.map +1 -1
  84. package/dist/tools/event.js +6 -5
  85. package/dist/tools/event.js.map +1 -1
  86. package/dist/tools/index.d.ts +1 -0
  87. package/dist/tools/index.d.ts.map +1 -1
  88. package/dist/tools/index.js +2 -2
  89. package/dist/tools/index.js.map +1 -1
  90. package/dist/ui-copy.d.ts +1 -0
  91. package/dist/ui-copy.d.ts.map +1 -1
  92. package/dist/ui-copy.js +3 -0
  93. package/dist/ui-copy.js.map +1 -1
  94. package/dist/vault-routing.d.ts +1 -2
  95. package/dist/vault-routing.d.ts.map +1 -1
  96. package/dist/vault-routing.js +1 -7
  97. package/dist/vault-routing.js.map +1 -1
  98. package/package.json +1 -1
  99. package/dist/vault.test.d.ts +0 -2
  100. package/dist/vault.test.d.ts.map +0 -1
  101. package/dist/vault.test.js +0 -67
  102. package/dist/vault.test.js.map +0 -1
@@ -3,9 +3,8 @@ import { WebClient } from "@slack/web-api";
3
3
  import { appendFileSync, existsSync, mkdirSync, readFileSync } from "fs";
4
4
  import { readFile } from "fs/promises";
5
5
  import { basename, join } from "path";
6
- import { parseLoginCommand } from "../../login.js";
7
6
  import * as log from "../../log.js";
8
- import { PRODUCT_NAME, formatAlreadyWorking, formatForceStopped, formatNothingRunning, } from "../../ui-copy.js";
7
+ import { PRODUCT_NAME, formatForceStopped, formatNothingRunning } from "../../ui-copy.js";
9
8
  import { createSlackAdapters } from "./context.js";
10
9
  // ============================================================================
11
10
  // Exponential backoff utility for Slack API calls
@@ -94,21 +93,6 @@ export class SlackBot {
94
93
  setEventsWatcher(watcher) {
95
94
  this.eventsWatcher = watcher;
96
95
  }
97
- toBotEvent(event) {
98
- return {
99
- type: event.type,
100
- conversationId: event.channel,
101
- ts: event.ts,
102
- thread_ts: event.thread_ts,
103
- user: event.user,
104
- text: event.text,
105
- attachments: event.attachments?.map((attachment) => ({
106
- name: attachment.original,
107
- localPath: attachment.localPath,
108
- })),
109
- sessionKey: event.sessionKey,
110
- };
111
- }
112
96
  // ==========================================================================
113
97
  // Public API
114
98
  // ==========================================================================
@@ -136,15 +120,30 @@ export class SlackBot {
136
120
  getAllChannels() {
137
121
  return Array.from(this.channels.values());
138
122
  }
139
- async postMessage(conversationId, text) {
123
+ async postMessage(channel, text) {
140
124
  return withRetry(async () => {
141
- const result = await this.webClient.chat.postMessage({ channel: conversationId, text });
125
+ const result = await this.webClient.chat.postMessage({ channel, text });
142
126
  return result.ts;
143
127
  });
144
128
  }
145
- async updateMessage(conversationId, ts, text) {
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
+ }
144
+ async updateMessage(channel, ts, text) {
146
145
  return withRetry(async () => {
147
- await this.webClient.chat.update({ channel: conversationId, ts, text });
146
+ await this.webClient.chat.update({ channel, ts, text });
148
147
  });
149
148
  }
150
149
  async deleteMessage(channel, ts) {
@@ -214,10 +213,10 @@ export class SlackBot {
214
213
  * This is the ONLY place messages are written to log.jsonl
215
214
  */
216
215
  logToFile(channel, entry) {
217
- const channelDir = join(this.workingDir, channel);
218
- if (!existsSync(channelDir))
219
- mkdirSync(channelDir, { recursive: true });
220
- appendFileSync(join(channelDir, "log.jsonl"), `${JSON.stringify(entry)}\n`);
216
+ const dir = join(this.workingDir, channel);
217
+ if (!existsSync(dir))
218
+ mkdirSync(dir, { recursive: true });
219
+ appendFileSync(join(dir, "log.jsonl"), `${JSON.stringify(entry)}\n`);
221
220
  }
222
221
  /**
223
222
  * Log a bot response to log.jsonl
@@ -253,20 +252,27 @@ export class SlackBot {
253
252
  * Returns true if enqueued, false if queue is full (max 5).
254
253
  */
255
254
  enqueueEvent(event) {
256
- const queue = this.getQueue(event.conversationId);
255
+ const conversationId = event.conversationId;
256
+ const queue = this.getQueue(conversationId);
257
257
  if (queue.size() >= 5) {
258
- log.logWarning(`Event queue full for ${event.conversationId}, discarding: ${event.text.substring(0, 50)}`);
258
+ log.logWarning(`Event queue full for ${conversationId}, discarding: ${event.text.substring(0, 50)}`);
259
259
  return false;
260
260
  }
261
- log.logInfo(`Enqueueing event for ${event.conversationId}: ${event.text.substring(0, 50)}`);
261
+ log.logInfo(`Enqueueing event for ${conversationId}: ${event.text.substring(0, 50)}`);
262
262
  queue.enqueue(() => {
263
263
  const slackEvent = {
264
- type: "mention",
265
- channel: event.conversationId,
264
+ type: event.type,
265
+ conversationId,
266
+ conversationKind: event.conversationKind,
267
+ channel: conversationId,
266
268
  ts: event.ts,
267
269
  thread_ts: event.thread_ts,
268
270
  user: event.user,
269
271
  text: event.text,
272
+ attachments: event.attachments?.map((attachment) => ({
273
+ original: attachment.name,
274
+ localPath: attachment.localPath,
275
+ })),
270
276
  sessionKey: event.sessionKey,
271
277
  };
272
278
  const adapters = createSlackAdapters(slackEvent, this, true);
@@ -387,21 +393,22 @@ export class SlackBot {
387
393
  });
388
394
  }
389
395
  else {
390
- const timestampFormatter = new Intl.DateTimeFormat(undefined, {
391
- month: "short",
392
- day: "numeric",
393
- hour: "2-digit",
394
- minute: "2-digit",
395
- });
396
396
  for (const ev of periodicEvents) {
397
397
  const channelLabel = ev.platform === "slack"
398
398
  ? (() => {
399
- const channel = this.channels.get(ev.channelId);
400
- const channelName = channel ? `#${channel.name}` : ev.channelId;
399
+ const channel = this.channels.get(ev.conversationId);
400
+ const channelName = channel ? `#${channel.name}` : ev.conversationId;
401
401
  return `${ev.platform}:${channelName}`;
402
402
  })()
403
- : `${ev.platform}:${ev.channelId}`;
404
- const nextStr = ev.nextRun ? timestampFormatter.format(new Date(ev.nextRun)) : "—";
403
+ : `${ev.platform}:${ev.conversationId}`;
404
+ const nextStr = ev.nextRun
405
+ ? new Date(ev.nextRun).toLocaleString("en-US", {
406
+ month: "short",
407
+ day: "numeric",
408
+ hour: "2-digit",
409
+ minute: "2-digit",
410
+ })
411
+ : "—";
405
412
  blocks.push({
406
413
  type: "section",
407
414
  text: {
@@ -438,6 +445,115 @@ export class SlackBot {
438
445
  }
439
446
  return this.handler.isRunning(channelId) ? channelId : null;
440
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
+ }
441
557
  setupEventHandlers() {
442
558
  // Channel @mentions
443
559
  this.socketClient.on("app_mention", ({ event, ack }) => {
@@ -452,6 +568,8 @@ export class SlackBot {
452
568
  const sessionKey = e.thread_ts ? `${e.channel}:${e.thread_ts}` : e.channel;
453
569
  const slackEvent = {
454
570
  type: "mention",
571
+ conversationId: e.channel,
572
+ conversationKind: "shared",
455
573
  channel: e.channel,
456
574
  ts: e.ts,
457
575
  thread_ts: e.thread_ts,
@@ -481,22 +599,10 @@ export class SlackBot {
481
599
  ack();
482
600
  return;
483
601
  }
484
- // Check for login command
485
- if (parseLoginCommand(slackEvent.text)) {
486
- void this.handler.handleLogin("slack", e.user, e.channel, this, slackEvent.text, false);
487
- ack();
488
- return;
489
- }
490
- // SYNC: Check if busy (per-thread)
491
- if (this.handler.isRunning(sessionKey)) {
492
- this.postMessage(e.channel, formatAlreadyWorking("slack", "@mama stop", { scope: "thread" }));
493
- }
494
- else {
495
- this.getQueue(sessionKey).enqueue(() => {
496
- const adapters = createSlackAdapters(slackEvent, this, false);
497
- return this.handler.handleEvent(this.toBotEvent(slackEvent), this, adapters, false);
498
- });
499
- }
602
+ this.getQueue(sessionKey).enqueue(() => {
603
+ const adapters = createSlackAdapters(slackEvent, this, false);
604
+ return this.handler.handleEvent(slackEvent, this, adapters, false);
605
+ });
500
606
  ack();
501
607
  });
502
608
  // All messages (for logging) + DMs (for triggering)
@@ -516,6 +622,7 @@ export class SlackBot {
516
622
  return;
517
623
  }
518
624
  const isDM = e.channel_type === "im";
625
+ const conversationKind = isDM ? "direct" : "shared";
519
626
  const isBotMention = e.text?.includes(`<@${this.botUserId}>`);
520
627
  // Skip channel @mentions - already handled by app_mention event
521
628
  if (!isDM && isBotMention) {
@@ -524,6 +631,8 @@ export class SlackBot {
524
631
  }
525
632
  const slackEvent = {
526
633
  type: isDM ? "dm" : "mention",
634
+ conversationId: e.channel,
635
+ conversationKind,
527
636
  channel: e.channel,
528
637
  ts: e.ts,
529
638
  thread_ts: e.thread_ts,
@@ -568,24 +677,42 @@ export class SlackBot {
568
677
  ack();
569
678
  return;
570
679
  }
571
- // Check for login command
572
- if (parseLoginCommand(slackEvent.text)) {
573
- void this.handler.handleLogin("slack", e.user, e.channel, this, slackEvent.text, true);
574
- ack();
575
- return;
576
- }
577
- if (this.handler.isRunning(dmSessionKey)) {
578
- this.postMessage(e.channel, formatAlreadyWorking("slack", "stop"));
579
- }
580
- else {
581
- this.getQueue(dmSessionKey).enqueue(() => {
582
- const adapters = createSlackAdapters(slackEvent, this, false);
583
- return this.handler.handleEvent(this.toBotEvent(slackEvent), this, adapters, false);
584
- });
585
- }
680
+ this.getQueue(dmSessionKey).enqueue(() => {
681
+ const adapters = createSlackAdapters(slackEvent, this, false);
682
+ return this.handler.handleEvent(slackEvent, this, adapters, false);
683
+ });
586
684
  }
587
685
  ack();
588
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
+ });
589
716
  // App Home tab
590
717
  this.socketClient.on("app_home_opened", ({ event, ack }) => {
591
718
  const e = event;
@@ -616,8 +743,7 @@ export class SlackBot {
616
743
  // Use handler's forceStop method
617
744
  this.handler.forceStop(sessionKey);
618
745
  // Notify in channel
619
- const actorLabel = userId ? `<@${userId}>` : "someone";
620
- await this.postMessage(channelId, formatForceStopped("slack", actorLabel));
746
+ await this.postMessage(channelId, formatForceStopped("slack", userId ?? "unknown"));
621
747
  // Refresh home tab
622
748
  if (userId) {
623
749
  this.webClient.views