@draht/mom 2026.3.2-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +448 -0
  2. package/README.md +490 -0
  3. package/dist/agent.d.ts +24 -0
  4. package/dist/agent.d.ts.map +1 -0
  5. package/dist/agent.js +758 -0
  6. package/dist/agent.js.map +1 -0
  7. package/dist/context.d.ts +70 -0
  8. package/dist/context.d.ts.map +1 -0
  9. package/dist/context.js +221 -0
  10. package/dist/context.js.map +1 -0
  11. package/dist/download.d.ts +2 -0
  12. package/dist/download.d.ts.map +1 -0
  13. package/dist/download.js +89 -0
  14. package/dist/download.js.map +1 -0
  15. package/dist/events.d.ts +57 -0
  16. package/dist/events.d.ts.map +1 -0
  17. package/dist/events.js +310 -0
  18. package/dist/events.js.map +1 -0
  19. package/dist/log.d.ts +39 -0
  20. package/dist/log.d.ts.map +1 -0
  21. package/dist/log.js +222 -0
  22. package/dist/log.js.map +1 -0
  23. package/dist/main.d.ts +3 -0
  24. package/dist/main.d.ts.map +1 -0
  25. package/dist/main.js +271 -0
  26. package/dist/main.js.map +1 -0
  27. package/dist/sandbox.d.ts +34 -0
  28. package/dist/sandbox.d.ts.map +1 -0
  29. package/dist/sandbox.js +183 -0
  30. package/dist/sandbox.js.map +1 -0
  31. package/dist/slack.d.ts +128 -0
  32. package/dist/slack.d.ts.map +1 -0
  33. package/dist/slack.js +455 -0
  34. package/dist/slack.js.map +1 -0
  35. package/dist/store.d.ts +60 -0
  36. package/dist/store.d.ts.map +1 -0
  37. package/dist/store.js +180 -0
  38. package/dist/store.js.map +1 -0
  39. package/dist/tools/attach.d.ts +10 -0
  40. package/dist/tools/attach.d.ts.map +1 -0
  41. package/dist/tools/attach.js +34 -0
  42. package/dist/tools/attach.js.map +1 -0
  43. package/dist/tools/bash.d.ts +10 -0
  44. package/dist/tools/bash.d.ts.map +1 -0
  45. package/dist/tools/bash.js +78 -0
  46. package/dist/tools/bash.js.map +1 -0
  47. package/dist/tools/edit.d.ts +11 -0
  48. package/dist/tools/edit.d.ts.map +1 -0
  49. package/dist/tools/edit.js +131 -0
  50. package/dist/tools/edit.js.map +1 -0
  51. package/dist/tools/index.d.ts +5 -0
  52. package/dist/tools/index.d.ts.map +1 -0
  53. package/dist/tools/index.js +16 -0
  54. package/dist/tools/index.js.map +1 -0
  55. package/dist/tools/read.d.ts +11 -0
  56. package/dist/tools/read.d.ts.map +1 -0
  57. package/dist/tools/read.js +134 -0
  58. package/dist/tools/read.js.map +1 -0
  59. package/dist/tools/truncate.d.ts +57 -0
  60. package/dist/tools/truncate.d.ts.map +1 -0
  61. package/dist/tools/truncate.js +184 -0
  62. package/dist/tools/truncate.js.map +1 -0
  63. package/dist/tools/write.d.ts +10 -0
  64. package/dist/tools/write.d.ts.map +1 -0
  65. package/dist/tools/write.js +33 -0
  66. package/dist/tools/write.js.map +1 -0
  67. package/package.json +54 -0
@@ -0,0 +1,57 @@
1
+ import type { SlackBot } from "./slack.js";
2
+ export interface ImmediateEvent {
3
+ type: "immediate";
4
+ channelId: string;
5
+ text: string;
6
+ }
7
+ export interface OneShotEvent {
8
+ type: "one-shot";
9
+ channelId: string;
10
+ text: string;
11
+ at: string;
12
+ }
13
+ export interface PeriodicEvent {
14
+ type: "periodic";
15
+ channelId: string;
16
+ text: string;
17
+ schedule: string;
18
+ timezone: string;
19
+ }
20
+ export type MomEvent = ImmediateEvent | OneShotEvent | PeriodicEvent;
21
+ export declare class EventsWatcher {
22
+ private eventsDir;
23
+ private slack;
24
+ private timers;
25
+ private crons;
26
+ private debounceTimers;
27
+ private startTime;
28
+ private watcher;
29
+ private knownFiles;
30
+ constructor(eventsDir: string, slack: SlackBot);
31
+ /**
32
+ * Start watching for events. Call this after SlackBot is ready.
33
+ */
34
+ start(): void;
35
+ /**
36
+ * Stop watching and cancel all scheduled events.
37
+ */
38
+ stop(): void;
39
+ private debounce;
40
+ private scanExisting;
41
+ private handleFileChange;
42
+ private handleDelete;
43
+ private cancelScheduled;
44
+ private handleFile;
45
+ private parseEvent;
46
+ private handleImmediate;
47
+ private handleOneShot;
48
+ private handlePeriodic;
49
+ private execute;
50
+ private deleteFile;
51
+ private sleep;
52
+ }
53
+ /**
54
+ * Create and start an events watcher.
55
+ */
56
+ export declare function createEventsWatcher(workspaceDir: string, slack: SlackBot): EventsWatcher;
57
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAc,MAAM,YAAY,CAAC;AAMvD,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,WAAW,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,UAAU,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACX;AAED,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,UAAU,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,QAAQ,GAAG,cAAc,GAAG,YAAY,GAAG,aAAa,CAAC;AAUrE,qBAAa,aAAa;IASxB,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,KAAK;IATd,OAAO,CAAC,MAAM,CAA0C;IACxD,OAAO,CAAC,KAAK,CAAgC;IAC7C,OAAO,CAAC,cAAc,CAA0C;IAChE,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,OAAO,CAA0B;IACzC,OAAO,CAAC,UAAU,CAA0B;IAE5C,YACS,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,QAAQ,EAGvB;IAED;;OAEG;IACH,KAAK,IAAI,IAAI,CAkBZ;IAED;;OAEG;IACH,IAAI,IAAI,IAAI,CA2BX;IAED,OAAO,CAAC,QAAQ;IAchB,OAAO,CAAC,YAAY;IAcpB,OAAO,CAAC,gBAAgB;IAgBxB,OAAO,CAAC,YAAY;IAQpB,OAAO,CAAC,eAAe;YAcT,UAAU;IA0CxB,OAAO,CAAC,UAAU;IAqClB,OAAO,CAAC,eAAe;IAoBvB,OAAO,CAAC,aAAa;IAuBrB,OAAO,CAAC,cAAc;IAiBtB,OAAO,CAAC,OAAO;IAyCf,OAAO,CAAC,UAAU;IAalB,OAAO,CAAC,KAAK;CAGb;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,aAAa,CAGxF","sourcesContent":["import { Cron } from \"croner\";\nimport { existsSync, type FSWatcher, mkdirSync, readdirSync, statSync, unlinkSync, watch } from \"fs\";\nimport { readFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport * as log from \"./log.js\";\nimport type { SlackBot, SlackEvent } from \"./slack.js\";\n\n// ============================================================================\n// Event Types\n// ============================================================================\n\nexport interface ImmediateEvent {\n\ttype: \"immediate\";\n\tchannelId: string;\n\ttext: string;\n}\n\nexport interface OneShotEvent {\n\ttype: \"one-shot\";\n\tchannelId: string;\n\ttext: string;\n\tat: string; // ISO 8601 with timezone offset\n}\n\nexport interface PeriodicEvent {\n\ttype: \"periodic\";\n\tchannelId: string;\n\ttext: string;\n\tschedule: string; // cron syntax\n\ttimezone: string; // IANA timezone\n}\n\nexport type MomEvent = ImmediateEvent | OneShotEvent | PeriodicEvent;\n\n// ============================================================================\n// EventsWatcher\n// ============================================================================\n\nconst DEBOUNCE_MS = 100;\nconst MAX_RETRIES = 3;\nconst RETRY_BASE_MS = 100;\n\nexport class EventsWatcher {\n\tprivate timers: Map<string, NodeJS.Timeout> = new Map();\n\tprivate crons: Map<string, Cron> = new Map();\n\tprivate debounceTimers: Map<string, NodeJS.Timeout> = new Map();\n\tprivate startTime: number;\n\tprivate watcher: FSWatcher | null = null;\n\tprivate knownFiles: Set<string> = new Set();\n\n\tconstructor(\n\t\tprivate eventsDir: string,\n\t\tprivate slack: SlackBot,\n\t) {\n\t\tthis.startTime = Date.now();\n\t}\n\n\t/**\n\t * Start watching for events. Call this after SlackBot is ready.\n\t */\n\tstart(): void {\n\t\t// Ensure events directory exists\n\t\tif (!existsSync(this.eventsDir)) {\n\t\t\tmkdirSync(this.eventsDir, { recursive: true });\n\t\t}\n\n\t\tlog.logInfo(`Events watcher starting, dir: ${this.eventsDir}`);\n\n\t\t// Scan existing files\n\t\tthis.scanExisting();\n\n\t\t// Watch for changes\n\t\tthis.watcher = watch(this.eventsDir, (_eventType, filename) => {\n\t\t\tif (!filename || !filename.endsWith(\".json\")) return;\n\t\t\tthis.debounce(filename, () => this.handleFileChange(filename));\n\t\t});\n\n\t\tlog.logInfo(`Events watcher started, tracking ${this.knownFiles.size} files`);\n\t}\n\n\t/**\n\t * Stop watching and cancel all scheduled events.\n\t */\n\tstop(): void {\n\t\t// Stop fs watcher\n\t\tif (this.watcher) {\n\t\t\tthis.watcher.close();\n\t\t\tthis.watcher = null;\n\t\t}\n\n\t\t// Cancel all debounce timers\n\t\tfor (const timer of this.debounceTimers.values()) {\n\t\t\tclearTimeout(timer);\n\t\t}\n\t\tthis.debounceTimers.clear();\n\n\t\t// Cancel all scheduled timers\n\t\tfor (const timer of this.timers.values()) {\n\t\t\tclearTimeout(timer);\n\t\t}\n\t\tthis.timers.clear();\n\n\t\t// Cancel all cron jobs\n\t\tfor (const cron of this.crons.values()) {\n\t\t\tcron.stop();\n\t\t}\n\t\tthis.crons.clear();\n\n\t\tthis.knownFiles.clear();\n\t\tlog.logInfo(\"Events watcher stopped\");\n\t}\n\n\tprivate debounce(filename: string, fn: () => void): void {\n\t\tconst existing = this.debounceTimers.get(filename);\n\t\tif (existing) {\n\t\t\tclearTimeout(existing);\n\t\t}\n\t\tthis.debounceTimers.set(\n\t\t\tfilename,\n\t\t\tsetTimeout(() => {\n\t\t\t\tthis.debounceTimers.delete(filename);\n\t\t\t\tfn();\n\t\t\t}, DEBOUNCE_MS),\n\t\t);\n\t}\n\n\tprivate scanExisting(): void {\n\t\tlet files: string[];\n\t\ttry {\n\t\t\tfiles = readdirSync(this.eventsDir).filter((f) => f.endsWith(\".json\"));\n\t\t} catch (err) {\n\t\t\tlog.logWarning(\"Failed to read events directory\", String(err));\n\t\t\treturn;\n\t\t}\n\n\t\tfor (const filename of files) {\n\t\t\tthis.handleFile(filename);\n\t\t}\n\t}\n\n\tprivate handleFileChange(filename: string): void {\n\t\tconst filePath = join(this.eventsDir, filename);\n\n\t\tif (!existsSync(filePath)) {\n\t\t\t// File was deleted\n\t\t\tthis.handleDelete(filename);\n\t\t} else if (this.knownFiles.has(filename)) {\n\t\t\t// File was modified - cancel existing and re-schedule\n\t\t\tthis.cancelScheduled(filename);\n\t\t\tthis.handleFile(filename);\n\t\t} else {\n\t\t\t// New file\n\t\t\tthis.handleFile(filename);\n\t\t}\n\t}\n\n\tprivate handleDelete(filename: string): void {\n\t\tif (!this.knownFiles.has(filename)) return;\n\n\t\tlog.logInfo(`Event file deleted: ${filename}`);\n\t\tthis.cancelScheduled(filename);\n\t\tthis.knownFiles.delete(filename);\n\t}\n\n\tprivate cancelScheduled(filename: string): void {\n\t\tconst timer = this.timers.get(filename);\n\t\tif (timer) {\n\t\t\tclearTimeout(timer);\n\t\t\tthis.timers.delete(filename);\n\t\t}\n\n\t\tconst cron = this.crons.get(filename);\n\t\tif (cron) {\n\t\t\tcron.stop();\n\t\t\tthis.crons.delete(filename);\n\t\t}\n\t}\n\n\tprivate async handleFile(filename: string): Promise<void> {\n\t\tconst filePath = join(this.eventsDir, filename);\n\n\t\t// Parse with retries\n\t\tlet event: MomEvent | null = null;\n\t\tlet lastError: Error | null = null;\n\n\t\tfor (let i = 0; i < MAX_RETRIES; i++) {\n\t\t\ttry {\n\t\t\t\tconst content = await readFile(filePath, \"utf-8\");\n\t\t\t\tevent = this.parseEvent(content, filename);\n\t\t\t\tbreak;\n\t\t\t} catch (err) {\n\t\t\t\tlastError = err instanceof Error ? err : new Error(String(err));\n\t\t\t\tif (i < MAX_RETRIES - 1) {\n\t\t\t\t\tawait this.sleep(RETRY_BASE_MS * 2 ** i);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (!event) {\n\t\t\tlog.logWarning(`Failed to parse event file after ${MAX_RETRIES} retries: ${filename}`, lastError?.message);\n\t\t\tthis.deleteFile(filename);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.knownFiles.add(filename);\n\n\t\t// Schedule based on type\n\t\tswitch (event.type) {\n\t\t\tcase \"immediate\":\n\t\t\t\tthis.handleImmediate(filename, event);\n\t\t\t\tbreak;\n\t\t\tcase \"one-shot\":\n\t\t\t\tthis.handleOneShot(filename, event);\n\t\t\t\tbreak;\n\t\t\tcase \"periodic\":\n\t\t\t\tthis.handlePeriodic(filename, event);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tprivate parseEvent(content: string, filename: string): MomEvent | null {\n\t\tconst data = JSON.parse(content);\n\n\t\tif (!data.type || !data.channelId || !data.text) {\n\t\t\tthrow new Error(`Missing required fields (type, channelId, text) in ${filename}`);\n\t\t}\n\n\t\tswitch (data.type) {\n\t\t\tcase \"immediate\":\n\t\t\t\treturn { type: \"immediate\", channelId: data.channelId, text: data.text };\n\n\t\t\tcase \"one-shot\":\n\t\t\t\tif (!data.at) {\n\t\t\t\t\tthrow new Error(`Missing 'at' field for one-shot event in ${filename}`);\n\t\t\t\t}\n\t\t\t\treturn { type: \"one-shot\", channelId: data.channelId, text: data.text, at: data.at };\n\n\t\t\tcase \"periodic\":\n\t\t\t\tif (!data.schedule) {\n\t\t\t\t\tthrow new Error(`Missing 'schedule' field for periodic event in ${filename}`);\n\t\t\t\t}\n\t\t\t\tif (!data.timezone) {\n\t\t\t\t\tthrow new Error(`Missing 'timezone' field for periodic event in ${filename}`);\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"periodic\",\n\t\t\t\t\tchannelId: data.channelId,\n\t\t\t\t\ttext: data.text,\n\t\t\t\t\tschedule: data.schedule,\n\t\t\t\t\ttimezone: data.timezone,\n\t\t\t\t};\n\n\t\t\tdefault:\n\t\t\t\tthrow new Error(`Unknown event type '${data.type}' in ${filename}`);\n\t\t}\n\t}\n\n\tprivate handleImmediate(filename: string, event: ImmediateEvent): void {\n\t\tconst filePath = join(this.eventsDir, filename);\n\n\t\t// Check if stale (created before harness started)\n\t\ttry {\n\t\t\tconst stat = statSync(filePath);\n\t\t\tif (stat.mtimeMs < this.startTime) {\n\t\t\t\tlog.logInfo(`Stale immediate event, deleting: ${filename}`);\n\t\t\t\tthis.deleteFile(filename);\n\t\t\t\treturn;\n\t\t\t}\n\t\t} catch {\n\t\t\t// File may have been deleted\n\t\t\treturn;\n\t\t}\n\n\t\tlog.logInfo(`Executing immediate event: ${filename}`);\n\t\tthis.execute(filename, event);\n\t}\n\n\tprivate handleOneShot(filename: string, event: OneShotEvent): void {\n\t\tconst atTime = new Date(event.at).getTime();\n\t\tconst now = Date.now();\n\n\t\tif (atTime <= now) {\n\t\t\t// Past - delete without executing\n\t\t\tlog.logInfo(`One-shot event in the past, deleting: ${filename}`);\n\t\t\tthis.deleteFile(filename);\n\t\t\treturn;\n\t\t}\n\n\t\tconst delay = atTime - now;\n\t\tlog.logInfo(`Scheduling one-shot event: ${filename} in ${Math.round(delay / 1000)}s`);\n\n\t\tconst timer = setTimeout(() => {\n\t\t\tthis.timers.delete(filename);\n\t\t\tlog.logInfo(`Executing one-shot event: ${filename}`);\n\t\t\tthis.execute(filename, event);\n\t\t}, delay);\n\n\t\tthis.timers.set(filename, timer);\n\t}\n\n\tprivate handlePeriodic(filename: string, event: PeriodicEvent): void {\n\t\ttry {\n\t\t\tconst cron = new Cron(event.schedule, { timezone: event.timezone }, () => {\n\t\t\t\tlog.logInfo(`Executing periodic event: ${filename}`);\n\t\t\t\tthis.execute(filename, event, false); // Don't delete periodic events\n\t\t\t});\n\n\t\t\tthis.crons.set(filename, cron);\n\n\t\t\tconst next = cron.nextRun();\n\t\t\tlog.logInfo(`Scheduled periodic event: ${filename}, next run: ${next?.toISOString() ?? \"unknown\"}`);\n\t\t} catch (err) {\n\t\t\tlog.logWarning(`Invalid cron schedule for ${filename}: ${event.schedule}`, String(err));\n\t\t\tthis.deleteFile(filename);\n\t\t}\n\t}\n\n\tprivate execute(filename: string, event: MomEvent, deleteAfter: boolean = true): void {\n\t\t// Format the message\n\t\tlet scheduleInfo: string;\n\t\tswitch (event.type) {\n\t\t\tcase \"immediate\":\n\t\t\t\tscheduleInfo = \"immediate\";\n\t\t\t\tbreak;\n\t\t\tcase \"one-shot\":\n\t\t\t\tscheduleInfo = event.at;\n\t\t\t\tbreak;\n\t\t\tcase \"periodic\":\n\t\t\t\tscheduleInfo = event.schedule;\n\t\t\t\tbreak;\n\t\t}\n\n\t\tconst message = `[EVENT:${filename}:${event.type}:${scheduleInfo}] ${event.text}`;\n\n\t\t// Create synthetic SlackEvent\n\t\tconst syntheticEvent: SlackEvent = {\n\t\t\ttype: \"mention\",\n\t\t\tchannel: event.channelId,\n\t\t\tuser: \"EVENT\",\n\t\t\ttext: message,\n\t\t\tts: Date.now().toString(),\n\t\t};\n\n\t\t// Enqueue for processing\n\t\tconst enqueued = this.slack.enqueueEvent(syntheticEvent);\n\n\t\tif (enqueued && deleteAfter) {\n\t\t\t// Delete file after successful enqueue (immediate and one-shot)\n\t\t\tthis.deleteFile(filename);\n\t\t} else if (!enqueued) {\n\t\t\tlog.logWarning(`Event queue full, discarded: ${filename}`);\n\t\t\t// Still delete immediate/one-shot even if discarded\n\t\t\tif (deleteAfter) {\n\t\t\t\tthis.deleteFile(filename);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate deleteFile(filename: string): void {\n\t\tconst filePath = join(this.eventsDir, filename);\n\t\ttry {\n\t\t\tunlinkSync(filePath);\n\t\t} catch (err) {\n\t\t\t// ENOENT is fine (file already deleted), other errors are warnings\n\t\t\tif (err instanceof Error && \"code\" in err && err.code !== \"ENOENT\") {\n\t\t\t\tlog.logWarning(`Failed to delete event file: ${filename}`, String(err));\n\t\t\t}\n\t\t}\n\t\tthis.knownFiles.delete(filename);\n\t}\n\n\tprivate sleep(ms: number): Promise<void> {\n\t\treturn new Promise((resolve) => setTimeout(resolve, ms));\n\t}\n}\n\n/**\n * Create and start an events watcher.\n */\nexport function createEventsWatcher(workspaceDir: string, slack: SlackBot): EventsWatcher {\n\tconst eventsDir = join(workspaceDir, \"events\");\n\treturn new EventsWatcher(eventsDir, slack);\n}\n"]}
package/dist/events.js ADDED
@@ -0,0 +1,310 @@
1
+ import { Cron } from "croner";
2
+ import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync, watch } from "fs";
3
+ import { readFile } from "fs/promises";
4
+ import { join } from "path";
5
+ import * as log from "./log.js";
6
+ // ============================================================================
7
+ // EventsWatcher
8
+ // ============================================================================
9
+ const DEBOUNCE_MS = 100;
10
+ const MAX_RETRIES = 3;
11
+ const RETRY_BASE_MS = 100;
12
+ export class EventsWatcher {
13
+ eventsDir;
14
+ slack;
15
+ timers = new Map();
16
+ crons = new Map();
17
+ debounceTimers = new Map();
18
+ startTime;
19
+ watcher = null;
20
+ knownFiles = new Set();
21
+ constructor(eventsDir, slack) {
22
+ this.eventsDir = eventsDir;
23
+ this.slack = slack;
24
+ this.startTime = Date.now();
25
+ }
26
+ /**
27
+ * Start watching for events. Call this after SlackBot is ready.
28
+ */
29
+ start() {
30
+ // Ensure events directory exists
31
+ if (!existsSync(this.eventsDir)) {
32
+ mkdirSync(this.eventsDir, { recursive: true });
33
+ }
34
+ log.logInfo(`Events watcher starting, dir: ${this.eventsDir}`);
35
+ // Scan existing files
36
+ this.scanExisting();
37
+ // Watch for changes
38
+ this.watcher = watch(this.eventsDir, (_eventType, filename) => {
39
+ if (!filename || !filename.endsWith(".json"))
40
+ return;
41
+ this.debounce(filename, () => this.handleFileChange(filename));
42
+ });
43
+ log.logInfo(`Events watcher started, tracking ${this.knownFiles.size} files`);
44
+ }
45
+ /**
46
+ * Stop watching and cancel all scheduled events.
47
+ */
48
+ stop() {
49
+ // Stop fs watcher
50
+ if (this.watcher) {
51
+ this.watcher.close();
52
+ this.watcher = null;
53
+ }
54
+ // Cancel all debounce timers
55
+ for (const timer of this.debounceTimers.values()) {
56
+ clearTimeout(timer);
57
+ }
58
+ this.debounceTimers.clear();
59
+ // Cancel all scheduled timers
60
+ for (const timer of this.timers.values()) {
61
+ clearTimeout(timer);
62
+ }
63
+ this.timers.clear();
64
+ // Cancel all cron jobs
65
+ for (const cron of this.crons.values()) {
66
+ cron.stop();
67
+ }
68
+ this.crons.clear();
69
+ this.knownFiles.clear();
70
+ log.logInfo("Events watcher stopped");
71
+ }
72
+ debounce(filename, fn) {
73
+ const existing = this.debounceTimers.get(filename);
74
+ if (existing) {
75
+ clearTimeout(existing);
76
+ }
77
+ this.debounceTimers.set(filename, setTimeout(() => {
78
+ this.debounceTimers.delete(filename);
79
+ fn();
80
+ }, DEBOUNCE_MS));
81
+ }
82
+ scanExisting() {
83
+ let files;
84
+ try {
85
+ files = readdirSync(this.eventsDir).filter((f) => f.endsWith(".json"));
86
+ }
87
+ catch (err) {
88
+ log.logWarning("Failed to read events directory", String(err));
89
+ return;
90
+ }
91
+ for (const filename of files) {
92
+ this.handleFile(filename);
93
+ }
94
+ }
95
+ handleFileChange(filename) {
96
+ const filePath = join(this.eventsDir, filename);
97
+ if (!existsSync(filePath)) {
98
+ // File was deleted
99
+ this.handleDelete(filename);
100
+ }
101
+ else if (this.knownFiles.has(filename)) {
102
+ // File was modified - cancel existing and re-schedule
103
+ this.cancelScheduled(filename);
104
+ this.handleFile(filename);
105
+ }
106
+ else {
107
+ // New file
108
+ this.handleFile(filename);
109
+ }
110
+ }
111
+ handleDelete(filename) {
112
+ if (!this.knownFiles.has(filename))
113
+ return;
114
+ log.logInfo(`Event file deleted: ${filename}`);
115
+ this.cancelScheduled(filename);
116
+ this.knownFiles.delete(filename);
117
+ }
118
+ cancelScheduled(filename) {
119
+ const timer = this.timers.get(filename);
120
+ if (timer) {
121
+ clearTimeout(timer);
122
+ this.timers.delete(filename);
123
+ }
124
+ const cron = this.crons.get(filename);
125
+ if (cron) {
126
+ cron.stop();
127
+ this.crons.delete(filename);
128
+ }
129
+ }
130
+ async handleFile(filename) {
131
+ const filePath = join(this.eventsDir, filename);
132
+ // Parse with retries
133
+ let event = null;
134
+ let lastError = null;
135
+ for (let i = 0; i < MAX_RETRIES; i++) {
136
+ try {
137
+ const content = await readFile(filePath, "utf-8");
138
+ event = this.parseEvent(content, filename);
139
+ break;
140
+ }
141
+ catch (err) {
142
+ lastError = err instanceof Error ? err : new Error(String(err));
143
+ if (i < MAX_RETRIES - 1) {
144
+ await this.sleep(RETRY_BASE_MS * 2 ** i);
145
+ }
146
+ }
147
+ }
148
+ if (!event) {
149
+ log.logWarning(`Failed to parse event file after ${MAX_RETRIES} retries: ${filename}`, lastError?.message);
150
+ this.deleteFile(filename);
151
+ return;
152
+ }
153
+ this.knownFiles.add(filename);
154
+ // Schedule based on type
155
+ switch (event.type) {
156
+ case "immediate":
157
+ this.handleImmediate(filename, event);
158
+ break;
159
+ case "one-shot":
160
+ this.handleOneShot(filename, event);
161
+ break;
162
+ case "periodic":
163
+ this.handlePeriodic(filename, event);
164
+ break;
165
+ }
166
+ }
167
+ parseEvent(content, filename) {
168
+ const data = JSON.parse(content);
169
+ if (!data.type || !data.channelId || !data.text) {
170
+ throw new Error(`Missing required fields (type, channelId, text) in ${filename}`);
171
+ }
172
+ switch (data.type) {
173
+ case "immediate":
174
+ return { type: "immediate", channelId: data.channelId, text: data.text };
175
+ case "one-shot":
176
+ if (!data.at) {
177
+ throw new Error(`Missing 'at' field for one-shot event in ${filename}`);
178
+ }
179
+ return { type: "one-shot", channelId: data.channelId, text: data.text, at: data.at };
180
+ case "periodic":
181
+ if (!data.schedule) {
182
+ throw new Error(`Missing 'schedule' field for periodic event in ${filename}`);
183
+ }
184
+ if (!data.timezone) {
185
+ throw new Error(`Missing 'timezone' field for periodic event in ${filename}`);
186
+ }
187
+ return {
188
+ type: "periodic",
189
+ channelId: data.channelId,
190
+ text: data.text,
191
+ schedule: data.schedule,
192
+ timezone: data.timezone,
193
+ };
194
+ default:
195
+ throw new Error(`Unknown event type '${data.type}' in ${filename}`);
196
+ }
197
+ }
198
+ handleImmediate(filename, event) {
199
+ const filePath = join(this.eventsDir, filename);
200
+ // Check if stale (created before harness started)
201
+ try {
202
+ const stat = statSync(filePath);
203
+ if (stat.mtimeMs < this.startTime) {
204
+ log.logInfo(`Stale immediate event, deleting: ${filename}`);
205
+ this.deleteFile(filename);
206
+ return;
207
+ }
208
+ }
209
+ catch {
210
+ // File may have been deleted
211
+ return;
212
+ }
213
+ log.logInfo(`Executing immediate event: ${filename}`);
214
+ this.execute(filename, event);
215
+ }
216
+ handleOneShot(filename, event) {
217
+ const atTime = new Date(event.at).getTime();
218
+ const now = Date.now();
219
+ if (atTime <= now) {
220
+ // Past - delete without executing
221
+ log.logInfo(`One-shot event in the past, deleting: ${filename}`);
222
+ this.deleteFile(filename);
223
+ return;
224
+ }
225
+ const delay = atTime - now;
226
+ log.logInfo(`Scheduling one-shot event: ${filename} in ${Math.round(delay / 1000)}s`);
227
+ const timer = setTimeout(() => {
228
+ this.timers.delete(filename);
229
+ log.logInfo(`Executing one-shot event: ${filename}`);
230
+ this.execute(filename, event);
231
+ }, delay);
232
+ this.timers.set(filename, timer);
233
+ }
234
+ handlePeriodic(filename, event) {
235
+ try {
236
+ const cron = new Cron(event.schedule, { timezone: event.timezone }, () => {
237
+ log.logInfo(`Executing periodic event: ${filename}`);
238
+ this.execute(filename, event, false); // Don't delete periodic events
239
+ });
240
+ this.crons.set(filename, cron);
241
+ const next = cron.nextRun();
242
+ log.logInfo(`Scheduled periodic event: ${filename}, next run: ${next?.toISOString() ?? "unknown"}`);
243
+ }
244
+ catch (err) {
245
+ log.logWarning(`Invalid cron schedule for ${filename}: ${event.schedule}`, String(err));
246
+ this.deleteFile(filename);
247
+ }
248
+ }
249
+ execute(filename, event, deleteAfter = true) {
250
+ // Format the message
251
+ let scheduleInfo;
252
+ switch (event.type) {
253
+ case "immediate":
254
+ scheduleInfo = "immediate";
255
+ break;
256
+ case "one-shot":
257
+ scheduleInfo = event.at;
258
+ break;
259
+ case "periodic":
260
+ scheduleInfo = event.schedule;
261
+ break;
262
+ }
263
+ const message = `[EVENT:${filename}:${event.type}:${scheduleInfo}] ${event.text}`;
264
+ // Create synthetic SlackEvent
265
+ const syntheticEvent = {
266
+ type: "mention",
267
+ channel: event.channelId,
268
+ user: "EVENT",
269
+ text: message,
270
+ ts: Date.now().toString(),
271
+ };
272
+ // Enqueue for processing
273
+ const enqueued = this.slack.enqueueEvent(syntheticEvent);
274
+ if (enqueued && deleteAfter) {
275
+ // Delete file after successful enqueue (immediate and one-shot)
276
+ this.deleteFile(filename);
277
+ }
278
+ else if (!enqueued) {
279
+ log.logWarning(`Event queue full, discarded: ${filename}`);
280
+ // Still delete immediate/one-shot even if discarded
281
+ if (deleteAfter) {
282
+ this.deleteFile(filename);
283
+ }
284
+ }
285
+ }
286
+ deleteFile(filename) {
287
+ const filePath = join(this.eventsDir, filename);
288
+ try {
289
+ unlinkSync(filePath);
290
+ }
291
+ catch (err) {
292
+ // ENOENT is fine (file already deleted), other errors are warnings
293
+ if (err instanceof Error && "code" in err && err.code !== "ENOENT") {
294
+ log.logWarning(`Failed to delete event file: ${filename}`, String(err));
295
+ }
296
+ }
297
+ this.knownFiles.delete(filename);
298
+ }
299
+ sleep(ms) {
300
+ return new Promise((resolve) => setTimeout(resolve, ms));
301
+ }
302
+ }
303
+ /**
304
+ * Create and start an events watcher.
305
+ */
306
+ export function createEventsWatcher(workspaceDir, slack) {
307
+ const eventsDir = join(workspaceDir, "events");
308
+ return new EventsWatcher(eventsDir, slack);
309
+ }
310
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"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","sourcesContent":["import { Cron } from \"croner\";\nimport { existsSync, type FSWatcher, mkdirSync, readdirSync, statSync, unlinkSync, watch } from \"fs\";\nimport { readFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport * as log from \"./log.js\";\nimport type { SlackBot, SlackEvent } from \"./slack.js\";\n\n// ============================================================================\n// Event Types\n// ============================================================================\n\nexport interface ImmediateEvent {\n\ttype: \"immediate\";\n\tchannelId: string;\n\ttext: string;\n}\n\nexport interface OneShotEvent {\n\ttype: \"one-shot\";\n\tchannelId: string;\n\ttext: string;\n\tat: string; // ISO 8601 with timezone offset\n}\n\nexport interface PeriodicEvent {\n\ttype: \"periodic\";\n\tchannelId: string;\n\ttext: string;\n\tschedule: string; // cron syntax\n\ttimezone: string; // IANA timezone\n}\n\nexport type MomEvent = ImmediateEvent | OneShotEvent | PeriodicEvent;\n\n// ============================================================================\n// EventsWatcher\n// ============================================================================\n\nconst DEBOUNCE_MS = 100;\nconst MAX_RETRIES = 3;\nconst RETRY_BASE_MS = 100;\n\nexport class EventsWatcher {\n\tprivate timers: Map<string, NodeJS.Timeout> = new Map();\n\tprivate crons: Map<string, Cron> = new Map();\n\tprivate debounceTimers: Map<string, NodeJS.Timeout> = new Map();\n\tprivate startTime: number;\n\tprivate watcher: FSWatcher | null = null;\n\tprivate knownFiles: Set<string> = new Set();\n\n\tconstructor(\n\t\tprivate eventsDir: string,\n\t\tprivate slack: SlackBot,\n\t) {\n\t\tthis.startTime = Date.now();\n\t}\n\n\t/**\n\t * Start watching for events. Call this after SlackBot is ready.\n\t */\n\tstart(): void {\n\t\t// Ensure events directory exists\n\t\tif (!existsSync(this.eventsDir)) {\n\t\t\tmkdirSync(this.eventsDir, { recursive: true });\n\t\t}\n\n\t\tlog.logInfo(`Events watcher starting, dir: ${this.eventsDir}`);\n\n\t\t// Scan existing files\n\t\tthis.scanExisting();\n\n\t\t// Watch for changes\n\t\tthis.watcher = watch(this.eventsDir, (_eventType, filename) => {\n\t\t\tif (!filename || !filename.endsWith(\".json\")) return;\n\t\t\tthis.debounce(filename, () => this.handleFileChange(filename));\n\t\t});\n\n\t\tlog.logInfo(`Events watcher started, tracking ${this.knownFiles.size} files`);\n\t}\n\n\t/**\n\t * Stop watching and cancel all scheduled events.\n\t */\n\tstop(): void {\n\t\t// Stop fs watcher\n\t\tif (this.watcher) {\n\t\t\tthis.watcher.close();\n\t\t\tthis.watcher = null;\n\t\t}\n\n\t\t// Cancel all debounce timers\n\t\tfor (const timer of this.debounceTimers.values()) {\n\t\t\tclearTimeout(timer);\n\t\t}\n\t\tthis.debounceTimers.clear();\n\n\t\t// Cancel all scheduled timers\n\t\tfor (const timer of this.timers.values()) {\n\t\t\tclearTimeout(timer);\n\t\t}\n\t\tthis.timers.clear();\n\n\t\t// Cancel all cron jobs\n\t\tfor (const cron of this.crons.values()) {\n\t\t\tcron.stop();\n\t\t}\n\t\tthis.crons.clear();\n\n\t\tthis.knownFiles.clear();\n\t\tlog.logInfo(\"Events watcher stopped\");\n\t}\n\n\tprivate debounce(filename: string, fn: () => void): void {\n\t\tconst existing = this.debounceTimers.get(filename);\n\t\tif (existing) {\n\t\t\tclearTimeout(existing);\n\t\t}\n\t\tthis.debounceTimers.set(\n\t\t\tfilename,\n\t\t\tsetTimeout(() => {\n\t\t\t\tthis.debounceTimers.delete(filename);\n\t\t\t\tfn();\n\t\t\t}, DEBOUNCE_MS),\n\t\t);\n\t}\n\n\tprivate scanExisting(): void {\n\t\tlet files: string[];\n\t\ttry {\n\t\t\tfiles = readdirSync(this.eventsDir).filter((f) => f.endsWith(\".json\"));\n\t\t} catch (err) {\n\t\t\tlog.logWarning(\"Failed to read events directory\", String(err));\n\t\t\treturn;\n\t\t}\n\n\t\tfor (const filename of files) {\n\t\t\tthis.handleFile(filename);\n\t\t}\n\t}\n\n\tprivate handleFileChange(filename: string): void {\n\t\tconst filePath = join(this.eventsDir, filename);\n\n\t\tif (!existsSync(filePath)) {\n\t\t\t// File was deleted\n\t\t\tthis.handleDelete(filename);\n\t\t} else if (this.knownFiles.has(filename)) {\n\t\t\t// File was modified - cancel existing and re-schedule\n\t\t\tthis.cancelScheduled(filename);\n\t\t\tthis.handleFile(filename);\n\t\t} else {\n\t\t\t// New file\n\t\t\tthis.handleFile(filename);\n\t\t}\n\t}\n\n\tprivate handleDelete(filename: string): void {\n\t\tif (!this.knownFiles.has(filename)) return;\n\n\t\tlog.logInfo(`Event file deleted: ${filename}`);\n\t\tthis.cancelScheduled(filename);\n\t\tthis.knownFiles.delete(filename);\n\t}\n\n\tprivate cancelScheduled(filename: string): void {\n\t\tconst timer = this.timers.get(filename);\n\t\tif (timer) {\n\t\t\tclearTimeout(timer);\n\t\t\tthis.timers.delete(filename);\n\t\t}\n\n\t\tconst cron = this.crons.get(filename);\n\t\tif (cron) {\n\t\t\tcron.stop();\n\t\t\tthis.crons.delete(filename);\n\t\t}\n\t}\n\n\tprivate async handleFile(filename: string): Promise<void> {\n\t\tconst filePath = join(this.eventsDir, filename);\n\n\t\t// Parse with retries\n\t\tlet event: MomEvent | null = null;\n\t\tlet lastError: Error | null = null;\n\n\t\tfor (let i = 0; i < MAX_RETRIES; i++) {\n\t\t\ttry {\n\t\t\t\tconst content = await readFile(filePath, \"utf-8\");\n\t\t\t\tevent = this.parseEvent(content, filename);\n\t\t\t\tbreak;\n\t\t\t} catch (err) {\n\t\t\t\tlastError = err instanceof Error ? err : new Error(String(err));\n\t\t\t\tif (i < MAX_RETRIES - 1) {\n\t\t\t\t\tawait this.sleep(RETRY_BASE_MS * 2 ** i);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (!event) {\n\t\t\tlog.logWarning(`Failed to parse event file after ${MAX_RETRIES} retries: ${filename}`, lastError?.message);\n\t\t\tthis.deleteFile(filename);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.knownFiles.add(filename);\n\n\t\t// Schedule based on type\n\t\tswitch (event.type) {\n\t\t\tcase \"immediate\":\n\t\t\t\tthis.handleImmediate(filename, event);\n\t\t\t\tbreak;\n\t\t\tcase \"one-shot\":\n\t\t\t\tthis.handleOneShot(filename, event);\n\t\t\t\tbreak;\n\t\t\tcase \"periodic\":\n\t\t\t\tthis.handlePeriodic(filename, event);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tprivate parseEvent(content: string, filename: string): MomEvent | null {\n\t\tconst data = JSON.parse(content);\n\n\t\tif (!data.type || !data.channelId || !data.text) {\n\t\t\tthrow new Error(`Missing required fields (type, channelId, text) in ${filename}`);\n\t\t}\n\n\t\tswitch (data.type) {\n\t\t\tcase \"immediate\":\n\t\t\t\treturn { type: \"immediate\", channelId: data.channelId, text: data.text };\n\n\t\t\tcase \"one-shot\":\n\t\t\t\tif (!data.at) {\n\t\t\t\t\tthrow new Error(`Missing 'at' field for one-shot event in ${filename}`);\n\t\t\t\t}\n\t\t\t\treturn { type: \"one-shot\", channelId: data.channelId, text: data.text, at: data.at };\n\n\t\t\tcase \"periodic\":\n\t\t\t\tif (!data.schedule) {\n\t\t\t\t\tthrow new Error(`Missing 'schedule' field for periodic event in ${filename}`);\n\t\t\t\t}\n\t\t\t\tif (!data.timezone) {\n\t\t\t\t\tthrow new Error(`Missing 'timezone' field for periodic event in ${filename}`);\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"periodic\",\n\t\t\t\t\tchannelId: data.channelId,\n\t\t\t\t\ttext: data.text,\n\t\t\t\t\tschedule: data.schedule,\n\t\t\t\t\ttimezone: data.timezone,\n\t\t\t\t};\n\n\t\t\tdefault:\n\t\t\t\tthrow new Error(`Unknown event type '${data.type}' in ${filename}`);\n\t\t}\n\t}\n\n\tprivate handleImmediate(filename: string, event: ImmediateEvent): void {\n\t\tconst filePath = join(this.eventsDir, filename);\n\n\t\t// Check if stale (created before harness started)\n\t\ttry {\n\t\t\tconst stat = statSync(filePath);\n\t\t\tif (stat.mtimeMs < this.startTime) {\n\t\t\t\tlog.logInfo(`Stale immediate event, deleting: ${filename}`);\n\t\t\t\tthis.deleteFile(filename);\n\t\t\t\treturn;\n\t\t\t}\n\t\t} catch {\n\t\t\t// File may have been deleted\n\t\t\treturn;\n\t\t}\n\n\t\tlog.logInfo(`Executing immediate event: ${filename}`);\n\t\tthis.execute(filename, event);\n\t}\n\n\tprivate handleOneShot(filename: string, event: OneShotEvent): void {\n\t\tconst atTime = new Date(event.at).getTime();\n\t\tconst now = Date.now();\n\n\t\tif (atTime <= now) {\n\t\t\t// Past - delete without executing\n\t\t\tlog.logInfo(`One-shot event in the past, deleting: ${filename}`);\n\t\t\tthis.deleteFile(filename);\n\t\t\treturn;\n\t\t}\n\n\t\tconst delay = atTime - now;\n\t\tlog.logInfo(`Scheduling one-shot event: ${filename} in ${Math.round(delay / 1000)}s`);\n\n\t\tconst timer = setTimeout(() => {\n\t\t\tthis.timers.delete(filename);\n\t\t\tlog.logInfo(`Executing one-shot event: ${filename}`);\n\t\t\tthis.execute(filename, event);\n\t\t}, delay);\n\n\t\tthis.timers.set(filename, timer);\n\t}\n\n\tprivate handlePeriodic(filename: string, event: PeriodicEvent): void {\n\t\ttry {\n\t\t\tconst cron = new Cron(event.schedule, { timezone: event.timezone }, () => {\n\t\t\t\tlog.logInfo(`Executing periodic event: ${filename}`);\n\t\t\t\tthis.execute(filename, event, false); // Don't delete periodic events\n\t\t\t});\n\n\t\t\tthis.crons.set(filename, cron);\n\n\t\t\tconst next = cron.nextRun();\n\t\t\tlog.logInfo(`Scheduled periodic event: ${filename}, next run: ${next?.toISOString() ?? \"unknown\"}`);\n\t\t} catch (err) {\n\t\t\tlog.logWarning(`Invalid cron schedule for ${filename}: ${event.schedule}`, String(err));\n\t\t\tthis.deleteFile(filename);\n\t\t}\n\t}\n\n\tprivate execute(filename: string, event: MomEvent, deleteAfter: boolean = true): void {\n\t\t// Format the message\n\t\tlet scheduleInfo: string;\n\t\tswitch (event.type) {\n\t\t\tcase \"immediate\":\n\t\t\t\tscheduleInfo = \"immediate\";\n\t\t\t\tbreak;\n\t\t\tcase \"one-shot\":\n\t\t\t\tscheduleInfo = event.at;\n\t\t\t\tbreak;\n\t\t\tcase \"periodic\":\n\t\t\t\tscheduleInfo = event.schedule;\n\t\t\t\tbreak;\n\t\t}\n\n\t\tconst message = `[EVENT:${filename}:${event.type}:${scheduleInfo}] ${event.text}`;\n\n\t\t// Create synthetic SlackEvent\n\t\tconst syntheticEvent: SlackEvent = {\n\t\t\ttype: \"mention\",\n\t\t\tchannel: event.channelId,\n\t\t\tuser: \"EVENT\",\n\t\t\ttext: message,\n\t\t\tts: Date.now().toString(),\n\t\t};\n\n\t\t// Enqueue for processing\n\t\tconst enqueued = this.slack.enqueueEvent(syntheticEvent);\n\n\t\tif (enqueued && deleteAfter) {\n\t\t\t// Delete file after successful enqueue (immediate and one-shot)\n\t\t\tthis.deleteFile(filename);\n\t\t} else if (!enqueued) {\n\t\t\tlog.logWarning(`Event queue full, discarded: ${filename}`);\n\t\t\t// Still delete immediate/one-shot even if discarded\n\t\t\tif (deleteAfter) {\n\t\t\t\tthis.deleteFile(filename);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate deleteFile(filename: string): void {\n\t\tconst filePath = join(this.eventsDir, filename);\n\t\ttry {\n\t\t\tunlinkSync(filePath);\n\t\t} catch (err) {\n\t\t\t// ENOENT is fine (file already deleted), other errors are warnings\n\t\t\tif (err instanceof Error && \"code\" in err && err.code !== \"ENOENT\") {\n\t\t\t\tlog.logWarning(`Failed to delete event file: ${filename}`, String(err));\n\t\t\t}\n\t\t}\n\t\tthis.knownFiles.delete(filename);\n\t}\n\n\tprivate sleep(ms: number): Promise<void> {\n\t\treturn new Promise((resolve) => setTimeout(resolve, ms));\n\t}\n}\n\n/**\n * Create and start an events watcher.\n */\nexport function createEventsWatcher(workspaceDir: string, slack: SlackBot): EventsWatcher {\n\tconst eventsDir = join(workspaceDir, \"events\");\n\treturn new EventsWatcher(eventsDir, slack);\n}\n"]}
package/dist/log.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ export interface LogContext {
2
+ channelId: string;
3
+ userName?: string;
4
+ channelName?: string;
5
+ }
6
+ export declare function logUserMessage(ctx: LogContext, text: string): void;
7
+ export declare function logToolStart(ctx: LogContext, toolName: string, label: string, args: Record<string, unknown>): void;
8
+ export declare function logToolSuccess(ctx: LogContext, toolName: string, durationMs: number, result: string): void;
9
+ export declare function logToolError(ctx: LogContext, toolName: string, durationMs: number, error: string): void;
10
+ export declare function logResponseStart(ctx: LogContext): void;
11
+ export declare function logThinking(ctx: LogContext, thinking: string): void;
12
+ export declare function logResponse(ctx: LogContext, text: string): void;
13
+ export declare function logDownloadStart(ctx: LogContext, filename: string, localPath: string): void;
14
+ export declare function logDownloadSuccess(ctx: LogContext, sizeKB: number): void;
15
+ export declare function logDownloadError(ctx: LogContext, filename: string, error: string): void;
16
+ export declare function logStopRequest(ctx: LogContext): void;
17
+ export declare function logInfo(message: string): void;
18
+ export declare function logWarning(message: string, details?: string): void;
19
+ export declare function logAgentError(ctx: LogContext | "system", error: string): void;
20
+ export declare function logUsageSummary(ctx: LogContext, usage: {
21
+ input: number;
22
+ output: number;
23
+ cacheRead: number;
24
+ cacheWrite: number;
25
+ cost: {
26
+ input: number;
27
+ output: number;
28
+ cacheRead: number;
29
+ cacheWrite: number;
30
+ total: number;
31
+ };
32
+ }, contextTokens?: number, contextWindow?: number): string;
33
+ export declare function logStartup(workingDir: string, sandbox: string): void;
34
+ export declare function logConnected(): void;
35
+ export declare function logDisconnected(): void;
36
+ export declare function logBackfillStart(channelCount: number): void;
37
+ export declare function logBackfillChannel(channelName: string, messageCount: number): void;
38
+ export declare function logBackfillComplete(totalMessages: number, durationMs: number): void;
39
+ //# sourceMappingURL=log.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"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","sourcesContent":["import chalk from \"chalk\";\n\nexport interface LogContext {\n\tchannelId: string;\n\tuserName?: string;\n\tchannelName?: string; // For display like #dev-team vs C16HET4EQ\n}\n\nfunction timestamp(): string {\n\tconst now = new Date();\n\tconst hh = String(now.getHours()).padStart(2, \"0\");\n\tconst mm = String(now.getMinutes()).padStart(2, \"0\");\n\tconst ss = String(now.getSeconds()).padStart(2, \"0\");\n\treturn `[${hh}:${mm}:${ss}]`;\n}\n\nfunction formatContext(ctx: LogContext): string {\n\t// DMs: [DM:username]\n\t// Channels: [#channel-name:username] or [C16HET4EQ:username] if no name\n\tif (ctx.channelId.startsWith(\"D\")) {\n\t\treturn `[DM:${ctx.userName || ctx.channelId}]`;\n\t}\n\tconst channel = ctx.channelName || ctx.channelId;\n\tconst user = ctx.userName || \"unknown\";\n\treturn `[${channel.startsWith(\"#\") ? channel : `#${channel}`}:${user}]`;\n}\n\nfunction truncate(text: string, maxLen: number): string {\n\tif (text.length <= maxLen) return text;\n\treturn `${text.substring(0, maxLen)}\\n(truncated at ${maxLen} chars)`;\n}\n\nfunction formatToolArgs(args: Record<string, unknown>): string {\n\tconst lines: string[] = [];\n\n\tfor (const [key, value] of Object.entries(args)) {\n\t\t// Skip the label - it's already shown in the tool name\n\t\tif (key === \"label\") continue;\n\n\t\t// For read tool, format path with offset/limit\n\t\tif (key === \"path\" && typeof value === \"string\") {\n\t\t\tconst offset = args.offset as number | undefined;\n\t\t\tconst limit = args.limit as number | undefined;\n\t\t\tif (offset !== undefined && limit !== undefined) {\n\t\t\t\tlines.push(`${value}:${offset}-${offset + limit}`);\n\t\t\t} else {\n\t\t\t\tlines.push(value);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Skip offset/limit since we already handled them\n\t\tif (key === \"offset\" || key === \"limit\") continue;\n\n\t\t// For other values, format them\n\t\tif (typeof value === \"string\") {\n\t\t\t// Multi-line strings get indented\n\t\t\tif (value.includes(\"\\n\")) {\n\t\t\t\tlines.push(value);\n\t\t\t} else {\n\t\t\t\tlines.push(value);\n\t\t\t}\n\t\t} else {\n\t\t\tlines.push(JSON.stringify(value));\n\t\t}\n\t}\n\n\treturn lines.join(\"\\n\");\n}\n\n// User messages\nexport function logUserMessage(ctx: LogContext, text: string): void {\n\tconsole.log(chalk.green(`${timestamp()} ${formatContext(ctx)} ${text}`));\n}\n\n// Tool execution\nexport function logToolStart(ctx: LogContext, toolName: string, label: string, args: Record<string, unknown>): void {\n\tconst formattedArgs = formatToolArgs(args);\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ↳ ${toolName}: ${label}`));\n\tif (formattedArgs) {\n\t\t// Indent the args\n\t\tconst indented = formattedArgs\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => ` ${line}`)\n\t\t\t.join(\"\\n\");\n\t\tconsole.log(chalk.dim(indented));\n\t}\n}\n\nexport function logToolSuccess(ctx: LogContext, toolName: string, durationMs: number, result: string): void {\n\tconst duration = (durationMs / 1000).toFixed(1);\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✓ ${toolName} (${duration}s)`));\n\n\tconst truncated = truncate(result, 1000);\n\tif (truncated) {\n\t\tconst indented = truncated\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => ` ${line}`)\n\t\t\t.join(\"\\n\");\n\t\tconsole.log(chalk.dim(indented));\n\t}\n}\n\nexport function logToolError(ctx: LogContext, toolName: string, durationMs: number, error: string): void {\n\tconst duration = (durationMs / 1000).toFixed(1);\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✗ ${toolName} (${duration}s)`));\n\n\tconst truncated = truncate(error, 1000);\n\tconst indented = truncated\n\t\t.split(\"\\n\")\n\t\t.map((line) => ` ${line}`)\n\t\t.join(\"\\n\");\n\tconsole.log(chalk.dim(indented));\n}\n\n// Response streaming\nexport function logResponseStart(ctx: LogContext): void {\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} → Streaming response...`));\n}\n\nexport function logThinking(ctx: LogContext, thinking: string): void {\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} 💭 Thinking`));\n\tconst truncated = truncate(thinking, 1000);\n\tconst indented = truncated\n\t\t.split(\"\\n\")\n\t\t.map((line) => ` ${line}`)\n\t\t.join(\"\\n\");\n\tconsole.log(chalk.dim(indented));\n}\n\nexport function logResponse(ctx: LogContext, text: string): void {\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} 💬 Response`));\n\tconst truncated = truncate(text, 1000);\n\tconst indented = truncated\n\t\t.split(\"\\n\")\n\t\t.map((line) => ` ${line}`)\n\t\t.join(\"\\n\");\n\tconsole.log(chalk.dim(indented));\n}\n\n// Attachments\nexport function logDownloadStart(ctx: LogContext, filename: string, localPath: string): void {\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ↓ Downloading attachment`));\n\tconsole.log(chalk.dim(` ${filename} → ${localPath}`));\n}\n\nexport function logDownloadSuccess(ctx: LogContext, sizeKB: number): void {\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✓ Downloaded (${sizeKB.toLocaleString()} KB)`));\n}\n\nexport function logDownloadError(ctx: LogContext, filename: string, error: string): void {\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✗ Download failed`));\n\tconsole.log(chalk.dim(` ${filename}: ${error}`));\n}\n\n// Control\nexport function logStopRequest(ctx: LogContext): void {\n\tconsole.log(chalk.green(`${timestamp()} ${formatContext(ctx)} stop`));\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ⊗ Stop requested - aborting`));\n}\n\n// System\nexport function logInfo(message: string): void {\n\tconsole.log(chalk.blue(`${timestamp()} [system] ${message}`));\n}\n\nexport function logWarning(message: string, details?: string): void {\n\tconsole.log(chalk.yellow(`${timestamp()} [system] ⚠ ${message}`));\n\tif (details) {\n\t\tconst indented = details\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => ` ${line}`)\n\t\t\t.join(\"\\n\");\n\t\tconsole.log(chalk.dim(indented));\n\t}\n}\n\nexport function logAgentError(ctx: LogContext | \"system\", error: string): void {\n\tconst context = ctx === \"system\" ? \"[system]\" : formatContext(ctx);\n\tconsole.log(chalk.yellow(`${timestamp()} ${context} ✗ Agent error`));\n\tconst indented = error\n\t\t.split(\"\\n\")\n\t\t.map((line) => ` ${line}`)\n\t\t.join(\"\\n\");\n\tconsole.log(chalk.dim(indented));\n}\n\n// Usage summary\nexport function logUsageSummary(\n\tctx: LogContext,\n\tusage: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\tcost: { input: number; output: number; cacheRead: number; cacheWrite: number; total: number };\n\t},\n\tcontextTokens?: number,\n\tcontextWindow?: number,\n): string {\n\tconst formatTokens = (count: number): string => {\n\t\tif (count < 1000) return count.toString();\n\t\tif (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n\t\tif (count < 1000000) return `${Math.round(count / 1000)}k`;\n\t\treturn `${(count / 1000000).toFixed(1)}M`;\n\t};\n\n\tconst lines: string[] = [];\n\tlines.push(\"*Usage Summary*\");\n\tlines.push(`Tokens: ${usage.input.toLocaleString()} in, ${usage.output.toLocaleString()} out`);\n\tif (usage.cacheRead > 0 || usage.cacheWrite > 0) {\n\t\tlines.push(`Cache: ${usage.cacheRead.toLocaleString()} read, ${usage.cacheWrite.toLocaleString()} write`);\n\t}\n\tif (contextTokens && contextWindow) {\n\t\tconst contextPercent = ((contextTokens / contextWindow) * 100).toFixed(1);\n\t\tlines.push(`Context: ${formatTokens(contextTokens)} / ${formatTokens(contextWindow)} (${contextPercent}%)`);\n\t}\n\tlines.push(\n\t\t`Cost: $${usage.cost.input.toFixed(4)} in, $${usage.cost.output.toFixed(4)} out` +\n\t\t\t(usage.cacheRead > 0 || usage.cacheWrite > 0\n\t\t\t\t? `, $${usage.cost.cacheRead.toFixed(4)} cache read, $${usage.cost.cacheWrite.toFixed(4)} cache write`\n\t\t\t\t: \"\"),\n\t);\n\tlines.push(`*Total: $${usage.cost.total.toFixed(4)}*`);\n\n\tconst summary = lines.join(\"\\n\");\n\n\t// Log to console\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} 💰 Usage`));\n\tconsole.log(\n\t\tchalk.dim(\n\t\t\t` ${usage.input.toLocaleString()} in + ${usage.output.toLocaleString()} out` +\n\t\t\t\t(usage.cacheRead > 0 || usage.cacheWrite > 0\n\t\t\t\t\t? ` (${usage.cacheRead.toLocaleString()} cache read, ${usage.cacheWrite.toLocaleString()} cache write)`\n\t\t\t\t\t: \"\") +\n\t\t\t\t` = $${usage.cost.total.toFixed(4)}`,\n\t\t),\n\t);\n\n\treturn summary;\n}\n\n// Startup (no context needed)\nexport function logStartup(workingDir: string, sandbox: string): void {\n\tconsole.log(\"Starting mom bot...\");\n\tconsole.log(` Working directory: ${workingDir}`);\n\tconsole.log(` Sandbox: ${sandbox}`);\n}\n\nexport function logConnected(): void {\n\tconsole.log(\"⚡️ Mom bot connected and listening!\");\n\tconsole.log(\"\");\n}\n\nexport function logDisconnected(): void {\n\tconsole.log(\"Mom bot disconnected.\");\n}\n\n// Backfill\nexport function logBackfillStart(channelCount: number): void {\n\tconsole.log(chalk.blue(`${timestamp()} [system] Backfilling ${channelCount} channels...`));\n}\n\nexport function logBackfillChannel(channelName: string, messageCount: number): void {\n\tconsole.log(chalk.blue(`${timestamp()} [system] #${channelName}: ${messageCount} messages`));\n}\n\nexport function logBackfillComplete(totalMessages: number, durationMs: number): void {\n\tconst duration = (durationMs / 1000).toFixed(1);\n\tconsole.log(chalk.blue(`${timestamp()} [system] Backfill complete: ${totalMessages} messages in ${duration}s`));\n}\n"]}