ko-qmd 2.8.3-ko.2

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +1376 -0
  2. package/LICENSE +21 -0
  3. package/README.ko.md +86 -0
  4. package/README.md +1307 -0
  5. package/bin/qmd +192 -0
  6. package/dist/ast.d.ts +65 -0
  7. package/dist/ast.js +334 -0
  8. package/dist/bench/bench.d.ts +35 -0
  9. package/dist/bench/bench.js +327 -0
  10. package/dist/bench/score.d.ts +33 -0
  11. package/dist/bench/score.js +88 -0
  12. package/dist/bench/types.d.ts +80 -0
  13. package/dist/bench/types.js +8 -0
  14. package/dist/cli/build-info.json +4 -0
  15. package/dist/cli/embed-lock.d.ts +24 -0
  16. package/dist/cli/embed-lock.js +94 -0
  17. package/dist/cli/formatter.d.ts +120 -0
  18. package/dist/cli/formatter.js +355 -0
  19. package/dist/cli/mcp-pid.d.ts +25 -0
  20. package/dist/cli/mcp-pid.js +86 -0
  21. package/dist/cli/qmd.d.ts +43 -0
  22. package/dist/cli/qmd.js +4424 -0
  23. package/dist/cli/version.d.ts +42 -0
  24. package/dist/cli/version.js +80 -0
  25. package/dist/collections.d.ts +167 -0
  26. package/dist/collections.js +410 -0
  27. package/dist/db.d.ts +63 -0
  28. package/dist/db.js +130 -0
  29. package/dist/hangul.d.ts +54 -0
  30. package/dist/hangul.js +140 -0
  31. package/dist/index.d.ts +237 -0
  32. package/dist/index.js +244 -0
  33. package/dist/llm.d.ts +543 -0
  34. package/dist/llm.js +1747 -0
  35. package/dist/maintenance.d.ts +33 -0
  36. package/dist/maintenance.js +52 -0
  37. package/dist/mcp/origin-guard.d.ts +67 -0
  38. package/dist/mcp/origin-guard.js +137 -0
  39. package/dist/mcp/server.d.ts +116 -0
  40. package/dist/mcp/server.js +883 -0
  41. package/dist/paths.d.ts +1 -0
  42. package/dist/paths.js +4 -0
  43. package/dist/store.d.ts +1100 -0
  44. package/dist/store.js +4965 -0
  45. package/dist/trust.d.ts +152 -0
  46. package/dist/trust.js +249 -0
  47. package/package.json +114 -0
  48. package/scripts/build.mjs +72 -0
  49. package/scripts/check-package-grammars.mjs +29 -0
  50. package/scripts/package-smoke.mjs +65 -0
  51. package/scripts/test-all.mjs +38 -0
  52. package/skills/qmd/SKILL.md +316 -0
  53. package/skills/qmd/references/mcp-setup.md +119 -0
  54. package/skills/release/SKILL.md +141 -0
  55. package/skills/release/scripts/install-hooks.sh +38 -0
  56. package/skills/release/scripts/release-context.sh +129 -0
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Build/commit identification for `qmd --version`.
3
+ *
4
+ * The commit is *stamped at build time* (scripts/build.mjs writes
5
+ * build-info.json next to the compiled CLI) rather than discovered at runtime.
6
+ * A published tarball carries no git history of its own, so a runtime lookup
7
+ * can only ever find some *other* repository's HEAD: `git -C <dir> rev-parse`
8
+ * walks up the tree, and a global install under a git-managed prefix (e.g.
9
+ * Homebrew's /opt/homebrew) reported that prefix's commit as qmd's.
10
+ *
11
+ * Running from a source checkout has no stamp, so the git lookup remains as a
12
+ * fallback — but only after confirming the enclosing repository is the package
13
+ * we are actually running from.
14
+ */
15
+ /** Written next to the compiled CLI (dist/cli/) by scripts/build.mjs. */
16
+ export declare const BUILD_INFO_FILENAME = "build-info.json";
17
+ export type BuildInfo = {
18
+ commit: string;
19
+ builtAt?: string;
20
+ };
21
+ /**
22
+ * Read the commit stamped into this build, or "" when there is none.
23
+ *
24
+ * Deliberately looks beside the running script rather than in the package's
25
+ * dist/ directory: a source run (src/cli/) must not pick up the stamp left by
26
+ * an earlier, possibly unrelated, build in dist/.
27
+ */
28
+ export declare function readStampedCommit(scriptDir: string): string;
29
+ /**
30
+ * Short HEAD of the checkout we are running from, or "" when we are not
31
+ * running from one.
32
+ *
33
+ * The guard is the whole point: an enclosing repository is only qmd's if its
34
+ * top level *is* this package's root. Without that check, any install nested
35
+ * inside an unrelated repository reports that repository's HEAD.
36
+ */
37
+ export declare function gitCommitForCheckout(scriptDir: string, packageRoot: string): string;
38
+ /**
39
+ * The commit to report for this invocation: the build stamp when there is one,
40
+ * otherwise the verified checkout HEAD, otherwise nothing.
41
+ */
42
+ export declare function resolveCommit(scriptDir: string, packageRoot: string): string;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Build/commit identification for `qmd --version`.
3
+ *
4
+ * The commit is *stamped at build time* (scripts/build.mjs writes
5
+ * build-info.json next to the compiled CLI) rather than discovered at runtime.
6
+ * A published tarball carries no git history of its own, so a runtime lookup
7
+ * can only ever find some *other* repository's HEAD: `git -C <dir> rev-parse`
8
+ * walks up the tree, and a global install under a git-managed prefix (e.g.
9
+ * Homebrew's /opt/homebrew) reported that prefix's commit as qmd's.
10
+ *
11
+ * Running from a source checkout has no stamp, so the git lookup remains as a
12
+ * fallback — but only after confirming the enclosing repository is the package
13
+ * we are actually running from.
14
+ */
15
+ import { execFileSync } from "node:child_process";
16
+ import { readFileSync, realpathSync } from "node:fs";
17
+ import { join } from "node:path";
18
+ /** Written next to the compiled CLI (dist/cli/) by scripts/build.mjs. */
19
+ export const BUILD_INFO_FILENAME = "build-info.json";
20
+ /**
21
+ * Read the commit stamped into this build, or "" when there is none.
22
+ *
23
+ * Deliberately looks beside the running script rather than in the package's
24
+ * dist/ directory: a source run (src/cli/) must not pick up the stamp left by
25
+ * an earlier, possibly unrelated, build in dist/.
26
+ */
27
+ export function readStampedCommit(scriptDir) {
28
+ try {
29
+ const raw = readFileSync(join(scriptDir, BUILD_INFO_FILENAME), "utf-8");
30
+ const info = JSON.parse(raw);
31
+ return typeof info.commit === "string" ? info.commit : "";
32
+ }
33
+ catch {
34
+ // No stamp (source run), unreadable, or malformed — fall through.
35
+ return "";
36
+ }
37
+ }
38
+ function git(args, cwd) {
39
+ // execFileSync, not execSync: no shell, so a path containing spaces or shell
40
+ // metacharacters is passed through intact instead of silently failing.
41
+ return execFileSync("git", args, {
42
+ cwd,
43
+ encoding: "utf-8",
44
+ stdio: ["pipe", "pipe", "pipe"],
45
+ }).trim();
46
+ }
47
+ /**
48
+ * Short HEAD of the checkout we are running from, or "" when we are not
49
+ * running from one.
50
+ *
51
+ * The guard is the whole point: an enclosing repository is only qmd's if its
52
+ * top level *is* this package's root. Without that check, any install nested
53
+ * inside an unrelated repository reports that repository's HEAD.
54
+ */
55
+ export function gitCommitForCheckout(scriptDir, packageRoot) {
56
+ try {
57
+ const top = git(["rev-parse", "--show-toplevel"], scriptDir);
58
+ if (realpathSync(top) !== realpathSync(packageRoot))
59
+ return "";
60
+ const commit = git(["rev-parse", "--short", "HEAD"], top);
61
+ if (!commit)
62
+ return "";
63
+ // Same "-dirty" marker scripts/build.mjs stamps: running edited sources is
64
+ // not the commit it names, and that distinction is the whole point of
65
+ // printing a commit at all.
66
+ const dirty = git(["status", "--porcelain"], top);
67
+ return dirty ? `${commit}-dirty` : commit;
68
+ }
69
+ catch {
70
+ // Not a git repo, git not installed, or an unborn branch.
71
+ return "";
72
+ }
73
+ }
74
+ /**
75
+ * The commit to report for this invocation: the build stamp when there is one,
76
+ * otherwise the verified checkout HEAD, otherwise nothing.
77
+ */
78
+ export function resolveCommit(scriptDir, packageRoot) {
79
+ return readStampedCommit(scriptDir) || gitCommitForCheckout(scriptDir, packageRoot);
80
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Collections configuration management
3
+ *
4
+ * This module manages the YAML-based collection configuration at ~/.config/qmd/index.yml.
5
+ * Collections define which directories to index and their associated contexts.
6
+ */
7
+ /**
8
+ * Context definitions for a collection
9
+ * Key is path prefix (e.g., "/", "/2024", "/Board of Directors")
10
+ * Value is the context description
11
+ */
12
+ export type ContextMap = Record<string, string>;
13
+ /**
14
+ * A single collection configuration
15
+ */
16
+ export interface Collection {
17
+ path: string;
18
+ pattern: string;
19
+ ignore?: string[];
20
+ context?: ContextMap;
21
+ update?: string;
22
+ includeByDefault?: boolean;
23
+ }
24
+ /**
25
+ * Model configuration for embedding, reranking, and generation
26
+ */
27
+ export interface ModelsConfig {
28
+ embed?: string;
29
+ rerank?: string;
30
+ generate?: string;
31
+ }
32
+ /**
33
+ * The complete configuration file structure
34
+ */
35
+ export interface CollectionConfig {
36
+ global_context?: string;
37
+ editor_uri?: string;
38
+ editor_uri_template?: string;
39
+ collections: Record<string, Collection>;
40
+ models?: ModelsConfig;
41
+ }
42
+ /**
43
+ * Collection with its name (for return values)
44
+ */
45
+ export interface NamedCollection extends Collection {
46
+ name: string;
47
+ }
48
+ /**
49
+ * Set the config source for SDK mode.
50
+ * - File path: load/save from a specific YAML file
51
+ * - Inline config: use an in-memory CollectionConfig (saveConfig updates in place, no file I/O)
52
+ * - undefined: reset to default file-based config
53
+ */
54
+ export declare function setConfigSource(source?: {
55
+ configPath?: string;
56
+ config?: CollectionConfig;
57
+ }): void;
58
+ /**
59
+ * Set the current index name for config file lookup
60
+ * Config file will be ~/.config/qmd/{indexName}.yml
61
+ */
62
+ export declare function setConfigIndexName(name: string): void;
63
+ export declare function getConfigDir(): string;
64
+ /**
65
+ * Find a project-local QMD config by walking upward from startDir.
66
+ * The local config lives at .qmd/index.yaml or .qmd/index.yml and,
67
+ * when used by the CLI, keeps both config and index DB writes inside
68
+ * the project instead of the global ~/.config / ~/.cache locations.
69
+ */
70
+ export declare function findLocalConfigPath(startDir?: string): string | undefined;
71
+ /** Return the local SQLite index path paired with a local .qmd/index.yaml file. */
72
+ export declare function getLocalDbPath(configPath: string): string;
73
+ /**
74
+ * Load configuration from the configured source.
75
+ * - Inline config: returns the in-memory object directly
76
+ * - File-based: reads from YAML file (default ~/.config/qmd/index.yml)
77
+ * Returns empty config if file doesn't exist
78
+ */
79
+ export declare function loadConfig(): CollectionConfig;
80
+ /**
81
+ * Save configuration to the configured source.
82
+ * - Inline config: updates the in-memory object (no file I/O)
83
+ * - File-based: writes to YAML file (default ~/.config/qmd/index.yml)
84
+ */
85
+ export declare function saveConfig(config: CollectionConfig): void;
86
+ /**
87
+ * Get a specific collection by name
88
+ * Returns null if not found
89
+ */
90
+ export declare function getCollection(name: string): NamedCollection | null;
91
+ /**
92
+ * List all collections
93
+ */
94
+ export declare function listCollections(): NamedCollection[];
95
+ /**
96
+ * Get collections that are included by default in queries
97
+ */
98
+ export declare function getDefaultCollections(): NamedCollection[];
99
+ /**
100
+ * Get collection names that are included by default
101
+ */
102
+ export declare function getDefaultCollectionNames(): string[];
103
+ /**
104
+ * Update a collection's settings
105
+ */
106
+ export declare function updateCollectionSettings(name: string, settings: {
107
+ update?: string | null;
108
+ includeByDefault?: boolean;
109
+ }): boolean;
110
+ /**
111
+ * Add or update a collection
112
+ */
113
+ export declare function addCollection(name: string, path: string, pattern?: string): void;
114
+ /**
115
+ * Remove a collection
116
+ */
117
+ export declare function removeCollection(name: string): boolean;
118
+ /**
119
+ * Rename a collection
120
+ */
121
+ export declare function renameCollection(oldName: string, newName: string): boolean;
122
+ /**
123
+ * Get global context
124
+ */
125
+ export declare function getGlobalContext(): string | undefined;
126
+ /**
127
+ * Set global context
128
+ */
129
+ export declare function setGlobalContext(context: string | undefined): void;
130
+ /**
131
+ * Get all contexts for a collection
132
+ */
133
+ export declare function getContexts(collectionName: string): ContextMap | undefined;
134
+ /**
135
+ * Add or update a context for a specific path in a collection
136
+ */
137
+ export declare function addContext(collectionName: string, pathPrefix: string, contextText: string): boolean;
138
+ /**
139
+ * Remove a context from a collection
140
+ */
141
+ export declare function removeContext(collectionName: string, pathPrefix: string): boolean;
142
+ /**
143
+ * List all contexts across all collections
144
+ */
145
+ export declare function listAllContexts(): Array<{
146
+ collection: string;
147
+ path: string;
148
+ context: string;
149
+ }>;
150
+ /**
151
+ * Find best matching context for a given collection and path
152
+ * Returns the most specific matching context (longest path prefix match)
153
+ */
154
+ export declare function findContextForPath(collectionName: string, filePath: string): string | undefined;
155
+ /**
156
+ * Get the config file path (useful for error messages)
157
+ */
158
+ export declare function getConfigPath(): string;
159
+ /**
160
+ * Check if config file exists
161
+ */
162
+ export declare function configExists(): boolean;
163
+ /**
164
+ * Validate a collection name
165
+ * Collection names must be valid and not contain special characters
166
+ */
167
+ export declare function isValidCollectionName(name: string): boolean;
@@ -0,0 +1,410 @@
1
+ /**
2
+ * Collections configuration management
3
+ *
4
+ * This module manages the YAML-based collection configuration at ~/.config/qmd/index.yml.
5
+ * Collections define which directories to index and their associated contexts.
6
+ */
7
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
8
+ import { join, dirname, resolve } from "path";
9
+ import { qmdHomedir } from "./paths.js";
10
+ import YAML from "yaml";
11
+ // ============================================================================
12
+ // Configuration paths
13
+ // ============================================================================
14
+ // Current index name (default: "index")
15
+ let currentIndexName = "index";
16
+ // SDK mode: optional in-memory config or custom config path
17
+ let configSource = { type: 'file' };
18
+ /**
19
+ * Set the config source for SDK mode.
20
+ * - File path: load/save from a specific YAML file
21
+ * - Inline config: use an in-memory CollectionConfig (saveConfig updates in place, no file I/O)
22
+ * - undefined: reset to default file-based config
23
+ */
24
+ export function setConfigSource(source) {
25
+ if (!source) {
26
+ configSource = { type: 'file' };
27
+ return;
28
+ }
29
+ if (source.config) {
30
+ // Ensure collections object exists
31
+ if (!source.config.collections) {
32
+ source.config.collections = {};
33
+ }
34
+ configSource = { type: 'inline', config: source.config };
35
+ }
36
+ else if (source.configPath) {
37
+ configSource = { type: 'file', path: source.configPath };
38
+ }
39
+ else {
40
+ configSource = { type: 'file' };
41
+ }
42
+ }
43
+ /**
44
+ * Set the current index name for config file lookup
45
+ * Config file will be ~/.config/qmd/{indexName}.yml
46
+ */
47
+ export function setConfigIndexName(name) {
48
+ // Resolve relative paths to absolute paths and sanitize for use as filename
49
+ if (name.includes('/')) {
50
+ const absolutePath = resolve(process.cwd(), name);
51
+ // Replace path separators with underscores to create a valid filename
52
+ currentIndexName = absolutePath.replace(/\//g, '_').replace(/^_/, '');
53
+ }
54
+ else {
55
+ currentIndexName = name;
56
+ }
57
+ }
58
+ export function getConfigDir() {
59
+ // Allow override via QMD_CONFIG_DIR for testing
60
+ if (process.env.QMD_CONFIG_DIR) {
61
+ return process.env.QMD_CONFIG_DIR;
62
+ }
63
+ // Respect XDG Base Directory specification (consistent with store.ts)
64
+ if (process.env.XDG_CONFIG_HOME) {
65
+ return join(process.env.XDG_CONFIG_HOME, "qmd");
66
+ }
67
+ return join(qmdHomedir(), ".config", "qmd");
68
+ }
69
+ function getConfigFilePath() {
70
+ return join(getConfigDir(), `${currentIndexName}.yml`);
71
+ }
72
+ /**
73
+ * Find a project-local QMD config by walking upward from startDir.
74
+ * The local config lives at .qmd/index.yaml or .qmd/index.yml and,
75
+ * when used by the CLI, keeps both config and index DB writes inside
76
+ * the project instead of the global ~/.config / ~/.cache locations.
77
+ */
78
+ export function findLocalConfigPath(startDir = process.cwd()) {
79
+ let dir = resolve(startDir);
80
+ while (true) {
81
+ const qmdDir = join(dir, ".qmd");
82
+ const yamlPath = join(qmdDir, "index.yaml");
83
+ if (existsSync(yamlPath))
84
+ return yamlPath;
85
+ const ymlPath = join(qmdDir, "index.yml");
86
+ if (existsSync(ymlPath))
87
+ return ymlPath;
88
+ const parent = dirname(dir);
89
+ if (parent === dir)
90
+ return undefined;
91
+ dir = parent;
92
+ }
93
+ }
94
+ /** Return the local SQLite index path paired with a local .qmd/index.yaml file. */
95
+ export function getLocalDbPath(configPath) {
96
+ return join(dirname(configPath), "index.sqlite");
97
+ }
98
+ /**
99
+ * Ensure config directory exists
100
+ */
101
+ function ensureConfigDir() {
102
+ const configDir = getConfigDir();
103
+ if (!existsSync(configDir)) {
104
+ mkdirSync(configDir, { recursive: true });
105
+ }
106
+ }
107
+ // ============================================================================
108
+ // Core functions
109
+ // ============================================================================
110
+ /**
111
+ * Load configuration from the configured source.
112
+ * - Inline config: returns the in-memory object directly
113
+ * - File-based: reads from YAML file (default ~/.config/qmd/index.yml)
114
+ * Returns empty config if file doesn't exist
115
+ */
116
+ export function loadConfig() {
117
+ // SDK inline config mode
118
+ if (configSource.type === 'inline') {
119
+ return configSource.config;
120
+ }
121
+ // File-based config (SDK custom path or default)
122
+ const configPath = configSource.path || getConfigFilePath();
123
+ if (!existsSync(configPath)) {
124
+ return { collections: {} };
125
+ }
126
+ try {
127
+ const content = readFileSync(configPath, "utf-8");
128
+ const parsed = YAML.parse(content);
129
+ const config = parsed ?? { collections: {} };
130
+ // Ensure collections object exists
131
+ if (!config.collections) {
132
+ config.collections = {};
133
+ }
134
+ return config;
135
+ }
136
+ catch (error) {
137
+ throw new Error(`Failed to parse ${configPath}: ${error}`);
138
+ }
139
+ }
140
+ /**
141
+ * Save configuration to the configured source.
142
+ * - Inline config: updates the in-memory object (no file I/O)
143
+ * - File-based: writes to YAML file (default ~/.config/qmd/index.yml)
144
+ */
145
+ export function saveConfig(config) {
146
+ // SDK inline config mode: update in place, no file I/O
147
+ if (configSource.type === 'inline') {
148
+ configSource.config = config;
149
+ return;
150
+ }
151
+ const configPath = configSource.path || getConfigFilePath();
152
+ const configDir = dirname(configPath);
153
+ if (!existsSync(configDir)) {
154
+ mkdirSync(configDir, { recursive: true });
155
+ }
156
+ try {
157
+ const yaml = YAML.stringify(config, {
158
+ indent: 2,
159
+ lineWidth: 0, // Don't wrap lines
160
+ });
161
+ writeFileSync(configPath, yaml, "utf-8");
162
+ }
163
+ catch (error) {
164
+ throw new Error(`Failed to write ${configPath}: ${error}`);
165
+ }
166
+ }
167
+ /**
168
+ * Get a specific collection by name
169
+ * Returns null if not found
170
+ */
171
+ export function getCollection(name) {
172
+ const config = loadConfig();
173
+ const collection = config.collections[name];
174
+ if (!collection) {
175
+ return null;
176
+ }
177
+ return { name, ...collection };
178
+ }
179
+ /**
180
+ * List all collections
181
+ */
182
+ export function listCollections() {
183
+ const config = loadConfig();
184
+ return Object.entries(config.collections).map(([name, collection]) => ({
185
+ name,
186
+ ...collection,
187
+ }));
188
+ }
189
+ /**
190
+ * Get collections that are included by default in queries
191
+ */
192
+ export function getDefaultCollections() {
193
+ return listCollections().filter(c => c.includeByDefault !== false);
194
+ }
195
+ /**
196
+ * Get collection names that are included by default
197
+ */
198
+ export function getDefaultCollectionNames() {
199
+ return getDefaultCollections().map(c => c.name);
200
+ }
201
+ /**
202
+ * Update a collection's settings
203
+ */
204
+ export function updateCollectionSettings(name, settings) {
205
+ const config = loadConfig();
206
+ const collection = config.collections[name];
207
+ if (!collection)
208
+ return false;
209
+ if (settings.update !== undefined) {
210
+ if (settings.update === null) {
211
+ delete collection.update;
212
+ }
213
+ else {
214
+ collection.update = settings.update;
215
+ }
216
+ }
217
+ if (settings.includeByDefault !== undefined) {
218
+ if (settings.includeByDefault === true) {
219
+ // true is default, remove the field
220
+ delete collection.includeByDefault;
221
+ }
222
+ else {
223
+ collection.includeByDefault = settings.includeByDefault;
224
+ }
225
+ }
226
+ saveConfig(config);
227
+ return true;
228
+ }
229
+ /**
230
+ * Add or update a collection
231
+ */
232
+ export function addCollection(name, path, pattern = "**/*.md") {
233
+ const config = loadConfig();
234
+ config.collections[name] = {
235
+ path,
236
+ pattern,
237
+ context: config.collections[name]?.context, // Preserve existing context
238
+ };
239
+ saveConfig(config);
240
+ }
241
+ /**
242
+ * Remove a collection
243
+ */
244
+ export function removeCollection(name) {
245
+ const config = loadConfig();
246
+ if (!config.collections[name]) {
247
+ return false;
248
+ }
249
+ delete config.collections[name];
250
+ saveConfig(config);
251
+ return true;
252
+ }
253
+ /**
254
+ * Rename a collection
255
+ */
256
+ export function renameCollection(oldName, newName) {
257
+ const config = loadConfig();
258
+ if (!config.collections[oldName]) {
259
+ return false;
260
+ }
261
+ if (config.collections[newName]) {
262
+ throw new Error(`Collection '${newName}' already exists`);
263
+ }
264
+ config.collections[newName] = config.collections[oldName];
265
+ delete config.collections[oldName];
266
+ saveConfig(config);
267
+ return true;
268
+ }
269
+ // ============================================================================
270
+ // Context management
271
+ // ============================================================================
272
+ /**
273
+ * Get global context
274
+ */
275
+ export function getGlobalContext() {
276
+ const config = loadConfig();
277
+ return config.global_context;
278
+ }
279
+ /**
280
+ * Set global context
281
+ */
282
+ export function setGlobalContext(context) {
283
+ const config = loadConfig();
284
+ config.global_context = context;
285
+ saveConfig(config);
286
+ }
287
+ /**
288
+ * Get all contexts for a collection
289
+ */
290
+ export function getContexts(collectionName) {
291
+ const collection = getCollection(collectionName);
292
+ return collection?.context;
293
+ }
294
+ /**
295
+ * Add or update a context for a specific path in a collection
296
+ */
297
+ export function addContext(collectionName, pathPrefix, contextText) {
298
+ const config = loadConfig();
299
+ const collection = config.collections[collectionName];
300
+ if (!collection) {
301
+ return false;
302
+ }
303
+ if (!collection.context) {
304
+ collection.context = {};
305
+ }
306
+ collection.context[pathPrefix] = contextText;
307
+ saveConfig(config);
308
+ return true;
309
+ }
310
+ /**
311
+ * Remove a context from a collection
312
+ */
313
+ export function removeContext(collectionName, pathPrefix) {
314
+ const config = loadConfig();
315
+ const collection = config.collections[collectionName];
316
+ if (!collection?.context?.[pathPrefix]) {
317
+ return false;
318
+ }
319
+ delete collection.context[pathPrefix];
320
+ // Remove empty context object
321
+ if (Object.keys(collection.context).length === 0) {
322
+ delete collection.context;
323
+ }
324
+ saveConfig(config);
325
+ return true;
326
+ }
327
+ /**
328
+ * List all contexts across all collections
329
+ */
330
+ export function listAllContexts() {
331
+ const config = loadConfig();
332
+ const results = [];
333
+ // Add global context if present
334
+ if (config.global_context) {
335
+ results.push({
336
+ collection: "*",
337
+ path: "/",
338
+ context: config.global_context,
339
+ });
340
+ }
341
+ // Add collection contexts
342
+ for (const [name, collection] of Object.entries(config.collections)) {
343
+ if (collection.context) {
344
+ for (const [path, context] of Object.entries(collection.context)) {
345
+ results.push({
346
+ collection: name,
347
+ path,
348
+ context,
349
+ });
350
+ }
351
+ }
352
+ }
353
+ return results;
354
+ }
355
+ /**
356
+ * Find best matching context for a given collection and path
357
+ * Returns the most specific matching context (longest path prefix match)
358
+ */
359
+ export function findContextForPath(collectionName, filePath) {
360
+ const config = loadConfig();
361
+ const collection = config.collections[collectionName];
362
+ if (!collection?.context) {
363
+ return config.global_context;
364
+ }
365
+ // Find all matching prefixes
366
+ const matches = [];
367
+ for (const [prefix, context] of Object.entries(collection.context)) {
368
+ // Normalize paths for comparison
369
+ const normalizedPath = filePath.startsWith("/") ? filePath : `/${filePath}`;
370
+ const normalizedPrefix = prefix.startsWith("/") ? prefix : `/${prefix}`;
371
+ if (normalizedPath.startsWith(normalizedPrefix)) {
372
+ matches.push({ prefix: normalizedPrefix, context });
373
+ }
374
+ }
375
+ // Return most specific match (longest prefix)
376
+ if (matches.length > 0) {
377
+ matches.sort((a, b) => b.prefix.length - a.prefix.length);
378
+ return matches[0].context;
379
+ }
380
+ // Fallback to global context
381
+ return config.global_context;
382
+ }
383
+ // ============================================================================
384
+ // Utility functions
385
+ // ============================================================================
386
+ /**
387
+ * Get the config file path (useful for error messages)
388
+ */
389
+ export function getConfigPath() {
390
+ if (configSource.type === 'inline')
391
+ return '<inline>';
392
+ return configSource.path || getConfigFilePath();
393
+ }
394
+ /**
395
+ * Check if config file exists
396
+ */
397
+ export function configExists() {
398
+ if (configSource.type === 'inline')
399
+ return true;
400
+ const path = configSource.path || getConfigFilePath();
401
+ return existsSync(path);
402
+ }
403
+ /**
404
+ * Validate a collection name
405
+ * Collection names must be valid and not contain special characters
406
+ */
407
+ export function isValidCollectionName(name) {
408
+ // Allow alphanumeric, hyphens, underscores
409
+ return /^[a-zA-Z0-9_-]+$/.test(name);
410
+ }