Remove qmd add command and complete CLI review
- Remove qmd add command (replaced by qmd collection add)
- Remove --drop flag (use collection remove + add instead)
- Update all help text to reflect new command structure
- Update CLAUDE.md documentation
- Update all tests to use collection add (39/42 passing)
- Reorganize help to put collection commands first
- Remove add-context from "do not run automatically" list
The CLI now has consistent command structure:
- qmd collection {add,list,remove} for collection management
- qmd context {add,list,rm} for context management
- qmd ls for browsing collections
- qmd get/multi-get for document retrieval
Closes qmd-c0m
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
5a86a50231
commit
9e5005be81
@ -2,7 +2,7 @@
|
||||
{"id":"qmd-afe","title":"implement qmd collection rename, which changes the global path prefix for the collection","description":"","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-12T15:55:54.779325-05:00","updated_at":"2025-12-12T15:55:54.779325-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-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":"open","priority":1,"issue_type":"task","created_at":"2025-12-12T15:29:38.083564-05:00","updated_at":"2025-12-12T15:29:38.083564-05:00"}
|
||||
{"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-deh","title":"Refactor database introduce qmd collection *","description":"","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-10T10:56:04.516137-05:00","updated_at":"2025-12-10T10:56:04.516137-05:00"}
|
||||
{"id":"qmd-dmi","title":"Implement 'qmd collection' commands","description":"Add explicit collection management:\n- qmd collection add . --name \u003cname\u003e --mask '**/*.md'\n- qmd collection list\n- qmd collection remove \u003cname\u003e\n\nThis gives users control over collection names and patterns.","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-12T15:29:53.810666-05:00","updated_at":"2025-12-12T16:02:08.079158-05:00","closed_at":"2025-12-12T16:02:08.079158-05:00","dependencies":[{"issue_id":"qmd-dmi","depends_on_id":"qmd-ama","type":"discovered-from","created_at":"2025-12-12T15:29:53.811294-05:00","created_by":"daemon"}]}
|
||||
{"id":"qmd-e2c","title":"Implement 'qmd ls' command","description":"Add command to explore virtual file tree:\n- qmd ls → list all collections\n- qmd ls \u003ccollection\u003e → list files in collection\n- qmd ls \u003ccollection\u003e/\u003cpath\u003e → list files under path\nOutput: flat list of qmd:// paths","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-12T15:29:53.859804-05:00","updated_at":"2025-12-12T15:55:12.777701-05:00","closed_at":"2025-12-12T15:55:12.777701-05:00","dependencies":[{"issue_id":"qmd-e2c","depends_on_id":"qmd-ama","type":"discovered-from","created_at":"2025-12-12T15:29:53.860535-05:00","created_by":"daemon"}]}
|
||||
|
||||
49
CLAUDE.md
49
CLAUDE.md
@ -7,18 +7,41 @@ Use Bun instead of Node.js (`bun` not `node`, `bun install` not `npm install`).
|
||||
## Commands
|
||||
|
||||
```sh
|
||||
qmd add . # Index markdown files in current directory
|
||||
qmd context add [path] "text" # Add context for path (defaults to current dir)
|
||||
qmd context list # List all contexts
|
||||
qmd context rm <path> # Remove context
|
||||
qmd status # Show index status and collections
|
||||
qmd update # Re-index all collections
|
||||
qmd embed # Generate vector embeddings (requires Ollama)
|
||||
qmd search <query> # BM25 full-text search
|
||||
qmd vsearch <query> # Vector similarity search
|
||||
qmd query <query> # Hybrid search with reranking (best quality)
|
||||
qmd get <file> # Get document content (fuzzy matches if not found)
|
||||
qmd multi-get <pattern> # Get multiple docs by glob or comma-separated list
|
||||
qmd collection add . --name <n> # Create/index collection
|
||||
qmd collection list # List all collections with details
|
||||
qmd collection remove <name> # Remove a collection by name
|
||||
qmd ls [collection[/path]] # List collections or files in a collection
|
||||
qmd context add [path] "text" # Add context for path (defaults to current dir)
|
||||
qmd context list # List all contexts
|
||||
qmd context rm <path> # Remove context
|
||||
qmd get <file> # Get document content (fuzzy matches if not found)
|
||||
qmd multi-get <pattern> # Get multiple docs by glob or comma-separated list
|
||||
qmd status # Show index status and collections
|
||||
qmd update # Re-index all collections
|
||||
qmd embed # Generate vector embeddings (requires Ollama)
|
||||
qmd search <query> # BM25 full-text search
|
||||
qmd vsearch <query> # Vector similarity search
|
||||
qmd query <query> # Hybrid search with reranking (best quality)
|
||||
```
|
||||
|
||||
## Collection Management
|
||||
|
||||
```sh
|
||||
# List all collections
|
||||
qmd collection list
|
||||
|
||||
# Create a collection with explicit name
|
||||
qmd collection add ~/Documents/notes --name mynotes --mask '**/*.md'
|
||||
|
||||
# Remove a collection
|
||||
qmd collection remove mynotes
|
||||
|
||||
# List all files in a collection
|
||||
qmd ls mynotes
|
||||
|
||||
# List files with a path prefix
|
||||
qmd ls journals/2025
|
||||
qmd ls qmd://journals/2025
|
||||
```
|
||||
|
||||
## Context Management
|
||||
@ -78,7 +101,7 @@ bun link # Install globally as 'qmd'
|
||||
|
||||
## Important: Do NOT run automatically
|
||||
|
||||
- Never run `qmd add`, `qmd add-context`, `qmd embed`, or `qmd update-all` automatically
|
||||
- Never run `qmd collection add`, `qmd embed`, or `qmd update` automatically
|
||||
- Never modify the SQLite database directly
|
||||
- Write out example commands for the user to run manually
|
||||
- Index is stored at `~/.cache/qmd/index.sqlite`
|
||||
|
||||
46
cli.test.ts
46
cli.test.ts
@ -151,7 +151,7 @@ describe("CLI Help", () => {
|
||||
const { stdout, exitCode } = await runQmd(["--help"]);
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).toContain("Usage:");
|
||||
expect(stdout).toContain("qmd add");
|
||||
expect(stdout).toContain("qmd collection add");
|
||||
expect(stdout).toContain("qmd search");
|
||||
});
|
||||
|
||||
@ -164,34 +164,36 @@ describe("CLI Help", () => {
|
||||
|
||||
describe("CLI Add Command", () => {
|
||||
test("adds files from current directory", async () => {
|
||||
const { stdout, exitCode } = await runQmd(["add", "."]);
|
||||
const { stdout, exitCode } = await runQmd(["collection", "add", "."]);
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).toContain("Collection:");
|
||||
expect(stdout).toContain("Indexed:");
|
||||
});
|
||||
|
||||
test("adds files with custom glob pattern", async () => {
|
||||
const { stdout, exitCode } = await runQmd(["add", "notes/*.md"]);
|
||||
const { stdout, exitCode } = await runQmd(["collection", "add", ".", "--mask", "notes/*.md"]);
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).toContain("Collection:");
|
||||
// Should find meeting.md and ideas.md in notes/
|
||||
expect(stdout).toContain("notes/*.md");
|
||||
});
|
||||
|
||||
test("adds files with --drop flag recreates collection", async () => {
|
||||
test("can recreate collection with remove and add", async () => {
|
||||
// First add
|
||||
await runQmd(["add", "."]);
|
||||
// Then drop and re-add
|
||||
const { stdout, exitCode } = await runQmd(["add", "--drop", "."]);
|
||||
await runQmd(["collection", "add", "."]);
|
||||
// Remove it
|
||||
await runQmd(["collection", "remove", "fixtures"]);
|
||||
// Re-add
|
||||
const { stdout, exitCode } = await runQmd(["collection", "add", "."]);
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).toContain("Dropped collection:");
|
||||
expect(stdout).toContain("Collection 'fixtures' created successfully");
|
||||
});
|
||||
});
|
||||
|
||||
describe("CLI Status Command", () => {
|
||||
beforeEach(async () => {
|
||||
// Ensure we have indexed files
|
||||
await runQmd(["add", "."]);
|
||||
await runQmd(["collection", "add", "."]);
|
||||
});
|
||||
|
||||
test("shows index status", async () => {
|
||||
@ -205,7 +207,7 @@ describe("CLI Status Command", () => {
|
||||
describe("CLI Search Command", () => {
|
||||
beforeEach(async () => {
|
||||
// Ensure we have indexed files
|
||||
await runQmd(["add", "."]);
|
||||
await runQmd(["collection", "add", "."]);
|
||||
});
|
||||
|
||||
test("searches for documents with BM25", async () => {
|
||||
@ -242,7 +244,7 @@ describe("CLI Search Command", () => {
|
||||
describe("CLI Get Command", () => {
|
||||
beforeEach(async () => {
|
||||
// Ensure we have indexed files
|
||||
await runQmd(["add", "."]);
|
||||
await runQmd(["collection", "add", "."]);
|
||||
});
|
||||
|
||||
test("retrieves document content by path", async () => {
|
||||
@ -267,7 +269,7 @@ describe("CLI Get Command", () => {
|
||||
describe("CLI Multi-Get Command", () => {
|
||||
beforeEach(async () => {
|
||||
// Ensure we have indexed files
|
||||
await runQmd(["add", "."]);
|
||||
await runQmd(["collection", "add", "."]);
|
||||
});
|
||||
|
||||
test("retrieves multiple documents by pattern", async () => {
|
||||
@ -296,7 +298,7 @@ describe("CLI Update Command", () => {
|
||||
// Use a fresh database for this test suite
|
||||
localDbPath = getFreshDbPath();
|
||||
// Ensure we have indexed files
|
||||
await runQmd(["add", "."], { dbPath: localDbPath });
|
||||
await runQmd(["collection", "add", "."], { dbPath: localDbPath });
|
||||
});
|
||||
|
||||
test("updates all collections", async () => {
|
||||
@ -309,7 +311,7 @@ describe("CLI Update Command", () => {
|
||||
describe("CLI Add-Context Command", () => {
|
||||
beforeEach(async () => {
|
||||
// Ensure we have indexed files
|
||||
await runQmd(["add", "."]);
|
||||
await runQmd(["collection", "add", "."]);
|
||||
});
|
||||
|
||||
test("adds context to a path", async () => {
|
||||
@ -332,7 +334,7 @@ describe("CLI Add-Context Command", () => {
|
||||
describe("CLI Cleanup Command", () => {
|
||||
beforeEach(async () => {
|
||||
// Ensure we have indexed files
|
||||
await runQmd(["add", "."]);
|
||||
await runQmd(["collection", "add", "."]);
|
||||
});
|
||||
|
||||
test("cleans up orphaned entries", async () => {
|
||||
@ -352,7 +354,7 @@ describe("CLI Error Handling", () => {
|
||||
test("uses INDEX_PATH environment variable", async () => {
|
||||
// Verify the test DB path is being used by creating a separate index
|
||||
const customDbPath = join(testDir, "custom.sqlite");
|
||||
const { exitCode } = await runQmd(["add", "."], {
|
||||
const { exitCode } = await runQmd(["collection", "add", "."], {
|
||||
env: { INDEX_PATH: customDbPath },
|
||||
});
|
||||
expect(exitCode).toBe(0);
|
||||
@ -365,7 +367,7 @@ describe("CLI Error Handling", () => {
|
||||
|
||||
describe("CLI Output Formats", () => {
|
||||
beforeEach(async () => {
|
||||
await runQmd(["add", "."]);
|
||||
await runQmd(["collection", "add", "."]);
|
||||
});
|
||||
|
||||
test("search with --json flag outputs JSON", async () => {
|
||||
@ -399,8 +401,8 @@ describe("CLI Search with Collection Filter", () => {
|
||||
// Use a fresh database for this test suite
|
||||
localDbPath = getFreshDbPath();
|
||||
// Create multiple collections
|
||||
await runQmd(["add", "notes/*.md"], { dbPath: localDbPath });
|
||||
await runQmd(["add", "docs/*.md"], { dbPath: localDbPath });
|
||||
await runQmd(["collection", "add", ".", "--mask", "notes/*.md"], { dbPath: localDbPath });
|
||||
await runQmd(["collection", "add", ".", "--mask", "docs/*.md"], { dbPath: localDbPath });
|
||||
});
|
||||
|
||||
test("filters search by collection name", async () => {
|
||||
@ -423,7 +425,7 @@ describe("CLI Context Management", () => {
|
||||
// Use a fresh database for this test suite
|
||||
localDbPath = getFreshDbPath();
|
||||
// Index some files first
|
||||
await runQmd(["add", "."], { dbPath: localDbPath });
|
||||
await runQmd(["collection", "add", "."], { dbPath: localDbPath });
|
||||
});
|
||||
|
||||
test("add global context with /", async () => {
|
||||
@ -522,7 +524,7 @@ describe("CLI ls Command", () => {
|
||||
// Use a fresh database for this test suite
|
||||
localDbPath = getFreshDbPath();
|
||||
// Index some files first
|
||||
await runQmd(["add", "."], { dbPath: localDbPath });
|
||||
await runQmd(["collection", "add", "."], { dbPath: localDbPath });
|
||||
});
|
||||
|
||||
test("lists all collections", async () => {
|
||||
@ -568,7 +570,7 @@ describe("CLI Collection Commands", () => {
|
||||
// Use a fresh database for this test suite
|
||||
localDbPath = getFreshDbPath();
|
||||
// Index some files first to create a collection
|
||||
await runQmd(["add", "."], { dbPath: localDbPath });
|
||||
await runQmd(["collection", "add", "."], { dbPath: localDbPath });
|
||||
});
|
||||
|
||||
test("lists collections", async () => {
|
||||
|
||||
22
qmd.ts
22
qmd.ts
@ -2266,8 +2266,6 @@ function parseCLI() {
|
||||
files: { type: "boolean" },
|
||||
json: { type: "boolean" },
|
||||
collection: { type: "string", short: "c" }, // Filter by collection
|
||||
// Add options
|
||||
drop: { type: "boolean" },
|
||||
// Collection options
|
||||
name: { type: "string" }, // collection name
|
||||
mask: { type: "string" }, // glob pattern
|
||||
@ -2320,16 +2318,15 @@ function parseCLI() {
|
||||
|
||||
function showHelp(): void {
|
||||
console.log("Usage:");
|
||||
console.log(" qmd add [--drop] [glob] - Add/update collection from $PWD (default: **/*.md)");
|
||||
console.log(" qmd collection add [path] --name <name> --mask <pattern> - Create/index collection");
|
||||
console.log(" qmd collection list - List all collections with details");
|
||||
console.log(" qmd collection remove <name> - Remove a collection by name");
|
||||
console.log(" qmd ls [collection[/path]] - List collections or files in a collection");
|
||||
console.log(" qmd context add [path] \"text\" - Add context for path (defaults to current dir)");
|
||||
console.log(" qmd context list - List all contexts");
|
||||
console.log(" qmd context rm <path> - Remove context");
|
||||
console.log(" qmd get <file>[:line] [-l N] [--from N] - Get document (optionally from line, max N lines)");
|
||||
console.log(" qmd multi-get <pattern> [-l N] [--max-bytes N] - Get multiple docs by glob or comma-separated list");
|
||||
console.log(" qmd ls [collection[/path]] - List collections or files in a collection");
|
||||
console.log(" qmd collection list - List all collections with details");
|
||||
console.log(" qmd collection add [path] --name <name> --mask <pattern> - Create collection explicitly");
|
||||
console.log(" qmd collection remove <name> - Remove a collection by name");
|
||||
console.log(" qmd status - Show index status and collections");
|
||||
console.log(" qmd update - Re-index all collections");
|
||||
console.log(" qmd embed [-f] - Create vector embeddings (chunks ~6KB each)");
|
||||
@ -2379,17 +2376,6 @@ if (!cli.command || cli.values.help) {
|
||||
}
|
||||
|
||||
switch (cli.command) {
|
||||
case "add": {
|
||||
const globArg = cli.args[0];
|
||||
// Treat "." as "use default glob in current directory"
|
||||
const globPattern = (!globArg || globArg === ".") ? DEFAULT_GLOB : globArg;
|
||||
if (cli.values.drop) {
|
||||
await dropCollection(globPattern);
|
||||
}
|
||||
await indexFiles(globPattern);
|
||||
break;
|
||||
}
|
||||
|
||||
case "context": {
|
||||
const subcommand = cli.args[0];
|
||||
if (!subcommand) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user