@knowsuchagency/fulcrum 1.3.0 → 1.5.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/bin/fulcrum.js +1823 -1356
- package/dist/assets/{index-J3L6hPet.js → index-fIJu9jIH.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +1 -1
package/bin/fulcrum.js
CHANGED
|
@@ -43866,12 +43866,16 @@ function registerTools(server, client) {
|
|
|
43866
43866
|
return handleToolError(err);
|
|
43867
43867
|
}
|
|
43868
43868
|
});
|
|
43869
|
-
server.tool("get_task", "Get details of a specific task by ID", {
|
|
43869
|
+
server.tool("get_task", "Get details of a specific task by ID, including dependencies and attachments", {
|
|
43870
43870
|
id: exports_external.string().describe("Task ID (UUID)")
|
|
43871
43871
|
}, async ({ id }) => {
|
|
43872
43872
|
try {
|
|
43873
|
-
const task = await
|
|
43874
|
-
|
|
43873
|
+
const [task, dependencies, attachments] = await Promise.all([
|
|
43874
|
+
client.getTask(id),
|
|
43875
|
+
client.getTaskDependencies(id),
|
|
43876
|
+
client.listTaskAttachments(id)
|
|
43877
|
+
]);
|
|
43878
|
+
return formatSuccess({ ...task, dependencies, attachments });
|
|
43875
43879
|
} catch (err) {
|
|
43876
43880
|
return handleToolError(err);
|
|
43877
43881
|
}
|
|
@@ -46168,15 +46172,62 @@ var STATUS_MAP = {
|
|
|
46168
46172
|
cancel: "CANCELED",
|
|
46169
46173
|
"in-progress": "IN_PROGRESS"
|
|
46170
46174
|
};
|
|
46171
|
-
function formatTask(task) {
|
|
46175
|
+
function formatTask(task, dependencies, attachments) {
|
|
46172
46176
|
console.log(`${task.title}`);
|
|
46173
|
-
console.log(` ID:
|
|
46174
|
-
console.log(` Status:
|
|
46175
|
-
|
|
46177
|
+
console.log(` ID: ${task.id}`);
|
|
46178
|
+
console.log(` Status: ${task.status}`);
|
|
46179
|
+
if (task.description) {
|
|
46180
|
+
console.log(` Description: ${task.description}`);
|
|
46181
|
+
}
|
|
46182
|
+
if (task.repoName)
|
|
46183
|
+
console.log(` Repo: ${task.repoName}`);
|
|
46176
46184
|
if (task.branch)
|
|
46177
|
-
console.log(` Branch:
|
|
46185
|
+
console.log(` Branch: ${task.branch}`);
|
|
46186
|
+
if (task.worktreePath)
|
|
46187
|
+
console.log(` Worktree: ${task.worktreePath}`);
|
|
46178
46188
|
if (task.prUrl)
|
|
46179
|
-
console.log(` PR:
|
|
46189
|
+
console.log(` PR: ${task.prUrl}`);
|
|
46190
|
+
if (task.links && task.links.length > 0) {
|
|
46191
|
+
console.log(` Links: ${task.links.map((l2) => l2.label || l2.url).join(", ")}`);
|
|
46192
|
+
}
|
|
46193
|
+
if (task.labels && task.labels.length > 0) {
|
|
46194
|
+
console.log(` Labels: ${task.labels.join(", ")}`);
|
|
46195
|
+
}
|
|
46196
|
+
if (task.dueDate)
|
|
46197
|
+
console.log(` Due: ${task.dueDate}`);
|
|
46198
|
+
if (task.projectId)
|
|
46199
|
+
console.log(` Project: ${task.projectId}`);
|
|
46200
|
+
console.log(` Agent: ${task.agent}`);
|
|
46201
|
+
if (task.aiMode)
|
|
46202
|
+
console.log(` AI Mode: ${task.aiMode}`);
|
|
46203
|
+
if (task.agentOptions && Object.keys(task.agentOptions).length > 0) {
|
|
46204
|
+
console.log(` Options: ${JSON.stringify(task.agentOptions)}`);
|
|
46205
|
+
}
|
|
46206
|
+
if (dependencies) {
|
|
46207
|
+
if (dependencies.isBlocked) {
|
|
46208
|
+
console.log(` Blocked: Yes`);
|
|
46209
|
+
}
|
|
46210
|
+
if (dependencies.dependsOn.length > 0) {
|
|
46211
|
+
console.log(` Depends on: ${dependencies.dependsOn.length} task(s)`);
|
|
46212
|
+
for (const dep of dependencies.dependsOn) {
|
|
46213
|
+
if (dep.task) {
|
|
46214
|
+
console.log(` - ${dep.task.title} [${dep.task.status}]`);
|
|
46215
|
+
}
|
|
46216
|
+
}
|
|
46217
|
+
}
|
|
46218
|
+
if (dependencies.dependents.length > 0) {
|
|
46219
|
+
console.log(` Blocking: ${dependencies.dependents.length} task(s)`);
|
|
46220
|
+
}
|
|
46221
|
+
}
|
|
46222
|
+
if (attachments && attachments.length > 0) {
|
|
46223
|
+
console.log(` Attachments: ${attachments.length} file(s)`);
|
|
46224
|
+
}
|
|
46225
|
+
if (task.notes) {
|
|
46226
|
+
console.log(` Notes: ${task.notes}`);
|
|
46227
|
+
}
|
|
46228
|
+
console.log(` Created: ${task.createdAt}`);
|
|
46229
|
+
if (task.startedAt)
|
|
46230
|
+
console.log(` Started: ${task.startedAt}`);
|
|
46180
46231
|
}
|
|
46181
46232
|
async function findCurrentTask(client, pathOverride) {
|
|
46182
46233
|
if (process.env.FULCRUM_TASK_ID) {
|
|
@@ -46204,9 +46255,17 @@ async function handleCurrentTaskCommand(action, rest, flags) {
|
|
|
46204
46255
|
if (!action) {
|
|
46205
46256
|
const task2 = await findCurrentTask(client, pathOverride);
|
|
46206
46257
|
if (isJsonOutput()) {
|
|
46207
|
-
|
|
46258
|
+
const [dependencies, attachments] = await Promise.all([
|
|
46259
|
+
client.getTaskDependencies(task2.id),
|
|
46260
|
+
client.listTaskAttachments(task2.id)
|
|
46261
|
+
]);
|
|
46262
|
+
output({ ...task2, dependencies, attachments });
|
|
46208
46263
|
} else {
|
|
46209
|
-
|
|
46264
|
+
const [dependencies, attachments] = await Promise.all([
|
|
46265
|
+
client.getTaskDependencies(task2.id),
|
|
46266
|
+
client.listTaskAttachments(task2.id)
|
|
46267
|
+
]);
|
|
46268
|
+
formatTask(task2, dependencies, attachments);
|
|
46210
46269
|
}
|
|
46211
46270
|
return;
|
|
46212
46271
|
}
|
|
@@ -46297,22 +46356,62 @@ init_client();
|
|
|
46297
46356
|
import { basename as basename3 } from "path";
|
|
46298
46357
|
init_errors();
|
|
46299
46358
|
var VALID_STATUSES = ["TO_DO", "IN_PROGRESS", "IN_REVIEW", "CANCELED"];
|
|
46300
|
-
function formatTask2(task) {
|
|
46359
|
+
function formatTask2(task, dependencies, attachments) {
|
|
46301
46360
|
console.log(`${task.title}`);
|
|
46302
|
-
console.log(` ID:
|
|
46303
|
-
console.log(` Status:
|
|
46361
|
+
console.log(` ID: ${task.id}`);
|
|
46362
|
+
console.log(` Status: ${task.status}`);
|
|
46363
|
+
if (task.description) {
|
|
46364
|
+
console.log(` Description: ${task.description}`);
|
|
46365
|
+
}
|
|
46304
46366
|
if (task.repoName)
|
|
46305
|
-
console.log(` Repo:
|
|
46367
|
+
console.log(` Repo: ${task.repoName}`);
|
|
46306
46368
|
if (task.branch)
|
|
46307
|
-
console.log(` Branch:
|
|
46369
|
+
console.log(` Branch: ${task.branch}`);
|
|
46370
|
+
if (task.worktreePath)
|
|
46371
|
+
console.log(` Worktree: ${task.worktreePath}`);
|
|
46308
46372
|
if (task.prUrl)
|
|
46309
|
-
console.log(` PR:
|
|
46310
|
-
if (task.
|
|
46311
|
-
console.log(`
|
|
46312
|
-
|
|
46313
|
-
|
|
46373
|
+
console.log(` PR: ${task.prUrl}`);
|
|
46374
|
+
if (task.links && task.links.length > 0) {
|
|
46375
|
+
console.log(` Links: ${task.links.map((l2) => l2.label || l2.url).join(", ")}`);
|
|
46376
|
+
}
|
|
46377
|
+
if (task.labels && task.labels.length > 0) {
|
|
46378
|
+
console.log(` Labels: ${task.labels.join(", ")}`);
|
|
46379
|
+
}
|
|
46314
46380
|
if (task.dueDate)
|
|
46315
|
-
console.log(` Due:
|
|
46381
|
+
console.log(` Due: ${task.dueDate}`);
|
|
46382
|
+
if (task.projectId)
|
|
46383
|
+
console.log(` Project: ${task.projectId}`);
|
|
46384
|
+
console.log(` Agent: ${task.agent}`);
|
|
46385
|
+
if (task.aiMode)
|
|
46386
|
+
console.log(` AI Mode: ${task.aiMode}`);
|
|
46387
|
+
if (task.agentOptions && Object.keys(task.agentOptions).length > 0) {
|
|
46388
|
+
console.log(` Options: ${JSON.stringify(task.agentOptions)}`);
|
|
46389
|
+
}
|
|
46390
|
+
if (dependencies) {
|
|
46391
|
+
if (dependencies.isBlocked) {
|
|
46392
|
+
console.log(` Blocked: Yes`);
|
|
46393
|
+
}
|
|
46394
|
+
if (dependencies.dependsOn.length > 0) {
|
|
46395
|
+
console.log(` Depends on: ${dependencies.dependsOn.length} task(s)`);
|
|
46396
|
+
for (const dep of dependencies.dependsOn) {
|
|
46397
|
+
if (dep.task) {
|
|
46398
|
+
console.log(` - ${dep.task.title} [${dep.task.status}]`);
|
|
46399
|
+
}
|
|
46400
|
+
}
|
|
46401
|
+
}
|
|
46402
|
+
if (dependencies.dependents.length > 0) {
|
|
46403
|
+
console.log(` Blocking: ${dependencies.dependents.length} task(s)`);
|
|
46404
|
+
}
|
|
46405
|
+
}
|
|
46406
|
+
if (attachments && attachments.length > 0) {
|
|
46407
|
+
console.log(` Attachments: ${attachments.length} file(s)`);
|
|
46408
|
+
}
|
|
46409
|
+
if (task.notes) {
|
|
46410
|
+
console.log(` Notes: ${task.notes}`);
|
|
46411
|
+
}
|
|
46412
|
+
console.log(` Created: ${task.createdAt}`);
|
|
46413
|
+
if (task.startedAt)
|
|
46414
|
+
console.log(` Started: ${task.startedAt}`);
|
|
46316
46415
|
}
|
|
46317
46416
|
function formatTaskList(tasks) {
|
|
46318
46417
|
if (tasks.length === 0) {
|
|
@@ -46394,10 +46493,14 @@ async function handleTasksCommand(action, positional, flags) {
|
|
|
46394
46493
|
throw new CliError("MISSING_ID", "Task ID required", ExitCodes.INVALID_ARGS);
|
|
46395
46494
|
}
|
|
46396
46495
|
const task = await client.getTask(id);
|
|
46496
|
+
const [dependencies, attachments] = await Promise.all([
|
|
46497
|
+
client.getTaskDependencies(id),
|
|
46498
|
+
client.listTaskAttachments(id)
|
|
46499
|
+
]);
|
|
46397
46500
|
if (isJsonOutput()) {
|
|
46398
|
-
output(task);
|
|
46501
|
+
output({ ...task, dependencies, attachments });
|
|
46399
46502
|
} else {
|
|
46400
|
-
formatTask2(task);
|
|
46503
|
+
formatTask2(task, dependencies, attachments);
|
|
46401
46504
|
}
|
|
46402
46505
|
break;
|
|
46403
46506
|
}
|
|
@@ -46649,8 +46752,98 @@ Labels:`);
|
|
|
46649
46752
|
}
|
|
46650
46753
|
break;
|
|
46651
46754
|
}
|
|
46755
|
+
case "attachments": {
|
|
46756
|
+
const [subAction, taskIdOrFile, fileOrAttachmentId] = positional;
|
|
46757
|
+
if (!subAction || subAction === "help") {
|
|
46758
|
+
console.log("Usage:");
|
|
46759
|
+
console.log(" fulcrum tasks attachments list <task-id>");
|
|
46760
|
+
console.log(" fulcrum tasks attachments upload <task-id> <file-path>");
|
|
46761
|
+
console.log(" fulcrum tasks attachments delete <task-id> <attachment-id>");
|
|
46762
|
+
console.log(" fulcrum tasks attachments path <task-id> <attachment-id>");
|
|
46763
|
+
break;
|
|
46764
|
+
}
|
|
46765
|
+
if (subAction === "list") {
|
|
46766
|
+
const taskId = taskIdOrFile;
|
|
46767
|
+
if (!taskId) {
|
|
46768
|
+
throw new CliError("MISSING_ID", "Task ID required", ExitCodes.INVALID_ARGS);
|
|
46769
|
+
}
|
|
46770
|
+
const attachments = await client.listTaskAttachments(taskId);
|
|
46771
|
+
if (isJsonOutput()) {
|
|
46772
|
+
output(attachments);
|
|
46773
|
+
} else {
|
|
46774
|
+
if (attachments.length === 0) {
|
|
46775
|
+
console.log("No attachments");
|
|
46776
|
+
} else {
|
|
46777
|
+
console.log(`
|
|
46778
|
+
Attachments (${attachments.length}):`);
|
|
46779
|
+
for (const att of attachments) {
|
|
46780
|
+
console.log(` ${att.filename}`);
|
|
46781
|
+
console.log(` ID: ${att.id}`);
|
|
46782
|
+
console.log(` Type: ${att.mimeType}`);
|
|
46783
|
+
console.log(` Size: ${att.size} bytes`);
|
|
46784
|
+
}
|
|
46785
|
+
}
|
|
46786
|
+
}
|
|
46787
|
+
break;
|
|
46788
|
+
}
|
|
46789
|
+
if (subAction === "upload") {
|
|
46790
|
+
const taskId = taskIdOrFile;
|
|
46791
|
+
const filePath = fileOrAttachmentId;
|
|
46792
|
+
if (!taskId) {
|
|
46793
|
+
throw new CliError("MISSING_ID", "Task ID required", ExitCodes.INVALID_ARGS);
|
|
46794
|
+
}
|
|
46795
|
+
if (!filePath) {
|
|
46796
|
+
throw new CliError("MISSING_FILE", "File path required", ExitCodes.INVALID_ARGS);
|
|
46797
|
+
}
|
|
46798
|
+
const attachment = await client.uploadTaskAttachment(taskId, filePath);
|
|
46799
|
+
if (isJsonOutput()) {
|
|
46800
|
+
output(attachment);
|
|
46801
|
+
} else {
|
|
46802
|
+
console.log(`Uploaded: ${attachment.filename}`);
|
|
46803
|
+
console.log(` ID: ${attachment.id}`);
|
|
46804
|
+
}
|
|
46805
|
+
break;
|
|
46806
|
+
}
|
|
46807
|
+
if (subAction === "delete") {
|
|
46808
|
+
const taskId = taskIdOrFile;
|
|
46809
|
+
const attachmentId = fileOrAttachmentId;
|
|
46810
|
+
if (!taskId) {
|
|
46811
|
+
throw new CliError("MISSING_ID", "Task ID required", ExitCodes.INVALID_ARGS);
|
|
46812
|
+
}
|
|
46813
|
+
if (!attachmentId) {
|
|
46814
|
+
throw new CliError("MISSING_ATTACHMENT_ID", "Attachment ID required", ExitCodes.INVALID_ARGS);
|
|
46815
|
+
}
|
|
46816
|
+
await client.deleteTaskAttachment(taskId, attachmentId);
|
|
46817
|
+
if (isJsonOutput()) {
|
|
46818
|
+
output({ success: true, deleted: attachmentId });
|
|
46819
|
+
} else {
|
|
46820
|
+
console.log(`Deleted attachment: ${attachmentId}`);
|
|
46821
|
+
}
|
|
46822
|
+
break;
|
|
46823
|
+
}
|
|
46824
|
+
if (subAction === "path") {
|
|
46825
|
+
const taskId = taskIdOrFile;
|
|
46826
|
+
const attachmentId = fileOrAttachmentId;
|
|
46827
|
+
if (!taskId) {
|
|
46828
|
+
throw new CliError("MISSING_ID", "Task ID required", ExitCodes.INVALID_ARGS);
|
|
46829
|
+
}
|
|
46830
|
+
if (!attachmentId) {
|
|
46831
|
+
throw new CliError("MISSING_ATTACHMENT_ID", "Attachment ID required", ExitCodes.INVALID_ARGS);
|
|
46832
|
+
}
|
|
46833
|
+
const result = await client.getTaskAttachmentPath(taskId, attachmentId);
|
|
46834
|
+
if (isJsonOutput()) {
|
|
46835
|
+
output(result);
|
|
46836
|
+
} else {
|
|
46837
|
+
console.log(`Path: ${result.path}`);
|
|
46838
|
+
console.log(`Filename: ${result.filename}`);
|
|
46839
|
+
console.log(`Type: ${result.mimeType}`);
|
|
46840
|
+
}
|
|
46841
|
+
break;
|
|
46842
|
+
}
|
|
46843
|
+
throw new CliError("UNKNOWN_SUBACTION", `Unknown attachments action: ${subAction}. Valid: list, upload, delete, path`, ExitCodes.INVALID_ARGS);
|
|
46844
|
+
}
|
|
46652
46845
|
default:
|
|
46653
|
-
throw new CliError("UNKNOWN_ACTION", `Unknown action: ${action}. Valid: list, get, create, update, move, delete, add-label, remove-label, set-due-date, add-dependency, remove-dependency, list-dependencies, labels`, ExitCodes.INVALID_ARGS);
|
|
46846
|
+
throw new CliError("UNKNOWN_ACTION", `Unknown action: ${action}. Valid: list, get, create, update, move, delete, add-label, remove-label, set-due-date, add-dependency, remove-dependency, list-dependencies, labels, attachments`, ExitCodes.INVALID_ARGS);
|
|
46654
46847
|
}
|
|
46655
46848
|
}
|
|
46656
46849
|
|
|
@@ -47617,8 +47810,8 @@ async function handleFsCommand(action, positional, flags) {
|
|
|
47617
47810
|
|
|
47618
47811
|
// cli/src/commands/up.ts
|
|
47619
47812
|
import { spawn } from "child_process";
|
|
47620
|
-
import { existsSync as
|
|
47621
|
-
import { dirname as
|
|
47813
|
+
import { existsSync as existsSync4 } from "fs";
|
|
47814
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
47622
47815
|
import { fileURLToPath } from "url";
|
|
47623
47816
|
init_errors();
|
|
47624
47817
|
|
|
@@ -47899,1093 +48092,77 @@ function installUv() {
|
|
|
47899
48092
|
return false;
|
|
47900
48093
|
return installDependency(dep);
|
|
47901
48094
|
}
|
|
47902
|
-
// package.json
|
|
47903
|
-
var package_default = {
|
|
47904
|
-
name: "@knowsuchagency/fulcrum",
|
|
47905
|
-
private: true,
|
|
47906
|
-
version: "1.3.0",
|
|
47907
|
-
description: "Harness Attention. Orchestrate Agents. Ship.",
|
|
47908
|
-
license: "PolyForm-Perimeter-1.0.0",
|
|
47909
|
-
type: "module",
|
|
47910
|
-
scripts: {
|
|
47911
|
-
dev: "vite --host",
|
|
47912
|
-
"dev:server": "mkdir -p ~/.fulcrum && bun --watch server/index.ts",
|
|
47913
|
-
build: "tsc -b && vite build",
|
|
47914
|
-
start: "NODE_ENV=production bun server/index.ts",
|
|
47915
|
-
lint: "eslint .",
|
|
47916
|
-
preview: "vite preview",
|
|
47917
|
-
"db:generate": "drizzle-kit generate",
|
|
47918
|
-
"db:migrate": "drizzle-kit migrate",
|
|
47919
|
-
"db:studio": "drizzle-kit studio"
|
|
47920
|
-
},
|
|
47921
|
-
dependencies: {
|
|
47922
|
-
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
|
47923
|
-
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
|
|
47924
|
-
"@azurity/pure-nerd-font": "^3.0.5",
|
|
47925
|
-
"@base-ui/react": "^1.0.0",
|
|
47926
|
-
"@dagrejs/dagre": "^1.1.8",
|
|
47927
|
-
"@fontsource-variable/jetbrains-mono": "^5.2.8",
|
|
47928
|
-
"@hono/node-server": "^1.19.7",
|
|
47929
|
-
"@hono/node-ws": "^1.2.0",
|
|
47930
|
-
"@hugeicons/core-free-icons": "^3.0.0",
|
|
47931
|
-
"@hugeicons/react": "^1.1.3",
|
|
47932
|
-
"@monaco-editor/react": "^4.7.0",
|
|
47933
|
-
"@octokit/rest": "^22.0.1",
|
|
47934
|
-
"@radix-ui/react-collapsible": "^1.1.12",
|
|
47935
|
-
"@tailwindcss/vite": "^4.1.17",
|
|
47936
|
-
"@tanstack/react-query": "^5.90.12",
|
|
47937
|
-
"@tanstack/react-router": "^1.141.8",
|
|
47938
|
-
"@uiw/react-markdown-preview": "^5.1.5",
|
|
47939
|
-
"@xterm/addon-clipboard": "^0.2.0",
|
|
47940
|
-
"@xterm/addon-fit": "^0.10.0",
|
|
47941
|
-
"@xterm/addon-web-links": "^0.11.0",
|
|
47942
|
-
"@xterm/xterm": "^5.5.0",
|
|
47943
|
-
"bun-pty": "^0.4.2",
|
|
47944
|
-
citty: "^0.1.6",
|
|
47945
|
-
"class-variance-authority": "^0.7.1",
|
|
47946
|
-
cloudflare: "^5.2.0",
|
|
47947
|
-
clsx: "^2.1.1",
|
|
47948
|
-
"date-fns": "^4.1.0",
|
|
47949
|
-
"drizzle-orm": "^0.45.1",
|
|
47950
|
-
"fancy-ansi": "^0.1.3",
|
|
47951
|
-
glob: "^13.0.0",
|
|
47952
|
-
hono: "^4.11.1",
|
|
47953
|
-
i18next: "^25.7.3",
|
|
47954
|
-
mobx: "^6.15.0",
|
|
47955
|
-
"mobx-react-lite": "^4.1.1",
|
|
47956
|
-
"mobx-state-tree": "^7.0.2",
|
|
47957
|
-
"next-themes": "^0.4.6",
|
|
47958
|
-
react: "^19.2.0",
|
|
47959
|
-
"react-day-picker": "^9.13.0",
|
|
47960
|
-
"react-dom": "^19.2.0",
|
|
47961
|
-
"react-i18next": "^16.5.0",
|
|
47962
|
-
"react-resizable-panels": "^4.0.11",
|
|
47963
|
-
reactflow: "^11.11.4",
|
|
47964
|
-
recharts: "2.15.4",
|
|
47965
|
-
shadcn: "^3.6.2",
|
|
47966
|
-
shiki: "^3.20.0",
|
|
47967
|
-
sonner: "^2.0.7",
|
|
47968
|
-
"tailwind-merge": "^3.4.0",
|
|
47969
|
-
tailwindcss: "^4.1.17",
|
|
47970
|
-
"tw-animate-css": "^1.4.0",
|
|
47971
|
-
ws: "^8.18.3",
|
|
47972
|
-
yaml: "^2.8.2"
|
|
47973
|
-
},
|
|
47974
|
-
devDependencies: {
|
|
47975
|
-
"@eslint/js": "^9.39.1",
|
|
47976
|
-
"@opencode-ai/plugin": "^1.1.8",
|
|
47977
|
-
"@tailwindcss/typography": "^0.5.19",
|
|
47978
|
-
"@tanstack/router-plugin": "^1.141.8",
|
|
47979
|
-
"@types/bun": "^1.2.14",
|
|
47980
|
-
"@types/node": "^24.10.1",
|
|
47981
|
-
"@types/react": "^19.2.5",
|
|
47982
|
-
"@types/react-dom": "^19.2.3",
|
|
47983
|
-
"@types/ws": "^8.18.1",
|
|
47984
|
-
"@vitejs/plugin-react": "^5.1.1",
|
|
47985
|
-
"drizzle-kit": "^0.31.8",
|
|
47986
|
-
eslint: "^9.39.1",
|
|
47987
|
-
"eslint-plugin-react-hooks": "^7.0.1",
|
|
47988
|
-
"eslint-plugin-react-refresh": "^0.4.24",
|
|
47989
|
-
globals: "^16.5.0",
|
|
47990
|
-
typescript: "~5.9.3",
|
|
47991
|
-
"typescript-eslint": "^8.46.4",
|
|
47992
|
-
vite: "^7.2.4"
|
|
47993
|
-
}
|
|
47994
|
-
};
|
|
47995
48095
|
|
|
47996
|
-
// cli/src/commands/
|
|
47997
|
-
|
|
47998
|
-
|
|
47999
|
-
|
|
48000
|
-
|
|
48001
|
-
|
|
48002
|
-
|
|
48096
|
+
// cli/src/commands/claude.ts
|
|
48097
|
+
init_errors();
|
|
48098
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
48099
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, existsSync as existsSync3, rmSync, readFileSync as readFileSync4 } from "fs";
|
|
48100
|
+
import { homedir as homedir2 } from "os";
|
|
48101
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
48102
|
+
|
|
48103
|
+
// plugins/fulcrum/.claude-plugin/marketplace.json
|
|
48104
|
+
var marketplace_default = `{
|
|
48105
|
+
"name": "fulcrum",
|
|
48106
|
+
"owner": {
|
|
48107
|
+
"name": "Fulcrum"
|
|
48108
|
+
},
|
|
48109
|
+
"plugins": [
|
|
48110
|
+
{
|
|
48111
|
+
"name": "fulcrum",
|
|
48112
|
+
"source": "./",
|
|
48113
|
+
"description": "Task orchestration for Claude Code",
|
|
48114
|
+
"version": "1.5.0",
|
|
48115
|
+
"skills": [
|
|
48116
|
+
"./skills/fulcrum"
|
|
48117
|
+
],
|
|
48118
|
+
"keywords": [
|
|
48119
|
+
"fulcrum",
|
|
48120
|
+
"task",
|
|
48121
|
+
"orchestration",
|
|
48122
|
+
"worktree"
|
|
48123
|
+
]
|
|
48003
48124
|
}
|
|
48004
|
-
|
|
48125
|
+
],
|
|
48126
|
+
"metadata": {
|
|
48127
|
+
"description": "Fulcrum task orchestration plugin marketplace",
|
|
48128
|
+
"version": "1.0.0"
|
|
48005
48129
|
}
|
|
48006
|
-
return dirname2(dirname2(dirname2(currentFile)));
|
|
48007
48130
|
}
|
|
48008
|
-
|
|
48009
|
-
|
|
48010
|
-
|
|
48011
|
-
|
|
48012
|
-
|
|
48013
|
-
|
|
48014
|
-
|
|
48015
|
-
|
|
48016
|
-
|
|
48017
|
-
|
|
48018
|
-
|
|
48019
|
-
|
|
48020
|
-
|
|
48021
|
-
console.error(" Bun is the JavaScript runtime that powers Fulcrum.");
|
|
48022
|
-
const shouldInstall = autoYes || await confirm(`Would you like to install bun via ${method}?`);
|
|
48023
|
-
if (shouldInstall) {
|
|
48024
|
-
const success2 = installBun();
|
|
48025
|
-
if (!success2) {
|
|
48026
|
-
throw new CliError("INSTALL_FAILED", "Failed to install bun", ExitCodes.ERROR);
|
|
48131
|
+
`;
|
|
48132
|
+
|
|
48133
|
+
// plugins/fulcrum/hooks/hooks.json
|
|
48134
|
+
var hooks_default = `{
|
|
48135
|
+
"hooks": {
|
|
48136
|
+
"Stop": [
|
|
48137
|
+
{
|
|
48138
|
+
"hooks": [
|
|
48139
|
+
{
|
|
48140
|
+
"type": "command",
|
|
48141
|
+
"command": "fulcrum current-task review 2>/dev/null || true"
|
|
48142
|
+
}
|
|
48143
|
+
]
|
|
48027
48144
|
}
|
|
48028
|
-
|
|
48029
|
-
|
|
48030
|
-
|
|
48031
|
-
|
|
48032
|
-
|
|
48033
|
-
|
|
48034
|
-
|
|
48035
|
-
|
|
48036
|
-
|
|
48037
|
-
console.error(" dtach enables persistent terminal sessions that survive disconnects.");
|
|
48038
|
-
const shouldInstall = autoYes || await confirm(`Would you like to install dtach via ${method}?`);
|
|
48039
|
-
if (shouldInstall) {
|
|
48040
|
-
const success2 = installDtach();
|
|
48041
|
-
if (!success2) {
|
|
48042
|
-
throw new CliError("INSTALL_FAILED", "Failed to install dtach", ExitCodes.ERROR);
|
|
48145
|
+
],
|
|
48146
|
+
"UserPromptSubmit": [
|
|
48147
|
+
{
|
|
48148
|
+
"hooks": [
|
|
48149
|
+
{
|
|
48150
|
+
"type": "command",
|
|
48151
|
+
"command": "fulcrum current-task in-progress 2>/dev/null || true"
|
|
48152
|
+
}
|
|
48153
|
+
]
|
|
48043
48154
|
}
|
|
48044
|
-
|
|
48045
|
-
|
|
48046
|
-
|
|
48047
|
-
|
|
48048
|
-
|
|
48049
|
-
|
|
48050
|
-
|
|
48051
|
-
|
|
48052
|
-
|
|
48053
|
-
|
|
48054
|
-
|
|
48055
|
-
if (shouldInstall) {
|
|
48056
|
-
const success2 = installUv();
|
|
48057
|
-
if (!success2) {
|
|
48058
|
-
throw new CliError("INSTALL_FAILED", "Failed to install uv", ExitCodes.ERROR);
|
|
48059
|
-
}
|
|
48060
|
-
console.error("uv installed successfully!");
|
|
48061
|
-
} else {
|
|
48062
|
-
throw new CliError("MISSING_DEPENDENCY", `uv is required. Install manually: ${getInstallCommand(uvDep)}`, ExitCodes.ERROR);
|
|
48063
|
-
}
|
|
48064
|
-
}
|
|
48065
|
-
const existingPid = readPid();
|
|
48066
|
-
if (existingPid && isProcessRunning(existingPid)) {
|
|
48067
|
-
console.error(`Fulcrum server is already running (PID: ${existingPid})`);
|
|
48068
|
-
const shouldReplace = autoYes || await confirm("Would you like to stop it and start a new instance?");
|
|
48069
|
-
if (shouldReplace) {
|
|
48070
|
-
console.error("Stopping existing instance...");
|
|
48071
|
-
process.kill(existingPid, "SIGTERM");
|
|
48072
|
-
let attempts = 0;
|
|
48073
|
-
while (attempts < 50 && isProcessRunning(existingPid)) {
|
|
48074
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
48075
|
-
attempts++;
|
|
48076
|
-
}
|
|
48077
|
-
if (isProcessRunning(existingPid)) {
|
|
48078
|
-
process.kill(existingPid, "SIGKILL");
|
|
48079
|
-
}
|
|
48080
|
-
removePid();
|
|
48081
|
-
console.error("Existing instance stopped.");
|
|
48082
|
-
} else {
|
|
48083
|
-
throw new CliError("ALREADY_RUNNING", `Server already running at http://localhost:${getPort(flags.port)}`, ExitCodes.ERROR);
|
|
48084
|
-
}
|
|
48085
|
-
}
|
|
48086
|
-
const port = getPort(flags.port);
|
|
48087
|
-
if (flags.port) {
|
|
48088
|
-
updateSettingsPort(port);
|
|
48089
|
-
}
|
|
48090
|
-
const host = flags.host ? "0.0.0.0" : "localhost";
|
|
48091
|
-
const packageRoot = getPackageRoot();
|
|
48092
|
-
const serverPath = join3(packageRoot, "server", "index.js");
|
|
48093
|
-
const platform2 = process.platform;
|
|
48094
|
-
const arch = process.arch;
|
|
48095
|
-
let ptyLibName;
|
|
48096
|
-
if (platform2 === "darwin") {
|
|
48097
|
-
ptyLibName = arch === "arm64" ? "librust_pty_arm64.dylib" : "librust_pty.dylib";
|
|
48098
|
-
} else if (platform2 === "win32") {
|
|
48099
|
-
ptyLibName = "rust_pty.dll";
|
|
48100
|
-
} else {
|
|
48101
|
-
ptyLibName = arch === "arm64" ? "librust_pty_arm64.so" : "librust_pty.so";
|
|
48102
|
-
}
|
|
48103
|
-
const ptyLibPath = join3(packageRoot, "lib", ptyLibName);
|
|
48104
|
-
const fulcrumDir = getFulcrumDir();
|
|
48105
|
-
const debug = flags.debug === "true";
|
|
48106
|
-
console.error(`Starting Fulcrum server${debug ? " (debug mode)" : ""}...`);
|
|
48107
|
-
const serverProc = spawn("bun", [serverPath], {
|
|
48108
|
-
detached: true,
|
|
48109
|
-
stdio: "ignore",
|
|
48110
|
-
env: {
|
|
48111
|
-
...process.env,
|
|
48112
|
-
NODE_ENV: "production",
|
|
48113
|
-
PORT: port.toString(),
|
|
48114
|
-
HOST: host,
|
|
48115
|
-
FULCRUM_DIR: fulcrumDir,
|
|
48116
|
-
FULCRUM_PACKAGE_ROOT: packageRoot,
|
|
48117
|
-
FULCRUM_VERSION: package_default.version,
|
|
48118
|
-
BUN_PTY_LIB: ptyLibPath,
|
|
48119
|
-
...isClaudeInstalled() && { FULCRUM_CLAUDE_INSTALLED: "1" },
|
|
48120
|
-
...isOpencodeInstalled() && { FULCRUM_OPENCODE_INSTALLED: "1" },
|
|
48121
|
-
...debug && { LOG_LEVEL: "debug", DEBUG: "1" }
|
|
48122
|
-
}
|
|
48123
|
-
});
|
|
48124
|
-
serverProc.unref();
|
|
48125
|
-
const pid = serverProc.pid;
|
|
48126
|
-
if (!pid) {
|
|
48127
|
-
throw new CliError("START_FAILED", "Failed to start server process", ExitCodes.ERROR);
|
|
48128
|
-
}
|
|
48129
|
-
writePid(pid);
|
|
48130
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
48131
|
-
if (!isProcessRunning(pid)) {
|
|
48132
|
-
throw new CliError("START_FAILED", "Server process died immediately after starting", ExitCodes.ERROR);
|
|
48133
|
-
}
|
|
48134
|
-
if (isJsonOutput()) {
|
|
48135
|
-
output({
|
|
48136
|
-
pid,
|
|
48137
|
-
port,
|
|
48138
|
-
url: `http://localhost:${port}`
|
|
48139
|
-
});
|
|
48140
|
-
} else {
|
|
48141
|
-
const hasAgent = isClaudeInstalled() || isOpencodeInstalled();
|
|
48142
|
-
showGettingStartedTips(port, hasAgent);
|
|
48143
|
-
}
|
|
48144
|
-
}
|
|
48145
|
-
function showGettingStartedTips(port, hasAgent) {
|
|
48146
|
-
console.error(`
|
|
48147
|
-
Fulcrum is running at http://localhost:${port}
|
|
48148
|
-
|
|
48149
|
-
Getting Started:
|
|
48150
|
-
1. Open http://localhost:${port} in your browser
|
|
48151
|
-
2. Add a repository to get started
|
|
48152
|
-
3. Create a task to spin up an isolated worktree
|
|
48153
|
-
4. Run your AI agent in the task terminal
|
|
48154
|
-
|
|
48155
|
-
Commands:
|
|
48156
|
-
fulcrum status Check server status
|
|
48157
|
-
fulcrum doctor Check all dependencies
|
|
48158
|
-
fulcrum down Stop the server
|
|
48159
|
-
`);
|
|
48160
|
-
if (!hasAgent) {
|
|
48161
|
-
console.error(`Note: No AI agents detected. Install one to get started:
|
|
48162
|
-
Claude Code: curl -fsSL https://claude.ai/install.sh | bash
|
|
48163
|
-
OpenCode: curl -fsSL https://opencode.ai/install | bash
|
|
48164
|
-
`);
|
|
48165
|
-
}
|
|
48166
|
-
}
|
|
48167
|
-
|
|
48168
|
-
// cli/src/commands/down.ts
|
|
48169
|
-
init_errors();
|
|
48170
|
-
async function handleDownCommand() {
|
|
48171
|
-
const pid = readPid();
|
|
48172
|
-
if (!pid) {
|
|
48173
|
-
throw new CliError("NOT_RUNNING", "No PID file found. Fulcrum server may not be running.", ExitCodes.ERROR);
|
|
48174
|
-
}
|
|
48175
|
-
if (!isProcessRunning(pid)) {
|
|
48176
|
-
removePid();
|
|
48177
|
-
if (isJsonOutput()) {
|
|
48178
|
-
output({ stopped: true, pid, wasRunning: false });
|
|
48179
|
-
} else {
|
|
48180
|
-
console.log(`Fulcrum was not running (stale PID file cleaned up)`);
|
|
48181
|
-
}
|
|
48182
|
-
return;
|
|
48183
|
-
}
|
|
48184
|
-
try {
|
|
48185
|
-
process.kill(pid, "SIGTERM");
|
|
48186
|
-
} catch (err) {
|
|
48187
|
-
throw new CliError("KILL_FAILED", `Failed to stop server (PID: ${pid}): ${err}`, ExitCodes.ERROR);
|
|
48188
|
-
}
|
|
48189
|
-
let attempts = 0;
|
|
48190
|
-
while (attempts < 50 && isProcessRunning(pid)) {
|
|
48191
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
48192
|
-
attempts++;
|
|
48193
|
-
}
|
|
48194
|
-
if (isProcessRunning(pid)) {
|
|
48195
|
-
try {
|
|
48196
|
-
process.kill(pid, "SIGKILL");
|
|
48197
|
-
} catch {}
|
|
48198
|
-
}
|
|
48199
|
-
removePid();
|
|
48200
|
-
if (isJsonOutput()) {
|
|
48201
|
-
output({ stopped: true, pid, wasRunning: true });
|
|
48202
|
-
} else {
|
|
48203
|
-
console.log(`Fulcrum stopped (PID: ${pid})`);
|
|
48204
|
-
}
|
|
48205
|
-
}
|
|
48206
|
-
|
|
48207
|
-
// cli/src/commands/migrate-from-vibora.ts
|
|
48208
|
-
init_server();
|
|
48209
|
-
async function handleMigrateFromViboraCommand(flags) {
|
|
48210
|
-
const autoYes = flags.yes === "true" || flags.y === "true";
|
|
48211
|
-
if (!needsViboraMigration()) {
|
|
48212
|
-
if (isJsonOutput()) {
|
|
48213
|
-
output({ migrated: false, reason: "no_migration_needed" });
|
|
48214
|
-
} else {
|
|
48215
|
-
console.error("No migration needed.");
|
|
48216
|
-
console.error(` ~/.vibora does not exist or ~/.fulcrum already has data.`);
|
|
48217
|
-
}
|
|
48218
|
-
return;
|
|
48219
|
-
}
|
|
48220
|
-
const viboraDir = getLegacyViboraDir();
|
|
48221
|
-
const fulcrumDir = getFulcrumDir();
|
|
48222
|
-
if (!isJsonOutput()) {
|
|
48223
|
-
console.error(`
|
|
48224
|
-
Found existing Vibora data at ${viboraDir}`);
|
|
48225
|
-
console.error("Fulcrum (formerly Vibora) now uses ~/.fulcrum for data storage.");
|
|
48226
|
-
console.error("");
|
|
48227
|
-
console.error("Your existing data can be copied to the new location.");
|
|
48228
|
-
console.error("This is non-destructive - your ~/.vibora directory will be left untouched.");
|
|
48229
|
-
console.error("");
|
|
48230
|
-
}
|
|
48231
|
-
const shouldMigrate = autoYes || await confirm("Would you like to copy your data to ~/.fulcrum?");
|
|
48232
|
-
if (!shouldMigrate) {
|
|
48233
|
-
if (isJsonOutput()) {
|
|
48234
|
-
output({ migrated: false, reason: "user_declined" });
|
|
48235
|
-
} else {
|
|
48236
|
-
console.error("Migration skipped.");
|
|
48237
|
-
console.error("You can run this command again later to migrate.");
|
|
48238
|
-
}
|
|
48239
|
-
return;
|
|
48240
|
-
}
|
|
48241
|
-
if (!isJsonOutput()) {
|
|
48242
|
-
console.error("Copying data from ~/.vibora to ~/.fulcrum...");
|
|
48243
|
-
}
|
|
48244
|
-
const success2 = migrateFromVibora();
|
|
48245
|
-
if (success2) {
|
|
48246
|
-
if (isJsonOutput()) {
|
|
48247
|
-
output({ migrated: true, from: viboraDir, to: fulcrumDir });
|
|
48248
|
-
} else {
|
|
48249
|
-
console.error("Migration complete!");
|
|
48250
|
-
console.error(` Data copied from ${viboraDir} to ${fulcrumDir}`);
|
|
48251
|
-
console.error(" Your original ~/.vibora directory has been preserved.");
|
|
48252
|
-
}
|
|
48253
|
-
} else {
|
|
48254
|
-
if (isJsonOutput()) {
|
|
48255
|
-
output({ migrated: false, reason: "migration_failed" });
|
|
48256
|
-
} else {
|
|
48257
|
-
console.error("Migration failed.");
|
|
48258
|
-
console.error("You can manually copy files from ~/.vibora to ~/.fulcrum");
|
|
48259
|
-
}
|
|
48260
|
-
process.exitCode = 1;
|
|
48261
|
-
}
|
|
48262
|
-
}
|
|
48263
|
-
|
|
48264
|
-
// cli/src/commands/status.ts
|
|
48265
|
-
init_server();
|
|
48266
|
-
async function handleStatusCommand(flags) {
|
|
48267
|
-
const pid = readPid();
|
|
48268
|
-
const port = getPort(flags.port);
|
|
48269
|
-
const serverUrl = discoverServerUrl(flags.url, flags.port);
|
|
48270
|
-
const pidRunning = pid !== null && isProcessRunning(pid);
|
|
48271
|
-
let healthOk = false;
|
|
48272
|
-
let version3 = null;
|
|
48273
|
-
let uptime = null;
|
|
48274
|
-
if (pidRunning) {
|
|
48275
|
-
try {
|
|
48276
|
-
const res = await fetch(`${serverUrl}/health`, { signal: AbortSignal.timeout(2000) });
|
|
48277
|
-
healthOk = res.ok;
|
|
48278
|
-
if (res.ok) {
|
|
48279
|
-
const health = await res.json();
|
|
48280
|
-
version3 = health.version || null;
|
|
48281
|
-
uptime = health.uptime || null;
|
|
48282
|
-
}
|
|
48283
|
-
} catch {}
|
|
48284
|
-
}
|
|
48285
|
-
const data = {
|
|
48286
|
-
running: pidRunning,
|
|
48287
|
-
healthy: healthOk,
|
|
48288
|
-
pid: pid || null,
|
|
48289
|
-
port,
|
|
48290
|
-
url: serverUrl,
|
|
48291
|
-
version: version3,
|
|
48292
|
-
uptime
|
|
48293
|
-
};
|
|
48294
|
-
if (isJsonOutput()) {
|
|
48295
|
-
output(data);
|
|
48296
|
-
} else {
|
|
48297
|
-
if (pidRunning) {
|
|
48298
|
-
const healthStatus = healthOk ? "healthy" : "not responding";
|
|
48299
|
-
console.log(`Fulcrum is running (${healthStatus})`);
|
|
48300
|
-
console.log(` PID: ${pid}`);
|
|
48301
|
-
console.log(` URL: ${serverUrl}`);
|
|
48302
|
-
if (version3)
|
|
48303
|
-
console.log(` Version: ${version3}`);
|
|
48304
|
-
if (uptime)
|
|
48305
|
-
console.log(` Uptime: ${Math.floor(uptime / 1000)}s`);
|
|
48306
|
-
} else {
|
|
48307
|
-
console.log("Fulcrum is not running");
|
|
48308
|
-
console.log(`
|
|
48309
|
-
Start with: fulcrum up`);
|
|
48310
|
-
}
|
|
48311
|
-
}
|
|
48312
|
-
}
|
|
48313
|
-
|
|
48314
|
-
// cli/src/commands/git.ts
|
|
48315
|
-
init_client();
|
|
48316
|
-
init_errors();
|
|
48317
|
-
async function handleGitCommand(action, flags) {
|
|
48318
|
-
const client = new FulcrumClient(flags.url, flags.port);
|
|
48319
|
-
switch (action) {
|
|
48320
|
-
case "status": {
|
|
48321
|
-
const path = flags.path || process.cwd();
|
|
48322
|
-
const status = await client.getStatus(path);
|
|
48323
|
-
if (isJsonOutput()) {
|
|
48324
|
-
output(status);
|
|
48325
|
-
} else {
|
|
48326
|
-
console.log(`Branch: ${status.branch}`);
|
|
48327
|
-
if (status.ahead)
|
|
48328
|
-
console.log(` Ahead: ${status.ahead}`);
|
|
48329
|
-
if (status.behind)
|
|
48330
|
-
console.log(` Behind: ${status.behind}`);
|
|
48331
|
-
if (status.staged?.length)
|
|
48332
|
-
console.log(` Staged: ${status.staged.length} files`);
|
|
48333
|
-
if (status.modified?.length)
|
|
48334
|
-
console.log(` Modified: ${status.modified.length} files`);
|
|
48335
|
-
if (status.untracked?.length)
|
|
48336
|
-
console.log(` Untracked: ${status.untracked.length} files`);
|
|
48337
|
-
if (!status.staged?.length && !status.modified?.length && !status.untracked?.length) {
|
|
48338
|
-
console.log(" Working tree clean");
|
|
48339
|
-
}
|
|
48340
|
-
}
|
|
48341
|
-
break;
|
|
48342
|
-
}
|
|
48343
|
-
case "diff": {
|
|
48344
|
-
const path = flags.path || process.cwd();
|
|
48345
|
-
const diff = await client.getDiff(path, {
|
|
48346
|
-
staged: flags.staged === "true",
|
|
48347
|
-
ignoreWhitespace: flags["ignore-whitespace"] === "true",
|
|
48348
|
-
includeUntracked: flags["include-untracked"] === "true"
|
|
48349
|
-
});
|
|
48350
|
-
if (isJsonOutput()) {
|
|
48351
|
-
output(diff);
|
|
48352
|
-
} else {
|
|
48353
|
-
console.log(diff.diff || "No changes");
|
|
48354
|
-
}
|
|
48355
|
-
break;
|
|
48356
|
-
}
|
|
48357
|
-
case "branches": {
|
|
48358
|
-
const repo = flags.repo;
|
|
48359
|
-
if (!repo) {
|
|
48360
|
-
throw new CliError("MISSING_REPO", "--repo is required", ExitCodes.INVALID_ARGS);
|
|
48361
|
-
}
|
|
48362
|
-
const branches = await client.getBranches(repo);
|
|
48363
|
-
if (isJsonOutput()) {
|
|
48364
|
-
output(branches);
|
|
48365
|
-
} else {
|
|
48366
|
-
for (const branch of branches) {
|
|
48367
|
-
const current = branch.current ? "* " : " ";
|
|
48368
|
-
console.log(`${current}${branch.name}`);
|
|
48369
|
-
}
|
|
48370
|
-
}
|
|
48371
|
-
break;
|
|
48372
|
-
}
|
|
48373
|
-
default:
|
|
48374
|
-
throw new CliError("UNKNOWN_ACTION", `Unknown action: ${action}. Valid: status, diff, branches`, ExitCodes.INVALID_ARGS);
|
|
48375
|
-
}
|
|
48376
|
-
}
|
|
48377
|
-
|
|
48378
|
-
// cli/src/commands/worktrees.ts
|
|
48379
|
-
init_client();
|
|
48380
|
-
init_errors();
|
|
48381
|
-
async function handleWorktreesCommand(action, flags) {
|
|
48382
|
-
const client = new FulcrumClient(flags.url, flags.port);
|
|
48383
|
-
switch (action) {
|
|
48384
|
-
case "list": {
|
|
48385
|
-
const worktrees = await client.listWorktrees();
|
|
48386
|
-
if (isJsonOutput()) {
|
|
48387
|
-
output(worktrees);
|
|
48388
|
-
} else {
|
|
48389
|
-
if (worktrees.length === 0) {
|
|
48390
|
-
console.log("No worktrees found");
|
|
48391
|
-
} else {
|
|
48392
|
-
for (const wt of worktrees) {
|
|
48393
|
-
console.log(`${wt.path}`);
|
|
48394
|
-
console.log(` Branch: ${wt.branch}`);
|
|
48395
|
-
if (wt.taskId)
|
|
48396
|
-
console.log(` Task: ${wt.taskId}`);
|
|
48397
|
-
}
|
|
48398
|
-
}
|
|
48399
|
-
}
|
|
48400
|
-
break;
|
|
48401
|
-
}
|
|
48402
|
-
case "delete": {
|
|
48403
|
-
const worktreePath = flags.path;
|
|
48404
|
-
if (!worktreePath) {
|
|
48405
|
-
throw new CliError("MISSING_PATH", "--path is required", ExitCodes.INVALID_ARGS);
|
|
48406
|
-
}
|
|
48407
|
-
const deleteLinkedTask = flags["delete-task"] === "true" || flags["delete-task"] === "";
|
|
48408
|
-
const result = await client.deleteWorktree(worktreePath, flags.repo, deleteLinkedTask);
|
|
48409
|
-
if (isJsonOutput()) {
|
|
48410
|
-
output(result);
|
|
48411
|
-
} else {
|
|
48412
|
-
console.log(`Deleted worktree: ${worktreePath}`);
|
|
48413
|
-
}
|
|
48414
|
-
break;
|
|
48415
|
-
}
|
|
48416
|
-
default:
|
|
48417
|
-
throw new CliError("UNKNOWN_ACTION", `Unknown action: ${action}. Valid: list, delete`, ExitCodes.INVALID_ARGS);
|
|
48418
|
-
}
|
|
48419
|
-
}
|
|
48420
|
-
|
|
48421
|
-
// cli/src/commands/config.ts
|
|
48422
|
-
init_client();
|
|
48423
|
-
init_errors();
|
|
48424
|
-
async function handleConfigCommand(action, positional, flags) {
|
|
48425
|
-
const client = new FulcrumClient(flags.url, flags.port);
|
|
48426
|
-
switch (action) {
|
|
48427
|
-
case "list": {
|
|
48428
|
-
const config3 = await client.getAllConfig();
|
|
48429
|
-
if (isJsonOutput()) {
|
|
48430
|
-
output(config3);
|
|
48431
|
-
} else {
|
|
48432
|
-
console.log("Configuration:");
|
|
48433
|
-
for (const [key, value] of Object.entries(config3)) {
|
|
48434
|
-
const displayValue = value === null ? "(not set)" : value;
|
|
48435
|
-
console.log(` ${key}: ${displayValue}`);
|
|
48436
|
-
}
|
|
48437
|
-
}
|
|
48438
|
-
break;
|
|
48439
|
-
}
|
|
48440
|
-
case "get": {
|
|
48441
|
-
const [key] = positional;
|
|
48442
|
-
if (!key) {
|
|
48443
|
-
throw new CliError("MISSING_KEY", "Config key is required", ExitCodes.INVALID_ARGS);
|
|
48444
|
-
}
|
|
48445
|
-
const config3 = await client.getConfig(key);
|
|
48446
|
-
if (isJsonOutput()) {
|
|
48447
|
-
output(config3);
|
|
48448
|
-
} else {
|
|
48449
|
-
const value = config3.value === null ? "(not set)" : config3.value;
|
|
48450
|
-
console.log(`${key}: ${value}`);
|
|
48451
|
-
}
|
|
48452
|
-
break;
|
|
48453
|
-
}
|
|
48454
|
-
case "set": {
|
|
48455
|
-
const [key, value] = positional;
|
|
48456
|
-
if (!key) {
|
|
48457
|
-
throw new CliError("MISSING_KEY", "Config key is required", ExitCodes.INVALID_ARGS);
|
|
48458
|
-
}
|
|
48459
|
-
if (value === undefined) {
|
|
48460
|
-
throw new CliError("MISSING_VALUE", "Config value is required", ExitCodes.INVALID_ARGS);
|
|
48461
|
-
}
|
|
48462
|
-
const parsedValue = /^\d+$/.test(value) ? parseInt(value, 10) : value;
|
|
48463
|
-
const config3 = await client.setConfig(key, parsedValue);
|
|
48464
|
-
if (isJsonOutput()) {
|
|
48465
|
-
output(config3);
|
|
48466
|
-
} else {
|
|
48467
|
-
console.log(`Set ${key} = ${config3.value}`);
|
|
48468
|
-
}
|
|
48469
|
-
break;
|
|
48470
|
-
}
|
|
48471
|
-
case "reset": {
|
|
48472
|
-
const [key] = positional;
|
|
48473
|
-
if (!key) {
|
|
48474
|
-
throw new CliError("MISSING_KEY", "Config key is required", ExitCodes.INVALID_ARGS);
|
|
48475
|
-
}
|
|
48476
|
-
const config3 = await client.resetConfig(key);
|
|
48477
|
-
if (isJsonOutput()) {
|
|
48478
|
-
output(config3);
|
|
48479
|
-
} else {
|
|
48480
|
-
console.log(`Reset ${key} to default: ${config3.value}`);
|
|
48481
|
-
}
|
|
48482
|
-
break;
|
|
48483
|
-
}
|
|
48484
|
-
default:
|
|
48485
|
-
throw new CliError("UNKNOWN_ACTION", `Unknown action: ${action}. Valid: list, get, set, reset`, ExitCodes.INVALID_ARGS);
|
|
48486
|
-
}
|
|
48487
|
-
}
|
|
48488
|
-
|
|
48489
|
-
// cli/src/commands/opencode.ts
|
|
48490
|
-
init_errors();
|
|
48491
|
-
import {
|
|
48492
|
-
mkdirSync as mkdirSync3,
|
|
48493
|
-
writeFileSync as writeFileSync3,
|
|
48494
|
-
existsSync as existsSync4,
|
|
48495
|
-
readFileSync as readFileSync4,
|
|
48496
|
-
unlinkSync as unlinkSync2,
|
|
48497
|
-
copyFileSync,
|
|
48498
|
-
renameSync
|
|
48499
|
-
} from "fs";
|
|
48500
|
-
import { homedir as homedir2 } from "os";
|
|
48501
|
-
import { join as join4 } from "path";
|
|
48502
|
-
|
|
48503
|
-
// plugins/fulcrum-opencode/index.ts
|
|
48504
|
-
var fulcrum_opencode_default = `import type { Plugin } from "@opencode-ai/plugin"
|
|
48505
|
-
import { appendFileSync } from "node:fs"
|
|
48506
|
-
import { spawn } from "node:child_process"
|
|
48507
|
-
import { tmpdir } from "node:os"
|
|
48508
|
-
import { join } from "node:path"
|
|
48509
|
-
|
|
48510
|
-
declare const process: { env: Record<string, string | undefined> }
|
|
48511
|
-
|
|
48512
|
-
const LOG_FILE = join(tmpdir(), "fulcrum-opencode.log")
|
|
48513
|
-
const NOISY_EVENTS = new Set([
|
|
48514
|
-
"message.part.updated",
|
|
48515
|
-
"file.watcher.updated",
|
|
48516
|
-
"tui.toast.show",
|
|
48517
|
-
"config.updated",
|
|
48518
|
-
])
|
|
48519
|
-
const log = (msg: string) => {
|
|
48520
|
-
try {
|
|
48521
|
-
appendFileSync(LOG_FILE, \`[\${new Date().toISOString()}] \${msg}\\n\`)
|
|
48522
|
-
} catch {
|
|
48523
|
-
// Silently ignore logging errors - logging is non-critical
|
|
48524
|
-
}
|
|
48525
|
-
}
|
|
48526
|
-
|
|
48527
|
-
/**
|
|
48528
|
-
* Execute fulcrum command using spawn with shell option for proper PATH resolution.
|
|
48529
|
-
* Using spawn with explicit args array prevents shell injection while shell:true
|
|
48530
|
-
* ensures PATH is properly resolved (for NVM, fnm, etc. managed node installations).
|
|
48531
|
-
* Includes 10 second timeout protection to prevent hanging.
|
|
48532
|
-
*/
|
|
48533
|
-
async function runFulcrumCommand(args: string[]): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
|
48534
|
-
return new Promise((resolve) => {
|
|
48535
|
-
let stdout = ''
|
|
48536
|
-
let stderr = ''
|
|
48537
|
-
let resolved = false
|
|
48538
|
-
let processExited = false
|
|
48539
|
-
let killTimeoutId: ReturnType<typeof setTimeout> | null = null
|
|
48540
|
-
|
|
48541
|
-
const child = spawn(FULCRUM_CMD, args, { shell: true })
|
|
48542
|
-
|
|
48543
|
-
const cleanup = () => {
|
|
48544
|
-
processExited = true
|
|
48545
|
-
if (killTimeoutId) {
|
|
48546
|
-
clearTimeout(killTimeoutId)
|
|
48547
|
-
killTimeoutId = null
|
|
48548
|
-
}
|
|
48549
|
-
}
|
|
48550
|
-
|
|
48551
|
-
child.stdout?.on('data', (data) => {
|
|
48552
|
-
stdout += data.toString()
|
|
48553
|
-
})
|
|
48554
|
-
|
|
48555
|
-
child.stderr?.on('data', (data) => {
|
|
48556
|
-
stderr += data.toString()
|
|
48557
|
-
})
|
|
48558
|
-
|
|
48559
|
-
child.on('close', (code) => {
|
|
48560
|
-
cleanup()
|
|
48561
|
-
if (!resolved) {
|
|
48562
|
-
resolved = true
|
|
48563
|
-
resolve({ exitCode: code || 0, stdout, stderr })
|
|
48564
|
-
}
|
|
48565
|
-
})
|
|
48566
|
-
|
|
48567
|
-
child.on('error', (err) => {
|
|
48568
|
-
cleanup()
|
|
48569
|
-
if (!resolved) {
|
|
48570
|
-
resolved = true
|
|
48571
|
-
resolve({ exitCode: 1, stdout, stderr: err.message || '' })
|
|
48572
|
-
}
|
|
48573
|
-
})
|
|
48574
|
-
|
|
48575
|
-
// Add timeout protection to prevent hanging
|
|
48576
|
-
const timeoutId = setTimeout(() => {
|
|
48577
|
-
if (!resolved) {
|
|
48578
|
-
resolved = true
|
|
48579
|
-
log(\`Command timeout: \${FULCRUM_CMD} \${args.join(' ')}\`)
|
|
48580
|
-
child.kill('SIGTERM')
|
|
48581
|
-
// Schedule SIGKILL if process doesn't exit after SIGTERM
|
|
48582
|
-
killTimeoutId = setTimeout(() => {
|
|
48583
|
-
if (!processExited) {
|
|
48584
|
-
log(\`Process didn't exit after SIGTERM, sending SIGKILL\`)
|
|
48585
|
-
child.kill('SIGKILL')
|
|
48586
|
-
}
|
|
48587
|
-
}, 2000)
|
|
48588
|
-
resolve({ exitCode: -1, stdout, stderr: \`Command timed out after \${FULCRUM_COMMAND_TIMEOUT_MS}ms\` })
|
|
48589
|
-
}
|
|
48590
|
-
}, FULCRUM_COMMAND_TIMEOUT_MS)
|
|
48591
|
-
|
|
48592
|
-
// Clear timeout if command completes
|
|
48593
|
-
child.on('exit', () => clearTimeout(timeoutId))
|
|
48594
|
-
})
|
|
48595
|
-
}
|
|
48596
|
-
|
|
48597
|
-
let mainSessionId: string | null = null
|
|
48598
|
-
const subagentSessions = new Set<string>()
|
|
48599
|
-
let pendingIdleTimer: ReturnType<typeof setTimeout> | null = null
|
|
48600
|
-
let activityVersion = 0
|
|
48601
|
-
let lastStatus: "in-progress" | "review" | "" = ""
|
|
48602
|
-
|
|
48603
|
-
const FULCRUM_CMD = "fulcrum"
|
|
48604
|
-
const IDLE_CONFIRMATION_DELAY_MS = 1500
|
|
48605
|
-
const FULCRUM_COMMAND_TIMEOUT_MS = 10000
|
|
48606
|
-
const STATUS_CHANGE_DEBOUNCE_MS = 500
|
|
48607
|
-
|
|
48608
|
-
let deferredContextCheck: Promise<boolean> | null = null
|
|
48609
|
-
let isFulcrumContext: boolean | null = null
|
|
48610
|
-
let pendingStatusCommand: Promise<{ exitCode: number; stdout: string; stderr: string }> | null = null
|
|
48611
|
-
|
|
48612
|
-
export const FulcrumPlugin: Plugin = async ({ $, directory }) => {
|
|
48613
|
-
log("Plugin initializing...")
|
|
48614
|
-
|
|
48615
|
-
if (process.env.FULCRUM_TASK_ID) {
|
|
48616
|
-
isFulcrumContext = true
|
|
48617
|
-
log("Fulcrum context detected via env var")
|
|
48618
|
-
} else {
|
|
48619
|
-
deferredContextCheck = Promise.all([
|
|
48620
|
-
$\`\${FULCRUM_CMD} --version\`.quiet().nothrow().text(),
|
|
48621
|
-
runFulcrumCommand(['current-task', '--path', directory]),
|
|
48622
|
-
])
|
|
48623
|
-
.then(([versionResult, taskResult]) => {
|
|
48624
|
-
if (!versionResult) {
|
|
48625
|
-
log("Fulcrum CLI not found")
|
|
48626
|
-
return false
|
|
48627
|
-
}
|
|
48628
|
-
const inContext = taskResult.exitCode === 0
|
|
48629
|
-
log(inContext ? "Fulcrum context active" : "Not a Fulcrum context")
|
|
48630
|
-
return inContext
|
|
48631
|
-
})
|
|
48632
|
-
.catch(() => {
|
|
48633
|
-
log("Fulcrum check failed")
|
|
48634
|
-
return false
|
|
48635
|
-
})
|
|
48636
|
-
}
|
|
48637
|
-
|
|
48638
|
-
log("Plugin hooks registered")
|
|
48639
|
-
|
|
48640
|
-
const checkContext = async (): Promise<boolean> => {
|
|
48641
|
-
if (isFulcrumContext !== null) return isFulcrumContext
|
|
48642
|
-
if (deferredContextCheck) {
|
|
48643
|
-
isFulcrumContext = await deferredContextCheck
|
|
48644
|
-
deferredContextCheck = null
|
|
48645
|
-
return isFulcrumContext
|
|
48646
|
-
}
|
|
48647
|
-
return false
|
|
48648
|
-
}
|
|
48649
|
-
|
|
48650
|
-
const cancelPendingIdle = () => {
|
|
48651
|
-
if (pendingIdleTimer) {
|
|
48652
|
-
clearTimeout(pendingIdleTimer)
|
|
48653
|
-
pendingIdleTimer = null
|
|
48654
|
-
log("Cancelled pending idle transition")
|
|
48655
|
-
}
|
|
48656
|
-
}
|
|
48657
|
-
|
|
48658
|
-
const setStatus = (status: "in-progress" | "review") => {
|
|
48659
|
-
if (status === lastStatus) return
|
|
48660
|
-
|
|
48661
|
-
cancelPendingIdle()
|
|
48662
|
-
|
|
48663
|
-
if (pendingStatusCommand) {
|
|
48664
|
-
log(\`Status change already in progress, will retry after \${STATUS_CHANGE_DEBOUNCE_MS}ms\`)
|
|
48665
|
-
setTimeout(() => setStatus(status), STATUS_CHANGE_DEBOUNCE_MS)
|
|
48666
|
-
return
|
|
48667
|
-
}
|
|
48668
|
-
|
|
48669
|
-
lastStatus = status
|
|
48670
|
-
|
|
48671
|
-
;(async () => {
|
|
48672
|
-
try {
|
|
48673
|
-
log(\`Setting status: \${status}\`)
|
|
48674
|
-
pendingStatusCommand = runFulcrumCommand(['current-task', status, '--path', directory])
|
|
48675
|
-
const res = await pendingStatusCommand
|
|
48676
|
-
pendingStatusCommand = null
|
|
48677
|
-
|
|
48678
|
-
if (res.exitCode !== 0) {
|
|
48679
|
-
log(\`Status update failed: exitCode=\${res.exitCode}, stderr=\${res.stderr}\`)
|
|
48680
|
-
}
|
|
48681
|
-
} catch (e) {
|
|
48682
|
-
log(\`Status update error: \${e}\`)
|
|
48683
|
-
pendingStatusCommand = null
|
|
48684
|
-
}
|
|
48685
|
-
})()
|
|
48686
|
-
}
|
|
48687
|
-
|
|
48688
|
-
const scheduleIdleTransition = () => {
|
|
48689
|
-
cancelPendingIdle()
|
|
48690
|
-
const currentVersion = ++activityVersion
|
|
48691
|
-
|
|
48692
|
-
pendingIdleTimer = setTimeout(() => {
|
|
48693
|
-
if (activityVersion !== currentVersion) {
|
|
48694
|
-
log(
|
|
48695
|
-
\`Stale idle transition (version \${currentVersion} vs \${activityVersion})\`,
|
|
48696
|
-
)
|
|
48697
|
-
return
|
|
48698
|
-
}
|
|
48699
|
-
setStatus("review")
|
|
48700
|
-
}, IDLE_CONFIRMATION_DELAY_MS)
|
|
48701
|
-
|
|
48702
|
-
log(
|
|
48703
|
-
\`Scheduled idle transition (version \${currentVersion}, delay \${IDLE_CONFIRMATION_DELAY_MS}ms)\`,
|
|
48704
|
-
)
|
|
48705
|
-
}
|
|
48706
|
-
|
|
48707
|
-
const recordActivity = (reason: string) => {
|
|
48708
|
-
activityVersion++
|
|
48709
|
-
cancelPendingIdle()
|
|
48710
|
-
log(\`Activity: \${reason} (version now \${activityVersion})\`)
|
|
48711
|
-
}
|
|
48712
|
-
|
|
48713
|
-
return {
|
|
48714
|
-
"chat.message": async (_input, output) => {
|
|
48715
|
-
if (!(await checkContext())) return
|
|
48716
|
-
|
|
48717
|
-
if (output.message.role === "user") {
|
|
48718
|
-
recordActivity("user message")
|
|
48719
|
-
setStatus("in-progress")
|
|
48720
|
-
} else if (output.message.role === "assistant") {
|
|
48721
|
-
recordActivity("assistant message")
|
|
48722
|
-
}
|
|
48723
|
-
},
|
|
48724
|
-
|
|
48725
|
-
event: async ({ event }) => {
|
|
48726
|
-
if (!NOISY_EVENTS.has(event.type)) {
|
|
48727
|
-
log(\`Event: \${event.type}\`)
|
|
48728
|
-
}
|
|
48729
|
-
|
|
48730
|
-
if (!(await checkContext())) return
|
|
48731
|
-
|
|
48732
|
-
const props = (event.properties as Record<string, unknown>) || {}
|
|
48733
|
-
|
|
48734
|
-
if (event.type === "session.created") {
|
|
48735
|
-
const info = (props.info as Record<string, unknown>) || {}
|
|
48736
|
-
const sessionId = info.id as string | undefined
|
|
48737
|
-
const parentId = info.parentID as string | undefined
|
|
48738
|
-
|
|
48739
|
-
if (parentId) {
|
|
48740
|
-
if (sessionId) subagentSessions.add(sessionId)
|
|
48741
|
-
log(\`Subagent session tracked: \${sessionId} (parent: \${parentId})\`)
|
|
48742
|
-
} else if (!mainSessionId && sessionId) {
|
|
48743
|
-
mainSessionId = sessionId
|
|
48744
|
-
log(\`Main session set: \${mainSessionId}\`)
|
|
48745
|
-
}
|
|
48746
|
-
|
|
48747
|
-
recordActivity("session.created")
|
|
48748
|
-
setStatus("in-progress")
|
|
48749
|
-
return
|
|
48750
|
-
}
|
|
48751
|
-
|
|
48752
|
-
const status = props.status as Record<string, unknown> | undefined
|
|
48753
|
-
if (
|
|
48754
|
-
(event.type === "session.status" && status?.type === "busy") ||
|
|
48755
|
-
event.type.startsWith("tool.execute")
|
|
48756
|
-
) {
|
|
48757
|
-
recordActivity(event.type)
|
|
48758
|
-
return
|
|
48759
|
-
}
|
|
48760
|
-
|
|
48761
|
-
if (
|
|
48762
|
-
event.type === "session.idle" ||
|
|
48763
|
-
(event.type === "session.status" && status?.type === "idle")
|
|
48764
|
-
) {
|
|
48765
|
-
const info = (props.info as Record<string, unknown>) || {}
|
|
48766
|
-
const sessionId =
|
|
48767
|
-
(props.sessionID as string) || (info.id as string) || null
|
|
48768
|
-
|
|
48769
|
-
if (sessionId && subagentSessions.has(sessionId)) {
|
|
48770
|
-
log(\`Ignoring subagent idle: \${sessionId}\`)
|
|
48771
|
-
return
|
|
48772
|
-
}
|
|
48773
|
-
|
|
48774
|
-
if (mainSessionId && sessionId && sessionId !== mainSessionId) {
|
|
48775
|
-
log(\`Ignoring non-main idle: \${sessionId} (main: \${mainSessionId})\`)
|
|
48776
|
-
return
|
|
48777
|
-
}
|
|
48778
|
-
|
|
48779
|
-
log(\`Main session idle detected: \${sessionId}\`)
|
|
48780
|
-
scheduleIdleTransition()
|
|
48781
|
-
}
|
|
48782
|
-
},
|
|
48783
|
-
}
|
|
48784
|
-
}
|
|
48785
|
-
`;
|
|
48786
|
-
|
|
48787
|
-
// cli/src/commands/opencode.ts
|
|
48788
|
-
var OPENCODE_DIR = join4(homedir2(), ".opencode");
|
|
48789
|
-
var OPENCODE_CONFIG_PATH = join4(OPENCODE_DIR, "opencode.json");
|
|
48790
|
-
var PLUGIN_DIR = join4(homedir2(), ".config", "opencode", "plugin");
|
|
48791
|
-
var PLUGIN_PATH = join4(PLUGIN_DIR, "fulcrum.ts");
|
|
48792
|
-
var FULCRUM_MCP_CONFIG = {
|
|
48793
|
-
type: "local",
|
|
48794
|
-
command: ["fulcrum", "mcp"],
|
|
48795
|
-
enabled: true
|
|
48796
|
-
};
|
|
48797
|
-
async function handleOpenCodeCommand(action) {
|
|
48798
|
-
if (action === "install") {
|
|
48799
|
-
await installOpenCodeIntegration();
|
|
48800
|
-
return;
|
|
48801
|
-
}
|
|
48802
|
-
if (action === "uninstall") {
|
|
48803
|
-
await uninstallOpenCodeIntegration();
|
|
48804
|
-
return;
|
|
48805
|
-
}
|
|
48806
|
-
throw new CliError("INVALID_ACTION", "Unknown action. Usage: fulcrum opencode install | fulcrum opencode uninstall", ExitCodes.INVALID_ARGS);
|
|
48807
|
-
}
|
|
48808
|
-
async function installOpenCodeIntegration() {
|
|
48809
|
-
try {
|
|
48810
|
-
console.log("Installing OpenCode plugin...");
|
|
48811
|
-
mkdirSync3(PLUGIN_DIR, { recursive: true });
|
|
48812
|
-
writeFileSync3(PLUGIN_PATH, fulcrum_opencode_default, "utf-8");
|
|
48813
|
-
console.log("\u2713 Installed plugin at " + PLUGIN_PATH);
|
|
48814
|
-
console.log("Configuring MCP server...");
|
|
48815
|
-
const mcpConfigured = addMcpServer();
|
|
48816
|
-
console.log("");
|
|
48817
|
-
if (mcpConfigured) {
|
|
48818
|
-
console.log("Installation complete! Restart OpenCode to apply changes.");
|
|
48819
|
-
} else {
|
|
48820
|
-
console.log("Plugin installed, but MCP configuration was skipped.");
|
|
48821
|
-
console.log("Please add the MCP server manually (see above).");
|
|
48822
|
-
}
|
|
48823
|
-
} catch (err) {
|
|
48824
|
-
throw new CliError("INSTALL_FAILED", `Failed to install OpenCode integration: ${err instanceof Error ? err.message : String(err)}`, ExitCodes.ERROR);
|
|
48825
|
-
}
|
|
48826
|
-
}
|
|
48827
|
-
async function uninstallOpenCodeIntegration() {
|
|
48828
|
-
try {
|
|
48829
|
-
let removedPlugin = false;
|
|
48830
|
-
let removedMcp = false;
|
|
48831
|
-
if (existsSync4(PLUGIN_PATH)) {
|
|
48832
|
-
unlinkSync2(PLUGIN_PATH);
|
|
48833
|
-
console.log("\u2713 Removed plugin from " + PLUGIN_PATH);
|
|
48834
|
-
removedPlugin = true;
|
|
48835
|
-
} else {
|
|
48836
|
-
console.log("\u2022 Plugin not found (already removed)");
|
|
48837
|
-
}
|
|
48838
|
-
removedMcp = removeMcpServer();
|
|
48839
|
-
if (!removedPlugin && !removedMcp) {
|
|
48840
|
-
console.log("Nothing to uninstall.");
|
|
48841
|
-
} else {
|
|
48842
|
-
console.log("");
|
|
48843
|
-
console.log("Uninstall complete! Restart OpenCode to apply changes.");
|
|
48844
|
-
}
|
|
48845
|
-
} catch (err) {
|
|
48846
|
-
throw new CliError("UNINSTALL_FAILED", `Failed to uninstall OpenCode integration: ${err instanceof Error ? err.message : String(err)}`, ExitCodes.ERROR);
|
|
48847
|
-
}
|
|
48848
|
-
}
|
|
48849
|
-
function getMcpObject(config3) {
|
|
48850
|
-
const mcp = config3.mcp;
|
|
48851
|
-
if (mcp && typeof mcp === "object" && !Array.isArray(mcp)) {
|
|
48852
|
-
return mcp;
|
|
48853
|
-
}
|
|
48854
|
-
return {};
|
|
48855
|
-
}
|
|
48856
|
-
function addMcpServer() {
|
|
48857
|
-
mkdirSync3(OPENCODE_DIR, { recursive: true });
|
|
48858
|
-
let config3 = {};
|
|
48859
|
-
if (existsSync4(OPENCODE_CONFIG_PATH)) {
|
|
48860
|
-
try {
|
|
48861
|
-
const content = readFileSync4(OPENCODE_CONFIG_PATH, "utf-8");
|
|
48862
|
-
config3 = JSON.parse(content);
|
|
48863
|
-
} catch {
|
|
48864
|
-
console.log("\u26A0 Could not parse existing opencode.json, skipping MCP configuration");
|
|
48865
|
-
console.log(" Add manually to ~/.opencode/opencode.json:");
|
|
48866
|
-
console.log(' "mcp": { "fulcrum": { "type": "local", "command": ["fulcrum", "mcp"], "enabled": true } }');
|
|
48867
|
-
return false;
|
|
48868
|
-
}
|
|
48869
|
-
}
|
|
48870
|
-
const mcp = getMcpObject(config3);
|
|
48871
|
-
if (mcp.fulcrum) {
|
|
48872
|
-
console.log("\u2022 MCP server already configured, preserving existing configuration");
|
|
48873
|
-
return true;
|
|
48874
|
-
}
|
|
48875
|
-
if (existsSync4(OPENCODE_CONFIG_PATH)) {
|
|
48876
|
-
copyFileSync(OPENCODE_CONFIG_PATH, OPENCODE_CONFIG_PATH + ".backup");
|
|
48877
|
-
}
|
|
48878
|
-
config3.mcp = {
|
|
48879
|
-
...mcp,
|
|
48880
|
-
fulcrum: FULCRUM_MCP_CONFIG
|
|
48881
|
-
};
|
|
48882
|
-
const tempPath = OPENCODE_CONFIG_PATH + ".tmp";
|
|
48883
|
-
try {
|
|
48884
|
-
writeFileSync3(tempPath, JSON.stringify(config3, null, 2), "utf-8");
|
|
48885
|
-
renameSync(tempPath, OPENCODE_CONFIG_PATH);
|
|
48886
|
-
} catch (error46) {
|
|
48887
|
-
try {
|
|
48888
|
-
if (existsSync4(tempPath)) {
|
|
48889
|
-
unlinkSync2(tempPath);
|
|
48890
|
-
}
|
|
48891
|
-
} catch {}
|
|
48892
|
-
throw error46;
|
|
48893
|
-
}
|
|
48894
|
-
console.log("\u2713 Added MCP server to " + OPENCODE_CONFIG_PATH);
|
|
48895
|
-
return true;
|
|
48896
|
-
}
|
|
48897
|
-
function removeMcpServer() {
|
|
48898
|
-
if (!existsSync4(OPENCODE_CONFIG_PATH)) {
|
|
48899
|
-
console.log("\u2022 MCP config not found (already removed)");
|
|
48900
|
-
return false;
|
|
48901
|
-
}
|
|
48902
|
-
let config3;
|
|
48903
|
-
try {
|
|
48904
|
-
const content = readFileSync4(OPENCODE_CONFIG_PATH, "utf-8");
|
|
48905
|
-
config3 = JSON.parse(content);
|
|
48906
|
-
} catch {
|
|
48907
|
-
console.log("\u26A0 Could not parse opencode.json, skipping MCP removal");
|
|
48908
|
-
return false;
|
|
48909
|
-
}
|
|
48910
|
-
const mcp = getMcpObject(config3);
|
|
48911
|
-
if (!mcp.fulcrum) {
|
|
48912
|
-
console.log("\u2022 MCP server not configured (already removed)");
|
|
48913
|
-
return false;
|
|
48914
|
-
}
|
|
48915
|
-
copyFileSync(OPENCODE_CONFIG_PATH, OPENCODE_CONFIG_PATH + ".backup");
|
|
48916
|
-
delete mcp.fulcrum;
|
|
48917
|
-
if (Object.keys(mcp).length === 0) {
|
|
48918
|
-
delete config3.mcp;
|
|
48919
|
-
} else {
|
|
48920
|
-
config3.mcp = mcp;
|
|
48921
|
-
}
|
|
48922
|
-
const tempPath = OPENCODE_CONFIG_PATH + ".tmp";
|
|
48923
|
-
try {
|
|
48924
|
-
writeFileSync3(tempPath, JSON.stringify(config3, null, 2), "utf-8");
|
|
48925
|
-
renameSync(tempPath, OPENCODE_CONFIG_PATH);
|
|
48926
|
-
} catch (error46) {
|
|
48927
|
-
try {
|
|
48928
|
-
if (existsSync4(tempPath)) {
|
|
48929
|
-
unlinkSync2(tempPath);
|
|
48930
|
-
}
|
|
48931
|
-
} catch {}
|
|
48932
|
-
throw error46;
|
|
48933
|
-
}
|
|
48934
|
-
console.log("\u2713 Removed MCP server from " + OPENCODE_CONFIG_PATH);
|
|
48935
|
-
return true;
|
|
48936
|
-
}
|
|
48937
|
-
|
|
48938
|
-
// cli/src/commands/claude.ts
|
|
48939
|
-
init_errors();
|
|
48940
|
-
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, existsSync as existsSync5, rmSync } from "fs";
|
|
48941
|
-
import { homedir as homedir3 } from "os";
|
|
48942
|
-
import { join as join5 } from "path";
|
|
48943
|
-
|
|
48944
|
-
// plugins/fulcrum/.claude-plugin/plugin.json
|
|
48945
|
-
var plugin_default = `{
|
|
48946
|
-
"name": "fulcrum",
|
|
48947
|
-
"description": "Fulcrum task orchestration for Claude Code",
|
|
48948
|
-
"version": "1.3.0",
|
|
48949
|
-
"author": {
|
|
48950
|
-
"name": "Fulcrum"
|
|
48951
|
-
},
|
|
48952
|
-
"hooks": "./hooks/hooks.json"
|
|
48953
|
-
}
|
|
48954
|
-
`;
|
|
48955
|
-
|
|
48956
|
-
// plugins/fulcrum/hooks/hooks.json
|
|
48957
|
-
var hooks_default = `{
|
|
48958
|
-
"hooks": {
|
|
48959
|
-
"Stop": [
|
|
48960
|
-
{
|
|
48961
|
-
"hooks": [
|
|
48962
|
-
{
|
|
48963
|
-
"type": "command",
|
|
48964
|
-
"command": "fulcrum current-task review 2>/dev/null || true"
|
|
48965
|
-
}
|
|
48966
|
-
]
|
|
48967
|
-
}
|
|
48968
|
-
],
|
|
48969
|
-
"UserPromptSubmit": [
|
|
48970
|
-
{
|
|
48971
|
-
"hooks": [
|
|
48972
|
-
{
|
|
48973
|
-
"type": "command",
|
|
48974
|
-
"command": "fulcrum current-task in-progress 2>/dev/null || true"
|
|
48975
|
-
}
|
|
48976
|
-
]
|
|
48977
|
-
}
|
|
48978
|
-
]
|
|
48979
|
-
}
|
|
48980
|
-
}
|
|
48981
|
-
`;
|
|
48982
|
-
|
|
48983
|
-
// plugins/fulcrum/mcp.json
|
|
48984
|
-
var mcp_default = `{
|
|
48985
|
-
"mcpServers": {
|
|
48986
|
-
"fulcrum": {
|
|
48987
|
-
"command": "fulcrum",
|
|
48988
|
-
"args": ["mcp"]
|
|
48155
|
+
]
|
|
48156
|
+
}
|
|
48157
|
+
}
|
|
48158
|
+
`;
|
|
48159
|
+
|
|
48160
|
+
// plugins/fulcrum/.mcp.json
|
|
48161
|
+
var _mcp_default = `{
|
|
48162
|
+
"mcpServers": {
|
|
48163
|
+
"fulcrum": {
|
|
48164
|
+
"command": "fulcrum",
|
|
48165
|
+
"args": ["mcp"]
|
|
48989
48166
|
}
|
|
48990
48167
|
}
|
|
48991
48168
|
}
|
|
@@ -49016,38 +48193,25 @@ Send a notification: \`fulcrum notify $ARGUMENTS\`
|
|
|
49016
48193
|
Format: fulcrum notify "Title" "Message body"
|
|
49017
48194
|
`;
|
|
49018
48195
|
|
|
49019
|
-
// plugins/fulcrum/
|
|
49020
|
-
var linear_default = `---
|
|
49021
|
-
description: Link a Linear ticket to the current fulcrum task
|
|
49022
|
-
---
|
|
49023
|
-
Link the Linear ticket to this task: \`fulcrum current-task linear $ARGUMENTS\`
|
|
49024
|
-
`;
|
|
49025
|
-
|
|
49026
|
-
// plugins/fulcrum/commands/review.md
|
|
49027
|
-
var review_default = `---
|
|
49028
|
-
description: Mark the current fulcrum task as ready for review
|
|
49029
|
-
---
|
|
49030
|
-
Mark this task ready for review: \`fulcrum current-task review\`
|
|
49031
|
-
|
|
49032
|
-
This sends a notification to the user.
|
|
49033
|
-
`;
|
|
49034
|
-
|
|
49035
|
-
// plugins/fulcrum/skills/vibora/SKILL.md
|
|
48196
|
+
// plugins/fulcrum/skills/fulcrum/SKILL.md
|
|
49036
48197
|
var SKILL_default = `---
|
|
49037
48198
|
name: fulcrum
|
|
49038
|
-
description:
|
|
48199
|
+
description: AI orchestration and task management platform. Use this skill when working in a Fulcrum task worktree, managing tasks/projects, or interacting with the Fulcrum server.
|
|
49039
48200
|
---
|
|
49040
48201
|
|
|
49041
|
-
# Fulcrum - AI
|
|
48202
|
+
# Fulcrum - AI Orchestration Platform
|
|
49042
48203
|
|
|
49043
48204
|
## Overview
|
|
49044
48205
|
|
|
49045
|
-
Fulcrum is
|
|
48206
|
+
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.
|
|
49046
48207
|
|
|
49047
|
-
**
|
|
49048
|
-
-
|
|
49049
|
-
-
|
|
49050
|
-
-
|
|
48208
|
+
**Key Features:**
|
|
48209
|
+
- Task management with kanban boards and status tracking
|
|
48210
|
+
- Project and repository organization
|
|
48211
|
+
- Git worktree isolation for parallel development
|
|
48212
|
+
- Docker Compose app deployment
|
|
48213
|
+
- Multi-channel notifications
|
|
48214
|
+
- MCP tools for AI agent integration
|
|
49051
48215
|
|
|
49052
48216
|
## When to Use This Skill
|
|
49053
48217
|
|
|
@@ -49055,9 +48219,9 @@ Use the Fulcrum CLI when:
|
|
|
49055
48219
|
- **Working in a task worktree** - Use \`current-task\` commands to manage your current task
|
|
49056
48220
|
- **Updating task status** - Mark tasks as in-progress, ready for review, done, or canceled
|
|
49057
48221
|
- **Linking PRs** - Associate a GitHub PR with the current task
|
|
49058
|
-
- **Linking Linear tickets** - Connect a Linear issue to the current task
|
|
49059
48222
|
- **Linking URLs** - Attach any relevant URLs (design docs, specs, external resources) to the task
|
|
49060
48223
|
- **Sending notifications** - Alert the user when work is complete or needs attention
|
|
48224
|
+
- **Managing projects and repositories** - Create, update, and organize projects
|
|
49061
48225
|
|
|
49062
48226
|
Use the Fulcrum MCP tools when:
|
|
49063
48227
|
- **Executing commands remotely** - Run shell commands on the Fulcrum server from Claude Desktop
|
|
@@ -49070,7 +48234,7 @@ Use the Fulcrum MCP tools when:
|
|
|
49070
48234
|
When running inside a Fulcrum task worktree, use these commands to manage the current task:
|
|
49071
48235
|
|
|
49072
48236
|
\`\`\`bash
|
|
49073
|
-
# Get current task info (
|
|
48237
|
+
# Get current task info (comprehensive output with dependencies, attachments, etc.)
|
|
49074
48238
|
fulcrum current-task
|
|
49075
48239
|
|
|
49076
48240
|
# Update task status
|
|
@@ -49107,7 +48271,7 @@ fulcrum tasks list --label="bug" # Filter by label
|
|
|
49107
48271
|
fulcrum tasks labels # Show all labels with counts
|
|
49108
48272
|
fulcrum tasks labels --search="comm" # Find labels matching substring
|
|
49109
48273
|
|
|
49110
|
-
# Get a specific task
|
|
48274
|
+
# Get a specific task (includes dependencies and attachments)
|
|
49111
48275
|
fulcrum tasks get <task-id>
|
|
49112
48276
|
|
|
49113
48277
|
# Create a new task
|
|
@@ -49122,6 +48286,25 @@ fulcrum tasks move <task-id> --status=IN_REVIEW
|
|
|
49122
48286
|
# Delete a task
|
|
49123
48287
|
fulcrum tasks delete <task-id>
|
|
49124
48288
|
fulcrum tasks delete <task-id> --delete-worktree # Also delete worktree
|
|
48289
|
+
|
|
48290
|
+
# Labels
|
|
48291
|
+
fulcrum tasks add-label <task-id> <label>
|
|
48292
|
+
fulcrum tasks remove-label <task-id> <label>
|
|
48293
|
+
|
|
48294
|
+
# Due dates
|
|
48295
|
+
fulcrum tasks set-due-date <task-id> 2026-01-25 # Set due date
|
|
48296
|
+
fulcrum tasks set-due-date <task-id> none # Clear due date
|
|
48297
|
+
|
|
48298
|
+
# Dependencies
|
|
48299
|
+
fulcrum tasks add-dependency <task-id> <depends-on-task-id>
|
|
48300
|
+
fulcrum tasks remove-dependency <task-id> <dependency-id>
|
|
48301
|
+
fulcrum tasks list-dependencies <task-id>
|
|
48302
|
+
|
|
48303
|
+
# Attachments
|
|
48304
|
+
fulcrum tasks attachments list <task-id>
|
|
48305
|
+
fulcrum tasks attachments upload <task-id> <file-path>
|
|
48306
|
+
fulcrum tasks attachments delete <task-id> <attachment-id>
|
|
48307
|
+
fulcrum tasks attachments path <task-id> <attachment-id> # Get local file path
|
|
49125
48308
|
\`\`\`
|
|
49126
48309
|
|
|
49127
48310
|
### notifications
|
|
@@ -49155,7 +48338,28 @@ fulcrum notifications set slack webhookUrl <url>
|
|
|
49155
48338
|
fulcrum up # Start Fulcrum server daemon
|
|
49156
48339
|
fulcrum down # Stop Fulcrum server
|
|
49157
48340
|
fulcrum status # Check if server is running
|
|
49158
|
-
fulcrum
|
|
48341
|
+
fulcrum doctor # Check all dependencies and versions
|
|
48342
|
+
\`\`\`
|
|
48343
|
+
|
|
48344
|
+
### Configuration
|
|
48345
|
+
|
|
48346
|
+
\`\`\`bash
|
|
48347
|
+
fulcrum config list # List all config values
|
|
48348
|
+
fulcrum config get <key> # Get a specific value
|
|
48349
|
+
fulcrum config set <key> <value> # Set a value
|
|
48350
|
+
fulcrum config reset <key> # Reset to default
|
|
48351
|
+
\`\`\`
|
|
48352
|
+
|
|
48353
|
+
### Agent Plugin Installation
|
|
48354
|
+
|
|
48355
|
+
\`\`\`bash
|
|
48356
|
+
# Claude Code integration
|
|
48357
|
+
fulcrum claude install # Install Fulcrum plugin for Claude Code
|
|
48358
|
+
fulcrum claude uninstall # Uninstall plugin
|
|
48359
|
+
|
|
48360
|
+
# OpenCode integration
|
|
48361
|
+
fulcrum opencode install # Install Fulcrum plugin for OpenCode
|
|
48362
|
+
fulcrum opencode uninstall # Uninstall plugin
|
|
49159
48363
|
\`\`\`
|
|
49160
48364
|
|
|
49161
48365
|
### Git Operations
|
|
@@ -49163,7 +48367,15 @@ fulcrum health # Check server health
|
|
|
49163
48367
|
\`\`\`bash
|
|
49164
48368
|
fulcrum git status # Git status for current worktree
|
|
49165
48369
|
fulcrum git diff # Git diff for current worktree
|
|
48370
|
+
fulcrum git diff --staged # Show staged changes only
|
|
48371
|
+
fulcrum git branches --repo=/path/to/repo # List branches
|
|
48372
|
+
\`\`\`
|
|
48373
|
+
|
|
48374
|
+
### Worktrees
|
|
48375
|
+
|
|
48376
|
+
\`\`\`bash
|
|
49166
48377
|
fulcrum worktrees list # List all worktrees
|
|
48378
|
+
fulcrum worktrees delete --path=/path/to/worktree # Delete a worktree
|
|
49167
48379
|
\`\`\`
|
|
49168
48380
|
|
|
49169
48381
|
### projects
|
|
@@ -49316,302 +48528,1434 @@ fulcrum fs stat --path=/path/to/check
|
|
|
49316
48528
|
4. **Ready for Review**: Agent marks complete: \`fulcrum current-task review\`
|
|
49317
48529
|
5. **Notification**: User receives notification that work is ready
|
|
49318
48530
|
|
|
49319
|
-
### Linking External Resources
|
|
48531
|
+
### Linking External Resources
|
|
48532
|
+
|
|
48533
|
+
\`\`\`bash
|
|
48534
|
+
# After creating a GitHub PR
|
|
48535
|
+
fulcrum current-task pr https://github.com/owner/repo/pull/123
|
|
48536
|
+
|
|
48537
|
+
# After identifying the relevant Linear ticket
|
|
48538
|
+
fulcrum current-task linear https://linear.app/team/issue/TEAM-123
|
|
48539
|
+
|
|
48540
|
+
# Add any URL link (design docs, figma, notion, external resources)
|
|
48541
|
+
fulcrum current-task link https://figma.com/file/abc123/design
|
|
48542
|
+
fulcrum current-task link https://notion.so/team/spec --label "Product Spec"
|
|
48543
|
+
\`\`\`
|
|
48544
|
+
|
|
48545
|
+
### Notifying the User
|
|
48546
|
+
|
|
48547
|
+
\`\`\`bash
|
|
48548
|
+
# When work is complete
|
|
48549
|
+
fulcrum notify "Task Complete" "Implemented the new feature and created PR #123"
|
|
48550
|
+
|
|
48551
|
+
# When blocked or need input
|
|
48552
|
+
fulcrum notify "Need Input" "Which approach should I use for the database migration?"
|
|
48553
|
+
\`\`\`
|
|
48554
|
+
|
|
48555
|
+
## Global Options
|
|
48556
|
+
|
|
48557
|
+
These flags work with most commands:
|
|
48558
|
+
|
|
48559
|
+
- \`--port=<port>\` - Server port (default: 7777)
|
|
48560
|
+
- \`--url=<url>\` - Override full server URL
|
|
48561
|
+
- \`--json\` - Output as JSON for programmatic use
|
|
48562
|
+
|
|
48563
|
+
## Task Statuses
|
|
48564
|
+
|
|
48565
|
+
- \`TO_DO\` - Task not yet started
|
|
48566
|
+
- \`IN_PROGRESS\` - Task is being worked on
|
|
48567
|
+
- \`IN_REVIEW\` - Task is complete and awaiting review
|
|
48568
|
+
- \`DONE\` - Task is finished
|
|
48569
|
+
- \`CANCELED\` - Task was abandoned
|
|
48570
|
+
|
|
48571
|
+
## MCP Tools
|
|
48572
|
+
|
|
48573
|
+
Fulcrum provides a comprehensive set of MCP tools for AI agents. Use \`search_tools\` to discover available tools.
|
|
48574
|
+
|
|
48575
|
+
### Tool Discovery
|
|
48576
|
+
|
|
48577
|
+
#### search_tools
|
|
48578
|
+
|
|
48579
|
+
Search for available tools by keyword or category:
|
|
48580
|
+
|
|
48581
|
+
\`\`\`json
|
|
48582
|
+
{
|
|
48583
|
+
"query": "deploy", // Optional: Search term
|
|
48584
|
+
"category": "apps" // Optional: Filter by category
|
|
48585
|
+
}
|
|
48586
|
+
\`\`\`
|
|
48587
|
+
|
|
48588
|
+
**Categories:** core, tasks, projects, repositories, apps, filesystem, git, notifications, exec
|
|
48589
|
+
|
|
48590
|
+
**Example Usage:**
|
|
48591
|
+
\`\`\`
|
|
48592
|
+
search_tools { query: "project create" }
|
|
48593
|
+
\u2192 Returns tools for creating projects
|
|
48594
|
+
|
|
48595
|
+
search_tools { category: "filesystem" }
|
|
48596
|
+
\u2192 Returns all filesystem tools
|
|
48597
|
+
\`\`\`
|
|
48598
|
+
|
|
48599
|
+
### Task Tools
|
|
48600
|
+
|
|
48601
|
+
- \`list_tasks\` - List tasks with flexible filtering (search, labels, statuses, date range, overdue)
|
|
48602
|
+
- \`get_task\` - Get task details by ID (includes dependencies and attachments)
|
|
48603
|
+
- \`create_task\` - Create a new task with worktree
|
|
48604
|
+
- \`update_task\` - Update task metadata
|
|
48605
|
+
- \`delete_task\` - Delete a task
|
|
48606
|
+
- \`move_task\` - Move task to different status
|
|
48607
|
+
- \`add_task_link\` - Add URL link to task
|
|
48608
|
+
- \`remove_task_link\` - Remove link from task
|
|
48609
|
+
- \`list_task_links\` - List all task links
|
|
48610
|
+
- \`add_task_tag\` - Add a label to a task (returns similar labels to catch typos)
|
|
48611
|
+
- \`remove_task_tag\` - Remove a label from a task
|
|
48612
|
+
- \`set_task_due_date\` - Set or clear task due date
|
|
48613
|
+
- \`list_tags\` - List all unique labels in use with optional search
|
|
48614
|
+
|
|
48615
|
+
#### Task Dependencies
|
|
48616
|
+
|
|
48617
|
+
- \`get_task_dependencies\` - Get dependencies and dependents of a task, and whether it is blocked
|
|
48618
|
+
- \`add_task_dependency\` - Add a dependency (task cannot start until dependency is done)
|
|
48619
|
+
- \`remove_task_dependency\` - Remove a dependency
|
|
48620
|
+
- \`get_task_dependency_graph\` - Get all tasks and dependencies as a graph for visualization
|
|
48621
|
+
|
|
48622
|
+
#### Task Attachments
|
|
48623
|
+
|
|
48624
|
+
- \`list_task_attachments\` - List all file attachments for a task
|
|
48625
|
+
- \`upload_task_attachment\` - Upload a file to a task from a local path
|
|
48626
|
+
- \`delete_task_attachment\` - Delete a file attachment from a task
|
|
48627
|
+
- \`get_task_attachment_path\` - Get the local file path for a task attachment
|
|
48628
|
+
|
|
48629
|
+
#### Task Search and Filtering
|
|
48630
|
+
|
|
48631
|
+
The \`list_tasks\` tool supports powerful filtering for AI agents:
|
|
48632
|
+
|
|
48633
|
+
\`\`\`json
|
|
48634
|
+
{
|
|
48635
|
+
"search": "ocai", // Text search across title, labels, project name
|
|
48636
|
+
"labels": ["bug", "urgent"], // Filter by multiple labels (OR logic)
|
|
48637
|
+
"statuses": ["TO_DO", "IN_PROGRESS"], // Filter by multiple statuses (OR logic)
|
|
48638
|
+
"dueDateStart": "2026-01-18", // Start of date range
|
|
48639
|
+
"dueDateEnd": "2026-01-25", // End of date range
|
|
48640
|
+
"overdue": true // Only show overdue tasks
|
|
48641
|
+
}
|
|
48642
|
+
\`\`\`
|
|
48643
|
+
|
|
48644
|
+
#### Label Discovery
|
|
48645
|
+
|
|
48646
|
+
Use \`list_labels\` to discover exact label names before filtering:
|
|
48647
|
+
|
|
48648
|
+
\`\`\`json
|
|
48649
|
+
// Find labels matching "communication"
|
|
48650
|
+
{ "search": "communication" }
|
|
48651
|
+
// Returns: [{ "name": "communication required", "count": 5 }]
|
|
48652
|
+
\`\`\`
|
|
48653
|
+
|
|
48654
|
+
This helps handle typos and variations - search first, then use the exact label name.
|
|
48655
|
+
|
|
48656
|
+
### Project Tools
|
|
48657
|
+
|
|
48658
|
+
- \`list_projects\` - List all projects
|
|
48659
|
+
- \`get_project\` - Get project details
|
|
48660
|
+
- \`create_project\` - Create from path, URL, or existing repo
|
|
48661
|
+
- \`update_project\` - Update name, description, status
|
|
48662
|
+
- \`delete_project\` - Delete project and optionally directory/app
|
|
48663
|
+
- \`scan_projects\` - Scan directory for git repos
|
|
48664
|
+
|
|
48665
|
+
#### Project Tags
|
|
48666
|
+
|
|
48667
|
+
- \`add_project_tag\` - Add a tag to a project (creates new if needed)
|
|
48668
|
+
- \`remove_project_tag\` - Remove a tag from a project
|
|
48669
|
+
|
|
48670
|
+
#### Project Attachments
|
|
48671
|
+
|
|
48672
|
+
- \`list_project_attachments\` - List all file attachments for a project
|
|
48673
|
+
- \`upload_project_attachment\` - Upload a file to a project from a local path
|
|
48674
|
+
- \`delete_project_attachment\` - Delete a file attachment from a project
|
|
48675
|
+
- \`get_project_attachment_path\` - Get the local file path for a project attachment
|
|
48676
|
+
|
|
48677
|
+
#### Project Links
|
|
48678
|
+
|
|
48679
|
+
- \`list_project_links\` - List all URL links attached to a project
|
|
48680
|
+
- \`add_project_link\` - Add a URL link to a project (auto-detects type)
|
|
48681
|
+
- \`remove_project_link\` - Remove a URL link from a project
|
|
48682
|
+
|
|
48683
|
+
### Repository Tools
|
|
48684
|
+
|
|
48685
|
+
- \`list_repositories\` - List all repositories (supports orphans filter)
|
|
48686
|
+
- \`get_repository\` - Get repository details by ID
|
|
48687
|
+
- \`add_repository\` - Add repository from local path
|
|
48688
|
+
- \`update_repository\` - Update repository metadata (name, agent, startup script)
|
|
48689
|
+
- \`delete_repository\` - Delete orphaned repository (fails if linked to project)
|
|
48690
|
+
- \`link_repository_to_project\` - Link repo to project (errors if already linked elsewhere)
|
|
48691
|
+
- \`unlink_repository_from_project\` - Unlink repo from project
|
|
48692
|
+
|
|
48693
|
+
### App/Deployment Tools
|
|
48694
|
+
|
|
48695
|
+
- \`list_apps\` - List all deployed apps
|
|
48696
|
+
- \`get_app\` - Get app details with services
|
|
48697
|
+
- \`create_app\` - Create app for deployment
|
|
48698
|
+
- \`deploy_app\` - Trigger deployment
|
|
48699
|
+
- \`stop_app\` - Stop running app
|
|
48700
|
+
- \`get_app_logs\` - Get container logs
|
|
48701
|
+
- \`get_app_status\` - Get container status
|
|
48702
|
+
- \`list_deployments\` - Get deployment history
|
|
48703
|
+
- \`delete_app\` - Delete app
|
|
48704
|
+
|
|
48705
|
+
### Filesystem Tools
|
|
48706
|
+
|
|
48707
|
+
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).
|
|
48708
|
+
|
|
48709
|
+
- \`list_directory\` - List directory contents
|
|
48710
|
+
- \`get_file_tree\` - Get recursive file tree
|
|
48711
|
+
- \`read_file\` - Read file contents (secured)
|
|
48712
|
+
- \`write_file\` - Write entire file content (secured)
|
|
48713
|
+
- \`edit_file\` - Edit file by replacing a unique string (secured)
|
|
48714
|
+
- \`file_stat\` - Get file/directory metadata
|
|
48715
|
+
- \`is_git_repo\` - Check if directory is git repo
|
|
48716
|
+
|
|
48717
|
+
### Command Execution
|
|
48718
|
+
|
|
48719
|
+
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.
|
|
48720
|
+
|
|
48721
|
+
#### execute_command
|
|
48722
|
+
|
|
48723
|
+
Execute shell commands with optional persistent session support:
|
|
48724
|
+
|
|
48725
|
+
\`\`\`json
|
|
48726
|
+
{
|
|
48727
|
+
"command": "echo hello world",
|
|
48728
|
+
"sessionId": "optional-session-id",
|
|
48729
|
+
"cwd": "/path/to/start",
|
|
48730
|
+
"timeout": 30000,
|
|
48731
|
+
"name": "my-session"
|
|
48732
|
+
}
|
|
48733
|
+
\`\`\`
|
|
48734
|
+
|
|
48735
|
+
**Parameters:**
|
|
48736
|
+
- \`command\` (required) \u2014 The shell command to execute
|
|
48737
|
+
- \`sessionId\` (optional) \u2014 Reuse a session to preserve env vars, cwd, and shell state
|
|
48738
|
+
- \`cwd\` (optional) \u2014 Initial working directory (only used when creating new session)
|
|
48739
|
+
- \`timeout\` (optional) \u2014 Timeout in milliseconds (default: 30000)
|
|
48740
|
+
- \`name\` (optional) \u2014 Session name for identification (only used when creating new session)
|
|
48741
|
+
|
|
48742
|
+
**Response:**
|
|
48743
|
+
\`\`\`json
|
|
48744
|
+
{
|
|
48745
|
+
"sessionId": "uuid",
|
|
48746
|
+
"stdout": "hello world",
|
|
48747
|
+
"stderr": "",
|
|
48748
|
+
"exitCode": 0,
|
|
48749
|
+
"timedOut": false
|
|
48750
|
+
}
|
|
48751
|
+
\`\`\`
|
|
48752
|
+
|
|
48753
|
+
### Session Workflow Example
|
|
48754
|
+
|
|
48755
|
+
\`\`\`
|
|
48756
|
+
1. First command (creates named session):
|
|
48757
|
+
execute_command { command: "cd /project && export API_KEY=secret", name: "dev-session" }
|
|
48758
|
+
\u2192 Returns sessionId: "abc-123"
|
|
48759
|
+
|
|
48760
|
+
2. Subsequent commands (reuse session):
|
|
48761
|
+
execute_command { command: "echo $API_KEY", sessionId: "abc-123" }
|
|
48762
|
+
\u2192 Returns stdout: "secret" (env var preserved)
|
|
48763
|
+
|
|
48764
|
+
execute_command { command: "pwd", sessionId: "abc-123" }
|
|
48765
|
+
\u2192 Returns stdout: "/project" (cwd preserved)
|
|
48766
|
+
|
|
48767
|
+
3. Rename session if needed:
|
|
48768
|
+
update_exec_session { sessionId: "abc-123", name: "new-name" }
|
|
48769
|
+
|
|
48770
|
+
4. Cleanup when done:
|
|
48771
|
+
destroy_exec_session { sessionId: "abc-123" }
|
|
48772
|
+
\`\`\`
|
|
48773
|
+
|
|
48774
|
+
Sessions persist until manually destroyed.
|
|
48775
|
+
|
|
48776
|
+
### list_exec_sessions
|
|
48777
|
+
|
|
48778
|
+
List all active sessions with their name, current working directory, and timestamps.
|
|
48779
|
+
|
|
48780
|
+
### update_exec_session
|
|
48781
|
+
|
|
48782
|
+
Rename an existing session for identification.
|
|
48783
|
+
|
|
48784
|
+
### destroy_exec_session
|
|
49320
48785
|
|
|
49321
|
-
|
|
49322
|
-
# After creating a GitHub PR
|
|
49323
|
-
fulcrum current-task pr https://github.com/owner/repo/pull/123
|
|
48786
|
+
Clean up a session when you're done to free resources.
|
|
49324
48787
|
|
|
49325
|
-
|
|
49326
|
-
fulcrum current-task linear https://linear.app/team/issue/TEAM-123
|
|
48788
|
+
## Best Practices
|
|
49327
48789
|
|
|
49328
|
-
|
|
49329
|
-
fulcrum current-task
|
|
49330
|
-
fulcrum current-task link
|
|
49331
|
-
|
|
48790
|
+
1. **Use \`current-task\` inside worktrees** - It auto-detects which task you're in
|
|
48791
|
+
2. **Link PRs immediately** - Run \`fulcrum current-task pr <url>\` right after creating a PR
|
|
48792
|
+
3. **Link relevant resources** - Attach design docs, specs, or reference materials with \`fulcrum current-task link <url>\`
|
|
48793
|
+
4. **Mark review when done** - \`fulcrum current-task review\` notifies the user
|
|
48794
|
+
5. **Send notifications for blocking issues** - Keep the user informed of progress
|
|
48795
|
+
6. **Name sessions for identification** - Use descriptive names to find sessions later
|
|
48796
|
+
7. **Reuse sessions for related commands** - Preserve state across multiple execute_command calls
|
|
48797
|
+
8. **Clean up sessions when done** - Use destroy_exec_session to free resources
|
|
48798
|
+
`;
|
|
49332
48799
|
|
|
49333
|
-
|
|
48800
|
+
// cli/src/commands/claude.ts
|
|
48801
|
+
var MARKETPLACE_DIR = join3(homedir2(), ".fulcrum", "claude-plugin");
|
|
48802
|
+
var MARKETPLACE_NAME = "fulcrum";
|
|
48803
|
+
var PLUGIN_NAME = "fulcrum";
|
|
48804
|
+
var PLUGIN_ID = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
|
|
48805
|
+
var PLUGIN_FILES = [
|
|
48806
|
+
{ path: ".claude-plugin/marketplace.json", content: marketplace_default },
|
|
48807
|
+
{ path: "hooks/hooks.json", content: hooks_default },
|
|
48808
|
+
{ path: ".mcp.json", content: _mcp_default },
|
|
48809
|
+
{ path: "commands/pr.md", content: pr_default },
|
|
48810
|
+
{ path: "commands/task-info.md", content: task_info_default },
|
|
48811
|
+
{ path: "commands/notify.md", content: notify_default },
|
|
48812
|
+
{ path: "skills/fulcrum/SKILL.md", content: SKILL_default }
|
|
48813
|
+
];
|
|
48814
|
+
var LEGACY_PLUGIN_DIR = join3(homedir2(), ".claude", "plugins", "fulcrum");
|
|
48815
|
+
var LEGACY_CACHE_DIR = join3(homedir2(), ".claude", "plugins", "cache", "fulcrum");
|
|
48816
|
+
function runClaude(args) {
|
|
48817
|
+
const result = spawnSync2("claude", args, { encoding: "utf-8" });
|
|
48818
|
+
return {
|
|
48819
|
+
success: result.status === 0,
|
|
48820
|
+
output: (result.stdout || "") + (result.stderr || "")
|
|
48821
|
+
};
|
|
48822
|
+
}
|
|
48823
|
+
function getBundledVersion() {
|
|
48824
|
+
try {
|
|
48825
|
+
const parsed = JSON.parse(marketplace_default);
|
|
48826
|
+
return parsed.plugins?.[0]?.version || "1.0.0";
|
|
48827
|
+
} catch {
|
|
48828
|
+
return "1.0.0";
|
|
48829
|
+
}
|
|
48830
|
+
}
|
|
48831
|
+
function getInstalledVersion() {
|
|
48832
|
+
const installedMarketplace = join3(MARKETPLACE_DIR, ".claude-plugin", "marketplace.json");
|
|
48833
|
+
if (!existsSync3(installedMarketplace)) {
|
|
48834
|
+
return null;
|
|
48835
|
+
}
|
|
48836
|
+
try {
|
|
48837
|
+
const installed = JSON.parse(readFileSync4(installedMarketplace, "utf-8"));
|
|
48838
|
+
return installed.plugins?.[0]?.version || null;
|
|
48839
|
+
} catch {
|
|
48840
|
+
return null;
|
|
48841
|
+
}
|
|
48842
|
+
}
|
|
48843
|
+
async function handleClaudeCommand(action) {
|
|
48844
|
+
if (action === "install") {
|
|
48845
|
+
await installClaudePlugin();
|
|
48846
|
+
return;
|
|
48847
|
+
}
|
|
48848
|
+
if (action === "uninstall") {
|
|
48849
|
+
await uninstallClaudePlugin();
|
|
48850
|
+
return;
|
|
48851
|
+
}
|
|
48852
|
+
throw new CliError("INVALID_ACTION", "Unknown action. Usage: fulcrum claude install | fulcrum claude uninstall", ExitCodes.INVALID_ARGS);
|
|
48853
|
+
}
|
|
48854
|
+
function needsPluginUpdate() {
|
|
48855
|
+
const installedVersion = getInstalledVersion();
|
|
48856
|
+
if (!installedVersion) {
|
|
48857
|
+
return true;
|
|
48858
|
+
}
|
|
48859
|
+
const bundledVersion = getBundledVersion();
|
|
48860
|
+
return installedVersion !== bundledVersion;
|
|
48861
|
+
}
|
|
48862
|
+
async function installClaudePlugin(options = {}) {
|
|
48863
|
+
const { silent = false } = options;
|
|
48864
|
+
const log = silent ? () => {} : console.log;
|
|
48865
|
+
try {
|
|
48866
|
+
log("Installing Claude Code plugin...");
|
|
48867
|
+
if (existsSync3(MARKETPLACE_DIR)) {
|
|
48868
|
+
rmSync(MARKETPLACE_DIR, { recursive: true });
|
|
48869
|
+
}
|
|
48870
|
+
for (const file2 of PLUGIN_FILES) {
|
|
48871
|
+
const fullPath = join3(MARKETPLACE_DIR, file2.path);
|
|
48872
|
+
mkdirSync3(dirname2(fullPath), { recursive: true });
|
|
48873
|
+
writeFileSync3(fullPath, file2.content, "utf-8");
|
|
48874
|
+
}
|
|
48875
|
+
log("\u2713 Created plugin files at " + MARKETPLACE_DIR);
|
|
48876
|
+
runClaude(["plugin", "marketplace", "remove", MARKETPLACE_NAME]);
|
|
48877
|
+
const addResult = runClaude(["plugin", "marketplace", "add", MARKETPLACE_DIR]);
|
|
48878
|
+
if (!addResult.success) {
|
|
48879
|
+
throw new Error("Failed to add marketplace: " + addResult.output);
|
|
48880
|
+
}
|
|
48881
|
+
log("\u2713 Registered marketplace");
|
|
48882
|
+
const installResult = runClaude(["plugin", "install", PLUGIN_ID, "--scope", "user"]);
|
|
48883
|
+
if (!installResult.success) {
|
|
48884
|
+
throw new Error("Failed to install plugin: " + installResult.output);
|
|
48885
|
+
}
|
|
48886
|
+
log("\u2713 Installed plugin");
|
|
48887
|
+
cleanupLegacyPaths(log);
|
|
48888
|
+
log("");
|
|
48889
|
+
log("Installation complete! Restart Claude Code to apply changes.");
|
|
48890
|
+
} catch (err) {
|
|
48891
|
+
throw new CliError("INSTALL_FAILED", `Failed to install Claude plugin: ${err instanceof Error ? err.message : String(err)}`, ExitCodes.ERROR);
|
|
48892
|
+
}
|
|
48893
|
+
}
|
|
48894
|
+
async function uninstallClaudePlugin() {
|
|
48895
|
+
try {
|
|
48896
|
+
runClaude(["plugin", "uninstall", PLUGIN_ID]);
|
|
48897
|
+
console.log("\u2713 Uninstalled plugin");
|
|
48898
|
+
runClaude(["plugin", "marketplace", "remove", MARKETPLACE_NAME]);
|
|
48899
|
+
console.log("\u2713 Removed marketplace");
|
|
48900
|
+
if (existsSync3(MARKETPLACE_DIR)) {
|
|
48901
|
+
rmSync(MARKETPLACE_DIR, { recursive: true });
|
|
48902
|
+
console.log("\u2713 Removed plugin files from " + MARKETPLACE_DIR);
|
|
48903
|
+
}
|
|
48904
|
+
cleanupLegacyPaths(console.log);
|
|
48905
|
+
console.log("");
|
|
48906
|
+
console.log("Uninstall complete! Restart Claude Code to apply changes.");
|
|
48907
|
+
} catch (err) {
|
|
48908
|
+
throw new CliError("UNINSTALL_FAILED", `Failed to uninstall Claude plugin: ${err instanceof Error ? err.message : String(err)}`, ExitCodes.ERROR);
|
|
48909
|
+
}
|
|
48910
|
+
}
|
|
48911
|
+
function cleanupLegacyPaths(log) {
|
|
48912
|
+
const legacyPaths = [LEGACY_PLUGIN_DIR, LEGACY_CACHE_DIR];
|
|
48913
|
+
for (const path of legacyPaths) {
|
|
48914
|
+
if (existsSync3(path)) {
|
|
48915
|
+
rmSync(path, { recursive: true });
|
|
48916
|
+
log("\u2713 Removed legacy files from " + path);
|
|
48917
|
+
}
|
|
48918
|
+
}
|
|
48919
|
+
}
|
|
48920
|
+
// package.json
|
|
48921
|
+
var package_default = {
|
|
48922
|
+
name: "@knowsuchagency/fulcrum",
|
|
48923
|
+
private: true,
|
|
48924
|
+
version: "1.5.0",
|
|
48925
|
+
description: "Harness Attention. Orchestrate Agents. Ship.",
|
|
48926
|
+
license: "PolyForm-Perimeter-1.0.0",
|
|
48927
|
+
type: "module",
|
|
48928
|
+
scripts: {
|
|
48929
|
+
dev: "vite --host",
|
|
48930
|
+
"dev:server": "mkdir -p ~/.fulcrum && bun --watch server/index.ts",
|
|
48931
|
+
build: "tsc -b && vite build",
|
|
48932
|
+
start: "NODE_ENV=production bun server/index.ts",
|
|
48933
|
+
lint: "eslint .",
|
|
48934
|
+
preview: "vite preview",
|
|
48935
|
+
"db:generate": "drizzle-kit generate",
|
|
48936
|
+
"db:migrate": "drizzle-kit migrate",
|
|
48937
|
+
"db:studio": "drizzle-kit studio"
|
|
48938
|
+
},
|
|
48939
|
+
dependencies: {
|
|
48940
|
+
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
|
48941
|
+
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
|
|
48942
|
+
"@azurity/pure-nerd-font": "^3.0.5",
|
|
48943
|
+
"@base-ui/react": "^1.0.0",
|
|
48944
|
+
"@dagrejs/dagre": "^1.1.8",
|
|
48945
|
+
"@fontsource-variable/jetbrains-mono": "^5.2.8",
|
|
48946
|
+
"@hono/node-server": "^1.19.7",
|
|
48947
|
+
"@hono/node-ws": "^1.2.0",
|
|
48948
|
+
"@hugeicons/core-free-icons": "^3.0.0",
|
|
48949
|
+
"@hugeicons/react": "^1.1.3",
|
|
48950
|
+
"@monaco-editor/react": "^4.7.0",
|
|
48951
|
+
"@octokit/rest": "^22.0.1",
|
|
48952
|
+
"@radix-ui/react-collapsible": "^1.1.12",
|
|
48953
|
+
"@tailwindcss/vite": "^4.1.17",
|
|
48954
|
+
"@tanstack/react-query": "^5.90.12",
|
|
48955
|
+
"@tanstack/react-router": "^1.141.8",
|
|
48956
|
+
"@uiw/react-markdown-preview": "^5.1.5",
|
|
48957
|
+
"@xterm/addon-clipboard": "^0.2.0",
|
|
48958
|
+
"@xterm/addon-fit": "^0.10.0",
|
|
48959
|
+
"@xterm/addon-web-links": "^0.11.0",
|
|
48960
|
+
"@xterm/xterm": "^5.5.0",
|
|
48961
|
+
"bun-pty": "^0.4.2",
|
|
48962
|
+
citty: "^0.1.6",
|
|
48963
|
+
"class-variance-authority": "^0.7.1",
|
|
48964
|
+
cloudflare: "^5.2.0",
|
|
48965
|
+
clsx: "^2.1.1",
|
|
48966
|
+
"date-fns": "^4.1.0",
|
|
48967
|
+
"drizzle-orm": "^0.45.1",
|
|
48968
|
+
"fancy-ansi": "^0.1.3",
|
|
48969
|
+
glob: "^13.0.0",
|
|
48970
|
+
hono: "^4.11.1",
|
|
48971
|
+
i18next: "^25.7.3",
|
|
48972
|
+
mobx: "^6.15.0",
|
|
48973
|
+
"mobx-react-lite": "^4.1.1",
|
|
48974
|
+
"mobx-state-tree": "^7.0.2",
|
|
48975
|
+
"next-themes": "^0.4.6",
|
|
48976
|
+
react: "^19.2.0",
|
|
48977
|
+
"react-day-picker": "^9.13.0",
|
|
48978
|
+
"react-dom": "^19.2.0",
|
|
48979
|
+
"react-i18next": "^16.5.0",
|
|
48980
|
+
"react-resizable-panels": "^4.0.11",
|
|
48981
|
+
reactflow: "^11.11.4",
|
|
48982
|
+
recharts: "2.15.4",
|
|
48983
|
+
shadcn: "^3.6.2",
|
|
48984
|
+
shiki: "^3.20.0",
|
|
48985
|
+
sonner: "^2.0.7",
|
|
48986
|
+
"tailwind-merge": "^3.4.0",
|
|
48987
|
+
tailwindcss: "^4.1.17",
|
|
48988
|
+
"tw-animate-css": "^1.4.0",
|
|
48989
|
+
ws: "^8.18.3",
|
|
48990
|
+
yaml: "^2.8.2"
|
|
48991
|
+
},
|
|
48992
|
+
devDependencies: {
|
|
48993
|
+
"@eslint/js": "^9.39.1",
|
|
48994
|
+
"@opencode-ai/plugin": "^1.1.8",
|
|
48995
|
+
"@tailwindcss/typography": "^0.5.19",
|
|
48996
|
+
"@tanstack/router-plugin": "^1.141.8",
|
|
48997
|
+
"@types/bun": "^1.2.14",
|
|
48998
|
+
"@types/node": "^24.10.1",
|
|
48999
|
+
"@types/react": "^19.2.5",
|
|
49000
|
+
"@types/react-dom": "^19.2.3",
|
|
49001
|
+
"@types/ws": "^8.18.1",
|
|
49002
|
+
"@vitejs/plugin-react": "^5.1.1",
|
|
49003
|
+
"drizzle-kit": "^0.31.8",
|
|
49004
|
+
eslint: "^9.39.1",
|
|
49005
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
49006
|
+
"eslint-plugin-react-refresh": "^0.4.24",
|
|
49007
|
+
globals: "^16.5.0",
|
|
49008
|
+
typescript: "~5.9.3",
|
|
49009
|
+
"typescript-eslint": "^8.46.4",
|
|
49010
|
+
vite: "^7.2.4"
|
|
49011
|
+
}
|
|
49012
|
+
};
|
|
49334
49013
|
|
|
49335
|
-
|
|
49336
|
-
|
|
49337
|
-
|
|
49014
|
+
// cli/src/commands/up.ts
|
|
49015
|
+
function getPackageRoot() {
|
|
49016
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
49017
|
+
let dir = dirname3(currentFile);
|
|
49018
|
+
for (let i2 = 0;i2 < 5; i2++) {
|
|
49019
|
+
if (existsSync4(join4(dir, "server", "index.js"))) {
|
|
49020
|
+
return dir;
|
|
49021
|
+
}
|
|
49022
|
+
dir = dirname3(dir);
|
|
49023
|
+
}
|
|
49024
|
+
return dirname3(dirname3(dirname3(currentFile)));
|
|
49025
|
+
}
|
|
49026
|
+
async function handleUpCommand(flags) {
|
|
49027
|
+
const autoYes = flags.yes === "true" || flags.y === "true";
|
|
49028
|
+
if (needsViboraMigration()) {
|
|
49029
|
+
const viboraDir = getLegacyViboraDir();
|
|
49030
|
+
console.error(`
|
|
49031
|
+
Found existing Vibora data at ${viboraDir}`);
|
|
49032
|
+
console.error('Run "fulcrum migrate-from-vibora" to copy your data to ~/.fulcrum');
|
|
49033
|
+
console.error("");
|
|
49034
|
+
}
|
|
49035
|
+
if (!isBunInstalled()) {
|
|
49036
|
+
const bunDep = getDependency("bun");
|
|
49037
|
+
const method = getInstallMethod(bunDep);
|
|
49038
|
+
console.error("Bun is required to run Fulcrum but is not installed.");
|
|
49039
|
+
console.error(" Bun is the JavaScript runtime that powers Fulcrum.");
|
|
49040
|
+
const shouldInstall = autoYes || await confirm(`Would you like to install bun via ${method}?`);
|
|
49041
|
+
if (shouldInstall) {
|
|
49042
|
+
const success2 = installBun();
|
|
49043
|
+
if (!success2) {
|
|
49044
|
+
throw new CliError("INSTALL_FAILED", "Failed to install bun", ExitCodes.ERROR);
|
|
49045
|
+
}
|
|
49046
|
+
console.error("Bun installed successfully!");
|
|
49047
|
+
} else {
|
|
49048
|
+
throw new CliError("MISSING_DEPENDENCY", `Bun is required. Install manually: ${getInstallCommand(bunDep)}`, ExitCodes.ERROR);
|
|
49049
|
+
}
|
|
49050
|
+
}
|
|
49051
|
+
if (!isDtachInstalled()) {
|
|
49052
|
+
const dtachDep = getDependency("dtach");
|
|
49053
|
+
const method = getInstallMethod(dtachDep);
|
|
49054
|
+
console.error("dtach is required for terminal persistence but is not installed.");
|
|
49055
|
+
console.error(" dtach enables persistent terminal sessions that survive disconnects.");
|
|
49056
|
+
const shouldInstall = autoYes || await confirm(`Would you like to install dtach via ${method}?`);
|
|
49057
|
+
if (shouldInstall) {
|
|
49058
|
+
const success2 = installDtach();
|
|
49059
|
+
if (!success2) {
|
|
49060
|
+
throw new CliError("INSTALL_FAILED", "Failed to install dtach", ExitCodes.ERROR);
|
|
49061
|
+
}
|
|
49062
|
+
console.error("dtach installed successfully!");
|
|
49063
|
+
} else {
|
|
49064
|
+
throw new CliError("MISSING_DEPENDENCY", `dtach is required. Install manually: ${getInstallCommand(dtachDep)}`, ExitCodes.ERROR);
|
|
49065
|
+
}
|
|
49066
|
+
}
|
|
49067
|
+
if (!isUvInstalled()) {
|
|
49068
|
+
const uvDep = getDependency("uv");
|
|
49069
|
+
const method = getInstallMethod(uvDep);
|
|
49070
|
+
console.error("uv is required but is not installed.");
|
|
49071
|
+
console.error(" uv is a fast Python package manager used by Claude Code.");
|
|
49072
|
+
const shouldInstall = autoYes || await confirm(`Would you like to install uv via ${method}?`);
|
|
49073
|
+
if (shouldInstall) {
|
|
49074
|
+
const success2 = installUv();
|
|
49075
|
+
if (!success2) {
|
|
49076
|
+
throw new CliError("INSTALL_FAILED", "Failed to install uv", ExitCodes.ERROR);
|
|
49077
|
+
}
|
|
49078
|
+
console.error("uv installed successfully!");
|
|
49079
|
+
} else {
|
|
49080
|
+
throw new CliError("MISSING_DEPENDENCY", `uv is required. Install manually: ${getInstallCommand(uvDep)}`, ExitCodes.ERROR);
|
|
49081
|
+
}
|
|
49082
|
+
}
|
|
49083
|
+
if (isClaudeInstalled() && needsPluginUpdate()) {
|
|
49084
|
+
console.error("Updating Fulcrum plugin for Claude Code...");
|
|
49085
|
+
await installClaudePlugin({ silent: true });
|
|
49086
|
+
console.error("\u2713 Fulcrum plugin updated");
|
|
49087
|
+
}
|
|
49088
|
+
const existingPid = readPid();
|
|
49089
|
+
if (existingPid && isProcessRunning(existingPid)) {
|
|
49090
|
+
console.error(`Fulcrum server is already running (PID: ${existingPid})`);
|
|
49091
|
+
const shouldReplace = autoYes || await confirm("Would you like to stop it and start a new instance?");
|
|
49092
|
+
if (shouldReplace) {
|
|
49093
|
+
console.error("Stopping existing instance...");
|
|
49094
|
+
process.kill(existingPid, "SIGTERM");
|
|
49095
|
+
let attempts = 0;
|
|
49096
|
+
while (attempts < 50 && isProcessRunning(existingPid)) {
|
|
49097
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
49098
|
+
attempts++;
|
|
49099
|
+
}
|
|
49100
|
+
if (isProcessRunning(existingPid)) {
|
|
49101
|
+
process.kill(existingPid, "SIGKILL");
|
|
49102
|
+
}
|
|
49103
|
+
removePid();
|
|
49104
|
+
console.error("Existing instance stopped.");
|
|
49105
|
+
} else {
|
|
49106
|
+
throw new CliError("ALREADY_RUNNING", `Server already running at http://localhost:${getPort(flags.port)}`, ExitCodes.ERROR);
|
|
49107
|
+
}
|
|
49108
|
+
}
|
|
49109
|
+
const port = getPort(flags.port);
|
|
49110
|
+
if (flags.port) {
|
|
49111
|
+
updateSettingsPort(port);
|
|
49112
|
+
}
|
|
49113
|
+
const host = flags.host ? "0.0.0.0" : "localhost";
|
|
49114
|
+
const packageRoot = getPackageRoot();
|
|
49115
|
+
const serverPath = join4(packageRoot, "server", "index.js");
|
|
49116
|
+
const platform2 = process.platform;
|
|
49117
|
+
const arch = process.arch;
|
|
49118
|
+
let ptyLibName;
|
|
49119
|
+
if (platform2 === "darwin") {
|
|
49120
|
+
ptyLibName = arch === "arm64" ? "librust_pty_arm64.dylib" : "librust_pty.dylib";
|
|
49121
|
+
} else if (platform2 === "win32") {
|
|
49122
|
+
ptyLibName = "rust_pty.dll";
|
|
49123
|
+
} else {
|
|
49124
|
+
ptyLibName = arch === "arm64" ? "librust_pty_arm64.so" : "librust_pty.so";
|
|
49125
|
+
}
|
|
49126
|
+
const ptyLibPath = join4(packageRoot, "lib", ptyLibName);
|
|
49127
|
+
const fulcrumDir = getFulcrumDir();
|
|
49128
|
+
const debug = flags.debug === "true";
|
|
49129
|
+
console.error(`Starting Fulcrum server${debug ? " (debug mode)" : ""}...`);
|
|
49130
|
+
const serverProc = spawn("bun", [serverPath], {
|
|
49131
|
+
detached: true,
|
|
49132
|
+
stdio: "ignore",
|
|
49133
|
+
env: {
|
|
49134
|
+
...process.env,
|
|
49135
|
+
NODE_ENV: "production",
|
|
49136
|
+
PORT: port.toString(),
|
|
49137
|
+
HOST: host,
|
|
49138
|
+
FULCRUM_DIR: fulcrumDir,
|
|
49139
|
+
FULCRUM_PACKAGE_ROOT: packageRoot,
|
|
49140
|
+
FULCRUM_VERSION: package_default.version,
|
|
49141
|
+
BUN_PTY_LIB: ptyLibPath,
|
|
49142
|
+
...isClaudeInstalled() && { FULCRUM_CLAUDE_INSTALLED: "1" },
|
|
49143
|
+
...isOpencodeInstalled() && { FULCRUM_OPENCODE_INSTALLED: "1" },
|
|
49144
|
+
...debug && { LOG_LEVEL: "debug", DEBUG: "1" }
|
|
49145
|
+
}
|
|
49146
|
+
});
|
|
49147
|
+
serverProc.unref();
|
|
49148
|
+
const pid = serverProc.pid;
|
|
49149
|
+
if (!pid) {
|
|
49150
|
+
throw new CliError("START_FAILED", "Failed to start server process", ExitCodes.ERROR);
|
|
49151
|
+
}
|
|
49152
|
+
writePid(pid);
|
|
49153
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
49154
|
+
if (!isProcessRunning(pid)) {
|
|
49155
|
+
throw new CliError("START_FAILED", "Server process died immediately after starting", ExitCodes.ERROR);
|
|
49156
|
+
}
|
|
49157
|
+
if (isJsonOutput()) {
|
|
49158
|
+
output({
|
|
49159
|
+
pid,
|
|
49160
|
+
port,
|
|
49161
|
+
url: `http://localhost:${port}`
|
|
49162
|
+
});
|
|
49163
|
+
} else {
|
|
49164
|
+
const hasAgent = isClaudeInstalled() || isOpencodeInstalled();
|
|
49165
|
+
showGettingStartedTips(port, hasAgent);
|
|
49166
|
+
}
|
|
49167
|
+
}
|
|
49168
|
+
function showGettingStartedTips(port, hasAgent) {
|
|
49169
|
+
console.error(`
|
|
49170
|
+
Fulcrum is running at http://localhost:${port}
|
|
49338
49171
|
|
|
49339
|
-
|
|
49340
|
-
|
|
49341
|
-
|
|
49172
|
+
Getting Started:
|
|
49173
|
+
1. Open http://localhost:${port} in your browser
|
|
49174
|
+
2. Add a repository to get started
|
|
49175
|
+
3. Create a task to spin up an isolated worktree
|
|
49176
|
+
4. Run your AI agent in the task terminal
|
|
49342
49177
|
|
|
49343
|
-
|
|
49178
|
+
Commands:
|
|
49179
|
+
fulcrum status Check server status
|
|
49180
|
+
fulcrum doctor Check all dependencies
|
|
49181
|
+
fulcrum down Stop the server
|
|
49182
|
+
`);
|
|
49183
|
+
if (!hasAgent) {
|
|
49184
|
+
console.error(`Note: No AI agents detected. Install one to get started:
|
|
49185
|
+
Claude Code: curl -fsSL https://claude.ai/install.sh | bash
|
|
49186
|
+
OpenCode: curl -fsSL https://opencode.ai/install | bash
|
|
49187
|
+
`);
|
|
49188
|
+
}
|
|
49189
|
+
}
|
|
49344
49190
|
|
|
49345
|
-
|
|
49191
|
+
// cli/src/commands/down.ts
|
|
49192
|
+
init_errors();
|
|
49193
|
+
async function handleDownCommand() {
|
|
49194
|
+
const pid = readPid();
|
|
49195
|
+
if (!pid) {
|
|
49196
|
+
throw new CliError("NOT_RUNNING", "No PID file found. Fulcrum server may not be running.", ExitCodes.ERROR);
|
|
49197
|
+
}
|
|
49198
|
+
if (!isProcessRunning(pid)) {
|
|
49199
|
+
removePid();
|
|
49200
|
+
if (isJsonOutput()) {
|
|
49201
|
+
output({ stopped: true, pid, wasRunning: false });
|
|
49202
|
+
} else {
|
|
49203
|
+
console.log(`Fulcrum was not running (stale PID file cleaned up)`);
|
|
49204
|
+
}
|
|
49205
|
+
return;
|
|
49206
|
+
}
|
|
49207
|
+
try {
|
|
49208
|
+
process.kill(pid, "SIGTERM");
|
|
49209
|
+
} catch (err) {
|
|
49210
|
+
throw new CliError("KILL_FAILED", `Failed to stop server (PID: ${pid}): ${err}`, ExitCodes.ERROR);
|
|
49211
|
+
}
|
|
49212
|
+
let attempts = 0;
|
|
49213
|
+
while (attempts < 50 && isProcessRunning(pid)) {
|
|
49214
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
49215
|
+
attempts++;
|
|
49216
|
+
}
|
|
49217
|
+
if (isProcessRunning(pid)) {
|
|
49218
|
+
try {
|
|
49219
|
+
process.kill(pid, "SIGKILL");
|
|
49220
|
+
} catch {}
|
|
49221
|
+
}
|
|
49222
|
+
removePid();
|
|
49223
|
+
if (isJsonOutput()) {
|
|
49224
|
+
output({ stopped: true, pid, wasRunning: true });
|
|
49225
|
+
} else {
|
|
49226
|
+
console.log(`Fulcrum stopped (PID: ${pid})`);
|
|
49227
|
+
}
|
|
49228
|
+
}
|
|
49346
49229
|
|
|
49347
|
-
|
|
49348
|
-
|
|
49349
|
-
|
|
49230
|
+
// cli/src/commands/migrate-from-vibora.ts
|
|
49231
|
+
init_server();
|
|
49232
|
+
async function handleMigrateFromViboraCommand(flags) {
|
|
49233
|
+
const autoYes = flags.yes === "true" || flags.y === "true";
|
|
49234
|
+
if (!needsViboraMigration()) {
|
|
49235
|
+
if (isJsonOutput()) {
|
|
49236
|
+
output({ migrated: false, reason: "no_migration_needed" });
|
|
49237
|
+
} else {
|
|
49238
|
+
console.error("No migration needed.");
|
|
49239
|
+
console.error(` ~/.vibora does not exist or ~/.fulcrum already has data.`);
|
|
49240
|
+
}
|
|
49241
|
+
return;
|
|
49242
|
+
}
|
|
49243
|
+
const viboraDir = getLegacyViboraDir();
|
|
49244
|
+
const fulcrumDir = getFulcrumDir();
|
|
49245
|
+
if (!isJsonOutput()) {
|
|
49246
|
+
console.error(`
|
|
49247
|
+
Found existing Vibora data at ${viboraDir}`);
|
|
49248
|
+
console.error("Fulcrum (formerly Vibora) now uses ~/.fulcrum for data storage.");
|
|
49249
|
+
console.error("");
|
|
49250
|
+
console.error("Your existing data can be copied to the new location.");
|
|
49251
|
+
console.error("This is non-destructive - your ~/.vibora directory will be left untouched.");
|
|
49252
|
+
console.error("");
|
|
49253
|
+
}
|
|
49254
|
+
const shouldMigrate = autoYes || await confirm("Would you like to copy your data to ~/.fulcrum?");
|
|
49255
|
+
if (!shouldMigrate) {
|
|
49256
|
+
if (isJsonOutput()) {
|
|
49257
|
+
output({ migrated: false, reason: "user_declined" });
|
|
49258
|
+
} else {
|
|
49259
|
+
console.error("Migration skipped.");
|
|
49260
|
+
console.error("You can run this command again later to migrate.");
|
|
49261
|
+
}
|
|
49262
|
+
return;
|
|
49263
|
+
}
|
|
49264
|
+
if (!isJsonOutput()) {
|
|
49265
|
+
console.error("Copying data from ~/.vibora to ~/.fulcrum...");
|
|
49266
|
+
}
|
|
49267
|
+
const success2 = migrateFromVibora();
|
|
49268
|
+
if (success2) {
|
|
49269
|
+
if (isJsonOutput()) {
|
|
49270
|
+
output({ migrated: true, from: viboraDir, to: fulcrumDir });
|
|
49271
|
+
} else {
|
|
49272
|
+
console.error("Migration complete!");
|
|
49273
|
+
console.error(` Data copied from ${viboraDir} to ${fulcrumDir}`);
|
|
49274
|
+
console.error(" Your original ~/.vibora directory has been preserved.");
|
|
49275
|
+
}
|
|
49276
|
+
} else {
|
|
49277
|
+
if (isJsonOutput()) {
|
|
49278
|
+
output({ migrated: false, reason: "migration_failed" });
|
|
49279
|
+
} else {
|
|
49280
|
+
console.error("Migration failed.");
|
|
49281
|
+
console.error("You can manually copy files from ~/.vibora to ~/.fulcrum");
|
|
49282
|
+
}
|
|
49283
|
+
process.exitCode = 1;
|
|
49284
|
+
}
|
|
49285
|
+
}
|
|
49350
49286
|
|
|
49351
|
-
|
|
49287
|
+
// cli/src/commands/status.ts
|
|
49288
|
+
init_server();
|
|
49289
|
+
async function handleStatusCommand(flags) {
|
|
49290
|
+
const pid = readPid();
|
|
49291
|
+
const port = getPort(flags.port);
|
|
49292
|
+
const serverUrl = discoverServerUrl(flags.url, flags.port);
|
|
49293
|
+
const pidRunning = pid !== null && isProcessRunning(pid);
|
|
49294
|
+
let healthOk = false;
|
|
49295
|
+
let version3 = null;
|
|
49296
|
+
let uptime = null;
|
|
49297
|
+
if (pidRunning) {
|
|
49298
|
+
try {
|
|
49299
|
+
const res = await fetch(`${serverUrl}/health`, { signal: AbortSignal.timeout(2000) });
|
|
49300
|
+
healthOk = res.ok;
|
|
49301
|
+
if (res.ok) {
|
|
49302
|
+
const health = await res.json();
|
|
49303
|
+
version3 = health.version || null;
|
|
49304
|
+
uptime = health.uptime || null;
|
|
49305
|
+
}
|
|
49306
|
+
} catch {}
|
|
49307
|
+
}
|
|
49308
|
+
const data = {
|
|
49309
|
+
running: pidRunning,
|
|
49310
|
+
healthy: healthOk,
|
|
49311
|
+
pid: pid || null,
|
|
49312
|
+
port,
|
|
49313
|
+
url: serverUrl,
|
|
49314
|
+
version: version3,
|
|
49315
|
+
uptime
|
|
49316
|
+
};
|
|
49317
|
+
if (isJsonOutput()) {
|
|
49318
|
+
output(data);
|
|
49319
|
+
} else {
|
|
49320
|
+
if (pidRunning) {
|
|
49321
|
+
const healthStatus = healthOk ? "healthy" : "not responding";
|
|
49322
|
+
console.log(`Fulcrum is running (${healthStatus})`);
|
|
49323
|
+
console.log(` PID: ${pid}`);
|
|
49324
|
+
console.log(` URL: ${serverUrl}`);
|
|
49325
|
+
if (version3)
|
|
49326
|
+
console.log(` Version: ${version3}`);
|
|
49327
|
+
if (uptime)
|
|
49328
|
+
console.log(` Uptime: ${Math.floor(uptime / 1000)}s`);
|
|
49329
|
+
} else {
|
|
49330
|
+
console.log("Fulcrum is not running");
|
|
49331
|
+
console.log(`
|
|
49332
|
+
Start with: fulcrum up`);
|
|
49333
|
+
}
|
|
49334
|
+
}
|
|
49335
|
+
}
|
|
49352
49336
|
|
|
49353
|
-
|
|
49354
|
-
|
|
49355
|
-
|
|
49356
|
-
|
|
49337
|
+
// cli/src/commands/git.ts
|
|
49338
|
+
init_client();
|
|
49339
|
+
init_errors();
|
|
49340
|
+
async function handleGitCommand(action, flags) {
|
|
49341
|
+
const client = new FulcrumClient(flags.url, flags.port);
|
|
49342
|
+
switch (action) {
|
|
49343
|
+
case "status": {
|
|
49344
|
+
const path = flags.path || process.cwd();
|
|
49345
|
+
const status = await client.getStatus(path);
|
|
49346
|
+
if (isJsonOutput()) {
|
|
49347
|
+
output(status);
|
|
49348
|
+
} else {
|
|
49349
|
+
console.log(`Branch: ${status.branch}`);
|
|
49350
|
+
if (status.ahead)
|
|
49351
|
+
console.log(` Ahead: ${status.ahead}`);
|
|
49352
|
+
if (status.behind)
|
|
49353
|
+
console.log(` Behind: ${status.behind}`);
|
|
49354
|
+
if (status.staged?.length)
|
|
49355
|
+
console.log(` Staged: ${status.staged.length} files`);
|
|
49356
|
+
if (status.modified?.length)
|
|
49357
|
+
console.log(` Modified: ${status.modified.length} files`);
|
|
49358
|
+
if (status.untracked?.length)
|
|
49359
|
+
console.log(` Untracked: ${status.untracked.length} files`);
|
|
49360
|
+
if (!status.staged?.length && !status.modified?.length && !status.untracked?.length) {
|
|
49361
|
+
console.log(" Working tree clean");
|
|
49362
|
+
}
|
|
49363
|
+
}
|
|
49364
|
+
break;
|
|
49365
|
+
}
|
|
49366
|
+
case "diff": {
|
|
49367
|
+
const path = flags.path || process.cwd();
|
|
49368
|
+
const diff = await client.getDiff(path, {
|
|
49369
|
+
staged: flags.staged === "true",
|
|
49370
|
+
ignoreWhitespace: flags["ignore-whitespace"] === "true",
|
|
49371
|
+
includeUntracked: flags["include-untracked"] === "true"
|
|
49372
|
+
});
|
|
49373
|
+
if (isJsonOutput()) {
|
|
49374
|
+
output(diff);
|
|
49375
|
+
} else {
|
|
49376
|
+
console.log(diff.diff || "No changes");
|
|
49377
|
+
}
|
|
49378
|
+
break;
|
|
49379
|
+
}
|
|
49380
|
+
case "branches": {
|
|
49381
|
+
const repo = flags.repo;
|
|
49382
|
+
if (!repo) {
|
|
49383
|
+
throw new CliError("MISSING_REPO", "--repo is required", ExitCodes.INVALID_ARGS);
|
|
49384
|
+
}
|
|
49385
|
+
const branches = await client.getBranches(repo);
|
|
49386
|
+
if (isJsonOutput()) {
|
|
49387
|
+
output(branches);
|
|
49388
|
+
} else {
|
|
49389
|
+
for (const branch of branches) {
|
|
49390
|
+
const current = branch.current ? "* " : " ";
|
|
49391
|
+
console.log(`${current}${branch.name}`);
|
|
49392
|
+
}
|
|
49393
|
+
}
|
|
49394
|
+
break;
|
|
49395
|
+
}
|
|
49396
|
+
default:
|
|
49397
|
+
throw new CliError("UNKNOWN_ACTION", `Unknown action: ${action}. Valid: status, diff, branches`, ExitCodes.INVALID_ARGS);
|
|
49398
|
+
}
|
|
49399
|
+
}
|
|
49357
49400
|
|
|
49358
|
-
|
|
49401
|
+
// cli/src/commands/worktrees.ts
|
|
49402
|
+
init_client();
|
|
49403
|
+
init_errors();
|
|
49404
|
+
async function handleWorktreesCommand(action, flags) {
|
|
49405
|
+
const client = new FulcrumClient(flags.url, flags.port);
|
|
49406
|
+
switch (action) {
|
|
49407
|
+
case "list": {
|
|
49408
|
+
const worktrees = await client.listWorktrees();
|
|
49409
|
+
if (isJsonOutput()) {
|
|
49410
|
+
output(worktrees);
|
|
49411
|
+
} else {
|
|
49412
|
+
if (worktrees.length === 0) {
|
|
49413
|
+
console.log("No worktrees found");
|
|
49414
|
+
} else {
|
|
49415
|
+
for (const wt of worktrees) {
|
|
49416
|
+
console.log(`${wt.path}`);
|
|
49417
|
+
console.log(` Branch: ${wt.branch}`);
|
|
49418
|
+
if (wt.taskId)
|
|
49419
|
+
console.log(` Task: ${wt.taskId}`);
|
|
49420
|
+
}
|
|
49421
|
+
}
|
|
49422
|
+
}
|
|
49423
|
+
break;
|
|
49424
|
+
}
|
|
49425
|
+
case "delete": {
|
|
49426
|
+
const worktreePath = flags.path;
|
|
49427
|
+
if (!worktreePath) {
|
|
49428
|
+
throw new CliError("MISSING_PATH", "--path is required", ExitCodes.INVALID_ARGS);
|
|
49429
|
+
}
|
|
49430
|
+
const deleteLinkedTask = flags["delete-task"] === "true" || flags["delete-task"] === "";
|
|
49431
|
+
const result = await client.deleteWorktree(worktreePath, flags.repo, deleteLinkedTask);
|
|
49432
|
+
if (isJsonOutput()) {
|
|
49433
|
+
output(result);
|
|
49434
|
+
} else {
|
|
49435
|
+
console.log(`Deleted worktree: ${worktreePath}`);
|
|
49436
|
+
}
|
|
49437
|
+
break;
|
|
49438
|
+
}
|
|
49439
|
+
default:
|
|
49440
|
+
throw new CliError("UNKNOWN_ACTION", `Unknown action: ${action}. Valid: list, delete`, ExitCodes.INVALID_ARGS);
|
|
49441
|
+
}
|
|
49442
|
+
}
|
|
49359
49443
|
|
|
49360
|
-
|
|
49444
|
+
// cli/src/commands/config.ts
|
|
49445
|
+
init_client();
|
|
49446
|
+
init_errors();
|
|
49447
|
+
async function handleConfigCommand(action, positional, flags) {
|
|
49448
|
+
const client = new FulcrumClient(flags.url, flags.port);
|
|
49449
|
+
switch (action) {
|
|
49450
|
+
case "list": {
|
|
49451
|
+
const config3 = await client.getAllConfig();
|
|
49452
|
+
if (isJsonOutput()) {
|
|
49453
|
+
output(config3);
|
|
49454
|
+
} else {
|
|
49455
|
+
console.log("Configuration:");
|
|
49456
|
+
for (const [key, value] of Object.entries(config3)) {
|
|
49457
|
+
const displayValue = value === null ? "(not set)" : value;
|
|
49458
|
+
console.log(` ${key}: ${displayValue}`);
|
|
49459
|
+
}
|
|
49460
|
+
}
|
|
49461
|
+
break;
|
|
49462
|
+
}
|
|
49463
|
+
case "get": {
|
|
49464
|
+
const [key] = positional;
|
|
49465
|
+
if (!key) {
|
|
49466
|
+
throw new CliError("MISSING_KEY", "Config key is required", ExitCodes.INVALID_ARGS);
|
|
49467
|
+
}
|
|
49468
|
+
const config3 = await client.getConfig(key);
|
|
49469
|
+
if (isJsonOutput()) {
|
|
49470
|
+
output(config3);
|
|
49471
|
+
} else {
|
|
49472
|
+
const value = config3.value === null ? "(not set)" : config3.value;
|
|
49473
|
+
console.log(`${key}: ${value}`);
|
|
49474
|
+
}
|
|
49475
|
+
break;
|
|
49476
|
+
}
|
|
49477
|
+
case "set": {
|
|
49478
|
+
const [key, value] = positional;
|
|
49479
|
+
if (!key) {
|
|
49480
|
+
throw new CliError("MISSING_KEY", "Config key is required", ExitCodes.INVALID_ARGS);
|
|
49481
|
+
}
|
|
49482
|
+
if (value === undefined) {
|
|
49483
|
+
throw new CliError("MISSING_VALUE", "Config value is required", ExitCodes.INVALID_ARGS);
|
|
49484
|
+
}
|
|
49485
|
+
const parsedValue = /^\d+$/.test(value) ? parseInt(value, 10) : value;
|
|
49486
|
+
const config3 = await client.setConfig(key, parsedValue);
|
|
49487
|
+
if (isJsonOutput()) {
|
|
49488
|
+
output(config3);
|
|
49489
|
+
} else {
|
|
49490
|
+
console.log(`Set ${key} = ${config3.value}`);
|
|
49491
|
+
}
|
|
49492
|
+
break;
|
|
49493
|
+
}
|
|
49494
|
+
case "reset": {
|
|
49495
|
+
const [key] = positional;
|
|
49496
|
+
if (!key) {
|
|
49497
|
+
throw new CliError("MISSING_KEY", "Config key is required", ExitCodes.INVALID_ARGS);
|
|
49498
|
+
}
|
|
49499
|
+
const config3 = await client.resetConfig(key);
|
|
49500
|
+
if (isJsonOutput()) {
|
|
49501
|
+
output(config3);
|
|
49502
|
+
} else {
|
|
49503
|
+
console.log(`Reset ${key} to default: ${config3.value}`);
|
|
49504
|
+
}
|
|
49505
|
+
break;
|
|
49506
|
+
}
|
|
49507
|
+
default:
|
|
49508
|
+
throw new CliError("UNKNOWN_ACTION", `Unknown action: ${action}. Valid: list, get, set, reset`, ExitCodes.INVALID_ARGS);
|
|
49509
|
+
}
|
|
49510
|
+
}
|
|
49361
49511
|
|
|
49362
|
-
|
|
49512
|
+
// cli/src/commands/opencode.ts
|
|
49513
|
+
init_errors();
|
|
49514
|
+
import {
|
|
49515
|
+
mkdirSync as mkdirSync4,
|
|
49516
|
+
writeFileSync as writeFileSync4,
|
|
49517
|
+
existsSync as existsSync5,
|
|
49518
|
+
readFileSync as readFileSync5,
|
|
49519
|
+
unlinkSync as unlinkSync2,
|
|
49520
|
+
copyFileSync,
|
|
49521
|
+
renameSync
|
|
49522
|
+
} from "fs";
|
|
49523
|
+
import { homedir as homedir3 } from "os";
|
|
49524
|
+
import { join as join5 } from "path";
|
|
49363
49525
|
|
|
49364
|
-
|
|
49526
|
+
// plugins/fulcrum-opencode/index.ts
|
|
49527
|
+
var fulcrum_opencode_default = `import type { Plugin } from "@opencode-ai/plugin"
|
|
49528
|
+
import { appendFileSync } from "node:fs"
|
|
49529
|
+
import { spawn } from "node:child_process"
|
|
49530
|
+
import { tmpdir } from "node:os"
|
|
49531
|
+
import { join } from "node:path"
|
|
49365
49532
|
|
|
49366
|
-
|
|
49533
|
+
declare const process: { env: Record<string, string | undefined> }
|
|
49367
49534
|
|
|
49368
|
-
|
|
49369
|
-
|
|
49370
|
-
"
|
|
49371
|
-
"
|
|
49535
|
+
const LOG_FILE = join(tmpdir(), "fulcrum-opencode.log")
|
|
49536
|
+
const NOISY_EVENTS = new Set([
|
|
49537
|
+
"message.part.updated",
|
|
49538
|
+
"file.watcher.updated",
|
|
49539
|
+
"tui.toast.show",
|
|
49540
|
+
"config.updated",
|
|
49541
|
+
])
|
|
49542
|
+
const log = (msg: string) => {
|
|
49543
|
+
try {
|
|
49544
|
+
appendFileSync(LOG_FILE, \`[\${new Date().toISOString()}] \${msg}\\n\`)
|
|
49545
|
+
} catch {
|
|
49546
|
+
// Silently ignore logging errors - logging is non-critical
|
|
49547
|
+
}
|
|
49372
49548
|
}
|
|
49373
|
-
\`\`\`
|
|
49374
|
-
|
|
49375
|
-
**Categories:** core, tasks, projects, repositories, apps, filesystem, git, notifications, exec
|
|
49376
|
-
|
|
49377
|
-
**Example Usage:**
|
|
49378
|
-
\`\`\`
|
|
49379
|
-
search_tools { query: "project create" }
|
|
49380
|
-
\u2192 Returns tools for creating projects
|
|
49381
|
-
|
|
49382
|
-
search_tools { category: "filesystem" }
|
|
49383
|
-
\u2192 Returns all filesystem tools
|
|
49384
|
-
\`\`\`
|
|
49385
49549
|
|
|
49386
|
-
|
|
49550
|
+
/**
|
|
49551
|
+
* Execute fulcrum command using spawn with shell option for proper PATH resolution.
|
|
49552
|
+
* Using spawn with explicit args array prevents shell injection while shell:true
|
|
49553
|
+
* ensures PATH is properly resolved (for NVM, fnm, etc. managed node installations).
|
|
49554
|
+
* Includes 10 second timeout protection to prevent hanging.
|
|
49555
|
+
*/
|
|
49556
|
+
async function runFulcrumCommand(args: string[]): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
|
49557
|
+
return new Promise((resolve) => {
|
|
49558
|
+
let stdout = ''
|
|
49559
|
+
let stderr = ''
|
|
49560
|
+
let resolved = false
|
|
49561
|
+
let processExited = false
|
|
49562
|
+
let killTimeoutId: ReturnType<typeof setTimeout> | null = null
|
|
49387
49563
|
|
|
49388
|
-
|
|
49389
|
-
- \`get_task\` - Get task details by ID
|
|
49390
|
-
- \`create_task\` - Create a new task with worktree
|
|
49391
|
-
- \`update_task\` - Update task metadata
|
|
49392
|
-
- \`delete_task\` - Delete a task
|
|
49393
|
-
- \`move_task\` - Move task to different status
|
|
49394
|
-
- \`add_task_link\` - Add URL link to task
|
|
49395
|
-
- \`remove_task_link\` - Remove link from task
|
|
49396
|
-
- \`list_task_links\` - List all task links
|
|
49397
|
-
- \`add_task_label\` - Add a label to a task (returns similar labels to catch typos)
|
|
49398
|
-
- \`remove_task_label\` - Remove a label from a task
|
|
49399
|
-
- \`set_task_due_date\` - Set or clear task due date
|
|
49400
|
-
- \`list_labels\` - List all unique labels in use with optional search
|
|
49564
|
+
const child = spawn(FULCRUM_CMD, args, { shell: true })
|
|
49401
49565
|
|
|
49402
|
-
|
|
49566
|
+
const cleanup = () => {
|
|
49567
|
+
processExited = true
|
|
49568
|
+
if (killTimeoutId) {
|
|
49569
|
+
clearTimeout(killTimeoutId)
|
|
49570
|
+
killTimeoutId = null
|
|
49571
|
+
}
|
|
49572
|
+
}
|
|
49403
49573
|
|
|
49404
|
-
|
|
49574
|
+
child.stdout?.on('data', (data) => {
|
|
49575
|
+
stdout += data.toString()
|
|
49576
|
+
})
|
|
49405
49577
|
|
|
49406
|
-
|
|
49407
|
-
|
|
49408
|
-
|
|
49409
|
-
"labels": ["bug", "urgent"], // Filter by multiple labels (OR logic)
|
|
49410
|
-
"statuses": ["TO_DO", "IN_PROGRESS"], // Filter by multiple statuses (OR logic)
|
|
49411
|
-
"dueDateStart": "2026-01-18", // Start of date range
|
|
49412
|
-
"dueDateEnd": "2026-01-25", // End of date range
|
|
49413
|
-
"overdue": true // Only show overdue tasks
|
|
49414
|
-
}
|
|
49415
|
-
\`\`\`
|
|
49578
|
+
child.stderr?.on('data', (data) => {
|
|
49579
|
+
stderr += data.toString()
|
|
49580
|
+
})
|
|
49416
49581
|
|
|
49417
|
-
|
|
49582
|
+
child.on('close', (code) => {
|
|
49583
|
+
cleanup()
|
|
49584
|
+
if (!resolved) {
|
|
49585
|
+
resolved = true
|
|
49586
|
+
resolve({ exitCode: code || 0, stdout, stderr })
|
|
49587
|
+
}
|
|
49588
|
+
})
|
|
49418
49589
|
|
|
49419
|
-
|
|
49590
|
+
child.on('error', (err) => {
|
|
49591
|
+
cleanup()
|
|
49592
|
+
if (!resolved) {
|
|
49593
|
+
resolved = true
|
|
49594
|
+
resolve({ exitCode: 1, stdout, stderr: err.message || '' })
|
|
49595
|
+
}
|
|
49596
|
+
})
|
|
49420
49597
|
|
|
49421
|
-
|
|
49422
|
-
|
|
49423
|
-
|
|
49424
|
-
|
|
49425
|
-
|
|
49598
|
+
// Add timeout protection to prevent hanging
|
|
49599
|
+
const timeoutId = setTimeout(() => {
|
|
49600
|
+
if (!resolved) {
|
|
49601
|
+
resolved = true
|
|
49602
|
+
log(\`Command timeout: \${FULCRUM_CMD} \${args.join(' ')}\`)
|
|
49603
|
+
child.kill('SIGTERM')
|
|
49604
|
+
// Schedule SIGKILL if process doesn't exit after SIGTERM
|
|
49605
|
+
killTimeoutId = setTimeout(() => {
|
|
49606
|
+
if (!processExited) {
|
|
49607
|
+
log(\`Process didn't exit after SIGTERM, sending SIGKILL\`)
|
|
49608
|
+
child.kill('SIGKILL')
|
|
49609
|
+
}
|
|
49610
|
+
}, 2000)
|
|
49611
|
+
resolve({ exitCode: -1, stdout, stderr: \`Command timed out after \${FULCRUM_COMMAND_TIMEOUT_MS}ms\` })
|
|
49612
|
+
}
|
|
49613
|
+
}, FULCRUM_COMMAND_TIMEOUT_MS)
|
|
49426
49614
|
|
|
49427
|
-
|
|
49615
|
+
// Clear timeout if command completes
|
|
49616
|
+
child.on('exit', () => clearTimeout(timeoutId))
|
|
49617
|
+
})
|
|
49618
|
+
}
|
|
49428
49619
|
|
|
49429
|
-
|
|
49620
|
+
let mainSessionId: string | null = null
|
|
49621
|
+
const subagentSessions = new Set<string>()
|
|
49622
|
+
let pendingIdleTimer: ReturnType<typeof setTimeout> | null = null
|
|
49623
|
+
let activityVersion = 0
|
|
49624
|
+
let lastStatus: "in-progress" | "review" | "" = ""
|
|
49430
49625
|
|
|
49431
|
-
|
|
49432
|
-
|
|
49433
|
-
|
|
49434
|
-
|
|
49435
|
-
- \`delete_project\` - Delete project and optionally directory/app
|
|
49436
|
-
- \`scan_projects\` - Scan directory for git repos
|
|
49437
|
-
- \`list_project_links\` - List all URL links attached to a project
|
|
49438
|
-
- \`add_project_link\` - Add a URL link to a project (auto-detects type)
|
|
49439
|
-
- \`remove_project_link\` - Remove a URL link from a project
|
|
49626
|
+
const FULCRUM_CMD = "fulcrum"
|
|
49627
|
+
const IDLE_CONFIRMATION_DELAY_MS = 1500
|
|
49628
|
+
const FULCRUM_COMMAND_TIMEOUT_MS = 10000
|
|
49629
|
+
const STATUS_CHANGE_DEBOUNCE_MS = 500
|
|
49440
49630
|
|
|
49441
|
-
|
|
49631
|
+
let deferredContextCheck: Promise<boolean> | null = null
|
|
49632
|
+
let isFulcrumContext: boolean | null = null
|
|
49633
|
+
let pendingStatusCommand: Promise<{ exitCode: number; stdout: string; stderr: string }> | null = null
|
|
49442
49634
|
|
|
49443
|
-
|
|
49444
|
-
|
|
49445
|
-
- \`add_repository\` - Add repository from local path
|
|
49446
|
-
- \`update_repository\` - Update repository metadata (name, agent, startup script)
|
|
49447
|
-
- \`delete_repository\` - Delete orphaned repository (fails if linked to project)
|
|
49448
|
-
- \`link_repository_to_project\` - Link repo to project (errors if already linked elsewhere)
|
|
49449
|
-
- \`unlink_repository_from_project\` - Unlink repo from project
|
|
49635
|
+
export const FulcrumPlugin: Plugin = async ({ $, directory }) => {
|
|
49636
|
+
log("Plugin initializing...")
|
|
49450
49637
|
|
|
49451
|
-
|
|
49638
|
+
if (process.env.FULCRUM_TASK_ID) {
|
|
49639
|
+
isFulcrumContext = true
|
|
49640
|
+
log("Fulcrum context detected via env var")
|
|
49641
|
+
} else {
|
|
49642
|
+
deferredContextCheck = Promise.all([
|
|
49643
|
+
$\`\${FULCRUM_CMD} --version\`.quiet().nothrow().text(),
|
|
49644
|
+
runFulcrumCommand(['current-task', '--path', directory]),
|
|
49645
|
+
])
|
|
49646
|
+
.then(([versionResult, taskResult]) => {
|
|
49647
|
+
if (!versionResult) {
|
|
49648
|
+
log("Fulcrum CLI not found")
|
|
49649
|
+
return false
|
|
49650
|
+
}
|
|
49651
|
+
const inContext = taskResult.exitCode === 0
|
|
49652
|
+
log(inContext ? "Fulcrum context active" : "Not a Fulcrum context")
|
|
49653
|
+
return inContext
|
|
49654
|
+
})
|
|
49655
|
+
.catch(() => {
|
|
49656
|
+
log("Fulcrum check failed")
|
|
49657
|
+
return false
|
|
49658
|
+
})
|
|
49659
|
+
}
|
|
49452
49660
|
|
|
49453
|
-
|
|
49454
|
-
- \`get_app\` - Get app details with services
|
|
49455
|
-
- \`create_app\` - Create app for deployment
|
|
49456
|
-
- \`deploy_app\` - Trigger deployment
|
|
49457
|
-
- \`stop_app\` - Stop running app
|
|
49458
|
-
- \`get_app_logs\` - Get container logs
|
|
49459
|
-
- \`get_app_status\` - Get container status
|
|
49460
|
-
- \`list_deployments\` - Get deployment history
|
|
49461
|
-
- \`delete_app\` - Delete app
|
|
49661
|
+
log("Plugin hooks registered")
|
|
49462
49662
|
|
|
49463
|
-
|
|
49663
|
+
const checkContext = async (): Promise<boolean> => {
|
|
49664
|
+
if (isFulcrumContext !== null) return isFulcrumContext
|
|
49665
|
+
if (deferredContextCheck) {
|
|
49666
|
+
isFulcrumContext = await deferredContextCheck
|
|
49667
|
+
deferredContextCheck = null
|
|
49668
|
+
return isFulcrumContext
|
|
49669
|
+
}
|
|
49670
|
+
return false
|
|
49671
|
+
}
|
|
49464
49672
|
|
|
49465
|
-
|
|
49673
|
+
const cancelPendingIdle = () => {
|
|
49674
|
+
if (pendingIdleTimer) {
|
|
49675
|
+
clearTimeout(pendingIdleTimer)
|
|
49676
|
+
pendingIdleTimer = null
|
|
49677
|
+
log("Cancelled pending idle transition")
|
|
49678
|
+
}
|
|
49679
|
+
}
|
|
49466
49680
|
|
|
49467
|
-
|
|
49468
|
-
|
|
49469
|
-
- \`read_file\` - Read file contents (secured)
|
|
49470
|
-
- \`write_file\` - Write entire file content (secured)
|
|
49471
|
-
- \`edit_file\` - Edit file by replacing a unique string (secured)
|
|
49472
|
-
- \`file_stat\` - Get file/directory metadata
|
|
49473
|
-
- \`is_git_repo\` - Check if directory is git repo
|
|
49681
|
+
const setStatus = (status: "in-progress" | "review") => {
|
|
49682
|
+
if (status === lastStatus) return
|
|
49474
49683
|
|
|
49475
|
-
|
|
49684
|
+
cancelPendingIdle()
|
|
49476
49685
|
|
|
49477
|
-
|
|
49686
|
+
if (pendingStatusCommand) {
|
|
49687
|
+
log(\`Status change already in progress, will retry after \${STATUS_CHANGE_DEBOUNCE_MS}ms\`)
|
|
49688
|
+
setTimeout(() => setStatus(status), STATUS_CHANGE_DEBOUNCE_MS)
|
|
49689
|
+
return
|
|
49690
|
+
}
|
|
49478
49691
|
|
|
49479
|
-
|
|
49692
|
+
lastStatus = status
|
|
49480
49693
|
|
|
49481
|
-
|
|
49694
|
+
;(async () => {
|
|
49695
|
+
try {
|
|
49696
|
+
log(\`Setting status: \${status}\`)
|
|
49697
|
+
pendingStatusCommand = runFulcrumCommand(['current-task', status, '--path', directory])
|
|
49698
|
+
const res = await pendingStatusCommand
|
|
49699
|
+
pendingStatusCommand = null
|
|
49482
49700
|
|
|
49483
|
-
|
|
49484
|
-
{
|
|
49485
|
-
|
|
49486
|
-
|
|
49487
|
-
|
|
49488
|
-
|
|
49489
|
-
|
|
49490
|
-
}
|
|
49491
|
-
|
|
49701
|
+
if (res.exitCode !== 0) {
|
|
49702
|
+
log(\`Status update failed: exitCode=\${res.exitCode}, stderr=\${res.stderr}\`)
|
|
49703
|
+
}
|
|
49704
|
+
} catch (e) {
|
|
49705
|
+
log(\`Status update error: \${e}\`)
|
|
49706
|
+
pendingStatusCommand = null
|
|
49707
|
+
}
|
|
49708
|
+
})()
|
|
49709
|
+
}
|
|
49492
49710
|
|
|
49493
|
-
|
|
49494
|
-
|
|
49495
|
-
|
|
49496
|
-
- \`cwd\` (optional) \u2014 Initial working directory (only used when creating new session)
|
|
49497
|
-
- \`timeout\` (optional) \u2014 Timeout in milliseconds (default: 30000)
|
|
49498
|
-
- \`name\` (optional) \u2014 Session name for identification (only used when creating new session)
|
|
49711
|
+
const scheduleIdleTransition = () => {
|
|
49712
|
+
cancelPendingIdle()
|
|
49713
|
+
const currentVersion = ++activityVersion
|
|
49499
49714
|
|
|
49500
|
-
|
|
49501
|
-
|
|
49502
|
-
|
|
49503
|
-
|
|
49504
|
-
|
|
49505
|
-
|
|
49506
|
-
|
|
49507
|
-
|
|
49508
|
-
}
|
|
49509
|
-
\`\`\`
|
|
49715
|
+
pendingIdleTimer = setTimeout(() => {
|
|
49716
|
+
if (activityVersion !== currentVersion) {
|
|
49717
|
+
log(
|
|
49718
|
+
\`Stale idle transition (version \${currentVersion} vs \${activityVersion})\`,
|
|
49719
|
+
)
|
|
49720
|
+
return
|
|
49721
|
+
}
|
|
49722
|
+
setStatus("review")
|
|
49723
|
+
}, IDLE_CONFIRMATION_DELAY_MS)
|
|
49510
49724
|
|
|
49511
|
-
|
|
49725
|
+
log(
|
|
49726
|
+
\`Scheduled idle transition (version \${currentVersion}, delay \${IDLE_CONFIRMATION_DELAY_MS}ms)\`,
|
|
49727
|
+
)
|
|
49728
|
+
}
|
|
49512
49729
|
|
|
49513
|
-
|
|
49514
|
-
|
|
49515
|
-
|
|
49516
|
-
|
|
49730
|
+
const recordActivity = (reason: string) => {
|
|
49731
|
+
activityVersion++
|
|
49732
|
+
cancelPendingIdle()
|
|
49733
|
+
log(\`Activity: \${reason} (version now \${activityVersion})\`)
|
|
49734
|
+
}
|
|
49517
49735
|
|
|
49518
|
-
|
|
49519
|
-
|
|
49520
|
-
|
|
49736
|
+
return {
|
|
49737
|
+
"chat.message": async (_input, output) => {
|
|
49738
|
+
if (!(await checkContext())) return
|
|
49521
49739
|
|
|
49522
|
-
|
|
49523
|
-
|
|
49740
|
+
if (output.message.role === "user") {
|
|
49741
|
+
recordActivity("user message")
|
|
49742
|
+
setStatus("in-progress")
|
|
49743
|
+
} else if (output.message.role === "assistant") {
|
|
49744
|
+
recordActivity("assistant message")
|
|
49745
|
+
}
|
|
49746
|
+
},
|
|
49524
49747
|
|
|
49525
|
-
|
|
49526
|
-
|
|
49748
|
+
event: async ({ event }) => {
|
|
49749
|
+
if (!NOISY_EVENTS.has(event.type)) {
|
|
49750
|
+
log(\`Event: \${event.type}\`)
|
|
49751
|
+
}
|
|
49527
49752
|
|
|
49528
|
-
|
|
49529
|
-
destroy_exec_session { sessionId: "abc-123" }
|
|
49530
|
-
\`\`\`
|
|
49753
|
+
if (!(await checkContext())) return
|
|
49531
49754
|
|
|
49532
|
-
|
|
49755
|
+
const props = (event.properties as Record<string, unknown>) || {}
|
|
49533
49756
|
|
|
49534
|
-
|
|
49757
|
+
if (event.type === "session.created") {
|
|
49758
|
+
const info = (props.info as Record<string, unknown>) || {}
|
|
49759
|
+
const sessionId = info.id as string | undefined
|
|
49760
|
+
const parentId = info.parentID as string | undefined
|
|
49535
49761
|
|
|
49536
|
-
|
|
49762
|
+
if (parentId) {
|
|
49763
|
+
if (sessionId) subagentSessions.add(sessionId)
|
|
49764
|
+
log(\`Subagent session tracked: \${sessionId} (parent: \${parentId})\`)
|
|
49765
|
+
} else if (!mainSessionId && sessionId) {
|
|
49766
|
+
mainSessionId = sessionId
|
|
49767
|
+
log(\`Main session set: \${mainSessionId}\`)
|
|
49768
|
+
}
|
|
49537
49769
|
|
|
49538
|
-
|
|
49770
|
+
recordActivity("session.created")
|
|
49771
|
+
setStatus("in-progress")
|
|
49772
|
+
return
|
|
49773
|
+
}
|
|
49539
49774
|
|
|
49540
|
-
|
|
49775
|
+
const status = props.status as Record<string, unknown> | undefined
|
|
49776
|
+
if (
|
|
49777
|
+
(event.type === "session.status" && status?.type === "busy") ||
|
|
49778
|
+
event.type.startsWith("tool.execute")
|
|
49779
|
+
) {
|
|
49780
|
+
recordActivity(event.type)
|
|
49781
|
+
return
|
|
49782
|
+
}
|
|
49541
49783
|
|
|
49542
|
-
|
|
49784
|
+
if (
|
|
49785
|
+
event.type === "session.idle" ||
|
|
49786
|
+
(event.type === "session.status" && status?.type === "idle")
|
|
49787
|
+
) {
|
|
49788
|
+
const info = (props.info as Record<string, unknown>) || {}
|
|
49789
|
+
const sessionId =
|
|
49790
|
+
(props.sessionID as string) || (info.id as string) || null
|
|
49543
49791
|
|
|
49544
|
-
|
|
49792
|
+
if (sessionId && subagentSessions.has(sessionId)) {
|
|
49793
|
+
log(\`Ignoring subagent idle: \${sessionId}\`)
|
|
49794
|
+
return
|
|
49795
|
+
}
|
|
49545
49796
|
|
|
49546
|
-
|
|
49797
|
+
if (mainSessionId && sessionId && sessionId !== mainSessionId) {
|
|
49798
|
+
log(\`Ignoring non-main idle: \${sessionId} (main: \${mainSessionId})\`)
|
|
49799
|
+
return
|
|
49800
|
+
}
|
|
49547
49801
|
|
|
49548
|
-
|
|
49549
|
-
|
|
49550
|
-
|
|
49551
|
-
|
|
49552
|
-
|
|
49553
|
-
|
|
49554
|
-
7. **Reuse sessions for related commands** - Preserve state across multiple execute_command calls
|
|
49555
|
-
8. **Clean up sessions when done** - Use destroy_exec_session to free resources
|
|
49802
|
+
log(\`Main session idle detected: \${sessionId}\`)
|
|
49803
|
+
scheduleIdleTransition()
|
|
49804
|
+
}
|
|
49805
|
+
},
|
|
49806
|
+
}
|
|
49807
|
+
}
|
|
49556
49808
|
`;
|
|
49557
49809
|
|
|
49558
|
-
// cli/src/commands/
|
|
49559
|
-
var
|
|
49560
|
-
var
|
|
49561
|
-
|
|
49562
|
-
|
|
49563
|
-
|
|
49564
|
-
|
|
49565
|
-
|
|
49566
|
-
|
|
49567
|
-
|
|
49568
|
-
|
|
49569
|
-
{ path: "skills/vibora/SKILL.md", content: SKILL_default }
|
|
49570
|
-
];
|
|
49571
|
-
async function handleClaudeCommand(action) {
|
|
49810
|
+
// cli/src/commands/opencode.ts
|
|
49811
|
+
var OPENCODE_DIR = join5(homedir3(), ".opencode");
|
|
49812
|
+
var OPENCODE_CONFIG_PATH = join5(OPENCODE_DIR, "opencode.json");
|
|
49813
|
+
var PLUGIN_DIR = join5(homedir3(), ".config", "opencode", "plugin");
|
|
49814
|
+
var PLUGIN_PATH = join5(PLUGIN_DIR, "fulcrum.ts");
|
|
49815
|
+
var FULCRUM_MCP_CONFIG = {
|
|
49816
|
+
type: "local",
|
|
49817
|
+
command: ["fulcrum", "mcp"],
|
|
49818
|
+
enabled: true
|
|
49819
|
+
};
|
|
49820
|
+
async function handleOpenCodeCommand(action) {
|
|
49572
49821
|
if (action === "install") {
|
|
49573
|
-
await
|
|
49822
|
+
await installOpenCodeIntegration();
|
|
49574
49823
|
return;
|
|
49575
49824
|
}
|
|
49576
49825
|
if (action === "uninstall") {
|
|
49577
|
-
await
|
|
49826
|
+
await uninstallOpenCodeIntegration();
|
|
49578
49827
|
return;
|
|
49579
49828
|
}
|
|
49580
|
-
throw new CliError("INVALID_ACTION", "Unknown action. Usage: fulcrum
|
|
49829
|
+
throw new CliError("INVALID_ACTION", "Unknown action. Usage: fulcrum opencode install | fulcrum opencode uninstall", ExitCodes.INVALID_ARGS);
|
|
49581
49830
|
}
|
|
49582
|
-
async function
|
|
49831
|
+
async function installOpenCodeIntegration() {
|
|
49583
49832
|
try {
|
|
49584
|
-
console.log("Installing
|
|
49585
|
-
|
|
49586
|
-
|
|
49587
|
-
|
|
49588
|
-
|
|
49589
|
-
|
|
49590
|
-
const fullPath = join5(PLUGIN_DIR2, file2.path);
|
|
49591
|
-
const dir = fullPath.substring(0, fullPath.lastIndexOf("/"));
|
|
49592
|
-
mkdirSync4(dir, { recursive: true });
|
|
49593
|
-
writeFileSync4(fullPath, file2.content, "utf-8");
|
|
49594
|
-
}
|
|
49595
|
-
console.log("\u2713 Installed plugin at " + PLUGIN_DIR2);
|
|
49833
|
+
console.log("Installing OpenCode plugin...");
|
|
49834
|
+
mkdirSync4(PLUGIN_DIR, { recursive: true });
|
|
49835
|
+
writeFileSync4(PLUGIN_PATH, fulcrum_opencode_default, "utf-8");
|
|
49836
|
+
console.log("\u2713 Installed plugin at " + PLUGIN_PATH);
|
|
49837
|
+
console.log("Configuring MCP server...");
|
|
49838
|
+
const mcpConfigured = addMcpServer();
|
|
49596
49839
|
console.log("");
|
|
49597
|
-
|
|
49840
|
+
if (mcpConfigured) {
|
|
49841
|
+
console.log("Installation complete! Restart OpenCode to apply changes.");
|
|
49842
|
+
} else {
|
|
49843
|
+
console.log("Plugin installed, but MCP configuration was skipped.");
|
|
49844
|
+
console.log("Please add the MCP server manually (see above).");
|
|
49845
|
+
}
|
|
49598
49846
|
} catch (err) {
|
|
49599
|
-
throw new CliError("INSTALL_FAILED", `Failed to install
|
|
49847
|
+
throw new CliError("INSTALL_FAILED", `Failed to install OpenCode integration: ${err instanceof Error ? err.message : String(err)}`, ExitCodes.ERROR);
|
|
49600
49848
|
}
|
|
49601
49849
|
}
|
|
49602
|
-
async function
|
|
49850
|
+
async function uninstallOpenCodeIntegration() {
|
|
49603
49851
|
try {
|
|
49604
|
-
|
|
49605
|
-
|
|
49606
|
-
|
|
49607
|
-
|
|
49608
|
-
console.log("
|
|
49852
|
+
let removedPlugin = false;
|
|
49853
|
+
let removedMcp = false;
|
|
49854
|
+
if (existsSync5(PLUGIN_PATH)) {
|
|
49855
|
+
unlinkSync2(PLUGIN_PATH);
|
|
49856
|
+
console.log("\u2713 Removed plugin from " + PLUGIN_PATH);
|
|
49857
|
+
removedPlugin = true;
|
|
49858
|
+
} else {
|
|
49859
|
+
console.log("\u2022 Plugin not found (already removed)");
|
|
49860
|
+
}
|
|
49861
|
+
removedMcp = removeMcpServer();
|
|
49862
|
+
if (!removedPlugin && !removedMcp) {
|
|
49863
|
+
console.log("Nothing to uninstall.");
|
|
49609
49864
|
} else {
|
|
49610
|
-
console.log("
|
|
49865
|
+
console.log("");
|
|
49866
|
+
console.log("Uninstall complete! Restart OpenCode to apply changes.");
|
|
49611
49867
|
}
|
|
49612
49868
|
} catch (err) {
|
|
49613
|
-
throw new CliError("UNINSTALL_FAILED", `Failed to uninstall
|
|
49869
|
+
throw new CliError("UNINSTALL_FAILED", `Failed to uninstall OpenCode integration: ${err instanceof Error ? err.message : String(err)}`, ExitCodes.ERROR);
|
|
49870
|
+
}
|
|
49871
|
+
}
|
|
49872
|
+
function getMcpObject(config3) {
|
|
49873
|
+
const mcp = config3.mcp;
|
|
49874
|
+
if (mcp && typeof mcp === "object" && !Array.isArray(mcp)) {
|
|
49875
|
+
return mcp;
|
|
49876
|
+
}
|
|
49877
|
+
return {};
|
|
49878
|
+
}
|
|
49879
|
+
function addMcpServer() {
|
|
49880
|
+
mkdirSync4(OPENCODE_DIR, { recursive: true });
|
|
49881
|
+
let config3 = {};
|
|
49882
|
+
if (existsSync5(OPENCODE_CONFIG_PATH)) {
|
|
49883
|
+
try {
|
|
49884
|
+
const content = readFileSync5(OPENCODE_CONFIG_PATH, "utf-8");
|
|
49885
|
+
config3 = JSON.parse(content);
|
|
49886
|
+
} catch {
|
|
49887
|
+
console.log("\u26A0 Could not parse existing opencode.json, skipping MCP configuration");
|
|
49888
|
+
console.log(" Add manually to ~/.opencode/opencode.json:");
|
|
49889
|
+
console.log(' "mcp": { "fulcrum": { "type": "local", "command": ["fulcrum", "mcp"], "enabled": true } }');
|
|
49890
|
+
return false;
|
|
49891
|
+
}
|
|
49892
|
+
}
|
|
49893
|
+
const mcp = getMcpObject(config3);
|
|
49894
|
+
if (mcp.fulcrum) {
|
|
49895
|
+
console.log("\u2022 MCP server already configured, preserving existing configuration");
|
|
49896
|
+
return true;
|
|
49897
|
+
}
|
|
49898
|
+
if (existsSync5(OPENCODE_CONFIG_PATH)) {
|
|
49899
|
+
copyFileSync(OPENCODE_CONFIG_PATH, OPENCODE_CONFIG_PATH + ".backup");
|
|
49900
|
+
}
|
|
49901
|
+
config3.mcp = {
|
|
49902
|
+
...mcp,
|
|
49903
|
+
fulcrum: FULCRUM_MCP_CONFIG
|
|
49904
|
+
};
|
|
49905
|
+
const tempPath = OPENCODE_CONFIG_PATH + ".tmp";
|
|
49906
|
+
try {
|
|
49907
|
+
writeFileSync4(tempPath, JSON.stringify(config3, null, 2), "utf-8");
|
|
49908
|
+
renameSync(tempPath, OPENCODE_CONFIG_PATH);
|
|
49909
|
+
} catch (error46) {
|
|
49910
|
+
try {
|
|
49911
|
+
if (existsSync5(tempPath)) {
|
|
49912
|
+
unlinkSync2(tempPath);
|
|
49913
|
+
}
|
|
49914
|
+
} catch {}
|
|
49915
|
+
throw error46;
|
|
49916
|
+
}
|
|
49917
|
+
console.log("\u2713 Added MCP server to " + OPENCODE_CONFIG_PATH);
|
|
49918
|
+
return true;
|
|
49919
|
+
}
|
|
49920
|
+
function removeMcpServer() {
|
|
49921
|
+
if (!existsSync5(OPENCODE_CONFIG_PATH)) {
|
|
49922
|
+
console.log("\u2022 MCP config not found (already removed)");
|
|
49923
|
+
return false;
|
|
49924
|
+
}
|
|
49925
|
+
let config3;
|
|
49926
|
+
try {
|
|
49927
|
+
const content = readFileSync5(OPENCODE_CONFIG_PATH, "utf-8");
|
|
49928
|
+
config3 = JSON.parse(content);
|
|
49929
|
+
} catch {
|
|
49930
|
+
console.log("\u26A0 Could not parse opencode.json, skipping MCP removal");
|
|
49931
|
+
return false;
|
|
49932
|
+
}
|
|
49933
|
+
const mcp = getMcpObject(config3);
|
|
49934
|
+
if (!mcp.fulcrum) {
|
|
49935
|
+
console.log("\u2022 MCP server not configured (already removed)");
|
|
49936
|
+
return false;
|
|
49937
|
+
}
|
|
49938
|
+
copyFileSync(OPENCODE_CONFIG_PATH, OPENCODE_CONFIG_PATH + ".backup");
|
|
49939
|
+
delete mcp.fulcrum;
|
|
49940
|
+
if (Object.keys(mcp).length === 0) {
|
|
49941
|
+
delete config3.mcp;
|
|
49942
|
+
} else {
|
|
49943
|
+
config3.mcp = mcp;
|
|
49944
|
+
}
|
|
49945
|
+
const tempPath = OPENCODE_CONFIG_PATH + ".tmp";
|
|
49946
|
+
try {
|
|
49947
|
+
writeFileSync4(tempPath, JSON.stringify(config3, null, 2), "utf-8");
|
|
49948
|
+
renameSync(tempPath, OPENCODE_CONFIG_PATH);
|
|
49949
|
+
} catch (error46) {
|
|
49950
|
+
try {
|
|
49951
|
+
if (existsSync5(tempPath)) {
|
|
49952
|
+
unlinkSync2(tempPath);
|
|
49953
|
+
}
|
|
49954
|
+
} catch {}
|
|
49955
|
+
throw error46;
|
|
49614
49956
|
}
|
|
49957
|
+
console.log("\u2713 Removed MCP server from " + OPENCODE_CONFIG_PATH);
|
|
49958
|
+
return true;
|
|
49615
49959
|
}
|
|
49616
49960
|
|
|
49617
49961
|
// cli/src/commands/notifications.ts
|
|
@@ -50245,6 +50589,127 @@ var tasksListDependenciesCommand = defineCommand({
|
|
|
50245
50589
|
await handleTasksCommand("list-dependencies", [args.id], toFlags(args));
|
|
50246
50590
|
}
|
|
50247
50591
|
});
|
|
50592
|
+
var tasksLabelsCommand = defineCommand({
|
|
50593
|
+
meta: {
|
|
50594
|
+
name: "labels",
|
|
50595
|
+
description: "List all labels in use across tasks"
|
|
50596
|
+
},
|
|
50597
|
+
args: {
|
|
50598
|
+
...globalArgs,
|
|
50599
|
+
search: {
|
|
50600
|
+
type: "string",
|
|
50601
|
+
description: "Filter labels by substring match"
|
|
50602
|
+
}
|
|
50603
|
+
},
|
|
50604
|
+
async run({ args }) {
|
|
50605
|
+
if (args.json)
|
|
50606
|
+
setJsonOutput(true);
|
|
50607
|
+
await handleTasksCommand("labels", [], toFlags(args));
|
|
50608
|
+
}
|
|
50609
|
+
});
|
|
50610
|
+
var tasksAttachmentsListCommand = defineCommand({
|
|
50611
|
+
meta: {
|
|
50612
|
+
name: "list",
|
|
50613
|
+
description: "List attachments for a task"
|
|
50614
|
+
},
|
|
50615
|
+
args: {
|
|
50616
|
+
...globalArgs,
|
|
50617
|
+
id: {
|
|
50618
|
+
type: "positional",
|
|
50619
|
+
description: "Task ID",
|
|
50620
|
+
required: true
|
|
50621
|
+
}
|
|
50622
|
+
},
|
|
50623
|
+
async run({ args }) {
|
|
50624
|
+
if (args.json)
|
|
50625
|
+
setJsonOutput(true);
|
|
50626
|
+
await handleTasksCommand("attachments", ["list", args.id], toFlags(args));
|
|
50627
|
+
}
|
|
50628
|
+
});
|
|
50629
|
+
var tasksAttachmentsUploadCommand = defineCommand({
|
|
50630
|
+
meta: {
|
|
50631
|
+
name: "upload",
|
|
50632
|
+
description: "Upload a file to a task"
|
|
50633
|
+
},
|
|
50634
|
+
args: {
|
|
50635
|
+
...globalArgs,
|
|
50636
|
+
id: {
|
|
50637
|
+
type: "positional",
|
|
50638
|
+
description: "Task ID",
|
|
50639
|
+
required: true
|
|
50640
|
+
},
|
|
50641
|
+
file: {
|
|
50642
|
+
type: "positional",
|
|
50643
|
+
description: "File path to upload",
|
|
50644
|
+
required: true
|
|
50645
|
+
}
|
|
50646
|
+
},
|
|
50647
|
+
async run({ args }) {
|
|
50648
|
+
if (args.json)
|
|
50649
|
+
setJsonOutput(true);
|
|
50650
|
+
await handleTasksCommand("attachments", ["upload", args.id, args.file], toFlags(args));
|
|
50651
|
+
}
|
|
50652
|
+
});
|
|
50653
|
+
var tasksAttachmentsDeleteCommand = defineCommand({
|
|
50654
|
+
meta: {
|
|
50655
|
+
name: "delete",
|
|
50656
|
+
description: "Delete an attachment from a task"
|
|
50657
|
+
},
|
|
50658
|
+
args: {
|
|
50659
|
+
...globalArgs,
|
|
50660
|
+
id: {
|
|
50661
|
+
type: "positional",
|
|
50662
|
+
description: "Task ID",
|
|
50663
|
+
required: true
|
|
50664
|
+
},
|
|
50665
|
+
"attachment-id": {
|
|
50666
|
+
type: "positional",
|
|
50667
|
+
description: "Attachment ID",
|
|
50668
|
+
required: true
|
|
50669
|
+
}
|
|
50670
|
+
},
|
|
50671
|
+
async run({ args }) {
|
|
50672
|
+
if (args.json)
|
|
50673
|
+
setJsonOutput(true);
|
|
50674
|
+
await handleTasksCommand("attachments", ["delete", args.id, args["attachment-id"]], toFlags(args));
|
|
50675
|
+
}
|
|
50676
|
+
});
|
|
50677
|
+
var tasksAttachmentsPathCommand = defineCommand({
|
|
50678
|
+
meta: {
|
|
50679
|
+
name: "path",
|
|
50680
|
+
description: "Get local file path for an attachment"
|
|
50681
|
+
},
|
|
50682
|
+
args: {
|
|
50683
|
+
...globalArgs,
|
|
50684
|
+
id: {
|
|
50685
|
+
type: "positional",
|
|
50686
|
+
description: "Task ID",
|
|
50687
|
+
required: true
|
|
50688
|
+
},
|
|
50689
|
+
"attachment-id": {
|
|
50690
|
+
type: "positional",
|
|
50691
|
+
description: "Attachment ID",
|
|
50692
|
+
required: true
|
|
50693
|
+
}
|
|
50694
|
+
},
|
|
50695
|
+
async run({ args }) {
|
|
50696
|
+
if (args.json)
|
|
50697
|
+
setJsonOutput(true);
|
|
50698
|
+
await handleTasksCommand("attachments", ["path", args.id, args["attachment-id"]], toFlags(args));
|
|
50699
|
+
}
|
|
50700
|
+
});
|
|
50701
|
+
var tasksAttachmentsCommand = defineCommand({
|
|
50702
|
+
meta: {
|
|
50703
|
+
name: "attachments",
|
|
50704
|
+
description: "Manage task attachments"
|
|
50705
|
+
},
|
|
50706
|
+
subCommands: {
|
|
50707
|
+
list: tasksAttachmentsListCommand,
|
|
50708
|
+
upload: tasksAttachmentsUploadCommand,
|
|
50709
|
+
delete: tasksAttachmentsDeleteCommand,
|
|
50710
|
+
path: tasksAttachmentsPathCommand
|
|
50711
|
+
}
|
|
50712
|
+
});
|
|
50248
50713
|
var tasksCommand = defineCommand({
|
|
50249
50714
|
meta: {
|
|
50250
50715
|
name: "tasks",
|
|
@@ -50262,7 +50727,9 @@ var tasksCommand = defineCommand({
|
|
|
50262
50727
|
"set-due-date": tasksSetDueDateCommand,
|
|
50263
50728
|
"add-dependency": tasksAddDependencyCommand,
|
|
50264
50729
|
"remove-dependency": tasksRemoveDependencyCommand,
|
|
50265
|
-
"list-dependencies": tasksListDependenciesCommand
|
|
50730
|
+
"list-dependencies": tasksListDependenciesCommand,
|
|
50731
|
+
labels: tasksLabelsCommand,
|
|
50732
|
+
attachments: tasksAttachmentsCommand
|
|
50266
50733
|
}
|
|
50267
50734
|
});
|
|
50268
50735
|
var projectsListCommand = defineCommand({
|