@clawstore/clawstore 1.0.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.
- package/index.ts +46 -0
- package/openclaw.plugin.json +24 -0
- package/package.json +30 -0
- package/src/__tests__/cli-sim.test.ts +303 -0
- package/src/__tests__/e2e.test.ts +186 -0
- package/src/cli.ts +513 -0
- package/src/commands.ts +196 -0
- package/src/constants.ts +80 -0
- package/src/core/agent-manager.ts +327 -0
- package/src/core/package-installer.ts +390 -0
- package/src/core/packager.ts +229 -0
- package/src/core/skill-installer.ts +221 -0
- package/src/core/store-client.ts +140 -0
- package/src/core/workspace.ts +269 -0
- package/src/types.ts +167 -0
- package/src/utils/checksum.ts +17 -0
- package/src/utils/output.ts +76 -0
- package/src/utils/zip.ts +55 -0
- package/tsconfig.json +23 -0
- package/typings/openclaw-plugin-sdk/core.d.ts +134 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { access, readFile, writeFile, mkdir, cp } from "node:fs/promises";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
import { exec } from "node:child_process";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import semver from "semver";
|
|
7
|
+
import { OPENCLAW_HOME, DEFAULT_AGENT, agentSkillRegistryFile } from "../constants.js";
|
|
8
|
+
import type { SkillDependency, SkillRegistry, InstalledSkillRecord } from "../types.js";
|
|
9
|
+
import { resolveWorkspace } from "./workspace.js";
|
|
10
|
+
|
|
11
|
+
const execAsync = promisify(exec);
|
|
12
|
+
|
|
13
|
+
async function exists(p: string): Promise<boolean> {
|
|
14
|
+
try {
|
|
15
|
+
await access(p);
|
|
16
|
+
return true;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ─── Skill Registry ─────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
async function loadSkillRegistry(agent: string = DEFAULT_AGENT): Promise<SkillRegistry> {
|
|
25
|
+
const file = agentSkillRegistryFile(agent);
|
|
26
|
+
if (!(await exists(file))) {
|
|
27
|
+
return { skills: {} };
|
|
28
|
+
}
|
|
29
|
+
const raw = await readFile(file, "utf-8");
|
|
30
|
+
return JSON.parse(raw) as SkillRegistry;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function saveSkillRegistry(reg: SkillRegistry, agent: string = DEFAULT_AGENT): Promise<void> {
|
|
34
|
+
const file = agentSkillRegistryFile(agent);
|
|
35
|
+
await mkdir(dirname(file), { recursive: true });
|
|
36
|
+
await writeFile(file, JSON.stringify(reg, null, 2), "utf-8");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── Version helpers ────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
function getInstalledSkillVersion(skillDir: string): string | null {
|
|
42
|
+
try {
|
|
43
|
+
const pkgPath = join(skillDir, "package.json");
|
|
44
|
+
if (!existsSync(pkgPath)) return null;
|
|
45
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
46
|
+
return pkg.version ?? null;
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function versionSatisfies(installed: string | null, required: string): boolean {
|
|
53
|
+
if (!installed) return false;
|
|
54
|
+
if (required === "latest" || required === "*") return true;
|
|
55
|
+
return semver.satisfies(installed, required);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ─── Core: check + install skills ───────────────────────────────────
|
|
59
|
+
|
|
60
|
+
export interface SkillCheckResult {
|
|
61
|
+
skillId: string;
|
|
62
|
+
required: boolean;
|
|
63
|
+
requiredVersion: string;
|
|
64
|
+
installedVersion: string | null;
|
|
65
|
+
action: "skip" | "install" | "upgrade";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function checkSkillDependencies(skills: SkillDependency[]): SkillCheckResult[] {
|
|
69
|
+
const skillsDir = join(OPENCLAW_HOME, "skills");
|
|
70
|
+
const results: SkillCheckResult[] = [];
|
|
71
|
+
|
|
72
|
+
for (const skill of skills) {
|
|
73
|
+
const dir = join(skillsDir, skill.id);
|
|
74
|
+
const installedVersion = getInstalledSkillVersion(dir);
|
|
75
|
+
const dirExists = existsSync(dir);
|
|
76
|
+
|
|
77
|
+
let action: "skip" | "install" | "upgrade";
|
|
78
|
+
if (!dirExists) {
|
|
79
|
+
action = "install";
|
|
80
|
+
} else if (!versionSatisfies(installedVersion, skill.version)) {
|
|
81
|
+
action = "upgrade";
|
|
82
|
+
} else {
|
|
83
|
+
action = "skip";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
results.push({
|
|
87
|
+
skillId: skill.id,
|
|
88
|
+
required: skill.required,
|
|
89
|
+
requiredVersion: skill.version,
|
|
90
|
+
installedVersion,
|
|
91
|
+
action,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return results;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Try to install a skill from the bundled package skills/ directory.
|
|
100
|
+
*/
|
|
101
|
+
async function installSkillFromBundle(
|
|
102
|
+
skillId: string,
|
|
103
|
+
packagePath: string,
|
|
104
|
+
agent: string = DEFAULT_AGENT,
|
|
105
|
+
): Promise<{ ok: boolean; message: string }> {
|
|
106
|
+
const bundledFile = join(packagePath, "skills", `${skillId}.md`);
|
|
107
|
+
const bundledDir = join(packagePath, "skills", skillId);
|
|
108
|
+
|
|
109
|
+
if (await exists(bundledFile)) {
|
|
110
|
+
const workspace = await resolveWorkspace(agent);
|
|
111
|
+
const destDir = join(workspace, "skills");
|
|
112
|
+
await mkdir(destDir, { recursive: true });
|
|
113
|
+
await cp(bundledFile, join(destDir, `${skillId}.md`), { force: true });
|
|
114
|
+
return { ok: true, message: `installed from bundle (${skillId}.md)` };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (await exists(bundledDir)) {
|
|
118
|
+
const workspace = await resolveWorkspace(agent);
|
|
119
|
+
const destDir = join(workspace, "skills", skillId);
|
|
120
|
+
await cp(bundledDir, destDir, { recursive: true, force: true });
|
|
121
|
+
return { ok: true, message: `installed from bundle (${skillId}/)` };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { ok: false, message: "not found in bundle" };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Install or upgrade a single skill.
|
|
129
|
+
*
|
|
130
|
+
* Strategy:
|
|
131
|
+
* 1. Try bundled skills/ in the package directory first
|
|
132
|
+
* 2. Fall back to `openclaw plugins install <skill-id>` (npm registry)
|
|
133
|
+
*/
|
|
134
|
+
export async function installSkill(
|
|
135
|
+
skillId: string,
|
|
136
|
+
version: string,
|
|
137
|
+
packagePath?: string,
|
|
138
|
+
agent: string = DEFAULT_AGENT,
|
|
139
|
+
): Promise<{ ok: boolean; message: string }> {
|
|
140
|
+
if (packagePath) {
|
|
141
|
+
const bundled = await installSkillFromBundle(skillId, packagePath, agent);
|
|
142
|
+
if (bundled.ok) {
|
|
143
|
+
const reg = await loadSkillRegistry(agent);
|
|
144
|
+
reg.skills[skillId] = {
|
|
145
|
+
skill_id: skillId,
|
|
146
|
+
version,
|
|
147
|
+
installed_at: new Date().toISOString(),
|
|
148
|
+
installed_by: "clawstore",
|
|
149
|
+
source: `bundle:${packagePath}`,
|
|
150
|
+
};
|
|
151
|
+
await saveSkillRegistry(reg, agent);
|
|
152
|
+
return bundled;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const spec = version && version !== "latest" ? `${skillId}@${version}` : skillId;
|
|
157
|
+
try {
|
|
158
|
+
const cmd = `openclaw plugins install ${JSON.stringify(spec)}`;
|
|
159
|
+
const { stdout, stderr } = await execAsync(cmd, { timeout: 60_000 });
|
|
160
|
+
const output = stdout || stderr;
|
|
161
|
+
|
|
162
|
+
const reg = await loadSkillRegistry(agent);
|
|
163
|
+
reg.skills[skillId] = {
|
|
164
|
+
skill_id: skillId,
|
|
165
|
+
version,
|
|
166
|
+
installed_at: new Date().toISOString(),
|
|
167
|
+
installed_by: "clawstore",
|
|
168
|
+
source: `npm:${spec}`,
|
|
169
|
+
};
|
|
170
|
+
await saveSkillRegistry(reg, agent);
|
|
171
|
+
|
|
172
|
+
return { ok: true, message: output.trim() || `installed ${spec}` };
|
|
173
|
+
} catch (err) {
|
|
174
|
+
return {
|
|
175
|
+
ok: false,
|
|
176
|
+
message: `failed to install ${spec}: ${(err as Error).message}`,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Install all required skills for an agent.
|
|
183
|
+
*/
|
|
184
|
+
export async function installRequiredSkills(
|
|
185
|
+
skills: SkillDependency[],
|
|
186
|
+
log?: (msg: string) => void,
|
|
187
|
+
packagePath?: string,
|
|
188
|
+
agent: string = DEFAULT_AGENT,
|
|
189
|
+
): Promise<{ allOk: boolean; results: Array<SkillCheckResult & { installResult?: { ok: boolean; message: string } }> }> {
|
|
190
|
+
const checks = checkSkillDependencies(skills);
|
|
191
|
+
let allOk = true;
|
|
192
|
+
|
|
193
|
+
const results: Array<SkillCheckResult & { installResult?: { ok: boolean; message: string } }> = [];
|
|
194
|
+
|
|
195
|
+
for (const check of checks) {
|
|
196
|
+
if (check.action === "skip") {
|
|
197
|
+
log?.(` - ${check.skillId} ${check.requiredVersion} ... already installed`);
|
|
198
|
+
results.push(check);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!check.required) {
|
|
203
|
+
log?.(` - ${check.skillId} ${check.requiredVersion} ... SKIPPED (optional)`);
|
|
204
|
+
log?.(` Run: openclaw plugins install ${check.skillId}`);
|
|
205
|
+
results.push(check);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
log?.(` - ${check.skillId} ${check.requiredVersion} ... installing`);
|
|
210
|
+
const installResult = await installSkill(check.skillId, check.requiredVersion, packagePath, agent);
|
|
211
|
+
if (installResult.ok) {
|
|
212
|
+
log?.(` ${installResult.message}`);
|
|
213
|
+
} else {
|
|
214
|
+
log?.(` FAILED: ${installResult.message}`);
|
|
215
|
+
allOk = false;
|
|
216
|
+
}
|
|
217
|
+
results.push({ ...check, installResult });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return { allOk, results };
|
|
221
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
StoreSearchResult,
|
|
3
|
+
StoreAgentDetail,
|
|
4
|
+
StoreAgentSummary,
|
|
5
|
+
ClawstoreConfig,
|
|
6
|
+
} from "../types.js";
|
|
7
|
+
import { DEFAULT_API_BASE_URL } from "../constants.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* HTTP client for the Clawstore Web API.
|
|
11
|
+
*
|
|
12
|
+
* All methods are async and use the native `fetch` API.
|
|
13
|
+
* When the Clawstore backend is not yet deployed, methods throw
|
|
14
|
+
* descriptive errors so the CLI can gracefully inform the user.
|
|
15
|
+
*/
|
|
16
|
+
export class StoreClient {
|
|
17
|
+
private baseUrl: string;
|
|
18
|
+
private authToken: string | null = null;
|
|
19
|
+
|
|
20
|
+
constructor(config?: Partial<ClawstoreConfig>) {
|
|
21
|
+
this.baseUrl = (config?.apiBaseUrl ?? DEFAULT_API_BASE_URL).replace(/\/+$/, "");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
setAuthToken(token: string): void {
|
|
25
|
+
this.authToken = token;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private headers(): Record<string, string> {
|
|
29
|
+
const h: Record<string, string> = {
|
|
30
|
+
"Content-Type": "application/json",
|
|
31
|
+
"User-Agent": "clawstore-plugin/1.0.0",
|
|
32
|
+
};
|
|
33
|
+
if (this.authToken) {
|
|
34
|
+
h["Authorization"] = `Bearer ${this.authToken}`;
|
|
35
|
+
}
|
|
36
|
+
return h;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private async request<T>(path: string, init?: RequestInit): Promise<T> {
|
|
40
|
+
const url = `${this.baseUrl}${path}`;
|
|
41
|
+
const res = await fetch(url, {
|
|
42
|
+
...init,
|
|
43
|
+
headers: { ...this.headers(), ...(init?.headers as Record<string, string> ?? {}) },
|
|
44
|
+
signal: AbortSignal.timeout(15_000),
|
|
45
|
+
});
|
|
46
|
+
if (!res.ok) {
|
|
47
|
+
const body = await res.text().catch(() => "");
|
|
48
|
+
throw new Error(`API ${res.status}: ${res.statusText}${body ? ` — ${body}` : ""}`);
|
|
49
|
+
}
|
|
50
|
+
return res.json() as Promise<T>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ─── Search ─────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
async search(query: string, opts?: {
|
|
56
|
+
category?: string;
|
|
57
|
+
sort?: string;
|
|
58
|
+
limit?: number;
|
|
59
|
+
}): Promise<StoreSearchResult> {
|
|
60
|
+
const params = new URLSearchParams();
|
|
61
|
+
if (query) params.set("q", query);
|
|
62
|
+
if (opts?.category) params.set("category", opts.category);
|
|
63
|
+
if (opts?.sort) params.set("sort", opts.sort);
|
|
64
|
+
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
65
|
+
return this.request<StoreSearchResult>(`/agents?${params}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─── Show detail ────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
async getAgentDetail(agentId: string): Promise<StoreAgentDetail> {
|
|
71
|
+
return this.request<StoreAgentDetail>(`/agents/${agentId}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── Download URL ───────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
async getDownloadUrl(agentId: string): Promise<string> {
|
|
77
|
+
const res = await this.request<{ url: string }>(`/agents/${agentId}/download`);
|
|
78
|
+
return res.url;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ─── Publish draft ──────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
async publishDraft(zipBuffer: Buffer, metadata: {
|
|
84
|
+
name: string;
|
|
85
|
+
description: string;
|
|
86
|
+
category: string;
|
|
87
|
+
price: number;
|
|
88
|
+
currency: string;
|
|
89
|
+
}): Promise<{ draftId: string; editUrl: string }> {
|
|
90
|
+
const formData = new FormData();
|
|
91
|
+
formData.append("file", new Blob([zipBuffer]), "agent.zip");
|
|
92
|
+
formData.append("metadata", JSON.stringify(metadata));
|
|
93
|
+
|
|
94
|
+
const url = `${this.baseUrl}/agents/drafts`;
|
|
95
|
+
const res = await fetch(url, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers: {
|
|
98
|
+
...(this.authToken ? { Authorization: `Bearer ${this.authToken}` } : {}),
|
|
99
|
+
"User-Agent": "clawstore-plugin/1.0.0",
|
|
100
|
+
},
|
|
101
|
+
body: formData,
|
|
102
|
+
signal: AbortSignal.timeout(60_000),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (!res.ok) {
|
|
106
|
+
throw new Error(`Publish failed: HTTP ${res.status}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return res.json() as Promise<{ draftId: string; editUrl: string }>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ─── Auth ───────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
async login(email: string, password: string): Promise<{ token: string; user: { id: string; name: string } }> {
|
|
115
|
+
return this.request<{ token: string; user: { id: string; name: string } }>("/auth/login", {
|
|
116
|
+
method: "POST",
|
|
117
|
+
body: JSON.stringify({ email, password }),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async whoami(): Promise<{ id: string; name: string; email: string } | null> {
|
|
122
|
+
if (!this.authToken) return null;
|
|
123
|
+
try {
|
|
124
|
+
return await this.request<{ id: string; name: string; email: string }>("/auth/me");
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ─── Health ─────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
async healthCheck(): Promise<boolean> {
|
|
133
|
+
try {
|
|
134
|
+
await this.request<{ status: string }>("/health");
|
|
135
|
+
return true;
|
|
136
|
+
} catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { readFile, access, readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import {
|
|
5
|
+
OPENCLAW_HOME,
|
|
6
|
+
CLAWSTORE_DIR,
|
|
7
|
+
DEFAULT_AGENT,
|
|
8
|
+
REQUIRED_FILES,
|
|
9
|
+
OPTIONAL_FILES,
|
|
10
|
+
agentRegistryFile,
|
|
11
|
+
agentSkillRegistryFile,
|
|
12
|
+
agentBackupDir,
|
|
13
|
+
} from "../constants.js";
|
|
14
|
+
import type { DiagnosticCheck, DiagnosticReport } from "../types.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolve the workspace directory for a given OpenClaw agent.
|
|
18
|
+
*
|
|
19
|
+
* @param agent OpenClaw agent name (default "main")
|
|
20
|
+
*
|
|
21
|
+
* Resolution priority:
|
|
22
|
+
* 1. Per-agent workspace in openclaw.json (`agents.<agent>.workspace` or
|
|
23
|
+
* `agents.entries.<agent>.workspace`)
|
|
24
|
+
* 2. For "main": `agents.defaults.workspace` or `agent.workspace`
|
|
25
|
+
* 3. OPENCLAW_PROFILE env -> `~/.openclaw/workspace-{profile}` (main only)
|
|
26
|
+
* 4. Default: `~/.openclaw/workspace` for main,
|
|
27
|
+
* `~/.openclaw/workspace-{agent}` for others
|
|
28
|
+
*/
|
|
29
|
+
export async function resolveWorkspace(agent?: string): Promise<string> {
|
|
30
|
+
const target = agent ?? DEFAULT_AGENT;
|
|
31
|
+
const configFile = join(OPENCLAW_HOME, "openclaw.json");
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const raw = await readFile(configFile, "utf-8");
|
|
35
|
+
const config = JSON.parse(raw);
|
|
36
|
+
|
|
37
|
+
let workspace: string | undefined;
|
|
38
|
+
|
|
39
|
+
if (target !== DEFAULT_AGENT) {
|
|
40
|
+
workspace =
|
|
41
|
+
config?.agents?.[target]?.workspace ??
|
|
42
|
+
config?.agents?.entries?.[target]?.workspace;
|
|
43
|
+
} else {
|
|
44
|
+
workspace =
|
|
45
|
+
config?.agents?.defaults?.workspace ??
|
|
46
|
+
config?.agent?.workspace;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (workspace && typeof workspace === "string") {
|
|
50
|
+
return expandHome(workspace);
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
// config not found or invalid — fall through
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (target === DEFAULT_AGENT) {
|
|
57
|
+
const profile = process.env.OPENCLAW_PROFILE ?? "default";
|
|
58
|
+
if (profile !== "default") {
|
|
59
|
+
return join(OPENCLAW_HOME, `workspace-${profile}`);
|
|
60
|
+
}
|
|
61
|
+
return join(OPENCLAW_HOME, "workspace");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return join(OPENCLAW_HOME, `workspace-${target}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function expandHome(p: string): string {
|
|
68
|
+
if (p.startsWith("~/") || p.startsWith("~\\")) {
|
|
69
|
+
return join(homedir(), p.slice(2));
|
|
70
|
+
}
|
|
71
|
+
if (p === "~") return homedir();
|
|
72
|
+
return p;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* List all OpenClaw agents that have at least one Persona installed via
|
|
77
|
+
* Clawstore. Returns agent names (e.g. ["main", "sales"]).
|
|
78
|
+
*/
|
|
79
|
+
export async function listClawstoreAgents(): Promise<string[]> {
|
|
80
|
+
const agents: string[] = [];
|
|
81
|
+
|
|
82
|
+
// "main" agent uses the root-level registry file
|
|
83
|
+
if (await exists(agentRegistryFile(DEFAULT_AGENT))) {
|
|
84
|
+
agents.push(DEFAULT_AGENT);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Non-main agents live under clawstore/agents/<name>/
|
|
88
|
+
const agentsDir = join(CLAWSTORE_DIR, "agents");
|
|
89
|
+
if (await exists(agentsDir)) {
|
|
90
|
+
const entries = await readdir(agentsDir, { withFileTypes: true });
|
|
91
|
+
for (const e of entries) {
|
|
92
|
+
if (e.isDirectory() && e.name !== DEFAULT_AGENT) {
|
|
93
|
+
if (await exists(agentRegistryFile(e.name))) {
|
|
94
|
+
agents.push(e.name);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return agents;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function exists(p: string): Promise<boolean> {
|
|
104
|
+
try {
|
|
105
|
+
await access(p);
|
|
106
|
+
return true;
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function dirItemCount(p: string): Promise<number> {
|
|
113
|
+
try {
|
|
114
|
+
const entries = await readdir(p);
|
|
115
|
+
return entries.length;
|
|
116
|
+
} catch {
|
|
117
|
+
return -1;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Run a full diagnostic for a specific OpenClaw agent.
|
|
123
|
+
*/
|
|
124
|
+
export async function diagnose(apiBaseUrl?: string, agent?: string): Promise<DiagnosticReport> {
|
|
125
|
+
const target = agent ?? DEFAULT_AGENT;
|
|
126
|
+
const checks: DiagnosticCheck[] = [];
|
|
127
|
+
let allOk = true;
|
|
128
|
+
const workspace = await resolveWorkspace(target);
|
|
129
|
+
const registryFile = agentRegistryFile(target);
|
|
130
|
+
const backupDir = agentBackupDir(target);
|
|
131
|
+
|
|
132
|
+
if (target !== DEFAULT_AGENT) {
|
|
133
|
+
checks.push({
|
|
134
|
+
label: "Target agent",
|
|
135
|
+
status: "ok",
|
|
136
|
+
detail: target,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 1. OpenClaw home
|
|
141
|
+
const homeOk = await exists(OPENCLAW_HOME);
|
|
142
|
+
checks.push({
|
|
143
|
+
label: "OpenClaw home",
|
|
144
|
+
path: OPENCLAW_HOME,
|
|
145
|
+
status: homeOk ? "ok" : "fail",
|
|
146
|
+
detail: homeOk ? undefined : "NOT FOUND",
|
|
147
|
+
});
|
|
148
|
+
if (!homeOk) allOk = false;
|
|
149
|
+
|
|
150
|
+
// 2. Workspace
|
|
151
|
+
const wsOk = await exists(workspace);
|
|
152
|
+
checks.push({
|
|
153
|
+
label: "Workspace",
|
|
154
|
+
path: workspace,
|
|
155
|
+
status: wsOk ? "ok" : "fail",
|
|
156
|
+
detail: wsOk ? undefined : "NOT FOUND",
|
|
157
|
+
});
|
|
158
|
+
if (!wsOk) allOk = false;
|
|
159
|
+
|
|
160
|
+
// 3. Workspace files
|
|
161
|
+
for (const f of [...REQUIRED_FILES, ...OPTIONAL_FILES]) {
|
|
162
|
+
const fp = join(workspace, f);
|
|
163
|
+
const found = await exists(fp);
|
|
164
|
+
checks.push({
|
|
165
|
+
label: `Workspace file: ${f}`,
|
|
166
|
+
path: fp,
|
|
167
|
+
status: found ? "ok" : "warn",
|
|
168
|
+
detail: found ? "found" : "missing",
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 4. Registry
|
|
173
|
+
const regOk = await exists(registryFile);
|
|
174
|
+
if (regOk) {
|
|
175
|
+
try {
|
|
176
|
+
const raw = await readFile(registryFile, "utf-8");
|
|
177
|
+
const reg = JSON.parse(raw);
|
|
178
|
+
const count = Object.keys(reg.agents ?? {}).length;
|
|
179
|
+
checks.push({
|
|
180
|
+
label: "Agent registry",
|
|
181
|
+
path: registryFile,
|
|
182
|
+
status: "ok",
|
|
183
|
+
detail: `${count} agent(s) registered`,
|
|
184
|
+
});
|
|
185
|
+
} catch {
|
|
186
|
+
checks.push({
|
|
187
|
+
label: "Agent registry",
|
|
188
|
+
path: registryFile,
|
|
189
|
+
status: "warn",
|
|
190
|
+
detail: "exists but cannot be parsed",
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
checks.push({
|
|
195
|
+
label: "Agent registry",
|
|
196
|
+
path: registryFile,
|
|
197
|
+
status: "ok",
|
|
198
|
+
detail: "not created yet (OK for first run)",
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 5. Skills directory
|
|
203
|
+
const skillsDir = join(workspace, "skills");
|
|
204
|
+
const skillCount = await dirItemCount(skillsDir);
|
|
205
|
+
checks.push({
|
|
206
|
+
label: "Skills directory",
|
|
207
|
+
path: skillsDir,
|
|
208
|
+
status: skillCount >= 0 ? "ok" : "warn",
|
|
209
|
+
detail: skillCount >= 0 ? `${skillCount} skill(s)` : "NOT FOUND",
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// 6. Backup directory
|
|
213
|
+
const backupCount = await dirItemCount(backupDir);
|
|
214
|
+
checks.push({
|
|
215
|
+
label: "Backup directory",
|
|
216
|
+
path: backupDir,
|
|
217
|
+
status: "ok",
|
|
218
|
+
detail: backupCount >= 0 ? `${backupCount} backup(s)` : "will be created on first install",
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// 7. Cache directory
|
|
222
|
+
const cacheCount = await dirItemCount(join(CLAWSTORE_DIR, "cache"));
|
|
223
|
+
checks.push({
|
|
224
|
+
label: "Cache directory",
|
|
225
|
+
path: join(CLAWSTORE_DIR, "cache"),
|
|
226
|
+
status: "ok",
|
|
227
|
+
detail: cacheCount >= 0 ? `${cacheCount} item(s)` : "will be created on first install",
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// 8. API connectivity (optional)
|
|
231
|
+
if (apiBaseUrl) {
|
|
232
|
+
try {
|
|
233
|
+
const res = await fetch(`${apiBaseUrl}/health`, { signal: AbortSignal.timeout(5000) });
|
|
234
|
+
checks.push({
|
|
235
|
+
label: "Clawstore API",
|
|
236
|
+
status: res.ok ? "ok" : "warn",
|
|
237
|
+
detail: res.ok ? "reachable" : `HTTP ${res.status}`,
|
|
238
|
+
});
|
|
239
|
+
} catch (err) {
|
|
240
|
+
checks.push({
|
|
241
|
+
label: "Clawstore API",
|
|
242
|
+
status: "warn",
|
|
243
|
+
detail: `unreachable (${(err as Error).message})`,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return { checks, allOk };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Format a diagnostic report for CLI output.
|
|
253
|
+
*/
|
|
254
|
+
export function formatDiagnosticReport(report: DiagnosticReport): string {
|
|
255
|
+
const lines: string[] = ["\n Clawstore Doctor\n"];
|
|
256
|
+
for (const c of report.checks) {
|
|
257
|
+
const icon = c.status === "ok" ? "OK" : c.status === "warn" ? "WARN" : "FAIL";
|
|
258
|
+
const pathPart = c.path ? ` ${c.path}` : "";
|
|
259
|
+
const detailPart = c.detail ? ` (${c.detail})` : "";
|
|
260
|
+
lines.push(` ${c.label}:${pathPart} ... ${icon}${detailPart}`);
|
|
261
|
+
}
|
|
262
|
+
lines.push("");
|
|
263
|
+
lines.push(
|
|
264
|
+
report.allOk
|
|
265
|
+
? " All checks passed.\n"
|
|
266
|
+
: " Some issues found. Run `openclaw setup` to initialize missing directories.\n"
|
|
267
|
+
);
|
|
268
|
+
return lines.join("\n");
|
|
269
|
+
}
|