vaultctl 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # vaultctl
2
+
3
+ Structured Obsidian vault operations without MCP servers.
4
+
5
+ `vaultctl` is a CLI tool that gives [Claude Code](https://claude.com/claude-code) and other AI agents structured access to Obsidian vaults — search, tag management, health checks, and template-based note creation — without running a persistent MCP server process.
6
+
7
+ ## Why?
8
+
9
+ MCP servers for Obsidian + Claude Code CLI are [fundamentally broken](https://github.com/anthropics/claude-code/issues/9176). Claude Code's stdio pipe lifecycle management kills server connections before they initialise, causing `BrokenPipeError` on every session. Meanwhile, Obsidian vaults are just directories of markdown files — there's no reason to run a persistent server process to read them.
10
+
11
+ `vaultctl` replaces MCP with stateless CLI commands. Every invocation reads the filesystem, does its thing, and exits. No pipes, no zombie processes, no lifecycle bugs.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install -g vaultctl
17
+ vaultctl config set vault ~/path/to/your/vault
18
+ ```
19
+
20
+ That's it. The config is saved to `~/.vaultctlrc` and works everywhere — interactive shells, AI agents, cron jobs, CI.
21
+
22
+ ### From Source
23
+
24
+ ```bash
25
+ git clone https://github.com/testing-in-production/vaultctl.git
26
+ cd vaultctl
27
+ npm install
28
+ npm run build
29
+ alias vaultctl="node $HOME/vaultctl/packages/cli/dist/index.js"
30
+ vaultctl config set vault ~/path/to/your/vault
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ ### Search
36
+
37
+ ```bash
38
+ # Full-text content search
39
+ vaultctl search "meeting notes"
40
+
41
+ # Filter by frontmatter fields
42
+ vaultctl search --type project --status active
43
+
44
+ # Filter by tag
45
+ vaultctl search --tag "status/active"
46
+
47
+ # Combine filters
48
+ vaultctl search "quarterly" --type project --status active
49
+ ```
50
+
51
+ ### Tags
52
+
53
+ ```bash
54
+ # List all tags with counts
55
+ vaultctl tags list
56
+
57
+ # Find notes with a specific tag
58
+ vaultctl tags find status/active
59
+
60
+ # Add/remove tags
61
+ vaultctl tags add 01_Projects/my-project.md priority/high
62
+ vaultctl tags remove 01_Projects/my-project.md priority/high
63
+
64
+ # Rename a tag across the entire vault (dry-run first)
65
+ vaultctl tags rename status/active status/in-progress
66
+ vaultctl tags rename status/active status/in-progress --yes
67
+ ```
68
+
69
+ ### Health
70
+
71
+ ```bash
72
+ # Run all health checks
73
+ vaultctl health
74
+
75
+ # Run specific checks
76
+ vaultctl health --check broken-links
77
+ vaultctl health --check orphans
78
+ vaultctl health --check stale --days 90
79
+ vaultctl health --check frontmatter
80
+ ```
81
+
82
+ ### Create
83
+
84
+ ```bash
85
+ # Create a note from a template
86
+ vaultctl create --template project "My New Project"
87
+ vaultctl create --template daily
88
+
89
+ # Specify target folder
90
+ vaultctl create --template concept --folder 03_Resources/concepts "Zettelkasten"
91
+
92
+ # List available templates
93
+ vaultctl create --list
94
+ ```
95
+
96
+ ### Read & Info
97
+
98
+ ```bash
99
+ # Read a note (parsed with frontmatter, wikilinks, tags)
100
+ vaultctl read 01_Projects/my-project.md
101
+
102
+ # Vault overview statistics
103
+ vaultctl info
104
+ ```
105
+
106
+ ## Output
107
+
108
+ JSON by default (designed for AI agent consumption). Add `--format table` for human-readable output.
109
+
110
+ Exit codes: `0` = success, `1` = error, `2` = no results found.
111
+
112
+ ## Vault Discovery
113
+
114
+ `vaultctl` finds your vault in this order:
115
+
116
+ 1. `--vault /path/to/vault` flag
117
+ 2. `VAULTCTL_PATH` environment variable
118
+ 3. Walk up from current directory looking for `.obsidian/`
119
+ 4. `~/.vaultctlrc` config file
120
+
121
+ ### Why `config set` Instead of Environment Variables?
122
+
123
+ Shell aliases and environment variables from `~/.zshrc` or `~/.bashrc` aren't available in non-interactive shells (AI agents, cron jobs, CI). `vaultctl config set vault` writes to `~/.vaultctlrc`, which is a file read — it works everywhere.
124
+
125
+ You can still use `VAULTCTL_PATH` or `--vault` if you prefer — they take higher precedence in the discovery chain.
126
+
127
+ ## Features
128
+
129
+ - **Search** — Full-text content search with type, status, and tag filters
130
+ - **Tags** — List, find, add, remove, and rename tags (frontmatter + inline `#tags` unified)
131
+ - **Health** — Broken `[[wikilinks]]`, orphaned notes, stale notes, missing frontmatter
132
+ - **Templates** — Create notes from `_templates/tpl-*.md` with date/title substitution
133
+ - **Wikilink resolution** — All `[[links]]` resolved to file paths or flagged as broken
134
+ - **Path traversal protection** — All write operations validated against vault root
135
+
136
+ ## Architecture
137
+
138
+ This is the CLI wrapper package. The core logic lives in [`@vaultctl/core`](https://www.npmjs.com/package/@vaultctl/core) — a pure TypeScript library with zero CLI dependencies that can be imported by other tools.
139
+
140
+ ## Blog Post
141
+
142
+ Read the full story: [I Replaced My Obsidian MCP Server With a CLI](https://www.testinginproduction.co/blog/i-replaced-mcp-with-a-cli)
143
+
144
+ ## License
145
+
146
+ MIT
@@ -0,0 +1,3 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerConfigCommand(program: Command): void;
3
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1,79 @@
1
+ import { resolve, join } from 'path';
2
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
3
+ import { homedir } from 'os';
4
+ import { formatOutput } from '../format.js';
5
+ const RC_PATH = join(homedir(), '.vaultctlrc');
6
+ export function registerConfigCommand(program) {
7
+ const config = program
8
+ .command('config')
9
+ .description('Manage vaultctl configuration');
10
+ config
11
+ .command('show')
12
+ .description('Show current configuration and vault discovery status')
13
+ .action((_opts, cmd) => {
14
+ const globalOpts = cmd.parent.parent.opts();
15
+ const format = (globalOpts.format ?? 'json');
16
+ const rcExists = existsSync(RC_PATH);
17
+ const rcValue = rcExists ? readFileSync(RC_PATH, 'utf-8').trim() : null;
18
+ const envValue = process.env['VAULTCTL_PATH'] ?? null;
19
+ // Determine which method would resolve
20
+ let resolvedBy = 'none';
21
+ let resolvedPath = null;
22
+ if (globalOpts.vault) {
23
+ const abs = resolve(globalOpts.vault);
24
+ if (existsSync(join(abs, '.obsidian'))) {
25
+ resolvedBy = '--vault flag';
26
+ resolvedPath = abs;
27
+ }
28
+ }
29
+ if (!resolvedPath && envValue) {
30
+ const abs = resolve(envValue);
31
+ if (existsSync(join(abs, '.obsidian'))) {
32
+ resolvedBy = 'VAULTCTL_PATH env var';
33
+ resolvedPath = abs;
34
+ }
35
+ }
36
+ if (!resolvedPath && rcValue) {
37
+ const abs = resolve(rcValue);
38
+ if (existsSync(join(abs, '.obsidian'))) {
39
+ resolvedBy = '~/.vaultctlrc';
40
+ resolvedPath = abs;
41
+ }
42
+ }
43
+ const result = {
44
+ rc_file: RC_PATH,
45
+ rc_value: rcValue,
46
+ env_var: envValue,
47
+ resolved_by: resolvedBy,
48
+ resolved_path: resolvedPath,
49
+ };
50
+ console.log(formatOutput(result, format));
51
+ });
52
+ config
53
+ .command('set')
54
+ .description('Set a configuration value')
55
+ .argument('<key>', 'Configuration key (currently: vault)')
56
+ .argument('<value>', 'Configuration value')
57
+ .action((key, value, _opts, cmd) => {
58
+ const globalOpts = cmd.parent.parent.opts();
59
+ const format = (globalOpts.format ?? 'json');
60
+ if (key !== 'vault') {
61
+ console.error(`Unknown config key: "${key}". Available keys: vault`);
62
+ process.exit(1);
63
+ }
64
+ const abs = resolve(value);
65
+ if (!existsSync(join(abs, '.obsidian'))) {
66
+ console.error(`No .obsidian directory found at: ${abs}`);
67
+ process.exit(1);
68
+ }
69
+ writeFileSync(RC_PATH, abs + '\n', 'utf-8');
70
+ const result = {
71
+ saved: true,
72
+ key: 'vault',
73
+ value: abs,
74
+ rc_file: RC_PATH,
75
+ };
76
+ console.log(formatOutput(result, format));
77
+ });
78
+ }
79
+ //# sourceMappingURL=config.js.map
package/dist/index.js CHANGED
@@ -6,11 +6,12 @@ import { registerHealthCommand } from './commands/health.js';
6
6
  import { registerCreateCommand } from './commands/create.js';
7
7
  import { registerReadCommand } from './commands/read.js';
8
8
  import { registerInfoCommand } from './commands/info.js';
9
+ import { registerConfigCommand } from './commands/config.js';
9
10
  const program = new Command();
10
11
  program
11
12
  .name('vaultctl')
12
13
  .description('Structured Obsidian vault operations without MCP servers')
13
- .version('0.3.1')
14
+ .version('0.4.0')
14
15
  .option('--vault <path>', 'Path to Obsidian vault')
15
16
  .option('--format <format>', 'Output format: json or table', 'json');
16
17
  registerSearchCommand(program);
@@ -19,5 +20,6 @@ registerHealthCommand(program);
19
20
  registerCreateCommand(program);
20
21
  registerReadCommand(program);
21
22
  registerInfoCommand(program);
23
+ registerConfigCommand(program);
22
24
  program.parse();
23
25
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vaultctl",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "CLI for structured Obsidian vault operations — search, tags, health checks, templates — without MCP servers",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -25,7 +25,7 @@
25
25
  "prepublishOnly": "npm run build"
26
26
  },
27
27
  "dependencies": {
28
- "@vaultctl/core": "^0.3.1",
28
+ "@vaultctl/core": "^0.4.0",
29
29
  "chalk": "^5.4.0",
30
30
  "commander": "^13.0.0"
31
31
  }