@knowsuchagency/fulcrum 4.2.0 → 4.3.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.
Files changed (4) hide show
  1. package/README.md +16 -12
  2. package/bin/fulcrum.js +694 -364
  3. package/package.json +1 -1
  4. package/server/index.js +54750 -52242
package/bin/fulcrum.js CHANGED
@@ -46712,7 +46712,7 @@ async function runMcpServer(urlOverride, portOverride) {
46712
46712
  const client = new FulcrumClient(urlOverride, portOverride);
46713
46713
  const server = new McpServer({
46714
46714
  name: "fulcrum",
46715
- version: "4.2.0"
46715
+ version: "4.3.0"
46716
46716
  });
46717
46717
  registerTools(server, client);
46718
46718
  const transport = new StdioServerTransport;
@@ -49061,7 +49061,7 @@ var marketplace_default = `{
49061
49061
  "name": "fulcrum",
49062
49062
  "source": "./",
49063
49063
  "description": "Task orchestration for Claude Code",
49064
- "version": "4.2.0",
49064
+ "version": "4.3.0",
49065
49065
  "skills": [
49066
49066
  "./skills/fulcrum"
49067
49067
  ],
@@ -49080,6 +49080,20 @@ var marketplace_default = `{
49080
49080
  }
49081
49081
  `;
49082
49082
 
49083
+ // plugins/fulcrum/.claude-plugin/plugin.json
49084
+ var plugin_default = `{
49085
+ "name": "fulcrum",
49086
+ "description": "Fulcrum task orchestration for Claude Code",
49087
+ "version": "4.3.0",
49088
+ "author": {
49089
+ "name": "Fulcrum"
49090
+ },
49091
+ "hooks": "./hooks/hooks.json",
49092
+ "skills": "./skills/",
49093
+ "commands": "./commands/"
49094
+ }
49095
+ `;
49096
+
49083
49097
  // plugins/fulcrum/hooks/hooks.json
49084
49098
  var hooks_default = `{
49085
49099
  "hooks": {
@@ -49107,17 +49121,6 @@ var hooks_default = `{
49107
49121
  }
49108
49122
  `;
49109
49123
 
49110
- // plugins/fulcrum/.mcp.json
49111
- var _mcp_default = `{
49112
- "mcpServers": {
49113
- "fulcrum": {
49114
- "command": "fulcrum",
49115
- "args": ["mcp"]
49116
- }
49117
- }
49118
- }
49119
- `;
49120
-
49121
49124
  // plugins/fulcrum/commands/pr.md
49122
49125
  var pr_default = `---
49123
49126
  description: Link a GitHub PR to the current fulcrum task
@@ -49151,88 +49154,55 @@ description: AI orchestration and task management platform. Use this skill when
49151
49154
 
49152
49155
  # Fulcrum - AI Orchestration Platform
49153
49156
 
49154
- ## Overview
49155
-
49156
- Fulcrum is an AI orchestration and task management platform. It provides task tracking, project management, and tools for AI agents to work autonomously across isolated git worktrees.
49157
-
49158
- **Key Features:**
49159
- - Task management with kanban boards and status tracking
49160
- - Project and repository organization
49161
- - Git worktree isolation for parallel development
49162
- - Docker Compose app deployment
49163
- - Multi-channel notifications
49164
- - MCP tools for AI agent integration
49165
-
49166
49157
  ## When to Use This Skill
49167
49158
 
49168
49159
  Use the Fulcrum CLI when:
49169
- - **Working in a task worktree** - Use \`current-task\` commands to manage your current task
49170
- - **Updating task status** - Mark tasks as in-progress, ready for review, done, or canceled
49171
- - **Linking PRs** - Associate a GitHub PR with the current task
49172
- - **Linking URLs** - Attach any relevant URLs (design docs, specs, external resources) to the task
49173
- - **Sending notifications** - Alert the user when work is complete or needs attention
49174
- - **Server management** - Start, stop, and check server status
49175
-
49176
- Use the Fulcrum MCP tools when:
49177
- - **Task CRUD operations** - Create, list, update, delete tasks
49178
- - **Project/repository management** - Create, organize, and manage projects and repositories
49179
- - **App deployment** - Deploy, stop, and monitor Docker Compose applications
49180
- - **Filesystem operations** - Read, write, and edit files on the Fulcrum server
49181
- - **Executing commands remotely** - Run shell commands on the Fulcrum server from Claude Desktop
49182
- - **Stateful workflows** - Use persistent sessions to maintain environment variables and working directory across commands
49183
-
49184
- ## Core CLI Commands
49160
+ - **Working in a task worktree** \u2014 Use \`current-task\` commands to manage your current task
49161
+ - **Updating task status** \u2014 Mark tasks as in-progress, ready for review, done, or canceled
49162
+ - **Linking PRs** \u2014 Associate a GitHub PR with the current task
49163
+ - **Linking URLs** \u2014 Attach relevant URLs (design docs, specs, external resources) to the task
49164
+ - **Sending notifications** \u2014 Alert the user when work is complete or needs attention
49165
+ - **Server management** \u2014 Start, stop, and check server status
49166
+ - **API access** \u2014 Query or modify any Fulcrum data via \`fulcrum api\`
49167
+
49168
+ ## CLI Commands
49185
49169
 
49186
49170
  ### current-task (Primary Agent Workflow)
49187
49171
 
49188
- When running inside a Fulcrum task worktree, use these commands to manage the current task:
49172
+ When running inside a Fulcrum task worktree, manage the current task:
49189
49173
 
49190
49174
  \`\`\`bash
49191
- # Get current task info (comprehensive output with dependencies, attachments, etc.)
49192
- fulcrum current-task
49193
-
49194
- # Update task status
49195
- fulcrum current-task in-progress # Mark as IN_PROGRESS
49196
- fulcrum current-task review # Mark as IN_REVIEW (notifies user)
49197
- fulcrum current-task done # Mark as DONE
49198
- fulcrum current-task cancel # Mark as CANCELED
49199
-
49200
- # Link a GitHub PR to the current task
49201
- fulcrum current-task pr <github-pr-url>
49202
-
49203
- # Link a Linear ticket to the current task
49204
- fulcrum current-task linear <linear-issue-url>
49205
-
49206
- # Add arbitrary URL links to the task
49207
- fulcrum current-task link <url> # Add link (auto-detects type/label)
49208
- fulcrum current-task link <url> --label "Docs" # Add link with custom label
49209
- fulcrum current-task link # List all links
49210
- fulcrum current-task link --remove <url-or-id> # Remove a link
49175
+ fulcrum current-task # Get full task info
49176
+ fulcrum current-task in-progress # Mark as IN_PROGRESS
49177
+ fulcrum current-task review # Mark as IN_REVIEW (notifies user)
49178
+ fulcrum current-task done # Mark as DONE
49179
+ fulcrum current-task cancel # Mark as CANCELED
49180
+ fulcrum current-task pr <github-pr-url> # Link a GitHub PR
49181
+ fulcrum current-task linear <linear-url> # Link a Linear ticket
49182
+ fulcrum current-task link <url> # Add link (auto-detects type/label)
49183
+ fulcrum current-task link <url> --label "Docs" # Add link with custom label
49184
+ fulcrum current-task link # List all links
49185
+ fulcrum current-task link --remove <url-or-id> # Remove a link
49211
49186
  \`\`\`
49212
49187
 
49213
49188
  ### notifications
49214
49189
 
49215
- Send notifications to the user:
49216
-
49217
49190
  \`\`\`bash
49218
- # Send a notification
49219
- fulcrum notify "Title" "Message body"
49220
-
49221
- # Check notification settings
49222
- fulcrum notifications
49223
-
49224
- # Enable/disable notifications
49225
- fulcrum notifications enable
49226
- fulcrum notifications disable
49191
+ fulcrum notify "Title" "Message body" # Send a notification
49192
+ fulcrum notifications # Check notification settings
49193
+ fulcrum notifications enable # Enable notifications
49194
+ fulcrum notifications disable # Disable notifications
49195
+ fulcrum notifications test slack # Test a channel
49196
+ fulcrum notifications set slack webhookUrl <url> # Configure a channel
49197
+ \`\`\`
49227
49198
 
49228
- # Test a notification channel
49229
- fulcrum notifications test sound
49230
- fulcrum notifications test slack
49231
- fulcrum notifications test discord
49232
- fulcrum notifications test pushover
49199
+ ### config
49233
49200
 
49234
- # Configure a channel
49235
- fulcrum notifications set slack webhookUrl <url>
49201
+ \`\`\`bash
49202
+ fulcrum config list # List all config values
49203
+ fulcrum config get <key> # Get a specific value
49204
+ fulcrum config set <key> <value> # Set a value
49205
+ fulcrum config reset <key> # Reset to default
49236
49206
  \`\`\`
49237
49207
 
49238
49208
  ### Server Management
@@ -49244,25 +49214,63 @@ fulcrum status # Check if server is running
49244
49214
  fulcrum doctor # Check all dependencies and versions
49245
49215
  \`\`\`
49246
49216
 
49247
- ### Configuration
49217
+ ## fulcrum api \u2014 REST API Access
49218
+
49219
+ The \`fulcrum api\` command provides direct access to all Fulcrum REST API endpoints via a resource/action CLI.
49220
+
49221
+ ### Syntax
49248
49222
 
49249
49223
  \`\`\`bash
49250
- fulcrum config list # List all config values
49251
- fulcrum config get <key> # Get a specific value
49252
- fulcrum config set <key> <value> # Set a value
49253
- fulcrum config reset <key> # Reset to default
49224
+ fulcrum api <resource> <action> [<id> ...] [--flag value ...]
49254
49225
  \`\`\`
49255
49226
 
49256
- ### Agent Plugin Installation
49227
+ ### Getting the Full Tool Reference
49257
49228
 
49258
49229
  \`\`\`bash
49259
- # Claude Code integration
49260
- fulcrum claude install # Install Fulcrum plugin for Claude Code
49261
- fulcrum claude uninstall # Uninstall plugin
49230
+ fulcrum api tools # Compact reference of all resources, actions, and flags
49231
+ \`\`\`
49232
+
49233
+ The \`tools\` output is designed for context window injection (~2,000 tokens for ~130 actions).
49234
+
49235
+ ### Common Examples
49236
+
49237
+ \`\`\`bash
49238
+ # Tasks
49239
+ fulcrum api tasks list --search bug --statuses TO_DO,IN_PROGRESS
49240
+ fulcrum api tasks create --title "Fix bug" --type worktree
49241
+ fulcrum api tasks get <id>
49242
+ fulcrum api tasks move <id> --status DONE
49243
+ fulcrum api tasks update <id> --priority high --tags "backend,urgent"
49244
+
49245
+ # Memory
49246
+ fulcrum api memory store --content "Learned X" --tags "project,pattern"
49247
+ fulcrum api memory search --q "deployment"
49248
+
49249
+ # Search
49250
+ fulcrum api search query --q "authentication" --entities tasks,projects
49251
+
49252
+ # Backup
49253
+ fulcrum api backup create --description "Before migration"
49254
+ fulcrum api backup list
49255
+
49256
+ # Calendar
49257
+ fulcrum api caldav events --from 2026-01-01 --to 2026-01-31
49258
+ fulcrum api caldav sync
49259
+ \`\`\`
49260
+
49261
+ ### Raw HTTP Mode (Backward Compatible)
49262
+
49263
+ \`\`\`bash
49264
+ fulcrum api GET /api/tasks
49265
+ fulcrum api POST /api/tasks -d '{"title":"Fix bug"}'
49266
+ \`\`\`
49262
49267
 
49263
- # OpenCode integration
49264
- fulcrum opencode install # Install Fulcrum plugin for OpenCode
49265
- fulcrum opencode uninstall # Uninstall plugin
49268
+ ### Route Discovery
49269
+
49270
+ \`\`\`bash
49271
+ fulcrum api routes # List all routes by category
49272
+ fulcrum api routes --category tasks # Filter by category
49273
+ fulcrum api routes --search calendar # Search routes by keyword
49266
49274
  \`\`\`
49267
49275
 
49268
49276
  ## Agent Workflow Patterns
@@ -49278,13 +49286,8 @@ fulcrum opencode uninstall # Uninstall plugin
49278
49286
  ### Linking External Resources
49279
49287
 
49280
49288
  \`\`\`bash
49281
- # After creating a GitHub PR
49282
49289
  fulcrum current-task pr https://github.com/owner/repo/pull/123
49283
-
49284
- # After identifying the relevant Linear ticket
49285
49290
  fulcrum current-task linear https://linear.app/team/issue/TEAM-123
49286
-
49287
- # Add any URL link (design docs, figma, notion, external resources)
49288
49291
  fulcrum current-task link https://figma.com/file/abc123/design
49289
49292
  fulcrum current-task link https://notion.so/team/spec --label "Product Spec"
49290
49293
  \`\`\`
@@ -49292,256 +49295,31 @@ fulcrum current-task link https://notion.so/team/spec --label "Product Spec"
49292
49295
  ### Notifying the User
49293
49296
 
49294
49297
  \`\`\`bash
49295
- # When work is complete
49296
49298
  fulcrum notify "Task Complete" "Implemented the new feature and created PR #123"
49297
-
49298
- # When blocked or need input
49299
49299
  fulcrum notify "Need Input" "Which approach should I use for the database migration?"
49300
49300
  \`\`\`
49301
49301
 
49302
49302
  ## Global Options
49303
49303
 
49304
- These flags work with most commands:
49305
-
49306
- - \`--port=<port>\` - Server port (default: 7777)
49307
- - \`--url=<url>\` - Override full server URL
49308
- - \`--json\` - Output as JSON for programmatic use
49304
+ - \`--port=<port>\` \u2014 Server port (default: 7777)
49305
+ - \`--url=<url>\` \u2014 Override full server URL
49306
+ - \`--json\` \u2014 Output as JSON for programmatic use
49309
49307
 
49310
49308
  ## Task Statuses
49311
49309
 
49312
- - \`TO_DO\` - Task not yet started
49313
- - \`IN_PROGRESS\` - Task is being worked on
49314
- - \`IN_REVIEW\` - Task is complete and awaiting review
49315
- - \`DONE\` - Task is finished
49316
- - \`CANCELED\` - Task was abandoned
49317
-
49318
- ## MCP Tools
49319
-
49320
- Fulcrum provides a comprehensive set of MCP tools for AI agents. Use \`search_tools\` to discover available tools.
49321
-
49322
- ### Tool Discovery
49323
-
49324
- #### search_tools
49325
-
49326
- Search for available tools by keyword or category:
49327
-
49328
- \`\`\`json
49329
- {
49330
- "query": "deploy", // Optional: Search term
49331
- "category": "apps" // Optional: Filter by category
49332
- }
49333
- \`\`\`
49334
-
49335
- **Categories:** core, tasks, projects, repositories, apps, filesystem, git, notifications, exec
49336
-
49337
- **Example Usage:**
49338
- \`\`\`
49339
- search_tools { query: "project create" }
49340
- \u2192 Returns tools for creating projects
49341
-
49342
- search_tools { category: "filesystem" }
49343
- \u2192 Returns all filesystem tools
49344
- \`\`\`
49345
-
49346
- ### Task Tools
49347
-
49348
- - \`list_tasks\` - List tasks with flexible filtering (search, tags, statuses, date range, overdue)
49349
- - \`get_task\` - Get task details by ID (includes dependencies and attachments)
49350
- - \`create_task\` - Create a new task with worktree
49351
- - \`update_task\` - Update task metadata
49352
- - \`delete_task\` - Delete a task
49353
- - \`move_task\` - Move task to different status
49354
- - \`add_task_link\` - Add URL link to task
49355
- - \`remove_task_link\` - Remove link from task
49356
- - \`list_task_links\` - List all task links
49357
- - \`add_task_tag\` - Add a tag to a task (returns similar tags to catch typos)
49358
- - \`remove_task_tag\` - Remove a tag from a task
49359
- - \`set_task_due_date\` - Set or clear task due date
49360
- - \`list_tags\` - List all unique tags in use with optional search
49361
-
49362
- #### Task Dependencies
49363
-
49364
- - \`get_task_dependencies\` - Get dependencies and dependents of a task, and whether it is blocked
49365
- - \`add_task_dependency\` - Add a dependency (task cannot start until dependency is done)
49366
- - \`remove_task_dependency\` - Remove a dependency
49367
- - \`get_task_dependency_graph\` - Get all tasks and dependencies as a graph for visualization
49368
-
49369
- #### Task Attachments
49370
-
49371
- - \`list_task_attachments\` - List all file attachments for a task
49372
- - \`upload_task_attachment\` - Upload a file to a task from a local path
49373
- - \`delete_task_attachment\` - Delete a file attachment from a task
49374
- - \`get_task_attachment_path\` - Get the local file path for a task attachment
49375
-
49376
- #### Task Search and Filtering
49377
-
49378
- The \`list_tasks\` tool supports powerful filtering for AI agents:
49379
-
49380
- \`\`\`json
49381
- {
49382
- "search": "ocai", // Text search across title, tags, project name
49383
- "tags": ["bug", "urgent"], // Filter by multiple tags (OR logic)
49384
- "statuses": ["TO_DO", "IN_PROGRESS"], // Filter by multiple statuses (OR logic)
49385
- "dueDateStart": "2026-01-18", // Start of date range
49386
- "dueDateEnd": "2026-01-25", // End of date range
49387
- "overdue": true // Only show overdue tasks
49388
- }
49389
- \`\`\`
49390
-
49391
- #### Tag Discovery
49392
-
49393
- Use \`list_tags\` to discover exact tag names before filtering:
49394
-
49395
- \`\`\`json
49396
- // Find tags matching "communication"
49397
- { "search": "communication" }
49398
- // Returns: [{ "name": "communication required", "count": 5 }]
49399
- \`\`\`
49400
-
49401
- This helps handle typos and variations - search first, then use the exact tag name.
49402
-
49403
- ### Project Tools
49404
-
49405
- - \`list_projects\` - List all projects
49406
- - \`get_project\` - Get project details
49407
- - \`create_project\` - Create from path, URL, or existing repo
49408
- - \`update_project\` - Update name, description, status
49409
- - \`delete_project\` - Delete project and optionally directory/app
49410
- - \`scan_projects\` - Scan directory for git repos
49411
-
49412
- #### Project Tags
49413
-
49414
- - \`add_project_tag\` - Add a tag to a project (creates new if needed)
49415
- - \`remove_project_tag\` - Remove a tag from a project
49416
-
49417
- #### Project Attachments
49418
-
49419
- - \`list_project_attachments\` - List all file attachments for a project
49420
- - \`upload_project_attachment\` - Upload a file to a project from a local path
49421
- - \`delete_project_attachment\` - Delete a file attachment from a project
49422
- - \`get_project_attachment_path\` - Get the local file path for a project attachment
49423
-
49424
- #### Project Links
49425
-
49426
- - \`list_project_links\` - List all URL links attached to a project
49427
- - \`add_project_link\` - Add a URL link to a project (auto-detects type)
49428
- - \`remove_project_link\` - Remove a URL link from a project
49429
-
49430
- ### Repository Tools
49431
-
49432
- - \`list_repositories\` - List all repositories (supports orphans filter)
49433
- - \`get_repository\` - Get repository details by ID
49434
- - \`add_repository\` - Add repository from local path
49435
- - \`update_repository\` - Update repository metadata (name, agent, startup script)
49436
- - \`delete_repository\` - Delete orphaned repository (fails if linked to project)
49437
- - \`link_repository_to_project\` - Link repo to project (errors if already linked elsewhere)
49438
- - \`unlink_repository_from_project\` - Unlink repo from project
49439
-
49440
- ### App/Deployment Tools
49441
-
49442
- - \`list_apps\` - List all deployed apps
49443
- - \`get_app\` - Get app details with services
49444
- - \`create_app\` - Create app for deployment
49445
- - \`deploy_app\` - Trigger deployment
49446
- - \`stop_app\` - Stop running app
49447
- - \`get_app_logs\` - Get container logs
49448
- - \`get_app_status\` - Get container status
49449
- - \`list_deployments\` - Get deployment history
49450
- - \`delete_app\` - Delete app
49451
-
49452
- ### Filesystem Tools
49453
-
49454
- Remote filesystem tools for working with files on the Fulcrum server. Useful when the agent runs on a different machine than the server (e.g., via SSH tunneling to Claude Desktop).
49455
-
49456
- - \`list_directory\` - List directory contents
49457
- - \`get_file_tree\` - Get recursive file tree
49458
- - \`read_file\` - Read file contents (secured)
49459
- - \`write_file\` - Write entire file content (secured)
49460
- - \`edit_file\` - Edit file by replacing a unique string (secured)
49461
- - \`file_stat\` - Get file/directory metadata
49462
- - \`is_git_repo\` - Check if directory is git repo
49463
-
49464
- ### Command Execution
49465
-
49466
- When using Claude Desktop with Fulcrum's MCP server, you can execute commands on the remote Fulcrum server. This is useful when connecting to Fulcrum via SSH port forwarding.
49467
-
49468
- #### execute_command
49469
-
49470
- Execute shell commands with optional persistent session support:
49471
-
49472
- \`\`\`json
49473
- {
49474
- "command": "echo hello world",
49475
- "sessionId": "optional-session-id",
49476
- "cwd": "/path/to/start",
49477
- "timeout": 30000,
49478
- "name": "my-session"
49479
- }
49480
- \`\`\`
49481
-
49482
- **Parameters:**
49483
- - \`command\` (required) \u2014 The shell command to execute
49484
- - \`sessionId\` (optional) \u2014 Reuse a session to preserve env vars, cwd, and shell state
49485
- - \`cwd\` (optional) \u2014 Initial working directory (only used when creating new session)
49486
- - \`timeout\` (optional) \u2014 Timeout in milliseconds (default: 30000)
49487
- - \`name\` (optional) \u2014 Session name for identification (only used when creating new session)
49488
-
49489
- **Response:**
49490
- \`\`\`json
49491
- {
49492
- "sessionId": "uuid",
49493
- "stdout": "hello world",
49494
- "stderr": "",
49495
- "exitCode": 0,
49496
- "timedOut": false
49497
- }
49498
- \`\`\`
49499
-
49500
- ### Session Workflow Example
49501
-
49502
- \`\`\`
49503
- 1. First command (creates named session):
49504
- execute_command { command: "cd /project && export API_KEY=secret", name: "dev-session" }
49505
- \u2192 Returns sessionId: "abc-123"
49506
-
49507
- 2. Subsequent commands (reuse session):
49508
- execute_command { command: "echo $API_KEY", sessionId: "abc-123" }
49509
- \u2192 Returns stdout: "secret" (env var preserved)
49510
-
49511
- execute_command { command: "pwd", sessionId: "abc-123" }
49512
- \u2192 Returns stdout: "/project" (cwd preserved)
49513
-
49514
- 3. Rename session if needed:
49515
- update_exec_session { sessionId: "abc-123", name: "new-name" }
49516
-
49517
- 4. Cleanup when done:
49518
- destroy_exec_session { sessionId: "abc-123" }
49519
- \`\`\`
49520
-
49521
- Sessions persist until manually destroyed.
49522
-
49523
- ### list_exec_sessions
49524
-
49525
- List all active sessions with their name, current working directory, and timestamps.
49526
-
49527
- ### update_exec_session
49528
-
49529
- Rename an existing session for identification.
49530
-
49531
- ### destroy_exec_session
49532
-
49533
- Clean up a session when you're done to free resources.
49310
+ - \`TO_DO\` \u2014 Task not yet started
49311
+ - \`IN_PROGRESS\` \u2014 Task is being worked on
49312
+ - \`IN_REVIEW\` \u2014 Task is complete and awaiting review
49313
+ - \`DONE\` \u2014 Task is finished
49314
+ - \`CANCELED\` \u2014 Task was abandoned
49534
49315
 
49535
49316
  ## Best Practices
49536
49317
 
49537
- 1. **Use \`current-task\` inside worktrees** - It auto-detects which task you're in
49538
- 2. **Link PRs immediately** - Run \`fulcrum current-task pr <url>\` right after creating a PR
49539
- 3. **Link relevant resources** - Attach design docs, specs, or reference materials with \`fulcrum current-task link <url>\`
49540
- 4. **Mark review when done** - \`fulcrum current-task review\` notifies the user
49541
- 5. **Send notifications for blocking issues** - Keep the user informed of progress
49542
- 6. **Name sessions for identification** - Use descriptive names to find sessions later
49543
- 7. **Reuse sessions for related commands** - Preserve state across multiple execute_command calls
49544
- 8. **Clean up sessions when done** - Use destroy_exec_session to free resources
49318
+ 1. **Use \`current-task\` inside worktrees** \u2014 It auto-detects which task you're in
49319
+ 2. **Link PRs immediately** \u2014 Run \`fulcrum current-task pr <url>\` right after creating a PR
49320
+ 3. **Use \`fulcrum api routes --search <keyword>\`** to discover endpoints before making calls
49321
+ 4. **Mark review when done** \u2014 \`fulcrum current-task review\` notifies the user
49322
+ 5. **Send notifications for blocking issues** \u2014 Keep the user informed of progress
49545
49323
  `;
49546
49324
 
49547
49325
  // cli/src/commands/claude.ts
@@ -49551,15 +49329,13 @@ var PLUGIN_NAME = "fulcrum";
49551
49329
  var PLUGIN_ID = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
49552
49330
  var PLUGIN_FILES = [
49553
49331
  { path: ".claude-plugin/marketplace.json", content: marketplace_default },
49332
+ { path: ".claude-plugin/plugin.json", content: plugin_default },
49554
49333
  { path: "hooks/hooks.json", content: hooks_default },
49555
- { path: ".mcp.json", content: _mcp_default },
49556
49334
  { path: "commands/pr.md", content: pr_default },
49557
49335
  { path: "commands/task-info.md", content: task_info_default },
49558
49336
  { path: "commands/notify.md", content: notify_default },
49559
49337
  { path: "skills/fulcrum/SKILL.md", content: SKILL_default }
49560
49338
  ];
49561
- var LEGACY_PLUGIN_DIR = join3(homedir3(), ".claude", "plugins", "fulcrum");
49562
- var LEGACY_CACHE_DIR = join3(homedir3(), ".claude", "plugins", "cache", "fulcrum");
49563
49339
  function runClaude(args) {
49564
49340
  const result = spawnSync("claude", args, { encoding: "utf-8" });
49565
49341
  return {
@@ -49631,7 +49407,6 @@ async function installClaudePlugin(options = {}) {
49631
49407
  throw new Error("Failed to install plugin: " + installResult.output);
49632
49408
  }
49633
49409
  log("\u2713 Installed plugin");
49634
- cleanupLegacyPaths(log);
49635
49410
  log("");
49636
49411
  log("Installation complete! Restart Claude Code to apply changes.");
49637
49412
  } catch (err) {
@@ -49648,22 +49423,12 @@ async function uninstallClaudePlugin() {
49648
49423
  rmSync(MARKETPLACE_DIR, { recursive: true });
49649
49424
  console.log("\u2713 Removed plugin files from " + MARKETPLACE_DIR);
49650
49425
  }
49651
- cleanupLegacyPaths(console.log);
49652
49426
  console.log("");
49653
49427
  console.log("Uninstall complete! Restart Claude Code to apply changes.");
49654
49428
  } catch (err) {
49655
49429
  throw new CliError("UNINSTALL_FAILED", `Failed to uninstall Claude plugin: ${err instanceof Error ? err.message : String(err)}`, ExitCodes.ERROR);
49656
49430
  }
49657
49431
  }
49658
- function cleanupLegacyPaths(log) {
49659
- const legacyPaths = [LEGACY_PLUGIN_DIR, LEGACY_CACHE_DIR];
49660
- for (const path of legacyPaths) {
49661
- if (existsSync3(path)) {
49662
- rmSync(path, { recursive: true });
49663
- log("\u2713 Removed legacy files from " + path);
49664
- }
49665
- }
49666
- }
49667
49432
  var claudeInstallCommand = defineCommand({
49668
49433
  meta: { name: "install", description: "Install Claude Code plugin" },
49669
49434
  args: globalArgs,
@@ -50374,7 +50139,7 @@ function compareVersions(v1, v2) {
50374
50139
  var package_default = {
50375
50140
  name: "@knowsuchagency/fulcrum",
50376
50141
  private: true,
50377
- version: "4.2.0",
50142
+ version: "4.3.0",
50378
50143
  description: "Harness Attention. Orchestrate Agents. Ship.",
50379
50144
  license: "PolyForm-Perimeter-1.0.0",
50380
50145
  type: "module",
@@ -50401,6 +50166,8 @@ var package_default = {
50401
50166
  "@fontsource/ibm-plex-mono": "^5.2.7",
50402
50167
  "@hono/node-server": "^1.19.7",
50403
50168
  "@hono/node-ws": "^1.2.0",
50169
+ "@hono/swagger-ui": "^0.5.3",
50170
+ "@hono/zod-openapi": "^1.2.2",
50404
50171
  "@hugeicons/core-free-icons": "^3.0.0",
50405
50172
  "@hugeicons/react": "^1.1.3",
50406
50173
  "@mdx-js/mdx": "^3.1.1",
@@ -50945,19 +50712,18 @@ async function handleStatusCommand(flags) {
50945
50712
  let healthOk = false;
50946
50713
  let version = null;
50947
50714
  let uptime = null;
50948
- if (pidRunning) {
50949
- try {
50950
- const res = await fetch(`${serverUrl}/health`, { signal: AbortSignal.timeout(2000) });
50951
- healthOk = res.ok;
50952
- if (res.ok) {
50953
- const health = await res.json();
50954
- version = health.version || null;
50955
- uptime = health.uptime || null;
50956
- }
50957
- } catch {}
50958
- }
50715
+ try {
50716
+ const res = await fetch(`${serverUrl}/health`, { signal: AbortSignal.timeout(2000) });
50717
+ healthOk = res.ok;
50718
+ if (res.ok) {
50719
+ const health = await res.json();
50720
+ version = health.version || null;
50721
+ uptime = health.uptime || null;
50722
+ }
50723
+ } catch {}
50724
+ const running = pidRunning || healthOk;
50959
50725
  const data = {
50960
- running: pidRunning,
50726
+ running,
50961
50727
  healthy: healthOk,
50962
50728
  pid: pid || null,
50963
50729
  port,
@@ -50968,10 +50734,11 @@ async function handleStatusCommand(flags) {
50968
50734
  if (isJsonOutput()) {
50969
50735
  output(data);
50970
50736
  } else {
50971
- if (pidRunning) {
50737
+ if (running) {
50972
50738
  const healthStatus = healthOk ? "healthy" : "not responding";
50973
50739
  console.log(`Fulcrum is running (${healthStatus})`);
50974
- console.log(` PID: ${pid}`);
50740
+ if (pidRunning)
50741
+ console.log(` PID: ${pid}`);
50975
50742
  console.log(` URL: ${serverUrl}`);
50976
50743
  if (version)
50977
50744
  console.log(` Version: ${version}`);
@@ -51140,6 +50907,568 @@ var mcpCommand = defineCommand({
51140
50907
  }
51141
50908
  });
51142
50909
 
50910
+ // cli/src/commands/api.ts
50911
+ init_server();
50912
+ import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync5, statSync } from "fs";
50913
+ import { tmpdir } from "os";
50914
+ import { join as join7 } from "path";
50915
+ init_errors();
50916
+
50917
+ // cli/src/openapi-parser.ts
50918
+ function extractQueryParams(params) {
50919
+ if (!params)
50920
+ return;
50921
+ const queryParams = params.filter((p) => p.in === "query").map((p) => p.name);
50922
+ return queryParams.length > 0 ? queryParams.join(", ") : undefined;
50923
+ }
50924
+ function extractBodyFields(op) {
50925
+ const schema = op.requestBody?.content?.["application/json"]?.schema;
50926
+ if (!schema)
50927
+ return;
50928
+ if (schema.description?.startsWith("Fields: ")) {
50929
+ return schema.description.slice("Fields: ".length);
50930
+ }
50931
+ if (schema.properties) {
50932
+ const required3 = new Set(schema.required || []);
50933
+ return Object.keys(schema.properties).map((name) => required3.has(name) ? name : `${name}?`).join(", ");
50934
+ }
50935
+ return;
50936
+ }
50937
+ function parseOpenAPISpec(spec) {
50938
+ const tagDescriptions = new Map;
50939
+ if (spec.tags) {
50940
+ for (const tag of spec.tags) {
50941
+ tagDescriptions.set(tag.name, tag.description || "");
50942
+ }
50943
+ }
50944
+ const categories = new Map;
50945
+ if (spec.paths) {
50946
+ for (const [path, methods] of Object.entries(spec.paths)) {
50947
+ for (const [method, op] of Object.entries(methods)) {
50948
+ if (!op || typeof op !== "object")
50949
+ continue;
50950
+ if (["parameters", "summary", "description", "servers"].includes(method))
50951
+ continue;
50952
+ const tag = op.tags?.[0] || "other";
50953
+ if (!categories.has(tag)) {
50954
+ categories.set(tag, []);
50955
+ }
50956
+ const displayPath = path.replace(/\{(\w+)\}/g, ":$1");
50957
+ categories.get(tag).push({
50958
+ method: method.toUpperCase(),
50959
+ path: displayPath,
50960
+ description: op.summary || op.description || "",
50961
+ queryParams: extractQueryParams(op.parameters),
50962
+ bodyFields: extractBodyFields(op)
50963
+ });
50964
+ }
50965
+ }
50966
+ }
50967
+ const result = [];
50968
+ for (const [tag, routes] of [...categories.entries()].sort((a2, b2) => a2[0].localeCompare(b2[0]))) {
50969
+ result.push({
50970
+ category: tag,
50971
+ description: tagDescriptions.get(tag) || "",
50972
+ routes
50973
+ });
50974
+ }
50975
+ return result;
50976
+ }
50977
+ function extractPathParams(path) {
50978
+ const matches = path.match(/\{(\w+)\}/g);
50979
+ return matches ? matches.map((m2) => m2.slice(1, -1)) : [];
50980
+ }
50981
+ function extractQueryParamNames(params) {
50982
+ if (!params)
50983
+ return [];
50984
+ return params.filter((p) => p.in === "query").map((p) => p.name);
50985
+ }
50986
+ function extractBodyFieldList(op) {
50987
+ const schema = op.requestBody?.content?.["application/json"]?.schema;
50988
+ if (!schema)
50989
+ return [];
50990
+ if (schema.description?.startsWith("Fields: ")) {
50991
+ const fieldsStr = schema.description.slice("Fields: ".length);
50992
+ return fieldsStr.split(",").map((f3) => {
50993
+ const trimmed = f3.trim();
50994
+ const isOptional = trimmed.endsWith("?");
50995
+ return {
50996
+ name: isOptional ? trimmed.slice(0, -1) : trimmed,
50997
+ required: !isOptional
50998
+ };
50999
+ });
51000
+ }
51001
+ if (schema.properties) {
51002
+ const requiredSet = new Set(schema.required || []);
51003
+ return Object.keys(schema.properties).map((name) => ({
51004
+ name,
51005
+ required: requiredSet.has(name)
51006
+ }));
51007
+ }
51008
+ return [];
51009
+ }
51010
+ function parseOpenAPIActions(spec) {
51011
+ const tagDescriptions = new Map;
51012
+ if (spec.tags) {
51013
+ for (const tag of spec.tags) {
51014
+ tagDescriptions.set(tag.name, tag.description || "");
51015
+ }
51016
+ }
51017
+ const resourceMap = new Map;
51018
+ if (spec.paths) {
51019
+ for (const [path, methods] of Object.entries(spec.paths)) {
51020
+ for (const [method, op] of Object.entries(methods)) {
51021
+ if (!op || typeof op !== "object")
51022
+ continue;
51023
+ if (["parameters", "summary", "description", "servers"].includes(method))
51024
+ continue;
51025
+ if (!op.operationId)
51026
+ continue;
51027
+ const tag = op.tags?.[0] || "other";
51028
+ if (!resourceMap.has(tag)) {
51029
+ resourceMap.set(tag, []);
51030
+ }
51031
+ const prefix = `${tag}-`;
51032
+ const action = op.operationId.startsWith(prefix) ? op.operationId.slice(prefix.length) : op.operationId;
51033
+ resourceMap.get(tag).push({
51034
+ operationId: op.operationId,
51035
+ action,
51036
+ method: method.toUpperCase(),
51037
+ path,
51038
+ description: op.summary || op.description || "",
51039
+ pathParams: extractPathParams(path),
51040
+ queryParams: extractQueryParamNames(op.parameters),
51041
+ bodyFields: extractBodyFieldList(op)
51042
+ });
51043
+ }
51044
+ }
51045
+ }
51046
+ const result = [];
51047
+ for (const [name, actions] of [...resourceMap.entries()].sort((a2, b2) => a2[0].localeCompare(b2[0]))) {
51048
+ result.push({
51049
+ name,
51050
+ description: tagDescriptions.get(name) || "",
51051
+ actions
51052
+ });
51053
+ }
51054
+ return result;
51055
+ }
51056
+
51057
+ // cli/src/commands/api.ts
51058
+ var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
51059
+ var SPEC_CACHE_TTL_MS = 5 * 60 * 1000;
51060
+ function getSpecCachePath(baseUrl) {
51061
+ const port = new URL(baseUrl).port || "7777";
51062
+ return join7(tmpdir(), `fulcrum-openapi-${port}.json`);
51063
+ }
51064
+ async function fetchSpec(baseUrl) {
51065
+ const cachePath = getSpecCachePath(baseUrl);
51066
+ if (existsSync7(cachePath)) {
51067
+ const stat = statSync(cachePath);
51068
+ const age = Date.now() - stat.mtimeMs;
51069
+ if (age < SPEC_CACHE_TTL_MS) {
51070
+ try {
51071
+ return JSON.parse(readFileSync6(cachePath, "utf-8"));
51072
+ } catch {}
51073
+ }
51074
+ }
51075
+ let res;
51076
+ try {
51077
+ res = await fetch(`${baseUrl}/openapi.json`);
51078
+ } catch {
51079
+ throw new CliError("SERVER_UNREACHABLE", `Server unreachable at ${baseUrl} \u2014 requires a running server`, ExitCodes.SERVER_UNREACHABLE);
51080
+ }
51081
+ if (!res.ok) {
51082
+ throw new CliError("SPEC_ERROR", `Failed to fetch OpenAPI spec: ${res.status} ${res.statusText}`, ExitCodes.ERROR);
51083
+ }
51084
+ const contentType = res.headers.get("content-type") || "";
51085
+ if (!contentType.includes("application/json")) {
51086
+ throw new CliError("SPEC_ERROR", `Server returned ${contentType || "unknown content type"} instead of JSON \u2014 is the server up to date?`, ExitCodes.ERROR);
51087
+ }
51088
+ const spec = await res.json();
51089
+ try {
51090
+ writeFileSync5(cachePath, JSON.stringify(spec));
51091
+ } catch {}
51092
+ return spec;
51093
+ }
51094
+ async function fetchRoutes(baseUrl) {
51095
+ const spec = await fetchSpec(baseUrl);
51096
+ return parseOpenAPISpec(spec);
51097
+ }
51098
+ async function fetchResources(baseUrl) {
51099
+ const spec = await fetchSpec(baseUrl);
51100
+ return parseOpenAPIActions(spec);
51101
+ }
51102
+ async function handleRoutes(flags) {
51103
+ const category = flags.category?.toLowerCase();
51104
+ const search = flags.search?.toLowerCase();
51105
+ const baseUrl = discoverServerUrl(flags.url, flags.port);
51106
+ const apiRoutes = await fetchRoutes(baseUrl);
51107
+ let filtered = apiRoutes;
51108
+ if (category) {
51109
+ filtered = filtered.filter((c3) => c3.category === category);
51110
+ if (filtered.length === 0) {
51111
+ const categories = apiRoutes.map((c3) => c3.category).join(", ");
51112
+ throw new CliError("UNKNOWN_CATEGORY", `Unknown category: ${category}. Available: ${categories}`, ExitCodes.INVALID_ARGS);
51113
+ }
51114
+ }
51115
+ if (search) {
51116
+ filtered = filtered.map((cat) => ({
51117
+ ...cat,
51118
+ routes: cat.routes.filter((r3) => r3.path.toLowerCase().includes(search) || r3.description.toLowerCase().includes(search) || r3.method.toLowerCase().includes(search))
51119
+ })).filter((cat) => cat.routes.length > 0);
51120
+ }
51121
+ if (isJsonOutput()) {
51122
+ output(filtered);
51123
+ return;
51124
+ }
51125
+ if (filtered.length === 0) {
51126
+ console.log("No routes match your search.");
51127
+ return;
51128
+ }
51129
+ for (const cat of filtered) {
51130
+ console.log(`
51131
+ ${cat.category} \u2014 ${cat.description}`);
51132
+ console.log("\u2500".repeat(60));
51133
+ for (const route of cat.routes) {
51134
+ const method = route.method.padEnd(6);
51135
+ console.log(` ${method} ${route.path}`);
51136
+ console.log(` ${route.description}`);
51137
+ if (route.queryParams) {
51138
+ console.log(` Query: ${route.queryParams}`);
51139
+ }
51140
+ if (route.bodyFields) {
51141
+ console.log(` Body: ${route.bodyFields}`);
51142
+ }
51143
+ }
51144
+ }
51145
+ console.log("");
51146
+ }
51147
+ async function handleHttpRequest(method, pathAndQuery, data, flags) {
51148
+ const upperMethod = method.toUpperCase();
51149
+ if (!HTTP_METHODS.includes(upperMethod)) {
51150
+ throw new CliError("INVALID_METHOD", `Invalid HTTP method: ${method}. Valid: ${HTTP_METHODS.join(", ")}`, ExitCodes.INVALID_ARGS);
51151
+ }
51152
+ if (!pathAndQuery) {
51153
+ throw new CliError("MISSING_PATH", "API path is required. Example: fulcrum api GET /api/tasks", ExitCodes.INVALID_ARGS);
51154
+ }
51155
+ const normalizedPath = pathAndQuery.startsWith("/") ? pathAndQuery : `/${pathAndQuery}`;
51156
+ const baseUrl = discoverServerUrl(flags.url, flags.port);
51157
+ const url2 = `${baseUrl}${normalizedPath}`;
51158
+ const fetchOptions = {
51159
+ method: upperMethod,
51160
+ headers: { "Content-Type": "application/json" }
51161
+ };
51162
+ if (data && ["POST", "PUT", "PATCH", "DELETE"].includes(upperMethod)) {
51163
+ fetchOptions.body = data;
51164
+ }
51165
+ let res;
51166
+ try {
51167
+ res = await fetch(url2, fetchOptions);
51168
+ } catch {
51169
+ throw new CliError("SERVER_UNREACHABLE", `Server unreachable: ${baseUrl}`, ExitCodes.SERVER_UNREACHABLE);
51170
+ }
51171
+ let body;
51172
+ const contentType = res.headers.get("content-type") || "";
51173
+ if (contentType.includes("application/json")) {
51174
+ body = await res.json();
51175
+ } else {
51176
+ body = await res.text();
51177
+ }
51178
+ if (!res.ok) {
51179
+ if (isJsonOutput()) {
51180
+ console.log(JSON.stringify({ success: false, status: res.status, error: body }));
51181
+ } else {
51182
+ console.error(`Error ${res.status}: ${typeof body === "string" ? body : JSON.stringify(body, null, 2)}`);
51183
+ }
51184
+ process.exit(ExitCodes.ERROR);
51185
+ }
51186
+ if (isJsonOutput()) {
51187
+ output(body);
51188
+ } else {
51189
+ console.log(typeof body === "string" ? body : JSON.stringify(body, null, 2));
51190
+ }
51191
+ }
51192
+ function resolveAction(resources, resourceName, actionName) {
51193
+ const resource = resources.find((r3) => r3.name === resourceName);
51194
+ if (!resource) {
51195
+ const available = resources.map((r3) => r3.name).join(", ");
51196
+ throw new CliError("UNKNOWN_RESOURCE", `Unknown resource: ${resourceName}. Available: ${available}`, ExitCodes.INVALID_ARGS);
51197
+ }
51198
+ if (!actionName) {
51199
+ return { resource };
51200
+ }
51201
+ const action = resource.actions.find((a2) => a2.action === actionName);
51202
+ if (!action) {
51203
+ const available = resource.actions.map((a2) => a2.action).join(", ");
51204
+ throw new CliError("UNKNOWN_ACTION", `Unknown action: ${actionName} for ${resourceName}. Available: ${available}`, ExitCodes.INVALID_ARGS);
51205
+ }
51206
+ return { resource, action };
51207
+ }
51208
+ function coerceValue(value, isArrayField) {
51209
+ if (isArrayField || value.includes(",")) {
51210
+ return value.split(",").map((v2) => v2.trim());
51211
+ }
51212
+ if (value === "true")
51213
+ return true;
51214
+ if (value === "false")
51215
+ return false;
51216
+ if (value !== "" && !isNaN(Number(value)) && isFinite(Number(value))) {
51217
+ return Number(value);
51218
+ }
51219
+ return value;
51220
+ }
51221
+ function buildRequest(action, positionalArgs, flags) {
51222
+ let path = action.path;
51223
+ for (let i2 = 0;i2 < action.pathParams.length; i2++) {
51224
+ const param = action.pathParams[i2];
51225
+ const value = positionalArgs[i2];
51226
+ if (!value) {
51227
+ throw new CliError("MISSING_PATH_PARAM", `Missing required path parameter: <${param}>`, ExitCodes.INVALID_ARGS);
51228
+ }
51229
+ path = path.replace(`{${param}}`, encodeURIComponent(value));
51230
+ }
51231
+ if (flags.data || flags.d) {
51232
+ const body = flags.data || flags.d;
51233
+ return { method: action.method, url: path, body };
51234
+ }
51235
+ const arrayBodyFields = new Set;
51236
+ for (const f3 of action.bodyFields) {
51237
+ if (["ids", "tags", "repositories", "services", "uids"].includes(f3.name)) {
51238
+ arrayBodyFields.add(f3.name);
51239
+ }
51240
+ }
51241
+ if (action.method === "GET" || action.method === "HEAD") {
51242
+ const queryParts = [];
51243
+ for (const [key, value] of Object.entries(flags)) {
51244
+ queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
51245
+ }
51246
+ const queryString = queryParts.length > 0 ? `?${queryParts.join("&")}` : "";
51247
+ return { method: action.method, url: `${path}${queryString}` };
51248
+ }
51249
+ if (Object.keys(flags).length > 0) {
51250
+ const body = {};
51251
+ for (const [key, value] of Object.entries(flags)) {
51252
+ body[key] = coerceValue(value, arrayBodyFields.has(key));
51253
+ }
51254
+ return { method: action.method, url: path, body: JSON.stringify(body) };
51255
+ }
51256
+ return { method: action.method, url: path };
51257
+ }
51258
+ function parseRawArgs2(rawArgs) {
51259
+ const positionalArgs = [];
51260
+ const actionFlags = {};
51261
+ const globalFlags = {};
51262
+ const globalFlagNames = new Set(["port", "url", "json", "debug", "data", "d"]);
51263
+ let i2 = 0;
51264
+ while (i2 < rawArgs.length) {
51265
+ const arg = rawArgs[i2];
51266
+ if (arg.startsWith("--")) {
51267
+ const eqIdx = arg.indexOf("=");
51268
+ let key;
51269
+ let value;
51270
+ if (eqIdx !== -1) {
51271
+ key = arg.slice(2, eqIdx);
51272
+ value = arg.slice(eqIdx + 1);
51273
+ } else {
51274
+ key = arg.slice(2);
51275
+ const next = rawArgs[i2 + 1];
51276
+ if (next && !next.startsWith("-")) {
51277
+ value = next;
51278
+ i2++;
51279
+ } else {
51280
+ value = "true";
51281
+ }
51282
+ }
51283
+ if (globalFlagNames.has(key)) {
51284
+ globalFlags[key] = value;
51285
+ } else {
51286
+ actionFlags[key] = value;
51287
+ }
51288
+ } else if (arg.startsWith("-") && arg.length === 2) {
51289
+ const key = arg.slice(1);
51290
+ const next = rawArgs[i2 + 1];
51291
+ if (next && !next.startsWith("-")) {
51292
+ if (globalFlagNames.has(key)) {
51293
+ globalFlags[key] = next;
51294
+ } else {
51295
+ actionFlags[key] = next;
51296
+ }
51297
+ i2++;
51298
+ } else {
51299
+ if (globalFlagNames.has(key)) {
51300
+ globalFlags[key] = "true";
51301
+ } else {
51302
+ actionFlags[key] = "true";
51303
+ }
51304
+ }
51305
+ } else {
51306
+ positionalArgs.push(arg);
51307
+ }
51308
+ i2++;
51309
+ }
51310
+ return { positionalArgs, actionFlags, globalFlags };
51311
+ }
51312
+ async function handleResourceAction(rawArgs, globalFlagsFromCitty) {
51313
+ const { positionalArgs, actionFlags, globalFlags } = parseRawArgs2(rawArgs);
51314
+ const mergedGlobalFlags = { ...globalFlags, ...globalFlagsFromCitty };
51315
+ const baseUrl = discoverServerUrl(mergedGlobalFlags.url, mergedGlobalFlags.port);
51316
+ const resources = await fetchResources(baseUrl);
51317
+ const resourceName = positionalArgs[0];
51318
+ const actionName = positionalArgs[1];
51319
+ const extraPositionals = positionalArgs.slice(2);
51320
+ if (!resourceName) {
51321
+ if (isJsonOutput()) {
51322
+ output(resources.map((r3) => ({ name: r3.name, description: r3.description, actions: r3.actions.length })));
51323
+ return;
51324
+ }
51325
+ console.log(`
51326
+ Fulcrum API Resources`);
51327
+ console.log("\u2500".repeat(40));
51328
+ for (const r3 of resources) {
51329
+ console.log(` ${r3.name.padEnd(16)} ${r3.description} (${r3.actions.length} actions)`);
51330
+ }
51331
+ console.log(`
51332
+ Use: fulcrum api <resource> to list actions`);
51333
+ console.log(` fulcrum api <resource> <action> [<id>] [--flag value]`);
51334
+ console.log("");
51335
+ return;
51336
+ }
51337
+ const resolved = resolveAction(resources, resourceName, actionName);
51338
+ if (!resolved.action) {
51339
+ if (isJsonOutput()) {
51340
+ output(resolved.resource);
51341
+ return;
51342
+ }
51343
+ console.log(`
51344
+ ${resolved.resource.name} \u2014 ${resolved.resource.description}`);
51345
+ console.log("\u2500".repeat(60));
51346
+ for (const a2 of resolved.resource.actions) {
51347
+ const params = a2.pathParams.map((p) => `<${p}>`).join(" ");
51348
+ const flags = [
51349
+ ...a2.queryParams.map((p) => `--${p}`),
51350
+ ...a2.bodyFields.map((f3) => f3.required ? `--${f3.name}` : `[--${f3.name}]`)
51351
+ ].join(" ");
51352
+ console.log(` ${a2.action} ${params} ${flags}`.trimEnd());
51353
+ console.log(` ${a2.description}`);
51354
+ }
51355
+ console.log("");
51356
+ return;
51357
+ }
51358
+ const req = buildRequest(resolved.action, extraPositionals, actionFlags);
51359
+ await handleHttpRequest(req.method, req.url, req.body, mergedGlobalFlags);
51360
+ }
51361
+ function formatToolsReference(resources) {
51362
+ const lines = [];
51363
+ lines.push("Fulcrum API \u2014 fulcrum api <resource> <action> [<id>] [--key value]");
51364
+ lines.push("");
51365
+ for (const r3 of resources) {
51366
+ lines.push(`${r3.name} \u2014 ${r3.description}`);
51367
+ for (const a2 of r3.actions) {
51368
+ const parts = [` ${a2.action}`];
51369
+ for (const p of a2.pathParams) {
51370
+ parts.push(`<${p}>`);
51371
+ }
51372
+ const requiredFlags = a2.bodyFields.filter((f3) => f3.required).map((f3) => `--${f3.name}`);
51373
+ const optionalFlags = [
51374
+ ...a2.queryParams.map((p) => `--${p}`),
51375
+ ...a2.bodyFields.filter((f3) => !f3.required).map((f3) => `--${f3.name}`)
51376
+ ];
51377
+ if (requiredFlags.length > 0) {
51378
+ parts.push(requiredFlags.join(" "));
51379
+ }
51380
+ if (optionalFlags.length > 0) {
51381
+ parts.push(`[${optionalFlags.join(" ")}]`);
51382
+ }
51383
+ if (a2.description.toLowerCase().includes("multipart")) {
51384
+ parts.push("(multipart)");
51385
+ }
51386
+ lines.push(parts.join(" "));
51387
+ }
51388
+ }
51389
+ return lines.join(`
51390
+ `);
51391
+ }
51392
+ async function handleTools(flags) {
51393
+ const baseUrl = discoverServerUrl(flags.url, flags.port);
51394
+ const resources = await fetchResources(baseUrl);
51395
+ if (isJsonOutput()) {
51396
+ output(resources);
51397
+ return;
51398
+ }
51399
+ console.log(formatToolsReference(resources));
51400
+ }
51401
+ function isHttpMethod(arg) {
51402
+ return HTTP_METHODS.includes(arg.toUpperCase());
51403
+ }
51404
+ function getRawArgsAfterApi() {
51405
+ const argv2 = process.argv;
51406
+ const apiIdx = argv2.indexOf("api");
51407
+ if (apiIdx === -1)
51408
+ return [];
51409
+ return argv2.slice(apiIdx + 1);
51410
+ }
51411
+ var routesCommand = defineCommand({
51412
+ meta: { name: "routes", description: "List available API routes" },
51413
+ args: {
51414
+ ...globalArgs,
51415
+ category: { type: "string", description: "Filter by category" },
51416
+ search: { type: "string", description: "Search routes by keyword" }
51417
+ },
51418
+ async run({ args }) {
51419
+ setupJsonOutput(args);
51420
+ await handleRoutes(toFlags(args));
51421
+ }
51422
+ });
51423
+ var toolsCommand = defineCommand({
51424
+ meta: { name: "tools", description: "Print compact tool reference for context window injection" },
51425
+ args: {
51426
+ ...globalArgs
51427
+ },
51428
+ async run({ args }) {
51429
+ setupJsonOutput(args);
51430
+ await handleTools(toFlags(args));
51431
+ }
51432
+ });
51433
+ var apiCommand = defineCommand({
51434
+ meta: { name: "api", description: "REST API access \u2014 route discovery, HTTP proxy, and resource/action CLI" },
51435
+ args: {
51436
+ ...globalArgs,
51437
+ method: { type: "positional", description: 'HTTP method, resource name, or "routes"/"tools"', required: false },
51438
+ path: { type: "positional", description: "API path or action name", required: false },
51439
+ data: { type: "string", alias: "d", description: "Request body (JSON)" },
51440
+ category: { type: "string", description: "Filter routes by category" },
51441
+ search: { type: "string", description: "Search routes by keyword" }
51442
+ },
51443
+ subCommands: {
51444
+ routes: routesCommand,
51445
+ tools: toolsCommand
51446
+ },
51447
+ async run({ args }) {
51448
+ setupJsonOutput(args);
51449
+ const flags = toFlags(args);
51450
+ const firstArg = args.method;
51451
+ if (!firstArg) {
51452
+ await handleResourceAction([], flags);
51453
+ return;
51454
+ }
51455
+ if (firstArg.toLowerCase() === "routes") {
51456
+ await handleRoutes(flags);
51457
+ return;
51458
+ }
51459
+ if (firstArg.toLowerCase() === "tools") {
51460
+ await handleTools(flags);
51461
+ return;
51462
+ }
51463
+ if (isHttpMethod(firstArg)) {
51464
+ await handleHttpRequest(firstArg, args.path, args.data, flags);
51465
+ return;
51466
+ }
51467
+ const rawArgs = getRawArgsAfterApi();
51468
+ await handleResourceAction(rawArgs, flags);
51469
+ }
51470
+ });
51471
+
51143
51472
  // cli/src/commands/migrate-from-vibora.ts
51144
51473
  init_server();
51145
51474
  async function handleMigrateFromViboraCommand(flags) {
@@ -51242,6 +51571,7 @@ var main = defineCommand({
51242
51571
  doctor: doctorCommand,
51243
51572
  dev: devCommand,
51244
51573
  mcp: mcpCommand,
51574
+ api: apiCommand,
51245
51575
  update: updateCommand,
51246
51576
  "migrate-from-vibora": migrateFromViboraCommand
51247
51577
  }