@keygraph/shannon 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +84 -186
- 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);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
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);
|
|
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;
|
|
128
98
|
}
|
|
129
|
-
|
|
130
|
-
process.exit(1);
|
|
99
|
+
await setTimeout$1(2e3);
|
|
131
100
|
}
|
|
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,11 +417,6 @@ 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
|
});
|
|
@@ -465,7 +432,6 @@ async function setupProvider(provider) {
|
|
|
465
432
|
case "custom_base_url": return setupCustomBaseUrl();
|
|
466
433
|
case "bedrock": return setupBedrock();
|
|
467
434
|
case "vertex": return setupVertex();
|
|
468
|
-
case "router": return setupRouter();
|
|
469
435
|
}
|
|
470
436
|
}
|
|
471
437
|
async function setupAnthropic() {
|
|
@@ -663,49 +629,6 @@ async function setupVertex() {
|
|
|
663
629
|
}
|
|
664
630
|
};
|
|
665
631
|
}
|
|
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
|
-
}]
|
|
676
|
-
});
|
|
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 };
|
|
708
|
-
}
|
|
709
632
|
async function promptSecret(message) {
|
|
710
633
|
const value = await p.password({
|
|
711
634
|
message,
|
|
@@ -793,21 +716,6 @@ const CONFIG_MAP = [
|
|
|
793
716
|
toml: "custom_base_url.auth_token",
|
|
794
717
|
type: "string"
|
|
795
718
|
},
|
|
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
719
|
{
|
|
812
720
|
env: "ANTHROPIC_SMALL_MODEL",
|
|
813
721
|
toml: "models.small",
|
|
@@ -906,13 +814,6 @@ function validateProviderFields(config, provider, errors) {
|
|
|
906
814
|
validateModelTiers(config, "vertex", errors);
|
|
907
815
|
break;
|
|
908
816
|
}
|
|
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
817
|
}
|
|
917
818
|
}
|
|
918
819
|
/** Bedrock and Vertex require a [models] section with all three tiers. */
|
|
@@ -965,8 +866,7 @@ function validateConfig(config) {
|
|
|
965
866
|
"anthropic",
|
|
966
867
|
"custom_base_url",
|
|
967
868
|
"bedrock",
|
|
968
|
-
"vertex"
|
|
969
|
-
"router"
|
|
869
|
+
"vertex"
|
|
970
870
|
].filter((s) => {
|
|
971
871
|
const section = config[s];
|
|
972
872
|
return section && typeof section === "object" && Object.keys(section).length > 0;
|
|
@@ -1014,7 +914,6 @@ const FORWARD_VARS = [
|
|
|
1014
914
|
"ANTHROPIC_API_KEY",
|
|
1015
915
|
"ANTHROPIC_BASE_URL",
|
|
1016
916
|
"ANTHROPIC_AUTH_TOKEN",
|
|
1017
|
-
"ROUTER_DEFAULT",
|
|
1018
917
|
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
1019
918
|
"CLAUDE_CODE_USE_BEDROCK",
|
|
1020
919
|
"AWS_REGION",
|
|
@@ -1026,9 +925,7 @@ const FORWARD_VARS = [
|
|
|
1026
925
|
"ANTHROPIC_SMALL_MODEL",
|
|
1027
926
|
"ANTHROPIC_MEDIUM_MODEL",
|
|
1028
927
|
"ANTHROPIC_LARGE_MODEL",
|
|
1029
|
-
"CLAUDE_CODE_MAX_OUTPUT_TOKENS"
|
|
1030
|
-
"OPENAI_API_KEY",
|
|
1031
|
-
"OPENROUTER_API_KEY"
|
|
928
|
+
"CLAUDE_CODE_MAX_OUTPUT_TOKENS"
|
|
1032
929
|
];
|
|
1033
930
|
/**
|
|
1034
931
|
* Load credentials into process.env.
|
|
@@ -1054,10 +951,6 @@ function buildEnvFlags() {
|
|
|
1054
951
|
}
|
|
1055
952
|
return flags;
|
|
1056
953
|
}
|
|
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
954
|
/** Check if a custom Anthropic-compatible base URL is configured. */
|
|
1062
955
|
function isCustomBaseUrlConfigured() {
|
|
1063
956
|
return !!(process.env.ANTHROPIC_BASE_URL && process.env.ANTHROPIC_AUTH_TOKEN);
|
|
@@ -1070,7 +963,6 @@ function detectProviders() {
|
|
|
1070
963
|
if (isCustomBaseUrlConfigured()) providers.push("Custom Base URL");
|
|
1071
964
|
if (process.env.CLAUDE_CODE_USE_BEDROCK === "1") providers.push("AWS Bedrock");
|
|
1072
965
|
if (process.env.CLAUDE_CODE_USE_VERTEX === "1") providers.push("Google Vertex");
|
|
1073
|
-
if (isRouterConfigured()) providers.push("Router");
|
|
1074
966
|
return providers;
|
|
1075
967
|
}
|
|
1076
968
|
/**
|
|
@@ -1091,13 +983,10 @@ function validateCredentials() {
|
|
|
1091
983
|
valid: true,
|
|
1092
984
|
mode: "oauth"
|
|
1093
985
|
};
|
|
1094
|
-
if (isCustomBaseUrlConfigured()) {
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
mode: "custom-base-url"
|
|
1099
|
-
};
|
|
1100
|
-
}
|
|
986
|
+
if (isCustomBaseUrlConfigured()) return {
|
|
987
|
+
valid: true,
|
|
988
|
+
mode: "custom-base-url"
|
|
989
|
+
};
|
|
1101
990
|
if (process.env.CLAUDE_CODE_USE_BEDROCK === "1") {
|
|
1102
991
|
const missing = [];
|
|
1103
992
|
if (!process.env.AWS_REGION) missing.push("AWS_REGION");
|
|
@@ -1137,13 +1026,6 @@ function validateCredentials() {
|
|
|
1137
1026
|
mode: "vertex"
|
|
1138
1027
|
};
|
|
1139
1028
|
}
|
|
1140
|
-
if (isRouterConfigured()) {
|
|
1141
|
-
process.env.ANTHROPIC_API_KEY = "router-mode";
|
|
1142
|
-
return {
|
|
1143
|
-
valid: true,
|
|
1144
|
-
mode: "router"
|
|
1145
|
-
};
|
|
1146
|
-
}
|
|
1147
1029
|
return {
|
|
1148
1030
|
valid: false,
|
|
1149
1031
|
mode: "api-key",
|
|
@@ -1207,14 +1089,6 @@ function resolveConfig(configArg) {
|
|
|
1207
1089
|
containerPath: `/app/configs/${path.basename(hostPath)}`
|
|
1208
1090
|
};
|
|
1209
1091
|
}
|
|
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
1092
|
//#endregion
|
|
1219
1093
|
//#region src/splash.ts
|
|
1220
1094
|
/**
|
|
@@ -1271,23 +1145,35 @@ async function start(args) {
|
|
|
1271
1145
|
console.error(`ERROR: ${creds.error}`);
|
|
1272
1146
|
process.exit(1);
|
|
1273
1147
|
}
|
|
1274
|
-
const useRouter = args.router || isRouterConfigured();
|
|
1275
1148
|
const repo = resolveRepo(args.repo);
|
|
1276
1149
|
const config = args.config ? resolveConfig(args.config) : void 0;
|
|
1277
|
-
ensureDeliverables(repo.hostPath);
|
|
1278
1150
|
const workspacesDir = getWorkspacesDir();
|
|
1279
1151
|
fs.mkdirSync(workspacesDir, { recursive: true });
|
|
1280
1152
|
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
1153
|
ensureImage(args.version);
|
|
1286
|
-
await ensureInfra(
|
|
1154
|
+
await ensureInfra();
|
|
1287
1155
|
const suffix = randomSuffix();
|
|
1288
1156
|
const taskQueue = `shannon-${suffix}`;
|
|
1289
1157
|
const containerName = `shannon-worker-${suffix}`;
|
|
1290
1158
|
const workspace = args.workspace ?? `${new URL(args.url).hostname.replace(/[^a-zA-Z0-9-]/g, "-")}_shannon-${Date.now()}`;
|
|
1159
|
+
const workspacePath = path.join(workspacesDir, workspace);
|
|
1160
|
+
fs.mkdirSync(workspacePath, { recursive: true });
|
|
1161
|
+
fs.chmodSync(workspacePath, 511);
|
|
1162
|
+
for (const dir of [
|
|
1163
|
+
"deliverables",
|
|
1164
|
+
"scratchpad",
|
|
1165
|
+
".playwright-cli"
|
|
1166
|
+
]) {
|
|
1167
|
+
const dirPath = path.join(workspacePath, dir);
|
|
1168
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
1169
|
+
fs.chmodSync(dirPath, 511);
|
|
1170
|
+
}
|
|
1171
|
+
const shannonDir = path.join(repo.hostPath, ".shannon");
|
|
1172
|
+
for (const dir of [
|
|
1173
|
+
"deliverables",
|
|
1174
|
+
"scratchpad",
|
|
1175
|
+
".playwright-cli"
|
|
1176
|
+
]) fs.mkdirSync(path.join(shannonDir, dir), { recursive: true });
|
|
1291
1177
|
const credentialsPath = getCredentialsPath();
|
|
1292
1178
|
const hasCredentials = fs.existsSync(credentialsPath);
|
|
1293
1179
|
if (hasCredentials) process.env.GOOGLE_APPLICATION_CREDENTIALS = "/app/credentials/google-sa-key.json";
|
|
@@ -1295,7 +1181,7 @@ async function start(args) {
|
|
|
1295
1181
|
if (outputDir) fs.mkdirSync(outputDir, { recursive: true });
|
|
1296
1182
|
const promptsDir = isLocal() ? path.resolve("apps/worker/prompts") : void 0;
|
|
1297
1183
|
displaySplash(isLocal() ? void 0 : args.version);
|
|
1298
|
-
spawnWorker({
|
|
1184
|
+
const proc = spawnWorker({
|
|
1299
1185
|
version: args.version,
|
|
1300
1186
|
url: args.url,
|
|
1301
1187
|
repo,
|
|
@@ -1307,12 +1193,17 @@ async function start(args) {
|
|
|
1307
1193
|
...hasCredentials && { credentials: credentialsPath },
|
|
1308
1194
|
...promptsDir && { promptsDir },
|
|
1309
1195
|
...outputDir && { outputDir },
|
|
1310
|
-
|
|
1311
|
-
...args.pipelineTesting && { pipelineTesting: true }
|
|
1312
|
-
|
|
1313
|
-
console.error(`Failed to start worker: ${err.message}`);
|
|
1314
|
-
process.exit(1);
|
|
1196
|
+
workspace,
|
|
1197
|
+
...args.pipelineTesting && { pipelineTesting: true },
|
|
1198
|
+
...args.debug && { debug: true }
|
|
1315
1199
|
});
|
|
1200
|
+
if (await new Promise((resolve) => {
|
|
1201
|
+
proc.once("exit", (code) => resolve(code ?? 1));
|
|
1202
|
+
proc.once("error", (err) => {
|
|
1203
|
+
console.error(`Failed to start worker: ${err.message}`);
|
|
1204
|
+
resolve(1);
|
|
1205
|
+
});
|
|
1206
|
+
}) !== 0) process.exit(1);
|
|
1316
1207
|
const sessionJson = path.join(workspacesDir, workspace, "session.json");
|
|
1317
1208
|
const isResume = fs.existsSync(sessionJson);
|
|
1318
1209
|
let initialResumeCount = 0;
|
|
@@ -1339,7 +1230,7 @@ async function start(args) {
|
|
|
1339
1230
|
started = true;
|
|
1340
1231
|
workflowId = resumeAttempts.at(-1)?.workflowId ?? session.session?.originalWorkflowId ?? "";
|
|
1341
1232
|
process.stdout.write("\r\x1B[K");
|
|
1342
|
-
printInfo(args,
|
|
1233
|
+
printInfo(args, workspace, workflowId, repo.hostPath, workspacesDir);
|
|
1343
1234
|
return;
|
|
1344
1235
|
}
|
|
1345
1236
|
} catch {}
|
|
@@ -1354,6 +1245,7 @@ async function start(args) {
|
|
|
1354
1245
|
try {
|
|
1355
1246
|
execFileSync("docker", ["stop", containerName], { stdio: "pipe" });
|
|
1356
1247
|
} catch {}
|
|
1248
|
+
if (args.debug) printDebugHint(containerName);
|
|
1357
1249
|
};
|
|
1358
1250
|
process.on("SIGINT", () => {
|
|
1359
1251
|
cleanup();
|
|
@@ -1365,7 +1257,14 @@ async function start(args) {
|
|
|
1365
1257
|
});
|
|
1366
1258
|
process.on("exit", cleanup);
|
|
1367
1259
|
}
|
|
1368
|
-
function
|
|
1260
|
+
function printDebugHint(containerName) {
|
|
1261
|
+
console.log("");
|
|
1262
|
+
console.log(` Worker container preserved: ${containerName}`);
|
|
1263
|
+
console.log(` Inspect logs: docker logs ${containerName}`);
|
|
1264
|
+
console.log(` Remove: docker rm ${containerName}`);
|
|
1265
|
+
console.log("");
|
|
1266
|
+
}
|
|
1267
|
+
function printInfo(args, workspace, workflowId, repoPath, workspacesDir) {
|
|
1369
1268
|
const logsCmd = isLocal() ? `./shannon logs ${workspace}` : `npx @keygraph/shannon logs ${workspace}`;
|
|
1370
1269
|
const reportsPath = path.join(workspacesDir, workspace);
|
|
1371
1270
|
console.log(` Target: ${args.url}`);
|
|
@@ -1373,7 +1272,6 @@ function printInfo(args, routerActive, workspace, workflowId, repoPath, workspac
|
|
|
1373
1272
|
console.log(` Workspace: ${workspace}`);
|
|
1374
1273
|
if (args.config) console.log(` Config: ${path.resolve(args.config)}`);
|
|
1375
1274
|
if (args.pipelineTesting) console.log(" Mode: Pipeline Testing");
|
|
1376
|
-
if (routerActive) console.log(" Router: Enabled");
|
|
1377
1275
|
console.log("");
|
|
1378
1276
|
console.log(" Monitor:");
|
|
1379
1277
|
if (workflowId) console.log(` Web UI: http://localhost:8233/namespaces/default/workflows/${workflowId}`);
|
|
@@ -1521,7 +1419,7 @@ Options for 'start':
|
|
|
1521
1419
|
-o, --output <path> Copy deliverables to this directory after run
|
|
1522
1420
|
-w, --workspace <name> Named workspace (auto-resumes if exists)
|
|
1523
1421
|
--pipeline-testing Use minimal prompts for fast testing
|
|
1524
|
-
--
|
|
1422
|
+
--debug Preserve worker container after exit for log inspection
|
|
1525
1423
|
|
|
1526
1424
|
Examples:
|
|
1527
1425
|
${prefix} start -u https://example.com -r ${mode === "local" ? "my-repo" : "./my-repo"}
|
|
@@ -1541,7 +1439,7 @@ function parseStartArgs(argv) {
|
|
|
1541
1439
|
let workspace;
|
|
1542
1440
|
let output;
|
|
1543
1441
|
let pipelineTesting = false;
|
|
1544
|
-
let
|
|
1442
|
+
let debug = false;
|
|
1545
1443
|
for (let i = 0; i < argv.length; i++) {
|
|
1546
1444
|
const arg = argv[i];
|
|
1547
1445
|
const next = argv[i + 1];
|
|
@@ -1584,8 +1482,8 @@ function parseStartArgs(argv) {
|
|
|
1584
1482
|
case "--pipeline-testing":
|
|
1585
1483
|
pipelineTesting = true;
|
|
1586
1484
|
break;
|
|
1587
|
-
case "--
|
|
1588
|
-
|
|
1485
|
+
case "--debug":
|
|
1486
|
+
debug = true;
|
|
1589
1487
|
break;
|
|
1590
1488
|
default:
|
|
1591
1489
|
console.error(`Unknown option: ${arg}`);
|
|
@@ -1602,7 +1500,7 @@ function parseStartArgs(argv) {
|
|
|
1602
1500
|
url,
|
|
1603
1501
|
repo,
|
|
1604
1502
|
pipelineTesting,
|
|
1605
|
-
|
|
1503
|
+
debug,
|
|
1606
1504
|
...config && { config },
|
|
1607
1505
|
...workspace && { workspace },
|
|
1608
1506
|
...output && { output }
|
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
|
-
}
|