agentcord 0.2.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/.env.example CHANGED
@@ -16,6 +16,11 @@ DISCORD_CLIENT_ID=your-application-client-id
16
16
  # Optional: Default working directory for new sessions
17
17
  # DEFAULT_DIRECTORY=/Users/me/Dev
18
18
 
19
+ # Optional: Default Codex execution policy for new/resumed Codex sessions
20
+ # CODEX_SANDBOX_MODE=workspace-write # read-only | workspace-write | danger-full-access
21
+ # CODEX_APPROVAL_POLICY=on-request # never | on-request | on-failure | untrusted
22
+ # CODEX_NETWORK_ACCESS_ENABLED=true # true | false
23
+
19
24
  # Optional: Auto-delete messages older than N days
20
25
  # MESSAGE_RETENTION_DAYS=7
21
26
 
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
 
@@ -118,8 +119,15 @@ ALLOWED_USERS=123456789,987654321 # Comma-separated user IDs
118
119
  ALLOW_ALL_USERS=false # Or true to skip whitelist
119
120
  ALLOWED_PATHS=/Users/me/Dev # Restrict accessible directories
120
121
  DEFAULT_DIRECTORY=/Users/me/Dev # Default for new sessions
122
+ CODEX_SANDBOX_MODE=workspace-write # read-only | workspace-write | danger-full-access
123
+ CODEX_APPROVAL_POLICY=on-request # never | on-request | on-failure | untrusted
124
+ CODEX_NETWORK_ACCESS_ENABLED=true # true | false
121
125
  ```
122
126
 
127
+ You can also override Codex policy per session when creating/resuming via:
128
+ - `/session new ... sandbox-mode:<mode> approval-policy:<policy> network-access:<bool>`
129
+ - `/session resume ... sandbox-mode:<mode> approval-policy:<policy> network-access:<bool>`
130
+
123
131
  ## Development
124
132
 
125
133
  ```bash
@@ -20,6 +20,45 @@ function getEnvOrExit(name) {
20
20
  }
21
21
  return value;
22
22
  }
23
+ var CODEX_SANDBOX_MODES = /* @__PURE__ */ new Set([
24
+ "read-only",
25
+ "workspace-write",
26
+ "danger-full-access"
27
+ ]);
28
+ var CODEX_APPROVAL_POLICIES = /* @__PURE__ */ new Set([
29
+ "never",
30
+ "on-request",
31
+ "on-failure",
32
+ "untrusted"
33
+ ]);
34
+ function parseCodexSandboxMode(value) {
35
+ if (!value) return void 0;
36
+ if (CODEX_SANDBOX_MODES.has(value)) {
37
+ return value;
38
+ }
39
+ console.error(
40
+ `ERROR: Invalid CODEX_SANDBOX_MODE "${value}". Expected one of: ${Array.from(CODEX_SANDBOX_MODES).join(", ")}`
41
+ );
42
+ process.exit(1);
43
+ }
44
+ function parseCodexApprovalPolicy(value) {
45
+ if (!value) return void 0;
46
+ if (CODEX_APPROVAL_POLICIES.has(value)) {
47
+ return value;
48
+ }
49
+ console.error(
50
+ `ERROR: Invalid CODEX_APPROVAL_POLICY "${value}". Expected one of: ${Array.from(CODEX_APPROVAL_POLICIES).join(", ")}`
51
+ );
52
+ process.exit(1);
53
+ }
54
+ function parseBoolean(name, value) {
55
+ if (value === void 0) return void 0;
56
+ const normalized = value.trim().toLowerCase();
57
+ if (normalized === "true") return true;
58
+ if (normalized === "false") return false;
59
+ console.error(`ERROR: Invalid ${name} "${value}". Expected "true" or "false".`);
60
+ process.exit(1);
61
+ }
23
62
  var config = {
24
63
  token: getEnvOrExit("DISCORD_TOKEN"),
25
64
  clientId: getEnvOrExit("DISCORD_CLIENT_ID"),
@@ -29,7 +68,10 @@ var config = {
29
68
  allowedPaths: process.env.ALLOWED_PATHS?.split(",").map((p) => p.trim()).filter(Boolean) || [],
30
69
  defaultDirectory: process.env.DEFAULT_DIRECTORY || process.cwd(),
31
70
  messageRetentionDays: process.env.MESSAGE_RETENTION_DAYS ? parseInt(process.env.MESSAGE_RETENTION_DAYS, 10) : null,
32
- rateLimitMs: process.env.RATE_LIMIT_MS ? parseInt(process.env.RATE_LIMIT_MS, 10) : 1e3
71
+ rateLimitMs: process.env.RATE_LIMIT_MS ? parseInt(process.env.RATE_LIMIT_MS, 10) : 1e3,
72
+ codexSandboxMode: parseCodexSandboxMode(process.env.CODEX_SANDBOX_MODE),
73
+ codexApprovalPolicy: parseCodexApprovalPolicy(process.env.CODEX_APPROVAL_POLICY),
74
+ codexNetworkAccessEnabled: parseBoolean("CODEX_NETWORK_ACCESS_ENABLED", process.env.CODEX_NETWORK_ACCESS_ENABLED)
33
75
  };
34
76
  if (config.allowedUsers.length > 0) {
35
77
  console.log(`User whitelist: ${config.allowedUsers.length} user(s) allowed`);
@@ -45,6 +87,13 @@ if (config.allowedPaths.length > 0) {
45
87
  if (config.messageRetentionDays) {
46
88
  console.log(`Message retention: ${config.messageRetentionDays} day(s)`);
47
89
  }
90
+ if (config.codexSandboxMode || config.codexApprovalPolicy || config.codexNetworkAccessEnabled !== void 0) {
91
+ const bits = [];
92
+ if (config.codexSandboxMode) bits.push(`sandbox=${config.codexSandboxMode}`);
93
+ if (config.codexApprovalPolicy) bits.push(`approval=${config.codexApprovalPolicy}`);
94
+ if (config.codexNetworkAccessEnabled !== void 0) bits.push(`network=${config.codexNetworkAccessEnabled}`);
95
+ console.log(`Codex defaults: ${bits.join(", ")}`);
96
+ }
48
97
 
49
98
  // src/commands.ts
50
99
  import {
@@ -56,10 +105,28 @@ function getCommandDefinitions() {
56
105
  const session = new SlashCommandBuilder().setName("session").setDescription("Manage AI coding sessions").addSubcommand((sub) => sub.setName("new").setDescription("Create a new coding session").addStringOption((opt) => opt.setName("name").setDescription("Session name").setRequired(true)).addStringOption((opt) => opt.setName("provider").setDescription("AI provider").addChoices(
57
106
  { name: "Claude Code", value: "claude" },
58
107
  { name: "OpenAI Codex", value: "codex" }
59
- )).addStringOption((opt) => opt.setName("directory").setDescription("Working directory (default: configured default)"))).addSubcommand((sub) => sub.setName("resume").setDescription("Resume an existing session from terminal").addStringOption((opt) => opt.setName("session-id").setDescription("Provider session ID").setRequired(true).setAutocomplete(true)).addStringOption((opt) => opt.setName("name").setDescription("Name for the Discord channel").setRequired(true)).addStringOption((opt) => opt.setName("provider").setDescription("AI provider").addChoices(
108
+ )).addStringOption((opt) => opt.setName("sandbox-mode").setDescription("Codex sandbox mode (Codex provider only)").addChoices(
109
+ { name: "Read-only", value: "read-only" },
110
+ { name: "Workspace write", value: "workspace-write" },
111
+ { name: "Danger full access", value: "danger-full-access" }
112
+ )).addStringOption((opt) => opt.setName("approval-policy").setDescription("Codex approval policy (Codex provider only)").addChoices(
113
+ { name: "Never ask", value: "never" },
114
+ { name: "On request", value: "on-request" },
115
+ { name: "On failure", value: "on-failure" },
116
+ { name: "Untrusted", value: "untrusted" }
117
+ )).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("resume").setDescription("Resume an existing session from terminal").addStringOption((opt) => opt.setName("session-id").setDescription("Provider session ID").setRequired(true).setAutocomplete(true)).addStringOption((opt) => opt.setName("name").setDescription("Name for the Discord channel").setRequired(true)).addStringOption((opt) => opt.setName("provider").setDescription("AI provider").addChoices(
60
118
  { name: "Claude Code", value: "claude" },
61
119
  { name: "OpenAI Codex", value: "codex" }
62
- )).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(
120
+ )).addStringOption((opt) => opt.setName("sandbox-mode").setDescription("Codex sandbox mode (Codex provider only)").addChoices(
121
+ { name: "Read-only", value: "read-only" },
122
+ { name: "Workspace write", value: "workspace-write" },
123
+ { name: "Danger full access", value: "danger-full-access" }
124
+ )).addStringOption((opt) => opt.setName("approval-policy").setDescription("Codex approval policy (Codex provider only)").addChoices(
125
+ { name: "Never ask", value: "never" },
126
+ { name: "On request", value: "on-request" },
127
+ { name: "On failure", value: "on-failure" },
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 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(
63
130
  { name: "Auto \u2014 full autonomy", value: "auto" },
64
131
  { name: "Plan \u2014 plan before executing", value: "plan" },
65
132
  { name: "Normal \u2014 ask before destructive ops", value: "normal" }
@@ -366,7 +433,7 @@ function installPackageGlobally(pkg) {
366
433
  async function loadCodexProvider() {
367
434
  const pkg = PROVIDER_PACKAGES.codex;
368
435
  try {
369
- const { CodexProvider } = await import("./codex-provider-672ILQC2.js");
436
+ const { CodexProvider } = await import("./codex-provider-BB2OO3OK.js");
370
437
  providers.set("codex", new CodexProvider());
371
438
  codexLoaded = true;
372
439
  return;
@@ -382,7 +449,7 @@ async function loadCodexProvider() {
382
449
  }
383
450
  }
384
451
  try {
385
- const { CodexProvider } = await import("./codex-provider-672ILQC2.js");
452
+ const { CodexProvider } = await import("./codex-provider-BB2OO3OK.js");
386
453
  providers.set("codex", new CodexProvider());
387
454
  codexLoaded = true;
388
455
  } catch (err) {
@@ -759,6 +826,7 @@ var MODE_PROMPTS = {
759
826
  var sessionStore = new Store("sessions.json");
760
827
  var sessions = /* @__PURE__ */ new Map();
761
828
  var channelToSession = /* @__PURE__ */ new Map();
829
+ var saveQueue = Promise.resolve();
762
830
  function tmux(...args) {
763
831
  return new Promise((resolve2, reject) => {
764
832
  execFile("tmux", args, { encoding: "utf-8" }, (err, stdout) => {
@@ -775,10 +843,24 @@ async function tmuxSessionExists(tmuxName) {
775
843
  return false;
776
844
  }
777
845
  }
846
+ function isPlaceholderChannelId(channelId) {
847
+ return !channelId || channelId === "pending";
848
+ }
778
849
  async function loadSessions() {
779
850
  const data = await sessionStore.read();
780
851
  if (!data) return;
852
+ let cleaned = false;
781
853
  for (const s of data) {
854
+ if (isPlaceholderChannelId(s.channelId)) {
855
+ cleaned = true;
856
+ console.warn(`Skipping invalid persisted session "${s.id}" (missing channel ID).`);
857
+ continue;
858
+ }
859
+ if (channelToSession.has(s.channelId)) {
860
+ cleaned = true;
861
+ console.warn(`Skipping duplicate persisted session "${s.id}" (channel ${s.channelId} already linked).`);
862
+ continue;
863
+ }
782
864
  const provider = s.provider ?? "claude";
783
865
  const providerSessionId = s.providerSessionId ?? s.claudeSessionId;
784
866
  if (provider === "claude") {
@@ -801,11 +883,15 @@ async function loadSessions() {
801
883
  });
802
884
  channelToSession.set(s.channelId, s.id);
803
885
  }
886
+ if (cleaned) {
887
+ await saveSessions();
888
+ }
804
889
  console.log(`Restored ${sessions.size} session(s)`);
805
890
  }
806
- async function saveSessions() {
891
+ async function persistSessionsNow() {
807
892
  const data = [];
808
893
  for (const [, s] of sessions) {
894
+ if (isPlaceholderChannelId(s.channelId)) continue;
809
895
  data.push({
810
896
  id: s.id,
811
897
  channelId: s.channelId,
@@ -815,6 +901,9 @@ async function saveSessions() {
815
901
  tmuxName: s.tmuxName,
816
902
  providerSessionId: s.providerSessionId,
817
903
  model: s.model,
904
+ sandboxMode: s.sandboxMode,
905
+ approvalPolicy: s.approvalPolicy,
906
+ networkAccessEnabled: s.networkAccessEnabled,
818
907
  agentPersona: s.agentPersona,
819
908
  verbose: s.verbose || void 0,
820
909
  mode: s.mode !== "auto" ? s.mode : void 0,
@@ -826,8 +915,24 @@ async function saveSessions() {
826
915
  }
827
916
  await sessionStore.write(data);
828
917
  }
829
- async function createSession(name, directory, channelId, projectName, provider = "claude", providerSessionId) {
918
+ function saveSessions() {
919
+ saveQueue = saveQueue.catch(() => {
920
+ }).then(async () => {
921
+ try {
922
+ await persistSessionsNow();
923
+ } catch (err) {
924
+ console.error(`Failed to persist sessions: ${err.message}`);
925
+ }
926
+ });
927
+ return saveQueue;
928
+ }
929
+ async function createSession(name, directory, channelId, projectName, provider = "claude", providerSessionId, options = {}) {
830
930
  const resolvedDir = resolvePath(directory);
931
+ const effectiveOptions = provider === "codex" ? {
932
+ sandboxMode: options.sandboxMode ?? config.codexSandboxMode,
933
+ approvalPolicy: options.approvalPolicy ?? config.codexApprovalPolicy,
934
+ networkAccessEnabled: options.networkAccessEnabled ?? config.codexNetworkAccessEnabled
935
+ } : options;
831
936
  if (!isPathAllowed(resolvedDir, config.allowedPaths)) {
832
937
  throw new Error(`Directory not in allowed paths: ${resolvedDir}`);
833
938
  }
@@ -838,14 +943,27 @@ async function createSession(name, directory, channelId, projectName, provider =
838
943
  const usesTmux = providerInstance.supports("tmux");
839
944
  let id = sanitizeSessionName(name);
840
945
  let tmuxName = usesTmux ? `${SESSION_PREFIX}${id}` : "";
841
- let suffix = 1;
842
- while (sessions.has(id) || usesTmux && await tmuxSessionExists(tmuxName)) {
843
- suffix++;
844
- id = sanitizeSessionName(`${name}-${suffix}`);
845
- 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
+ }
846
957
  }
847
958
  if (usesTmux) {
848
- 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
+ }
849
967
  }
850
968
  const session = {
851
969
  id,
@@ -855,6 +973,9 @@ async function createSession(name, directory, channelId, projectName, provider =
855
973
  provider,
856
974
  tmuxName,
857
975
  providerSessionId,
976
+ sandboxMode: effectiveOptions.sandboxMode,
977
+ approvalPolicy: effectiveOptions.approvalPolicy,
978
+ networkAccessEnabled: effectiveOptions.networkAccessEnabled,
858
979
  verbose: false,
859
980
  mode: "auto",
860
981
  isGenerating: false,
@@ -864,8 +985,10 @@ async function createSession(name, directory, channelId, projectName, provider =
864
985
  totalCost: 0
865
986
  };
866
987
  sessions.set(id, session);
867
- channelToSession.set(channelId, id);
868
- await saveSessions();
988
+ if (!isPlaceholderChannelId(channelId)) {
989
+ channelToSession.set(channelId, id);
990
+ await saveSessions();
991
+ }
869
992
  return session;
870
993
  }
871
994
  function getSession(id) {
@@ -890,29 +1013,38 @@ async function endSession(id) {
890
1013
  } catch {
891
1014
  }
892
1015
  }
893
- channelToSession.delete(session.channelId);
1016
+ if (!isPlaceholderChannelId(session.channelId)) {
1017
+ channelToSession.delete(session.channelId);
1018
+ }
894
1019
  sessions.delete(id);
895
1020
  await saveSessions();
896
1021
  }
897
- function linkChannel(sessionId, channelId) {
1022
+ async function linkChannel(sessionId, channelId) {
898
1023
  const session = sessions.get(sessionId);
899
- if (session) {
1024
+ if (!session) {
1025
+ throw new Error(`Session "${sessionId}" not found`);
1026
+ }
1027
+ if (!isPlaceholderChannelId(session.channelId)) {
900
1028
  channelToSession.delete(session.channelId);
901
- session.channelId = channelId;
902
- channelToSession.set(channelId, sessionId);
903
- saveSessions();
904
1029
  }
1030
+ session.channelId = channelId;
1031
+ channelToSession.set(channelId, sessionId);
1032
+ await saveSessions();
905
1033
  }
906
- function unlinkChannel(channelId) {
907
- const sessionId = channelToSession.get(channelId);
908
- if (sessionId) {
909
- channelToSession.delete(channelId);
910
- const session = sessions.get(sessionId);
911
- if (session) {
912
- sessions.delete(sessionId);
1034
+ async function unlinkChannel(channelId) {
1035
+ let sessionId = channelToSession.get(channelId);
1036
+ if (!sessionId) {
1037
+ for (const [id, session] of sessions) {
1038
+ if (session.channelId === channelId) {
1039
+ sessionId = id;
1040
+ break;
1041
+ }
913
1042
  }
914
- saveSessions();
915
1043
  }
1044
+ if (!sessionId) return;
1045
+ channelToSession.delete(channelId);
1046
+ sessions.delete(sessionId);
1047
+ await saveSessions();
916
1048
  }
917
1049
  function setModel(sessionId, model) {
918
1050
  const session = sessions.get(sessionId);
@@ -969,6 +1101,9 @@ async function* sendPrompt(sessionId, prompt) {
969
1101
  directory: session.directory,
970
1102
  providerSessionId: session.providerSessionId,
971
1103
  model: session.model,
1104
+ sandboxMode: session.sandboxMode,
1105
+ approvalPolicy: session.approvalPolicy,
1106
+ networkAccessEnabled: session.networkAccessEnabled,
972
1107
  systemPromptParts,
973
1108
  abortController: controller
974
1109
  });
@@ -1010,6 +1145,9 @@ async function* continueSession(sessionId) {
1010
1145
  directory: session.directory,
1011
1146
  providerSessionId: session.providerSessionId,
1012
1147
  model: session.model,
1148
+ sandboxMode: session.sandboxMode,
1149
+ approvalPolicy: session.approvalPolicy,
1150
+ networkAccessEnabled: session.networkAccessEnabled,
1013
1151
  systemPromptParts,
1014
1152
  abortController: controller
1015
1153
  });
@@ -1742,6 +1880,17 @@ ${event.message}
1742
1880
  break;
1743
1881
  }
1744
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
+ }
1745
1894
  break;
1746
1895
  }
1747
1896
  }
@@ -1929,9 +2078,38 @@ var PROVIDER_COLORS = {
1929
2078
  claude: 3447003,
1930
2079
  codex: 1090431
1931
2080
  };
2081
+ function resolveCodexSessionOptions(interaction, provider) {
2082
+ if (provider !== "codex") return {};
2083
+ const sandboxMode = interaction.options.getString("sandbox-mode") ?? config.codexSandboxMode;
2084
+ const approvalPolicy = interaction.options.getString("approval-policy") ?? config.codexApprovalPolicy;
2085
+ const networkAccessEnabled = interaction.options.getBoolean("network-access") ?? config.codexNetworkAccessEnabled;
2086
+ return { sandboxMode, approvalPolicy, networkAccessEnabled };
2087
+ }
2088
+ function addCodexPolicyFields(fields, options) {
2089
+ if (options.sandboxMode) {
2090
+ fields.push({ name: "Sandbox", value: options.sandboxMode, inline: true });
2091
+ }
2092
+ if (options.approvalPolicy) {
2093
+ fields.push({ name: "Approval", value: options.approvalPolicy, inline: true });
2094
+ }
2095
+ if (options.networkAccessEnabled !== void 0) {
2096
+ fields.push({ name: "Network Access", value: options.networkAccessEnabled ? "enabled" : "disabled", inline: true });
2097
+ }
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
+ }
1932
2109
  async function handleSessionNew(interaction) {
1933
2110
  const name = interaction.options.getString("name", true);
1934
2111
  const provider = interaction.options.getString("provider") || "claude";
2112
+ const codexOptions = resolveCodexSessionOptions(interaction, provider);
1935
2113
  let directory = interaction.options.getString("directory");
1936
2114
  if (!directory) {
1937
2115
  const parentId = interaction.channel?.parentId;
@@ -1943,18 +2121,19 @@ async function handleSessionNew(interaction) {
1943
2121
  }
1944
2122
  await interaction.deferReply();
1945
2123
  let channel;
2124
+ let session;
1946
2125
  try {
1947
2126
  const guild = interaction.guild;
1948
2127
  const projectName = projectNameFromDir(directory);
1949
2128
  const { category } = await ensureProjectCategory(guild, projectName, directory);
1950
- const session = await createSession(name, directory, "pending", projectName, provider);
2129
+ session = await createSession(name, directory, "pending", projectName, provider, void 0, codexOptions);
1951
2130
  channel = await guild.channels.create({
1952
2131
  name: `${provider}-${session.id}`,
1953
2132
  type: ChannelType.GuildText,
1954
2133
  parent: category.id,
1955
2134
  topic: `${PROVIDER_LABELS[provider]} session | Dir: ${directory}`
1956
2135
  });
1957
- linkChannel(session.id, channel.id);
2136
+ await linkChannel(session.id, channel.id);
1958
2137
  const fields = [
1959
2138
  { name: "Channel", value: `#${provider}-${session.id}`, inline: true },
1960
2139
  { name: "Provider", value: PROVIDER_LABELS[provider], inline: true },
@@ -1964,6 +2143,7 @@ async function handleSessionNew(interaction) {
1964
2143
  if (session.tmuxName) {
1965
2144
  fields.push({ name: "Terminal", value: `\`tmux attach -t ${session.tmuxName}\``, inline: false });
1966
2145
  }
2146
+ addCodexPolicyFields(fields, codexOptions);
1967
2147
  const embed = new EmbedBuilder3().setColor(3066993).setTitle(`Session Created: ${session.id}`).addFields(fields);
1968
2148
  await interaction.editReply({ embeds: [embed] });
1969
2149
  log(`Session "${session.id}" (${provider}) created by ${interaction.user.tag} in ${directory}`);
@@ -1974,6 +2154,7 @@ async function handleSessionNew(interaction) {
1974
2154
  if (session.tmuxName) {
1975
2155
  welcomeFields.push({ name: "Terminal Access", value: `\`tmux attach -t ${session.tmuxName}\``, inline: false });
1976
2156
  }
2157
+ addCodexPolicyFields(welcomeFields, codexOptions);
1977
2158
  welcomeEmbed.addFields(welcomeFields);
1978
2159
  await channel.send({ embeds: [welcomeEmbed] });
1979
2160
  } catch (err) {
@@ -1983,6 +2164,12 @@ async function handleSessionNew(interaction) {
1983
2164
  } catch {
1984
2165
  }
1985
2166
  }
2167
+ if (session) {
2168
+ try {
2169
+ await endSession(session.id);
2170
+ } catch {
2171
+ }
2172
+ }
1986
2173
  await interaction.editReply(`Failed to create session: ${err.message}`);
1987
2174
  }
1988
2175
  }
@@ -2094,6 +2281,7 @@ async function handleSessionResume(interaction) {
2094
2281
  const providerSessionId = interaction.options.getString("session-id", true);
2095
2282
  const name = interaction.options.getString("name", true);
2096
2283
  const provider = interaction.options.getString("provider") || "claude";
2284
+ const codexOptions = resolveCodexSessionOptions(interaction, provider);
2097
2285
  const directory = interaction.options.getString("directory") || config.defaultDirectory;
2098
2286
  if (provider === "claude") {
2099
2287
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -2107,18 +2295,27 @@ async function handleSessionResume(interaction) {
2107
2295
  }
2108
2296
  await interaction.deferReply();
2109
2297
  let channel;
2298
+ let session;
2110
2299
  try {
2111
2300
  const guild = interaction.guild;
2112
2301
  const projectName = projectNameFromDir(directory);
2113
2302
  const { category } = await ensureProjectCategory(guild, projectName, directory);
2114
- const session = await createSession(name, directory, "pending", projectName, provider, providerSessionId);
2303
+ session = await createSession(
2304
+ name,
2305
+ directory,
2306
+ "pending",
2307
+ projectName,
2308
+ provider,
2309
+ providerSessionId,
2310
+ codexOptions
2311
+ );
2115
2312
  channel = await guild.channels.create({
2116
2313
  name: `${provider}-${session.id}`,
2117
2314
  type: ChannelType.GuildText,
2118
2315
  parent: category.id,
2119
- topic: `${PROVIDER_LABELS[provider]} session (resumed) | Dir: ${directory}`
2316
+ topic: `${PROVIDER_LABELS[provider]} session (resumed) | Dir: ${directory} | Provider Session: ${providerSessionId}`
2120
2317
  });
2121
- linkChannel(session.id, channel.id);
2318
+ await linkChannel(session.id, channel.id);
2122
2319
  const fields = [
2123
2320
  { name: "Channel", value: `#${provider}-${session.id}`, inline: true },
2124
2321
  { name: "Provider", value: PROVIDER_LABELS[provider], inline: true },
@@ -2129,6 +2326,7 @@ async function handleSessionResume(interaction) {
2129
2326
  if (session.tmuxName) {
2130
2327
  fields.push({ name: "Terminal", value: `\`tmux attach -t ${session.tmuxName}\``, inline: false });
2131
2328
  }
2329
+ addCodexPolicyFields(fields, codexOptions);
2132
2330
  const embed = new EmbedBuilder3().setColor(15105570).setTitle(`Session Resumed: ${session.id}`).addFields(fields);
2133
2331
  await interaction.editReply({ embeds: [embed] });
2134
2332
  log(`Session "${session.id}" (${provider}, resumed ${providerSessionId}) created by ${interaction.user.tag} in ${directory}`);
@@ -2139,6 +2337,7 @@ async function handleSessionResume(interaction) {
2139
2337
  if (session.tmuxName) {
2140
2338
  welcomeFields.push({ name: "Terminal Access", value: `\`tmux attach -t ${session.tmuxName}\``, inline: false });
2141
2339
  }
2340
+ addCodexPolicyFields(welcomeFields, codexOptions);
2142
2341
  await channel.send({
2143
2342
  embeds: [
2144
2343
  new EmbedBuilder3().setColor(15105570).setTitle(`${PROVIDER_LABELS[provider]} Session (Resumed)`).setDescription(
@@ -2153,6 +2352,12 @@ async function handleSessionResume(interaction) {
2153
2352
  } catch {
2154
2353
  }
2155
2354
  }
2355
+ if (session) {
2356
+ try {
2357
+ await endSession(session.id);
2358
+ } catch {
2359
+ }
2360
+ }
2156
2361
  await interaction.editReply(`Failed to resume session: ${err.message}`);
2157
2362
  }
2158
2363
  }
@@ -2174,7 +2379,13 @@ async function handleSessionList(interaction) {
2174
2379
  const status = s.isGenerating ? "\u{1F7E2} generating" : "\u26AA idle";
2175
2380
  const modeEmoji = { auto: "\u26A1", plan: "\u{1F4CB}", normal: "\u{1F6E1}\uFE0F" }[s.mode] || "\u26A1";
2176
2381
  const providerTag = `[${s.provider}]`;
2177
- return `**${s.id}** ${providerTag} \u2014 ${status} ${modeEmoji} ${s.mode} | ${formatUptime(s.createdAt)} uptime | ${s.messageCount} msgs | $${s.totalCost.toFixed(4)} | ${formatLastActivity(s.lastActivity)}`;
2382
+ const codexPolicy = s.provider === "codex" ? [
2383
+ s.sandboxMode ? `sandbox:${s.sandboxMode}` : "",
2384
+ s.approvalPolicy ? `approval:${s.approvalPolicy}` : "",
2385
+ s.networkAccessEnabled !== void 0 ? `network:${s.networkAccessEnabled ? "on" : "off"}` : ""
2386
+ ].filter(Boolean).join(" ") : "";
2387
+ const policySuffix = codexPolicy ? ` | ${codexPolicy}` : "";
2388
+ return `**${s.id}** ${providerTag} \u2014 ${status} ${modeEmoji} ${s.mode} | ${formatUptime(s.createdAt)} uptime | ${s.messageCount} msgs | $${s.totalCost.toFixed(4)} | ${formatLastActivity(s.lastActivity)}${policySuffix}`;
2178
2389
  });
2179
2390
  embed.addFields({ name: `\u{1F4C1} ${project}`, value: lines.join("\n") });
2180
2391
  }
@@ -2281,7 +2492,38 @@ async function handleSessionSync(interaction) {
2281
2492
  const tmuxSessions = await listTmuxSessions();
2282
2493
  const currentSessions = getAllSessions();
2283
2494
  const currentIds = new Set(currentSessions.map((s) => s.id));
2284
- 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
+ }
2285
2527
  for (const tmuxSession of tmuxSessions) {
2286
2528
  if (currentIds.has(tmuxSession.id)) continue;
2287
2529
  const projectName = projectNameFromDir(tmuxSession.directory);
@@ -2292,11 +2534,25 @@ async function handleSessionSync(interaction) {
2292
2534
  parent: category.id,
2293
2535
  topic: `Claude Code session (synced) | Dir: ${tmuxSession.directory}`
2294
2536
  });
2295
- await createSession(tmuxSession.id, tmuxSession.directory, channel.id, projectName, "claude");
2296
- synced++;
2297
- }
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`);
2298
2554
  await interaction.editReply(
2299
- 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."
2300
2556
  );
2301
2557
  }
2302
2558
  async function handleSessionId(interaction) {
@@ -3530,7 +3786,7 @@ async function startBot() {
3530
3786
  client.on("messageCreate", handleMessage);
3531
3787
  client.on("channelDelete", (channel) => {
3532
3788
  if (channel.type === ChannelType2.GuildText) {
3533
- unlinkChannel(channel.id);
3789
+ void unlinkChannel(channel.id);
3534
3790
  }
3535
3791
  });
3536
3792
  client.once("ready", async () => {
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@
4
4
  var command = process.argv[2];
5
5
  switch (command) {
6
6
  case "setup": {
7
- const { runSetup } = await import("./setup-FO4HRB3B.js");
7
+ const { runSetup } = await import("./setup-VHXX7YM6.js");
8
8
  await runSetup();
9
9
  break;
10
10
  }
@@ -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-HGP3MV5P.js");
21
+ const { startBot } = await import("./bot-QXUF3LNK.js");
22
22
  console.log("agentcord starting...");
23
23
  await startBot();
24
24
  break;
@@ -96,6 +96,11 @@ var CodexProvider = class {
96
96
  skipGitRepoCheck: true
97
97
  };
98
98
  if (options.model) threadOptions.model = options.model;
99
+ if (options.sandboxMode) threadOptions.sandboxMode = options.sandboxMode;
100
+ if (options.approvalPolicy) threadOptions.approvalPolicy = options.approvalPolicy;
101
+ if (options.networkAccessEnabled !== void 0) {
102
+ threadOptions.networkAccessEnabled = options.networkAccessEnabled;
103
+ }
99
104
  const thread = options.providerSessionId ? codex.resumeThread(options.providerSessionId, threadOptions) : codex.startThread(threadOptions);
100
105
  const { events } = await thread.runStreamed(input);
101
106
  yield* this.translateEvents(events, options.abortController);
@@ -118,6 +123,11 @@ var CodexProvider = class {
118
123
  skipGitRepoCheck: true
119
124
  };
120
125
  if (options.model) threadOptions.model = options.model;
126
+ if (options.sandboxMode) threadOptions.sandboxMode = options.sandboxMode;
127
+ if (options.approvalPolicy) threadOptions.approvalPolicy = options.approvalPolicy;
128
+ if (options.networkAccessEnabled !== void 0) {
129
+ threadOptions.networkAccessEnabled = options.networkAccessEnabled;
130
+ }
121
131
  const thread = codex.resumeThread(options.providerSessionId, threadOptions);
122
132
  const { events } = await thread.runStreamed("Continue from where you left off.");
123
133
  yield* this.translateEvents(events, options.abortController);
@@ -40,6 +40,7 @@ function writeEnvFile(env) {
40
40
  { comment: "# Discord App", keys: ["DISCORD_TOKEN", "DISCORD_CLIENT_ID", "DISCORD_GUILD_ID"] },
41
41
  { comment: "# Security", keys: ["ALLOWED_USERS", "ALLOW_ALL_USERS"] },
42
42
  { comment: "# Paths", keys: ["ALLOWED_PATHS", "DEFAULT_DIRECTORY"] },
43
+ { comment: "# Codex Defaults", keys: ["CODEX_SANDBOX_MODE", "CODEX_APPROVAL_POLICY", "CODEX_NETWORK_ACCESS_ENABLED"] },
43
44
  { comment: "# Optional", keys: ["MESSAGE_RETENTION_DAYS", "RATE_LIMIT_MS"] }
44
45
  ];
45
46
  for (const section of sections) {
@@ -237,6 +238,9 @@ async function runSetup() {
237
238
  if (allowedPaths) env.ALLOWED_PATHS = allowedPaths;
238
239
  if (existing.MESSAGE_RETENTION_DAYS) env.MESSAGE_RETENTION_DAYS = existing.MESSAGE_RETENTION_DAYS;
239
240
  if (existing.RATE_LIMIT_MS) env.RATE_LIMIT_MS = existing.RATE_LIMIT_MS;
241
+ if (existing.CODEX_SANDBOX_MODE) env.CODEX_SANDBOX_MODE = existing.CODEX_SANDBOX_MODE;
242
+ if (existing.CODEX_APPROVAL_POLICY) env.CODEX_APPROVAL_POLICY = existing.CODEX_APPROVAL_POLICY;
243
+ if (existing.CODEX_NETWORK_ACCESS_ENABLED) env.CODEX_NETWORK_ACCESS_ENABLED = existing.CODEX_NETWORK_ACCESS_ENABLED;
240
244
  const s = p.spinner();
241
245
  s.start("Writing .env file");
242
246
  writeEnvFile(env);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentcord",
3
- "version": "0.2.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": {