@geminixiang/mama 0.2.0-beta.23 → 0.2.0-beta.25

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 (95) hide show
  1. package/dist/adapters/discord/bot.d.ts.map +1 -1
  2. package/dist/adapters/discord/bot.js +1 -1
  3. package/dist/adapters/discord/bot.js.map +1 -1
  4. package/dist/adapters/discord/context.d.ts.map +1 -1
  5. package/dist/adapters/discord/context.js +1 -1
  6. package/dist/adapters/discord/context.js.map +1 -1
  7. package/dist/adapters/slack/bot.d.ts.map +1 -1
  8. package/dist/adapters/slack/bot.js +1 -3
  9. package/dist/adapters/slack/bot.js.map +1 -1
  10. package/dist/adapters/slack/branch-manager.d.ts +1 -1
  11. package/dist/adapters/slack/branch-manager.d.ts.map +1 -1
  12. package/dist/adapters/slack/branch-manager.js +1 -1
  13. package/dist/adapters/slack/branch-manager.js.map +1 -1
  14. package/dist/adapters/slack/session.d.ts.map +1 -1
  15. package/dist/adapters/slack/session.js +1 -1
  16. package/dist/adapters/slack/session.js.map +1 -1
  17. package/dist/adapters/telegram/bot.d.ts +2 -1
  18. package/dist/adapters/telegram/bot.d.ts.map +1 -1
  19. package/dist/adapters/telegram/bot.js +5 -5
  20. package/dist/adapters/telegram/bot.js.map +1 -1
  21. package/dist/agent.d.ts +1 -1
  22. package/dist/agent.d.ts.map +1 -1
  23. package/dist/agent.js +3 -5
  24. package/dist/agent.js.map +1 -1
  25. package/dist/commands/auto-reply.d.ts +0 -11
  26. package/dist/commands/auto-reply.d.ts.map +1 -1
  27. package/dist/commands/auto-reply.js +14 -7
  28. package/dist/commands/auto-reply.js.map +1 -1
  29. package/dist/commands/model.d.ts.map +1 -1
  30. package/dist/commands/model.js +9 -11
  31. package/dist/commands/model.js.map +1 -1
  32. package/dist/commands/new.d.ts +0 -4
  33. package/dist/commands/new.d.ts.map +1 -1
  34. package/dist/commands/new.js +5 -9
  35. package/dist/commands/new.js.map +1 -1
  36. package/dist/commands/parse.d.ts +7 -0
  37. package/dist/commands/parse.d.ts.map +1 -0
  38. package/dist/commands/parse.js +17 -0
  39. package/dist/commands/parse.js.map +1 -0
  40. package/dist/commands/sandbox.d.ts.map +1 -1
  41. package/dist/commands/sandbox.js +8 -13
  42. package/dist/commands/sandbox.js.map +1 -1
  43. package/dist/conversation-history.d.ts.map +1 -1
  44. package/dist/conversation-history.js +28 -5
  45. package/dist/conversation-history.js.map +1 -1
  46. package/dist/download.d.ts.map +1 -1
  47. package/dist/download.js +1 -1
  48. package/dist/download.js.map +1 -1
  49. package/dist/events.d.ts.map +1 -1
  50. package/dist/events.js +1 -1
  51. package/dist/events.js.map +1 -1
  52. package/dist/index.d.ts +3 -0
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +3 -0
  55. package/dist/index.js.map +1 -1
  56. package/dist/login/index.d.ts.map +1 -1
  57. package/dist/login/index.js +11 -14
  58. package/dist/login/index.js.map +1 -1
  59. package/dist/runtime/session-runtime.d.ts.map +1 -1
  60. package/dist/runtime/session-runtime.js +1 -1
  61. package/dist/runtime/session-runtime.js.map +1 -1
  62. package/dist/session-view/command.d.ts.map +1 -1
  63. package/dist/session-view/command.js +4 -8
  64. package/dist/session-view/command.js.map +1 -1
  65. package/dist/session-view/portal.d.ts.map +1 -1
  66. package/dist/session-view/portal.js +1 -1
  67. package/dist/session-view/portal.js.map +1 -1
  68. package/dist/session-view/service.d.ts.map +1 -1
  69. package/dist/session-view/service.js +2 -2
  70. package/dist/session-view/service.js.map +1 -1
  71. package/dist/{session-metadata.d.ts → sessions/metadata.d.ts} +1 -1
  72. package/dist/sessions/metadata.d.ts.map +1 -0
  73. package/dist/{session-metadata.js → sessions/metadata.js} +1 -1
  74. package/dist/sessions/metadata.js.map +1 -0
  75. package/dist/{session-policy.d.ts → sessions/policy.d.ts} +2 -2
  76. package/dist/sessions/policy.d.ts.map +1 -0
  77. package/dist/{session-policy.js → sessions/policy.js} +1 -1
  78. package/dist/sessions/policy.js.map +1 -0
  79. package/dist/{session-store.d.ts → sessions/store.d.ts} +1 -1
  80. package/dist/sessions/store.d.ts.map +1 -0
  81. package/dist/{session-store.js → sessions/store.js} +4 -4
  82. package/dist/sessions/store.js.map +1 -0
  83. package/dist/tools/index.d.ts +2 -1
  84. package/dist/tools/index.d.ts.map +1 -1
  85. package/dist/tools/index.js.map +1 -1
  86. package/dist/trigger.d.ts.map +1 -1
  87. package/dist/trigger.js +2 -2
  88. package/dist/trigger.js.map +1 -1
  89. package/package.json +3 -3
  90. package/dist/session-metadata.d.ts.map +0 -1
  91. package/dist/session-metadata.js.map +0 -1
  92. package/dist/session-policy.d.ts.map +0 -1
  93. package/dist/session-policy.js.map +0 -1
  94. package/dist/session-store.d.ts.map +0 -1
  95. package/dist/session-store.js.map +0 -1
@@ -2,6 +2,7 @@ import { AuthStorage, ModelRegistry } from "@earendil-works/pi-coding-agent";
2
2
  import { homedir } from "os";
3
3
  import { join } from "path";
4
4
  import { loadAgentConfigForConversation, saveConversationModelConfig } from "../config.js";
5
+ import { matchCommand } from "./parse.js";
5
6
  import { replyDiagnosticWithContext } from "./utils.js";
6
7
  const PI_AI_THINKING_LEVELS = [
7
8
  "minimal",
@@ -11,26 +12,23 @@ const PI_AI_THINKING_LEVELS = [
11
12
  "xhigh",
12
13
  ];
13
14
  const THINKING_LEVELS = new Set(["off", ...PI_AI_THINKING_LEVELS]);
15
+ const MODEL_COMMANDS = ["model", "/model", "/pi-model"];
14
16
  export function parseModelCommand(text) {
15
- const tokens = text.trim().split(/\s+/).filter(Boolean);
16
- if (tokens.length === 0)
17
+ const matched = matchCommand(text, MODEL_COMMANDS);
18
+ if (!matched)
17
19
  return null;
18
- const command = tokens[0].toLowerCase();
19
- if (command !== "model" && command !== "/model" && command !== "/pi-model") {
20
- return null;
21
- }
22
- if (tokens.length === 1) {
23
- return { command: command };
20
+ if (matched.args.length === 0) {
21
+ return { command: matched.command };
24
22
  }
25
- const spec = tokens[1];
23
+ const spec = matched.args[0];
26
24
  const slash = spec.indexOf("/");
27
25
  if (slash <= 0 || slash === spec.length - 1) {
28
- return { command: command };
26
+ return { command: matched.command };
29
27
  }
30
28
  const modelSpec = spec.slice(slash + 1);
31
29
  const parsedModel = parseModelThinkingLevel(modelSpec);
32
30
  return {
33
- command: command,
31
+ command: matched.command,
34
32
  provider: spec.slice(0, slash),
35
33
  model: parsedModel.model,
36
34
  thinkingLevel: parsedModel.thinkingLevel,
@@ -1 +1 @@
1
- {"version":3,"file":"model.js","sourceRoot":"","sources":["../../src/commands/model.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ThinkingLevel } from \"@earendil-works/pi-agent-core\";\nimport type { ThinkingLevel as PiAiThinkingLevel } from \"@earendil-works/pi-ai\";\nimport { AuthStorage, ModelRegistry } from \"@earendil-works/pi-coding-agent\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\nimport { loadAgentConfigForConversation, saveConversationModelConfig } from \"../config.js\";\nimport type { CommandContext, CommandHandler } from \"./types.js\";\nimport { replyDiagnosticWithContext } from \"./utils.js\";\n\nconst PI_AI_THINKING_LEVELS = [\n \"minimal\",\n \"low\",\n \"medium\",\n \"high\",\n \"xhigh\",\n] satisfies PiAiThinkingLevel[];\nconst THINKING_LEVELS = new Set<ThinkingLevel>([\"off\", ...PI_AI_THINKING_LEVELS]);\n\nexport interface ParsedModelCommand {\n command: \"model\" | \"/model\" | \"/pi-model\";\n provider?: string;\n model?: string;\n thinkingLevel?: ThinkingLevel;\n}\n\nexport function parseModelCommand(text: string): ParsedModelCommand | null {\n const tokens = text.trim().split(/\\s+/).filter(Boolean);\n if (tokens.length === 0) return null;\n\n const command = tokens[0].toLowerCase();\n if (command !== \"model\" && command !== \"/model\" && command !== \"/pi-model\") {\n return null;\n }\n\n if (tokens.length === 1) {\n return { command: command as ParsedModelCommand[\"command\"] };\n }\n\n const spec = tokens[1];\n const slash = spec.indexOf(\"/\");\n if (slash <= 0 || slash === spec.length - 1) {\n return { command: command as ParsedModelCommand[\"command\"] };\n }\n\n const modelSpec = spec.slice(slash + 1);\n const parsedModel = parseModelThinkingLevel(modelSpec);\n\n return {\n command: command as ParsedModelCommand[\"command\"],\n provider: spec.slice(0, slash),\n model: parsedModel.model,\n thinkingLevel: parsedModel.thinkingLevel,\n };\n}\n\nfunction parseModelThinkingLevel(modelSpec: string): {\n model: string;\n thinkingLevel?: ThinkingLevel;\n} {\n const colon = modelSpec.lastIndexOf(\":\");\n if (colon <= 0 || colon === modelSpec.length - 1) {\n return { model: modelSpec };\n }\n\n const suffix = modelSpec.slice(colon + 1);\n if (!THINKING_LEVELS.has(suffix as ThinkingLevel)) {\n return { model: modelSpec };\n }\n\n return {\n model: modelSpec.slice(0, colon),\n thinkingLevel: suffix as ThinkingLevel,\n };\n}\n\nfunction formatModelSpec(provider: string, model: string, thinkingLevel?: ThinkingLevel): string {\n return `${provider}/${model}${thinkingLevel ? `:${thinkingLevel}` : \"\"}`;\n}\n\nfunction formatModelCommandSummary(lines: string[]): string {\n return [\"_Model_\", ...lines].join(\"\\n\");\n}\n\nexport class ModelCommandHandler implements CommandHandler {\n async tryHandle(context: CommandContext): Promise<boolean> {\n const parsed = parseModelCommand(context.commandText);\n if (!parsed) return false;\n\n const conversationDir = join(context.services.workingDir, context.conversationId);\n if (!parsed.provider || !parsed.model) {\n const current = loadAgentConfigForConversation(conversationDir);\n await replyDiagnosticWithContext(\n context.responseCtx,\n formatModelCommandSummary([\n `Current: \\`${formatModelSpec(current.provider, current.model, current.thinkingLevel)}\\``,\n \"\",\n \"Usage: `/pi-model provider/model[:thinking]`\",\n \"Example: `/pi-model anthropic/claude-sonnet-4-6:off`\",\n ]),\n { style: \"muted\" },\n );\n return true;\n }\n\n if (!this.isKnownModel(parsed.provider, parsed.model)) {\n await replyDiagnosticWithContext(\n context.responseCtx,\n formatModelCommandSummary([\n `找不到模型:\\`${formatModelSpec(parsed.provider, parsed.model, parsed.thinkingLevel)}\\``,\n \"請確認 provider/model 名稱,或先在 pi models.json 註冊自訂模型。\",\n ]),\n { style: \"muted\" },\n );\n return true;\n }\n\n if (!context.services.runtime) {\n await replyDiagnosticWithContext(\n context.responseCtx,\n formatModelCommandSummary([\n \"Model command is not configured correctly on the server. Please try again later.\",\n ]),\n { style: \"muted\" },\n );\n return true;\n }\n\n const switched = context.services.runtime.switchConversationModel(\n context.conversationId,\n parsed.provider,\n parsed.model,\n );\n if (!switched) {\n await replyDiagnosticWithContext(\n context.responseCtx,\n formatModelCommandSummary([\n \"目前這個 conversation 有執行中的工作,請等它完成或先 `/stop` 後再切換模型。\",\n ]),\n { style: \"muted\" },\n );\n return true;\n }\n\n saveConversationModelConfig(conversationDir, {\n provider: parsed.provider,\n model: parsed.model,\n ...(parsed.thinkingLevel ? { thinkingLevel: parsed.thinkingLevel } : {}),\n });\n\n await replyDiagnosticWithContext(\n context.responseCtx,\n formatModelCommandSummary([\n `Switched: \\`${formatModelSpec(parsed.provider, parsed.model, parsed.thinkingLevel)}\\``,\n \"下一則訊息會使用新模型。\",\n ]),\n { style: \"muted\" },\n );\n return true;\n }\n\n private isKnownModel(provider: string, model: string): boolean {\n const authStorage = AuthStorage.create(join(homedir(), \".pi\", \"mama\", \"auth.json\"));\n const registry = ModelRegistry.create(authStorage);\n return registry.find(provider, model) !== undefined;\n }\n}\n"]}
1
+ {"version":3,"file":"model.js","sourceRoot":"","sources":["../../src/commands/model.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ThinkingLevel } from \"@earendil-works/pi-agent-core\";\nimport type { ThinkingLevel as PiAiThinkingLevel } from \"@earendil-works/pi-ai\";\nimport { AuthStorage, ModelRegistry } from \"@earendil-works/pi-coding-agent\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\nimport { loadAgentConfigForConversation, saveConversationModelConfig } from \"../config.js\";\nimport { matchCommand } from \"./parse.js\";\nimport type { CommandContext, CommandHandler } from \"./types.js\";\nimport { replyDiagnosticWithContext } from \"./utils.js\";\n\nconst PI_AI_THINKING_LEVELS = [\n \"minimal\",\n \"low\",\n \"medium\",\n \"high\",\n \"xhigh\",\n] satisfies PiAiThinkingLevel[];\nconst THINKING_LEVELS = new Set<ThinkingLevel>([\"off\", ...PI_AI_THINKING_LEVELS]);\n\nexport interface ParsedModelCommand {\n command: \"model\" | \"/model\" | \"/pi-model\";\n provider?: string;\n model?: string;\n thinkingLevel?: ThinkingLevel;\n}\n\nconst MODEL_COMMANDS = [\"model\", \"/model\", \"/pi-model\"] as const;\n\nexport function parseModelCommand(text: string): ParsedModelCommand | null {\n const matched = matchCommand(text, MODEL_COMMANDS);\n if (!matched) return null;\n\n if (matched.args.length === 0) {\n return { command: matched.command };\n }\n\n const spec = matched.args[0];\n const slash = spec.indexOf(\"/\");\n if (slash <= 0 || slash === spec.length - 1) {\n return { command: matched.command };\n }\n\n const modelSpec = spec.slice(slash + 1);\n const parsedModel = parseModelThinkingLevel(modelSpec);\n\n return {\n command: matched.command,\n provider: spec.slice(0, slash),\n model: parsedModel.model,\n thinkingLevel: parsedModel.thinkingLevel,\n };\n}\n\nfunction parseModelThinkingLevel(modelSpec: string): {\n model: string;\n thinkingLevel?: ThinkingLevel;\n} {\n const colon = modelSpec.lastIndexOf(\":\");\n if (colon <= 0 || colon === modelSpec.length - 1) {\n return { model: modelSpec };\n }\n\n const suffix = modelSpec.slice(colon + 1);\n if (!THINKING_LEVELS.has(suffix as ThinkingLevel)) {\n return { model: modelSpec };\n }\n\n return {\n model: modelSpec.slice(0, colon),\n thinkingLevel: suffix as ThinkingLevel,\n };\n}\n\nfunction formatModelSpec(provider: string, model: string, thinkingLevel?: ThinkingLevel): string {\n return `${provider}/${model}${thinkingLevel ? `:${thinkingLevel}` : \"\"}`;\n}\n\nfunction formatModelCommandSummary(lines: string[]): string {\n return [\"_Model_\", ...lines].join(\"\\n\");\n}\n\nexport class ModelCommandHandler implements CommandHandler {\n async tryHandle(context: CommandContext): Promise<boolean> {\n const parsed = parseModelCommand(context.commandText);\n if (!parsed) return false;\n\n const conversationDir = join(context.services.workingDir, context.conversationId);\n if (!parsed.provider || !parsed.model) {\n const current = loadAgentConfigForConversation(conversationDir);\n await replyDiagnosticWithContext(\n context.responseCtx,\n formatModelCommandSummary([\n `Current: \\`${formatModelSpec(current.provider, current.model, current.thinkingLevel)}\\``,\n \"\",\n \"Usage: `/pi-model provider/model[:thinking]`\",\n \"Example: `/pi-model anthropic/claude-sonnet-4-6:off`\",\n ]),\n { style: \"muted\" },\n );\n return true;\n }\n\n if (!this.isKnownModel(parsed.provider, parsed.model)) {\n await replyDiagnosticWithContext(\n context.responseCtx,\n formatModelCommandSummary([\n `找不到模型:\\`${formatModelSpec(parsed.provider, parsed.model, parsed.thinkingLevel)}\\``,\n \"請確認 provider/model 名稱,或先在 pi models.json 註冊自訂模型。\",\n ]),\n { style: \"muted\" },\n );\n return true;\n }\n\n if (!context.services.runtime) {\n await replyDiagnosticWithContext(\n context.responseCtx,\n formatModelCommandSummary([\n \"Model command is not configured correctly on the server. Please try again later.\",\n ]),\n { style: \"muted\" },\n );\n return true;\n }\n\n const switched = context.services.runtime.switchConversationModel(\n context.conversationId,\n parsed.provider,\n parsed.model,\n );\n if (!switched) {\n await replyDiagnosticWithContext(\n context.responseCtx,\n formatModelCommandSummary([\n \"目前這個 conversation 有執行中的工作,請等它完成或先 `/stop` 後再切換模型。\",\n ]),\n { style: \"muted\" },\n );\n return true;\n }\n\n saveConversationModelConfig(conversationDir, {\n provider: parsed.provider,\n model: parsed.model,\n ...(parsed.thinkingLevel ? { thinkingLevel: parsed.thinkingLevel } : {}),\n });\n\n await replyDiagnosticWithContext(\n context.responseCtx,\n formatModelCommandSummary([\n `Switched: \\`${formatModelSpec(parsed.provider, parsed.model, parsed.thinkingLevel)}\\``,\n \"下一則訊息會使用新模型。\",\n ]),\n { style: \"muted\" },\n );\n return true;\n }\n\n private isKnownModel(provider: string, model: string): boolean {\n const authStorage = AuthStorage.create(join(homedir(), \".pi\", \"mama\", \"auth.json\"));\n const registry = ModelRegistry.create(authStorage);\n return registry.find(provider, model) !== undefined;\n }\n}\n"]}
@@ -1,8 +1,4 @@
1
1
  import type { CommandContext, CommandHandler } from "./types.js";
2
- export interface ParsedNewCommand {
3
- command: "new" | "/new" | "/pi-new";
4
- }
5
- export declare function parseNewCommand(text: string): ParsedNewCommand | null;
6
2
  export declare class NewCommandHandler implements CommandHandler {
7
3
  tryHandle(context: CommandContext): Promise<boolean>;
8
4
  }
@@ -1 +1 @@
1
- {"version":3,"file":"new.d.ts","sourceRoot":"","sources":["../../src/commands/new.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAGjE,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,KAAK,GAAG,MAAM,GAAG,SAAS,CAAC;CACrC;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAUrE;AAED,qBAAa,iBAAkB,YAAW,cAAc;IAChD,SAAS,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC,CAyBzD;CACF","sourcesContent":["import type { CommandContext, CommandHandler } from \"./types.js\";\nimport { replyWithContext } from \"./utils.js\";\n\nexport interface ParsedNewCommand {\n command: \"new\" | \"/new\" | \"/pi-new\";\n}\n\nexport function parseNewCommand(text: string): ParsedNewCommand | null {\n const tokens = text.trim().split(/\\s+/).filter(Boolean);\n if (tokens.length === 0) return null;\n\n const command = tokens[0].toLowerCase();\n if (command !== \"new\" && command !== \"/new\" && command !== \"/pi-new\") {\n return null;\n }\n\n return { command: command as ParsedNewCommand[\"command\"] };\n}\n\nexport class NewCommandHandler implements CommandHandler {\n async tryHandle(context: CommandContext): Promise<boolean> {\n if (!parseNewCommand(context.commandText)) return false;\n\n if (!context.privateConversation) {\n await replyWithContext(\n context.responseCtx,\n \"為了避免誤清除共享上下文,`/new` 目前只能在與機器人的私訊 / DM 中使用。\",\n );\n return true;\n }\n\n if (!context.services.runtime) {\n await replyWithContext(\n context.responseCtx,\n \"New command is not configured correctly on the server. Please try again later.\",\n );\n return true;\n }\n\n await context.services.runtime.handleNewCommand(\n context.sessionKey,\n context.conversationId,\n context.bot,\n );\n return true;\n }\n}\n"]}
1
+ {"version":3,"file":"new.d.ts","sourceRoot":"","sources":["../../src/commands/new.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAcjE,qBAAa,iBAAkB,YAAW,cAAc;IAChD,SAAS,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC,CAyBzD;CACF","sourcesContent":["import { matchCommand } from \"./parse.js\";\nimport type { CommandContext, CommandHandler } from \"./types.js\";\nimport { replyWithContext } from \"./utils.js\";\n\ntype ParsedNewCommand = {\n command: \"new\" | \"/new\" | \"/pi-new\";\n};\n\nconst NEW_COMMANDS = [\"new\", \"/new\", \"/pi-new\"] as const;\n\nfunction parseNewCommand(text: string): ParsedNewCommand | null {\n const matched = matchCommand(text, NEW_COMMANDS);\n return matched ? { command: matched.command } : null;\n}\n\nexport class NewCommandHandler implements CommandHandler {\n async tryHandle(context: CommandContext): Promise<boolean> {\n if (!parseNewCommand(context.commandText)) return false;\n\n if (!context.privateConversation) {\n await replyWithContext(\n context.responseCtx,\n \"為了避免誤清除共享上下文,`/new` 目前只能在與機器人的私訊 / DM 中使用。\",\n );\n return true;\n }\n\n if (!context.services.runtime) {\n await replyWithContext(\n context.responseCtx,\n \"New command is not configured correctly on the server. Please try again later.\",\n );\n return true;\n }\n\n await context.services.runtime.handleNewCommand(\n context.sessionKey,\n context.conversationId,\n context.bot,\n );\n return true;\n }\n}\n"]}
@@ -1,13 +1,9 @@
1
+ import { matchCommand } from "./parse.js";
1
2
  import { replyWithContext } from "./utils.js";
2
- export function parseNewCommand(text) {
3
- const tokens = text.trim().split(/\s+/).filter(Boolean);
4
- if (tokens.length === 0)
5
- return null;
6
- const command = tokens[0].toLowerCase();
7
- if (command !== "new" && command !== "/new" && command !== "/pi-new") {
8
- return null;
9
- }
10
- return { command: command };
3
+ const NEW_COMMANDS = ["new", "/new", "/pi-new"];
4
+ function parseNewCommand(text) {
5
+ const matched = matchCommand(text, NEW_COMMANDS);
6
+ return matched ? { command: matched.command } : null;
11
7
  }
12
8
  export class NewCommandHandler {
13
9
  async tryHandle(context) {
@@ -1 +1 @@
1
- {"version":3,"file":"new.js","sourceRoot":"","sources":["../../src/commands/new.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAM9C,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACxD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAErC,MAAM,OAAO,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;IACxC,IAAI,OAAO,KAAK,KAAK,IAAI,OAAO,KAAK,MAAM,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QACrE,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,OAAsC,EAAE,CAAC;AAC7D,CAAC;AAED,MAAM,OAAO,iBAAiB;IAC5B,KAAK,CAAC,SAAS,CAAC,OAAuB;QACrC,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,WAAW,CAAC;YAAE,OAAO,KAAK,CAAC;QAExD,IAAI,CAAC,OAAO,CAAC,mBAAmB,EAAE,CAAC;YACjC,MAAM,gBAAgB,CACpB,OAAO,CAAC,WAAW,EACnB,4CAA4C,CAC7C,CAAC;YACF,OAAO,IAAI,CAAC;QACd,CAAC;QAED,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;YAC9B,MAAM,gBAAgB,CACpB,OAAO,CAAC,WAAW,EACnB,gFAAgF,CACjF,CAAC;YACF,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,gBAAgB,CAC7C,OAAO,CAAC,UAAU,EAClB,OAAO,CAAC,cAAc,EACtB,OAAO,CAAC,GAAG,CACZ,CAAC;QACF,OAAO,IAAI,CAAC;IACd,CAAC;CACF","sourcesContent":["import type { CommandContext, CommandHandler } from \"./types.js\";\nimport { replyWithContext } from \"./utils.js\";\n\nexport interface ParsedNewCommand {\n command: \"new\" | \"/new\" | \"/pi-new\";\n}\n\nexport function parseNewCommand(text: string): ParsedNewCommand | null {\n const tokens = text.trim().split(/\\s+/).filter(Boolean);\n if (tokens.length === 0) return null;\n\n const command = tokens[0].toLowerCase();\n if (command !== \"new\" && command !== \"/new\" && command !== \"/pi-new\") {\n return null;\n }\n\n return { command: command as ParsedNewCommand[\"command\"] };\n}\n\nexport class NewCommandHandler implements CommandHandler {\n async tryHandle(context: CommandContext): Promise<boolean> {\n if (!parseNewCommand(context.commandText)) return false;\n\n if (!context.privateConversation) {\n await replyWithContext(\n context.responseCtx,\n \"為了避免誤清除共享上下文,`/new` 目前只能在與機器人的私訊 / DM 中使用。\",\n );\n return true;\n }\n\n if (!context.services.runtime) {\n await replyWithContext(\n context.responseCtx,\n \"New command is not configured correctly on the server. Please try again later.\",\n );\n return true;\n }\n\n await context.services.runtime.handleNewCommand(\n context.sessionKey,\n context.conversationId,\n context.bot,\n );\n return true;\n }\n}\n"]}
1
+ {"version":3,"file":"new.js","sourceRoot":"","sources":["../../src/commands/new.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1C,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAM9C,MAAM,YAAY,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,CAAU,CAAC;AAEzD,SAAS,eAAe,CAAC,IAAY;IACnC,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;IACjD,OAAO,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AACvD,CAAC;AAED,MAAM,OAAO,iBAAiB;IAC5B,KAAK,CAAC,SAAS,CAAC,OAAuB;QACrC,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,WAAW,CAAC;YAAE,OAAO,KAAK,CAAC;QAExD,IAAI,CAAC,OAAO,CAAC,mBAAmB,EAAE,CAAC;YACjC,MAAM,gBAAgB,CACpB,OAAO,CAAC,WAAW,EACnB,4CAA4C,CAC7C,CAAC;YACF,OAAO,IAAI,CAAC;QACd,CAAC;QAED,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;YAC9B,MAAM,gBAAgB,CACpB,OAAO,CAAC,WAAW,EACnB,gFAAgF,CACjF,CAAC;YACF,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,gBAAgB,CAC7C,OAAO,CAAC,UAAU,EAClB,OAAO,CAAC,cAAc,EACtB,OAAO,CAAC,GAAG,CACZ,CAAC;QACF,OAAO,IAAI,CAAC;IACd,CAAC;CACF","sourcesContent":["import { matchCommand } from \"./parse.js\";\nimport type { CommandContext, CommandHandler } from \"./types.js\";\nimport { replyWithContext } from \"./utils.js\";\n\ntype ParsedNewCommand = {\n command: \"new\" | \"/new\" | \"/pi-new\";\n};\n\nconst NEW_COMMANDS = [\"new\", \"/new\", \"/pi-new\"] as const;\n\nfunction parseNewCommand(text: string): ParsedNewCommand | null {\n const matched = matchCommand(text, NEW_COMMANDS);\n return matched ? { command: matched.command } : null;\n}\n\nexport class NewCommandHandler implements CommandHandler {\n async tryHandle(context: CommandContext): Promise<boolean> {\n if (!parseNewCommand(context.commandText)) return false;\n\n if (!context.privateConversation) {\n await replyWithContext(\n context.responseCtx,\n \"為了避免誤清除共享上下文,`/new` 目前只能在與機器人的私訊 / DM 中使用。\",\n );\n return true;\n }\n\n if (!context.services.runtime) {\n await replyWithContext(\n context.responseCtx,\n \"New command is not configured correctly on the server. Please try again later.\",\n );\n return true;\n }\n\n await context.services.runtime.handleNewCommand(\n context.sessionKey,\n context.conversationId,\n context.bot,\n );\n return true;\n }\n}\n"]}
@@ -0,0 +1,7 @@
1
+ export declare function matchCommand<Command extends string>(text: string, aliases: readonly Command[], options?: {
2
+ stripMention?: boolean;
3
+ }): {
4
+ command: Command;
5
+ args: string[];
6
+ } | null;
7
+ //# sourceMappingURL=parse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/commands/parse.ts"],"names":[],"mappings":"AASA,wBAAgB,YAAY,CAAC,OAAO,SAAS,MAAM,EACjD,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,SAAS,OAAO,EAAE,EAC3B,OAAO,CAAC,EAAE;IAAE,YAAY,CAAC,EAAE,OAAO,CAAA;CAAE,GACnC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,IAAI,CAQ7C","sourcesContent":["function commandTokens(text: string): string[] {\n return text.trim().split(/\\s+/).filter(Boolean);\n}\n\nfunction normalizeCommandToken(token: string, options?: { stripMention?: boolean }): string {\n const command = options?.stripMention ? token.replace(/@\\w+$/i, \"\") : token;\n return command.toLowerCase();\n}\n\nexport function matchCommand<Command extends string>(\n text: string,\n aliases: readonly Command[],\n options?: { stripMention?: boolean },\n): { command: Command; args: string[] } | null {\n const tokens = commandTokens(text);\n if (tokens.length === 0) return null;\n\n const command = normalizeCommandToken(tokens[0], options);\n return aliases.includes(command as Command)\n ? { command: command as Command, args: tokens.slice(1) }\n : null;\n}\n"]}
@@ -0,0 +1,17 @@
1
+ function commandTokens(text) {
2
+ return text.trim().split(/\s+/).filter(Boolean);
3
+ }
4
+ function normalizeCommandToken(token, options) {
5
+ const command = options?.stripMention ? token.replace(/@\w+$/i, "") : token;
6
+ return command.toLowerCase();
7
+ }
8
+ export function matchCommand(text, aliases, options) {
9
+ const tokens = commandTokens(text);
10
+ if (tokens.length === 0)
11
+ return null;
12
+ const command = normalizeCommandToken(tokens[0], options);
13
+ return aliases.includes(command)
14
+ ? { command: command, args: tokens.slice(1) }
15
+ : null;
16
+ }
17
+ //# sourceMappingURL=parse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.js","sourceRoot":"","sources":["../../src/commands/parse.ts"],"names":[],"mappings":"AAAA,SAAS,aAAa,CAAC,IAAY;IACjC,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;AAClD,CAAC;AAED,SAAS,qBAAqB,CAAC,KAAa,EAAE,OAAoC;IAChF,MAAM,OAAO,GAAG,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IAC5E,OAAO,OAAO,CAAC,WAAW,EAAE,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,YAAY,CAC1B,IAAY,EACZ,OAA2B,EAC3B,OAAoC;IAEpC,MAAM,MAAM,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IACnC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAErC,MAAM,OAAO,GAAG,qBAAqB,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IAC1D,OAAO,OAAO,CAAC,QAAQ,CAAC,OAAkB,CAAC;QACzC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAkB,EAAE,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;QACxD,CAAC,CAAC,IAAI,CAAC;AACX,CAAC","sourcesContent":["function commandTokens(text: string): string[] {\n return text.trim().split(/\\s+/).filter(Boolean);\n}\n\nfunction normalizeCommandToken(token: string, options?: { stripMention?: boolean }): string {\n const command = options?.stripMention ? token.replace(/@\\w+$/i, \"\") : token;\n return command.toLowerCase();\n}\n\nexport function matchCommand<Command extends string>(\n text: string,\n aliases: readonly Command[],\n options?: { stripMention?: boolean },\n): { command: Command; args: string[] } | null {\n const tokens = commandTokens(text);\n if (tokens.length === 0) return null;\n\n const command = normalizeCommandToken(tokens[0], options);\n return aliases.includes(command as Command)\n ? { command: command as Command, args: tokens.slice(1) }\n : null;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../../src/commands/sandbox.ts"],"names":[],"mappings":"AAIA,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,GAAG,SAAS,GAAG,MAAM,CAAC;CACvC;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,oBAAoB,GAAG,IAAI,CAc7E;AAMD,qBAAa,qBAAsB,YAAW,cAAc;IACpD,SAAS,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC,CA2FzD;CACF","sourcesContent":["import { join } from \"node:path\";\nimport { saveConversationSandboxConfig } from \"../config.js\";\nimport { readConversationWorkspaceMountMode } from \"../execution-resolver.js\";\nimport { 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\" | \"private\" | \"full\";\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) {\n const action = tokens[1].toLowerCase();\n if (action === \"boost\" || action === \"private\" || action === \"full\") {\n return { command, action };\n }\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 === \"private\" || parsed.action === \"full\") {\n saveConversationSandboxConfig(join(context.services.workingDir, context.conversationId), {\n imageWorkspaceMount: parsed.action,\n });\n await replyDiagnosticWithContext(\n context.responseCtx,\n formatSandboxCommandSummary(\"Sandbox Workspace\", [\n parsed.action === \"full\"\n ? \"已將此 conversation 的 sandbox 設為 full workspace mode。\"\n : \"已將此 conversation 的 sandbox 設為 private workspace mode。\",\n `Workspace mount: ${parsed.action}`,\n parsed.action === \"full\"\n ? \"之後這個 container 會把整個 host workspace 掛到 /workspace。\"\n : \"之後這個 container 只會掛載 private workspace 檔案與當前 conversation 目錄。\",\n ]),\n { style: \"muted\" },\n );\n return true;\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 const workspaceMount = readConversationWorkspaceMountMode(\n context.services.workingDir,\n context.conversationId,\n );\n await replyDiagnosticWithContext(\n context.responseCtx,\n formatSandboxCommandSummary(\n \"Sandbox\",\n [\n `Current: ${formatLimits(status.limits)}`,\n `Status: ${status.boosted ? \"boosted\" : \"default\"}`,\n `Workspace mount: ${workspaceMount}`,\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
+ {"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../../src/commands/sandbox.ts"],"names":[],"mappings":"AAKA,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,GAAG,SAAS,GAAG,MAAM,CAAC;CACvC;AAID,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,oBAAoB,GAAG,IAAI,CAS7E;AAMD,qBAAa,qBAAsB,YAAW,cAAc;IACpD,SAAS,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC,CA2FzD;CACF","sourcesContent":["import { join } from \"node:path\";\nimport { saveConversationSandboxConfig } from \"../config.js\";\nimport { readConversationWorkspaceMountMode } from \"../execution-resolver.js\";\nimport { resolveActorVaultKey } from \"../vault-routing.js\";\nimport { matchCommand } from \"./parse.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\" | \"private\" | \"full\";\n}\n\nconst SANDBOX_COMMANDS = [\"/pi-sandbox\", \"/sandbox\"] as const;\n\nexport function parseSandboxCommand(text: string): ParsedSandboxCommand | null {\n const matched = matchCommand(text, SANDBOX_COMMANDS, { stripMention: true });\n if (!matched) return null;\n\n const action = matched.args.length === 1 ? matched.args[0].toLowerCase() : undefined;\n if (action === \"boost\" || action === \"private\" || action === \"full\") {\n return { command: matched.command, action };\n }\n return { command: matched.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 === \"private\" || parsed.action === \"full\") {\n saveConversationSandboxConfig(join(context.services.workingDir, context.conversationId), {\n imageWorkspaceMount: parsed.action,\n });\n await replyDiagnosticWithContext(\n context.responseCtx,\n formatSandboxCommandSummary(\"Sandbox Workspace\", [\n parsed.action === \"full\"\n ? \"已將此 conversation 的 sandbox 設為 full workspace mode。\"\n : \"已將此 conversation 的 sandbox 設為 private workspace mode。\",\n `Workspace mount: ${parsed.action}`,\n parsed.action === \"full\"\n ? \"之後這個 container 會把整個 host workspace 掛到 /workspace。\"\n : \"之後這個 container 只會掛載 private workspace 檔案與當前 conversation 目錄。\",\n ]),\n { style: \"muted\" },\n );\n return true;\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 const workspaceMount = readConversationWorkspaceMountMode(\n context.services.workingDir,\n context.conversationId,\n );\n await replyDiagnosticWithContext(\n context.responseCtx,\n formatSandboxCommandSummary(\n \"Sandbox\",\n [\n `Current: ${formatLimits(status.limits)}`,\n `Status: ${status.boosted ? \"boosted\" : \"default\"}`,\n `Workspace mount: ${workspaceMount}`,\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"]}
@@ -2,23 +2,18 @@ import { join } from "node:path";
2
2
  import { saveConversationSandboxConfig } from "../config.js";
3
3
  import { readConversationWorkspaceMountMode } from "../execution-resolver.js";
4
4
  import { resolveActorVaultKey } from "../vault-routing.js";
5
+ import { matchCommand } from "./parse.js";
5
6
  import { replyDiagnosticWithContext } from "./utils.js";
7
+ const SANDBOX_COMMANDS = ["/pi-sandbox", "/sandbox"];
6
8
  export function parseSandboxCommand(text) {
7
- const tokens = text.trim().split(/\s+/).filter(Boolean);
8
- if (tokens.length === 0)
9
+ const matched = matchCommand(text, SANDBOX_COMMANDS, { stripMention: true });
10
+ if (!matched)
9
11
  return null;
10
- const command = tokens[0].replace(/@\w+$/i, "").toLowerCase();
11
- if (command !== "/pi-sandbox" && command !== "/sandbox")
12
- return null;
13
- if (tokens.length === 1)
14
- return { command };
15
- if (tokens.length === 2) {
16
- const action = tokens[1].toLowerCase();
17
- if (action === "boost" || action === "private" || action === "full") {
18
- return { command, action };
19
- }
12
+ const action = matched.args.length === 1 ? matched.args[0].toLowerCase() : undefined;
13
+ if (action === "boost" || action === "private" || action === "full") {
14
+ return { command: matched.command, action };
20
15
  }
21
- return { command };
16
+ return { command: matched.command };
22
17
  }
23
18
  function formatSandboxCommandSummary(title, lines) {
24
19
  return [`_${title}_`, ...lines].join("\n");
@@ -1 +1 @@
1
- {"version":3,"file":"sandbox.js","sourceRoot":"","sources":["../../src/commands/sandbox.ts"],"names":[],"mappings":"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","sourcesContent":["import { join } from \"node:path\";\nimport { saveConversationSandboxConfig } from \"../config.js\";\nimport { readConversationWorkspaceMountMode } from \"../execution-resolver.js\";\nimport { 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\" | \"private\" | \"full\";\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) {\n const action = tokens[1].toLowerCase();\n if (action === \"boost\" || action === \"private\" || action === \"full\") {\n return { command, action };\n }\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 === \"private\" || parsed.action === \"full\") {\n saveConversationSandboxConfig(join(context.services.workingDir, context.conversationId), {\n imageWorkspaceMount: parsed.action,\n });\n await replyDiagnosticWithContext(\n context.responseCtx,\n formatSandboxCommandSummary(\"Sandbox Workspace\", [\n parsed.action === \"full\"\n ? \"已將此 conversation 的 sandbox 設為 full workspace mode。\"\n : \"已將此 conversation 的 sandbox 設為 private workspace mode。\",\n `Workspace mount: ${parsed.action}`,\n parsed.action === \"full\"\n ? \"之後這個 container 會把整個 host workspace 掛到 /workspace。\"\n : \"之後這個 container 只會掛載 private workspace 檔案與當前 conversation 目錄。\",\n ]),\n { style: \"muted\" },\n );\n return true;\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 const workspaceMount = readConversationWorkspaceMountMode(\n context.services.workingDir,\n context.conversationId,\n );\n await replyDiagnosticWithContext(\n context.responseCtx,\n formatSandboxCommandSummary(\n \"Sandbox\",\n [\n `Current: ${formatLimits(status.limits)}`,\n `Status: ${status.boosted ? \"boosted\" : \"default\"}`,\n `Workspace mount: ${workspaceMount}`,\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
+ {"version":3,"file":"sandbox.js","sourceRoot":"","sources":["../../src/commands/sandbox.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,6BAA6B,EAAE,MAAM,cAAc,CAAC;AAC7D,OAAO,EAAE,kCAAkC,EAAE,MAAM,0BAA0B,CAAC;AAC9E,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1C,OAAO,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAC;AAOxD,MAAM,gBAAgB,GAAG,CAAC,aAAa,EAAE,UAAU,CAAU,CAAC;AAE9D,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,EAAE,gBAAgB,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7E,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAE1B,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IACrF,IAAI,MAAM,KAAK,OAAO,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;QACpE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAC9C,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC;AACtC,CAAC;AAED,SAAS,2BAA2B,CAAC,KAAa,EAAE,KAAe;IACjE,OAAO,CAAC,IAAI,KAAK,GAAG,EAAE,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC7C,CAAC;AAED,MAAM,OAAO,qBAAqB;IAChC,KAAK,CAAC,SAAS,CAAC,OAAuB;QACrC,MAAM,MAAM,GAAG,mBAAmB,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QACxD,IAAI,CAAC,MAAM;YAAE,OAAO,KAAK,CAAC;QAE1B,IAAI,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,KAAK,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC;YAC/E,MAAM,0BAA0B,CAC9B,OAAO,CAAC,WAAW,EACnB,2BAA2B,CAAC,SAAS,EAAE;gBACrC,gDAAgD;aACjD,CAAC,EACF,EAAE,KAAK,EAAE,OAAO,EAAE,CACnB,CAAC;YACF,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,YAAY,GAAG,oBAAoB,CACvC,OAAO,CAAC,QAAQ,CAAC,OAAO,EACxB,OAAO,CAAC,cAAc,EACtB,OAAO,CAAC,cAAc,CACvB,CAAC;QAEF,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,IAAI,MAAM,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YAC5D,6BAA6B,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,cAAc,CAAC,EAAE;gBACvF,mBAAmB,EAAE,MAAM,CAAC,MAAM;aACnC,CAAC,CAAC;YACH,MAAM,0BAA0B,CAC9B,OAAO,CAAC,WAAW,EACnB,2BAA2B,CAAC,mBAAmB,EAAE;gBAC/C,MAAM,CAAC,MAAM,KAAK,MAAM;oBACtB,CAAC,CAAC,oDAAoD;oBACtD,CAAC,CAAC,uDAAuD;gBAC3D,oBAAoB,MAAM,CAAC,MAAM,EAAE;gBACnC,MAAM,CAAC,MAAM,KAAK,MAAM;oBACtB,CAAC,CAAC,mDAAmD;oBACrD,CAAC,CAAC,8DAA8D;aACnE,CAAC,EACF,EAAE,KAAK,EAAE,OAAO,EAAE,CACnB,CAAC;YACF,OAAO,IAAI,CAAC;QACd,CAAC;QAED,IAAI,MAAM,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC;YAC9B,MAAM,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,cAAc,EAAE,CAAC;YAClE,IAAI,CAAC,WAAW,EAAE,IAAI,IAAI,CAAC,WAAW,EAAE,MAAM,EAAE,CAAC;gBAC/C,MAAM,0BAA0B,CAC9B,OAAO,CAAC,WAAW,EACnB,2BAA2B,CAAC,eAAe,EAAE;oBAC3C,wCAAwC;oBACxC,yCAAyC;iBAC1C,CAAC,EACF,EAAE,KAAK,EAAE,OAAO,EAAE,CACnB,CAAC;gBACF,OAAO,IAAI,CAAC;YACd,CAAC;YAED,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;YACtE,MAAM,0BAA0B,CAC9B,OAAO,CAAC,WAAW,EACnB,2BAA2B,CAAC,eAAe,EAAE;gBAC3C,mCAAmC;gBACnC,YAAY,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE;gBACzC,oCAAoC;aACrC,CAAC,EACF,EAAE,KAAK,EAAE,OAAO,EAAE,CACnB,CAAC;YACF,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,cAAc,CAAC,YAAY,CAAC,CAAC;QACzE,MAAM,aAAa,GAAG,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,gBAAgB,EAAE,CAAC;QACtE,MAAM,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,cAAc,EAAE,CAAC;QAClE,MAAM,cAAc,GAAG,kCAAkC,CACvD,OAAO,CAAC,QAAQ,CAAC,UAAU,EAC3B,OAAO,CAAC,cAAc,CACvB,CAAC;QACF,MAAM,0BAA0B,CAC9B,OAAO,CAAC,WAAW,EACnB,2BAA2B,CACzB,SAAS,EACT;YACE,YAAY,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE;YACzC,WAAW,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,EAAE;YACnD,oBAAoB,cAAc,EAAE;YACpC,EAAE;YACF,YAAY,YAAY,CAAC,aAAa,CAAC,EAAE;YACzC,WAAW,CAAC,CAAC,CAAC,UAAU,YAAY,CAAC,EAAE,GAAG,aAAa,EAAE,GAAG,WAAW,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS;SACzF,CAAC,MAAM,CAAC,CAAC,IAAI,EAAkB,EAAE,CAAC,IAAI,KAAK,SAAS,CAAC,CACvD,EACD,EAAE,KAAK,EAAE,OAAO,EAAE,CACnB,CAAC;QACF,OAAO,IAAI,CAAC;IACd,CAAC;CACF;AAED,SAAS,YAAY,CAAC,MAAsD;IAC1E,OAAO,OAAO,MAAM,EAAE,IAAI,IAAI,WAAW,aAAa,MAAM,EAAE,MAAM,IAAI,WAAW,EAAE,CAAC;AACxF,CAAC","sourcesContent":["import { join } from \"node:path\";\nimport { saveConversationSandboxConfig } from \"../config.js\";\nimport { readConversationWorkspaceMountMode } from \"../execution-resolver.js\";\nimport { resolveActorVaultKey } from \"../vault-routing.js\";\nimport { matchCommand } from \"./parse.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\" | \"private\" | \"full\";\n}\n\nconst SANDBOX_COMMANDS = [\"/pi-sandbox\", \"/sandbox\"] as const;\n\nexport function parseSandboxCommand(text: string): ParsedSandboxCommand | null {\n const matched = matchCommand(text, SANDBOX_COMMANDS, { stripMention: true });\n if (!matched) return null;\n\n const action = matched.args.length === 1 ? matched.args[0].toLowerCase() : undefined;\n if (action === \"boost\" || action === \"private\" || action === \"full\") {\n return { command: matched.command, action };\n }\n return { command: matched.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 === \"private\" || parsed.action === \"full\") {\n saveConversationSandboxConfig(join(context.services.workingDir, context.conversationId), {\n imageWorkspaceMount: parsed.action,\n });\n await replyDiagnosticWithContext(\n context.responseCtx,\n formatSandboxCommandSummary(\"Sandbox Workspace\", [\n parsed.action === \"full\"\n ? \"已將此 conversation 的 sandbox 設為 full workspace mode。\"\n : \"已將此 conversation 的 sandbox 設為 private workspace mode。\",\n `Workspace mount: ${parsed.action}`,\n parsed.action === \"full\"\n ? \"之後這個 container 會把整個 host workspace 掛到 /workspace。\"\n : \"之後這個 container 只會掛載 private workspace 檔案與當前 conversation 目錄。\",\n ]),\n { style: \"muted\" },\n );\n return true;\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 const workspaceMount = readConversationWorkspaceMountMode(\n context.services.workingDir,\n context.conversationId,\n );\n await replyDiagnosticWithContext(\n context.responseCtx,\n formatSandboxCommandSummary(\n \"Sandbox\",\n [\n `Current: ${formatLimits(status.limits)}`,\n `Status: ${status.boosted ? \"boosted\" : \"default\"}`,\n `Workspace mount: ${workspaceMount}`,\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":"conversation-history.d.ts","sourceRoot":"","sources":["../src/conversation-history.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAG3D,MAAM,WAAW,iCAAiC;IAChD,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ;AAKD,wBAAgB,mCAAmC,CACjD,OAAO,EAAE,iCAAiC,GAAG;IAAE,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAClF,MAAM,GAAG,IAAI,CAKf;AAED,wBAAgB,iCAAiC,CAC/C,OAAO,EAAE,iCAAiC,GACzC,MAAM,GAAG,IAAI,CAqCf;AAED,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,IAAI,CAAC,iCAAiC,EAAE,YAAY,GAAG,KAAK,CAAC,GACrE,MAAM,GAAG,IAAI,CAMf;AAED,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,IAAI,CAAC,iCAAiC,EAAE,YAAY,GAAG,KAAK,CAAC,GACrE,sBAAsB,EAAE,CAiC1B","sourcesContent":["import { randomUUID } from \"crypto\";\nimport { mkdirSync, statSync } from \"fs\";\nimport { join } from \"path\";\nimport { isRecord, parseJsonValue, readTextFileIfExists } from \"./file-guards.js\";\nimport { atomicWritePrivateFile } from \"./fs-atomic.js\";\nimport type { ConversationLogMessage } from \"./context.js\";\nimport * as log from \"./log.js\";\n\nexport interface MaterializeTopLevelHistoryOptions {\n conversationDir: string;\n sessionDir: string;\n cwd: string;\n recentDays?: number;\n maxMessages?: number;\n now?: Date;\n}\n\nconst DEFAULT_RECENT_DAYS = 14;\nconst DEFAULT_MAX_MESSAGES = 200;\n\nexport function resolveUsableTopLevelHistorySession(\n options: MaterializeTopLevelHistoryOptions & { existingSessionFile: string | null },\n): string | null {\n if (options.existingSessionFile && isSessionFreshForTopLevelHistory(options)) {\n return options.existingSessionFile;\n }\n return materializeTopLevelHistorySession(options);\n}\n\nexport function materializeTopLevelHistorySession(\n options: MaterializeTopLevelHistoryOptions,\n): string | null {\n const messages = readTopLevelHistoryMessages(options);\n if (messages.length === 0) return null;\n\n mkdirSync(options.sessionDir, { recursive: true });\n const now = options.now ?? new Date();\n const sessionId = randomUUID();\n const filename = `${now.toISOString().replace(/[:.]/g, \"-\")}_${sessionId.slice(0, 8)}_history.jsonl`;\n const sessionFile = join(options.sessionDir, filename);\n const header = {\n type: \"session\",\n version: 3,\n id: sessionId,\n timestamp: now.toISOString(),\n cwd: options.cwd,\n source: {\n kind: \"platform-history\",\n file: \"log.jsonl\",\n recentDays: options.recentDays ?? DEFAULT_RECENT_DAYS,\n },\n };\n const entries = messages.map((message) => ({\n type: \"message\",\n id: randomUUID().slice(0, 8),\n parentId: null,\n timestamp: new Date(message.date ?? now.toISOString()).toISOString(),\n message: {\n role: message.isBot ? \"assistant\" : \"user\",\n content: [{ type: \"text\", text: formatHistoryMessage(message) }],\n ...(message.date ? { timestamp: new Date(message.date).getTime() } : {}),\n },\n }));\n\n const content = [header, ...entries].map((entry) => JSON.stringify(entry)).join(\"\\n\");\n atomicWritePrivateFile(sessionFile, `${content}\\n`);\n atomicWritePrivateFile(join(options.sessionDir, \"current\"), filename);\n return sessionFile;\n}\n\nexport function latestTopLevelHistoryTime(\n options: Omit<MaterializeTopLevelHistoryOptions, \"sessionDir\" | \"cwd\">,\n): number | null {\n const messages = readTopLevelHistoryMessages({ ...options, maxMessages: 1 });\n const latest = messages.at(-1);\n if (!latest?.date) return null;\n const ms = new Date(latest.date).getTime();\n return Number.isFinite(ms) ? ms : null;\n}\n\nexport function readTopLevelHistoryMessages(\n options: Omit<MaterializeTopLevelHistoryOptions, \"sessionDir\" | \"cwd\">,\n): ConversationLogMessage[] {\n const logFile = join(options.conversationDir, \"log.jsonl\");\n const raw = readTextFileIfExists(logFile);\n if (raw === undefined) return [];\n\n const nowMs = (options.now ?? new Date()).getTime();\n const sinceMs = nowMs - (options.recentDays ?? DEFAULT_RECENT_DAYS) * 24 * 60 * 60 * 1000;\n const messages: ConversationLogMessage[] = [];\n const lines = raw.trim().split(\"\\n\").filter(Boolean);\n\n for (let i = 0; i < lines.length; i++) {\n try {\n const entry = parseJsonValue(\n lines[i],\n (value): value is ConversationLogMessage => isRecord(value),\n (detail) => (detail === \"unexpected JSON shape\" ? \"expected a JSON object\" : detail),\n );\n if (entry.threadTs) continue;\n if (!entry.text?.trim()) continue;\n if (entry.date) {\n const dateMs = new Date(entry.date).getTime();\n if (Number.isFinite(dateMs) && dateMs < sinceMs) continue;\n }\n messages.push(entry);\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 }\n }\n\n return messages.slice(-(options.maxMessages ?? DEFAULT_MAX_MESSAGES));\n}\n\nfunction isSessionFreshForTopLevelHistory(\n options: MaterializeTopLevelHistoryOptions & { existingSessionFile: string | null },\n): boolean {\n if (!options.existingSessionFile) return false;\n const latestHistoryMs = latestTopLevelHistoryTime(options);\n if (latestHistoryMs === null) return true;\n\n try {\n return statSync(options.existingSessionFile).mtimeMs >= latestHistoryMs;\n } catch {\n return false;\n }\n}\n\nfunction formatHistoryMessage(message: ConversationLogMessage): string {\n const text = message.text?.trim() ?? \"\";\n if (message.isBot) return text;\n const userLabel = message.userName || message.user || \"unknown\";\n const timestamp = message.date ? formatLocalTimestamp(new Date(message.date)) : null;\n return timestamp ? `[${timestamp}] [${userLabel}]: ${text}` : `[${userLabel}]: ${text}`;\n}\n\nfunction formatLocalTimestamp(date: Date): string {\n const offset = -date.getTimezoneOffset();\n const sign = offset >= 0 ? \"+\" : \"-\";\n const abs = Math.abs(offset);\n return (\n `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` +\n `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}` +\n `${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`\n );\n}\n\nfunction pad(n: number): string {\n return n.toString().padStart(2, \"0\");\n}\n"]}
1
+ {"version":3,"file":"conversation-history.d.ts","sourceRoot":"","sources":["../src/conversation-history.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAG3D,MAAM,WAAW,iCAAiC;IAChD,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ;AAKD,wBAAgB,mCAAmC,CACjD,OAAO,EAAE,iCAAiC,GAAG;IAAE,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAClF,MAAM,GAAG,IAAI,CAKf;AAED,wBAAgB,iCAAiC,CAC/C,OAAO,EAAE,iCAAiC,GACzC,MAAM,GAAG,IAAI,CAiCf;AAED,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,IAAI,CAAC,iCAAiC,EAAE,YAAY,GAAG,KAAK,CAAC,GACrE,MAAM,GAAG,IAAI,CAMf;AAED,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,IAAI,CAAC,iCAAiC,EAAE,YAAY,GAAG,KAAK,CAAC,GACrE,sBAAsB,EAAE,CAiC1B","sourcesContent":["import { randomUUID } from \"crypto\";\nimport { mkdirSync, statSync } from \"fs\";\nimport { join } from \"path\";\nimport { isRecord, parseJsonValue, readTextFileIfExists } from \"./file-guards.js\";\nimport { atomicWritePrivateFile } from \"./fs-atomic.js\";\nimport type { ConversationLogMessage } from \"./context.js\";\nimport * as log from \"./log.js\";\n\nexport interface MaterializeTopLevelHistoryOptions {\n conversationDir: string;\n sessionDir: string;\n cwd: string;\n recentDays?: number;\n maxMessages?: number;\n now?: Date;\n}\n\nconst DEFAULT_RECENT_DAYS = 14;\nconst DEFAULT_MAX_MESSAGES = 200;\n\nexport function resolveUsableTopLevelHistorySession(\n options: MaterializeTopLevelHistoryOptions & { existingSessionFile: string | null },\n): string | null {\n if (options.existingSessionFile && isSessionFreshForTopLevelHistory(options)) {\n return options.existingSessionFile;\n }\n return materializeTopLevelHistorySession(options);\n}\n\nexport function materializeTopLevelHistorySession(\n options: MaterializeTopLevelHistoryOptions,\n): string | null {\n const messages = readTopLevelHistoryMessages(options);\n if (messages.length === 0) return null;\n\n mkdirSync(options.sessionDir, { recursive: true });\n const now = options.now ?? new Date();\n const sessionId = randomUUID();\n const filename = `${now.toISOString().replace(/[:.]/g, \"-\")}_${sessionId.slice(0, 8)}_history.jsonl`;\n const sessionFile = join(options.sessionDir, filename);\n const header = {\n type: \"session\",\n version: 3,\n id: sessionId,\n timestamp: now.toISOString(),\n cwd: options.cwd,\n source: {\n kind: \"platform-history\",\n file: \"log.jsonl\",\n recentDays: options.recentDays ?? DEFAULT_RECENT_DAYS,\n },\n };\n const entries = messages.map((message) => ({\n type: \"message\",\n id: randomUUID().slice(0, 8),\n parentId: null,\n timestamp: new Date(message.date ?? now.toISOString()).toISOString(),\n message: buildHistorySessionMessage(message),\n }));\n\n const content = [header, ...entries].map((entry) => JSON.stringify(entry)).join(\"\\n\");\n atomicWritePrivateFile(sessionFile, `${content}\\n`);\n atomicWritePrivateFile(join(options.sessionDir, \"current\"), filename);\n return sessionFile;\n}\n\nexport function latestTopLevelHistoryTime(\n options: Omit<MaterializeTopLevelHistoryOptions, \"sessionDir\" | \"cwd\">,\n): number | null {\n const messages = readTopLevelHistoryMessages({ ...options, maxMessages: 1 });\n const latest = messages.at(-1);\n if (!latest?.date) return null;\n const ms = new Date(latest.date).getTime();\n return Number.isFinite(ms) ? ms : null;\n}\n\nexport function readTopLevelHistoryMessages(\n options: Omit<MaterializeTopLevelHistoryOptions, \"sessionDir\" | \"cwd\">,\n): ConversationLogMessage[] {\n const logFile = join(options.conversationDir, \"log.jsonl\");\n const raw = readTextFileIfExists(logFile);\n if (raw === undefined) return [];\n\n const nowMs = (options.now ?? new Date()).getTime();\n const sinceMs = nowMs - (options.recentDays ?? DEFAULT_RECENT_DAYS) * 24 * 60 * 60 * 1000;\n const messages: ConversationLogMessage[] = [];\n const lines = raw.trim().split(\"\\n\").filter(Boolean);\n\n for (let i = 0; i < lines.length; i++) {\n try {\n const entry = parseJsonValue(\n lines[i],\n (value): value is ConversationLogMessage => isRecord(value),\n (detail) => (detail === \"unexpected JSON shape\" ? \"expected a JSON object\" : detail),\n );\n if (entry.threadTs) continue;\n if (!entry.text?.trim()) continue;\n if (entry.date) {\n const dateMs = new Date(entry.date).getTime();\n if (Number.isFinite(dateMs) && dateMs < sinceMs) continue;\n }\n messages.push(entry);\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 }\n }\n\n return messages.slice(-(options.maxMessages ?? DEFAULT_MAX_MESSAGES));\n}\n\nfunction isSessionFreshForTopLevelHistory(\n options: MaterializeTopLevelHistoryOptions & { existingSessionFile: string | null },\n): boolean {\n if (!options.existingSessionFile) return false;\n const latestHistoryMs = latestTopLevelHistoryTime(options);\n if (latestHistoryMs === null) return true;\n\n try {\n return statSync(options.existingSessionFile).mtimeMs >= latestHistoryMs;\n } catch {\n return false;\n }\n}\n\nfunction buildHistorySessionMessage(message: ConversationLogMessage): object {\n const base = {\n role: message.isBot ? \"assistant\" : \"user\",\n content: [{ type: \"text\", text: formatHistoryMessage(message) }],\n ...(message.date ? { timestamp: new Date(message.date).getTime() } : {}),\n };\n if (!message.isBot) return base;\n\n return {\n ...base,\n api: \"platform-history\",\n provider: \"platform-history\",\n model: \"platform-history\",\n usage: zeroUsage(),\n stopReason: \"stop\",\n };\n}\n\nfunction zeroUsage(): object {\n return {\n input: 0,\n output: 0,\n cacheRead: 0,\n cacheWrite: 0,\n totalTokens: 0,\n cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n };\n}\n\nfunction formatHistoryMessage(message: ConversationLogMessage): string {\n const text = message.text?.trim() ?? \"\";\n if (message.isBot) return text;\n const userLabel = message.userName || message.user || \"unknown\";\n const timestamp = message.date ? formatLocalTimestamp(new Date(message.date)) : null;\n return timestamp ? `[${timestamp}] [${userLabel}]: ${text}` : `[${userLabel}]: ${text}`;\n}\n\nfunction formatLocalTimestamp(date: Date): string {\n const offset = -date.getTimezoneOffset();\n const sign = offset >= 0 ? \"+\" : \"-\";\n const abs = Math.abs(offset);\n return (\n `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` +\n `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}` +\n `${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`\n );\n}\n\nfunction pad(n: number): string {\n return n.toString().padStart(2, \"0\");\n}\n"]}
@@ -38,11 +38,7 @@ export function materializeTopLevelHistorySession(options) {
38
38
  id: randomUUID().slice(0, 8),
39
39
  parentId: null,
40
40
  timestamp: new Date(message.date ?? now.toISOString()).toISOString(),
41
- message: {
42
- role: message.isBot ? "assistant" : "user",
43
- content: [{ type: "text", text: formatHistoryMessage(message) }],
44
- ...(message.date ? { timestamp: new Date(message.date).getTime() } : {}),
45
- },
41
+ message: buildHistorySessionMessage(message),
46
42
  }));
47
43
  const content = [header, ...entries].map((entry) => JSON.stringify(entry)).join("\n");
48
44
  atomicWritePrivateFile(sessionFile, `${content}\n`);
@@ -99,6 +95,33 @@ function isSessionFreshForTopLevelHistory(options) {
99
95
  return false;
100
96
  }
101
97
  }
98
+ function buildHistorySessionMessage(message) {
99
+ const base = {
100
+ role: message.isBot ? "assistant" : "user",
101
+ content: [{ type: "text", text: formatHistoryMessage(message) }],
102
+ ...(message.date ? { timestamp: new Date(message.date).getTime() } : {}),
103
+ };
104
+ if (!message.isBot)
105
+ return base;
106
+ return {
107
+ ...base,
108
+ api: "platform-history",
109
+ provider: "platform-history",
110
+ model: "platform-history",
111
+ usage: zeroUsage(),
112
+ stopReason: "stop",
113
+ };
114
+ }
115
+ function zeroUsage() {
116
+ return {
117
+ input: 0,
118
+ output: 0,
119
+ cacheRead: 0,
120
+ cacheWrite: 0,
121
+ totalTokens: 0,
122
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
123
+ };
124
+ }
102
125
  function formatHistoryMessage(message) {
103
126
  const text = message.text?.trim() ?? "";
104
127
  if (message.isBot)
@@ -1 +1 @@
1
- {"version":3,"file":"conversation-history.js","sourceRoot":"","sources":["../src/conversation-history.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from \"crypto\";\nimport { mkdirSync, statSync } from \"fs\";\nimport { join } from \"path\";\nimport { isRecord, parseJsonValue, readTextFileIfExists } from \"./file-guards.js\";\nimport { atomicWritePrivateFile } from \"./fs-atomic.js\";\nimport type { ConversationLogMessage } from \"./context.js\";\nimport * as log from \"./log.js\";\n\nexport interface MaterializeTopLevelHistoryOptions {\n conversationDir: string;\n sessionDir: string;\n cwd: string;\n recentDays?: number;\n maxMessages?: number;\n now?: Date;\n}\n\nconst DEFAULT_RECENT_DAYS = 14;\nconst DEFAULT_MAX_MESSAGES = 200;\n\nexport function resolveUsableTopLevelHistorySession(\n options: MaterializeTopLevelHistoryOptions & { existingSessionFile: string | null },\n): string | null {\n if (options.existingSessionFile && isSessionFreshForTopLevelHistory(options)) {\n return options.existingSessionFile;\n }\n return materializeTopLevelHistorySession(options);\n}\n\nexport function materializeTopLevelHistorySession(\n options: MaterializeTopLevelHistoryOptions,\n): string | null {\n const messages = readTopLevelHistoryMessages(options);\n if (messages.length === 0) return null;\n\n mkdirSync(options.sessionDir, { recursive: true });\n const now = options.now ?? new Date();\n const sessionId = randomUUID();\n const filename = `${now.toISOString().replace(/[:.]/g, \"-\")}_${sessionId.slice(0, 8)}_history.jsonl`;\n const sessionFile = join(options.sessionDir, filename);\n const header = {\n type: \"session\",\n version: 3,\n id: sessionId,\n timestamp: now.toISOString(),\n cwd: options.cwd,\n source: {\n kind: \"platform-history\",\n file: \"log.jsonl\",\n recentDays: options.recentDays ?? DEFAULT_RECENT_DAYS,\n },\n };\n const entries = messages.map((message) => ({\n type: \"message\",\n id: randomUUID().slice(0, 8),\n parentId: null,\n timestamp: new Date(message.date ?? now.toISOString()).toISOString(),\n message: {\n role: message.isBot ? \"assistant\" : \"user\",\n content: [{ type: \"text\", text: formatHistoryMessage(message) }],\n ...(message.date ? { timestamp: new Date(message.date).getTime() } : {}),\n },\n }));\n\n const content = [header, ...entries].map((entry) => JSON.stringify(entry)).join(\"\\n\");\n atomicWritePrivateFile(sessionFile, `${content}\\n`);\n atomicWritePrivateFile(join(options.sessionDir, \"current\"), filename);\n return sessionFile;\n}\n\nexport function latestTopLevelHistoryTime(\n options: Omit<MaterializeTopLevelHistoryOptions, \"sessionDir\" | \"cwd\">,\n): number | null {\n const messages = readTopLevelHistoryMessages({ ...options, maxMessages: 1 });\n const latest = messages.at(-1);\n if (!latest?.date) return null;\n const ms = new Date(latest.date).getTime();\n return Number.isFinite(ms) ? ms : null;\n}\n\nexport function readTopLevelHistoryMessages(\n options: Omit<MaterializeTopLevelHistoryOptions, \"sessionDir\" | \"cwd\">,\n): ConversationLogMessage[] {\n const logFile = join(options.conversationDir, \"log.jsonl\");\n const raw = readTextFileIfExists(logFile);\n if (raw === undefined) return [];\n\n const nowMs = (options.now ?? new Date()).getTime();\n const sinceMs = nowMs - (options.recentDays ?? DEFAULT_RECENT_DAYS) * 24 * 60 * 60 * 1000;\n const messages: ConversationLogMessage[] = [];\n const lines = raw.trim().split(\"\\n\").filter(Boolean);\n\n for (let i = 0; i < lines.length; i++) {\n try {\n const entry = parseJsonValue(\n lines[i],\n (value): value is ConversationLogMessage => isRecord(value),\n (detail) => (detail === \"unexpected JSON shape\" ? \"expected a JSON object\" : detail),\n );\n if (entry.threadTs) continue;\n if (!entry.text?.trim()) continue;\n if (entry.date) {\n const dateMs = new Date(entry.date).getTime();\n if (Number.isFinite(dateMs) && dateMs < sinceMs) continue;\n }\n messages.push(entry);\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 }\n }\n\n return messages.slice(-(options.maxMessages ?? DEFAULT_MAX_MESSAGES));\n}\n\nfunction isSessionFreshForTopLevelHistory(\n options: MaterializeTopLevelHistoryOptions & { existingSessionFile: string | null },\n): boolean {\n if (!options.existingSessionFile) return false;\n const latestHistoryMs = latestTopLevelHistoryTime(options);\n if (latestHistoryMs === null) return true;\n\n try {\n return statSync(options.existingSessionFile).mtimeMs >= latestHistoryMs;\n } catch {\n return false;\n }\n}\n\nfunction formatHistoryMessage(message: ConversationLogMessage): string {\n const text = message.text?.trim() ?? \"\";\n if (message.isBot) return text;\n const userLabel = message.userName || message.user || \"unknown\";\n const timestamp = message.date ? formatLocalTimestamp(new Date(message.date)) : null;\n return timestamp ? `[${timestamp}] [${userLabel}]: ${text}` : `[${userLabel}]: ${text}`;\n}\n\nfunction formatLocalTimestamp(date: Date): string {\n const offset = -date.getTimezoneOffset();\n const sign = offset >= 0 ? \"+\" : \"-\";\n const abs = Math.abs(offset);\n return (\n `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` +\n `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}` +\n `${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`\n );\n}\n\nfunction pad(n: number): string {\n return n.toString().padStart(2, \"0\");\n}\n"]}
1
+ {"version":3,"file":"conversation-history.js","sourceRoot":"","sources":["../src/conversation-history.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from \"crypto\";\nimport { mkdirSync, statSync } from \"fs\";\nimport { join } from \"path\";\nimport { isRecord, parseJsonValue, readTextFileIfExists } from \"./file-guards.js\";\nimport { atomicWritePrivateFile } from \"./fs-atomic.js\";\nimport type { ConversationLogMessage } from \"./context.js\";\nimport * as log from \"./log.js\";\n\nexport interface MaterializeTopLevelHistoryOptions {\n conversationDir: string;\n sessionDir: string;\n cwd: string;\n recentDays?: number;\n maxMessages?: number;\n now?: Date;\n}\n\nconst DEFAULT_RECENT_DAYS = 14;\nconst DEFAULT_MAX_MESSAGES = 200;\n\nexport function resolveUsableTopLevelHistorySession(\n options: MaterializeTopLevelHistoryOptions & { existingSessionFile: string | null },\n): string | null {\n if (options.existingSessionFile && isSessionFreshForTopLevelHistory(options)) {\n return options.existingSessionFile;\n }\n return materializeTopLevelHistorySession(options);\n}\n\nexport function materializeTopLevelHistorySession(\n options: MaterializeTopLevelHistoryOptions,\n): string | null {\n const messages = readTopLevelHistoryMessages(options);\n if (messages.length === 0) return null;\n\n mkdirSync(options.sessionDir, { recursive: true });\n const now = options.now ?? new Date();\n const sessionId = randomUUID();\n const filename = `${now.toISOString().replace(/[:.]/g, \"-\")}_${sessionId.slice(0, 8)}_history.jsonl`;\n const sessionFile = join(options.sessionDir, filename);\n const header = {\n type: \"session\",\n version: 3,\n id: sessionId,\n timestamp: now.toISOString(),\n cwd: options.cwd,\n source: {\n kind: \"platform-history\",\n file: \"log.jsonl\",\n recentDays: options.recentDays ?? DEFAULT_RECENT_DAYS,\n },\n };\n const entries = messages.map((message) => ({\n type: \"message\",\n id: randomUUID().slice(0, 8),\n parentId: null,\n timestamp: new Date(message.date ?? now.toISOString()).toISOString(),\n message: buildHistorySessionMessage(message),\n }));\n\n const content = [header, ...entries].map((entry) => JSON.stringify(entry)).join(\"\\n\");\n atomicWritePrivateFile(sessionFile, `${content}\\n`);\n atomicWritePrivateFile(join(options.sessionDir, \"current\"), filename);\n return sessionFile;\n}\n\nexport function latestTopLevelHistoryTime(\n options: Omit<MaterializeTopLevelHistoryOptions, \"sessionDir\" | \"cwd\">,\n): number | null {\n const messages = readTopLevelHistoryMessages({ ...options, maxMessages: 1 });\n const latest = messages.at(-1);\n if (!latest?.date) return null;\n const ms = new Date(latest.date).getTime();\n return Number.isFinite(ms) ? ms : null;\n}\n\nexport function readTopLevelHistoryMessages(\n options: Omit<MaterializeTopLevelHistoryOptions, \"sessionDir\" | \"cwd\">,\n): ConversationLogMessage[] {\n const logFile = join(options.conversationDir, \"log.jsonl\");\n const raw = readTextFileIfExists(logFile);\n if (raw === undefined) return [];\n\n const nowMs = (options.now ?? new Date()).getTime();\n const sinceMs = nowMs - (options.recentDays ?? DEFAULT_RECENT_DAYS) * 24 * 60 * 60 * 1000;\n const messages: ConversationLogMessage[] = [];\n const lines = raw.trim().split(\"\\n\").filter(Boolean);\n\n for (let i = 0; i < lines.length; i++) {\n try {\n const entry = parseJsonValue(\n lines[i],\n (value): value is ConversationLogMessage => isRecord(value),\n (detail) => (detail === \"unexpected JSON shape\" ? \"expected a JSON object\" : detail),\n );\n if (entry.threadTs) continue;\n if (!entry.text?.trim()) continue;\n if (entry.date) {\n const dateMs = new Date(entry.date).getTime();\n if (Number.isFinite(dateMs) && dateMs < sinceMs) continue;\n }\n messages.push(entry);\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 }\n }\n\n return messages.slice(-(options.maxMessages ?? DEFAULT_MAX_MESSAGES));\n}\n\nfunction isSessionFreshForTopLevelHistory(\n options: MaterializeTopLevelHistoryOptions & { existingSessionFile: string | null },\n): boolean {\n if (!options.existingSessionFile) return false;\n const latestHistoryMs = latestTopLevelHistoryTime(options);\n if (latestHistoryMs === null) return true;\n\n try {\n return statSync(options.existingSessionFile).mtimeMs >= latestHistoryMs;\n } catch {\n return false;\n }\n}\n\nfunction buildHistorySessionMessage(message: ConversationLogMessage): object {\n const base = {\n role: message.isBot ? \"assistant\" : \"user\",\n content: [{ type: \"text\", text: formatHistoryMessage(message) }],\n ...(message.date ? { timestamp: new Date(message.date).getTime() } : {}),\n };\n if (!message.isBot) return base;\n\n return {\n ...base,\n api: \"platform-history\",\n provider: \"platform-history\",\n model: \"platform-history\",\n usage: zeroUsage(),\n stopReason: \"stop\",\n };\n}\n\nfunction zeroUsage(): object {\n return {\n input: 0,\n output: 0,\n cacheRead: 0,\n cacheWrite: 0,\n totalTokens: 0,\n cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n };\n}\n\nfunction formatHistoryMessage(message: ConversationLogMessage): string {\n const text = message.text?.trim() ?? \"\";\n if (message.isBot) return text;\n const userLabel = message.userName || message.user || \"unknown\";\n const timestamp = message.date ? formatLocalTimestamp(new Date(message.date)) : null;\n return timestamp ? `[${timestamp}] [${userLabel}]: ${text}` : `[${userLabel}]: ${text}`;\n}\n\nfunction formatLocalTimestamp(date: Date): string {\n const offset = -date.getTimezoneOffset();\n const sign = offset >= 0 ? \"+\" : \"-\";\n const abs = Math.abs(offset);\n return (\n `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` +\n `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}` +\n `${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`\n );\n}\n\nfunction pad(n: number): string {\n return n.toString().padStart(2, \"0\");\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"download.d.ts","sourceRoot":"","sources":["../src/download.ts"],"names":[],"mappings":"AA6BA,wBAAsB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAuFxF","sourcesContent":["import { LogLevel, WebClient } from \"@slack/web-api\";\n\ninterface Message {\n ts: string;\n user?: string;\n text?: string;\n thread_ts?: string;\n reply_count?: number;\n files?: Array<{ name: string; url_private?: string }>;\n}\n\nfunction formatTs(ts: string): string {\n const date = new Date(parseFloat(ts) * 1000);\n return date\n .toISOString()\n .replace(\"T\", \" \")\n .replace(/\\.\\d+Z$/, \"\");\n}\n\nfunction formatMessage(ts: string, user: string, text: string, indent = \"\"): string {\n const prefix = `[${formatTs(ts)}] ${user}: `;\n const lines = text.split(\"\\n\");\n const firstLine = `${indent}${prefix}${lines[0]}`;\n if (lines.length === 1) return firstLine;\n // All continuation lines get same indent as content start\n const contentIndent = indent + \" \".repeat(prefix.length);\n return [firstLine, ...lines.slice(1).map((l) => contentIndent + l)].join(\"\\n\");\n}\n\nexport async function downloadChannel(channelId: string, botToken: string): Promise<void> {\n const client = new WebClient(botToken, { logLevel: LogLevel.ERROR });\n\n console.error(`Fetching channel info for ${channelId}...`);\n\n // Get channel info\n let channelName = channelId;\n try {\n const info = await client.conversations.info({ channel: channelId });\n channelName = (info.channel as any)?.name || channelId;\n } catch {\n // DM channels don't have names, that's fine\n }\n\n console.error(`Downloading history for #${channelName} (${channelId})...`);\n\n // Fetch all messages\n const messages: Message[] = [];\n let cursor: string | undefined;\n\n do {\n const response = await client.conversations.history({\n channel: channelId,\n limit: 200,\n cursor,\n });\n\n if (response.messages) {\n messages.push(...(response.messages as Message[]));\n }\n\n cursor = response.response_metadata?.next_cursor;\n console.error(` Fetched ${messages.length} messages...`);\n } while (cursor);\n\n // Reverse to chronological order\n messages.reverse();\n\n // Build map of thread replies\n const threadReplies = new Map<string, Message[]>();\n const threadsToFetch = messages.filter((m) => m.reply_count && m.reply_count > 0);\n\n console.error(`Fetching ${threadsToFetch.length} threads...`);\n\n for (let i = 0; i < threadsToFetch.length; i++) {\n const parent = threadsToFetch[i];\n console.error(` Thread ${i + 1}/${threadsToFetch.length} (${parent.reply_count} replies)...`);\n\n const replies: Message[] = [];\n let threadCursor: string | undefined;\n\n do {\n const response = await client.conversations.replies({\n channel: channelId,\n ts: parent.ts,\n limit: 200,\n cursor: threadCursor,\n });\n\n if (response.messages) {\n // Skip the first message (it's the parent)\n replies.push(...(response.messages as Message[]).slice(1));\n }\n\n threadCursor = response.response_metadata?.next_cursor;\n } while (threadCursor);\n\n threadReplies.set(parent.ts, replies);\n }\n\n // Output messages with thread replies interleaved\n let totalReplies = 0;\n for (const msg of messages) {\n // Output the message\n console.log(formatMessage(msg.ts, msg.user || \"unknown\", msg.text || \"\"));\n\n // Output thread replies right after parent (indented)\n const replies = threadReplies.get(msg.ts);\n if (replies) {\n for (const reply of replies) {\n console.log(formatMessage(reply.ts, reply.user || \"unknown\", reply.text || \"\", \" \"));\n totalReplies++;\n }\n }\n }\n\n console.error(`Done! ${messages.length} messages, ${totalReplies} thread replies`);\n}\n"]}
1
+ {"version":3,"file":"download.d.ts","sourceRoot":"","sources":["../src/download.ts"],"names":[],"mappings":"AA6BA,wBAAsB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAuFxF","sourcesContent":["import { LogLevel, WebClient } from \"@slack/web-api\";\n\ninterface Message {\n ts: string;\n user?: string;\n text?: string;\n thread_ts?: string;\n reply_count?: number;\n files?: Array<{ name: string; url_private?: string }>;\n}\n\nfunction formatTs(ts: string): string {\n const date = new Date(parseFloat(ts) * 1000);\n return date\n .toISOString()\n .replace(\"T\", \" \")\n .replace(/\\.\\d+Z$/, \"\");\n}\n\nfunction formatMessage(ts: string, user: string, text: string, indent = \"\"): string {\n const prefix = `[${formatTs(ts)}] ${user}: `;\n const lines = text.split(\"\\n\");\n const firstLine = `${indent}${prefix}${lines[0]}`;\n if (lines.length === 1) return firstLine;\n // All continuation lines get same indent as content start\n const contentIndent = indent + \" \".repeat(prefix.length);\n return [firstLine, ...lines.slice(1).map((l) => contentIndent + l)].join(\"\\n\");\n}\n\nexport async function downloadChannel(channelId: string, botToken: string): Promise<void> {\n const client = new WebClient(botToken, { logLevel: LogLevel.ERROR });\n\n console.error(`Fetching channel info for ${channelId}...`);\n\n // Get channel info\n let channelName = channelId;\n try {\n const info = await client.conversations.info({ channel: channelId });\n channelName = typeof info.channel?.name === \"string\" ? info.channel.name : channelId;\n } catch {\n // DM channels don't have names, that's fine\n }\n\n console.error(`Downloading history for #${channelName} (${channelId})...`);\n\n // Fetch all messages\n const messages: Message[] = [];\n let cursor: string | undefined;\n\n do {\n const response = await client.conversations.history({\n channel: channelId,\n limit: 200,\n cursor,\n });\n\n if (response.messages) {\n messages.push(...(response.messages as Message[]));\n }\n\n cursor = response.response_metadata?.next_cursor;\n console.error(` Fetched ${messages.length} messages...`);\n } while (cursor);\n\n // Reverse to chronological order\n messages.reverse();\n\n // Build map of thread replies\n const threadReplies = new Map<string, Message[]>();\n const threadsToFetch = messages.filter((m) => m.reply_count && m.reply_count > 0);\n\n console.error(`Fetching ${threadsToFetch.length} threads...`);\n\n for (let i = 0; i < threadsToFetch.length; i++) {\n const parent = threadsToFetch[i];\n console.error(` Thread ${i + 1}/${threadsToFetch.length} (${parent.reply_count} replies)...`);\n\n const replies: Message[] = [];\n let threadCursor: string | undefined;\n\n do {\n const response = await client.conversations.replies({\n channel: channelId,\n ts: parent.ts,\n limit: 200,\n cursor: threadCursor,\n });\n\n if (response.messages) {\n // Skip the first message (it's the parent)\n replies.push(...(response.messages as Message[]).slice(1));\n }\n\n threadCursor = response.response_metadata?.next_cursor;\n } while (threadCursor);\n\n threadReplies.set(parent.ts, replies);\n }\n\n // Output messages with thread replies interleaved\n let totalReplies = 0;\n for (const msg of messages) {\n // Output the message\n console.log(formatMessage(msg.ts, msg.user || \"unknown\", msg.text || \"\"));\n\n // Output thread replies right after parent (indented)\n const replies = threadReplies.get(msg.ts);\n if (replies) {\n for (const reply of replies) {\n console.log(formatMessage(reply.ts, reply.user || \"unknown\", reply.text || \"\", \" \"));\n totalReplies++;\n }\n }\n }\n\n console.error(`Done! ${messages.length} messages, ${totalReplies} thread replies`);\n}\n"]}
package/dist/download.js CHANGED
@@ -23,7 +23,7 @@ export async function downloadChannel(channelId, botToken) {
23
23
  let channelName = channelId;
24
24
  try {
25
25
  const info = await client.conversations.info({ channel: channelId });
26
- channelName = info.channel?.name || channelId;
26
+ channelName = typeof info.channel?.name === "string" ? info.channel.name : channelId;
27
27
  }
28
28
  catch {
29
29
  // DM channels don't have names, that's fine
@@ -1 +1 @@
1
- {"version":3,"file":"download.js","sourceRoot":"","sources":["../src/download.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAWrD,SAAS,QAAQ,CAAC,EAAU;IAC1B,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;IAC7C,OAAO,IAAI;SACR,WAAW,EAAE;SACb,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC;SACjB,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;AAC5B,CAAC;AAED,SAAS,aAAa,CAAC,EAAU,EAAE,IAAY,EAAE,IAAY,EAAE,MAAM,GAAG,EAAE;IACxE,MAAM,MAAM,GAAG,IAAI,QAAQ,CAAC,EAAE,CAAC,KAAK,IAAI,IAAI,CAAC;IAC7C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,SAAS,GAAG,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;IAClD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IACzC,0DAA0D;IAC1D,MAAM,aAAa,GAAG,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACzD,OAAO,CAAC,SAAS,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACjF,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,SAAiB,EAAE,QAAgB;IACvE,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,QAAQ,EAAE,EAAE,QAAQ,EAAE,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;IAErE,OAAO,CAAC,KAAK,CAAC,6BAA6B,SAAS,KAAK,CAAC,CAAC;IAE3D,mBAAmB;IACnB,IAAI,WAAW,GAAG,SAAS,CAAC;IAC5B,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC;QACrE,WAAW,GAAI,IAAI,CAAC,OAAe,EAAE,IAAI,IAAI,SAAS,CAAC;IACzD,CAAC;IAAC,MAAM,CAAC;QACP,4CAA4C;IAC9C,CAAC;IAED,OAAO,CAAC,KAAK,CAAC,4BAA4B,WAAW,KAAK,SAAS,MAAM,CAAC,CAAC;IAE3E,qBAAqB;IACrB,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,IAAI,MAA0B,CAAC;IAE/B,GAAG,CAAC;QACF,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC;YAClD,OAAO,EAAE,SAAS;YAClB,KAAK,EAAE,GAAG;YACV,MAAM;SACP,CAAC,CAAC;QAEH,IAAI,QAAQ,CAAC,QAAQ,EAAE,CAAC;YACtB,QAAQ,CAAC,IAAI,CAAC,GAAI,QAAQ,CAAC,QAAsB,CAAC,CAAC;QACrD,CAAC;QAED,MAAM,GAAG,QAAQ,CAAC,iBAAiB,EAAE,WAAW,CAAC;QACjD,OAAO,CAAC,KAAK,CAAC,aAAa,QAAQ,CAAC,MAAM,cAAc,CAAC,CAAC;IAC5D,CAAC,QAAQ,MAAM,EAAE;IAEjB,iCAAiC;IACjC,QAAQ,CAAC,OAAO,EAAE,CAAC;IAEnB,8BAA8B;IAC9B,MAAM,aAAa,GAAG,IAAI,GAAG,EAAqB,CAAC;IACnD,MAAM,cAAc,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,IAAI,CAAC,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC;IAElF,OAAO,CAAC,KAAK,CAAC,YAAY,cAAc,CAAC,MAAM,aAAa,CAAC,CAAC;IAE9D,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,cAAc,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC/C,MAAM,MAAM,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC;QACjC,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,cAAc,CAAC,MAAM,KAAK,MAAM,CAAC,WAAW,cAAc,CAAC,CAAC;QAE/F,MAAM,OAAO,GAAc,EAAE,CAAC;QAC9B,IAAI,YAAgC,CAAC;QAErC,GAAG,CAAC;YACF,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC;gBAClD,OAAO,EAAE,SAAS;gBAClB,EAAE,EAAE,MAAM,CAAC,EAAE;gBACb,KAAK,EAAE,GAAG;gBACV,MAAM,EAAE,YAAY;aACrB,CAAC,CAAC;YAEH,IAAI,QAAQ,CAAC,QAAQ,EAAE,CAAC;gBACtB,2CAA2C;gBAC3C,OAAO,CAAC,IAAI,CAAC,GAAI,QAAQ,CAAC,QAAsB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;YAC7D,CAAC;YAED,YAAY,GAAG,QAAQ,CAAC,iBAAiB,EAAE,WAAW,CAAC;QACzD,CAAC,QAAQ,YAAY,EAAE;QAEvB,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;IACxC,CAAC;IAED,kDAAkD;IAClD,IAAI,YAAY,GAAG,CAAC,CAAC;IACrB,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;QAC3B,qBAAqB;QACrB,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,IAAI,IAAI,SAAS,EAAE,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC;QAE1E,sDAAsD;QACtD,MAAM,OAAO,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC1C,IAAI,OAAO,EAAE,CAAC;YACZ,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;gBAC5B,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,IAAI,SAAS,EAAE,KAAK,CAAC,IAAI,IAAI,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC;gBACtF,YAAY,EAAE,CAAC;YACjB,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,CAAC,KAAK,CAAC,SAAS,QAAQ,CAAC,MAAM,cAAc,YAAY,iBAAiB,CAAC,CAAC;AACrF,CAAC","sourcesContent":["import { LogLevel, WebClient } from \"@slack/web-api\";\n\ninterface Message {\n ts: string;\n user?: string;\n text?: string;\n thread_ts?: string;\n reply_count?: number;\n files?: Array<{ name: string; url_private?: string }>;\n}\n\nfunction formatTs(ts: string): string {\n const date = new Date(parseFloat(ts) * 1000);\n return date\n .toISOString()\n .replace(\"T\", \" \")\n .replace(/\\.\\d+Z$/, \"\");\n}\n\nfunction formatMessage(ts: string, user: string, text: string, indent = \"\"): string {\n const prefix = `[${formatTs(ts)}] ${user}: `;\n const lines = text.split(\"\\n\");\n const firstLine = `${indent}${prefix}${lines[0]}`;\n if (lines.length === 1) return firstLine;\n // All continuation lines get same indent as content start\n const contentIndent = indent + \" \".repeat(prefix.length);\n return [firstLine, ...lines.slice(1).map((l) => contentIndent + l)].join(\"\\n\");\n}\n\nexport async function downloadChannel(channelId: string, botToken: string): Promise<void> {\n const client = new WebClient(botToken, { logLevel: LogLevel.ERROR });\n\n console.error(`Fetching channel info for ${channelId}...`);\n\n // Get channel info\n let channelName = channelId;\n try {\n const info = await client.conversations.info({ channel: channelId });\n channelName = (info.channel as any)?.name || channelId;\n } catch {\n // DM channels don't have names, that's fine\n }\n\n console.error(`Downloading history for #${channelName} (${channelId})...`);\n\n // Fetch all messages\n const messages: Message[] = [];\n let cursor: string | undefined;\n\n do {\n const response = await client.conversations.history({\n channel: channelId,\n limit: 200,\n cursor,\n });\n\n if (response.messages) {\n messages.push(...(response.messages as Message[]));\n }\n\n cursor = response.response_metadata?.next_cursor;\n console.error(` Fetched ${messages.length} messages...`);\n } while (cursor);\n\n // Reverse to chronological order\n messages.reverse();\n\n // Build map of thread replies\n const threadReplies = new Map<string, Message[]>();\n const threadsToFetch = messages.filter((m) => m.reply_count && m.reply_count > 0);\n\n console.error(`Fetching ${threadsToFetch.length} threads...`);\n\n for (let i = 0; i < threadsToFetch.length; i++) {\n const parent = threadsToFetch[i];\n console.error(` Thread ${i + 1}/${threadsToFetch.length} (${parent.reply_count} replies)...`);\n\n const replies: Message[] = [];\n let threadCursor: string | undefined;\n\n do {\n const response = await client.conversations.replies({\n channel: channelId,\n ts: parent.ts,\n limit: 200,\n cursor: threadCursor,\n });\n\n if (response.messages) {\n // Skip the first message (it's the parent)\n replies.push(...(response.messages as Message[]).slice(1));\n }\n\n threadCursor = response.response_metadata?.next_cursor;\n } while (threadCursor);\n\n threadReplies.set(parent.ts, replies);\n }\n\n // Output messages with thread replies interleaved\n let totalReplies = 0;\n for (const msg of messages) {\n // Output the message\n console.log(formatMessage(msg.ts, msg.user || \"unknown\", msg.text || \"\"));\n\n // Output thread replies right after parent (indented)\n const replies = threadReplies.get(msg.ts);\n if (replies) {\n for (const reply of replies) {\n console.log(formatMessage(reply.ts, reply.user || \"unknown\", reply.text || \"\", \" \"));\n totalReplies++;\n }\n }\n }\n\n console.error(`Done! ${messages.length} messages, ${totalReplies} thread replies`);\n}\n"]}
1
+ {"version":3,"file":"download.js","sourceRoot":"","sources":["../src/download.ts"],"names":[],"mappings":"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","sourcesContent":["import { LogLevel, WebClient } from \"@slack/web-api\";\n\ninterface Message {\n ts: string;\n user?: string;\n text?: string;\n thread_ts?: string;\n reply_count?: number;\n files?: Array<{ name: string; url_private?: string }>;\n}\n\nfunction formatTs(ts: string): string {\n const date = new Date(parseFloat(ts) * 1000);\n return date\n .toISOString()\n .replace(\"T\", \" \")\n .replace(/\\.\\d+Z$/, \"\");\n}\n\nfunction formatMessage(ts: string, user: string, text: string, indent = \"\"): string {\n const prefix = `[${formatTs(ts)}] ${user}: `;\n const lines = text.split(\"\\n\");\n const firstLine = `${indent}${prefix}${lines[0]}`;\n if (lines.length === 1) return firstLine;\n // All continuation lines get same indent as content start\n const contentIndent = indent + \" \".repeat(prefix.length);\n return [firstLine, ...lines.slice(1).map((l) => contentIndent + l)].join(\"\\n\");\n}\n\nexport async function downloadChannel(channelId: string, botToken: string): Promise<void> {\n const client = new WebClient(botToken, { logLevel: LogLevel.ERROR });\n\n console.error(`Fetching channel info for ${channelId}...`);\n\n // Get channel info\n let channelName = channelId;\n try {\n const info = await client.conversations.info({ channel: channelId });\n channelName = typeof info.channel?.name === \"string\" ? info.channel.name : channelId;\n } catch {\n // DM channels don't have names, that's fine\n }\n\n console.error(`Downloading history for #${channelName} (${channelId})...`);\n\n // Fetch all messages\n const messages: Message[] = [];\n let cursor: string | undefined;\n\n do {\n const response = await client.conversations.history({\n channel: channelId,\n limit: 200,\n cursor,\n });\n\n if (response.messages) {\n messages.push(...(response.messages as Message[]));\n }\n\n cursor = response.response_metadata?.next_cursor;\n console.error(` Fetched ${messages.length} messages...`);\n } while (cursor);\n\n // Reverse to chronological order\n messages.reverse();\n\n // Build map of thread replies\n const threadReplies = new Map<string, Message[]>();\n const threadsToFetch = messages.filter((m) => m.reply_count && m.reply_count > 0);\n\n console.error(`Fetching ${threadsToFetch.length} threads...`);\n\n for (let i = 0; i < threadsToFetch.length; i++) {\n const parent = threadsToFetch[i];\n console.error(` Thread ${i + 1}/${threadsToFetch.length} (${parent.reply_count} replies)...`);\n\n const replies: Message[] = [];\n let threadCursor: string | undefined;\n\n do {\n const response = await client.conversations.replies({\n channel: channelId,\n ts: parent.ts,\n limit: 200,\n cursor: threadCursor,\n });\n\n if (response.messages) {\n // Skip the first message (it's the parent)\n replies.push(...(response.messages as Message[]).slice(1));\n }\n\n threadCursor = response.response_metadata?.next_cursor;\n } while (threadCursor);\n\n threadReplies.set(parent.ts, replies);\n }\n\n // Output messages with thread replies interleaved\n let totalReplies = 0;\n for (const msg of messages) {\n // Output the message\n console.log(formatMessage(msg.ts, msg.user || \"unknown\", msg.text || \"\"));\n\n // Output thread replies right after parent (indented)\n const replies = threadReplies.get(msg.ts);\n if (replies) {\n for (const reply of replies) {\n console.log(formatMessage(reply.ts, reply.user || \"unknown\", reply.text || \"\", \" \"));\n totalReplies++;\n }\n }\n }\n\n console.error(`Done! ${messages.length} messages, ${totalReplies} thread replies`);\n}\n"]}