@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.
- package/README.md +16 -12
- package/bin/fulcrum.js +694 -364
- package/package.json +1 -1
- 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.
|
|
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.
|
|
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**
|
|
49170
|
-
- **Updating task status**
|
|
49171
|
-
- **Linking PRs**
|
|
49172
|
-
- **Linking URLs**
|
|
49173
|
-
- **Sending notifications**
|
|
49174
|
-
- **Server management**
|
|
49175
|
-
|
|
49176
|
-
|
|
49177
|
-
|
|
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,
|
|
49172
|
+
When running inside a Fulcrum task worktree, manage the current task:
|
|
49189
49173
|
|
|
49190
49174
|
\`\`\`bash
|
|
49191
|
-
# Get
|
|
49192
|
-
fulcrum current-task
|
|
49193
|
-
|
|
49194
|
-
#
|
|
49195
|
-
fulcrum current-task
|
|
49196
|
-
fulcrum current-task
|
|
49197
|
-
fulcrum current-task
|
|
49198
|
-
fulcrum current-task
|
|
49199
|
-
|
|
49200
|
-
|
|
49201
|
-
fulcrum current-task
|
|
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
|
|
49220
|
-
|
|
49221
|
-
#
|
|
49222
|
-
fulcrum notifications
|
|
49223
|
-
|
|
49224
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49235
|
-
fulcrum
|
|
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
|
-
|
|
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
|
|
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
|
-
###
|
|
49227
|
+
### Getting the Full Tool Reference
|
|
49257
49228
|
|
|
49258
49229
|
\`\`\`bash
|
|
49259
|
-
#
|
|
49260
|
-
|
|
49261
|
-
|
|
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
|
-
|
|
49264
|
-
|
|
49265
|
-
|
|
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
|
-
|
|
49305
|
-
|
|
49306
|
-
- \`--
|
|
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\`
|
|
49313
|
-
- \`IN_PROGRESS\`
|
|
49314
|
-
- \`IN_REVIEW\`
|
|
49315
|
-
- \`DONE\`
|
|
49316
|
-
- \`CANCELED\`
|
|
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**
|
|
49538
|
-
2. **Link PRs immediately**
|
|
49539
|
-
3. **
|
|
49540
|
-
4. **Mark review when done**
|
|
49541
|
-
5. **Send notifications for blocking issues**
|
|
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.
|
|
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
|
-
|
|
50949
|
-
|
|
50950
|
-
|
|
50951
|
-
|
|
50952
|
-
|
|
50953
|
-
|
|
50954
|
-
|
|
50955
|
-
|
|
50956
|
-
|
|
50957
|
-
|
|
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
|
|
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 (
|
|
50737
|
+
if (running) {
|
|
50972
50738
|
const healthStatus = healthOk ? "healthy" : "not responding";
|
|
50973
50739
|
console.log(`Fulcrum is running (${healthStatus})`);
|
|
50974
|
-
|
|
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
|
}
|