@grimoire-ai/cli
Advanced tools
+139
| # Grimoire AI | ||
| Local-first, AI-native requirements management for software projects. | ||
| Grimoire stores project knowledge — features, requirements, tasks, and architecture decisions — as structured markdown files in your git repository. A CLI designed for AI coding agents provides fast, structured access to project context. | ||
| **Grimoire answers the question: "How does a new AI agent session get oriented in a project — fast?"** | ||
| ## Quick start | ||
| ```bash | ||
| # Install the CLI | ||
| npm install -g @grimoire-ai/cli | ||
| # Initialize grimoire in your project | ||
| grimoire init --name "My Project" | ||
| # Install AI agent skills (agentskills.io) | ||
| npx skills add mikevalstar/grimoire | ||
| ``` | ||
| ## Usage | ||
| ### Read project context | ||
| ```bash | ||
| grimoire overview # Project overview | ||
| grimoire feature list # List all features | ||
| grimoire task list --status todo # Find open work | ||
| grimoire search "authentication" # Full-text + semantic search | ||
| grimoire context "implement OAuth login" # AI-optimized context retrieval | ||
| ``` | ||
| ### Create documents | ||
| ```bash | ||
| grimoire feature create --title "User Authentication" --priority high --tag security | ||
| grimoire requirement create --title "OAuth 2.0 Login" --feature feat-xxxxx-user-authentication | ||
| grimoire task create --title "Setup Google OAuth" --requirement req-xxxxx-oauth-20-login | ||
| grimoire decision create --title "Use JWT Over Sessions" --status accepted | ||
| ``` | ||
| ### Update and track progress | ||
| ```bash | ||
| grimoire task update <id> --status in-progress | ||
| grimoire log <id> "Implemented OAuth callback handler" --author claude-code | ||
| grimoire comment <id> "Should we support SAML as well?" | ||
| ``` | ||
| ### Web UI | ||
| ```bash | ||
| grimoire ui # Launch web dashboard on port 4444 | ||
| grimoire ui --port 8080 # Custom port | ||
| ``` | ||
| The web UI provides a visual dashboard with document browsing, filtering, sorting, and rendered markdown — useful for reviewing project state at a glance. | ||
| ### Validate | ||
| ```bash | ||
| grimoire validate # Check for broken links, missing fields | ||
| ``` | ||
| ## Document types | ||
| | Type | Directory | Purpose | | ||
| | --------------- | --------------- | ------------------------------------ | | ||
| | **overview** | `overview.md` | Single project overview | | ||
| | **feature** | `features/` | High-level capabilities | | ||
| | **requirement** | `requirements/` | Detailed specs, linked to features | | ||
| | **task** | `tasks/` | Implementation work items | | ||
| | **decision** | `decisions/` | Architecture Decision Records (ADRs) | | ||
| All documents are markdown files with YAML frontmatter, stored in `.grimoire/` and committed to git. | ||
| ## How it works | ||
| ``` | ||
| .grimoire/ | ||
| overview.md # Project overview | ||
| config.yaml # Configuration | ||
| features/ # Feature documents | ||
| requirements/ # Requirement documents | ||
| tasks/ # Task documents | ||
| decisions/ # Architecture decisions | ||
| .cache/ # Gitignored — derived database | ||
| ``` | ||
| Markdown files are the source of truth. The database (DuckDB) is a derived cache that enables full-text search, semantic search, and relational queries — it's always rebuildable from files via `grimoire sync`. | ||
| ## AI agent workflow | ||
| Grimoire is designed for AI coding agents (Claude Code, Cursor, Copilot, etc.) to consume. All commands output structured JSON by default. | ||
| ```bash | ||
| # Agent starting work | ||
| grimoire overview # Understand the project | ||
| grimoire task list --status todo # Find available work | ||
| grimoire task get <id> # Read task details | ||
| grimoire task update <id> --status in-progress # Claim a task | ||
| # Agent recording progress | ||
| grimoire log <id> "Completed implementation" # Log what was done | ||
| grimoire decision create --title "..." --body "..." # Record decisions | ||
| grimoire task update <id> --status done # Mark complete | ||
| ``` | ||
| Install the [agentskills.io](https://agentskills.io/home) skill to give your AI agent full knowledge of Grimoire commands: | ||
| ```bash | ||
| npx skills add mikevalstar/grimoire | ||
| ``` | ||
| ## Output format | ||
| All commands output JSON by default (AI mode). Use `--format cli` for human-readable output: | ||
| ```bash | ||
| grimoire feature list --format cli | ||
| ``` | ||
| ## Packages | ||
| | Package | Description | | ||
| | ------------------------------------------------------------------------ | ----------------------------------------- | | ||
| | [@grimoire-ai/cli](https://www.npmjs.com/package/@grimoire-ai/cli) | CLI tool (this package) | | ||
| | [@grimoire-ai/core](https://www.npmjs.com/package/@grimoire-ai/core) | Core library — file I/O, database, search | | ||
| | [@grimoire-ai/server](https://www.npmjs.com/package/@grimoire-ai/server) | Fastify server for the web UI | | ||
| ## Links | ||
| - [Website](https://grimoireai.quest) | ||
| - [GitHub](https://github.com/mikevalstar/grimoire) | ||
| ## License | ||
| [MIT](https://github.com/mikevalstar/grimoire/blob/main/LICENSE) |
+220
-8
| #!/usr/bin/env node | ||
| import { Command } from "commander"; | ||
| import { VERSION, appendComment, appendLog, createDocument, deleteDocument, getDocument, init, initOptionsSchema, listDocuments, overview, resolveDocumentId, search, sync, updateDocument, updateOverview, validate } from "@grimoire-ai/core"; | ||
| import { VERSION, appendComment, appendLog, createDocument, deleteDocument, getDocument, init, initOptionsSchema, links, listDocuments, loadConfig, orphans, overview, resolveDocumentId, search, status, sync, tree, updateDocument, updateOverview, validate } from "@grimoire-ai/core"; | ||
| import pc from "picocolors"; | ||
| import { dirname, resolve } from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
| import { startServer } from "@grimoire-ai/server"; | ||
| //#region src/colors.ts | ||
@@ -21,3 +24,3 @@ /** Green checkmark-style success text */ | ||
| /** Status badge with color coding */ | ||
| function status(s) { | ||
| function status$1(s) { | ||
| switch (s) { | ||
@@ -196,3 +199,3 @@ case "draft": return pc.yellow(s); | ||
| const title = asText(d.title); | ||
| const status$2 = asText(d.status); | ||
| const status = asText(d.status); | ||
| const priority$2 = asText(d.priority); | ||
@@ -203,3 +206,3 @@ const tags = asStringArray(d.tags); | ||
| if (title) lines.push(` ${bold(title)}`); | ||
| if (status$2) lines.push(` ${label("Status:", status(status$2))}`); | ||
| if (status) lines.push(` ${label("Status:", status$1(status))}`); | ||
| if (priority$2) lines.push(` ${label("Priority:", priority(priority$2))}`); | ||
@@ -334,3 +337,3 @@ if (tags.length > 0) lines.push(` ${label("Tags:", tags.join(", "))}`); | ||
| const title = asText(item.title); | ||
| const status$3 = asText(item.status); | ||
| const status = asText(item.status); | ||
| const priority$1 = asText(item.priority); | ||
@@ -340,3 +343,3 @@ const docType = asText(item.type); | ||
| if (title) parts.push(` ${title}`); | ||
| if (status$3) parts.push(` [${status(status$3)}]`); | ||
| if (status) parts.push(` [${status$1(status)}]`); | ||
| if (priority$1) parts.push(` (${priority(priority$1)})`); | ||
@@ -528,3 +531,3 @@ return parts.join(""); | ||
| const title = String(item.title); | ||
| const status$1 = String(item.status); | ||
| const status = String(item.status); | ||
| const score = Number(item.score).toFixed(4); | ||
@@ -534,3 +537,3 @@ const snippet = String(item.snippet); | ||
| parts.push(` ${title}`); | ||
| if (status$1) parts.push(` [${status(status$1)}]`); | ||
| if (status) parts.push(` [${status$1(status)}]`); | ||
| parts.push(` ${dim(`(${score})`)}`); | ||
@@ -542,2 +545,206 @@ parts.push(`\n ${dim(snippet.slice(0, 120))}`); | ||
| //#endregion | ||
| //#region src/commands/links.ts | ||
| const DIRECTION_ARROWS = { | ||
| out: "->", | ||
| in: "<-" | ||
| }; | ||
| function formatLinksResult(data) { | ||
| const result = data; | ||
| const lines = []; | ||
| if (result.count === 0) { | ||
| lines.push(dim(`No relationships found for ${id(result.id)}`)); | ||
| return lines.join("\n"); | ||
| } | ||
| lines.push(bold(`Links for ${id(result.id)}`)); | ||
| lines.push(""); | ||
| const byDepth = /* @__PURE__ */ new Map(); | ||
| for (const link of result.links) { | ||
| const group = byDepth.get(link.depth) ?? []; | ||
| group.push(link); | ||
| byDepth.set(link.depth, group); | ||
| } | ||
| for (const [depth, depthLinks] of byDepth) { | ||
| if (byDepth.size > 1) lines.push(dim(` Depth ${depth}:`)); | ||
| for (const link of depthLinks) { | ||
| const arrow = DIRECTION_ARROWS[link.direction] ?? "--"; | ||
| const rel = dim(`[${link.relationship}]`); | ||
| const docId = id(link.id); | ||
| const title = link.title; | ||
| const st = link.status ? ` ${status$1(link.status)}` : ""; | ||
| lines.push(` ${arrow} ${rel} ${docId} ${title}${st}`); | ||
| } | ||
| } | ||
| lines.push(""); | ||
| lines.push(dim(`${result.count} relationship${result.count !== 1 ? "s" : ""}`)); | ||
| return lines.join("\n"); | ||
| } | ||
| function registerLinksCommand(program) { | ||
| program.command("links <id>").description("Show all relationships for a document").option("--direction <dir>", "Filter by direction: in, out, or both (default: both)").option("--type <type>", "Filter by relationship type").option("--depth <n>", "Traversal depth 1-5 (default: 1)", "1").action(async (id, opts) => { | ||
| const globalOpts = program.opts(); | ||
| try { | ||
| printResult(await links({ | ||
| id, | ||
| direction: opts.direction ?? "both", | ||
| type: opts.type, | ||
| depth: opts.depth ? Number.parseInt(opts.depth, 10) : 1, | ||
| cwd: globalOpts.cwd | ||
| }), formatLinksResult); | ||
| } catch (err) { | ||
| printError(err instanceof Error ? err.message : String(err)); | ||
| } | ||
| }); | ||
| } | ||
| //#endregion | ||
| //#region src/commands/tree.ts | ||
| function renderNode(node, indent, isLast, collapsed) { | ||
| const lines = []; | ||
| const connector = isLast ? "└── " : "├── "; | ||
| const st = node.status ? ` ${status$1(node.status)}` : ""; | ||
| lines.push(`${indent}${connector}${id(node.id)} ${node.title}${st}`); | ||
| if (!collapsed || node.children.length > 0) { | ||
| const childIndent = indent + (isLast ? " " : "│ "); | ||
| for (let i = 0; i < node.children.length; i++) { | ||
| const child = node.children[i]; | ||
| const childIsLast = i === node.children.length - 1; | ||
| lines.push(...renderNode(child, childIndent, childIsLast, collapsed)); | ||
| } | ||
| } | ||
| return lines; | ||
| } | ||
| function formatTreeResult(data, collapsed) { | ||
| const result = data; | ||
| const lines = []; | ||
| if (result.count === 0) { | ||
| lines.push(dim("No documents found.")); | ||
| return lines.join("\n"); | ||
| } | ||
| for (let i = 0; i < result.tree.length; i++) { | ||
| const root = result.tree[i]; | ||
| const isLast = i === result.tree.length - 1; | ||
| const st = root.status ? ` ${status$1(root.status)}` : ""; | ||
| lines.push(`${id(root.id)} ${bold(root.title)}${st}`); | ||
| for (let j = 0; j < root.children.length; j++) { | ||
| const child = root.children[j]; | ||
| const childIsLast = j === root.children.length - 1; | ||
| lines.push(...renderNode(child, "", childIsLast, collapsed)); | ||
| } | ||
| if (!isLast) lines.push(""); | ||
| } | ||
| lines.push(""); | ||
| lines.push(dim(`${result.count} document${result.count !== 1 ? "s" : ""}`)); | ||
| return lines.join("\n"); | ||
| } | ||
| function registerTreeCommand(program) { | ||
| program.command("tree").description("Display the feature → requirement → task hierarchy").option("--feature <feature-id>", "Show tree for a specific feature").option("--status <status>", "Filter nodes by status").option("--collapsed", "Show IDs and titles only").action(async (opts) => { | ||
| const globalOpts = program.opts(); | ||
| try { | ||
| printResult(await tree({ | ||
| feature: opts.feature, | ||
| status: opts.status, | ||
| cwd: globalOpts.cwd | ||
| }), (data) => formatTreeResult(data, opts.collapsed ?? false)); | ||
| } catch (err) { | ||
| printError(err instanceof Error ? err.message : String(err)); | ||
| } | ||
| }); | ||
| } | ||
| //#endregion | ||
| //#region src/commands/orphans.ts | ||
| function formatOrphansResult(data) { | ||
| const result = data; | ||
| const lines = []; | ||
| if (result.count === 0) { | ||
| lines.push(success("No orphaned documents found.")); | ||
| return lines.join("\n"); | ||
| } | ||
| lines.push(warn(`Found ${result.count} orphaned document${result.count !== 1 ? "s" : ""}:`)); | ||
| lines.push(""); | ||
| for (const orphan of result.orphans) { | ||
| const st = orphan.status ? ` ${status$1(orphan.status)}` : ""; | ||
| const tp = dim(`[${orphan.type}]`); | ||
| lines.push(` ${tp} ${id(orphan.id)} ${orphan.title}${st}`); | ||
| } | ||
| return lines.join("\n"); | ||
| } | ||
| function registerOrphansCommand(program) { | ||
| program.command("orphans").description("Find documents with no relationships to any other document").option("--type <type>", "Filter by document type: feature, requirement, task, decision").action(async (opts) => { | ||
| const globalOpts = program.opts(); | ||
| try { | ||
| printResult(await orphans({ | ||
| type: opts.type ?? "all", | ||
| cwd: globalOpts.cwd | ||
| }), formatOrphansResult); | ||
| } catch (err) { | ||
| printError(err instanceof Error ? err.message : String(err)); | ||
| } | ||
| }); | ||
| } | ||
| //#endregion | ||
| //#region src/commands/status.ts | ||
| function formatStatusResult(data) { | ||
| const r = data; | ||
| const lines = []; | ||
| lines.push(bold("Project Status")); | ||
| lines.push(""); | ||
| lines.push(bold("Documents")); | ||
| lines.push(` Features: ${r.counts.features} Requirements: ${r.counts.requirements} Tasks: ${r.counts.tasks} Decisions: ${r.counts.decisions}`); | ||
| lines.push(dim(` Total: ${r.counts.total}`)); | ||
| lines.push(""); | ||
| for (const [type, statuses] of Object.entries(r.by_status)) { | ||
| const parts = Object.entries(statuses).map(([st, cnt]) => `${status$1(st)}: ${cnt}`).join(" "); | ||
| lines.push(` ${bold(type)}: ${parts}`); | ||
| } | ||
| lines.push(""); | ||
| lines.push(bold("Task Health")); | ||
| lines.push(` Open tasks: ${r.open_tasks}`); | ||
| if (r.blocked_tasks > 0) lines.push(` ${error(`Blocked tasks: ${r.blocked_tasks}`)}`); | ||
| lines.push(""); | ||
| lines.push(bold("Health")); | ||
| if (r.orphaned_documents > 0) lines.push(warn(` Orphaned documents: ${r.orphaned_documents}`)); | ||
| else lines.push(success(" No orphaned documents")); | ||
| if (r.stale_documents > 0) lines.push(warn(` Stale documents (>${r.stale_threshold_days}d): ${r.stale_documents}`)); | ||
| else lines.push(success(` No stale documents (>${r.stale_threshold_days}d)`)); | ||
| lines.push(""); | ||
| if (r.recent.length > 0) { | ||
| lines.push(bold("Recent Updates")); | ||
| for (const doc of r.recent) { | ||
| const tp = dim(`[${doc.type}]`); | ||
| const dt = dim(doc.updated); | ||
| lines.push(` ${dt} ${tp} ${id(doc.id)} ${doc.title} ${status$1(doc.status)}`); | ||
| } | ||
| } | ||
| return lines.join("\n"); | ||
| } | ||
| function registerStatusCommand(program) { | ||
| program.command("status").description("Show project-wide status dashboard").option("--limit <n>", "Number of recent documents to show (default: 10)", "10").option("--stale-days <n>", "Days before a document is considered stale (default: 30)", "30").action(async (opts) => { | ||
| const globalOpts = program.opts(); | ||
| try { | ||
| printResult(await status({ | ||
| limit: opts.limit ? Number.parseInt(opts.limit, 10) : 10, | ||
| staleDays: opts.staleDays ? Number.parseInt(opts.staleDays, 10) : 30, | ||
| cwd: globalOpts.cwd | ||
| }), formatStatusResult); | ||
| } catch (err) { | ||
| printError(err instanceof Error ? err.message : String(err)); | ||
| } | ||
| }); | ||
| } | ||
| //#endregion | ||
| //#region src/commands/ui.ts | ||
| function registerUiCommand(program) { | ||
| program.command("ui").description("Launch Grimoire web UI in browser").option("--port <port>", "Port to listen on").option("--no-open", "Do not auto-open browser").action(async (opts) => { | ||
| const cwd = program.opts().cwd ?? process.cwd(); | ||
| const config = await loadConfig(cwd); | ||
| const port = opts.port ? Number.parseInt(opts.port, 10) : config.ui.port; | ||
| const autoOpen = opts.open && config.ui.auto_open; | ||
| const { address } = await startServer({ | ||
| cwd, | ||
| port, | ||
| staticDir: resolve(dirname(fileURLToPath(import.meta.url)), "../../website/dist") | ||
| }); | ||
| console.log(`Grimoire UI running at ${address}`); | ||
| if (autoOpen) await (await import("open")).default(address); | ||
| }); | ||
| } | ||
| //#endregion | ||
| //#region src/index.ts | ||
@@ -556,4 +763,9 @@ const program = new Command(); | ||
| registerSearchCommand(program); | ||
| registerLinksCommand(program); | ||
| registerTreeCommand(program); | ||
| registerOrphansCommand(program); | ||
| registerStatusCommand(program); | ||
| registerUiCommand(program); | ||
| program.parse(); | ||
| //#endregion | ||
| export {}; |
+6
-4
| { | ||
| "name": "@grimoire-ai/cli", | ||
| "version": "0.2.0", | ||
| "version": "0.3.0", | ||
| "description": "Local-first, AI-native requirements management CLI", | ||
@@ -18,8 +18,10 @@ "license": "MIT", | ||
| "commander": "^14.0.3", | ||
| "open": "^11", | ||
| "picocolors": "^1.1.1", | ||
| "@grimoire-ai/core": "0.2.0" | ||
| "@grimoire-ai/core": "0.3.0", | ||
| "@grimoire-ai/server": "0.3.0" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/node": "^24", | ||
| "typescript": "^6", | ||
| "@types/node": "^25", | ||
| "typescript": "^6.0.2", | ||
| "vite-plus": "latest" | ||
@@ -26,0 +28,0 @@ }, |
No README
QualityPackage does not have a README. This may indicate a failed publish or a low quality package.
Found 1 instance in 1 package
37154
57.14%4
33.33%775
38.39%0
-100%140
Infinity%5
66.67%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
Updated