@learnrudi/cli 1.9.12 → 1.10.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 CHANGED
@@ -1,206 +1,263 @@
1
1
  # RUDI CLI
2
2
 
3
- Universal tool manager for MCP stacks, CLI tools, runtimes, and AI agents.
3
+ A universal tool manager for MCP stacks, CLI tools, runtimes, and AI agents.
4
4
 
5
- ## Install
5
+ RUDI provides a unified installation and management system for:
6
+ - **MCP Stacks** - Model Context Protocol servers for Claude, Codex, and Gemini
7
+ - **CLI Tools** - Any npm package or upstream binary (ffmpeg, ripgrep, etc.)
8
+ - **Runtimes** - Node.js, Python, Deno, Bun
9
+ - **AI Agents** - Claude Code, Codex CLI, Gemini CLI
10
+
11
+ ## Installation
6
12
 
7
13
  ```bash
8
- npm i -g @learnrudi/cli
14
+ npm install -g @learnrudi/cli
9
15
  ```
10
16
 
11
- Requires Node.js 18+. The postinstall step bootstraps `~/.rudi` and creates shims.
17
+ Requires Node.js 18 or later. The installer creates `~/.rudi/` and adds shims to `~/.rudi/bins/`.
12
18
 
13
- ## Quick Start
19
+ Add to your shell profile (`.bashrc`, `.zshrc`, or `.profile`):
14
20
 
15
21
  ```bash
16
- # Install any npm CLI tool
17
- rudi install npm:typescript
18
- rudi install npm:@stripe/cli
19
- rudi install npm:vercel
22
+ export PATH="$HOME/.rudi/bins:$PATH"
23
+ ```
20
24
 
21
- # Install curated stacks and tools
22
- rudi install slack
23
- rudi install binary:ffmpeg
24
- rudi install binary:supabase
25
+ ## Core Concepts
25
26
 
26
- # All tools available via ~/.rudi/bins/
27
- tsc --version
28
- ffmpeg -version
29
- supabase --version
27
+ ### Shim-Based Architecture
30
28
 
31
- # Configure secrets for stacks
32
- rudi secrets set SLACK_BOT_TOKEN "xoxb-your-token"
29
+ Every tool installed through RUDI gets a wrapper script (shim) in `~/.rudi/bins/`. This provides:
33
30
 
34
- # Wire up your AI agents
35
- rudi integrate all
36
- ```
31
+ - Clean PATH integration without modifying system directories
32
+ - Version isolation per package
33
+ - Ownership tracking for clean uninstalls
34
+ - Consistent invocation across different package sources
37
35
 
38
- ## Features
36
+ When you run `tsc`, the shell finds `~/.rudi/bins/tsc`, which delegates to the actual TypeScript installation at `~/.rudi/binaries/npm/typescript/node_modules/.bin/tsc`.
39
37
 
40
- ### Universal Tool Installation
38
+ ### Package Sources
41
39
 
42
- RUDI supports multiple installation paths:
40
+ RUDI supports three installation sources:
43
41
 
44
- ```bash
45
- # Dynamic npm packages (any npm CLI)
46
- rudi install npm:cowsay
47
- rudi install npm:typescript
48
- rudi install npm:@railway/cli
42
+ 1. **Dynamic npm** (`npm:<package>`) - Any npm package with a `bin` field
43
+ 2. **Curated Registry** - Pre-configured stacks and binaries with documentation
44
+ 3. **Upstream Binaries** - Direct downloads from official sources
49
45
 
50
- # Curated registry (stacks and binaries with docs)
51
- rudi install slack # MCP stack
52
- rudi install binary:ffmpeg # Upstream binary
53
- rudi install binary:supabase # npm-based CLI
46
+ ### Secret Management
54
47
 
55
- # All tools resolve through ~/.rudi/bins/
56
- ```
48
+ MCP stacks often require API keys and tokens. RUDI stores secrets in `~/.rudi/secrets.json` (mode 0600) and injects them as environment variables when running stacks. Secrets are never exposed in process listings or logs.
57
49
 
58
- ### Shim-First Architecture
50
+ ## Usage
59
51
 
60
- Every installed tool gets a shim in `~/.rudi/bins/`:
52
+ ### Installing Packages
61
53
 
62
54
  ```bash
63
- # Add to your shell profile (.bashrc, .zshrc)
64
- export PATH="$HOME/.rudi/bins:$PATH"
55
+ # Install any npm CLI tool
56
+ rudi install npm:typescript # Installs tsc, tsserver
57
+ rudi install npm:@stripe/cli # Installs stripe
58
+ rudi install npm:vercel # Installs vercel
65
59
 
66
- # Then use tools directly
67
- tsc --version # ~/.rudi/bins/tsc
68
- ffmpeg -version # ~/.rudi/bins/ffmpeg
69
- supabase --help # ~/.rudi/bins/supabase
60
+ # Install from curated registry
61
+ rudi install slack # MCP stack for Slack
62
+ rudi install binary:ffmpeg # Upstream ffmpeg binary
63
+ rudi install binary:supabase # Supabase CLI
64
+
65
+ # Install with scripts enabled (when needed)
66
+ rudi install npm:puppeteer --allow-scripts
70
67
  ```
71
68
 
72
- ### Security by Default
69
+ ### Listing Installed Packages
73
70
 
74
- npm packages run with `--ignore-scripts` by default:
71
+ ```bash
72
+ rudi list # All installed packages
73
+ rudi list stacks # MCP stacks only
74
+ rudi list binaries # CLI tools only
75
+ rudi list runtimes # Language runtimes
76
+ rudi list agents # AI agent CLIs
77
+ ```
78
+
79
+ ### Searching the Registry
75
80
 
76
81
  ```bash
77
- # Safe install (scripts skipped)
78
- rudi install npm:some-package
82
+ rudi search pdf # Search for packages
83
+ rudi search --all # List all available packages
84
+ rudi search --stacks # Filter to MCP stacks
85
+ rudi search --binaries # Filter to CLI tools
86
+ ```
79
87
 
80
- # If CLI fails, opt-in to scripts
81
- rudi install npm:some-package --allow-scripts
88
+ ### Managing Secrets
89
+
90
+ ```bash
91
+ rudi secrets list # Show configured secrets (masked)
92
+ rudi secrets set SLACK_BOT_TOKEN # Set a secret (prompts for value)
93
+ rudi secrets set OPENAI_API_KEY "sk-..." # Set with value
94
+ rudi secrets remove SLACK_BOT_TOKEN # Remove a secret
82
95
  ```
83
96
 
84
- ## Commands
97
+ ### Integrating with AI Agents
85
98
 
86
99
  ```bash
87
- # Search and install
88
- rudi search <query> # Search for packages
89
- rudi search --all # List all packages
90
- rudi install <pkg> # Install a package
91
- rudi install npm:<pkg> # Install any npm CLI
92
- rudi remove <pkg> # Remove a package
93
-
94
- # List and inspect
95
- rudi list [kind] # List installed (stacks, binaries, agents)
96
- rudi pkg <id> # Show package details and shim status
97
- rudi shims list # List all shims
98
- rudi shims check # Validate shim targets
99
-
100
- # Secrets and integration
101
- rudi secrets list # Show configured secrets
102
- rudi secrets set <key> # Set a secret
103
- rudi integrate <agent> # Wire stack to agent config
104
-
105
- # Maintenance
106
- rudi update [pkg] # Update packages
107
- rudi doctor # Check system health
100
+ rudi integrate claude # Add stacks to Claude Desktop config
101
+ rudi integrate codex # Add stacks to Codex config
102
+ rudi integrate gemini # Add stacks to Gemini config
103
+ rudi integrate all # Add to all detected agents
108
104
  ```
109
105
 
110
- ## How It Works
106
+ This modifies the agent's MCP configuration file (e.g., `~/Library/Application Support/Claude/claude_desktop_config.json`) to include your installed stacks with proper secret injection.
111
107
 
112
- ### Installing a Package
108
+ ### Inspecting Packages
113
109
 
114
110
  ```bash
115
- rudi install npm:typescript
116
- ```
111
+ rudi pkg slack # Show package details
112
+ rudi pkg npm:typescript # Show shims and paths
117
113
 
118
- 1. Resolves package from npm registry
119
- 2. Creates install directory at `~/.rudi/binaries/npm/typescript/`
120
- 3. Runs `npm install typescript --ignore-scripts`
121
- 4. Discovers binaries from package.json (`tsc`, `tsserver`)
122
- 5. Creates wrapper shims in `~/.rudi/bins/`
123
- 6. Records ownership in shim registry
114
+ rudi shims list # List all shims
115
+ rudi shims check # Validate shim targets exist
116
+ ```
124
117
 
125
- ### Installing an MCP Stack
118
+ ### Maintenance
126
119
 
127
120
  ```bash
128
- rudi install slack
121
+ rudi update # Update all packages
122
+ rudi update slack # Update specific package
123
+ rudi remove slack # Uninstall a package
124
+ rudi doctor # Check system health
125
+ ```
126
+
127
+ ## Directory Structure
128
+
129
+ ```
130
+ ~/.rudi/
131
+ ├── bins/ # Shims (add to PATH)
132
+ │ ├── tsc # → binaries/npm/typescript/...
133
+ │ ├── ffmpeg # → binaries/ffmpeg/...
134
+ │ └── rudi-mcp # MCP router for agents
135
+
136
+ ├── stacks/ # MCP server installations
137
+ │ ├── slack/
138
+ │ │ ├── manifest.json
139
+ │ │ ├── index.js
140
+ │ │ └── node_modules/
141
+ │ └── google-workspace/
142
+
143
+ ├── binaries/ # CLI tool installations
144
+ │ ├── ffmpeg/ # Upstream binary
145
+ │ ├── supabase/ # npm-based CLI
146
+ │ └── npm/ # Dynamic npm packages
147
+ │ ├── typescript/
148
+ │ └── vercel/
149
+
150
+ ├── runtimes/ # Language runtimes
151
+ │ ├── node/
152
+ │ └── python/
153
+
154
+ ├── agents/ # AI agent CLI installations
155
+
156
+ ├── secrets.json # API keys (mode 0600)
157
+ ├── shim-registry.json # Shim ownership tracking
158
+ └── rudi.db # Local metadata database
129
159
  ```
130
160
 
131
- 1. Downloads stack tarball from registry
132
- 2. Extracts to `~/.rudi/stacks/slack/`
133
- 3. Runs `npm install` for dependencies
134
- 4. Shows which secrets need configuration
135
- 5. Ready for `rudi integrate` to wire to agents
161
+ ## How MCP Integration Works
162
+
163
+ When you run `rudi integrate claude`, RUDI:
136
164
 
137
- ### Running MCP Stacks
165
+ 1. Reads the Claude Desktop config at `~/Library/Application Support/Claude/claude_desktop_config.json`
166
+ 2. Adds entries for each installed stack pointing to `~/.rudi/bins/rudi-mcp`
167
+ 3. Passes the stack ID as an argument
138
168
 
139
- When an AI agent runs a stack:
169
+ When Claude invokes the MCP server:
140
170
 
141
- 1. Agent config points to `~/.rudi/bins/rudi-mcp`
142
- 2. RUDI loads secrets from `~/.rudi/secrets.json`
171
+ 1. `rudi-mcp` receives the stack ID
172
+ 2. Loads secrets from `~/.rudi/secrets.json`
143
173
  3. Injects secrets as environment variables
144
- 4. Runs the MCP server with bundled runtime
174
+ 4. Spawns the actual MCP server process
175
+ 5. Proxies stdio between Claude and the server
145
176
 
146
- ## Directory Structure
177
+ This architecture means secrets stay local and are never written to agent config files.
178
+
179
+ ## Security Model
147
180
 
181
+ ### npm Package Installation
182
+
183
+ By default, npm packages install with `--ignore-scripts` to prevent arbitrary code execution during install. If a package requires lifecycle scripts (e.g., native compilation), use:
184
+
185
+ ```bash
186
+ rudi install npm:puppeteer --allow-scripts
148
187
  ```
149
- ~/.rudi/
150
- ├── bins/ # Shims for all tools (add to PATH)
151
- ├── stacks/ # Installed MCP stacks
152
- ├── binaries/ # Installed CLI tools
153
- │ ├── ffmpeg/ # Upstream binary
154
- │ ├── supabase/ # npm-based CLI
155
- │ └── npm/ # Dynamic npm packages
156
- │ ├── typescript/
157
- │ └── cowsay/
158
- ├── runtimes/ # Bundled Node.js, Python
159
- ├── agents/ # AI agent CLIs
160
- ├── secrets.json # Encrypted secrets
161
- ├── shim-registry.json # Shim ownership tracking
162
- └── rudi.db # Local database
188
+
189
+ ### Secret Storage
190
+
191
+ Secrets are stored in `~/.rudi/secrets.json` with file permissions `0600` (owner read/write only). This matches the security model used by SSH, AWS CLI, and other credential stores.
192
+
193
+ ### Shim Isolation
194
+
195
+ Each package installs to its own directory. Shims are thin wrappers that set up the environment and delegate to the real binary. This prevents packages from interfering with each other.
196
+
197
+ ## Available Stacks
198
+
199
+ | Stack | Description | Required Secrets |
200
+ |-------|-------------|------------------|
201
+ | slack | Channels, messages, reactions | `SLACK_BOT_TOKEN` |
202
+ | google-workspace | Gmail, Sheets, Docs, Drive | `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` |
203
+ | notion-workspace | Pages, databases, search | `NOTION_API_KEY` |
204
+ | github | Issues, PRs, repos, actions | `GITHUB_TOKEN` |
205
+ | postgres | SQL queries | `DATABASE_URL` |
206
+ | stripe | Payments, subscriptions | `STRIPE_SECRET_KEY` |
207
+ | openai | DALL-E, Whisper, TTS | `OPENAI_API_KEY` |
208
+ | google-ai | Gemini, Imagen | `GOOGLE_AI_API_KEY` |
209
+
210
+ ## Available Binaries
211
+
212
+ | Binary | Description | Source |
213
+ |--------|-------------|--------|
214
+ | ffmpeg | Video/audio processing | Upstream |
215
+ | ripgrep | Fast text search | Upstream |
216
+ | supabase | Supabase CLI | npm |
217
+ | vercel | Vercel CLI | npm |
218
+ | uv | Python package manager | Upstream |
219
+
220
+ ## Troubleshooting
221
+
222
+ ### Command not found after install
223
+
224
+ Ensure `~/.rudi/bins` is in your PATH:
225
+
226
+ ```bash
227
+ echo $PATH | grep -q '.rudi/bins' && echo "OK" || echo "Add ~/.rudi/bins to PATH"
163
228
  ```
164
229
 
165
- ## Available Packages
230
+ ### Shim points to missing target
166
231
 
167
- ### MCP Stacks
232
+ Run `rudi shims check` to validate all shims. If a target is missing, reinstall the package:
168
233
 
169
- | Stack | Description |
170
- |-------|-------------|
171
- | slack | Send messages, search channels, manage reactions |
172
- | google-workspace | Gmail, Sheets, Docs, Drive, Calendar |
173
- | notion-workspace | Pages, databases, search |
174
- | google-ai | Gemini, Imagen, Veo |
175
- | openai | DALL-E, Whisper, TTS, Sora |
176
- | postgres | PostgreSQL database queries |
177
- | video-editor | ffmpeg-based video editing |
178
- | github | Issues, PRs, repos, actions |
179
- | stripe | Payments, subscriptions, invoices |
234
+ ```bash
235
+ rudi remove npm:typescript
236
+ rudi install npm:typescript
237
+ ```
180
238
 
181
- ### Binaries
239
+ ### MCP stack not appearing in agent
182
240
 
183
- | Binary | Description |
184
- |--------|-------------|
185
- | ffmpeg | Video/audio processing |
186
- | ripgrep | Fast search |
187
- | supabase | Supabase CLI |
188
- | vercel | Vercel CLI |
189
- | uv | Fast Python package manager |
241
+ 1. Check the stack is installed: `rudi list stacks`
242
+ 2. Run integration: `rudi integrate claude`
243
+ 3. Restart the AI agent application
190
244
 
191
- ### Dynamic npm
245
+ ### Permission denied on secrets
192
246
 
193
- Any npm package with a `bin` field works:
247
+ Ensure correct permissions:
194
248
 
195
249
  ```bash
196
- rudi install npm:typescript # tsc, tsserver
197
- rudi install npm:cowsay # cowsay, cowthink
198
- rudi install npm:@stripe/cli # stripe
199
- rudi install npm:netlify-cli # netlify
250
+ chmod 600 ~/.rudi/secrets.json
200
251
  ```
201
252
 
202
253
  ## Links
203
254
 
204
- - Website: https://learnrudi.com
255
+ - Documentation: https://learn-rudi.github.io/cli/
256
+ - Repository: https://github.com/learn-rudi/cli
205
257
  - Registry: https://github.com/learn-rudi/registry
258
+ - npm: https://www.npmjs.com/package/@learnrudi/cli
206
259
  - Issues: https://github.com/learn-rudi/cli/issues
260
+
261
+ ## License
262
+
263
+ MIT
package/dist/index.cjs CHANGED
@@ -1016,8 +1016,7 @@ async function resolvePackage(id) {
1016
1016
  if (id.startsWith("npm:")) {
1017
1017
  return resolveDynamicNpm(id);
1018
1018
  }
1019
- const normalizedId = id.includes(":") ? id : `stack:${id}`;
1020
- const pkg = await getPackage(normalizedId);
1019
+ const pkg = await getPackage(id);
1021
1020
  if (!pkg) {
1022
1021
  throw new Error(`Package not found: ${id}`);
1023
1022
  }
@@ -17303,6 +17302,7 @@ __export(sqlite_exports, {
17303
17302
  clearEmbeddings: () => clearEmbeddings,
17304
17303
  deleteEmbedding: () => deleteEmbedding,
17305
17304
  ensureEmbeddingsSchema: () => ensureEmbeddingsSchema,
17305
+ getAllEmbeddingStats: () => getAllEmbeddingStats,
17306
17306
  getEmbeddingStats: () => getEmbeddingStats,
17307
17307
  getErrorTurns: () => getErrorTurns,
17308
17308
  getMissingTurns: () => getMissingTurns,
@@ -17472,6 +17472,38 @@ function getEmbeddingStats(model) {
17472
17472
  }
17473
17473
  return stats;
17474
17474
  }
17475
+ function getAllEmbeddingStats() {
17476
+ const db3 = getDb2();
17477
+ const totalStmt = db3.prepare(`
17478
+ SELECT COUNT(*) as count
17479
+ FROM turns
17480
+ WHERE (user_message IS NOT NULL AND length(trim(user_message)) > 0)
17481
+ OR (assistant_response IS NOT NULL AND length(trim(assistant_response)) > 0)
17482
+ `);
17483
+ const total = totalStmt.get().count;
17484
+ const statsStmt = db3.prepare(`
17485
+ SELECT
17486
+ status,
17487
+ COUNT(*) as count
17488
+ FROM turn_embeddings
17489
+ GROUP BY status
17490
+ `);
17491
+ const stats = { total, done: 0, queued: 0, error: 0 };
17492
+ for (const row of statsStmt.all()) {
17493
+ stats[row.status] = row.count;
17494
+ }
17495
+ const modelsStmt = db3.prepare(`
17496
+ SELECT model, dimensions, COUNT(*) as count
17497
+ FROM turn_embeddings
17498
+ WHERE status = 'done'
17499
+ GROUP BY model, dimensions
17500
+ `);
17501
+ stats.models = {};
17502
+ for (const row of modelsStmt.all()) {
17503
+ stats.models[row.model] = { dimensions: row.dimensions, count: row.count };
17504
+ }
17505
+ return stats;
17506
+ }
17475
17507
  function deleteEmbedding(turnId) {
17476
17508
  const db3 = getDb2();
17477
17509
  db3.prepare("DELETE FROM turn_embeddings WHERE turn_id = ?").run(turnId);
@@ -35517,6 +35549,7 @@ function dbTables(flags) {
35517
35549
  }
35518
35550
 
35519
35551
  // src/commands/session.js
35552
+ var import_readline2 = require("readline");
35520
35553
  var embeddingsModule = null;
35521
35554
  async function getEmbeddings() {
35522
35555
  if (!embeddingsModule) {
@@ -35524,6 +35557,92 @@ async function getEmbeddings() {
35524
35557
  }
35525
35558
  return embeddingsModule;
35526
35559
  }
35560
+ async function confirm(message) {
35561
+ const rl = (0, import_readline2.createInterface)({ input: process.stdin, output: process.stdout });
35562
+ return new Promise((resolve) => {
35563
+ rl.question(`${message} [Y/n] `, (answer) => {
35564
+ rl.close();
35565
+ resolve(answer.toLowerCase() !== "n");
35566
+ });
35567
+ });
35568
+ }
35569
+ async function ensureEmbeddingProvider(preferredProvider = "auto", options = {}) {
35570
+ const { checkProviderStatus: checkProviderStatus2, getProvider: getProvider2 } = await getEmbeddings();
35571
+ const status = await checkProviderStatus2();
35572
+ if (preferredProvider === "openai") {
35573
+ if (status.openai.configured) {
35574
+ return await getProvider2("openai");
35575
+ }
35576
+ console.log("OpenAI not configured. Set OPENAI_API_KEY environment variable.");
35577
+ return null;
35578
+ }
35579
+ try {
35580
+ return await getProvider2("auto");
35581
+ } catch {
35582
+ }
35583
+ if (status.openai.configured) {
35584
+ console.log("\nOllama not available. OpenAI is configured.");
35585
+ const useOpenAI = await confirm("Use OpenAI for embeddings? (costs ~$0.02/1M tokens)");
35586
+ if (useOpenAI) {
35587
+ return await getProvider2("openai");
35588
+ }
35589
+ }
35590
+ console.log("\nNo embedding provider available.\n");
35591
+ console.log("Options:");
35592
+ console.log(" [1] Install Ollama (recommended - free, local, works offline)");
35593
+ console.log(" [2] Use OpenAI (requires OPENAI_API_KEY)");
35594
+ console.log(" [3] Cancel\n");
35595
+ const rl = (0, import_readline2.createInterface)({ input: process.stdin, output: process.stdout });
35596
+ const choice = await new Promise((resolve) => {
35597
+ rl.question("Choice [1]: ", (answer) => {
35598
+ rl.close();
35599
+ resolve(answer || "1");
35600
+ });
35601
+ });
35602
+ if (choice === "3" || choice.toLowerCase() === "cancel") {
35603
+ return null;
35604
+ }
35605
+ if (choice === "2") {
35606
+ if (!status.openai.configured) {
35607
+ console.log("\nOpenAI not configured.");
35608
+ console.log("Set: export OPENAI_API_KEY=your-key");
35609
+ return null;
35610
+ }
35611
+ return await getProvider2("openai");
35612
+ }
35613
+ console.log("\nInstalling Ollama...");
35614
+ try {
35615
+ const { installPackage: installPackage2 } = await Promise.resolve().then(() => (init_src4(), src_exports2));
35616
+ await installPackage2("runtime:ollama", {
35617
+ onProgress: (p2) => {
35618
+ if (p2.phase === "downloading") process.stdout.write("\r Downloading...");
35619
+ if (p2.phase === "extracting") process.stdout.write("\r Installing... ");
35620
+ }
35621
+ });
35622
+ console.log("\r \u2713 Ollama installed ");
35623
+ console.log(" Starting ollama serve...");
35624
+ const { spawn: spawn4 } = await import("child_process");
35625
+ const server = spawn4("ollama", ["serve"], {
35626
+ detached: true,
35627
+ stdio: "ignore",
35628
+ env: { ...process.env, HOME: process.env.HOME }
35629
+ });
35630
+ server.unref();
35631
+ await new Promise((r2) => setTimeout(r2, 2e3));
35632
+ console.log(" Pulling nomic-embed-text model (274MB)...");
35633
+ const { execSync: execSync10 } = await import("child_process");
35634
+ execSync10("ollama pull nomic-embed-text", { stdio: "inherit" });
35635
+ console.log(" \u2713 Model ready\n");
35636
+ return await getProvider2("ollama");
35637
+ } catch (err) {
35638
+ console.error("\nSetup failed:", err.message);
35639
+ console.log("\nManual setup:");
35640
+ console.log(" rudi install ollama");
35641
+ console.log(" ollama serve");
35642
+ console.log(" ollama pull nomic-embed-text");
35643
+ return null;
35644
+ }
35645
+ }
35527
35646
  async function cmdSession(args, flags) {
35528
35647
  const subcommand = args[0];
35529
35648
  switch (subcommand) {
@@ -35560,6 +35679,9 @@ async function cmdSession(args, flags) {
35560
35679
  case "setup":
35561
35680
  await sessionSetup(flags);
35562
35681
  break;
35682
+ case "organize":
35683
+ await sessionOrganize(flags);
35684
+ break;
35563
35685
  default:
35564
35686
  console.log(`
35565
35687
  rudi session - Manage RUDI sessions
@@ -35579,6 +35701,9 @@ SEMANTIC SEARCH
35579
35701
  index [--embeddings] [--provider X] Index sessions for semantic search
35580
35702
  similar <id> [--limit] Find similar sessions
35581
35703
 
35704
+ ORGANIZATION (batch operations)
35705
+ organize [--dry-run] [--out plan.json] Auto-organize sessions into projects
35706
+
35582
35707
  OPTIONS
35583
35708
  --provider <name> Filter by provider (claude, codex, gemini)
35584
35709
  --project <name> Filter by project name
@@ -35926,8 +36051,12 @@ Found ${results.length} result(s) for "${query}":
35926
36051
  async function semanticSearch(query, options) {
35927
36052
  const { limit: limit2, format } = options;
35928
36053
  try {
35929
- const { createClient: createClient2, getProvider: getProvider2 } = await getEmbeddings();
35930
- const { provider, model } = await getProvider2("auto");
36054
+ const { createClient: createClient2 } = await getEmbeddings();
36055
+ const result = await ensureEmbeddingProvider("auto");
36056
+ if (!result) {
36057
+ return;
36058
+ }
36059
+ const { provider, model } = result;
35931
36060
  console.log(`Using ${provider.id} with ${model.name}`);
35932
36061
  const client = createClient2({ provider, model });
35933
36062
  const stats = client.getStats();
@@ -35976,18 +36105,25 @@ async function sessionIndex(flags) {
35976
36105
  const providerName = flags.provider || "auto";
35977
36106
  if (!flags.embeddings) {
35978
36107
  try {
35979
- const { createClient: createClient2, getProvider: getProvider2, store } = await getEmbeddings();
35980
- const model = { name: "text-embedding-3-small", dimensions: 1536 };
35981
- const stats = store.getEmbeddingStats(model);
36108
+ const { store } = await getEmbeddings();
36109
+ const stats = store.getAllEmbeddingStats();
35982
36110
  const pct = stats.total > 0 ? (stats.done / stats.total * 100).toFixed(1) : 0;
35983
36111
  console.log("\nEmbedding Index Status:");
35984
36112
  console.log(` Total turns: ${stats.total}`);
35985
36113
  console.log(` Indexed: ${stats.done} (${pct}%)`);
35986
36114
  console.log(` Queued: ${stats.queued}`);
35987
36115
  console.log(` Errors: ${stats.error}`);
35988
- console.log("\nTo index missing turns:");
35989
- console.log(" rudi session index --embeddings");
35990
- console.log(" rudi session index --embeddings --provider ollama");
36116
+ if (Object.keys(stats.models).length > 0) {
36117
+ console.log("\nIndexed by model:");
36118
+ for (const [model, info] of Object.entries(stats.models)) {
36119
+ console.log(` ${model} (${info.dimensions}d): ${info.count} turns`);
36120
+ }
36121
+ }
36122
+ if (stats.done < stats.total) {
36123
+ console.log("\nTo index missing turns:");
36124
+ console.log(" rudi session index --embeddings");
36125
+ console.log(" rudi session index --embeddings --provider ollama");
36126
+ }
35991
36127
  } catch (err) {
35992
36128
  console.log("Embedding status unavailable:", err.message);
35993
36129
  }
@@ -35995,8 +36131,12 @@ async function sessionIndex(flags) {
35995
36131
  }
35996
36132
  console.log("Indexing sessions for semantic search...\n");
35997
36133
  try {
35998
- const { createClient: createClient2, getProvider: getProvider2 } = await getEmbeddings();
35999
- const { provider, model } = await getProvider2(providerName);
36134
+ const { createClient: createClient2 } = await getEmbeddings();
36135
+ const providerResult = await ensureEmbeddingProvider(providerName);
36136
+ if (!providerResult) {
36137
+ return;
36138
+ }
36139
+ const { provider, model } = providerResult;
36000
36140
  console.log(`Provider: ${provider.id}`);
36001
36141
  console.log(`Model: ${model.name} (${model.dimensions}d)
36002
36142
  `);
@@ -36057,8 +36197,12 @@ async function sessionSimilar(args, flags) {
36057
36197
  const format = flags.format || "table";
36058
36198
  const providerName = flags.provider || "auto";
36059
36199
  try {
36060
- const { createClient: createClient2, getProvider: getProvider2 } = await getEmbeddings();
36061
- const { provider, model } = await getProvider2(providerName);
36200
+ const { createClient: createClient2 } = await getEmbeddings();
36201
+ const result = await ensureEmbeddingProvider(providerName);
36202
+ if (!result) {
36203
+ return;
36204
+ }
36205
+ const { provider, model } = result;
36062
36206
  const client = createClient2({ provider, model });
36063
36207
  const results = await client.findSimilar(turnId, { limit: limit2 });
36064
36208
  if (format === "json") {
@@ -36107,6 +36251,223 @@ async function sessionSetup(flags) {
36107
36251
  console.error("Setup error:", err.message);
36108
36252
  }
36109
36253
  }
36254
+ async function sessionOrganize(flags) {
36255
+ if (!isDatabaseInitialized()) {
36256
+ console.log("Database not initialized.");
36257
+ return;
36258
+ }
36259
+ const dryRun = flags["dry-run"] || flags.dryRun || true;
36260
+ const outputFile = flags.out || flags.output || "organize-plan.json";
36261
+ const threshold = parseFloat(flags.threshold) || 0.65;
36262
+ const db3 = getDb();
36263
+ console.log("\u2550".repeat(60));
36264
+ console.log("Session Organization");
36265
+ console.log("\u2550".repeat(60));
36266
+ console.log(`Mode: ${dryRun ? "Dry run (preview only)" : "LIVE - will apply changes"}`);
36267
+ console.log(`Output: ${outputFile}`);
36268
+ console.log(`Similarity threshold: ${(threshold * 100).toFixed(0)}%`);
36269
+ console.log("\u2550".repeat(60));
36270
+ const sessions = db3.prepare(`
36271
+ SELECT
36272
+ s.id, s.provider, s.title, s.title_override, s.project_id, s.cwd,
36273
+ s.turn_count, s.total_cost, s.created_at, s.last_active_at,
36274
+ p.name as project_name
36275
+ FROM sessions s
36276
+ LEFT JOIN projects p ON s.project_id = p.id
36277
+ WHERE s.status = 'active'
36278
+ ORDER BY s.total_cost DESC
36279
+ `).all();
36280
+ console.log(`
36281
+ Analyzing ${sessions.length} sessions...
36282
+ `);
36283
+ const projects = db3.prepare("SELECT id, name FROM projects").all();
36284
+ const projectMap = new Map(projects.map((p2) => [p2.name.toLowerCase(), p2]));
36285
+ console.log(`Existing projects: ${projects.map((p2) => p2.name).join(", ") || "(none)"}
36286
+ `);
36287
+ const cwdGroups = /* @__PURE__ */ new Map();
36288
+ for (const s2 of sessions) {
36289
+ if (!s2.cwd) continue;
36290
+ const match = s2.cwd.match(/\/([^/]+)$/);
36291
+ const projectKey = match ? match[1] : "other";
36292
+ if (!cwdGroups.has(projectKey)) {
36293
+ cwdGroups.set(projectKey, []);
36294
+ }
36295
+ cwdGroups.get(projectKey).push(s2);
36296
+ }
36297
+ const genericTitlePatterns = [
36298
+ /^(Imported|Agent|New|Untitled|Chat) Session$/i,
36299
+ /^Session \d+$/i,
36300
+ /^Untitled$/i,
36301
+ /^[A-Z][a-z]+ [A-Z][a-z]+ [A-Z][a-z]+$/
36302
+ // "Adjective Verb Noun" (Claude auto-generated)
36303
+ ];
36304
+ const sessionsNeedingTitles = sessions.filter((s2) => {
36305
+ if (s2.title_override && s2.title_override !== s2.title) {
36306
+ return false;
36307
+ }
36308
+ const title = s2.title || "";
36309
+ return !title || genericTitlePatterns.some((p2) => p2.test(title));
36310
+ });
36311
+ console.log(`Sessions with generic titles: ${sessionsNeedingTitles.length}`);
36312
+ const titleSuggestions = [];
36313
+ for (const s2 of sessionsNeedingTitles.slice(0, 100)) {
36314
+ const firstTurn = db3.prepare(`
36315
+ SELECT user_message
36316
+ FROM turns
36317
+ WHERE session_id = ? AND user_message IS NOT NULL AND length(trim(user_message)) > 10
36318
+ ORDER BY turn_number
36319
+ LIMIT 1
36320
+ `).get(s2.id);
36321
+ if (firstTurn && firstTurn.user_message) {
36322
+ const msg = firstTurn.user_message.trim();
36323
+ let suggestedTitle = msg.split("\n")[0].slice(0, 60).trim();
36324
+ const skipPatterns = [
36325
+ /^\/[A-Za-z]/,
36326
+ // Unix paths
36327
+ /^<[a-z-]+>/,
36328
+ // XML tags
36329
+ /^[A-Z]:\\[A-Za-z]/,
36330
+ // Windows paths
36331
+ /^(cd|ls|cat|npm|node|git|rudi|pnpm|yarn)\s/i,
36332
+ // Commands
36333
+ /^[a-f0-9-]{8,}/i,
36334
+ // UUIDs or hashes
36335
+ /^https?:\/\//i,
36336
+ // URLs
36337
+ /^[>\*\-#\d\.]\s/,
36338
+ // Markdown list/quote starts
36339
+ /^(yes|no|ok|sure|y|n)$/i,
36340
+ // Single word responses
36341
+ /^[^a-zA-Z]*$/,
36342
+ // No letters at all
36343
+ /^\s*\[/,
36344
+ // JSON/array starts
36345
+ /^\s*\{/
36346
+ // Object starts
36347
+ ];
36348
+ if (skipPatterns.some((p2) => p2.test(suggestedTitle))) {
36349
+ continue;
36350
+ }
36351
+ const wordCount = suggestedTitle.split(/\s+/).length;
36352
+ if (wordCount < 3) {
36353
+ continue;
36354
+ }
36355
+ if (suggestedTitle.length > 50) {
36356
+ suggestedTitle = suggestedTitle.slice(0, 47) + "...";
36357
+ }
36358
+ if (suggestedTitle && suggestedTitle.length > 10) {
36359
+ titleSuggestions.push({
36360
+ sessionId: s2.id,
36361
+ currentTitle: s2.title || "(none)",
36362
+ suggestedTitle,
36363
+ cost: s2.total_cost,
36364
+ confidence: "medium"
36365
+ // Could add scoring later
36366
+ });
36367
+ }
36368
+ }
36369
+ }
36370
+ const projectSuggestions = [];
36371
+ const moveSuggestions = [];
36372
+ const knownProjects = {
36373
+ "studio": "Prompt Stack Studio",
36374
+ "prompt-stack": "Prompt Stack Studio",
36375
+ "RUDI": "RUDI",
36376
+ "rudi": "RUDI",
36377
+ "cli": "RUDI",
36378
+ "registry": "RUDI",
36379
+ "resonance": "Resonance",
36380
+ "cloud": "Cloud"
36381
+ };
36382
+ for (const [cwdKey, cwdSessions] of cwdGroups) {
36383
+ const projectName = knownProjects[cwdKey];
36384
+ if (projectName && cwdSessions.length >= 2) {
36385
+ const existingProject = projectMap.get(projectName.toLowerCase());
36386
+ for (const s2 of cwdSessions) {
36387
+ if (!s2.project_id || existingProject && s2.project_id !== existingProject.id) {
36388
+ moveSuggestions.push({
36389
+ sessionId: s2.id,
36390
+ sessionTitle: s2.title_override || s2.title,
36391
+ currentProject: s2.project_name || null,
36392
+ suggestedProject: projectName,
36393
+ reason: `Working directory: ${cwdKey}`,
36394
+ cost: s2.total_cost
36395
+ });
36396
+ }
36397
+ }
36398
+ if (!existingProject && cwdSessions.length >= 3) {
36399
+ projectSuggestions.push({
36400
+ name: projectName,
36401
+ sessionCount: cwdSessions.length,
36402
+ totalCost: cwdSessions.reduce((sum, s2) => sum + (s2.total_cost || 0), 0)
36403
+ });
36404
+ }
36405
+ }
36406
+ }
36407
+ const plan = {
36408
+ version: "1.0",
36409
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
36410
+ dryRun,
36411
+ threshold,
36412
+ summary: {
36413
+ totalSessions: sessions.length,
36414
+ sessionsWithProjects: sessions.filter((s2) => s2.project_id).length,
36415
+ sessionsNeedingTitles: sessionsNeedingTitles.length,
36416
+ projectsToCreate: projectSuggestions.length,
36417
+ movesToApply: moveSuggestions.length,
36418
+ titlesToUpdate: titleSuggestions.length
36419
+ },
36420
+ actions: {
36421
+ createProjects: projectSuggestions,
36422
+ moveSessions: moveSuggestions.slice(0, 200),
36423
+ // Limit batch size
36424
+ updateTitles: titleSuggestions.slice(0, 100)
36425
+ // Limit batch size
36426
+ }
36427
+ };
36428
+ console.log("\n" + "\u2500".repeat(60));
36429
+ console.log("PLAN SUMMARY");
36430
+ console.log("\u2500".repeat(60));
36431
+ console.log(`Sessions analyzed: ${plan.summary.totalSessions}`);
36432
+ console.log(`Already in projects: ${plan.summary.sessionsWithProjects}`);
36433
+ console.log(`
36434
+ Proposed actions:`);
36435
+ console.log(` Create projects: ${plan.summary.projectsToCreate}`);
36436
+ console.log(` Move sessions: ${plan.summary.movesToApply}`);
36437
+ console.log(` Update titles: ${plan.summary.titlesToUpdate}`);
36438
+ if (projectSuggestions.length > 0) {
36439
+ console.log("\nProjects to create:");
36440
+ for (const p2 of projectSuggestions) {
36441
+ console.log(` \u2022 ${p2.name} (${p2.sessionCount} sessions, $${p2.totalCost.toFixed(2)})`);
36442
+ }
36443
+ }
36444
+ if (moveSuggestions.length > 0) {
36445
+ console.log("\nTop session moves:");
36446
+ for (const m2 of moveSuggestions.slice(0, 10)) {
36447
+ console.log(` \u2022 "${m2.sessionTitle?.slice(0, 30) || m2.sessionId.slice(0, 8)}..." \u2192 ${m2.suggestedProject}`);
36448
+ }
36449
+ if (moveSuggestions.length > 10) {
36450
+ console.log(` ... and ${moveSuggestions.length - 10} more`);
36451
+ }
36452
+ }
36453
+ if (titleSuggestions.length > 0) {
36454
+ console.log("\nTop title updates:");
36455
+ for (const t2 of titleSuggestions.slice(0, 5)) {
36456
+ console.log(` \u2022 "${t2.currentTitle?.slice(0, 20) || "(none)"}..." \u2192 "${t2.suggestedTitle.slice(0, 30)}..."`);
36457
+ }
36458
+ if (titleSuggestions.length > 5) {
36459
+ console.log(` ... and ${titleSuggestions.length - 5} more`);
36460
+ }
36461
+ }
36462
+ const { writeFileSync: writeFileSync7 } = await import("fs");
36463
+ writeFileSync7(outputFile, JSON.stringify(plan, null, 2));
36464
+ console.log(`
36465
+ \u2713 Plan saved to: ${outputFile}`);
36466
+ console.log("\nTo apply this plan:");
36467
+ console.log(` rudi apply ${outputFile}`);
36468
+ console.log("\nTo review the full plan:");
36469
+ console.log(` cat ${outputFile} | jq .`);
36470
+ }
36110
36471
 
36111
36472
  // src/commands/import.js
36112
36473
  var import_fs17 = require("fs");
@@ -39256,6 +39617,371 @@ Lockfile: ${lockPath}`);
39256
39617
  }
39257
39618
  }
39258
39619
 
39620
+ // src/commands/apply.js
39621
+ var import_fs28 = require("fs");
39622
+ var import_path26 = require("path");
39623
+ var import_os10 = require("os");
39624
+ var import_crypto3 = require("crypto");
39625
+ async function cmdApply(args, flags) {
39626
+ const planFile = args[0];
39627
+ const force = flags.force;
39628
+ const undoPlanId = flags.undo;
39629
+ const only = flags.only;
39630
+ if (undoPlanId) {
39631
+ return undoPlan(undoPlanId);
39632
+ }
39633
+ if (!planFile) {
39634
+ console.log(`
39635
+ rudi apply - Execute organization plans
39636
+
39637
+ USAGE
39638
+ rudi apply <plan.json> Apply a plan file
39639
+ rudi apply --undo <id> Undo a previously applied plan
39640
+
39641
+ OPTIONS
39642
+ --force Skip confirmation prompts
39643
+ --only <type> Apply only specific operations:
39644
+ move - session moves only
39645
+ rename - title updates only
39646
+ project - project creation only
39647
+
39648
+ EXAMPLES
39649
+ rudi session organize --dry-run --out plan.json
39650
+ rudi apply plan.json
39651
+ rudi apply plan.json --only move # Moves first (low regret)
39652
+ rudi apply plan.json --only rename # Renames second
39653
+ rudi apply --undo plan-20260109-abc123
39654
+ `);
39655
+ return;
39656
+ }
39657
+ if (!(0, import_fs28.existsSync)(planFile)) {
39658
+ console.error(`Plan file not found: ${planFile}`);
39659
+ process.exit(1);
39660
+ }
39661
+ if (!isDatabaseInitialized()) {
39662
+ console.error("Database not initialized. Run: rudi db init");
39663
+ process.exit(1);
39664
+ }
39665
+ let plan;
39666
+ try {
39667
+ plan = JSON.parse((0, import_fs28.readFileSync)(planFile, "utf-8"));
39668
+ } catch (err) {
39669
+ console.error(`Invalid plan file: ${err.message}`);
39670
+ process.exit(1);
39671
+ }
39672
+ if (!plan.version || !plan.actions) {
39673
+ console.error("Invalid plan format: missing version or actions");
39674
+ process.exit(1);
39675
+ }
39676
+ console.log("\u2550".repeat(60));
39677
+ console.log("Apply Organization Plan");
39678
+ console.log("\u2550".repeat(60));
39679
+ console.log(`Plan file: ${planFile}`);
39680
+ console.log(`Created: ${plan.createdAt}`);
39681
+ console.log("\u2550".repeat(60));
39682
+ let { createProjects = [], moveSessions = [], updateTitles = [] } = plan.actions;
39683
+ if (only) {
39684
+ console.log(`
39685
+ Filter: --only ${only}`);
39686
+ if (only === "move") {
39687
+ createProjects = [];
39688
+ updateTitles = [];
39689
+ } else if (only === "rename") {
39690
+ createProjects = [];
39691
+ moveSessions = [];
39692
+ } else if (only === "project") {
39693
+ moveSessions = [];
39694
+ updateTitles = [];
39695
+ } else {
39696
+ console.error(`Unknown filter: ${only}. Use: move, rename, project`);
39697
+ process.exit(1);
39698
+ }
39699
+ }
39700
+ console.log("\nActions to apply:");
39701
+ console.log(` Create projects: ${createProjects.length}`);
39702
+ console.log(` Move sessions: ${moveSessions.length}`);
39703
+ console.log(` Update titles: ${updateTitles.length}`);
39704
+ const totalActions = createProjects.length + moveSessions.length + updateTitles.length;
39705
+ if (totalActions === 0) {
39706
+ console.log("\nNo actions to apply (filtered out or empty).");
39707
+ return;
39708
+ }
39709
+ if (!force) {
39710
+ console.log("\nThis will modify your database.");
39711
+ console.log("Add --force to skip this confirmation.\n");
39712
+ const readline3 = await import("readline");
39713
+ const rl = readline3.createInterface({
39714
+ input: process.stdin,
39715
+ output: process.stdout
39716
+ });
39717
+ const answer = await new Promise((resolve) => {
39718
+ rl.question("Apply this plan? (y/N): ", resolve);
39719
+ });
39720
+ rl.close();
39721
+ if (answer.toLowerCase() !== "y") {
39722
+ console.log("Cancelled.");
39723
+ return;
39724
+ }
39725
+ }
39726
+ const db3 = getDb();
39727
+ const planId = `plan-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, "")}-${(0, import_crypto3.randomUUID)().slice(0, 6)}`;
39728
+ const undoActions = [];
39729
+ console.log(`
39730
+ Applying plan ${planId}...
39731
+ `);
39732
+ if (createProjects.length > 0) {
39733
+ console.log("Creating projects...");
39734
+ const insertProject = db3.prepare(`
39735
+ INSERT OR IGNORE INTO projects (id, provider, name, created_at)
39736
+ VALUES (?, 'claude', ?, datetime('now'))
39737
+ `);
39738
+ for (const p2 of createProjects) {
39739
+ const projectId = `proj-${p2.name.toLowerCase().replace(/\s+/g, "-")}`;
39740
+ try {
39741
+ insertProject.run(projectId, p2.name);
39742
+ console.log(` \u2713 Created: ${p2.name}`);
39743
+ undoActions.push({ type: "deleteProject", projectId, name: p2.name });
39744
+ } catch (err) {
39745
+ console.log(` \u26A0 Skipped (exists): ${p2.name}`);
39746
+ }
39747
+ }
39748
+ }
39749
+ if (moveSessions.length > 0) {
39750
+ console.log("\nMoving sessions...");
39751
+ const projectIds = /* @__PURE__ */ new Map();
39752
+ const projects = db3.prepare("SELECT id, name FROM projects").all();
39753
+ for (const p2 of projects) {
39754
+ projectIds.set(p2.name.toLowerCase(), p2.id);
39755
+ }
39756
+ const updateSession = db3.prepare(`
39757
+ UPDATE sessions SET project_id = ? WHERE id = ?
39758
+ `);
39759
+ let moved = 0;
39760
+ for (const m2 of moveSessions) {
39761
+ const projectId = projectIds.get(m2.suggestedProject.toLowerCase());
39762
+ if (!projectId) {
39763
+ console.log(` \u26A0 Project not found: ${m2.suggestedProject}`);
39764
+ continue;
39765
+ }
39766
+ const current = db3.prepare("SELECT project_id FROM sessions WHERE id = ?").get(m2.sessionId);
39767
+ try {
39768
+ updateSession.run(projectId, m2.sessionId);
39769
+ moved++;
39770
+ undoActions.push({
39771
+ type: "moveSession",
39772
+ sessionId: m2.sessionId,
39773
+ fromProject: current?.project_id,
39774
+ toProject: projectId
39775
+ });
39776
+ } catch (err) {
39777
+ console.log(` \u26A0 Failed: ${m2.sessionId} - ${err.message}`);
39778
+ }
39779
+ }
39780
+ console.log(` \u2713 Moved ${moved} sessions`);
39781
+ }
39782
+ if (updateTitles.length > 0) {
39783
+ console.log("\nUpdating titles...");
39784
+ const updateTitle = db3.prepare(`
39785
+ UPDATE sessions SET title = ?, title_override = ? WHERE id = ?
39786
+ `);
39787
+ let updated = 0;
39788
+ for (const t2 of updateTitles) {
39789
+ const current = db3.prepare("SELECT title, title_override FROM sessions WHERE id = ?").get(t2.sessionId);
39790
+ try {
39791
+ updateTitle.run(t2.suggestedTitle, t2.suggestedTitle, t2.sessionId);
39792
+ updated++;
39793
+ undoActions.push({
39794
+ type: "updateTitle",
39795
+ sessionId: t2.sessionId,
39796
+ fromTitle: current?.title,
39797
+ fromTitleOverride: current?.title_override,
39798
+ toTitle: t2.suggestedTitle
39799
+ });
39800
+ } catch (err) {
39801
+ console.log(` \u26A0 Failed: ${t2.sessionId} - ${err.message}`);
39802
+ }
39803
+ }
39804
+ console.log(` \u2713 Updated ${updated} titles`);
39805
+ }
39806
+ const undoDir = (0, import_path26.join)((0, import_os10.homedir)(), ".rudi", "plans");
39807
+ const { mkdirSync: mkdirSync5 } = await import("fs");
39808
+ try {
39809
+ mkdirSync5(undoDir, { recursive: true });
39810
+ } catch (e2) {
39811
+ }
39812
+ const undoFile = (0, import_path26.join)(undoDir, `${planId}.undo.json`);
39813
+ const undoPlan = {
39814
+ planId,
39815
+ appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
39816
+ sourceFile: planFile,
39817
+ actions: undoActions
39818
+ };
39819
+ (0, import_fs28.writeFileSync)(undoFile, JSON.stringify(undoPlan, null, 2));
39820
+ console.log("\n" + "\u2550".repeat(60));
39821
+ console.log("Plan applied successfully!");
39822
+ console.log("\u2550".repeat(60));
39823
+ console.log(`Plan ID: ${planId}`);
39824
+ console.log(`Undo file: ${undoFile}`);
39825
+ console.log(`
39826
+ To undo: rudi apply --undo ${planId}`);
39827
+ }
39828
+
39829
+ // src/commands/project.js
39830
+ async function cmdProject(args, flags) {
39831
+ const subcommand = args[0];
39832
+ switch (subcommand) {
39833
+ case "list":
39834
+ case "ls":
39835
+ projectList(flags);
39836
+ break;
39837
+ case "create":
39838
+ case "add":
39839
+ projectCreate(args.slice(1), flags);
39840
+ break;
39841
+ case "rename":
39842
+ projectRename(args.slice(1), flags);
39843
+ break;
39844
+ case "delete":
39845
+ case "rm":
39846
+ projectDelete(args.slice(1), flags);
39847
+ break;
39848
+ default:
39849
+ console.log(`
39850
+ rudi project - Manage session projects
39851
+
39852
+ COMMANDS
39853
+ list List all projects
39854
+ create <name> Create a new project
39855
+ rename <id> <new-name> Rename a project
39856
+ delete <id> Delete a project (sessions become unassigned)
39857
+
39858
+ OPTIONS
39859
+ --provider <name> Provider (claude, codex, gemini). Default: claude
39860
+
39861
+ EXAMPLES
39862
+ rudi project list
39863
+ rudi project create "RUDI CLI"
39864
+ rudi project rename proj-rudi "RUDI Tooling"
39865
+ rudi project delete proj-old
39866
+ `);
39867
+ }
39868
+ }
39869
+ function projectList(flags) {
39870
+ if (!isDatabaseInitialized()) {
39871
+ console.log("Database not initialized. Run: rudi db init");
39872
+ return;
39873
+ }
39874
+ const db3 = getDb();
39875
+ const provider = flags.provider;
39876
+ let query = `
39877
+ SELECT
39878
+ p.id, p.provider, p.name, p.color, p.created_at,
39879
+ COUNT(s.id) as session_count,
39880
+ ROUND(SUM(s.total_cost), 2) as total_cost
39881
+ FROM projects p
39882
+ LEFT JOIN sessions s ON s.project_id = p.id
39883
+ `;
39884
+ if (provider) {
39885
+ query += ` WHERE p.provider = '${provider}'`;
39886
+ }
39887
+ query += ` GROUP BY p.id ORDER BY total_cost DESC`;
39888
+ const projects = db3.prepare(query).all();
39889
+ if (projects.length === 0) {
39890
+ console.log("No projects found.");
39891
+ console.log('\nCreate one with: rudi project create "My Project"');
39892
+ return;
39893
+ }
39894
+ console.log(`
39895
+ Projects (${projects.length}):
39896
+ `);
39897
+ for (const p2 of projects) {
39898
+ console.log(`${p2.name}`);
39899
+ console.log(` ID: ${p2.id}`);
39900
+ console.log(` Provider: ${p2.provider}`);
39901
+ console.log(` Sessions: ${p2.session_count || 0}`);
39902
+ console.log(` Total cost: $${p2.total_cost || 0}`);
39903
+ console.log("");
39904
+ }
39905
+ }
39906
+ function projectCreate(args, flags) {
39907
+ if (!isDatabaseInitialized()) {
39908
+ console.log("Database not initialized. Run: rudi db init");
39909
+ return;
39910
+ }
39911
+ const name = args.join(" ");
39912
+ if (!name) {
39913
+ console.log("Error: Project name required");
39914
+ console.log('Usage: rudi project create "Project Name"');
39915
+ return;
39916
+ }
39917
+ const provider = flags.provider || "claude";
39918
+ const id = `proj-${name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "")}`;
39919
+ const db3 = getDb();
39920
+ try {
39921
+ db3.prepare(`
39922
+ INSERT INTO projects (id, provider, name, created_at)
39923
+ VALUES (?, ?, ?, datetime('now'))
39924
+ `).run(id, provider, name);
39925
+ console.log(`
39926
+ Project created:`);
39927
+ console.log(` ID: ${id}`);
39928
+ console.log(` Name: ${name}`);
39929
+ console.log(` Provider: ${provider}`);
39930
+ } catch (err) {
39931
+ if (err.message.includes("UNIQUE")) {
39932
+ console.log(`Error: Project "${name}" already exists for ${provider}`);
39933
+ } else {
39934
+ console.log(`Error: ${err.message}`);
39935
+ }
39936
+ }
39937
+ }
39938
+ function projectRename(args, flags) {
39939
+ if (!isDatabaseInitialized()) {
39940
+ console.log("Database not initialized.");
39941
+ return;
39942
+ }
39943
+ const [id, ...nameParts] = args;
39944
+ const newName = nameParts.join(" ");
39945
+ if (!id || !newName) {
39946
+ console.log("Error: Project ID and new name required");
39947
+ console.log('Usage: rudi project rename <id> "New Name"');
39948
+ return;
39949
+ }
39950
+ const db3 = getDb();
39951
+ const result = db3.prepare("UPDATE projects SET name = ? WHERE id = ?").run(newName, id);
39952
+ if (result.changes === 0) {
39953
+ console.log(`Project not found: ${id}`);
39954
+ return;
39955
+ }
39956
+ console.log(`
39957
+ Project renamed to: ${newName}`);
39958
+ }
39959
+ function projectDelete(args, flags) {
39960
+ if (!isDatabaseInitialized()) {
39961
+ console.log("Database not initialized.");
39962
+ return;
39963
+ }
39964
+ const id = args[0];
39965
+ if (!id) {
39966
+ console.log("Error: Project ID required");
39967
+ console.log("Usage: rudi project delete <id>");
39968
+ return;
39969
+ }
39970
+ const db3 = getDb();
39971
+ const project = db3.prepare("SELECT name FROM projects WHERE id = ?").get(id);
39972
+ if (!project) {
39973
+ console.log(`Project not found: ${id}`);
39974
+ return;
39975
+ }
39976
+ const sessionsResult = db3.prepare("UPDATE sessions SET project_id = NULL WHERE project_id = ?").run(id);
39977
+ db3.prepare("DELETE FROM projects WHERE id = ?").run(id);
39978
+ console.log(`
39979
+ Project deleted: ${project.name}`);
39980
+ if (sessionsResult.changes > 0) {
39981
+ console.log(`Unassigned ${sessionsResult.changes} sessions`);
39982
+ }
39983
+ }
39984
+
39259
39985
  // src/index.js
39260
39986
  var VERSION2 = "2.0.0";
39261
39987
  async function main() {
@@ -39306,6 +40032,13 @@ async function main() {
39306
40032
  case "import":
39307
40033
  await cmdImport(args, flags);
39308
40034
  break;
40035
+ case "apply":
40036
+ await cmdApply(args, flags);
40037
+ break;
40038
+ case "project":
40039
+ case "projects":
40040
+ await cmdProject(args, flags);
40041
+ break;
39309
40042
  case "doctor":
39310
40043
  await cmdDoctor(args, flags);
39311
40044
  break;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "1.0.0",
3
- "generated": "2026-01-09T20:10:12.259Z",
3
+ "generated": "2026-01-09T21:15:38.064Z",
4
4
  "packages": {
5
5
  "runtimes": [
6
6
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@learnrudi/cli",
3
- "version": "1.9.12",
3
+ "version": "1.10.0",
4
4
  "description": "RUDI CLI - Install and manage MCP stacks, runtimes, and AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -12,18 +12,26 @@
12
12
  "scripts",
13
13
  "README.md"
14
14
  ],
15
+ "scripts": {
16
+ "start": "node src/index.js",
17
+ "prebuild": "node scripts/generate-manifest.js",
18
+ "build": "esbuild src/index.js --bundle --platform=node --format=cjs --outfile=dist/index.cjs --external:better-sqlite3 && cp src/router-mcp.js dist/router-mcp.js && cp src/packages-manifest.json dist/packages-manifest.json",
19
+ "postinstall": "node scripts/postinstall.js",
20
+ "prepublishOnly": "npm run build",
21
+ "test": "node --test src/__tests__/"
22
+ },
15
23
  "dependencies": {
16
- "better-sqlite3": "^12.5.0",
17
- "@learnrudi/core": "1.0.5",
18
- "@learnrudi/db": "1.0.2",
19
- "@learnrudi/env": "1.0.1",
20
- "@learnrudi/embeddings": "0.1.0",
21
- "@learnrudi/manifest": "1.0.0",
22
- "@learnrudi/mcp": "1.0.0",
23
- "@learnrudi/registry-client": "1.0.5",
24
- "@learnrudi/runner": "1.0.1",
25
- "@learnrudi/utils": "1.0.0",
26
- "@learnrudi/secrets": "1.0.1"
24
+ "@learnrudi/core": "workspace:*",
25
+ "@learnrudi/db": "workspace:*",
26
+ "@learnrudi/embeddings": "workspace:*",
27
+ "@learnrudi/env": "workspace:*",
28
+ "@learnrudi/manifest": "workspace:*",
29
+ "@learnrudi/mcp": "workspace:*",
30
+ "@learnrudi/registry-client": "workspace:*",
31
+ "@learnrudi/runner": "workspace:*",
32
+ "@learnrudi/secrets": "workspace:*",
33
+ "@learnrudi/utils": "workspace:*",
34
+ "better-sqlite3": "^12.5.0"
27
35
  },
28
36
  "devDependencies": {
29
37
  "esbuild": "^0.27.2"
@@ -49,12 +57,5 @@
49
57
  "publishConfig": {
50
58
  "access": "public"
51
59
  },
52
- "license": "MIT",
53
- "scripts": {
54
- "start": "node src/index.js",
55
- "prebuild": "node scripts/generate-manifest.js",
56
- "build": "esbuild src/index.js --bundle --platform=node --format=cjs --outfile=dist/index.cjs --external:better-sqlite3 && cp src/router-mcp.js dist/router-mcp.js && cp src/packages-manifest.json dist/packages-manifest.json",
57
- "postinstall": "node scripts/postinstall.js",
58
- "test": "node --test src/__tests__/"
59
- }
60
- }
60
+ "license": "MIT"
61
+ }