@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.
@@ -0,0 +1,196 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
2
+ import type { ClawstoreConfig } from "./types.js";
3
+ import { StoreClient } from "./core/store-client.js";
4
+ import { DEFAULT_AGENT } from "./constants.js";
5
+ import {
6
+ listInstalledAgents,
7
+ getInstalledAgent,
8
+ enableAgent,
9
+ disableAgent,
10
+ } from "./core/agent-manager.js";
11
+ import { installAgent } from "./core/package-installer.js";
12
+ import { diagnose } from "./core/workspace.js";
13
+
14
+ /**
15
+ * Register the `/clawstore` slash command.
16
+ *
17
+ * Slash commands return short text and are designed for quick in-session
18
+ * operations. Use `--agent <name>` anywhere in args to target a
19
+ * non-default OpenClaw agent.
20
+ */
21
+ export function registerClawstoreCommand(api: OpenClawPluginApi): void {
22
+ const rawConfig = api.pluginConfig as Partial<ClawstoreConfig> | undefined;
23
+ const client = new StoreClient(rawConfig);
24
+
25
+ api.registerCommand({
26
+ name: "clawstore",
27
+ description: "Quick access to Clawstore agent marketplace",
28
+ acceptsArgs: true,
29
+
30
+ async handler(ctx) {
31
+ const raw = (ctx.args ?? "").trim();
32
+ const { agent, parts } = parseAgentFlag(raw);
33
+ const action = parts[0]?.toLowerCase() ?? "help";
34
+ const rest = parts.slice(1);
35
+
36
+ switch (action) {
37
+
38
+ // ── /clawstore search <query> ───────────────────────────
39
+ case "search": {
40
+ const query = rest.join(" ");
41
+ if (!query) return { text: "Usage: /clawstore search <query>" };
42
+ try {
43
+ const results = await client.search(query, { limit: 5 });
44
+ if (results.total === 0) return { text: `No agents found for "${query}".` };
45
+ const lines = results.agents.map(
46
+ (a, i) => `${i + 1}. ${a.id} — ${a.name} (${a.price === 0 ? "Free" : `$${a.price}`}, by ${a.author})`
47
+ );
48
+ return { text: `Found ${results.total} agent(s)\n${lines.join("\n")}\n\nUse \`openclaw clawstore search ${query}\` for full results.` };
49
+ } catch (err) {
50
+ return { text: `Search failed: ${(err as Error).message}\nThe Clawstore API may not be available yet.` };
51
+ }
52
+ }
53
+
54
+ // ── /clawstore show <agent-id> ──────────────────────────
55
+ case "show": {
56
+ const agentId = rest[0];
57
+ if (!agentId) return { text: "Usage: /clawstore show <agent-id>" };
58
+ const local = await getInstalledAgent(agentId, agent);
59
+ if (local) {
60
+ return { text: `Agent: ${local.id}\nName: ${local.name}\nVersion: ${local.version}\nStatus: ${local.enabled ? "enabled" : "disabled"}\nCategory: ${local.category}${agent !== DEFAULT_AGENT ? `\nOC Agent: ${agent}` : ""}` };
61
+ }
62
+ try {
63
+ const remote = await client.getAgentDetail(agentId);
64
+ return { text: `Agent: ${remote.id}\nName: ${remote.name}\nVersion: ${remote.version}\nPrice: ${remote.price === 0 ? "Free" : `$${remote.price}`}\nCategory: ${remote.category}\nStatus: not installed` };
65
+ } catch {
66
+ return { text: `Agent '${agentId}' not found.` };
67
+ }
68
+ }
69
+
70
+ // ── /clawstore install <source> ─────────────────────────
71
+ case "install": {
72
+ const source = rest[0];
73
+ if (!source) return { text: "Usage: /clawstore install <agent-id|path>" };
74
+ const lines: string[] = [];
75
+ const result = await installAgent({
76
+ packagePath: source,
77
+ agent,
78
+ log: (msg) => lines.push(msg),
79
+ });
80
+ if (result.state === "success") {
81
+ return { text: `Installed ${result.agentId}${agent !== DEFAULT_AGENT ? ` (agent: ${agent})` : ""}\n\nUse \`openclaw clawstore status ${result.agentId}\` for details.` };
82
+ }
83
+ return { text: `Install failed: ${result.error}\n\nUse CLI for details: \`openclaw clawstore install ${source}\`` };
84
+ }
85
+
86
+ // ── /clawstore installed ────────────────────────────────
87
+ case "installed": {
88
+ const agents = await listInstalledAgents(agent);
89
+ if (agents.length === 0) return { text: `No agents installed${agent !== DEFAULT_AGENT ? ` (agent: ${agent})` : ""}.` };
90
+ const lines = agents.map(
91
+ (a, i) => `${i + 1}. ${a.id} v${a.version} (${a.enabled ? "enabled" : "disabled"})`
92
+ );
93
+ return { text: `Installed agents${agent !== DEFAULT_AGENT ? ` [${agent}]` : ""}: ${agents.length}\n${lines.join("\n")}` };
94
+ }
95
+
96
+ // ── /clawstore enable <agent-id> ────────────────────────
97
+ case "enable": {
98
+ const id = rest[0];
99
+ if (!id) return { text: "Usage: /clawstore enable <agent-id>" };
100
+ const result = await enableAgent(id, agent);
101
+ return { text: result ? `Enabled: ${result.name} (${id}). Workspace files restored.` : `Agent '${id}' is not installed.` };
102
+ }
103
+
104
+ // ── /clawstore disable <agent-id> ───────────────────────
105
+ case "disable": {
106
+ const id = rest[0];
107
+ if (!id) return { text: "Usage: /clawstore disable <agent-id>" };
108
+ const result = await disableAgent(id, agent);
109
+ return { text: result ? `Disabled: ${result.agent.name} (${id}). Workspace files removed.` : `Agent '${id}' is not installed.` };
110
+ }
111
+
112
+ // ── /clawstore update <agent-id> ────────────────────────
113
+ case "update": {
114
+ const id = rest[0];
115
+ if (!id) return { text: "Usage: /clawstore update <agent-id>\n\nUse CLI for full options: `openclaw clawstore update <agent-id> --package <path>`" };
116
+ return { text: `Update via slash command is limited.\nUse: \`openclaw clawstore update ${id} --package <path>\`` };
117
+ }
118
+
119
+ // ── /clawstore uninstall <agent-id> ─────────────────────
120
+ case "uninstall": {
121
+ const id = rest[0];
122
+ if (!id) return { text: "Usage: /clawstore uninstall <agent-id>" };
123
+ return { text: `For safety, uninstall requires the CLI:\n\`openclaw clawstore uninstall ${id}\`` };
124
+ }
125
+
126
+ // ── /clawstore pack ─────────────────────────────────────
127
+ case "pack": {
128
+ return { text: "Pack requires the CLI for interactive confirmation:\n`openclaw clawstore pack [path]`" };
129
+ }
130
+
131
+ // ── /clawstore doctor ───────────────────────────────────
132
+ case "doctor": {
133
+ const report = await diagnose(undefined, agent);
134
+ const summary = report.checks
135
+ .filter((c) => c.status !== "ok")
136
+ .map((c) => `${c.label}: ${c.detail}`)
137
+ .join("\n");
138
+ return {
139
+ text: report.allOk
140
+ ? "All checks passed."
141
+ : `Issues found:\n${summary}\n\nRun \`openclaw clawstore doctor\` for full report.`,
142
+ };
143
+ }
144
+
145
+ // ── /clawstore help ─────────────────────────────────────
146
+ case "help":
147
+ default:
148
+ return {
149
+ text: [
150
+ "Clawstore — Agent Marketplace",
151
+ "",
152
+ "Usage: /clawstore <action> [args] [--agent <name>]",
153
+ "",
154
+ "Actions:",
155
+ " search <query> Search marketplace",
156
+ " show <agent-id> Show agent details",
157
+ " install <source> Install an agent",
158
+ " installed List installed agents",
159
+ " enable <agent-id> Enable an agent",
160
+ " disable <agent-id> Disable an agent",
161
+ " update <agent-id> Update an agent",
162
+ " uninstall <agent-id> Uninstall an agent",
163
+ " doctor Run diagnostics",
164
+ " help Show this help",
165
+ "",
166
+ "Options:",
167
+ " --agent <name> Target OpenClaw agent (default: main)",
168
+ "",
169
+ "For full options, use the CLI: `openclaw clawstore --help`",
170
+ ].join("\n"),
171
+ };
172
+ }
173
+ },
174
+ });
175
+ }
176
+
177
+ /**
178
+ * Extract `--agent <name>` from raw args string and return the remaining
179
+ * parts plus the resolved agent name.
180
+ */
181
+ function parseAgentFlag(raw: string): { agent: string; parts: string[] } {
182
+ const tokens = raw.split(/\s+/).filter(Boolean);
183
+ let agent = DEFAULT_AGENT;
184
+ const parts: string[] = [];
185
+
186
+ for (let i = 0; i < tokens.length; i++) {
187
+ if (tokens[i] === "--agent" && i + 1 < tokens.length) {
188
+ agent = tokens[i + 1];
189
+ i++;
190
+ } else {
191
+ parts.push(tokens[i]);
192
+ }
193
+ }
194
+
195
+ return { agent, parts };
196
+ }
@@ -0,0 +1,80 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+
4
+ export const PLUGIN_ID = "clawstore";
5
+ export const PLUGIN_VERSION = "1.0.0";
6
+
7
+ // ─── OpenClaw Paths ─────────────────────────────────────────────────
8
+
9
+ export const OPENCLAW_HOME = join(homedir(), ".openclaw");
10
+ export const DEFAULT_AGENT = "main";
11
+
12
+ export const CLAWSTORE_DIR = join(OPENCLAW_HOME, "clawstore");
13
+ export const CACHE_DIR = join(CLAWSTORE_DIR, "cache");
14
+ export const DOWNLOADS_DIR = join(CLAWSTORE_DIR, "downloads_cache");
15
+
16
+ // ─── Per-agent path resolvers ───────────────────────────────────────
17
+ // `agent` = OpenClaw agent name (e.g. "main", "sales", "support")
18
+
19
+ export function agentSessionsDir(agent: string = DEFAULT_AGENT): string {
20
+ return join(OPENCLAW_HOME, "agents", agent, "sessions");
21
+ }
22
+
23
+ export function agentSessionsIndex(agent: string = DEFAULT_AGENT): string {
24
+ return join(agentSessionsDir(agent), "sessions.json");
25
+ }
26
+
27
+ export function agentRegistryFile(agent: string = DEFAULT_AGENT): string {
28
+ if (agent === DEFAULT_AGENT) return join(CLAWSTORE_DIR, "installed_agents.json");
29
+ return join(CLAWSTORE_DIR, "agents", agent, "installed_agents.json");
30
+ }
31
+
32
+ export function agentSkillRegistryFile(agent: string = DEFAULT_AGENT): string {
33
+ if (agent === DEFAULT_AGENT) return join(CLAWSTORE_DIR, "installed_skills.json");
34
+ return join(CLAWSTORE_DIR, "agents", agent, "installed_skills.json");
35
+ }
36
+
37
+ export function agentBackupDir(agent: string = DEFAULT_AGENT): string {
38
+ if (agent === DEFAULT_AGENT) return join(CLAWSTORE_DIR, "backups");
39
+ return join(CLAWSTORE_DIR, "agents", agent, "backups");
40
+ }
41
+
42
+ // Legacy constants — resolve to "main" agent for backward compatibility
43
+ export const SESSIONS_DIR = agentSessionsDir(DEFAULT_AGENT);
44
+ export const SESSIONS_INDEX = agentSessionsIndex(DEFAULT_AGENT);
45
+ export const REGISTRY_FILE = agentRegistryFile(DEFAULT_AGENT);
46
+ export const SKILL_REGISTRY_FILE = agentSkillRegistryFile(DEFAULT_AGENT);
47
+ export const BACKUP_DIR = agentBackupDir(DEFAULT_AGENT);
48
+
49
+ // ─── Agent Package File Conventions ─────────────────────────────────
50
+
51
+ export const REQUIRED_FILES = ["SOUL.md", "IDENTITY.md", "AGENTS.md"] as const;
52
+
53
+ export const OPTIONAL_FILES = [
54
+ "USER.md",
55
+ "BOOTSTRAP.md",
56
+ "HEARTBEAT.md",
57
+ "MEMORY.md",
58
+ "TOOLS.md",
59
+ "BOOT.md",
60
+ ] as const;
61
+
62
+ export const MANIFEST_FILE = "agent.json";
63
+
64
+ export const MANIFEST_REQUIRED_FIELDS = [
65
+ "id",
66
+ "name",
67
+ "version",
68
+ "category",
69
+ ] as const;
70
+
71
+ // ─── Default API ────────────────────────────────────────────────────
72
+
73
+ export const DEFAULT_API_BASE_URL = "https://api.shopclawmart.com";
74
+
75
+ // ─── Install Steps ──────────────────────────────────────────────────
76
+
77
+ export const INSTALL_TOTAL_STEPS = 7;
78
+ export const UPDATE_TOTAL_STEPS = 5;
79
+ export const UNINSTALL_TOTAL_STEPS = 4;
80
+ export const PACK_TOTAL_STEPS = 3;
@@ -0,0 +1,327 @@
1
+ import { readFile, writeFile, mkdir, access, readdir, unlink, cp, rm } from "node:fs/promises";
2
+ import { join, dirname } from "node:path";
3
+ import {
4
+ DEFAULT_AGENT,
5
+ REQUIRED_FILES,
6
+ OPTIONAL_FILES,
7
+ agentRegistryFile,
8
+ agentBackupDir,
9
+ agentSessionsDir,
10
+ agentSessionsIndex,
11
+ } from "../constants.js";
12
+ import type { AgentRegistry, InstalledAgentRecord, AgentManifest } from "../types.js";
13
+ import { resolveWorkspace } from "./workspace.js";
14
+
15
+ // In this module, `agent` always refers to the OpenClaw agent name
16
+ // (e.g. "main", "sales"), while `agentId` / `personaId` refers to the
17
+ // Persona package identifier (e.g. "sellex-ecommerce-ops").
18
+
19
+ // ─── Registry I/O with simple file-lock guard ───────────────────────
20
+
21
+ let registryLock = false;
22
+
23
+ async function acquireLock(): Promise<void> {
24
+ let attempts = 0;
25
+ while (registryLock && attempts < 50) {
26
+ await new Promise((r) => setTimeout(r, 100));
27
+ attempts++;
28
+ }
29
+ if (registryLock) throw new Error("Could not acquire registry lock after 5s");
30
+ registryLock = true;
31
+ }
32
+
33
+ function releaseLock(): void {
34
+ registryLock = false;
35
+ }
36
+
37
+ async function exists(p: string): Promise<boolean> {
38
+ try {
39
+ await access(p);
40
+ return true;
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+
46
+ export async function ensureDirs(agent: string = DEFAULT_AGENT): Promise<void> {
47
+ const workspace = await resolveWorkspace(agent);
48
+ const registryFile = agentRegistryFile(agent);
49
+ const backupDir = agentBackupDir(agent);
50
+ for (const d of [workspace, dirname(registryFile), backupDir]) {
51
+ await mkdir(d, { recursive: true });
52
+ }
53
+ }
54
+
55
+ export async function loadRegistry(agent: string = DEFAULT_AGENT): Promise<AgentRegistry> {
56
+ const registryFile = agentRegistryFile(agent);
57
+ if (!(await exists(registryFile))) {
58
+ return { agents: {} };
59
+ }
60
+ const raw = await readFile(registryFile, "utf-8");
61
+ return JSON.parse(raw) as AgentRegistry;
62
+ }
63
+
64
+ export async function saveRegistry(registry: AgentRegistry, agent: string = DEFAULT_AGENT): Promise<void> {
65
+ const registryFile = agentRegistryFile(agent);
66
+ await mkdir(dirname(registryFile), { recursive: true });
67
+ await writeFile(registryFile, JSON.stringify(registry, null, 2), "utf-8");
68
+ }
69
+
70
+ // ─── Agent CRUD ─────────────────────────────────────────────────────
71
+
72
+ export async function getInstalledAgent(personaId: string, agent: string = DEFAULT_AGENT): Promise<InstalledAgentRecord | null> {
73
+ const reg = await loadRegistry(agent);
74
+ return reg.agents[personaId] ?? null;
75
+ }
76
+
77
+ export async function listInstalledAgents(agent: string = DEFAULT_AGENT): Promise<InstalledAgentRecord[]> {
78
+ const reg = await loadRegistry(agent);
79
+ return Object.values(reg.agents);
80
+ }
81
+
82
+ export async function registerAgent(record: InstalledAgentRecord, agent: string = DEFAULT_AGENT): Promise<void> {
83
+ await acquireLock();
84
+ try {
85
+ const reg = await loadRegistry(agent);
86
+ reg.agents[record.id] = record;
87
+ await saveRegistry(reg, agent);
88
+ } finally {
89
+ releaseLock();
90
+ }
91
+ }
92
+
93
+ export async function unregisterAgent(personaId: string, agent: string = DEFAULT_AGENT): Promise<InstalledAgentRecord | null> {
94
+ await acquireLock();
95
+ try {
96
+ const reg = await loadRegistry(agent);
97
+ const record = reg.agents[personaId];
98
+ if (!record) return null;
99
+ delete reg.agents[personaId];
100
+ await saveRegistry(reg, agent);
101
+ return record;
102
+ } finally {
103
+ releaseLock();
104
+ }
105
+ }
106
+
107
+ export async function updateAgentRecord(
108
+ personaId: string,
109
+ patch: Partial<InstalledAgentRecord>,
110
+ agent: string = DEFAULT_AGENT,
111
+ ): Promise<InstalledAgentRecord | null> {
112
+ await acquireLock();
113
+ try {
114
+ const reg = await loadRegistry(agent);
115
+ const record = reg.agents[personaId];
116
+ if (!record) return null;
117
+ Object.assign(record, patch);
118
+ await saveRegistry(reg, agent);
119
+ return record;
120
+ } finally {
121
+ releaseLock();
122
+ }
123
+ }
124
+
125
+ // ─── Enable / Disable ───────────────────────────────────────────────
126
+
127
+ export async function enableAgent(personaId: string, agent: string = DEFAULT_AGENT): Promise<InstalledAgentRecord | null> {
128
+ const record = await getInstalledAgent(personaId, agent);
129
+ if (!record) return null;
130
+
131
+ if (record.disable_backup_path && (await exists(record.disable_backup_path))) {
132
+ await restoreFromBackup(record.disable_backup_path, agent);
133
+ }
134
+
135
+ return updateAgentRecord(personaId, {
136
+ enabled: true,
137
+ disable_backup_path: undefined,
138
+ }, agent);
139
+ }
140
+
141
+ export async function disableAgent(personaId: string, agent: string = DEFAULT_AGENT): Promise<{ agent: InstalledAgentRecord; backupPath: string } | null> {
142
+ const record = await getInstalledAgent(personaId, agent);
143
+ if (!record) return null;
144
+
145
+ const backupPath = await backupCurrentWorkspace(personaId, "disable", agent);
146
+ await removeAgentFiles(record, agent);
147
+
148
+ const workspace = await resolveWorkspace(agent);
149
+ const skillsDir = join(workspace, "skills");
150
+ if (await exists(skillsDir)) {
151
+ await rm(skillsDir, { recursive: true, force: true });
152
+ }
153
+
154
+ const updated = await updateAgentRecord(personaId, {
155
+ enabled: false,
156
+ disable_backup_path: backupPath,
157
+ }, agent);
158
+
159
+ return updated ? { agent: updated, backupPath } : null;
160
+ }
161
+
162
+ // ─── Backup ─────────────────────────────────────────────────────────
163
+
164
+ export async function backupCurrentWorkspace(personaId: string, label: string = "", agent: string = DEFAULT_AGENT): Promise<string> {
165
+ const workspace = await resolveWorkspace(agent);
166
+ const backupDir = agentBackupDir(agent);
167
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
168
+ const dirName = label ? `${personaId}_${label}_${timestamp}` : `${personaId}_${timestamp}`;
169
+ const backupPath = join(backupDir, dirName);
170
+ await mkdir(backupPath, { recursive: true });
171
+
172
+ const allFiles = [...REQUIRED_FILES, ...OPTIONAL_FILES];
173
+ let count = 0;
174
+ for (const f of allFiles) {
175
+ const src = join(workspace, f);
176
+ if (await exists(src)) {
177
+ await cp(src, join(backupPath, f));
178
+ count++;
179
+ }
180
+ }
181
+
182
+ const skillsDir = join(workspace, "skills");
183
+ if (await exists(skillsDir)) {
184
+ await cp(skillsDir, join(backupPath, "skills"), { recursive: true });
185
+ count++;
186
+ }
187
+
188
+ return backupPath;
189
+ }
190
+
191
+ // ─── Restore (rollback) ────────────────────────────────────────────
192
+
193
+ export async function restoreFromBackup(backupPath: string, agent: string = DEFAULT_AGENT): Promise<number> {
194
+ const workspace = await resolveWorkspace(agent);
195
+ let count = 0;
196
+
197
+ const allFiles = [...REQUIRED_FILES, ...OPTIONAL_FILES];
198
+ for (const f of allFiles) {
199
+ const src = join(backupPath, f);
200
+ if (await exists(src)) {
201
+ await cp(src, join(workspace, f), { force: true });
202
+ count++;
203
+ }
204
+ }
205
+
206
+ const skillsBackup = join(backupPath, "skills");
207
+ if (await exists(skillsBackup)) {
208
+ const skillsDest = join(workspace, "skills");
209
+ await cp(skillsBackup, skillsDest, { recursive: true, force: true });
210
+ count++;
211
+ }
212
+
213
+ return count;
214
+ }
215
+
216
+ // ─── Uninstall (file removal) ───────────────────────────────────────
217
+
218
+ export async function removeAgentFiles(record: InstalledAgentRecord, agent: string = DEFAULT_AGENT): Promise<number> {
219
+ const workspace = await resolveWorkspace(agent);
220
+ let count = 0;
221
+
222
+ for (const f of [...REQUIRED_FILES, ...OPTIONAL_FILES]) {
223
+ const target = join(workspace, f);
224
+ if (await exists(target)) {
225
+ await unlink(target);
226
+ count++;
227
+ }
228
+ }
229
+
230
+ if (record.skills && record.skills.length > 0) {
231
+ const skillsDir = join(workspace, "skills");
232
+ for (const skillId of record.skills) {
233
+ const skillFile = join(skillsDir, `${skillId}.md`);
234
+ if (await exists(skillFile)) {
235
+ await unlink(skillFile);
236
+ count++;
237
+ }
238
+ }
239
+ if (await exists(skillsDir)) {
240
+ const remaining = await readdir(skillsDir);
241
+ if (remaining.length === 0) {
242
+ await rm(skillsDir, { recursive: true });
243
+ }
244
+ }
245
+ }
246
+
247
+ return count;
248
+ }
249
+
250
+ /**
251
+ * Remove all known agent files from workspace without needing a registry record.
252
+ */
253
+ export async function removeAllWorkspaceAgentFiles(agent: string = DEFAULT_AGENT): Promise<number> {
254
+ const workspace = await resolveWorkspace(agent);
255
+ let count = 0;
256
+
257
+ for (const f of [...REQUIRED_FILES, ...OPTIONAL_FILES]) {
258
+ const target = join(workspace, f);
259
+ if (await exists(target)) {
260
+ await unlink(target);
261
+ count++;
262
+ }
263
+ }
264
+
265
+ const skillsDir = join(workspace, "skills");
266
+ if (await exists(skillsDir)) {
267
+ const files = await readdir(skillsDir);
268
+ for (const f of files) {
269
+ await unlink(join(skillsDir, f));
270
+ count++;
271
+ }
272
+ await rm(skillsDir, { recursive: true, force: true });
273
+ }
274
+
275
+ return count;
276
+ }
277
+
278
+ // ─── Status / detail ────────────────────────────────────────────────
279
+
280
+ export async function getAgentStatus(personaId: string, agent: string = DEFAULT_AGENT): Promise<{
281
+ agent: InstalledAgentRecord;
282
+ fileHealth: { file: string; exists: boolean }[];
283
+ skillHealth: { skillId: string; installed: boolean }[];
284
+ } | null> {
285
+ const record = await getInstalledAgent(personaId, agent);
286
+ if (!record) return null;
287
+
288
+ const workspace = await resolveWorkspace(agent);
289
+ const fileHealth: { file: string; exists: boolean }[] = [];
290
+ for (const f of record.files) {
291
+ const fp = join(workspace, f);
292
+ fileHealth.push({ file: f, exists: await exists(fp) });
293
+ }
294
+
295
+ const skillsDir = join(workspace, "skills");
296
+ const skillHealth: { skillId: string; installed: boolean }[] = [];
297
+ for (const sid of record.skills) {
298
+ const sp = join(skillsDir, `${sid}.md`);
299
+ skillHealth.push({ skillId: sid, installed: await exists(sp) });
300
+ }
301
+
302
+ return { agent: record, fileHealth, skillHealth };
303
+ }
304
+
305
+ // ─── Session cleanup ────────────────────────────────────────────────
306
+
307
+ export async function clearSessions(agent: string = DEFAULT_AGENT): Promise<number> {
308
+ const sessDir = agentSessionsDir(agent);
309
+ const sessIndex = agentSessionsIndex(agent);
310
+ let count = 0;
311
+
312
+ if (await exists(sessDir)) {
313
+ const files = await readdir(sessDir);
314
+ for (const f of files) {
315
+ if (f.endsWith(".jsonl")) {
316
+ await unlink(join(sessDir, f));
317
+ count++;
318
+ }
319
+ }
320
+ }
321
+
322
+ if (await exists(sessIndex)) {
323
+ await writeFile(sessIndex, "{}", "utf-8");
324
+ }
325
+
326
+ return count;
327
+ }