ADR-029: CLI Theory of Operation - Hybrid Unix/Domain-Specific Design
Status: Proposed Date: 2025-10-12 Supersedes: N/A - First formal CLI design specification
Overview
Command-line interfaces face a fundamental tension: Unix experts want familiar verbs like ls and rm, while domain users want commands that reflect their work, like job approve and ontology merge. Most tools pick one approach and frustrate half their users.
Our knowledge graph CLI grew organically with mixed patterns—some commands used hierarchies (kg ontology list), others were flat, and inconsistencies emerged. Aliases appeared without rationale. This ADR confronts the design tension directly with a deliberate hybrid approach.
We provide two ways to interact with the same functionality: domain-oriented commands (kg job approve <id>) for power users learning the system, and Unix-style shortcuts (kg rm job <id>) for those who think in traditional file operations. The verb shortcuts delegate to the primary domain commands through a router pattern, ensuring one source of truth while supporting multiple mental models. Best of both worlds.
Context
The Knowledge Graph CLI (kg) has evolved organically, resulting in inconsistent command structures:
Current Issues:
1. Inconsistent hierarchies: Some commands use hierarchical patterns (kg ontology list), others are flat
2. Arbitrary aliases: db, resource, role, perm without clear rationale
3. Mixed verbosity: Some commands are terse, others verbose
4. No design philosophy: Commands added ad-hoc without architectural guidance
Design Tension:
Users expect both Unix-style brevity (ls, rm, stat) AND domain-specific organization (kg job approve, kg role assign). How do we reconcile these competing needs?
Observation from Code Review:
"We're unintentionally building an operating system... maybe we need to use a well-known pattern?"
This is true. The CLI is becoming a domain-specific shell for knowledge graph operations. We should embrace Unix/BusyBox patterns deliberately rather than accidentally.
Decision
Implement a hybrid architecture with two command interfaces:
1. Primary Interface: Noun → Verb (Domain-Oriented)
Structure: kg <resource> <verb> [args]
Rationale:
- Groups operations by resource domain (jobs, ontologies, concepts)
- Namespace isolation: job.stat vs database.stat can have different output schemas
- Scales naturally for domain-specific verbs: approve, assign, revoke, merge
- Enables contextual help: kg job --help shows all job operations
Examples:
kg job list
kg job stat <id>
kg job approve <id>
kg job cancel <id>
kg ontology list
kg ontology info <name>
kg ontology delete <name>
kg role list
kg role assign <user> <role>
kg role revoke <user> <role>
2. Convenience Layer: Unix Verb Shortcuts
Structure: kg <verb> <resource> [args]
Rationale:
- Unix muscle memory for common operations
- Reduces typing for frequent commands
- Familiar to users from ls, rm, stat, cat, etc.
Implementation: Verb shortcuts delegate to primary commands via a router
Examples:
# List operations
kg ls job → kg job list
kg ls ontology → kg ontology list
kg ls backup → kg admin backup list
# Status/Stats operations
kg stat job <id> → kg job stat <id>
kg stat database → kg database stats
# Remove operations
kg rm job <id> → kg job cancel <id>
kg rm ontology <name> → kg ontology delete <name>
# Show/Display operations
kg cat concept <id> → kg search details <id>
kg cat config <key> → kg config get <key>
3. Command Router Architecture
Clean separation between verb shortcuts and primary commands:
// client/src/cli/verb-router.ts
export function createVerbRouter(): Command {
const router = new Command();
// ls - Universal list operation
router
.command('ls')
.description('List resources (Unix-style shortcut)')
.argument('<resource>', 'Resource type: job, ontology, backup, config, role, etc.')
.action(async (resource, options, command) => {
// Delegate to primary command
switch (resource) {
case 'job':
case 'jobs':
return executeCommand(['job', 'list'], command.parent);
case 'ontology':
case 'ontologies':
return executeCommand(['ontology', 'list'], command.parent);
case 'backup':
case 'backups':
return executeCommand(['admin', 'backup', 'list'], command.parent);
// ... more mappings
default:
console.error(`Unknown resource: ${resource}`);
console.log('Try: kg ls --help');
process.exit(1);
}
});
// rm - Universal remove operation
router
.command('rm')
.description('Remove/delete resources (Unix-style shortcut)')
.argument('<resource>', 'Resource type')
.argument('<id>', 'Resource identifier')
.action(async (resource, id, options, command) => {
switch (resource) {
case 'job':
return executeCommand(['job', 'cancel', id], command.parent);
case 'ontology':
return executeCommand(['ontology', 'delete', id], command.parent);
// ... more mappings
}
});
// stat - Universal status operation
// cat - Universal display operation
// ... more verbs
return router;
}
Helper Function:
function executeCommand(args: string[], rootCommand: Command): void {
// Navigate command tree and execute
let cmd = rootCommand;
for (const arg of args.slice(0, -1)) {
cmd = cmd.commands.find(c => c.name() === arg);
if (!cmd) throw new Error(`Command not found: ${arg}`);
}
cmd.parse(args, { from: 'user' });
}
4. Command Naming Conventions
Use singular nouns (shorter by default):
job not jobs
ontology not ontologies
role not roles
permission not permissions
resource not resources
Descriptions can be plural:
Sensible aliases (5-6 chars max, well-known only):
config → cfg (3)
database → db (2)
ontology → onto (4)
job (already 3)
permission → perm (4)
resource → res (3)
5. Universal JSON Mode
Machine-Readable Interface: All commands support JSON input/output for automation.
Global Toggle:
# Set JSON mode globally
kg config set output_format json
# Check current mode
kg config get output_format
Per-Command Override:
# Override to JSON for single command
kg job list --json
# Override to table when in JSON mode
kg job list --table
Consistent Behavior: - ALL commands respect output mode - ALL output is valid JSON (no mixed formats) - ALL input accepts JSON where applicable
Examples:
# Table mode (default)
kg job list
# ─────────────────────────────
# Job ID Status Progress
# ─────────────────────────────
# job-123 running 45%
# job-456 completed 100%
# JSON mode
kg job list --json
# [
# {"job_id": "job-123", "status": "running", "progress": 0.45},
# {"job_id": "job-456", "status": "completed", "progress": 1.0}
# ]
# Piping for automation
kg job list --json | jq '.[] | select(.status == "failed")' | kg job cancel --json
Architectural Insight: CLI as API Abstraction Layer
The JSON I/O mode transforms kg into a typed API abstraction that provides:
- Safety Layer
- Client-side validation before API calls
- Confirmation prompts for destructive operations (can be overridden with --force)
-
Schema validation (TypeScript types ensure correct data structures)
-
Protocol Versioning
- CLI handles API version differences transparently
- Backward compatibility for older scripts
-
Deprecation warnings without breaking existing automation
-
Offline Capabilities (future)
- Local config operations without API calls
- Batch operations with optimistic execution
-
Queue commands when API is unreachable
-
Automation Interface
- Scripts/tools that can't use REST API directly
- CI/CD pipelines (no HTTP client needed)
- Shell scripts (simpler than curl + jq)
- Other CLIs (compose with Unix tools)
Example: CI/CD Pipeline
#!/bin/bash
# Deploy ontology from CI/CD without REST client
# Safer than raw API calls - CLI validates before sending
kg ontology create ai_models --json < ontology.json
# CLI handles retries, auth, error messages
if kg job list --json | jq -e '.[] | select(.status == "failed")' > /dev/null; then
echo "Failed jobs detected"
exit 1
fi
# Confirmation prompts can be overridden for automation
kg database reset --force --yes
Configuration Integration:
// client/src/lib/config.ts
export interface KgConfig {
// ... existing fields
output_format?: 'table' | 'json'; // Default: 'table'
}
// Usage in commands
function getOutputFormat(options: any): 'table' | 'json' {
const config = getConfig();
// 1. Command-line flag takes precedence
if (options.json) return 'json';
if (options.table) return 'table';
// 2. Fall back to config
return config.get('output_format') || 'table';
}
Implementation:
- Add --json flag to ALL commands (Commander.js parent option)
- Add --table flag to ALL commands (override JSON mode)
- Refactor all output to check format before printing
- Ensure error messages are also JSON in JSON mode
6. Verb Vocabulary
Unix-Inspired Verbs:
- ls - List resources
- rm - Remove/delete
- stat - Status/statistics
- cat - Display/show details
- mv - Move/rename (future)
- cp - Copy/duplicate (future)
Domain-Specific Verbs:
- approve - Approve jobs, vocabulary
- cancel - Cancel jobs
- assign - Assign roles to users
- revoke - Revoke permissions
- grant - Grant permissions
- ingest - Ingest documents
- search - Search concepts
- connect - Find connections
Benefits
- Best of Both Worlds
- Power users: Use domain commands (
kg job approve <id>) -
Unix users: Use verb shortcuts (
kg ls job) -
Scalability
- Domain-specific operations don't need Unix analogs
-
Easy to add specialized verbs without polluting Unix verb namespace
-
Discoverability
kg <resource> --helpshows all operations for that resourcekg ls --helpshows all listable resources-
Tab completion works naturally
-
Consistency
- All commands follow noun→verb structure
- Verb shortcuts are additive (don't break existing usage)
-
Clean separation via router pattern
-
Maintainability
- Router is single source of truth for verb mappings
- Primary commands remain unchanged
-
Easy to add/remove verb shortcuts
-
CLI as API Abstraction (JSON Mode)
- Safety layer: Client-side validation, confirmation prompts
- Type safety: TypeScript ensures correct data structures
- Protocol versioning: Handle API changes transparently
- Automation-friendly: No HTTP client needed in scripts
- Offline operations: Local config, batch processing (future)
- Composability: Pipe between commands or integrate with Unix tools
Consequences
Positive
- ✅ Intuitive for both Unix and domain experts
- ✅ Reduces typing without sacrificing clarity
- ✅ Scales to complex domain operations
- ✅ Clean, maintainable architecture
- ✅ Universal JSON mode enables complete automation
- ✅ Consistent interface for scripting/piping
Negative
- ⚠️ Two ways to do the same thing (may confuse new users)
- Mitigation: Documentation emphasizes primary commands, verb shortcuts as "convenience aliases"
- ⚠️ Router adds indirection
- Mitigation: Router is simple delegation, no business logic
- ⚠️ Breaking change for existing users
- Mitigation: Phase migration (add singulars as aliases first, deprecate plurals later)
- ⚠️ JSON mode requires refactoring ALL commands
- Mitigation: Implement incrementally, starting with high-value commands
Neutral
- Router pattern adds ~100 lines of code
- Help text needs to explain both interfaces
- Tab completion needs to support both patterns
Examples
Before (Current)
kg jobs list # Inconsistent plural
kg job status <id> # Inconsistent with above
kg database stats # Why not db stats?
kg ontology list # Verbose
kg config mcp # Custom structure
kg admin rbac resources list # Too deep
After (Hybrid)
# Primary interface (noun → verb)
kg job list
kg job stat <id>
kg database stats
kg ontology list
kg config mcp list
kg rbac resource list
# Convenience shortcuts (verb → noun)
kg ls job
kg stat job <id>
kg stat database
kg ls ontology
kg ls config
kg ls resource
Complex Operations (Domain-Specific)
# These don't have Unix verb equivalents - and that's OK!
kg job approve <id>
kg role assign <user> <role>
kg permission grant <role> <resource> <action>
kg ontology merge <source> <target>
kg search connect <from> <to>
Migration Path
Phase 1: Add Verb Router (Non-Breaking)
- Implement verb router with delegation
- Add verb shortcuts alongside existing commands
- Both work simultaneously
Phase 2: Singularize Resources (Breaking)
- Rename
jobs→job,roles→role, etc. - Add deprecation warnings for plural forms
- Update documentation
Phase 3: Add Useful Aliases
cfg,db,onto,perm,res- Document recommended shortcuts
Phase 4: Deprecation (Optional)
- After 6 months, optionally remove plural commands
- Or keep both indefinitely (user preference)
Implementation Checklist
Phase 1: Verb Router
- [ ] Create
client/src/cli/verb-router.ts - [ ] Implement
executeCommand()helper - [ ] Add
lsverb with resource delegation - [ ] Add
rmverb with resource delegation - [ ] Add
statverb with resource delegation - [ ] Add
catverb with resource delegation - [ ] Register verb router in main CLI
Phase 2: Singularization
- [ ] Rename
jobs→job - [ ] Rename
roles→role - [ ] Rename
permissions→permission - [ ] Rename
resources→resource - [ ] Update all references in codebase
Phase 3: Aliases
- [ ] Add
dbalias fordatabase - [ ] Add
cfgalias forconfig - [ ] Add
ontoalias forontology - [ ] Add
permalias forpermission - [ ] Add
resalias forresource
Phase 4: Universal JSON Mode
- [ ] Add
output_formatfield to config schema - [ ] Add
--jsonglobal flag (Commander.js parent option) - [ ] Add
--tableglobal flag (override JSON mode) - [ ] Create
getOutputFormat()utility - [ ] Refactor ALL commands to check output format
- [ ] Ensure Table utility supports JSON output
- [ ] Ensure error messages are JSON in JSON mode
- [ ] Test JSON mode with piping/automation
Phase 5: Documentation & Testing
- [ ] Update help text to explain both interfaces
- [ ] Add tab completion for verb shortcuts
- [ ] Update user documentation
- [ ] Update QUICKSTART guide
- [ ] Write integration tests
- [ ] Test backwards compatibility
Future Enhancements
-
Interactive Mode
-
Fuzzy Matching
-
Shell Completion
-
Piping Support
References
- BusyBox Design: https://busybox.net/
- Git Command Design: https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Porcelain
- Kubectl Command Patterns: https://kubernetes.io/docs/reference/kubectl/
- Commander.js Documentation: https://github.com/tj/commander.js
Conclusion
By embracing a hybrid design, we get: - Organized complexity via noun→verb (domain operations) - Unix familiarity via verb shortcuts (common operations) - Clean architecture via router delegation (maintainable)
This positions kg as a professional domain-specific shell rather than an ad-hoc collection of commands. The design scales from simple CRUD to complex workflows while remaining intuitive for both Unix users and domain experts.