@keygraph/shannon 1.0.0 → 1.2.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/dist/index.mjs CHANGED
@@ -76,59 +76,30 @@ function isTemporalReady() {
76
76
  "localhost:7233"
77
77
  ]).includes("SERVING");
78
78
  }
79
- /** Check if the router container is running and healthy. */
80
- function isRouterReady() {
81
- return runOutput("docker", [
82
- "inspect",
83
- "--format",
84
- "{{.State.Health.Status}}",
85
- "shannon-router"
86
- ]) === "healthy";
87
- }
88
79
  /**
89
- * Ensure Temporal (and optionally router) are running via compose.
90
- * If Temporal is already up but router is needed and missing, starts router only.
80
+ * Ensure Temporal is running via compose.
91
81
  */
92
- async function ensureInfra(useRouter) {
93
- const temporalReady = isTemporalReady();
94
- const routerNeeded = useRouter && !isRouterReady();
95
- if (temporalReady && !routerNeeded) return;
96
- const composeArgs = [
82
+ async function ensureInfra() {
83
+ if (isTemporalReady()) return;
84
+ const composeFile = getComposeFile();
85
+ console.log("Starting Shannon infrastructure...");
86
+ execFileSync("docker", [
97
87
  "compose",
98
88
  "-f",
99
- getComposeFile()
100
- ];
101
- if (useRouter) composeArgs.push("--profile", "router");
102
- composeArgs.push("up", "-d");
103
- if (temporalReady && routerNeeded) console.log("Starting router...");
104
- else console.log("Starting Shannon infrastructure...");
105
- execFileSync("docker", composeArgs, { stdio: "inherit" });
106
- if (!temporalReady) {
107
- console.log("Waiting for Temporal to be ready...");
108
- for (let i = 0; i < 30; i++) {
109
- if (isTemporalReady()) {
110
- console.log("Temporal is ready!");
111
- break;
112
- }
113
- if (i === 29) {
114
- console.error("Timeout waiting for Temporal");
115
- process.exit(1);
116
- }
117
- await setTimeout$1(2e3);
89
+ composeFile,
90
+ "up",
91
+ "-d"
92
+ ], { stdio: "inherit" });
93
+ console.log("Waiting for Temporal to be ready...");
94
+ for (let i = 0; i < 30; i++) {
95
+ if (isTemporalReady()) {
96
+ console.log("Temporal is ready!");
97
+ return;
118
98
  }
99
+ await setTimeout$1(2e3);
119
100
  }
120
- if (routerNeeded) {
121
- console.log("Waiting for router to be ready...");
122
- for (let i = 0; i < 15; i++) {
123
- if (isRouterReady()) {
124
- console.log("Router is ready!");
125
- return;
126
- }
127
- await setTimeout$1(2e3);
128
- }
129
- console.error("Timeout waiting for router");
130
- process.exit(1);
131
- }
101
+ console.error("Timeout waiting for Temporal");
102
+ process.exit(1);
132
103
  }
133
104
  /**
134
105
  * Build the worker image locally (local mode only).
@@ -180,21 +151,20 @@ function addHostFlag() {
180
151
  }
181
152
  /**
182
153
  * Spawn the worker container in detached mode and return the process.
154
+ * When `opts.debug` is true, omits `--rm` so the container persists for log inspection.
183
155
  */
184
156
  function spawnWorker(opts) {
185
- const args = [
186
- "run",
187
- "-d",
188
- "--rm",
189
- "--name",
190
- opts.containerName,
191
- "--network",
192
- "shannon-net"
193
- ];
157
+ const args = ["run", "-d"];
158
+ if (!opts.debug) args.push("--rm");
159
+ args.push("--name", opts.containerName, "--network", "shannon-net");
194
160
  args.push(...addHostFlag());
195
161
  if (os.platform() === "linux" && process.getuid && process.getgid) args.push("-e", `SHANNON_HOST_UID=${process.getuid()}`, "-e", `SHANNON_HOST_GID=${process.getgid()}`);
196
162
  args.push("-v", `${opts.workspacesDir}:/app/workspaces`);
197
- args.push("-v", `${opts.repo.hostPath}:${opts.repo.containerPath}`);
163
+ args.push("-v", `${opts.repo.hostPath}:${opts.repo.containerPath}:ro`);
164
+ const workspacePath = path.join(opts.workspacesDir, opts.workspace);
165
+ args.push("-v", `${path.join(workspacePath, "deliverables")}:${opts.repo.containerPath}/.shannon/deliverables`);
166
+ args.push("-v", `${path.join(workspacePath, "scratchpad")}:${opts.repo.containerPath}/.shannon/scratchpad`);
167
+ args.push("-v", `${path.join(workspacePath, ".playwright-cli")}:${opts.repo.containerPath}/.shannon/.playwright-cli`);
198
168
  if (opts.promptsDir) args.push("-v", `${opts.promptsDir}:/app/apps/worker/prompts:ro`);
199
169
  if (opts.config) args.push("-v", `${opts.config.hostPath}:${opts.config.containerPath}:ro`);
200
170
  if (opts.outputDir) args.push("-v", `${opts.outputDir}:/app/output`);
@@ -206,10 +176,14 @@ function spawnWorker(opts) {
206
176
  args.push("--task-queue", opts.taskQueue);
207
177
  if (opts.config) args.push("--config", opts.config.containerPath);
208
178
  if (opts.outputDir) args.push("--output", "/app/output");
209
- if (opts.workspace) args.push("--workspace", opts.workspace);
179
+ args.push("--workspace", opts.workspace);
210
180
  if (opts.pipelineTesting) args.push("--pipeline-testing");
211
181
  return spawn("docker", args, {
212
- stdio: "pipe",
182
+ stdio: [
183
+ "ignore",
184
+ "ignore",
185
+ "inherit"
186
+ ],
213
187
  ...os.platform() === "win32" && { env: {
214
188
  ...process.env,
215
189
  MSYS_NO_PATHCONV: "1"
@@ -239,8 +213,6 @@ function stopInfra(clean) {
239
213
  "compose",
240
214
  "-f",
241
215
  getComposeFile(),
242
- "--profile",
243
- "router",
244
216
  "down"
245
217
  ];
246
218
  if (clean) args.push("-v");
@@ -445,16 +417,13 @@ async function setup() {
445
417
  {
446
418
  value: "vertex",
447
419
  label: "Claude via Google Vertex AI"
448
- },
449
- {
450
- value: "router",
451
- label: "Router",
452
- hint: "experimental"
453
420
  }
454
421
  ]
455
422
  });
456
423
  if (p.isCancel(provider)) return cancelAndExit();
457
- saveConfig(await setupProvider(provider));
424
+ const config = await setupProvider(provider);
425
+ await maybePromptAdaptiveThinking(config);
426
+ saveConfig(config);
458
427
  const configPath = path.join(SHANNON_HOME$1, "config.toml");
459
428
  p.log.success(`Configuration saved to ${configPath}`);
460
429
  p.outro("Run `npx @keygraph/shannon start` to begin a scan.");
@@ -465,7 +434,6 @@ async function setupProvider(provider) {
465
434
  case "custom_base_url": return setupCustomBaseUrl();
466
435
  case "bedrock": return setupBedrock();
467
436
  case "vertex": return setupVertex();
468
- case "router": return setupRouter();
469
437
  }
470
438
  }
471
439
  async function setupAnthropic() {
@@ -484,7 +452,7 @@ async function setupAnthropic() {
484
452
  if (authMethod === "oauth") config.anthropic = { oauth_token: await promptSecret("Enter your OAuth token") };
485
453
  else config.anthropic = { api_key: await promptSecret("Enter your Anthropic API key") };
486
454
  const customizeModels = await p.confirm({
487
- message: "Do you want to change the default models?\n Small - claude-haiku-4-5-20251001\n Medium - claude-sonnet-4-6\n Large - claude-opus-4-6",
455
+ message: "Do you want to change the default models?\n Small - claude-haiku-4-5-20251001\n Medium - claude-sonnet-4-6\n Large - claude-opus-4-7",
488
456
  initialValue: false
489
457
  });
490
458
  if (p.isCancel(customizeModels)) return cancelAndExit();
@@ -503,7 +471,7 @@ async function setupAnthropic() {
503
471
  if (p.isCancel(medium)) return cancelAndExit();
504
472
  const large = await p.text({
505
473
  message: "Large model ID",
506
- initialValue: "claude-opus-4-6",
474
+ initialValue: "claude-opus-4-7",
507
475
  validate: required("Large model ID is required")
508
476
  });
509
477
  if (p.isCancel(large)) return cancelAndExit();
@@ -534,7 +502,7 @@ async function setupCustomBaseUrl() {
534
502
  auth_token: await promptSecret("Enter the auth token for the custom endpoint")
535
503
  } };
536
504
  const customizeModels = await p.confirm({
537
- message: "Do you want to change the default models?\n Small - claude-haiku-4-5-20251001\n Medium - claude-sonnet-4-6\n Large - claude-opus-4-6",
505
+ message: "Do you want to change the default models?\n Small - claude-haiku-4-5-20251001\n Medium - claude-sonnet-4-6\n Large - claude-opus-4-7",
538
506
  initialValue: false
539
507
  });
540
508
  if (p.isCancel(customizeModels)) return cancelAndExit();
@@ -553,7 +521,7 @@ async function setupCustomBaseUrl() {
553
521
  if (p.isCancel(medium)) return cancelAndExit();
554
522
  const large = await p.text({
555
523
  message: "Large model ID",
556
- initialValue: "claude-opus-4-6",
524
+ initialValue: "claude-opus-4-7",
557
525
  validate: required("Large model ID is required")
558
526
  });
559
527
  if (p.isCancel(large)) return cancelAndExit();
@@ -587,7 +555,7 @@ async function setupBedrock() {
587
555
  if (p.isCancel(medium)) return cancelAndExit();
588
556
  const large = await p.text({
589
557
  message: "Large model ID",
590
- placeholder: "us.anthropic.claude-opus-4-6",
558
+ placeholder: "us.anthropic.claude-opus-4-7",
591
559
  validate: required("Large model ID is required")
592
560
  });
593
561
  if (p.isCancel(large)) return cancelAndExit();
@@ -644,7 +612,7 @@ async function setupVertex() {
644
612
  }),
645
613
  large: () => p.text({
646
614
  message: "Large model ID",
647
- placeholder: "claude-opus-4-6",
615
+ placeholder: "claude-opus-4-7",
648
616
  validate: required("Large model ID is required")
649
617
  })
650
618
  });
@@ -663,48 +631,22 @@ async function setupVertex() {
663
631
  }
664
632
  };
665
633
  }
666
- async function setupRouter() {
667
- const routerProvider = await p.select({
668
- message: "Router provider",
669
- options: [{
670
- value: "openai",
671
- label: "OpenAI"
672
- }, {
673
- value: "openrouter",
674
- label: "OpenRouter"
675
- }]
634
+ async function maybePromptAdaptiveThinking(config) {
635
+ const m = config.models;
636
+ if (!(!m || [
637
+ m.small,
638
+ m.medium,
639
+ m.large
640
+ ].some((v) => v && /opus-4-[67]/.test(v)))) return;
641
+ const enable = await p.confirm({
642
+ message: "Enable adaptive thinking on Opus 4.6/4.7? Claude decides when and how deeply to reason.",
643
+ initialValue: true
676
644
  });
677
- if (p.isCancel(routerProvider)) return cancelAndExit();
678
- const apiKey = await promptSecret(routerProvider === "openai" ? "Enter your OpenAI API key" : "Enter your OpenRouter API key");
679
- let defaultModel;
680
- if (routerProvider === "openai") {
681
- const model = await p.select({
682
- message: "Default model",
683
- options: [{
684
- value: "gpt-5.2",
685
- label: "GPT-5.2"
686
- }, {
687
- value: "gpt-5-mini",
688
- label: "GPT-5 Mini"
689
- }]
690
- });
691
- if (p.isCancel(model)) return cancelAndExit();
692
- defaultModel = `openai,${model}`;
693
- } else {
694
- const model = await p.select({
695
- message: "Default model",
696
- options: [{
697
- value: "google/gemini-3-flash-preview",
698
- label: "Google Gemini 3 Flash Preview"
699
- }]
700
- });
701
- if (p.isCancel(model)) return cancelAndExit();
702
- defaultModel = `openrouter,${model}`;
703
- }
704
- const router = { default: defaultModel };
705
- if (routerProvider === "openai") router.openai_key = apiKey;
706
- else router.openrouter_key = apiKey;
707
- return { router };
645
+ if (p.isCancel(enable)) return cancelAndExit();
646
+ config.core = {
647
+ ...config.core,
648
+ adaptive_thinking: enable
649
+ };
708
650
  }
709
651
  async function promptSecret(message) {
710
652
  const value = await p.password({
@@ -738,6 +680,12 @@ const CONFIG_MAP = [
738
680
  toml: "core.max_tokens",
739
681
  type: "number"
740
682
  },
683
+ {
684
+ env: "CLAUDE_ADAPTIVE_THINKING",
685
+ toml: "core.adaptive_thinking",
686
+ type: "boolean",
687
+ boolFormat: "literal"
688
+ },
741
689
  {
742
690
  env: "ANTHROPIC_API_KEY",
743
691
  toml: "anthropic.api_key",
@@ -793,21 +741,6 @@ const CONFIG_MAP = [
793
741
  toml: "custom_base_url.auth_token",
794
742
  type: "string"
795
743
  },
796
- {
797
- env: "ROUTER_DEFAULT",
798
- toml: "router.default",
799
- type: "string"
800
- },
801
- {
802
- env: "OPENAI_API_KEY",
803
- toml: "router.openai_key",
804
- type: "string"
805
- },
806
- {
807
- env: "OPENROUTER_API_KEY",
808
- toml: "router.openrouter_key",
809
- type: "string"
810
- },
811
744
  {
812
745
  env: "ANTHROPIC_SMALL_MODEL",
813
746
  toml: "models.small",
@@ -824,15 +757,18 @@ const CONFIG_MAP = [
824
757
  type: "string"
825
758
  }
826
759
  ];
827
- /** Read a nested TOML value by dotted path (e.g. "anthropic.api_key"). */
828
- function getTomlValue(config, path) {
829
- const [section, key] = path.split(".");
760
+ /** Read a nested TOML value for a given mapping. */
761
+ function getTomlValue(config, mapping) {
762
+ const [section, key] = mapping.toml.split(".");
830
763
  if (!section || !key) return void 0;
831
764
  const sectionObj = config[section];
832
765
  if (!sectionObj || typeof sectionObj !== "object") return void 0;
833
766
  const value = sectionObj[key];
834
767
  if (value === void 0 || value === null) return void 0;
835
- if (typeof value === "boolean") return value ? "1" : "0";
768
+ if (typeof value === "boolean") {
769
+ if (mapping.boolFormat === "literal") return value ? "true" : "false";
770
+ return value ? "1" : "0";
771
+ }
836
772
  return String(value);
837
773
  }
838
774
  /** Parse the global TOML config file, returning null if it doesn't exist. */
@@ -906,13 +842,6 @@ function validateProviderFields(config, provider, errors) {
906
842
  validateModelTiers(config, "vertex", errors);
907
843
  break;
908
844
  }
909
- case "router": {
910
- if (!keys.includes("default")) errors.push("[router] missing required key: default");
911
- if (!keys.includes("openai_key") && !keys.includes("openrouter_key")) errors.push("[router] requires either openai_key or openrouter_key");
912
- const models = config.models;
913
- if (models && typeof models === "object" && Object.keys(models).length > 0) errors.push("[models] is not supported with [router]");
914
- break;
915
- }
916
845
  }
917
846
  }
918
847
  /** Bedrock and Vertex require a [models] section with all three tiers. */
@@ -965,8 +894,7 @@ function validateConfig(config) {
965
894
  "anthropic",
966
895
  "custom_base_url",
967
896
  "bedrock",
968
- "vertex",
969
- "router"
897
+ "vertex"
970
898
  ].filter((s) => {
971
899
  const section = config[s];
972
900
  return section && typeof section === "object" && Object.keys(section).length > 0;
@@ -997,7 +925,7 @@ function resolveConfig$1() {
997
925
  }
998
926
  for (const mapping of CONFIG_MAP) {
999
927
  if (process.env[mapping.env]) continue;
1000
- const value = getTomlValue(toml, mapping.toml);
928
+ const value = getTomlValue(toml, mapping);
1001
929
  if (value) process.env[mapping.env] = value;
1002
930
  }
1003
931
  }
@@ -1014,7 +942,6 @@ const FORWARD_VARS = [
1014
942
  "ANTHROPIC_API_KEY",
1015
943
  "ANTHROPIC_BASE_URL",
1016
944
  "ANTHROPIC_AUTH_TOKEN",
1017
- "ROUTER_DEFAULT",
1018
945
  "CLAUDE_CODE_OAUTH_TOKEN",
1019
946
  "CLAUDE_CODE_USE_BEDROCK",
1020
947
  "AWS_REGION",
@@ -1027,8 +954,7 @@ const FORWARD_VARS = [
1027
954
  "ANTHROPIC_MEDIUM_MODEL",
1028
955
  "ANTHROPIC_LARGE_MODEL",
1029
956
  "CLAUDE_CODE_MAX_OUTPUT_TOKENS",
1030
- "OPENAI_API_KEY",
1031
- "OPENROUTER_API_KEY"
957
+ "CLAUDE_ADAPTIVE_THINKING"
1032
958
  ];
1033
959
  /**
1034
960
  * Load credentials into process.env.
@@ -1054,10 +980,6 @@ function buildEnvFlags() {
1054
980
  }
1055
981
  return flags;
1056
982
  }
1057
- /** Check if router credentials are present in the environment. */
1058
- function isRouterConfigured() {
1059
- return !!(process.env.ROUTER_DEFAULT && (process.env.OPENAI_API_KEY || process.env.OPENROUTER_API_KEY));
1060
- }
1061
983
  /** Check if a custom Anthropic-compatible base URL is configured. */
1062
984
  function isCustomBaseUrlConfigured() {
1063
985
  return !!(process.env.ANTHROPIC_BASE_URL && process.env.ANTHROPIC_AUTH_TOKEN);
@@ -1070,7 +992,6 @@ function detectProviders() {
1070
992
  if (isCustomBaseUrlConfigured()) providers.push("Custom Base URL");
1071
993
  if (process.env.CLAUDE_CODE_USE_BEDROCK === "1") providers.push("AWS Bedrock");
1072
994
  if (process.env.CLAUDE_CODE_USE_VERTEX === "1") providers.push("Google Vertex");
1073
- if (isRouterConfigured()) providers.push("Router");
1074
995
  return providers;
1075
996
  }
1076
997
  /**
@@ -1091,13 +1012,10 @@ function validateCredentials() {
1091
1012
  valid: true,
1092
1013
  mode: "oauth"
1093
1014
  };
1094
- if (isCustomBaseUrlConfigured()) {
1095
- process.env.ANTHROPIC_API_KEY = process.env.ANTHROPIC_AUTH_TOKEN;
1096
- return {
1097
- valid: true,
1098
- mode: "custom-base-url"
1099
- };
1100
- }
1015
+ if (isCustomBaseUrlConfigured()) return {
1016
+ valid: true,
1017
+ mode: "custom-base-url"
1018
+ };
1101
1019
  if (process.env.CLAUDE_CODE_USE_BEDROCK === "1") {
1102
1020
  const missing = [];
1103
1021
  if (!process.env.AWS_REGION) missing.push("AWS_REGION");
@@ -1137,13 +1055,6 @@ function validateCredentials() {
1137
1055
  mode: "vertex"
1138
1056
  };
1139
1057
  }
1140
- if (isRouterConfigured()) {
1141
- process.env.ANTHROPIC_API_KEY = "router-mode";
1142
- return {
1143
- valid: true,
1144
- mode: "router"
1145
- };
1146
- }
1147
1058
  return {
1148
1059
  valid: false,
1149
1060
  mode: "api-key",
@@ -1207,14 +1118,6 @@ function resolveConfig(configArg) {
1207
1118
  containerPath: `/app/configs/${path.basename(hostPath)}`
1208
1119
  };
1209
1120
  }
1210
- /**
1211
- * Ensure the deliverables directory exists and is writable by the container user.
1212
- */
1213
- function ensureDeliverables(repoHostPath) {
1214
- const deliverables = path.join(repoHostPath, "deliverables");
1215
- fs.mkdirSync(deliverables, { recursive: true });
1216
- fs.chmodSync(deliverables, 511);
1217
- }
1218
1121
  //#endregion
1219
1122
  //#region src/splash.ts
1220
1123
  /**
@@ -1271,23 +1174,35 @@ async function start(args) {
1271
1174
  console.error(`ERROR: ${creds.error}`);
1272
1175
  process.exit(1);
1273
1176
  }
1274
- const useRouter = args.router || isRouterConfigured();
1275
1177
  const repo = resolveRepo(args.repo);
1276
1178
  const config = args.config ? resolveConfig(args.config) : void 0;
1277
- ensureDeliverables(repo.hostPath);
1278
1179
  const workspacesDir = getWorkspacesDir();
1279
1180
  fs.mkdirSync(workspacesDir, { recursive: true });
1280
1181
  fs.chmodSync(workspacesDir, 511);
1281
- if (useRouter) {
1282
- process.env.ANTHROPIC_BASE_URL = "http://shannon-router:3456";
1283
- process.env.ANTHROPIC_AUTH_TOKEN = "shannon-router-key";
1284
- }
1285
1182
  ensureImage(args.version);
1286
- await ensureInfra(useRouter);
1183
+ await ensureInfra();
1287
1184
  const suffix = randomSuffix();
1288
1185
  const taskQueue = `shannon-${suffix}`;
1289
1186
  const containerName = `shannon-worker-${suffix}`;
1290
1187
  const workspace = args.workspace ?? `${new URL(args.url).hostname.replace(/[^a-zA-Z0-9-]/g, "-")}_shannon-${Date.now()}`;
1188
+ const workspacePath = path.join(workspacesDir, workspace);
1189
+ fs.mkdirSync(workspacePath, { recursive: true });
1190
+ fs.chmodSync(workspacePath, 511);
1191
+ for (const dir of [
1192
+ "deliverables",
1193
+ "scratchpad",
1194
+ ".playwright-cli"
1195
+ ]) {
1196
+ const dirPath = path.join(workspacePath, dir);
1197
+ fs.mkdirSync(dirPath, { recursive: true });
1198
+ fs.chmodSync(dirPath, 511);
1199
+ }
1200
+ const shannonDir = path.join(repo.hostPath, ".shannon");
1201
+ for (const dir of [
1202
+ "deliverables",
1203
+ "scratchpad",
1204
+ ".playwright-cli"
1205
+ ]) fs.mkdirSync(path.join(shannonDir, dir), { recursive: true });
1291
1206
  const credentialsPath = getCredentialsPath();
1292
1207
  const hasCredentials = fs.existsSync(credentialsPath);
1293
1208
  if (hasCredentials) process.env.GOOGLE_APPLICATION_CREDENTIALS = "/app/credentials/google-sa-key.json";
@@ -1295,7 +1210,7 @@ async function start(args) {
1295
1210
  if (outputDir) fs.mkdirSync(outputDir, { recursive: true });
1296
1211
  const promptsDir = isLocal() ? path.resolve("apps/worker/prompts") : void 0;
1297
1212
  displaySplash(isLocal() ? void 0 : args.version);
1298
- spawnWorker({
1213
+ const proc = spawnWorker({
1299
1214
  version: args.version,
1300
1215
  url: args.url,
1301
1216
  repo,
@@ -1307,12 +1222,17 @@ async function start(args) {
1307
1222
  ...hasCredentials && { credentials: credentialsPath },
1308
1223
  ...promptsDir && { promptsDir },
1309
1224
  ...outputDir && { outputDir },
1310
- ...workspace && { workspace },
1311
- ...args.pipelineTesting && { pipelineTesting: true }
1312
- }).on("error", (err) => {
1313
- console.error(`Failed to start worker: ${err.message}`);
1314
- process.exit(1);
1225
+ workspace,
1226
+ ...args.pipelineTesting && { pipelineTesting: true },
1227
+ ...args.debug && { debug: true }
1315
1228
  });
1229
+ if (await new Promise((resolve) => {
1230
+ proc.once("exit", (code) => resolve(code ?? 1));
1231
+ proc.once("error", (err) => {
1232
+ console.error(`Failed to start worker: ${err.message}`);
1233
+ resolve(1);
1234
+ });
1235
+ }) !== 0) process.exit(1);
1316
1236
  const sessionJson = path.join(workspacesDir, workspace, "session.json");
1317
1237
  const isResume = fs.existsSync(sessionJson);
1318
1238
  let initialResumeCount = 0;
@@ -1339,7 +1259,7 @@ async function start(args) {
1339
1259
  started = true;
1340
1260
  workflowId = resumeAttempts.at(-1)?.workflowId ?? session.session?.originalWorkflowId ?? "";
1341
1261
  process.stdout.write("\r\x1B[K");
1342
- printInfo(args, useRouter, workspace, workflowId, repo.hostPath, workspacesDir);
1262
+ printInfo(args, workspace, workflowId, repo.hostPath, workspacesDir);
1343
1263
  return;
1344
1264
  }
1345
1265
  } catch {}
@@ -1354,6 +1274,7 @@ async function start(args) {
1354
1274
  try {
1355
1275
  execFileSync("docker", ["stop", containerName], { stdio: "pipe" });
1356
1276
  } catch {}
1277
+ if (args.debug) printDebugHint(containerName);
1357
1278
  };
1358
1279
  process.on("SIGINT", () => {
1359
1280
  cleanup();
@@ -1365,7 +1286,14 @@ async function start(args) {
1365
1286
  });
1366
1287
  process.on("exit", cleanup);
1367
1288
  }
1368
- function printInfo(args, routerActive, workspace, workflowId, repoPath, workspacesDir) {
1289
+ function printDebugHint(containerName) {
1290
+ console.log("");
1291
+ console.log(` Worker container preserved: ${containerName}`);
1292
+ console.log(` Inspect logs: docker logs ${containerName}`);
1293
+ console.log(` Remove: docker rm ${containerName}`);
1294
+ console.log("");
1295
+ }
1296
+ function printInfo(args, workspace, workflowId, repoPath, workspacesDir) {
1369
1297
  const logsCmd = isLocal() ? `./shannon logs ${workspace}` : `npx @keygraph/shannon logs ${workspace}`;
1370
1298
  const reportsPath = path.join(workspacesDir, workspace);
1371
1299
  console.log(` Target: ${args.url}`);
@@ -1373,7 +1301,6 @@ function printInfo(args, routerActive, workspace, workflowId, repoPath, workspac
1373
1301
  console.log(` Workspace: ${workspace}`);
1374
1302
  if (args.config) console.log(` Config: ${path.resolve(args.config)}`);
1375
1303
  if (args.pipelineTesting) console.log(" Mode: Pipeline Testing");
1376
- if (routerActive) console.log(" Router: Enabled");
1377
1304
  console.log("");
1378
1305
  console.log(" Monitor:");
1379
1306
  if (workflowId) console.log(` Web UI: http://localhost:8233/namespaces/default/workflows/${workflowId}`);
@@ -1488,6 +1415,23 @@ function workspaces(version) {
1488
1415
  * in the current working directory.
1489
1416
  */
1490
1417
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
1418
+ function blockSudo() {
1419
+ const isSudo = !!process.env.SUDO_USER;
1420
+ const isRoot = process.geteuid?.() === 0;
1421
+ if (!isSudo && !isRoot) return;
1422
+ if (isSudo) {
1423
+ console.error("ERROR: Shannon must not be run with sudo.");
1424
+ console.error("Re-run this command as your normal user.");
1425
+ } else {
1426
+ console.error("ERROR: Shannon must not be run as the root user.");
1427
+ console.error("Switch to a regular user account and re-run this command.");
1428
+ }
1429
+ if (process.platform === "linux") {
1430
+ console.error("Configure Docker to run without sudo first:");
1431
+ console.error("https://docs.docker.com/engine/install/linux-postinstall");
1432
+ }
1433
+ process.exit(1);
1434
+ }
1491
1435
  function getVersion() {
1492
1436
  try {
1493
1437
  const pkgPath = path.join(__dirname, "..", "package.json");
@@ -1521,7 +1465,7 @@ Options for 'start':
1521
1465
  -o, --output <path> Copy deliverables to this directory after run
1522
1466
  -w, --workspace <name> Named workspace (auto-resumes if exists)
1523
1467
  --pipeline-testing Use minimal prompts for fast testing
1524
- --router Route requests through claude-code-router
1468
+ --debug Preserve worker container after exit for log inspection
1525
1469
 
1526
1470
  Examples:
1527
1471
  ${prefix} start -u https://example.com -r ${mode === "local" ? "my-repo" : "./my-repo"}
@@ -1541,7 +1485,7 @@ function parseStartArgs(argv) {
1541
1485
  let workspace;
1542
1486
  let output;
1543
1487
  let pipelineTesting = false;
1544
- let router = false;
1488
+ let debug = false;
1545
1489
  for (let i = 0; i < argv.length; i++) {
1546
1490
  const arg = argv[i];
1547
1491
  const next = argv[i + 1];
@@ -1584,8 +1528,8 @@ function parseStartArgs(argv) {
1584
1528
  case "--pipeline-testing":
1585
1529
  pipelineTesting = true;
1586
1530
  break;
1587
- case "--router":
1588
- router = true;
1531
+ case "--debug":
1532
+ debug = true;
1589
1533
  break;
1590
1534
  default:
1591
1535
  console.error(`Unknown option: ${arg}`);
@@ -1602,12 +1546,13 @@ function parseStartArgs(argv) {
1602
1546
  url,
1603
1547
  repo,
1604
1548
  pipelineTesting,
1605
- router,
1549
+ debug,
1606
1550
  ...config && { config },
1607
1551
  ...workspace && { workspace },
1608
1552
  ...output && { output }
1609
1553
  };
1610
1554
  }
1555
+ blockSudo();
1611
1556
  const args = process.argv.slice(2);
1612
1557
  const command = args[0];
1613
1558
  switch (command) {
package/infra/compose.yml CHANGED
@@ -19,32 +19,5 @@ services:
19
19
  retries: 10
20
20
  start_period: 30s
21
21
 
22
- router:
23
- image: node:20-slim
24
- container_name: shannon-router
25
- profiles: ["router"]
26
- command: >
27
- sh -c "apt-get update && apt-get install -y gettext-base &&
28
- npm install -g @musistudio/claude-code-router &&
29
- mkdir -p /root/.claude-code-router &&
30
- envsubst < /config/router-config.json > /root/.claude-code-router/config.json &&
31
- ccr start"
32
- ports:
33
- - "127.0.0.1:3456:3456"
34
- volumes:
35
- - ./router-config.json:/config/router-config.json:ro
36
- environment:
37
- - HOST=0.0.0.0
38
- - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
39
- - OPENAI_API_KEY=${OPENAI_API_KEY:-}
40
- - OPENROUTER_API_KEY=${OPENROUTER_API_KEY:-}
41
- - ROUTER_DEFAULT=${ROUTER_DEFAULT:-openai,gpt-4o}
42
- healthcheck:
43
- test: ["CMD", "node", "-e", "require('http').get('http://localhost:3456/health', r => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"]
44
- interval: 10s
45
- timeout: 5s
46
- retries: 5
47
- start_period: 30s
48
-
49
22
  volumes:
50
23
  temporal-data:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keygraph/shannon",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Shannon - Autonomous white-box AI pentester for web applications and APIs by Keygraph",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -1,31 +0,0 @@
1
- {
2
- "HOST": "0.0.0.0",
3
- "APIKEY": "shannon-router-key",
4
- "LOG": true,
5
- "LOG_LEVEL": "info",
6
- "NON_INTERACTIVE_MODE": true,
7
- "API_TIMEOUT_MS": 600000,
8
- "Providers": [
9
- {
10
- "name": "openai",
11
- "api_base_url": "https://api.openai.com/v1/chat/completions",
12
- "api_key": "$OPENAI_API_KEY",
13
- "models": ["gpt-5.2", "gpt-5-mini"],
14
- "transformer": {
15
- "use": [["maxcompletiontokens", { "max_completion_tokens": 16384 }]]
16
- }
17
- },
18
- {
19
- "name": "openrouter",
20
- "api_base_url": "https://openrouter.ai/api/v1/chat/completions",
21
- "api_key": "$OPENROUTER_API_KEY",
22
- "models": ["google/gemini-3-flash-preview"],
23
- "transformer": {
24
- "use": ["openrouter"]
25
- }
26
- }
27
- ],
28
- "Router": {
29
- "default": "$ROUTER_DEFAULT"
30
- }
31
- }