@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.
- package/lib/public/css/explorer.css +4 -7
- package/lib/public/css/shell.css +14 -2
- package/lib/public/css/theme.css +4 -0
- package/lib/public/js/app.js +62 -38
- package/lib/public/js/components/doctor/findings-list.js +190 -12
- package/lib/public/js/components/doctor/fix-card-modal.js +20 -73
- package/lib/public/js/components/doctor/helpers.js +7 -27
- package/lib/public/js/components/doctor/index.js +5 -5
- package/lib/public/js/components/file-tree.js +1 -1
- package/lib/public/js/components/file-viewer/constants.js +4 -3
- package/lib/public/js/components/file-viewer/editor-surface.js +1 -0
- package/lib/public/js/components/file-viewer/index.js +4 -0
- package/lib/public/js/components/file-viewer/storage.js +1 -4
- package/lib/public/js/components/file-viewer/use-editor-selection-restore.js +130 -17
- package/lib/public/js/components/file-viewer/use-file-viewer.js +4 -0
- package/lib/public/js/components/google/gmail-setup-wizard.js +18 -51
- package/lib/public/js/components/google/gmail-watch-toggle.js +4 -1
- package/lib/public/js/components/onboarding/use-welcome-storage.js +2 -1
- package/lib/public/js/components/telegram-workspace/index.js +5 -2
- package/lib/public/js/hooks/useAgentSessions.js +128 -0
- package/lib/public/js/lib/browse-draft-state.js +9 -13
- package/lib/public/js/lib/storage-keys.js +28 -0
- package/lib/public/js/lib/ui-settings.js +3 -1
- package/lib/server/doctor/normalize.js +57 -23
- package/lib/server/doctor/prompt.js +30 -4
- package/lib/server/doctor/service.js +46 -0
- package/lib/server/doctor/workspace-fingerprint.js +46 -6
- package/package.json +1 -1
|
@@ -89,42 +89,76 @@ const normalizeCardStatus = (value) => {
|
|
|
89
89
|
return kDoctorCardStatus.open;
|
|
90
90
|
};
|
|
91
91
|
|
|
92
|
-
const
|
|
93
|
-
if (
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
127
|
-
|
|
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": [
|
|
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
|
-
|
|
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,
|
|
70
|
+
for (const [relativePath, entry] of entries) {
|
|
50
71
|
hash.update(relativePath);
|
|
51
72
|
hash.update("\0");
|
|
52
|
-
hash.update(
|
|
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
|
|
104
|
-
const
|
|
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
|
};
|