New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket

@grimoire-ai/cli

Package Overview
Dependencies
Maintainers
1
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@grimoire-ai/cli - npm Package Compare versions

Comparing version
0.2.0
to
0.3.0
+139
README.md
# 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 @@ },