acp-discord 0.7.1 → 0.8.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.
package/README.md CHANGED
@@ -12,6 +12,7 @@ Send a message in Discord, get AI coding assistance back — with tool call visu
12
12
  - **Tool call visualization** — see what the agent is doing (⏳ pending → 🔄 running → ✅ done / ❌ failed), with a ⏹️ stop button to cancel
13
13
  - **Permission UI** — Discord buttons for approving/denying agent actions, with file diffs shown inline for review before approval
14
14
  - **Discord channel management** — agents can create/delete/modify Discord channels via MCP tools, with user confirmation for all mutating operations
15
+ - **Scheduled tasks** — agents can create once/cron/interval tasks that fire in isolated sessions, with configurable Discord notifications
15
16
  - **Auto-reply mode** — optionally respond to all messages in a channel, not just mentions
16
17
  - **Multi-agent support** — different channels can use different agents
17
18
  - **Daemon mode** — runs in background with auto-start (systemd/launchd)
@@ -48,6 +49,7 @@ args = ["--acp"]
48
49
  cwd = "/path/to/your/project"
49
50
  idle_timeout = 600 # seconds before idle session is terminated (default: 600)
50
51
  discord_tools = true # enable Discord channel management MCP tools (default: false)
52
+ scheduled_tasks = true # enable scheduled task MCP tools (default: false)
51
53
 
52
54
  [channels.1234567890123456]
53
55
  agent = "claude"
@@ -109,6 +111,30 @@ When `discord_tools = true` is set on an agent, the bot injects an MCP server th
109
111
 
110
112
  All mutating operations (create, delete, update) require user approval via Discord buttons before executing. Newly created channels are automatically registered so the bot responds to messages there.
111
113
 
114
+ ### Scheduled Tasks
115
+
116
+ When `scheduled_tasks = true` is set on an agent, the bot injects an MCP server that gives the agent tools to create and manage scheduled tasks. Each task fires by spawning an isolated, ephemeral agent session.
117
+
118
+ | Tool | Description | Requires Approval |
119
+ |------|-------------|:-----------------:|
120
+ | `list_scheduled_tasks` | List all scheduled tasks for this channel | No |
121
+ | `create_scheduled_task` | Create a new scheduled task | Yes |
122
+ | `update_scheduled_task` | Update an existing task | Yes |
123
+ | `delete_scheduled_task` | Delete a task | Yes |
124
+ | `get_task_logs` | Get execution history | No |
125
+
126
+ **Schedule types:**
127
+ - `once` — fire at a specific ISO datetime, then auto-complete
128
+ - `cron` — recurring via cron expression (e.g. `*/5 * * * *`)
129
+ - `interval` — recurring every N seconds, anchored to last run to prevent drift
130
+
131
+ **Notification modes** (per task, default `on_error`):
132
+ - `always` — post agent output to Discord after every run
133
+ - `on_error` — only post if execution fails
134
+ - `never` — silent, log only
135
+
136
+ Tasks are scoped to the channel that created them. Task data is persisted at `~/.acp-discord/scheduled-tasks.json` and execution history at `~/.acp-discord/task-run-logs.json`.
137
+
112
138
  ### Development
113
139
 
114
140
  ```bash
@@ -124,13 +150,18 @@ Discord User
124
150
  ↓ slash command / mention
125
151
  Discord Bot (discord.js)
126
152
  ↓ channel routing ↑ IPC (Unix socket)
127
- Session Manager MCP Server (discord-channels)
153
+ Session Manager MCP Servers (discord-channels, scheduled-tasks)
128
154
  ↓ spawn agent subprocess ↑ MCP tools (stdio)
129
155
  ACP Client (JSON-RPC over stdio)
130
156
  ↓ prompt / permissions / tool calls
131
157
  Agent (claude-code, codex, etc.)
132
158
 
133
159
  Discord messages, embeds, buttons
160
+
161
+ TaskScheduler (15s poll loop)
162
+ ↓ fires due tasks
163
+ Ephemeral ACP Session (isolated, destroyed after)
164
+ ↓ conditionally posts to Discord (based on notify setting)
134
165
  ```
135
166
 
136
167
  ## License
@@ -28,8 +28,8 @@ function isDaemonRunning(pidPath) {
28
28
  }
29
29
 
30
30
  // src/shared/config.ts
31
- import { parse } from "smol-toml";
32
- import { readFileSync as readFileSync2 } from "fs";
31
+ import { parse, stringify } from "smol-toml";
32
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
33
33
  function parseConfig(toml) {
34
34
  const raw = parse(toml);
35
35
  const discord = raw.discord;
@@ -64,12 +64,16 @@ function parseConfig(toml) {
64
64
  if (agent.discord_tools !== void 0 && typeof agent.discord_tools !== "boolean") {
65
65
  throw new Error(`agents.${name}.discord_tools must be a boolean`);
66
66
  }
67
+ if (agent.scheduled_tasks !== void 0 && typeof agent.scheduled_tasks !== "boolean") {
68
+ throw new Error(`agents.${name}.scheduled_tasks must be a boolean`);
69
+ }
67
70
  parsedAgents[name] = {
68
71
  command: agent.command,
69
72
  args: agent.args ?? [],
70
73
  cwd: typeof agent.cwd === "string" ? agent.cwd : process.cwd(),
71
74
  idle_timeout: typeof agent.idle_timeout === "number" ? agent.idle_timeout : 600,
72
- discord_tools: agent.discord_tools === true
75
+ discord_tools: agent.discord_tools === true,
76
+ scheduled_tasks: agent.scheduled_tasks === true
73
77
  };
74
78
  }
75
79
  const channels = raw.channels ?? {};
@@ -91,11 +95,15 @@ function parseConfig(toml) {
91
95
  if (ch.discord_tools !== void 0 && typeof ch.discord_tools !== "boolean") {
92
96
  throw new Error(`channels.${id}.discord_tools must be a boolean`);
93
97
  }
98
+ if (ch.scheduled_tasks !== void 0 && typeof ch.scheduled_tasks !== "boolean") {
99
+ throw new Error(`channels.${id}.scheduled_tasks must be a boolean`);
100
+ }
94
101
  parsedChannels[id] = {
95
102
  agent: agentRef,
96
103
  cwd: ch.cwd ? String(ch.cwd) : void 0,
97
104
  auto_reply: ch.auto_reply === true,
98
- discord_tools: ch.discord_tools ?? void 0
105
+ discord_tools: ch.discord_tools ?? void 0,
106
+ scheduled_tasks: ch.scheduled_tasks ?? void 0
99
107
  };
100
108
  }
101
109
  return {
@@ -108,6 +116,38 @@ function loadConfig(configPath) {
108
116
  const content = readFileSync2(configPath, "utf-8");
109
117
  return parseConfig(content);
110
118
  }
119
+ function saveConfig(configPath, config) {
120
+ const tomlObj = {
121
+ discord: { token: config.discord.token },
122
+ agents: {},
123
+ channels: {}
124
+ };
125
+ const agents = tomlObj.agents;
126
+ for (const [name, agent] of Object.entries(config.agents)) {
127
+ const a = {
128
+ command: agent.command
129
+ };
130
+ if (agent.args.length > 0) a.args = agent.args;
131
+ if (agent.cwd !== process.cwd()) a.cwd = agent.cwd;
132
+ if (agent.idle_timeout !== 600) a.idle_timeout = agent.idle_timeout;
133
+ if (agent.discord_tools) a.discord_tools = agent.discord_tools;
134
+ if ("scheduled_tasks" in agent && agent.scheduled_tasks) a.scheduled_tasks = agent.scheduled_tasks;
135
+ agents[name] = a;
136
+ }
137
+ const channels = tomlObj.channels;
138
+ for (const [id, ch] of Object.entries(config.channels)) {
139
+ const c = {
140
+ agent: ch.agent
141
+ };
142
+ if (ch.cwd !== void 0) c.cwd = ch.cwd;
143
+ if (ch.auto_reply !== void 0) c.auto_reply = ch.auto_reply;
144
+ if (ch.discord_tools !== void 0) c.discord_tools = ch.discord_tools;
145
+ if ("scheduled_tasks" in ch && ch.scheduled_tasks !== void 0) c.scheduled_tasks = ch.scheduled_tasks;
146
+ channels[id] = c;
147
+ }
148
+ const toml = stringify(tomlObj);
149
+ writeFileSync2(configPath, toml, "utf-8");
150
+ }
111
151
  function resolveChannelConfig(config, channelId) {
112
152
  const channelConf = config.channels[channelId];
113
153
  if (!channelConf) return null;
@@ -119,7 +159,8 @@ function resolveChannelConfig(config, channelId) {
119
159
  agent: {
120
160
  ...agentConf,
121
161
  cwd: channelConf.cwd ?? agentConf.cwd,
122
- discord_tools: channelConf.discord_tools ?? agentConf.discord_tools
162
+ discord_tools: channelConf.discord_tools ?? agentConf.discord_tools,
163
+ scheduled_tasks: channelConf.scheduled_tasks ?? agentConf.scheduled_tasks
123
164
  },
124
165
  autoReply: channelConf.auto_reply === true
125
166
  };
@@ -132,6 +173,7 @@ export {
132
173
  isDaemonRunning,
133
174
  parseConfig,
134
175
  loadConfig,
176
+ saveConfig,
135
177
  resolveChannelConfig
136
178
  };
137
- //# sourceMappingURL=chunk-IS5DAXVE.js.map
179
+ //# sourceMappingURL=chunk-RKSH5HHZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/pid.ts","../src/shared/config.ts"],"sourcesContent":["import { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\n\nexport function writePid(pidPath: string, pid: number): void {\n mkdirSync(dirname(pidPath), { recursive: true });\n writeFileSync(pidPath, String(pid), \"utf-8\");\n}\n\nexport function readPid(pidPath: string): number | null {\n if (!existsSync(pidPath)) return null;\n const content = readFileSync(pidPath, \"utf-8\").trim();\n const pid = parseInt(content, 10);\n return isNaN(pid) ? null : pid;\n}\n\nexport function removePid(pidPath: string): void {\n if (existsSync(pidPath)) unlinkSync(pidPath);\n}\n\nexport function isDaemonRunning(pidPath: string): boolean {\n const pid = readPid(pidPath);\n if (pid === null) return false;\n try {\n process.kill(pid, 0); // signal 0 = check if process exists\n return true;\n } catch {\n return false;\n }\n}\n","import { parse, stringify } from \"smol-toml\";\nimport { readFileSync, writeFileSync } from \"node:fs\";\nimport type { AppConfig, ChannelConfig, ResolvedChannelConfig } from \"./types.js\";\n\nexport function parseConfig(toml: string): AppConfig {\n const raw = parse(toml) as Record<string, unknown>;\n\n const discord = raw.discord as Record<string, unknown> | undefined;\n if (!discord?.token || typeof discord.token !== \"string\") {\n throw new Error(\"Missing required: discord.token\");\n }\n if (discord.token.trim().length === 0) {\n throw new Error(\"discord.token must not be empty\");\n }\n\n const agents = raw.agents as Record<string, Record<string, unknown>> | undefined;\n if (!agents || Object.keys(agents).length === 0) {\n throw new Error(\"Missing required: at least one agent in [agents.*]\");\n }\n\n const parsedAgents: AppConfig[\"agents\"] = {};\n for (const [name, agent] of Object.entries(agents)) {\n // Validate command is a non-empty string\n if (!agent.command || typeof agent.command !== \"string\") {\n throw new Error(`agents.${name}.command must be a non-empty string`);\n }\n\n // Validate args is an array of strings\n if (agent.args !== undefined) {\n if (!Array.isArray(agent.args) || !agent.args.every((a: unknown) => typeof a === \"string\")) {\n throw new Error(`agents.${name}.args must be an array of strings`);\n }\n }\n\n // Validate idle_timeout is a positive number\n if (agent.idle_timeout !== undefined) {\n if (typeof agent.idle_timeout !== \"number\" || agent.idle_timeout <= 0) {\n throw new Error(`agents.${name}.idle_timeout must be a positive number`);\n }\n }\n\n // Validate cwd is a string if provided\n if (agent.cwd !== undefined && typeof agent.cwd !== \"string\") {\n throw new Error(`agents.${name}.cwd must be a string`);\n }\n\n // Validate discord_tools is a boolean if provided\n if (agent.discord_tools !== undefined && typeof agent.discord_tools !== \"boolean\") {\n throw new Error(`agents.${name}.discord_tools must be a boolean`);\n }\n\n // Validate scheduled_tasks is a boolean if provided\n if (agent.scheduled_tasks !== undefined && typeof agent.scheduled_tasks !== \"boolean\") {\n throw new Error(`agents.${name}.scheduled_tasks must be a boolean`);\n }\n\n parsedAgents[name] = {\n command: agent.command,\n args: (agent.args as string[]) ?? [],\n cwd: typeof agent.cwd === \"string\" ? agent.cwd : process.cwd(),\n idle_timeout: typeof agent.idle_timeout === \"number\" ? agent.idle_timeout : 600,\n discord_tools: agent.discord_tools === true,\n scheduled_tasks: agent.scheduled_tasks === true,\n };\n }\n\n const channels = (raw.channels ?? {}) as Record<string, Record<string, unknown>>;\n const parsedChannels: AppConfig[\"channels\"] = {};\n for (const [id, ch] of Object.entries(channels)) {\n const agentRef = ch.agent ?? \"default\";\n if (typeof agentRef !== \"string\") {\n throw new Error(`channels.${id}.agent must be a string`);\n }\n if (!parsedAgents[agentRef]) {\n throw new Error(`channels.${id}.agent references unknown agent \"${agentRef}\"`);\n }\n if (ch.cwd !== undefined && typeof ch.cwd !== \"string\") {\n throw new Error(`channels.${id}.cwd must be a string`);\n }\n if (ch.auto_reply !== undefined && typeof ch.auto_reply !== \"boolean\") {\n throw new Error(`channels.${id}.auto_reply must be a boolean`);\n }\n if (ch.discord_tools !== undefined && typeof ch.discord_tools !== \"boolean\") {\n throw new Error(`channels.${id}.discord_tools must be a boolean`);\n }\n if (ch.scheduled_tasks !== undefined && typeof ch.scheduled_tasks !== \"boolean\") {\n throw new Error(`channels.${id}.scheduled_tasks must be a boolean`);\n }\n\n parsedChannels[id] = {\n agent: agentRef,\n cwd: ch.cwd ? String(ch.cwd) : undefined,\n auto_reply: ch.auto_reply === true,\n discord_tools: ch.discord_tools ?? undefined,\n scheduled_tasks: ch.scheduled_tasks ?? undefined,\n };\n }\n\n return {\n discord: { token: String(discord.token) },\n agents: parsedAgents,\n channels: parsedChannels,\n };\n}\n\nexport function loadConfig(configPath: string): AppConfig {\n const content = readFileSync(configPath, \"utf-8\");\n return parseConfig(content);\n}\n\nexport function saveConfig(configPath: string, config: AppConfig): void {\n // Convert AppConfig to a plain object suitable for smol-toml stringify\n const tomlObj: Record<string, unknown> = {\n discord: { token: config.discord.token },\n agents: {} as Record<string, Record<string, unknown>>,\n channels: {} as Record<string, Record<string, unknown>>,\n };\n\n const agents = tomlObj.agents as Record<string, Record<string, unknown>>;\n for (const [name, agent] of Object.entries(config.agents)) {\n const a: Record<string, unknown> = {\n command: agent.command,\n };\n if (agent.args.length > 0) a.args = agent.args;\n if (agent.cwd !== process.cwd()) a.cwd = agent.cwd;\n if (agent.idle_timeout !== 600) a.idle_timeout = agent.idle_timeout;\n if (agent.discord_tools) a.discord_tools = agent.discord_tools;\n if (\"scheduled_tasks\" in agent && agent.scheduled_tasks) a.scheduled_tasks = agent.scheduled_tasks;\n agents[name] = a;\n }\n\n const channels = tomlObj.channels as Record<string, Record<string, unknown>>;\n for (const [id, ch] of Object.entries(config.channels)) {\n const c: Record<string, unknown> = {\n agent: ch.agent,\n };\n if (ch.cwd !== undefined) c.cwd = ch.cwd;\n if (ch.auto_reply !== undefined) c.auto_reply = ch.auto_reply;\n if (ch.discord_tools !== undefined) c.discord_tools = ch.discord_tools;\n if (\"scheduled_tasks\" in ch && ch.scheduled_tasks !== undefined) c.scheduled_tasks = ch.scheduled_tasks;\n channels[id] = c;\n }\n\n const toml = stringify(tomlObj);\n writeFileSync(configPath, toml, \"utf-8\");\n}\n\nexport function addChannelToConfig(\n configPath: string,\n channelId: string,\n channelConfig: ChannelConfig,\n): AppConfig {\n const config = loadConfig(configPath);\n config.channels[channelId] = channelConfig;\n saveConfig(configPath, config);\n return config;\n}\n\nexport function removeChannelFromConfig(\n configPath: string,\n channelId: string,\n): AppConfig {\n const config = loadConfig(configPath);\n delete config.channels[channelId];\n saveConfig(configPath, config);\n return config;\n}\n\nexport function resolveChannelConfig(\n config: AppConfig,\n channelId: string,\n): ResolvedChannelConfig | null {\n const channelConf = config.channels[channelId];\n if (!channelConf) return null;\n\n const agentConf = config.agents[channelConf.agent];\n if (!agentConf) return null;\n\n return {\n channelId,\n agentName: channelConf.agent,\n agent: {\n ...agentConf,\n cwd: channelConf.cwd ?? agentConf.cwd,\n discord_tools: channelConf.discord_tools ?? agentConf.discord_tools,\n scheduled_tasks: channelConf.scheduled_tasks ?? agentConf.scheduled_tasks,\n },\n autoReply: channelConf.auto_reply === true,\n };\n}\n"],"mappings":"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","names":["readFileSync","writeFileSync"]}