agentcord 0.1.9 → 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) {
@@ -738,6 +805,12 @@ function detectNumberedOptions(text) {
738
805
  const hasQuestion = /\?\s*$/.test(preamble.trim()) || /\b(which|choose|select|pick|prefer|would you like|how would you|what approach|option)\b/.test(preamble);
739
806
  return hasQuestion ? options : null;
740
807
  }
808
+ var ABORT_PATTERNS = ["abort", "cancel", "interrupt", "killed", "signal"];
809
+ function isAbortError(err) {
810
+ if (err instanceof Error && err.name === "AbortError") return true;
811
+ const msg = (err.message || "").toLowerCase();
812
+ return ABORT_PATTERNS.some((p) => msg.includes(p));
813
+ }
741
814
  function detectYesNoPrompt(text) {
742
815
  const lower = text.toLowerCase();
743
816
  return /\b(y\/n|yes\/no|confirm|proceed)\b/.test(lower) || /\?\s*$/.test(text.trim()) && /\b(should|would you|do you want|shall)\b/.test(lower);
@@ -753,6 +826,7 @@ var MODE_PROMPTS = {
753
826
  var sessionStore = new Store("sessions.json");
754
827
  var sessions = /* @__PURE__ */ new Map();
755
828
  var channelToSession = /* @__PURE__ */ new Map();
829
+ var saveQueue = Promise.resolve();
756
830
  function tmux(...args) {
757
831
  return new Promise((resolve2, reject) => {
758
832
  execFile("tmux", args, { encoding: "utf-8" }, (err, stdout) => {
@@ -769,10 +843,24 @@ async function tmuxSessionExists(tmuxName) {
769
843
  return false;
770
844
  }
771
845
  }
846
+ function isPlaceholderChannelId(channelId) {
847
+ return !channelId || channelId === "pending";
848
+ }
772
849
  async function loadSessions() {
773
850
  const data = await sessionStore.read();
774
851
  if (!data) return;
852
+ let cleaned = false;
775
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
+ }
776
864
  const provider = s.provider ?? "claude";
777
865
  const providerSessionId = s.providerSessionId ?? s.claudeSessionId;
778
866
  if (provider === "claude") {
@@ -795,11 +883,15 @@ async function loadSessions() {
795
883
  });
796
884
  channelToSession.set(s.channelId, s.id);
797
885
  }
886
+ if (cleaned) {
887
+ await saveSessions();
888
+ }
798
889
  console.log(`Restored ${sessions.size} session(s)`);
799
890
  }
800
- async function saveSessions() {
891
+ async function persistSessionsNow() {
801
892
  const data = [];
802
893
  for (const [, s] of sessions) {
894
+ if (isPlaceholderChannelId(s.channelId)) continue;
803
895
  data.push({
804
896
  id: s.id,
805
897
  channelId: s.channelId,
@@ -809,6 +901,9 @@ async function saveSessions() {
809
901
  tmuxName: s.tmuxName,
810
902
  providerSessionId: s.providerSessionId,
811
903
  model: s.model,
904
+ sandboxMode: s.sandboxMode,
905
+ approvalPolicy: s.approvalPolicy,
906
+ networkAccessEnabled: s.networkAccessEnabled,
812
907
  agentPersona: s.agentPersona,
813
908
  verbose: s.verbose || void 0,
814
909
  mode: s.mode !== "auto" ? s.mode : void 0,
@@ -820,8 +915,24 @@ async function saveSessions() {
820
915
  }
821
916
  await sessionStore.write(data);
822
917
  }
823
- 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 = {}) {
824
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;
825
936
  if (!isPathAllowed(resolvedDir, config.allowedPaths)) {
826
937
  throw new Error(`Directory not in allowed paths: ${resolvedDir}`);
827
938
  }
@@ -849,6 +960,9 @@ async function createSession(name, directory, channelId, projectName, provider =
849
960
  provider,
850
961
  tmuxName,
851
962
  providerSessionId,
963
+ sandboxMode: effectiveOptions.sandboxMode,
964
+ approvalPolicy: effectiveOptions.approvalPolicy,
965
+ networkAccessEnabled: effectiveOptions.networkAccessEnabled,
852
966
  verbose: false,
853
967
  mode: "auto",
854
968
  isGenerating: false,
@@ -858,8 +972,10 @@ async function createSession(name, directory, channelId, projectName, provider =
858
972
  totalCost: 0
859
973
  };
860
974
  sessions.set(id, session);
861
- channelToSession.set(channelId, id);
862
- await saveSessions();
975
+ if (!isPlaceholderChannelId(channelId)) {
976
+ channelToSession.set(channelId, id);
977
+ await saveSessions();
978
+ }
863
979
  return session;
864
980
  }
865
981
  function getSession(id) {
@@ -884,29 +1000,38 @@ async function endSession(id) {
884
1000
  } catch {
885
1001
  }
886
1002
  }
887
- channelToSession.delete(session.channelId);
1003
+ if (!isPlaceholderChannelId(session.channelId)) {
1004
+ channelToSession.delete(session.channelId);
1005
+ }
888
1006
  sessions.delete(id);
889
1007
  await saveSessions();
890
1008
  }
891
- function linkChannel(sessionId, channelId) {
1009
+ async function linkChannel(sessionId, channelId) {
892
1010
  const session = sessions.get(sessionId);
893
- if (session) {
1011
+ if (!session) {
1012
+ throw new Error(`Session "${sessionId}" not found`);
1013
+ }
1014
+ if (!isPlaceholderChannelId(session.channelId)) {
894
1015
  channelToSession.delete(session.channelId);
895
- session.channelId = channelId;
896
- channelToSession.set(channelId, sessionId);
897
- saveSessions();
898
1016
  }
1017
+ session.channelId = channelId;
1018
+ channelToSession.set(channelId, sessionId);
1019
+ await saveSessions();
899
1020
  }
900
- function unlinkChannel(channelId) {
901
- const sessionId = channelToSession.get(channelId);
902
- if (sessionId) {
903
- channelToSession.delete(channelId);
904
- const session = sessions.get(sessionId);
905
- if (session) {
906
- 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
+ }
907
1029
  }
908
- saveSessions();
909
1030
  }
1031
+ if (!sessionId) return;
1032
+ channelToSession.delete(channelId);
1033
+ sessions.delete(sessionId);
1034
+ await saveSessions();
910
1035
  }
911
1036
  function setModel(sessionId, model) {
912
1037
  const session = sessions.get(sessionId);
@@ -948,13 +1073,6 @@ function buildSystemPromptParts(session) {
948
1073
  if (modePrompt) parts.push(modePrompt);
949
1074
  return parts;
950
1075
  }
951
- function resetProviderSession(sessionId) {
952
- const session = sessions.get(sessionId);
953
- if (session) {
954
- session.providerSessionId = void 0;
955
- saveSessions();
956
- }
957
- }
958
1076
  async function* sendPrompt(sessionId, prompt) {
959
1077
  const session = sessions.get(sessionId);
960
1078
  if (!session) throw new Error(`Session "${sessionId}" not found`);
@@ -970,6 +1088,9 @@ async function* sendPrompt(sessionId, prompt) {
970
1088
  directory: session.directory,
971
1089
  providerSessionId: session.providerSessionId,
972
1090
  model: session.model,
1091
+ sandboxMode: session.sandboxMode,
1092
+ approvalPolicy: session.approvalPolicy,
1093
+ networkAccessEnabled: session.networkAccessEnabled,
973
1094
  systemPromptParts,
974
1095
  abortController: controller
975
1096
  });
@@ -985,7 +1106,7 @@ async function* sendPrompt(sessionId, prompt) {
985
1106
  }
986
1107
  session.messageCount++;
987
1108
  } catch (err) {
988
- if (err.name === "AbortError") {
1109
+ if (isAbortError(err)) {
989
1110
  } else {
990
1111
  throw err;
991
1112
  }
@@ -1011,6 +1132,9 @@ async function* continueSession(sessionId) {
1011
1132
  directory: session.directory,
1012
1133
  providerSessionId: session.providerSessionId,
1013
1134
  model: session.model,
1135
+ sandboxMode: session.sandboxMode,
1136
+ approvalPolicy: session.approvalPolicy,
1137
+ networkAccessEnabled: session.networkAccessEnabled,
1014
1138
  systemPromptParts,
1015
1139
  abortController: controller
1016
1140
  });
@@ -1026,7 +1150,7 @@ async function* continueSession(sessionId) {
1026
1150
  }
1027
1151
  session.messageCount++;
1028
1152
  } catch (err) {
1029
- if (err.name === "AbortError") {
1153
+ if (isAbortError(err)) {
1030
1154
  } else {
1031
1155
  throw err;
1032
1156
  }
@@ -1254,15 +1378,6 @@ function renderCodexTodoListEmbed(event) {
1254
1378
  }
1255
1379
 
1256
1380
  // src/output-handler.ts
1257
- var ABORT_PATTERNS = ["abort", "cancel", "interrupt", "killed", "signal"];
1258
- function isAbortLike(err) {
1259
- if (err.name === "AbortError") return true;
1260
- const msg = (err.message || "").toLowerCase();
1261
- return ABORT_PATTERNS.some((p) => msg.includes(p));
1262
- }
1263
- function isAbortError(errors) {
1264
- return errors.some((e) => ABORT_PATTERNS.some((p) => e.toLowerCase().includes(p)));
1265
- }
1266
1381
  var expandableStore = /* @__PURE__ */ new Map();
1267
1382
  var expandCounter = 0;
1268
1383
  var pendingAnswersStore = /* @__PURE__ */ new Map();
@@ -1730,10 +1845,6 @@ ${statusLine}`);
1730
1845
  ${event.errors.join("\n")}
1731
1846
  \`\`\``);
1732
1847
  }
1733
- if (!event.success && !isAbortError(event.errors)) {
1734
- resetProviderSession(sessionId);
1735
- streamer.append("\n-# Session reset \u2014 next message will start a fresh provider session.");
1736
- }
1737
1848
  await streamer.finalize();
1738
1849
  const components = [];
1739
1850
  const checkText = lastText || "";
@@ -1762,13 +1873,11 @@ ${event.message}
1762
1873
  }
1763
1874
  } catch (err) {
1764
1875
  await streamer.finalize();
1765
- const errMsg = err.message || "";
1766
- if (!isAbortLike(err)) {
1767
- resetProviderSession(sessionId);
1876
+ if (!isAbortError(err)) {
1877
+ const errMsg = err.message || "";
1768
1878
  const embed = new EmbedBuilder2().setColor(15158332).setTitle("Error").setDescription(`\`\`\`
1769
1879
  ${errMsg}
1770
- \`\`\`
1771
- -# Session reset \u2014 next message will start a fresh provider session.`);
1880
+ \`\`\``);
1772
1881
  await channel.send({ embeds: [embed] });
1773
1882
  }
1774
1883
  } finally {
@@ -1945,9 +2054,28 @@ var PROVIDER_COLORS = {
1945
2054
  claude: 3447003,
1946
2055
  codex: 1090431
1947
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
+ }
1948
2075
  async function handleSessionNew(interaction) {
1949
2076
  const name = interaction.options.getString("name", true);
1950
2077
  const provider = interaction.options.getString("provider") || "claude";
2078
+ const codexOptions = resolveCodexSessionOptions(interaction, provider);
1951
2079
  let directory = interaction.options.getString("directory");
1952
2080
  if (!directory) {
1953
2081
  const parentId = interaction.channel?.parentId;
@@ -1959,18 +2087,19 @@ async function handleSessionNew(interaction) {
1959
2087
  }
1960
2088
  await interaction.deferReply();
1961
2089
  let channel;
2090
+ let session;
1962
2091
  try {
1963
2092
  const guild = interaction.guild;
1964
2093
  const projectName = projectNameFromDir(directory);
1965
2094
  const { category } = await ensureProjectCategory(guild, projectName, directory);
1966
- const session = await createSession(name, directory, "pending", projectName, provider);
2095
+ session = await createSession(name, directory, "pending", projectName, provider, void 0, codexOptions);
1967
2096
  channel = await guild.channels.create({
1968
2097
  name: `${provider}-${session.id}`,
1969
2098
  type: ChannelType.GuildText,
1970
2099
  parent: category.id,
1971
2100
  topic: `${PROVIDER_LABELS[provider]} session | Dir: ${directory}`
1972
2101
  });
1973
- linkChannel(session.id, channel.id);
2102
+ await linkChannel(session.id, channel.id);
1974
2103
  const fields = [
1975
2104
  { name: "Channel", value: `#${provider}-${session.id}`, inline: true },
1976
2105
  { name: "Provider", value: PROVIDER_LABELS[provider], inline: true },
@@ -1980,6 +2109,7 @@ async function handleSessionNew(interaction) {
1980
2109
  if (session.tmuxName) {
1981
2110
  fields.push({ name: "Terminal", value: `\`tmux attach -t ${session.tmuxName}\``, inline: false });
1982
2111
  }
2112
+ addCodexPolicyFields(fields, codexOptions);
1983
2113
  const embed = new EmbedBuilder3().setColor(3066993).setTitle(`Session Created: ${session.id}`).addFields(fields);
1984
2114
  await interaction.editReply({ embeds: [embed] });
1985
2115
  log(`Session "${session.id}" (${provider}) created by ${interaction.user.tag} in ${directory}`);
@@ -1990,6 +2120,7 @@ async function handleSessionNew(interaction) {
1990
2120
  if (session.tmuxName) {
1991
2121
  welcomeFields.push({ name: "Terminal Access", value: `\`tmux attach -t ${session.tmuxName}\``, inline: false });
1992
2122
  }
2123
+ addCodexPolicyFields(welcomeFields, codexOptions);
1993
2124
  welcomeEmbed.addFields(welcomeFields);
1994
2125
  await channel.send({ embeds: [welcomeEmbed] });
1995
2126
  } catch (err) {
@@ -1999,6 +2130,12 @@ async function handleSessionNew(interaction) {
1999
2130
  } catch {
2000
2131
  }
2001
2132
  }
2133
+ if (session) {
2134
+ try {
2135
+ await endSession(session.id);
2136
+ } catch {
2137
+ }
2138
+ }
2002
2139
  await interaction.editReply(`Failed to create session: ${err.message}`);
2003
2140
  }
2004
2141
  }
@@ -2110,6 +2247,7 @@ async function handleSessionResume(interaction) {
2110
2247
  const providerSessionId = interaction.options.getString("session-id", true);
2111
2248
  const name = interaction.options.getString("name", true);
2112
2249
  const provider = interaction.options.getString("provider") || "claude";
2250
+ const codexOptions = resolveCodexSessionOptions(interaction, provider);
2113
2251
  const directory = interaction.options.getString("directory") || config.defaultDirectory;
2114
2252
  if (provider === "claude") {
2115
2253
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -2123,18 +2261,27 @@ async function handleSessionResume(interaction) {
2123
2261
  }
2124
2262
  await interaction.deferReply();
2125
2263
  let channel;
2264
+ let session;
2126
2265
  try {
2127
2266
  const guild = interaction.guild;
2128
2267
  const projectName = projectNameFromDir(directory);
2129
2268
  const { category } = await ensureProjectCategory(guild, projectName, directory);
2130
- 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
+ );
2131
2278
  channel = await guild.channels.create({
2132
2279
  name: `${provider}-${session.id}`,
2133
2280
  type: ChannelType.GuildText,
2134
2281
  parent: category.id,
2135
2282
  topic: `${PROVIDER_LABELS[provider]} session (resumed) | Dir: ${directory}`
2136
2283
  });
2137
- linkChannel(session.id, channel.id);
2284
+ await linkChannel(session.id, channel.id);
2138
2285
  const fields = [
2139
2286
  { name: "Channel", value: `#${provider}-${session.id}`, inline: true },
2140
2287
  { name: "Provider", value: PROVIDER_LABELS[provider], inline: true },
@@ -2145,6 +2292,7 @@ async function handleSessionResume(interaction) {
2145
2292
  if (session.tmuxName) {
2146
2293
  fields.push({ name: "Terminal", value: `\`tmux attach -t ${session.tmuxName}\``, inline: false });
2147
2294
  }
2295
+ addCodexPolicyFields(fields, codexOptions);
2148
2296
  const embed = new EmbedBuilder3().setColor(15105570).setTitle(`Session Resumed: ${session.id}`).addFields(fields);
2149
2297
  await interaction.editReply({ embeds: [embed] });
2150
2298
  log(`Session "${session.id}" (${provider}, resumed ${providerSessionId}) created by ${interaction.user.tag} in ${directory}`);
@@ -2155,6 +2303,7 @@ async function handleSessionResume(interaction) {
2155
2303
  if (session.tmuxName) {
2156
2304
  welcomeFields.push({ name: "Terminal Access", value: `\`tmux attach -t ${session.tmuxName}\``, inline: false });
2157
2305
  }
2306
+ addCodexPolicyFields(welcomeFields, codexOptions);
2158
2307
  await channel.send({
2159
2308
  embeds: [
2160
2309
  new EmbedBuilder3().setColor(15105570).setTitle(`${PROVIDER_LABELS[provider]} Session (Resumed)`).setDescription(
@@ -2169,6 +2318,12 @@ async function handleSessionResume(interaction) {
2169
2318
  } catch {
2170
2319
  }
2171
2320
  }
2321
+ if (session) {
2322
+ try {
2323
+ await endSession(session.id);
2324
+ } catch {
2325
+ }
2326
+ }
2172
2327
  await interaction.editReply(`Failed to resume session: ${err.message}`);
2173
2328
  }
2174
2329
  }
@@ -2190,7 +2345,13 @@ async function handleSessionList(interaction) {
2190
2345
  const status = s.isGenerating ? "\u{1F7E2} generating" : "\u26AA idle";
2191
2346
  const modeEmoji = { auto: "\u26A1", plan: "\u{1F4CB}", normal: "\u{1F6E1}\uFE0F" }[s.mode] || "\u26A1";
2192
2347
  const providerTag = `[${s.provider}]`;
2193
- 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}`;
2194
2355
  });
2195
2356
  embed.addFields({ name: `\u{1F4C1} ${project}`, value: lines.join("\n") });
2196
2357
  }
@@ -3055,17 +3216,7 @@ async function handleMessage(message) {
3055
3216
  userLastMessage.set(message.author.id, now);
3056
3217
  if (session.isGenerating) {
3057
3218
  abortSession(session.id);
3058
- const deadline = Date.now() + 5e3;
3059
- while (session.isGenerating && Date.now() < deadline) {
3060
- await new Promise((r) => setTimeout(r, 100));
3061
- }
3062
- if (session.isGenerating) {
3063
- await message.reply({
3064
- content: "Could not interrupt the current generation. Try `/session stop`.",
3065
- allowedMentions: { repliedUser: false }
3066
- });
3067
- return;
3068
- }
3219
+ await new Promise((r) => setTimeout(r, 200));
3069
3220
  }
3070
3221
  const text = message.content.trim();
3071
3222
  const imageAttachments = message.attachments.filter(
@@ -3126,15 +3277,11 @@ ${result.value.content}
3126
3277
  const stream = sendPrompt(session.id, prompt);
3127
3278
  await handleOutputStream(stream, channel, session.id, session.verbose, session.mode, session.provider);
3128
3279
  } catch (err) {
3129
- const errMsg = err.message || "";
3130
- const isAbort = err.name === "AbortError" || /abort|cancel|interrupt/i.test(errMsg);
3131
- if (isAbort) {
3280
+ if (isAbortError(err)) {
3132
3281
  return;
3133
3282
  }
3134
- resetProviderSession(session.id);
3135
3283
  await message.reply({
3136
- content: `Error: ${errMsg}
3137
- -# Session reset \u2014 next message will start a fresh provider session.`,
3284
+ content: `Error: ${err.message || "Unknown error"}`,
3138
3285
  allowedMentions: { repliedUser: false }
3139
3286
  });
3140
3287
  }
@@ -3560,7 +3707,7 @@ async function startBot() {
3560
3707
  client.on("messageCreate", handleMessage);
3561
3708
  client.on("channelDelete", (channel) => {
3562
3709
  if (channel.type === ChannelType2.GuildText) {
3563
- unlinkChannel(channel.id);
3710
+ void unlinkChannel(channel.id);
3564
3711
  }
3565
3712
  });
3566
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-MOK6APSV.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.1.9",
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": {