@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.
@@ -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
- sessionId = "",
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
- let command = `agent --agent main --message ${shellEscapeArg(resolvedPrompt)}`;
398
- const trimmedSessionId = String(sessionId || "").trim();
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
- if (trimmedReplyChannel && trimmedReplyTo) {
402
- command +=
403
- ` --deliver --reply-channel ${shellEscapeArg(trimmedReplyChannel)}` +
404
- ` --reply-to ${shellEscapeArg(trimmedReplyTo)}`;
405
- } else if (trimmedSessionId) {
406
- command += ` --session-id ${shellEscapeArg(trimmedSessionId)}`;
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 result = await clawCmd(command, {
409
- quiet: true,
410
- timeoutMs: kDoctorRunTimeoutMs,
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 selectedProvider = resolveModelProvider(modelKey);
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
- sessionId: req.body?.sessionId,
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.9.27",
3
+ "version": "0.9.29",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },