@llmkb/claude-code 0.1.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 +83 -0
- package/dist/cli.js +3214 -0
- package/dist/cli.js.map +1 -0
- package/lib/color.ts +61 -0
- package/lib/config-validation.ts +332 -0
- package/lib/config.ts +61 -0
- package/lib/credentials.ts +164 -0
- package/lib/output.ts +130 -0
- package/lib/parser.ts +274 -0
- package/lib/skills.ts +554 -0
- package/lib/sync-spaces-config.ts +180 -0
- package/lib/sync-state.ts +152 -0
- package/lib/sync.ts +437 -0
- package/lib/types.ts +153 -0
- package/lib/watch-lock.ts +78 -0
- package/lib/writer.ts +409 -0
- package/package.json +55 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,3214 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// lib/types.ts
|
|
13
|
+
function getDefaultPluginConfig() {
|
|
14
|
+
return {
|
|
15
|
+
llmkb_base_url: "https://api.llmkb.ai",
|
|
16
|
+
watch: {
|
|
17
|
+
enabled: true,
|
|
18
|
+
debounce_ms: 300,
|
|
19
|
+
gitignore: true,
|
|
20
|
+
llmkbignore: true,
|
|
21
|
+
default_verbosity: "silent"
|
|
22
|
+
},
|
|
23
|
+
sync: {
|
|
24
|
+
concurrency: 5,
|
|
25
|
+
retry_attempts: 3,
|
|
26
|
+
retry_delay_ms: 1e3
|
|
27
|
+
},
|
|
28
|
+
hook: {
|
|
29
|
+
timeout_ms: 5e3,
|
|
30
|
+
skip: false
|
|
31
|
+
},
|
|
32
|
+
debug: false
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
var PACKAGE_VERSION;
|
|
36
|
+
var init_types = __esm({
|
|
37
|
+
"lib/types.ts"() {
|
|
38
|
+
"use strict";
|
|
39
|
+
PACKAGE_VERSION = "0.1.0";
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// lib/parser.ts
|
|
44
|
+
import { readFile, writeFile } from "fs/promises";
|
|
45
|
+
import { existsSync } from "fs";
|
|
46
|
+
import { join, dirname, resolve } from "path";
|
|
47
|
+
import { parse, stringify, parseDocument } from "yaml";
|
|
48
|
+
import { writeFile as writeVersion } from "fs/promises";
|
|
49
|
+
function findProjectRoot(startDir = process.cwd()) {
|
|
50
|
+
let current = resolve(startDir);
|
|
51
|
+
while (current !== dirname(current)) {
|
|
52
|
+
if (existsSync(join(current, ".llmkb"))) {
|
|
53
|
+
return current;
|
|
54
|
+
}
|
|
55
|
+
current = dirname(current);
|
|
56
|
+
}
|
|
57
|
+
if (existsSync(join(current, ".llmkb"))) {
|
|
58
|
+
return current;
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
async function readSpaceConfig(projectDir) {
|
|
63
|
+
const filePath = join(projectDir, ".llmkb", "spaces.yml");
|
|
64
|
+
if (!existsSync(filePath)) return null;
|
|
65
|
+
try {
|
|
66
|
+
const content = await readFile(filePath, "utf-8");
|
|
67
|
+
const raw = parse(content);
|
|
68
|
+
if (raw === null) {
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
71
|
+
if (raw.active !== void 0 || isOldSpaceFormat(raw)) {
|
|
72
|
+
const migrated = migrateOldSpaceConfig(raw);
|
|
73
|
+
await writeSpaceConfig(projectDir, migrated);
|
|
74
|
+
await writeVersion(
|
|
75
|
+
join(projectDir, ".llmkb", ".llmkb-version"),
|
|
76
|
+
PACKAGE_VERSION + "\n",
|
|
77
|
+
"utf-8"
|
|
78
|
+
);
|
|
79
|
+
return migrated;
|
|
80
|
+
}
|
|
81
|
+
return raw;
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function isOldSpaceFormat(raw) {
|
|
87
|
+
if (!raw.spaces || !Array.isArray(raw.spaces)) return false;
|
|
88
|
+
return raw.spaces.length > 0 && typeof raw.spaces[0] === "object" && raw.spaces[0] !== null && "name" in raw.spaces[0] && !("id" in raw.spaces[0]);
|
|
89
|
+
}
|
|
90
|
+
function migrateOldSpaceConfig(old) {
|
|
91
|
+
const result = {};
|
|
92
|
+
const oldSpaces = old.spaces ?? [];
|
|
93
|
+
const activeName = old.active ?? oldSpaces[0]?.name;
|
|
94
|
+
if (oldSpaces.length > 0) {
|
|
95
|
+
if (activeName) {
|
|
96
|
+
const activeDef = oldSpaces.find((s) => s.name === activeName);
|
|
97
|
+
const projectEntry = {
|
|
98
|
+
id: String(activeDef?.name ?? activeName),
|
|
99
|
+
name: String(activeDef?.label ?? activeName)
|
|
100
|
+
};
|
|
101
|
+
result.project_space = [projectEntry];
|
|
102
|
+
}
|
|
103
|
+
result.spaces = oldSpaces.map((s) => ({
|
|
104
|
+
id: String(s.name),
|
|
105
|
+
name: s.label ? String(s.label) : String(s.name)
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
async function writeSpaceConfig(projectDir, config) {
|
|
111
|
+
const filePath = join(projectDir, ".llmkb", "spaces.yml");
|
|
112
|
+
const yaml = stringify(config, { lineWidth: 120 });
|
|
113
|
+
await writeFile(filePath, yaml + "\n", "utf-8");
|
|
114
|
+
}
|
|
115
|
+
async function addSpaceEntry(projectDir, spaceId, spaceName) {
|
|
116
|
+
let config = await readSpaceConfig(projectDir);
|
|
117
|
+
if (!config) config = {};
|
|
118
|
+
if (!config.spaces) config.spaces = [];
|
|
119
|
+
if (!config.spaces.some((s) => s.id === spaceId)) {
|
|
120
|
+
config.spaces.push({ id: spaceId, name: spaceName });
|
|
121
|
+
}
|
|
122
|
+
await writeSpaceConfig(projectDir, config);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
async function removeSpaceEntry(projectDir, spaceId) {
|
|
126
|
+
const config = await readSpaceConfig(projectDir);
|
|
127
|
+
if (!config?.spaces) return false;
|
|
128
|
+
const idx = config.spaces.findIndex((s) => s.id === spaceId);
|
|
129
|
+
if (idx === -1) return false;
|
|
130
|
+
config.spaces.splice(idx, 1);
|
|
131
|
+
await writeSpaceConfig(projectDir, config);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
async function updateProjectSpace(projectDir, spaceId) {
|
|
135
|
+
const config = await readSpaceConfig(projectDir);
|
|
136
|
+
if (!config) return false;
|
|
137
|
+
if (!config.project_space || config.project_space.length === 0) {
|
|
138
|
+
config.project_space = [{ id: spaceId }];
|
|
139
|
+
} else {
|
|
140
|
+
config.project_space[0].id = spaceId;
|
|
141
|
+
}
|
|
142
|
+
await writeSpaceConfig(projectDir, config);
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
async function deleteAllSpaces(projectDir) {
|
|
146
|
+
const config = await readSpaceConfig(projectDir);
|
|
147
|
+
if (!config) return false;
|
|
148
|
+
config.spaces = [];
|
|
149
|
+
await writeSpaceConfig(projectDir, config);
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
async function readVersionStamp(projectDir) {
|
|
153
|
+
const filePath = join(projectDir, ".llmkb", ".llmkb-version");
|
|
154
|
+
if (!existsSync(filePath)) return null;
|
|
155
|
+
try {
|
|
156
|
+
const content = await readFile(filePath, "utf-8");
|
|
157
|
+
return content.trim();
|
|
158
|
+
} catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
var init_parser = __esm({
|
|
163
|
+
"lib/parser.ts"() {
|
|
164
|
+
"use strict";
|
|
165
|
+
init_types();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// lib/sync-state.ts
|
|
170
|
+
var sync_state_exports = {};
|
|
171
|
+
__export(sync_state_exports, {
|
|
172
|
+
clearSyncState: () => clearSyncState,
|
|
173
|
+
computeSha256: () => computeSha256,
|
|
174
|
+
getChangedFiles: () => getChangedFiles,
|
|
175
|
+
readSyncState: () => readSyncState,
|
|
176
|
+
updateSyncEntry: () => updateSyncEntry,
|
|
177
|
+
writeSyncState: () => writeSyncState
|
|
178
|
+
});
|
|
179
|
+
import { readFile as readFile4, writeFile as writeFile6, unlink, mkdir as mkdir4 } from "fs/promises";
|
|
180
|
+
import { existsSync as existsSync7 } from "fs";
|
|
181
|
+
import { createHash } from "crypto";
|
|
182
|
+
import { createReadStream } from "fs";
|
|
183
|
+
import { resolve as resolve5 } from "path";
|
|
184
|
+
function statePath(projectDir) {
|
|
185
|
+
return resolve5(projectDir, SYNC_STATE_FILE);
|
|
186
|
+
}
|
|
187
|
+
function llmkbDir(projectDir) {
|
|
188
|
+
return resolve5(projectDir, ".llmkb");
|
|
189
|
+
}
|
|
190
|
+
async function computeSha256(filePath) {
|
|
191
|
+
return new Promise((resolve8, reject) => {
|
|
192
|
+
const hash = createHash("sha256");
|
|
193
|
+
const stream = createReadStream(filePath);
|
|
194
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
195
|
+
stream.on("end", () => resolve8(hash.digest("hex")));
|
|
196
|
+
stream.on("error", reject);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
async function readSyncState(projectDir) {
|
|
200
|
+
const filePath = statePath(projectDir);
|
|
201
|
+
if (!existsSync7(filePath)) return null;
|
|
202
|
+
try {
|
|
203
|
+
const raw = await readFile4(filePath, "utf-8");
|
|
204
|
+
return JSON.parse(raw);
|
|
205
|
+
} catch {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async function writeSyncState(projectDir, state) {
|
|
210
|
+
const dir = llmkbDir(projectDir);
|
|
211
|
+
if (!existsSync7(dir)) {
|
|
212
|
+
await mkdir4(dir, { recursive: true });
|
|
213
|
+
}
|
|
214
|
+
await writeFile6(statePath(projectDir), JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
215
|
+
}
|
|
216
|
+
async function updateSyncEntry(projectDir, relativePath, entry) {
|
|
217
|
+
const state = await readSyncState(projectDir) ?? {
|
|
218
|
+
space: "",
|
|
219
|
+
lastSyncAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
220
|
+
files: {}
|
|
221
|
+
};
|
|
222
|
+
state.files[relativePath] = entry;
|
|
223
|
+
state.lastSyncAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
224
|
+
await writeSyncState(projectDir, state);
|
|
225
|
+
}
|
|
226
|
+
async function clearSyncState(projectDir) {
|
|
227
|
+
const filePath = statePath(projectDir);
|
|
228
|
+
if (existsSync7(filePath)) {
|
|
229
|
+
await unlink(filePath);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
async function getChangedFiles(files, state) {
|
|
233
|
+
const newFiles = [];
|
|
234
|
+
const changedFiles = [];
|
|
235
|
+
const unchangedFiles = [];
|
|
236
|
+
const renamed = [];
|
|
237
|
+
const hashToPath = /* @__PURE__ */ new Map();
|
|
238
|
+
if (state) {
|
|
239
|
+
for (const [path, entry] of Object.entries(state.files)) {
|
|
240
|
+
hashToPath.set(entry.sha256, path);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
for (const [relPath, absPath] of files) {
|
|
244
|
+
const hash = await computeSha256(absPath);
|
|
245
|
+
if (!state) {
|
|
246
|
+
newFiles.push(relPath);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const existingEntry = state.files[relPath];
|
|
250
|
+
if (!existingEntry) {
|
|
251
|
+
const oldPath = hashToPath.get(hash);
|
|
252
|
+
if (oldPath && oldPath !== relPath) {
|
|
253
|
+
renamed.push({ oldPath, newPath: relPath });
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
newFiles.push(relPath);
|
|
257
|
+
} else if (existingEntry.sha256 !== hash) {
|
|
258
|
+
changedFiles.push(relPath);
|
|
259
|
+
} else {
|
|
260
|
+
unchangedFiles.push(relPath);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return { newFiles, changedFiles, unchangedFiles, renamed };
|
|
264
|
+
}
|
|
265
|
+
var SYNC_STATE_FILE;
|
|
266
|
+
var init_sync_state = __esm({
|
|
267
|
+
"lib/sync-state.ts"() {
|
|
268
|
+
"use strict";
|
|
269
|
+
SYNC_STATE_FILE = ".llmkb/sync-state.json";
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// bin/cli.ts
|
|
274
|
+
import { readFileSync } from "fs";
|
|
275
|
+
import { fileURLToPath } from "url";
|
|
276
|
+
import { dirname as dirname2, resolve as resolve7 } from "path";
|
|
277
|
+
import { Command as Command15 } from "commander";
|
|
278
|
+
|
|
279
|
+
// src/commands/init.ts
|
|
280
|
+
import { Command as Command2 } from "commander";
|
|
281
|
+
import * as readline from "readline/promises";
|
|
282
|
+
import { stdin, stdout } from "process";
|
|
283
|
+
import { resolve as resolve4 } from "path";
|
|
284
|
+
|
|
285
|
+
// lib/writer.ts
|
|
286
|
+
init_types();
|
|
287
|
+
init_parser();
|
|
288
|
+
import { mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
|
|
289
|
+
import { join as join3, resolve as resolve3 } from "path";
|
|
290
|
+
import { stringify as stringify2 } from "yaml";
|
|
291
|
+
|
|
292
|
+
// src/commands/hooks.ts
|
|
293
|
+
import { Command } from "commander";
|
|
294
|
+
import { existsSync as existsSync2 } from "fs";
|
|
295
|
+
import { mkdir, writeFile as writeFile2, rm } from "fs/promises";
|
|
296
|
+
import { join as join2, resolve as resolve2 } from "path";
|
|
297
|
+
var HOOKS_DIR = ".claude/hooks/llmkb";
|
|
298
|
+
function hooksJson() {
|
|
299
|
+
return JSON.stringify(
|
|
300
|
+
{
|
|
301
|
+
hooks: {
|
|
302
|
+
PreToolUse: [
|
|
303
|
+
{
|
|
304
|
+
matcher: "Read|Bash",
|
|
305
|
+
hooks: [
|
|
306
|
+
{
|
|
307
|
+
type: "command",
|
|
308
|
+
command: "node ${CLAUDE_PLUGIN_ROOT}/hooks/llmkb-hook.js --pre --tool ${TOOL_NAME}",
|
|
309
|
+
timeout: 5,
|
|
310
|
+
statusMessage: "Checking llmkb space context..."
|
|
311
|
+
}
|
|
312
|
+
]
|
|
313
|
+
}
|
|
314
|
+
],
|
|
315
|
+
PostToolUse: [
|
|
316
|
+
{
|
|
317
|
+
matcher: "Edit|Write|Bash",
|
|
318
|
+
hooks: [
|
|
319
|
+
{
|
|
320
|
+
type: "command",
|
|
321
|
+
command: "node ${CLAUDE_PLUGIN_ROOT}/hooks/llmkb-hook.js --post --tool ${TOOL_NAME} --exit-code ${EXIT_CODE}",
|
|
322
|
+
timeout: 5,
|
|
323
|
+
statusMessage: "Checking sync freshness..."
|
|
324
|
+
}
|
|
325
|
+
]
|
|
326
|
+
}
|
|
327
|
+
]
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
null,
|
|
331
|
+
2
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
function hookScript() {
|
|
335
|
+
return `#!/usr/bin/env node
|
|
336
|
+
// llmkb Claude Code hook \u2014 PreToolUse (space context) + PostToolUse (sync freshness)
|
|
337
|
+
// Input: --pre/--post, --tool <name>, --exit-code <code>
|
|
338
|
+
// Stdin: { hook_event_name, tool_name, tool_input, cwd }
|
|
339
|
+
// Stdout: JSON { hookSpecificOutput: { hookEventName, additionalContext } }
|
|
340
|
+
|
|
341
|
+
const fs = require("fs");
|
|
342
|
+
const path = require("path");
|
|
343
|
+
const { spawnSync } = require("child_process");
|
|
344
|
+
|
|
345
|
+
// --- Config ---
|
|
346
|
+
const HOOK_TIMEOUT = parseInt(process.env.LLMKB_HOOK_TIMEOUT_MS || "5000", 10);
|
|
347
|
+
const HOOK_SKIP = process.env.LLMKB_HOOK_SKIP === "1" || process.env.LLMKB_HOOK_SKIP === "true";
|
|
348
|
+
const DEBUG = process.env.LLMKB_DEBUG === "1" || process.env.LLMKB_DEBUG === "true";
|
|
349
|
+
const POWERSHELL_PATH = process.env.LLMKB_HOOK_POWERSHELL_PATH || "powershell.exe";
|
|
350
|
+
|
|
351
|
+
function log(msg) {
|
|
352
|
+
if (DEBUG) process.stderr.write("[llmkb hook] " + msg + "\\n");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function findProjectRoot(startDir) {
|
|
356
|
+
let dir = startDir;
|
|
357
|
+
for (let i = 0; i < 10; i++) {
|
|
358
|
+
if (fs.existsSync(path.join(dir, ".llmkb", "spaces.yml"))) return dir;
|
|
359
|
+
const parent = path.dirname(dir);
|
|
360
|
+
if (parent === dir) return null;
|
|
361
|
+
dir = parent;
|
|
362
|
+
}
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function getActiveSpace(projectRoot) {
|
|
367
|
+
try {
|
|
368
|
+
const content = fs.readFileSync(path.join(projectRoot, ".llmkb", "spaces.yml"), "utf-8");
|
|
369
|
+
const match = content.match(/^active:\\s*(\\S+)/m);
|
|
370
|
+
return match ? match[1] : null;
|
|
371
|
+
} catch { return null; }
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function handlePreToolUse(input) {
|
|
375
|
+
if (HOOK_SKIP) return;
|
|
376
|
+
const cwd = input.cwd || process.cwd();
|
|
377
|
+
const projectRoot = findProjectRoot(cwd);
|
|
378
|
+
if (!projectRoot) return; // silent skip
|
|
379
|
+
const space = getActiveSpace(projectRoot);
|
|
380
|
+
if (!space) return;
|
|
381
|
+
log("active space: " + space);
|
|
382
|
+
console.log(JSON.stringify({
|
|
383
|
+
hookSpecificOutput: {
|
|
384
|
+
hookEventName: "PreToolUse",
|
|
385
|
+
additionalContext: "llmkb space: " + space
|
|
386
|
+
}
|
|
387
|
+
}));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function handlePostToolUse(input) {
|
|
391
|
+
if (HOOK_SKIP) return;
|
|
392
|
+
const toolName = input.tool_name || "";
|
|
393
|
+
if (toolName !== "Edit" && toolName !== "Write" && toolName !== "Bash") return;
|
|
394
|
+
const exitCode = input.tool_output ? input.tool_output.exit_code : 0;
|
|
395
|
+
if (exitCode !== 0) return;
|
|
396
|
+
const cwd = input.cwd || process.cwd();
|
|
397
|
+
const projectRoot = findProjectRoot(cwd);
|
|
398
|
+
if (!projectRoot) return;
|
|
399
|
+
log("post-tool: suggesting sync freshness check");
|
|
400
|
+
console.log(JSON.stringify({
|
|
401
|
+
hookSpecificOutput: {
|
|
402
|
+
hookEventName: "PostToolUse",
|
|
403
|
+
additionalContext: "Files may have changed. Consider running \`llmkb sync\` to update the space."
|
|
404
|
+
}
|
|
405
|
+
}));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function main() {
|
|
409
|
+
try {
|
|
410
|
+
const args = process.argv.slice(2);
|
|
411
|
+
const isPre = args.includes("--pre");
|
|
412
|
+
const isPost = args.includes("--post");
|
|
413
|
+
const raw = fs.readFileSync(0, "utf-8").trim();
|
|
414
|
+
const input = raw ? JSON.parse(raw) : {};
|
|
415
|
+
|
|
416
|
+
if (isPre) handlePreToolUse(input);
|
|
417
|
+
else if (isPost) handlePostToolUse(input);
|
|
418
|
+
} catch (err) {
|
|
419
|
+
if (DEBUG) process.stderr.write("[llmkb hook] error: " + (err.message || "") + "\\n");
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
main();
|
|
424
|
+
`;
|
|
425
|
+
}
|
|
426
|
+
function ps1Script() {
|
|
427
|
+
return `# llmkb Claude Code hook \u2014 PowerShell fallback for Windows
|
|
428
|
+
# Input: --pre/--post, --tool <name>, --exit-code <code>
|
|
429
|
+
# Stdin: JSON { hook_event_name, tool_name, tool_input, cwd }
|
|
430
|
+
|
|
431
|
+
param(
|
|
432
|
+
[switch]$pre,
|
|
433
|
+
[switch]$post,
|
|
434
|
+
[string]$tool = "",
|
|
435
|
+
[int]$exitCode = 0
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
$skip = $env:LLMKB_HOOK_SKIP
|
|
439
|
+
if ($skip -eq "1" -or $skip -eq "true") { exit 0 }
|
|
440
|
+
|
|
441
|
+
$debug = $env:LLMKB_DEBUG
|
|
442
|
+
function Log($msg) {
|
|
443
|
+
if ($debug -eq "1" -or $debug -eq "true") {
|
|
444
|
+
[Console.Error]::WriteLine("[llmkb hook] $msg")
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
# Walk up to find .llmkb/spaces.yml
|
|
449
|
+
$dir = (Get-Location).Path
|
|
450
|
+
for ($i = 0; $i -lt 10; $i++) {
|
|
451
|
+
$check = Join-Path $dir ".llmkb" "spaces.yml"
|
|
452
|
+
if (Test-Path $check) {
|
|
453
|
+
$content = Get-Content $check -Raw
|
|
454
|
+
if ($content -match 'active:\\s*(\\S+)') {
|
|
455
|
+
$space = $Matches[1]
|
|
456
|
+
if ($pre) {
|
|
457
|
+
$result = @{
|
|
458
|
+
hookSpecificOutput = @{
|
|
459
|
+
hookEventName = "PreToolUse"
|
|
460
|
+
additionalContext = "llmkb space: $space"
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
ConvertTo-Json $result -Compress
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
break
|
|
467
|
+
}
|
|
468
|
+
$parent = Split-Path $dir -Parent
|
|
469
|
+
if ($parent -eq $dir) { break }
|
|
470
|
+
$dir = $parent
|
|
471
|
+
}
|
|
472
|
+
`;
|
|
473
|
+
}
|
|
474
|
+
async function writeHooks(projectDir, force) {
|
|
475
|
+
const hooksDir = resolve2(projectDir, HOOKS_DIR);
|
|
476
|
+
const written = [];
|
|
477
|
+
await mkdir(hooksDir, { recursive: true });
|
|
478
|
+
const hooksJsonPath = join2(hooksDir, "hooks.json");
|
|
479
|
+
if (!existsSync2(hooksJsonPath) || force) {
|
|
480
|
+
await writeFile2(hooksJsonPath, hooksJson() + "\n", "utf-8");
|
|
481
|
+
written.push(".claude/hooks/llmkb/hooks.json");
|
|
482
|
+
}
|
|
483
|
+
const hookJsPath = join2(hooksDir, "llmkb-hook.js");
|
|
484
|
+
if (!existsSync2(hookJsPath) || force) {
|
|
485
|
+
await writeFile2(hookJsPath, hookScript(), "utf-8");
|
|
486
|
+
written.push(".claude/hooks/llmkb/llmkb-hook.js");
|
|
487
|
+
}
|
|
488
|
+
const ps1Path = join2(hooksDir, "llmkb-hook.ps1");
|
|
489
|
+
if (!existsSync2(ps1Path) || force) {
|
|
490
|
+
await writeFile2(ps1Path, ps1Script(), "utf-8");
|
|
491
|
+
written.push(".claude/hooks/llmkb/llmkb-hook.ps1");
|
|
492
|
+
}
|
|
493
|
+
return written;
|
|
494
|
+
}
|
|
495
|
+
async function removeHooks(projectDir) {
|
|
496
|
+
const hooksDir = resolve2(projectDir, HOOKS_DIR);
|
|
497
|
+
if (existsSync2(hooksDir)) {
|
|
498
|
+
await rm(hooksDir, { recursive: true, force: true });
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
function hooksInstalled(projectDir) {
|
|
504
|
+
const hooksDir = resolve2(projectDir, HOOKS_DIR);
|
|
505
|
+
return existsSync2(join2(hooksDir, "hooks.json"));
|
|
506
|
+
}
|
|
507
|
+
var hooksCommand = new Command("hooks").description("Manage Claude Code hooks for llmkb context enrichment").addCommand(
|
|
508
|
+
new Command("install").description("Install llmkb hooks").option("-f, --force", "Overwrite existing hook files").action(async (opts) => {
|
|
509
|
+
const projectDir = process.cwd();
|
|
510
|
+
try {
|
|
511
|
+
const files = await writeHooks(projectDir, opts.force);
|
|
512
|
+
if (files.length > 0) {
|
|
513
|
+
console.log(`Installed ${files.length} hook file(s):`);
|
|
514
|
+
for (const f of files) console.log(` \u2714 ${f}`);
|
|
515
|
+
} else {
|
|
516
|
+
console.log("Hooks already installed. Use --force to overwrite.");
|
|
517
|
+
}
|
|
518
|
+
} catch (err) {
|
|
519
|
+
console.error("Error installing hooks:", err.message);
|
|
520
|
+
process.exit(1);
|
|
521
|
+
}
|
|
522
|
+
})
|
|
523
|
+
).addCommand(
|
|
524
|
+
new Command("uninstall").description("Remove llmkb hooks").action(async () => {
|
|
525
|
+
const projectDir = process.cwd();
|
|
526
|
+
const removed = await removeHooks(projectDir);
|
|
527
|
+
if (removed) {
|
|
528
|
+
console.log("Hooks removed.");
|
|
529
|
+
} else {
|
|
530
|
+
console.log("No hooks found.");
|
|
531
|
+
}
|
|
532
|
+
})
|
|
533
|
+
).addCommand(
|
|
534
|
+
new Command("status").description("Check whether hooks are installed").action(() => {
|
|
535
|
+
const projectDir = process.cwd();
|
|
536
|
+
if (hooksInstalled(projectDir)) {
|
|
537
|
+
console.log("llmkb hooks are installed.");
|
|
538
|
+
} else {
|
|
539
|
+
console.log(
|
|
540
|
+
"llmkb hooks are not installed. Run `llmkb hooks install`."
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
})
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
// lib/writer.ts
|
|
547
|
+
function projectPath(projectDir, ...parts) {
|
|
548
|
+
return resolve3(projectDir, ...parts);
|
|
549
|
+
}
|
|
550
|
+
async function ensureDir(filePath) {
|
|
551
|
+
await mkdir2(filePath, { recursive: true });
|
|
552
|
+
}
|
|
553
|
+
var PLUGIN_JSON = {
|
|
554
|
+
name: "llmkb",
|
|
555
|
+
description: "LLM-powered knowledge base with wiki generation, semantic search, and knowledge graphs. Sync codebases, query spaces, and explore architecture from Claude.",
|
|
556
|
+
version: PACKAGE_VERSION,
|
|
557
|
+
author: { name: "llmkb" },
|
|
558
|
+
homepage: "https://llmkb.ai",
|
|
559
|
+
keywords: ["knowledge-base", "wiki", "mcp"]
|
|
560
|
+
};
|
|
561
|
+
var MCP_JSON_CLAUDE = {
|
|
562
|
+
mcpServers: {
|
|
563
|
+
llmkb: {
|
|
564
|
+
description: "llmkb wiki \u2014 search, read, and write your knowledge base via MCP tools",
|
|
565
|
+
command: "docker",
|
|
566
|
+
args: [
|
|
567
|
+
"compose",
|
|
568
|
+
"--profile",
|
|
569
|
+
"mvp",
|
|
570
|
+
"exec",
|
|
571
|
+
"-i",
|
|
572
|
+
"api",
|
|
573
|
+
"python",
|
|
574
|
+
"-m",
|
|
575
|
+
"app.mcp.stdio"
|
|
576
|
+
]
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
var MCP_JSON_CURSOR = {
|
|
581
|
+
mcpServers: {
|
|
582
|
+
llmkb: {
|
|
583
|
+
description: "llmkb wiki \u2014 search, read, and write your knowledge base via MCP tools",
|
|
584
|
+
command: "docker",
|
|
585
|
+
args: [
|
|
586
|
+
"compose",
|
|
587
|
+
"--profile",
|
|
588
|
+
"mvp",
|
|
589
|
+
"exec",
|
|
590
|
+
"-i",
|
|
591
|
+
"api",
|
|
592
|
+
"python",
|
|
593
|
+
"-m",
|
|
594
|
+
"app.mcp.stdio"
|
|
595
|
+
]
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
function buildSpaceConfig(spaceDefs) {
|
|
600
|
+
const config = {};
|
|
601
|
+
if (spaceDefs.length > 0) {
|
|
602
|
+
config.project_space = [{ id: spaceDefs[0].id }];
|
|
603
|
+
config.spaces = spaceDefs;
|
|
604
|
+
}
|
|
605
|
+
return config;
|
|
606
|
+
}
|
|
607
|
+
function spacesYml(projectSpaceId) {
|
|
608
|
+
if (!projectSpaceId) {
|
|
609
|
+
return [
|
|
610
|
+
`# llmkb space configuration`,
|
|
611
|
+
`# Managed by \`llmkb init\`. Edit to add or remove spaces.`,
|
|
612
|
+
`#`,
|
|
613
|
+
`# Example:`,
|
|
614
|
+
`# project_space:`,
|
|
615
|
+
`# - id: b6087faa-0bf0-4935-98b2-f3100905b99c`,
|
|
616
|
+
`# slug: my_project`,
|
|
617
|
+
`# name: "My Project"`,
|
|
618
|
+
`# dirs: ['app', 'src']`,
|
|
619
|
+
`# spaces:`,
|
|
620
|
+
`# - id: c599f13a-2d75-4e4e-8fb3-03afbac7e115`,
|
|
621
|
+
`# name: "Reference Library"`,
|
|
622
|
+
`# slug: reference_library`,
|
|
623
|
+
``
|
|
624
|
+
].join("\n");
|
|
625
|
+
}
|
|
626
|
+
const config = {
|
|
627
|
+
project_space: [{ id: projectSpaceId }]
|
|
628
|
+
};
|
|
629
|
+
return `# llmkb space configuration
|
|
630
|
+
# Managed by \`llmkb init\`. Edit to add or remove spaces.
|
|
631
|
+
|
|
632
|
+
` + stringify2(config, { lineWidth: 120 });
|
|
633
|
+
}
|
|
634
|
+
function configYml() {
|
|
635
|
+
const defaults = getDefaultPluginConfig();
|
|
636
|
+
return [
|
|
637
|
+
`# .llmkb/config.yml \u2014 plugin behavior settings`,
|
|
638
|
+
`# Managed by \`llmkb init\`. Uncomment fields to override defaults.`,
|
|
639
|
+
``,
|
|
640
|
+
`# llmkb_base_url: ${defaults.llmkb_base_url}`,
|
|
641
|
+
`#`,
|
|
642
|
+
`# watch:`,
|
|
643
|
+
`# enabled: ${defaults.watch.enabled}`,
|
|
644
|
+
`# debounce_ms: ${defaults.watch.debounce_ms}`,
|
|
645
|
+
`# gitignore: ${defaults.watch.gitignore}`,
|
|
646
|
+
`# llmkbignore: ${defaults.watch.llmkbignore}`,
|
|
647
|
+
`# default_verbosity: ${defaults.watch.default_verbosity}`,
|
|
648
|
+
`#`,
|
|
649
|
+
`# sync:`,
|
|
650
|
+
`# concurrency: ${defaults.sync.concurrency}`,
|
|
651
|
+
`# retry_attempts: ${defaults.sync.retry_attempts}`,
|
|
652
|
+
`# retry_delay_ms: ${defaults.sync.retry_delay_ms}`,
|
|
653
|
+
`#`,
|
|
654
|
+
`# hook:`,
|
|
655
|
+
`# timeout_ms: ${defaults.hook.timeout_ms}`,
|
|
656
|
+
`# skip: ${defaults.hook.skip}`,
|
|
657
|
+
`#`,
|
|
658
|
+
`# debug: ${defaults.debug}`,
|
|
659
|
+
``
|
|
660
|
+
].join("\n");
|
|
661
|
+
}
|
|
662
|
+
async function writePluginJson(projectDir) {
|
|
663
|
+
const dir = projectPath(projectDir, ".claude-plugin");
|
|
664
|
+
await ensureDir(dir);
|
|
665
|
+
await writeFile3(
|
|
666
|
+
join3(dir, "plugin.json"),
|
|
667
|
+
JSON.stringify(PLUGIN_JSON, null, 2) + "\n",
|
|
668
|
+
"utf-8"
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
async function writeMcpJson(projectDir, platform) {
|
|
672
|
+
if (platform === "cursor") {
|
|
673
|
+
const dir = projectPath(projectDir, ".cursor");
|
|
674
|
+
await ensureDir(dir);
|
|
675
|
+
await writeFile3(
|
|
676
|
+
join3(dir, "mcp.json"),
|
|
677
|
+
JSON.stringify(MCP_JSON_CURSOR, null, 2) + "\n",
|
|
678
|
+
"utf-8"
|
|
679
|
+
);
|
|
680
|
+
} else {
|
|
681
|
+
await writeFile3(
|
|
682
|
+
projectPath(projectDir, ".mcp.json"),
|
|
683
|
+
JSON.stringify(MCP_JSON_CLAUDE, null, 2) + "\n",
|
|
684
|
+
"utf-8"
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
async function writeLlmkbDir(projectDir, spaceName, spaceDefs, force) {
|
|
689
|
+
const dir = projectPath(projectDir, ".llmkb");
|
|
690
|
+
const written = [];
|
|
691
|
+
if (!force) {
|
|
692
|
+
const stamp = await readVersionStamp(projectDir);
|
|
693
|
+
if (stamp === PACKAGE_VERSION) return written;
|
|
694
|
+
}
|
|
695
|
+
await ensureDir(dir);
|
|
696
|
+
if (spaceDefs && spaceDefs.length > 0) {
|
|
697
|
+
const config = buildSpaceConfig(spaceDefs);
|
|
698
|
+
await writeFile3(
|
|
699
|
+
join3(dir, "spaces.yml"),
|
|
700
|
+
stringify2(config, { lineWidth: 120 }),
|
|
701
|
+
"utf-8"
|
|
702
|
+
);
|
|
703
|
+
} else {
|
|
704
|
+
await writeFile3(
|
|
705
|
+
join3(dir, "spaces.yml"),
|
|
706
|
+
spacesYml(spaceName) + "\n",
|
|
707
|
+
"utf-8"
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
written.push(".llmkb/spaces.yml");
|
|
711
|
+
await writeFile3(join3(dir, "config.yml"), configYml(), "utf-8");
|
|
712
|
+
written.push(".llmkb/config.yml");
|
|
713
|
+
await writeFile3(join3(dir, ".llmkb-version"), PACKAGE_VERSION + "\n", "utf-8");
|
|
714
|
+
written.push(".llmkb/.llmkb-version");
|
|
715
|
+
return written;
|
|
716
|
+
}
|
|
717
|
+
var CURSOR_RULES = [
|
|
718
|
+
{
|
|
719
|
+
name: "llmkb-guide.mdc",
|
|
720
|
+
description: "llmkb overview, architecture, getting started, and how to use llmkb spaces",
|
|
721
|
+
globs: ["*"],
|
|
722
|
+
content: [
|
|
723
|
+
"llmkb is an AI-powered knowledge base. It uses a three-layer model: raw sources (PDFs, code, markdown) -> LLM-generated wiki pages -> knowledge graph.",
|
|
724
|
+
"",
|
|
725
|
+
"- Run `docker compose --profile mvp ps` to verify the MCP server is running",
|
|
726
|
+
"- Use `space_search(space_name, query)` to find relevant wiki pages",
|
|
727
|
+
"- Use `space_read(space_name, path)` to read a specific wiki page",
|
|
728
|
+
"- Use `space_graph(space_name, mode, entity)` to explore entity relationships",
|
|
729
|
+
"- Check `llmkb status` to verify setup is correct"
|
|
730
|
+
].join("\n")
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
name: "llmkb-query.mdc",
|
|
734
|
+
description: "Search and read wiki content via MCP tools \u2014 use space_search and space_read",
|
|
735
|
+
globs: ["*"],
|
|
736
|
+
content: [
|
|
737
|
+
"When asked to find information, always:",
|
|
738
|
+
"",
|
|
739
|
+
"1. Start with `space_search` to locate relevant wiki pages:",
|
|
740
|
+
` space_search(space_name: "my-project", query: "<user's question>")`,
|
|
741
|
+
"2. Read the full content of the best matching page:",
|
|
742
|
+
` space_read(space_name: "my-project", path: "/path/to/page")`,
|
|
743
|
+
"3. For relationship context, use:",
|
|
744
|
+
` space_graph(space_name: "my-project", mode: "neighbours", entity: "<entity name>")`
|
|
745
|
+
].join("\n")
|
|
746
|
+
},
|
|
747
|
+
{
|
|
748
|
+
name: "llmkb-sync.mdc",
|
|
749
|
+
description: "File sync and upload rules \u2014 run llmkb sync after making significant changes",
|
|
750
|
+
globs: ["*"],
|
|
751
|
+
content: [
|
|
752
|
+
"Files modified under this project should be synced to the configured llmkb space.",
|
|
753
|
+
"",
|
|
754
|
+
"- Run `llmkb sync` from the project root after significant changes",
|
|
755
|
+
"- For continuous syncing: `llmkb sync --watch`",
|
|
756
|
+
"- Files are content-addressed (SHA256); unchanged files skip re-processing",
|
|
757
|
+
"- Respects .gitignore and .llmkbignore patterns",
|
|
758
|
+
"- After syncing, use `llmkb status` to check ingestion progress"
|
|
759
|
+
].join("\n")
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
name: "llmkb-exploring.mdc",
|
|
763
|
+
description: "Architecture and graph exploration \u2014 use space_graph for entity relationships",
|
|
764
|
+
globs: ["*"],
|
|
765
|
+
content: [
|
|
766
|
+
"When asked about code architecture or entity relationships:",
|
|
767
|
+
"",
|
|
768
|
+
"1. Start with `space_guide` for a structured overview:",
|
|
769
|
+
` space_guide(space_name: "my-project")`,
|
|
770
|
+
"2. Explore entity neighbourhoods:",
|
|
771
|
+
` space_graph(space_name: "my-project", mode: "neighbours", entity: "<entity>")`,
|
|
772
|
+
"3. Find paths between entities:",
|
|
773
|
+
` space_graph(space_name: "my-project", mode: "path", source: "<source>", target: "<target>")`,
|
|
774
|
+
"4. Follow up with `space_search` for deeper context"
|
|
775
|
+
].join("\n")
|
|
776
|
+
},
|
|
777
|
+
{
|
|
778
|
+
name: "llmkb-admin.mdc",
|
|
779
|
+
description: "Space management, token setup, credentials, configuration, and llmkb CLI commands",
|
|
780
|
+
globs: ["*"],
|
|
781
|
+
content: [
|
|
782
|
+
"When setting up or managing llmkb:",
|
|
783
|
+
"",
|
|
784
|
+
"- `llmkb init` \u2014 scaffold project config files and skills",
|
|
785
|
+
"- `llmkb init --no-with-skills` \u2014 config-only setup (skip skills)",
|
|
786
|
+
"- `llmkb init --platform cursor` \u2014 use Cursor-compatible paths",
|
|
787
|
+
"- `llmkb login --project-space <id>` \u2014 store access token in keychain",
|
|
788
|
+
"- `llmkb whoami` \u2014 show current user identity and access token status",
|
|
789
|
+
"- `llmkb status` \u2014 verify versions, config, and installed skills",
|
|
790
|
+
"- Set LLMKB_ACCESS_TOKEN env var for CI/headless environments"
|
|
791
|
+
].join("\n")
|
|
792
|
+
}
|
|
793
|
+
];
|
|
794
|
+
async function writeCursorRules(projectDir) {
|
|
795
|
+
const rulesDir = projectPath(projectDir, ".cursor", "rules", "llmkb");
|
|
796
|
+
await ensureDir(rulesDir);
|
|
797
|
+
for (const rule of CURSOR_RULES) {
|
|
798
|
+
const frontmatter = [
|
|
799
|
+
"---",
|
|
800
|
+
`name: ${rule.name}`,
|
|
801
|
+
`description: ${rule.description}`,
|
|
802
|
+
`globs: ${JSON.stringify(rule.globs)}`,
|
|
803
|
+
"---",
|
|
804
|
+
""
|
|
805
|
+
].join("\n");
|
|
806
|
+
await writeFile3(
|
|
807
|
+
join3(rulesDir, rule.name),
|
|
808
|
+
frontmatter + rule.content + "\n",
|
|
809
|
+
"utf-8"
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
async function scaffoldConfig(opts) {
|
|
814
|
+
const projectDir = opts.projectDir ?? process.cwd();
|
|
815
|
+
const platform = opts.platform ?? "claude";
|
|
816
|
+
const spaceName = opts.spaceName;
|
|
817
|
+
const spaceDefs = opts.spaceDefs;
|
|
818
|
+
const filesWritten = [];
|
|
819
|
+
await writePluginJson(projectDir);
|
|
820
|
+
filesWritten.push(".claude-plugin/plugin.json");
|
|
821
|
+
await writeMcpJson(projectDir, platform);
|
|
822
|
+
filesWritten.push(platform === "cursor" ? ".cursor/mcp.json" : ".mcp.json");
|
|
823
|
+
if (platform === "cursor") {
|
|
824
|
+
await writeCursorRules(projectDir);
|
|
825
|
+
filesWritten.push(".cursor/rules/llmkb/ (5 rule files)");
|
|
826
|
+
}
|
|
827
|
+
const llmkbFiles = await writeLlmkbDir(
|
|
828
|
+
projectDir,
|
|
829
|
+
spaceName,
|
|
830
|
+
spaceDefs,
|
|
831
|
+
opts.force
|
|
832
|
+
);
|
|
833
|
+
for (const f of llmkbFiles) {
|
|
834
|
+
filesWritten.push(f);
|
|
835
|
+
}
|
|
836
|
+
if (opts.withHooks) {
|
|
837
|
+
const hookFiles = await writeHooks(projectDir, opts.force);
|
|
838
|
+
for (const f of hookFiles) {
|
|
839
|
+
filesWritten.push(f);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return { filesWritten };
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// lib/skills.ts
|
|
846
|
+
init_types();
|
|
847
|
+
import { mkdir as mkdir3, writeFile as writeFile4, readFile as readFile2 } from "fs/promises";
|
|
848
|
+
import { existsSync as existsSync3 } from "fs";
|
|
849
|
+
import { join as join4 } from "path";
|
|
850
|
+
var SKILLS = [
|
|
851
|
+
{
|
|
852
|
+
name: "llmkb-guide",
|
|
853
|
+
description: "Use when the user asks how llmkb works, how to get started, or what llmkb can do. Examples: 'How do I use llmkb?', 'What is llmkb?', 'Get started with llmkb', 'Show me the llmkb architecture'",
|
|
854
|
+
body: [
|
|
855
|
+
"## Overview",
|
|
856
|
+
"",
|
|
857
|
+
"llmkb is an AI-powered knowledge base that transforms documents into an organized, interlinked wiki and knowledge graph. It uses a three-layer model: raw sources (PDFs, code, markdown) \u2192 LLM-generated wiki pages \u2192 structured knowledge graph with entity relationships.",
|
|
858
|
+
"",
|
|
859
|
+
"## Quick Start",
|
|
860
|
+
"",
|
|
861
|
+
"```",
|
|
862
|
+
"llmkb status -- check your current configuration",
|
|
863
|
+
"llmkb whoami -- show active space and credentials",
|
|
864
|
+
"docker compose --profile mvp ps -- verify MCP server is running",
|
|
865
|
+
"```",
|
|
866
|
+
"",
|
|
867
|
+
"## Workflow",
|
|
868
|
+
"",
|
|
869
|
+
"### 1. Verify Setup",
|
|
870
|
+
"",
|
|
871
|
+
"Start by checking that everything is configured:",
|
|
872
|
+
"",
|
|
873
|
+
"- Run `llmkb status` to confirm version stamps match",
|
|
874
|
+
"- Run `llmkb whoami` to verify credentials are stored",
|
|
875
|
+
"- Check `docker compose --profile mvp ps` to confirm the API container is running",
|
|
876
|
+
"",
|
|
877
|
+
"### 2. Search the Wiki",
|
|
878
|
+
"",
|
|
879
|
+
"Use `space_search` with a specific query targeting your space:",
|
|
880
|
+
"",
|
|
881
|
+
"```",
|
|
882
|
+
"-- space_search example --",
|
|
883
|
+
`space_search(space_name: "my-project", query: "authentication flow")`,
|
|
884
|
+
"-- result: ranked list of wiki pages with snippets and confidence scores --",
|
|
885
|
+
"```",
|
|
886
|
+
"",
|
|
887
|
+
"### 3. Read a Page",
|
|
888
|
+
"",
|
|
889
|
+
"Get the full content of a wiki page:",
|
|
890
|
+
"",
|
|
891
|
+
"```",
|
|
892
|
+
"-- space_read example --",
|
|
893
|
+
`space_read(space_name: "my-project", path: "/docs/architecture")`,
|
|
894
|
+
"-- result: full wiki content with frontmatter, body, and metadata --",
|
|
895
|
+
"```",
|
|
896
|
+
"",
|
|
897
|
+
"### 4. Explore the Graph",
|
|
898
|
+
"",
|
|
899
|
+
"When you need to understand how concepts connect:",
|
|
900
|
+
"",
|
|
901
|
+
"```",
|
|
902
|
+
"-- space_graph example --",
|
|
903
|
+
`space_graph(space_name: "my-project", mode: "neighbours", entity: "AuthModule")`,
|
|
904
|
+
"-- result: entity nodes, relationship edges, and community clusters --",
|
|
905
|
+
"```",
|
|
906
|
+
"",
|
|
907
|
+
"### 5. Save New Information",
|
|
908
|
+
"",
|
|
909
|
+
"Write directly to a space when you discover something new:",
|
|
910
|
+
"",
|
|
911
|
+
"```",
|
|
912
|
+
"-- space_write example --",
|
|
913
|
+
`space_write(`,
|
|
914
|
+
` space_name: "my-project",`,
|
|
915
|
+
` path: "/notes/api-pattern",`,
|
|
916
|
+
` content: "# API Pattern\\nThe project uses repository pattern with SQLAlchemy."`,
|
|
917
|
+
`)`,
|
|
918
|
+
"-- result: new wiki page created in the space --",
|
|
919
|
+
"```",
|
|
920
|
+
"",
|
|
921
|
+
"## Troubleshooting",
|
|
922
|
+
"",
|
|
923
|
+
"| Error | Cause | Fix |",
|
|
924
|
+
"|-------|-------|-----|",
|
|
925
|
+
'| "Space not found" | Wrong space name | Run `llmkb use` to see configured spaces |',
|
|
926
|
+
'| "Token required" | Private space needs auth | Run `llmkb login --space <name>` |',
|
|
927
|
+
"| MCP server timeout | Docker not running | `docker compose --profile mvp ps` to check |",
|
|
928
|
+
"| Version mismatch | Skills or config out of date | `llmkb init` to re-install |",
|
|
929
|
+
"",
|
|
930
|
+
"## Self-Check",
|
|
931
|
+
"",
|
|
932
|
+
"- [ ] Active space configured via `llmkb status`",
|
|
933
|
+
"- [ ] Tokens stored in keychain or env vars",
|
|
934
|
+
"- [ ] MCP server reachable: `docker compose --profile mvp ps`",
|
|
935
|
+
"- [ ] Version stamps match package version"
|
|
936
|
+
].join("\n")
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
name: "llmkb-query",
|
|
940
|
+
description: "Use when searching or reading wiki content via MCP tools. Examples: 'Search the wiki for X', 'Read this wiki page', 'Find relevant documentation', 'Look up authentication patterns', 'What does the wiki say about pgvector?'",
|
|
941
|
+
body: [
|
|
942
|
+
"## Overview",
|
|
943
|
+
"",
|
|
944
|
+
"Search and read your llmkb wiki using hybrid full-text + vector search and knowledge graph navigation. Start with `space_search` to find relevant pages, then use `space_read` to get full content.",
|
|
945
|
+
"",
|
|
946
|
+
"## Quick Start",
|
|
947
|
+
"",
|
|
948
|
+
"```",
|
|
949
|
+
"-- search for information --",
|
|
950
|
+
`space_search(space_name: "my-project", query: "pgvector indexing")`,
|
|
951
|
+
"",
|
|
952
|
+
"-- read a specific page --",
|
|
953
|
+
`space_read(space_name: "my-project", path: "/docs/indexing-strategy")`,
|
|
954
|
+
"```",
|
|
955
|
+
"",
|
|
956
|
+
"## Workflow",
|
|
957
|
+
"",
|
|
958
|
+
"### 1. Find Relevant Content",
|
|
959
|
+
"",
|
|
960
|
+
"Start with `space_search` to locate relevant wiki pages:",
|
|
961
|
+
"",
|
|
962
|
+
"```",
|
|
963
|
+
"-- search with a specific query --",
|
|
964
|
+
`space_search(space_name: "my-project", query: "authentication flow")`,
|
|
965
|
+
"",
|
|
966
|
+
"-- search with confidence filter --",
|
|
967
|
+
`space_search(space_name: "my-project", query: "database schema", min_confidence: 0.8)`,
|
|
968
|
+
"```",
|
|
969
|
+
"",
|
|
970
|
+
"Returns ranked results with paths, snippets, confidence scores, and entity types.",
|
|
971
|
+
"",
|
|
972
|
+
"### 2. Read Full Content",
|
|
973
|
+
"",
|
|
974
|
+
"Once you have identified the right page, read it fully:",
|
|
975
|
+
"",
|
|
976
|
+
"```",
|
|
977
|
+
"-- read by path --",
|
|
978
|
+
`space_read(space_name: "my-project", path: "/docs/architecture")`,
|
|
979
|
+
"",
|
|
980
|
+
"-- result includes frontmatter (type, category, source_pages) and full body content --",
|
|
981
|
+
"```",
|
|
982
|
+
"",
|
|
983
|
+
"### 3. Get a Space Overview",
|
|
984
|
+
"",
|
|
985
|
+
"For a structured summary of a space's content:",
|
|
986
|
+
"",
|
|
987
|
+
"```",
|
|
988
|
+
"-- overview of all entities and pages --",
|
|
989
|
+
`space_guide(space_name: "my-project")`,
|
|
990
|
+
"-- result: summary of entity types, page categories, and top-level relationships --",
|
|
991
|
+
"```",
|
|
992
|
+
"",
|
|
993
|
+
"### 4. Explore Entity Relationships",
|
|
994
|
+
"",
|
|
995
|
+
"When search results surface an entity, dig into its connections:",
|
|
996
|
+
"",
|
|
997
|
+
"```",
|
|
998
|
+
"-- find neighbours of a specific entity --",
|
|
999
|
+
`space_graph(space_name: "my-project", mode: "neighbours", entity: "AuthModule")`,
|
|
1000
|
+
"-- result: all entities directly connected to AuthModule with relationship types --",
|
|
1001
|
+
"```",
|
|
1002
|
+
"",
|
|
1003
|
+
"## Troubleshooting",
|
|
1004
|
+
"",
|
|
1005
|
+
"| Error | Cause | Fix |",
|
|
1006
|
+
"|-------|-------|-----|",
|
|
1007
|
+
"| Empty results | Query too narrow | Broaden terms or check space has content with `space_guide` |",
|
|
1008
|
+
'| "Space not found" | Wrong space name | `llmkb status` to list configured spaces |',
|
|
1009
|
+
"| Path not found | Page path incorrect | Search first with `space_search` to find the exact path |",
|
|
1010
|
+
"| Token error | Expired credentials | `llmkb login --space <name>` |",
|
|
1011
|
+
"",
|
|
1012
|
+
"## Self-Check",
|
|
1013
|
+
"",
|
|
1014
|
+
"- [ ] Used `space_search` first to find relevant pages",
|
|
1015
|
+
"- [ ] Used `space_read` to get full context when snippet is insufficient",
|
|
1016
|
+
"- [ ] Verified the space name matches a configured space",
|
|
1017
|
+
"- [ ] Used `space_graph` for deeper relationship exploration when needed"
|
|
1018
|
+
].join("\n")
|
|
1019
|
+
},
|
|
1020
|
+
{
|
|
1021
|
+
name: "llmkb-sync",
|
|
1022
|
+
description: "Use when syncing local files to an llmkb space for ingestion. Examples: 'Upload these docs', 'Sync this folder to my wiki', 'Ingest this README', 'Watch this directory for changes', 'Upload my codebase'",
|
|
1023
|
+
body: [
|
|
1024
|
+
"## Overview",
|
|
1025
|
+
"",
|
|
1026
|
+
"Sync local files and codebases to your llmkb spaces for processing and wiki generation. Files are content-addressed (SHA256) so unchanged files skip re-processing. Supports PDF, Markdown, code files, images, and audio.",
|
|
1027
|
+
"",
|
|
1028
|
+
"## Quick Start",
|
|
1029
|
+
"",
|
|
1030
|
+
"```",
|
|
1031
|
+
"llmkb sync -- sync current directory to active space",
|
|
1032
|
+
"llmkb sync --space my-project -- sync to a specific space",
|
|
1033
|
+
"llmkb sync --watch -- watch directory for changes",
|
|
1034
|
+
"```",
|
|
1035
|
+
"",
|
|
1036
|
+
"## Workflow",
|
|
1037
|
+
"",
|
|
1038
|
+
"### 1. Sync Files to a Space",
|
|
1039
|
+
"",
|
|
1040
|
+
"Navigate to the directory containing files you want to ingest and run:",
|
|
1041
|
+
"",
|
|
1042
|
+
"```",
|
|
1043
|
+
"cd /path/to/your/project",
|
|
1044
|
+
"llmkb sync --space my-project",
|
|
1045
|
+
"```",
|
|
1046
|
+
"",
|
|
1047
|
+
"The command will:",
|
|
1048
|
+
"",
|
|
1049
|
+
"1. Walk the directory recursively",
|
|
1050
|
+
"2. Compute SHA256 hashes for each file",
|
|
1051
|
+
"3. Skip unchanged files (cached in `.llmkb/sync-state.json`)",
|
|
1052
|
+
"4. Upload new/changed files via TUS protocol",
|
|
1053
|
+
"5. Report progress per file",
|
|
1054
|
+
"",
|
|
1055
|
+
"### 2. Watch for Changes",
|
|
1056
|
+
"",
|
|
1057
|
+
"Keep a directory synced automatically:",
|
|
1058
|
+
"",
|
|
1059
|
+
"```",
|
|
1060
|
+
"llmkb sync --watch --space my-project",
|
|
1061
|
+
"```",
|
|
1062
|
+
"",
|
|
1063
|
+
"Watcher behaviour:",
|
|
1064
|
+
"",
|
|
1065
|
+
"- Respects `.gitignore` and `.llmkbignore` patterns",
|
|
1066
|
+
"- Debounces rapid file changes",
|
|
1067
|
+
"- Detects file renames by content hash (not path)",
|
|
1068
|
+
"- Press Ctrl+C to stop watching",
|
|
1069
|
+
"",
|
|
1070
|
+
"### 3. Check Sync Status",
|
|
1071
|
+
"",
|
|
1072
|
+
"After syncing, verify ingestion progress:",
|
|
1073
|
+
"",
|
|
1074
|
+
"```",
|
|
1075
|
+
"llmkb status --space my-project",
|
|
1076
|
+
"```",
|
|
1077
|
+
"",
|
|
1078
|
+
"### 4. Query Synced Content",
|
|
1079
|
+
"",
|
|
1080
|
+
"Once processing completes, search the newly ingested content:",
|
|
1081
|
+
"",
|
|
1082
|
+
"```",
|
|
1083
|
+
`space_search(space_name: "my-project", query: "topics from the synced files")`,
|
|
1084
|
+
"```",
|
|
1085
|
+
"",
|
|
1086
|
+
"## Troubleshooting",
|
|
1087
|
+
"",
|
|
1088
|
+
"| Error | Cause | Fix |",
|
|
1089
|
+
"|-------|-------|-----|",
|
|
1090
|
+
"| File skipped | Matches .gitignore or .llmkbignore | Check ignore files |",
|
|
1091
|
+
"| Upload failed | TUS endpoint unreachable | Verify Docker is running |",
|
|
1092
|
+
"| Hash mismatch | File changed during upload | Re-run `llmkb sync` |",
|
|
1093
|
+
'| "No space configured" | No active space | `llmkb use <space-name>` |',
|
|
1094
|
+
"| Sync slow | Large directory | Use `--verbose` to see progress; files over 5MB use chunked TUS uploads |",
|
|
1095
|
+
"",
|
|
1096
|
+
"## Self-Check",
|
|
1097
|
+
"",
|
|
1098
|
+
"- [ ] Files upload successfully (check terminal output for each file)",
|
|
1099
|
+
"- [ ] Ingestion job progresses through phases",
|
|
1100
|
+
"- [ ] Wiki pages generated after processing completes",
|
|
1101
|
+
"- [ ] Unchanged files are skipped (cached by SHA256)"
|
|
1102
|
+
].join("\n")
|
|
1103
|
+
},
|
|
1104
|
+
{
|
|
1105
|
+
name: "llmkb-exploring",
|
|
1106
|
+
description: "Use when exploring architecture, codebase structure, and entity relationships. Examples: 'How does X work?', 'Show me the codebase architecture', 'What connections exist in this space?', 'Find relationships between these concepts', 'Map the auth module dependencies'",
|
|
1107
|
+
body: [
|
|
1108
|
+
"## Overview",
|
|
1109
|
+
"",
|
|
1110
|
+
"Explore entity relationships, knowledge graph structure, and how different pieces of information connect in your llmkb spaces. Use graph queries to discover neighbours, find paths between entities, and get structured space overviews.",
|
|
1111
|
+
"",
|
|
1112
|
+
"## Quick Start",
|
|
1113
|
+
"",
|
|
1114
|
+
"```",
|
|
1115
|
+
"-- explore neighbours of an entity --",
|
|
1116
|
+
`space_graph(space_name: "my-project", mode: "neighbours", entity: "AuthModule")`,
|
|
1117
|
+
"",
|
|
1118
|
+
"-- find path between two entities --",
|
|
1119
|
+
`space_graph(space_name: "my-project", mode: "path", source: "ServiceA", target: "ServiceB")`,
|
|
1120
|
+
"```",
|
|
1121
|
+
"",
|
|
1122
|
+
"## Workflow",
|
|
1123
|
+
"",
|
|
1124
|
+
"### 1. Get a Space Overview",
|
|
1125
|
+
"",
|
|
1126
|
+
"Start with `space_guide` for a high-level summary:",
|
|
1127
|
+
"",
|
|
1128
|
+
"```",
|
|
1129
|
+
`space_guide(space_name: "my-project")`,
|
|
1130
|
+
"-- result: entity count, relationship count, page categories, top-level clusters --",
|
|
1131
|
+
"```",
|
|
1132
|
+
"",
|
|
1133
|
+
"### 2. Explore Neighbourhoods",
|
|
1134
|
+
"",
|
|
1135
|
+
"Drill into a specific entity to see its direct connections:",
|
|
1136
|
+
"",
|
|
1137
|
+
"```",
|
|
1138
|
+
`space_graph(space_name: "my-project", mode: "neighbours", entity: "AuthModule")`,
|
|
1139
|
+
"-- result: all entities directly connected to AuthModule with relationship types and confidence scores --",
|
|
1140
|
+
"```",
|
|
1141
|
+
"",
|
|
1142
|
+
"### 3. Find Paths",
|
|
1143
|
+
"",
|
|
1144
|
+
"Discover how two entities connect through intermediate nodes:",
|
|
1145
|
+
"",
|
|
1146
|
+
"```",
|
|
1147
|
+
`space_graph(space_name: "my-project", mode: "path", source: "Database", target: "UserService")`,
|
|
1148
|
+
"-- result: shortest path showing each hop with relationship type --",
|
|
1149
|
+
"```",
|
|
1150
|
+
"",
|
|
1151
|
+
"### 4. Search Within the Exploration",
|
|
1152
|
+
"",
|
|
1153
|
+
"When you find interesting entities, search for more details:",
|
|
1154
|
+
"",
|
|
1155
|
+
"```",
|
|
1156
|
+
`space_search(space_name: "my-project", query: "user authentication")`,
|
|
1157
|
+
"```",
|
|
1158
|
+
"",
|
|
1159
|
+
"## Troubleshooting",
|
|
1160
|
+
"",
|
|
1161
|
+
"| Error | Cause | Fix |",
|
|
1162
|
+
"|-------|-------|-----|",
|
|
1163
|
+
"| Empty graph | Space has no processed content | Run `llmkb sync` to upload files first |",
|
|
1164
|
+
'| "Entity not found" | Wrong entity name | Use `space_search` to find the exact label |',
|
|
1165
|
+
"| No path found | Entities not connected in the graph | Try broader search to find bridging entities |",
|
|
1166
|
+
"| Graph too large | Too many neighbours | Narrow the entity or use `space_search` instead |",
|
|
1167
|
+
"",
|
|
1168
|
+
"## Self-Check",
|
|
1169
|
+
"",
|
|
1170
|
+
"- [ ] Started with `space_guide` for orientation",
|
|
1171
|
+
"- [ ] Used `space_graph` with `neighbours` mode for direct connections",
|
|
1172
|
+
"- [ ] Used `space_graph` with `path` mode for indirect connections",
|
|
1173
|
+
"- [ ] Followed up with `space_search` for deeper context"
|
|
1174
|
+
].join("\n")
|
|
1175
|
+
},
|
|
1176
|
+
{
|
|
1177
|
+
name: "llmkb-admin",
|
|
1178
|
+
description: "Use when managing spaces, tokens, configuration, or credentials. Examples: 'Show my space members', 'Configure a new space', 'Manage API tokens', 'Switch active space', 'Check my credentials', 'Set up a new project with llmkb'",
|
|
1179
|
+
body: [
|
|
1180
|
+
"## Overview",
|
|
1181
|
+
"",
|
|
1182
|
+
"Manage llmkb spaces, API tokens, configuration settings, and credentials. The CLI provides commands for switching spaces, authenticating, and verifying setup.",
|
|
1183
|
+
"",
|
|
1184
|
+
"## Quick Start",
|
|
1185
|
+
"",
|
|
1186
|
+
"```",
|
|
1187
|
+
"llmkb status -- check version stamps and active config",
|
|
1188
|
+
"llmkb use my-project -- switch active space",
|
|
1189
|
+
"llmkb login --space admin -- store credentials for a space",
|
|
1190
|
+
"```",
|
|
1191
|
+
"",
|
|
1192
|
+
"## Workflow",
|
|
1193
|
+
"",
|
|
1194
|
+
"### 1. Initialise a Project",
|
|
1195
|
+
"",
|
|
1196
|
+
"Set up a new project with llmkb:",
|
|
1197
|
+
"",
|
|
1198
|
+
"```",
|
|
1199
|
+
"cd /path/to/project",
|
|
1200
|
+
"llmkb init --space my-project",
|
|
1201
|
+
"```",
|
|
1202
|
+
"",
|
|
1203
|
+
"This creates:",
|
|
1204
|
+
"",
|
|
1205
|
+
"- `.claude-plugin/plugin.json` \u2014 plugin metadata for discovery",
|
|
1206
|
+
"- `.mcp.json` \u2014 MCP server registration with Docker stdio transport",
|
|
1207
|
+
"- `.llmkb/spaces.yml` \u2014 space configuration",
|
|
1208
|
+
"- `.llmkb/config.yml` \u2014 plugin behaviour settings",
|
|
1209
|
+
"- `.claude/skills/llmkb/` \u2014 5 Claude Code skills",
|
|
1210
|
+
"",
|
|
1211
|
+
"For Cursor: `llmkb init --platform cursor` writes equivalent `.cursor/` files.",
|
|
1212
|
+
"",
|
|
1213
|
+
"To skip skill installation: `llmkb init --no-with-skills` (for config-only setup).",
|
|
1214
|
+
"",
|
|
1215
|
+
"### 2. Authenticate",
|
|
1216
|
+
"",
|
|
1217
|
+
"Store credentials for a space after init:",
|
|
1218
|
+
"",
|
|
1219
|
+
"```",
|
|
1220
|
+
"llmkb login --space my-project",
|
|
1221
|
+
"```",
|
|
1222
|
+
"",
|
|
1223
|
+
"This stores the space token in the OS keychain. For CI/headless environments, set the `LLMKB_TOKEN` environment variable instead.",
|
|
1224
|
+
"",
|
|
1225
|
+
"### 3. Switch Spaces",
|
|
1226
|
+
"",
|
|
1227
|
+
"When working with multiple spaces:",
|
|
1228
|
+
"",
|
|
1229
|
+
"```",
|
|
1230
|
+
"llmkb use client-project",
|
|
1231
|
+
"llmkb login --space client-project",
|
|
1232
|
+
"llmkb status --space client-project",
|
|
1233
|
+
"```",
|
|
1234
|
+
"",
|
|
1235
|
+
"### 4. Verify Setup",
|
|
1236
|
+
"",
|
|
1237
|
+
"Check that everything is configured correctly:",
|
|
1238
|
+
"",
|
|
1239
|
+
"```",
|
|
1240
|
+
"llmkb status",
|
|
1241
|
+
"llmkb whoami",
|
|
1242
|
+
"```",
|
|
1243
|
+
"",
|
|
1244
|
+
"The `status` command warns on:",
|
|
1245
|
+
"",
|
|
1246
|
+
"- Config version mismatch: `llmkb init` to update",
|
|
1247
|
+
"- Skill version mismatch: `llmkb init` to re-install skills",
|
|
1248
|
+
"- Missing or expired tokens",
|
|
1249
|
+
"",
|
|
1250
|
+
"### 5. Token Management",
|
|
1251
|
+
"",
|
|
1252
|
+
"- Space tokens grant read access to a specific space",
|
|
1253
|
+
"- Admin tokens grant write/admin access",
|
|
1254
|
+
"- Tokens are stored in the OS keychain (requires `keychain` package)",
|
|
1255
|
+
"- Set `LLMKB_TOKEN` / `LLMKB_ADMIN_TOKEN` env vars for CI",
|
|
1256
|
+
"- Token groups allow sharing a single token across multiple spaces",
|
|
1257
|
+
"",
|
|
1258
|
+
"## Troubleshooting",
|
|
1259
|
+
"",
|
|
1260
|
+
"| Error | Cause | Fix |",
|
|
1261
|
+
"|-------|-------|-----|",
|
|
1262
|
+
'| "Version mismatch" | Config or skills out of date | `llmkb init` to update |',
|
|
1263
|
+
'| "Not found: .llmkb/" | Project not initialised | `llmkb init` first |',
|
|
1264
|
+
"| Keychain access denied | Missing keychain permission | Fall back to `LLMKB_TOKEN` env var |",
|
|
1265
|
+
"| Token expired | Credentials rotated | `llmkb login --space <name>` |",
|
|
1266
|
+
"| Multiple spaces confusing | Wrong active space | `llmkb use <name>` to switch |",
|
|
1267
|
+
"",
|
|
1268
|
+
"## Self-Check",
|
|
1269
|
+
"",
|
|
1270
|
+
"- [ ] Project initialised with `llmkb init`",
|
|
1271
|
+
"- [ ] Tokens stored and accessible (keychain or env vars)",
|
|
1272
|
+
"- [ ] Correct space is active via `llmkb use`",
|
|
1273
|
+
"- [ ] Version stamps match package version (`llmkb status`)",
|
|
1274
|
+
"- [ ] MCP server is running: `docker compose --profile mvp ps`"
|
|
1275
|
+
].join("\n")
|
|
1276
|
+
}
|
|
1277
|
+
];
|
|
1278
|
+
async function installSkills(platform = "claude", projectDir) {
|
|
1279
|
+
if (platform === "cursor") {
|
|
1280
|
+
return 0;
|
|
1281
|
+
}
|
|
1282
|
+
const projectRoot = projectDir ?? process.cwd();
|
|
1283
|
+
const targetDir = join4(projectRoot, ".claude", "skills", "llmkb");
|
|
1284
|
+
for (const skill of SKILLS) {
|
|
1285
|
+
const skillDir = join4(targetDir, skill.name);
|
|
1286
|
+
await mkdir3(skillDir, { recursive: true });
|
|
1287
|
+
const trigger = skill.name.replace("llmkb-", "/llmkb-");
|
|
1288
|
+
const frontmatter = [
|
|
1289
|
+
"---",
|
|
1290
|
+
`name: ${skill.name}`,
|
|
1291
|
+
`description: ${skill.description}`,
|
|
1292
|
+
`trigger: ${trigger}`,
|
|
1293
|
+
"---",
|
|
1294
|
+
""
|
|
1295
|
+
].join("\n");
|
|
1296
|
+
await writeFile4(
|
|
1297
|
+
join4(skillDir, "SKILL.md"),
|
|
1298
|
+
frontmatter + skill.body + "\n",
|
|
1299
|
+
"utf-8"
|
|
1300
|
+
);
|
|
1301
|
+
await writeFile4(
|
|
1302
|
+
join4(skillDir, ".llmkb-version"),
|
|
1303
|
+
PACKAGE_VERSION + "\n",
|
|
1304
|
+
"utf-8"
|
|
1305
|
+
);
|
|
1306
|
+
}
|
|
1307
|
+
return SKILLS.length;
|
|
1308
|
+
}
|
|
1309
|
+
async function checkSkillVersions(skillsDir) {
|
|
1310
|
+
const mismatches = [];
|
|
1311
|
+
if (!existsSync3(skillsDir)) return mismatches;
|
|
1312
|
+
const { readdir: readdir2 } = await import("fs/promises");
|
|
1313
|
+
const entries = await readdir2(skillsDir, { withFileTypes: true });
|
|
1314
|
+
for (const entry of entries) {
|
|
1315
|
+
if (!entry.isDirectory()) continue;
|
|
1316
|
+
const versionPath = join4(skillsDir, entry.name, ".llmkb-version");
|
|
1317
|
+
try {
|
|
1318
|
+
const content = await readFile2(versionPath, "utf-8");
|
|
1319
|
+
const installedVersion = content.split("\n")[0]?.trim();
|
|
1320
|
+
if (installedVersion && installedVersion !== PACKAGE_VERSION) {
|
|
1321
|
+
mismatches.push({
|
|
1322
|
+
name: entry.name,
|
|
1323
|
+
installed: installedVersion,
|
|
1324
|
+
current: PACKAGE_VERSION
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
} catch {
|
|
1328
|
+
mismatches.push({
|
|
1329
|
+
name: entry.name,
|
|
1330
|
+
installed: "(none)",
|
|
1331
|
+
current: PACKAGE_VERSION
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
return mismatches;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// src/commands/init.ts
|
|
1339
|
+
function rl() {
|
|
1340
|
+
return readline.createInterface({ input: stdin, output: stdout });
|
|
1341
|
+
}
|
|
1342
|
+
async function promptSpace(rl2, index) {
|
|
1343
|
+
console.log(`
|
|
1344
|
+
--- Space #${index + 1} ---`);
|
|
1345
|
+
const id = await rl2.question(" Space UUID (e.g. b6087faa-...): ");
|
|
1346
|
+
if (!id.trim()) return null;
|
|
1347
|
+
const name = await rl2.question(` Name (optional): `);
|
|
1348
|
+
return {
|
|
1349
|
+
id: id.trim(),
|
|
1350
|
+
...name.trim() ? { name: name.trim() } : {}
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
async function interactiveSetup() {
|
|
1354
|
+
const rli = rl();
|
|
1355
|
+
const spaceDefs = [];
|
|
1356
|
+
try {
|
|
1357
|
+
console.log("llmkb init \u2014 interactive setup");
|
|
1358
|
+
console.log("Configure one or more spaces for this project.");
|
|
1359
|
+
console.log("Press Enter with an empty space UUID to finish.\n");
|
|
1360
|
+
let index = 0;
|
|
1361
|
+
while (true) {
|
|
1362
|
+
const def = await promptSpace(rli, index);
|
|
1363
|
+
if (!def) break;
|
|
1364
|
+
spaceDefs.push(def);
|
|
1365
|
+
index++;
|
|
1366
|
+
}
|
|
1367
|
+
if (spaceDefs.length === 0) {
|
|
1368
|
+
console.log("\nNo spaces configured. A template config will be created.");
|
|
1369
|
+
} else {
|
|
1370
|
+
console.log(`
|
|
1371
|
+
Configured ${spaceDefs.length} space(s).`);
|
|
1372
|
+
}
|
|
1373
|
+
} finally {
|
|
1374
|
+
rli.close();
|
|
1375
|
+
}
|
|
1376
|
+
return { spaceDefs };
|
|
1377
|
+
}
|
|
1378
|
+
var initCommand = new Command2("init").description("Scaffold llmkb config files and skills").argument("[directory]", "Target directory (defaults to cwd)").option(
|
|
1379
|
+
"-p, --platform <type>",
|
|
1380
|
+
"Target platform: claude or cursor",
|
|
1381
|
+
"claude"
|
|
1382
|
+
).option("--with-skills", "Install Claude Code skills").option("--without-skills", "Skip skill installation").option("--with-hooks", "Install Claude Code hooks (default: false)", false).option(
|
|
1383
|
+
"--non-interactive",
|
|
1384
|
+
"Skip confirmation prompts (for CI/headless environments)"
|
|
1385
|
+
).action(
|
|
1386
|
+
async (directory, opts) => {
|
|
1387
|
+
const targetDir = directory ? resolve4(process.cwd(), directory) : process.cwd();
|
|
1388
|
+
const platform = opts.platform === "cursor" ? "cursor" : "claude";
|
|
1389
|
+
const interactive = !opts.nonInteractive;
|
|
1390
|
+
const installSkillsFlag = opts.withSkills ?? !opts.withoutSkills;
|
|
1391
|
+
let spaceDefs;
|
|
1392
|
+
if (interactive) {
|
|
1393
|
+
const result = await interactiveSetup();
|
|
1394
|
+
spaceDefs = result.spaceDefs;
|
|
1395
|
+
}
|
|
1396
|
+
if (interactive) {
|
|
1397
|
+
console.log(
|
|
1398
|
+
"\nThis will create the following files in the current directory:"
|
|
1399
|
+
);
|
|
1400
|
+
console.log(" - .claude-plugin/plugin.json");
|
|
1401
|
+
console.log(
|
|
1402
|
+
` - ${platform === "cursor" ? ".cursor/mcp.json" : ".mcp.json"}`
|
|
1403
|
+
);
|
|
1404
|
+
console.log(" - .llmkb/spaces.yml");
|
|
1405
|
+
console.log(" - .llmkb/config.yml");
|
|
1406
|
+
console.log(" - .llmkb/.llmkb-version");
|
|
1407
|
+
console.log(" - .claude/skills/llmkb/ (5 skills)");
|
|
1408
|
+
if (opts.withHooks) {
|
|
1409
|
+
console.log(" - .claude/hooks/llmkb/ (hooks.json + scripts)\n");
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
try {
|
|
1413
|
+
const { filesWritten } = await scaffoldConfig({
|
|
1414
|
+
platform,
|
|
1415
|
+
spaceDefs,
|
|
1416
|
+
projectDir: targetDir,
|
|
1417
|
+
withHooks: opts.withHooks
|
|
1418
|
+
});
|
|
1419
|
+
console.log(`Created ${filesWritten.length} config file(s):`);
|
|
1420
|
+
for (const f of filesWritten) {
|
|
1421
|
+
console.log(` \u2714 ${f}`);
|
|
1422
|
+
}
|
|
1423
|
+
if (installSkillsFlag) {
|
|
1424
|
+
const skillCount = await installSkills(platform, targetDir);
|
|
1425
|
+
console.log(
|
|
1426
|
+
` \u2714 ${skillCount} skills installed to .claude/skills/llmkb/`
|
|
1427
|
+
);
|
|
1428
|
+
}
|
|
1429
|
+
const loginSpace = spaceDefs?.[0]?.id;
|
|
1430
|
+
if (loginSpace) {
|
|
1431
|
+
console.log(
|
|
1432
|
+
`
|
|
1433
|
+
Run \`llmkb login --project-space ${loginSpace}\` to authenticate.`
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
console.log("\nPost-install note:");
|
|
1437
|
+
console.log(
|
|
1438
|
+
" Docker --profile mvp : Used for MCP tools only (what this config creates)."
|
|
1439
|
+
);
|
|
1440
|
+
console.log(
|
|
1441
|
+
" Docker --profile poc : Use for full stack with Nuxt frontend on port 3000."
|
|
1442
|
+
);
|
|
1443
|
+
console.log(
|
|
1444
|
+
" See infra/mcp/claude_desktop_config.example.json for the alternative."
|
|
1445
|
+
);
|
|
1446
|
+
console.log("\nDone. Run `llmkb status` to verify the installation.");
|
|
1447
|
+
} catch (err) {
|
|
1448
|
+
console.error("Error:", err.message);
|
|
1449
|
+
process.exit(1);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
);
|
|
1453
|
+
|
|
1454
|
+
// src/commands/login.ts
|
|
1455
|
+
import { Command as Command3 } from "commander";
|
|
1456
|
+
|
|
1457
|
+
// lib/config.ts
|
|
1458
|
+
import { env } from "process";
|
|
1459
|
+
var DEFAULT_ENDPOINT = "http://localhost:8000";
|
|
1460
|
+
function getConfig() {
|
|
1461
|
+
return {
|
|
1462
|
+
endpoint: env["LLMKB_ENDPOINT"] ?? DEFAULT_ENDPOINT
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// lib/credentials.ts
|
|
1467
|
+
import { env as env2 } from "process";
|
|
1468
|
+
var kcGetPassword = null;
|
|
1469
|
+
var kcSetPassword = null;
|
|
1470
|
+
var kcDeletePassword = null;
|
|
1471
|
+
var keychainAvailable = false;
|
|
1472
|
+
async function ensureKeychain() {
|
|
1473
|
+
if (keychainAvailable) return true;
|
|
1474
|
+
try {
|
|
1475
|
+
const mod = await import("keychain");
|
|
1476
|
+
const kc = mod.default ?? mod;
|
|
1477
|
+
if (typeof kc.setPassword === "function") {
|
|
1478
|
+
kcGetPassword = kc.getPassword.bind(kc);
|
|
1479
|
+
kcSetPassword = kc.setPassword.bind(kc);
|
|
1480
|
+
kcDeletePassword = kc.deletePassword.bind(kc);
|
|
1481
|
+
keychainAvailable = true;
|
|
1482
|
+
return true;
|
|
1483
|
+
}
|
|
1484
|
+
return false;
|
|
1485
|
+
} catch {
|
|
1486
|
+
return false;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
async function getToken() {
|
|
1490
|
+
const envVal = env2["LLMKB_ACCESS_TOKEN"];
|
|
1491
|
+
if (envVal) return envVal;
|
|
1492
|
+
const kcToken = await keychainGet();
|
|
1493
|
+
if (kcToken) return kcToken;
|
|
1494
|
+
return null;
|
|
1495
|
+
}
|
|
1496
|
+
async function keychainGet() {
|
|
1497
|
+
const available = await ensureKeychain();
|
|
1498
|
+
if (!available) return null;
|
|
1499
|
+
return new Promise((resolve8) => {
|
|
1500
|
+
try {
|
|
1501
|
+
kcGetPassword(
|
|
1502
|
+
{ service: "llmkb", account: "user/access_token" },
|
|
1503
|
+
(err, password) => {
|
|
1504
|
+
if (err) resolve8(null);
|
|
1505
|
+
else resolve8(password);
|
|
1506
|
+
}
|
|
1507
|
+
);
|
|
1508
|
+
} catch {
|
|
1509
|
+
resolve8(null);
|
|
1510
|
+
}
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
async function setToken(token) {
|
|
1514
|
+
const available = await ensureKeychain();
|
|
1515
|
+
if (!available) {
|
|
1516
|
+
throw new Error(
|
|
1517
|
+
"Keychain is not available. Set LLMKB_ACCESS_TOKEN env var instead."
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1520
|
+
return new Promise((resolve8, reject) => {
|
|
1521
|
+
kcSetPassword(
|
|
1522
|
+
{
|
|
1523
|
+
service: "llmkb",
|
|
1524
|
+
account: "user/access_token",
|
|
1525
|
+
password: token
|
|
1526
|
+
},
|
|
1527
|
+
(err) => {
|
|
1528
|
+
if (err) reject(err);
|
|
1529
|
+
else resolve8();
|
|
1530
|
+
}
|
|
1531
|
+
);
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
async function deleteToken() {
|
|
1535
|
+
const available = await ensureKeychain();
|
|
1536
|
+
if (!available) return;
|
|
1537
|
+
return new Promise((resolve8, reject) => {
|
|
1538
|
+
kcDeletePassword(
|
|
1539
|
+
{ service: "llmkb", account: "user/access_token" },
|
|
1540
|
+
(err) => {
|
|
1541
|
+
if (err) reject(err);
|
|
1542
|
+
else resolve8();
|
|
1543
|
+
}
|
|
1544
|
+
);
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// src/commands/login.ts
|
|
1549
|
+
init_parser();
|
|
1550
|
+
|
|
1551
|
+
// lib/sync-spaces-config.ts
|
|
1552
|
+
init_parser();
|
|
1553
|
+
import { readFile as readFile3, writeFile as writeFile5 } from "fs/promises";
|
|
1554
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1555
|
+
import { join as join5 } from "path";
|
|
1556
|
+
async function syncRelatedSpaces(projectDir, endpoint, token) {
|
|
1557
|
+
const config = await readSpaceConfig(projectDir);
|
|
1558
|
+
if (!config?.project_space?.length) {
|
|
1559
|
+
return { added: 0, alreadyPresent: 0, removed: 0 };
|
|
1560
|
+
}
|
|
1561
|
+
const projectSpaceId = config.project_space[0].id;
|
|
1562
|
+
const relations = await fetchRelatedSpaces(endpoint, token, projectSpaceId);
|
|
1563
|
+
const backendIds = new Set(relations.map((r) => r.id));
|
|
1564
|
+
const syncedIdsPath = join5(projectDir, ".llmkb", "sync-relations.json");
|
|
1565
|
+
const prevSyncedIds = await readSyncedIds(syncedIdsPath);
|
|
1566
|
+
let added = 0;
|
|
1567
|
+
let alreadyPresent = 0;
|
|
1568
|
+
let removed = 0;
|
|
1569
|
+
if (config.spaces) {
|
|
1570
|
+
for (const s of config.spaces) {
|
|
1571
|
+
if (s.id === projectSpaceId) {
|
|
1572
|
+
await removeSpaceEntry(projectDir, s.id);
|
|
1573
|
+
removed++;
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
for (const rel of relations) {
|
|
1578
|
+
if (rel.id === projectSpaceId) continue;
|
|
1579
|
+
const existingIds = /* @__PURE__ */ new Set([
|
|
1580
|
+
...(config.spaces ?? []).map((s) => s.id),
|
|
1581
|
+
...(config.project_space ?? []).map((s) => s.id)
|
|
1582
|
+
]);
|
|
1583
|
+
if (existingIds.has(rel.id)) {
|
|
1584
|
+
alreadyPresent++;
|
|
1585
|
+
} else {
|
|
1586
|
+
await addSpaceEntry(projectDir, rel.id, rel.name);
|
|
1587
|
+
added++;
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
if (prevSyncedIds.size > 0 && config.spaces) {
|
|
1591
|
+
for (const s of config.spaces) {
|
|
1592
|
+
if (prevSyncedIds.has(s.id) && !backendIds.has(s.id)) {
|
|
1593
|
+
await removeSpaceEntry(projectDir, s.id);
|
|
1594
|
+
removed++;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
await writeSyncedIds(syncedIdsPath, backendIds);
|
|
1599
|
+
return { added, alreadyPresent, removed };
|
|
1600
|
+
}
|
|
1601
|
+
async function readSyncedIds(filePath) {
|
|
1602
|
+
try {
|
|
1603
|
+
if (existsSync4(filePath)) {
|
|
1604
|
+
const raw = await readFile3(filePath, "utf-8");
|
|
1605
|
+
const parsed = JSON.parse(raw);
|
|
1606
|
+
return new Set(parsed);
|
|
1607
|
+
}
|
|
1608
|
+
} catch {
|
|
1609
|
+
}
|
|
1610
|
+
return /* @__PURE__ */ new Set();
|
|
1611
|
+
}
|
|
1612
|
+
async function writeSyncedIds(filePath, ids) {
|
|
1613
|
+
await writeFile5(filePath, JSON.stringify([...ids], null, 2) + "\n", "utf-8");
|
|
1614
|
+
}
|
|
1615
|
+
async function fetchRelatedSpaces(endpoint, token, spaceId) {
|
|
1616
|
+
try {
|
|
1617
|
+
const url = `${endpoint.replace(/\/+$/, "")}/api/spaces/${encodeURIComponent(spaceId)}/relations`;
|
|
1618
|
+
const res = await fetch(url, {
|
|
1619
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1620
|
+
});
|
|
1621
|
+
if (!res.ok) return [];
|
|
1622
|
+
const body = await res.json();
|
|
1623
|
+
if (!body.data) return [];
|
|
1624
|
+
return body.data.filter((d) => d.attributes?.relatedSpaceId).map((d) => ({
|
|
1625
|
+
id: d.attributes.relatedSpaceId,
|
|
1626
|
+
name: d.attributes?.relatedSpaceName ?? void 0
|
|
1627
|
+
}));
|
|
1628
|
+
} catch {
|
|
1629
|
+
return [];
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
// src/commands/login.ts
|
|
1634
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
1635
|
+
|
|
1636
|
+
// lib/color.ts
|
|
1637
|
+
import pc from "picocolors";
|
|
1638
|
+
var success = pc.green;
|
|
1639
|
+
var error = pc.red;
|
|
1640
|
+
var warning = pc.yellow;
|
|
1641
|
+
var info = pc.cyan;
|
|
1642
|
+
var highlight = (s) => pc.bold(pc.white(s));
|
|
1643
|
+
var muted = pc.dim;
|
|
1644
|
+
var ICON_COLORS = {
|
|
1645
|
+
"\u2714": success,
|
|
1646
|
+
"\u2713": success,
|
|
1647
|
+
"\u2716": error,
|
|
1648
|
+
"\u2717": error,
|
|
1649
|
+
"\u26A0": warning,
|
|
1650
|
+
"\u2139": info,
|
|
1651
|
+
"\u231A": info
|
|
1652
|
+
};
|
|
1653
|
+
function label(icon, text) {
|
|
1654
|
+
const colorFn = ICON_COLORS[icon];
|
|
1655
|
+
if (colorFn) {
|
|
1656
|
+
return `${colorFn(icon)} ${text}`;
|
|
1657
|
+
}
|
|
1658
|
+
return `${icon} ${text}`;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
// src/commands/login.ts
|
|
1662
|
+
function prompt(query) {
|
|
1663
|
+
const rl2 = createInterface2({ input: process.stdin, output: process.stdout });
|
|
1664
|
+
return rl2.question(query).finally(() => rl2.close());
|
|
1665
|
+
}
|
|
1666
|
+
async function validateToken(endpoint, token) {
|
|
1667
|
+
try {
|
|
1668
|
+
const url = `${endpoint.replace(/\/+$/, "")}/api/v1/auth/me`;
|
|
1669
|
+
const res = await fetch(url, {
|
|
1670
|
+
method: "GET",
|
|
1671
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1672
|
+
});
|
|
1673
|
+
const body = await res.json();
|
|
1674
|
+
if (res.ok && body.data?.attributes?.token_status === "active") {
|
|
1675
|
+
return {
|
|
1676
|
+
valid: true,
|
|
1677
|
+
user_id: body.data.attributes.user_id,
|
|
1678
|
+
email: body.data.attributes.email,
|
|
1679
|
+
token_status: body.data.attributes.token_status
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
const detail = body.errors?.[0]?.detail ?? body.data?.attributes?.token_status ?? "Token rejected";
|
|
1683
|
+
return { valid: false, error: detail };
|
|
1684
|
+
} catch (err) {
|
|
1685
|
+
return {
|
|
1686
|
+
valid: false,
|
|
1687
|
+
error: `Cannot reach server: ${err.message}`
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
async function discoverSpaces(endpoint, token) {
|
|
1692
|
+
try {
|
|
1693
|
+
const url = `${endpoint.replace(/\/+$/, "")}/api/v1/auth/spaces`;
|
|
1694
|
+
const res = await fetch(url, {
|
|
1695
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1696
|
+
});
|
|
1697
|
+
const body = await res.json();
|
|
1698
|
+
if (res.ok && body.data) {
|
|
1699
|
+
return body.data.filter((d) => d.attributes?.space_id).map((d) => ({
|
|
1700
|
+
space_id: d.attributes.space_id,
|
|
1701
|
+
space_name: d.attributes.space_name ?? d.attributes.space_id,
|
|
1702
|
+
slug: d.attributes.slug ?? "",
|
|
1703
|
+
role: d.attributes.role ?? "guest"
|
|
1704
|
+
}));
|
|
1705
|
+
}
|
|
1706
|
+
return [];
|
|
1707
|
+
} catch {
|
|
1708
|
+
return [];
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
var loginCommand = new Command3("login").description("Authenticate with an access token and configure the project space").requiredOption("-p, --project-space <id>", "Project space UUID to use as primary space").action(async (opts) => {
|
|
1712
|
+
const config = getConfig();
|
|
1713
|
+
const projectSpaceId = opts.projectSpace;
|
|
1714
|
+
const endpoint = config.endpoint;
|
|
1715
|
+
const existing = await getToken();
|
|
1716
|
+
if (existing) {
|
|
1717
|
+
console.log(info("Already logged in."));
|
|
1718
|
+
const ans = await prompt(`${warning("Re-authenticate? (y/N):")} `);
|
|
1719
|
+
if (ans.toLowerCase() !== "y") {
|
|
1720
|
+
console.log(muted("Aborted."));
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
const accessToken = await prompt(`${info("Access token (paste, then Enter):")} `);
|
|
1725
|
+
if (!accessToken.trim()) {
|
|
1726
|
+
console.log(error("Access token is required."));
|
|
1727
|
+
process.exit(1);
|
|
1728
|
+
}
|
|
1729
|
+
console.log(` ${muted("Validating token against")} ${info(endpoint)}${muted("...")}`);
|
|
1730
|
+
const result = await validateToken(endpoint, accessToken.trim());
|
|
1731
|
+
if (!result.valid) {
|
|
1732
|
+
console.log(`${error("Authentication failed:")} ${result.error}`);
|
|
1733
|
+
process.exit(1);
|
|
1734
|
+
}
|
|
1735
|
+
console.log(` ${success("\u2714 Authenticated as")} ${info(result.email ?? result.user_id ?? "unknown")} ${muted(`(status: ${result.token_status})`)}`);
|
|
1736
|
+
await setToken(accessToken.trim());
|
|
1737
|
+
console.log(` ${success("\u2714 Access token stored in keychain.")}`);
|
|
1738
|
+
console.log(` ${muted("Discovering accessible spaces...")}`);
|
|
1739
|
+
const spaces = await discoverSpaces(endpoint, accessToken.trim());
|
|
1740
|
+
console.log(` ${info(`Found ${spaces.length} space(s).`)}`);
|
|
1741
|
+
const projectRoot = findProjectRoot();
|
|
1742
|
+
const spaceConfig = {
|
|
1743
|
+
project_space: [{ id: projectSpaceId }],
|
|
1744
|
+
spaces: spaces.map((s) => ({
|
|
1745
|
+
id: s.space_id,
|
|
1746
|
+
name: s.space_name,
|
|
1747
|
+
slug: s.slug || void 0
|
|
1748
|
+
}))
|
|
1749
|
+
};
|
|
1750
|
+
if (projectRoot) {
|
|
1751
|
+
await writeSpaceConfig(projectRoot, spaceConfig);
|
|
1752
|
+
const syncResult = await syncRelatedSpaces(projectRoot, endpoint, accessToken.trim());
|
|
1753
|
+
if (syncResult.added > 0) {
|
|
1754
|
+
console.log(` ${info(`\u2139 Also synced ${syncResult.added} related space(s) from space relations.`)}`);
|
|
1755
|
+
}
|
|
1756
|
+
console.log(` ${success("\u2714 Project space set to")} ${info(projectSpaceId)}`);
|
|
1757
|
+
}
|
|
1758
|
+
console.log(`
|
|
1759
|
+
${success("Logged in.")} ${muted("Accessible spaces:")}`);
|
|
1760
|
+
for (const s of spaces) {
|
|
1761
|
+
const roleColor = s.role === "owner" || s.role === "admin" ? success : warning;
|
|
1762
|
+
console.log(` ${muted(s.space_id)} ${roleColor(`(${s.role})`)}`);
|
|
1763
|
+
}
|
|
1764
|
+
const verifyResult = await validateToken(endpoint, accessToken.trim());
|
|
1765
|
+
if (verifyResult.valid) {
|
|
1766
|
+
console.log(` ${success("\u2714 Connection verified.")} ${muted(`User: ${verifyResult.email}`)}`);
|
|
1767
|
+
}
|
|
1768
|
+
});
|
|
1769
|
+
|
|
1770
|
+
// src/commands/logout.ts
|
|
1771
|
+
import { Command as Command4 } from "commander";
|
|
1772
|
+
var logoutCommand = new Command4("logout").description("Remove the access token from the OS keychain").action(async () => {
|
|
1773
|
+
await deleteToken();
|
|
1774
|
+
console.log(`${success("\u2714")} ${muted("Access token removed from keychain.")}`);
|
|
1775
|
+
});
|
|
1776
|
+
|
|
1777
|
+
// src/commands/add.ts
|
|
1778
|
+
init_parser();
|
|
1779
|
+
import { Command as Command5 } from "commander";
|
|
1780
|
+
var addCommand = new Command5("add").description("Add a space to .llmkb/spaces.yml").requiredOption("-s, --space <id>", "Space UUID to add").option("-n, --name <name>", "Optional space name").action(async (opts) => {
|
|
1781
|
+
const projectRoot = findProjectRoot();
|
|
1782
|
+
if (!projectRoot) {
|
|
1783
|
+
console.log(error("No .llmkb/ directory found. Run `llmkb init` first."));
|
|
1784
|
+
process.exit(1);
|
|
1785
|
+
}
|
|
1786
|
+
const ok = await addSpaceEntry(projectRoot, opts.space, opts.name);
|
|
1787
|
+
if (ok) {
|
|
1788
|
+
console.log(`${success("\u2714")} Space ${info(opts.space)} added to .llmkb/spaces.yml.`);
|
|
1789
|
+
} else {
|
|
1790
|
+
console.log(error("Could not find or parse .llmkb/spaces.yml."));
|
|
1791
|
+
process.exit(1);
|
|
1792
|
+
}
|
|
1793
|
+
});
|
|
1794
|
+
|
|
1795
|
+
// src/commands/remove.ts
|
|
1796
|
+
init_parser();
|
|
1797
|
+
import { Command as Command6 } from "commander";
|
|
1798
|
+
var removeCommand = new Command6("remove").description("Remove a space from .llmkb/spaces.yml by id").requiredOption("-s, --space <id>", "Space UUID to remove").action(async (opts) => {
|
|
1799
|
+
const projectRoot = findProjectRoot();
|
|
1800
|
+
if (!projectRoot) {
|
|
1801
|
+
console.log(error("No .llmkb/ directory found. Run `llmkb init` first."));
|
|
1802
|
+
process.exit(1);
|
|
1803
|
+
}
|
|
1804
|
+
const ok = await removeSpaceEntry(projectRoot, opts.space);
|
|
1805
|
+
if (ok) {
|
|
1806
|
+
console.log(`${success("\u2714")} Space ${info(opts.space)} removed from .llmkb/spaces.yml.`);
|
|
1807
|
+
} else {
|
|
1808
|
+
console.log(warning(`Space "${opts.space}" not found in .llmkb/spaces.yml.`));
|
|
1809
|
+
process.exit(1);
|
|
1810
|
+
}
|
|
1811
|
+
});
|
|
1812
|
+
|
|
1813
|
+
// src/commands/spaces.ts
|
|
1814
|
+
init_parser();
|
|
1815
|
+
import { Command as Command7 } from "commander";
|
|
1816
|
+
import { createInterface as createInterface3 } from "readline/promises";
|
|
1817
|
+
function prompt2(query) {
|
|
1818
|
+
const rl2 = createInterface3({ input: process.stdin, output: process.stdout });
|
|
1819
|
+
return rl2.question(query).finally(() => rl2.close());
|
|
1820
|
+
}
|
|
1821
|
+
var spacesCommand = new Command7("spaces").description("List all spaces in .llmkb/spaces.yml").option("--delete-all", "Remove all space entries (requires confirmation)").action(async (opts) => {
|
|
1822
|
+
const projectRoot = findProjectRoot();
|
|
1823
|
+
if (!projectRoot) {
|
|
1824
|
+
console.log(error("No .llmkb/ directory found. Run `llmkb init` first."));
|
|
1825
|
+
process.exit(1);
|
|
1826
|
+
}
|
|
1827
|
+
const config = await readSpaceConfig(projectRoot);
|
|
1828
|
+
if (!config) {
|
|
1829
|
+
console.log(warning("No .llmkb/spaces.yml found."));
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
if (opts.deleteAll) {
|
|
1833
|
+
console.log(warning("This will remove ALL space entries from .llmkb/spaces.yml."));
|
|
1834
|
+
const ans = await prompt2(`${warning("Continue? (y/N):")} `);
|
|
1835
|
+
if (ans.toLowerCase() !== "y") {
|
|
1836
|
+
console.log(muted("Aborted."));
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
await deleteAllSpaces(projectRoot);
|
|
1840
|
+
console.log(success("All space entries removed."));
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
if (config.project_space && config.project_space.length > 0) {
|
|
1844
|
+
console.log(info("Project space:"));
|
|
1845
|
+
for (const ps of config.project_space) {
|
|
1846
|
+
console.log(` ${ps.id}${ps.name ? success(` (${ps.name})`) : ""}`);
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
if (config.spaces && config.spaces.length > 0) {
|
|
1850
|
+
console.log(info("Additional spaces:"));
|
|
1851
|
+
for (const s of config.spaces) {
|
|
1852
|
+
console.log(` ${muted(s.id)}${s.name ? ` (${s.name})` : ""}`);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
if ((!config.project_space || config.project_space.length === 0) && (!config.spaces || config.spaces.length === 0)) {
|
|
1856
|
+
console.log(warning("No spaces configured."));
|
|
1857
|
+
}
|
|
1858
|
+
});
|
|
1859
|
+
|
|
1860
|
+
// src/commands/update.ts
|
|
1861
|
+
init_parser();
|
|
1862
|
+
import { Command as Command8 } from "commander";
|
|
1863
|
+
var updateCommand = new Command8("update").description("Update the project space in .llmkb/spaces.yml").requiredOption("-p, --project-space <id>", "New project space UUID").action(async (opts) => {
|
|
1864
|
+
const projectRoot = findProjectRoot();
|
|
1865
|
+
if (!projectRoot) {
|
|
1866
|
+
console.log(error("No .llmkb/ directory found. Run `llmkb init` first."));
|
|
1867
|
+
process.exit(1);
|
|
1868
|
+
}
|
|
1869
|
+
const ok = await updateProjectSpace(projectRoot, opts.projectSpace);
|
|
1870
|
+
if (ok) {
|
|
1871
|
+
console.log(`${success("\u2714")} Project space updated to ${info(opts.projectSpace)}.`);
|
|
1872
|
+
} else {
|
|
1873
|
+
console.log(error("Could not update project space in .llmkb/spaces.yml."));
|
|
1874
|
+
process.exit(1);
|
|
1875
|
+
}
|
|
1876
|
+
});
|
|
1877
|
+
|
|
1878
|
+
// src/commands/whoami.ts
|
|
1879
|
+
import { Command as Command9 } from "commander";
|
|
1880
|
+
var whoamiCommand = new Command9("whoami").description("Show current authenticated user and access token status").action(async () => {
|
|
1881
|
+
const config = getConfig();
|
|
1882
|
+
const token = await getToken();
|
|
1883
|
+
if (!token) {
|
|
1884
|
+
console.log(error("Not logged in."));
|
|
1885
|
+
console.log(`Run ${info("`llmkb login --project-space <id>`")} to authenticate.`);
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
console.log(`${muted("Access token:")} ${success("\u2714 present")}`);
|
|
1889
|
+
try {
|
|
1890
|
+
const url = `${config.endpoint}/api/v1/auth/me`;
|
|
1891
|
+
const res = await fetch(url, {
|
|
1892
|
+
method: "GET",
|
|
1893
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1894
|
+
});
|
|
1895
|
+
if (res.ok) {
|
|
1896
|
+
const body = await res.json();
|
|
1897
|
+
const attrs = body.data?.attributes;
|
|
1898
|
+
if (attrs) {
|
|
1899
|
+
console.log(` ${muted("User:")} ${info(attrs.email ?? attrs.user_id)}`);
|
|
1900
|
+
console.log(` ${muted("Token status:")} ${success(attrs.token_status ?? "active")}`);
|
|
1901
|
+
if (attrs.expires_at) {
|
|
1902
|
+
console.log(` ${muted("Expires at:")} ${attrs.expires_at}`);
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
} else {
|
|
1906
|
+
const statusText = res.status === 401 ? error("invalid or expired") : warning(`server returned ${res.status}`);
|
|
1907
|
+
console.log(` ${muted("Token status:")} ${statusText}`);
|
|
1908
|
+
console.log(` ${muted("Endpoint:")} ${config.endpoint}`);
|
|
1909
|
+
}
|
|
1910
|
+
} catch {
|
|
1911
|
+
console.log(` ${muted("Endpoint:")} ${warning(config.endpoint + " (unreachable)")}`);
|
|
1912
|
+
}
|
|
1913
|
+
});
|
|
1914
|
+
|
|
1915
|
+
// src/commands/use.ts
|
|
1916
|
+
import { Command as Command10 } from "commander";
|
|
1917
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1918
|
+
import { join as join6 } from "path";
|
|
1919
|
+
init_parser();
|
|
1920
|
+
init_parser();
|
|
1921
|
+
init_types();
|
|
1922
|
+
var useCommand = new Command10("use").description("Set the project space in .llmkb/spaces.yml").argument("<space-id>", "Space UUID to set as project space").action(async (spaceId) => {
|
|
1923
|
+
const config = getConfig();
|
|
1924
|
+
const projectRoot = findProjectRoot();
|
|
1925
|
+
if (!projectRoot) {
|
|
1926
|
+
console.log(error("No .llmkb/ directory found. Run `llmkb init` first."));
|
|
1927
|
+
process.exit(1);
|
|
1928
|
+
}
|
|
1929
|
+
const spacesFile = join6(projectRoot, ".llmkb", "spaces.yml");
|
|
1930
|
+
if (!existsSync5(spacesFile)) {
|
|
1931
|
+
console.log(error("No .llmkb/spaces.yml found. Run `llmkb init` first."));
|
|
1932
|
+
process.exit(1);
|
|
1933
|
+
}
|
|
1934
|
+
const stamp = await readVersionStamp(projectRoot);
|
|
1935
|
+
if (stamp && stamp !== PACKAGE_VERSION) {
|
|
1936
|
+
console.log(warning(` \u26A0 Version stamp mismatch (${stamp}). Re-run \`llmkb init\` to update.`));
|
|
1937
|
+
}
|
|
1938
|
+
const token = await getToken();
|
|
1939
|
+
if (!token) {
|
|
1940
|
+
console.log(warning("No access token found."));
|
|
1941
|
+
console.log(`Run ${info("`llmkb login --project-space <id>`")} to authenticate.`);
|
|
1942
|
+
}
|
|
1943
|
+
const url = `${config.endpoint}/api/v1/auth/me`;
|
|
1944
|
+
try {
|
|
1945
|
+
const res = await fetch(url, {
|
|
1946
|
+
method: "GET",
|
|
1947
|
+
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
|
1948
|
+
});
|
|
1949
|
+
if (!res.ok) {
|
|
1950
|
+
console.log(warning(`Warning: Cannot reach server \u2014 returned ${res.status}`));
|
|
1951
|
+
}
|
|
1952
|
+
} catch {
|
|
1953
|
+
console.log(warning(`Warning: Cannot reach ${config.endpoint} \u2014 server may be offline`));
|
|
1954
|
+
}
|
|
1955
|
+
const updated = await updateProjectSpace(projectRoot, spaceId);
|
|
1956
|
+
if (updated) {
|
|
1957
|
+
console.log(`${success("\u2714")} Project space set to ${info(spaceId)}.`);
|
|
1958
|
+
} else {
|
|
1959
|
+
console.log(error("Failed to update .llmkb/spaces.yml."));
|
|
1960
|
+
process.exit(1);
|
|
1961
|
+
}
|
|
1962
|
+
});
|
|
1963
|
+
|
|
1964
|
+
// src/commands/status.ts
|
|
1965
|
+
init_parser();
|
|
1966
|
+
init_types();
|
|
1967
|
+
import { Command as Command11 } from "commander";
|
|
1968
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1969
|
+
import { join as join7 } from "path";
|
|
1970
|
+
var statusCommand = new Command11("status").description("Show version stamps, configuration, and access token status").action(async () => {
|
|
1971
|
+
console.log(`${info("llmkb status")}
|
|
1972
|
+
`);
|
|
1973
|
+
console.log(` ${muted("Package version:")} ${PACKAGE_VERSION}`);
|
|
1974
|
+
const token = await getToken();
|
|
1975
|
+
const tokenStatus = token ? success("\u2714 present") : error("\u2717 missing");
|
|
1976
|
+
console.log(` ${muted("Access token:")} ${tokenStatus}`);
|
|
1977
|
+
const projectRoot = findProjectRoot();
|
|
1978
|
+
if (!projectRoot) {
|
|
1979
|
+
console.log(` ${muted("Project root:")} ${warning("(not found \u2014 run `llmkb init`)")}`);
|
|
1980
|
+
console.log(` ${muted("Config version:")} ${warning("(not found)")}`);
|
|
1981
|
+
console.log(` ${muted("Skills installed:")} 0`);
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
console.log(` ${muted("Project root:")} ${projectRoot}`);
|
|
1985
|
+
const configVersion = await readVersionStamp(projectRoot);
|
|
1986
|
+
if (configVersion) {
|
|
1987
|
+
const versionOk = configVersion === PACKAGE_VERSION;
|
|
1988
|
+
console.log(` ${muted("Config version:")} ${versionOk ? success(configVersion) : warning(configVersion)}`);
|
|
1989
|
+
if (!versionOk) {
|
|
1990
|
+
console.log(` ${warning("\u26A0 Version mismatch! Re-run `llmkb init` to update config.")}`);
|
|
1991
|
+
}
|
|
1992
|
+
} else {
|
|
1993
|
+
console.log(` ${muted("Config version:")} ${warning("(not found \u2014 run `llmkb init`)")}`);
|
|
1994
|
+
}
|
|
1995
|
+
const spaceConfig = await readSpaceConfig(projectRoot);
|
|
1996
|
+
if (spaceConfig) {
|
|
1997
|
+
if (spaceConfig.project_space && spaceConfig.project_space.length > 0) {
|
|
1998
|
+
const ps = spaceConfig.project_space[0];
|
|
1999
|
+
console.log(` ${muted("Project space:")} ${info(ps.id)}${ps.name ? ` (${ps.name})` : ""}`);
|
|
2000
|
+
} else {
|
|
2001
|
+
console.log(` ${muted("Project space:")} ${warning("(not configured)")}`);
|
|
2002
|
+
}
|
|
2003
|
+
const spaceCount = spaceConfig.spaces?.length ?? 0;
|
|
2004
|
+
console.log(` ${muted("Spaces defined:")} ${spaceCount}`);
|
|
2005
|
+
if (spaceCount > 0 && spaceConfig.spaces) {
|
|
2006
|
+
console.log(` ${muted(spaceConfig.spaces.map((s) => s.id).join(", "))}`);
|
|
2007
|
+
}
|
|
2008
|
+
} else {
|
|
2009
|
+
console.log(` ${muted("Project space:")} ${warning("(not configured)")}`);
|
|
2010
|
+
}
|
|
2011
|
+
const skillsDir = join7(projectRoot, ".claude", "skills", "llmkb");
|
|
2012
|
+
if (existsSync6(skillsDir)) {
|
|
2013
|
+
const mismatches = await checkSkillVersions(skillsDir);
|
|
2014
|
+
const entryCount = await (await import("fs/promises")).readdir(skillsDir, { withFileTypes: true }).then((e) => e.filter((x) => x.isDirectory()).length).catch(() => 0);
|
|
2015
|
+
console.log(` ${muted("Skills installed:")} ${entryCount}`);
|
|
2016
|
+
for (const m of mismatches) {
|
|
2017
|
+
console.log(` ${warning(` ${m.name}: ${m.installed} \u26A0 mismatch`)}`);
|
|
2018
|
+
}
|
|
2019
|
+
if (mismatches.length > 0) {
|
|
2020
|
+
console.log(` ${warning("\u26A0 Some skills have version mismatches. Re-run `llmkb init`.")}`);
|
|
2021
|
+
}
|
|
2022
|
+
} else {
|
|
2023
|
+
console.log(` ${muted("Skills installed:")} 0 (run \`llmkb init\`)`);
|
|
2024
|
+
}
|
|
2025
|
+
});
|
|
2026
|
+
|
|
2027
|
+
// src/commands/sync.ts
|
|
2028
|
+
import { Command as Command12 } from "commander";
|
|
2029
|
+
import { join as join11 } from "path";
|
|
2030
|
+
init_parser();
|
|
2031
|
+
init_sync_state();
|
|
2032
|
+
|
|
2033
|
+
// lib/sync.ts
|
|
2034
|
+
import { readdir, stat as fsStat, readFile as readFile5 } from "fs/promises";
|
|
2035
|
+
import { join as join9, relative, resolve as resolve6 } from "path";
|
|
2036
|
+
import ignore from "ignore";
|
|
2037
|
+
import * as tus from "tus-js-client";
|
|
2038
|
+
import { existsSync as existsSync8 } from "fs";
|
|
2039
|
+
init_parser();
|
|
2040
|
+
var IGNORE_FILES = [".gitignore", ".llmkbignore"];
|
|
2041
|
+
var DEFAULT_SKIP_PATTERNS = [
|
|
2042
|
+
"node_modules/**",
|
|
2043
|
+
".git/**",
|
|
2044
|
+
".llmkb/**",
|
|
2045
|
+
".claude/**",
|
|
2046
|
+
".cursor/**",
|
|
2047
|
+
".next/**",
|
|
2048
|
+
"dist/**",
|
|
2049
|
+
".venv/**",
|
|
2050
|
+
"__pycache__/**",
|
|
2051
|
+
"*.pyc",
|
|
2052
|
+
"*.exe",
|
|
2053
|
+
"*.dll",
|
|
2054
|
+
"*.so",
|
|
2055
|
+
"*.dylib",
|
|
2056
|
+
"*.bin",
|
|
2057
|
+
// Test & ephemeral artifacts — exclude by default to avoid polluting
|
|
2058
|
+
// the knowledge graph with test fixtures, snapshots, and build artifacts.
|
|
2059
|
+
"**/__tests__/**",
|
|
2060
|
+
"**/__snapshots__/**",
|
|
2061
|
+
"**/test/**",
|
|
2062
|
+
"**/tests/**",
|
|
2063
|
+
"**/*.test.*",
|
|
2064
|
+
"**/*.spec.*",
|
|
2065
|
+
"**/fixtures/**",
|
|
2066
|
+
"**/coverage/**",
|
|
2067
|
+
"*.log",
|
|
2068
|
+
"tmp/**",
|
|
2069
|
+
"temp/**"
|
|
2070
|
+
];
|
|
2071
|
+
async function buildFilter(projectDir, targetDir, respectGitignore, respectLlmkbignore) {
|
|
2072
|
+
const ig = ignore().add(DEFAULT_SKIP_PATTERNS);
|
|
2073
|
+
const searchDirs = [projectDir, targetDir];
|
|
2074
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2075
|
+
for (const baseDir of searchDirs) {
|
|
2076
|
+
for (const fileName of IGNORE_FILES) {
|
|
2077
|
+
const shouldRespect = fileName === ".gitignore" ? respectGitignore : respectLlmkbignore;
|
|
2078
|
+
if (!shouldRespect) continue;
|
|
2079
|
+
const filePath = join9(baseDir, fileName);
|
|
2080
|
+
if (seen.has(filePath)) continue;
|
|
2081
|
+
seen.add(filePath);
|
|
2082
|
+
if (existsSync8(filePath)) {
|
|
2083
|
+
const st = await fsStat(filePath);
|
|
2084
|
+
if (!st.isFile()) continue;
|
|
2085
|
+
const content = await readFile5(filePath, "utf-8");
|
|
2086
|
+
ig.add(content);
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
return (testPath) => ig.ignores(testPath);
|
|
2091
|
+
}
|
|
2092
|
+
async function walkFiles(opts) {
|
|
2093
|
+
const projectDir = resolve6(opts.projectDir);
|
|
2094
|
+
const targetDir = resolve6(opts.targetDir ?? projectDir);
|
|
2095
|
+
const respectGitignore = opts.respectGitignore ?? true;
|
|
2096
|
+
const respectLlmkbignore = opts.respectLlmkbignore ?? true;
|
|
2097
|
+
const isIgnored = await buildFilter(projectDir, targetDir, respectGitignore, respectLlmkbignore);
|
|
2098
|
+
const files = [];
|
|
2099
|
+
const skipped = [];
|
|
2100
|
+
async function walk(dir) {
|
|
2101
|
+
let entries;
|
|
2102
|
+
try {
|
|
2103
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
2104
|
+
} catch {
|
|
2105
|
+
skipped.push({ path: relative(targetDir, dir), reason: "unreadable" });
|
|
2106
|
+
return;
|
|
2107
|
+
}
|
|
2108
|
+
for (const entry of entries) {
|
|
2109
|
+
const fullPath = join9(dir, entry.name);
|
|
2110
|
+
const relPath = relative(targetDir, fullPath);
|
|
2111
|
+
if (entry.name.startsWith(".") && !IGNORE_FILES.includes(entry.name)) {
|
|
2112
|
+
skipped.push({ path: relPath, reason: "hidden" });
|
|
2113
|
+
continue;
|
|
2114
|
+
}
|
|
2115
|
+
if (isIgnored(relPath)) {
|
|
2116
|
+
skipped.push({ path: relPath, reason: "ignored" });
|
|
2117
|
+
continue;
|
|
2118
|
+
}
|
|
2119
|
+
if (entry.isDirectory()) {
|
|
2120
|
+
await walk(fullPath);
|
|
2121
|
+
} else if (entry.isFile()) {
|
|
2122
|
+
files.push([relPath, fullPath]);
|
|
2123
|
+
} else if (entry.isSymbolicLink()) {
|
|
2124
|
+
try {
|
|
2125
|
+
const st = await fsStat(fullPath);
|
|
2126
|
+
if (st.isFile()) {
|
|
2127
|
+
files.push([relPath, fullPath]);
|
|
2128
|
+
} else {
|
|
2129
|
+
skipped.push({ path: relPath, reason: "symlink-to-dir" });
|
|
2130
|
+
}
|
|
2131
|
+
} catch {
|
|
2132
|
+
skipped.push({ path: relPath, reason: "broken-symlink" });
|
|
2133
|
+
}
|
|
2134
|
+
} else {
|
|
2135
|
+
skipped.push({ path: relPath, reason: "not-a-file" });
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
await walk(targetDir);
|
|
2140
|
+
return { files, skipped };
|
|
2141
|
+
}
|
|
2142
|
+
async function tusUpload(opts) {
|
|
2143
|
+
const st = await fsStat(opts.absolutePath);
|
|
2144
|
+
if (!st.isFile()) {
|
|
2145
|
+
throw new Error(`Not a file: ${opts.relativePath}`);
|
|
2146
|
+
}
|
|
2147
|
+
const fileBuffer = await readFile5(opts.absolutePath);
|
|
2148
|
+
const fileName = opts.relativePath.split("/").pop() ?? opts.relativePath;
|
|
2149
|
+
return new Promise((resolve8, reject) => {
|
|
2150
|
+
const upload = new tus.Upload(
|
|
2151
|
+
fileBuffer,
|
|
2152
|
+
{
|
|
2153
|
+
endpoint: `${opts.endpoint}/api/spaces/${opts.space}/uploads`,
|
|
2154
|
+
metadata: {
|
|
2155
|
+
filename: opts.relativePath,
|
|
2156
|
+
filetype: "application/octet-stream"
|
|
2157
|
+
},
|
|
2158
|
+
headers: {
|
|
2159
|
+
Authorization: `Bearer ${opts.token}`
|
|
2160
|
+
},
|
|
2161
|
+
chunkSize: 5 * 1024 * 1024,
|
|
2162
|
+
// 5 MB chunks
|
|
2163
|
+
retryDelays: [0, 1e3, 3e3, 5e3],
|
|
2164
|
+
removeFingerprintOnSuccess: true,
|
|
2165
|
+
onError: (err) => reject(err),
|
|
2166
|
+
onProgress: (bytesSent, bytesTotal) => {
|
|
2167
|
+
opts.onProgress?.(bytesSent, bytesTotal);
|
|
2168
|
+
},
|
|
2169
|
+
onSuccess: () => {
|
|
2170
|
+
resolve8(upload.url ?? "");
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
);
|
|
2174
|
+
upload.start();
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2177
|
+
async function finalizeUpload(uploadUrl, token, ingestionJobId) {
|
|
2178
|
+
const params = new URLSearchParams({ postpone_ingestion: "true" });
|
|
2179
|
+
if (ingestionJobId) {
|
|
2180
|
+
params.set("ingestion_job_id", ingestionJobId);
|
|
2181
|
+
}
|
|
2182
|
+
const completeUrl = `${uploadUrl}/complete?${params.toString()}`;
|
|
2183
|
+
const res = await fetch(completeUrl, {
|
|
2184
|
+
method: "POST",
|
|
2185
|
+
headers: {
|
|
2186
|
+
Authorization: `Bearer ${token}`,
|
|
2187
|
+
"Tus-Resumable": "1.0.0"
|
|
2188
|
+
}
|
|
2189
|
+
});
|
|
2190
|
+
if (!res.ok) {
|
|
2191
|
+
const body = await res.text().catch(() => "");
|
|
2192
|
+
const shortId = uploadUrl.split("/").pop() ?? "unknown";
|
|
2193
|
+
throw new Error(`Upload completion failed for ${shortId} (${res.status}): ${body}`);
|
|
2194
|
+
}
|
|
2195
|
+
return uploadUrl;
|
|
2196
|
+
}
|
|
2197
|
+
async function runSync(space, syncState, opts) {
|
|
2198
|
+
const projectRoot = findProjectRoot();
|
|
2199
|
+
if (!projectRoot) throw new Error("No .llmkb/ directory found. Run `llmkb init` first.");
|
|
2200
|
+
const config = getConfig();
|
|
2201
|
+
const endpoint = config.endpoint;
|
|
2202
|
+
const token = await getToken();
|
|
2203
|
+
const result = { uploaded: 0, skipped: 0, unchanged: 0, renamed: 0, errors: [] };
|
|
2204
|
+
const walkResult = await walkFiles({
|
|
2205
|
+
projectDir: projectRoot,
|
|
2206
|
+
targetDir: resolve6(opts.path)
|
|
2207
|
+
});
|
|
2208
|
+
for (const s of walkResult.skipped) {
|
|
2209
|
+
result.skipped++;
|
|
2210
|
+
opts.onFile?.(s.path, "skipped", s.reason);
|
|
2211
|
+
}
|
|
2212
|
+
if (walkResult.files.length === 0) {
|
|
2213
|
+
result.status = "no_files";
|
|
2214
|
+
const filteredCount = walkResult.skipped.length;
|
|
2215
|
+
if (filteredCount > 0) {
|
|
2216
|
+
result.reason = `No files to sync \u2014 all ${filteredCount} file(s) were filtered out by .gitignore / .llmkbignore rules.`;
|
|
2217
|
+
} else {
|
|
2218
|
+
result.reason = "No files found in sync target. The directory appears to be empty.";
|
|
2219
|
+
}
|
|
2220
|
+
return result;
|
|
2221
|
+
}
|
|
2222
|
+
const changes = await Promise.resolve().then(() => (init_sync_state(), sync_state_exports)).then(
|
|
2223
|
+
(m) => m.getChangedFiles(walkResult.files, syncState)
|
|
2224
|
+
);
|
|
2225
|
+
for (const f of changes.unchangedFiles) {
|
|
2226
|
+
result.unchanged++;
|
|
2227
|
+
opts.onFile?.(f, "unchanged");
|
|
2228
|
+
}
|
|
2229
|
+
for (const r of changes.renamed) {
|
|
2230
|
+
result.renamed++;
|
|
2231
|
+
opts.onFile?.(r.newPath, "renamed", `${r.oldPath} \u2192 ${r.newPath}`);
|
|
2232
|
+
}
|
|
2233
|
+
const toUpload = [...changes.newFiles, ...changes.changedFiles];
|
|
2234
|
+
if (toUpload.length === 0) {
|
|
2235
|
+
result.status = "up_to_date";
|
|
2236
|
+
result.reason = `No changes detected \u2014 all files are up to date (${walkResult.files.length} files, unchanged: ${result.unchanged}, renamed: ${result.renamed}, skipped: ${result.skipped}).`;
|
|
2237
|
+
return result;
|
|
2238
|
+
}
|
|
2239
|
+
const uploadUrls = [];
|
|
2240
|
+
const ingestionJobId = void 0;
|
|
2241
|
+
for (const relPath of toUpload) {
|
|
2242
|
+
const absPath = join9(resolve6(opts.path), relPath);
|
|
2243
|
+
if (!token) {
|
|
2244
|
+
result.errors.push({ path: relPath, error: "No token found" });
|
|
2245
|
+
opts.onFile?.(relPath, "failed", "no token");
|
|
2246
|
+
continue;
|
|
2247
|
+
}
|
|
2248
|
+
try {
|
|
2249
|
+
const absEntry = walkResult.files.find(([, a]) => a === absPath);
|
|
2250
|
+
const absPathResolved = absEntry?.[1] ?? absPath;
|
|
2251
|
+
const url = await tusUpload({
|
|
2252
|
+
space,
|
|
2253
|
+
relativePath: relPath,
|
|
2254
|
+
absolutePath: absPathResolved,
|
|
2255
|
+
endpoint,
|
|
2256
|
+
token
|
|
2257
|
+
});
|
|
2258
|
+
uploadUrls.push(url);
|
|
2259
|
+
result.uploaded++;
|
|
2260
|
+
opts.onFile?.(relPath, "uploading");
|
|
2261
|
+
} catch (err) {
|
|
2262
|
+
result.errors.push({ path: relPath, error: err.message });
|
|
2263
|
+
opts.onFile?.(relPath, "failed", err.message);
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
if (uploadUrls.length > 0) {
|
|
2267
|
+
for (const url of uploadUrls) {
|
|
2268
|
+
try {
|
|
2269
|
+
await finalizeUpload(url, token, ingestionJobId);
|
|
2270
|
+
} catch (err) {
|
|
2271
|
+
const shortId = url.split("/").pop() ?? "unknown";
|
|
2272
|
+
result.errors.push({ path: shortId, error: err.message });
|
|
2273
|
+
opts.onFile?.(shortId, "failed", err.message);
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
if (token) {
|
|
2277
|
+
try {
|
|
2278
|
+
const ingestRes = await fetch(
|
|
2279
|
+
`${endpoint}/api/spaces/${space}/uploads/start-ingest`,
|
|
2280
|
+
{
|
|
2281
|
+
method: "POST",
|
|
2282
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
2283
|
+
}
|
|
2284
|
+
);
|
|
2285
|
+
if (ingestRes.ok) {
|
|
2286
|
+
const ingestBody = await ingestRes.json();
|
|
2287
|
+
const realUrl = ingestBody.data?.attributes?.url;
|
|
2288
|
+
if (realUrl) {
|
|
2289
|
+
result.ingestionJobUrl = realUrl;
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
} catch {
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
result.status = "synced";
|
|
2297
|
+
return result;
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
// lib/output.ts
|
|
2301
|
+
import { stdout as stdout2 } from "process";
|
|
2302
|
+
import cliProgress from "cli-progress";
|
|
2303
|
+
function isTTY() {
|
|
2304
|
+
return stdout2.isTTY ?? false;
|
|
2305
|
+
}
|
|
2306
|
+
var ProgressBar = class {
|
|
2307
|
+
bar = null;
|
|
2308
|
+
label;
|
|
2309
|
+
constructor(opts) {
|
|
2310
|
+
this.label = opts.label ?? "Progress";
|
|
2311
|
+
if (isTTY()) {
|
|
2312
|
+
this.bar = new cliProgress.SingleBar(
|
|
2313
|
+
{
|
|
2314
|
+
format: `{label} [{bar}] {percentage}% | {value}/{total} | {eta_formatted}`,
|
|
2315
|
+
barCompleteChar: "\u2588",
|
|
2316
|
+
barIncompleteChar: "\u2591",
|
|
2317
|
+
hideCursor: true
|
|
2318
|
+
},
|
|
2319
|
+
cliProgress.Presets.shades_classic
|
|
2320
|
+
);
|
|
2321
|
+
this.bar.start(opts.total, 0, { label: this.label });
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
/** Advance the progress bar by one step. */
|
|
2325
|
+
increment() {
|
|
2326
|
+
if (this.bar) this.bar.increment();
|
|
2327
|
+
}
|
|
2328
|
+
/** Set the current value directly. */
|
|
2329
|
+
update(value) {
|
|
2330
|
+
if (this.bar) this.bar.update(value);
|
|
2331
|
+
}
|
|
2332
|
+
/** Finalise the progress bar. */
|
|
2333
|
+
stop() {
|
|
2334
|
+
if (this.bar) this.bar.stop();
|
|
2335
|
+
}
|
|
2336
|
+
};
|
|
2337
|
+
function writeJson(result) {
|
|
2338
|
+
stdout2.write(JSON.stringify(result, null, 2) + "\n");
|
|
2339
|
+
}
|
|
2340
|
+
function writeLine(message, icon) {
|
|
2341
|
+
if (icon) {
|
|
2342
|
+
stdout2.write(`${label(icon, message)}
|
|
2343
|
+
`);
|
|
2344
|
+
} else {
|
|
2345
|
+
stdout2.write(`${message}
|
|
2346
|
+
`);
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
function writeDetail(message) {
|
|
2350
|
+
stdout2.write(` ${muted(message)}
|
|
2351
|
+
`);
|
|
2352
|
+
}
|
|
2353
|
+
function writeWarning(message) {
|
|
2354
|
+
stdout2.write(`${warning("\u26A0")} ${warning(message)}
|
|
2355
|
+
`);
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
// src/commands/sync.ts
|
|
2359
|
+
init_parser();
|
|
2360
|
+
|
|
2361
|
+
// lib/watch-lock.ts
|
|
2362
|
+
import { writeFile as writeFile7, readFile as readFile6, unlink as unlink2 } from "fs/promises";
|
|
2363
|
+
import { existsSync as existsSync9 } from "fs";
|
|
2364
|
+
import { join as join10 } from "path";
|
|
2365
|
+
var LOCK_FILE = ".llmkb/.watch.lock";
|
|
2366
|
+
async function acquireWatchLock(projectRoot) {
|
|
2367
|
+
if (!projectRoot) return true;
|
|
2368
|
+
const lockPath = join10(projectRoot, LOCK_FILE);
|
|
2369
|
+
if (existsSync9(lockPath)) {
|
|
2370
|
+
try {
|
|
2371
|
+
const content = await readFile6(lockPath, "utf-8");
|
|
2372
|
+
const pid = parseInt(content.trim(), 10);
|
|
2373
|
+
if (Number.isFinite(pid) && pid > 0) {
|
|
2374
|
+
try {
|
|
2375
|
+
process.kill(pid, 0);
|
|
2376
|
+
return false;
|
|
2377
|
+
} catch (e) {
|
|
2378
|
+
if (e.code !== "ESRCH") {
|
|
2379
|
+
return false;
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
} catch {
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
await writeFile7(lockPath, String(process.pid), "utf-8");
|
|
2387
|
+
return true;
|
|
2388
|
+
}
|
|
2389
|
+
async function releaseWatchLock(projectRoot) {
|
|
2390
|
+
if (!projectRoot) return;
|
|
2391
|
+
const lockPath = join10(projectRoot, LOCK_FILE);
|
|
2392
|
+
try {
|
|
2393
|
+
if (existsSync9(lockPath)) {
|
|
2394
|
+
const content = await readFile6(lockPath, "utf-8");
|
|
2395
|
+
if (content.trim() === String(process.pid)) {
|
|
2396
|
+
await unlink2(lockPath);
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
} catch {
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
// src/commands/sync.ts
|
|
2404
|
+
var DEFAULT_SKIP_PATTERNS2 = [
|
|
2405
|
+
"node_modules/**",
|
|
2406
|
+
".git/**",
|
|
2407
|
+
".llmkb/**",
|
|
2408
|
+
".claude/**",
|
|
2409
|
+
".cursor/**",
|
|
2410
|
+
".next/**",
|
|
2411
|
+
"dist/**",
|
|
2412
|
+
".venv/**",
|
|
2413
|
+
"__pycache__/**",
|
|
2414
|
+
"*.pyc",
|
|
2415
|
+
"*.exe",
|
|
2416
|
+
"*.dll",
|
|
2417
|
+
"*.so",
|
|
2418
|
+
"*.dylib",
|
|
2419
|
+
"*.bin",
|
|
2420
|
+
"**/__tests__/**",
|
|
2421
|
+
"**/__snapshots__/**",
|
|
2422
|
+
"**/test/**",
|
|
2423
|
+
"**/tests/**",
|
|
2424
|
+
"**/*.test.*",
|
|
2425
|
+
"**/*.spec.*",
|
|
2426
|
+
"**/fixtures/**",
|
|
2427
|
+
"**/coverage/**",
|
|
2428
|
+
"*.log",
|
|
2429
|
+
"tmp/**",
|
|
2430
|
+
"temp/**"
|
|
2431
|
+
];
|
|
2432
|
+
async function startWatch(targetPath, space, opts) {
|
|
2433
|
+
const projectRoot = findProjectRoot();
|
|
2434
|
+
if (projectRoot) {
|
|
2435
|
+
const lockReleased = await acquireWatchLock(projectRoot);
|
|
2436
|
+
if (!lockReleased) {
|
|
2437
|
+
writeLine(
|
|
2438
|
+
"Another watcher is already running. Use --force to override or stop the existing watcher.",
|
|
2439
|
+
"\u2716"
|
|
2440
|
+
);
|
|
2441
|
+
process.exit(1);
|
|
2442
|
+
}
|
|
2443
|
+
process.on("exit", () => releaseWatchLock(projectRoot));
|
|
2444
|
+
process.on("SIGINT", () => {
|
|
2445
|
+
releaseWatchLock(projectRoot);
|
|
2446
|
+
process.exit(0);
|
|
2447
|
+
});
|
|
2448
|
+
process.on("SIGTERM", () => {
|
|
2449
|
+
releaseWatchLock(projectRoot);
|
|
2450
|
+
process.exit(0);
|
|
2451
|
+
});
|
|
2452
|
+
}
|
|
2453
|
+
const chokidar = await import("chokidar");
|
|
2454
|
+
const ignore2 = await import("ignore");
|
|
2455
|
+
const { readFile: readFile8 } = await import("fs/promises");
|
|
2456
|
+
const { existsSync: existsSync11 } = await import("fs");
|
|
2457
|
+
const ig = ignore2.default();
|
|
2458
|
+
ig.add([
|
|
2459
|
+
"node_modules/**",
|
|
2460
|
+
".git/**",
|
|
2461
|
+
".llmkb/**",
|
|
2462
|
+
".claude/**",
|
|
2463
|
+
".cursor/**",
|
|
2464
|
+
".next/**",
|
|
2465
|
+
"dist/**",
|
|
2466
|
+
".venv/**",
|
|
2467
|
+
"__pycache__/**",
|
|
2468
|
+
"*.pyc",
|
|
2469
|
+
"*.exe",
|
|
2470
|
+
"*.dll",
|
|
2471
|
+
"*.so",
|
|
2472
|
+
"*.dylib",
|
|
2473
|
+
"*.bin"
|
|
2474
|
+
]);
|
|
2475
|
+
for (const fn of [".gitignore", ".llmkbignore"]) {
|
|
2476
|
+
const fp = join11(targetPath, fn);
|
|
2477
|
+
if (existsSync11(fp)) {
|
|
2478
|
+
ig.add(String(await readFile8(fp)));
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
function fmtIgnoreRule(rel) {
|
|
2482
|
+
for (const p of DEFAULT_SKIP_PATTERNS2) {
|
|
2483
|
+
if (ig.ignores(rel)) return `(${p})`;
|
|
2484
|
+
}
|
|
2485
|
+
if (/(^|[\/\\])\../.test(rel)) return "(dotfile)";
|
|
2486
|
+
return null;
|
|
2487
|
+
}
|
|
2488
|
+
function isWatched(filePath) {
|
|
2489
|
+
const rel = join11(targetPath, filePath);
|
|
2490
|
+
const ignored = ig.ignores(rel) || /(^|[\/\\])\../.test(rel);
|
|
2491
|
+
if (ignored && opts.debug) {
|
|
2492
|
+
const rule = fmtIgnoreRule(rel);
|
|
2493
|
+
writeDetail(`[debug] skip ${rel} ${rule ?? ""}`);
|
|
2494
|
+
}
|
|
2495
|
+
return !ignored;
|
|
2496
|
+
}
|
|
2497
|
+
const watcher = chokidar.watch(targetPath, {
|
|
2498
|
+
ignored: (path) => !isWatched(path),
|
|
2499
|
+
persistent: true,
|
|
2500
|
+
ignoreInitial: true
|
|
2501
|
+
});
|
|
2502
|
+
let timer = null;
|
|
2503
|
+
const onChange = async (filePath) => {
|
|
2504
|
+
if (timer) clearTimeout(timer);
|
|
2505
|
+
timer = setTimeout(async () => {
|
|
2506
|
+
if (opts.debug) writeDetail(`[debug] detected: ${filePath}`);
|
|
2507
|
+
try {
|
|
2508
|
+
await performSync(targetPath, space, { ...opts, watchEvent: filePath });
|
|
2509
|
+
} catch (err) {
|
|
2510
|
+
writeLine(`Sync error: ${err.message}`, "\u2716");
|
|
2511
|
+
}
|
|
2512
|
+
}, opts.debounceMs);
|
|
2513
|
+
};
|
|
2514
|
+
watcher.on("change", onChange);
|
|
2515
|
+
watcher.on("add", onChange);
|
|
2516
|
+
watcher.on("unlink", (p) => {
|
|
2517
|
+
if (opts.verbose) writeDetail(`[watch] removed: ${p}`);
|
|
2518
|
+
});
|
|
2519
|
+
writeLine(
|
|
2520
|
+
`Watching ${targetPath} for changes (debounce: ${opts.debounceMs}ms)...`,
|
|
2521
|
+
"\u231A"
|
|
2522
|
+
);
|
|
2523
|
+
}
|
|
2524
|
+
async function performSync(targetPath, space, opts) {
|
|
2525
|
+
const config = getConfig();
|
|
2526
|
+
const endpoint = config.endpoint;
|
|
2527
|
+
const projectRoot = findProjectRoot();
|
|
2528
|
+
if (!projectRoot) {
|
|
2529
|
+
writeLine("No .llmkb/ directory found. Run `llmkb init` first.", "\u2716");
|
|
2530
|
+
process.exit(1);
|
|
2531
|
+
}
|
|
2532
|
+
const spaceConfig = await readSpaceConfig(projectRoot);
|
|
2533
|
+
const activeSpace = space || spaceConfig?.project_space?.[0]?.id;
|
|
2534
|
+
if (!activeSpace) {
|
|
2535
|
+
writeLine(
|
|
2536
|
+
"No space specified. Use --space <id> or configure a project space.",
|
|
2537
|
+
"\u2716"
|
|
2538
|
+
);
|
|
2539
|
+
process.exit(1);
|
|
2540
|
+
}
|
|
2541
|
+
const token = await getToken();
|
|
2542
|
+
if (!token && !opts.dryRun) {
|
|
2543
|
+
writeLine(
|
|
2544
|
+
`No credentials. Run \`llmkb login --project-space <id>\`.`,
|
|
2545
|
+
"\u2716"
|
|
2546
|
+
);
|
|
2547
|
+
process.exit(1);
|
|
2548
|
+
}
|
|
2549
|
+
const startTime = Date.now();
|
|
2550
|
+
const syncState = opts.force ? null : await readSyncState(projectRoot);
|
|
2551
|
+
if (opts.force) {
|
|
2552
|
+
await clearSyncState(projectRoot);
|
|
2553
|
+
}
|
|
2554
|
+
if (opts.dryRun) {
|
|
2555
|
+
const walkResult = await walkFiles({
|
|
2556
|
+
projectDir: projectRoot,
|
|
2557
|
+
targetDir: targetPath
|
|
2558
|
+
});
|
|
2559
|
+
const changes = await Promise.resolve().then(() => (init_sync_state(), sync_state_exports)).then(
|
|
2560
|
+
(m) => m.getChangedFiles(walkResult.files, syncState)
|
|
2561
|
+
);
|
|
2562
|
+
if (opts.json) {
|
|
2563
|
+
writeJson({
|
|
2564
|
+
command: "sync",
|
|
2565
|
+
success: true,
|
|
2566
|
+
data: {
|
|
2567
|
+
space: activeSpace,
|
|
2568
|
+
dryRun: true,
|
|
2569
|
+
newFiles: changes.newFiles,
|
|
2570
|
+
changedFiles: changes.changedFiles,
|
|
2571
|
+
unchangedFiles: changes.unchangedFiles,
|
|
2572
|
+
renamed: changes.renamed,
|
|
2573
|
+
skipped: walkResult.skipped.map((s) => ({
|
|
2574
|
+
path: s.path,
|
|
2575
|
+
reason: s.reason
|
|
2576
|
+
}))
|
|
2577
|
+
},
|
|
2578
|
+
meta: {
|
|
2579
|
+
durationMs: Date.now() - startTime,
|
|
2580
|
+
totalItems: walkResult.files.length,
|
|
2581
|
+
processedItems: changes.newFiles.length + changes.changedFiles.length,
|
|
2582
|
+
skippedItems: walkResult.skipped.length,
|
|
2583
|
+
renamedItems: changes.renamed.length
|
|
2584
|
+
}
|
|
2585
|
+
});
|
|
2586
|
+
} else {
|
|
2587
|
+
writeLine(`Dry-run for space "${activeSpace}":`, "\u2139");
|
|
2588
|
+
for (const f of changes.newFiles) writeDetail(`[new] ${f}`);
|
|
2589
|
+
for (const f of changes.changedFiles) writeDetail(`[changed] ${f}`);
|
|
2590
|
+
for (const f of changes.unchangedFiles) writeDetail(`[unchanged] ${f}`);
|
|
2591
|
+
for (const r of changes.renamed)
|
|
2592
|
+
writeDetail(`[rename] ${r.oldPath} \u2192 ${r.newPath}`);
|
|
2593
|
+
for (const s of walkResult.skipped)
|
|
2594
|
+
writeDetail(`[skip] ${s.path} (${s.reason})`);
|
|
2595
|
+
writeLine(
|
|
2596
|
+
`${changes.newFiles.length} new, ${changes.changedFiles.length} changed, ${changes.unchangedFiles.length} unchanged, ${changes.renamed.length} renamed, ${walkResult.skipped.length} skipped`
|
|
2597
|
+
);
|
|
2598
|
+
}
|
|
2599
|
+
return;
|
|
2600
|
+
}
|
|
2601
|
+
const filesResult = await walkFiles({
|
|
2602
|
+
projectDir: projectRoot,
|
|
2603
|
+
targetDir: targetPath
|
|
2604
|
+
});
|
|
2605
|
+
const totalFiles = filesResult.files.length;
|
|
2606
|
+
const bar = isTTY() && !opts.json ? new ProgressBar({ total: totalFiles || 1, label: "Syncing" }) : null;
|
|
2607
|
+
const result = await runSync(activeSpace, syncState, {
|
|
2608
|
+
path: targetPath,
|
|
2609
|
+
onFile: (_relPath, _status) => {
|
|
2610
|
+
bar?.increment();
|
|
2611
|
+
}
|
|
2612
|
+
});
|
|
2613
|
+
bar?.stop();
|
|
2614
|
+
if (result.uploaded === 0 && result.errors.length === 0) {
|
|
2615
|
+
if (opts.json) {
|
|
2616
|
+
writeJson({
|
|
2617
|
+
command: "sync",
|
|
2618
|
+
success: true,
|
|
2619
|
+
data: result,
|
|
2620
|
+
meta: { durationMs: Date.now() - startTime }
|
|
2621
|
+
});
|
|
2622
|
+
} else {
|
|
2623
|
+
const { status, reason, unchanged, renamed, skipped } = result;
|
|
2624
|
+
if (status === "no_files") {
|
|
2625
|
+
writeWarning(reason ?? "No files to sync.");
|
|
2626
|
+
} else if (unchanged === 0 && renamed === 0 && skipped === 0) {
|
|
2627
|
+
writeWarning("No files found in sync target. The directory appears to be empty.");
|
|
2628
|
+
} else {
|
|
2629
|
+
writeLine(
|
|
2630
|
+
`No changes detected \u2014 all files up to date (${unchanged} unchanged, ${renamed} renamed, ${skipped} skipped).`,
|
|
2631
|
+
"\u2714"
|
|
2632
|
+
);
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
return;
|
|
2636
|
+
}
|
|
2637
|
+
const newState = {
|
|
2638
|
+
space: activeSpace,
|
|
2639
|
+
lastSyncAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2640
|
+
files: {}
|
|
2641
|
+
};
|
|
2642
|
+
for (const [relPath, absPath] of filesResult.files) {
|
|
2643
|
+
try {
|
|
2644
|
+
const hash = await Promise.resolve().then(() => (init_sync_state(), sync_state_exports)).then(
|
|
2645
|
+
(m) => m.computeSha256(absPath)
|
|
2646
|
+
);
|
|
2647
|
+
newState.files[relPath] = {
|
|
2648
|
+
sha256: hash,
|
|
2649
|
+
lastUploaded: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2650
|
+
status: "synced"
|
|
2651
|
+
};
|
|
2652
|
+
} catch {
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
await writeSyncState(projectRoot, newState);
|
|
2656
|
+
if (token) {
|
|
2657
|
+
triggerDedup(endpoint, activeSpace, token).catch((err) => {
|
|
2658
|
+
if (opts.json) {
|
|
2659
|
+
writeJson({
|
|
2660
|
+
command: "dedup",
|
|
2661
|
+
success: false,
|
|
2662
|
+
data: null,
|
|
2663
|
+
error: err.message
|
|
2664
|
+
});
|
|
2665
|
+
} else if (opts.verbose) {
|
|
2666
|
+
writeDetail(`Dedup skipped: ${err.message}`);
|
|
2667
|
+
}
|
|
2668
|
+
});
|
|
2669
|
+
}
|
|
2670
|
+
if (projectRoot && token) {
|
|
2671
|
+
const syncSpacesResult = await syncRelatedSpaces(projectRoot, endpoint, token).catch(() => null);
|
|
2672
|
+
if (opts.verbose && syncSpacesResult && syncSpacesResult.added > 0) {
|
|
2673
|
+
writeDetail(`Related spaces: ${syncSpacesResult.added} new space(s) synced to spaces.yml`);
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
const duration = Date.now() - startTime;
|
|
2677
|
+
if (opts.json) {
|
|
2678
|
+
writeJson({
|
|
2679
|
+
command: "sync",
|
|
2680
|
+
success: result.errors.length === 0,
|
|
2681
|
+
data: result,
|
|
2682
|
+
meta: {
|
|
2683
|
+
durationMs: duration,
|
|
2684
|
+
totalItems: result.uploaded + result.unchanged + result.skipped + result.renamed + result.errors.length,
|
|
2685
|
+
processedItems: result.uploaded,
|
|
2686
|
+
skippedItems: result.skipped,
|
|
2687
|
+
renamedItems: result.renamed
|
|
2688
|
+
}
|
|
2689
|
+
});
|
|
2690
|
+
} else {
|
|
2691
|
+
writeLine(
|
|
2692
|
+
`Sync complete: ${result.uploaded} uploaded, ${result.unchanged} unchanged, ${result.skipped} skipped, ${result.renamed} renamed, ${result.errors.length} errors (${duration}ms)`,
|
|
2693
|
+
"\u2714"
|
|
2694
|
+
);
|
|
2695
|
+
}
|
|
2696
|
+
if (result.ingestionJobUrl && !opts.json) {
|
|
2697
|
+
writeLine(`Ingestion job: ${result.ingestionJobUrl}`, "\u2139");
|
|
2698
|
+
}
|
|
2699
|
+
if (result.errors.length > 0 && !opts.json) {
|
|
2700
|
+
for (const e of result.errors) {
|
|
2701
|
+
writeDetail(`\u2716 ${e.path}: ${e.error}`);
|
|
2702
|
+
}
|
|
2703
|
+
process.exitCode = 1;
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
async function triggerDedup(endpoint, space, token) {
|
|
2707
|
+
const url = `${endpoint}/api/spaces/${encodeURIComponent(space)}/dedup`;
|
|
2708
|
+
const res = await fetch(url, {
|
|
2709
|
+
method: "POST",
|
|
2710
|
+
headers: {
|
|
2711
|
+
Authorization: `Bearer ${token}`,
|
|
2712
|
+
"Content-Type": "application/json"
|
|
2713
|
+
}
|
|
2714
|
+
});
|
|
2715
|
+
if (!res.ok) {
|
|
2716
|
+
const body = await res.text().catch(() => "unknown");
|
|
2717
|
+
throw new Error(`Dedup failed (${res.status}): ${body}`);
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
var syncCommand = new Command12("sync").description("Sync local files to an llmkb space").argument("[path]", "Directory or file to sync", ".").option("-s, --space <id>", "Space UUID (overrides project_space)").option("-n, --dry-run", "Show what would be uploaded without uploading").option("-f, --force", "Re-upload everything, bypassing cache").option("-w, --watch", "Watch for file changes and auto-sync").option("--debounce <ms>", "Watch debounce in ms", "300").option("--json", "Output JSON for machine parsing").option("--verbose", "Detailed output including skipped files").option(
|
|
2721
|
+
"--debug",
|
|
2722
|
+
"Show all events including ignored files with matching rule"
|
|
2723
|
+
).action(
|
|
2724
|
+
async (path, opts) => {
|
|
2725
|
+
try {
|
|
2726
|
+
const resolvedSpace = opts.space ?? "";
|
|
2727
|
+
if (opts.watch) {
|
|
2728
|
+
await startWatch(path, resolvedSpace, {
|
|
2729
|
+
debounceMs: parseInt(opts.debounce, 10) || 300,
|
|
2730
|
+
json: opts.json ?? false,
|
|
2731
|
+
verbose: opts.verbose ?? false,
|
|
2732
|
+
debug: opts.debug ?? false
|
|
2733
|
+
});
|
|
2734
|
+
return;
|
|
2735
|
+
}
|
|
2736
|
+
await performSync(path, resolvedSpace, {
|
|
2737
|
+
dryRun: opts.dryRun,
|
|
2738
|
+
force: opts.force,
|
|
2739
|
+
json: opts.json,
|
|
2740
|
+
verbose: opts.verbose
|
|
2741
|
+
});
|
|
2742
|
+
} catch (err) {
|
|
2743
|
+
if (opts.json) {
|
|
2744
|
+
writeJson({
|
|
2745
|
+
command: "sync",
|
|
2746
|
+
success: false,
|
|
2747
|
+
data: null,
|
|
2748
|
+
error: err.message
|
|
2749
|
+
});
|
|
2750
|
+
} else {
|
|
2751
|
+
writeLine(`Error: ${err.message}`, "\u2716");
|
|
2752
|
+
}
|
|
2753
|
+
process.exit(1);
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
);
|
|
2757
|
+
|
|
2758
|
+
// src/commands/query.ts
|
|
2759
|
+
import { Command as Command13 } from "commander";
|
|
2760
|
+
init_parser();
|
|
2761
|
+
async function search(space, query, endpoint, token, limit) {
|
|
2762
|
+
const url = `${endpoint}/api/v1/${space}/search?q=${encodeURIComponent(query)}&limit=${limit}`;
|
|
2763
|
+
const res = await fetch(url, {
|
|
2764
|
+
headers: {
|
|
2765
|
+
Authorization: `Bearer ${token}`,
|
|
2766
|
+
Accept: "application/json"
|
|
2767
|
+
}
|
|
2768
|
+
});
|
|
2769
|
+
if (!res.ok) {
|
|
2770
|
+
const body = await res.text().catch(() => "");
|
|
2771
|
+
throw new Error(`Search failed (${res.status}): ${body}`);
|
|
2772
|
+
}
|
|
2773
|
+
return await res.json();
|
|
2774
|
+
}
|
|
2775
|
+
var queryCommand = new Command13("query").description("Search an llmkb space and print ranked results").argument("<text>", "Search query text").option("-s, --space <id>", "Space UUID (overrides project_space)").option("-n, --limit <count>", "Max results", "10").option("--json", "Output JSON for machine parsing").action(async (text, opts) => {
|
|
2776
|
+
const startTime = Date.now();
|
|
2777
|
+
const config = getConfig();
|
|
2778
|
+
try {
|
|
2779
|
+
const projectRoot = findProjectRoot();
|
|
2780
|
+
let endpoint = config.endpoint;
|
|
2781
|
+
let token = null;
|
|
2782
|
+
let activeSpace = opts.space;
|
|
2783
|
+
if (projectRoot) {
|
|
2784
|
+
const spaceConfig = await readSpaceConfig(projectRoot);
|
|
2785
|
+
if (spaceConfig) {
|
|
2786
|
+
activeSpace = activeSpace ?? spaceConfig.project_space?.[0]?.id;
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
token = await getToken();
|
|
2790
|
+
if (!token) {
|
|
2791
|
+
throw new Error(
|
|
2792
|
+
"No access token found. Set LLMKB_ACCESS_TOKEN env var or run `llmkb login --project-space <id>`."
|
|
2793
|
+
);
|
|
2794
|
+
}
|
|
2795
|
+
const limit = parseInt(opts.limit, 10) || 10;
|
|
2796
|
+
const result = await search(activeSpace ?? "", text, endpoint, token, limit);
|
|
2797
|
+
if (opts.json) {
|
|
2798
|
+
writeJson({
|
|
2799
|
+
command: "query",
|
|
2800
|
+
success: true,
|
|
2801
|
+
data: result.data,
|
|
2802
|
+
meta: {
|
|
2803
|
+
durationMs: Date.now() - startTime,
|
|
2804
|
+
totalItems: result.meta.total,
|
|
2805
|
+
processedItems: result.data.length
|
|
2806
|
+
}
|
|
2807
|
+
});
|
|
2808
|
+
} else {
|
|
2809
|
+
writeLine(`Search results for "${text}" in "${activeSpace}" (${result.meta.total} total):`, "\u2139");
|
|
2810
|
+
for (const r of result.data) {
|
|
2811
|
+
writeLine(`[${r.score.toFixed(2)}] ${r.title}`, "\u25CF");
|
|
2812
|
+
writeDetail(`${r.path} (${r.matchType})`);
|
|
2813
|
+
if (r.excerpt) writeDetail(r.excerpt);
|
|
2814
|
+
}
|
|
2815
|
+
writeDetail(`Completed in ${Date.now() - startTime}ms`);
|
|
2816
|
+
}
|
|
2817
|
+
} catch (err) {
|
|
2818
|
+
if (opts.json) {
|
|
2819
|
+
writeJson({
|
|
2820
|
+
command: "query",
|
|
2821
|
+
success: false,
|
|
2822
|
+
data: null,
|
|
2823
|
+
error: err.message,
|
|
2824
|
+
meta: { durationMs: Date.now() - startTime }
|
|
2825
|
+
});
|
|
2826
|
+
} else {
|
|
2827
|
+
writeLine(`Error: ${err.message}`, "\u2716");
|
|
2828
|
+
}
|
|
2829
|
+
process.exit(1);
|
|
2830
|
+
}
|
|
2831
|
+
});
|
|
2832
|
+
|
|
2833
|
+
// src/commands/doctor.ts
|
|
2834
|
+
import { Command as Command14 } from "commander";
|
|
2835
|
+
init_parser();
|
|
2836
|
+
|
|
2837
|
+
// lib/config-validation.ts
|
|
2838
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
2839
|
+
import { existsSync as existsSync10 } from "fs";
|
|
2840
|
+
import { join as join12 } from "path";
|
|
2841
|
+
import { parse as parse2 } from "yaml";
|
|
2842
|
+
function validateBaseUrl(url) {
|
|
2843
|
+
try {
|
|
2844
|
+
const parsed = new URL(url);
|
|
2845
|
+
if (!parsed.protocol || !parsed.host) {
|
|
2846
|
+
return {
|
|
2847
|
+
name: "Base URL format",
|
|
2848
|
+
passed: false,
|
|
2849
|
+
message: "URL missing scheme or host",
|
|
2850
|
+
detail: `Got: ${url}`
|
|
2851
|
+
};
|
|
2852
|
+
}
|
|
2853
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
2854
|
+
return {
|
|
2855
|
+
name: "Base URL format",
|
|
2856
|
+
passed: false,
|
|
2857
|
+
message: `Unsupported protocol: ${parsed.protocol}`,
|
|
2858
|
+
detail: `Got: ${url}`
|
|
2859
|
+
};
|
|
2860
|
+
}
|
|
2861
|
+
return {
|
|
2862
|
+
name: "Base URL format",
|
|
2863
|
+
passed: true,
|
|
2864
|
+
message: `Valid URL: ${url}`
|
|
2865
|
+
};
|
|
2866
|
+
} catch {
|
|
2867
|
+
return {
|
|
2868
|
+
name: "Base URL format",
|
|
2869
|
+
passed: false,
|
|
2870
|
+
message: "Invalid URL format",
|
|
2871
|
+
detail: `Got: ${url}. Must include scheme (e.g., https://api.llmkb.ai)`
|
|
2872
|
+
};
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
async function validateConfigYml(projectDir) {
|
|
2876
|
+
const filePath = join12(projectDir, ".llmkb", "config.yml");
|
|
2877
|
+
if (!existsSync10(filePath)) {
|
|
2878
|
+
return {
|
|
2879
|
+
name: "config.yml",
|
|
2880
|
+
passed: false,
|
|
2881
|
+
message: "File not found",
|
|
2882
|
+
detail: "Run `llmkb init` to create it."
|
|
2883
|
+
};
|
|
2884
|
+
}
|
|
2885
|
+
try {
|
|
2886
|
+
const content = await readFile7(filePath, "utf-8");
|
|
2887
|
+
const cfg = parse2(content);
|
|
2888
|
+
if (cfg === null) {
|
|
2889
|
+
return {
|
|
2890
|
+
name: "config.yml",
|
|
2891
|
+
passed: true,
|
|
2892
|
+
message: "Valid config file (all defaults)"
|
|
2893
|
+
};
|
|
2894
|
+
}
|
|
2895
|
+
if (cfg.llmkb_base_url !== void 0) {
|
|
2896
|
+
const urlResult = validateBaseUrl(String(cfg.llmkb_base_url));
|
|
2897
|
+
if (!urlResult.passed) {
|
|
2898
|
+
return {
|
|
2899
|
+
name: "config.yml",
|
|
2900
|
+
passed: false,
|
|
2901
|
+
message: "llmkb_base_url has invalid value",
|
|
2902
|
+
detail: urlResult.detail
|
|
2903
|
+
};
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
return {
|
|
2907
|
+
name: "config.yml",
|
|
2908
|
+
passed: true,
|
|
2909
|
+
message: "Valid config file"
|
|
2910
|
+
};
|
|
2911
|
+
} catch (err) {
|
|
2912
|
+
return {
|
|
2913
|
+
name: "config.yml",
|
|
2914
|
+
passed: false,
|
|
2915
|
+
message: "Invalid YAML",
|
|
2916
|
+
detail: err.message
|
|
2917
|
+
};
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
async function validateSpacesYml(projectDir) {
|
|
2921
|
+
const filePath = join12(projectDir, ".llmkb", "spaces.yml");
|
|
2922
|
+
if (!existsSync10(filePath)) {
|
|
2923
|
+
return {
|
|
2924
|
+
name: "spaces.yml",
|
|
2925
|
+
passed: false,
|
|
2926
|
+
message: "File not found",
|
|
2927
|
+
detail: "Run `llmkb init` or `llmkb login` to create it."
|
|
2928
|
+
};
|
|
2929
|
+
}
|
|
2930
|
+
try {
|
|
2931
|
+
const content = await readFile7(filePath, "utf-8");
|
|
2932
|
+
const cfg = parse2(content);
|
|
2933
|
+
if (cfg.project_space && Array.isArray(cfg.project_space)) {
|
|
2934
|
+
for (const ps of cfg.project_space) {
|
|
2935
|
+
if (!ps.id) {
|
|
2936
|
+
return {
|
|
2937
|
+
name: "spaces.yml",
|
|
2938
|
+
passed: false,
|
|
2939
|
+
message: "project_space entry missing required 'id' field"
|
|
2940
|
+
};
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
if (cfg.spaces && Array.isArray(cfg.spaces)) {
|
|
2945
|
+
for (const s of cfg.spaces) {
|
|
2946
|
+
if (!s.id) {
|
|
2947
|
+
return {
|
|
2948
|
+
name: "spaces.yml",
|
|
2949
|
+
passed: false,
|
|
2950
|
+
message: "spaces entry missing required 'id' field"
|
|
2951
|
+
};
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
return {
|
|
2956
|
+
name: "spaces.yml",
|
|
2957
|
+
passed: true,
|
|
2958
|
+
message: `Valid config (${cfg.spaces?.length ?? 0} space(s))`
|
|
2959
|
+
};
|
|
2960
|
+
} catch (err) {
|
|
2961
|
+
return {
|
|
2962
|
+
name: "spaces.yml",
|
|
2963
|
+
passed: false,
|
|
2964
|
+
message: "Invalid YAML",
|
|
2965
|
+
detail: err.message
|
|
2966
|
+
};
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
async function validateAccessToken(endpoint) {
|
|
2970
|
+
const token = await getToken();
|
|
2971
|
+
if (!token) {
|
|
2972
|
+
return {
|
|
2973
|
+
name: "Access token",
|
|
2974
|
+
passed: false,
|
|
2975
|
+
message: "No access token found",
|
|
2976
|
+
detail: "Set LLMKB_ACCESS_TOKEN env var or run `llmkb login --project-space <id>`."
|
|
2977
|
+
};
|
|
2978
|
+
}
|
|
2979
|
+
try {
|
|
2980
|
+
const url = `${endpoint.replace(/\/+$/, "")}/api/v1/auth/me`;
|
|
2981
|
+
const res = await fetch(url, {
|
|
2982
|
+
method: "GET",
|
|
2983
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
2984
|
+
});
|
|
2985
|
+
if (res.ok) {
|
|
2986
|
+
const body = await res.json();
|
|
2987
|
+
const attrs = body.data?.attributes;
|
|
2988
|
+
const status = attrs?.token_status;
|
|
2989
|
+
if (status === "active") {
|
|
2990
|
+
return {
|
|
2991
|
+
name: "Access token",
|
|
2992
|
+
passed: true,
|
|
2993
|
+
message: `Token is valid (user: ${attrs?.email ?? "unknown"})`
|
|
2994
|
+
};
|
|
2995
|
+
}
|
|
2996
|
+
return {
|
|
2997
|
+
name: "Access token",
|
|
2998
|
+
passed: false,
|
|
2999
|
+
message: `Token status: ${status}`,
|
|
3000
|
+
detail: "Generate a new token at /settings/access-tokens."
|
|
3001
|
+
};
|
|
3002
|
+
}
|
|
3003
|
+
return {
|
|
3004
|
+
name: "Access token",
|
|
3005
|
+
passed: false,
|
|
3006
|
+
message: `Server returned ${res.status}`,
|
|
3007
|
+
detail: "Token may be expired or invalid. Run `llmkb login --project-space <id>`."
|
|
3008
|
+
};
|
|
3009
|
+
} catch (err) {
|
|
3010
|
+
return {
|
|
3011
|
+
name: "Access token",
|
|
3012
|
+
passed: false,
|
|
3013
|
+
message: "Cannot validate \u2014 server unreachable",
|
|
3014
|
+
detail: err.message
|
|
3015
|
+
};
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
async function checkBackendConnectivity(endpoint) {
|
|
3019
|
+
try {
|
|
3020
|
+
const url = `${endpoint.replace(/\/+$/, "")}/health`;
|
|
3021
|
+
const res = await fetch(url);
|
|
3022
|
+
if (res.ok) {
|
|
3023
|
+
return {
|
|
3024
|
+
name: "Backend connectivity",
|
|
3025
|
+
passed: true,
|
|
3026
|
+
message: `Server reachable at ${endpoint}`
|
|
3027
|
+
};
|
|
3028
|
+
}
|
|
3029
|
+
return {
|
|
3030
|
+
name: "Backend connectivity",
|
|
3031
|
+
passed: false,
|
|
3032
|
+
message: `Server returned ${res.status}`,
|
|
3033
|
+
detail: "Check that the server is running."
|
|
3034
|
+
};
|
|
3035
|
+
} catch (err) {
|
|
3036
|
+
return {
|
|
3037
|
+
name: "Backend connectivity",
|
|
3038
|
+
passed: false,
|
|
3039
|
+
message: "Server unreachable",
|
|
3040
|
+
detail: err.message
|
|
3041
|
+
};
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
var WRITE_ROLES = /* @__PURE__ */ new Set(["owner", "admin"]);
|
|
3045
|
+
async function checkSpaceAccess(endpoint, spaceId, token, checkWrite) {
|
|
3046
|
+
try {
|
|
3047
|
+
const url = `${endpoint.replace(/\/+$/, "")}/api/v1/auth/spaces`;
|
|
3048
|
+
const res = await fetch(url, {
|
|
3049
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
3050
|
+
});
|
|
3051
|
+
if (res.ok) {
|
|
3052
|
+
const body = await res.json();
|
|
3053
|
+
const space = body.data?.find((d) => d.attributes?.space_id === spaceId);
|
|
3054
|
+
if (space) {
|
|
3055
|
+
const role = space.attributes.role ?? "guest";
|
|
3056
|
+
if (checkWrite && !WRITE_ROLES.has(role)) {
|
|
3057
|
+
return {
|
|
3058
|
+
name: `Space access (${spaceId})`,
|
|
3059
|
+
passed: false,
|
|
3060
|
+
message: `Access granted (role: ${role}) but no write permission`,
|
|
3061
|
+
detail: `Role "${role}" is read-only. Need owner or admin to sync. Ask the space owner to upgrade your role.`
|
|
3062
|
+
};
|
|
3063
|
+
}
|
|
3064
|
+
return {
|
|
3065
|
+
name: `Space access (${spaceId})`,
|
|
3066
|
+
passed: true,
|
|
3067
|
+
message: `Access granted (role: ${role})${checkWrite ? " \u2014 write permission OK" : ""}`
|
|
3068
|
+
};
|
|
3069
|
+
}
|
|
3070
|
+
return {
|
|
3071
|
+
name: `Space access (${spaceId})`,
|
|
3072
|
+
passed: false,
|
|
3073
|
+
message: "No access to this space",
|
|
3074
|
+
detail: "Ask the space owner to invite you."
|
|
3075
|
+
};
|
|
3076
|
+
}
|
|
3077
|
+
return {
|
|
3078
|
+
name: `Space access (${spaceId})`,
|
|
3079
|
+
passed: false,
|
|
3080
|
+
message: `Server returned ${res.status}`
|
|
3081
|
+
};
|
|
3082
|
+
} catch (err) {
|
|
3083
|
+
return {
|
|
3084
|
+
name: `Space access (${spaceId})`,
|
|
3085
|
+
passed: false,
|
|
3086
|
+
message: "Cannot check \u2014 server unreachable",
|
|
3087
|
+
detail: err.message
|
|
3088
|
+
};
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
// src/commands/doctor.ts
|
|
3093
|
+
async function report(label2, result) {
|
|
3094
|
+
const icon = result.passed ? success("\u2714") : error("\u2716");
|
|
3095
|
+
const msg = result.passed ? success(result.message) : error(result.message);
|
|
3096
|
+
console.log(` ${icon} ${label2}: ${msg}`);
|
|
3097
|
+
if (result.detail) {
|
|
3098
|
+
console.log(` ${muted(result.detail)}`);
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
function spaceNameMap(config) {
|
|
3102
|
+
const map = {};
|
|
3103
|
+
for (const ps of config.project_space ?? []) {
|
|
3104
|
+
if (ps.name) map[ps.id] = ps.name;
|
|
3105
|
+
}
|
|
3106
|
+
for (const s of config.spaces ?? []) {
|
|
3107
|
+
if (s.name) map[s.id] = s.name;
|
|
3108
|
+
}
|
|
3109
|
+
return map;
|
|
3110
|
+
}
|
|
3111
|
+
function formatSpaceId(spaceId, nameMap) {
|
|
3112
|
+
const name = nameMap[spaceId];
|
|
3113
|
+
return name ? `${spaceId} (${info(name)})` : muted(spaceId);
|
|
3114
|
+
}
|
|
3115
|
+
var doctorCommand = new Command14("doctor").description("Diagnose configuration and permission issues").action(async () => {
|
|
3116
|
+
const config = getConfig();
|
|
3117
|
+
const endpoint = config.endpoint;
|
|
3118
|
+
console.log(`${info("llmkb doctor")} ${muted("\u2014 diagnostic report")}
|
|
3119
|
+
`);
|
|
3120
|
+
const projectRoot = findProjectRoot();
|
|
3121
|
+
if (!projectRoot) {
|
|
3122
|
+
console.log(` ${error("\u2716 Project root: No .llmkb/ directory found.")}`);
|
|
3123
|
+
console.log(` ${info("Run `llmkb init` to create one.")}`);
|
|
3124
|
+
return;
|
|
3125
|
+
}
|
|
3126
|
+
console.log(` ${success("\u2714")} ${muted("Project root:")} ${projectRoot}
|
|
3127
|
+
`);
|
|
3128
|
+
console.log(muted("1. Config file checks"));
|
|
3129
|
+
const configResult = await validateConfigYml(projectRoot);
|
|
3130
|
+
await report("config.yml", configResult);
|
|
3131
|
+
const spaceConfig = await readSpaceConfig(projectRoot);
|
|
3132
|
+
const spacesResult = await validateSpacesYml(projectRoot);
|
|
3133
|
+
await report("spaces.yml", spacesResult);
|
|
3134
|
+
console.log(`
|
|
3135
|
+
${muted("2. Backend connectivity")}`);
|
|
3136
|
+
const connectivityResult = await checkBackendConnectivity(endpoint);
|
|
3137
|
+
await report("Backend", connectivityResult);
|
|
3138
|
+
console.log(`
|
|
3139
|
+
${muted("3. Authentication")}`);
|
|
3140
|
+
const tokenResult = await validateAccessToken(endpoint);
|
|
3141
|
+
await report("Access token", tokenResult);
|
|
3142
|
+
if (tokenResult.passed && projectRoot && spaceConfig?.project_space?.length) {
|
|
3143
|
+
const token = await getToken();
|
|
3144
|
+
if (token) {
|
|
3145
|
+
const syncResult = await syncRelatedSpaces(projectRoot, endpoint, token);
|
|
3146
|
+
const parts = [];
|
|
3147
|
+
if (syncResult.added > 0) parts.push(`added ${syncResult.added}`);
|
|
3148
|
+
if (syncResult.removed > 0) parts.push(`removed ${syncResult.removed}`);
|
|
3149
|
+
if (syncResult.alreadyPresent > 0) parts.push(`${syncResult.alreadyPresent} already configured`);
|
|
3150
|
+
if (parts.length > 0) {
|
|
3151
|
+
console.log(` ${info(`\u2139 Related spaces synced: ${parts.join(", ")}.`)}`);
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
console.log(`
|
|
3156
|
+
${muted("4. Space access")}`);
|
|
3157
|
+
if (tokenResult.passed) {
|
|
3158
|
+
const token = await getToken();
|
|
3159
|
+
const nameMap = spaceConfig ? spaceNameMap(spaceConfig) : {};
|
|
3160
|
+
const allSpaces = [];
|
|
3161
|
+
if (spaceConfig?.project_space) {
|
|
3162
|
+
for (const ps of spaceConfig.project_space) {
|
|
3163
|
+
allSpaces.push({ id: ps.id, isProject: true });
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
if (spaceConfig?.spaces) {
|
|
3167
|
+
for (const s of spaceConfig.spaces) {
|
|
3168
|
+
if (!allSpaces.find((e) => e.id === s.id)) {
|
|
3169
|
+
allSpaces.push({ id: s.id, isProject: false });
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
if (allSpaces.length > 0) {
|
|
3174
|
+
for (const entry of allSpaces) {
|
|
3175
|
+
const accessResult = await checkSpaceAccess(endpoint, entry.id, token, entry.isProject);
|
|
3176
|
+
const label2 = entry.isProject ? `${highlight("\u2605 PROJECT")} ${formatSpaceId(entry.id, nameMap)}` : formatSpaceId(entry.id, nameMap);
|
|
3177
|
+
await report(label2, accessResult);
|
|
3178
|
+
}
|
|
3179
|
+
} else {
|
|
3180
|
+
console.log(` ${info("\u2139 No spaces configured. Run `llmkb add --space <id>`.")}`);
|
|
3181
|
+
}
|
|
3182
|
+
} else {
|
|
3183
|
+
console.log(` ${warning("\u26A0 Skipping space access checks \u2014 not authenticated.")}`);
|
|
3184
|
+
}
|
|
3185
|
+
console.log(`
|
|
3186
|
+
${success("Diagnostic complete.")}`);
|
|
3187
|
+
});
|
|
3188
|
+
|
|
3189
|
+
// bin/cli.ts
|
|
3190
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
3191
|
+
var __dirname = dirname2(__filename);
|
|
3192
|
+
var pkg = JSON.parse(
|
|
3193
|
+
readFileSync(resolve7(__dirname, "../package.json"), "utf-8")
|
|
3194
|
+
);
|
|
3195
|
+
var program = new Command15();
|
|
3196
|
+
program.name("llmkb").description(
|
|
3197
|
+
"llmkb Claude Code plugin \u2014 manage spaces, sync code, and query your knowledge base"
|
|
3198
|
+
).version(pkg.version);
|
|
3199
|
+
program.addCommand(initCommand);
|
|
3200
|
+
program.addCommand(loginCommand);
|
|
3201
|
+
program.addCommand(logoutCommand);
|
|
3202
|
+
program.addCommand(addCommand);
|
|
3203
|
+
program.addCommand(removeCommand);
|
|
3204
|
+
program.addCommand(spacesCommand);
|
|
3205
|
+
program.addCommand(updateCommand);
|
|
3206
|
+
program.addCommand(whoamiCommand);
|
|
3207
|
+
program.addCommand(useCommand);
|
|
3208
|
+
program.addCommand(statusCommand);
|
|
3209
|
+
program.addCommand(syncCommand);
|
|
3210
|
+
program.addCommand(queryCommand);
|
|
3211
|
+
program.addCommand(doctorCommand);
|
|
3212
|
+
program.addCommand(hooksCommand);
|
|
3213
|
+
program.parse();
|
|
3214
|
+
//# sourceMappingURL=cli.js.map
|