clawvault 3.2.1 → 3.3.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/README.md +56 -16
- package/bin/clawvault.js +0 -2
- package/bin/command-registration.test.js +13 -1
- package/bin/help-contract.test.js +14 -0
- package/bin/register-core-commands.js +88 -0
- package/bin/register-core-commands.test.js +80 -0
- package/bin/register-maintenance-commands.js +57 -6
- package/bin/register-query-commands.js +10 -28
- package/bin/test-helpers/cli-command-fixtures.js +1 -0
- package/dist/chunk-2PKBIKDH.js +130 -0
- package/dist/{chunk-U67V476Y.js → chunk-2ZDO52B4.js} +18 -1
- package/dist/{chunk-ZZA73MFY.js → chunk-33DOSHTA.js} +176 -36
- package/dist/{chunk-AZYOKJYC.js → chunk-4PY655YM.js} +13 -1
- package/dist/{chunk-2JQ3O2YL.js → chunk-5EFSWZO6.js} +3 -3
- package/dist/{chunk-Y3TIJEBP.js → chunk-7SWP5FKU.js} +34 -613
- package/dist/{chunk-4VQTUVH7.js → chunk-7YZWHM36.js} +52 -26
- package/dist/{chunk-URXDAUVH.js → chunk-AXSJIFOJ.js} +174 -1
- package/dist/{chunk-4ITRXIVT.js → chunk-BLQXXX7Q.js} +6 -6
- package/dist/chunk-CSHO3PJB.js +684 -0
- package/dist/{chunk-S5OJEGFG.js → chunk-DOIUYIXV.js} +2 -2
- package/dist/{chunk-YXQCA6B7.js → chunk-DVOUSOR3.js} +112 -7
- package/dist/{chunk-YDWHS4LJ.js → chunk-ECGJYWNA.js} +205 -33
- package/dist/{chunk-QMHPQYUV.js → chunk-EL6UBSX5.js} +7 -6
- package/dist/chunk-FZ5I2NF7.js +352 -0
- package/dist/{chunk-WJVWINEM.js → chunk-GFCHWMGD.js} +55 -6
- package/dist/{chunk-GNJL4YGR.js → chunk-GJO3CFUN.js} +30 -6
- package/dist/chunk-H3JZIB5O.js +322 -0
- package/dist/chunk-HEHO7SMV.js +51 -0
- package/dist/{chunk-UCQAOZHW.js → chunk-HGDDW24U.js} +3 -3
- package/dist/chunk-J3YUXVID.js +907 -0
- package/dist/{chunk-Y6VJKXGL.js → chunk-KCYWJDDW.js} +1 -1
- package/dist/{chunk-P5EPF6MB.js → chunk-MW5C6ZQA.js} +110 -13
- package/dist/{chunk-YNIPYN4F.js → chunk-OFOCU2V4.js} +6 -5
- package/dist/{chunk-42MXU7A6.js → chunk-P62WHA27.js} +58 -47
- package/dist/chunk-PTWPPVC7.js +972 -0
- package/dist/{chunk-FAKNOB7Y.js → chunk-QFWERBDP.js} +2 -2
- package/dist/{chunk-IIOU45CK.js → chunk-S7N7HI5E.js} +2 -2
- package/dist/{chunk-ECRZL5XR.js → chunk-T7E764W3.js} +23 -7
- package/dist/chunk-TDWFBDAQ.js +1016 -0
- package/dist/{chunk-MNPUYCHQ.js → chunk-TWMI3SNN.js} +6 -5
- package/dist/{chunk-2RAZ4ZFE.js → chunk-VBILES4B.js} +1 -1
- package/dist/{chunk-PI4WMLMG.js → chunk-VXAGOLDP.js} +1 -1
- package/dist/chunk-YCUVAOFC.js +158 -0
- package/dist/{chunk-SS4B7P7V.js → chunk-YIDV4VV2.js} +1 -1
- package/dist/chunk-ZKWPCBYT.js +600 -0
- package/dist/cli/index.js +27 -21
- package/dist/commands/archive.js +3 -3
- package/dist/commands/backlog.js +1 -1
- package/dist/commands/benchmark.d.ts +12 -0
- package/dist/commands/benchmark.js +12 -0
- package/dist/commands/blocked.js +1 -1
- package/dist/commands/canvas.js +2 -2
- package/dist/commands/checkpoint.js +1 -1
- package/dist/commands/compat.js +1 -1
- package/dist/commands/context.js +8 -7
- package/dist/commands/doctor.d.ts +8 -3
- package/dist/commands/doctor.js +8 -22
- package/dist/commands/embed.js +6 -5
- package/dist/commands/entities.js +2 -2
- package/dist/commands/graph.js +4 -4
- package/dist/commands/inbox.d.ts +23 -0
- package/dist/commands/inbox.js +11 -0
- package/dist/commands/inject.d.ts +1 -1
- package/dist/commands/inject.js +5 -5
- package/dist/commands/kanban.js +1 -1
- package/dist/commands/link.js +9 -9
- package/dist/commands/maintain.d.ts +32 -0
- package/dist/commands/maintain.js +12 -0
- package/dist/commands/migrate-observations.js +3 -3
- package/dist/commands/observe.js +11 -10
- package/dist/commands/project.js +2 -2
- package/dist/commands/rebuild-embeddings.js +48 -17
- package/dist/commands/rebuild.js +9 -8
- package/dist/commands/recover.js +1 -1
- package/dist/commands/reflect.js +6 -6
- package/dist/commands/repair-session.js +1 -1
- package/dist/commands/replay.js +10 -9
- package/dist/commands/session-recap.js +1 -1
- package/dist/commands/setup.js +4 -3
- package/dist/commands/shell-init.js +1 -1
- package/dist/commands/sleep.d.ts +1 -1
- package/dist/commands/sleep.js +20 -18
- package/dist/commands/status.js +40 -26
- package/dist/commands/sync-bd.js +3 -3
- package/dist/commands/tailscale.js +3 -3
- package/dist/commands/task.js +1 -1
- package/dist/commands/template.js +1 -1
- package/dist/commands/wake.d.ts +1 -1
- package/dist/commands/wake.js +10 -9
- package/dist/index.d.ts +175 -16
- package/dist/index.js +277 -108
- package/dist/{inject-DYUrDqQO.d.ts → inject-DEb_jpLi.d.ts} +3 -1
- package/dist/lib/auto-linker.js +2 -2
- package/dist/lib/canvas-layout.js +1 -1
- package/dist/lib/config.js +2 -2
- package/dist/lib/entity-index.js +1 -1
- package/dist/lib/project-utils.js +2 -2
- package/dist/lib/session-repair.js +1 -1
- package/dist/lib/session-utils.js +1 -1
- package/dist/lib/tailscale.js +1 -1
- package/dist/lib/task-utils.js +1 -1
- package/dist/lib/template-engine.js +1 -1
- package/dist/lib/webdav.js +1 -1
- package/dist/onnxruntime_binding-5QEF3SUC.node +0 -0
- package/dist/onnxruntime_binding-BKPKNEGC.node +0 -0
- package/dist/onnxruntime_binding-FMOXGIUT.node +0 -0
- package/dist/onnxruntime_binding-OI2KMXC5.node +0 -0
- package/dist/onnxruntime_binding-UX44MLAZ.node +0 -0
- package/dist/onnxruntime_binding-Y2W7N7WY.node +0 -0
- package/dist/openclaw-plugin.d.ts +8 -0
- package/dist/openclaw-plugin.js +14 -0
- package/dist/transformers.node-A2ZRORSQ.js +46775 -0
- package/dist/{types-BbWJoC1c.d.ts → types-DslKvCaj.d.ts} +51 -1
- package/hooks/clawvault/HOOK.md +25 -8
- package/hooks/clawvault/handler.js +215 -78
- package/hooks/clawvault/handler.test.js +109 -43
- package/hooks/clawvault/integrity.js +112 -0
- package/hooks/clawvault/integrity.test.js +32 -0
- package/hooks/clawvault/openclaw.plugin.json +133 -15
- package/openclaw.plugin.json +131 -203
- package/package.json +10 -7
- package/bin/register-workgraph-commands.js +0 -451
- package/dist/chunk-5PJ4STIC.js +0 -465
- package/dist/chunk-ERNE2FZ5.js +0 -189
- package/dist/chunk-HR4KN6S2.js +0 -152
- package/dist/chunk-IJBFGPCS.js +0 -33
- package/dist/chunk-K7PNYS45.js +0 -93
- package/dist/chunk-NTOPJI7W.js +0 -207
- package/dist/chunk-PG56HX5T.js +0 -154
- package/dist/chunk-QPDDIHXE.js +0 -501
- package/dist/chunk-WIOLLGAD.js +0 -190
- package/dist/chunk-WMGIIABP.js +0 -15
- package/dist/ledger-B7g7jhqG.d.ts +0 -44
- package/dist/plugin/index.d.ts +0 -352
- package/dist/plugin/index.js +0 -4264
- package/dist/registry-BR4326o0.d.ts +0 -30
- package/dist/store-CA-6sKCJ.d.ts +0 -34
- package/dist/thread-B9LhXNU0.d.ts +0 -41
- package/dist/workgraph/index.d.ts +0 -5
- package/dist/workgraph/index.js +0 -23
- package/dist/workgraph/ledger.d.ts +0 -2
- package/dist/workgraph/ledger.js +0 -25
- package/dist/workgraph/registry.d.ts +0 -2
- package/dist/workgraph/registry.js +0 -19
- package/dist/workgraph/store.d.ts +0 -2
- package/dist/workgraph/store.js +0 -25
- package/dist/workgraph/thread.d.ts +0 -2
- package/dist/workgraph/thread.js +0 -25
- package/dist/workgraph/types.d.ts +0 -54
- package/dist/workgraph/types.js +0 -7
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ClawVault,
|
|
3
|
+
findVault
|
|
4
|
+
} from "./chunk-ECGJYWNA.js";
|
|
5
|
+
import {
|
|
6
|
+
listQmdCollections,
|
|
7
|
+
loadVaultQmdConfig
|
|
8
|
+
} from "./chunk-FZ5I2NF7.js";
|
|
9
|
+
import {
|
|
10
|
+
QMD_INSTALL_COMMAND,
|
|
11
|
+
QMD_INSTALL_URL,
|
|
12
|
+
hasQmd
|
|
13
|
+
} from "./chunk-PTWPPVC7.js";
|
|
14
|
+
import {
|
|
15
|
+
findNearestVaultPath
|
|
16
|
+
} from "./chunk-GJO3CFUN.js";
|
|
17
|
+
|
|
18
|
+
// src/commands/doctor.ts
|
|
19
|
+
import * as fs from "fs";
|
|
20
|
+
import * as os from "os";
|
|
21
|
+
import * as path from "path";
|
|
22
|
+
import { spawnSync } from "child_process";
|
|
23
|
+
import { parse as parseYaml } from "yaml";
|
|
24
|
+
var CONFIG_FILE_CANDIDATES = [".clawvault.json", ".clawvault.yaml", ".clawvault.yml"];
|
|
25
|
+
var RECOMMENDED_NODE_MAJOR = 22;
|
|
26
|
+
var MINIMUM_NODE_MAJOR = 18;
|
|
27
|
+
var MINIMUM_NPM_MAJOR = 9;
|
|
28
|
+
var DISK_ERROR_THRESHOLD_BYTES = 512 * 1024 * 1024;
|
|
29
|
+
var DISK_WARN_THRESHOLD_BYTES = 5 * 1024 * 1024 * 1024;
|
|
30
|
+
var V2_COLLECTION_PATTERNS = [/^clawvault$/i, /^vault$/i, /^memory$/i, /^notes$/i];
|
|
31
|
+
function humanBytes(value) {
|
|
32
|
+
if (!Number.isFinite(value) || value <= 0) return "0 B";
|
|
33
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
34
|
+
let size = value;
|
|
35
|
+
let unitIndex = 0;
|
|
36
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
37
|
+
size /= 1024;
|
|
38
|
+
unitIndex += 1;
|
|
39
|
+
}
|
|
40
|
+
return `${size.toFixed(unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`;
|
|
41
|
+
}
|
|
42
|
+
function majorVersionOf(versionText) {
|
|
43
|
+
const match = versionText.trim().match(/^v?(\d+)/);
|
|
44
|
+
if (!match) return null;
|
|
45
|
+
const parsed = Number.parseInt(match[1], 10);
|
|
46
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
47
|
+
}
|
|
48
|
+
function toNonEmptyString(value) {
|
|
49
|
+
if (typeof value !== "string") return void 0;
|
|
50
|
+
const trimmed = value.trim();
|
|
51
|
+
return trimmed ? trimmed : void 0;
|
|
52
|
+
}
|
|
53
|
+
function coerceVaultPath(value) {
|
|
54
|
+
const direct = toNonEmptyString(value);
|
|
55
|
+
if (direct) return direct;
|
|
56
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
57
|
+
return void 0;
|
|
58
|
+
}
|
|
59
|
+
const record = value;
|
|
60
|
+
const candidates = [record.vaultPath, record.path, record.vault, record.explicitPath];
|
|
61
|
+
for (const candidate of candidates) {
|
|
62
|
+
const coerced = toNonEmptyString(candidate);
|
|
63
|
+
if (coerced) {
|
|
64
|
+
return coerced;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return void 0;
|
|
68
|
+
}
|
|
69
|
+
function normalizeDoctorOptions(input) {
|
|
70
|
+
if (typeof input === "string") {
|
|
71
|
+
return { vaultPath: input };
|
|
72
|
+
}
|
|
73
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
const raw = input;
|
|
77
|
+
const normalized = {};
|
|
78
|
+
const vaultPath = coerceVaultPath(raw.vaultPath);
|
|
79
|
+
if (vaultPath) {
|
|
80
|
+
normalized.vaultPath = vaultPath;
|
|
81
|
+
}
|
|
82
|
+
if (typeof raw.fix === "boolean") {
|
|
83
|
+
normalized.fix = raw.fix;
|
|
84
|
+
}
|
|
85
|
+
return normalized;
|
|
86
|
+
}
|
|
87
|
+
function resolveDoctorVaultPath(explicitPath) {
|
|
88
|
+
const normalizedExplicitPath = coerceVaultPath(explicitPath);
|
|
89
|
+
if (normalizedExplicitPath) {
|
|
90
|
+
return path.resolve(normalizedExplicitPath);
|
|
91
|
+
}
|
|
92
|
+
const envVaultPath = toNonEmptyString(process.env.CLAWVAULT_PATH);
|
|
93
|
+
if (envVaultPath) {
|
|
94
|
+
return path.resolve(envVaultPath);
|
|
95
|
+
}
|
|
96
|
+
const nearest = findNearestVaultPath(process.cwd());
|
|
97
|
+
return nearest ? path.resolve(nearest) : void 0;
|
|
98
|
+
}
|
|
99
|
+
function selectConfigPath(vaultPath) {
|
|
100
|
+
for (const filename of CONFIG_FILE_CANDIDATES) {
|
|
101
|
+
const candidate = path.join(vaultPath, filename);
|
|
102
|
+
if (fs.existsSync(candidate)) {
|
|
103
|
+
return candidate;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return void 0;
|
|
107
|
+
}
|
|
108
|
+
function parseConfigDocument(configPath) {
|
|
109
|
+
try {
|
|
110
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
111
|
+
const isJson = configPath.endsWith(".json");
|
|
112
|
+
const parsed = isJson ? JSON.parse(raw) : parseYaml(raw);
|
|
113
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
114
|
+
return {
|
|
115
|
+
format: isJson ? "json" : "yaml",
|
|
116
|
+
error: "Config root must be an object."
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
format: isJson ? "json" : "yaml",
|
|
121
|
+
data: parsed
|
|
122
|
+
};
|
|
123
|
+
} catch (error) {
|
|
124
|
+
return {
|
|
125
|
+
format: configPath.endsWith(".json") ? "json" : "yaml",
|
|
126
|
+
error: error?.message || "Unable to parse config file."
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function isLikelyV2CollectionName(name) {
|
|
131
|
+
return V2_COLLECTION_PATTERNS.some((pattern) => pattern.test(name));
|
|
132
|
+
}
|
|
133
|
+
function checkQmdCollectionExists(collections, expectedName) {
|
|
134
|
+
const found = collections.find((collection) => collection.name === expectedName);
|
|
135
|
+
return { exists: Boolean(found), collection: found };
|
|
136
|
+
}
|
|
137
|
+
function checkCollectionPathMatches(collection, expectedRoot) {
|
|
138
|
+
if (!collection.root) return false;
|
|
139
|
+
return path.resolve(collection.root) === path.resolve(expectedRoot);
|
|
140
|
+
}
|
|
141
|
+
function detectMigrationIssues(vaultPath, configuredCollection, configuredRoot) {
|
|
142
|
+
const issues = [];
|
|
143
|
+
if (!hasQmd()) return issues;
|
|
144
|
+
let collections;
|
|
145
|
+
try {
|
|
146
|
+
collections = listQmdCollections();
|
|
147
|
+
} catch {
|
|
148
|
+
return issues;
|
|
149
|
+
}
|
|
150
|
+
const vaultConfig = loadVaultQmdConfig(vaultPath);
|
|
151
|
+
const expectedCollection = configuredCollection || vaultConfig.qmdCollection;
|
|
152
|
+
const expectedRoot = configuredRoot || vaultConfig.qmdRoot;
|
|
153
|
+
if (!expectedCollection || !expectedRoot) return issues;
|
|
154
|
+
const { exists, collection } = checkQmdCollectionExists(collections, expectedCollection);
|
|
155
|
+
if (!exists) {
|
|
156
|
+
const potentialV2Collections = collections.filter(
|
|
157
|
+
(entry) => isLikelyV2CollectionName(entry.name) && entry.root && path.resolve(entry.root) === path.resolve(expectedRoot)
|
|
158
|
+
);
|
|
159
|
+
if (potentialV2Collections.length > 0) {
|
|
160
|
+
issues.push({
|
|
161
|
+
type: "stale_collection_name",
|
|
162
|
+
description: `Found v2-style collection "${potentialV2Collections[0].name}" that should be renamed to "${expectedCollection}"`,
|
|
163
|
+
autoFixable: true,
|
|
164
|
+
details: {
|
|
165
|
+
oldName: potentialV2Collections[0].name,
|
|
166
|
+
newName: expectedCollection,
|
|
167
|
+
root: potentialV2Collections[0].root
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
} else {
|
|
171
|
+
issues.push({
|
|
172
|
+
type: "missing_qmd_collection",
|
|
173
|
+
description: `qmd collection "${expectedCollection}" does not exist`,
|
|
174
|
+
autoFixable: true,
|
|
175
|
+
details: {
|
|
176
|
+
collectionName: expectedCollection,
|
|
177
|
+
expectedRoot
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
} else if (collection && !checkCollectionPathMatches(collection, expectedRoot)) {
|
|
182
|
+
issues.push({
|
|
183
|
+
type: "wrong_vault_path",
|
|
184
|
+
description: `Collection "${expectedCollection}" points to "${collection.root}" but vault is at "${expectedRoot}"`,
|
|
185
|
+
autoFixable: true,
|
|
186
|
+
details: {
|
|
187
|
+
collectionName: expectedCollection,
|
|
188
|
+
currentRoot: collection.root,
|
|
189
|
+
expectedRoot
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
const orphanedCollections = collections.filter((entry) => {
|
|
194
|
+
if (entry.name === expectedCollection || !entry.root) return false;
|
|
195
|
+
const collectionRoot = path.resolve(entry.root);
|
|
196
|
+
const vaultRoot = path.resolve(expectedRoot);
|
|
197
|
+
return collectionRoot === vaultRoot || collectionRoot.startsWith(`${vaultRoot}${path.sep}`);
|
|
198
|
+
});
|
|
199
|
+
for (const orphan of orphanedCollections) {
|
|
200
|
+
issues.push({
|
|
201
|
+
type: "orphaned_collection",
|
|
202
|
+
description: `Orphaned collection "${orphan.name}" points to vault path but is not the configured collection`,
|
|
203
|
+
autoFixable: true,
|
|
204
|
+
details: {
|
|
205
|
+
collectionName: orphan.name,
|
|
206
|
+
root: orphan.root
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
const jsonConfigPath = path.join(vaultPath, ".clawvault.json");
|
|
211
|
+
if (fs.existsSync(jsonConfigPath)) {
|
|
212
|
+
try {
|
|
213
|
+
const config = JSON.parse(fs.readFileSync(jsonConfigPath, "utf-8"));
|
|
214
|
+
if (!config.qmdCollection || !config.qmdRoot) {
|
|
215
|
+
issues.push({
|
|
216
|
+
type: "missing_qmd_config",
|
|
217
|
+
description: "Vault config is missing qmdCollection or qmdRoot settings",
|
|
218
|
+
autoFixable: true,
|
|
219
|
+
details: {
|
|
220
|
+
hasQmdCollection: Boolean(config.qmdCollection),
|
|
221
|
+
hasQmdRoot: Boolean(config.qmdRoot)
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
issues.push({
|
|
227
|
+
type: "legacy_config_format",
|
|
228
|
+
description: "Unable to parse .clawvault.json - may need migration",
|
|
229
|
+
autoFixable: false
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return issues;
|
|
234
|
+
}
|
|
235
|
+
function migrationIssuesToChecks(issues) {
|
|
236
|
+
return issues.map((issue) => ({
|
|
237
|
+
label: `migration: ${issue.type.replace(/_/g, " ")}`,
|
|
238
|
+
status: "warn",
|
|
239
|
+
detail: issue.description,
|
|
240
|
+
hint: issue.autoFixable ? "Run `clawvault migrate` to auto-fix this issue." : "Manual intervention required.",
|
|
241
|
+
category: "migration"
|
|
242
|
+
}));
|
|
243
|
+
}
|
|
244
|
+
async function resolveVault(vaultPath) {
|
|
245
|
+
if (vaultPath) {
|
|
246
|
+
const vault = new ClawVault(path.resolve(vaultPath));
|
|
247
|
+
await vault.load();
|
|
248
|
+
return vault;
|
|
249
|
+
}
|
|
250
|
+
const found = await findVault();
|
|
251
|
+
if (!found) {
|
|
252
|
+
throw new Error("No ClawVault found. Run `clawvault init` first.");
|
|
253
|
+
}
|
|
254
|
+
return found;
|
|
255
|
+
}
|
|
256
|
+
function computeAvailableDiskBytes(targetPath) {
|
|
257
|
+
const statfsSync2 = fs.statfsSync;
|
|
258
|
+
if (typeof statfsSync2 !== "function") {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
const stats = statfsSync2(targetPath, { bigint: true });
|
|
262
|
+
if (typeof stats.bsize === "undefined") {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
const blockSize = typeof stats.bsize === "bigint" ? stats.bsize : BigInt(stats.bsize);
|
|
266
|
+
const availableBlocks = typeof stats.bavail === "bigint" ? stats.bavail : BigInt(stats.bavail);
|
|
267
|
+
const availableBytes = blockSize * availableBlocks;
|
|
268
|
+
return Number(availableBytes);
|
|
269
|
+
}
|
|
270
|
+
async function doctor(input) {
|
|
271
|
+
const options = normalizeDoctorOptions(input);
|
|
272
|
+
const checks = [];
|
|
273
|
+
const migrationIssues = [];
|
|
274
|
+
const pushCheck = (check) => {
|
|
275
|
+
checks.push(check);
|
|
276
|
+
};
|
|
277
|
+
const nodeVersion = process.versions.node;
|
|
278
|
+
const nodeMajor = majorVersionOf(nodeVersion);
|
|
279
|
+
if (nodeMajor === null) {
|
|
280
|
+
pushCheck({
|
|
281
|
+
label: "Node.js version",
|
|
282
|
+
status: "error",
|
|
283
|
+
detail: `Unable to parse Node.js version "${nodeVersion}"`,
|
|
284
|
+
hint: `Install Node.js ${MINIMUM_NODE_MAJOR}+ (recommended ${RECOMMENDED_NODE_MAJOR}+).`,
|
|
285
|
+
category: "system"
|
|
286
|
+
});
|
|
287
|
+
} else if (nodeMajor < MINIMUM_NODE_MAJOR) {
|
|
288
|
+
pushCheck({
|
|
289
|
+
label: "Node.js version",
|
|
290
|
+
status: "error",
|
|
291
|
+
detail: `Detected ${nodeVersion}; minimum supported is ${MINIMUM_NODE_MAJOR}.`,
|
|
292
|
+
hint: `Upgrade Node.js to ${RECOMMENDED_NODE_MAJOR}+ and reinstall clawvault.`,
|
|
293
|
+
category: "system"
|
|
294
|
+
});
|
|
295
|
+
} else if (nodeMajor < RECOMMENDED_NODE_MAJOR) {
|
|
296
|
+
pushCheck({
|
|
297
|
+
label: "Node.js version",
|
|
298
|
+
status: "warn",
|
|
299
|
+
detail: `Detected ${nodeVersion}; recommended runtime is ${RECOMMENDED_NODE_MAJOR}+.`,
|
|
300
|
+
hint: `Upgrade Node.js for best compatibility: use nvm/fnm and install Node.js ${RECOMMENDED_NODE_MAJOR}+.`,
|
|
301
|
+
category: "system"
|
|
302
|
+
});
|
|
303
|
+
} else {
|
|
304
|
+
pushCheck({
|
|
305
|
+
label: "Node.js version",
|
|
306
|
+
status: "ok",
|
|
307
|
+
detail: nodeVersion,
|
|
308
|
+
category: "system"
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
const npmVersionResult = spawnSync("npm", ["--version"], { encoding: "utf-8", shell: process.platform === "win32" });
|
|
312
|
+
const npmPrefixResult = spawnSync("npm", ["config", "get", "prefix"], {
|
|
313
|
+
encoding: "utf-8",
|
|
314
|
+
shell: process.platform === "win32"
|
|
315
|
+
});
|
|
316
|
+
if (npmPrefixResult.error || npmPrefixResult.status !== 0) {
|
|
317
|
+
pushCheck({
|
|
318
|
+
label: "npm global install location",
|
|
319
|
+
status: "error",
|
|
320
|
+
detail: npmPrefixResult.error?.message || npmPrefixResult.stderr?.trim() || "Unable to read npm global prefix.",
|
|
321
|
+
hint: "Verify npm is installed and available in PATH.",
|
|
322
|
+
category: "system"
|
|
323
|
+
});
|
|
324
|
+
} else {
|
|
325
|
+
const prefix = String(npmPrefixResult.stdout ?? "").trim();
|
|
326
|
+
const npmVersionText = npmVersionResult.status === 0 ? String(npmVersionResult.stdout ?? "").trim() : "unknown";
|
|
327
|
+
const npmMajor = majorVersionOf(npmVersionText);
|
|
328
|
+
if (!prefix) {
|
|
329
|
+
pushCheck({
|
|
330
|
+
label: "npm global install location",
|
|
331
|
+
status: "warn",
|
|
332
|
+
detail: `npm ${npmVersionText}, but global prefix is empty.`,
|
|
333
|
+
hint: "Run `npm config get prefix` and configure a writable prefix if needed.",
|
|
334
|
+
category: "system"
|
|
335
|
+
});
|
|
336
|
+
} else {
|
|
337
|
+
let writable = false;
|
|
338
|
+
try {
|
|
339
|
+
fs.accessSync(prefix, fs.constants.W_OK);
|
|
340
|
+
writable = true;
|
|
341
|
+
} catch {
|
|
342
|
+
writable = false;
|
|
343
|
+
}
|
|
344
|
+
if (!writable) {
|
|
345
|
+
pushCheck({
|
|
346
|
+
label: "npm global install location",
|
|
347
|
+
status: "warn",
|
|
348
|
+
detail: `npm ${npmVersionText}, prefix ${prefix} is not writable.`,
|
|
349
|
+
hint: "Run `npm config set prefix ~/.npm-global` and add `~/.npm-global/bin` to PATH.",
|
|
350
|
+
category: "system"
|
|
351
|
+
});
|
|
352
|
+
} else if (npmMajor !== null && npmMajor < MINIMUM_NPM_MAJOR) {
|
|
353
|
+
pushCheck({
|
|
354
|
+
label: "npm global install location",
|
|
355
|
+
status: "warn",
|
|
356
|
+
detail: `npm ${npmVersionText}, prefix ${prefix} is writable.`,
|
|
357
|
+
hint: `Upgrade npm to ${MINIMUM_NPM_MAJOR}+ for best install compatibility.`,
|
|
358
|
+
category: "system"
|
|
359
|
+
});
|
|
360
|
+
} else {
|
|
361
|
+
pushCheck({
|
|
362
|
+
label: "npm global install location",
|
|
363
|
+
status: "ok",
|
|
364
|
+
detail: `npm ${npmVersionText}, prefix ${prefix} is writable.`,
|
|
365
|
+
category: "system"
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const qmdInstalled = hasQmd();
|
|
371
|
+
if (qmdInstalled) {
|
|
372
|
+
pushCheck({
|
|
373
|
+
label: "qmd availability",
|
|
374
|
+
status: "ok",
|
|
375
|
+
detail: "qmd detected in PATH (optional fallback enabled).",
|
|
376
|
+
category: "search"
|
|
377
|
+
});
|
|
378
|
+
} else {
|
|
379
|
+
pushCheck({
|
|
380
|
+
label: "qmd availability",
|
|
381
|
+
status: "warn",
|
|
382
|
+
detail: "qmd not found in PATH (optional dependency).",
|
|
383
|
+
hint: `In-process BM25 is available by default. To enable qmd fallback: ${QMD_INSTALL_COMMAND} (${QMD_INSTALL_URL})`,
|
|
384
|
+
category: "search"
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
const resolvedVaultPath = resolveDoctorVaultPath(options.vaultPath);
|
|
388
|
+
let vaultPathForChecks = resolvedVaultPath;
|
|
389
|
+
let vaultReadyForDeepChecks = false;
|
|
390
|
+
if (!resolvedVaultPath) {
|
|
391
|
+
pushCheck({
|
|
392
|
+
label: "vault directory",
|
|
393
|
+
status: "warn",
|
|
394
|
+
detail: "No vault path detected from --vault, CLAWVAULT_PATH, or current directory.",
|
|
395
|
+
hint: "Run `clawvault init <path>` and set `CLAWVAULT_PATH` (or use `--vault`).",
|
|
396
|
+
category: "vault"
|
|
397
|
+
});
|
|
398
|
+
} else {
|
|
399
|
+
try {
|
|
400
|
+
const stats = fs.statSync(resolvedVaultPath);
|
|
401
|
+
if (!stats.isDirectory()) {
|
|
402
|
+
pushCheck({
|
|
403
|
+
label: "vault directory",
|
|
404
|
+
status: "error",
|
|
405
|
+
detail: `${resolvedVaultPath} exists but is not a directory.`,
|
|
406
|
+
hint: "Use `clawvault doctor --vault <vault-directory>` with a valid vault folder.",
|
|
407
|
+
category: "vault"
|
|
408
|
+
});
|
|
409
|
+
} else {
|
|
410
|
+
try {
|
|
411
|
+
fs.accessSync(resolvedVaultPath, fs.constants.W_OK);
|
|
412
|
+
pushCheck({
|
|
413
|
+
label: "vault directory",
|
|
414
|
+
status: "ok",
|
|
415
|
+
detail: `${resolvedVaultPath} is writable.`,
|
|
416
|
+
category: "vault"
|
|
417
|
+
});
|
|
418
|
+
vaultReadyForDeepChecks = true;
|
|
419
|
+
} catch {
|
|
420
|
+
pushCheck({
|
|
421
|
+
label: "vault directory",
|
|
422
|
+
status: "error",
|
|
423
|
+
detail: `${resolvedVaultPath} exists but is not writable.`,
|
|
424
|
+
hint: "Fix directory permissions (`chmod`/`chown`) or select another vault path.",
|
|
425
|
+
category: "vault"
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
} catch {
|
|
430
|
+
pushCheck({
|
|
431
|
+
label: "vault directory",
|
|
432
|
+
status: "error",
|
|
433
|
+
detail: `${resolvedVaultPath} does not exist.`,
|
|
434
|
+
hint: "Create the vault with `clawvault init <path>` or point to an existing vault via `--vault`.",
|
|
435
|
+
category: "vault"
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
let parsedConfig;
|
|
440
|
+
if (!vaultReadyForDeepChecks || !vaultPathForChecks) {
|
|
441
|
+
pushCheck({
|
|
442
|
+
label: "vault config file",
|
|
443
|
+
status: "warn",
|
|
444
|
+
detail: "Skipped because no usable vault directory was detected.",
|
|
445
|
+
hint: "Resolve vault directory issues first, then rerun `clawvault doctor`.",
|
|
446
|
+
category: "config"
|
|
447
|
+
});
|
|
448
|
+
} else {
|
|
449
|
+
const configPath = selectConfigPath(vaultPathForChecks);
|
|
450
|
+
if (!configPath) {
|
|
451
|
+
pushCheck({
|
|
452
|
+
label: "vault config file",
|
|
453
|
+
status: "error",
|
|
454
|
+
detail: "No config file found (.clawvault.json/.yaml/.yml).",
|
|
455
|
+
hint: "Run `clawvault init` or create a valid vault config file.",
|
|
456
|
+
category: "config"
|
|
457
|
+
});
|
|
458
|
+
} else {
|
|
459
|
+
const parsed = parseConfigDocument(configPath);
|
|
460
|
+
if (parsed.error || !parsed.data) {
|
|
461
|
+
pushCheck({
|
|
462
|
+
label: "vault config file",
|
|
463
|
+
status: "error",
|
|
464
|
+
detail: `${path.basename(configPath)} is invalid ${parsed.format.toUpperCase()}: ${parsed.error}`,
|
|
465
|
+
hint: `Fix ${path.basename(configPath)} syntax and rerun \`clawvault doctor\`.`,
|
|
466
|
+
category: "config"
|
|
467
|
+
});
|
|
468
|
+
} else {
|
|
469
|
+
parsedConfig = parsed.data;
|
|
470
|
+
pushCheck({
|
|
471
|
+
label: "vault config file",
|
|
472
|
+
status: "ok",
|
|
473
|
+
detail: `${path.basename(configPath)} (${parsed.format.toUpperCase()})`,
|
|
474
|
+
category: "config"
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
pushCheck({
|
|
480
|
+
label: "in-process BM25 engine",
|
|
481
|
+
status: "ok",
|
|
482
|
+
detail: "Built-in search backend is available.",
|
|
483
|
+
category: "search"
|
|
484
|
+
});
|
|
485
|
+
if (!parsedConfig) {
|
|
486
|
+
pushCheck({
|
|
487
|
+
label: "semantic embeddings",
|
|
488
|
+
status: "warn",
|
|
489
|
+
detail: "Could not evaluate embeddings configuration without a valid vault config.",
|
|
490
|
+
hint: "Fix vault config issues, then configure embeddings with `clawvault config set search.embeddings.provider <provider>`.",
|
|
491
|
+
category: "search"
|
|
492
|
+
});
|
|
493
|
+
} else {
|
|
494
|
+
const search = parsedConfig.search && typeof parsedConfig.search === "object" && !Array.isArray(parsedConfig.search) ? parsedConfig.search : {};
|
|
495
|
+
const embeddings = search.embeddings && typeof search.embeddings === "object" && !Array.isArray(search.embeddings) ? search.embeddings : {};
|
|
496
|
+
const backend = typeof search.backend === "string" ? search.backend : "in-process";
|
|
497
|
+
const provider = typeof embeddings.provider === "string" ? embeddings.provider : "none";
|
|
498
|
+
const apiKey = typeof embeddings.apiKey === "string" ? embeddings.apiKey.trim() : "";
|
|
499
|
+
if (backend === "qmd" && !qmdInstalled) {
|
|
500
|
+
pushCheck({
|
|
501
|
+
label: "search backend configuration",
|
|
502
|
+
status: "warn",
|
|
503
|
+
detail: "Config prefers qmd backend, but qmd is not installed.",
|
|
504
|
+
hint: "Install qmd or run `clawvault config set search.backend in-process`.",
|
|
505
|
+
category: "search"
|
|
506
|
+
});
|
|
507
|
+
} else {
|
|
508
|
+
pushCheck({
|
|
509
|
+
label: "search backend configuration",
|
|
510
|
+
status: "ok",
|
|
511
|
+
detail: `backend=${backend}`,
|
|
512
|
+
category: "search"
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
if (provider === "none") {
|
|
516
|
+
pushCheck({
|
|
517
|
+
label: "semantic embeddings",
|
|
518
|
+
status: "warn",
|
|
519
|
+
detail: "Embeddings provider is not configured.",
|
|
520
|
+
hint: "Configure semantic search: `clawvault config set search.embeddings.provider openai|gemini|ollama`.",
|
|
521
|
+
category: "search"
|
|
522
|
+
});
|
|
523
|
+
} else if ((provider === "openai" || provider === "gemini") && !apiKey && !process.env.OPENAI_API_KEY && !process.env.GEMINI_API_KEY && !process.env.GOOGLE_API_KEY) {
|
|
524
|
+
pushCheck({
|
|
525
|
+
label: "semantic embeddings",
|
|
526
|
+
status: "warn",
|
|
527
|
+
detail: `${provider} provider configured, but no API key was found.`,
|
|
528
|
+
hint: provider === "openai" ? "Set `search.embeddings.apiKey` or export `OPENAI_API_KEY`." : "Set `search.embeddings.apiKey` or export `GEMINI_API_KEY`/`GOOGLE_API_KEY`.",
|
|
529
|
+
category: "search"
|
|
530
|
+
});
|
|
531
|
+
} else {
|
|
532
|
+
pushCheck({
|
|
533
|
+
label: "semantic embeddings",
|
|
534
|
+
status: "ok",
|
|
535
|
+
detail: `${provider} provider configured.`,
|
|
536
|
+
category: "search"
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
const openClawResult = spawnSync("openclaw", ["hooks", "list", "--verbose"], {
|
|
541
|
+
encoding: "utf-8",
|
|
542
|
+
shell: process.platform === "win32"
|
|
543
|
+
});
|
|
544
|
+
const openClawError = openClawResult.error;
|
|
545
|
+
if (openClawError?.code === "ENOENT") {
|
|
546
|
+
pushCheck({
|
|
547
|
+
label: "OpenClaw plugin registration",
|
|
548
|
+
status: "warn",
|
|
549
|
+
detail: "openclaw CLI not found; integration check skipped.",
|
|
550
|
+
hint: "Install OpenClaw CLI, then run `openclaw hooks install clawvault && openclaw hooks enable clawvault`.",
|
|
551
|
+
category: "integration"
|
|
552
|
+
});
|
|
553
|
+
} else if (openClawError || openClawResult.status !== 0) {
|
|
554
|
+
pushCheck({
|
|
555
|
+
label: "OpenClaw plugin registration",
|
|
556
|
+
status: "warn",
|
|
557
|
+
detail: openClawError?.message || openClawResult.stderr?.trim() || "Unable to read OpenClaw hooks list.",
|
|
558
|
+
hint: "Run `openclaw hooks list --verbose` to inspect hook/plugin state.",
|
|
559
|
+
category: "integration"
|
|
560
|
+
});
|
|
561
|
+
} else if (/clawvault/i.test(`${openClawResult.stdout}
|
|
562
|
+
${openClawResult.stderr}`)) {
|
|
563
|
+
pushCheck({
|
|
564
|
+
label: "OpenClaw plugin registration",
|
|
565
|
+
status: "ok",
|
|
566
|
+
detail: "clawvault appears in OpenClaw hook/plugin list.",
|
|
567
|
+
category: "integration"
|
|
568
|
+
});
|
|
569
|
+
} else {
|
|
570
|
+
pushCheck({
|
|
571
|
+
label: "OpenClaw plugin registration",
|
|
572
|
+
status: "warn",
|
|
573
|
+
detail: "clawvault is not currently registered in OpenClaw hooks/plugins.",
|
|
574
|
+
hint: "Run `openclaw hooks install clawvault && openclaw hooks enable clawvault`.",
|
|
575
|
+
category: "integration"
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
if (!vaultReadyForDeepChecks || !vaultPathForChecks) {
|
|
579
|
+
pushCheck({
|
|
580
|
+
label: "disk space",
|
|
581
|
+
status: "warn",
|
|
582
|
+
detail: "Skipped because vault directory is unavailable.",
|
|
583
|
+
hint: "Resolve vault directory issues and rerun doctor.",
|
|
584
|
+
category: "storage"
|
|
585
|
+
});
|
|
586
|
+
} else {
|
|
587
|
+
try {
|
|
588
|
+
const availableBytes = computeAvailableDiskBytes(vaultPathForChecks);
|
|
589
|
+
if (availableBytes === null) {
|
|
590
|
+
pushCheck({
|
|
591
|
+
label: "disk space",
|
|
592
|
+
status: "warn",
|
|
593
|
+
detail: "Disk space check is not supported on this platform/runtime.",
|
|
594
|
+
hint: "Use `df -h` (Linux/macOS) or File Explorer properties (Windows) to verify free space.",
|
|
595
|
+
category: "storage"
|
|
596
|
+
});
|
|
597
|
+
} else if (availableBytes < DISK_ERROR_THRESHOLD_BYTES) {
|
|
598
|
+
pushCheck({
|
|
599
|
+
label: "disk space",
|
|
600
|
+
status: "error",
|
|
601
|
+
detail: `${humanBytes(availableBytes)} free in vault filesystem.`,
|
|
602
|
+
hint: "Free disk space immediately or move the vault to a filesystem with more capacity.",
|
|
603
|
+
category: "storage"
|
|
604
|
+
});
|
|
605
|
+
} else if (availableBytes < DISK_WARN_THRESHOLD_BYTES) {
|
|
606
|
+
pushCheck({
|
|
607
|
+
label: "disk space",
|
|
608
|
+
status: "warn",
|
|
609
|
+
detail: `${humanBytes(availableBytes)} free in vault filesystem.`,
|
|
610
|
+
hint: "Consider freeing space to avoid indexing and write failures.",
|
|
611
|
+
category: "storage"
|
|
612
|
+
});
|
|
613
|
+
} else {
|
|
614
|
+
pushCheck({
|
|
615
|
+
label: "disk space",
|
|
616
|
+
status: "ok",
|
|
617
|
+
detail: `${humanBytes(availableBytes)} free in vault filesystem.`,
|
|
618
|
+
category: "storage"
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
} catch (error) {
|
|
622
|
+
pushCheck({
|
|
623
|
+
label: "disk space",
|
|
624
|
+
status: "warn",
|
|
625
|
+
detail: error?.message || "Unable to read filesystem stats.",
|
|
626
|
+
hint: "Verify disk free space manually and rerun doctor.",
|
|
627
|
+
category: "storage"
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
const gitResult = spawnSync("git", ["--version"], { encoding: "utf-8", shell: process.platform === "win32" });
|
|
632
|
+
if (gitResult.error || gitResult.status !== 0) {
|
|
633
|
+
pushCheck({
|
|
634
|
+
label: "git availability",
|
|
635
|
+
status: "warn",
|
|
636
|
+
detail: gitResult.error?.message || gitResult.stderr?.trim() || "git is not available in PATH.",
|
|
637
|
+
hint: "Install git and ensure `git --version` works in this shell.",
|
|
638
|
+
category: "system"
|
|
639
|
+
});
|
|
640
|
+
} else {
|
|
641
|
+
pushCheck({
|
|
642
|
+
label: "git availability",
|
|
643
|
+
status: "ok",
|
|
644
|
+
detail: String(gitResult.stdout ?? "").trim() || "git available",
|
|
645
|
+
category: "system"
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
pushCheck({
|
|
649
|
+
label: "OS / architecture",
|
|
650
|
+
status: "ok",
|
|
651
|
+
detail: `${os.platform()} ${os.release()} (${os.arch()})`,
|
|
652
|
+
category: "system"
|
|
653
|
+
});
|
|
654
|
+
if (qmdInstalled && vaultReadyForDeepChecks && vaultPathForChecks) {
|
|
655
|
+
try {
|
|
656
|
+
const vault = await resolveVault(vaultPathForChecks);
|
|
657
|
+
const detectedMigrationIssues = detectMigrationIssues(
|
|
658
|
+
vault.getPath(),
|
|
659
|
+
vault.getQmdCollection(),
|
|
660
|
+
vault.getQmdRoot()
|
|
661
|
+
);
|
|
662
|
+
migrationIssues.push(...detectedMigrationIssues);
|
|
663
|
+
for (const check of migrationIssuesToChecks(detectedMigrationIssues)) {
|
|
664
|
+
pushCheck(check);
|
|
665
|
+
}
|
|
666
|
+
vaultPathForChecks = vault.getPath();
|
|
667
|
+
} catch {
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
const warnings = checks.filter((check) => check.status === "warn").length;
|
|
671
|
+
const errors = checks.filter((check) => check.status === "error").length;
|
|
672
|
+
return {
|
|
673
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
674
|
+
vaultPath: vaultPathForChecks,
|
|
675
|
+
checks,
|
|
676
|
+
warnings,
|
|
677
|
+
errors,
|
|
678
|
+
migrationIssues
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
export {
|
|
683
|
+
doctor
|
|
684
|
+
};
|