diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 1af9615..3182c4c 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,11 +1,17 @@ {"id":"qmd-0ic","title":"in qmd status, list all the additonal contexts under the collections that match","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-12T16:41:42.126194-05:00","updated_at":"2025-12-12T17:14:48.268119-05:00","closed_at":"2025-12-12T17:14:48.268119-05:00"} {"id":"qmd-18s","title":"Move cleanup/maintenance DB operations to store.ts","description":"Move cleanup operations from cleanup() command to store.ts. Create methods like deleteInactiveDocuments(), vacuumDatabase(), cleanupOrphanedContent(), etc.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-12T16:36:21.815781-05:00","updated_at":"2025-12-12T16:42:36.896806-05:00","closed_at":"2025-12-12T16:42:36.896806-05:00","dependencies":[{"issue_id":"qmd-18s","depends_on_id":"qmd-29c","type":"parent-child","created_at":"2025-12-12T16:37:03.014111-05:00","created_by":"daemon"}]} +{"id":"qmd-1xd","title":"Update tests for YAML-based collections","description":"Update all tests to use YAML config instead of DB collections. Update test helpers to create temporary YAML configs.","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-13T09:54:53.349545-05:00","updated_at":"2025-12-13T09:54:53.349545-05:00","dependencies":[{"issue_id":"qmd-1xd","depends_on_id":"qmd-thw","type":"blocks","created_at":"2025-12-13T09:55:08.14305-05:00","created_by":"daemon"}]} {"id":"qmd-29c","title":"Move all database operations from qmd.ts to store.ts","description":"Currently qmd.ts has ~70 direct database operations (db.prepare, db.exec). All database operations should be moved to store.ts to improve separation of concerns. qmd.ts should only use high-level methods from store.ts that don't require direct SQL knowledge.","notes":"Phase 1 complete: Moved collection operations (listCollections, removeCollection, renameCollection) to store.ts. Created 4 subtasks for remaining work: document indexing, context management, embeddings, and cleanup operations.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-12T16:32:13.722223-05:00","updated_at":"2025-12-12T16:49:53.829124-05:00","closed_at":"2025-12-12T16:49:53.829124-05:00"} +{"id":"qmd-3z9","title":"Design YAML schema and create collections.ts module","description":"Create collections.ts to manage YAML-based collection configuration at ~/.config/qmd/index.yml. Define TypeScript types for collections and contexts. Implement load/save functions with Bun's native YAML support.","design":"YAML structure:\n```yaml\n# Global context for all collections\nglobal_context: \"...\"\n\ncollections:\n name:\n path: /absolute/path\n pattern: \"**/*.md\"\n context:\n \"/path/prefix\": \"Description\"\n \"/\": \"Root context\"\n```\n\nTypeScript types:\n- Collection: { path, pattern, context }\n- CollectionConfig: { global_context?, collections }\n- Functions: loadConfig(), saveConfig(), getCollection(), listCollections()","status":"in_progress","priority":1,"issue_type":"task","created_at":"2025-12-13T09:54:52.586027-05:00","updated_at":"2025-12-13T09:56:11.735574-05:00"} {"id":"qmd-4ru","title":"Update document retrieval for new schema","description":"Functions like getDocument, findDocument, getMultipleDocuments need to work with new schema (path instead of filepath, content joins, virtual paths).","status":"closed","priority":0,"issue_type":"task","created_at":"2025-12-12T15:29:53.911881-05:00","updated_at":"2025-12-12T15:56:11.054888-05:00","closed_at":"2025-12-12T15:56:11.054888-05:00","dependencies":[{"issue_id":"qmd-4ru","depends_on_id":"qmd-ama","type":"discovered-from","created_at":"2025-12-12T15:29:53.912607-05:00","created_by":"daemon"}]} {"id":"qmd-4u4","title":"Move embedding/vector DB operations to store.ts","description":"Move vector indexing DB operations from vectorIndex() to store.ts. Create methods like getHashesForEmbedding(), insertEmbedding(), clearEmbeddings(), etc.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-12T16:36:21.683434-05:00","updated_at":"2025-12-12T16:42:40.42653-05:00","closed_at":"2025-12-12T16:42:40.42653-05:00","dependencies":[{"issue_id":"qmd-4u4","depends_on_id":"qmd-29c","type":"parent-child","created_at":"2025-12-12T16:37:02.944591-05:00","created_by":"daemon"}]} +{"id":"qmd-6s5","title":"Export current database to index.yml","description":"Write a script to export current collections and path_contexts from SQLite to ~/.config/qmd/index.yml format. Include all collection metadata and contexts.","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-13T09:54:52.707844-05:00","updated_at":"2025-12-13T09:54:52.707844-05:00","dependencies":[{"issue_id":"qmd-6s5","depends_on_id":"qmd-3z9","type":"blocks","created_at":"2025-12-13T09:55:07.606834-05:00","created_by":"daemon"}]} {"id":"qmd-7ss","title":"remove all the symlinks and stuff in the git repo, clean up the root directory","description":"","status":"closed","priority":4,"issue_type":"task","created_at":"2025-12-12T16:40:00.744982-05:00","updated_at":"2025-12-12T17:11:18.034215-05:00","closed_at":"2025-12-12T17:11:18.034215-05:00"} +{"id":"qmd-8eu","title":"Update documents table schema for collection names","description":"Change documents.collection_id (integer FK) to documents.collection (text). Update all queries and indices. Keep backwards compatibility during transition.","design":"Schema change:\n- Add `collection TEXT` column\n- Migrate data: UPDATE documents SET collection = (SELECT name FROM collections WHERE id = collection_id)\n- Drop collection_id column\n- Update FTS5 trigger\n- Update all queries in store.ts","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-13T09:54:52.830305-05:00","updated_at":"2025-12-13T09:54:52.830305-05:00","dependencies":[{"issue_id":"qmd-8eu","depends_on_id":"qmd-6s5","type":"blocks","created_at":"2025-12-13T09:55:07.662048-05:00","created_by":"daemon"}]} +{"id":"qmd-9ua","title":"Update all qmd commands for YAML-based collections","description":"Update qmd.ts commands: collection add/list/remove/rename, status, update, ls. All should use collections.ts instead of store.ts collection functions.","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-13T09:54:53.14644-05:00","updated_at":"2025-12-13T09:54:53.14644-05:00","dependencies":[{"issue_id":"qmd-9ua","depends_on_id":"qmd-u84","type":"blocks","created_at":"2025-12-13T09:55:07.893268-05:00","created_by":"daemon"},{"issue_id":"qmd-9ua","depends_on_id":"qmd-oxy","type":"blocks","created_at":"2025-12-13T09:55:07.942221-05:00","created_by":"daemon"}]} {"id":"qmd-afe","title":"implement qmd collection rename, which changes the global path prefix for the collection","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-12T15:55:54.779325-05:00","updated_at":"2025-12-12T16:29:24.153196-05:00","closed_at":"2025-12-12T16:29:24.153196-05:00"} {"id":"qmd-ama","title":"Refactor database system","description":"All documents should be stored as content addressable hash, e.g. hash, doc, created_at,\n┃ updated_at. documents should be a file system layer on top e.g. collection, path, hash,\n┃ created_at, updated_at. (collection,path)\n┃\n┃\n\n┃ All documents should be stored as content addressable hash, e.g. hash, doc, created_at,\n┃ updated_at. documents should be a file system layer on top e.g. collection_id, path, hash,\n┃ created_at, updated_at. (collection,path) is unique. There is also collection which stores PWD\n┃ + glob pattern, name (\\w+). Every document is treated as path qmd://collection.name/","notes":"## Completed\n- ✅ Implemented content-addressable storage (content table with hash→doc mapping)\n- ✅ Refactored documents table as file system layer (collection_id, path, hash)\n- ✅ Added collection names (e.g., \"pages\", \"journals\", \"archive\")\n- ✅ Implemented virtual paths (qmd://collection-name/path/to/file.md)\n- ✅ Added hierarchical context support (collection-scoped)\n- ✅ Successfully migrated existing database\n- ✅ Updated search functions to work with new schema\n- ✅ Updated indexing logic to use content-addressable storage\n- ✅ Orphaned content hash cleanup\n\n## Still TODO\n- Fix migration SQL to properly extract basename (currently needs manual fix)\n- Implement `qmd collection add . --name \u003cname\u003e --mask '**/*.md'`\n- Implement `qmd ls [path]` for exploring virtual file tree","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-10T10:57:35.497489-05:00","updated_at":"2025-12-12T15:39:48.879143-05:00","closed_at":"2025-12-12T15:39:48.879143-05:00"} +{"id":"qmd-bs8","title":"Update documentation for YAML configuration","description":"Update CLAUDE.md, README.md with new YAML configuration approach. Document index.yml format and manual editing instructions.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-13T09:54:53.449584-05:00","updated_at":"2025-12-13T09:54:53.449584-05:00","dependencies":[{"issue_id":"qmd-bs8","depends_on_id":"qmd-1xd","type":"blocks","created_at":"2025-12-13T09:55:08.264615-05:00","created_by":"daemon"}]} {"id":"qmd-bx1","title":"Fix migration SQL for proper basename extraction","description":"The migration currently generates collection names incorrectly (uses full path instead of basename). Need to fix the SQL in migrateToContentAddressable to properly extract the directory basename.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-12T15:29:53.757723-05:00","updated_at":"2025-12-12T15:50:29.349134-05:00","closed_at":"2025-12-12T15:50:29.349134-05:00","dependencies":[{"issue_id":"qmd-bx1","depends_on_id":"qmd-ama","type":"discovered-from","created_at":"2025-12-12T15:29:53.758524-05:00","created_by":"daemon"}]} {"id":"qmd-c0m","title":"Comprehensive CLI review and consistency pass","description":"Review entire CLI command structure:\n- Consistent naming (add vs create, remove vs delete)\n- Consistent flag usage (--name, --mask, etc)\n- Update help text for all commands\n- Ensure virtual paths work everywhere\n- Test all commands end-to-end","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-12T15:29:38.083564-05:00","updated_at":"2025-12-12T16:06:51.544695-05:00","closed_at":"2025-12-12T16:06:51.544695-05:00"} {"id":"qmd-clr","title":"fix embed","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-12T16:14:55.292114-05:00","updated_at":"2025-12-12T16:31:27.661829-05:00","closed_at":"2025-12-12T16:31:27.661829-05:00"} @@ -17,10 +23,13 @@ {"id":"qmd-j9z","title":"Add unit tests for content addressable hashes","description":"add same file from multiple places and verify that they both point at same hash. drop one collection and the content stays.","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-12T15:39:15.459504-05:00","updated_at":"2025-12-12T16:21:35.473776-05:00","closed_at":"2025-12-12T16:21:35.473776-05:00"} {"id":"qmd-kf8","title":"Move document indexing DB operations to store.ts","description":"Move INSERT/UPDATE/DELETE operations for documents and content tables from indexFiles() to store.ts. Create methods like insertDocument(), updateDocument(), deactivateDocuments(), etc.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-12T16:36:14.558702-05:00","updated_at":"2025-12-12T16:45:38.830978-05:00","closed_at":"2025-12-12T16:45:38.830978-05:00","dependencies":[{"issue_id":"qmd-kf8","depends_on_id":"qmd-29c","type":"parent-child","created_at":"2025-12-12T16:37:02.770251-05:00","created_by":"daemon"}]} {"id":"qmd-ltg","title":"look for missing context","description":"i ran qmd context list and thats only one bit of context, i had a lot more. i think the path matching isn't quite working right","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-12T16:42:57.324769-05:00","updated_at":"2025-12-12T17:16:27.835047-05:00","closed_at":"2025-12-12T17:16:27.835047-05:00"} +{"id":"qmd-oxy","title":"Update context system to use YAML","description":"Remove path_contexts table. Implement context management in collections.ts. Update context add/list/rm commands to modify YAML file instead of database.","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-13T09:54:53.042839-05:00","updated_at":"2025-12-13T09:54:53.042839-05:00","dependencies":[{"issue_id":"qmd-oxy","depends_on_id":"qmd-3z9","type":"blocks","created_at":"2025-12-13T09:55:07.842488-05:00","created_by":"daemon"}]} {"id":"qmd-p1h","title":"Create collection add|remove","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-10T10:57:00.717864-05:00","updated_at":"2025-12-12T16:12:00.557003-05:00","closed_at":"2025-12-12T16:12:00.557003-05:00"} {"id":"qmd-rck","title":"move the source files to src/*, clean up teh directory","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-12T16:40:19.198119-05:00","updated_at":"2025-12-12T17:12:22.502746-05:00","closed_at":"2025-12-12T17:12:22.502746-05:00"} {"id":"qmd-rhd","title":"Fix 'qmd status' output for new schema","description":"Update status to show collections by name, cleaner context display, virtual path examples.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-12T15:29:54.020596-05:00","updated_at":"2025-12-12T16:13:28.08389-05:00","closed_at":"2025-12-12T16:13:28.08389-05:00","dependencies":[{"issue_id":"qmd-rhd","depends_on_id":"qmd-ama","type":"discovered-from","created_at":"2025-12-12T15:29:54.021095-05:00","created_by":"daemon"}]} {"id":"qmd-s1y","title":"Update 'qmd add-context' for collection scoping","description":"Update add-context to work with collection-scoped contexts using new path_contexts schema.","notes":"Refactoring to:\n- qmd context add [path] \"text\" (defaults to current collection if in one)\n- qmd context list\n- qmd context rm \u003cpath\u003e\n- Support \"/\" for global/system context\n- Auto-detect collection from pwd","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-12T15:29:54.076582-05:00","updated_at":"2025-12-12T15:37:47.683263-05:00","closed_at":"2025-12-12T15:37:47.683263-05:00"} +{"id":"qmd-thw","title":"Drop collections and path_contexts tables","description":"Remove collections and path_contexts tables from schema. Update initDb() to not create these tables. Only keep documents, content, and search indices.","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-13T09:54:53.247136-05:00","updated_at":"2025-12-13T09:54:53.247136-05:00","dependencies":[{"issue_id":"qmd-thw","depends_on_id":"qmd-9ua","type":"blocks","created_at":"2025-12-13T09:55:08.027101-05:00","created_by":"daemon"}]} +{"id":"qmd-u84","title":"Refactor store.ts to use collections.ts","description":"Replace all collection DB queries with collections.ts calls. Remove getCollectionById, getCollectionByName, listCollections DB functions. Use YAML config instead.","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-13T09:54:52.936782-05:00","updated_at":"2025-12-13T09:54:52.936782-05:00","dependencies":[{"issue_id":"qmd-u84","depends_on_id":"qmd-3z9","type":"blocks","created_at":"2025-12-13T09:55:07.720439-05:00","created_by":"daemon"},{"issue_id":"qmd-u84","depends_on_id":"qmd-8eu","type":"blocks","created_at":"2025-12-13T09:55:07.782051-05:00","created_by":"daemon"}]} {"id":"qmd-vro","title":"Update 'qmd get' to support virtual paths","description":"Allow qmd get to accept both virtual paths (qmd://journals/...) and filesystem paths, plus fuzzy matching by filename.","status":"closed","priority":0,"issue_type":"task","created_at":"2025-12-12T15:29:53.963113-05:00","updated_at":"2025-12-12T15:47:29.178955-05:00","closed_at":"2025-12-12T15:47:29.178955-05:00","dependencies":[{"issue_id":"qmd-vro","depends_on_id":"qmd-ama","type":"discovered-from","created_at":"2025-12-12T15:29:53.963641-05:00","created_by":"daemon"}]} {"id":"qmd-x19","title":"Update 'qmd add-context' for collection-scoped contexts","description":"Update add-context to work with collections:\n- qmd add-context \u003ccollection\u003e/\u003cpath\u003e \"context description\"\n- Support both virtual and filesystem paths\n- Update to use new path_contexts schema","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-12T15:29:38.142575-05:00","updated_at":"2025-12-12T15:53:00.525001-05:00","closed_at":"2025-12-12T15:53:00.525001-05:00"} {"id":"qmd-x64","title":"for each collection, on update, check if there is a .git directory, if so write out the git status, add --pull as a qmd update --pull parameter which also executes git pull before reindexing\n","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-12T17:04:15.994054-05:00","updated_at":"2025-12-12T17:14:40.107181-05:00","closed_at":"2025-12-12T17:14:40.107181-05:00"} diff --git a/example-index.yml b/example-index.yml new file mode 100644 index 0000000..eb1b794 --- /dev/null +++ b/example-index.yml @@ -0,0 +1,59 @@ +# QMD Collections Configuration +# Location: ~/.config/qmd/index.yml +# +# This file defines all collections and their contexts. +# You can manually edit this file - changes take effect immediately. + +# Global context applied to all collections +# Use this for universal search instructions or patterns +global_context: "If you see relevant [[WikiWord]] you can do a search for WikiWord to get more context on the matter" + +# Collection definitions +collections: + # Meeting notes + Meetings: + path: /Users/tobi/Documents/Meetings + pattern: "**/*.md" + context: + "/": "Meeting notes and summaries" + + # Archived content from Shopify + archive: + path: /Users/tobi/src/github.com/Shopify/archive/obsidian/archive + pattern: "**/*.md" + context: + # Context can be defined at any path level + "/Board of Directors": "Public communications with the Shopify BOD" + "/Context/": "Shopify Internal Podcasts, almost all of them hosted by Tobi" + "/Summit/": "Tobi's major internal Shopify Summit Keynotes" + "/": "Shopify archive - historical documents and communications" + + # Daily journal entries + journals: + path: /Users/tobi/src/github.com/tobi/Brain/journals + pattern: "**/*.md" + context: + "/2024": "Daily notes from 2024" + "/2025": "Daily notes from 2025" + "/": "Logseq - daily notes. Unstructured text in logseq bullet point format" + + # Knowledge base pages + pages: + path: /Users/tobi/src/github.com/tobi/Brain/pages + pattern: "**/*.md" + context: + "/": "Logseq knowledge base - structured notes and reference material" + + # Technical RFCs + rfcs: + path: /Users/tobi/src/github.com/Shopify/codex/rfcs + pattern: "**/*.md" + context: + "/": "Request for Comments - technical design documents" + + # Thematic collections + themes: + path: /Users/tobi/src/github.com/Shopify/codex/themes + pattern: "**/*.md" + context: + "/": "Thematic collections of important concepts and discussions" diff --git a/package.json b/package.json index d3dd836..f3c3d85 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.24.3", "sqlite-vec": "^0.1.7-alpha.2", + "yaml": "^2.8.2", "zod": "^4.1.13" }, "optionalDependencies": { diff --git a/src/collections.ts b/src/collections.ts new file mode 100644 index 0000000..6305688 --- /dev/null +++ b/src/collections.ts @@ -0,0 +1,366 @@ +/** + * Collections configuration management + * + * This module manages the YAML-based collection configuration at ~/.config/qmd/index.yml. + * Collections define which directories to index and their associated contexts. + */ + +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; +import { join } from "path"; +import { homedir } from "os"; +import YAML from "yaml"; + +// ============================================================================ +// Types +// ============================================================================ + +/** + * Context definitions for a collection + * Key is path prefix (e.g., "/", "/2024", "/Board of Directors") + * Value is the context description + */ +export type ContextMap = Record; + +/** + * A single collection configuration + */ +export interface Collection { + path: string; // Absolute path to index + pattern: string; // Glob pattern (e.g., "**/*.md") + context?: ContextMap; // Optional context definitions +} + +/** + * The complete configuration file structure + */ +export interface CollectionConfig { + global_context?: string; // Context applied to all collections + collections: Record; // Collection name -> config +} + +/** + * Collection with its name (for return values) + */ +export interface NamedCollection extends Collection { + name: string; +} + +// ============================================================================ +// Configuration paths +// ============================================================================ + +const CONFIG_DIR = join(homedir(), ".config", "qmd"); +const CONFIG_PATH = join(CONFIG_DIR, "index.yml"); + +/** + * Ensure config directory exists + */ +function ensureConfigDir(): void { + if (!existsSync(CONFIG_DIR)) { + mkdirSync(CONFIG_DIR, { recursive: true }); + } +} + +// ============================================================================ +// Core functions +// ============================================================================ + +/** + * Load configuration from ~/.config/qmd/index.yml + * Returns empty config if file doesn't exist + */ +export function loadConfig(): CollectionConfig { + if (!existsSync(CONFIG_PATH)) { + return { collections: {} }; + } + + try { + const content = readFileSync(CONFIG_PATH, "utf-8"); + const config = YAML.parse(content) as CollectionConfig; + + // Ensure collections object exists + if (!config.collections) { + config.collections = {}; + } + + return config; + } catch (error) { + throw new Error(`Failed to parse ${CONFIG_PATH}: ${error}`); + } +} + +/** + * Save configuration to ~/.config/qmd/index.yml + */ +export function saveConfig(config: CollectionConfig): void { + ensureConfigDir(); + + try { + const yaml = YAML.stringify(config, { + indent: 2, + lineWidth: 0, // Don't wrap lines + }); + writeFileSync(CONFIG_PATH, yaml, "utf-8"); + } catch (error) { + throw new Error(`Failed to write ${CONFIG_PATH}: ${error}`); + } +} + +/** + * Get a specific collection by name + * Returns null if not found + */ +export function getCollection(name: string): NamedCollection | null { + const config = loadConfig(); + const collection = config.collections[name]; + + if (!collection) { + return null; + } + + return { name, ...collection }; +} + +/** + * List all collections + */ +export function listCollections(): NamedCollection[] { + const config = loadConfig(); + return Object.entries(config.collections).map(([name, collection]) => ({ + name, + ...collection, + })); +} + +/** + * Add or update a collection + */ +export function addCollection( + name: string, + path: string, + pattern: string = "**/*.md" +): void { + const config = loadConfig(); + + config.collections[name] = { + path, + pattern, + context: config.collections[name]?.context, // Preserve existing context + }; + + saveConfig(config); +} + +/** + * Remove a collection + */ +export function removeCollection(name: string): boolean { + const config = loadConfig(); + + if (!config.collections[name]) { + return false; + } + + delete config.collections[name]; + saveConfig(config); + return true; +} + +/** + * Rename a collection + */ +export function renameCollection(oldName: string, newName: string): boolean { + const config = loadConfig(); + + if (!config.collections[oldName]) { + return false; + } + + if (config.collections[newName]) { + throw new Error(`Collection '${newName}' already exists`); + } + + config.collections[newName] = config.collections[oldName]; + delete config.collections[oldName]; + saveConfig(config); + return true; +} + +// ============================================================================ +// Context management +// ============================================================================ + +/** + * Get global context + */ +export function getGlobalContext(): string | undefined { + const config = loadConfig(); + return config.global_context; +} + +/** + * Set global context + */ +export function setGlobalContext(context: string | undefined): void { + const config = loadConfig(); + config.global_context = context; + saveConfig(config); +} + +/** + * Get all contexts for a collection + */ +export function getContexts(collectionName: string): ContextMap | undefined { + const collection = getCollection(collectionName); + return collection?.context; +} + +/** + * Add or update a context for a specific path in a collection + */ +export function addContext( + collectionName: string, + pathPrefix: string, + contextText: string +): boolean { + const config = loadConfig(); + const collection = config.collections[collectionName]; + + if (!collection) { + return false; + } + + if (!collection.context) { + collection.context = {}; + } + + collection.context[pathPrefix] = contextText; + saveConfig(config); + return true; +} + +/** + * Remove a context from a collection + */ +export function removeContext( + collectionName: string, + pathPrefix: string +): boolean { + const config = loadConfig(); + const collection = config.collections[collectionName]; + + if (!collection?.context?.[pathPrefix]) { + return false; + } + + delete collection.context[pathPrefix]; + + // Remove empty context object + if (Object.keys(collection.context).length === 0) { + delete collection.context; + } + + saveConfig(config); + return true; +} + +/** + * List all contexts across all collections + */ +export function listAllContexts(): Array<{ + collection: string; + path: string; + context: string; +}> { + const config = loadConfig(); + const results: Array<{ collection: string; path: string; context: string }> = []; + + // Add global context if present + if (config.global_context) { + results.push({ + collection: "*", + path: "/", + context: config.global_context, + }); + } + + // Add collection contexts + for (const [name, collection] of Object.entries(config.collections)) { + if (collection.context) { + for (const [path, context] of Object.entries(collection.context)) { + results.push({ + collection: name, + path, + context, + }); + } + } + } + + return results; +} + +/** + * Find best matching context for a given collection and path + * Returns the most specific matching context (longest path prefix match) + */ +export function findContextForPath( + collectionName: string, + filePath: string +): string | undefined { + const config = loadConfig(); + const collection = config.collections[collectionName]; + + if (!collection?.context) { + return config.global_context; + } + + // Find all matching prefixes + const matches: Array<{ prefix: string; context: string }> = []; + + for (const [prefix, context] of Object.entries(collection.context)) { + // Normalize paths for comparison + const normalizedPath = filePath.startsWith("/") ? filePath : `/${filePath}`; + const normalizedPrefix = prefix.startsWith("/") ? prefix : `/${prefix}`; + + if (normalizedPath.startsWith(normalizedPrefix)) { + matches.push({ prefix: normalizedPrefix, context }); + } + } + + // Return most specific match (longest prefix) + if (matches.length > 0) { + matches.sort((a, b) => b.prefix.length - a.prefix.length); + return matches[0].context; + } + + // Fallback to global context + return config.global_context; +} + +// ============================================================================ +// Utility functions +// ============================================================================ + +/** + * Get the config file path (useful for error messages) + */ +export function getConfigPath(): string { + return CONFIG_PATH; +} + +/** + * Check if config file exists + */ +export function configExists(): boolean { + return existsSync(CONFIG_PATH); +} + +/** + * Validate a collection name + * Collection names must be valid and not contain special characters + */ +export function isValidCollectionName(name: string): boolean { + // Allow alphanumeric, hyphens, underscores + return /^[a-zA-Z0-9_-]+$/.test(name); +}