@geminixiang/mama 0.1.0

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 (83) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +158 -0
  3. package/dist/adapter.d.ts +38 -0
  4. package/dist/adapter.d.ts.map +1 -0
  5. package/dist/adapter.js +2 -0
  6. package/dist/adapter.js.map +1 -0
  7. package/dist/adapters/slack/bot.d.ts +130 -0
  8. package/dist/adapters/slack/bot.d.ts.map +1 -0
  9. package/dist/adapters/slack/bot.js +516 -0
  10. package/dist/adapters/slack/bot.js.map +1 -0
  11. package/dist/adapters/slack/context.d.ts +11 -0
  12. package/dist/adapters/slack/context.d.ts.map +1 -0
  13. package/dist/adapters/slack/context.js +178 -0
  14. package/dist/adapters/slack/context.js.map +1 -0
  15. package/dist/adapters/slack/index.d.ts +3 -0
  16. package/dist/adapters/slack/index.d.ts.map +1 -0
  17. package/dist/adapters/slack/index.js +3 -0
  18. package/dist/adapters/slack/index.js.map +1 -0
  19. package/dist/adapters/slack/tools/attach.d.ts +12 -0
  20. package/dist/adapters/slack/tools/attach.d.ts.map +1 -0
  21. package/dist/adapters/slack/tools/attach.js +38 -0
  22. package/dist/adapters/slack/tools/attach.js.map +1 -0
  23. package/dist/agent.d.ts +26 -0
  24. package/dist/agent.d.ts.map +1 -0
  25. package/dist/agent.js +763 -0
  26. package/dist/agent.js.map +1 -0
  27. package/dist/config.d.ts +10 -0
  28. package/dist/config.d.ts.map +1 -0
  29. package/dist/config.js +54 -0
  30. package/dist/config.js.map +1 -0
  31. package/dist/context.d.ts +34 -0
  32. package/dist/context.d.ts.map +1 -0
  33. package/dist/context.js +110 -0
  34. package/dist/context.js.map +1 -0
  35. package/dist/download.d.ts +2 -0
  36. package/dist/download.d.ts.map +1 -0
  37. package/dist/download.js +89 -0
  38. package/dist/download.js.map +1 -0
  39. package/dist/events.d.ts +57 -0
  40. package/dist/events.d.ts.map +1 -0
  41. package/dist/events.js +310 -0
  42. package/dist/events.js.map +1 -0
  43. package/dist/log.d.ts +39 -0
  44. package/dist/log.d.ts.map +1 -0
  45. package/dist/log.js +222 -0
  46. package/dist/log.js.map +1 -0
  47. package/dist/main.d.ts +3 -0
  48. package/dist/main.d.ts.map +1 -0
  49. package/dist/main.js +247 -0
  50. package/dist/main.js.map +1 -0
  51. package/dist/sandbox.d.ts +34 -0
  52. package/dist/sandbox.d.ts.map +1 -0
  53. package/dist/sandbox.js +183 -0
  54. package/dist/sandbox.js.map +1 -0
  55. package/dist/store.d.ts +60 -0
  56. package/dist/store.d.ts.map +1 -0
  57. package/dist/store.js +180 -0
  58. package/dist/store.js.map +1 -0
  59. package/dist/tools/bash.d.ts +10 -0
  60. package/dist/tools/bash.d.ts.map +1 -0
  61. package/dist/tools/bash.js +78 -0
  62. package/dist/tools/bash.js.map +1 -0
  63. package/dist/tools/edit.d.ts +11 -0
  64. package/dist/tools/edit.d.ts.map +1 -0
  65. package/dist/tools/edit.js +131 -0
  66. package/dist/tools/edit.js.map +1 -0
  67. package/dist/tools/index.d.ts +7 -0
  68. package/dist/tools/index.d.ts.map +1 -0
  69. package/dist/tools/index.js +19 -0
  70. package/dist/tools/index.js.map +1 -0
  71. package/dist/tools/read.d.ts +11 -0
  72. package/dist/tools/read.d.ts.map +1 -0
  73. package/dist/tools/read.js +134 -0
  74. package/dist/tools/read.js.map +1 -0
  75. package/dist/tools/truncate.d.ts +57 -0
  76. package/dist/tools/truncate.d.ts.map +1 -0
  77. package/dist/tools/truncate.js +184 -0
  78. package/dist/tools/truncate.js.map +1 -0
  79. package/dist/tools/write.d.ts +10 -0
  80. package/dist/tools/write.d.ts.map +1 -0
  81. package/dist/tools/write.js +33 -0
  82. package/dist/tools/write.js.map +1 -0
  83. package/package.json +57 -0
@@ -0,0 +1,183 @@
1
+ import { spawn } from "child_process";
2
+ export function parseSandboxArg(value) {
3
+ if (value === "host") {
4
+ return { type: "host" };
5
+ }
6
+ if (value.startsWith("docker:")) {
7
+ const container = value.slice("docker:".length);
8
+ if (!container) {
9
+ console.error("Error: docker sandbox requires container name (e.g., docker:mama-sandbox)");
10
+ process.exit(1);
11
+ }
12
+ return { type: "docker", container };
13
+ }
14
+ console.error(`Error: Invalid sandbox type '${value}'. Use 'host' or 'docker:<container-name>'`);
15
+ process.exit(1);
16
+ }
17
+ export async function validateSandbox(config) {
18
+ if (config.type === "host") {
19
+ return;
20
+ }
21
+ // Check if Docker is available
22
+ try {
23
+ await execSimple("docker", ["--version"]);
24
+ }
25
+ catch {
26
+ console.error("Error: Docker is not installed or not in PATH");
27
+ process.exit(1);
28
+ }
29
+ // Check if container exists and is running
30
+ try {
31
+ const result = await execSimple("docker", ["inspect", "-f", "{{.State.Running}}", config.container]);
32
+ if (result.trim() !== "true") {
33
+ console.error(`Error: Container '${config.container}' is not running.`);
34
+ console.error(`Start it with: docker start ${config.container}`);
35
+ process.exit(1);
36
+ }
37
+ }
38
+ catch {
39
+ console.error(`Error: Container '${config.container}' does not exist.`);
40
+ console.error("Create it with: ./docker.sh create <data-dir>");
41
+ process.exit(1);
42
+ }
43
+ console.log(` Docker container '${config.container}' is running.`);
44
+ }
45
+ function execSimple(cmd, args) {
46
+ return new Promise((resolve, reject) => {
47
+ const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
48
+ let stdout = "";
49
+ let stderr = "";
50
+ child.stdout?.on("data", (d) => {
51
+ stdout += d;
52
+ });
53
+ child.stderr?.on("data", (d) => {
54
+ stderr += d;
55
+ });
56
+ child.on("close", (code) => {
57
+ if (code === 0)
58
+ resolve(stdout);
59
+ else
60
+ reject(new Error(stderr || `Exit code ${code}`));
61
+ });
62
+ });
63
+ }
64
+ /**
65
+ * Create an executor that runs commands either on host or in Docker container
66
+ */
67
+ export function createExecutor(config) {
68
+ if (config.type === "host") {
69
+ return new HostExecutor();
70
+ }
71
+ return new DockerExecutor(config.container);
72
+ }
73
+ class HostExecutor {
74
+ async exec(command, options) {
75
+ return new Promise((resolve, reject) => {
76
+ const shell = process.platform === "win32" ? "cmd" : "sh";
77
+ const shellArgs = process.platform === "win32" ? ["/c"] : ["-c"];
78
+ const child = spawn(shell, [...shellArgs, command], {
79
+ detached: true,
80
+ stdio: ["ignore", "pipe", "pipe"],
81
+ });
82
+ let stdout = "";
83
+ let stderr = "";
84
+ let timedOut = false;
85
+ const timeoutHandle = options?.timeout && options.timeout > 0
86
+ ? setTimeout(() => {
87
+ timedOut = true;
88
+ killProcessTree(child.pid);
89
+ }, options.timeout * 1000)
90
+ : undefined;
91
+ const onAbort = () => {
92
+ if (child.pid)
93
+ killProcessTree(child.pid);
94
+ };
95
+ if (options?.signal) {
96
+ if (options.signal.aborted) {
97
+ onAbort();
98
+ }
99
+ else {
100
+ options.signal.addEventListener("abort", onAbort, { once: true });
101
+ }
102
+ }
103
+ child.stdout?.on("data", (data) => {
104
+ stdout += data.toString();
105
+ if (stdout.length > 10 * 1024 * 1024) {
106
+ stdout = stdout.slice(0, 10 * 1024 * 1024);
107
+ }
108
+ });
109
+ child.stderr?.on("data", (data) => {
110
+ stderr += data.toString();
111
+ if (stderr.length > 10 * 1024 * 1024) {
112
+ stderr = stderr.slice(0, 10 * 1024 * 1024);
113
+ }
114
+ });
115
+ child.on("close", (code) => {
116
+ if (timeoutHandle)
117
+ clearTimeout(timeoutHandle);
118
+ if (options?.signal) {
119
+ options.signal.removeEventListener("abort", onAbort);
120
+ }
121
+ if (options?.signal?.aborted) {
122
+ reject(new Error(`${stdout}\n${stderr}\nCommand aborted`.trim()));
123
+ return;
124
+ }
125
+ if (timedOut) {
126
+ reject(new Error(`${stdout}\n${stderr}\nCommand timed out after ${options?.timeout} seconds`.trim()));
127
+ return;
128
+ }
129
+ resolve({ stdout, stderr, code: code ?? 0 });
130
+ });
131
+ });
132
+ }
133
+ getWorkspacePath(hostPath) {
134
+ return hostPath;
135
+ }
136
+ }
137
+ class DockerExecutor {
138
+ container;
139
+ constructor(container) {
140
+ this.container = container;
141
+ }
142
+ async exec(command, options) {
143
+ // Wrap command for docker exec
144
+ const dockerCmd = `docker exec ${this.container} sh -c ${shellEscape(command)}`;
145
+ const hostExecutor = new HostExecutor();
146
+ return hostExecutor.exec(dockerCmd, options);
147
+ }
148
+ getWorkspacePath(_hostPath) {
149
+ // Docker container sees /workspace
150
+ return "/workspace";
151
+ }
152
+ }
153
+ function killProcessTree(pid) {
154
+ if (process.platform === "win32") {
155
+ try {
156
+ spawn("taskkill", ["/F", "/T", "/PID", String(pid)], {
157
+ stdio: "ignore",
158
+ detached: true,
159
+ });
160
+ }
161
+ catch {
162
+ // Ignore errors
163
+ }
164
+ }
165
+ else {
166
+ try {
167
+ process.kill(-pid, "SIGKILL");
168
+ }
169
+ catch {
170
+ try {
171
+ process.kill(pid, "SIGKILL");
172
+ }
173
+ catch {
174
+ // Process already dead
175
+ }
176
+ }
177
+ }
178
+ }
179
+ function shellEscape(s) {
180
+ // Escape for passing to sh -c
181
+ return `'${s.replace(/'/g, "'\\''")}'`;
182
+ }
183
+ //# sourceMappingURL=sandbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox.js","sourceRoot":"","sources":["../src/sandbox.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from \"child_process\";\n\nexport type SandboxConfig = { type: \"host\" } | { type: \"docker\"; container: string };\n\nexport function parseSandboxArg(value: string): SandboxConfig {\n\tif (value === \"host\") {\n\t\treturn { type: \"host\" };\n\t}\n\tif (value.startsWith(\"docker:\")) {\n\t\tconst container = value.slice(\"docker:\".length);\n\t\tif (!container) {\n\t\t\tconsole.error(\"Error: docker sandbox requires container name (e.g., docker:mama-sandbox)\");\n\t\t\tprocess.exit(1);\n\t\t}\n\t\treturn { type: \"docker\", container };\n\t}\n\tconsole.error(`Error: Invalid sandbox type '${value}'. Use 'host' or 'docker:<container-name>'`);\n\tprocess.exit(1);\n}\n\nexport async function validateSandbox(config: SandboxConfig): Promise<void> {\n\tif (config.type === \"host\") {\n\t\treturn;\n\t}\n\n\t// Check if Docker is available\n\ttry {\n\t\tawait execSimple(\"docker\", [\"--version\"]);\n\t} catch {\n\t\tconsole.error(\"Error: Docker is not installed or not in PATH\");\n\t\tprocess.exit(1);\n\t}\n\n\t// Check if container exists and is running\n\ttry {\n\t\tconst result = await execSimple(\"docker\", [\"inspect\", \"-f\", \"{{.State.Running}}\", config.container]);\n\t\tif (result.trim() !== \"true\") {\n\t\t\tconsole.error(`Error: Container '${config.container}' is not running.`);\n\t\t\tconsole.error(`Start it with: docker start ${config.container}`);\n\t\t\tprocess.exit(1);\n\t\t}\n\t} catch {\n\t\tconsole.error(`Error: Container '${config.container}' does not exist.`);\n\t\tconsole.error(\"Create it with: ./docker.sh create <data-dir>\");\n\t\tprocess.exit(1);\n\t}\n\n\tconsole.log(` Docker container '${config.container}' is running.`);\n}\n\nfunction execSimple(cmd: string, args: string[]): Promise<string> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst child = spawn(cmd, args, { stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tchild.stdout?.on(\"data\", (d) => {\n\t\t\tstdout += d;\n\t\t});\n\t\tchild.stderr?.on(\"data\", (d) => {\n\t\t\tstderr += d;\n\t\t});\n\t\tchild.on(\"close\", (code) => {\n\t\t\tif (code === 0) resolve(stdout);\n\t\t\telse reject(new Error(stderr || `Exit code ${code}`));\n\t\t});\n\t});\n}\n\n/**\n * Create an executor that runs commands either on host or in Docker container\n */\nexport function createExecutor(config: SandboxConfig): Executor {\n\tif (config.type === \"host\") {\n\t\treturn new HostExecutor();\n\t}\n\treturn new DockerExecutor(config.container);\n}\n\nexport interface Executor {\n\t/**\n\t * Execute a bash command\n\t */\n\texec(command: string, options?: ExecOptions): Promise<ExecResult>;\n\n\t/**\n\t * Get the workspace path prefix for this executor\n\t * Host: returns the actual path\n\t * Docker: returns /workspace\n\t */\n\tgetWorkspacePath(hostPath: string): string;\n}\n\nexport interface ExecOptions {\n\ttimeout?: number;\n\tsignal?: AbortSignal;\n}\n\nexport interface ExecResult {\n\tstdout: string;\n\tstderr: string;\n\tcode: number;\n}\n\nclass HostExecutor implements Executor {\n\tasync exec(command: string, options?: ExecOptions): Promise<ExecResult> {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst shell = process.platform === \"win32\" ? \"cmd\" : \"sh\";\n\t\t\tconst shellArgs = process.platform === \"win32\" ? [\"/c\"] : [\"-c\"];\n\n\t\t\tconst child = spawn(shell, [...shellArgs, command], {\n\t\t\t\tdetached: true,\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t});\n\n\t\t\tlet stdout = \"\";\n\t\t\tlet stderr = \"\";\n\t\t\tlet timedOut = false;\n\n\t\t\tconst timeoutHandle =\n\t\t\t\toptions?.timeout && options.timeout > 0\n\t\t\t\t\t? setTimeout(() => {\n\t\t\t\t\t\t\ttimedOut = true;\n\t\t\t\t\t\t\tkillProcessTree(child.pid!);\n\t\t\t\t\t\t}, options.timeout * 1000)\n\t\t\t\t\t: undefined;\n\n\t\t\tconst onAbort = () => {\n\t\t\t\tif (child.pid) killProcessTree(child.pid);\n\t\t\t};\n\n\t\t\tif (options?.signal) {\n\t\t\t\tif (options.signal.aborted) {\n\t\t\t\t\tonAbort();\n\t\t\t\t} else {\n\t\t\t\t\toptions.signal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tchild.stdout?.on(\"data\", (data) => {\n\t\t\t\tstdout += data.toString();\n\t\t\t\tif (stdout.length > 10 * 1024 * 1024) {\n\t\t\t\t\tstdout = stdout.slice(0, 10 * 1024 * 1024);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tchild.stderr?.on(\"data\", (data) => {\n\t\t\t\tstderr += data.toString();\n\t\t\t\tif (stderr.length > 10 * 1024 * 1024) {\n\t\t\t\t\tstderr = stderr.slice(0, 10 * 1024 * 1024);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tchild.on(\"close\", (code) => {\n\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\tif (options?.signal) {\n\t\t\t\t\toptions.signal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t}\n\n\t\t\t\tif (options?.signal?.aborted) {\n\t\t\t\t\treject(new Error(`${stdout}\\n${stderr}\\nCommand aborted`.trim()));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (timedOut) {\n\t\t\t\t\treject(new Error(`${stdout}\\n${stderr}\\nCommand timed out after ${options?.timeout} seconds`.trim()));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tresolve({ stdout, stderr, code: code ?? 0 });\n\t\t\t});\n\t\t});\n\t}\n\n\tgetWorkspacePath(hostPath: string): string {\n\t\treturn hostPath;\n\t}\n}\n\nclass DockerExecutor implements Executor {\n\tconstructor(private container: string) {}\n\n\tasync exec(command: string, options?: ExecOptions): Promise<ExecResult> {\n\t\t// Wrap command for docker exec\n\t\tconst dockerCmd = `docker exec ${this.container} sh -c ${shellEscape(command)}`;\n\t\tconst hostExecutor = new HostExecutor();\n\t\treturn hostExecutor.exec(dockerCmd, options);\n\t}\n\n\tgetWorkspacePath(_hostPath: string): string {\n\t\t// Docker container sees /workspace\n\t\treturn \"/workspace\";\n\t}\n}\n\nfunction killProcessTree(pid: number): void {\n\tif (process.platform === \"win32\") {\n\t\ttry {\n\t\t\tspawn(\"taskkill\", [\"/F\", \"/T\", \"/PID\", String(pid)], {\n\t\t\t\tstdio: \"ignore\",\n\t\t\t\tdetached: true,\n\t\t\t});\n\t\t} catch {\n\t\t\t// Ignore errors\n\t\t}\n\t} else {\n\t\ttry {\n\t\t\tprocess.kill(-pid, \"SIGKILL\");\n\t\t} catch {\n\t\t\ttry {\n\t\t\t\tprocess.kill(pid, \"SIGKILL\");\n\t\t\t} catch {\n\t\t\t\t// Process already dead\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction shellEscape(s: string): string {\n\t// Escape for passing to sh -c\n\treturn `'${s.replace(/'/g, \"'\\\\''\")}'`;\n}\n"]}
@@ -0,0 +1,60 @@
1
+ export interface Attachment {
2
+ original: string;
3
+ local: string;
4
+ }
5
+ export interface LoggedMessage {
6
+ date: string;
7
+ ts: string;
8
+ user: string;
9
+ userName?: string;
10
+ displayName?: string;
11
+ text: string;
12
+ attachments: Attachment[];
13
+ isBot: boolean;
14
+ }
15
+ export interface ChannelStoreConfig {
16
+ workingDir: string;
17
+ botToken: string;
18
+ }
19
+ export declare class ChannelStore {
20
+ private workingDir;
21
+ private botToken;
22
+ private pendingDownloads;
23
+ private isDownloading;
24
+ private recentlyLogged;
25
+ constructor(config: ChannelStoreConfig);
26
+ /**
27
+ * Get or create the directory for a channel/DM
28
+ */
29
+ getChannelDir(channelId: string): string;
30
+ /**
31
+ * Generate a unique local filename for an attachment
32
+ */
33
+ generateLocalFilename(originalName: string, timestamp: string): string;
34
+ /**
35
+ * Process attachments from a Slack message event
36
+ * Returns attachment metadata and queues downloads
37
+ */
38
+ processAttachments(channelId: string, files: Array<{
39
+ name?: string;
40
+ url_private_download?: string;
41
+ url_private?: string;
42
+ }>, timestamp: string): Attachment[];
43
+ /**
44
+ * Log a message to the channel's log.jsonl
45
+ * Returns false if message was already logged (duplicate)
46
+ */
47
+ logMessage(channelId: string, message: LoggedMessage): Promise<boolean>;
48
+ /**
49
+ * Log a bot response
50
+ */
51
+ logBotResponse(channelId: string, text: string, ts: string): Promise<void>;
52
+ /**
53
+ * Get the timestamp of the last logged message for a channel
54
+ * Returns null if no log exists
55
+ */
56
+ getLastTimestamp(channelId: string): string | null;
57
+ private processDownloadQueue;
58
+ private downloadAttachment;
59
+ }
60
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,UAAU;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,KAAK,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAQD,qBAAa,YAAY;IACxB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,gBAAgB,CAAyB;IACjD,OAAO,CAAC,aAAa,CAAS;IAG9B,OAAO,CAAC,cAAc,CAA6B;IAEnD,YAAY,MAAM,EAAE,kBAAkB,EAQrC;IAED;;OAEG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAMvC;IAED;;OAEG;IACH,qBAAqB,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAMrE;IAED;;;OAGG;IACH,kBAAkB,CACjB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,EACpF,SAAS,EAAE,MAAM,GACf,UAAU,EAAE,CA2Bd;IAED;;;OAGG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,CA8B5E;IAED;;OAEG;IACG,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAS/E;IAED;;;OAGG;IACH,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkBjD;YAKa,oBAAoB;YAwBpB,kBAAkB;CAsBhC","sourcesContent":["import { existsSync, mkdirSync, readFileSync } from \"fs\";\nimport { appendFile, writeFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport * as log from \"./log.js\";\n\nexport interface Attachment {\n\toriginal: string; // original filename from uploader\n\tlocal: string; // path relative to working dir (e.g., \"C12345/attachments/1732531234567_file.png\")\n}\n\nexport interface LoggedMessage {\n\tdate: string; // ISO 8601 date (e.g., \"2025-11-26T10:44:00.000Z\") for easy grepping\n\tts: string; // slack timestamp or epoch ms\n\tuser: string; // user ID (or \"bot\" for bot responses)\n\tuserName?: string; // handle (e.g., \"mario\")\n\tdisplayName?: string; // display name (e.g., \"Mario Zechner\")\n\ttext: string;\n\tattachments: Attachment[];\n\tisBot: boolean;\n}\n\nexport interface ChannelStoreConfig {\n\tworkingDir: string;\n\tbotToken: string; // needed for authenticated file downloads\n}\n\ninterface PendingDownload {\n\tchannelId: string;\n\tlocalPath: string; // relative path\n\turl: string;\n}\n\nexport class ChannelStore {\n\tprivate workingDir: string;\n\tprivate botToken: string;\n\tprivate pendingDownloads: PendingDownload[] = [];\n\tprivate isDownloading = false;\n\t// Track recently logged message timestamps to prevent duplicates\n\t// Key: \"channelId:ts\", automatically cleaned up after 60 seconds\n\tprivate recentlyLogged = new Map<string, number>();\n\n\tconstructor(config: ChannelStoreConfig) {\n\t\tthis.workingDir = config.workingDir;\n\t\tthis.botToken = config.botToken;\n\n\t\t// Ensure working directory exists\n\t\tif (!existsSync(this.workingDir)) {\n\t\t\tmkdirSync(this.workingDir, { recursive: true });\n\t\t}\n\t}\n\n\t/**\n\t * Get or create the directory for a channel/DM\n\t */\n\tgetChannelDir(channelId: string): string {\n\t\tconst dir = join(this.workingDir, channelId);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\t\treturn dir;\n\t}\n\n\t/**\n\t * Generate a unique local filename for an attachment\n\t */\n\tgenerateLocalFilename(originalName: string, timestamp: string): string {\n\t\t// Convert slack timestamp (1234567890.123456) to milliseconds\n\t\tconst ts = Math.floor(parseFloat(timestamp) * 1000);\n\t\t// Sanitize original name (remove problematic characters)\n\t\tconst sanitized = originalName.replace(/[^a-zA-Z0-9._-]/g, \"_\");\n\t\treturn `${ts}_${sanitized}`;\n\t}\n\n\t/**\n\t * Process attachments from a Slack message event\n\t * Returns attachment metadata and queues downloads\n\t */\n\tprocessAttachments(\n\t\tchannelId: string,\n\t\tfiles: Array<{ name?: string; url_private_download?: string; url_private?: string }>,\n\t\ttimestamp: string,\n\t): Attachment[] {\n\t\tconst attachments: Attachment[] = [];\n\n\t\tfor (const file of files) {\n\t\t\tconst url = file.url_private_download || file.url_private;\n\t\t\tif (!url) continue;\n\t\t\tif (!file.name) {\n\t\t\t\tlog.logWarning(\"Attachment missing name, skipping\", url);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst filename = this.generateLocalFilename(file.name, timestamp);\n\t\t\tconst localPath = `${channelId}/attachments/${filename}`;\n\n\t\t\tattachments.push({\n\t\t\t\toriginal: file.name,\n\t\t\t\tlocal: localPath,\n\t\t\t});\n\n\t\t\t// Queue for background download\n\t\t\tthis.pendingDownloads.push({ channelId, localPath, url });\n\t\t}\n\n\t\t// Trigger background download\n\t\tthis.processDownloadQueue();\n\n\t\treturn attachments;\n\t}\n\n\t/**\n\t * Log a message to the channel's log.jsonl\n\t * Returns false if message was already logged (duplicate)\n\t */\n\tasync logMessage(channelId: string, message: LoggedMessage): Promise<boolean> {\n\t\t// Check for duplicate (same channel + timestamp)\n\t\tconst dedupeKey = `${channelId}:${message.ts}`;\n\t\tif (this.recentlyLogged.has(dedupeKey)) {\n\t\t\treturn false; // Already logged\n\t\t}\n\n\t\t// Mark as logged and schedule cleanup after 60 seconds\n\t\tthis.recentlyLogged.set(dedupeKey, Date.now());\n\t\tsetTimeout(() => this.recentlyLogged.delete(dedupeKey), 60000);\n\n\t\tconst logPath = join(this.getChannelDir(channelId), \"log.jsonl\");\n\n\t\t// Ensure message has a date field\n\t\tif (!message.date) {\n\t\t\t// Parse timestamp to get date\n\t\t\tlet date: Date;\n\t\t\tif (message.ts.includes(\".\")) {\n\t\t\t\t// Slack timestamp format (1234567890.123456)\n\t\t\t\tdate = new Date(parseFloat(message.ts) * 1000);\n\t\t\t} else {\n\t\t\t\t// Epoch milliseconds\n\t\t\t\tdate = new Date(parseInt(message.ts, 10));\n\t\t\t}\n\t\t\tmessage.date = date.toISOString();\n\t\t}\n\n\t\tconst line = `${JSON.stringify(message)}\\n`;\n\t\tawait appendFile(logPath, line, \"utf-8\");\n\t\treturn true;\n\t}\n\n\t/**\n\t * Log a bot response\n\t */\n\tasync logBotResponse(channelId: string, text: string, ts: string): Promise<void> {\n\t\tawait this.logMessage(channelId, {\n\t\t\tdate: new Date().toISOString(),\n\t\t\tts,\n\t\t\tuser: \"bot\",\n\t\t\ttext,\n\t\t\tattachments: [],\n\t\t\tisBot: true,\n\t\t});\n\t}\n\n\t/**\n\t * Get the timestamp of the last logged message for a channel\n\t * Returns null if no log exists\n\t */\n\tgetLastTimestamp(channelId: string): string | null {\n\t\tconst logPath = join(this.workingDir, channelId, \"log.jsonl\");\n\t\tif (!existsSync(logPath)) {\n\t\t\treturn null;\n\t\t}\n\n\t\ttry {\n\t\t\tconst content = readFileSync(logPath, \"utf-8\");\n\t\t\tconst lines = content.trim().split(\"\\n\");\n\t\t\tif (lines.length === 0 || lines[0] === \"\") {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst lastLine = lines[lines.length - 1];\n\t\t\tconst message = JSON.parse(lastLine) as LoggedMessage;\n\t\t\treturn message.ts;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t/**\n\t * Process the download queue in the background\n\t */\n\tprivate async processDownloadQueue(): Promise<void> {\n\t\tif (this.isDownloading || this.pendingDownloads.length === 0) return;\n\n\t\tthis.isDownloading = true;\n\n\t\twhile (this.pendingDownloads.length > 0) {\n\t\t\tconst item = this.pendingDownloads.shift();\n\t\t\tif (!item) break;\n\n\t\t\ttry {\n\t\t\t\tawait this.downloadAttachment(item.localPath, item.url);\n\t\t\t\t// Success - could add success logging here if we have context\n\t\t\t} catch (error) {\n\t\t\t\tconst errorMsg = error instanceof Error ? error.message : String(error);\n\t\t\t\tlog.logWarning(`Failed to download attachment`, `${item.localPath}: ${errorMsg}`);\n\t\t\t}\n\t\t}\n\n\t\tthis.isDownloading = false;\n\t}\n\n\t/**\n\t * Download a single attachment\n\t */\n\tprivate async downloadAttachment(localPath: string, url: string): Promise<void> {\n\t\tconst filePath = join(this.workingDir, localPath);\n\n\t\t// Ensure directory exists\n\t\tconst dir = join(this.workingDir, localPath.substring(0, localPath.lastIndexOf(\"/\")));\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\n\t\tconst response = await fetch(url, {\n\t\t\theaders: {\n\t\t\t\tAuthorization: `Bearer ${this.botToken}`,\n\t\t\t},\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(`HTTP ${response.status}: ${response.statusText}`);\n\t\t}\n\n\t\tconst buffer = await response.arrayBuffer();\n\t\tawait writeFile(filePath, Buffer.from(buffer));\n\t}\n}\n"]}
package/dist/store.js ADDED
@@ -0,0 +1,180 @@
1
+ import { existsSync, mkdirSync, readFileSync } from "fs";
2
+ import { appendFile, writeFile } from "fs/promises";
3
+ import { join } from "path";
4
+ import * as log from "./log.js";
5
+ export class ChannelStore {
6
+ workingDir;
7
+ botToken;
8
+ pendingDownloads = [];
9
+ isDownloading = false;
10
+ // Track recently logged message timestamps to prevent duplicates
11
+ // Key: "channelId:ts", automatically cleaned up after 60 seconds
12
+ recentlyLogged = new Map();
13
+ constructor(config) {
14
+ this.workingDir = config.workingDir;
15
+ this.botToken = config.botToken;
16
+ // Ensure working directory exists
17
+ if (!existsSync(this.workingDir)) {
18
+ mkdirSync(this.workingDir, { recursive: true });
19
+ }
20
+ }
21
+ /**
22
+ * Get or create the directory for a channel/DM
23
+ */
24
+ getChannelDir(channelId) {
25
+ const dir = join(this.workingDir, channelId);
26
+ if (!existsSync(dir)) {
27
+ mkdirSync(dir, { recursive: true });
28
+ }
29
+ return dir;
30
+ }
31
+ /**
32
+ * Generate a unique local filename for an attachment
33
+ */
34
+ generateLocalFilename(originalName, timestamp) {
35
+ // Convert slack timestamp (1234567890.123456) to milliseconds
36
+ const ts = Math.floor(parseFloat(timestamp) * 1000);
37
+ // Sanitize original name (remove problematic characters)
38
+ const sanitized = originalName.replace(/[^a-zA-Z0-9._-]/g, "_");
39
+ return `${ts}_${sanitized}`;
40
+ }
41
+ /**
42
+ * Process attachments from a Slack message event
43
+ * Returns attachment metadata and queues downloads
44
+ */
45
+ processAttachments(channelId, files, timestamp) {
46
+ const attachments = [];
47
+ for (const file of files) {
48
+ const url = file.url_private_download || file.url_private;
49
+ if (!url)
50
+ continue;
51
+ if (!file.name) {
52
+ log.logWarning("Attachment missing name, skipping", url);
53
+ continue;
54
+ }
55
+ const filename = this.generateLocalFilename(file.name, timestamp);
56
+ const localPath = `${channelId}/attachments/${filename}`;
57
+ attachments.push({
58
+ original: file.name,
59
+ local: localPath,
60
+ });
61
+ // Queue for background download
62
+ this.pendingDownloads.push({ channelId, localPath, url });
63
+ }
64
+ // Trigger background download
65
+ this.processDownloadQueue();
66
+ return attachments;
67
+ }
68
+ /**
69
+ * Log a message to the channel's log.jsonl
70
+ * Returns false if message was already logged (duplicate)
71
+ */
72
+ async logMessage(channelId, message) {
73
+ // Check for duplicate (same channel + timestamp)
74
+ const dedupeKey = `${channelId}:${message.ts}`;
75
+ if (this.recentlyLogged.has(dedupeKey)) {
76
+ return false; // Already logged
77
+ }
78
+ // Mark as logged and schedule cleanup after 60 seconds
79
+ this.recentlyLogged.set(dedupeKey, Date.now());
80
+ setTimeout(() => this.recentlyLogged.delete(dedupeKey), 60000);
81
+ const logPath = join(this.getChannelDir(channelId), "log.jsonl");
82
+ // Ensure message has a date field
83
+ if (!message.date) {
84
+ // Parse timestamp to get date
85
+ let date;
86
+ if (message.ts.includes(".")) {
87
+ // Slack timestamp format (1234567890.123456)
88
+ date = new Date(parseFloat(message.ts) * 1000);
89
+ }
90
+ else {
91
+ // Epoch milliseconds
92
+ date = new Date(parseInt(message.ts, 10));
93
+ }
94
+ message.date = date.toISOString();
95
+ }
96
+ const line = `${JSON.stringify(message)}\n`;
97
+ await appendFile(logPath, line, "utf-8");
98
+ return true;
99
+ }
100
+ /**
101
+ * Log a bot response
102
+ */
103
+ async logBotResponse(channelId, text, ts) {
104
+ await this.logMessage(channelId, {
105
+ date: new Date().toISOString(),
106
+ ts,
107
+ user: "bot",
108
+ text,
109
+ attachments: [],
110
+ isBot: true,
111
+ });
112
+ }
113
+ /**
114
+ * Get the timestamp of the last logged message for a channel
115
+ * Returns null if no log exists
116
+ */
117
+ getLastTimestamp(channelId) {
118
+ const logPath = join(this.workingDir, channelId, "log.jsonl");
119
+ if (!existsSync(logPath)) {
120
+ return null;
121
+ }
122
+ try {
123
+ const content = readFileSync(logPath, "utf-8");
124
+ const lines = content.trim().split("\n");
125
+ if (lines.length === 0 || lines[0] === "") {
126
+ return null;
127
+ }
128
+ const lastLine = lines[lines.length - 1];
129
+ const message = JSON.parse(lastLine);
130
+ return message.ts;
131
+ }
132
+ catch {
133
+ return null;
134
+ }
135
+ }
136
+ /**
137
+ * Process the download queue in the background
138
+ */
139
+ async processDownloadQueue() {
140
+ if (this.isDownloading || this.pendingDownloads.length === 0)
141
+ return;
142
+ this.isDownloading = true;
143
+ while (this.pendingDownloads.length > 0) {
144
+ const item = this.pendingDownloads.shift();
145
+ if (!item)
146
+ break;
147
+ try {
148
+ await this.downloadAttachment(item.localPath, item.url);
149
+ // Success - could add success logging here if we have context
150
+ }
151
+ catch (error) {
152
+ const errorMsg = error instanceof Error ? error.message : String(error);
153
+ log.logWarning(`Failed to download attachment`, `${item.localPath}: ${errorMsg}`);
154
+ }
155
+ }
156
+ this.isDownloading = false;
157
+ }
158
+ /**
159
+ * Download a single attachment
160
+ */
161
+ async downloadAttachment(localPath, url) {
162
+ const filePath = join(this.workingDir, localPath);
163
+ // Ensure directory exists
164
+ const dir = join(this.workingDir, localPath.substring(0, localPath.lastIndexOf("/")));
165
+ if (!existsSync(dir)) {
166
+ mkdirSync(dir, { recursive: true });
167
+ }
168
+ const response = await fetch(url, {
169
+ headers: {
170
+ Authorization: `Bearer ${this.botToken}`,
171
+ },
172
+ });
173
+ if (!response.ok) {
174
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
175
+ }
176
+ const buffer = await response.arrayBuffer();
177
+ await writeFile(filePath, Buffer.from(buffer));
178
+ }
179
+ }
180
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AA6BhC,MAAM,OAAO,YAAY;IAChB,UAAU,CAAS;IACnB,QAAQ,CAAS;IACjB,gBAAgB,GAAsB,EAAE,CAAC;IACzC,aAAa,GAAG,KAAK,CAAC;IAC9B,iEAAiE;IACjE,iEAAiE;IACzD,cAAc,GAAG,IAAI,GAAG,EAAkB,CAAC;IAEnD,YAAY,MAA0B,EAAE;QACvC,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;QACpC,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;QAEhC,kCAAkC;QAClC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;YAClC,SAAS,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACjD,CAAC;IAAA,CACD;IAED;;OAEG;IACH,aAAa,CAAC,SAAiB,EAAU;QACxC,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;QAC7C,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACtB,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACrC,CAAC;QACD,OAAO,GAAG,CAAC;IAAA,CACX;IAED;;OAEG;IACH,qBAAqB,CAAC,YAAoB,EAAE,SAAiB,EAAU;QACtE,8DAA8D;QAC9D,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,CAAC;QACpD,yDAAyD;QACzD,MAAM,SAAS,GAAG,YAAY,CAAC,OAAO,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAC;QAChE,OAAO,GAAG,EAAE,IAAI,SAAS,EAAE,CAAC;IAAA,CAC5B;IAED;;;OAGG;IACH,kBAAkB,CACjB,SAAiB,EACjB,KAAoF,EACpF,SAAiB,EACF;QACf,MAAM,WAAW,GAAiB,EAAE,CAAC;QAErC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YAC1B,MAAM,GAAG,GAAG,IAAI,CAAC,oBAAoB,IAAI,IAAI,CAAC,WAAW,CAAC;YAC1D,IAAI,CAAC,GAAG;gBAAE,SAAS;YACnB,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;gBAChB,GAAG,CAAC,UAAU,CAAC,mCAAmC,EAAE,GAAG,CAAC,CAAC;gBACzD,SAAS;YACV,CAAC;YAED,MAAM,QAAQ,GAAG,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;YAClE,MAAM,SAAS,GAAG,GAAG,SAAS,gBAAgB,QAAQ,EAAE,CAAC;YAEzD,WAAW,CAAC,IAAI,CAAC;gBAChB,QAAQ,EAAE,IAAI,CAAC,IAAI;gBACnB,KAAK,EAAE,SAAS;aAChB,CAAC,CAAC;YAEH,gCAAgC;YAChC,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC;QAC3D,CAAC;QAED,8BAA8B;QAC9B,IAAI,CAAC,oBAAoB,EAAE,CAAC;QAE5B,OAAO,WAAW,CAAC;IAAA,CACnB;IAED;;;OAGG;IACH,KAAK,CAAC,UAAU,CAAC,SAAiB,EAAE,OAAsB,EAAoB;QAC7E,iDAAiD;QACjD,MAAM,SAAS,GAAG,GAAG,SAAS,IAAI,OAAO,CAAC,EAAE,EAAE,CAAC;QAC/C,IAAI,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;YACxC,OAAO,KAAK,CAAC,CAAC,iBAAiB;QAChC,CAAC;QAED,uDAAuD;QACvD,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QAC/C,UAAU,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,KAAK,CAAC,CAAC;QAE/D,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,EAAE,WAAW,CAAC,CAAC;QAEjE,kCAAkC;QAClC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;YACnB,8BAA8B;YAC9B,IAAI,IAAU,CAAC;YACf,IAAI,OAAO,CAAC,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC9B,6CAA6C;gBAC7C,IAAI,GAAG,IAAI,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;YAChD,CAAC;iBAAM,CAAC;gBACP,qBAAqB;gBACrB,IAAI,GAAG,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;YAC3C,CAAC;YACD,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;QACnC,CAAC;QAED,MAAM,IAAI,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC;QAC5C,MAAM,UAAU,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;QACzC,OAAO,IAAI,CAAC;IAAA,CACZ;IAED;;OAEG;IACH,KAAK,CAAC,cAAc,CAAC,SAAiB,EAAE,IAAY,EAAE,EAAU,EAAiB;QAChF,MAAM,IAAI,CAAC,UAAU,CAAC,SAAS,EAAE;YAChC,IAAI,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YAC9B,EAAE;YACF,IAAI,EAAE,KAAK;YACX,IAAI;YACJ,WAAW,EAAE,EAAE;YACf,KAAK,EAAE,IAAI;SACX,CAAC,CAAC;IAAA,CACH;IAED;;;OAGG;IACH,gBAAgB,CAAC,SAAiB,EAAiB;QAClD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,EAAE,WAAW,CAAC,CAAC;QAC9D,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YAC1B,OAAO,IAAI,CAAC;QACb,CAAC;QAED,IAAI,CAAC;YACJ,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC/C,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACzC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC;gBAC3C,OAAO,IAAI,CAAC;YACb,CAAC;YACD,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YACzC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAkB,CAAC;YACtD,OAAO,OAAO,CAAC,EAAE,CAAC;QACnB,CAAC;QAAC,MAAM,CAAC;YACR,OAAO,IAAI,CAAC;QACb,CAAC;IAAA,CACD;IAED;;OAEG;IACK,KAAK,CAAC,oBAAoB,GAAkB;QACnD,IAAI,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,gBAAgB,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAErE,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;QAE1B,OAAO,IAAI,CAAC,gBAAgB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACzC,MAAM,IAAI,GAAG,IAAI,CAAC,gBAAgB,CAAC,KAAK,EAAE,CAAC;YAC3C,IAAI,CAAC,IAAI;gBAAE,MAAM;YAEjB,IAAI,CAAC;gBACJ,MAAM,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;gBACxD,8DAA8D;YAC/D,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,MAAM,QAAQ,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBACxE,GAAG,CAAC,UAAU,CAAC,+BAA+B,EAAE,GAAG,IAAI,CAAC,SAAS,KAAK,QAAQ,EAAE,CAAC,CAAC;YACnF,CAAC;QACF,CAAC;QAED,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC;IAAA,CAC3B;IAED;;OAEG;IACK,KAAK,CAAC,kBAAkB,CAAC,SAAiB,EAAE,GAAW,EAAiB;QAC/E,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;QAElD,0BAA0B;QAC1B,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC,EAAE,SAAS,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACtF,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACtB,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACrC,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YACjC,OAAO,EAAE;gBACR,aAAa,EAAE,UAAU,IAAI,CAAC,QAAQ,EAAE;aACxC;SACD,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YAClB,MAAM,IAAI,KAAK,CAAC,QAAQ,QAAQ,CAAC,MAAM,KAAK,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC;QACpE,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC;QAC5C,MAAM,SAAS,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;IAAA,CAC/C;CACD","sourcesContent":["import { existsSync, mkdirSync, readFileSync } from \"fs\";\nimport { appendFile, writeFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport * as log from \"./log.js\";\n\nexport interface Attachment {\n\toriginal: string; // original filename from uploader\n\tlocal: string; // path relative to working dir (e.g., \"C12345/attachments/1732531234567_file.png\")\n}\n\nexport interface LoggedMessage {\n\tdate: string; // ISO 8601 date (e.g., \"2025-11-26T10:44:00.000Z\") for easy grepping\n\tts: string; // slack timestamp or epoch ms\n\tuser: string; // user ID (or \"bot\" for bot responses)\n\tuserName?: string; // handle (e.g., \"mario\")\n\tdisplayName?: string; // display name (e.g., \"Mario Zechner\")\n\ttext: string;\n\tattachments: Attachment[];\n\tisBot: boolean;\n}\n\nexport interface ChannelStoreConfig {\n\tworkingDir: string;\n\tbotToken: string; // needed for authenticated file downloads\n}\n\ninterface PendingDownload {\n\tchannelId: string;\n\tlocalPath: string; // relative path\n\turl: string;\n}\n\nexport class ChannelStore {\n\tprivate workingDir: string;\n\tprivate botToken: string;\n\tprivate pendingDownloads: PendingDownload[] = [];\n\tprivate isDownloading = false;\n\t// Track recently logged message timestamps to prevent duplicates\n\t// Key: \"channelId:ts\", automatically cleaned up after 60 seconds\n\tprivate recentlyLogged = new Map<string, number>();\n\n\tconstructor(config: ChannelStoreConfig) {\n\t\tthis.workingDir = config.workingDir;\n\t\tthis.botToken = config.botToken;\n\n\t\t// Ensure working directory exists\n\t\tif (!existsSync(this.workingDir)) {\n\t\t\tmkdirSync(this.workingDir, { recursive: true });\n\t\t}\n\t}\n\n\t/**\n\t * Get or create the directory for a channel/DM\n\t */\n\tgetChannelDir(channelId: string): string {\n\t\tconst dir = join(this.workingDir, channelId);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\t\treturn dir;\n\t}\n\n\t/**\n\t * Generate a unique local filename for an attachment\n\t */\n\tgenerateLocalFilename(originalName: string, timestamp: string): string {\n\t\t// Convert slack timestamp (1234567890.123456) to milliseconds\n\t\tconst ts = Math.floor(parseFloat(timestamp) * 1000);\n\t\t// Sanitize original name (remove problematic characters)\n\t\tconst sanitized = originalName.replace(/[^a-zA-Z0-9._-]/g, \"_\");\n\t\treturn `${ts}_${sanitized}`;\n\t}\n\n\t/**\n\t * Process attachments from a Slack message event\n\t * Returns attachment metadata and queues downloads\n\t */\n\tprocessAttachments(\n\t\tchannelId: string,\n\t\tfiles: Array<{ name?: string; url_private_download?: string; url_private?: string }>,\n\t\ttimestamp: string,\n\t): Attachment[] {\n\t\tconst attachments: Attachment[] = [];\n\n\t\tfor (const file of files) {\n\t\t\tconst url = file.url_private_download || file.url_private;\n\t\t\tif (!url) continue;\n\t\t\tif (!file.name) {\n\t\t\t\tlog.logWarning(\"Attachment missing name, skipping\", url);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst filename = this.generateLocalFilename(file.name, timestamp);\n\t\t\tconst localPath = `${channelId}/attachments/${filename}`;\n\n\t\t\tattachments.push({\n\t\t\t\toriginal: file.name,\n\t\t\t\tlocal: localPath,\n\t\t\t});\n\n\t\t\t// Queue for background download\n\t\t\tthis.pendingDownloads.push({ channelId, localPath, url });\n\t\t}\n\n\t\t// Trigger background download\n\t\tthis.processDownloadQueue();\n\n\t\treturn attachments;\n\t}\n\n\t/**\n\t * Log a message to the channel's log.jsonl\n\t * Returns false if message was already logged (duplicate)\n\t */\n\tasync logMessage(channelId: string, message: LoggedMessage): Promise<boolean> {\n\t\t// Check for duplicate (same channel + timestamp)\n\t\tconst dedupeKey = `${channelId}:${message.ts}`;\n\t\tif (this.recentlyLogged.has(dedupeKey)) {\n\t\t\treturn false; // Already logged\n\t\t}\n\n\t\t// Mark as logged and schedule cleanup after 60 seconds\n\t\tthis.recentlyLogged.set(dedupeKey, Date.now());\n\t\tsetTimeout(() => this.recentlyLogged.delete(dedupeKey), 60000);\n\n\t\tconst logPath = join(this.getChannelDir(channelId), \"log.jsonl\");\n\n\t\t// Ensure message has a date field\n\t\tif (!message.date) {\n\t\t\t// Parse timestamp to get date\n\t\t\tlet date: Date;\n\t\t\tif (message.ts.includes(\".\")) {\n\t\t\t\t// Slack timestamp format (1234567890.123456)\n\t\t\t\tdate = new Date(parseFloat(message.ts) * 1000);\n\t\t\t} else {\n\t\t\t\t// Epoch milliseconds\n\t\t\t\tdate = new Date(parseInt(message.ts, 10));\n\t\t\t}\n\t\t\tmessage.date = date.toISOString();\n\t\t}\n\n\t\tconst line = `${JSON.stringify(message)}\\n`;\n\t\tawait appendFile(logPath, line, \"utf-8\");\n\t\treturn true;\n\t}\n\n\t/**\n\t * Log a bot response\n\t */\n\tasync logBotResponse(channelId: string, text: string, ts: string): Promise<void> {\n\t\tawait this.logMessage(channelId, {\n\t\t\tdate: new Date().toISOString(),\n\t\t\tts,\n\t\t\tuser: \"bot\",\n\t\t\ttext,\n\t\t\tattachments: [],\n\t\t\tisBot: true,\n\t\t});\n\t}\n\n\t/**\n\t * Get the timestamp of the last logged message for a channel\n\t * Returns null if no log exists\n\t */\n\tgetLastTimestamp(channelId: string): string | null {\n\t\tconst logPath = join(this.workingDir, channelId, \"log.jsonl\");\n\t\tif (!existsSync(logPath)) {\n\t\t\treturn null;\n\t\t}\n\n\t\ttry {\n\t\t\tconst content = readFileSync(logPath, \"utf-8\");\n\t\t\tconst lines = content.trim().split(\"\\n\");\n\t\t\tif (lines.length === 0 || lines[0] === \"\") {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst lastLine = lines[lines.length - 1];\n\t\t\tconst message = JSON.parse(lastLine) as LoggedMessage;\n\t\t\treturn message.ts;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t/**\n\t * Process the download queue in the background\n\t */\n\tprivate async processDownloadQueue(): Promise<void> {\n\t\tif (this.isDownloading || this.pendingDownloads.length === 0) return;\n\n\t\tthis.isDownloading = true;\n\n\t\twhile (this.pendingDownloads.length > 0) {\n\t\t\tconst item = this.pendingDownloads.shift();\n\t\t\tif (!item) break;\n\n\t\t\ttry {\n\t\t\t\tawait this.downloadAttachment(item.localPath, item.url);\n\t\t\t\t// Success - could add success logging here if we have context\n\t\t\t} catch (error) {\n\t\t\t\tconst errorMsg = error instanceof Error ? error.message : String(error);\n\t\t\t\tlog.logWarning(`Failed to download attachment`, `${item.localPath}: ${errorMsg}`);\n\t\t\t}\n\t\t}\n\n\t\tthis.isDownloading = false;\n\t}\n\n\t/**\n\t * Download a single attachment\n\t */\n\tprivate async downloadAttachment(localPath: string, url: string): Promise<void> {\n\t\tconst filePath = join(this.workingDir, localPath);\n\n\t\t// Ensure directory exists\n\t\tconst dir = join(this.workingDir, localPath.substring(0, localPath.lastIndexOf(\"/\")));\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\n\t\tconst response = await fetch(url, {\n\t\t\theaders: {\n\t\t\t\tAuthorization: `Bearer ${this.botToken}`,\n\t\t\t},\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(`HTTP ${response.status}: ${response.statusText}`);\n\t\t}\n\n\t\tconst buffer = await response.arrayBuffer();\n\t\tawait writeFile(filePath, Buffer.from(buffer));\n\t}\n}\n"]}
@@ -0,0 +1,10 @@
1
+ import type { AgentTool } from "@mariozechner/pi-agent-core";
2
+ import type { Executor } from "../sandbox.js";
3
+ declare const bashSchema: import("@sinclair/typebox").TObject<{
4
+ label: import("@sinclair/typebox").TString;
5
+ command: import("@sinclair/typebox").TString;
6
+ timeout: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
7
+ }>;
8
+ export declare function createBashTool(executor: Executor): AgentTool<typeof bashSchema>;
9
+ export {};
10
+ //# sourceMappingURL=bash.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../src/tools/bash.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAE7D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAW9C,QAAA,MAAM,UAAU;;;;EAId,CAAC;AAOH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAAC,OAAO,UAAU,CAAC,CAoE/E","sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { createWriteStream } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { AgentTool } from \"@mariozechner/pi-agent-core\";\nimport { Type } from \"@sinclair/typebox\";\nimport type { Executor } from \"../sandbox.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateTail } from \"./truncate.js\";\n\n/**\n * Generate a unique temp file path for bash output\n */\nfunction getTempFilePath(): string {\n\tconst id = randomBytes(8).toString(\"hex\");\n\treturn join(tmpdir(), `mama-bash-${id}.log`);\n}\n\nconst bashSchema = Type.Object({\n\tlabel: Type.String({ description: \"Brief description of what this command does (shown to user)\" }),\n\tcommand: Type.String({ description: \"Bash command to execute\" }),\n\ttimeout: Type.Optional(Type.Number({ description: \"Timeout in seconds (optional, no default timeout)\" })),\n});\n\ninterface BashToolDetails {\n\ttruncation?: TruncationResult;\n\tfullOutputPath?: string;\n}\n\nexport function createBashTool(executor: Executor): AgentTool<typeof bashSchema> {\n\treturn {\n\t\tname: \"bash\",\n\t\tlabel: \"bash\",\n\t\tdescription: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,\n\t\tparameters: bashSchema,\n\t\texecute: async (\n\t\t\t_toolCallId: string,\n\t\t\t{ command, timeout }: { label: string; command: string; timeout?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t) => {\n\t\t\t// Track output for potential temp file writing\n\t\t\tlet tempFilePath: string | undefined;\n\t\t\tlet tempFileStream: ReturnType<typeof createWriteStream> | undefined;\n\n\t\t\tconst result = await executor.exec(command, { timeout, signal });\n\t\t\tlet output = \"\";\n\t\t\tif (result.stdout) output += result.stdout;\n\t\t\tif (result.stderr) {\n\t\t\t\tif (output) output += \"\\n\";\n\t\t\t\toutput += result.stderr;\n\t\t\t}\n\n\t\t\tconst totalBytes = Buffer.byteLength(output, \"utf-8\");\n\n\t\t\t// Write to temp file if output exceeds limit\n\t\t\tif (totalBytes > DEFAULT_MAX_BYTES) {\n\t\t\t\ttempFilePath = getTempFilePath();\n\t\t\t\ttempFileStream = createWriteStream(tempFilePath);\n\t\t\t\ttempFileStream.write(output);\n\t\t\t\ttempFileStream.end();\n\t\t\t}\n\n\t\t\t// Apply tail truncation\n\t\t\tconst truncation = truncateTail(output);\n\t\t\tlet outputText = truncation.content || \"(no output)\";\n\n\t\t\t// Build details with truncation info\n\t\t\tlet details: BashToolDetails | undefined;\n\n\t\t\tif (truncation.truncated) {\n\t\t\t\tdetails = {\n\t\t\t\t\ttruncation,\n\t\t\t\t\tfullOutputPath: tempFilePath,\n\t\t\t\t};\n\n\t\t\t\t// Build actionable notice\n\t\t\t\tconst startLine = truncation.totalLines - truncation.outputLines + 1;\n\t\t\t\tconst endLine = truncation.totalLines;\n\n\t\t\t\tif (truncation.lastLinePartial) {\n\t\t\t\t\t// Edge case: last line alone > 50KB\n\t\t\t\t\tconst lastLineSize = formatSize(Buffer.byteLength(output.split(\"\\n\").pop() || \"\", \"utf-8\"));\n\t\t\t\t\toutputText += `\\n\\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${tempFilePath}]`;\n\t\t\t\t} else if (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${tempFilePath}]`;\n\t\t\t\t} else {\n\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${tempFilePath}]`;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (result.code !== 0) {\n\t\t\t\tthrow new Error(`${outputText}\\n\\nCommand exited with code ${result.code}`.trim());\n\t\t\t}\n\n\t\t\treturn { content: [{ type: \"text\", text: outputText }], details };\n\t\t},\n\t};\n}\n"]}
@@ -0,0 +1,78 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { createWriteStream } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { Type } from "@sinclair/typebox";
6
+ import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateTail } from "./truncate.js";
7
+ /**
8
+ * Generate a unique temp file path for bash output
9
+ */
10
+ function getTempFilePath() {
11
+ const id = randomBytes(8).toString("hex");
12
+ return join(tmpdir(), `mama-bash-${id}.log`);
13
+ }
14
+ const bashSchema = Type.Object({
15
+ label: Type.String({ description: "Brief description of what this command does (shown to user)" }),
16
+ command: Type.String({ description: "Bash command to execute" }),
17
+ timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional, no default timeout)" })),
18
+ });
19
+ export function createBashTool(executor) {
20
+ return {
21
+ name: "bash",
22
+ label: "bash",
23
+ description: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,
24
+ parameters: bashSchema,
25
+ execute: async (_toolCallId, { command, timeout }, signal) => {
26
+ // Track output for potential temp file writing
27
+ let tempFilePath;
28
+ let tempFileStream;
29
+ const result = await executor.exec(command, { timeout, signal });
30
+ let output = "";
31
+ if (result.stdout)
32
+ output += result.stdout;
33
+ if (result.stderr) {
34
+ if (output)
35
+ output += "\n";
36
+ output += result.stderr;
37
+ }
38
+ const totalBytes = Buffer.byteLength(output, "utf-8");
39
+ // Write to temp file if output exceeds limit
40
+ if (totalBytes > DEFAULT_MAX_BYTES) {
41
+ tempFilePath = getTempFilePath();
42
+ tempFileStream = createWriteStream(tempFilePath);
43
+ tempFileStream.write(output);
44
+ tempFileStream.end();
45
+ }
46
+ // Apply tail truncation
47
+ const truncation = truncateTail(output);
48
+ let outputText = truncation.content || "(no output)";
49
+ // Build details with truncation info
50
+ let details;
51
+ if (truncation.truncated) {
52
+ details = {
53
+ truncation,
54
+ fullOutputPath: tempFilePath,
55
+ };
56
+ // Build actionable notice
57
+ const startLine = truncation.totalLines - truncation.outputLines + 1;
58
+ const endLine = truncation.totalLines;
59
+ if (truncation.lastLinePartial) {
60
+ // Edge case: last line alone > 50KB
61
+ const lastLineSize = formatSize(Buffer.byteLength(output.split("\n").pop() || "", "utf-8"));
62
+ outputText += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${tempFilePath}]`;
63
+ }
64
+ else if (truncation.truncatedBy === "lines") {
65
+ outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${tempFilePath}]`;
66
+ }
67
+ else {
68
+ outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${tempFilePath}]`;
69
+ }
70
+ }
71
+ if (result.code !== 0) {
72
+ throw new Error(`${outputText}\n\nCommand exited with code ${result.code}`.trim());
73
+ }
74
+ return { content: [{ type: "text", text: outputText }], details };
75
+ },
76
+ };
77
+ }
78
+ //# sourceMappingURL=bash.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bash.js","sourceRoot":"","sources":["../../src/tools/bash.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { createWriteStream } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { AgentTool } from \"@mariozechner/pi-agent-core\";\nimport { Type } from \"@sinclair/typebox\";\nimport type { Executor } from \"../sandbox.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateTail } from \"./truncate.js\";\n\n/**\n * Generate a unique temp file path for bash output\n */\nfunction getTempFilePath(): string {\n\tconst id = randomBytes(8).toString(\"hex\");\n\treturn join(tmpdir(), `mama-bash-${id}.log`);\n}\n\nconst bashSchema = Type.Object({\n\tlabel: Type.String({ description: \"Brief description of what this command does (shown to user)\" }),\n\tcommand: Type.String({ description: \"Bash command to execute\" }),\n\ttimeout: Type.Optional(Type.Number({ description: \"Timeout in seconds (optional, no default timeout)\" })),\n});\n\ninterface BashToolDetails {\n\ttruncation?: TruncationResult;\n\tfullOutputPath?: string;\n}\n\nexport function createBashTool(executor: Executor): AgentTool<typeof bashSchema> {\n\treturn {\n\t\tname: \"bash\",\n\t\tlabel: \"bash\",\n\t\tdescription: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,\n\t\tparameters: bashSchema,\n\t\texecute: async (\n\t\t\t_toolCallId: string,\n\t\t\t{ command, timeout }: { label: string; command: string; timeout?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t) => {\n\t\t\t// Track output for potential temp file writing\n\t\t\tlet tempFilePath: string | undefined;\n\t\t\tlet tempFileStream: ReturnType<typeof createWriteStream> | undefined;\n\n\t\t\tconst result = await executor.exec(command, { timeout, signal });\n\t\t\tlet output = \"\";\n\t\t\tif (result.stdout) output += result.stdout;\n\t\t\tif (result.stderr) {\n\t\t\t\tif (output) output += \"\\n\";\n\t\t\t\toutput += result.stderr;\n\t\t\t}\n\n\t\t\tconst totalBytes = Buffer.byteLength(output, \"utf-8\");\n\n\t\t\t// Write to temp file if output exceeds limit\n\t\t\tif (totalBytes > DEFAULT_MAX_BYTES) {\n\t\t\t\ttempFilePath = getTempFilePath();\n\t\t\t\ttempFileStream = createWriteStream(tempFilePath);\n\t\t\t\ttempFileStream.write(output);\n\t\t\t\ttempFileStream.end();\n\t\t\t}\n\n\t\t\t// Apply tail truncation\n\t\t\tconst truncation = truncateTail(output);\n\t\t\tlet outputText = truncation.content || \"(no output)\";\n\n\t\t\t// Build details with truncation info\n\t\t\tlet details: BashToolDetails | undefined;\n\n\t\t\tif (truncation.truncated) {\n\t\t\t\tdetails = {\n\t\t\t\t\ttruncation,\n\t\t\t\t\tfullOutputPath: tempFilePath,\n\t\t\t\t};\n\n\t\t\t\t// Build actionable notice\n\t\t\t\tconst startLine = truncation.totalLines - truncation.outputLines + 1;\n\t\t\t\tconst endLine = truncation.totalLines;\n\n\t\t\t\tif (truncation.lastLinePartial) {\n\t\t\t\t\t// Edge case: last line alone > 50KB\n\t\t\t\t\tconst lastLineSize = formatSize(Buffer.byteLength(output.split(\"\\n\").pop() || \"\", \"utf-8\"));\n\t\t\t\t\toutputText += `\\n\\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${tempFilePath}]`;\n\t\t\t\t} else if (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${tempFilePath}]`;\n\t\t\t\t} else {\n\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${tempFilePath}]`;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (result.code !== 0) {\n\t\t\t\tthrow new Error(`${outputText}\\n\\nCommand exited with code ${result.code}`.trim());\n\t\t\t}\n\n\t\t\treturn { content: [{ type: \"text\", text: outputText }], details };\n\t\t},\n\t};\n}\n"]}
@@ -0,0 +1,11 @@
1
+ import type { AgentTool } from "@mariozechner/pi-agent-core";
2
+ import type { Executor } from "../sandbox.js";
3
+ declare const editSchema: import("@sinclair/typebox").TObject<{
4
+ label: import("@sinclair/typebox").TString;
5
+ path: import("@sinclair/typebox").TString;
6
+ oldText: import("@sinclair/typebox").TString;
7
+ newText: import("@sinclair/typebox").TString;
8
+ }>;
9
+ export declare function createEditTool(executor: Executor): AgentTool<typeof editSchema>;
10
+ export {};
11
+ //# sourceMappingURL=edit.d.ts.map