@chrysb/alphaclaw 0.9.27 → 0.9.29
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/README.md +11 -4
- package/bin/alphaclaw.js +59 -0
- package/lib/public/dist/app.bundle.js +1143 -1129
- package/lib/public/js/components/doctor/findings-list.js +18 -1
- package/lib/public/js/components/doctor/fix-card-modal.js +8 -17
- package/lib/public/js/components/doctor/helpers.js +7 -1
- package/lib/public/js/components/doctor/index.js +1 -1
- package/lib/public/js/components/onboarding/welcome-config.js +6 -2
- package/lib/public/js/components/onboarding/welcome-setup-step.js +58 -4
- package/lib/public/js/components/welcome/use-welcome.js +15 -8
- package/lib/public/js/lib/api.js +7 -2
- package/lib/public/js/lib/model-config.js +30 -8
- package/lib/server/db/doctor/index.js +94 -5
- package/lib/server/db/doctor/schema.js +4 -0
- package/lib/server/doctor/constants.js +1 -0
- package/lib/server/doctor/fix-completion.js +6 -0
- package/lib/server/doctor/service.js +93 -14
- package/lib/server/onboarding/index.js +11 -0
- package/lib/server/onboarding/validation.js +14 -3
- package/lib/server/routes/doctor.js +5 -2
- package/lib/server/routes/onboarding.js +33 -0
- package/lib/server.js +5 -0
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
1
2
|
const fs = require("fs");
|
|
2
3
|
const path = require("path");
|
|
3
4
|
const {
|
|
@@ -5,9 +6,11 @@ const {
|
|
|
5
6
|
buildBootstrapTruncationCards,
|
|
6
7
|
} = require("./bootstrap-context");
|
|
7
8
|
const { buildDoctorPrompt } = require("./prompt");
|
|
9
|
+
const { hashDoctorFixToken } = require("./fix-completion");
|
|
8
10
|
const { normalizeDoctorResult } = require("./normalize");
|
|
9
11
|
const { calculateWorkspaceDelta, computeWorkspaceSnapshot } = require("./workspace-fingerprint");
|
|
10
12
|
const {
|
|
13
|
+
kDoctorCardStatus,
|
|
11
14
|
kDoctorEngine,
|
|
12
15
|
kDoctorMeaningfulChangeScoreThreshold,
|
|
13
16
|
kDoctorPromptVersion,
|
|
@@ -17,6 +20,35 @@ const {
|
|
|
17
20
|
} = require("./constants");
|
|
18
21
|
|
|
19
22
|
const kMaxSnippetLines = 20;
|
|
23
|
+
const kDoctorFixDispatchTimeoutMs = 30000;
|
|
24
|
+
|
|
25
|
+
const buildDoctorFixCompletionInstructions = ({
|
|
26
|
+
cardId,
|
|
27
|
+
runId,
|
|
28
|
+
token,
|
|
29
|
+
alphaclawRootDir,
|
|
30
|
+
}) => {
|
|
31
|
+
const completionCommand = [
|
|
32
|
+
"alphaclaw",
|
|
33
|
+
"--root-dir",
|
|
34
|
+
shellEscapeArg(alphaclawRootDir),
|
|
35
|
+
"doctor finding complete",
|
|
36
|
+
"--id",
|
|
37
|
+
shellEscapeArg(String(Number(cardId || 0))),
|
|
38
|
+
"--run",
|
|
39
|
+
shellEscapeArg(runId),
|
|
40
|
+
"--token",
|
|
41
|
+
shellEscapeArg(token),
|
|
42
|
+
].join(" ");
|
|
43
|
+
return [
|
|
44
|
+
"AlphaClaw completion callback:",
|
|
45
|
+
"After you have successfully applied and verified the requested fix, run this command exactly:",
|
|
46
|
+
"```sh",
|
|
47
|
+
completionCommand,
|
|
48
|
+
"```",
|
|
49
|
+
"Do not call the completion callback if the fix was not applied or verification failed. Explain the problem to the user instead.",
|
|
50
|
+
].join("\n");
|
|
51
|
+
};
|
|
20
52
|
|
|
21
53
|
const shellEscapeArg = (value) => {
|
|
22
54
|
const safeValue = String(value || "");
|
|
@@ -91,8 +123,11 @@ const createDoctorService = ({
|
|
|
91
123
|
getDoctorCardsByRunId,
|
|
92
124
|
getDoctorCard,
|
|
93
125
|
updateDoctorCardStatus,
|
|
126
|
+
startDoctorCardFix,
|
|
127
|
+
cancelDoctorCardFix,
|
|
94
128
|
workspaceRoot,
|
|
95
129
|
managedRoot,
|
|
130
|
+
alphaclawRootDir = process.env.ALPHACLAW_ROOT_DIR || "~/.alphaclaw",
|
|
96
131
|
protectedPaths = [],
|
|
97
132
|
lockedPaths = [],
|
|
98
133
|
}) => {
|
|
@@ -133,7 +168,13 @@ const createDoctorService = ({
|
|
|
133
168
|
};
|
|
134
169
|
|
|
135
170
|
const cloneRunCards = ({ sourceRunId, targetRunId }) => {
|
|
136
|
-
const sourceCards = getDoctorCardsByRunId(sourceRunId)
|
|
171
|
+
const sourceCards = getDoctorCardsByRunId(sourceRunId).map((card) => ({
|
|
172
|
+
...card,
|
|
173
|
+
status:
|
|
174
|
+
card.status === kDoctorCardStatus.working
|
|
175
|
+
? kDoctorCardStatus.open
|
|
176
|
+
: card.status,
|
|
177
|
+
}));
|
|
137
178
|
insertDoctorCards({
|
|
138
179
|
runId: targetRunId,
|
|
139
180
|
cards: sourceCards,
|
|
@@ -385,7 +426,7 @@ const createDoctorService = ({
|
|
|
385
426
|
|
|
386
427
|
const requestCardFix = async ({
|
|
387
428
|
cardId,
|
|
388
|
-
|
|
429
|
+
sessionKey = "",
|
|
389
430
|
replyChannel = "",
|
|
390
431
|
replyTo = "",
|
|
391
432
|
prompt = "",
|
|
@@ -394,28 +435,65 @@ const createDoctorService = ({
|
|
|
394
435
|
if (!card) throw new Error("Doctor card not found");
|
|
395
436
|
const resolvedPrompt = String(prompt || card.fixPrompt || "").trim();
|
|
396
437
|
if (!resolvedPrompt) throw new Error("Doctor card does not include a fix prompt");
|
|
397
|
-
|
|
398
|
-
|
|
438
|
+
const trimmedSessionKey = String(sessionKey || "").trim();
|
|
439
|
+
if (!trimmedSessionKey) throw new Error("Doctor fix request requires a session key");
|
|
399
440
|
const trimmedReplyChannel = String(replyChannel || "").trim();
|
|
400
441
|
const trimmedReplyTo = String(replyTo || "").trim();
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
442
|
+
const runId = `doctor-fix-${Number(card.id || cardId)}-${crypto.randomUUID()}`;
|
|
443
|
+
const callbackToken = crypto.randomBytes(32).toString("hex");
|
|
444
|
+
const workingCard = startDoctorCardFix({
|
|
445
|
+
id: card.id,
|
|
446
|
+
runId,
|
|
447
|
+
tokenHash: hashDoctorFixToken(callbackToken),
|
|
448
|
+
});
|
|
449
|
+
if (!workingCard) {
|
|
450
|
+
const currentCard = getDoctorCard(card.id);
|
|
451
|
+
if (currentCard?.status === kDoctorCardStatus.working) {
|
|
452
|
+
throw new Error("Doctor fix already in progress");
|
|
453
|
+
}
|
|
454
|
+
throw new Error("Doctor finding must be open before requesting a fix");
|
|
407
455
|
}
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
456
|
+
const completionInstructions = buildDoctorFixCompletionInstructions({
|
|
457
|
+
cardId: card.id,
|
|
458
|
+
runId,
|
|
459
|
+
token: callbackToken,
|
|
460
|
+
alphaclawRootDir,
|
|
411
461
|
});
|
|
462
|
+
const gatewayParams = {
|
|
463
|
+
idempotencyKey: runId,
|
|
464
|
+
message: `${resolvedPrompt}\n\n${completionInstructions}`,
|
|
465
|
+
sessionKey: trimmedSessionKey,
|
|
466
|
+
};
|
|
467
|
+
if (trimmedReplyChannel && trimmedReplyTo) {
|
|
468
|
+
gatewayParams.deliver = true;
|
|
469
|
+
gatewayParams.replyChannel = trimmedReplyChannel;
|
|
470
|
+
gatewayParams.replyTo = trimmedReplyTo;
|
|
471
|
+
}
|
|
472
|
+
let result = null;
|
|
473
|
+
try {
|
|
474
|
+
result = await clawCmd(
|
|
475
|
+
`gateway call agent --json --timeout ${kDoctorFixDispatchTimeoutMs} --params ${shellEscapeArg(
|
|
476
|
+
JSON.stringify(gatewayParams),
|
|
477
|
+
)}`,
|
|
478
|
+
{
|
|
479
|
+
quiet: true,
|
|
480
|
+
timeoutMs: kDoctorFixDispatchTimeoutMs,
|
|
481
|
+
},
|
|
482
|
+
);
|
|
483
|
+
} catch (error) {
|
|
484
|
+
cancelDoctorCardFix({ id: card.id, runId });
|
|
485
|
+
throw error;
|
|
486
|
+
}
|
|
412
487
|
if (!result?.ok) {
|
|
488
|
+
cancelDoctorCardFix({ id: card.id, runId });
|
|
413
489
|
throw new Error(result?.stderr || "Could not send Doctor fix request");
|
|
414
490
|
}
|
|
415
491
|
return {
|
|
416
492
|
ok: true,
|
|
493
|
+
queued: true,
|
|
494
|
+
runId,
|
|
417
495
|
stdout: result.stdout || "",
|
|
418
|
-
card,
|
|
496
|
+
card: workingCard,
|
|
419
497
|
};
|
|
420
498
|
};
|
|
421
499
|
|
|
@@ -443,6 +521,7 @@ const createDoctorService = ({
|
|
|
443
521
|
};
|
|
444
522
|
|
|
445
523
|
module.exports = {
|
|
524
|
+
buildDoctorFixCompletionInstructions,
|
|
446
525
|
buildDoctorIdempotencyKey,
|
|
447
526
|
buildDoctorSessionKey,
|
|
448
527
|
buildDoctorSessionId,
|
|
@@ -369,7 +369,14 @@ const createOnboardingService = ({
|
|
|
369
369
|
vars,
|
|
370
370
|
modelKey,
|
|
371
371
|
importMode = false,
|
|
372
|
+
onProgress,
|
|
372
373
|
}) => {
|
|
374
|
+
const reportProgress = (stage) => {
|
|
375
|
+
if (typeof onProgress !== "function") return;
|
|
376
|
+
try {
|
|
377
|
+
onProgress(stage);
|
|
378
|
+
} catch {}
|
|
379
|
+
};
|
|
373
380
|
const validation = validateOnboardingInput({
|
|
374
381
|
vars,
|
|
375
382
|
modelKey,
|
|
@@ -425,6 +432,7 @@ const createOnboardingService = ({
|
|
|
425
432
|
syncApiKeyAuthProfilesFromEnvVars(authProfiles, varsToSave);
|
|
426
433
|
|
|
427
434
|
const [, repoName] = repoUrl.split("/");
|
|
435
|
+
reportProgress("creating_repo");
|
|
428
436
|
const repoCheck = await ensureGithubRepoAccessible({
|
|
429
437
|
repoUrl,
|
|
430
438
|
repoName,
|
|
@@ -481,6 +489,7 @@ const createOnboardingService = ({
|
|
|
481
489
|
);
|
|
482
490
|
}
|
|
483
491
|
|
|
492
|
+
reportProgress("running_openclaw_onboard");
|
|
484
493
|
if (!existingConfigPresent) {
|
|
485
494
|
const onboardArgs = buildOnboardArgs({
|
|
486
495
|
varMap,
|
|
@@ -551,6 +560,7 @@ const createOnboardingService = ({
|
|
|
551
560
|
|
|
552
561
|
ensureGatewayProxyConfig(getBaseUrl(req));
|
|
553
562
|
|
|
563
|
+
reportProgress("initial_git_push");
|
|
554
564
|
try {
|
|
555
565
|
const commitMsg = importMode
|
|
556
566
|
? "imported existing setup via AlphaClaw"
|
|
@@ -567,6 +577,7 @@ const createOnboardingService = ({
|
|
|
567
577
|
console.error("[onboard] Git push error:", e.message);
|
|
568
578
|
}
|
|
569
579
|
|
|
580
|
+
reportProgress("starting_gateway");
|
|
570
581
|
runOnboardedBootSequence();
|
|
571
582
|
return { status: 200, body: { ok: true } };
|
|
572
583
|
};
|
|
@@ -2,6 +2,14 @@ const { getEnvVarForApiKeyProvider } = require("../auth-profiles");
|
|
|
2
2
|
|
|
3
3
|
const kAnthropicSetupTokenPrefix = "sk-ant-oat01-";
|
|
4
4
|
const kAnthropicApiKeyPrefix = "sk-ant-api";
|
|
5
|
+
const kCanonicalCodexOauthModelKeys = new Set([
|
|
6
|
+
"openai/gpt-5.4-mini",
|
|
7
|
+
"openai/gpt-5.5",
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
const usesCodexOauth = (modelKey, provider) =>
|
|
11
|
+
provider === "openai-codex" ||
|
|
12
|
+
kCanonicalCodexOauthModelKeys.has(String(modelKey || "").trim());
|
|
5
13
|
|
|
6
14
|
const validateAnthropicCredentialShape = (varMap) => {
|
|
7
15
|
const anthropicToken = String(varMap.ANTHROPIC_TOKEN || "").trim();
|
|
@@ -71,7 +79,10 @@ const validateOnboardingInput = ({ vars, modelKey, resolveModelProvider, hasCode
|
|
|
71
79
|
if (!anthropicValidation.ok) return anthropicValidation;
|
|
72
80
|
const githubToken = String(varMap.GITHUB_TOKEN || "");
|
|
73
81
|
const githubRepoInput = String(varMap.GITHUB_WORKSPACE_REPO || "").trim();
|
|
74
|
-
const
|
|
82
|
+
const resolvedProvider = resolveModelProvider(modelKey);
|
|
83
|
+
const selectedProvider = usesCodexOauth(modelKey, resolvedProvider)
|
|
84
|
+
? "openai-codex"
|
|
85
|
+
: resolvedProvider;
|
|
75
86
|
const hasCodexOauth = hasCodexOauthProfile();
|
|
76
87
|
const hasAnyAi = !!(
|
|
77
88
|
varMap.ANTHROPIC_API_KEY ||
|
|
@@ -82,7 +93,7 @@ const validateOnboardingInput = ({ vars, modelKey, resolveModelProvider, hasCode
|
|
|
82
93
|
);
|
|
83
94
|
const hasAi = (() => {
|
|
84
95
|
if (selectedProvider === "openai-codex") {
|
|
85
|
-
return hasCodexOauth;
|
|
96
|
+
return hasCodexOauth || !!String(varMap.OPENAI_API_KEY || "").trim();
|
|
86
97
|
}
|
|
87
98
|
if (selectedProvider === "anthropic") {
|
|
88
99
|
return !!(varMap.ANTHROPIC_API_KEY || varMap.ANTHROPIC_TOKEN);
|
|
@@ -101,7 +112,7 @@ const validateOnboardingInput = ({ vars, modelKey, resolveModelProvider, hasCode
|
|
|
101
112
|
return {
|
|
102
113
|
ok: false,
|
|
103
114
|
status: 400,
|
|
104
|
-
error: "Connect OpenAI Codex OAuth before continuing",
|
|
115
|
+
error: "Connect OpenAI Codex OAuth or add an OpenAI API key before continuing",
|
|
105
116
|
};
|
|
106
117
|
}
|
|
107
118
|
return {
|
|
@@ -106,13 +106,16 @@ const registerDoctorRoutes = ({ app, requireAuth, doctorService }) => {
|
|
|
106
106
|
try {
|
|
107
107
|
const result = await doctorService.requestCardFix({
|
|
108
108
|
cardId: req.params.id,
|
|
109
|
-
|
|
109
|
+
sessionKey: req.body?.sessionKey,
|
|
110
110
|
replyChannel: req.body?.replyChannel,
|
|
111
111
|
replyTo: req.body?.replyTo,
|
|
112
112
|
prompt: req.body?.prompt,
|
|
113
113
|
});
|
|
114
|
-
return res.json(result);
|
|
114
|
+
return res.status(result?.queued ? 202 : 200).json(result);
|
|
115
115
|
} catch (error) {
|
|
116
|
+
if (/already in progress|must be open/i.test(error.message || "")) {
|
|
117
|
+
return res.status(409).json({ ok: false, error: error.message });
|
|
118
|
+
}
|
|
116
119
|
if (/not found/i.test(error.message || "")) {
|
|
117
120
|
return res.status(404).json({ ok: false, error: error.message });
|
|
118
121
|
}
|
|
@@ -95,6 +95,28 @@ const registerOnboardingRoutes = ({
|
|
|
95
95
|
getBaseUrl,
|
|
96
96
|
runOnboardedBootSequence,
|
|
97
97
|
}) => {
|
|
98
|
+
const kOnboardingProgressMessages = {
|
|
99
|
+
creating_repo: "Creating repo...",
|
|
100
|
+
running_openclaw_onboard: "Running openclaw onboard...",
|
|
101
|
+
initial_git_push: "Initial git commit/push...",
|
|
102
|
+
starting_gateway: "Starting gateway...",
|
|
103
|
+
};
|
|
104
|
+
let onboardingProgress = {
|
|
105
|
+
active: false,
|
|
106
|
+
stage: "idle",
|
|
107
|
+
message: "",
|
|
108
|
+
updatedAt: null,
|
|
109
|
+
};
|
|
110
|
+
const setOnboardingProgress = (stage, { active = true } = {}) => {
|
|
111
|
+
const normalizedStage = String(stage || "").trim();
|
|
112
|
+
onboardingProgress = {
|
|
113
|
+
active,
|
|
114
|
+
stage: normalizedStage,
|
|
115
|
+
message: kOnboardingProgressMessages[normalizedStage] || "",
|
|
116
|
+
updatedAt: new Date().toISOString(),
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
|
|
98
120
|
// Keep mutating onboarding routes marker-gated so in-progress imports
|
|
99
121
|
// can promote files before the final completion marker is written.
|
|
100
122
|
const hasExplicitOnboardingMarker = () =>
|
|
@@ -157,21 +179,32 @@ const registerOnboardingRoutes = ({
|
|
|
157
179
|
res.json({ onboarded: hasExplicitOnboardingMarker() });
|
|
158
180
|
});
|
|
159
181
|
|
|
182
|
+
app.get("/api/onboard/progress", (req, res) => {
|
|
183
|
+
res.json(onboardingProgress);
|
|
184
|
+
});
|
|
185
|
+
|
|
160
186
|
app.post("/api/onboard", async (req, res) => {
|
|
161
187
|
if (hasExplicitOnboardingMarker())
|
|
162
188
|
return res.json({ ok: false, error: "Already onboarded" });
|
|
163
189
|
|
|
164
190
|
try {
|
|
191
|
+
setOnboardingProgress("creating_repo");
|
|
165
192
|
const { vars, modelKey, importMode } = req.body;
|
|
166
193
|
const result = await onboardingService.completeOnboarding({
|
|
167
194
|
req,
|
|
168
195
|
vars,
|
|
169
196
|
modelKey,
|
|
170
197
|
importMode: !!importMode,
|
|
198
|
+
onProgress: (stage) => setOnboardingProgress(stage),
|
|
171
199
|
});
|
|
200
|
+
setOnboardingProgress(
|
|
201
|
+
result.body?.ok ? "starting_gateway" : "failed",
|
|
202
|
+
{ active: false },
|
|
203
|
+
);
|
|
172
204
|
res.status(result.status).json(result.body);
|
|
173
205
|
} catch (err) {
|
|
174
206
|
console.error("[onboard] Error:", err);
|
|
207
|
+
setOnboardingProgress("failed", { active: false });
|
|
175
208
|
res.status(500).json({ ok: false, error: sanitizeOnboardingError(err) });
|
|
176
209
|
}
|
|
177
210
|
});
|
package/lib/server.js
CHANGED
|
@@ -64,6 +64,8 @@ const {
|
|
|
64
64
|
getDoctorCardsByRunId,
|
|
65
65
|
getDoctorCard,
|
|
66
66
|
updateDoctorCardStatus,
|
|
67
|
+
startDoctorCardFix,
|
|
68
|
+
cancelDoctorCardFix,
|
|
67
69
|
} = require("./server/db/doctor");
|
|
68
70
|
const {
|
|
69
71
|
initAuthDb,
|
|
@@ -306,8 +308,11 @@ const doctorService = createDoctorService({
|
|
|
306
308
|
getDoctorCardsByRunId,
|
|
307
309
|
getDoctorCard,
|
|
308
310
|
updateDoctorCardStatus,
|
|
311
|
+
startDoctorCardFix,
|
|
312
|
+
cancelDoctorCardFix,
|
|
309
313
|
workspaceRoot: constants.WORKSPACE_DIR,
|
|
310
314
|
managedRoot: constants.OPENCLAW_DIR,
|
|
315
|
+
alphaclawRootDir: constants.kRootDir,
|
|
311
316
|
protectedPaths: Array.from(constants.kProtectedBrowsePaths || []),
|
|
312
317
|
lockedPaths: Array.from(constants.kLockedBrowsePaths || []),
|
|
313
318
|
});
|