skimpyclaw 0.3.14 → 0.4.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.
Files changed (222) hide show
  1. package/README.md +47 -37
  2. package/dist/__tests__/adapter-types.test.d.ts +4 -0
  3. package/dist/__tests__/adapter-types.test.js +63 -0
  4. package/dist/__tests__/anthropic-adapter.test.d.ts +4 -0
  5. package/dist/__tests__/anthropic-adapter.test.js +264 -0
  6. package/dist/__tests__/api.test.js +0 -1
  7. package/dist/__tests__/cli.integration.test.js +2 -4
  8. package/dist/__tests__/cli.test.js +0 -1
  9. package/dist/__tests__/code-agents-notifications.test.js +137 -0
  10. package/dist/__tests__/code-agents-parser.test.js +19 -1
  11. package/dist/__tests__/code-agents-preflight.test.js +3 -28
  12. package/dist/__tests__/code-agents-utils.test.js +34 -9
  13. package/dist/__tests__/code-agents-worktrees.test.js +116 -0
  14. package/dist/__tests__/codex-adapter.test.js +184 -0
  15. package/dist/__tests__/codex-auth.test.js +66 -0
  16. package/dist/__tests__/codex-provider-gating.test.js +35 -0
  17. package/dist/__tests__/codex-unified-loop.test.js +111 -0
  18. package/dist/__tests__/config-security.test.js +127 -0
  19. package/dist/__tests__/config.test.js +23 -0
  20. package/dist/__tests__/context-manager.test.js +243 -164
  21. package/dist/__tests__/cron-run.test.js +250 -0
  22. package/dist/__tests__/cron.test.js +12 -38
  23. package/dist/__tests__/digests.test.js +67 -0
  24. package/dist/__tests__/discord-attachments.test.js +211 -0
  25. package/dist/__tests__/discord-docs.test.d.ts +1 -0
  26. package/dist/__tests__/discord-docs.test.js +27 -0
  27. package/dist/__tests__/discord-thread-agents.test.d.ts +1 -0
  28. package/dist/__tests__/discord-thread-agents.test.js +115 -0
  29. package/dist/__tests__/discord-thread-context.test.d.ts +1 -0
  30. package/dist/__tests__/discord-thread-context.test.js +42 -0
  31. package/dist/__tests__/doctor.formatters.test.js +4 -4
  32. package/dist/__tests__/doctor.index.test.js +1 -1
  33. package/dist/__tests__/doctor.runner.test.js +3 -15
  34. package/dist/__tests__/env-sanitizer.test.d.ts +1 -0
  35. package/dist/__tests__/env-sanitizer.test.js +45 -0
  36. package/dist/__tests__/exec-approval.test.js +61 -0
  37. package/dist/__tests__/fetch-tool.test.d.ts +1 -0
  38. package/dist/__tests__/fetch-tool.test.js +85 -0
  39. package/dist/__tests__/gateway-status-auth.test.d.ts +1 -0
  40. package/dist/__tests__/gateway-status-auth.test.js +72 -0
  41. package/dist/__tests__/heartbeat.test.js +3 -3
  42. package/dist/__tests__/interactive-sessions.test.d.ts +1 -0
  43. package/dist/__tests__/interactive-sessions.test.js +96 -0
  44. package/dist/__tests__/langfuse.test.js +6 -18
  45. package/dist/__tests__/model-selection.test.js +3 -4
  46. package/dist/__tests__/providers-init.test.js +2 -8
  47. package/dist/__tests__/providers-routing.test.js +1 -1
  48. package/dist/__tests__/providers-utils.test.js +13 -3
  49. package/dist/__tests__/sessions.test.js +14 -10
  50. package/dist/__tests__/setup.test.js +12 -29
  51. package/dist/__tests__/skills.test.js +10 -7
  52. package/dist/__tests__/stream-formatter.test.d.ts +1 -0
  53. package/dist/__tests__/stream-formatter.test.js +114 -0
  54. package/dist/__tests__/token-efficiency.test.js +131 -15
  55. package/dist/__tests__/tool-loop.test.d.ts +4 -0
  56. package/dist/__tests__/tool-loop.test.js +505 -0
  57. package/dist/__tests__/tools.test.js +101 -276
  58. package/dist/__tests__/utils.test.d.ts +1 -0
  59. package/dist/__tests__/utils.test.js +14 -0
  60. package/dist/__tests__/voice.test.js +21 -0
  61. package/dist/agent.js +35 -4
  62. package/dist/api.js +113 -37
  63. package/dist/channels/discord/attachments.d.ts +50 -0
  64. package/dist/channels/discord/attachments.js +137 -0
  65. package/dist/channels/discord/delegation.d.ts +5 -0
  66. package/dist/channels/discord/delegation.js +136 -0
  67. package/dist/channels/discord/handlers.js +694 -7
  68. package/dist/channels/discord/index.d.ts +16 -1
  69. package/dist/channels/discord/index.js +64 -1
  70. package/dist/channels/discord/thread-agents.d.ts +54 -0
  71. package/dist/channels/discord/thread-agents.js +323 -0
  72. package/dist/channels/discord/threads.d.ts +58 -0
  73. package/dist/channels/discord/threads.js +192 -0
  74. package/dist/channels/discord/types.js +4 -2
  75. package/dist/channels/discord/utils.d.ts +16 -0
  76. package/dist/channels/discord/utils.js +86 -6
  77. package/dist/channels/telegram/index.d.ts +1 -1
  78. package/dist/channels/telegram/types.js +1 -1
  79. package/dist/channels/telegram/utils.js +9 -3
  80. package/dist/channels.d.ts +1 -1
  81. package/dist/cli.js +20 -400
  82. package/dist/code-agents/executor.d.ts +1 -1
  83. package/dist/code-agents/executor.js +101 -45
  84. package/dist/code-agents/index.d.ts +2 -7
  85. package/dist/code-agents/index.js +111 -80
  86. package/dist/code-agents/interactive-resume.d.ts +6 -0
  87. package/dist/code-agents/interactive-resume.js +98 -0
  88. package/dist/code-agents/interactive-sessions.d.ts +20 -0
  89. package/dist/code-agents/interactive-sessions.js +132 -0
  90. package/dist/code-agents/parser.js +5 -1
  91. package/dist/code-agents/registry.d.ts +7 -1
  92. package/dist/code-agents/registry.js +11 -23
  93. package/dist/code-agents/stream-formatter.d.ts +8 -0
  94. package/dist/code-agents/stream-formatter.js +92 -0
  95. package/dist/code-agents/types.d.ts +16 -24
  96. package/dist/code-agents/utils.d.ts +35 -11
  97. package/dist/code-agents/utils.js +349 -95
  98. package/dist/code-agents/worktrees.d.ts +37 -0
  99. package/dist/code-agents/worktrees.js +116 -0
  100. package/dist/config.d.ts +2 -4
  101. package/dist/config.js +123 -23
  102. package/dist/cron.d.ts +1 -6
  103. package/dist/cron.js +175 -82
  104. package/dist/dashboard/assets/index-B345aOO-.js +65 -0
  105. package/dist/dashboard/assets/index-ZWK4dalJ.css +1 -0
  106. package/dist/dashboard/index.html +2 -2
  107. package/dist/digests.d.ts +1 -0
  108. package/dist/digests.js +132 -42
  109. package/dist/doctor/checks.d.ts +0 -3
  110. package/dist/doctor/checks.js +1 -108
  111. package/dist/doctor/runner.js +1 -4
  112. package/dist/env-sanitizer.d.ts +2 -0
  113. package/dist/env-sanitizer.js +61 -0
  114. package/dist/exec-approval.d.ts +11 -1
  115. package/dist/exec-approval.js +17 -4
  116. package/dist/gateway.d.ts +3 -1
  117. package/dist/gateway.js +17 -7
  118. package/dist/heartbeat.js +1 -6
  119. package/dist/langfuse.js +3 -29
  120. package/dist/model-selection.js +3 -1
  121. package/dist/providers/adapter.d.ts +118 -0
  122. package/dist/providers/adapter.js +6 -0
  123. package/dist/providers/adapters/anthropic-adapter.d.ts +22 -0
  124. package/dist/providers/adapters/anthropic-adapter.js +204 -0
  125. package/dist/providers/adapters/codex-adapter.d.ts +26 -0
  126. package/dist/providers/adapters/codex-adapter.js +203 -0
  127. package/dist/providers/anthropic.d.ts +1 -0
  128. package/dist/providers/anthropic.js +10 -272
  129. package/dist/providers/codex.d.ts +21 -0
  130. package/dist/providers/codex.js +149 -330
  131. package/dist/providers/content.d.ts +1 -1
  132. package/dist/providers/content.js +2 -2
  133. package/dist/providers/context-manager.d.ts +18 -6
  134. package/dist/providers/context-manager.js +199 -223
  135. package/dist/providers/index.d.ts +9 -1
  136. package/dist/providers/index.js +73 -64
  137. package/dist/providers/loop-utils.d.ts +20 -0
  138. package/dist/providers/loop-utils.js +30 -0
  139. package/dist/providers/tool-loop.d.ts +12 -0
  140. package/dist/providers/tool-loop.js +251 -0
  141. package/dist/providers/utils.d.ts +19 -3
  142. package/dist/providers/utils.js +100 -29
  143. package/dist/secure-store.d.ts +8 -0
  144. package/dist/secure-store.js +80 -0
  145. package/dist/service.js +3 -28
  146. package/dist/sessions.d.ts +3 -0
  147. package/dist/sessions.js +147 -18
  148. package/dist/setup-templates.js +13 -25
  149. package/dist/setup.d.ts +10 -6
  150. package/dist/setup.js +84 -292
  151. package/dist/skills.js +3 -11
  152. package/dist/tools/agent-delegation.d.ts +19 -0
  153. package/dist/tools/agent-delegation.js +49 -0
  154. package/dist/tools/bash-tool.js +89 -34
  155. package/dist/tools/definitions.d.ts +199 -302
  156. package/dist/tools/definitions.js +70 -123
  157. package/dist/tools/execute-context.d.ts +13 -4
  158. package/dist/tools/fetch-tool.js +109 -13
  159. package/dist/tools/file-tools.js +7 -1
  160. package/dist/tools.d.ts +7 -7
  161. package/dist/tools.js +133 -151
  162. package/dist/types.d.ts +37 -30
  163. package/dist/utils.js +4 -6
  164. package/dist/voice.d.ts +1 -1
  165. package/dist/voice.js +17 -4
  166. package/package.json +33 -23
  167. package/templates/TOOLS.md +0 -27
  168. package/dist/__tests__/audit.test.js +0 -122
  169. package/dist/__tests__/code-agents-orchestrator.test.js +0 -216
  170. package/dist/__tests__/code-agents-sandbox.test.js +0 -163
  171. package/dist/__tests__/orchestrator.test.js +0 -425
  172. package/dist/__tests__/sandbox-bridge.test.js +0 -116
  173. package/dist/__tests__/sandbox-manager.test.js +0 -144
  174. package/dist/__tests__/sandbox-mount-security.test.js +0 -139
  175. package/dist/__tests__/sandbox-runtime.test.js +0 -176
  176. package/dist/__tests__/subagent.test.js +0 -240
  177. package/dist/__tests__/telegram.test.js +0 -42
  178. package/dist/code-agents/orchestrator.d.ts +0 -29
  179. package/dist/code-agents/orchestrator.js +0 -694
  180. package/dist/code-agents/worktree.d.ts +0 -40
  181. package/dist/code-agents/worktree.js +0 -215
  182. package/dist/dashboard/assets/index-BoTHPby4.js +0 -65
  183. package/dist/dashboard/assets/index-D4mufvBg.css +0 -1
  184. package/dist/dashboard.d.ts +0 -8
  185. package/dist/dashboard.js +0 -4071
  186. package/dist/discord.d.ts +0 -8
  187. package/dist/discord.js +0 -792
  188. package/dist/mcp-context-a8c.d.ts +0 -13
  189. package/dist/mcp-context-a8c.js +0 -34
  190. package/dist/orchestrator.d.ts +0 -15
  191. package/dist/orchestrator.js +0 -676
  192. package/dist/providers/openai.d.ts +0 -10
  193. package/dist/providers/openai.js +0 -355
  194. package/dist/sandbox/bridge.d.ts +0 -5
  195. package/dist/sandbox/bridge.js +0 -63
  196. package/dist/sandbox/index.d.ts +0 -5
  197. package/dist/sandbox/index.js +0 -4
  198. package/dist/sandbox/manager.d.ts +0 -7
  199. package/dist/sandbox/manager.js +0 -100
  200. package/dist/sandbox/mount-security.d.ts +0 -12
  201. package/dist/sandbox/mount-security.js +0 -122
  202. package/dist/sandbox/runtime.d.ts +0 -39
  203. package/dist/sandbox/runtime.js +0 -192
  204. package/dist/sandbox-utils.d.ts +0 -6
  205. package/dist/sandbox-utils.js +0 -36
  206. package/dist/subagent.d.ts +0 -19
  207. package/dist/subagent.js +0 -407
  208. package/dist/telegram.d.ts +0 -2
  209. package/dist/telegram.js +0 -11
  210. package/dist/tools/browser-tool.d.ts +0 -3
  211. package/dist/tools/browser-tool.js +0 -266
  212. package/sandbox/Dockerfile +0 -40
  213. /package/dist/__tests__/{audit.test.d.ts → code-agents-notifications.test.d.ts} +0 -0
  214. /package/dist/__tests__/{code-agents-orchestrator.test.d.ts → code-agents-worktrees.test.d.ts} +0 -0
  215. /package/dist/__tests__/{code-agents-sandbox.test.d.ts → codex-adapter.test.d.ts} +0 -0
  216. /package/dist/__tests__/{orchestrator.test.d.ts → codex-auth.test.d.ts} +0 -0
  217. /package/dist/__tests__/{sandbox-bridge.test.d.ts → codex-provider-gating.test.d.ts} +0 -0
  218. /package/dist/__tests__/{sandbox-manager.test.d.ts → codex-unified-loop.test.d.ts} +0 -0
  219. /package/dist/__tests__/{sandbox-mount-security.test.d.ts → config-security.test.d.ts} +0 -0
  220. /package/dist/__tests__/{sandbox-runtime.test.d.ts → cron-run.test.d.ts} +0 -0
  221. /package/dist/__tests__/{subagent.test.d.ts → digests.test.d.ts} +0 -0
  222. /package/dist/__tests__/{telegram.test.d.ts → discord-attachments.test.d.ts} +0 -0
package/dist/setup.d.ts CHANGED
@@ -3,19 +3,21 @@ interface SetupOptions {
3
3
  dryRun?: boolean;
4
4
  }
5
5
  export declare function renderGatewayPlist(): string;
6
- type ProviderChoice = 'anthropic-api' | 'anthropic-oauth' | 'openai-api' | 'codex-oauth' | 'minimax-api' | 'kimi-api';
6
+ type ProviderChoice = 'anthropic-api' | 'anthropic-oauth' | 'codex-oauth';
7
7
  interface ProviderSecrets {
8
8
  anthropicKey?: string;
9
9
  oauthToken?: string;
10
- openaiKey?: string;
11
- minimaxKey?: string;
12
- kimiKey?: string;
10
+ }
11
+ /** Map of secret name → config reference string (KEYCHAIN ref or ENV ref). */
12
+ interface SecretRefs {
13
+ anthropicKey?: string;
14
+ oauthToken?: string;
15
+ telegramToken?: string;
16
+ discordToken?: string;
13
17
  }
14
18
  interface SetupFeatures {
15
- browser: boolean;
16
19
  voice: boolean;
17
20
  mcp: boolean;
18
- sandbox: boolean;
19
21
  }
20
22
  interface SetupBuildInput {
21
23
  workspaceDir: string;
@@ -28,6 +30,8 @@ interface SetupBuildInput {
28
30
  agentName: string;
29
31
  selectedProviders: Set<ProviderChoice>;
30
32
  providerSecrets: ProviderSecrets;
33
+ /** Resolved secret references (KEYCHAIN or ENV). If not provided, falls back to env var refs. */
34
+ secretRefs?: SecretRefs;
31
35
  features?: SetupFeatures;
32
36
  starters?: SetupStarters;
33
37
  }
package/dist/setup.js CHANGED
@@ -8,7 +8,7 @@ import { spawnSync } from 'child_process';
8
8
  import { randomUUID } from 'crypto';
9
9
  import { runDoctor as runDoctorChecks } from './doctor/runner.js';
10
10
  import { toErrorMessage } from './utils.js';
11
- import { detectSandboxRuntime, isSandboxRuntimeRunning, sandboxNetworkExists, defaultSandboxNetwork, } from './sandbox-utils.js';
11
+ import { secureStoreAvailable, setSecureValue } from './secure-store.js';
12
12
  import { ensureCoreTemplates, ensureStarterSkills, buildStarterCronJobs, } from './setup-templates.js';
13
13
  const __filename = fileURLToPath(import.meta.url);
14
14
  const __dirname = dirname(__filename);
@@ -59,33 +59,6 @@ function loadExistingSetup() {
59
59
  }
60
60
  return { config, env };
61
61
  }
62
- function bootstrapSandbox(runtime, image, network) {
63
- const sandboxDir = join(__dirname, '..', 'sandbox');
64
- const dockerfile = join(sandboxDir, 'Dockerfile');
65
- if (!existsSync(dockerfile)) {
66
- return { ok: false, message: `Sandbox Dockerfile not found: ${dockerfile}` };
67
- }
68
- if (!isSandboxRuntimeRunning(runtime)) {
69
- const hint = runtime === 'container'
70
- ? 'Run `container system start` and rerun onboarding.'
71
- : 'Start Docker Desktop and rerun onboarding.';
72
- return { ok: false, message: `Runtime "${runtime}" is not running. ${hint}` };
73
- }
74
- if (!sandboxNetworkExists(runtime, network)) {
75
- return { ok: false, message: `Network "${network}" not found for ${runtime}. Update sandbox.network and run \`skimpyclaw sandbox init\`.` };
76
- }
77
- const build = spawnSync(runtime, ['build', '--build-arg', 'SKIMPY_PROFILE=minimal', '-t', image, sandboxDir], { encoding: 'utf-8' });
78
- if (build.status !== 0) {
79
- const detail = `${build.stderr || ''}\n${build.stdout || ''}`.trim();
80
- return { ok: false, message: `Image build failed: ${detail.slice(-800)}` };
81
- }
82
- const smoke = spawnSync(runtime, ['run', '--rm', '--network', network, image, 'sh', '-lc', 'hostname && command -v gh >/dev/null && command -v rg >/dev/null && echo sandbox-ok'], { encoding: 'utf-8' });
83
- if (smoke.status !== 0) {
84
- const detail = `${smoke.stderr || ''}\n${smoke.stdout || ''}`.trim();
85
- return { ok: false, message: `Sandbox smoke test failed: ${detail.slice(-800)}` };
86
- }
87
- return { ok: true, message: (smoke.stdout || '').trim().split('\n').join(' | ') };
88
- }
89
62
  function ask(rl, question) {
90
63
  return new Promise((resolve) => {
91
64
  rl.question(question, (answer) => {
@@ -124,10 +97,7 @@ export function renderGatewayPlist() {
124
97
  const PROVIDER_OPTIONS = [
125
98
  { key: 'anthropic-api', label: 'Anthropic API key' },
126
99
  { key: 'anthropic-oauth', label: 'Anthropic OAuth (Claude Code)' },
127
- { key: 'openai-api', label: 'OpenAI API key' },
128
100
  { key: 'codex-oauth', label: 'OpenAI Codex OAuth' },
129
- { key: 'minimax-api', label: 'MiniMax API key' },
130
- { key: 'kimi-api', label: 'Kimi (Moonshot) API key' },
131
101
  ];
132
102
  function detectExistingProviders(config) {
133
103
  const existing = new Set();
@@ -138,14 +108,8 @@ function detectExistingProviders(config) {
138
108
  existing.add('anthropic-oauth');
139
109
  else if (providers.anthropic?.apiKey)
140
110
  existing.add('anthropic-api');
141
- if (providers.openai?.apiKey)
142
- existing.add('openai-api');
143
111
  if (providers.codex || providers.openai?.authToken === 'codex')
144
112
  existing.add('codex-oauth');
145
- if (providers.minimax)
146
- existing.add('minimax-api');
147
- if (providers.kimi)
148
- existing.add('kimi-api');
149
113
  return existing;
150
114
  }
151
115
  async function askProviders(rl, existingProviders) {
@@ -192,6 +156,56 @@ async function askProviders(rl, existingProviders) {
192
156
  return choices;
193
157
  }
194
158
  }
159
+ const KEYCHAIN_SERVICE = 'skimpyclaw';
160
+ /** Secret name → Keychain account name mapping. */
161
+ const SECRET_KEYCHAIN_ACCOUNTS = {
162
+ anthropicKey: 'anthropic-api-key',
163
+ oauthToken: 'anthropic-oauth-token',
164
+ telegramToken: 'telegram-bot-token',
165
+ discordToken: 'discord-bot-token',
166
+ };
167
+ /** Secret name → env var name mapping (fallback when Keychain unavailable). */
168
+ const SECRET_ENV_VARS = {
169
+ anthropicKey: 'ANTHROPIC_API_KEY',
170
+ oauthToken: 'CLAUDE_CODE_OAUTH_TOKEN',
171
+ telegramToken: 'TELEGRAM_BOT_TOKEN',
172
+ discordToken: 'DISCORD_BOT_TOKEN',
173
+ };
174
+ /**
175
+ * Store secrets securely. On macOS, uses Keychain and returns ${KEYCHAIN:...} refs.
176
+ * On other platforms, returns ${ENV_VAR} refs for .env file fallback.
177
+ * Returns the reference map and whether Keychain was used.
178
+ */
179
+ function storeSecretsSecurely(secrets, telegramToken, discordToken) {
180
+ const keychainOk = secureStoreAvailable().ok;
181
+ const refs = {};
182
+ let storedCount = 0;
183
+ const allSecrets = [
184
+ { key: 'anthropicKey', value: secrets.anthropicKey },
185
+ { key: 'oauthToken', value: secrets.oauthToken },
186
+ { key: 'telegramToken', value: telegramToken },
187
+ { key: 'discordToken', value: discordToken },
188
+ ];
189
+ for (const { key, value } of allSecrets) {
190
+ if (!value)
191
+ continue;
192
+ if (keychainOk) {
193
+ try {
194
+ const account = SECRET_KEYCHAIN_ACCOUNTS[key];
195
+ setSecureValue(KEYCHAIN_SERVICE, account, value);
196
+ refs[key] = `\${KEYCHAIN:${KEYCHAIN_SERVICE}/${account}}`;
197
+ storedCount++;
198
+ continue;
199
+ }
200
+ catch {
201
+ // Fall through to env var ref
202
+ }
203
+ }
204
+ const envVar = SECRET_ENV_VARS[key];
205
+ refs[key] = `\${${envVar}}`;
206
+ }
207
+ return { refs, useKeychain: keychainOk && storedCount > 0, storedCount };
208
+ }
195
209
  async function collectProviderSecrets(rl, providers, existingEnv) {
196
210
  const secrets = {};
197
211
  const env = existingEnv || {};
@@ -232,45 +246,6 @@ async function collectProviderSecrets(rl, providers, existingEnv) {
232
246
  console.log(` ${c.yellow('⚠')} Skipped — run \`claude setup-token\`, then add CLAUDE_CODE_OAUTH_TOKEN to ~/.skimpyclaw/.env`);
233
247
  }
234
248
  }
235
- if (providers.has('openai-api')) {
236
- const existing = env.OPENAI_API_KEY || '';
237
- console.log('\n OpenAI API Key');
238
- if (existing) {
239
- const input = await ask(rl, ` Enter key [${maskInput(existing)}]: `);
240
- secrets.openaiKey = input || existing;
241
- }
242
- else {
243
- console.log(' Get one from: https://platform.openai.com/api-keys');
244
- secrets.openaiKey = await ask(rl, ' Enter key: ');
245
- }
246
- console.log(` ✓ ${maskInput(secrets.openaiKey)}`);
247
- }
248
- if (providers.has('minimax-api')) {
249
- const existing = env.MINIMAX_API_KEY || '';
250
- console.log('\n MiniMax API Key');
251
- if (existing) {
252
- const input = await ask(rl, ` Enter key [${maskInput(existing)}]: `);
253
- secrets.minimaxKey = input || existing;
254
- }
255
- else {
256
- console.log(' Get one from: https://platform.minimax.io/user-center/basic-information/interface-key');
257
- secrets.minimaxKey = await ask(rl, ' Enter key: ');
258
- }
259
- console.log(` ✓ ${maskInput(secrets.minimaxKey)}`);
260
- }
261
- if (providers.has('kimi-api')) {
262
- const existing = env.KIMI_API_KEY || '';
263
- console.log('\n Kimi (Moonshot) API Key');
264
- if (existing) {
265
- const input = await ask(rl, ` Enter key [${maskInput(existing)}]: `);
266
- secrets.kimiKey = input || existing;
267
- }
268
- else {
269
- console.log(' Get one from: https://platform.moonshot.cn/console/api-keys');
270
- secrets.kimiKey = await ask(rl, ' Enter key: ');
271
- }
272
- console.log(` ✓ ${maskInput(secrets.kimiKey)}`);
273
- }
274
249
  if (providers.has('codex-oauth')) {
275
250
  console.log('\n OpenAI Codex OAuth');
276
251
  console.log(' No key needed — uses ~/.codex/auth.json at runtime.');
@@ -279,22 +254,13 @@ async function collectProviderSecrets(rl, providers, existingEnv) {
279
254
  console.log('');
280
255
  return secrets;
281
256
  }
282
- function buildProviders(providers) {
257
+ function buildProviders(providers, refs) {
283
258
  const result = {};
284
259
  if (providers.has('anthropic-api')) {
285
- result.anthropic = { apiKey: '${ANTHROPIC_API_KEY}' };
260
+ result.anthropic = { apiKey: refs?.anthropicKey || '${ANTHROPIC_API_KEY}' };
286
261
  }
287
262
  else if (providers.has('anthropic-oauth')) {
288
- result.anthropic = { authToken: '${CLAUDE_CODE_OAUTH_TOKEN}' };
289
- }
290
- if (providers.has('openai-api')) {
291
- result.openai = { apiKey: '${OPENAI_API_KEY}', baseURL: 'https://api.openai.com/v1' };
292
- }
293
- if (providers.has('minimax-api')) {
294
- result.minimax = { apiKey: '${MINIMAX_API_KEY}', baseURL: 'https://api.minimax.io/v1' };
295
- }
296
- if (providers.has('kimi-api')) {
297
- result.kimi = { apiKey: '${KIMI_API_KEY}', baseURL: 'https://api.kimi.com/coding/v1' };
263
+ result.anthropic = { authToken: refs?.oauthToken || '${CLAUDE_CODE_OAUTH_TOKEN}' };
298
264
  }
299
265
  if (providers.has('codex-oauth')) {
300
266
  result.codex = {
@@ -308,48 +274,31 @@ function buildProviders(providers) {
308
274
  function buildDefaultModel(providers) {
309
275
  const hasAnthropic = providers.has('anthropic-api') || providers.has('anthropic-oauth');
310
276
  if (hasAnthropic)
311
- return 'claude-opus';
277
+ return 'anthropic/claude-opus-4-7';
312
278
  if (providers.has('codex-oauth'))
313
- return 'codex/gpt-5.3-codex';
314
- if (providers.has('kimi-api'))
315
- return 'kimi/kimi-for-coding';
316
- if (providers.has('minimax-api'))
317
- return 'minimax/MiniMax-M2.5';
318
- return 'openai/gpt-4o';
279
+ return 'codex/gpt-5.5';
280
+ return 'anthropic/claude-opus-4-7';
319
281
  }
320
282
  function buildAliases(providers) {
321
- // Always include well-known aliases so users can switch models easily
322
283
  const aliases = {
323
- 'claude-fast': 'anthropic/claude-haiku-4-5',
324
- 'claude-think': 'anthropic/claude-sonnet-4-6',
325
- 'claude-opus': 'anthropic/claude-opus-4-6',
326
284
  'codex5.1': 'codex/gpt-5.1-codex',
327
285
  'codex5.2': 'codex/gpt-5.2-codex',
328
286
  'codex5.3': 'codex/gpt-5.3-codex',
329
- 'minimax': 'minimax/MiniMax-M2.5',
330
- 'kimi': 'kimi/kimi-for-coding',
287
+ 'codex5.5': 'codex/gpt-5.5',
331
288
  };
332
- if (providers.has('openai-api')) {
333
- aliases['gpt-fast'] = 'openai/gpt-4o-mini';
334
- aliases.gpt = 'openai/gpt-4o';
335
- }
336
289
  if (providers.has('codex-oauth')) {
337
- aliases.codex = 'codex/gpt-5.3-codex';
338
- }
339
- if (providers.has('minimax-api')) {
340
- aliases.minimax = 'minimax/MiniMax-M2.5';
341
- }
342
- if (providers.has('kimi-api')) {
343
- aliases.kimi = 'kimi/kimi-for-coding';
290
+ aliases.codex = 'codex/gpt-5.5';
344
291
  }
345
292
  return aliases;
346
293
  }
347
- function buildEnvContent(telegramToken, providers, secrets, discordToken) {
294
+ function buildEnvContent(telegramToken, providers, secrets, discordToken, keychainRefs) {
348
295
  const lines = ['# SkimpyClaw secrets'];
349
- if (providers.has('anthropic-api') && secrets.anthropicKey) {
296
+ // Helper: only write to .env if NOT stored in Keychain
297
+ const isInKeychain = (key) => keychainRefs?.[key]?.includes('KEYCHAIN:');
298
+ if (providers.has('anthropic-api') && secrets.anthropicKey && !isInKeychain('anthropicKey')) {
350
299
  lines.push(`ANTHROPIC_API_KEY=${secrets.anthropicKey}`);
351
300
  }
352
- if (providers.has('anthropic-oauth')) {
301
+ if (providers.has('anthropic-oauth') && !isInKeychain('oauthToken')) {
353
302
  if (secrets.oauthToken) {
354
303
  lines.push(`CLAUDE_CODE_OAUTH_TOKEN=${secrets.oauthToken}`);
355
304
  }
@@ -358,17 +307,10 @@ function buildEnvContent(telegramToken, providers, secrets, discordToken) {
358
307
  lines.push('CLAUDE_CODE_OAUTH_TOKEN=');
359
308
  }
360
309
  }
361
- if (providers.has('openai-api') && secrets.openaiKey) {
362
- lines.push(`OPENAI_API_KEY=${secrets.openaiKey}`);
363
- }
364
- if (providers.has('minimax-api') && secrets.minimaxKey) {
365
- lines.push(`MINIMAX_API_KEY=${secrets.minimaxKey}`);
366
- }
367
- if (providers.has('kimi-api') && secrets.kimiKey) {
368
- lines.push(`KIMI_API_KEY=${secrets.kimiKey}`);
310
+ if (!isInKeychain('telegramToken')) {
311
+ lines.push(`TELEGRAM_BOT_TOKEN=${telegramToken}`);
369
312
  }
370
- lines.push(`TELEGRAM_BOT_TOKEN=${telegramToken}`);
371
- if (discordToken) {
313
+ if (discordToken && !isInKeychain('discordToken')) {
372
314
  lines.push(`DISCORD_BOT_TOKEN=${discordToken}`);
373
315
  }
374
316
  lines.push('');
@@ -376,7 +318,7 @@ function buildEnvContent(telegramToken, providers, secrets, discordToken) {
376
318
  }
377
319
  export function buildSetupConfig(input) {
378
320
  const useDiscord = Boolean(input.discordToken);
379
- const features = input.features ?? { browser: false, voice: false, mcp: false, sandbox: false };
321
+ const features = input.features ?? { voice: false, mcp: false };
380
322
  const starters = input.starters ?? {
381
323
  cronTechNews: false,
382
324
  cronWeather: false,
@@ -416,14 +358,14 @@ export function buildSetupConfig(input) {
416
358
  },
417
359
  },
418
360
  models: {
419
- providers: buildProviders(input.selectedProviders),
361
+ providers: buildProviders(input.selectedProviders, input.secretRefs),
420
362
  aliases: buildAliases(input.selectedProviders),
421
363
  },
422
364
  channels: {
423
365
  active: useDiscord ? 'discord' : 'telegram',
424
366
  telegram: {
425
367
  enabled: true,
426
- token: '${TELEGRAM_BOT_TOKEN}',
368
+ token: input.secretRefs?.telegramToken || '${TELEGRAM_BOT_TOKEN}',
427
369
  allowFrom: [parseInt(input.telegramId, 10) || input.telegramId],
428
370
  dailyNotesDir: '${HOME}/.skimpyclaw/Daily Notes',
429
371
  defaultAllowedPaths: allPaths,
@@ -432,12 +374,11 @@ export function buildSetupConfig(input) {
432
374
  allowedPaths: allPaths,
433
375
  maxIterations: 100,
434
376
  bashTimeout: 15000,
435
- ...(features.browser ? { browser: { type: 'chromium', enabled: true, headless: true } } : {}),
436
377
  },
437
378
  },
438
379
  discord: {
439
380
  enabled: useDiscord,
440
- token: useDiscord ? '${DISCORD_BOT_TOKEN}' : '',
381
+ token: useDiscord ? (input.secretRefs?.discordToken || '${DISCORD_BOT_TOKEN}') : '',
441
382
  allowFrom: useDiscord ? [input.discordUserId || ''] : [],
442
383
  defaultAllowedPaths: allPaths,
443
384
  ...(input.discordDefaultChannelId ? { defaultChannelId: input.discordDefaultChannelId } : {}),
@@ -446,7 +387,6 @@ export function buildSetupConfig(input) {
446
387
  allowedPaths: allPaths,
447
388
  maxIterations: 100,
448
389
  bashTimeout: 15000,
449
- ...(features.browser ? { browser: { type: 'chromium', enabled: true, headless: false } } : {}),
450
390
  },
451
391
  },
452
392
  },
@@ -461,7 +401,6 @@ export function buildSetupConfig(input) {
461
401
  allowedPaths: allPaths,
462
402
  maxIterations: 10,
463
403
  bashTimeout: 15000,
464
- ...(features.browser ? { browser: { enabled: true } } : { browser: { enabled: false } }),
465
404
  },
466
405
  },
467
406
  ...(features.voice ? {
@@ -470,13 +409,6 @@ export function buildSetupConfig(input) {
470
409
  defaultProvider: 'macos',
471
410
  providers: {
472
411
  macos: { tts: { voice: 'Samantha' } },
473
- ...(input.selectedProviders.has('openai-api') ? {
474
- openai: {
475
- apiKey: '${OPENAI_API_KEY}',
476
- baseURL: 'https://api.openai.com/v1',
477
- stt: { model: 'whisper-1' },
478
- },
479
- } : {}),
480
412
  },
481
413
  channels: {
482
414
  telegram: { enabled: true, acceptVoice: true, sendVoice: true },
@@ -484,16 +416,6 @@ export function buildSetupConfig(input) {
484
416
  },
485
417
  },
486
418
  } : {}),
487
- ...(features.sandbox ? {
488
- sandbox: {
489
- enabled: true,
490
- image: 'skimpyclaw-sandbox',
491
- cpus: 2,
492
- memory: '2G',
493
- network: 'bridge',
494
- idleTimeoutMs: 3600000,
495
- },
496
- } : {}),
497
419
  ...(Object.keys(starterSkillEntries).length > 0 ? {
498
420
  skills: {
499
421
  enabled: true,
@@ -509,7 +431,7 @@ export function buildSetupArtifacts(input) {
509
431
  const config = buildSetupConfig(input);
510
432
  return {
511
433
  configJson: JSON.stringify(config, null, 2),
512
- envContent: buildEnvContent(input.telegramToken, input.selectedProviders, input.providerSecrets, input.discordToken),
434
+ envContent: buildEnvContent(input.telegramToken, input.selectedProviders, input.providerSecrets, input.discordToken, input.secretRefs),
513
435
  config,
514
436
  };
515
437
  }
@@ -550,34 +472,6 @@ async function validateProviderAuth(providers, secrets) {
550
472
  checks.push({ name: 'Anthropic API', ok: false, detail: toErrorMessage(err) });
551
473
  }
552
474
  }
553
- if (providers.has('openai-api') && secrets.openaiKey) {
554
- try {
555
- const res = await quickFetch('https://api.openai.com/v1/models', {
556
- headers: { authorization: `Bearer ${secrets.openaiKey}` },
557
- });
558
- checks.push({ name: 'OpenAI API', ok: res.ok, detail: res.ok ? 'auth ok' : `HTTP ${res.status}` });
559
- }
560
- catch (err) {
561
- checks.push({ name: 'OpenAI API', ok: false, detail: toErrorMessage(err) });
562
- }
563
- }
564
- if (providers.has('minimax-api') && secrets.minimaxKey) {
565
- try {
566
- const res = await quickFetch('https://api.minimax.io/anthropic/v1/messages', {
567
- method: 'POST',
568
- headers: {
569
- authorization: `Bearer ${secrets.minimaxKey}`,
570
- 'content-type': 'application/json',
571
- 'anthropic-version': '2023-06-01',
572
- },
573
- body: JSON.stringify({ model: 'MiniMax-M2.5', max_tokens: 8, messages: [{ role: 'user', content: 'ping' }] }),
574
- });
575
- checks.push({ name: 'MiniMax API', ok: res.ok, detail: res.ok ? 'auth ok' : `HTTP ${res.status}` });
576
- }
577
- catch (err) {
578
- checks.push({ name: 'MiniMax API', ok: false, detail: toErrorMessage(err) });
579
- }
580
- }
581
475
  if (providers.has('codex-oauth')) {
582
476
  const authPath = join(homedir(), '.codex', 'auth.json');
583
477
  checks.push({ name: 'Codex OAuth', ok: existsSync(authPath), detail: existsSync(authPath) ? authPath : `missing ${authPath}` });
@@ -720,43 +614,9 @@ export async function runSetup(options = {}) {
720
614
  }
721
615
  statusOk('Acknowledged');
722
616
  // 8. Optional Features
723
- const existingBrowser = existing.config?.heartbeat?.tools?.browser?.enabled === true
724
- || existing.config?.channels?.telegram?.tools?.browser?.enabled === true;
725
617
  const existingVoice = existing.config?.voice?.enabled === true;
726
618
  sectionHeader('Optional Features');
727
- // 6a. Browser tool
728
- const browserDefault = existingBrowser ? 'Y' : 'N';
729
- const enableBrowser = /^y(es)?$/i.test(await ask(rl, ` Enable browser tool? (requires Chrome) [${existingBrowser ? 'Y/n' : 'y/N'}]: `) || browserDefault);
730
- if (enableBrowser) {
731
- const macChrome = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
732
- const which = spawnSync('which', ['google-chrome'], { encoding: 'utf-8' });
733
- if (which.status === 0 || existsSync(macChrome)) {
734
- statusOk('Chrome detected');
735
- }
736
- else {
737
- statusWarn('Chrome not found — browser tool may not work until Chrome is installed');
738
- }
739
- // Check for Playwright
740
- const pw = spawnSync('npx', ['playwright', '--version'], { encoding: 'utf-8', timeout: 10000 });
741
- if (pw.status === 0) {
742
- statusOk('Playwright detected');
743
- }
744
- else {
745
- console.log('');
746
- console.log(' ┌─────────────────────────────────────────────────────────┐');
747
- console.log(' │ Browser tool requires Playwright. Install it: │');
748
- console.log(' │ │');
749
- console.log(' │ npx playwright install chromium │');
750
- console.log(' │ │');
751
- console.log(' │ Without this, the browser tool will fail at runtime. │');
752
- console.log(' └─────────────────────────────────────────────────────────┘');
753
- console.log('');
754
- }
755
- }
756
- else {
757
- statusOk('browser disabled');
758
- }
759
- // 6b. Voice/TTS
619
+ // Voice/TTS
760
620
  const voiceDefault = existingVoice ? 'Y' : 'N';
761
621
  const enableVoice = /^y(es)?$/i.test(await ask(rl, ` Enable voice/TTS? (requires ffmpeg) [${existingVoice ? 'Y/n' : 'y/N'}]: `) || voiceDefault);
762
622
  if (enableVoice) {
@@ -779,16 +639,12 @@ export async function runSetup(options = {}) {
779
639
  else {
780
640
  console.log('');
781
641
  console.log(' ┌─────────────────────────────────────────────────────────┐');
782
- console.log(' │ Voice transcription (STT) requires one of: │');
642
+ console.log(' │ Voice transcription (STT) requires local whisper.cpp: │');
783
643
  console.log(' │ │');
784
- console.log(' │ Option A: Local whisper.cpp (free, recommended) │');
644
+ console.log(' │ Local whisper.cpp (free, recommended) │');
785
645
  console.log(' │ brew install whisper-cpp │');
786
646
  console.log(' │ whisper-cpp-download-ggml-model small │');
787
647
  console.log(' │ │');
788
- console.log(' │ Option B: OpenAI Whisper API ($0.006/min) │');
789
- console.log(' │ Add OPENAI_API_KEY to ~/.skimpyclaw/.env │');
790
- console.log(' │ Config auto-includes openai.stt if provider selected │');
791
- console.log(' │ │');
792
648
  console.log(' │ Without either, voice messages cannot be transcribed. │');
793
649
  console.log(' └─────────────────────────────────────────────────────────┘');
794
650
  console.log('');
@@ -797,7 +653,7 @@ export async function runSetup(options = {}) {
797
653
  else {
798
654
  statusOk('voice disabled');
799
655
  }
800
- // 6c. MCP tools
656
+ // MCP tools
801
657
  console.log(' Install mcporter: https://github.com/steipete/mcporter');
802
658
  const enableMcp = /^y(es)?$/i.test(await ask(rl, ' Enable MCP tools? (requires mcporter at ~/.mcporter/) [y/N]: '));
803
659
  if (enableMcp) {
@@ -812,34 +668,9 @@ export async function runSetup(options = {}) {
812
668
  else {
813
669
  statusOk('MCP tools disabled');
814
670
  }
815
- // 6d. Sandbox (container isolation)
816
- const existingSandbox = existing.config?.sandbox?.enabled === true;
817
- const sandboxDefault = existingSandbox ? 'Y' : 'N';
818
- const enableSandbox = /^y(es)?$/i.test(await ask(rl, ` Enable sandbox? (requires Docker or Apple Containers) [${existingSandbox ? 'Y/n' : 'y/N'}]: `) || sandboxDefault);
819
- let detectedSandboxRuntime = null;
820
- if (enableSandbox) {
821
- const containerCli = spawnSync('which', ['container'], { encoding: 'utf-8' });
822
- const docker = spawnSync('which', ['docker'], { encoding: 'utf-8' });
823
- if (containerCli.status === 0) {
824
- detectedSandboxRuntime = 'container';
825
- statusOk('Apple Containers detected');
826
- }
827
- else if (docker.status === 0) {
828
- detectedSandboxRuntime = 'docker';
829
- statusOk('Docker detected');
830
- }
831
- else {
832
- statusWarn('No container runtime found — sandbox features won\'t work until Docker or Apple Containers is installed');
833
- }
834
- }
835
- else {
836
- statusOk('sandbox disabled');
837
- }
838
671
  const features = {
839
- browser: enableBrowser,
840
672
  voice: enableVoice,
841
673
  mcp: enableMcp,
842
- sandbox: enableSandbox,
843
674
  };
844
675
  sectionHeader('Starter Packs (optional)');
845
676
  const localTz = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
@@ -866,6 +697,11 @@ export async function runSetup(options = {}) {
866
697
  skillWeather: false,
867
698
  skillWebSearch: false,
868
699
  };
700
+ // Store secrets in macOS Keychain when available (default on macOS)
701
+ const { refs: secretRefs, useKeychain, storedCount } = storeSecretsSecurely(providerSecrets, telegramToken, useDiscord ? discordToken : undefined);
702
+ if (useKeychain) {
703
+ statusOk(`${storedCount} secret(s) stored in macOS Keychain`);
704
+ }
869
705
  const { envContent, config: generatedConfig } = buildSetupArtifacts({
870
706
  workspaceDir: extraAllowedPaths[0] || join(homedir(), '.skimpyclaw'),
871
707
  extraAllowedPaths,
@@ -877,6 +713,7 @@ export async function runSetup(options = {}) {
877
713
  agentName,
878
714
  selectedProviders,
879
715
  providerSecrets,
716
+ secretRefs,
880
717
  features,
881
718
  starters,
882
719
  });
@@ -908,9 +745,6 @@ export async function runSetup(options = {}) {
908
745
  if (existing.config.langfuse) {
909
746
  generatedConfig.langfuse = existing.config.langfuse;
910
747
  }
911
- if (existing.config.sandbox) {
912
- generatedConfig.sandbox = existing.config.sandbox;
913
- }
914
748
  // Preserve voice provider config if voice was already configured
915
749
  if (existing.config.voice?.providers && Object.keys(existing.config.voice.providers).length > 0) {
916
750
  generatedConfig.voice = existing.config.voice;
@@ -927,16 +761,6 @@ export async function runSetup(options = {}) {
927
761
  };
928
762
  }
929
763
  }
930
- if (enableSandbox && generatedConfig.sandbox) {
931
- const runtime = detectSandboxRuntime(detectedSandboxRuntime) || detectSandboxRuntime();
932
- if (runtime) {
933
- generatedConfig.sandbox.runtime = runtime;
934
- generatedConfig.sandbox.network = defaultSandboxNetwork(runtime);
935
- }
936
- if (!generatedConfig.sandbox.image) {
937
- generatedConfig.sandbox.image = 'skimpyclaw-sandbox:latest';
938
- }
939
- }
940
764
  // Create directories
941
765
  console.log('Creating directories...');
942
766
  mkdirSync(CONFIG_DIR, { recursive: true });
@@ -995,27 +819,6 @@ export async function runSetup(options = {}) {
995
819
  writeFileSync(envPath, envContent);
996
820
  console.log(`✓ Secrets written to ${envPath}`);
997
821
  }
998
- if (enableSandbox) {
999
- sectionHeader('Sandbox Bootstrap');
1000
- const sandboxCfg = generatedConfig.sandbox || {};
1001
- const runtime = detectSandboxRuntime(sandboxCfg.runtime);
1002
- const image = String(sandboxCfg.image || 'skimpyclaw-sandbox:latest');
1003
- const network = String(sandboxCfg.network || (runtime ? defaultSandboxNetwork(runtime) : 'bridge'));
1004
- if (!runtime) {
1005
- statusWarn('Sandbox enabled, but no runtime detected. Run `skimpyclaw sandbox init` later.');
1006
- }
1007
- else {
1008
- console.log(` Building sandbox image (${runtime}, network=${network})...`);
1009
- const bootstrap = bootstrapSandbox(runtime, image, network);
1010
- if (bootstrap.ok) {
1011
- statusOk(`sandbox ready (${bootstrap.message})`);
1012
- }
1013
- else {
1014
- statusWarn(bootstrap.message);
1015
- console.log(` ${c.dim('You can retry later with: skimpyclaw sandbox init')}`);
1016
- }
1017
- }
1018
- }
1019
822
  // Update USER.md with name
1020
823
  writeFileSync(join(AGENTS_DIR, 'USER.md'), `# USER.md - About ${userName}\n\nName: ${userName}\n\n## Preferences\n\n- Direct communication, no fluff\n\n## Routines\n\n- Morning: Review tasks and messages\n- EOD: Review completed work, plan tomorrow\n`);
1021
824
  // Create launchd plist from template
@@ -1054,17 +857,6 @@ export async function runSetup(options = {}) {
1054
857
  console.log('\nNext steps:');
1055
858
  let step = 1;
1056
859
  console.log(`${step++}. Review templates in ~/.skimpyclaw/agents/main/`);
1057
- if (enableSandbox) {
1058
- const runtimeHint = detectedSandboxRuntime === 'docker'
1059
- ? 'open -a Docker # or start Docker Desktop'
1060
- : 'container system start';
1061
- console.log(`${step++}. Start the container runtime (if not already running):`);
1062
- console.log(` ${runtimeHint}`);
1063
- console.log(`${step++}. Initialize the sandbox:`);
1064
- console.log(' skimpyclaw sandbox init');
1065
- console.log(`${step++}. Verify sandbox is working:`);
1066
- console.log(' skimpyclaw sandbox doctor');
1067
- }
1068
860
  console.log(`${step++}. Start the daemon:`);
1069
861
  console.log(' skimpyclaw start --daemon');
1070
862
  console.log(`${step++}. Check health:`);
@@ -1072,10 +864,10 @@ export async function runSetup(options = {}) {
1072
864
  console.log(`${step++}. Optional daemon controls: skimpyclaw stop | skimpyclaw restart`);
1073
865
  console.log(`${step++}. Send /help in your ${useDiscord ? 'Discord bot DM/server' : 'Telegram bot'}`);
1074
866
  console.log('');
1075
- console.log(`${c.yellow('Note:')} The /team and /code tools require an external coding CLI on your PATH:`);
867
+ console.log(`${c.yellow('Note:')} The /code tool requires an external coding CLI on your PATH:`);
1076
868
  console.log(' • Claude Code CLI → https://docs.anthropic.com/en/docs/claude-code');
1077
869
  console.log(' • Codex CLI → https://github.com/openai/codex');
1078
- console.log(' Install at least one to use code_with_agent / code_with_team.');
870
+ console.log(' Install at least one to use code_with_agent.');
1079
871
  console.log('\n👙🦞 Enjoy!');
1080
872
  }
1081
873
  finally {