agentcord 2.1.0 → 2.1.1

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
@@ -1,6 +1,6 @@
1
1
  # agentcord
2
2
 
3
- Run and manage AI coding agent sessions on your machine through Discord. Currently supports [Claude Code](https://docs.anthropic.com/en/docs/claude-code), with more agents coming soon.
3
+ Run and manage AI coding agent sessions on your machine through Discord. Supports [Claude Code](https://docs.anthropic.com/en/docs/claude-code) and OpenAI Codex.
4
4
 
5
5
  Each session gets a Discord channel for chatting with the agent and a tmux session for direct terminal access. Sessions are organized by project — create multiple sessions in the same codebase, each with their own channel.
6
6
 
@@ -20,6 +20,7 @@ The setup wizard walks you through creating a Discord app, configuring the bot t
20
20
  - **Node.js 22.6+** (uses native TypeScript execution)
21
21
  - **tmux** (for terminal session access)
22
22
  - **Claude Code** installed on the machine (`@anthropic-ai/claude-agent-sdk`)
23
+ - **OpenAI Codex SDK** for Codex sessions (`@openai/codex-sdk`)
23
24
 
24
25
  ## How It Works
25
26
 
@@ -58,7 +59,7 @@ Discord Server
58
59
  | `/claude attach` | Show tmux attach command for terminal access |
59
60
  | `/claude model <model>` | Change model for the session |
60
61
  | `/claude verbose` | Toggle tool call/result visibility |
61
- | `/claude sync` | Reconnect orphaned tmux sessions |
62
+ | `/claude sync` | Reconnect orphaned sessions (tmux + existing provider channels, including Codex) |
62
63
 
63
64
  ### Shell
64
65
 
@@ -126,7 +126,7 @@ function getCommandDefinitions() {
126
126
  { name: "On request", value: "on-request" },
127
127
  { name: "On failure", value: "on-failure" },
128
128
  { name: "Untrusted", value: "untrusted" }
129
- )).addBooleanOption((opt) => opt.setName("network-access").setDescription("Allow network in workspace-write sandbox (Codex only)")).addStringOption((opt) => opt.setName("directory").setDescription("Working directory (default: configured default)"))).addSubcommand((sub) => sub.setName("list").setDescription("List active sessions")).addSubcommand((sub) => sub.setName("end").setDescription("End the session in this channel")).addSubcommand((sub) => sub.setName("continue").setDescription("Continue the last conversation")).addSubcommand((sub) => sub.setName("stop").setDescription("Stop current generation")).addSubcommand((sub) => sub.setName("output").setDescription("Show recent conversation output").addIntegerOption((opt) => opt.setName("lines").setDescription("Number of lines (default 50)").setMinValue(1).setMaxValue(500))).addSubcommand((sub) => sub.setName("attach").setDescription("Show tmux attach command for terminal access")).addSubcommand((sub) => sub.setName("sync").setDescription("Reconnect orphaned tmux sessions")).addSubcommand((sub) => sub.setName("model").setDescription("Change the model for this session").addStringOption((opt) => opt.setName("model").setDescription("Model name (e.g. claude-sonnet-4-5-20250929, gpt-5.3-codex)").setRequired(true))).addSubcommand((sub) => sub.setName("id").setDescription("Show the provider session ID for this channel")).addSubcommand((sub) => sub.setName("verbose").setDescription("Toggle showing tool calls and results in this session")).addSubcommand((sub) => sub.setName("mode").setDescription("Set session mode (auto/plan/normal)").addStringOption((opt) => opt.setName("mode").setDescription("Session mode").setRequired(true).addChoices(
129
+ )).addBooleanOption((opt) => opt.setName("network-access").setDescription("Allow network in workspace-write sandbox (Codex only)")).addStringOption((opt) => opt.setName("directory").setDescription("Working directory (default: configured default)"))).addSubcommand((sub) => sub.setName("list").setDescription("List active sessions")).addSubcommand((sub) => sub.setName("end").setDescription("End the session in this channel")).addSubcommand((sub) => sub.setName("continue").setDescription("Continue the last conversation")).addSubcommand((sub) => sub.setName("stop").setDescription("Stop current generation")).addSubcommand((sub) => sub.setName("output").setDescription("Show recent conversation output").addIntegerOption((opt) => opt.setName("lines").setDescription("Number of lines (default 50)").setMinValue(1).setMaxValue(500))).addSubcommand((sub) => sub.setName("attach").setDescription("Show tmux attach command for terminal access")).addSubcommand((sub) => sub.setName("sync").setDescription("Reconnect orphaned sessions (tmux + provider channels)")).addSubcommand((sub) => sub.setName("model").setDescription("Change the model for this session").addStringOption((opt) => opt.setName("model").setDescription("Model name (e.g. claude-sonnet-4-5-20250929, gpt-5.3-codex)").setRequired(true))).addSubcommand((sub) => sub.setName("id").setDescription("Show the provider session ID for this channel")).addSubcommand((sub) => sub.setName("verbose").setDescription("Toggle showing tool calls and results in this session")).addSubcommand((sub) => sub.setName("mode").setDescription("Set session mode (auto/plan/normal)").addStringOption((opt) => opt.setName("mode").setDescription("Session mode").setRequired(true).addChoices(
130
130
  { name: "Auto \u2014 full autonomy", value: "auto" },
131
131
  { name: "Plan \u2014 plan before executing", value: "plan" },
132
132
  { name: "Normal \u2014 ask before destructive ops", value: "normal" }
@@ -943,14 +943,27 @@ async function createSession(name, directory, channelId, projectName, provider =
943
943
  const usesTmux = providerInstance.supports("tmux");
944
944
  let id = sanitizeSessionName(name);
945
945
  let tmuxName = usesTmux ? `${SESSION_PREFIX}${id}` : "";
946
- let suffix = 1;
947
- while (sessions.has(id) || usesTmux && await tmuxSessionExists(tmuxName)) {
948
- suffix++;
949
- id = sanitizeSessionName(`${name}-${suffix}`);
950
- if (usesTmux) tmuxName = `${SESSION_PREFIX}${id}`;
946
+ if (options.recoverExisting) {
947
+ if (sessions.has(id)) {
948
+ throw new Error(`Session "${id}" already exists`);
949
+ }
950
+ } else {
951
+ let suffix = 1;
952
+ while (sessions.has(id) || usesTmux && await tmuxSessionExists(tmuxName)) {
953
+ suffix++;
954
+ id = sanitizeSessionName(`${name}-${suffix}`);
955
+ if (usesTmux) tmuxName = `${SESSION_PREFIX}${id}`;
956
+ }
951
957
  }
952
958
  if (usesTmux) {
953
- await tmux("new-session", "-d", "-s", tmuxName, "-c", resolvedDir);
959
+ if (options.recoverExisting) {
960
+ const existing = await tmuxSessionExists(tmuxName);
961
+ if (!existing) {
962
+ await tmux("new-session", "-d", "-s", tmuxName, "-c", resolvedDir);
963
+ }
964
+ } else {
965
+ await tmux("new-session", "-d", "-s", tmuxName, "-c", resolvedDir);
966
+ }
954
967
  }
955
968
  const session = {
956
969
  id,
@@ -1867,6 +1880,17 @@ ${event.message}
1867
1880
  break;
1868
1881
  }
1869
1882
  case "session_init": {
1883
+ const session = getSession(sessionId);
1884
+ const providerSessionId = event.providerSessionId || session?.providerSessionId;
1885
+ if (providerSessionId) {
1886
+ const currentTopic = channel.topic ?? "";
1887
+ const topicBase = currentTopic ? currentTopic.replace(/\s*\|\s*Provider Session:\s*[^\s|]+/i, "") : `${session?.provider === "codex" ? "OpenAI Codex" : "Claude Code"} session | Dir: ${session?.directory || "unknown"}`;
1888
+ const nextTopic = truncate(`${topicBase} | Provider Session: ${providerSessionId}`, 1024);
1889
+ if (nextTopic !== currentTopic) {
1890
+ await channel.setTopic(nextTopic).catch(() => {
1891
+ });
1892
+ }
1893
+ }
1870
1894
  break;
1871
1895
  }
1872
1896
  }
@@ -2072,6 +2096,16 @@ function addCodexPolicyFields(fields, options) {
2072
2096
  fields.push({ name: "Network Access", value: options.networkAccessEnabled ? "enabled" : "disabled", inline: true });
2073
2097
  }
2074
2098
  }
2099
+ function parseTopicDirectory(topic) {
2100
+ if (!topic) return null;
2101
+ const m = topic.match(/\bDir:\s*(.+?)(?:\s*\|\s*Provider Session:|$)/i);
2102
+ return m?.[1]?.trim() || null;
2103
+ }
2104
+ function parseTopicProviderSessionId(topic) {
2105
+ if (!topic) return void 0;
2106
+ const m = topic.match(/\bProvider Session:\s*([^\s|]+)/i);
2107
+ return m?.[1]?.trim() || void 0;
2108
+ }
2075
2109
  async function handleSessionNew(interaction) {
2076
2110
  const name = interaction.options.getString("name", true);
2077
2111
  const provider = interaction.options.getString("provider") || "claude";
@@ -2279,7 +2313,7 @@ async function handleSessionResume(interaction) {
2279
2313
  name: `${provider}-${session.id}`,
2280
2314
  type: ChannelType.GuildText,
2281
2315
  parent: category.id,
2282
- topic: `${PROVIDER_LABELS[provider]} session (resumed) | Dir: ${directory}`
2316
+ topic: `${PROVIDER_LABELS[provider]} session (resumed) | Dir: ${directory} | Provider Session: ${providerSessionId}`
2283
2317
  });
2284
2318
  await linkChannel(session.id, channel.id);
2285
2319
  const fields = [
@@ -2458,7 +2492,38 @@ async function handleSessionSync(interaction) {
2458
2492
  const tmuxSessions = await listTmuxSessions();
2459
2493
  const currentSessions = getAllSessions();
2460
2494
  const currentIds = new Set(currentSessions.map((s) => s.id));
2461
- let synced = 0;
2495
+ const currentChannelIds = new Set(currentSessions.map((s) => s.channelId));
2496
+ let syncedTmux = 0;
2497
+ let syncedChannels = 0;
2498
+ for (const ch of guild.channels.cache.values()) {
2499
+ if (ch.type !== ChannelType.GuildText) continue;
2500
+ if (currentChannelIds.has(ch.id)) continue;
2501
+ const m = ch.name.match(/^(claude|codex)-(.+)$/);
2502
+ if (!m) continue;
2503
+ const provider = m[1];
2504
+ const sessionName = m[2];
2505
+ const directory = parseTopicDirectory(ch.topic) || config.defaultDirectory;
2506
+ const providerSessionId = parseTopicProviderSessionId(ch.topic);
2507
+ const projectName = projectNameFromDir(directory);
2508
+ if (ch.parentId) {
2509
+ getOrCreateProject(projectName, directory, ch.parentId);
2510
+ }
2511
+ try {
2512
+ const recovered = await createSession(
2513
+ sessionName,
2514
+ directory,
2515
+ ch.id,
2516
+ projectName,
2517
+ provider,
2518
+ providerSessionId,
2519
+ { recoverExisting: true }
2520
+ );
2521
+ syncedChannels++;
2522
+ currentIds.add(recovered.id);
2523
+ currentChannelIds.add(ch.id);
2524
+ } catch {
2525
+ }
2526
+ }
2462
2527
  for (const tmuxSession of tmuxSessions) {
2463
2528
  if (currentIds.has(tmuxSession.id)) continue;
2464
2529
  const projectName = projectNameFromDir(tmuxSession.directory);
@@ -2469,11 +2534,25 @@ async function handleSessionSync(interaction) {
2469
2534
  parent: category.id,
2470
2535
  topic: `Claude Code session (synced) | Dir: ${tmuxSession.directory}`
2471
2536
  });
2472
- await createSession(tmuxSession.id, tmuxSession.directory, channel.id, projectName, "claude");
2473
- synced++;
2474
- }
2537
+ const recovered = await createSession(
2538
+ tmuxSession.id,
2539
+ tmuxSession.directory,
2540
+ channel.id,
2541
+ projectName,
2542
+ "claude",
2543
+ void 0,
2544
+ { recoverExisting: true }
2545
+ );
2546
+ syncedTmux++;
2547
+ currentIds.add(recovered.id);
2548
+ currentChannelIds.add(channel.id);
2549
+ }
2550
+ const synced = syncedChannels + syncedTmux;
2551
+ const detail = [];
2552
+ if (syncedChannels > 0) detail.push(`${syncedChannels} channel`);
2553
+ if (syncedTmux > 0) detail.push(`${syncedTmux} tmux`);
2475
2554
  await interaction.editReply(
2476
- synced > 0 ? `Synced ${synced} orphaned session(s).` : "No orphaned sessions found."
2555
+ synced > 0 ? `Synced ${synced} orphaned session(s) (${detail.join(", ")}).` : "No orphaned sessions found."
2477
2556
  );
2478
2557
  }
2479
2558
  async function handleSessionId(interaction) {
package/dist/cli.js CHANGED
@@ -18,7 +18,7 @@ switch (command) {
18
18
  console.log("Run \x1B[36magentcord setup\x1B[0m to configure.\n");
19
19
  process.exit(1);
20
20
  }
21
- const { startBot } = await import("./bot-C6RYOLAL.js");
21
+ const { startBot } = await import("./bot-QXUF3LNK.js");
22
22
  console.log("agentcord starting...");
23
23
  await startBot();
24
24
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentcord",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "type": "module",
5
5
  "description": "Discord bot for managing AI coding agent sessions (Claude Code, Codex, and more)",
6
6
  "bin": {