@williamthorsen/kb 0.0.0 → 0.3.1
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/LICENSE +7 -0
- package/README.md +228 -1
- package/bin/kb.js +26 -0
- package/dist/esm/check/check.d.ts +11 -0
- package/dist/esm/check/check.js +17 -0
- package/dist/esm/check/enumerate.d.ts +14 -0
- package/dist/esm/check/enumerate.js +71 -0
- package/dist/esm/check/glob-segments.d.ts +2 -0
- package/dist/esm/check/glob-segments.js +12 -0
- package/dist/esm/check/index.d.ts +2 -0
- package/dist/esm/check/index.js +2 -0
- package/dist/esm/cli/commands/check.d.ts +20 -0
- package/dist/esm/cli/commands/check.js +179 -0
- package/dist/esm/cli/commands/create.d.ts +16 -0
- package/dist/esm/cli/commands/create.js +111 -0
- package/dist/esm/cli/commands/set-default.d.ts +15 -0
- package/dist/esm/cli/commands/set-default.js +123 -0
- package/dist/esm/cli/format.d.ts +24 -0
- package/dist/esm/cli/format.js +55 -0
- package/dist/esm/cli/index.d.ts +1 -0
- package/dist/esm/cli/index.js +25 -0
- package/dist/esm/cli/run.d.ts +9 -0
- package/dist/esm/cli/run.js +37 -0
- package/dist/esm/cli/select-kb-prompt.d.ts +16 -0
- package/dist/esm/cli/select-kb-prompt.js +44 -0
- package/dist/esm/cli/targeting/resolve-changed-paths.d.ts +11 -0
- package/dist/esm/cli/targeting/resolve-changed-paths.js +53 -0
- package/dist/esm/cli/targeting/select-notes.d.ts +10 -0
- package/dist/esm/cli/targeting/select-notes.js +60 -0
- package/dist/esm/config/config-schema.d.ts +10 -0
- package/dist/esm/config/config-schema.js +10 -0
- package/dist/esm/config/index.d.ts +4 -0
- package/dist/esm/config/index.js +4 -0
- package/dist/esm/config/kb-loader-error.d.ts +5 -0
- package/dist/esm/config/kb-loader-error.js +14 -0
- package/dist/esm/config/load-config.d.ts +5 -0
- package/dist/esm/config/load-config.js +36 -0
- package/dist/esm/config/note-scope.d.ts +7 -0
- package/dist/esm/config/note-scope.js +10 -0
- package/dist/esm/create/create.d.ts +26 -0
- package/dist/esm/create/create.js +51 -0
- package/dist/esm/create/index.d.ts +3 -0
- package/dist/esm/create/index.js +2 -0
- package/dist/esm/create/render-seeds.d.ts +2 -0
- package/dist/esm/create/render-seeds.js +29 -0
- package/dist/esm/discovery/find-kb-root.d.ts +4 -0
- package/dist/esm/discovery/find-kb-root.js +22 -0
- package/dist/esm/discovery/index.d.ts +9 -0
- package/dist/esm/discovery/index.js +5 -0
- package/dist/esm/discovery/kb-registry-schema.d.ts +16 -0
- package/dist/esm/discovery/kb-registry-schema.js +10 -0
- package/dist/esm/discovery/load-registry.d.ts +15 -0
- package/dist/esm/discovery/load-registry.js +114 -0
- package/dist/esm/discovery/register-store.d.ts +9 -0
- package/dist/esm/discovery/register-store.js +26 -0
- package/dist/esm/discovery/registry-document.d.ts +2 -0
- package/dist/esm/discovery/registry-document.js +17 -0
- package/dist/esm/discovery/set-default-kb.d.ts +7 -0
- package/dist/esm/discovery/set-default-kb.js +53 -0
- package/dist/esm/filesystem/exists.d.ts +6 -0
- package/dist/esm/filesystem/exists.js +24 -0
- package/dist/esm/filesystem/index.d.ts +1 -0
- package/dist/esm/filesystem/index.js +1 -0
- package/dist/esm/frontmatter/index.d.ts +2 -0
- package/dist/esm/frontmatter/index.js +1 -0
- package/dist/esm/frontmatter/parse-note.d.ts +8 -0
- package/dist/esm/frontmatter/parse-note.js +98 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/layout/index.d.ts +1 -0
- package/dist/esm/layout/index.js +1 -0
- package/dist/esm/layout/store-layout.d.ts +15 -0
- package/dist/esm/layout/store-layout.js +23 -0
- package/dist/esm/lints/index.d.ts +2 -0
- package/dist/esm/lints/index.js +2 -0
- package/dist/esm/lints/paths.d.ts +6 -0
- package/dist/esm/lints/paths.js +12 -0
- package/dist/esm/lints/tag-alias.d.ts +6 -0
- package/dist/esm/lints/tag-alias.js +21 -0
- package/dist/esm/note-io/field-validators.d.ts +2 -0
- package/dist/esm/note-io/field-validators.js +30 -0
- package/dist/esm/note-io/index.d.ts +4 -0
- package/dist/esm/note-io/index.js +4 -0
- package/dist/esm/note-io/read-note.d.ts +8 -0
- package/dist/esm/note-io/read-note.js +22 -0
- package/dist/esm/note-io/write-note.d.ts +2 -0
- package/dist/esm/note-io/write-note.js +21 -0
- package/dist/esm/note-io/yaml-fields.d.ts +5 -0
- package/dist/esm/note-io/yaml-fields.js +49 -0
- package/dist/esm/records/assertion.d.ts +25 -0
- package/dist/esm/records/assertion.js +136 -0
- package/dist/esm/records/event.d.ts +28 -0
- package/dist/esm/records/event.js +128 -0
- package/dist/esm/records/index.d.ts +2 -0
- package/dist/esm/records/index.js +2 -0
- package/dist/esm/tags/canonicalize.d.ts +3 -0
- package/dist/esm/tags/canonicalize.js +6 -0
- package/dist/esm/tags/index.d.ts +3 -0
- package/dist/esm/tags/index.js +2 -0
- package/dist/esm/tags/load-aliases.d.ts +5 -0
- package/dist/esm/tags/load-aliases.js +72 -0
- package/dist/esm/type-guards.d.ts +3 -0
- package/dist/esm/type-guards.js +12 -0
- package/dist/esm/types.d.ts +50 -0
- package/dist/esm/types.js +1 -0
- package/dist/esm/vault-integrity/build-vault-index.d.ts +4 -0
- package/dist/esm/vault-integrity/build-vault-index.js +14 -0
- package/dist/esm/vault-integrity/check-vault-integrity.d.ts +7 -0
- package/dist/esm/vault-integrity/check-vault-integrity.js +49 -0
- package/dist/esm/vault-integrity/index.d.ts +3 -0
- package/dist/esm/vault-integrity/index.js +3 -0
- package/dist/esm/vault-integrity/wikilink-parse.d.ts +7 -0
- package/dist/esm/vault-integrity/wikilink-parse.js +75 -0
- package/package.json +108 -2
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { parse } from 'yaml';
|
|
4
|
+
import { CONFIG_FILE } from "../layout/index.js";
|
|
5
|
+
import { isEnoent } from "../type-guards.js";
|
|
6
|
+
import { configFileShape, defaultKbConfig } from "./config-schema.js";
|
|
7
|
+
import { KbLoaderError } from "./kb-loader-error.js";
|
|
8
|
+
export async function loadKbConfig(input) {
|
|
9
|
+
const path = join(input.kbRoot.path, CONFIG_FILE);
|
|
10
|
+
let text;
|
|
11
|
+
try {
|
|
12
|
+
text = await readFile(path, 'utf8');
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (isEnoent(error)) {
|
|
16
|
+
return defaultKbConfig;
|
|
17
|
+
}
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
let parsed;
|
|
21
|
+
try {
|
|
22
|
+
parsed = parse(text);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
26
|
+
throw new KbLoaderError(`${path}: malformed YAML — ${message}`);
|
|
27
|
+
}
|
|
28
|
+
const result = configFileShape.safeParse(parsed ?? {});
|
|
29
|
+
if (!result.success) {
|
|
30
|
+
throw new KbLoaderError(`${path}: invalid config.yaml — ${result.error.issues[0]?.message ?? 'unknown error'}`);
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
targets: result.data.targets ?? defaultKbConfig.targets,
|
|
34
|
+
exclude: result.data.exclude ?? defaultKbConfig.exclude,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { KbConfig } from './config-schema.js';
|
|
2
|
+
export interface NoteScopeMatcher {
|
|
3
|
+
isTarget(relativePath: string): boolean;
|
|
4
|
+
isExcluded(relativePath: string): boolean;
|
|
5
|
+
isNote(relativePath: string): boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function createNoteScopeMatcher(config: KbConfig): NoteScopeMatcher;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import picomatch from 'picomatch';
|
|
2
|
+
export function createNoteScopeMatcher(config) {
|
|
3
|
+
const isTarget = picomatch([...config.targets], { dot: false });
|
|
4
|
+
const isExcluded = picomatch([...config.exclude], { dot: false });
|
|
5
|
+
return {
|
|
6
|
+
isTarget,
|
|
7
|
+
isExcluded,
|
|
8
|
+
isNote: (relativePath) => isTarget(relativePath) && !isExcluded(relativePath),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type DefaultKbOutcome = 'set' | 'unchanged' | 'needs-selection';
|
|
2
|
+
export interface CreatedStore {
|
|
3
|
+
name: string;
|
|
4
|
+
storePath: string;
|
|
5
|
+
registered: boolean;
|
|
6
|
+
created: readonly string[];
|
|
7
|
+
defaultKb?: DefaultKbOutcome;
|
|
8
|
+
}
|
|
9
|
+
export type CreateInput = {
|
|
10
|
+
targetDir: string;
|
|
11
|
+
name?: string;
|
|
12
|
+
} & ({
|
|
13
|
+
register: false;
|
|
14
|
+
} | {
|
|
15
|
+
register: true;
|
|
16
|
+
registryPath: string;
|
|
17
|
+
});
|
|
18
|
+
export type CreateOutcome = {
|
|
19
|
+
ok: true;
|
|
20
|
+
created: CreatedStore;
|
|
21
|
+
} | {
|
|
22
|
+
ok: false;
|
|
23
|
+
reason: 'kb-exists' | 'name-registered';
|
|
24
|
+
message: string;
|
|
25
|
+
};
|
|
26
|
+
export declare function create(input: CreateInput): Promise<CreateOutcome>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { basename, join, resolve } from 'node:path';
|
|
3
|
+
import { loadKbRegistry } from "../discovery/load-registry.js";
|
|
4
|
+
import { registerStore } from "../discovery/register-store.js";
|
|
5
|
+
import { setDefaultKb } from "../discovery/set-default-kb.js";
|
|
6
|
+
import { pathExists } from "../filesystem/exists.js";
|
|
7
|
+
import { ALIASES_FILE, CONFIG_FILE, CONTENT_DIR, EVENTS_DIR, KB_DIR, resolveEventsDir, resolveKbDir, } from "../layout/index.js";
|
|
8
|
+
import { renderAliasesSeed, renderConfigSeed } from "./render-seeds.js";
|
|
9
|
+
export async function create(input) {
|
|
10
|
+
const storePath = resolve(input.targetDir);
|
|
11
|
+
const name = input.name ?? basename(storePath);
|
|
12
|
+
if (await pathExists(resolveKbDir(storePath))) {
|
|
13
|
+
return { ok: false, reason: 'kb-exists', message: `a ${KB_DIR}/ store already exists at ${storePath}` };
|
|
14
|
+
}
|
|
15
|
+
if (!input.register) {
|
|
16
|
+
const created = await scaffold(storePath);
|
|
17
|
+
return { ok: true, created: { name, storePath, registered: false, created } };
|
|
18
|
+
}
|
|
19
|
+
const { registryPath } = input;
|
|
20
|
+
const before = await loadKbRegistry({ userConfigPath: registryPath });
|
|
21
|
+
if (before.entries.some((entry) => entry.name === name)) {
|
|
22
|
+
return { ok: false, reason: 'name-registered', message: nameRegisteredMessage(name, registryPath) };
|
|
23
|
+
}
|
|
24
|
+
const created = await scaffold(storePath);
|
|
25
|
+
const result = await registerStore({ registryPath, name, storePath });
|
|
26
|
+
if (result.status === 'already-present') {
|
|
27
|
+
return { ok: false, reason: 'name-registered', message: nameRegisteredMessage(name, registryPath) };
|
|
28
|
+
}
|
|
29
|
+
const defaultKb = await ensureDefaultKb({ registryPath, name, before });
|
|
30
|
+
return { ok: true, created: { name, storePath, registered: true, created, defaultKb } };
|
|
31
|
+
}
|
|
32
|
+
async function ensureDefaultKb(input) {
|
|
33
|
+
if (input.before.defaultKb !== undefined) {
|
|
34
|
+
return 'unchanged';
|
|
35
|
+
}
|
|
36
|
+
if (input.before.entries.length > 0) {
|
|
37
|
+
return 'needs-selection';
|
|
38
|
+
}
|
|
39
|
+
await setDefaultKb({ registryPath: input.registryPath, name: input.name });
|
|
40
|
+
return 'set';
|
|
41
|
+
}
|
|
42
|
+
function nameRegisteredMessage(name, registryPath) {
|
|
43
|
+
return `a store named "${name}" is already registered in ${registryPath}`;
|
|
44
|
+
}
|
|
45
|
+
async function scaffold(storePath) {
|
|
46
|
+
await mkdir(resolveKbDir(storePath), { recursive: true });
|
|
47
|
+
await writeFile(join(storePath, CONFIG_FILE), renderConfigSeed(), 'utf8');
|
|
48
|
+
await writeFile(join(storePath, ALIASES_FILE), renderAliasesSeed(), 'utf8');
|
|
49
|
+
await mkdir(resolveEventsDir(storePath), { recursive: true });
|
|
50
|
+
return [CONFIG_FILE, ALIASES_FILE, `${CONTENT_DIR}/`, `${EVENTS_DIR}/`];
|
|
51
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { stringify } from 'yaml';
|
|
2
|
+
import { defaultKbConfig } from "../config/config-schema.js";
|
|
3
|
+
const ALIASES_HEADER = `# Tag aliases for this knowledge store.
|
|
4
|
+
#
|
|
5
|
+
# Map each canonical tag to the aliases that should resolve to it, for example:
|
|
6
|
+
# aliases:
|
|
7
|
+
# typescript: [ts, type-script]
|
|
8
|
+
# The \`aliases:\` key is required even when empty.
|
|
9
|
+
`;
|
|
10
|
+
const CONFIG_HEADER = `# Check configuration for this knowledge store.
|
|
11
|
+
#
|
|
12
|
+
# Both keys are optional and fall back to the defaults shown below; an absent file uses these defaults too. Uncomment
|
|
13
|
+
# and edit to override. \`targets\` selects which notes \`kb check\` enumerates and \`exclude\` removes matches;
|
|
14
|
+
# patterns are slash-separated and relative to the store root.
|
|
15
|
+
#
|
|
16
|
+
`;
|
|
17
|
+
export function renderAliasesSeed() {
|
|
18
|
+
return `${ALIASES_HEADER}aliases: {}\n`;
|
|
19
|
+
}
|
|
20
|
+
export function renderConfigSeed() {
|
|
21
|
+
return `${CONFIG_HEADER}${commentBlock(stringify(defaultKbConfig))}\n`;
|
|
22
|
+
}
|
|
23
|
+
function commentBlock(text) {
|
|
24
|
+
return text
|
|
25
|
+
.trimEnd()
|
|
26
|
+
.split('\n')
|
|
27
|
+
.map((line) => (line === '' ? '#' : `# ${line}`))
|
|
28
|
+
.join('\n');
|
|
29
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { dirname, resolve } from 'node:path';
|
|
2
|
+
import { directoryExists } from "../filesystem/exists.js";
|
|
3
|
+
import { resolveKbDir } from "../layout/index.js";
|
|
4
|
+
export async function findKbRoot(input) {
|
|
5
|
+
for (const current of ancestorDirs(input.startDir)) {
|
|
6
|
+
const kbDir = resolveKbDir(current);
|
|
7
|
+
if (await directoryExists(kbDir, { treatErrorsAsAbsent: true })) {
|
|
8
|
+
return { path: current, kbDir };
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
function* ancestorDirs(startDir) {
|
|
14
|
+
let current = resolve(startDir);
|
|
15
|
+
yield current;
|
|
16
|
+
let parent = dirname(current);
|
|
17
|
+
while (parent !== current) {
|
|
18
|
+
current = parent;
|
|
19
|
+
yield current;
|
|
20
|
+
parent = dirname(current);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type { KbRegistry, KbRegistryEntry, KbRoot } from '../types.js';
|
|
2
|
+
export { findKbRoot } from './find-kb-root.js';
|
|
3
|
+
export type { KbRegistryFile, KbRegistryFileEntry } from './kb-registry-schema.js';
|
|
4
|
+
export { kbRegistryFileEntrySchema, kbRegistryFileSchema } from './kb-registry-schema.js';
|
|
5
|
+
export type { KbRegistryLoadResult } from './load-registry.js';
|
|
6
|
+
export { loadKbRegistry, tryLoadKbRegistry } from './load-registry.js';
|
|
7
|
+
export type { RegisterStoreResult } from './register-store.js';
|
|
8
|
+
export { registerStore } from './register-store.js';
|
|
9
|
+
export { clearDefaultKb, setDefaultKb } from './set-default-kb.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { findKbRoot } from "./find-kb-root.js";
|
|
2
|
+
export { kbRegistryFileEntrySchema, kbRegistryFileSchema } from "./kb-registry-schema.js";
|
|
3
|
+
export { loadKbRegistry, tryLoadKbRegistry } from "./load-registry.js";
|
|
4
|
+
export { registerStore } from "./register-store.js";
|
|
5
|
+
export { clearDefaultKb, setDefaultKb } from "./set-default-kb.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const kbRegistryFileEntrySchema: z.ZodObject<{
|
|
3
|
+
path: z.ZodString;
|
|
4
|
+
description: z.ZodOptional<z.ZodString>;
|
|
5
|
+
readonly: z.ZodOptional<z.ZodBoolean>;
|
|
6
|
+
}, z.core.$strip>;
|
|
7
|
+
export declare const kbRegistryFileSchema: z.ZodObject<{
|
|
8
|
+
default_kb: z.ZodOptional<z.ZodString>;
|
|
9
|
+
kbs: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
10
|
+
path: z.ZodString;
|
|
11
|
+
description: z.ZodOptional<z.ZodString>;
|
|
12
|
+
readonly: z.ZodOptional<z.ZodBoolean>;
|
|
13
|
+
}, z.core.$strip>>>;
|
|
14
|
+
}, z.core.$strip>;
|
|
15
|
+
export type KbRegistryFileEntry = z.infer<typeof kbRegistryFileEntrySchema>;
|
|
16
|
+
export type KbRegistryFile = z.infer<typeof kbRegistryFileSchema>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const kbRegistryFileEntrySchema = z.object({
|
|
3
|
+
path: z.string().min(1),
|
|
4
|
+
description: z.string().optional(),
|
|
5
|
+
readonly: z.boolean().optional(),
|
|
6
|
+
});
|
|
7
|
+
export const kbRegistryFileSchema = z.object({
|
|
8
|
+
default_kb: z.string().min(1).optional(),
|
|
9
|
+
kbs: z.record(z.string(), kbRegistryFileEntrySchema).optional(),
|
|
10
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { KbRegistry } from '../types.js';
|
|
2
|
+
export declare function loadKbRegistry(input?: {
|
|
3
|
+
userConfigPath?: string;
|
|
4
|
+
projectDir?: string;
|
|
5
|
+
home?: string;
|
|
6
|
+
}): Promise<KbRegistry>;
|
|
7
|
+
export interface KbRegistryLoadResult {
|
|
8
|
+
config: KbRegistry;
|
|
9
|
+
error?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function tryLoadKbRegistry(input?: {
|
|
12
|
+
userConfigPath?: string;
|
|
13
|
+
projectDir?: string;
|
|
14
|
+
home?: string;
|
|
15
|
+
}): Promise<KbRegistryLoadResult>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
import { parse } from 'yaml';
|
|
5
|
+
import { isEnoent } from "../type-guards.js";
|
|
6
|
+
import { kbRegistryFileSchema } from "./kb-registry-schema.js";
|
|
7
|
+
const USER_CONFIG_RELATIVE = join('.agents', 'kb.yaml');
|
|
8
|
+
const PROJECT_CONFIG_RELATIVE = join('.agents', 'kb.yaml');
|
|
9
|
+
export async function loadKbRegistry(input = {}) {
|
|
10
|
+
const home = input.home ?? homedir();
|
|
11
|
+
const userConfigPath = input.userConfigPath ?? join(home, USER_CONFIG_RELATIVE);
|
|
12
|
+
const projectConfigPath = input.projectDir === undefined ? undefined : join(input.projectDir, PROJECT_CONFIG_RELATIVE);
|
|
13
|
+
const userFile = await loadRegistryFile(userConfigPath, 'user', home);
|
|
14
|
+
const projectFile = projectConfigPath === undefined ? undefined : await loadRegistryFile(projectConfigPath, 'project', home);
|
|
15
|
+
const merged = mergeEntries(userFile?.entries ?? [], projectFile?.entries ?? []);
|
|
16
|
+
let defaultKbName;
|
|
17
|
+
let defaultKbSource = userConfigPath;
|
|
18
|
+
if (projectFile?.defaultKb !== undefined) {
|
|
19
|
+
defaultKbName = projectFile.defaultKb;
|
|
20
|
+
defaultKbSource = projectConfigPath ?? userConfigPath;
|
|
21
|
+
}
|
|
22
|
+
else if (userFile?.defaultKb !== undefined) {
|
|
23
|
+
defaultKbName = userFile.defaultKb;
|
|
24
|
+
}
|
|
25
|
+
const defaultKb = resolveDefaultKb(merged, defaultKbName, defaultKbSource);
|
|
26
|
+
const sources = {};
|
|
27
|
+
if (userFile !== undefined)
|
|
28
|
+
sources.user = userConfigPath;
|
|
29
|
+
if (projectConfigPath !== undefined && projectFile !== undefined) {
|
|
30
|
+
sources.project = projectConfigPath;
|
|
31
|
+
}
|
|
32
|
+
return { entries: merged, ...(defaultKb !== undefined && { defaultKb }), sources };
|
|
33
|
+
}
|
|
34
|
+
export async function tryLoadKbRegistry(input = {}) {
|
|
35
|
+
try {
|
|
36
|
+
return { config: await loadKbRegistry(input) };
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
40
|
+
return { config: { entries: [], sources: {} }, error: message };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function expandTilde(value, home) {
|
|
44
|
+
if (value !== '~' && !value.startsWith('~/')) {
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
if (home === '') {
|
|
48
|
+
throw new Error(`cannot expand "${value}": HOME is not set`);
|
|
49
|
+
}
|
|
50
|
+
return value === '~' ? home : join(home, value.slice(2));
|
|
51
|
+
}
|
|
52
|
+
async function loadRegistryFile(path, source, home) {
|
|
53
|
+
let text;
|
|
54
|
+
try {
|
|
55
|
+
text = await readFile(path, 'utf8');
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (isEnoent(error)) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
let parsed;
|
|
64
|
+
try {
|
|
65
|
+
parsed = parse(text);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
69
|
+
throw new Error(`${path}: malformed YAML — ${message}`);
|
|
70
|
+
}
|
|
71
|
+
if (parsed === null || parsed === undefined) {
|
|
72
|
+
return { entries: [] };
|
|
73
|
+
}
|
|
74
|
+
const result = kbRegistryFileSchema.safeParse(parsed);
|
|
75
|
+
if (!result.success) {
|
|
76
|
+
throw new Error(`${path}: invalid kb.yaml — ${result.error.issues[0]?.message ?? 'unknown error'}`);
|
|
77
|
+
}
|
|
78
|
+
const configDir = dirname(path);
|
|
79
|
+
const entries = [];
|
|
80
|
+
for (const [name, fileEntry] of Object.entries(result.data.kbs ?? {})) {
|
|
81
|
+
entries.push({
|
|
82
|
+
name,
|
|
83
|
+
path: resolvePath(fileEntry.path, configDir, home),
|
|
84
|
+
source,
|
|
85
|
+
...(fileEntry.description !== undefined && { description: fileEntry.description }),
|
|
86
|
+
...(fileEntry.readonly !== undefined && { readonly: fileEntry.readonly }),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return { entries, ...(result.data.default_kb !== undefined && { defaultKb: result.data.default_kb }) };
|
|
90
|
+
}
|
|
91
|
+
function mergeEntries(userEntries, projectEntries) {
|
|
92
|
+
const byName = new Map();
|
|
93
|
+
for (const entry of userEntries) {
|
|
94
|
+
byName.set(entry.name, entry);
|
|
95
|
+
}
|
|
96
|
+
for (const entry of projectEntries) {
|
|
97
|
+
byName.set(entry.name, entry);
|
|
98
|
+
}
|
|
99
|
+
return [...byName.values()];
|
|
100
|
+
}
|
|
101
|
+
function resolveDefaultKb(entries, name, sourcePath) {
|
|
102
|
+
if (name === undefined) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
const match = entries.find((entry) => entry.name === name);
|
|
106
|
+
if (match === undefined) {
|
|
107
|
+
throw new Error(`${sourcePath}: default_kb "${name}" does not match any registered KB`);
|
|
108
|
+
}
|
|
109
|
+
return match;
|
|
110
|
+
}
|
|
111
|
+
function resolvePath(value, configDir, home) {
|
|
112
|
+
const expanded = expandTilde(value, home);
|
|
113
|
+
return isAbsolute(expanded) ? expanded : resolve(configDir, expanded);
|
|
114
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { kbRegistryFileSchema } from "./kb-registry-schema.js";
|
|
4
|
+
import { loadRegistryDocument } from "./registry-document.js";
|
|
5
|
+
export async function registerStore(input) {
|
|
6
|
+
const doc = await loadRegistryDocument(input.registryPath);
|
|
7
|
+
const existing = kbRegistryFileSchema.safeParse(doc.toJS() ?? {});
|
|
8
|
+
if (!existing.success) {
|
|
9
|
+
throw new Error(`${input.registryPath}: invalid kb.yaml — ${existing.error.issues[0]?.message ?? 'unknown error'}`);
|
|
10
|
+
}
|
|
11
|
+
if (doc.hasIn(['kbs', input.name])) {
|
|
12
|
+
return { status: 'already-present' };
|
|
13
|
+
}
|
|
14
|
+
const entry = { path: input.storePath };
|
|
15
|
+
if (input.description !== undefined) {
|
|
16
|
+
entry.description = input.description;
|
|
17
|
+
}
|
|
18
|
+
doc.setIn(['kbs', input.name], entry);
|
|
19
|
+
const result = kbRegistryFileSchema.safeParse(doc.toJS());
|
|
20
|
+
if (!result.success) {
|
|
21
|
+
throw new Error(`${input.registryPath}: cannot register "${input.name}" — ${result.error.issues[0]?.message ?? 'invalid entry'}`);
|
|
22
|
+
}
|
|
23
|
+
await mkdir(dirname(input.registryPath), { recursive: true });
|
|
24
|
+
await writeFile(input.registryPath, doc.toString(), 'utf8');
|
|
25
|
+
return { status: 'added' };
|
|
26
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { Document, parseDocument } from 'yaml';
|
|
3
|
+
import { isEnoent } from "../type-guards.js";
|
|
4
|
+
export async function loadRegistryDocument(path) {
|
|
5
|
+
let text;
|
|
6
|
+
try {
|
|
7
|
+
text = await readFile(path, 'utf8');
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
if (isEnoent(error)) {
|
|
11
|
+
return new Document({});
|
|
12
|
+
}
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
const doc = parseDocument(text);
|
|
16
|
+
return doc.contents === null ? new Document({}) : doc;
|
|
17
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { isMap, isScalar } from 'yaml';
|
|
4
|
+
import { kbRegistryFileSchema } from "./kb-registry-schema.js";
|
|
5
|
+
import { loadRegistryDocument } from "./registry-document.js";
|
|
6
|
+
export async function clearDefaultKb(input) {
|
|
7
|
+
const doc = await loadRegistryDocument(input.registryPath);
|
|
8
|
+
const existing = kbRegistryFileSchema.safeParse(doc.toJS() ?? {});
|
|
9
|
+
if (!existing.success) {
|
|
10
|
+
throw new Error(`${input.registryPath}: invalid kb.yaml — ${existing.error.issues[0]?.message ?? 'unknown error'}`);
|
|
11
|
+
}
|
|
12
|
+
if (!doc.has('default_kb')) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
deleteDefaultKb(doc);
|
|
16
|
+
await writeFile(input.registryPath, doc.toString(), 'utf8');
|
|
17
|
+
}
|
|
18
|
+
export async function setDefaultKb(input) {
|
|
19
|
+
const doc = await loadRegistryDocument(input.registryPath);
|
|
20
|
+
const existing = kbRegistryFileSchema.safeParse(doc.toJS() ?? {});
|
|
21
|
+
if (!existing.success) {
|
|
22
|
+
throw new Error(`${input.registryPath}: invalid kb.yaml — ${existing.error.issues[0]?.message ?? 'unknown error'}`);
|
|
23
|
+
}
|
|
24
|
+
if (!doc.hasIn(['kbs', input.name])) {
|
|
25
|
+
throw new Error(`${input.registryPath}: "${input.name}" is not a registered knowledge base`);
|
|
26
|
+
}
|
|
27
|
+
doc.set('default_kb', input.name);
|
|
28
|
+
const result = kbRegistryFileSchema.safeParse(doc.toJS());
|
|
29
|
+
if (!result.success) {
|
|
30
|
+
throw new Error(`${input.registryPath}: cannot set default_kb — ${result.error.issues[0]?.message ?? 'invalid registry'}`);
|
|
31
|
+
}
|
|
32
|
+
await mkdir(dirname(input.registryPath), { recursive: true });
|
|
33
|
+
await writeFile(input.registryPath, doc.toString(), 'utf8');
|
|
34
|
+
}
|
|
35
|
+
function deleteDefaultKb(doc) {
|
|
36
|
+
const map = doc.contents;
|
|
37
|
+
if (!isMap(map)) {
|
|
38
|
+
doc.delete('default_kb');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const index = map.items.findIndex((pair) => isScalar(pair.key) && pair.key.value === 'default_kb');
|
|
42
|
+
const removed = index !== -1 ? map.items[index] : undefined;
|
|
43
|
+
const carried = removed !== undefined && isScalar(removed.key) ? removed.key.commentBefore : undefined;
|
|
44
|
+
doc.delete('default_kb');
|
|
45
|
+
if (carried === undefined) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const successor = map.items[index];
|
|
49
|
+
if (successor !== undefined && isScalar(successor.key)) {
|
|
50
|
+
successor.key.commentBefore =
|
|
51
|
+
successor.key.commentBefore !== undefined ? `${carried}\n${successor.key.commentBefore}` : carried;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface ExistsOptions {
|
|
2
|
+
absentCodes?: readonly string[];
|
|
3
|
+
treatErrorsAsAbsent?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export declare function directoryExists(path: string, options?: ExistsOptions): Promise<boolean>;
|
|
6
|
+
export declare function pathExists(path: string, options?: ExistsOptions): Promise<boolean>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
|
+
import { isErrorCode } from "../type-guards.js";
|
|
3
|
+
export async function directoryExists(path, options = {}) {
|
|
4
|
+
return statExists(path, options, (stats) => stats.isDirectory());
|
|
5
|
+
}
|
|
6
|
+
export async function pathExists(path, options = {}) {
|
|
7
|
+
return statExists(path, options, () => true);
|
|
8
|
+
}
|
|
9
|
+
const DEFAULT_ABSENT_CODES = ['ENOENT'];
|
|
10
|
+
async function statExists(path, options, onStats) {
|
|
11
|
+
try {
|
|
12
|
+
return onStats(await stat(path));
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (options.treatErrorsAsAbsent) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
const absentCodes = options.absentCodes ?? DEFAULT_ABSENT_CODES;
|
|
19
|
+
if (absentCodes.some((code) => isErrorCode(error, code))) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { directoryExists, type ExistsOptions, pathExists } from './exists.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { directoryExists, pathExists } from "./exists.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { parseNote, parseNoteContent } from "./parse-note.js";
|