@geminixiang/mama 0.2.0-beta.6 → 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 (82) hide show
  1. package/README.md +19 -11
  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.map +1 -1
  6. package/dist/adapters/discord/bot.js +22 -5
  7. package/dist/adapters/discord/bot.js.map +1 -1
  8. package/dist/adapters/slack/bot.d.ts +8 -0
  9. package/dist/adapters/slack/bot.d.ts.map +1 -1
  10. package/dist/adapters/slack/bot.js +160 -8
  11. package/dist/adapters/slack/bot.js.map +1 -1
  12. package/dist/adapters/slack/context.d.ts.map +1 -1
  13. package/dist/adapters/slack/context.js +5 -0
  14. package/dist/adapters/slack/context.js.map +1 -1
  15. package/dist/adapters/slack/tools/attach.d.ts +1 -1
  16. package/dist/adapters/slack/tools/attach.d.ts.map +1 -1
  17. package/dist/adapters/slack/tools/attach.js.map +1 -1
  18. package/dist/adapters/telegram/bot.d.ts.map +1 -1
  19. package/dist/adapters/telegram/bot.js +31 -0
  20. package/dist/adapters/telegram/bot.js.map +1 -1
  21. package/dist/agent.d.ts.map +1 -1
  22. package/dist/agent.js +39 -23
  23. package/dist/agent.js.map +1 -1
  24. package/dist/commands/index.d.ts.map +1 -1
  25. package/dist/commands/index.js +2 -0
  26. package/dist/commands/index.js.map +1 -1
  27. package/dist/commands/model.d.ts +1 -1
  28. package/dist/commands/model.d.ts.map +1 -1
  29. package/dist/commands/model.js +25 -7
  30. package/dist/commands/model.js.map +1 -1
  31. package/dist/commands/sandbox.d.ts +10 -0
  32. package/dist/commands/sandbox.d.ts.map +1 -0
  33. package/dist/commands/sandbox.js +65 -0
  34. package/dist/commands/sandbox.js.map +1 -0
  35. package/dist/commands/session-view.d.ts.map +1 -1
  36. package/dist/commands/session-view.js +29 -9
  37. package/dist/commands/session-view.js.map +1 -1
  38. package/dist/commands/utils.d.ts +3 -0
  39. package/dist/commands/utils.d.ts.map +1 -1
  40. package/dist/commands/utils.js +5 -0
  41. package/dist/commands/utils.js.map +1 -1
  42. package/dist/config.d.ts +3 -1
  43. package/dist/config.d.ts.map +1 -1
  44. package/dist/config.js +29 -0
  45. package/dist/config.js.map +1 -1
  46. package/dist/context.d.ts +1 -1
  47. package/dist/context.d.ts.map +1 -1
  48. package/dist/context.js +50 -35
  49. package/dist/context.js.map +1 -1
  50. package/dist/main.d.ts.map +1 -1
  51. package/dist/main.js +7 -1
  52. package/dist/main.js.map +1 -1
  53. package/dist/provisioner.d.ts +12 -0
  54. package/dist/provisioner.d.ts.map +1 -1
  55. package/dist/provisioner.js +41 -10
  56. package/dist/provisioner.js.map +1 -1
  57. package/dist/session-store.d.ts +1 -1
  58. package/dist/session-store.d.ts.map +1 -1
  59. package/dist/session-store.js +1 -1
  60. package/dist/session-store.js.map +1 -1
  61. package/dist/session-view/service.d.ts.map +1 -1
  62. package/dist/session-view/service.js +1 -1
  63. package/dist/session-view/service.js.map +1 -1
  64. package/dist/tools/bash.d.ts +1 -1
  65. package/dist/tools/bash.d.ts.map +1 -1
  66. package/dist/tools/bash.js.map +1 -1
  67. package/dist/tools/edit.d.ts +1 -1
  68. package/dist/tools/edit.d.ts.map +1 -1
  69. package/dist/tools/edit.js.map +1 -1
  70. package/dist/tools/event.d.ts +1 -1
  71. package/dist/tools/event.d.ts.map +1 -1
  72. package/dist/tools/event.js.map +1 -1
  73. package/dist/tools/index.d.ts +1 -1
  74. package/dist/tools/index.d.ts.map +1 -1
  75. package/dist/tools/index.js.map +1 -1
  76. package/dist/tools/read.d.ts +1 -1
  77. package/dist/tools/read.d.ts.map +1 -1
  78. package/dist/tools/read.js.map +1 -1
  79. package/dist/tools/write.d.ts +1 -1
  80. package/dist/tools/write.d.ts.map +1 -1
  81. package/dist/tools/write.js.map +1 -1
  82. package/package.json +4 -4
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../../src/commands/sandbox.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAGjE,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,aAAa,GAAG,UAAU,CAAC;IACpC,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,oBAAoB,GAAG,IAAI,CAW7E;AAMD,qBAAa,qBAAsB,YAAW,cAAc;IACpD,SAAS,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC,CAkEzD;CACF","sourcesContent":["import { resolveActorVaultKey } from \"../vault-routing.js\";\nimport type { CommandContext, CommandHandler } from \"./types.js\";\nimport { replyDiagnosticWithContext } from \"./utils.js\";\n\nexport interface ParsedSandboxCommand {\n command: \"/pi-sandbox\" | \"/sandbox\";\n action?: \"boost\";\n}\n\nexport function parseSandboxCommand(text: string): ParsedSandboxCommand | null {\n const tokens = text.trim().split(/\\s+/).filter(Boolean);\n if (tokens.length === 0) return null;\n\n const command = tokens[0].replace(/@\\w+$/i, \"\").toLowerCase();\n if (command !== \"/pi-sandbox\" && command !== \"/sandbox\") return null;\n if (tokens.length === 1) return { command };\n if (tokens.length === 2 && tokens[1].toLowerCase() === \"boost\") {\n return { command, action: \"boost\" };\n }\n return { command };\n}\n\nfunction formatSandboxCommandSummary(title: string, lines: string[]): string {\n return [`_${title}_`, ...lines].join(\"\\n\");\n}\n\nexport class SandboxCommandHandler implements CommandHandler {\n async tryHandle(context: CommandContext): Promise<boolean> {\n const parsed = parseSandboxCommand(context.commandText);\n if (!parsed) return false;\n\n if (context.services.sandbox.type !== \"image\" || !context.services.provisioner) {\n await replyDiagnosticWithContext(\n context.responseCtx,\n formatSandboxCommandSummary(\"Sandbox\", [\n \"`/pi-sandbox` 目前只支援 `image:*` managed sandbox。\",\n ]),\n { style: \"muted\" },\n );\n return true;\n }\n\n const containerKey = resolveActorVaultKey(\n context.services.sandbox,\n context.platformUserId,\n context.conversationId,\n );\n\n if (parsed.action === \"boost\") {\n const boostLimits = context.services.provisioner.getBoostLimits();\n if (!boostLimits?.cpus && !boostLimits?.memory) {\n await replyDiagnosticWithContext(\n context.responseCtx,\n formatSandboxCommandSummary(\"Sandbox Boost\", [\n \"此 mama instance 尚未設定 sandbox boost 規格。\",\n \"請先在全域 settings.json 設定 `sandbox.boost`。\",\n ]),\n { style: \"muted\" },\n );\n return true;\n }\n\n const status = await context.services.provisioner.boost(containerKey);\n await replyDiagnosticWithContext(\n context.responseCtx,\n formatSandboxCommandSummary(\"Sandbox Boost\", [\n \"已暫時提升此 conversation 的 sandbox 規格。\",\n `Current: ${formatLimits(status.limits)}`,\n \"boost 會在此 sandbox container 關閉後結束。\",\n ]),\n { style: \"muted\" },\n );\n return true;\n }\n\n const status = context.services.provisioner.getLimitStatus(containerKey);\n const defaultLimits = context.services.provisioner.getDefaultLimits();\n const boostLimits = context.services.provisioner.getBoostLimits();\n await replyDiagnosticWithContext(\n context.responseCtx,\n formatSandboxCommandSummary(\n \"Sandbox\",\n [\n `Current: ${formatLimits(status.limits)}`,\n `Status: ${status.boosted ? \"boosted\" : \"default\"}`,\n \"\",\n `Default: ${formatLimits(defaultLimits)}`,\n boostLimits ? `Boost: ${formatLimits({ ...defaultLimits, ...boostLimits })}` : undefined,\n ].filter((line): line is string => line !== undefined),\n ),\n { style: \"muted\" },\n );\n return true;\n }\n}\n\nfunction formatLimits(limits: { cpus?: string; memory?: string } | undefined): string {\n return `CPU ${limits?.cpus ?? \"unlimited\"} / Memory ${limits?.memory ?? \"unlimited\"}`;\n}\n"]}
@@ -0,0 +1,65 @@
1
+ import { resolveActorVaultKey } from "../vault-routing.js";
2
+ import { replyDiagnosticWithContext } from "./utils.js";
3
+ export function parseSandboxCommand(text) {
4
+ const tokens = text.trim().split(/\s+/).filter(Boolean);
5
+ if (tokens.length === 0)
6
+ return null;
7
+ const command = tokens[0].replace(/@\w+$/i, "").toLowerCase();
8
+ if (command !== "/pi-sandbox" && command !== "/sandbox")
9
+ return null;
10
+ if (tokens.length === 1)
11
+ return { command };
12
+ if (tokens.length === 2 && tokens[1].toLowerCase() === "boost") {
13
+ return { command, action: "boost" };
14
+ }
15
+ return { command };
16
+ }
17
+ function formatSandboxCommandSummary(title, lines) {
18
+ return [`_${title}_`, ...lines].join("\n");
19
+ }
20
+ export class SandboxCommandHandler {
21
+ async tryHandle(context) {
22
+ const parsed = parseSandboxCommand(context.commandText);
23
+ if (!parsed)
24
+ return false;
25
+ if (context.services.sandbox.type !== "image" || !context.services.provisioner) {
26
+ await replyDiagnosticWithContext(context.responseCtx, formatSandboxCommandSummary("Sandbox", [
27
+ "`/pi-sandbox` 目前只支援 `image:*` managed sandbox。",
28
+ ]), { style: "muted" });
29
+ return true;
30
+ }
31
+ const containerKey = resolveActorVaultKey(context.services.sandbox, context.platformUserId, context.conversationId);
32
+ if (parsed.action === "boost") {
33
+ const boostLimits = context.services.provisioner.getBoostLimits();
34
+ if (!boostLimits?.cpus && !boostLimits?.memory) {
35
+ await replyDiagnosticWithContext(context.responseCtx, formatSandboxCommandSummary("Sandbox Boost", [
36
+ "此 mama instance 尚未設定 sandbox boost 規格。",
37
+ "請先在全域 settings.json 設定 `sandbox.boost`。",
38
+ ]), { style: "muted" });
39
+ return true;
40
+ }
41
+ const status = await context.services.provisioner.boost(containerKey);
42
+ await replyDiagnosticWithContext(context.responseCtx, formatSandboxCommandSummary("Sandbox Boost", [
43
+ "已暫時提升此 conversation 的 sandbox 規格。",
44
+ `Current: ${formatLimits(status.limits)}`,
45
+ "boost 會在此 sandbox container 關閉後結束。",
46
+ ]), { style: "muted" });
47
+ return true;
48
+ }
49
+ const status = context.services.provisioner.getLimitStatus(containerKey);
50
+ const defaultLimits = context.services.provisioner.getDefaultLimits();
51
+ const boostLimits = context.services.provisioner.getBoostLimits();
52
+ await replyDiagnosticWithContext(context.responseCtx, formatSandboxCommandSummary("Sandbox", [
53
+ `Current: ${formatLimits(status.limits)}`,
54
+ `Status: ${status.boosted ? "boosted" : "default"}`,
55
+ "",
56
+ `Default: ${formatLimits(defaultLimits)}`,
57
+ boostLimits ? `Boost: ${formatLimits({ ...defaultLimits, ...boostLimits })}` : undefined,
58
+ ].filter((line) => line !== undefined)), { style: "muted" });
59
+ return true;
60
+ }
61
+ }
62
+ function formatLimits(limits) {
63
+ return `CPU ${limits?.cpus ?? "unlimited"} / Memory ${limits?.memory ?? "unlimited"}`;
64
+ }
65
+ //# sourceMappingURL=sandbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox.js","sourceRoot":"","sources":["../../src/commands/sandbox.ts"],"names":[],"mappings":"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","sourcesContent":["import { resolveActorVaultKey } from \"../vault-routing.js\";\nimport type { CommandContext, CommandHandler } from \"./types.js\";\nimport { replyDiagnosticWithContext } from \"./utils.js\";\n\nexport interface ParsedSandboxCommand {\n command: \"/pi-sandbox\" | \"/sandbox\";\n action?: \"boost\";\n}\n\nexport function parseSandboxCommand(text: string): ParsedSandboxCommand | null {\n const tokens = text.trim().split(/\\s+/).filter(Boolean);\n if (tokens.length === 0) return null;\n\n const command = tokens[0].replace(/@\\w+$/i, \"\").toLowerCase();\n if (command !== \"/pi-sandbox\" && command !== \"/sandbox\") return null;\n if (tokens.length === 1) return { command };\n if (tokens.length === 2 && tokens[1].toLowerCase() === \"boost\") {\n return { command, action: \"boost\" };\n }\n return { command };\n}\n\nfunction formatSandboxCommandSummary(title: string, lines: string[]): string {\n return [`_${title}_`, ...lines].join(\"\\n\");\n}\n\nexport class SandboxCommandHandler implements CommandHandler {\n async tryHandle(context: CommandContext): Promise<boolean> {\n const parsed = parseSandboxCommand(context.commandText);\n if (!parsed) return false;\n\n if (context.services.sandbox.type !== \"image\" || !context.services.provisioner) {\n await replyDiagnosticWithContext(\n context.responseCtx,\n formatSandboxCommandSummary(\"Sandbox\", [\n \"`/pi-sandbox` 目前只支援 `image:*` managed sandbox。\",\n ]),\n { style: \"muted\" },\n );\n return true;\n }\n\n const containerKey = resolveActorVaultKey(\n context.services.sandbox,\n context.platformUserId,\n context.conversationId,\n );\n\n if (parsed.action === \"boost\") {\n const boostLimits = context.services.provisioner.getBoostLimits();\n if (!boostLimits?.cpus && !boostLimits?.memory) {\n await replyDiagnosticWithContext(\n context.responseCtx,\n formatSandboxCommandSummary(\"Sandbox Boost\", [\n \"此 mama instance 尚未設定 sandbox boost 規格。\",\n \"請先在全域 settings.json 設定 `sandbox.boost`。\",\n ]),\n { style: \"muted\" },\n );\n return true;\n }\n\n const status = await context.services.provisioner.boost(containerKey);\n await replyDiagnosticWithContext(\n context.responseCtx,\n formatSandboxCommandSummary(\"Sandbox Boost\", [\n \"已暫時提升此 conversation 的 sandbox 規格。\",\n `Current: ${formatLimits(status.limits)}`,\n \"boost 會在此 sandbox container 關閉後結束。\",\n ]),\n { style: \"muted\" },\n );\n return true;\n }\n\n const status = context.services.provisioner.getLimitStatus(containerKey);\n const defaultLimits = context.services.provisioner.getDefaultLimits();\n const boostLimits = context.services.provisioner.getBoostLimits();\n await replyDiagnosticWithContext(\n context.responseCtx,\n formatSandboxCommandSummary(\n \"Sandbox\",\n [\n `Current: ${formatLimits(status.limits)}`,\n `Status: ${status.boosted ? \"boosted\" : \"default\"}`,\n \"\",\n `Default: ${formatLimits(defaultLimits)}`,\n boostLimits ? `Boost: ${formatLimits({ ...defaultLimits, ...boostLimits })}` : undefined,\n ].filter((line): line is string => line !== undefined),\n ),\n { style: \"muted\" },\n );\n return true;\n }\n}\n\nfunction formatLimits(limits: { cpus?: string; memory?: string } | undefined): string {\n return `CPU ${limits?.cpus ?? \"unlimited\"} / Memory ${limits?.memory ?? \"unlimited\"}`;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"session-view.d.ts","sourceRoot":"","sources":["../../src/commands/session-view.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAGjE,qBAAa,yBAA0B,YAAW,cAAc;IACxD,SAAS,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC,CAsDzD;CACF","sourcesContent":["import { resolveExistingSessionFile } from \"../session-view/service.js\";\nimport { parseSessionViewCommand } from \"../session-view/command.js\";\nimport type { CommandContext, CommandHandler } from \"./types.js\";\nimport { replyWithContext } from \"./utils.js\";\n\nexport class SessionViewCommandHandler implements CommandHandler {\n async tryHandle(context: CommandContext): Promise<boolean> {\n if (!parseSessionViewCommand(context.commandText)) return false;\n\n const sendSessionViewReply = async (text: string): Promise<void> => {\n if (context.privateConversation) {\n await replyWithContext(context.responseCtx, text);\n return;\n }\n\n if (context.bot.postPrivate) {\n await context.bot.postPrivate(context.conversationId, context.platformUserId, text);\n return;\n }\n\n await replyWithContext(context.responseCtx, text);\n };\n\n if (!context.privateConversation && !context.bot.postPrivate) {\n await sendSessionViewReply(\n \"為了保護對話內容,`/session` 目前只能在與機器人的私訊 / DM 中使用。\",\n );\n return true;\n }\n\n if (!context.services.portalBaseUrl) {\n await sendSessionViewReply(\n \"Session viewer is not configured. Set `MAMA_LINK_URL` or `MAMA_LINK_PORT` on the server.\",\n );\n return true;\n }\n\n const sessionFile = resolveExistingSessionFile(\n context.services.workingDir,\n context.conversationId,\n context.sessionKey,\n );\n if (!sessionFile) {\n await sendSessionViewReply(\n \"目前還沒有可查看的 session。先和機器人對話一次,建立 session 後再試。\",\n );\n return true;\n }\n\n const token = context.services.sessionViewTokenStore.create(\n context.platform,\n context.platformUserId,\n context.conversationId,\n context.sessionKey,\n sessionFile,\n );\n\n const linkText = `Open this read-only session link (expires in 24 hours):\\n${context.services.portalBaseUrl}/session?token=${token.token}`;\n await sendSessionViewReply(linkText);\n return true;\n }\n}\n"]}
1
+ {"version":3,"file":"session-view.d.ts","sourceRoot":"","sources":["../../src/commands/session-view.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAOjE,qBAAa,yBAA0B,YAAW,cAAc;IACxD,SAAS,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC,CAuEzD;CACF","sourcesContent":["import { resolveExistingSessionFile } from \"../session-view/service.js\";\nimport { parseSessionViewCommand } from \"../session-view/command.js\";\nimport type { CommandContext, CommandHandler } from \"./types.js\";\nimport { replyDiagnosticWithContext } from \"./utils.js\";\n\nfunction formatSessionCommandSummary(lines: string[]): string {\n return [\"_Session_\", ...lines].join(\"\\n\");\n}\n\nexport class SessionViewCommandHandler implements CommandHandler {\n async tryHandle(context: CommandContext): Promise<boolean> {\n if (!parseSessionViewCommand(context.commandText)) return false;\n\n const sendSessionViewReply = async (lines: string[]): Promise<void> => {\n const text = formatSessionCommandSummary(lines);\n if (context.privateConversation) {\n await replyDiagnosticWithContext(context.responseCtx, text, { style: \"muted\" });\n return;\n }\n\n if (context.bot.postPrivateDiagnostic) {\n await context.bot.postPrivateDiagnostic(\n context.conversationId,\n context.platformUserId,\n text,\n {\n style: \"muted\",\n },\n );\n return;\n }\n\n if (context.bot.postPrivate) {\n await context.bot.postPrivate(context.conversationId, context.platformUserId, text);\n return;\n }\n\n await replyDiagnosticWithContext(context.responseCtx, text, { style: \"muted\" });\n };\n\n if (!context.privateConversation && !context.bot.postPrivate) {\n await sendSessionViewReply([\n \"為了保護對話內容,`/session` 目前只能在與機器人的私訊 / DM 中使用。\",\n ]);\n return true;\n }\n\n if (!context.services.portalBaseUrl) {\n await sendSessionViewReply([\n \"Session viewer is not configured.\",\n \"Set `MAMA_LINK_URL` or `MAMA_LINK_PORT` on the server.\",\n ]);\n return true;\n }\n\n const sessionFile = resolveExistingSessionFile(\n context.services.workingDir,\n context.conversationId,\n context.sessionKey,\n );\n if (!sessionFile) {\n await sendSessionViewReply([\n \"目前還沒有可查看的 session。\",\n \"先和機器人對話一次,建立 session 後再試。\",\n ]);\n return true;\n }\n\n const token = context.services.sessionViewTokenStore.create(\n context.platform,\n context.platformUserId,\n context.conversationId,\n context.sessionKey,\n sessionFile,\n );\n\n await sendSessionViewReply([\n \"Open this read-only session link (expires in 24 hours):\",\n `${context.services.portalBaseUrl}/session?token=${token.token}`,\n ]);\n return true;\n }\n}\n"]}
@@ -1,37 +1,57 @@
1
1
  import { resolveExistingSessionFile } from "../session-view/service.js";
2
2
  import { parseSessionViewCommand } from "../session-view/command.js";
3
- import { replyWithContext } from "./utils.js";
3
+ import { replyDiagnosticWithContext } from "./utils.js";
4
+ function formatSessionCommandSummary(lines) {
5
+ return ["_Session_", ...lines].join("\n");
6
+ }
4
7
  export class SessionViewCommandHandler {
5
8
  async tryHandle(context) {
6
9
  if (!parseSessionViewCommand(context.commandText))
7
10
  return false;
8
- const sendSessionViewReply = async (text) => {
11
+ const sendSessionViewReply = async (lines) => {
12
+ const text = formatSessionCommandSummary(lines);
9
13
  if (context.privateConversation) {
10
- await replyWithContext(context.responseCtx, text);
14
+ await replyDiagnosticWithContext(context.responseCtx, text, { style: "muted" });
15
+ return;
16
+ }
17
+ if (context.bot.postPrivateDiagnostic) {
18
+ await context.bot.postPrivateDiagnostic(context.conversationId, context.platformUserId, text, {
19
+ style: "muted",
20
+ });
11
21
  return;
12
22
  }
13
23
  if (context.bot.postPrivate) {
14
24
  await context.bot.postPrivate(context.conversationId, context.platformUserId, text);
15
25
  return;
16
26
  }
17
- await replyWithContext(context.responseCtx, text);
27
+ await replyDiagnosticWithContext(context.responseCtx, text, { style: "muted" });
18
28
  };
19
29
  if (!context.privateConversation && !context.bot.postPrivate) {
20
- await sendSessionViewReply("為了保護對話內容,`/session` 目前只能在與機器人的私訊 / DM 中使用。");
30
+ await sendSessionViewReply([
31
+ "為了保護對話內容,`/session` 目前只能在與機器人的私訊 / DM 中使用。",
32
+ ]);
21
33
  return true;
22
34
  }
23
35
  if (!context.services.portalBaseUrl) {
24
- await sendSessionViewReply("Session viewer is not configured. Set `MAMA_LINK_URL` or `MAMA_LINK_PORT` on the server.");
36
+ await sendSessionViewReply([
37
+ "Session viewer is not configured.",
38
+ "Set `MAMA_LINK_URL` or `MAMA_LINK_PORT` on the server.",
39
+ ]);
25
40
  return true;
26
41
  }
27
42
  const sessionFile = resolveExistingSessionFile(context.services.workingDir, context.conversationId, context.sessionKey);
28
43
  if (!sessionFile) {
29
- await sendSessionViewReply("目前還沒有可查看的 session。先和機器人對話一次,建立 session 後再試。");
44
+ await sendSessionViewReply([
45
+ "目前還沒有可查看的 session。",
46
+ "先和機器人對話一次,建立 session 後再試。",
47
+ ]);
30
48
  return true;
31
49
  }
32
50
  const token = context.services.sessionViewTokenStore.create(context.platform, context.platformUserId, context.conversationId, context.sessionKey, sessionFile);
33
- const linkText = `Open this read-only session link (expires in 24 hours):\n${context.services.portalBaseUrl}/session?token=${token.token}`;
34
- await sendSessionViewReply(linkText);
51
+ await sendSessionViewReply([
52
+ "Open this read-only session link (expires in 24 hours):",
53
+ `${context.services.portalBaseUrl}/session?token=${token.token}`,
54
+ ]);
35
55
  return true;
36
56
  }
37
57
  }
@@ -1 +1 @@
1
- {"version":3,"file":"session-view.js","sourceRoot":"","sources":["../../src/commands/session-view.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAC;AACxE,OAAO,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAErE,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C,MAAM,OAAO,yBAAyB;IACpC,KAAK,CAAC,SAAS,CAAC,OAAuB;QACrC,IAAI,CAAC,uBAAuB,CAAC,OAAO,CAAC,WAAW,CAAC;YAAE,OAAO,KAAK,CAAC;QAEhE,MAAM,oBAAoB,GAAG,KAAK,EAAE,IAAY,EAAiB,EAAE;YACjE,IAAI,OAAO,CAAC,mBAAmB,EAAE,CAAC;gBAChC,MAAM,gBAAgB,CAAC,OAAO,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;gBAClD,OAAO;YACT,CAAC;YAED,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;gBAC5B,MAAM,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,OAAO,CAAC,cAAc,EAAE,OAAO,CAAC,cAAc,EAAE,IAAI,CAAC,CAAC;gBACpF,OAAO;YACT,CAAC;YAED,MAAM,gBAAgB,CAAC,OAAO,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;QACpD,CAAC,CAAC;QAEF,IAAI,CAAC,OAAO,CAAC,mBAAmB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;YAC7D,MAAM,oBAAoB,CACxB,4CAA4C,CAC7C,CAAC;YACF,OAAO,IAAI,CAAC;QACd,CAAC;QAED,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,aAAa,EAAE,CAAC;YACpC,MAAM,oBAAoB,CACxB,0FAA0F,CAC3F,CAAC;YACF,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,WAAW,GAAG,0BAA0B,CAC5C,OAAO,CAAC,QAAQ,CAAC,UAAU,EAC3B,OAAO,CAAC,cAAc,EACtB,OAAO,CAAC,UAAU,CACnB,CAAC;QACF,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,MAAM,oBAAoB,CACxB,6CAA6C,CAC9C,CAAC;YACF,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAC,MAAM,CACzD,OAAO,CAAC,QAAQ,EAChB,OAAO,CAAC,cAAc,EACtB,OAAO,CAAC,cAAc,EACtB,OAAO,CAAC,UAAU,EAClB,WAAW,CACZ,CAAC;QAEF,MAAM,QAAQ,GAAG,4DAA4D,OAAO,CAAC,QAAQ,CAAC,aAAa,kBAAkB,KAAK,CAAC,KAAK,EAAE,CAAC;QAC3I,MAAM,oBAAoB,CAAC,QAAQ,CAAC,CAAC;QACrC,OAAO,IAAI,CAAC;IACd,CAAC;CACF","sourcesContent":["import { resolveExistingSessionFile } from \"../session-view/service.js\";\nimport { parseSessionViewCommand } from \"../session-view/command.js\";\nimport type { CommandContext, CommandHandler } from \"./types.js\";\nimport { replyWithContext } from \"./utils.js\";\n\nexport class SessionViewCommandHandler implements CommandHandler {\n async tryHandle(context: CommandContext): Promise<boolean> {\n if (!parseSessionViewCommand(context.commandText)) return false;\n\n const sendSessionViewReply = async (text: string): Promise<void> => {\n if (context.privateConversation) {\n await replyWithContext(context.responseCtx, text);\n return;\n }\n\n if (context.bot.postPrivate) {\n await context.bot.postPrivate(context.conversationId, context.platformUserId, text);\n return;\n }\n\n await replyWithContext(context.responseCtx, text);\n };\n\n if (!context.privateConversation && !context.bot.postPrivate) {\n await sendSessionViewReply(\n \"為了保護對話內容,`/session` 目前只能在與機器人的私訊 / DM 中使用。\",\n );\n return true;\n }\n\n if (!context.services.portalBaseUrl) {\n await sendSessionViewReply(\n \"Session viewer is not configured. Set `MAMA_LINK_URL` or `MAMA_LINK_PORT` on the server.\",\n );\n return true;\n }\n\n const sessionFile = resolveExistingSessionFile(\n context.services.workingDir,\n context.conversationId,\n context.sessionKey,\n );\n if (!sessionFile) {\n await sendSessionViewReply(\n \"目前還沒有可查看的 session。先和機器人對話一次,建立 session 後再試。\",\n );\n return true;\n }\n\n const token = context.services.sessionViewTokenStore.create(\n context.platform,\n context.platformUserId,\n context.conversationId,\n context.sessionKey,\n sessionFile,\n );\n\n const linkText = `Open this read-only session link (expires in 24 hours):\\n${context.services.portalBaseUrl}/session?token=${token.token}`;\n await sendSessionViewReply(linkText);\n return true;\n }\n}\n"]}
1
+ {"version":3,"file":"session-view.js","sourceRoot":"","sources":["../../src/commands/session-view.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAC;AACxE,OAAO,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAErE,OAAO,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAC;AAExD,SAAS,2BAA2B,CAAC,KAAe;IAClD,OAAO,CAAC,WAAW,EAAE,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC5C,CAAC;AAED,MAAM,OAAO,yBAAyB;IACpC,KAAK,CAAC,SAAS,CAAC,OAAuB;QACrC,IAAI,CAAC,uBAAuB,CAAC,OAAO,CAAC,WAAW,CAAC;YAAE,OAAO,KAAK,CAAC;QAEhE,MAAM,oBAAoB,GAAG,KAAK,EAAE,KAAe,EAAiB,EAAE;YACpE,MAAM,IAAI,GAAG,2BAA2B,CAAC,KAAK,CAAC,CAAC;YAChD,IAAI,OAAO,CAAC,mBAAmB,EAAE,CAAC;gBAChC,MAAM,0BAA0B,CAAC,OAAO,CAAC,WAAW,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;gBAChF,OAAO;YACT,CAAC;YAED,IAAI,OAAO,CAAC,GAAG,CAAC,qBAAqB,EAAE,CAAC;gBACtC,MAAM,OAAO,CAAC,GAAG,CAAC,qBAAqB,CACrC,OAAO,CAAC,cAAc,EACtB,OAAO,CAAC,cAAc,EACtB,IAAI,EACJ;oBACE,KAAK,EAAE,OAAO;iBACf,CACF,CAAC;gBACF,OAAO;YACT,CAAC;YAED,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;gBAC5B,MAAM,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,OAAO,CAAC,cAAc,EAAE,OAAO,CAAC,cAAc,EAAE,IAAI,CAAC,CAAC;gBACpF,OAAO;YACT,CAAC;YAED,MAAM,0BAA0B,CAAC,OAAO,CAAC,WAAW,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;QAClF,CAAC,CAAC;QAEF,IAAI,CAAC,OAAO,CAAC,mBAAmB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;YAC7D,MAAM,oBAAoB,CAAC;gBACzB,4CAA4C;aAC7C,CAAC,CAAC;YACH,OAAO,IAAI,CAAC;QACd,CAAC;QAED,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,aAAa,EAAE,CAAC;YACpC,MAAM,oBAAoB,CAAC;gBACzB,mCAAmC;gBACnC,wDAAwD;aACzD,CAAC,CAAC;YACH,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,WAAW,GAAG,0BAA0B,CAC5C,OAAO,CAAC,QAAQ,CAAC,UAAU,EAC3B,OAAO,CAAC,cAAc,EACtB,OAAO,CAAC,UAAU,CACnB,CAAC;QACF,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,MAAM,oBAAoB,CAAC;gBACzB,oBAAoB;gBACpB,2BAA2B;aAC5B,CAAC,CAAC;YACH,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAC,MAAM,CACzD,OAAO,CAAC,QAAQ,EAChB,OAAO,CAAC,cAAc,EACtB,OAAO,CAAC,cAAc,EACtB,OAAO,CAAC,UAAU,EAClB,WAAW,CACZ,CAAC;QAEF,MAAM,oBAAoB,CAAC;YACzB,yDAAyD;YACzD,GAAG,OAAO,CAAC,QAAQ,CAAC,aAAa,kBAAkB,KAAK,CAAC,KAAK,EAAE;SACjE,CAAC,CAAC;QACH,OAAO,IAAI,CAAC;IACd,CAAC;CACF","sourcesContent":["import { resolveExistingSessionFile } from \"../session-view/service.js\";\nimport { parseSessionViewCommand } from \"../session-view/command.js\";\nimport type { CommandContext, CommandHandler } from \"./types.js\";\nimport { replyDiagnosticWithContext } from \"./utils.js\";\n\nfunction formatSessionCommandSummary(lines: string[]): string {\n return [\"_Session_\", ...lines].join(\"\\n\");\n}\n\nexport class SessionViewCommandHandler implements CommandHandler {\n async tryHandle(context: CommandContext): Promise<boolean> {\n if (!parseSessionViewCommand(context.commandText)) return false;\n\n const sendSessionViewReply = async (lines: string[]): Promise<void> => {\n const text = formatSessionCommandSummary(lines);\n if (context.privateConversation) {\n await replyDiagnosticWithContext(context.responseCtx, text, { style: \"muted\" });\n return;\n }\n\n if (context.bot.postPrivateDiagnostic) {\n await context.bot.postPrivateDiagnostic(\n context.conversationId,\n context.platformUserId,\n text,\n {\n style: \"muted\",\n },\n );\n return;\n }\n\n if (context.bot.postPrivate) {\n await context.bot.postPrivate(context.conversationId, context.platformUserId, text);\n return;\n }\n\n await replyDiagnosticWithContext(context.responseCtx, text, { style: \"muted\" });\n };\n\n if (!context.privateConversation && !context.bot.postPrivate) {\n await sendSessionViewReply([\n \"為了保護對話內容,`/session` 目前只能在與機器人的私訊 / DM 中使用。\",\n ]);\n return true;\n }\n\n if (!context.services.portalBaseUrl) {\n await sendSessionViewReply([\n \"Session viewer is not configured.\",\n \"Set `MAMA_LINK_URL` or `MAMA_LINK_PORT` on the server.\",\n ]);\n return true;\n }\n\n const sessionFile = resolveExistingSessionFile(\n context.services.workingDir,\n context.conversationId,\n context.sessionKey,\n );\n if (!sessionFile) {\n await sendSessionViewReply([\n \"目前還沒有可查看的 session。\",\n \"先和機器人對話一次,建立 session 後再試。\",\n ]);\n return true;\n }\n\n const token = context.services.sessionViewTokenStore.create(\n context.platform,\n context.platformUserId,\n context.conversationId,\n context.sessionKey,\n sessionFile,\n );\n\n await sendSessionViewReply([\n \"Open this read-only session link (expires in 24 hours):\",\n `${context.services.portalBaseUrl}/session?token=${token.token}`,\n ]);\n return true;\n }\n}\n"]}
@@ -1,5 +1,8 @@
1
1
  import type { BotEvent } from "../adapter.js";
2
2
  import type { CommandContext } from "./types.js";
3
3
  export declare function replyWithContext(responseCtx: CommandContext["responseCtx"], text: string): Promise<void>;
4
+ export declare function replyDiagnosticWithContext(responseCtx: CommandContext["responseCtx"], text: string, options?: {
5
+ style?: "muted" | "error";
6
+ }): Promise<void>;
4
7
  export declare function isPrivateConversation(event: BotEvent): boolean;
5
8
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/commands/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,cAAc,CAAC,aAAa,CAAC,EAC1C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC,CAIf;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAE9D","sourcesContent":["import type { BotEvent } from \"../adapter.js\";\nimport type { CommandContext } from \"./types.js\";\n\nexport async function replyWithContext(\n responseCtx: CommandContext[\"responseCtx\"],\n text: string,\n): Promise<void> {\n await responseCtx.setTyping(false);\n await responseCtx.setWorking(false);\n await responseCtx.respond(text);\n}\n\nexport function isPrivateConversation(event: BotEvent): boolean {\n return event.conversationKind === \"direct\" || event.type === \"dm\";\n}\n"]}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/commands/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,cAAc,CAAC,aAAa,CAAC,EAC1C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC,CAIf;AAED,wBAAsB,0BAA0B,CAC9C,WAAW,EAAE,cAAc,CAAC,aAAa,CAAC,EAC1C,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,GAAG,OAAO,CAAA;CAAE,GACtC,OAAO,CAAC,IAAI,CAAC,CAIf;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAE9D","sourcesContent":["import type { BotEvent } from \"../adapter.js\";\nimport type { CommandContext } from \"./types.js\";\n\nexport async function replyWithContext(\n responseCtx: CommandContext[\"responseCtx\"],\n text: string,\n): Promise<void> {\n await responseCtx.setTyping(false);\n await responseCtx.setWorking(false);\n await responseCtx.respond(text);\n}\n\nexport async function replyDiagnosticWithContext(\n responseCtx: CommandContext[\"responseCtx\"],\n text: string,\n options?: { style?: \"muted\" | \"error\" },\n): Promise<void> {\n await responseCtx.setTyping(false);\n await responseCtx.setWorking(false);\n await responseCtx.respondDiagnostic(text, options);\n}\n\nexport function isPrivateConversation(event: BotEvent): boolean {\n return event.conversationKind === \"direct\" || event.type === \"dm\";\n}\n"]}
@@ -3,6 +3,11 @@ export async function replyWithContext(responseCtx, text) {
3
3
  await responseCtx.setWorking(false);
4
4
  await responseCtx.respond(text);
5
5
  }
6
+ export async function replyDiagnosticWithContext(responseCtx, text, options) {
7
+ await responseCtx.setTyping(false);
8
+ await responseCtx.setWorking(false);
9
+ await responseCtx.respondDiagnostic(text, options);
10
+ }
6
11
  export function isPrivateConversation(event) {
7
12
  return event.conversationKind === "direct" || event.type === "dm";
8
13
  }
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/commands/utils.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,WAA0C,EAC1C,IAAY;IAEZ,MAAM,WAAW,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IACnC,MAAM,WAAW,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;IACpC,MAAM,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,KAAe;IACnD,OAAO,KAAK,CAAC,gBAAgB,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC;AACpE,CAAC","sourcesContent":["import type { BotEvent } from \"../adapter.js\";\nimport type { CommandContext } from \"./types.js\";\n\nexport async function replyWithContext(\n responseCtx: CommandContext[\"responseCtx\"],\n text: string,\n): Promise<void> {\n await responseCtx.setTyping(false);\n await responseCtx.setWorking(false);\n await responseCtx.respond(text);\n}\n\nexport function isPrivateConversation(event: BotEvent): boolean {\n return event.conversationKind === \"direct\" || event.type === \"dm\";\n}\n"]}
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/commands/utils.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,WAA0C,EAC1C,IAAY;IAEZ,MAAM,WAAW,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IACnC,MAAM,WAAW,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;IACpC,MAAM,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAC9C,WAA0C,EAC1C,IAAY,EACZ,OAAuC;IAEvC,MAAM,WAAW,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IACnC,MAAM,WAAW,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;IACpC,MAAM,WAAW,CAAC,iBAAiB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AACrD,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,KAAe;IACnD,OAAO,KAAK,CAAC,gBAAgB,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC;AACpE,CAAC","sourcesContent":["import type { BotEvent } from \"../adapter.js\";\nimport type { CommandContext } from \"./types.js\";\n\nexport async function replyWithContext(\n responseCtx: CommandContext[\"responseCtx\"],\n text: string,\n): Promise<void> {\n await responseCtx.setTyping(false);\n await responseCtx.setWorking(false);\n await responseCtx.respond(text);\n}\n\nexport async function replyDiagnosticWithContext(\n responseCtx: CommandContext[\"responseCtx\"],\n text: string,\n options?: { style?: \"muted\" | \"error\" },\n): Promise<void> {\n await responseCtx.setTyping(false);\n await responseCtx.setWorking(false);\n await responseCtx.respondDiagnostic(text, options);\n}\n\nexport function isPrivateConversation(event: BotEvent): boolean {\n return event.conversationKind === \"direct\" || event.type === \"dm\";\n}\n"]}
package/dist/config.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
1
+ import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
2
2
  export declare class MissingGlobalSettingsError extends Error {
3
3
  readonly settingsPath: string;
4
4
  constructor(settingsPath: string);
@@ -12,6 +12,8 @@ export interface AgentConfig {
12
12
  sentryDsn?: string;
13
13
  sandboxCpus?: string;
14
14
  sandboxMemory?: string;
15
+ sandboxBoostCpus?: string;
16
+ sandboxBoostMemory?: string;
15
17
  }
16
18
  export declare function loadAgentConfig(): AgentConfig;
17
19
  export declare function loadAgentConfigForConversation(conversationDir: string): AgentConfig;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAMjE,qBAAa,0BAA2B,SAAQ,KAAK;aACvB,YAAY,EAAE,MAAM;IAAhD,YAA4B,YAAY,EAAE,MAAM,EAG/C;CACF;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,aAAa,CAAC;IAC7B,SAAS,EAAE,SAAS,GAAG,MAAM,CAAC;IAC9B,QAAQ,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IACxD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AA+HD,wBAAgB,eAAe,IAAI,WAAW,CAE7C;AAED,wBAAgB,8BAA8B,CAAC,eAAe,EAAE,MAAM,GAAG,WAAW,CAMnF;AAED,wBAAgB,2BAA2B,CACzC,eAAe,EAAE,MAAM,EACvB,MAAM,EAAE,IAAI,CAAC,WAAW,EAAE,UAAU,GAAG,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,eAAe,CAAC,CAAC,GAC5F,IAAI,CAWN;AAED,wBAAgB,2BAA2B,CAAC,IAAI,WAAwB,GAAG,MAAM,GAAG,SAAS,CA2B5F;AAED,wBAAgB,uBAAuB,CAAC,IAAI,WAAwB,GAAG,MAAM,CAY5E;AAED,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,SAAS,CAOrD;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAUjE;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,GAAG,SAAS,CAIvD;AA6CD,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG,IAAI,CAwBlE","sourcesContent":["import type { ThinkingLevel } from \"@mariozechner/pi-agent-core\";\nimport { existsSync, mkdirSync, readFileSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { dirname, join, resolve } from \"path\";\nimport { atomicWritePrivateFile } from \"./fs-atomic.js\";\n\nexport class MissingGlobalSettingsError extends Error {\n constructor(public readonly settingsPath: string) {\n super(`Missing global settings file at ${settingsPath}`);\n this.name = \"MissingGlobalSettingsError\";\n }\n}\n\nexport interface AgentConfig {\n provider: string;\n model: string;\n thinkingLevel: ThinkingLevel;\n logFormat: \"console\" | \"json\";\n logLevel: \"trace\" | \"debug\" | \"info\" | \"warn\" | \"error\";\n sentryDsn?: string;\n sandboxCpus?: string;\n sandboxMemory?: string;\n}\n\nconst ONBOARD_SETTINGS: SettingsFileConfig = {\n llm: {\n provider: \"anthropic\",\n model: \"claude-sonnet-4-5\",\n thinkingLevel: \"off\",\n },\n log: {\n format: \"console\",\n level: \"info\",\n },\n};\n\ninterface SettingsFileConfig {\n llm?: Partial<Pick<AgentConfig, \"provider\" | \"model\" | \"thinkingLevel\">>;\n log?: { format?: AgentConfig[\"logFormat\"]; level?: AgentConfig[\"logLevel\"] };\n sentry?: { dsn?: string };\n sandbox?: { cpus?: string; memory?: string };\n}\n\nfunction loadSettingsFile(settingsPath: string): SettingsFileConfig | undefined {\n if (!existsSync(settingsPath)) {\n return undefined;\n }\n\n const raw = readFileSync(settingsPath, \"utf-8\");\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n throw new Error(`Malformed settings file at ${settingsPath}: ${detail}`);\n }\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new Error(\n `Malformed settings file at ${settingsPath}: expected a JSON object at the top level`,\n );\n }\n return parsed as SettingsFileConfig;\n}\n\nfunction getStateDir(): string {\n const raw = process.env.MAMA_STATE_DIR?.trim();\n return raw ? resolve(raw) : join(homedir(), \".mama\");\n}\n\nfunction normalizeSettingsConfig(config: SettingsFileConfig): Partial<AgentConfig> {\n return {\n ...(config.llm?.provider !== undefined ? { provider: config.llm.provider } : {}),\n ...(config.llm?.model !== undefined ? { model: config.llm.model } : {}),\n ...(config.llm?.thinkingLevel !== undefined ? { thinkingLevel: config.llm.thinkingLevel } : {}),\n ...(config.log?.format !== undefined ? { logFormat: config.log.format } : {}),\n ...(config.log?.level !== undefined ? { logLevel: config.log.level } : {}),\n ...(config.sentry?.dsn !== undefined ? { sentryDsn: config.sentry.dsn } : {}),\n ...(config.sandbox?.cpus !== undefined ? { sandboxCpus: config.sandbox.cpus } : {}),\n ...(config.sandbox?.memory !== undefined ? { sandboxMemory: config.sandbox.memory } : {}),\n };\n}\n\nfunction getSettingsPath(): string {\n return join(getStateDir(), \"settings.json\");\n}\n\nfunction requireGlobalSettings(): SettingsFileConfig {\n const settingsPath = getSettingsPath();\n const config = loadSettingsFile(settingsPath);\n if (!config) {\n throw new MissingGlobalSettingsError(settingsPath);\n }\n return config;\n}\n\nfunction requireString(value: string | undefined, path: string): string {\n if (!value) {\n throw new Error(\n `Missing required global setting: ${path}. Run \\`mama --onboard\\` to create settings.json.`,\n );\n }\n return value;\n}\n\nfunction requireThinkingLevel(value: ThinkingLevel | undefined): ThinkingLevel {\n return requireString(value, \"llm.thinkingLevel\") as ThinkingLevel;\n}\n\nfunction requireLogFormat(value: AgentConfig[\"logFormat\"] | undefined): AgentConfig[\"logFormat\"] {\n if (value !== \"console\" && value !== \"json\") {\n throw new Error(\"Missing or invalid required global setting: log.format\");\n }\n return value;\n}\n\nfunction requireLogLevel(value: AgentConfig[\"logLevel\"] | undefined): AgentConfig[\"logLevel\"] {\n const allowed = [\"trace\", \"debug\", \"info\", \"warn\", \"error\"];\n if (!value || !allowed.includes(value)) {\n throw new Error(\"Missing or invalid required global setting: log.level\");\n }\n return value;\n}\n\nfunction toAgentConfig(fromFile: Partial<AgentConfig>): AgentConfig {\n const provider = requireString(fromFile.provider, \"llm.provider\");\n const model = requireString(fromFile.model, \"llm.model\");\n const thinkingLevel = requireThinkingLevel(fromFile.thinkingLevel);\n const logFormat = requireLogFormat(fromFile.logFormat);\n const logLevel = requireLogLevel(fromFile.logLevel);\n const sentryDsn = fromFile.sentryDsn ?? process.env.SENTRY_DSN;\n const sandboxCpus = fromFile.sandboxCpus;\n const sandboxMemory = fromFile.sandboxMemory;\n\n return {\n provider,\n model,\n thinkingLevel,\n logFormat,\n logLevel,\n sentryDsn,\n sandboxCpus,\n sandboxMemory,\n };\n}\n\nfunction loadRawAgentConfig(): Partial<AgentConfig> {\n return normalizeSettingsConfig(requireGlobalSettings());\n}\n\nexport function loadAgentConfig(): AgentConfig {\n return toAgentConfig(loadRawAgentConfig());\n}\n\nexport function loadAgentConfigForConversation(conversationDir: string): AgentConfig {\n const globalConfig = loadRawAgentConfig();\n const conversationConfig = normalizeSettingsConfig(\n loadSettingsFile(join(conversationDir, \"settings.json\")) ?? {},\n );\n return toAgentConfig({ ...globalConfig, ...conversationConfig });\n}\n\nexport function saveConversationModelConfig(\n conversationDir: string,\n config: Pick<AgentConfig, \"provider\" | \"model\"> & Partial<Pick<AgentConfig, \"thinkingLevel\">>,\n): void {\n if (!existsSync(conversationDir)) {\n mkdirSync(conversationDir, { recursive: true });\n }\n const settingsPath = join(conversationDir, \"settings.json\");\n const existing = loadSettingsFile(settingsPath) ?? {};\n const scopedConfig: SettingsFileConfig = {\n ...existing,\n llm: { ...existing.llm, ...config },\n };\n atomicWritePrivateFile(settingsPath, JSON.stringify(scopedConfig, null, 2));\n}\n\nexport function resolveWorkspaceDirFromArgv(args = process.argv.slice(2)): string | undefined {\n for (let i = 0; i < args.length; i++) {\n const arg = args[i];\n\n if (arg === \"--sandbox\" || arg === \"--download\" || arg === \"--state-dir\") {\n i += 1;\n continue;\n }\n\n if (arg === \"--version\" || arg === \"-v\" || arg === \"-V\" || arg === \"--onboard\") {\n continue;\n }\n\n if (\n arg.startsWith(\"--sandbox=\") ||\n arg.startsWith(\"--download=\") ||\n arg.startsWith(\"--state-dir=\")\n ) {\n continue;\n }\n\n if (!arg.startsWith(\"-\")) {\n return arg;\n }\n }\n\n return undefined;\n}\n\nexport function resolveStateDirFromArgv(args = process.argv.slice(2)): string {\n for (let i = 0; i < args.length; i++) {\n const arg = args[i];\n if (arg.startsWith(\"--state-dir=\")) {\n return resolve(arg.slice(\"--state-dir=\".length));\n }\n if (arg === \"--state-dir\") {\n return resolve(args[++i] || \"\");\n }\n }\n\n return join(homedir(), \".mama\");\n}\n\nexport function resolveSentryDsn(): string | undefined {\n const fromFile = normalizeSettingsConfig(loadSettingsFile(getSettingsPath()) ?? {});\n if (fromFile.sentryDsn) {\n return fromFile.sentryDsn;\n }\n\n return process.env.SENTRY_DSN;\n}\n\nexport function createGlobalSettingsFile(stateDir: string): string {\n const settingsPath = join(stateDir, \"settings.json\");\n if (existsSync(settingsPath)) {\n throw new Error(`Global settings already exists at ${settingsPath}`);\n }\n if (!existsSync(stateDir)) {\n mkdirSync(stateDir, { recursive: true });\n }\n atomicWritePrivateFile(settingsPath, JSON.stringify(ONBOARD_SETTINGS, null, 2));\n return settingsPath;\n}\n\n/**\n * Externally-visible base URL of the link/OAuth server, e.g.\n * `https://mama.example.com` (no trailing slash). Read from `MAMA_LINK_URL`,\n * the same env var the bot uses to build credential onboarding links.\n */\nexport function resolveLinkBaseUrl(): string | undefined {\n const raw = process.env.MAMA_LINK_URL?.trim();\n if (!raw) return undefined;\n return raw.replace(/\\/+$/, \"\");\n}\n\nfunction hasDefinedValue(values: Record<string, unknown> | undefined): boolean {\n return values !== undefined && Object.values(values).some((value) => value !== undefined);\n}\n\nfunction compactSettingsConfig(config: SettingsFileConfig): SettingsFileConfig {\n return {\n ...(hasDefinedValue(config.llm) ? { llm: config.llm } : {}),\n ...(hasDefinedValue(config.log) ? { log: config.log } : {}),\n ...(hasDefinedValue(config.sentry) ? { sentry: config.sentry } : {}),\n ...(hasDefinedValue(config.sandbox) ? { sandbox: config.sandbox } : {}),\n };\n}\n\nfunction patchSettingsConfig(\n existing: SettingsFileConfig,\n config: Partial<AgentConfig>,\n): SettingsFileConfig {\n const patched: SettingsFileConfig = {\n ...existing,\n llm: {\n ...existing.llm,\n ...(config.provider !== undefined ? { provider: config.provider } : {}),\n ...(config.model !== undefined ? { model: config.model } : {}),\n ...(config.thinkingLevel !== undefined ? { thinkingLevel: config.thinkingLevel } : {}),\n },\n log: {\n ...existing.log,\n ...(config.logFormat !== undefined ? { format: config.logFormat } : {}),\n ...(config.logLevel !== undefined ? { level: config.logLevel } : {}),\n },\n sentry: {\n ...existing.sentry,\n ...(config.sentryDsn !== undefined ? { dsn: config.sentryDsn } : {}),\n },\n sandbox: {\n ...existing.sandbox,\n ...(config.sandboxCpus !== undefined ? { cpus: config.sandboxCpus } : {}),\n ...(config.sandboxMemory !== undefined ? { memory: config.sandboxMemory } : {}),\n },\n };\n return compactSettingsConfig(patched);\n}\n\nexport function saveAgentConfig(config: Partial<AgentConfig>): void {\n const settingsPath = join(getStateDir(), \"settings.json\");\n\n let existing: SettingsFileConfig = ONBOARD_SETTINGS;\n if (existsSync(settingsPath)) {\n try {\n existing = loadSettingsFile(settingsPath) ?? {};\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n const message = detail.startsWith(\"Malformed settings file\")\n ? detail.replace(\"Malformed settings file\", \"Refusing to overwrite malformed settings file\")\n : detail;\n throw new Error(message);\n }\n }\n\n const merged = patchSettingsConfig(existing, config);\n\n const dir = dirname(settingsPath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n atomicWritePrivateFile(settingsPath, JSON.stringify(merged, null, 2));\n}\n"]}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAMnE,qBAAa,0BAA2B,SAAQ,KAAK;aACvB,YAAY,EAAE,MAAM;IAAhD,YAA4B,YAAY,EAAE,MAAM,EAG/C;CACF;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,aAAa,CAAC;IAC7B,SAAS,EAAE,SAAS,GAAG,MAAM,CAAC;IAC9B,QAAQ,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IACxD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAiJD,wBAAgB,eAAe,IAAI,WAAW,CAE7C;AAED,wBAAgB,8BAA8B,CAAC,eAAe,EAAE,MAAM,GAAG,WAAW,CAMnF;AAED,wBAAgB,2BAA2B,CACzC,eAAe,EAAE,MAAM,EACvB,MAAM,EAAE,IAAI,CAAC,WAAW,EAAE,UAAU,GAAG,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,eAAe,CAAC,CAAC,GAC5F,IAAI,CAWN;AAED,wBAAgB,2BAA2B,CAAC,IAAI,WAAwB,GAAG,MAAM,GAAG,SAAS,CA2B5F;AAED,wBAAgB,uBAAuB,CAAC,IAAI,WAAwB,GAAG,MAAM,CAY5E;AAED,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,SAAS,CAOrD;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAUjE;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,GAAG,SAAS,CAIvD;AAwDD,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG,IAAI,CAwBlE","sourcesContent":["import type { ThinkingLevel } from \"@earendil-works/pi-agent-core\";\nimport { existsSync, mkdirSync, readFileSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { dirname, join, resolve } from \"path\";\nimport { atomicWritePrivateFile } from \"./fs-atomic.js\";\n\nexport class MissingGlobalSettingsError extends Error {\n constructor(public readonly settingsPath: string) {\n super(`Missing global settings file at ${settingsPath}`);\n this.name = \"MissingGlobalSettingsError\";\n }\n}\n\nexport interface AgentConfig {\n provider: string;\n model: string;\n thinkingLevel: ThinkingLevel;\n logFormat: \"console\" | \"json\";\n logLevel: \"trace\" | \"debug\" | \"info\" | \"warn\" | \"error\";\n sentryDsn?: string;\n sandboxCpus?: string;\n sandboxMemory?: string;\n sandboxBoostCpus?: string;\n sandboxBoostMemory?: string;\n}\n\nconst ONBOARD_SETTINGS: SettingsFileConfig = {\n llm: {\n provider: \"anthropic\",\n model: \"claude-sonnet-4-5\",\n thinkingLevel: \"off\",\n },\n log: {\n format: \"console\",\n level: \"info\",\n },\n sandbox: {\n cpus: \"0.5\",\n memory: \"1g\",\n boost: {\n cpus: \"2\",\n memory: \"4g\",\n },\n },\n};\n\ninterface SettingsFileConfig {\n llm?: Partial<Pick<AgentConfig, \"provider\" | \"model\" | \"thinkingLevel\">>;\n log?: { format?: AgentConfig[\"logFormat\"]; level?: AgentConfig[\"logLevel\"] };\n sentry?: { dsn?: string };\n sandbox?: { cpus?: string; memory?: string; boost?: { cpus?: string; memory?: string } };\n}\n\nfunction loadSettingsFile(settingsPath: string): SettingsFileConfig | undefined {\n if (!existsSync(settingsPath)) {\n return undefined;\n }\n\n const raw = readFileSync(settingsPath, \"utf-8\");\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n throw new Error(`Malformed settings file at ${settingsPath}: ${detail}`);\n }\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new Error(\n `Malformed settings file at ${settingsPath}: expected a JSON object at the top level`,\n );\n }\n return parsed as SettingsFileConfig;\n}\n\nfunction getStateDir(): string {\n const raw = process.env.MAMA_STATE_DIR?.trim();\n return raw ? resolve(raw) : join(homedir(), \".mama\");\n}\n\nfunction normalizeSettingsConfig(config: SettingsFileConfig): Partial<AgentConfig> {\n return {\n ...(config.llm?.provider !== undefined ? { provider: config.llm.provider } : {}),\n ...(config.llm?.model !== undefined ? { model: config.llm.model } : {}),\n ...(config.llm?.thinkingLevel !== undefined ? { thinkingLevel: config.llm.thinkingLevel } : {}),\n ...(config.log?.format !== undefined ? { logFormat: config.log.format } : {}),\n ...(config.log?.level !== undefined ? { logLevel: config.log.level } : {}),\n ...(config.sentry?.dsn !== undefined ? { sentryDsn: config.sentry.dsn } : {}),\n ...(config.sandbox?.cpus !== undefined ? { sandboxCpus: config.sandbox.cpus } : {}),\n ...(config.sandbox?.memory !== undefined ? { sandboxMemory: config.sandbox.memory } : {}),\n ...(config.sandbox?.boost?.cpus !== undefined\n ? { sandboxBoostCpus: config.sandbox.boost.cpus }\n : {}),\n ...(config.sandbox?.boost?.memory !== undefined\n ? { sandboxBoostMemory: config.sandbox.boost.memory }\n : {}),\n };\n}\n\nfunction getSettingsPath(): string {\n return join(getStateDir(), \"settings.json\");\n}\n\nfunction requireGlobalSettings(): SettingsFileConfig {\n const settingsPath = getSettingsPath();\n const config = loadSettingsFile(settingsPath);\n if (!config) {\n throw new MissingGlobalSettingsError(settingsPath);\n }\n return config;\n}\n\nfunction requireString(value: string | undefined, path: string): string {\n if (!value) {\n throw new Error(\n `Missing required global setting: ${path}. Run \\`mama --onboard\\` to create settings.json.`,\n );\n }\n return value;\n}\n\nfunction requireThinkingLevel(value: ThinkingLevel | undefined): ThinkingLevel {\n return requireString(value, \"llm.thinkingLevel\") as ThinkingLevel;\n}\n\nfunction requireLogFormat(value: AgentConfig[\"logFormat\"] | undefined): AgentConfig[\"logFormat\"] {\n if (value !== \"console\" && value !== \"json\") {\n throw new Error(\"Missing or invalid required global setting: log.format\");\n }\n return value;\n}\n\nfunction requireLogLevel(value: AgentConfig[\"logLevel\"] | undefined): AgentConfig[\"logLevel\"] {\n const allowed = [\"trace\", \"debug\", \"info\", \"warn\", \"error\"];\n if (!value || !allowed.includes(value)) {\n throw new Error(\"Missing or invalid required global setting: log.level\");\n }\n return value;\n}\n\nfunction toAgentConfig(fromFile: Partial<AgentConfig>): AgentConfig {\n const provider = requireString(fromFile.provider, \"llm.provider\");\n const model = requireString(fromFile.model, \"llm.model\");\n const thinkingLevel = requireThinkingLevel(fromFile.thinkingLevel);\n const logFormat = requireLogFormat(fromFile.logFormat);\n const logLevel = requireLogLevel(fromFile.logLevel);\n const sentryDsn = fromFile.sentryDsn ?? process.env.SENTRY_DSN;\n const sandboxCpus = fromFile.sandboxCpus;\n const sandboxMemory = fromFile.sandboxMemory;\n const sandboxBoostCpus = fromFile.sandboxBoostCpus;\n const sandboxBoostMemory = fromFile.sandboxBoostMemory;\n\n return {\n provider,\n model,\n thinkingLevel,\n logFormat,\n logLevel,\n sentryDsn,\n sandboxCpus,\n sandboxMemory,\n sandboxBoostCpus,\n sandboxBoostMemory,\n };\n}\n\nfunction loadRawAgentConfig(): Partial<AgentConfig> {\n return normalizeSettingsConfig(requireGlobalSettings());\n}\n\nexport function loadAgentConfig(): AgentConfig {\n return toAgentConfig(loadRawAgentConfig());\n}\n\nexport function loadAgentConfigForConversation(conversationDir: string): AgentConfig {\n const globalConfig = loadRawAgentConfig();\n const conversationConfig = normalizeSettingsConfig(\n loadSettingsFile(join(conversationDir, \"settings.json\")) ?? {},\n );\n return toAgentConfig({ ...globalConfig, ...conversationConfig });\n}\n\nexport function saveConversationModelConfig(\n conversationDir: string,\n config: Pick<AgentConfig, \"provider\" | \"model\"> & Partial<Pick<AgentConfig, \"thinkingLevel\">>,\n): void {\n if (!existsSync(conversationDir)) {\n mkdirSync(conversationDir, { recursive: true });\n }\n const settingsPath = join(conversationDir, \"settings.json\");\n const existing = loadSettingsFile(settingsPath) ?? {};\n const scopedConfig: SettingsFileConfig = {\n ...existing,\n llm: { ...existing.llm, ...config },\n };\n atomicWritePrivateFile(settingsPath, JSON.stringify(scopedConfig, null, 2));\n}\n\nexport function resolveWorkspaceDirFromArgv(args = process.argv.slice(2)): string | undefined {\n for (let i = 0; i < args.length; i++) {\n const arg = args[i];\n\n if (arg === \"--sandbox\" || arg === \"--download\" || arg === \"--state-dir\") {\n i += 1;\n continue;\n }\n\n if (arg === \"--version\" || arg === \"-v\" || arg === \"-V\" || arg === \"--onboard\") {\n continue;\n }\n\n if (\n arg.startsWith(\"--sandbox=\") ||\n arg.startsWith(\"--download=\") ||\n arg.startsWith(\"--state-dir=\")\n ) {\n continue;\n }\n\n if (!arg.startsWith(\"-\")) {\n return arg;\n }\n }\n\n return undefined;\n}\n\nexport function resolveStateDirFromArgv(args = process.argv.slice(2)): string {\n for (let i = 0; i < args.length; i++) {\n const arg = args[i];\n if (arg.startsWith(\"--state-dir=\")) {\n return resolve(arg.slice(\"--state-dir=\".length));\n }\n if (arg === \"--state-dir\") {\n return resolve(args[++i] || \"\");\n }\n }\n\n return join(homedir(), \".mama\");\n}\n\nexport function resolveSentryDsn(): string | undefined {\n const fromFile = normalizeSettingsConfig(loadSettingsFile(getSettingsPath()) ?? {});\n if (fromFile.sentryDsn) {\n return fromFile.sentryDsn;\n }\n\n return process.env.SENTRY_DSN;\n}\n\nexport function createGlobalSettingsFile(stateDir: string): string {\n const settingsPath = join(stateDir, \"settings.json\");\n if (existsSync(settingsPath)) {\n throw new Error(`Global settings already exists at ${settingsPath}`);\n }\n if (!existsSync(stateDir)) {\n mkdirSync(stateDir, { recursive: true });\n }\n atomicWritePrivateFile(settingsPath, JSON.stringify(ONBOARD_SETTINGS, null, 2));\n return settingsPath;\n}\n\n/**\n * Externally-visible base URL of the link/OAuth server, e.g.\n * `https://mama.example.com` (no trailing slash). Read from `MAMA_LINK_URL`,\n * the same env var the bot uses to build credential onboarding links.\n */\nexport function resolveLinkBaseUrl(): string | undefined {\n const raw = process.env.MAMA_LINK_URL?.trim();\n if (!raw) return undefined;\n return raw.replace(/\\/+$/, \"\");\n}\n\nfunction hasDefinedValue(values: Record<string, unknown> | undefined): boolean {\n return values !== undefined && Object.values(values).some((value) => value !== undefined);\n}\n\nfunction compactSettingsConfig(config: SettingsFileConfig): SettingsFileConfig {\n return {\n ...(hasDefinedValue(config.llm) ? { llm: config.llm } : {}),\n ...(hasDefinedValue(config.log) ? { log: config.log } : {}),\n ...(hasDefinedValue(config.sentry) ? { sentry: config.sentry } : {}),\n ...(hasDefinedValue(config.sandbox) ? { sandbox: config.sandbox } : {}),\n };\n}\n\nfunction patchSettingsConfig(\n existing: SettingsFileConfig,\n config: Partial<AgentConfig>,\n): SettingsFileConfig {\n const patched: SettingsFileConfig = {\n ...existing,\n llm: {\n ...existing.llm,\n ...(config.provider !== undefined ? { provider: config.provider } : {}),\n ...(config.model !== undefined ? { model: config.model } : {}),\n ...(config.thinkingLevel !== undefined ? { thinkingLevel: config.thinkingLevel } : {}),\n },\n log: {\n ...existing.log,\n ...(config.logFormat !== undefined ? { format: config.logFormat } : {}),\n ...(config.logLevel !== undefined ? { level: config.logLevel } : {}),\n },\n sentry: {\n ...existing.sentry,\n ...(config.sentryDsn !== undefined ? { dsn: config.sentryDsn } : {}),\n },\n sandbox: {\n ...existing.sandbox,\n ...(config.sandboxCpus !== undefined ? { cpus: config.sandboxCpus } : {}),\n ...(config.sandboxMemory !== undefined ? { memory: config.sandboxMemory } : {}),\n ...(config.sandboxBoostCpus !== undefined || config.sandboxBoostMemory !== undefined\n ? {\n boost: {\n ...existing.sandbox?.boost,\n ...(config.sandboxBoostCpus !== undefined ? { cpus: config.sandboxBoostCpus } : {}),\n ...(config.sandboxBoostMemory !== undefined\n ? { memory: config.sandboxBoostMemory }\n : {}),\n },\n }\n : {}),\n },\n };\n return compactSettingsConfig(patched);\n}\n\nexport function saveAgentConfig(config: Partial<AgentConfig>): void {\n const settingsPath = join(getStateDir(), \"settings.json\");\n\n let existing: SettingsFileConfig = ONBOARD_SETTINGS;\n if (existsSync(settingsPath)) {\n try {\n existing = loadSettingsFile(settingsPath) ?? {};\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n const message = detail.startsWith(\"Malformed settings file\")\n ? detail.replace(\"Malformed settings file\", \"Refusing to overwrite malformed settings file\")\n : detail;\n throw new Error(message);\n }\n }\n\n const merged = patchSettingsConfig(existing, config);\n\n const dir = dirname(settingsPath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n atomicWritePrivateFile(settingsPath, JSON.stringify(merged, null, 2));\n}\n"]}
package/dist/config.js CHANGED
@@ -19,6 +19,14 @@ const ONBOARD_SETTINGS = {
19
19
  format: "console",
20
20
  level: "info",
21
21
  },
22
+ sandbox: {
23
+ cpus: "0.5",
24
+ memory: "1g",
25
+ boost: {
26
+ cpus: "2",
27
+ memory: "4g",
28
+ },
29
+ },
22
30
  };
23
31
  function loadSettingsFile(settingsPath) {
24
32
  if (!existsSync(settingsPath)) {
@@ -52,6 +60,12 @@ function normalizeSettingsConfig(config) {
52
60
  ...(config.sentry?.dsn !== undefined ? { sentryDsn: config.sentry.dsn } : {}),
53
61
  ...(config.sandbox?.cpus !== undefined ? { sandboxCpus: config.sandbox.cpus } : {}),
54
62
  ...(config.sandbox?.memory !== undefined ? { sandboxMemory: config.sandbox.memory } : {}),
63
+ ...(config.sandbox?.boost?.cpus !== undefined
64
+ ? { sandboxBoostCpus: config.sandbox.boost.cpus }
65
+ : {}),
66
+ ...(config.sandbox?.boost?.memory !== undefined
67
+ ? { sandboxBoostMemory: config.sandbox.boost.memory }
68
+ : {}),
55
69
  };
56
70
  }
57
71
  function getSettingsPath() {
@@ -96,6 +110,8 @@ function toAgentConfig(fromFile) {
96
110
  const sentryDsn = fromFile.sentryDsn ?? process.env.SENTRY_DSN;
97
111
  const sandboxCpus = fromFile.sandboxCpus;
98
112
  const sandboxMemory = fromFile.sandboxMemory;
113
+ const sandboxBoostCpus = fromFile.sandboxBoostCpus;
114
+ const sandboxBoostMemory = fromFile.sandboxBoostMemory;
99
115
  return {
100
116
  provider,
101
117
  model,
@@ -105,6 +121,8 @@ function toAgentConfig(fromFile) {
105
121
  sentryDsn,
106
122
  sandboxCpus,
107
123
  sandboxMemory,
124
+ sandboxBoostCpus,
125
+ sandboxBoostMemory,
108
126
  };
109
127
  }
110
128
  function loadRawAgentConfig() {
@@ -225,6 +243,17 @@ function patchSettingsConfig(existing, config) {
225
243
  ...existing.sandbox,
226
244
  ...(config.sandboxCpus !== undefined ? { cpus: config.sandboxCpus } : {}),
227
245
  ...(config.sandboxMemory !== undefined ? { memory: config.sandboxMemory } : {}),
246
+ ...(config.sandboxBoostCpus !== undefined || config.sandboxBoostMemory !== undefined
247
+ ? {
248
+ boost: {
249
+ ...existing.sandbox?.boost,
250
+ ...(config.sandboxBoostCpus !== undefined ? { cpus: config.sandboxBoostCpus } : {}),
251
+ ...(config.sandboxBoostMemory !== undefined
252
+ ? { memory: config.sandboxBoostMemory }
253
+ : {}),
254
+ },
255
+ }
256
+ : {}),
228
257
  },
229
258
  };
230
259
  return compactSettingsConfig(patched);
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ThinkingLevel } from \"@mariozechner/pi-agent-core\";\nimport { existsSync, mkdirSync, readFileSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { dirname, join, resolve } from \"path\";\nimport { atomicWritePrivateFile } from \"./fs-atomic.js\";\n\nexport class MissingGlobalSettingsError extends Error {\n constructor(public readonly settingsPath: string) {\n super(`Missing global settings file at ${settingsPath}`);\n this.name = \"MissingGlobalSettingsError\";\n }\n}\n\nexport interface AgentConfig {\n provider: string;\n model: string;\n thinkingLevel: ThinkingLevel;\n logFormat: \"console\" | \"json\";\n logLevel: \"trace\" | \"debug\" | \"info\" | \"warn\" | \"error\";\n sentryDsn?: string;\n sandboxCpus?: string;\n sandboxMemory?: string;\n}\n\nconst ONBOARD_SETTINGS: SettingsFileConfig = {\n llm: {\n provider: \"anthropic\",\n model: \"claude-sonnet-4-5\",\n thinkingLevel: \"off\",\n },\n log: {\n format: \"console\",\n level: \"info\",\n },\n};\n\ninterface SettingsFileConfig {\n llm?: Partial<Pick<AgentConfig, \"provider\" | \"model\" | \"thinkingLevel\">>;\n log?: { format?: AgentConfig[\"logFormat\"]; level?: AgentConfig[\"logLevel\"] };\n sentry?: { dsn?: string };\n sandbox?: { cpus?: string; memory?: string };\n}\n\nfunction loadSettingsFile(settingsPath: string): SettingsFileConfig | undefined {\n if (!existsSync(settingsPath)) {\n return undefined;\n }\n\n const raw = readFileSync(settingsPath, \"utf-8\");\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n throw new Error(`Malformed settings file at ${settingsPath}: ${detail}`);\n }\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new Error(\n `Malformed settings file at ${settingsPath}: expected a JSON object at the top level`,\n );\n }\n return parsed as SettingsFileConfig;\n}\n\nfunction getStateDir(): string {\n const raw = process.env.MAMA_STATE_DIR?.trim();\n return raw ? resolve(raw) : join(homedir(), \".mama\");\n}\n\nfunction normalizeSettingsConfig(config: SettingsFileConfig): Partial<AgentConfig> {\n return {\n ...(config.llm?.provider !== undefined ? { provider: config.llm.provider } : {}),\n ...(config.llm?.model !== undefined ? { model: config.llm.model } : {}),\n ...(config.llm?.thinkingLevel !== undefined ? { thinkingLevel: config.llm.thinkingLevel } : {}),\n ...(config.log?.format !== undefined ? { logFormat: config.log.format } : {}),\n ...(config.log?.level !== undefined ? { logLevel: config.log.level } : {}),\n ...(config.sentry?.dsn !== undefined ? { sentryDsn: config.sentry.dsn } : {}),\n ...(config.sandbox?.cpus !== undefined ? { sandboxCpus: config.sandbox.cpus } : {}),\n ...(config.sandbox?.memory !== undefined ? { sandboxMemory: config.sandbox.memory } : {}),\n };\n}\n\nfunction getSettingsPath(): string {\n return join(getStateDir(), \"settings.json\");\n}\n\nfunction requireGlobalSettings(): SettingsFileConfig {\n const settingsPath = getSettingsPath();\n const config = loadSettingsFile(settingsPath);\n if (!config) {\n throw new MissingGlobalSettingsError(settingsPath);\n }\n return config;\n}\n\nfunction requireString(value: string | undefined, path: string): string {\n if (!value) {\n throw new Error(\n `Missing required global setting: ${path}. Run \\`mama --onboard\\` to create settings.json.`,\n );\n }\n return value;\n}\n\nfunction requireThinkingLevel(value: ThinkingLevel | undefined): ThinkingLevel {\n return requireString(value, \"llm.thinkingLevel\") as ThinkingLevel;\n}\n\nfunction requireLogFormat(value: AgentConfig[\"logFormat\"] | undefined): AgentConfig[\"logFormat\"] {\n if (value !== \"console\" && value !== \"json\") {\n throw new Error(\"Missing or invalid required global setting: log.format\");\n }\n return value;\n}\n\nfunction requireLogLevel(value: AgentConfig[\"logLevel\"] | undefined): AgentConfig[\"logLevel\"] {\n const allowed = [\"trace\", \"debug\", \"info\", \"warn\", \"error\"];\n if (!value || !allowed.includes(value)) {\n throw new Error(\"Missing or invalid required global setting: log.level\");\n }\n return value;\n}\n\nfunction toAgentConfig(fromFile: Partial<AgentConfig>): AgentConfig {\n const provider = requireString(fromFile.provider, \"llm.provider\");\n const model = requireString(fromFile.model, \"llm.model\");\n const thinkingLevel = requireThinkingLevel(fromFile.thinkingLevel);\n const logFormat = requireLogFormat(fromFile.logFormat);\n const logLevel = requireLogLevel(fromFile.logLevel);\n const sentryDsn = fromFile.sentryDsn ?? process.env.SENTRY_DSN;\n const sandboxCpus = fromFile.sandboxCpus;\n const sandboxMemory = fromFile.sandboxMemory;\n\n return {\n provider,\n model,\n thinkingLevel,\n logFormat,\n logLevel,\n sentryDsn,\n sandboxCpus,\n sandboxMemory,\n };\n}\n\nfunction loadRawAgentConfig(): Partial<AgentConfig> {\n return normalizeSettingsConfig(requireGlobalSettings());\n}\n\nexport function loadAgentConfig(): AgentConfig {\n return toAgentConfig(loadRawAgentConfig());\n}\n\nexport function loadAgentConfigForConversation(conversationDir: string): AgentConfig {\n const globalConfig = loadRawAgentConfig();\n const conversationConfig = normalizeSettingsConfig(\n loadSettingsFile(join(conversationDir, \"settings.json\")) ?? {},\n );\n return toAgentConfig({ ...globalConfig, ...conversationConfig });\n}\n\nexport function saveConversationModelConfig(\n conversationDir: string,\n config: Pick<AgentConfig, \"provider\" | \"model\"> & Partial<Pick<AgentConfig, \"thinkingLevel\">>,\n): void {\n if (!existsSync(conversationDir)) {\n mkdirSync(conversationDir, { recursive: true });\n }\n const settingsPath = join(conversationDir, \"settings.json\");\n const existing = loadSettingsFile(settingsPath) ?? {};\n const scopedConfig: SettingsFileConfig = {\n ...existing,\n llm: { ...existing.llm, ...config },\n };\n atomicWritePrivateFile(settingsPath, JSON.stringify(scopedConfig, null, 2));\n}\n\nexport function resolveWorkspaceDirFromArgv(args = process.argv.slice(2)): string | undefined {\n for (let i = 0; i < args.length; i++) {\n const arg = args[i];\n\n if (arg === \"--sandbox\" || arg === \"--download\" || arg === \"--state-dir\") {\n i += 1;\n continue;\n }\n\n if (arg === \"--version\" || arg === \"-v\" || arg === \"-V\" || arg === \"--onboard\") {\n continue;\n }\n\n if (\n arg.startsWith(\"--sandbox=\") ||\n arg.startsWith(\"--download=\") ||\n arg.startsWith(\"--state-dir=\")\n ) {\n continue;\n }\n\n if (!arg.startsWith(\"-\")) {\n return arg;\n }\n }\n\n return undefined;\n}\n\nexport function resolveStateDirFromArgv(args = process.argv.slice(2)): string {\n for (let i = 0; i < args.length; i++) {\n const arg = args[i];\n if (arg.startsWith(\"--state-dir=\")) {\n return resolve(arg.slice(\"--state-dir=\".length));\n }\n if (arg === \"--state-dir\") {\n return resolve(args[++i] || \"\");\n }\n }\n\n return join(homedir(), \".mama\");\n}\n\nexport function resolveSentryDsn(): string | undefined {\n const fromFile = normalizeSettingsConfig(loadSettingsFile(getSettingsPath()) ?? {});\n if (fromFile.sentryDsn) {\n return fromFile.sentryDsn;\n }\n\n return process.env.SENTRY_DSN;\n}\n\nexport function createGlobalSettingsFile(stateDir: string): string {\n const settingsPath = join(stateDir, \"settings.json\");\n if (existsSync(settingsPath)) {\n throw new Error(`Global settings already exists at ${settingsPath}`);\n }\n if (!existsSync(stateDir)) {\n mkdirSync(stateDir, { recursive: true });\n }\n atomicWritePrivateFile(settingsPath, JSON.stringify(ONBOARD_SETTINGS, null, 2));\n return settingsPath;\n}\n\n/**\n * Externally-visible base URL of the link/OAuth server, e.g.\n * `https://mama.example.com` (no trailing slash). Read from `MAMA_LINK_URL`,\n * the same env var the bot uses to build credential onboarding links.\n */\nexport function resolveLinkBaseUrl(): string | undefined {\n const raw = process.env.MAMA_LINK_URL?.trim();\n if (!raw) return undefined;\n return raw.replace(/\\/+$/, \"\");\n}\n\nfunction hasDefinedValue(values: Record<string, unknown> | undefined): boolean {\n return values !== undefined && Object.values(values).some((value) => value !== undefined);\n}\n\nfunction compactSettingsConfig(config: SettingsFileConfig): SettingsFileConfig {\n return {\n ...(hasDefinedValue(config.llm) ? { llm: config.llm } : {}),\n ...(hasDefinedValue(config.log) ? { log: config.log } : {}),\n ...(hasDefinedValue(config.sentry) ? { sentry: config.sentry } : {}),\n ...(hasDefinedValue(config.sandbox) ? { sandbox: config.sandbox } : {}),\n };\n}\n\nfunction patchSettingsConfig(\n existing: SettingsFileConfig,\n config: Partial<AgentConfig>,\n): SettingsFileConfig {\n const patched: SettingsFileConfig = {\n ...existing,\n llm: {\n ...existing.llm,\n ...(config.provider !== undefined ? { provider: config.provider } : {}),\n ...(config.model !== undefined ? { model: config.model } : {}),\n ...(config.thinkingLevel !== undefined ? { thinkingLevel: config.thinkingLevel } : {}),\n },\n log: {\n ...existing.log,\n ...(config.logFormat !== undefined ? { format: config.logFormat } : {}),\n ...(config.logLevel !== undefined ? { level: config.logLevel } : {}),\n },\n sentry: {\n ...existing.sentry,\n ...(config.sentryDsn !== undefined ? { dsn: config.sentryDsn } : {}),\n },\n sandbox: {\n ...existing.sandbox,\n ...(config.sandboxCpus !== undefined ? { cpus: config.sandboxCpus } : {}),\n ...(config.sandboxMemory !== undefined ? { memory: config.sandboxMemory } : {}),\n },\n };\n return compactSettingsConfig(patched);\n}\n\nexport function saveAgentConfig(config: Partial<AgentConfig>): void {\n const settingsPath = join(getStateDir(), \"settings.json\");\n\n let existing: SettingsFileConfig = ONBOARD_SETTINGS;\n if (existsSync(settingsPath)) {\n try {\n existing = loadSettingsFile(settingsPath) ?? {};\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n const message = detail.startsWith(\"Malformed settings file\")\n ? detail.replace(\"Malformed settings file\", \"Refusing to overwrite malformed settings file\")\n : detail;\n throw new Error(message);\n }\n }\n\n const merged = patchSettingsConfig(existing, config);\n\n const dir = dirname(settingsPath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n atomicWritePrivateFile(settingsPath, JSON.stringify(merged, null, 2));\n}\n"]}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ThinkingLevel } from \"@earendil-works/pi-agent-core\";\nimport { existsSync, mkdirSync, readFileSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { dirname, join, resolve } from \"path\";\nimport { atomicWritePrivateFile } from \"./fs-atomic.js\";\n\nexport class MissingGlobalSettingsError extends Error {\n constructor(public readonly settingsPath: string) {\n super(`Missing global settings file at ${settingsPath}`);\n this.name = \"MissingGlobalSettingsError\";\n }\n}\n\nexport interface AgentConfig {\n provider: string;\n model: string;\n thinkingLevel: ThinkingLevel;\n logFormat: \"console\" | \"json\";\n logLevel: \"trace\" | \"debug\" | \"info\" | \"warn\" | \"error\";\n sentryDsn?: string;\n sandboxCpus?: string;\n sandboxMemory?: string;\n sandboxBoostCpus?: string;\n sandboxBoostMemory?: string;\n}\n\nconst ONBOARD_SETTINGS: SettingsFileConfig = {\n llm: {\n provider: \"anthropic\",\n model: \"claude-sonnet-4-5\",\n thinkingLevel: \"off\",\n },\n log: {\n format: \"console\",\n level: \"info\",\n },\n sandbox: {\n cpus: \"0.5\",\n memory: \"1g\",\n boost: {\n cpus: \"2\",\n memory: \"4g\",\n },\n },\n};\n\ninterface SettingsFileConfig {\n llm?: Partial<Pick<AgentConfig, \"provider\" | \"model\" | \"thinkingLevel\">>;\n log?: { format?: AgentConfig[\"logFormat\"]; level?: AgentConfig[\"logLevel\"] };\n sentry?: { dsn?: string };\n sandbox?: { cpus?: string; memory?: string; boost?: { cpus?: string; memory?: string } };\n}\n\nfunction loadSettingsFile(settingsPath: string): SettingsFileConfig | undefined {\n if (!existsSync(settingsPath)) {\n return undefined;\n }\n\n const raw = readFileSync(settingsPath, \"utf-8\");\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n throw new Error(`Malformed settings file at ${settingsPath}: ${detail}`);\n }\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new Error(\n `Malformed settings file at ${settingsPath}: expected a JSON object at the top level`,\n );\n }\n return parsed as SettingsFileConfig;\n}\n\nfunction getStateDir(): string {\n const raw = process.env.MAMA_STATE_DIR?.trim();\n return raw ? resolve(raw) : join(homedir(), \".mama\");\n}\n\nfunction normalizeSettingsConfig(config: SettingsFileConfig): Partial<AgentConfig> {\n return {\n ...(config.llm?.provider !== undefined ? { provider: config.llm.provider } : {}),\n ...(config.llm?.model !== undefined ? { model: config.llm.model } : {}),\n ...(config.llm?.thinkingLevel !== undefined ? { thinkingLevel: config.llm.thinkingLevel } : {}),\n ...(config.log?.format !== undefined ? { logFormat: config.log.format } : {}),\n ...(config.log?.level !== undefined ? { logLevel: config.log.level } : {}),\n ...(config.sentry?.dsn !== undefined ? { sentryDsn: config.sentry.dsn } : {}),\n ...(config.sandbox?.cpus !== undefined ? { sandboxCpus: config.sandbox.cpus } : {}),\n ...(config.sandbox?.memory !== undefined ? { sandboxMemory: config.sandbox.memory } : {}),\n ...(config.sandbox?.boost?.cpus !== undefined\n ? { sandboxBoostCpus: config.sandbox.boost.cpus }\n : {}),\n ...(config.sandbox?.boost?.memory !== undefined\n ? { sandboxBoostMemory: config.sandbox.boost.memory }\n : {}),\n };\n}\n\nfunction getSettingsPath(): string {\n return join(getStateDir(), \"settings.json\");\n}\n\nfunction requireGlobalSettings(): SettingsFileConfig {\n const settingsPath = getSettingsPath();\n const config = loadSettingsFile(settingsPath);\n if (!config) {\n throw new MissingGlobalSettingsError(settingsPath);\n }\n return config;\n}\n\nfunction requireString(value: string | undefined, path: string): string {\n if (!value) {\n throw new Error(\n `Missing required global setting: ${path}. Run \\`mama --onboard\\` to create settings.json.`,\n );\n }\n return value;\n}\n\nfunction requireThinkingLevel(value: ThinkingLevel | undefined): ThinkingLevel {\n return requireString(value, \"llm.thinkingLevel\") as ThinkingLevel;\n}\n\nfunction requireLogFormat(value: AgentConfig[\"logFormat\"] | undefined): AgentConfig[\"logFormat\"] {\n if (value !== \"console\" && value !== \"json\") {\n throw new Error(\"Missing or invalid required global setting: log.format\");\n }\n return value;\n}\n\nfunction requireLogLevel(value: AgentConfig[\"logLevel\"] | undefined): AgentConfig[\"logLevel\"] {\n const allowed = [\"trace\", \"debug\", \"info\", \"warn\", \"error\"];\n if (!value || !allowed.includes(value)) {\n throw new Error(\"Missing or invalid required global setting: log.level\");\n }\n return value;\n}\n\nfunction toAgentConfig(fromFile: Partial<AgentConfig>): AgentConfig {\n const provider = requireString(fromFile.provider, \"llm.provider\");\n const model = requireString(fromFile.model, \"llm.model\");\n const thinkingLevel = requireThinkingLevel(fromFile.thinkingLevel);\n const logFormat = requireLogFormat(fromFile.logFormat);\n const logLevel = requireLogLevel(fromFile.logLevel);\n const sentryDsn = fromFile.sentryDsn ?? process.env.SENTRY_DSN;\n const sandboxCpus = fromFile.sandboxCpus;\n const sandboxMemory = fromFile.sandboxMemory;\n const sandboxBoostCpus = fromFile.sandboxBoostCpus;\n const sandboxBoostMemory = fromFile.sandboxBoostMemory;\n\n return {\n provider,\n model,\n thinkingLevel,\n logFormat,\n logLevel,\n sentryDsn,\n sandboxCpus,\n sandboxMemory,\n sandboxBoostCpus,\n sandboxBoostMemory,\n };\n}\n\nfunction loadRawAgentConfig(): Partial<AgentConfig> {\n return normalizeSettingsConfig(requireGlobalSettings());\n}\n\nexport function loadAgentConfig(): AgentConfig {\n return toAgentConfig(loadRawAgentConfig());\n}\n\nexport function loadAgentConfigForConversation(conversationDir: string): AgentConfig {\n const globalConfig = loadRawAgentConfig();\n const conversationConfig = normalizeSettingsConfig(\n loadSettingsFile(join(conversationDir, \"settings.json\")) ?? {},\n );\n return toAgentConfig({ ...globalConfig, ...conversationConfig });\n}\n\nexport function saveConversationModelConfig(\n conversationDir: string,\n config: Pick<AgentConfig, \"provider\" | \"model\"> & Partial<Pick<AgentConfig, \"thinkingLevel\">>,\n): void {\n if (!existsSync(conversationDir)) {\n mkdirSync(conversationDir, { recursive: true });\n }\n const settingsPath = join(conversationDir, \"settings.json\");\n const existing = loadSettingsFile(settingsPath) ?? {};\n const scopedConfig: SettingsFileConfig = {\n ...existing,\n llm: { ...existing.llm, ...config },\n };\n atomicWritePrivateFile(settingsPath, JSON.stringify(scopedConfig, null, 2));\n}\n\nexport function resolveWorkspaceDirFromArgv(args = process.argv.slice(2)): string | undefined {\n for (let i = 0; i < args.length; i++) {\n const arg = args[i];\n\n if (arg === \"--sandbox\" || arg === \"--download\" || arg === \"--state-dir\") {\n i += 1;\n continue;\n }\n\n if (arg === \"--version\" || arg === \"-v\" || arg === \"-V\" || arg === \"--onboard\") {\n continue;\n }\n\n if (\n arg.startsWith(\"--sandbox=\") ||\n arg.startsWith(\"--download=\") ||\n arg.startsWith(\"--state-dir=\")\n ) {\n continue;\n }\n\n if (!arg.startsWith(\"-\")) {\n return arg;\n }\n }\n\n return undefined;\n}\n\nexport function resolveStateDirFromArgv(args = process.argv.slice(2)): string {\n for (let i = 0; i < args.length; i++) {\n const arg = args[i];\n if (arg.startsWith(\"--state-dir=\")) {\n return resolve(arg.slice(\"--state-dir=\".length));\n }\n if (arg === \"--state-dir\") {\n return resolve(args[++i] || \"\");\n }\n }\n\n return join(homedir(), \".mama\");\n}\n\nexport function resolveSentryDsn(): string | undefined {\n const fromFile = normalizeSettingsConfig(loadSettingsFile(getSettingsPath()) ?? {});\n if (fromFile.sentryDsn) {\n return fromFile.sentryDsn;\n }\n\n return process.env.SENTRY_DSN;\n}\n\nexport function createGlobalSettingsFile(stateDir: string): string {\n const settingsPath = join(stateDir, \"settings.json\");\n if (existsSync(settingsPath)) {\n throw new Error(`Global settings already exists at ${settingsPath}`);\n }\n if (!existsSync(stateDir)) {\n mkdirSync(stateDir, { recursive: true });\n }\n atomicWritePrivateFile(settingsPath, JSON.stringify(ONBOARD_SETTINGS, null, 2));\n return settingsPath;\n}\n\n/**\n * Externally-visible base URL of the link/OAuth server, e.g.\n * `https://mama.example.com` (no trailing slash). Read from `MAMA_LINK_URL`,\n * the same env var the bot uses to build credential onboarding links.\n */\nexport function resolveLinkBaseUrl(): string | undefined {\n const raw = process.env.MAMA_LINK_URL?.trim();\n if (!raw) return undefined;\n return raw.replace(/\\/+$/, \"\");\n}\n\nfunction hasDefinedValue(values: Record<string, unknown> | undefined): boolean {\n return values !== undefined && Object.values(values).some((value) => value !== undefined);\n}\n\nfunction compactSettingsConfig(config: SettingsFileConfig): SettingsFileConfig {\n return {\n ...(hasDefinedValue(config.llm) ? { llm: config.llm } : {}),\n ...(hasDefinedValue(config.log) ? { log: config.log } : {}),\n ...(hasDefinedValue(config.sentry) ? { sentry: config.sentry } : {}),\n ...(hasDefinedValue(config.sandbox) ? { sandbox: config.sandbox } : {}),\n };\n}\n\nfunction patchSettingsConfig(\n existing: SettingsFileConfig,\n config: Partial<AgentConfig>,\n): SettingsFileConfig {\n const patched: SettingsFileConfig = {\n ...existing,\n llm: {\n ...existing.llm,\n ...(config.provider !== undefined ? { provider: config.provider } : {}),\n ...(config.model !== undefined ? { model: config.model } : {}),\n ...(config.thinkingLevel !== undefined ? { thinkingLevel: config.thinkingLevel } : {}),\n },\n log: {\n ...existing.log,\n ...(config.logFormat !== undefined ? { format: config.logFormat } : {}),\n ...(config.logLevel !== undefined ? { level: config.logLevel } : {}),\n },\n sentry: {\n ...existing.sentry,\n ...(config.sentryDsn !== undefined ? { dsn: config.sentryDsn } : {}),\n },\n sandbox: {\n ...existing.sandbox,\n ...(config.sandboxCpus !== undefined ? { cpus: config.sandboxCpus } : {}),\n ...(config.sandboxMemory !== undefined ? { memory: config.sandboxMemory } : {}),\n ...(config.sandboxBoostCpus !== undefined || config.sandboxBoostMemory !== undefined\n ? {\n boost: {\n ...existing.sandbox?.boost,\n ...(config.sandboxBoostCpus !== undefined ? { cpus: config.sandboxBoostCpus } : {}),\n ...(config.sandboxBoostMemory !== undefined\n ? { memory: config.sandboxBoostMemory }\n : {}),\n },\n }\n : {}),\n },\n };\n return compactSettingsConfig(patched);\n}\n\nexport function saveAgentConfig(config: Partial<AgentConfig>): void {\n const settingsPath = join(getStateDir(), \"settings.json\");\n\n let existing: SettingsFileConfig = ONBOARD_SETTINGS;\n if (existsSync(settingsPath)) {\n try {\n existing = loadSettingsFile(settingsPath) ?? {};\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n const message = detail.startsWith(\"Malformed settings file\")\n ? detail.replace(\"Malformed settings file\", \"Refusing to overwrite malformed settings file\")\n : detail;\n throw new Error(message);\n }\n }\n\n const merged = patchSettingsConfig(existing, config);\n\n const dir = dirname(settingsPath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n atomicWritePrivateFile(settingsPath, JSON.stringify(merged, null, 2));\n}\n"]}
package/dist/context.d.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  * - syncLogToSessionManager: Syncs messages from log.jsonl to SessionManager
10
10
  * - createMamaSettingsManager: Creates an in-memory SettingsManager for AgentSession
11
11
  */
12
- import { type SessionManager, SettingsManager } from "@mariozechner/pi-coding-agent";
12
+ import { type SessionManager, SettingsManager } from "@earendil-works/pi-coding-agent";
13
13
  /**
14
14
  * Time range for filtering log messages
15
15
  */
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EACL,KAAK,cAAc,EAEnB,eAAe,EAChB,MAAM,+BAA+B,CAAC;AAUvC;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAOD,MAAM,WAAW,sBAAsB;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAQD;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,sGAAsG;IACtG,KAAK,CAAC,EAAE,QAAQ,GAAG,WAAW,CAAC;IAC/B,uFAAuF;IACvF,MAAM,EAAE,MAAM,CAAC;IACf,6FAA6F;IAC7F,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,uBAAuB,CAC3C,cAAc,EAAE,cAAc,EAC9B,eAAe,EAAE,MAAM,EACvB,cAAc,CAAC,EAAE,MAAM,EACvB,SAAS,CAAC,EAAE,SAAS,EACrB,YAAY,CAAC,EAAE,YAAY,GAC1B,OAAO,CAAC,MAAM,CAAC,CAuIjB;AASD,wBAAgB,yBAAyB,CAAC,aAAa,EAAE,MAAM,GAAG,eAAe,CAEhF;AAED,wBAAsB,kBAAkB,CACtC,eAAe,EAAE,MAAM,EACvB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAwBxC","sourcesContent":["/**\n * Context management for mama.\n *\n * Mama uses two data sources per conversation:\n * - sessions/*.jsonl: Structured session history for agent context\n * - log.jsonl: Human-readable conversation history for grep (no tool results)\n *\n * This module provides:\n * - syncLogToSessionManager: Syncs messages from log.jsonl to SessionManager\n * - createMamaSettingsManager: Creates an in-memory SettingsManager for AgentSession\n */\n\nimport type { Message, UserMessage } from \"@mariozechner/pi-ai\";\nimport {\n type SessionManager,\n type SessionMessageEntry,\n SettingsManager,\n} from \"@mariozechner/pi-coding-agent\";\nimport { existsSync } from \"fs\";\nimport { readFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport * as log from \"./log.js\";\n\n// ============================================================================\n// Sync log.jsonl to SessionManager\n// ============================================================================\n\n/**\n * Time range for filtering log messages\n */\nexport interface TimeRange {\n start: number; // Unix timestamp in ms\n end: number;\n}\n\n/**\n * Default number of days to sync when no time range is specified\n */\nconst DEFAULT_SYNC_DAYS = 10;\n\nexport interface ConversationLogMessage {\n date?: string;\n ts?: string;\n threadTs?: string;\n user?: string;\n userName?: string;\n text?: string;\n isBot?: boolean;\n}\n\ninterface ExistingSessionMessage {\n timestamp?: number;\n rawText: string;\n normalizedText: string;\n}\n\n/**\n * Thread filter for scoping log sync to a specific thread session.\n * When provided, only messages belonging to this thread are synced,\n * preventing cross-thread context contamination.\n */\nexport interface ThreadFilter {\n /** Filter mode: a specific thread, or top-level messages only for persistent channel/chat sessions */\n scope?: \"thread\" | \"top-level\";\n /** The root message timestamp (user's original message ts, derived from sessionKey) */\n rootTs: string;\n /** The thread anchor timestamp (bot's first reply ts, used as thread_ts by Slack replies) */\n threadTs?: string;\n}\n\n/**\n * Sync user messages from log.jsonl to SessionManager.\n *\n * This ensures that messages logged while mama wasn't running (conversation chatter,\n * backfilled messages, messages while busy) are added to the LLM context.\n *\n * @param sessionManager - The SessionManager to sync to\n * @param conversationDir - Path to the conversation directory containing log.jsonl\n * @param excludeSlackTs - Current platform message ID/timestamp (will be added via prompt(), not sync)\n * @param timeRange - Optional time range to filter log entries (defaults to last 10 days)\n * @param threadFilter - Optional thread filter to scope sync to a specific thread\n * @returns Number of messages synced\n */\nexport async function syncLogToSessionManager(\n sessionManager: SessionManager,\n conversationDir: string,\n excludeSlackTs?: string,\n timeRange?: TimeRange,\n threadFilter?: ThreadFilter,\n): Promise<number> {\n // Calculate default time range (last 10 days) if not provided\n const now = Date.now();\n const defaultStart = now - DEFAULT_SYNC_DAYS * 24 * 60 * 60 * 1000;\n const range = timeRange ?? { start: defaultStart, end: now };\n const logFile = join(conversationDir, \"log.jsonl\");\n\n if (!existsSync(logFile)) return 0;\n\n // Build a list of existing session messages for dedupe.\n // Live user prompts carry a formatted timestamp in the text and use Date.now(),\n // while log.jsonl uses the platform event timestamp. We therefore need a small\n // fuzzy match window in addition to the exact timestamp/content match used for\n // already-synced log entries.\n const existingMessages: ExistingSessionMessage[] = [];\n const existingMessageKeys = new Set<string>();\n for (const entry of sessionManager.getEntries()) {\n if (entry.type !== \"message\") continue;\n const msgEntry = entry as SessionMessageEntry;\n const message = msgEntry.message as Message;\n const contentText = Array.isArray(message.content)\n ? message.content\n .filter((part): part is { type: \"text\"; text: string } => part.type === \"text\")\n .map((part) => part.text)\n .join(\"\\n\\n\")\n : typeof message.content === \"string\"\n ? message.content\n : \"\";\n existingMessages.push({\n timestamp: typeof message.timestamp === \"number\" ? message.timestamp : undefined,\n rawText: contentText,\n normalizedText: normalizeComparableUserText(contentText),\n });\n if (typeof message.timestamp === \"number\") {\n existingMessageKeys.add(`${message.timestamp}:${contentText}`);\n }\n }\n\n // Read log.jsonl and find user messages not in context\n const logContent = await readFile(logFile, \"utf-8\");\n const logLines = logContent.trim().split(\"\\n\").filter(Boolean);\n\n const newMessages: Array<{ timestamp: number; message: UserMessage }> = [];\n\n for (let lineIdx = 0; lineIdx < logLines.length; lineIdx++) {\n const line = logLines[lineIdx];\n let logMsg: ConversationLogMessage;\n try {\n logMsg = JSON.parse(line) as ConversationLogMessage;\n } catch (err) {\n log.logWarning(\n `Skipping malformed log entry at ${logFile}:${lineIdx + 1}`,\n err instanceof Error ? err.message : String(err),\n );\n continue;\n }\n\n const slackTs = logMsg.ts;\n const date = logMsg.date;\n if (!slackTs || !date) continue;\n\n // Skip the current message being processed (will be added via prompt())\n if (excludeSlackTs && slackTs === excludeSlackTs) continue;\n\n // While queued messages are being processed, newer messages may already be present\n // in log.jsonl. Do not look ahead into those future messages when building the\n // current turn's context.\n if (!isMessageAtOrBeforeCurrent(slackTs, excludeSlackTs)) continue;\n\n // Skip bot messages - added through agent flow\n if (logMsg.isBot) continue;\n\n // Thread filtering: only sync messages belonging to this session's thread\n if (threadFilter) {\n if (threadFilter.scope === \"top-level\") {\n // Persistent top-level sessions should only ingest top-level messages.\n // This avoids pulling in unrelated replies from other threads.\n if (logMsg.threadTs) {\n continue;\n }\n } else {\n if (logMsg.threadTs) {\n // Thread reply: only include if threadTs matches our thread anchor or rootTs\n if (\n logMsg.threadTs !== threadFilter.threadTs &&\n logMsg.threadTs !== threadFilter.rootTs\n ) {\n continue;\n }\n } else {\n // Top-level message: only include if it's this session's root message\n if (slackTs !== threadFilter.rootTs) {\n continue;\n }\n }\n }\n }\n\n // Build the message text as it would appear in context\n const threadContext = logMsg.threadTs ? ` [in-thread:${logMsg.threadTs}]` : \"\";\n const messageText = `[${logMsg.userName || logMsg.user || \"unknown\"}]${threadContext}: ${logMsg.text || \"\"}`;\n\n const msgTime = new Date(date).getTime() || Date.now();\n const messageKey = `${msgTime}:${messageText}`;\n if (existingMessageKeys.has(messageKey)) continue;\n if (hasExistingSessionMessage(existingMessages, msgTime, messageText)) continue;\n\n // Skip messages outside the time range\n if (msgTime < range.start || msgTime > range.end) continue;\n\n const userMessage: UserMessage = {\n role: \"user\",\n content: [{ type: \"text\", text: messageText }],\n timestamp: msgTime,\n };\n\n newMessages.push({ timestamp: msgTime, message: userMessage });\n existingMessages.push({\n timestamp: msgTime,\n rawText: messageText,\n normalizedText: normalizeComparableUserText(messageText),\n });\n existingMessageKeys.add(messageKey); // Track to avoid duplicates within this sync\n }\n\n if (newMessages.length === 0) return 0;\n\n // Sort by timestamp and add to session\n newMessages.sort((a, b) => a.timestamp - b.timestamp);\n\n for (const { message } of newMessages) {\n sessionManager.appendMessage(message);\n }\n\n return newMessages.length;\n}\n\n// ============================================================================\n// Settings manager for mama\n// ============================================================================\n\n// Mama manages model/provider config through its own config.ts / settings.json.\n// We use an in-memory SettingsManager so AgentSession has valid defaults\n// without interfering with coding-agent's global settings files.\nexport function createMamaSettingsManager(_workspaceDir: string): SettingsManager {\n return SettingsManager.inMemory();\n}\n\nexport async function findLogMessageById(\n conversationDir: string,\n messageId: string,\n): Promise<ConversationLogMessage | null> {\n const logFile = join(conversationDir, \"log.jsonl\");\n if (!existsSync(logFile)) return null;\n\n const logContent = await readFile(logFile, \"utf-8\");\n const logLines = logContent.trim().split(\"\\n\").filter(Boolean);\n\n for (let i = logLines.length - 1; i >= 0; i--) {\n let entry: ConversationLogMessage;\n try {\n entry = JSON.parse(logLines[i]) as ConversationLogMessage;\n } catch (err) {\n log.logWarning(\n `Skipping malformed log entry at ${logFile}:${i + 1}`,\n err instanceof Error ? err.message : String(err),\n );\n continue;\n }\n if (entry.ts === messageId) {\n return entry;\n }\n }\n\n return null;\n}\n\nfunction stripSlackAttachmentBlock(text: string): string {\n return text.replace(/\\n*<slack_attachments>\\n[\\s\\S]*?\\n<\\/slack_attachments>\\s*$/g, \"\");\n}\n\nfunction normalizeComparableUserText(text: string): string {\n const withoutTimestamp = text.replace(\n /^\\[[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}[+-][0-9]{2}:[0-9]{2}\\]\\s+(?=\\[[^\\]]+\\](?:\\s+\\[in-thread:[^\\]]+\\])?:\\s)/,\n \"\",\n );\n return stripSlackAttachmentBlock(withoutTimestamp).trim();\n}\n\nfunction hasExistingSessionMessage(\n existingMessages: ExistingSessionMessage[],\n timestamp: number,\n text: string,\n): boolean {\n const normalizedText = normalizeComparableUserText(text);\n return existingMessages.some((existing) => {\n if (existing.timestamp === timestamp && existing.rawText === text) {\n return true;\n }\n if (existing.normalizedText !== normalizedText || existing.timestamp === undefined) {\n return false;\n }\n return existing.timestamp >= timestamp;\n });\n}\n\nfunction isMessageAtOrBeforeCurrent(messageId: string, currentMessageId?: string): boolean {\n if (!currentMessageId) return true;\n const comparison = compareMessageIds(messageId, currentMessageId);\n return comparison === null || comparison <= 0;\n}\n\nfunction compareMessageIds(a: string, b: string): number | null {\n if (/^\\d+$/.test(a) && /^\\d+$/.test(b)) {\n const left = BigInt(a);\n const right = BigInt(b);\n return left < right ? -1 : left > right ? 1 : 0;\n }\n\n const left = Number(a);\n const right = Number(b);\n if (Number.isFinite(left) && Number.isFinite(right)) {\n return left < right ? -1 : left > right ? 1 : 0;\n }\n\n return null;\n}\n"]}
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EACL,KAAK,cAAc,EAEnB,eAAe,EAChB,MAAM,iCAAiC,CAAC;AAWzC;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAOD,MAAM,WAAW,sBAAsB;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAQD;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,sGAAsG;IACtG,KAAK,CAAC,EAAE,QAAQ,GAAG,WAAW,CAAC;IAC/B,uFAAuF;IACvF,MAAM,EAAE,MAAM,CAAC;IACf,6FAA6F;IAC7F,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,uBAAuB,CAC3C,cAAc,EAAE,cAAc,EAC9B,eAAe,EAAE,MAAM,EACvB,cAAc,CAAC,EAAE,MAAM,EACvB,SAAS,CAAC,EAAE,SAAS,EACrB,YAAY,CAAC,EAAE,YAAY,GAC1B,OAAO,CAAC,MAAM,CAAC,CAmHjB;AASD,wBAAgB,yBAAyB,CAAC,aAAa,EAAE,MAAM,GAAG,eAAe,CAEhF;AAED,wBAAsB,kBAAkB,CACtC,eAAe,EAAE,MAAM,EACvB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAwBxC","sourcesContent":["/**\n * Context management for mama.\n *\n * Mama uses two data sources per conversation:\n * - sessions/*.jsonl: Structured session history for agent context\n * - log.jsonl: Human-readable conversation history for grep (no tool results)\n *\n * This module provides:\n * - syncLogToSessionManager: Syncs messages from log.jsonl to SessionManager\n * - createMamaSettingsManager: Creates an in-memory SettingsManager for AgentSession\n */\n\nimport type { Message, UserMessage } from \"@earendil-works/pi-ai\";\nimport {\n type SessionManager,\n type SessionMessageEntry,\n SettingsManager,\n} from \"@earendil-works/pi-coding-agent\";\nimport { existsSync } from \"fs\";\nimport { readFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport { parseNewCommand } from \"./commands/new.js\";\nimport * as log from \"./log.js\";\n\n// ============================================================================\n// Sync log.jsonl to SessionManager\n// ============================================================================\n\n/**\n * Time range for filtering log messages\n */\nexport interface TimeRange {\n start: number; // Unix timestamp in ms\n end: number;\n}\n\n/**\n * Default number of days to sync when no time range is specified\n */\nconst DEFAULT_SYNC_DAYS = 10;\n\nexport interface ConversationLogMessage {\n date?: string;\n ts?: string;\n threadTs?: string;\n user?: string;\n userName?: string;\n text?: string;\n isBot?: boolean;\n}\n\ninterface ExistingSessionMessage {\n timestamp?: number;\n rawText: string;\n normalizedText: string;\n}\n\n/**\n * Thread filter for scoping log sync to a specific thread session.\n * When provided, only messages belonging to this thread are synced,\n * preventing cross-thread context contamination.\n */\nexport interface ThreadFilter {\n /** Filter mode: a specific thread, or top-level messages only for persistent channel/chat sessions */\n scope?: \"thread\" | \"top-level\";\n /** The root message timestamp (user's original message ts, derived from sessionKey) */\n rootTs: string;\n /** The thread anchor timestamp (bot's first reply ts, used as thread_ts by Slack replies) */\n threadTs?: string;\n}\n\n/**\n * Sync user messages from log.jsonl to SessionManager.\n *\n * This ensures that messages logged while mama wasn't running (conversation chatter,\n * backfilled messages, messages while busy) are added to the LLM context.\n *\n * @param sessionManager - The SessionManager to sync to\n * @param conversationDir - Path to the conversation directory containing log.jsonl\n * @param excludeSlackTs - Current platform message ID/timestamp (will be added via prompt(), not sync)\n * @param timeRange - Optional time range to filter log entries (defaults to last 10 days)\n * @param threadFilter - Optional thread filter to scope sync to a specific thread\n * @returns Number of messages synced\n */\nexport async function syncLogToSessionManager(\n sessionManager: SessionManager,\n conversationDir: string,\n excludeSlackTs?: string,\n timeRange?: TimeRange,\n threadFilter?: ThreadFilter,\n): Promise<number> {\n // Calculate default time range (last 10 days) if not provided\n const now = Date.now();\n const defaultStart = now - DEFAULT_SYNC_DAYS * 24 * 60 * 60 * 1000;\n const range = timeRange ?? { start: defaultStart, end: now };\n const logFile = join(conversationDir, \"log.jsonl\");\n\n if (!existsSync(logFile)) return 0;\n\n // Build a list of existing session messages for dedupe.\n // Live user prompts carry a formatted timestamp in the text and use Date.now(),\n // while log.jsonl uses the platform event timestamp. We therefore need a small\n // fuzzy match window in addition to the exact timestamp/content match used for\n // already-synced log entries.\n const existingMessages: ExistingSessionMessage[] = [];\n const existingMessageKeys = new Set<string>();\n for (const entry of sessionManager.getEntries()) {\n if (entry.type !== \"message\") continue;\n const msgEntry = entry as SessionMessageEntry;\n const message = msgEntry.message as Message;\n const contentText = Array.isArray(message.content)\n ? message.content\n .filter((part): part is { type: \"text\"; text: string } => part.type === \"text\")\n .map((part) => part.text)\n .join(\"\\n\\n\")\n : typeof message.content === \"string\"\n ? message.content\n : \"\";\n existingMessages.push({\n timestamp: typeof message.timestamp === \"number\" ? message.timestamp : undefined,\n rawText: contentText,\n normalizedText: normalizeComparableUserText(contentText),\n });\n if (typeof message.timestamp === \"number\") {\n existingMessageKeys.add(`${message.timestamp}:${contentText}`);\n }\n }\n\n // Read log.jsonl and find user messages not in context\n const logContent = await readFile(logFile, \"utf-8\");\n const logLines = logContent.trim().split(\"\\n\").filter(Boolean);\n const logEntries: ConversationLogMessage[] = [];\n\n for (let lineIdx = 0; lineIdx < logLines.length; lineIdx++) {\n try {\n logEntries.push(JSON.parse(logLines[lineIdx]) as ConversationLogMessage);\n } catch (err) {\n log.logWarning(\n `Skipping malformed log entry at ${logFile}:${lineIdx + 1}`,\n err instanceof Error ? err.message : String(err),\n );\n }\n }\n\n const resetCutoff = findLatestResetCutoff(logEntries, excludeSlackTs, threadFilter);\n const newMessages: Array<{ timestamp: number; message: UserMessage }> = [];\n\n for (const logMsg of logEntries) {\n const slackTs = logMsg.ts;\n const date = logMsg.date;\n if (!slackTs || !date) continue;\n\n // Skip the current message being processed (will be added via prompt())\n if (excludeSlackTs && slackTs === excludeSlackTs) continue;\n\n // While queued messages are being processed, newer messages may already be present\n // in log.jsonl. Do not look ahead into those future messages when building the\n // current turn's context.\n if (!isMessageAtOrBeforeCurrent(slackTs, excludeSlackTs)) continue;\n\n // Skip mama's own responses - added through agent flow. Keep external app/bot\n // messages (for example Sentry alerts) in context so mama can reason about them.\n if (logMsg.isBot && logMsg.user === \"bot\") continue;\n\n const msgTime = new Date(date).getTime() || Date.now();\n if (resetCutoff !== null && msgTime <= resetCutoff) continue;\n\n if (!isLogMessageInThreadScope(logMsg, threadFilter)) continue;\n\n // Build the message text as it would appear in context\n const threadContext = logMsg.threadTs ? ` [in-thread:${logMsg.threadTs}]` : \"\";\n const messageText = `[${logMsg.userName || logMsg.user || \"unknown\"}]${threadContext}: ${logMsg.text || \"\"}`;\n\n const messageKey = `${msgTime}:${messageText}`;\n if (existingMessageKeys.has(messageKey)) continue;\n if (hasExistingSessionMessage(existingMessages, msgTime, messageText)) continue;\n\n // Skip messages outside the time range\n if (msgTime < range.start || msgTime > range.end) continue;\n\n const userMessage: UserMessage = {\n role: \"user\",\n content: [{ type: \"text\", text: messageText }],\n timestamp: msgTime,\n };\n\n newMessages.push({ timestamp: msgTime, message: userMessage });\n existingMessages.push({\n timestamp: msgTime,\n rawText: messageText,\n normalizedText: normalizeComparableUserText(messageText),\n });\n existingMessageKeys.add(messageKey); // Track to avoid duplicates within this sync\n }\n\n if (newMessages.length === 0) return 0;\n\n // Sort by timestamp and add to session\n newMessages.sort((a, b) => a.timestamp - b.timestamp);\n\n for (const { message } of newMessages) {\n sessionManager.appendMessage(message);\n }\n\n return newMessages.length;\n}\n\n// ============================================================================\n// Settings manager for mama\n// ============================================================================\n\n// Mama manages model/provider config through its own config.ts / settings.json.\n// We use an in-memory SettingsManager so AgentSession has valid defaults\n// without interfering with coding-agent's global settings files.\nexport function createMamaSettingsManager(_workspaceDir: string): SettingsManager {\n return SettingsManager.inMemory();\n}\n\nexport async function findLogMessageById(\n conversationDir: string,\n messageId: string,\n): Promise<ConversationLogMessage | null> {\n const logFile = join(conversationDir, \"log.jsonl\");\n if (!existsSync(logFile)) return null;\n\n const logContent = await readFile(logFile, \"utf-8\");\n const logLines = logContent.trim().split(\"\\n\").filter(Boolean);\n\n for (let i = logLines.length - 1; i >= 0; i--) {\n let entry: ConversationLogMessage;\n try {\n entry = JSON.parse(logLines[i]) as ConversationLogMessage;\n } catch (err) {\n log.logWarning(\n `Skipping malformed log entry at ${logFile}:${i + 1}`,\n err instanceof Error ? err.message : String(err),\n );\n continue;\n }\n if (entry.ts === messageId) {\n return entry;\n }\n }\n\n return null;\n}\n\nfunction findLatestResetCutoff(\n entries: ConversationLogMessage[],\n currentMessageId?: string,\n threadFilter?: ThreadFilter,\n): number | null {\n let cutoff: number | null = null;\n\n for (const entry of entries) {\n if (!entry.ts || !entry.date) continue;\n if (!isMessageAtOrBeforeCurrent(entry.ts, currentMessageId)) continue;\n if (!isResetCommandLogMessage(entry)) continue;\n if (!isLogMessageInThreadScope(entry, threadFilter)) continue;\n\n const timestamp = new Date(entry.date).getTime();\n if (!Number.isFinite(timestamp)) continue;\n cutoff = cutoff === null ? timestamp : Math.max(cutoff, timestamp);\n }\n\n return cutoff;\n}\n\nfunction isResetCommandLogMessage(entry: ConversationLogMessage): boolean {\n if (entry.isBot) return false;\n return parseNewCommand(entry.text ?? \"\") !== null;\n}\n\nfunction isLogMessageInThreadScope(\n entry: ConversationLogMessage,\n threadFilter?: ThreadFilter,\n): boolean {\n if (!threadFilter) return true;\n if (threadFilter.scope === \"top-level\") return !entry.threadTs;\n if (entry.threadTs) {\n return entry.threadTs === threadFilter.threadTs || entry.threadTs === threadFilter.rootTs;\n }\n return entry.ts === threadFilter.rootTs;\n}\n\nfunction stripSlackAttachmentBlock(text: string): string {\n return text.replace(/\\n*<slack_attachments>\\n[\\s\\S]*?\\n<\\/slack_attachments>\\s*$/g, \"\");\n}\n\nfunction normalizeComparableUserText(text: string): string {\n const withoutTimestamp = text.replace(\n /^\\[[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}[+-][0-9]{2}:[0-9]{2}\\]\\s+(?=\\[[^\\]]+\\](?:\\s+\\[in-thread:[^\\]]+\\])?:\\s)/,\n \"\",\n );\n return stripSlackAttachmentBlock(withoutTimestamp).trim();\n}\n\nfunction hasExistingSessionMessage(\n existingMessages: ExistingSessionMessage[],\n timestamp: number,\n text: string,\n): boolean {\n const normalizedText = normalizeComparableUserText(text);\n return existingMessages.some((existing) => {\n if (existing.timestamp === timestamp && existing.rawText === text) {\n return true;\n }\n if (existing.normalizedText !== normalizedText || existing.timestamp === undefined) {\n return false;\n }\n return existing.timestamp >= timestamp;\n });\n}\n\nfunction isMessageAtOrBeforeCurrent(messageId: string, currentMessageId?: string): boolean {\n if (!currentMessageId) return true;\n const comparison = compareMessageIds(messageId, currentMessageId);\n return comparison === null || comparison <= 0;\n}\n\nfunction compareMessageIds(a: string, b: string): number | null {\n if (/^\\d+$/.test(a) && /^\\d+$/.test(b)) {\n const left = BigInt(a);\n const right = BigInt(b);\n return left < right ? -1 : left > right ? 1 : 0;\n }\n\n const left = Number(a);\n const right = Number(b);\n if (Number.isFinite(left) && Number.isFinite(right)) {\n return left < right ? -1 : left > right ? 1 : 0;\n }\n\n return null;\n}\n"]}