@chrysb/alphaclaw 0.4.4 → 0.4.6-beta.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.
Files changed (37) hide show
  1. package/README.md +21 -18
  2. package/lib/public/css/theme.css +29 -0
  3. package/lib/public/js/app.js +41 -2
  4. package/lib/public/js/components/badge.js +4 -0
  5. package/lib/public/js/components/doctor/findings-list.js +191 -0
  6. package/lib/public/js/components/doctor/fix-card-modal.js +144 -0
  7. package/lib/public/js/components/doctor/general-warning.js +37 -0
  8. package/lib/public/js/components/doctor/helpers.js +169 -0
  9. package/lib/public/js/components/doctor/index.js +536 -0
  10. package/lib/public/js/components/doctor/summary-cards.js +24 -0
  11. package/lib/public/js/lib/api.js +79 -0
  12. package/lib/server/commands.js +8 -4
  13. package/lib/server/constants.js +22 -26
  14. package/lib/server/db/doctor/index.js +529 -0
  15. package/lib/server/db/doctor/schema.js +69 -0
  16. package/lib/server/doctor/constants.js +43 -0
  17. package/lib/server/doctor/normalize.js +214 -0
  18. package/lib/server/doctor/prompt.js +89 -0
  19. package/lib/server/doctor/service.js +392 -0
  20. package/lib/server/doctor/workspace-fingerprint.js +126 -0
  21. package/lib/server/gmail-push.js +102 -6
  22. package/lib/server/gmail-watch.js +5 -20
  23. package/lib/server/helpers.js +5 -21
  24. package/lib/server/routes/doctor.js +123 -0
  25. package/lib/server/routes/google.js +2 -10
  26. package/lib/server/routes/system.js +7 -1
  27. package/lib/server/routes/telegram.js +3 -14
  28. package/lib/server/routes/usage.js +1 -5
  29. package/lib/server/routes/webhooks.js +2 -6
  30. package/lib/server/utils/boolean.js +22 -0
  31. package/lib/server/utils/json.js +77 -0
  32. package/lib/server/utils/network.js +5 -0
  33. package/lib/server/utils/number.js +8 -0
  34. package/lib/server/utils/shell.js +16 -0
  35. package/lib/server/webhook-middleware.js +1 -2
  36. package/lib/server.js +42 -0
  37. package/package.json +1 -1
@@ -0,0 +1,24 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import htm from "https://esm.sh/htm";
3
+ import { buildDoctorPriorityCounts } from "./helpers.js";
4
+
5
+ const html = htm.bind(h);
6
+
7
+ const SummaryCard = ({ title = "", value = 0, toneClassName = "" }) => html`
8
+ <div class="bg-surface border border-border rounded-xl p-4">
9
+ <h3 class="card-label text-xs">${title}</h3>
10
+ <div class=${`text-lg font-semibold mt-1 ${toneClassName}`}>${value}</div>
11
+ </div>
12
+ `;
13
+
14
+ export const DoctorSummaryCards = ({ cards = [] }) => {
15
+ const counts = buildDoctorPriorityCounts(cards);
16
+ return html`
17
+ <div class="grid grid-cols-1 md:grid-cols-4 gap-3">
18
+ <${SummaryCard} title="Open Findings" value=${cards.length} />
19
+ <${SummaryCard} title="P0" value=${counts.P0} toneClassName="text-red-400" />
20
+ <${SummaryCard} title="P1" value=${counts.P1} toneClassName="text-yellow-400" />
21
+ <${SummaryCard} title="P2" value=${counts.P2} toneClassName="text-gray-300" />
22
+ </div>
23
+ `;
24
+ };
@@ -198,6 +198,85 @@ export const fetchAgentSessions = async () => {
198
198
  return parseJsonOrThrow(res, "Could not load agent sessions");
199
199
  };
200
200
 
201
+ export const fetchDoctorStatus = async () => {
202
+ const res = await authFetch("/api/doctor/status");
203
+ return parseJsonOrThrow(res, "Could not load Doctor status");
204
+ };
205
+
206
+ export const startDoctorRun = async () => {
207
+ const res = await authFetch("/api/doctor/run", {
208
+ method: "POST",
209
+ headers: { "Content-Type": "application/json" },
210
+ body: JSON.stringify({}),
211
+ });
212
+ return parseJsonOrThrow(res, "Could not start Doctor run");
213
+ };
214
+
215
+ export const importDoctorResult = async (rawOutput = "") => {
216
+ const res = await authFetch("/api/doctor/import", {
217
+ method: "POST",
218
+ headers: { "Content-Type": "application/json" },
219
+ body: JSON.stringify({ rawOutput: String(rawOutput || "") }),
220
+ });
221
+ return parseJsonOrThrow(res, "Could not import Doctor result");
222
+ };
223
+
224
+ export const fetchDoctorRuns = async (limit = 10) => {
225
+ const params = new URLSearchParams({ limit: String(limit) });
226
+ const res = await authFetch(`/api/doctor/runs?${params.toString()}`);
227
+ return parseJsonOrThrow(res, "Could not load Doctor runs");
228
+ };
229
+
230
+ export const fetchDoctorCards = async ({ runId = "all" } = {}) => {
231
+ const params = new URLSearchParams();
232
+ if (String(runId || "").trim()) params.set("runId", String(runId || ""));
233
+ const suffix = params.toString() ? `?${params.toString()}` : "";
234
+ const res = await authFetch(`/api/doctor/cards${suffix}`);
235
+ return parseJsonOrThrow(res, "Could not load Doctor findings");
236
+ };
237
+
238
+ export const fetchDoctorRun = async (runId) => {
239
+ const res = await authFetch(`/api/doctor/runs/${encodeURIComponent(String(runId || ""))}`);
240
+ return parseJsonOrThrow(res, "Could not load Doctor run");
241
+ };
242
+
243
+ export const fetchDoctorRunCards = async (runId) => {
244
+ const res = await authFetch(
245
+ `/api/doctor/runs/${encodeURIComponent(String(runId || ""))}/cards`,
246
+ );
247
+ return parseJsonOrThrow(res, "Could not load Doctor cards");
248
+ };
249
+
250
+ export const updateDoctorCardStatus = async ({ cardId, status }) => {
251
+ const res = await authFetch(`/api/doctor/cards/${encodeURIComponent(String(cardId || ""))}/status`, {
252
+ method: "POST",
253
+ headers: { "Content-Type": "application/json" },
254
+ body: JSON.stringify({ status: String(status || "") }),
255
+ });
256
+ return parseJsonOrThrow(res, "Could not update Doctor card status");
257
+ };
258
+
259
+ export const sendDoctorCardFix = async ({
260
+ cardId,
261
+ sessionId = "",
262
+ replyChannel = "",
263
+ replyTo = "",
264
+ } = {}) => {
265
+ const res = await authFetch(
266
+ `/api/doctor/findings/${encodeURIComponent(String(cardId || ""))}/fix`,
267
+ {
268
+ method: "POST",
269
+ headers: { "Content-Type": "application/json" },
270
+ body: JSON.stringify({
271
+ sessionId: String(sessionId || ""),
272
+ replyChannel: String(replyChannel || ""),
273
+ replyTo: String(replyTo || ""),
274
+ }),
275
+ },
276
+ );
277
+ return parseJsonOrThrow(res, "Could not send Doctor fix request");
278
+ };
279
+
201
280
  export const sendAgentMessage = async ({
202
281
  message = "",
203
282
  sessionKey = "",
@@ -4,7 +4,11 @@ const { OPENCLAW_DIR, GOG_KEYRING_PASSWORD } = require("./constants");
4
4
  const createCommands = ({ gatewayEnv }) => {
5
5
  const shellCmd = (cmd, opts = {}) =>
6
6
  new Promise((resolve, reject) => {
7
- const { logStdout, ...execOpts } = opts;
7
+ const {
8
+ logStdout,
9
+ timeoutMs = 60000,
10
+ ...execOpts
11
+ } = opts;
8
12
  const shouldLogStdout =
9
13
  typeof logStdout === "boolean" ? logStdout : !cmd.includes("--json");
10
14
  console.log(
@@ -13,7 +17,7 @@ const createCommands = ({ gatewayEnv }) => {
13
17
  .replace(/sk-[^\s"]+/g, "***")
14
18
  .slice(0, 200)}`,
15
19
  );
16
- exec(cmd, { timeout: 60000, ...execOpts }, (err, stdout, stderr) => {
20
+ exec(cmd, { timeout: timeoutMs, ...execOpts }, (err, stdout, stderr) => {
17
21
  if (err) {
18
22
  console.error(
19
23
  `[onboard] Error: ${(stderr || err.message).slice(0, 300)}`,
@@ -27,14 +31,14 @@ const createCommands = ({ gatewayEnv }) => {
27
31
  });
28
32
  });
29
33
 
30
- const clawCmd = (cmd, { quiet = false } = {}) =>
34
+ const clawCmd = (cmd, { quiet = false, timeoutMs = 15000 } = {}) =>
31
35
  new Promise((resolve) => {
32
36
  if (!quiet) console.log(`[alphaclaw] Running: openclaw ${cmd}`);
33
37
  exec(
34
38
  `openclaw ${cmd}`,
35
39
  {
36
40
  env: gatewayEnv(),
37
- timeout: 15000,
41
+ timeout: timeoutMs,
38
42
  },
39
43
  (err, stdout, stderr) => {
40
44
  const result = {
@@ -1,11 +1,7 @@
1
1
  const os = require("os");
2
2
  const path = require("path");
3
3
  const kBrowseFilePolicies = require("../public/shared/browse-file-policies.json");
4
-
5
- const parsePositiveIntEnv = (value, fallbackValue) => {
6
- const parsed = Number.parseInt(String(value || ""), 10);
7
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallbackValue;
8
- };
4
+ const { parsePositiveInt } = require("./utils/number");
9
5
 
10
6
  // Portable root directory: --root-dir flag sets ALPHACLAW_ROOT_DIR before require
11
7
  const kRootDir =
@@ -38,29 +34,29 @@ const CODEX_OAUTH_SCOPE = "openid profile email offline_access";
38
34
  const CODEX_JWT_CLAIM_PATH = "https://api.openai.com/auth";
39
35
  const kCodexOauthStateTtlMs = 10 * 60 * 1000;
40
36
 
41
- const kTrustProxyHops = parsePositiveIntEnv(process.env.TRUST_PROXY_HOPS, 1);
42
- const kLoginWindowMs = parsePositiveIntEnv(
37
+ const kTrustProxyHops = parsePositiveInt(process.env.TRUST_PROXY_HOPS, 1);
38
+ const kLoginWindowMs = parsePositiveInt(
43
39
  process.env.LOGIN_RATE_WINDOW_MS,
44
40
  10 * 60 * 1000,
45
41
  );
46
- const kLoginMaxAttempts = parsePositiveIntEnv(
42
+ const kLoginMaxAttempts = parsePositiveInt(
47
43
  process.env.LOGIN_RATE_MAX_ATTEMPTS,
48
44
  5,
49
45
  );
50
- const kLoginBaseLockMs = parsePositiveIntEnv(
46
+ const kLoginBaseLockMs = parsePositiveInt(
51
47
  process.env.LOGIN_RATE_BASE_LOCK_MS,
52
48
  60 * 1000,
53
49
  );
54
- const kLoginMaxLockMs = parsePositiveIntEnv(
50
+ const kLoginMaxLockMs = parsePositiveInt(
55
51
  process.env.LOGIN_RATE_MAX_LOCK_MS,
56
52
  15 * 60 * 1000,
57
53
  );
58
- const kLoginCleanupIntervalMs = parsePositiveIntEnv(
54
+ const kLoginCleanupIntervalMs = parsePositiveInt(
59
55
  process.env.LOGIN_RATE_CLEANUP_INTERVAL_MS,
60
56
  60 * 1000,
61
57
  );
62
58
  const kLoginStateTtlMs = Math.max(
63
- parsePositiveIntEnv(
59
+ parsePositiveInt(
64
60
  process.env.LOGIN_RATE_STATE_TTL_MS,
65
61
  Math.max(kLoginWindowMs, kLoginMaxLockMs) * 3,
66
62
  ),
@@ -116,31 +112,31 @@ const kLatestVersionCacheTtlMs = 10 * 60 * 1000;
116
112
  const kOpenclawRegistryUrl = "https://registry.npmjs.org/openclaw";
117
113
  const kAlphaclawRegistryUrl = "https://registry.npmjs.org/@chrysb%2falphaclaw";
118
114
  const kAppDir = kNpmPackageRoot;
119
- const kMaxPayloadBytes = parsePositiveIntEnv(process.env.WEBHOOK_LOG_MAX_BYTES, 50 * 1024);
120
- const kWebhookPruneDays = parsePositiveIntEnv(process.env.WEBHOOK_LOG_RETENTION_DAYS, 30);
115
+ const kMaxPayloadBytes = parsePositiveInt(process.env.WEBHOOK_LOG_MAX_BYTES, 50 * 1024);
116
+ const kWebhookPruneDays = parsePositiveInt(process.env.WEBHOOK_LOG_RETENTION_DAYS, 30);
121
117
  const kWatchdogCheckIntervalMs =
122
- parsePositiveIntEnv(process.env.WATCHDOG_CHECK_INTERVAL, 120) * 1000;
118
+ parsePositiveInt(process.env.WATCHDOG_CHECK_INTERVAL, 120) * 1000;
123
119
  const kWatchdogDegradedCheckIntervalMs =
124
- parsePositiveIntEnv(process.env.WATCHDOG_DEGRADED_CHECK_INTERVAL, 5) * 1000;
125
- const kWatchdogStartupFailureThreshold = parsePositiveIntEnv(
120
+ parsePositiveInt(process.env.WATCHDOG_DEGRADED_CHECK_INTERVAL, 5) * 1000;
121
+ const kWatchdogStartupFailureThreshold = parsePositiveInt(
126
122
  process.env.WATCHDOG_STARTUP_FAILURE_THRESHOLD,
127
123
  3,
128
124
  );
129
- const kWatchdogMaxRepairAttempts = parsePositiveIntEnv(
125
+ const kWatchdogMaxRepairAttempts = parsePositiveInt(
130
126
  process.env.WATCHDOG_MAX_REPAIR_ATTEMPTS,
131
127
  2,
132
128
  );
133
129
  const kWatchdogCrashLoopWindowMs =
134
- parsePositiveIntEnv(process.env.WATCHDOG_CRASH_LOOP_WINDOW, 300) * 1000;
135
- const kWatchdogCrashLoopThreshold = parsePositiveIntEnv(
130
+ parsePositiveInt(process.env.WATCHDOG_CRASH_LOOP_WINDOW, 300) * 1000;
131
+ const kWatchdogCrashLoopThreshold = parsePositiveInt(
136
132
  process.env.WATCHDOG_CRASH_LOOP_THRESHOLD,
137
133
  3,
138
134
  );
139
- const kWatchdogLogRetentionDays = parsePositiveIntEnv(
135
+ const kWatchdogLogRetentionDays = parsePositiveInt(
140
136
  process.env.WATCHDOG_LOG_RETENTION_DAYS,
141
137
  30,
142
138
  );
143
- const kLogMaxBytes = parsePositiveIntEnv(
139
+ const kLogMaxBytes = parsePositiveInt(
144
140
  process.env.LOG_MAX_BYTES,
145
141
  2 * 1024 * 1024,
146
142
  );
@@ -266,17 +262,17 @@ const GOG_CREDENTIALS_PATH = path.join(GOG_CONFIG_DIR, "credentials.json");
266
262
  const GOG_STATE_PATH = path.join(GOG_CONFIG_DIR, "state.json");
267
263
  const GOG_KEYRING_PASSWORD = process.env.GOG_KEYRING_PASSWORD || "alphaclaw";
268
264
  const kMaxGoogleAccounts = 5;
269
- const kGmailServeBasePort = parsePositiveIntEnv(
265
+ const kGmailServeBasePort = parsePositiveInt(
270
266
  process.env.GMAIL_SERVE_BASE_PORT,
271
267
  18801,
272
268
  );
273
269
  const kGmailWatchRenewalIntervalMs =
274
- parsePositiveIntEnv(process.env.GMAIL_WATCH_RENEWAL_INTERVAL_SECONDS, 6 * 60 * 60) *
270
+ parsePositiveInt(process.env.GMAIL_WATCH_RENEWAL_INTERVAL_SECONDS, 6 * 60 * 60) *
275
271
  1000;
276
272
  const kGmailWatchRenewalThresholdMs =
277
- parsePositiveIntEnv(process.env.GMAIL_WATCH_RENEWAL_THRESHOLD_SECONDS, 24 * 60 * 60) *
273
+ parsePositiveInt(process.env.GMAIL_WATCH_RENEWAL_THRESHOLD_SECONDS, 24 * 60 * 60) *
278
274
  1000;
279
- const kGmailMaxBodyBytes = parsePositiveIntEnv(
275
+ const kGmailMaxBodyBytes = parsePositiveInt(
280
276
  process.env.GMAIL_WATCH_MAX_BODY_BYTES,
281
277
  20000,
282
278
  );