agentcord 0.2.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -118,8 +118,15 @@ ALLOWED_USERS=123456789,987654321 # Comma-separated user IDs
118
118
  ALLOW_ALL_USERS=false # Or true to skip whitelist
119
119
  ALLOWED_PATHS=/Users/me/Dev # Restrict accessible directories
120
120
  DEFAULT_DIRECTORY=/Users/me/Dev # Default for new sessions
121
+ CODEX_SANDBOX_MODE=workspace-write # read-only | workspace-write | danger-full-access
122
+ CODEX_APPROVAL_POLICY=on-request # never | on-request | on-failure | untrusted
123
+ CODEX_NETWORK_ACCESS_ENABLED=true # true | false
121
124
  ```
122
125
 
126
+ You can also override Codex policy per session when creating/resuming via:
127
+ - `/session new ... sandbox-mode:<mode> approval-policy:<policy> network-access:<bool>`
128
+ - `/session resume ... sandbox-mode:<mode> approval-policy:<policy> network-access:<bool>`
129
+
123
130
  ## Development
124
131
 
125
132
  ```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 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(
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
  }
@@ -855,6 +960,9 @@ async function createSession(name, directory, channelId, projectName, provider =
855
960
  provider,
856
961
  tmuxName,
857
962
  providerSessionId,
963
+ sandboxMode: effectiveOptions.sandboxMode,
964
+ approvalPolicy: effectiveOptions.approvalPolicy,
965
+ networkAccessEnabled: effectiveOptions.networkAccessEnabled,
858
966
  verbose: false,
859
967
  mode: "auto",
860
968
  isGenerating: false,
@@ -864,8 +972,10 @@ async function createSession(name, directory, channelId, projectName, provider =
864
972
  totalCost: 0
865
973
  };
866
974
  sessions.set(id, session);
867
- channelToSession.set(channelId, id);
868
- await saveSessions();
975
+ if (!isPlaceholderChannelId(channelId)) {
976
+ channelToSession.set(channelId, id);
977
+ await saveSessions();
978
+ }
869
979
  return session;
870
980
  }
871
981
  function getSession(id) {
@@ -890,29 +1000,38 @@ async function endSession(id) {
890
1000
  } catch {
891
1001
  }
892
1002
  }
893
- channelToSession.delete(session.channelId);
1003
+ if (!isPlaceholderChannelId(session.channelId)) {
1004
+ channelToSession.delete(session.channelId);
1005
+ }
894
1006
  sessions.delete(id);
895
1007
  await saveSessions();
896
1008
  }
897
- function linkChannel(sessionId, channelId) {
1009
+ async function linkChannel(sessionId, channelId) {
898
1010
  const session = sessions.get(sessionId);
899
- if (session) {
1011
+ if (!session) {
1012
+ throw new Error(`Session "${sessionId}" not found`);
1013
+ }
1014
+ if (!isPlaceholderChannelId(session.channelId)) {
900
1015
  channelToSession.delete(session.channelId);
901
- session.channelId = channelId;
902
- channelToSession.set(channelId, sessionId);
903
- saveSessions();
904
1016
  }
1017
+ session.channelId = channelId;
1018
+ channelToSession.set(channelId, sessionId);
1019
+ await saveSessions();
905
1020
  }
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);
1021
+ async function unlinkChannel(channelId) {
1022
+ let sessionId = channelToSession.get(channelId);
1023
+ if (!sessionId) {
1024
+ for (const [id, session] of sessions) {
1025
+ if (session.channelId === channelId) {
1026
+ sessionId = id;
1027
+ break;
1028
+ }
913
1029
  }
914
- saveSessions();
915
1030
  }
1031
+ if (!sessionId) return;
1032
+ channelToSession.delete(channelId);
1033
+ sessions.delete(sessionId);
1034
+ await saveSessions();
916
1035
  }
917
1036
  function setModel(sessionId, model) {
918
1037
  const session = sessions.get(sessionId);
@@ -969,6 +1088,9 @@ async function* sendPrompt(sessionId, prompt) {
969
1088
  directory: session.directory,
970
1089
  providerSessionId: session.providerSessionId,
971
1090
  model: session.model,
1091
+ sandboxMode: session.sandboxMode,
1092
+ approvalPolicy: session.approvalPolicy,
1093
+ networkAccessEnabled: session.networkAccessEnabled,
972
1094
  systemPromptParts,
973
1095
  abortController: controller
974
1096
  });
@@ -1010,6 +1132,9 @@ async function* continueSession(sessionId) {
1010
1132
  directory: session.directory,
1011
1133
  providerSessionId: session.providerSessionId,
1012
1134
  model: session.model,
1135
+ sandboxMode: session.sandboxMode,
1136
+ approvalPolicy: session.approvalPolicy,
1137
+ networkAccessEnabled: session.networkAccessEnabled,
1013
1138
  systemPromptParts,
1014
1139
  abortController: controller
1015
1140
  });
@@ -1929,9 +2054,28 @@ var PROVIDER_COLORS = {
1929
2054
  claude: 3447003,
1930
2055
  codex: 1090431
1931
2056
  };
2057
+ function resolveCodexSessionOptions(interaction, provider) {
2058
+ if (provider !== "codex") return {};
2059
+ const sandboxMode = interaction.options.getString("sandbox-mode") ?? config.codexSandboxMode;
2060
+ const approvalPolicy = interaction.options.getString("approval-policy") ?? config.codexApprovalPolicy;
2061
+ const networkAccessEnabled = interaction.options.getBoolean("network-access") ?? config.codexNetworkAccessEnabled;
2062
+ return { sandboxMode, approvalPolicy, networkAccessEnabled };
2063
+ }
2064
+ function addCodexPolicyFields(fields, options) {
2065
+ if (options.sandboxMode) {
2066
+ fields.push({ name: "Sandbox", value: options.sandboxMode, inline: true });
2067
+ }
2068
+ if (options.approvalPolicy) {
2069
+ fields.push({ name: "Approval", value: options.approvalPolicy, inline: true });
2070
+ }
2071
+ if (options.networkAccessEnabled !== void 0) {
2072
+ fields.push({ name: "Network Access", value: options.networkAccessEnabled ? "enabled" : "disabled", inline: true });
2073
+ }
2074
+ }
1932
2075
  async function handleSessionNew(interaction) {
1933
2076
  const name = interaction.options.getString("name", true);
1934
2077
  const provider = interaction.options.getString("provider") || "claude";
2078
+ const codexOptions = resolveCodexSessionOptions(interaction, provider);
1935
2079
  let directory = interaction.options.getString("directory");
1936
2080
  if (!directory) {
1937
2081
  const parentId = interaction.channel?.parentId;
@@ -1943,18 +2087,19 @@ async function handleSessionNew(interaction) {
1943
2087
  }
1944
2088
  await interaction.deferReply();
1945
2089
  let channel;
2090
+ let session;
1946
2091
  try {
1947
2092
  const guild = interaction.guild;
1948
2093
  const projectName = projectNameFromDir(directory);
1949
2094
  const { category } = await ensureProjectCategory(guild, projectName, directory);
1950
- const session = await createSession(name, directory, "pending", projectName, provider);
2095
+ session = await createSession(name, directory, "pending", projectName, provider, void 0, codexOptions);
1951
2096
  channel = await guild.channels.create({
1952
2097
  name: `${provider}-${session.id}`,
1953
2098
  type: ChannelType.GuildText,
1954
2099
  parent: category.id,
1955
2100
  topic: `${PROVIDER_LABELS[provider]} session | Dir: ${directory}`
1956
2101
  });
1957
- linkChannel(session.id, channel.id);
2102
+ await linkChannel(session.id, channel.id);
1958
2103
  const fields = [
1959
2104
  { name: "Channel", value: `#${provider}-${session.id}`, inline: true },
1960
2105
  { name: "Provider", value: PROVIDER_LABELS[provider], inline: true },
@@ -1964,6 +2109,7 @@ async function handleSessionNew(interaction) {
1964
2109
  if (session.tmuxName) {
1965
2110
  fields.push({ name: "Terminal", value: `\`tmux attach -t ${session.tmuxName}\``, inline: false });
1966
2111
  }
2112
+ addCodexPolicyFields(fields, codexOptions);
1967
2113
  const embed = new EmbedBuilder3().setColor(3066993).setTitle(`Session Created: ${session.id}`).addFields(fields);
1968
2114
  await interaction.editReply({ embeds: [embed] });
1969
2115
  log(`Session "${session.id}" (${provider}) created by ${interaction.user.tag} in ${directory}`);
@@ -1974,6 +2120,7 @@ async function handleSessionNew(interaction) {
1974
2120
  if (session.tmuxName) {
1975
2121
  welcomeFields.push({ name: "Terminal Access", value: `\`tmux attach -t ${session.tmuxName}\``, inline: false });
1976
2122
  }
2123
+ addCodexPolicyFields(welcomeFields, codexOptions);
1977
2124
  welcomeEmbed.addFields(welcomeFields);
1978
2125
  await channel.send({ embeds: [welcomeEmbed] });
1979
2126
  } catch (err) {
@@ -1983,6 +2130,12 @@ async function handleSessionNew(interaction) {
1983
2130
  } catch {
1984
2131
  }
1985
2132
  }
2133
+ if (session) {
2134
+ try {
2135
+ await endSession(session.id);
2136
+ } catch {
2137
+ }
2138
+ }
1986
2139
  await interaction.editReply(`Failed to create session: ${err.message}`);
1987
2140
  }
1988
2141
  }
@@ -2094,6 +2247,7 @@ async function handleSessionResume(interaction) {
2094
2247
  const providerSessionId = interaction.options.getString("session-id", true);
2095
2248
  const name = interaction.options.getString("name", true);
2096
2249
  const provider = interaction.options.getString("provider") || "claude";
2250
+ const codexOptions = resolveCodexSessionOptions(interaction, provider);
2097
2251
  const directory = interaction.options.getString("directory") || config.defaultDirectory;
2098
2252
  if (provider === "claude") {
2099
2253
  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 +2261,27 @@ async function handleSessionResume(interaction) {
2107
2261
  }
2108
2262
  await interaction.deferReply();
2109
2263
  let channel;
2264
+ let session;
2110
2265
  try {
2111
2266
  const guild = interaction.guild;
2112
2267
  const projectName = projectNameFromDir(directory);
2113
2268
  const { category } = await ensureProjectCategory(guild, projectName, directory);
2114
- const session = await createSession(name, directory, "pending", projectName, provider, providerSessionId);
2269
+ session = await createSession(
2270
+ name,
2271
+ directory,
2272
+ "pending",
2273
+ projectName,
2274
+ provider,
2275
+ providerSessionId,
2276
+ codexOptions
2277
+ );
2115
2278
  channel = await guild.channels.create({
2116
2279
  name: `${provider}-${session.id}`,
2117
2280
  type: ChannelType.GuildText,
2118
2281
  parent: category.id,
2119
2282
  topic: `${PROVIDER_LABELS[provider]} session (resumed) | Dir: ${directory}`
2120
2283
  });
2121
- linkChannel(session.id, channel.id);
2284
+ await linkChannel(session.id, channel.id);
2122
2285
  const fields = [
2123
2286
  { name: "Channel", value: `#${provider}-${session.id}`, inline: true },
2124
2287
  { name: "Provider", value: PROVIDER_LABELS[provider], inline: true },
@@ -2129,6 +2292,7 @@ async function handleSessionResume(interaction) {
2129
2292
  if (session.tmuxName) {
2130
2293
  fields.push({ name: "Terminal", value: `\`tmux attach -t ${session.tmuxName}\``, inline: false });
2131
2294
  }
2295
+ addCodexPolicyFields(fields, codexOptions);
2132
2296
  const embed = new EmbedBuilder3().setColor(15105570).setTitle(`Session Resumed: ${session.id}`).addFields(fields);
2133
2297
  await interaction.editReply({ embeds: [embed] });
2134
2298
  log(`Session "${session.id}" (${provider}, resumed ${providerSessionId}) created by ${interaction.user.tag} in ${directory}`);
@@ -2139,6 +2303,7 @@ async function handleSessionResume(interaction) {
2139
2303
  if (session.tmuxName) {
2140
2304
  welcomeFields.push({ name: "Terminal Access", value: `\`tmux attach -t ${session.tmuxName}\``, inline: false });
2141
2305
  }
2306
+ addCodexPolicyFields(welcomeFields, codexOptions);
2142
2307
  await channel.send({
2143
2308
  embeds: [
2144
2309
  new EmbedBuilder3().setColor(15105570).setTitle(`${PROVIDER_LABELS[provider]} Session (Resumed)`).setDescription(
@@ -2153,6 +2318,12 @@ async function handleSessionResume(interaction) {
2153
2318
  } catch {
2154
2319
  }
2155
2320
  }
2321
+ if (session) {
2322
+ try {
2323
+ await endSession(session.id);
2324
+ } catch {
2325
+ }
2326
+ }
2156
2327
  await interaction.editReply(`Failed to resume session: ${err.message}`);
2157
2328
  }
2158
2329
  }
@@ -2174,7 +2345,13 @@ async function handleSessionList(interaction) {
2174
2345
  const status = s.isGenerating ? "\u{1F7E2} generating" : "\u26AA idle";
2175
2346
  const modeEmoji = { auto: "\u26A1", plan: "\u{1F4CB}", normal: "\u{1F6E1}\uFE0F" }[s.mode] || "\u26A1";
2176
2347
  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)}`;
2348
+ const codexPolicy = s.provider === "codex" ? [
2349
+ s.sandboxMode ? `sandbox:${s.sandboxMode}` : "",
2350
+ s.approvalPolicy ? `approval:${s.approvalPolicy}` : "",
2351
+ s.networkAccessEnabled !== void 0 ? `network:${s.networkAccessEnabled ? "on" : "off"}` : ""
2352
+ ].filter(Boolean).join(" ") : "";
2353
+ const policySuffix = codexPolicy ? ` | ${codexPolicy}` : "";
2354
+ 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
2355
  });
2179
2356
  embed.addFields({ name: `\u{1F4C1} ${project}`, value: lines.join("\n") });
2180
2357
  }
@@ -3530,7 +3707,7 @@ async function startBot() {
3530
3707
  client.on("messageCreate", handleMessage);
3531
3708
  client.on("channelDelete", (channel) => {
3532
3709
  if (channel.type === ChannelType2.GuildText) {
3533
- unlinkChannel(channel.id);
3710
+ void unlinkChannel(channel.id);
3534
3711
  }
3535
3712
  });
3536
3713
  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-C6RYOLAL.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.0",
4
4
  "type": "module",
5
5
  "description": "Discord bot for managing AI coding agent sessions (Claude Code, Codex, and more)",
6
6
  "bin": {