Sfoglia il codice sorgente

Add YAML-based collections configuration system

- Create src/collections.ts module for managing collections in YAML
- Collections defined in ~/.config/qmd/index.yml instead of SQLite
- Support for nested contexts at any path level
- Global context applies to all collections
- Functions: load/save config, add/remove/rename collections
- Context management: add, remove, find best match for path
- Add yaml package dependency
- Include example-index.yml showing the clean YAML format

This is the foundation for removing collections and path_contexts
tables from SQLite, moving all configuration to YAML.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Tobi Lutke 5 mesi fa
parent
commit
691c56d051
4 ha cambiato i file con 435 aggiunte e 0 eliminazioni
  1. 9 0
      .beads/issues.jsonl
  2. 59 0
      example-index.yml
  3. 1 0
      package.json
  4. 366 0
      src/collections.ts

+ 9 - 0
.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"}

+ 59 - 0
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"

+ 1 - 0
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": {

+ 366 - 0
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<string, string>;
+
+/**
+ * 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<string, Collection>;    // 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);
+}