@chrysb/alphaclaw 0.9.8 → 0.9.10
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/lib/public/dist/app.bundle.js +1424 -1398
- package/lib/public/js/components/agents-tab/agent-overview/use-model-card.js +4 -5
- package/lib/public/js/components/agents-tab/agent-pairing-section.js +11 -5
- package/lib/public/js/components/cron-tab/cron-helpers.js +13 -1
- package/lib/public/js/components/cron-tab/use-cron-tab.js +4 -3
- package/lib/public/js/components/general/index.js +6 -1
- package/lib/public/js/components/general/use-general-tab.js +17 -20
- package/lib/public/js/components/models-tab/index.js +5 -1
- package/lib/public/js/components/models-tab/model-picker.js +52 -0
- package/lib/public/js/components/onboarding/use-welcome-pairing.js +4 -1
- package/lib/public/js/components/pairings.js +75 -4
- package/lib/public/js/hooks/usePolling.js +46 -13
- package/lib/public/js/lib/model-config.js +4 -0
- package/lib/server/agents/channels.js +53 -9
- package/lib/server/commands.js +4 -1
- package/lib/server/constants.js +5 -0
- package/lib/server/cost-utils.js +9 -0
- package/lib/server/cron-service.js +12 -1
- package/lib/server/db/doctor/index.js +9 -0
- package/lib/server/db/usage/index.js +13 -0
- package/lib/server/db/watchdog/index.js +13 -1
- package/lib/server/db/webhooks/index.js +13 -1
- package/lib/server/init/register-server-routes.js +2 -0
- package/lib/server/internal-files-migration.js +11 -1
- package/lib/server/model-catalog-cache.js +85 -6
- package/lib/server/onboarding/github.js +79 -2
- package/lib/server/openclaw-version.js +2 -1
- package/lib/server/routes/models.js +26 -0
- package/lib/server/routes/pairings.js +106 -15
- package/lib/server/utils/command-output.js +11 -0
- package/lib/setup/gitignore +2 -0
- package/package.json +2 -2
- package/patches/openclaw+2026.4.21.patch +13 -0
- package/patches/openclaw+2026.4.14.patch +0 -13
package/lib/server/commands.js
CHANGED
|
@@ -20,8 +20,11 @@ const createCommands = ({ gatewayEnv }) => {
|
|
|
20
20
|
);
|
|
21
21
|
exec(cmd, { timeout: timeoutMs, ...execOpts }, (err, stdout, stderr) => {
|
|
22
22
|
if (err) {
|
|
23
|
+
err.stdout = String(stdout || "").trim();
|
|
24
|
+
err.stderr = String(stderr || "").trim();
|
|
25
|
+
err.cmd = cmd;
|
|
23
26
|
console.error(
|
|
24
|
-
`[onboard] Error: ${(stderr || err.message).slice(0, 300)}`,
|
|
27
|
+
`[onboard] Error: ${String(stderr || err.message || "").slice(0, 300)}`,
|
|
25
28
|
);
|
|
26
29
|
return reject(err);
|
|
27
30
|
}
|
package/lib/server/constants.js
CHANGED
|
@@ -91,6 +91,11 @@ const kOnboardingModelProviders = new Set([
|
|
|
91
91
|
"vllm",
|
|
92
92
|
]);
|
|
93
93
|
const kFallbackOnboardingModels = [
|
|
94
|
+
{
|
|
95
|
+
key: "anthropic/claude-opus-4-7",
|
|
96
|
+
provider: "anthropic",
|
|
97
|
+
label: "Claude Opus 4.7",
|
|
98
|
+
},
|
|
94
99
|
{
|
|
95
100
|
key: "anthropic/claude-opus-4-6",
|
|
96
101
|
provider: "anthropic",
|
package/lib/server/cost-utils.js
CHANGED
|
@@ -5,7 +5,16 @@ const kTokensPerMillion = 1_000_000;
|
|
|
5
5
|
const kLongContextThresholdTokens = 200_000;
|
|
6
6
|
const kNodeModulesPricingCacheTtlMs = 60_000;
|
|
7
7
|
|
|
8
|
+
const kClaudeOpus47Pricing = {
|
|
9
|
+
input: 5.0,
|
|
10
|
+
output: 25.0,
|
|
11
|
+
cacheRead: 0.5,
|
|
12
|
+
cacheWrite: 6.25,
|
|
13
|
+
};
|
|
14
|
+
|
|
8
15
|
const kGlobalModelPricing = {
|
|
16
|
+
"claude-opus-4-7": kClaudeOpus47Pricing,
|
|
17
|
+
"claude-opus-4.7": kClaudeOpus47Pricing,
|
|
9
18
|
"claude-opus-4-6": {
|
|
10
19
|
input: (tokens) => (tokens > kLongContextThresholdTokens ? 10.0 : 5.0),
|
|
11
20
|
output: (tokens) => (tokens > kLongContextThresholdTokens ? 37.5 : 25.0),
|
|
@@ -485,6 +485,16 @@ const parseCommandJson = (rawOutput) => {
|
|
|
485
485
|
return null;
|
|
486
486
|
};
|
|
487
487
|
|
|
488
|
+
const resolvePromptEditFlag = ({ cronDir, jobId }) => {
|
|
489
|
+
const store = readCronStore({ cronDir });
|
|
490
|
+
const job = store.jobs.find((entry) => String(entry?.id || "") === jobId);
|
|
491
|
+
if (!job) throw new Error(`unknown cron job id: ${jobId}`);
|
|
492
|
+
const payloadKind = String(job?.payload?.kind || "").trim();
|
|
493
|
+
if (payloadKind === "systemEvent") return "--system-event";
|
|
494
|
+
if (payloadKind === "agentTurn") return "--message";
|
|
495
|
+
throw new Error(`unsupported cron payload kind: ${payloadKind || "unknown"}`);
|
|
496
|
+
};
|
|
497
|
+
|
|
488
498
|
const createCronService = ({
|
|
489
499
|
clawCmd,
|
|
490
500
|
OPENCLAW_DIR,
|
|
@@ -548,7 +558,8 @@ const createCronService = ({
|
|
|
548
558
|
|
|
549
559
|
const updateJobPrompt = async ({ jobId, message }) => {
|
|
550
560
|
const safeJobId = sanitizeCronJobId(jobId);
|
|
551
|
-
const
|
|
561
|
+
const promptFlag = resolvePromptEditFlag({ cronDir, jobId: safeJobId });
|
|
562
|
+
const command = `cron edit ${shellEscapeArg(safeJobId)} ${promptFlag} ${shellEscapeArg(message || "")}`;
|
|
552
563
|
return runCommand(command, { timeoutMs: 60000 });
|
|
553
564
|
};
|
|
554
565
|
|
|
@@ -18,6 +18,13 @@ const ensureDb = () => {
|
|
|
18
18
|
return db;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
const closeDoctorDb = () => {
|
|
22
|
+
if (!db) return;
|
|
23
|
+
const database = db;
|
|
24
|
+
db = null;
|
|
25
|
+
database.close();
|
|
26
|
+
};
|
|
27
|
+
|
|
21
28
|
const parseJsonText = (value, fallbackValue) => {
|
|
22
29
|
if (typeof value !== "string" || !value) return fallbackValue;
|
|
23
30
|
try {
|
|
@@ -171,6 +178,7 @@ const toRunModel = (row) => {
|
|
|
171
178
|
};
|
|
172
179
|
|
|
173
180
|
const initDoctorDb = ({ rootDir }) => {
|
|
181
|
+
closeDoctorDb();
|
|
174
182
|
const dbDir = path.join(rootDir, "db");
|
|
175
183
|
fs.mkdirSync(dbDir, { recursive: true });
|
|
176
184
|
const dbPath = path.join(dbDir, "doctor.db");
|
|
@@ -511,6 +519,7 @@ const updateDoctorCardStatus = ({ id, status }) => {
|
|
|
511
519
|
|
|
512
520
|
module.exports = {
|
|
513
521
|
initDoctorDb,
|
|
522
|
+
closeDoctorDb,
|
|
514
523
|
markIncompleteRunsFailed,
|
|
515
524
|
getDoctorMeta,
|
|
516
525
|
setDoctorMeta,
|
|
@@ -15,7 +15,19 @@ const ensureDb = () => {
|
|
|
15
15
|
return db;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
const closeUsageDb = () => {
|
|
19
|
+
if (!db) {
|
|
20
|
+
usageDbPath = "";
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const database = db;
|
|
24
|
+
db = null;
|
|
25
|
+
usageDbPath = "";
|
|
26
|
+
database.close();
|
|
27
|
+
};
|
|
28
|
+
|
|
18
29
|
const initUsageDb = ({ rootDir }) => {
|
|
30
|
+
closeUsageDb();
|
|
19
31
|
const dbDir = path.join(rootDir, "db");
|
|
20
32
|
fs.mkdirSync(dbDir, { recursive: true });
|
|
21
33
|
usageDbPath = path.join(dbDir, "usage.db");
|
|
@@ -155,6 +167,7 @@ const getSessionUsageByKeyPattern = ({ keyPattern = "", sinceMs = 0 } = {}) => {
|
|
|
155
167
|
|
|
156
168
|
module.exports = {
|
|
157
169
|
initUsageDb,
|
|
170
|
+
closeUsageDb,
|
|
158
171
|
getDailySummary: (options = {}) => getDailySummary({ database: ensureDb(), ...options }),
|
|
159
172
|
getSessionsList: (options = {}) => getSessionsList({ database: ensureDb(), ...options }),
|
|
160
173
|
getSessionDetail: (options = {}) => getSessionDetail({ database: ensureDb(), ...options }),
|
|
@@ -15,14 +15,25 @@ const ensureDb = () => {
|
|
|
15
15
|
return db;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
const closeWatchdogDb = () => {
|
|
19
|
+
if (pruneTimer) {
|
|
20
|
+
clearInterval(pruneTimer);
|
|
21
|
+
pruneTimer = null;
|
|
22
|
+
}
|
|
23
|
+
if (!db) return;
|
|
24
|
+
const database = db;
|
|
25
|
+
db = null;
|
|
26
|
+
database.close();
|
|
27
|
+
};
|
|
28
|
+
|
|
18
29
|
const initWatchdogDb = ({ rootDir, pruneDays = 30 }) => {
|
|
30
|
+
closeWatchdogDb();
|
|
19
31
|
const dbDir = path.join(rootDir, "db");
|
|
20
32
|
fs.mkdirSync(dbDir, { recursive: true });
|
|
21
33
|
const dbPath = path.join(dbDir, "watchdog.db");
|
|
22
34
|
db = new DatabaseSync(dbPath);
|
|
23
35
|
createSchema(db);
|
|
24
36
|
pruneWatchdogEvents(pruneDays);
|
|
25
|
-
if (pruneTimer) clearInterval(pruneTimer);
|
|
26
37
|
pruneTimer = setInterval(() => {
|
|
27
38
|
try {
|
|
28
39
|
pruneWatchdogEvents(pruneDays);
|
|
@@ -125,6 +136,7 @@ const pruneWatchdogEvents = (days = 30) => {
|
|
|
125
136
|
|
|
126
137
|
module.exports = {
|
|
127
138
|
initWatchdogDb,
|
|
139
|
+
closeWatchdogDb,
|
|
128
140
|
insertWatchdogEvent,
|
|
129
141
|
getRecentEvents,
|
|
130
142
|
pruneWatchdogEvents,
|
|
@@ -17,14 +17,25 @@ const ensureDb = () => {
|
|
|
17
17
|
return db;
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
+
const closeWebhooksDb = () => {
|
|
21
|
+
if (pruneTimer) {
|
|
22
|
+
clearInterval(pruneTimer);
|
|
23
|
+
pruneTimer = null;
|
|
24
|
+
}
|
|
25
|
+
if (!db) return;
|
|
26
|
+
const database = db;
|
|
27
|
+
db = null;
|
|
28
|
+
database.close();
|
|
29
|
+
};
|
|
30
|
+
|
|
20
31
|
const initWebhooksDb = ({ rootDir, pruneDays = 30 }) => {
|
|
32
|
+
closeWebhooksDb();
|
|
21
33
|
const dbDir = path.join(rootDir, "db");
|
|
22
34
|
fs.mkdirSync(dbDir, { recursive: true });
|
|
23
35
|
const dbPath = path.join(dbDir, "webhooks.db");
|
|
24
36
|
db = new DatabaseSync(dbPath);
|
|
25
37
|
createSchema(db);
|
|
26
38
|
pruneOldEntries(pruneDays);
|
|
27
|
-
if (pruneTimer) clearInterval(pruneTimer);
|
|
28
39
|
pruneTimer = setInterval(() => {
|
|
29
40
|
try {
|
|
30
41
|
pruneOldEntries(pruneDays);
|
|
@@ -413,6 +424,7 @@ const pruneOldEntries = (days = 30) => {
|
|
|
413
424
|
|
|
414
425
|
module.exports = {
|
|
415
426
|
initWebhooksDb,
|
|
427
|
+
closeWebhooksDb,
|
|
416
428
|
insertRequest,
|
|
417
429
|
getRequests,
|
|
418
430
|
getRequestById,
|
|
@@ -9,6 +9,16 @@ const kOpenclawGitignoreHookEntries = [
|
|
|
9
9
|
"!hooks/transforms/**",
|
|
10
10
|
];
|
|
11
11
|
|
|
12
|
+
const kOpenclawGitignoreCronRuntimeEntries = [
|
|
13
|
+
"# OpenClaw cron runtime state (local only; job definitions stay in cron/jobs.json)",
|
|
14
|
+
"cron/jobs-state.json",
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const kOpenclawGitignoreAppendEntries = [
|
|
18
|
+
...kOpenclawGitignoreHookEntries,
|
|
19
|
+
...kOpenclawGitignoreCronRuntimeEntries,
|
|
20
|
+
];
|
|
21
|
+
|
|
12
22
|
const buildManagedPaths = ({ openclawDir, pathModule = path }) => {
|
|
13
23
|
const internalDir = pathModule.join(openclawDir, kInternalDirName);
|
|
14
24
|
return {
|
|
@@ -89,7 +99,7 @@ const migrateManagedInternalFiles = ({
|
|
|
89
99
|
const raw = String(fs.readFileSync(gitignorePath, "utf8") || "");
|
|
90
100
|
const existingLines = raw.split(/\r?\n/);
|
|
91
101
|
const existingSet = new Set(existingLines.map((line) => line.trim()));
|
|
92
|
-
const missing =
|
|
102
|
+
const missing = kOpenclawGitignoreAppendEntries.filter(
|
|
93
103
|
(line) => !existingSet.has(line),
|
|
94
104
|
);
|
|
95
105
|
if (missing.length) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const { ALPHACLAW_DIR, kFallbackOnboardingModels } = require("./constants");
|
|
4
|
+
const { getCommandOutputCandidates } = require("./utils/command-output");
|
|
4
5
|
|
|
5
6
|
const kModelCatalogCacheVersion = 1;
|
|
6
7
|
const kModelCatalogRefreshBackoffMs = 30 * 1000;
|
|
@@ -21,6 +22,12 @@ const createResponse = ({
|
|
|
21
22
|
models,
|
|
22
23
|
});
|
|
23
24
|
|
|
25
|
+
const normalizeOpenclawVersion = (value) => {
|
|
26
|
+
if (typeof value !== "string") return null;
|
|
27
|
+
const normalized = value.trim();
|
|
28
|
+
return normalized || null;
|
|
29
|
+
};
|
|
30
|
+
|
|
24
31
|
const normalizeCachedModels = ({
|
|
25
32
|
models,
|
|
26
33
|
normalizeOnboardingModels = (items) => items,
|
|
@@ -48,10 +55,20 @@ const normalizeCacheEntry = ({
|
|
|
48
55
|
return {
|
|
49
56
|
version: kModelCatalogCacheVersion,
|
|
50
57
|
fetchedAt,
|
|
58
|
+
openclawVersion: normalizeOpenclawVersion(raw.openclawVersion),
|
|
51
59
|
models,
|
|
52
60
|
};
|
|
53
61
|
};
|
|
54
62
|
|
|
63
|
+
const parseCatalogModelsFromOutput = ({
|
|
64
|
+
rawOutput,
|
|
65
|
+
parseJsonFromNoisyOutput = () => ({}),
|
|
66
|
+
normalizeOnboardingModels = (items) => items,
|
|
67
|
+
} = {}) => {
|
|
68
|
+
const parsed = parseJsonFromNoisyOutput(rawOutput);
|
|
69
|
+
return normalizeOnboardingModels(parsed?.models || []);
|
|
70
|
+
};
|
|
71
|
+
|
|
55
72
|
const createModelCatalogCache = ({
|
|
56
73
|
fsModule = fs,
|
|
57
74
|
pathModule = path,
|
|
@@ -59,6 +76,7 @@ const createModelCatalogCache = ({
|
|
|
59
76
|
gatewayEnv = () => ({}),
|
|
60
77
|
parseJsonFromNoisyOutput = () => ({}),
|
|
61
78
|
normalizeOnboardingModels = (items) => items,
|
|
79
|
+
readOpenclawVersion = () => null,
|
|
62
80
|
fallbackModels = kFallbackOnboardingModels,
|
|
63
81
|
cachePath = kDefaultCachePath,
|
|
64
82
|
refreshBackoffMs = kModelCatalogRefreshBackoffMs,
|
|
@@ -74,6 +92,23 @@ const createModelCatalogCache = ({
|
|
|
74
92
|
let retryTimer = null;
|
|
75
93
|
let backoffUntilMs = 0;
|
|
76
94
|
|
|
95
|
+
const readCurrentOpenclawVersion = ({ refresh = false } = {}) => {
|
|
96
|
+
try {
|
|
97
|
+
return normalizeOpenclawVersion(readOpenclawVersion({ refresh }));
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const isCompatibleWithCurrentOpenclaw = ({
|
|
104
|
+
entry,
|
|
105
|
+
currentOpenclawVersion,
|
|
106
|
+
} = {}) => {
|
|
107
|
+
if (!entry) return false;
|
|
108
|
+
if (!currentOpenclawVersion) return true;
|
|
109
|
+
return entry.openclawVersion === currentOpenclawVersion;
|
|
110
|
+
};
|
|
111
|
+
|
|
77
112
|
const clearRetryTimer = () => {
|
|
78
113
|
if (!retryTimer) return;
|
|
79
114
|
clearTimeoutFn(retryTimer);
|
|
@@ -121,22 +156,51 @@ const createModelCatalogCache = ({
|
|
|
121
156
|
};
|
|
122
157
|
|
|
123
158
|
const loadFreshCatalog = async () => {
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
159
|
+
const openclawVersion = readCurrentOpenclawVersion({ refresh: true });
|
|
160
|
+
let models = [];
|
|
161
|
+
let recoveredFromCommandError = false;
|
|
162
|
+
try {
|
|
163
|
+
const output = await shellCmd("openclaw models list --all --json", {
|
|
164
|
+
env: gatewayEnv(),
|
|
165
|
+
timeout: 30000,
|
|
166
|
+
});
|
|
167
|
+
models = parseCatalogModelsFromOutput({
|
|
168
|
+
rawOutput: output,
|
|
169
|
+
parseJsonFromNoisyOutput,
|
|
170
|
+
normalizeOnboardingModels,
|
|
171
|
+
});
|
|
172
|
+
} catch (err) {
|
|
173
|
+
for (const rawOutput of getCommandOutputCandidates(err)) {
|
|
174
|
+
models = parseCatalogModelsFromOutput({
|
|
175
|
+
rawOutput,
|
|
176
|
+
parseJsonFromNoisyOutput,
|
|
177
|
+
normalizeOnboardingModels,
|
|
178
|
+
});
|
|
179
|
+
if (models.length > 0) {
|
|
180
|
+
recoveredFromCommandError = true;
|
|
181
|
+
logger.warn?.(
|
|
182
|
+
`[models] Recovered model catalog from failed command output: ${err.message || String(err)}`,
|
|
183
|
+
);
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (models.length === 0) throw err;
|
|
188
|
+
}
|
|
130
189
|
if (models.length === 0) {
|
|
131
190
|
throw new Error("No models found");
|
|
132
191
|
}
|
|
133
192
|
const entry = {
|
|
134
193
|
version: kModelCatalogCacheVersion,
|
|
135
194
|
fetchedAt: now(),
|
|
195
|
+
openclawVersion,
|
|
136
196
|
models,
|
|
137
197
|
};
|
|
138
198
|
writeDiskCache(entry);
|
|
139
199
|
setCacheEntry(entry, { fresh: true });
|
|
200
|
+
if (recoveredFromCommandError) {
|
|
201
|
+
backoffUntilMs = 0;
|
|
202
|
+
clearRetryTimer();
|
|
203
|
+
}
|
|
140
204
|
return entry;
|
|
141
205
|
};
|
|
142
206
|
|
|
@@ -190,6 +254,21 @@ const createModelCatalogCache = ({
|
|
|
190
254
|
return {
|
|
191
255
|
async getCatalogResponse() {
|
|
192
256
|
readDiskCache();
|
|
257
|
+
if (memoryCache && !cacheIsStale) {
|
|
258
|
+
const currentOpenclawVersion = readCurrentOpenclawVersion({
|
|
259
|
+
refresh: true,
|
|
260
|
+
});
|
|
261
|
+
if (
|
|
262
|
+
!isCompatibleWithCurrentOpenclaw({
|
|
263
|
+
entry: memoryCache,
|
|
264
|
+
currentOpenclawVersion,
|
|
265
|
+
})
|
|
266
|
+
) {
|
|
267
|
+
cacheIsStale = true;
|
|
268
|
+
backoffUntilMs = 0;
|
|
269
|
+
clearRetryTimer();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
193
272
|
if (memoryCache && !cacheIsStale) {
|
|
194
273
|
return createResponse({
|
|
195
274
|
source: "openclaw",
|
|
@@ -113,6 +113,42 @@ const findOwnedRepoByName = async ({
|
|
|
113
113
|
return null;
|
|
114
114
|
};
|
|
115
115
|
|
|
116
|
+
const findAccessibleOrgByLogin = async ({ repoOwner, ghHeaders }) => {
|
|
117
|
+
const normalizedRepoOwner = String(repoOwner || "").trim().toLowerCase();
|
|
118
|
+
if (!normalizedRepoOwner) return null;
|
|
119
|
+
|
|
120
|
+
let nextUrl = "https://api.github.com/user/orgs?per_page=100&page=1";
|
|
121
|
+
while (nextUrl) {
|
|
122
|
+
const res = await fetch(nextUrl, { headers: ghHeaders });
|
|
123
|
+
if (!res.ok) {
|
|
124
|
+
const details = await parseGithubErrorMessage(res);
|
|
125
|
+
return {
|
|
126
|
+
error:
|
|
127
|
+
`Cannot verify organization "${repoOwner}" access: ${details}. ` +
|
|
128
|
+
"Check the owner name or use a token that can access that organization.",
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const orgs = await res.json();
|
|
133
|
+
if (!Array.isArray(orgs)) {
|
|
134
|
+
return {
|
|
135
|
+
error: `Cannot verify organization "${repoOwner}" access from GitHub response.`,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const org = orgs.find(
|
|
140
|
+
(item) =>
|
|
141
|
+
String(item?.login || "").trim().toLowerCase() ===
|
|
142
|
+
normalizedRepoOwner,
|
|
143
|
+
);
|
|
144
|
+
if (org) return { org };
|
|
145
|
+
|
|
146
|
+
nextUrl = getNextGithubPageUrl(res.headers?.get?.("link"));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return null;
|
|
150
|
+
};
|
|
151
|
+
|
|
116
152
|
const isClassicPat = (token) => String(token || "").startsWith("ghp_");
|
|
117
153
|
const isFineGrainedPat = (token) =>
|
|
118
154
|
String(token || "").startsWith("github_pat_");
|
|
@@ -187,7 +223,43 @@ const verifyGithubRepoForOnboarding = async ({
|
|
|
187
223
|
error: `Repository "${repoUrl}" not found. Check the repo name and token permissions.`,
|
|
188
224
|
};
|
|
189
225
|
}
|
|
190
|
-
|
|
226
|
+
if (!viewerLogin) {
|
|
227
|
+
return {
|
|
228
|
+
ok: false,
|
|
229
|
+
status: 400,
|
|
230
|
+
error: "Cannot verify GitHub account owner for this token.",
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
if (repoOwner.toLowerCase() !== viewerLogin.toLowerCase()) {
|
|
234
|
+
const orgLookup = await findAccessibleOrgByLogin({
|
|
235
|
+
repoOwner,
|
|
236
|
+
ghHeaders,
|
|
237
|
+
});
|
|
238
|
+
if (!orgLookup?.org) {
|
|
239
|
+
return {
|
|
240
|
+
ok: false,
|
|
241
|
+
status: 400,
|
|
242
|
+
error:
|
|
243
|
+
orgLookup?.error ||
|
|
244
|
+
`Repository owner "${repoOwner}" does not match the authenticated GitHub user "${viewerLogin}" ` +
|
|
245
|
+
"and was not found in the token's accessible organizations. Check the owner name or use a token that can create repositories for that organization.",
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
ok: true,
|
|
250
|
+
repoExists: false,
|
|
251
|
+
repoIsEmpty: false,
|
|
252
|
+
createOwnerType: "org",
|
|
253
|
+
viewerLogin,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
ok: true,
|
|
258
|
+
repoExists: false,
|
|
259
|
+
repoIsEmpty: false,
|
|
260
|
+
createOwnerType: "user",
|
|
261
|
+
viewerLogin,
|
|
262
|
+
};
|
|
191
263
|
}
|
|
192
264
|
if (checkRes.ok) {
|
|
193
265
|
const commitsRes = await fetch(
|
|
@@ -250,6 +322,7 @@ const ensureGithubRepoAccessible = async ({
|
|
|
250
322
|
githubToken,
|
|
251
323
|
}) => {
|
|
252
324
|
const ghHeaders = buildGithubHeaders(githubToken);
|
|
325
|
+
const [repoOwner = ""] = String(repoUrl || "").split("/");
|
|
253
326
|
const verification = await verifyGithubRepoForOnboarding({
|
|
254
327
|
repoUrl,
|
|
255
328
|
githubToken,
|
|
@@ -262,7 +335,11 @@ const ensureGithubRepoAccessible = async ({
|
|
|
262
335
|
|
|
263
336
|
try {
|
|
264
337
|
console.log(`[onboard] Creating repo ${repoUrl}...`);
|
|
265
|
-
const
|
|
338
|
+
const createUrl =
|
|
339
|
+
verification.createOwnerType === "org"
|
|
340
|
+
? `https://api.github.com/orgs/${repoOwner}/repos`
|
|
341
|
+
: "https://api.github.com/user/repos";
|
|
342
|
+
const createRes = await fetch(createUrl, {
|
|
266
343
|
method: "POST",
|
|
267
344
|
headers: { ...ghHeaders, "Content-Type": "application/json" },
|
|
268
345
|
body: JSON.stringify({
|
|
@@ -24,9 +24,10 @@ const createOpenclawVersionService = ({
|
|
|
24
24
|
};
|
|
25
25
|
let kOpenclawUpdateInProgress = false;
|
|
26
26
|
|
|
27
|
-
const readOpenclawVersion = () => {
|
|
27
|
+
const readOpenclawVersion = ({ refresh = false } = {}) => {
|
|
28
28
|
const now = Date.now();
|
|
29
29
|
if (
|
|
30
|
+
!refresh &&
|
|
30
31
|
kOpenclawVersionCache.value &&
|
|
31
32
|
now - kOpenclawVersionCache.fetchedAt < kVersionCacheTtlMs
|
|
32
33
|
) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { kFallbackOnboardingModels } = require("../constants");
|
|
2
2
|
const { createModelCatalogCache } = require("../model-catalog-cache");
|
|
3
|
+
const { getCommandOutputCandidates } = require("../utils/command-output");
|
|
3
4
|
|
|
4
5
|
const runModelsGitSync = async (shellCmd) => {
|
|
5
6
|
if (typeof shellCmd !== "function") return null;
|
|
@@ -13,12 +14,24 @@ const runModelsGitSync = async (shellCmd) => {
|
|
|
13
14
|
}
|
|
14
15
|
};
|
|
15
16
|
|
|
17
|
+
const parseJsonFromShellError = ({
|
|
18
|
+
error,
|
|
19
|
+
parseJsonFromNoisyOutput = () => null,
|
|
20
|
+
} = {}) => {
|
|
21
|
+
for (const rawOutput of getCommandOutputCandidates(error)) {
|
|
22
|
+
const parsed = parseJsonFromNoisyOutput(rawOutput);
|
|
23
|
+
if (parsed) return parsed;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
};
|
|
27
|
+
|
|
16
28
|
const registerModelRoutes = ({
|
|
17
29
|
app,
|
|
18
30
|
shellCmd,
|
|
19
31
|
gatewayEnv,
|
|
20
32
|
parseJsonFromNoisyOutput,
|
|
21
33
|
normalizeOnboardingModels,
|
|
34
|
+
readOpenclawVersion,
|
|
22
35
|
authProfiles,
|
|
23
36
|
readEnvFile,
|
|
24
37
|
writeEnvFile,
|
|
@@ -28,6 +41,7 @@ const registerModelRoutes = ({
|
|
|
28
41
|
gatewayEnv,
|
|
29
42
|
parseJsonFromNoisyOutput,
|
|
30
43
|
normalizeOnboardingModels,
|
|
44
|
+
readOpenclawVersion,
|
|
31
45
|
fallbackModels: kFallbackOnboardingModels,
|
|
32
46
|
}),
|
|
33
47
|
}) => {
|
|
@@ -180,6 +194,18 @@ const registerModelRoutes = ({
|
|
|
180
194
|
imageModel: parsed.imageModel || null,
|
|
181
195
|
});
|
|
182
196
|
} catch (err) {
|
|
197
|
+
const parsed = parseJsonFromShellError({
|
|
198
|
+
error: err,
|
|
199
|
+
parseJsonFromNoisyOutput,
|
|
200
|
+
});
|
|
201
|
+
if (parsed) {
|
|
202
|
+
return res.json({
|
|
203
|
+
ok: true,
|
|
204
|
+
modelKey: parsed.resolvedDefault || parsed.defaultModel || null,
|
|
205
|
+
fallbacks: parsed.fallbacks || [],
|
|
206
|
+
imageModel: parsed.imageModel || null,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
183
209
|
res.json({
|
|
184
210
|
ok: false,
|
|
185
211
|
error: err.message || "Failed to read model status",
|