@chrysb/alphaclaw 0.4.6-beta.1 → 0.4.6-beta.3

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 (28) hide show
  1. package/lib/public/css/explorer.css +4 -7
  2. package/lib/public/css/shell.css +14 -2
  3. package/lib/public/css/theme.css +4 -0
  4. package/lib/public/js/app.js +62 -38
  5. package/lib/public/js/components/doctor/findings-list.js +190 -12
  6. package/lib/public/js/components/doctor/fix-card-modal.js +20 -73
  7. package/lib/public/js/components/doctor/helpers.js +7 -27
  8. package/lib/public/js/components/doctor/index.js +5 -5
  9. package/lib/public/js/components/file-tree.js +1 -1
  10. package/lib/public/js/components/file-viewer/constants.js +4 -3
  11. package/lib/public/js/components/file-viewer/editor-surface.js +1 -0
  12. package/lib/public/js/components/file-viewer/index.js +4 -0
  13. package/lib/public/js/components/file-viewer/storage.js +1 -4
  14. package/lib/public/js/components/file-viewer/use-editor-selection-restore.js +130 -17
  15. package/lib/public/js/components/file-viewer/use-file-viewer.js +4 -0
  16. package/lib/public/js/components/google/gmail-setup-wizard.js +18 -51
  17. package/lib/public/js/components/google/gmail-watch-toggle.js +4 -1
  18. package/lib/public/js/components/onboarding/use-welcome-storage.js +2 -1
  19. package/lib/public/js/components/telegram-workspace/index.js +5 -2
  20. package/lib/public/js/hooks/useAgentSessions.js +128 -0
  21. package/lib/public/js/lib/browse-draft-state.js +9 -13
  22. package/lib/public/js/lib/storage-keys.js +28 -0
  23. package/lib/public/js/lib/ui-settings.js +3 -1
  24. package/lib/server/doctor/normalize.js +57 -23
  25. package/lib/server/doctor/prompt.js +30 -4
  26. package/lib/server/doctor/service.js +46 -0
  27. package/lib/server/doctor/workspace-fingerprint.js +46 -6
  28. package/package.json +1 -1
@@ -89,42 +89,76 @@ const normalizeCardStatus = (value) => {
89
89
  return kDoctorCardStatus.open;
90
90
  };
91
91
 
92
- const normalizeEvidence = (value) => {
93
- if (Array.isArray(value)) {
94
- return value
95
- .map((item) => {
96
- if (item == null) return null;
97
- if (typeof item === "string") {
98
- const text = toTrimmedString(item);
99
- return text ? { type: "text", text } : null;
100
- }
101
- if (typeof item === "object") return item;
102
- return { type: "text", text: String(item) };
103
- })
104
- .filter(Boolean);
92
+ const normalizeEvidenceItem = (item) => {
93
+ if (item == null) return null;
94
+ if (typeof item === "string") {
95
+ const text = toTrimmedString(item);
96
+ return text ? { type: "text", text } : null;
97
+ }
98
+ if (typeof item === "object") {
99
+ const entry = { ...item };
100
+ if (entry.type === "path" && entry.path) {
101
+ entry.path = toTrimmedString(entry.path);
102
+ if (Number.isFinite(entry.startLine) && entry.startLine > 0) {
103
+ entry.startLine = entry.startLine;
104
+ } else {
105
+ delete entry.startLine;
106
+ }
107
+ if (Number.isFinite(entry.endLine) && entry.endLine > 0) {
108
+ entry.endLine = entry.endLine;
109
+ } else {
110
+ delete entry.endLine;
111
+ }
112
+ }
113
+ return entry;
105
114
  }
115
+ return { type: "text", text: String(item) };
116
+ };
117
+
118
+ const normalizeEvidence = (value) => {
119
+ if (Array.isArray(value)) return value.map(normalizeEvidenceItem).filter(Boolean);
106
120
  if (typeof value === "string") {
107
121
  const text = toTrimmedString(value);
108
122
  return text ? [{ type: "text", text }] : [];
109
123
  }
110
- if (value && typeof value === "object") return [value];
124
+ if (value && typeof value === "object") return [normalizeEvidenceItem(value)].filter(Boolean);
111
125
  return [];
112
126
  };
113
127
 
128
+ const normalizeTargetPathItem = (item) => {
129
+ if (item == null) return null;
130
+ if (typeof item === "string") {
131
+ const path = toTrimmedString(item);
132
+ return path ? { path } : null;
133
+ }
134
+ if (typeof item === "object" && item.path) {
135
+ const path = toTrimmedString(item.path);
136
+ if (!path) return null;
137
+ const entry = { path };
138
+ if (Number.isFinite(item.startLine) && item.startLine > 0) entry.startLine = item.startLine;
139
+ if (Number.isFinite(item.endLine) && item.endLine > 0) entry.endLine = item.endLine;
140
+ return entry;
141
+ }
142
+ return null;
143
+ };
144
+
114
145
  const normalizeTargetPaths = (value) => {
115
146
  const values = Array.isArray(value) ? value : value == null ? [] : [value];
116
- return Array.from(
117
- new Set(
118
- values
119
- .map((item) => toTrimmedString(item))
120
- .filter(Boolean),
121
- ),
122
- );
147
+ const seen = new Set();
148
+ return values
149
+ .map(normalizeTargetPathItem)
150
+ .filter((item) => {
151
+ if (!item) return false;
152
+ if (seen.has(item.path)) return false;
153
+ seen.add(item.path);
154
+ return true;
155
+ });
123
156
  };
124
157
 
125
158
  const buildFallbackFixPrompt = ({ title, recommendation, targetPaths }) => {
126
- const targetLine = targetPaths.length
127
- ? `Focus on these paths if relevant: ${targetPaths.join(", ")}.`
159
+ const pathStrings = targetPaths.map((item) => item?.path || String(item)).filter(Boolean);
160
+ const targetLine = pathStrings.length
161
+ ? `Focus on these paths if relevant: ${pathStrings.join(", ")}.`
128
162
  : "Inspect the relevant workspace files before making changes.";
129
163
  return (
130
164
  `Please address this Doctor finding safely.\n\n` +
@@ -1,13 +1,29 @@
1
1
  const renderList = (items = []) =>
2
2
  items.length ? items.map((item) => `- ${item}`).join("\n") : "- (none)";
3
3
 
4
+ const renderResolvedCards = (cards = []) => {
5
+ if (!cards.length) return "";
6
+ const lines = cards.map(
7
+ (card) =>
8
+ `- [${card.status}] ${card.title}` +
9
+ (card.category ? ` (${card.category})` : ""),
10
+ );
11
+ return `
12
+
13
+ Previously resolved findings (do not re-suggest these):
14
+ ${lines.join("\n")}
15
+ `;
16
+ };
17
+
4
18
  const buildDoctorPrompt = ({
5
19
  workspaceRoot = "",
6
20
  managedRoot = "",
7
21
  protectedPaths = [],
8
22
  lockedPaths = [],
23
+ resolvedCards = [],
9
24
  promptVersion = "doctor-v1",
10
- }) => `
25
+ }) =>
26
+ `
11
27
  You are AlphaClaw Doctor. Analyze this OpenClaw workspace for guidance drift, redundancy, misplacement, and cleanup opportunities.
12
28
 
13
29
  Important:
@@ -20,6 +36,10 @@ Important:
20
36
  - A fresh install can be healthy even if it includes broad default guidance.
21
37
  - Return ONLY valid JSON. No markdown fences. No extra prose.
22
38
 
39
+ OpenClaw context injection:
40
+ - OpenClaw automatically injects ALL root-level \`.md\` files (e.g. AGENTS.md, SOUL.md, TOOLS.md, IDENTITY.md, USER.md, HEARTBEAT.md) into the agent's context window as "project context" on every turn.
41
+ - Additionally, AlphaClaw injects bootstrap files from \`hooks/bootstrap/\` (e.g. AGENTS.md, TOOLS.md) as extra context on every turn.
42
+
23
43
  OpenClaw default context:
24
44
  - \`AGENTS.md\` is the workspace home file in the default OpenClaw template. It may intentionally include first-run instructions, session-startup guidance, memory conventions, safety rules, tool pointers, and optional behavioral guidance.
25
45
  - Do not treat default-template content as drift just because it is broad or multi-purpose.
@@ -64,20 +84,26 @@ Return exactly this JSON shape:
64
84
  "summary": "what is wrong and why it matters",
65
85
  "recommendation": "clear recommended action",
66
86
  "evidence": [
67
- { "type": "path", "path": "relative/path" },
87
+ { "type": "path", "path": "relative/path", "startLine": 10, "endLine": 25 },
68
88
  { "type": "note", "text": "short supporting note" }
69
89
  ],
70
- "targetPaths": ["relative/path/one", "relative/path/two"],
90
+ "targetPaths": [
91
+ { "path": "relative/path/one", "startLine": 10 },
92
+ { "path": "relative/path/two" }
93
+ ],
71
94
  "fixPrompt": "a concise message another agent can use to fix just this finding safely",
72
95
  "status": "open"
73
96
  }
74
97
  ]
75
98
  }
76
99
 
77
- Constraints:
100
+ ${renderResolvedCards(resolvedCards)}Constraints:
78
101
  - Maximum 12 cards
79
102
  - Use relative paths in evidence and targetPaths
103
+ - Include startLine (and optionally endLine) in evidence and targetPaths when the finding relates to a specific section of a file
104
+ - targetPaths items can be strings or objects with { path, startLine? }
80
105
  - Do not include duplicate cards
106
+ - Do not re-suggest findings that appear in the "Previously resolved" list above
81
107
  - Do not create cards for healthy default-template behavior
82
108
  - Do not create cards whose primary recommendation is to refactor AlphaClaw-managed file structure
83
109
  - If there are no meaningful findings, return an empty cards array
@@ -1,3 +1,5 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
1
3
  const { buildDoctorPrompt } = require("./prompt");
2
4
  const { normalizeDoctorResult } = require("./normalize");
3
5
  const { calculateWorkspaceDelta, computeWorkspaceSnapshot } = require("./workspace-fingerprint");
@@ -10,6 +12,8 @@ const {
10
12
  kDoctorStaleThresholdMs,
11
13
  } = require("./constants");
12
14
 
15
+ const kMaxSnippetLines = 20;
16
+
13
17
  const shellEscapeArg = (value) => {
14
18
  const safeValue = String(value || "");
15
19
  return `'${safeValue.replace(/'/g, `'\\''`)}'`;
@@ -35,6 +39,37 @@ const formatElapsedSince = (isoTime) => {
35
39
  return `${elapsedDays} day${elapsedDays === 1 ? "" : "s"} ago`;
36
40
  };
37
41
 
42
+ const readFileSnippet = (rootDir, relativePath, startLine, endLine) => {
43
+ try {
44
+ const fullPath = path.join(rootDir, String(relativePath || ""));
45
+ const content = fs.readFileSync(fullPath, "utf-8");
46
+ const lines = content.split("\n");
47
+ const start = Math.max(0, (startLine || 1) - 1);
48
+ const end = endLine && endLine >= startLine ? Math.min(lines.length, endLine) : start + 1;
49
+ const cappedEnd = Math.min(end, start + kMaxSnippetLines);
50
+ return {
51
+ text: lines.slice(start, cappedEnd).join("\n"),
52
+ startLine: start + 1,
53
+ endLine: start + (cappedEnd - start),
54
+ truncated: cappedEnd < end,
55
+ totalFileLines: lines.length,
56
+ };
57
+ } catch {
58
+ return null;
59
+ }
60
+ };
61
+
62
+ const captureEvidenceSnippets = (cards, rootDir) => {
63
+ for (const card of cards) {
64
+ if (!Array.isArray(card.evidence)) continue;
65
+ for (const item of card.evidence) {
66
+ if (!item || item.type !== "path" || !item.path || !item.startLine) continue;
67
+ const snippet = readFileSnippet(rootDir, item.path, item.startLine, item.endLine);
68
+ if (snippet) item.snippet = snippet;
69
+ }
70
+ }
71
+ };
72
+
38
73
  const buildDoctorSessionKey = (runId) => `agent:main:doctor:${Number(runId || 0)}`;
39
74
  const buildDoctorSessionId = (runId) => buildDoctorSessionKey(runId);
40
75
  const buildDoctorIdempotencyKey = (runId) => `doctor-run-${Number(runId || 0)}`;
@@ -156,11 +191,20 @@ const createDoctorService = ({
156
191
 
157
192
  const executeDoctorRun = async (runId) => {
158
193
  try {
194
+ const allCards = listDoctorCards();
195
+ const resolvedCards = allCards
196
+ .filter((card) => card.status === "dismissed" || card.status === "fixed")
197
+ .map((card) => ({
198
+ status: card.status,
199
+ title: card.title || "",
200
+ category: card.category || "",
201
+ }));
159
202
  const prompt = buildDoctorPrompt({
160
203
  workspaceRoot,
161
204
  managedRoot,
162
205
  protectedPaths,
163
206
  lockedPaths,
207
+ resolvedCards,
164
208
  promptVersion: kDoctorPromptVersion,
165
209
  });
166
210
  const gatewayTimeoutMs = kDoctorRunTimeoutMs + 30000;
@@ -204,6 +248,7 @@ const createDoctorService = ({
204
248
  console.error(`[doctor] run ${runId} stderr end`);
205
249
  throw error;
206
250
  }
251
+ captureEvidenceSnippets(normalizedResult.cards, workspaceRoot);
207
252
  insertDoctorCards({
208
253
  runId,
209
254
  cards: normalizedResult.cards,
@@ -300,6 +345,7 @@ const createDoctorService = ({
300
345
  throw new Error("Doctor import requires raw output");
301
346
  }
302
347
  const normalizedResult = normalizeDoctorResult(normalizedRawOutput);
348
+ captureEvidenceSnippets(normalizedResult.cards, workspaceRoot);
303
349
  const workspaceSnapshot = getCurrentWorkspaceSnapshot();
304
350
  const runId = createDoctorRun({
305
351
  status: kDoctorRunStatus.completed,
@@ -4,6 +4,17 @@ const crypto = require("crypto");
4
4
 
5
5
  const kIgnoredDirectoryNames = new Set([".git", "node_modules"]);
6
6
 
7
+ const kContentFileExtensions = new Set([
8
+ ".md", ".json", ".js", ".ts", ".jsx", ".tsx", ".yaml", ".yml",
9
+ ".txt", ".sh", ".css", ".html", ".xml", ".toml", ".ini", ".cfg",
10
+ ".py", ".rb", ".go", ".rs", ".java", ".c", ".cpp", ".h",
11
+ ]);
12
+
13
+ const isContentFile = (relativePath = "") => {
14
+ const ext = path.extname(String(relativePath || "")).toLowerCase();
15
+ return kContentFileExtensions.has(ext);
16
+ };
17
+
7
18
  const hashFile = (filePath) => {
8
19
  const buffer = fs.readFileSync(filePath);
9
20
  return crypto.createHash("sha256").update(buffer).digest("hex");
@@ -34,11 +45,21 @@ const buildWorkspaceManifest = (rootDir) => {
34
45
  const normalizedRootDir = path.resolve(String(rootDir || ""));
35
46
  const files = walkFiles(normalizedRootDir);
36
47
  return files.reduce((manifest, filePath) => {
37
- manifest[normalizeRelativePath(normalizedRootDir, filePath)] = hashFile(filePath);
48
+ const stat = fs.statSync(filePath);
49
+ manifest[normalizeRelativePath(normalizedRootDir, filePath)] = {
50
+ hash: hashFile(filePath),
51
+ size: stat.size,
52
+ };
38
53
  return manifest;
39
54
  }, {});
40
55
  };
41
56
 
57
+ const getManifestEntryHash = (entry) =>
58
+ typeof entry === "object" && entry !== null ? String(entry.hash || "") : String(entry || "");
59
+
60
+ const getManifestEntrySize = (entry) =>
61
+ typeof entry === "object" && entry !== null ? Number(entry.size || 0) : 0;
62
+
42
63
  const computeWorkspaceFingerprintFromManifest = (manifest = {}) => {
43
64
  const hash = crypto.createHash("sha256");
44
65
  const entries = Object.entries(manifest).sort(([leftPath], [rightPath]) =>
@@ -46,10 +67,10 @@ const computeWorkspaceFingerprintFromManifest = (manifest = {}) => {
46
67
  );
47
68
 
48
69
  hash.update("workspace-fingerprint-v1");
49
- for (const [relativePath, fileHash] of entries) {
70
+ for (const [relativePath, entry] of entries) {
50
71
  hash.update(relativePath);
51
72
  hash.update("\0");
52
- hash.update(fileHash);
73
+ hash.update(getManifestEntryHash(entry));
53
74
  hash.update("\0");
54
75
  }
55
76
 
@@ -84,6 +105,20 @@ const getPathChangeWeight = (relativePath = "") => {
84
105
  return 1;
85
106
  };
86
107
 
108
+ const kByteDeltaSmallThreshold = 100;
109
+ const kByteDeltaSignificantThreshold = 500;
110
+
111
+ const getModifiedFileScore = (relativePath, previousEntry, currentEntry) => {
112
+ if (!isContentFile(relativePath)) return 1;
113
+ const previousSize = getManifestEntrySize(previousEntry);
114
+ const currentSize = getManifestEntrySize(currentEntry);
115
+ if (!previousSize && !currentSize) return getPathChangeWeight(relativePath);
116
+ const byteDelta = Math.abs(currentSize - previousSize);
117
+ if (byteDelta < kByteDeltaSmallThreshold) return 1;
118
+ if (byteDelta < kByteDeltaSignificantThreshold) return 2;
119
+ return getPathChangeWeight(relativePath);
120
+ };
121
+
87
122
  const calculateWorkspaceDelta = ({ previousManifest = {}, currentManifest = {} } = {}) => {
88
123
  const previousPaths = Object.keys(previousManifest);
89
124
  const currentPaths = Object.keys(currentManifest);
@@ -100,19 +135,23 @@ const calculateWorkspaceDelta = ({ previousManifest = {}, currentManifest = {} }
100
135
  };
101
136
 
102
137
  for (const relativePath of allPaths) {
103
- const previousHash = previousManifest[relativePath] || "";
104
- const currentHash = currentManifest[relativePath] || "";
138
+ const previousEntry = previousManifest[relativePath];
139
+ const currentEntry = currentManifest[relativePath];
140
+ const previousHash = getManifestEntryHash(previousEntry);
141
+ const currentHash = getManifestEntryHash(currentEntry);
105
142
  if (!previousHash && currentHash) {
106
143
  changeSummary.addedFilesCount += 1;
144
+ changeSummary.deltaScore += getPathChangeWeight(relativePath);
107
145
  } else if (previousHash && !currentHash) {
108
146
  changeSummary.removedFilesCount += 1;
147
+ changeSummary.deltaScore += getPathChangeWeight(relativePath);
109
148
  } else if (previousHash !== currentHash) {
110
149
  changeSummary.modifiedFilesCount += 1;
150
+ changeSummary.deltaScore += getModifiedFileScore(relativePath, previousEntry, currentEntry);
111
151
  } else {
112
152
  continue;
113
153
  }
114
154
  changeSummary.changedFilesCount += 1;
115
- changeSummary.deltaScore += getPathChangeWeight(relativePath);
116
155
  changeSummary.changedPaths.push(relativePath);
117
156
  }
118
157
 
@@ -123,4 +162,5 @@ module.exports = {
123
162
  calculateWorkspaceDelta,
124
163
  computeWorkspaceFingerprintFromManifest,
125
164
  computeWorkspaceSnapshot,
165
+ isContentFile,
126
166
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.4.6-beta.1",
3
+ "version": "0.4.6-beta.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },