@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 +140 -195
- package/infra/compose.yml +0 -27
- package/package.json +1 -1
- package/infra/router-config.json +0 -31
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
|
|
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(
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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
|
-
|
|
179
|
+
args.push("--workspace", opts.workspace);
|
|
210
180
|
if (opts.pipelineTesting) args.push("--pipeline-testing");
|
|
211
181
|
return spawn("docker", args, {
|
|
212
|
-
stdio:
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
667
|
-
const
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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(
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
|
828
|
-
function getTomlValue(config,
|
|
829
|
-
const [section, key] =
|
|
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")
|
|
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
|
|
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
|
-
"
|
|
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
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
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(
|
|
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
|
-
|
|
1311
|
-
...args.pipelineTesting && { pipelineTesting: true }
|
|
1312
|
-
|
|
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,
|
|
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
|
|
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
|
-
--
|
|
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
|
|
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 "--
|
|
1588
|
-
|
|
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
|
-
|
|
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
package/infra/router-config.json
DELETED
|
@@ -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
|
-
}
|