@knowsuchagency/fulcrum 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/fulcrum.js +1578 -1472
- package/package.json +1 -1
package/bin/fulcrum.js
CHANGED
|
@@ -47617,8 +47617,8 @@ async function handleFsCommand(action, positional, flags) {
|
|
|
47617
47617
|
|
|
47618
47618
|
// cli/src/commands/up.ts
|
|
47619
47619
|
import { spawn } from "child_process";
|
|
47620
|
-
import { existsSync as
|
|
47621
|
-
import { dirname as dirname2, join as
|
|
47620
|
+
import { existsSync as existsSync4 } from "fs";
|
|
47621
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
47622
47622
|
import { fileURLToPath } from "url";
|
|
47623
47623
|
init_errors();
|
|
47624
47624
|
|
|
@@ -47899,1367 +47899,335 @@ function installUv() {
|
|
|
47899
47899
|
return false;
|
|
47900
47900
|
return installDependency(dep);
|
|
47901
47901
|
}
|
|
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
47902
|
|
|
47996
|
-
// cli/src/commands/
|
|
47997
|
-
|
|
47998
|
-
|
|
47999
|
-
|
|
48000
|
-
|
|
48001
|
-
|
|
48002
|
-
|
|
48003
|
-
|
|
48004
|
-
|
|
48005
|
-
|
|
48006
|
-
|
|
47903
|
+
// cli/src/commands/claude.ts
|
|
47904
|
+
init_errors();
|
|
47905
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, existsSync as existsSync3, rmSync, readFileSync as readFileSync4 } from "fs";
|
|
47906
|
+
import { homedir as homedir2 } from "os";
|
|
47907
|
+
import { join as join3 } from "path";
|
|
47908
|
+
|
|
47909
|
+
// plugins/fulcrum/.claude-plugin/plugin.json
|
|
47910
|
+
var plugin_default = `{
|
|
47911
|
+
"name": "fulcrum",
|
|
47912
|
+
"description": "Fulcrum task orchestration for Claude Code",
|
|
47913
|
+
"version": "1.4.0",
|
|
47914
|
+
"author": {
|
|
47915
|
+
"name": "Fulcrum"
|
|
47916
|
+
},
|
|
47917
|
+
"hooks": "./hooks/hooks.json",
|
|
47918
|
+
"mcpServers": "./.mcp.json",
|
|
47919
|
+
"skills": "./skills/",
|
|
47920
|
+
"commands": "./commands/"
|
|
48007
47921
|
}
|
|
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);
|
|
48027
|
-
}
|
|
48028
|
-
console.error("Bun installed successfully!");
|
|
48029
|
-
} else {
|
|
48030
|
-
throw new CliError("MISSING_DEPENDENCY", `Bun is required. Install manually: ${getInstallCommand(bunDep)}`, ExitCodes.ERROR);
|
|
48031
|
-
}
|
|
48032
|
-
}
|
|
48033
|
-
if (!isDtachInstalled()) {
|
|
48034
|
-
const dtachDep = getDependency("dtach");
|
|
48035
|
-
const method = getInstallMethod(dtachDep);
|
|
48036
|
-
console.error("dtach is required for terminal persistence but is not installed.");
|
|
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);
|
|
47922
|
+
`;
|
|
47923
|
+
|
|
47924
|
+
// plugins/fulcrum/hooks/hooks.json
|
|
47925
|
+
var hooks_default = `{
|
|
47926
|
+
"hooks": {
|
|
47927
|
+
"Stop": [
|
|
47928
|
+
{
|
|
47929
|
+
"hooks": [
|
|
47930
|
+
{
|
|
47931
|
+
"type": "command",
|
|
47932
|
+
"command": "fulcrum current-task review 2>/dev/null || true"
|
|
47933
|
+
}
|
|
47934
|
+
]
|
|
48043
47935
|
}
|
|
48044
|
-
|
|
48045
|
-
|
|
48046
|
-
|
|
48047
|
-
|
|
48048
|
-
|
|
48049
|
-
|
|
48050
|
-
|
|
48051
|
-
|
|
48052
|
-
|
|
48053
|
-
console.error(" uv is a fast Python package manager used by Claude Code.");
|
|
48054
|
-
const shouldInstall = autoYes || await confirm(`Would you like to install uv via ${method}?`);
|
|
48055
|
-
if (shouldInstall) {
|
|
48056
|
-
const success2 = installUv();
|
|
48057
|
-
if (!success2) {
|
|
48058
|
-
throw new CliError("INSTALL_FAILED", "Failed to install uv", ExitCodes.ERROR);
|
|
47936
|
+
],
|
|
47937
|
+
"UserPromptSubmit": [
|
|
47938
|
+
{
|
|
47939
|
+
"hooks": [
|
|
47940
|
+
{
|
|
47941
|
+
"type": "command",
|
|
47942
|
+
"command": "fulcrum current-task in-progress 2>/dev/null || true"
|
|
47943
|
+
}
|
|
47944
|
+
]
|
|
48059
47945
|
}
|
|
48060
|
-
|
|
48061
|
-
} else {
|
|
48062
|
-
throw new CliError("MISSING_DEPENDENCY", `uv is required. Install manually: ${getInstallCommand(uvDep)}`, ExitCodes.ERROR);
|
|
48063
|
-
}
|
|
47946
|
+
]
|
|
48064
47947
|
}
|
|
48065
|
-
|
|
48066
|
-
|
|
48067
|
-
|
|
48068
|
-
|
|
48069
|
-
|
|
48070
|
-
|
|
48071
|
-
|
|
48072
|
-
|
|
48073
|
-
|
|
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);
|
|
47948
|
+
}
|
|
47949
|
+
`;
|
|
47950
|
+
|
|
47951
|
+
// plugins/fulcrum/.mcp.json
|
|
47952
|
+
var _mcp_default = `{
|
|
47953
|
+
"mcpServers": {
|
|
47954
|
+
"fulcrum": {
|
|
47955
|
+
"command": "fulcrum",
|
|
47956
|
+
"args": ["mcp"]
|
|
48084
47957
|
}
|
|
48085
47958
|
}
|
|
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
47959
|
}
|
|
48145
|
-
|
|
48146
|
-
console.error(`
|
|
48147
|
-
Fulcrum is running at http://localhost:${port}
|
|
47960
|
+
`;
|
|
48148
47961
|
|
|
48149
|
-
|
|
48150
|
-
|
|
48151
|
-
|
|
48152
|
-
|
|
48153
|
-
|
|
47962
|
+
// plugins/fulcrum/commands/pr.md
|
|
47963
|
+
var pr_default = `---
|
|
47964
|
+
description: Link a GitHub PR to the current fulcrum task
|
|
47965
|
+
---
|
|
47966
|
+
Link the PR to this task: \`fulcrum current-task pr $ARGUMENTS\`
|
|
48154
47967
|
|
|
48155
|
-
|
|
48156
|
-
|
|
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
|
-
}
|
|
47968
|
+
This enables auto-completion when the PR is merged.
|
|
47969
|
+
`;
|
|
48167
47970
|
|
|
48168
|
-
//
|
|
48169
|
-
|
|
48170
|
-
|
|
48171
|
-
|
|
48172
|
-
|
|
48173
|
-
|
|
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
|
-
}
|
|
47971
|
+
// plugins/fulcrum/commands/task-info.md
|
|
47972
|
+
var task_info_default = `---
|
|
47973
|
+
description: Show current fulcrum task info
|
|
47974
|
+
---
|
|
47975
|
+
Show current task details: \`fulcrum current-task\`
|
|
47976
|
+
`;
|
|
48206
47977
|
|
|
48207
|
-
//
|
|
48208
|
-
|
|
48209
|
-
|
|
48210
|
-
|
|
48211
|
-
|
|
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
|
-
}
|
|
47978
|
+
// plugins/fulcrum/commands/notify.md
|
|
47979
|
+
var notify_default = `---
|
|
47980
|
+
description: Send a notification to the user
|
|
47981
|
+
---
|
|
47982
|
+
Send a notification: \`fulcrum notify $ARGUMENTS\`
|
|
48263
47983
|
|
|
48264
|
-
|
|
48265
|
-
|
|
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
|
-
}
|
|
47984
|
+
Format: fulcrum notify "Title" "Message body"
|
|
47985
|
+
`;
|
|
48313
47986
|
|
|
48314
|
-
//
|
|
48315
|
-
|
|
48316
|
-
|
|
48317
|
-
|
|
48318
|
-
|
|
48319
|
-
|
|
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
|
-
}
|
|
47987
|
+
// plugins/fulcrum/commands/linear.md
|
|
47988
|
+
var linear_default = `---
|
|
47989
|
+
description: Link a Linear ticket to the current fulcrum task
|
|
47990
|
+
---
|
|
47991
|
+
Link the Linear ticket to this task: \`fulcrum current-task linear $ARGUMENTS\`
|
|
47992
|
+
`;
|
|
48377
47993
|
|
|
48378
|
-
//
|
|
48379
|
-
|
|
48380
|
-
|
|
48381
|
-
|
|
48382
|
-
|
|
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
|
-
}
|
|
47994
|
+
// plugins/fulcrum/commands/review.md
|
|
47995
|
+
var review_default = `---
|
|
47996
|
+
description: Mark the current fulcrum task as ready for review
|
|
47997
|
+
---
|
|
47998
|
+
Mark this task ready for review: \`fulcrum current-task review\`
|
|
48420
47999
|
|
|
48421
|
-
|
|
48422
|
-
|
|
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
|
-
}
|
|
48000
|
+
This sends a notification to the user.
|
|
48001
|
+
`;
|
|
48488
48002
|
|
|
48489
|
-
//
|
|
48490
|
-
|
|
48491
|
-
|
|
48492
|
-
|
|
48493
|
-
|
|
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";
|
|
48003
|
+
// plugins/fulcrum/skills/vibora/SKILL.md
|
|
48004
|
+
var SKILL_default = `---
|
|
48005
|
+
name: fulcrum
|
|
48006
|
+
description: Fulcrum is a terminal-first tool for orchestrating AI coding agents across isolated git worktrees. Use this skill when working in a Fulcrum task worktree or managing tasks.
|
|
48007
|
+
---
|
|
48502
48008
|
|
|
48503
|
-
|
|
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"
|
|
48009
|
+
# Fulcrum - AI Agent Orchestration
|
|
48509
48010
|
|
|
48510
|
-
|
|
48011
|
+
## Overview
|
|
48511
48012
|
|
|
48512
|
-
|
|
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
|
-
}
|
|
48013
|
+
Fulcrum is a terminal-first tool for orchestrating AI coding agents (like Claude Code) across isolated git worktrees. Each task runs in its own worktree, enabling parallel work on multiple features or fixes without branch switching.
|
|
48526
48014
|
|
|
48527
|
-
|
|
48528
|
-
|
|
48529
|
-
|
|
48530
|
-
|
|
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
|
|
48015
|
+
**Philosophy:**
|
|
48016
|
+
- Agents run natively in terminals - no abstraction layer or wrapper APIs
|
|
48017
|
+
- Tasks create isolated git worktrees for clean separation
|
|
48018
|
+
- Persistent terminals organized in tabs across tasks
|
|
48540
48019
|
|
|
48541
|
-
|
|
48020
|
+
## When to Use This Skill
|
|
48542
48021
|
|
|
48543
|
-
|
|
48544
|
-
|
|
48545
|
-
|
|
48546
|
-
|
|
48547
|
-
|
|
48548
|
-
|
|
48549
|
-
|
|
48022
|
+
Use the Fulcrum CLI when:
|
|
48023
|
+
- **Working in a task worktree** - Use \`current-task\` commands to manage your current task
|
|
48024
|
+
- **Updating task status** - Mark tasks as in-progress, ready for review, done, or canceled
|
|
48025
|
+
- **Linking PRs** - Associate a GitHub PR with the current task
|
|
48026
|
+
- **Linking Linear tickets** - Connect a Linear issue to the current task
|
|
48027
|
+
- **Linking URLs** - Attach any relevant URLs (design docs, specs, external resources) to the task
|
|
48028
|
+
- **Sending notifications** - Alert the user when work is complete or needs attention
|
|
48550
48029
|
|
|
48551
|
-
|
|
48552
|
-
|
|
48553
|
-
|
|
48030
|
+
Use the Fulcrum MCP tools when:
|
|
48031
|
+
- **Executing commands remotely** - Run shell commands on the Fulcrum server from Claude Desktop
|
|
48032
|
+
- **Stateful workflows** - Use persistent sessions to maintain environment variables and working directory across commands
|
|
48554
48033
|
|
|
48555
|
-
|
|
48556
|
-
stderr += data.toString()
|
|
48557
|
-
})
|
|
48034
|
+
## Core CLI Commands
|
|
48558
48035
|
|
|
48559
|
-
|
|
48560
|
-
cleanup()
|
|
48561
|
-
if (!resolved) {
|
|
48562
|
-
resolved = true
|
|
48563
|
-
resolve({ exitCode: code || 0, stdout, stderr })
|
|
48564
|
-
}
|
|
48565
|
-
})
|
|
48036
|
+
### current-task (Primary Agent Workflow)
|
|
48566
48037
|
|
|
48567
|
-
|
|
48568
|
-
cleanup()
|
|
48569
|
-
if (!resolved) {
|
|
48570
|
-
resolved = true
|
|
48571
|
-
resolve({ exitCode: 1, stdout, stderr: err.message || '' })
|
|
48572
|
-
}
|
|
48573
|
-
})
|
|
48038
|
+
When running inside a Fulcrum task worktree, use these commands to manage the current task:
|
|
48574
48039
|
|
|
48575
|
-
|
|
48576
|
-
|
|
48577
|
-
|
|
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)
|
|
48040
|
+
\`\`\`bash
|
|
48041
|
+
# Get current task info (JSON output)
|
|
48042
|
+
fulcrum current-task
|
|
48591
48043
|
|
|
48592
|
-
|
|
48593
|
-
|
|
48594
|
-
|
|
48595
|
-
|
|
48044
|
+
# Update task status
|
|
48045
|
+
fulcrum current-task in-progress # Mark as IN_PROGRESS
|
|
48046
|
+
fulcrum current-task review # Mark as IN_REVIEW (notifies user)
|
|
48047
|
+
fulcrum current-task done # Mark as DONE
|
|
48048
|
+
fulcrum current-task cancel # Mark as CANCELED
|
|
48596
48049
|
|
|
48597
|
-
|
|
48598
|
-
|
|
48599
|
-
let pendingIdleTimer: ReturnType<typeof setTimeout> | null = null
|
|
48600
|
-
let activityVersion = 0
|
|
48601
|
-
let lastStatus: "in-progress" | "review" | "" = ""
|
|
48050
|
+
# Link a GitHub PR to the current task
|
|
48051
|
+
fulcrum current-task pr <github-pr-url>
|
|
48602
48052
|
|
|
48603
|
-
|
|
48604
|
-
|
|
48605
|
-
const FULCRUM_COMMAND_TIMEOUT_MS = 10000
|
|
48606
|
-
const STATUS_CHANGE_DEBOUNCE_MS = 500
|
|
48053
|
+
# Link a Linear ticket to the current task
|
|
48054
|
+
fulcrum current-task linear <linear-issue-url>
|
|
48607
48055
|
|
|
48608
|
-
|
|
48609
|
-
|
|
48610
|
-
|
|
48056
|
+
# Add arbitrary URL links to the task
|
|
48057
|
+
fulcrum current-task link <url> # Add link (auto-detects type/label)
|
|
48058
|
+
fulcrum current-task link <url> --label "Docs" # Add link with custom label
|
|
48059
|
+
fulcrum current-task link # List all links
|
|
48060
|
+
fulcrum current-task link --remove <url-or-id> # Remove a link
|
|
48061
|
+
\`\`\`
|
|
48611
48062
|
|
|
48612
|
-
|
|
48613
|
-
log("Plugin initializing...")
|
|
48063
|
+
### tasks
|
|
48614
48064
|
|
|
48615
|
-
|
|
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
|
-
}
|
|
48065
|
+
Manage tasks across the system:
|
|
48637
48066
|
|
|
48638
|
-
|
|
48067
|
+
\`\`\`bash
|
|
48068
|
+
# List all tasks
|
|
48069
|
+
fulcrum tasks list
|
|
48070
|
+
fulcrum tasks list --status=IN_PROGRESS # Filter by status
|
|
48071
|
+
fulcrum tasks list --search="ocai" # Search by title, labels
|
|
48072
|
+
fulcrum tasks list --label="bug" # Filter by label
|
|
48639
48073
|
|
|
48640
|
-
|
|
48641
|
-
|
|
48642
|
-
|
|
48643
|
-
isFulcrumContext = await deferredContextCheck
|
|
48644
|
-
deferredContextCheck = null
|
|
48645
|
-
return isFulcrumContext
|
|
48646
|
-
}
|
|
48647
|
-
return false
|
|
48648
|
-
}
|
|
48074
|
+
# List all labels in use
|
|
48075
|
+
fulcrum tasks labels # Show all labels with counts
|
|
48076
|
+
fulcrum tasks labels --search="comm" # Find labels matching substring
|
|
48649
48077
|
|
|
48650
|
-
|
|
48651
|
-
|
|
48652
|
-
clearTimeout(pendingIdleTimer)
|
|
48653
|
-
pendingIdleTimer = null
|
|
48654
|
-
log("Cancelled pending idle transition")
|
|
48655
|
-
}
|
|
48656
|
-
}
|
|
48078
|
+
# Get a specific task
|
|
48079
|
+
fulcrum tasks get <task-id>
|
|
48657
48080
|
|
|
48658
|
-
|
|
48659
|
-
|
|
48081
|
+
# Create a new task
|
|
48082
|
+
fulcrum tasks create --title="My Task" --repo=/path/to/repo
|
|
48660
48083
|
|
|
48661
|
-
|
|
48084
|
+
# Update task metadata
|
|
48085
|
+
fulcrum tasks update <task-id> --title="New Title"
|
|
48662
48086
|
|
|
48663
|
-
|
|
48664
|
-
|
|
48665
|
-
setTimeout(() => setStatus(status), STATUS_CHANGE_DEBOUNCE_MS)
|
|
48666
|
-
return
|
|
48667
|
-
}
|
|
48087
|
+
# Move task to different status
|
|
48088
|
+
fulcrum tasks move <task-id> --status=IN_REVIEW
|
|
48668
48089
|
|
|
48669
|
-
|
|
48090
|
+
# Delete a task
|
|
48091
|
+
fulcrum tasks delete <task-id>
|
|
48092
|
+
fulcrum tasks delete <task-id> --delete-worktree # Also delete worktree
|
|
48093
|
+
\`\`\`
|
|
48670
48094
|
|
|
48671
|
-
|
|
48672
|
-
try {
|
|
48673
|
-
log(\`Setting status: \${status}\`)
|
|
48674
|
-
pendingStatusCommand = runFulcrumCommand(['current-task', status, '--path', directory])
|
|
48675
|
-
const res = await pendingStatusCommand
|
|
48676
|
-
pendingStatusCommand = null
|
|
48095
|
+
### notifications
|
|
48677
48096
|
|
|
48678
|
-
|
|
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
|
-
}
|
|
48097
|
+
Send notifications to the user:
|
|
48687
48098
|
|
|
48688
|
-
|
|
48689
|
-
|
|
48690
|
-
|
|
48099
|
+
\`\`\`bash
|
|
48100
|
+
# Send a notification
|
|
48101
|
+
fulcrum notify "Title" "Message body"
|
|
48691
48102
|
|
|
48692
|
-
|
|
48693
|
-
|
|
48694
|
-
log(
|
|
48695
|
-
\`Stale idle transition (version \${currentVersion} vs \${activityVersion})\`,
|
|
48696
|
-
)
|
|
48697
|
-
return
|
|
48698
|
-
}
|
|
48699
|
-
setStatus("review")
|
|
48700
|
-
}, IDLE_CONFIRMATION_DELAY_MS)
|
|
48103
|
+
# Check notification settings
|
|
48104
|
+
fulcrum notifications
|
|
48701
48105
|
|
|
48702
|
-
|
|
48703
|
-
|
|
48704
|
-
|
|
48705
|
-
}
|
|
48106
|
+
# Enable/disable notifications
|
|
48107
|
+
fulcrum notifications enable
|
|
48108
|
+
fulcrum notifications disable
|
|
48706
48109
|
|
|
48707
|
-
|
|
48708
|
-
|
|
48709
|
-
|
|
48710
|
-
|
|
48711
|
-
|
|
48110
|
+
# Test a notification channel
|
|
48111
|
+
fulcrum notifications test sound
|
|
48112
|
+
fulcrum notifications test slack
|
|
48113
|
+
fulcrum notifications test discord
|
|
48114
|
+
fulcrum notifications test pushover
|
|
48712
48115
|
|
|
48713
|
-
|
|
48714
|
-
|
|
48715
|
-
|
|
48116
|
+
# Configure a channel
|
|
48117
|
+
fulcrum notifications set slack webhookUrl <url>
|
|
48118
|
+
\`\`\`
|
|
48716
48119
|
|
|
48717
|
-
|
|
48718
|
-
recordActivity("user message")
|
|
48719
|
-
setStatus("in-progress")
|
|
48720
|
-
} else if (output.message.role === "assistant") {
|
|
48721
|
-
recordActivity("assistant message")
|
|
48722
|
-
}
|
|
48723
|
-
},
|
|
48120
|
+
### Server Management
|
|
48724
48121
|
|
|
48725
|
-
|
|
48726
|
-
|
|
48727
|
-
|
|
48728
|
-
|
|
48122
|
+
\`\`\`bash
|
|
48123
|
+
fulcrum up # Start Fulcrum server daemon
|
|
48124
|
+
fulcrum down # Stop Fulcrum server
|
|
48125
|
+
fulcrum status # Check if server is running
|
|
48126
|
+
fulcrum health # Check server health
|
|
48127
|
+
\`\`\`
|
|
48729
48128
|
|
|
48730
|
-
|
|
48129
|
+
### Git Operations
|
|
48731
48130
|
|
|
48732
|
-
|
|
48131
|
+
\`\`\`bash
|
|
48132
|
+
fulcrum git status # Git status for current worktree
|
|
48133
|
+
fulcrum git diff # Git diff for current worktree
|
|
48134
|
+
fulcrum worktrees list # List all worktrees
|
|
48135
|
+
\`\`\`
|
|
48733
48136
|
|
|
48734
|
-
|
|
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
|
|
48137
|
+
### projects
|
|
48738
48138
|
|
|
48739
|
-
|
|
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
|
-
}
|
|
48139
|
+
Manage projects (repositories with metadata):
|
|
48746
48140
|
|
|
48747
|
-
|
|
48748
|
-
|
|
48749
|
-
|
|
48750
|
-
|
|
48141
|
+
\`\`\`bash
|
|
48142
|
+
# List all projects
|
|
48143
|
+
fulcrum projects list
|
|
48144
|
+
fulcrum projects list --status=active # Filter by status (active, archived)
|
|
48751
48145
|
|
|
48752
|
-
|
|
48753
|
-
|
|
48754
|
-
(event.type === "session.status" && status?.type === "busy") ||
|
|
48755
|
-
event.type.startsWith("tool.execute")
|
|
48756
|
-
) {
|
|
48757
|
-
recordActivity(event.type)
|
|
48758
|
-
return
|
|
48759
|
-
}
|
|
48146
|
+
# Get project details
|
|
48147
|
+
fulcrum projects get <project-id>
|
|
48760
48148
|
|
|
48761
|
-
|
|
48762
|
-
|
|
48763
|
-
|
|
48764
|
-
|
|
48765
|
-
const info = (props.info as Record<string, unknown>) || {}
|
|
48766
|
-
const sessionId =
|
|
48767
|
-
(props.sessionID as string) || (info.id as string) || null
|
|
48149
|
+
# Create a new project
|
|
48150
|
+
fulcrum projects create --name="My Project" --path=/path/to/repo # From local path
|
|
48151
|
+
fulcrum projects create --name="My Project" --url=https://github.com/... # Clone from URL
|
|
48152
|
+
fulcrum projects create --name="My Project" --repository-id=<repo-id> # Link existing repo
|
|
48768
48153
|
|
|
48769
|
-
|
|
48770
|
-
|
|
48771
|
-
|
|
48772
|
-
}
|
|
48154
|
+
# Update project
|
|
48155
|
+
fulcrum projects update <project-id> --name="New Name"
|
|
48156
|
+
fulcrum projects update <project-id> --status=archived
|
|
48773
48157
|
|
|
48774
|
-
|
|
48775
|
-
|
|
48776
|
-
|
|
48777
|
-
|
|
48158
|
+
# Delete project
|
|
48159
|
+
fulcrum projects delete <project-id>
|
|
48160
|
+
fulcrum projects delete <project-id> --delete-directory # Also delete directory
|
|
48161
|
+
fulcrum projects delete <project-id> --delete-app # Also delete linked app
|
|
48778
48162
|
|
|
48779
|
-
|
|
48780
|
-
|
|
48781
|
-
|
|
48782
|
-
},
|
|
48783
|
-
}
|
|
48784
|
-
}
|
|
48785
|
-
`;
|
|
48163
|
+
# Scan for git repositories
|
|
48164
|
+
fulcrum projects scan # Scan default directory
|
|
48165
|
+
fulcrum projects scan --directory=/path # Scan specific directory
|
|
48786
48166
|
|
|
48787
|
-
|
|
48788
|
-
|
|
48789
|
-
|
|
48790
|
-
|
|
48791
|
-
|
|
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
|
-
}
|
|
48167
|
+
# Manage project links (URLs)
|
|
48168
|
+
fulcrum projects links list <project-id>
|
|
48169
|
+
fulcrum projects links add <project-id> <url> --label="Custom Label"
|
|
48170
|
+
fulcrum projects links remove <project-id> <link-id>
|
|
48171
|
+
\`\`\`
|
|
48937
48172
|
|
|
48938
|
-
|
|
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";
|
|
48173
|
+
### repositories
|
|
48943
48174
|
|
|
48944
|
-
|
|
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
|
-
`;
|
|
48175
|
+
Manage repositories (code sources that can be linked to projects):
|
|
48955
48176
|
|
|
48956
|
-
|
|
48957
|
-
|
|
48958
|
-
|
|
48959
|
-
|
|
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
|
-
`;
|
|
48177
|
+
\`\`\`bash
|
|
48178
|
+
# List repositories
|
|
48179
|
+
fulcrum repositories list
|
|
48180
|
+
fulcrum repositories list --orphans # Unlinked repos only
|
|
48181
|
+
fulcrum repositories list --project-id=<id> # Filter by project
|
|
48982
48182
|
|
|
48983
|
-
|
|
48984
|
-
|
|
48985
|
-
"mcpServers": {
|
|
48986
|
-
"fulcrum": {
|
|
48987
|
-
"command": "fulcrum",
|
|
48988
|
-
"args": ["mcp"]
|
|
48989
|
-
}
|
|
48990
|
-
}
|
|
48991
|
-
}
|
|
48992
|
-
`;
|
|
48183
|
+
# Get repository details
|
|
48184
|
+
fulcrum repositories get <repo-id>
|
|
48993
48185
|
|
|
48994
|
-
|
|
48995
|
-
|
|
48996
|
-
|
|
48997
|
-
---
|
|
48998
|
-
Link the PR to this task: \`fulcrum current-task pr $ARGUMENTS\`
|
|
48186
|
+
# Add a new repository from local path
|
|
48187
|
+
fulcrum repositories add --path=/path/to/repo
|
|
48188
|
+
fulcrum repositories add --path=/path/to/repo --display-name="My Repo"
|
|
48999
48189
|
|
|
49000
|
-
|
|
49001
|
-
|
|
48190
|
+
# Update repository
|
|
48191
|
+
fulcrum repositories update <repo-id> --display-name="New Name"
|
|
48192
|
+
fulcrum repositories update <repo-id> --default-agent=claude
|
|
48193
|
+
fulcrum repositories update <repo-id> --startup-script="mise run dev"
|
|
48194
|
+
fulcrum repositories update <repo-id> --copy-files=".env,.env.local"
|
|
49002
48195
|
|
|
49003
|
-
|
|
49004
|
-
|
|
49005
|
-
description: Show current fulcrum task info
|
|
49006
|
-
---
|
|
49007
|
-
Show current task details: \`fulcrum current-task\`
|
|
49008
|
-
`;
|
|
48196
|
+
# Delete orphaned repository (fails if linked to a project)
|
|
48197
|
+
fulcrum repositories delete <repo-id>
|
|
49009
48198
|
|
|
49010
|
-
|
|
49011
|
-
|
|
49012
|
-
|
|
49013
|
-
|
|
49014
|
-
Send a notification: \`fulcrum notify $ARGUMENTS\`
|
|
48199
|
+
# Link repository to project (repos can only be linked to one project)
|
|
48200
|
+
fulcrum repositories link <repo-id> <project-id>
|
|
48201
|
+
fulcrum repositories link <repo-id> <project-id> --as-primary
|
|
48202
|
+
fulcrum repositories link <repo-id> <project-id> --force # Move from existing project
|
|
49015
48203
|
|
|
49016
|
-
|
|
49017
|
-
|
|
48204
|
+
# Unlink repository from project
|
|
48205
|
+
fulcrum repositories unlink <repo-id> <project-id>
|
|
48206
|
+
\`\`\`
|
|
49018
48207
|
|
|
49019
|
-
|
|
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
|
-
`;
|
|
48208
|
+
### apps
|
|
49025
48209
|
|
|
49026
|
-
|
|
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\`
|
|
48210
|
+
Manage Docker Compose app deployments:
|
|
49031
48211
|
|
|
49032
|
-
|
|
49033
|
-
|
|
48212
|
+
\`\`\`bash
|
|
48213
|
+
# List all apps
|
|
48214
|
+
fulcrum apps list
|
|
48215
|
+
fulcrum apps list --status=running # Filter by status (stopped, building, running, failed)
|
|
49034
48216
|
|
|
49035
|
-
|
|
49036
|
-
|
|
49037
|
-
name: fulcrum
|
|
49038
|
-
description: Fulcrum is a terminal-first tool for orchestrating AI coding agents across isolated git worktrees. Use this skill when working in a Fulcrum task worktree or managing tasks.
|
|
49039
|
-
---
|
|
48217
|
+
# Get app details
|
|
48218
|
+
fulcrum apps get <app-id>
|
|
49040
48219
|
|
|
49041
|
-
#
|
|
48220
|
+
# Create a new app
|
|
48221
|
+
fulcrum apps create --name="My App" --repository-id=<repo-id>
|
|
48222
|
+
fulcrum apps create --name="My App" --repository-id=<repo-id> --branch=develop --auto-deploy
|
|
49042
48223
|
|
|
49043
|
-
|
|
48224
|
+
# Update app
|
|
48225
|
+
fulcrum apps update <app-id> --name="New Name"
|
|
48226
|
+
fulcrum apps update <app-id> --auto-deploy # Enable auto-deploy
|
|
48227
|
+
fulcrum apps update <app-id> --no-cache # Enable no-cache builds
|
|
49044
48228
|
|
|
49045
|
-
|
|
49046
|
-
|
|
49047
|
-
**Philosophy:**
|
|
49048
|
-
- Agents run natively in terminals - no abstraction layer or wrapper APIs
|
|
49049
|
-
- Tasks create isolated git worktrees for clean separation
|
|
49050
|
-
- Persistent terminals organized in tabs across tasks
|
|
49051
|
-
|
|
49052
|
-
## When to Use This Skill
|
|
49053
|
-
|
|
49054
|
-
Use the Fulcrum CLI when:
|
|
49055
|
-
- **Working in a task worktree** - Use \`current-task\` commands to manage your current task
|
|
49056
|
-
- **Updating task status** - Mark tasks as in-progress, ready for review, done, or canceled
|
|
49057
|
-
- **Linking PRs** - Associate a GitHub PR with the current task
|
|
49058
|
-
- **Linking Linear tickets** - Connect a Linear issue to the current task
|
|
49059
|
-
- **Linking URLs** - Attach any relevant URLs (design docs, specs, external resources) to the task
|
|
49060
|
-
- **Sending notifications** - Alert the user when work is complete or needs attention
|
|
49061
|
-
|
|
49062
|
-
Use the Fulcrum MCP tools when:
|
|
49063
|
-
- **Executing commands remotely** - Run shell commands on the Fulcrum server from Claude Desktop
|
|
49064
|
-
- **Stateful workflows** - Use persistent sessions to maintain environment variables and working directory across commands
|
|
49065
|
-
|
|
49066
|
-
## Core CLI Commands
|
|
49067
|
-
|
|
49068
|
-
### current-task (Primary Agent Workflow)
|
|
49069
|
-
|
|
49070
|
-
When running inside a Fulcrum task worktree, use these commands to manage the current task:
|
|
49071
|
-
|
|
49072
|
-
\`\`\`bash
|
|
49073
|
-
# Get current task info (JSON output)
|
|
49074
|
-
fulcrum current-task
|
|
49075
|
-
|
|
49076
|
-
# Update task status
|
|
49077
|
-
fulcrum current-task in-progress # Mark as IN_PROGRESS
|
|
49078
|
-
fulcrum current-task review # Mark as IN_REVIEW (notifies user)
|
|
49079
|
-
fulcrum current-task done # Mark as DONE
|
|
49080
|
-
fulcrum current-task cancel # Mark as CANCELED
|
|
49081
|
-
|
|
49082
|
-
# Link a GitHub PR to the current task
|
|
49083
|
-
fulcrum current-task pr <github-pr-url>
|
|
49084
|
-
|
|
49085
|
-
# Link a Linear ticket to the current task
|
|
49086
|
-
fulcrum current-task linear <linear-issue-url>
|
|
49087
|
-
|
|
49088
|
-
# Add arbitrary URL links to the task
|
|
49089
|
-
fulcrum current-task link <url> # Add link (auto-detects type/label)
|
|
49090
|
-
fulcrum current-task link <url> --label "Docs" # Add link with custom label
|
|
49091
|
-
fulcrum current-task link # List all links
|
|
49092
|
-
fulcrum current-task link --remove <url-or-id> # Remove a link
|
|
49093
|
-
\`\`\`
|
|
49094
|
-
|
|
49095
|
-
### tasks
|
|
49096
|
-
|
|
49097
|
-
Manage tasks across the system:
|
|
49098
|
-
|
|
49099
|
-
\`\`\`bash
|
|
49100
|
-
# List all tasks
|
|
49101
|
-
fulcrum tasks list
|
|
49102
|
-
fulcrum tasks list --status=IN_PROGRESS # Filter by status
|
|
49103
|
-
fulcrum tasks list --search="ocai" # Search by title, labels
|
|
49104
|
-
fulcrum tasks list --label="bug" # Filter by label
|
|
49105
|
-
|
|
49106
|
-
# List all labels in use
|
|
49107
|
-
fulcrum tasks labels # Show all labels with counts
|
|
49108
|
-
fulcrum tasks labels --search="comm" # Find labels matching substring
|
|
49109
|
-
|
|
49110
|
-
# Get a specific task
|
|
49111
|
-
fulcrum tasks get <task-id>
|
|
49112
|
-
|
|
49113
|
-
# Create a new task
|
|
49114
|
-
fulcrum tasks create --title="My Task" --repo=/path/to/repo
|
|
49115
|
-
|
|
49116
|
-
# Update task metadata
|
|
49117
|
-
fulcrum tasks update <task-id> --title="New Title"
|
|
49118
|
-
|
|
49119
|
-
# Move task to different status
|
|
49120
|
-
fulcrum tasks move <task-id> --status=IN_REVIEW
|
|
49121
|
-
|
|
49122
|
-
# Delete a task
|
|
49123
|
-
fulcrum tasks delete <task-id>
|
|
49124
|
-
fulcrum tasks delete <task-id> --delete-worktree # Also delete worktree
|
|
49125
|
-
\`\`\`
|
|
49126
|
-
|
|
49127
|
-
### notifications
|
|
49128
|
-
|
|
49129
|
-
Send notifications to the user:
|
|
49130
|
-
|
|
49131
|
-
\`\`\`bash
|
|
49132
|
-
# Send a notification
|
|
49133
|
-
fulcrum notify "Title" "Message body"
|
|
49134
|
-
|
|
49135
|
-
# Check notification settings
|
|
49136
|
-
fulcrum notifications
|
|
49137
|
-
|
|
49138
|
-
# Enable/disable notifications
|
|
49139
|
-
fulcrum notifications enable
|
|
49140
|
-
fulcrum notifications disable
|
|
49141
|
-
|
|
49142
|
-
# Test a notification channel
|
|
49143
|
-
fulcrum notifications test sound
|
|
49144
|
-
fulcrum notifications test slack
|
|
49145
|
-
fulcrum notifications test discord
|
|
49146
|
-
fulcrum notifications test pushover
|
|
49147
|
-
|
|
49148
|
-
# Configure a channel
|
|
49149
|
-
fulcrum notifications set slack webhookUrl <url>
|
|
49150
|
-
\`\`\`
|
|
49151
|
-
|
|
49152
|
-
### Server Management
|
|
49153
|
-
|
|
49154
|
-
\`\`\`bash
|
|
49155
|
-
fulcrum up # Start Fulcrum server daemon
|
|
49156
|
-
fulcrum down # Stop Fulcrum server
|
|
49157
|
-
fulcrum status # Check if server is running
|
|
49158
|
-
fulcrum health # Check server health
|
|
49159
|
-
\`\`\`
|
|
49160
|
-
|
|
49161
|
-
### Git Operations
|
|
49162
|
-
|
|
49163
|
-
\`\`\`bash
|
|
49164
|
-
fulcrum git status # Git status for current worktree
|
|
49165
|
-
fulcrum git diff # Git diff for current worktree
|
|
49166
|
-
fulcrum worktrees list # List all worktrees
|
|
49167
|
-
\`\`\`
|
|
49168
|
-
|
|
49169
|
-
### projects
|
|
49170
|
-
|
|
49171
|
-
Manage projects (repositories with metadata):
|
|
49172
|
-
|
|
49173
|
-
\`\`\`bash
|
|
49174
|
-
# List all projects
|
|
49175
|
-
fulcrum projects list
|
|
49176
|
-
fulcrum projects list --status=active # Filter by status (active, archived)
|
|
49177
|
-
|
|
49178
|
-
# Get project details
|
|
49179
|
-
fulcrum projects get <project-id>
|
|
49180
|
-
|
|
49181
|
-
# Create a new project
|
|
49182
|
-
fulcrum projects create --name="My Project" --path=/path/to/repo # From local path
|
|
49183
|
-
fulcrum projects create --name="My Project" --url=https://github.com/... # Clone from URL
|
|
49184
|
-
fulcrum projects create --name="My Project" --repository-id=<repo-id> # Link existing repo
|
|
49185
|
-
|
|
49186
|
-
# Update project
|
|
49187
|
-
fulcrum projects update <project-id> --name="New Name"
|
|
49188
|
-
fulcrum projects update <project-id> --status=archived
|
|
49189
|
-
|
|
49190
|
-
# Delete project
|
|
49191
|
-
fulcrum projects delete <project-id>
|
|
49192
|
-
fulcrum projects delete <project-id> --delete-directory # Also delete directory
|
|
49193
|
-
fulcrum projects delete <project-id> --delete-app # Also delete linked app
|
|
49194
|
-
|
|
49195
|
-
# Scan for git repositories
|
|
49196
|
-
fulcrum projects scan # Scan default directory
|
|
49197
|
-
fulcrum projects scan --directory=/path # Scan specific directory
|
|
49198
|
-
|
|
49199
|
-
# Manage project links (URLs)
|
|
49200
|
-
fulcrum projects links list <project-id>
|
|
49201
|
-
fulcrum projects links add <project-id> <url> --label="Custom Label"
|
|
49202
|
-
fulcrum projects links remove <project-id> <link-id>
|
|
49203
|
-
\`\`\`
|
|
49204
|
-
|
|
49205
|
-
### repositories
|
|
49206
|
-
|
|
49207
|
-
Manage repositories (code sources that can be linked to projects):
|
|
49208
|
-
|
|
49209
|
-
\`\`\`bash
|
|
49210
|
-
# List repositories
|
|
49211
|
-
fulcrum repositories list
|
|
49212
|
-
fulcrum repositories list --orphans # Unlinked repos only
|
|
49213
|
-
fulcrum repositories list --project-id=<id> # Filter by project
|
|
49214
|
-
|
|
49215
|
-
# Get repository details
|
|
49216
|
-
fulcrum repositories get <repo-id>
|
|
49217
|
-
|
|
49218
|
-
# Add a new repository from local path
|
|
49219
|
-
fulcrum repositories add --path=/path/to/repo
|
|
49220
|
-
fulcrum repositories add --path=/path/to/repo --display-name="My Repo"
|
|
49221
|
-
|
|
49222
|
-
# Update repository
|
|
49223
|
-
fulcrum repositories update <repo-id> --display-name="New Name"
|
|
49224
|
-
fulcrum repositories update <repo-id> --default-agent=claude
|
|
49225
|
-
fulcrum repositories update <repo-id> --startup-script="mise run dev"
|
|
49226
|
-
fulcrum repositories update <repo-id> --copy-files=".env,.env.local"
|
|
49227
|
-
|
|
49228
|
-
# Delete orphaned repository (fails if linked to a project)
|
|
49229
|
-
fulcrum repositories delete <repo-id>
|
|
49230
|
-
|
|
49231
|
-
# Link repository to project (repos can only be linked to one project)
|
|
49232
|
-
fulcrum repositories link <repo-id> <project-id>
|
|
49233
|
-
fulcrum repositories link <repo-id> <project-id> --as-primary
|
|
49234
|
-
fulcrum repositories link <repo-id> <project-id> --force # Move from existing project
|
|
49235
|
-
|
|
49236
|
-
# Unlink repository from project
|
|
49237
|
-
fulcrum repositories unlink <repo-id> <project-id>
|
|
49238
|
-
\`\`\`
|
|
49239
|
-
|
|
49240
|
-
### apps
|
|
49241
|
-
|
|
49242
|
-
Manage Docker Compose app deployments:
|
|
49243
|
-
|
|
49244
|
-
\`\`\`bash
|
|
49245
|
-
# List all apps
|
|
49246
|
-
fulcrum apps list
|
|
49247
|
-
fulcrum apps list --status=running # Filter by status (stopped, building, running, failed)
|
|
49248
|
-
|
|
49249
|
-
# Get app details
|
|
49250
|
-
fulcrum apps get <app-id>
|
|
49251
|
-
|
|
49252
|
-
# Create a new app
|
|
49253
|
-
fulcrum apps create --name="My App" --repository-id=<repo-id>
|
|
49254
|
-
fulcrum apps create --name="My App" --repository-id=<repo-id> --branch=develop --auto-deploy
|
|
49255
|
-
|
|
49256
|
-
# Update app
|
|
49257
|
-
fulcrum apps update <app-id> --name="New Name"
|
|
49258
|
-
fulcrum apps update <app-id> --auto-deploy # Enable auto-deploy
|
|
49259
|
-
fulcrum apps update <app-id> --no-cache # Enable no-cache builds
|
|
49260
|
-
|
|
49261
|
-
# Deploy an app
|
|
49262
|
-
fulcrum apps deploy <app-id>
|
|
48229
|
+
# Deploy an app
|
|
48230
|
+
fulcrum apps deploy <app-id>
|
|
49263
48231
|
|
|
49264
48232
|
# Stop an app
|
|
49265
48233
|
fulcrum apps stop <app-id>
|
|
@@ -49361,257 +48329,1395 @@ Fulcrum provides a comprehensive set of MCP tools for AI agents. Use \`search_to
|
|
|
49361
48329
|
|
|
49362
48330
|
### Tool Discovery
|
|
49363
48331
|
|
|
49364
|
-
#### search_tools
|
|
48332
|
+
#### search_tools
|
|
48333
|
+
|
|
48334
|
+
Search for available tools by keyword or category:
|
|
48335
|
+
|
|
48336
|
+
\`\`\`json
|
|
48337
|
+
{
|
|
48338
|
+
"query": "deploy", // Optional: Search term
|
|
48339
|
+
"category": "apps" // Optional: Filter by category
|
|
48340
|
+
}
|
|
48341
|
+
\`\`\`
|
|
48342
|
+
|
|
48343
|
+
**Categories:** core, tasks, projects, repositories, apps, filesystem, git, notifications, exec
|
|
48344
|
+
|
|
48345
|
+
**Example Usage:**
|
|
48346
|
+
\`\`\`
|
|
48347
|
+
search_tools { query: "project create" }
|
|
48348
|
+
\u2192 Returns tools for creating projects
|
|
48349
|
+
|
|
48350
|
+
search_tools { category: "filesystem" }
|
|
48351
|
+
\u2192 Returns all filesystem tools
|
|
48352
|
+
\`\`\`
|
|
48353
|
+
|
|
48354
|
+
### Task Tools
|
|
48355
|
+
|
|
48356
|
+
- \`list_tasks\` - List tasks with flexible filtering (search, labels, statuses, date range, overdue)
|
|
48357
|
+
- \`get_task\` - Get task details by ID
|
|
48358
|
+
- \`create_task\` - Create a new task with worktree
|
|
48359
|
+
- \`update_task\` - Update task metadata
|
|
48360
|
+
- \`delete_task\` - Delete a task
|
|
48361
|
+
- \`move_task\` - Move task to different status
|
|
48362
|
+
- \`add_task_link\` - Add URL link to task
|
|
48363
|
+
- \`remove_task_link\` - Remove link from task
|
|
48364
|
+
- \`list_task_links\` - List all task links
|
|
48365
|
+
- \`add_task_label\` - Add a label to a task (returns similar labels to catch typos)
|
|
48366
|
+
- \`remove_task_label\` - Remove a label from a task
|
|
48367
|
+
- \`set_task_due_date\` - Set or clear task due date
|
|
48368
|
+
- \`list_labels\` - List all unique labels in use with optional search
|
|
48369
|
+
|
|
48370
|
+
#### Task Search and Filtering
|
|
48371
|
+
|
|
48372
|
+
The \`list_tasks\` tool supports powerful filtering for AI agents:
|
|
48373
|
+
|
|
48374
|
+
\`\`\`json
|
|
48375
|
+
{
|
|
48376
|
+
"search": "ocai", // Text search across title, labels, project name
|
|
48377
|
+
"labels": ["bug", "urgent"], // Filter by multiple labels (OR logic)
|
|
48378
|
+
"statuses": ["TO_DO", "IN_PROGRESS"], // Filter by multiple statuses (OR logic)
|
|
48379
|
+
"dueDateStart": "2026-01-18", // Start of date range
|
|
48380
|
+
"dueDateEnd": "2026-01-25", // End of date range
|
|
48381
|
+
"overdue": true // Only show overdue tasks
|
|
48382
|
+
}
|
|
48383
|
+
\`\`\`
|
|
48384
|
+
|
|
48385
|
+
#### Label Discovery
|
|
48386
|
+
|
|
48387
|
+
Use \`list_labels\` to discover exact label names before filtering:
|
|
48388
|
+
|
|
48389
|
+
\`\`\`json
|
|
48390
|
+
// Find labels matching "communication"
|
|
48391
|
+
{ "search": "communication" }
|
|
48392
|
+
// Returns: [{ "name": "communication required", "count": 5 }]
|
|
48393
|
+
\`\`\`
|
|
48394
|
+
|
|
48395
|
+
This helps handle typos and variations - search first, then use the exact label name.
|
|
48396
|
+
|
|
48397
|
+
### Project Tools
|
|
48398
|
+
|
|
48399
|
+
- \`list_projects\` - List all projects
|
|
48400
|
+
- \`get_project\` - Get project details
|
|
48401
|
+
- \`create_project\` - Create from path, URL, or existing repo
|
|
48402
|
+
- \`update_project\` - Update name, description, status
|
|
48403
|
+
- \`delete_project\` - Delete project and optionally directory/app
|
|
48404
|
+
- \`scan_projects\` - Scan directory for git repos
|
|
48405
|
+
- \`list_project_links\` - List all URL links attached to a project
|
|
48406
|
+
- \`add_project_link\` - Add a URL link to a project (auto-detects type)
|
|
48407
|
+
- \`remove_project_link\` - Remove a URL link from a project
|
|
48408
|
+
|
|
48409
|
+
### Repository Tools
|
|
48410
|
+
|
|
48411
|
+
- \`list_repositories\` - List all repositories (supports orphans filter)
|
|
48412
|
+
- \`get_repository\` - Get repository details by ID
|
|
48413
|
+
- \`add_repository\` - Add repository from local path
|
|
48414
|
+
- \`update_repository\` - Update repository metadata (name, agent, startup script)
|
|
48415
|
+
- \`delete_repository\` - Delete orphaned repository (fails if linked to project)
|
|
48416
|
+
- \`link_repository_to_project\` - Link repo to project (errors if already linked elsewhere)
|
|
48417
|
+
- \`unlink_repository_from_project\` - Unlink repo from project
|
|
48418
|
+
|
|
48419
|
+
### App/Deployment Tools
|
|
48420
|
+
|
|
48421
|
+
- \`list_apps\` - List all deployed apps
|
|
48422
|
+
- \`get_app\` - Get app details with services
|
|
48423
|
+
- \`create_app\` - Create app for deployment
|
|
48424
|
+
- \`deploy_app\` - Trigger deployment
|
|
48425
|
+
- \`stop_app\` - Stop running app
|
|
48426
|
+
- \`get_app_logs\` - Get container logs
|
|
48427
|
+
- \`get_app_status\` - Get container status
|
|
48428
|
+
- \`list_deployments\` - Get deployment history
|
|
48429
|
+
- \`delete_app\` - Delete app
|
|
48430
|
+
|
|
48431
|
+
### Filesystem Tools
|
|
48432
|
+
|
|
48433
|
+
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).
|
|
48434
|
+
|
|
48435
|
+
- \`list_directory\` - List directory contents
|
|
48436
|
+
- \`get_file_tree\` - Get recursive file tree
|
|
48437
|
+
- \`read_file\` - Read file contents (secured)
|
|
48438
|
+
- \`write_file\` - Write entire file content (secured)
|
|
48439
|
+
- \`edit_file\` - Edit file by replacing a unique string (secured)
|
|
48440
|
+
- \`file_stat\` - Get file/directory metadata
|
|
48441
|
+
- \`is_git_repo\` - Check if directory is git repo
|
|
48442
|
+
|
|
48443
|
+
### Command Execution
|
|
48444
|
+
|
|
48445
|
+
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.
|
|
48446
|
+
|
|
48447
|
+
#### execute_command
|
|
48448
|
+
|
|
48449
|
+
Execute shell commands with optional persistent session support:
|
|
48450
|
+
|
|
48451
|
+
\`\`\`json
|
|
48452
|
+
{
|
|
48453
|
+
"command": "echo hello world",
|
|
48454
|
+
"sessionId": "optional-session-id",
|
|
48455
|
+
"cwd": "/path/to/start",
|
|
48456
|
+
"timeout": 30000,
|
|
48457
|
+
"name": "my-session"
|
|
48458
|
+
}
|
|
48459
|
+
\`\`\`
|
|
48460
|
+
|
|
48461
|
+
**Parameters:**
|
|
48462
|
+
- \`command\` (required) \u2014 The shell command to execute
|
|
48463
|
+
- \`sessionId\` (optional) \u2014 Reuse a session to preserve env vars, cwd, and shell state
|
|
48464
|
+
- \`cwd\` (optional) \u2014 Initial working directory (only used when creating new session)
|
|
48465
|
+
- \`timeout\` (optional) \u2014 Timeout in milliseconds (default: 30000)
|
|
48466
|
+
- \`name\` (optional) \u2014 Session name for identification (only used when creating new session)
|
|
48467
|
+
|
|
48468
|
+
**Response:**
|
|
48469
|
+
\`\`\`json
|
|
48470
|
+
{
|
|
48471
|
+
"sessionId": "uuid",
|
|
48472
|
+
"stdout": "hello world",
|
|
48473
|
+
"stderr": "",
|
|
48474
|
+
"exitCode": 0,
|
|
48475
|
+
"timedOut": false
|
|
48476
|
+
}
|
|
48477
|
+
\`\`\`
|
|
48478
|
+
|
|
48479
|
+
### Session Workflow Example
|
|
48480
|
+
|
|
48481
|
+
\`\`\`
|
|
48482
|
+
1. First command (creates named session):
|
|
48483
|
+
execute_command { command: "cd /project && export API_KEY=secret", name: "dev-session" }
|
|
48484
|
+
\u2192 Returns sessionId: "abc-123"
|
|
48485
|
+
|
|
48486
|
+
2. Subsequent commands (reuse session):
|
|
48487
|
+
execute_command { command: "echo $API_KEY", sessionId: "abc-123" }
|
|
48488
|
+
\u2192 Returns stdout: "secret" (env var preserved)
|
|
48489
|
+
|
|
48490
|
+
execute_command { command: "pwd", sessionId: "abc-123" }
|
|
48491
|
+
\u2192 Returns stdout: "/project" (cwd preserved)
|
|
48492
|
+
|
|
48493
|
+
3. Rename session if needed:
|
|
48494
|
+
update_exec_session { sessionId: "abc-123", name: "new-name" }
|
|
48495
|
+
|
|
48496
|
+
4. Cleanup when done:
|
|
48497
|
+
destroy_exec_session { sessionId: "abc-123" }
|
|
48498
|
+
\`\`\`
|
|
48499
|
+
|
|
48500
|
+
Sessions persist until manually destroyed.
|
|
48501
|
+
|
|
48502
|
+
### list_exec_sessions
|
|
48503
|
+
|
|
48504
|
+
List all active sessions with their name, current working directory, and timestamps.
|
|
48505
|
+
|
|
48506
|
+
### update_exec_session
|
|
48507
|
+
|
|
48508
|
+
Rename an existing session for identification.
|
|
48509
|
+
|
|
48510
|
+
### destroy_exec_session
|
|
48511
|
+
|
|
48512
|
+
Clean up a session when you're done to free resources.
|
|
48513
|
+
|
|
48514
|
+
## Best Practices
|
|
48515
|
+
|
|
48516
|
+
1. **Use \`current-task\` inside worktrees** - It auto-detects which task you're in
|
|
48517
|
+
2. **Link PRs immediately** - Run \`fulcrum current-task pr <url>\` right after creating a PR
|
|
48518
|
+
3. **Link relevant resources** - Attach design docs, specs, or reference materials with \`fulcrum current-task link <url>\`
|
|
48519
|
+
4. **Mark review when done** - \`fulcrum current-task review\` notifies the user
|
|
48520
|
+
5. **Send notifications for blocking issues** - Keep the user informed of progress
|
|
48521
|
+
6. **Name sessions for identification** - Use descriptive names to find sessions later
|
|
48522
|
+
7. **Reuse sessions for related commands** - Preserve state across multiple execute_command calls
|
|
48523
|
+
8. **Clean up sessions when done** - Use destroy_exec_session to free resources
|
|
48524
|
+
`;
|
|
48525
|
+
|
|
48526
|
+
// cli/src/commands/claude.ts
|
|
48527
|
+
var PLUGIN_DIR = join3(homedir2(), ".claude", "plugins", "fulcrum");
|
|
48528
|
+
var PLUGIN_FILES = [
|
|
48529
|
+
{ path: ".claude-plugin/plugin.json", content: plugin_default },
|
|
48530
|
+
{ path: "hooks/hooks.json", content: hooks_default },
|
|
48531
|
+
{ path: ".mcp.json", content: _mcp_default },
|
|
48532
|
+
{ path: "commands/pr.md", content: pr_default },
|
|
48533
|
+
{ path: "commands/task-info.md", content: task_info_default },
|
|
48534
|
+
{ path: "commands/notify.md", content: notify_default },
|
|
48535
|
+
{ path: "commands/linear.md", content: linear_default },
|
|
48536
|
+
{ path: "commands/review.md", content: review_default },
|
|
48537
|
+
{ path: "skills/vibora/SKILL.md", content: SKILL_default }
|
|
48538
|
+
];
|
|
48539
|
+
var PLUGIN_ID = "fulcrum@fulcrum";
|
|
48540
|
+
var INSTALLED_PLUGINS_PATH = join3(homedir2(), ".claude", "plugins", "installed_plugins.json");
|
|
48541
|
+
var CLAUDE_SETTINGS_PATH = join3(homedir2(), ".claude", "settings.json");
|
|
48542
|
+
function getPluginVersion() {
|
|
48543
|
+
try {
|
|
48544
|
+
const parsed = JSON.parse(plugin_default);
|
|
48545
|
+
return parsed.version || "1.0.0";
|
|
48546
|
+
} catch {
|
|
48547
|
+
return "1.0.0";
|
|
48548
|
+
}
|
|
48549
|
+
}
|
|
48550
|
+
function registerPlugin() {
|
|
48551
|
+
const version3 = getPluginVersion();
|
|
48552
|
+
const now = new Date().toISOString();
|
|
48553
|
+
let data = { version: 2, plugins: {} };
|
|
48554
|
+
if (existsSync3(INSTALLED_PLUGINS_PATH)) {
|
|
48555
|
+
try {
|
|
48556
|
+
data = JSON.parse(readFileSync4(INSTALLED_PLUGINS_PATH, "utf-8"));
|
|
48557
|
+
} catch {}
|
|
48558
|
+
}
|
|
48559
|
+
data.plugins = data.plugins || {};
|
|
48560
|
+
data.plugins[PLUGIN_ID] = [
|
|
48561
|
+
{
|
|
48562
|
+
scope: "user",
|
|
48563
|
+
installPath: PLUGIN_DIR,
|
|
48564
|
+
version: version3,
|
|
48565
|
+
installedAt: now,
|
|
48566
|
+
lastUpdated: now
|
|
48567
|
+
}
|
|
48568
|
+
];
|
|
48569
|
+
const dir = INSTALLED_PLUGINS_PATH.substring(0, INSTALLED_PLUGINS_PATH.lastIndexOf("/"));
|
|
48570
|
+
mkdirSync3(dir, { recursive: true });
|
|
48571
|
+
writeFileSync3(INSTALLED_PLUGINS_PATH, JSON.stringify(data, null, 2), "utf-8");
|
|
48572
|
+
}
|
|
48573
|
+
function enablePlugin() {
|
|
48574
|
+
let data = {};
|
|
48575
|
+
if (existsSync3(CLAUDE_SETTINGS_PATH)) {
|
|
48576
|
+
try {
|
|
48577
|
+
data = JSON.parse(readFileSync4(CLAUDE_SETTINGS_PATH, "utf-8"));
|
|
48578
|
+
} catch {}
|
|
48579
|
+
}
|
|
48580
|
+
const enabledPlugins = data.enabledPlugins || {};
|
|
48581
|
+
enabledPlugins[PLUGIN_ID] = true;
|
|
48582
|
+
data.enabledPlugins = enabledPlugins;
|
|
48583
|
+
const dir = CLAUDE_SETTINGS_PATH.substring(0, CLAUDE_SETTINGS_PATH.lastIndexOf("/"));
|
|
48584
|
+
mkdirSync3(dir, { recursive: true });
|
|
48585
|
+
writeFileSync3(CLAUDE_SETTINGS_PATH, JSON.stringify(data, null, 2), "utf-8");
|
|
48586
|
+
}
|
|
48587
|
+
function unregisterPlugin() {
|
|
48588
|
+
if (!existsSync3(INSTALLED_PLUGINS_PATH))
|
|
48589
|
+
return;
|
|
48590
|
+
try {
|
|
48591
|
+
const data = JSON.parse(readFileSync4(INSTALLED_PLUGINS_PATH, "utf-8"));
|
|
48592
|
+
if (data.plugins && data.plugins[PLUGIN_ID]) {
|
|
48593
|
+
delete data.plugins[PLUGIN_ID];
|
|
48594
|
+
writeFileSync3(INSTALLED_PLUGINS_PATH, JSON.stringify(data, null, 2), "utf-8");
|
|
48595
|
+
}
|
|
48596
|
+
} catch {}
|
|
48597
|
+
}
|
|
48598
|
+
function disablePlugin() {
|
|
48599
|
+
if (!existsSync3(CLAUDE_SETTINGS_PATH))
|
|
48600
|
+
return;
|
|
48601
|
+
try {
|
|
48602
|
+
const data = JSON.parse(readFileSync4(CLAUDE_SETTINGS_PATH, "utf-8"));
|
|
48603
|
+
if (data.enabledPlugins && data.enabledPlugins[PLUGIN_ID] !== undefined) {
|
|
48604
|
+
delete data.enabledPlugins[PLUGIN_ID];
|
|
48605
|
+
writeFileSync3(CLAUDE_SETTINGS_PATH, JSON.stringify(data, null, 2), "utf-8");
|
|
48606
|
+
}
|
|
48607
|
+
} catch {}
|
|
48608
|
+
}
|
|
48609
|
+
async function handleClaudeCommand(action) {
|
|
48610
|
+
if (action === "install") {
|
|
48611
|
+
await installClaudePlugin();
|
|
48612
|
+
return;
|
|
48613
|
+
}
|
|
48614
|
+
if (action === "uninstall") {
|
|
48615
|
+
await uninstallClaudePlugin();
|
|
48616
|
+
return;
|
|
48617
|
+
}
|
|
48618
|
+
throw new CliError("INVALID_ACTION", "Unknown action. Usage: fulcrum claude install | fulcrum claude uninstall", ExitCodes.INVALID_ARGS);
|
|
48619
|
+
}
|
|
48620
|
+
function needsPluginUpdate() {
|
|
48621
|
+
const installedPluginJson = join3(PLUGIN_DIR, ".claude-plugin", "plugin.json");
|
|
48622
|
+
if (!existsSync3(installedPluginJson)) {
|
|
48623
|
+
return true;
|
|
48624
|
+
}
|
|
48625
|
+
try {
|
|
48626
|
+
const installed = JSON.parse(readFileSync4(installedPluginJson, "utf-8"));
|
|
48627
|
+
const bundled = JSON.parse(plugin_default);
|
|
48628
|
+
return installed.version !== bundled.version;
|
|
48629
|
+
} catch {
|
|
48630
|
+
return true;
|
|
48631
|
+
}
|
|
48632
|
+
}
|
|
48633
|
+
async function installClaudePlugin(options = {}) {
|
|
48634
|
+
const { silent = false } = options;
|
|
48635
|
+
const log = silent ? () => {} : console.log;
|
|
48636
|
+
try {
|
|
48637
|
+
log("Installing Claude Code plugin...");
|
|
48638
|
+
if (existsSync3(PLUGIN_DIR)) {
|
|
48639
|
+
log("\u2022 Removing existing plugin installation...");
|
|
48640
|
+
rmSync(PLUGIN_DIR, { recursive: true });
|
|
48641
|
+
}
|
|
48642
|
+
for (const file2 of PLUGIN_FILES) {
|
|
48643
|
+
const fullPath = join3(PLUGIN_DIR, file2.path);
|
|
48644
|
+
const dir = fullPath.substring(0, fullPath.lastIndexOf("/"));
|
|
48645
|
+
mkdirSync3(dir, { recursive: true });
|
|
48646
|
+
writeFileSync3(fullPath, file2.content, "utf-8");
|
|
48647
|
+
}
|
|
48648
|
+
log("\u2713 Installed plugin files at " + PLUGIN_DIR);
|
|
48649
|
+
registerPlugin();
|
|
48650
|
+
log("\u2713 Registered plugin in installed_plugins.json");
|
|
48651
|
+
enablePlugin();
|
|
48652
|
+
log("\u2713 Enabled plugin in settings.json");
|
|
48653
|
+
log("");
|
|
48654
|
+
log("Installation complete! Restart Claude Code to apply changes.");
|
|
48655
|
+
} catch (err) {
|
|
48656
|
+
throw new CliError("INSTALL_FAILED", `Failed to install Claude plugin: ${err instanceof Error ? err.message : String(err)}`, ExitCodes.ERROR);
|
|
48657
|
+
}
|
|
48658
|
+
}
|
|
48659
|
+
async function uninstallClaudePlugin() {
|
|
48660
|
+
try {
|
|
48661
|
+
let didSomething = false;
|
|
48662
|
+
if (existsSync3(PLUGIN_DIR)) {
|
|
48663
|
+
rmSync(PLUGIN_DIR, { recursive: true });
|
|
48664
|
+
console.log("\u2713 Removed plugin files from " + PLUGIN_DIR);
|
|
48665
|
+
didSomething = true;
|
|
48666
|
+
}
|
|
48667
|
+
unregisterPlugin();
|
|
48668
|
+
console.log("\u2713 Unregistered plugin from installed_plugins.json");
|
|
48669
|
+
disablePlugin();
|
|
48670
|
+
console.log("\u2713 Disabled plugin in settings.json");
|
|
48671
|
+
if (didSomething) {
|
|
48672
|
+
console.log("");
|
|
48673
|
+
console.log("Uninstall complete! Restart Claude Code to apply changes.");
|
|
48674
|
+
} else {
|
|
48675
|
+
console.log("");
|
|
48676
|
+
console.log("Plugin files were not found, but registration entries have been cleaned up.");
|
|
48677
|
+
}
|
|
48678
|
+
} catch (err) {
|
|
48679
|
+
throw new CliError("UNINSTALL_FAILED", `Failed to uninstall Claude plugin: ${err instanceof Error ? err.message : String(err)}`, ExitCodes.ERROR);
|
|
48680
|
+
}
|
|
48681
|
+
}
|
|
48682
|
+
// package.json
|
|
48683
|
+
var package_default = {
|
|
48684
|
+
name: "@knowsuchagency/fulcrum",
|
|
48685
|
+
private: true,
|
|
48686
|
+
version: "1.4.0",
|
|
48687
|
+
description: "Harness Attention. Orchestrate Agents. Ship.",
|
|
48688
|
+
license: "PolyForm-Perimeter-1.0.0",
|
|
48689
|
+
type: "module",
|
|
48690
|
+
scripts: {
|
|
48691
|
+
dev: "vite --host",
|
|
48692
|
+
"dev:server": "mkdir -p ~/.fulcrum && bun --watch server/index.ts",
|
|
48693
|
+
build: "tsc -b && vite build",
|
|
48694
|
+
start: "NODE_ENV=production bun server/index.ts",
|
|
48695
|
+
lint: "eslint .",
|
|
48696
|
+
preview: "vite preview",
|
|
48697
|
+
"db:generate": "drizzle-kit generate",
|
|
48698
|
+
"db:migrate": "drizzle-kit migrate",
|
|
48699
|
+
"db:studio": "drizzle-kit studio"
|
|
48700
|
+
},
|
|
48701
|
+
dependencies: {
|
|
48702
|
+
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
|
48703
|
+
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
|
|
48704
|
+
"@azurity/pure-nerd-font": "^3.0.5",
|
|
48705
|
+
"@base-ui/react": "^1.0.0",
|
|
48706
|
+
"@dagrejs/dagre": "^1.1.8",
|
|
48707
|
+
"@fontsource-variable/jetbrains-mono": "^5.2.8",
|
|
48708
|
+
"@hono/node-server": "^1.19.7",
|
|
48709
|
+
"@hono/node-ws": "^1.2.0",
|
|
48710
|
+
"@hugeicons/core-free-icons": "^3.0.0",
|
|
48711
|
+
"@hugeicons/react": "^1.1.3",
|
|
48712
|
+
"@monaco-editor/react": "^4.7.0",
|
|
48713
|
+
"@octokit/rest": "^22.0.1",
|
|
48714
|
+
"@radix-ui/react-collapsible": "^1.1.12",
|
|
48715
|
+
"@tailwindcss/vite": "^4.1.17",
|
|
48716
|
+
"@tanstack/react-query": "^5.90.12",
|
|
48717
|
+
"@tanstack/react-router": "^1.141.8",
|
|
48718
|
+
"@uiw/react-markdown-preview": "^5.1.5",
|
|
48719
|
+
"@xterm/addon-clipboard": "^0.2.0",
|
|
48720
|
+
"@xterm/addon-fit": "^0.10.0",
|
|
48721
|
+
"@xterm/addon-web-links": "^0.11.0",
|
|
48722
|
+
"@xterm/xterm": "^5.5.0",
|
|
48723
|
+
"bun-pty": "^0.4.2",
|
|
48724
|
+
citty: "^0.1.6",
|
|
48725
|
+
"class-variance-authority": "^0.7.1",
|
|
48726
|
+
cloudflare: "^5.2.0",
|
|
48727
|
+
clsx: "^2.1.1",
|
|
48728
|
+
"date-fns": "^4.1.0",
|
|
48729
|
+
"drizzle-orm": "^0.45.1",
|
|
48730
|
+
"fancy-ansi": "^0.1.3",
|
|
48731
|
+
glob: "^13.0.0",
|
|
48732
|
+
hono: "^4.11.1",
|
|
48733
|
+
i18next: "^25.7.3",
|
|
48734
|
+
mobx: "^6.15.0",
|
|
48735
|
+
"mobx-react-lite": "^4.1.1",
|
|
48736
|
+
"mobx-state-tree": "^7.0.2",
|
|
48737
|
+
"next-themes": "^0.4.6",
|
|
48738
|
+
react: "^19.2.0",
|
|
48739
|
+
"react-day-picker": "^9.13.0",
|
|
48740
|
+
"react-dom": "^19.2.0",
|
|
48741
|
+
"react-i18next": "^16.5.0",
|
|
48742
|
+
"react-resizable-panels": "^4.0.11",
|
|
48743
|
+
reactflow: "^11.11.4",
|
|
48744
|
+
recharts: "2.15.4",
|
|
48745
|
+
shadcn: "^3.6.2",
|
|
48746
|
+
shiki: "^3.20.0",
|
|
48747
|
+
sonner: "^2.0.7",
|
|
48748
|
+
"tailwind-merge": "^3.4.0",
|
|
48749
|
+
tailwindcss: "^4.1.17",
|
|
48750
|
+
"tw-animate-css": "^1.4.0",
|
|
48751
|
+
ws: "^8.18.3",
|
|
48752
|
+
yaml: "^2.8.2"
|
|
48753
|
+
},
|
|
48754
|
+
devDependencies: {
|
|
48755
|
+
"@eslint/js": "^9.39.1",
|
|
48756
|
+
"@opencode-ai/plugin": "^1.1.8",
|
|
48757
|
+
"@tailwindcss/typography": "^0.5.19",
|
|
48758
|
+
"@tanstack/router-plugin": "^1.141.8",
|
|
48759
|
+
"@types/bun": "^1.2.14",
|
|
48760
|
+
"@types/node": "^24.10.1",
|
|
48761
|
+
"@types/react": "^19.2.5",
|
|
48762
|
+
"@types/react-dom": "^19.2.3",
|
|
48763
|
+
"@types/ws": "^8.18.1",
|
|
48764
|
+
"@vitejs/plugin-react": "^5.1.1",
|
|
48765
|
+
"drizzle-kit": "^0.31.8",
|
|
48766
|
+
eslint: "^9.39.1",
|
|
48767
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
48768
|
+
"eslint-plugin-react-refresh": "^0.4.24",
|
|
48769
|
+
globals: "^16.5.0",
|
|
48770
|
+
typescript: "~5.9.3",
|
|
48771
|
+
"typescript-eslint": "^8.46.4",
|
|
48772
|
+
vite: "^7.2.4"
|
|
48773
|
+
}
|
|
48774
|
+
};
|
|
48775
|
+
|
|
48776
|
+
// cli/src/commands/up.ts
|
|
48777
|
+
function getPackageRoot() {
|
|
48778
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
48779
|
+
let dir = dirname2(currentFile);
|
|
48780
|
+
for (let i2 = 0;i2 < 5; i2++) {
|
|
48781
|
+
if (existsSync4(join4(dir, "server", "index.js"))) {
|
|
48782
|
+
return dir;
|
|
48783
|
+
}
|
|
48784
|
+
dir = dirname2(dir);
|
|
48785
|
+
}
|
|
48786
|
+
return dirname2(dirname2(dirname2(currentFile)));
|
|
48787
|
+
}
|
|
48788
|
+
async function handleUpCommand(flags) {
|
|
48789
|
+
const autoYes = flags.yes === "true" || flags.y === "true";
|
|
48790
|
+
if (needsViboraMigration()) {
|
|
48791
|
+
const viboraDir = getLegacyViboraDir();
|
|
48792
|
+
console.error(`
|
|
48793
|
+
Found existing Vibora data at ${viboraDir}`);
|
|
48794
|
+
console.error('Run "fulcrum migrate-from-vibora" to copy your data to ~/.fulcrum');
|
|
48795
|
+
console.error("");
|
|
48796
|
+
}
|
|
48797
|
+
if (!isBunInstalled()) {
|
|
48798
|
+
const bunDep = getDependency("bun");
|
|
48799
|
+
const method = getInstallMethod(bunDep);
|
|
48800
|
+
console.error("Bun is required to run Fulcrum but is not installed.");
|
|
48801
|
+
console.error(" Bun is the JavaScript runtime that powers Fulcrum.");
|
|
48802
|
+
const shouldInstall = autoYes || await confirm(`Would you like to install bun via ${method}?`);
|
|
48803
|
+
if (shouldInstall) {
|
|
48804
|
+
const success2 = installBun();
|
|
48805
|
+
if (!success2) {
|
|
48806
|
+
throw new CliError("INSTALL_FAILED", "Failed to install bun", ExitCodes.ERROR);
|
|
48807
|
+
}
|
|
48808
|
+
console.error("Bun installed successfully!");
|
|
48809
|
+
} else {
|
|
48810
|
+
throw new CliError("MISSING_DEPENDENCY", `Bun is required. Install manually: ${getInstallCommand(bunDep)}`, ExitCodes.ERROR);
|
|
48811
|
+
}
|
|
48812
|
+
}
|
|
48813
|
+
if (!isDtachInstalled()) {
|
|
48814
|
+
const dtachDep = getDependency("dtach");
|
|
48815
|
+
const method = getInstallMethod(dtachDep);
|
|
48816
|
+
console.error("dtach is required for terminal persistence but is not installed.");
|
|
48817
|
+
console.error(" dtach enables persistent terminal sessions that survive disconnects.");
|
|
48818
|
+
const shouldInstall = autoYes || await confirm(`Would you like to install dtach via ${method}?`);
|
|
48819
|
+
if (shouldInstall) {
|
|
48820
|
+
const success2 = installDtach();
|
|
48821
|
+
if (!success2) {
|
|
48822
|
+
throw new CliError("INSTALL_FAILED", "Failed to install dtach", ExitCodes.ERROR);
|
|
48823
|
+
}
|
|
48824
|
+
console.error("dtach installed successfully!");
|
|
48825
|
+
} else {
|
|
48826
|
+
throw new CliError("MISSING_DEPENDENCY", `dtach is required. Install manually: ${getInstallCommand(dtachDep)}`, ExitCodes.ERROR);
|
|
48827
|
+
}
|
|
48828
|
+
}
|
|
48829
|
+
if (!isUvInstalled()) {
|
|
48830
|
+
const uvDep = getDependency("uv");
|
|
48831
|
+
const method = getInstallMethod(uvDep);
|
|
48832
|
+
console.error("uv is required but is not installed.");
|
|
48833
|
+
console.error(" uv is a fast Python package manager used by Claude Code.");
|
|
48834
|
+
const shouldInstall = autoYes || await confirm(`Would you like to install uv via ${method}?`);
|
|
48835
|
+
if (shouldInstall) {
|
|
48836
|
+
const success2 = installUv();
|
|
48837
|
+
if (!success2) {
|
|
48838
|
+
throw new CliError("INSTALL_FAILED", "Failed to install uv", ExitCodes.ERROR);
|
|
48839
|
+
}
|
|
48840
|
+
console.error("uv installed successfully!");
|
|
48841
|
+
} else {
|
|
48842
|
+
throw new CliError("MISSING_DEPENDENCY", `uv is required. Install manually: ${getInstallCommand(uvDep)}`, ExitCodes.ERROR);
|
|
48843
|
+
}
|
|
48844
|
+
}
|
|
48845
|
+
if (isClaudeInstalled() && needsPluginUpdate()) {
|
|
48846
|
+
console.error("Updating Fulcrum plugin for Claude Code...");
|
|
48847
|
+
await installClaudePlugin({ silent: true });
|
|
48848
|
+
console.error("\u2713 Fulcrum plugin updated");
|
|
48849
|
+
}
|
|
48850
|
+
const existingPid = readPid();
|
|
48851
|
+
if (existingPid && isProcessRunning(existingPid)) {
|
|
48852
|
+
console.error(`Fulcrum server is already running (PID: ${existingPid})`);
|
|
48853
|
+
const shouldReplace = autoYes || await confirm("Would you like to stop it and start a new instance?");
|
|
48854
|
+
if (shouldReplace) {
|
|
48855
|
+
console.error("Stopping existing instance...");
|
|
48856
|
+
process.kill(existingPid, "SIGTERM");
|
|
48857
|
+
let attempts = 0;
|
|
48858
|
+
while (attempts < 50 && isProcessRunning(existingPid)) {
|
|
48859
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
48860
|
+
attempts++;
|
|
48861
|
+
}
|
|
48862
|
+
if (isProcessRunning(existingPid)) {
|
|
48863
|
+
process.kill(existingPid, "SIGKILL");
|
|
48864
|
+
}
|
|
48865
|
+
removePid();
|
|
48866
|
+
console.error("Existing instance stopped.");
|
|
48867
|
+
} else {
|
|
48868
|
+
throw new CliError("ALREADY_RUNNING", `Server already running at http://localhost:${getPort(flags.port)}`, ExitCodes.ERROR);
|
|
48869
|
+
}
|
|
48870
|
+
}
|
|
48871
|
+
const port = getPort(flags.port);
|
|
48872
|
+
if (flags.port) {
|
|
48873
|
+
updateSettingsPort(port);
|
|
48874
|
+
}
|
|
48875
|
+
const host = flags.host ? "0.0.0.0" : "localhost";
|
|
48876
|
+
const packageRoot = getPackageRoot();
|
|
48877
|
+
const serverPath = join4(packageRoot, "server", "index.js");
|
|
48878
|
+
const platform2 = process.platform;
|
|
48879
|
+
const arch = process.arch;
|
|
48880
|
+
let ptyLibName;
|
|
48881
|
+
if (platform2 === "darwin") {
|
|
48882
|
+
ptyLibName = arch === "arm64" ? "librust_pty_arm64.dylib" : "librust_pty.dylib";
|
|
48883
|
+
} else if (platform2 === "win32") {
|
|
48884
|
+
ptyLibName = "rust_pty.dll";
|
|
48885
|
+
} else {
|
|
48886
|
+
ptyLibName = arch === "arm64" ? "librust_pty_arm64.so" : "librust_pty.so";
|
|
48887
|
+
}
|
|
48888
|
+
const ptyLibPath = join4(packageRoot, "lib", ptyLibName);
|
|
48889
|
+
const fulcrumDir = getFulcrumDir();
|
|
48890
|
+
const debug = flags.debug === "true";
|
|
48891
|
+
console.error(`Starting Fulcrum server${debug ? " (debug mode)" : ""}...`);
|
|
48892
|
+
const serverProc = spawn("bun", [serverPath], {
|
|
48893
|
+
detached: true,
|
|
48894
|
+
stdio: "ignore",
|
|
48895
|
+
env: {
|
|
48896
|
+
...process.env,
|
|
48897
|
+
NODE_ENV: "production",
|
|
48898
|
+
PORT: port.toString(),
|
|
48899
|
+
HOST: host,
|
|
48900
|
+
FULCRUM_DIR: fulcrumDir,
|
|
48901
|
+
FULCRUM_PACKAGE_ROOT: packageRoot,
|
|
48902
|
+
FULCRUM_VERSION: package_default.version,
|
|
48903
|
+
BUN_PTY_LIB: ptyLibPath,
|
|
48904
|
+
...isClaudeInstalled() && { FULCRUM_CLAUDE_INSTALLED: "1" },
|
|
48905
|
+
...isOpencodeInstalled() && { FULCRUM_OPENCODE_INSTALLED: "1" },
|
|
48906
|
+
...debug && { LOG_LEVEL: "debug", DEBUG: "1" }
|
|
48907
|
+
}
|
|
48908
|
+
});
|
|
48909
|
+
serverProc.unref();
|
|
48910
|
+
const pid = serverProc.pid;
|
|
48911
|
+
if (!pid) {
|
|
48912
|
+
throw new CliError("START_FAILED", "Failed to start server process", ExitCodes.ERROR);
|
|
48913
|
+
}
|
|
48914
|
+
writePid(pid);
|
|
48915
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
48916
|
+
if (!isProcessRunning(pid)) {
|
|
48917
|
+
throw new CliError("START_FAILED", "Server process died immediately after starting", ExitCodes.ERROR);
|
|
48918
|
+
}
|
|
48919
|
+
if (isJsonOutput()) {
|
|
48920
|
+
output({
|
|
48921
|
+
pid,
|
|
48922
|
+
port,
|
|
48923
|
+
url: `http://localhost:${port}`
|
|
48924
|
+
});
|
|
48925
|
+
} else {
|
|
48926
|
+
const hasAgent = isClaudeInstalled() || isOpencodeInstalled();
|
|
48927
|
+
showGettingStartedTips(port, hasAgent);
|
|
48928
|
+
}
|
|
48929
|
+
}
|
|
48930
|
+
function showGettingStartedTips(port, hasAgent) {
|
|
48931
|
+
console.error(`
|
|
48932
|
+
Fulcrum is running at http://localhost:${port}
|
|
48933
|
+
|
|
48934
|
+
Getting Started:
|
|
48935
|
+
1. Open http://localhost:${port} in your browser
|
|
48936
|
+
2. Add a repository to get started
|
|
48937
|
+
3. Create a task to spin up an isolated worktree
|
|
48938
|
+
4. Run your AI agent in the task terminal
|
|
48939
|
+
|
|
48940
|
+
Commands:
|
|
48941
|
+
fulcrum status Check server status
|
|
48942
|
+
fulcrum doctor Check all dependencies
|
|
48943
|
+
fulcrum down Stop the server
|
|
48944
|
+
`);
|
|
48945
|
+
if (!hasAgent) {
|
|
48946
|
+
console.error(`Note: No AI agents detected. Install one to get started:
|
|
48947
|
+
Claude Code: curl -fsSL https://claude.ai/install.sh | bash
|
|
48948
|
+
OpenCode: curl -fsSL https://opencode.ai/install | bash
|
|
48949
|
+
`);
|
|
48950
|
+
}
|
|
48951
|
+
}
|
|
48952
|
+
|
|
48953
|
+
// cli/src/commands/down.ts
|
|
48954
|
+
init_errors();
|
|
48955
|
+
async function handleDownCommand() {
|
|
48956
|
+
const pid = readPid();
|
|
48957
|
+
if (!pid) {
|
|
48958
|
+
throw new CliError("NOT_RUNNING", "No PID file found. Fulcrum server may not be running.", ExitCodes.ERROR);
|
|
48959
|
+
}
|
|
48960
|
+
if (!isProcessRunning(pid)) {
|
|
48961
|
+
removePid();
|
|
48962
|
+
if (isJsonOutput()) {
|
|
48963
|
+
output({ stopped: true, pid, wasRunning: false });
|
|
48964
|
+
} else {
|
|
48965
|
+
console.log(`Fulcrum was not running (stale PID file cleaned up)`);
|
|
48966
|
+
}
|
|
48967
|
+
return;
|
|
48968
|
+
}
|
|
48969
|
+
try {
|
|
48970
|
+
process.kill(pid, "SIGTERM");
|
|
48971
|
+
} catch (err) {
|
|
48972
|
+
throw new CliError("KILL_FAILED", `Failed to stop server (PID: ${pid}): ${err}`, ExitCodes.ERROR);
|
|
48973
|
+
}
|
|
48974
|
+
let attempts = 0;
|
|
48975
|
+
while (attempts < 50 && isProcessRunning(pid)) {
|
|
48976
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
48977
|
+
attempts++;
|
|
48978
|
+
}
|
|
48979
|
+
if (isProcessRunning(pid)) {
|
|
48980
|
+
try {
|
|
48981
|
+
process.kill(pid, "SIGKILL");
|
|
48982
|
+
} catch {}
|
|
48983
|
+
}
|
|
48984
|
+
removePid();
|
|
48985
|
+
if (isJsonOutput()) {
|
|
48986
|
+
output({ stopped: true, pid, wasRunning: true });
|
|
48987
|
+
} else {
|
|
48988
|
+
console.log(`Fulcrum stopped (PID: ${pid})`);
|
|
48989
|
+
}
|
|
48990
|
+
}
|
|
48991
|
+
|
|
48992
|
+
// cli/src/commands/migrate-from-vibora.ts
|
|
48993
|
+
init_server();
|
|
48994
|
+
async function handleMigrateFromViboraCommand(flags) {
|
|
48995
|
+
const autoYes = flags.yes === "true" || flags.y === "true";
|
|
48996
|
+
if (!needsViboraMigration()) {
|
|
48997
|
+
if (isJsonOutput()) {
|
|
48998
|
+
output({ migrated: false, reason: "no_migration_needed" });
|
|
48999
|
+
} else {
|
|
49000
|
+
console.error("No migration needed.");
|
|
49001
|
+
console.error(` ~/.vibora does not exist or ~/.fulcrum already has data.`);
|
|
49002
|
+
}
|
|
49003
|
+
return;
|
|
49004
|
+
}
|
|
49005
|
+
const viboraDir = getLegacyViboraDir();
|
|
49006
|
+
const fulcrumDir = getFulcrumDir();
|
|
49007
|
+
if (!isJsonOutput()) {
|
|
49008
|
+
console.error(`
|
|
49009
|
+
Found existing Vibora data at ${viboraDir}`);
|
|
49010
|
+
console.error("Fulcrum (formerly Vibora) now uses ~/.fulcrum for data storage.");
|
|
49011
|
+
console.error("");
|
|
49012
|
+
console.error("Your existing data can be copied to the new location.");
|
|
49013
|
+
console.error("This is non-destructive - your ~/.vibora directory will be left untouched.");
|
|
49014
|
+
console.error("");
|
|
49015
|
+
}
|
|
49016
|
+
const shouldMigrate = autoYes || await confirm("Would you like to copy your data to ~/.fulcrum?");
|
|
49017
|
+
if (!shouldMigrate) {
|
|
49018
|
+
if (isJsonOutput()) {
|
|
49019
|
+
output({ migrated: false, reason: "user_declined" });
|
|
49020
|
+
} else {
|
|
49021
|
+
console.error("Migration skipped.");
|
|
49022
|
+
console.error("You can run this command again later to migrate.");
|
|
49023
|
+
}
|
|
49024
|
+
return;
|
|
49025
|
+
}
|
|
49026
|
+
if (!isJsonOutput()) {
|
|
49027
|
+
console.error("Copying data from ~/.vibora to ~/.fulcrum...");
|
|
49028
|
+
}
|
|
49029
|
+
const success2 = migrateFromVibora();
|
|
49030
|
+
if (success2) {
|
|
49031
|
+
if (isJsonOutput()) {
|
|
49032
|
+
output({ migrated: true, from: viboraDir, to: fulcrumDir });
|
|
49033
|
+
} else {
|
|
49034
|
+
console.error("Migration complete!");
|
|
49035
|
+
console.error(` Data copied from ${viboraDir} to ${fulcrumDir}`);
|
|
49036
|
+
console.error(" Your original ~/.vibora directory has been preserved.");
|
|
49037
|
+
}
|
|
49038
|
+
} else {
|
|
49039
|
+
if (isJsonOutput()) {
|
|
49040
|
+
output({ migrated: false, reason: "migration_failed" });
|
|
49041
|
+
} else {
|
|
49042
|
+
console.error("Migration failed.");
|
|
49043
|
+
console.error("You can manually copy files from ~/.vibora to ~/.fulcrum");
|
|
49044
|
+
}
|
|
49045
|
+
process.exitCode = 1;
|
|
49046
|
+
}
|
|
49047
|
+
}
|
|
49048
|
+
|
|
49049
|
+
// cli/src/commands/status.ts
|
|
49050
|
+
init_server();
|
|
49051
|
+
async function handleStatusCommand(flags) {
|
|
49052
|
+
const pid = readPid();
|
|
49053
|
+
const port = getPort(flags.port);
|
|
49054
|
+
const serverUrl = discoverServerUrl(flags.url, flags.port);
|
|
49055
|
+
const pidRunning = pid !== null && isProcessRunning(pid);
|
|
49056
|
+
let healthOk = false;
|
|
49057
|
+
let version3 = null;
|
|
49058
|
+
let uptime = null;
|
|
49059
|
+
if (pidRunning) {
|
|
49060
|
+
try {
|
|
49061
|
+
const res = await fetch(`${serverUrl}/health`, { signal: AbortSignal.timeout(2000) });
|
|
49062
|
+
healthOk = res.ok;
|
|
49063
|
+
if (res.ok) {
|
|
49064
|
+
const health = await res.json();
|
|
49065
|
+
version3 = health.version || null;
|
|
49066
|
+
uptime = health.uptime || null;
|
|
49067
|
+
}
|
|
49068
|
+
} catch {}
|
|
49069
|
+
}
|
|
49070
|
+
const data = {
|
|
49071
|
+
running: pidRunning,
|
|
49072
|
+
healthy: healthOk,
|
|
49073
|
+
pid: pid || null,
|
|
49074
|
+
port,
|
|
49075
|
+
url: serverUrl,
|
|
49076
|
+
version: version3,
|
|
49077
|
+
uptime
|
|
49078
|
+
};
|
|
49079
|
+
if (isJsonOutput()) {
|
|
49080
|
+
output(data);
|
|
49081
|
+
} else {
|
|
49082
|
+
if (pidRunning) {
|
|
49083
|
+
const healthStatus = healthOk ? "healthy" : "not responding";
|
|
49084
|
+
console.log(`Fulcrum is running (${healthStatus})`);
|
|
49085
|
+
console.log(` PID: ${pid}`);
|
|
49086
|
+
console.log(` URL: ${serverUrl}`);
|
|
49087
|
+
if (version3)
|
|
49088
|
+
console.log(` Version: ${version3}`);
|
|
49089
|
+
if (uptime)
|
|
49090
|
+
console.log(` Uptime: ${Math.floor(uptime / 1000)}s`);
|
|
49091
|
+
} else {
|
|
49092
|
+
console.log("Fulcrum is not running");
|
|
49093
|
+
console.log(`
|
|
49094
|
+
Start with: fulcrum up`);
|
|
49095
|
+
}
|
|
49096
|
+
}
|
|
49097
|
+
}
|
|
49365
49098
|
|
|
49366
|
-
|
|
49099
|
+
// cli/src/commands/git.ts
|
|
49100
|
+
init_client();
|
|
49101
|
+
init_errors();
|
|
49102
|
+
async function handleGitCommand(action, flags) {
|
|
49103
|
+
const client = new FulcrumClient(flags.url, flags.port);
|
|
49104
|
+
switch (action) {
|
|
49105
|
+
case "status": {
|
|
49106
|
+
const path = flags.path || process.cwd();
|
|
49107
|
+
const status = await client.getStatus(path);
|
|
49108
|
+
if (isJsonOutput()) {
|
|
49109
|
+
output(status);
|
|
49110
|
+
} else {
|
|
49111
|
+
console.log(`Branch: ${status.branch}`);
|
|
49112
|
+
if (status.ahead)
|
|
49113
|
+
console.log(` Ahead: ${status.ahead}`);
|
|
49114
|
+
if (status.behind)
|
|
49115
|
+
console.log(` Behind: ${status.behind}`);
|
|
49116
|
+
if (status.staged?.length)
|
|
49117
|
+
console.log(` Staged: ${status.staged.length} files`);
|
|
49118
|
+
if (status.modified?.length)
|
|
49119
|
+
console.log(` Modified: ${status.modified.length} files`);
|
|
49120
|
+
if (status.untracked?.length)
|
|
49121
|
+
console.log(` Untracked: ${status.untracked.length} files`);
|
|
49122
|
+
if (!status.staged?.length && !status.modified?.length && !status.untracked?.length) {
|
|
49123
|
+
console.log(" Working tree clean");
|
|
49124
|
+
}
|
|
49125
|
+
}
|
|
49126
|
+
break;
|
|
49127
|
+
}
|
|
49128
|
+
case "diff": {
|
|
49129
|
+
const path = flags.path || process.cwd();
|
|
49130
|
+
const diff = await client.getDiff(path, {
|
|
49131
|
+
staged: flags.staged === "true",
|
|
49132
|
+
ignoreWhitespace: flags["ignore-whitespace"] === "true",
|
|
49133
|
+
includeUntracked: flags["include-untracked"] === "true"
|
|
49134
|
+
});
|
|
49135
|
+
if (isJsonOutput()) {
|
|
49136
|
+
output(diff);
|
|
49137
|
+
} else {
|
|
49138
|
+
console.log(diff.diff || "No changes");
|
|
49139
|
+
}
|
|
49140
|
+
break;
|
|
49141
|
+
}
|
|
49142
|
+
case "branches": {
|
|
49143
|
+
const repo = flags.repo;
|
|
49144
|
+
if (!repo) {
|
|
49145
|
+
throw new CliError("MISSING_REPO", "--repo is required", ExitCodes.INVALID_ARGS);
|
|
49146
|
+
}
|
|
49147
|
+
const branches = await client.getBranches(repo);
|
|
49148
|
+
if (isJsonOutput()) {
|
|
49149
|
+
output(branches);
|
|
49150
|
+
} else {
|
|
49151
|
+
for (const branch of branches) {
|
|
49152
|
+
const current = branch.current ? "* " : " ";
|
|
49153
|
+
console.log(`${current}${branch.name}`);
|
|
49154
|
+
}
|
|
49155
|
+
}
|
|
49156
|
+
break;
|
|
49157
|
+
}
|
|
49158
|
+
default:
|
|
49159
|
+
throw new CliError("UNKNOWN_ACTION", `Unknown action: ${action}. Valid: status, diff, branches`, ExitCodes.INVALID_ARGS);
|
|
49160
|
+
}
|
|
49161
|
+
}
|
|
49367
49162
|
|
|
49368
|
-
|
|
49369
|
-
|
|
49370
|
-
|
|
49371
|
-
|
|
49163
|
+
// cli/src/commands/worktrees.ts
|
|
49164
|
+
init_client();
|
|
49165
|
+
init_errors();
|
|
49166
|
+
async function handleWorktreesCommand(action, flags) {
|
|
49167
|
+
const client = new FulcrumClient(flags.url, flags.port);
|
|
49168
|
+
switch (action) {
|
|
49169
|
+
case "list": {
|
|
49170
|
+
const worktrees = await client.listWorktrees();
|
|
49171
|
+
if (isJsonOutput()) {
|
|
49172
|
+
output(worktrees);
|
|
49173
|
+
} else {
|
|
49174
|
+
if (worktrees.length === 0) {
|
|
49175
|
+
console.log("No worktrees found");
|
|
49176
|
+
} else {
|
|
49177
|
+
for (const wt of worktrees) {
|
|
49178
|
+
console.log(`${wt.path}`);
|
|
49179
|
+
console.log(` Branch: ${wt.branch}`);
|
|
49180
|
+
if (wt.taskId)
|
|
49181
|
+
console.log(` Task: ${wt.taskId}`);
|
|
49182
|
+
}
|
|
49183
|
+
}
|
|
49184
|
+
}
|
|
49185
|
+
break;
|
|
49186
|
+
}
|
|
49187
|
+
case "delete": {
|
|
49188
|
+
const worktreePath = flags.path;
|
|
49189
|
+
if (!worktreePath) {
|
|
49190
|
+
throw new CliError("MISSING_PATH", "--path is required", ExitCodes.INVALID_ARGS);
|
|
49191
|
+
}
|
|
49192
|
+
const deleteLinkedTask = flags["delete-task"] === "true" || flags["delete-task"] === "";
|
|
49193
|
+
const result = await client.deleteWorktree(worktreePath, flags.repo, deleteLinkedTask);
|
|
49194
|
+
if (isJsonOutput()) {
|
|
49195
|
+
output(result);
|
|
49196
|
+
} else {
|
|
49197
|
+
console.log(`Deleted worktree: ${worktreePath}`);
|
|
49198
|
+
}
|
|
49199
|
+
break;
|
|
49200
|
+
}
|
|
49201
|
+
default:
|
|
49202
|
+
throw new CliError("UNKNOWN_ACTION", `Unknown action: ${action}. Valid: list, delete`, ExitCodes.INVALID_ARGS);
|
|
49203
|
+
}
|
|
49372
49204
|
}
|
|
49373
|
-
\`\`\`
|
|
49374
49205
|
|
|
49375
|
-
|
|
49206
|
+
// cli/src/commands/config.ts
|
|
49207
|
+
init_client();
|
|
49208
|
+
init_errors();
|
|
49209
|
+
async function handleConfigCommand(action, positional, flags) {
|
|
49210
|
+
const client = new FulcrumClient(flags.url, flags.port);
|
|
49211
|
+
switch (action) {
|
|
49212
|
+
case "list": {
|
|
49213
|
+
const config3 = await client.getAllConfig();
|
|
49214
|
+
if (isJsonOutput()) {
|
|
49215
|
+
output(config3);
|
|
49216
|
+
} else {
|
|
49217
|
+
console.log("Configuration:");
|
|
49218
|
+
for (const [key, value] of Object.entries(config3)) {
|
|
49219
|
+
const displayValue = value === null ? "(not set)" : value;
|
|
49220
|
+
console.log(` ${key}: ${displayValue}`);
|
|
49221
|
+
}
|
|
49222
|
+
}
|
|
49223
|
+
break;
|
|
49224
|
+
}
|
|
49225
|
+
case "get": {
|
|
49226
|
+
const [key] = positional;
|
|
49227
|
+
if (!key) {
|
|
49228
|
+
throw new CliError("MISSING_KEY", "Config key is required", ExitCodes.INVALID_ARGS);
|
|
49229
|
+
}
|
|
49230
|
+
const config3 = await client.getConfig(key);
|
|
49231
|
+
if (isJsonOutput()) {
|
|
49232
|
+
output(config3);
|
|
49233
|
+
} else {
|
|
49234
|
+
const value = config3.value === null ? "(not set)" : config3.value;
|
|
49235
|
+
console.log(`${key}: ${value}`);
|
|
49236
|
+
}
|
|
49237
|
+
break;
|
|
49238
|
+
}
|
|
49239
|
+
case "set": {
|
|
49240
|
+
const [key, value] = positional;
|
|
49241
|
+
if (!key) {
|
|
49242
|
+
throw new CliError("MISSING_KEY", "Config key is required", ExitCodes.INVALID_ARGS);
|
|
49243
|
+
}
|
|
49244
|
+
if (value === undefined) {
|
|
49245
|
+
throw new CliError("MISSING_VALUE", "Config value is required", ExitCodes.INVALID_ARGS);
|
|
49246
|
+
}
|
|
49247
|
+
const parsedValue = /^\d+$/.test(value) ? parseInt(value, 10) : value;
|
|
49248
|
+
const config3 = await client.setConfig(key, parsedValue);
|
|
49249
|
+
if (isJsonOutput()) {
|
|
49250
|
+
output(config3);
|
|
49251
|
+
} else {
|
|
49252
|
+
console.log(`Set ${key} = ${config3.value}`);
|
|
49253
|
+
}
|
|
49254
|
+
break;
|
|
49255
|
+
}
|
|
49256
|
+
case "reset": {
|
|
49257
|
+
const [key] = positional;
|
|
49258
|
+
if (!key) {
|
|
49259
|
+
throw new CliError("MISSING_KEY", "Config key is required", ExitCodes.INVALID_ARGS);
|
|
49260
|
+
}
|
|
49261
|
+
const config3 = await client.resetConfig(key);
|
|
49262
|
+
if (isJsonOutput()) {
|
|
49263
|
+
output(config3);
|
|
49264
|
+
} else {
|
|
49265
|
+
console.log(`Reset ${key} to default: ${config3.value}`);
|
|
49266
|
+
}
|
|
49267
|
+
break;
|
|
49268
|
+
}
|
|
49269
|
+
default:
|
|
49270
|
+
throw new CliError("UNKNOWN_ACTION", `Unknown action: ${action}. Valid: list, get, set, reset`, ExitCodes.INVALID_ARGS);
|
|
49271
|
+
}
|
|
49272
|
+
}
|
|
49376
49273
|
|
|
49377
|
-
|
|
49378
|
-
|
|
49379
|
-
|
|
49380
|
-
|
|
49274
|
+
// cli/src/commands/opencode.ts
|
|
49275
|
+
init_errors();
|
|
49276
|
+
import {
|
|
49277
|
+
mkdirSync as mkdirSync4,
|
|
49278
|
+
writeFileSync as writeFileSync4,
|
|
49279
|
+
existsSync as existsSync5,
|
|
49280
|
+
readFileSync as readFileSync5,
|
|
49281
|
+
unlinkSync as unlinkSync2,
|
|
49282
|
+
copyFileSync,
|
|
49283
|
+
renameSync
|
|
49284
|
+
} from "fs";
|
|
49285
|
+
import { homedir as homedir3 } from "os";
|
|
49286
|
+
import { join as join5 } from "path";
|
|
49381
49287
|
|
|
49382
|
-
|
|
49383
|
-
|
|
49384
|
-
|
|
49288
|
+
// plugins/fulcrum-opencode/index.ts
|
|
49289
|
+
var fulcrum_opencode_default = `import type { Plugin } from "@opencode-ai/plugin"
|
|
49290
|
+
import { appendFileSync } from "node:fs"
|
|
49291
|
+
import { spawn } from "node:child_process"
|
|
49292
|
+
import { tmpdir } from "node:os"
|
|
49293
|
+
import { join } from "node:path"
|
|
49385
49294
|
|
|
49386
|
-
|
|
49295
|
+
declare const process: { env: Record<string, string | undefined> }
|
|
49387
49296
|
|
|
49388
|
-
|
|
49389
|
-
|
|
49390
|
-
|
|
49391
|
-
|
|
49392
|
-
|
|
49393
|
-
|
|
49394
|
-
|
|
49395
|
-
|
|
49396
|
-
|
|
49397
|
-
|
|
49398
|
-
|
|
49399
|
-
|
|
49400
|
-
|
|
49297
|
+
const LOG_FILE = join(tmpdir(), "fulcrum-opencode.log")
|
|
49298
|
+
const NOISY_EVENTS = new Set([
|
|
49299
|
+
"message.part.updated",
|
|
49300
|
+
"file.watcher.updated",
|
|
49301
|
+
"tui.toast.show",
|
|
49302
|
+
"config.updated",
|
|
49303
|
+
])
|
|
49304
|
+
const log = (msg: string) => {
|
|
49305
|
+
try {
|
|
49306
|
+
appendFileSync(LOG_FILE, \`[\${new Date().toISOString()}] \${msg}\\n\`)
|
|
49307
|
+
} catch {
|
|
49308
|
+
// Silently ignore logging errors - logging is non-critical
|
|
49309
|
+
}
|
|
49310
|
+
}
|
|
49401
49311
|
|
|
49402
|
-
|
|
49312
|
+
/**
|
|
49313
|
+
* Execute fulcrum command using spawn with shell option for proper PATH resolution.
|
|
49314
|
+
* Using spawn with explicit args array prevents shell injection while shell:true
|
|
49315
|
+
* ensures PATH is properly resolved (for NVM, fnm, etc. managed node installations).
|
|
49316
|
+
* Includes 10 second timeout protection to prevent hanging.
|
|
49317
|
+
*/
|
|
49318
|
+
async function runFulcrumCommand(args: string[]): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
|
49319
|
+
return new Promise((resolve) => {
|
|
49320
|
+
let stdout = ''
|
|
49321
|
+
let stderr = ''
|
|
49322
|
+
let resolved = false
|
|
49323
|
+
let processExited = false
|
|
49324
|
+
let killTimeoutId: ReturnType<typeof setTimeout> | null = null
|
|
49403
49325
|
|
|
49404
|
-
|
|
49326
|
+
const child = spawn(FULCRUM_CMD, args, { shell: true })
|
|
49405
49327
|
|
|
49406
|
-
|
|
49407
|
-
|
|
49408
|
-
|
|
49409
|
-
|
|
49410
|
-
|
|
49411
|
-
|
|
49412
|
-
|
|
49413
|
-
"overdue": true // Only show overdue tasks
|
|
49414
|
-
}
|
|
49415
|
-
\`\`\`
|
|
49328
|
+
const cleanup = () => {
|
|
49329
|
+
processExited = true
|
|
49330
|
+
if (killTimeoutId) {
|
|
49331
|
+
clearTimeout(killTimeoutId)
|
|
49332
|
+
killTimeoutId = null
|
|
49333
|
+
}
|
|
49334
|
+
}
|
|
49416
49335
|
|
|
49417
|
-
|
|
49336
|
+
child.stdout?.on('data', (data) => {
|
|
49337
|
+
stdout += data.toString()
|
|
49338
|
+
})
|
|
49418
49339
|
|
|
49419
|
-
|
|
49340
|
+
child.stderr?.on('data', (data) => {
|
|
49341
|
+
stderr += data.toString()
|
|
49342
|
+
})
|
|
49420
49343
|
|
|
49421
|
-
|
|
49422
|
-
|
|
49423
|
-
|
|
49424
|
-
|
|
49425
|
-
|
|
49344
|
+
child.on('close', (code) => {
|
|
49345
|
+
cleanup()
|
|
49346
|
+
if (!resolved) {
|
|
49347
|
+
resolved = true
|
|
49348
|
+
resolve({ exitCode: code || 0, stdout, stderr })
|
|
49349
|
+
}
|
|
49350
|
+
})
|
|
49426
49351
|
|
|
49427
|
-
|
|
49352
|
+
child.on('error', (err) => {
|
|
49353
|
+
cleanup()
|
|
49354
|
+
if (!resolved) {
|
|
49355
|
+
resolved = true
|
|
49356
|
+
resolve({ exitCode: 1, stdout, stderr: err.message || '' })
|
|
49357
|
+
}
|
|
49358
|
+
})
|
|
49428
49359
|
|
|
49429
|
-
|
|
49360
|
+
// Add timeout protection to prevent hanging
|
|
49361
|
+
const timeoutId = setTimeout(() => {
|
|
49362
|
+
if (!resolved) {
|
|
49363
|
+
resolved = true
|
|
49364
|
+
log(\`Command timeout: \${FULCRUM_CMD} \${args.join(' ')}\`)
|
|
49365
|
+
child.kill('SIGTERM')
|
|
49366
|
+
// Schedule SIGKILL if process doesn't exit after SIGTERM
|
|
49367
|
+
killTimeoutId = setTimeout(() => {
|
|
49368
|
+
if (!processExited) {
|
|
49369
|
+
log(\`Process didn't exit after SIGTERM, sending SIGKILL\`)
|
|
49370
|
+
child.kill('SIGKILL')
|
|
49371
|
+
}
|
|
49372
|
+
}, 2000)
|
|
49373
|
+
resolve({ exitCode: -1, stdout, stderr: \`Command timed out after \${FULCRUM_COMMAND_TIMEOUT_MS}ms\` })
|
|
49374
|
+
}
|
|
49375
|
+
}, FULCRUM_COMMAND_TIMEOUT_MS)
|
|
49430
49376
|
|
|
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
|
|
49377
|
+
// Clear timeout if command completes
|
|
49378
|
+
child.on('exit', () => clearTimeout(timeoutId))
|
|
49379
|
+
})
|
|
49380
|
+
}
|
|
49440
49381
|
|
|
49441
|
-
|
|
49382
|
+
let mainSessionId: string | null = null
|
|
49383
|
+
const subagentSessions = new Set<string>()
|
|
49384
|
+
let pendingIdleTimer: ReturnType<typeof setTimeout> | null = null
|
|
49385
|
+
let activityVersion = 0
|
|
49386
|
+
let lastStatus: "in-progress" | "review" | "" = ""
|
|
49442
49387
|
|
|
49443
|
-
|
|
49444
|
-
|
|
49445
|
-
|
|
49446
|
-
|
|
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
|
|
49388
|
+
const FULCRUM_CMD = "fulcrum"
|
|
49389
|
+
const IDLE_CONFIRMATION_DELAY_MS = 1500
|
|
49390
|
+
const FULCRUM_COMMAND_TIMEOUT_MS = 10000
|
|
49391
|
+
const STATUS_CHANGE_DEBOUNCE_MS = 500
|
|
49450
49392
|
|
|
49451
|
-
|
|
49393
|
+
let deferredContextCheck: Promise<boolean> | null = null
|
|
49394
|
+
let isFulcrumContext: boolean | null = null
|
|
49395
|
+
let pendingStatusCommand: Promise<{ exitCode: number; stdout: string; stderr: string }> | null = null
|
|
49452
49396
|
|
|
49453
|
-
|
|
49454
|
-
|
|
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
|
|
49397
|
+
export const FulcrumPlugin: Plugin = async ({ $, directory }) => {
|
|
49398
|
+
log("Plugin initializing...")
|
|
49462
49399
|
|
|
49463
|
-
|
|
49400
|
+
if (process.env.FULCRUM_TASK_ID) {
|
|
49401
|
+
isFulcrumContext = true
|
|
49402
|
+
log("Fulcrum context detected via env var")
|
|
49403
|
+
} else {
|
|
49404
|
+
deferredContextCheck = Promise.all([
|
|
49405
|
+
$\`\${FULCRUM_CMD} --version\`.quiet().nothrow().text(),
|
|
49406
|
+
runFulcrumCommand(['current-task', '--path', directory]),
|
|
49407
|
+
])
|
|
49408
|
+
.then(([versionResult, taskResult]) => {
|
|
49409
|
+
if (!versionResult) {
|
|
49410
|
+
log("Fulcrum CLI not found")
|
|
49411
|
+
return false
|
|
49412
|
+
}
|
|
49413
|
+
const inContext = taskResult.exitCode === 0
|
|
49414
|
+
log(inContext ? "Fulcrum context active" : "Not a Fulcrum context")
|
|
49415
|
+
return inContext
|
|
49416
|
+
})
|
|
49417
|
+
.catch(() => {
|
|
49418
|
+
log("Fulcrum check failed")
|
|
49419
|
+
return false
|
|
49420
|
+
})
|
|
49421
|
+
}
|
|
49464
49422
|
|
|
49465
|
-
|
|
49423
|
+
log("Plugin hooks registered")
|
|
49466
49424
|
|
|
49467
|
-
|
|
49468
|
-
|
|
49469
|
-
|
|
49470
|
-
|
|
49471
|
-
|
|
49472
|
-
|
|
49473
|
-
|
|
49425
|
+
const checkContext = async (): Promise<boolean> => {
|
|
49426
|
+
if (isFulcrumContext !== null) return isFulcrumContext
|
|
49427
|
+
if (deferredContextCheck) {
|
|
49428
|
+
isFulcrumContext = await deferredContextCheck
|
|
49429
|
+
deferredContextCheck = null
|
|
49430
|
+
return isFulcrumContext
|
|
49431
|
+
}
|
|
49432
|
+
return false
|
|
49433
|
+
}
|
|
49474
49434
|
|
|
49475
|
-
|
|
49435
|
+
const cancelPendingIdle = () => {
|
|
49436
|
+
if (pendingIdleTimer) {
|
|
49437
|
+
clearTimeout(pendingIdleTimer)
|
|
49438
|
+
pendingIdleTimer = null
|
|
49439
|
+
log("Cancelled pending idle transition")
|
|
49440
|
+
}
|
|
49441
|
+
}
|
|
49476
49442
|
|
|
49477
|
-
|
|
49443
|
+
const setStatus = (status: "in-progress" | "review") => {
|
|
49444
|
+
if (status === lastStatus) return
|
|
49478
49445
|
|
|
49479
|
-
|
|
49446
|
+
cancelPendingIdle()
|
|
49480
49447
|
|
|
49481
|
-
|
|
49448
|
+
if (pendingStatusCommand) {
|
|
49449
|
+
log(\`Status change already in progress, will retry after \${STATUS_CHANGE_DEBOUNCE_MS}ms\`)
|
|
49450
|
+
setTimeout(() => setStatus(status), STATUS_CHANGE_DEBOUNCE_MS)
|
|
49451
|
+
return
|
|
49452
|
+
}
|
|
49482
49453
|
|
|
49483
|
-
|
|
49484
|
-
{
|
|
49485
|
-
"command": "echo hello world",
|
|
49486
|
-
"sessionId": "optional-session-id",
|
|
49487
|
-
"cwd": "/path/to/start",
|
|
49488
|
-
"timeout": 30000,
|
|
49489
|
-
"name": "my-session"
|
|
49490
|
-
}
|
|
49491
|
-
\`\`\`
|
|
49454
|
+
lastStatus = status
|
|
49492
49455
|
|
|
49493
|
-
|
|
49494
|
-
|
|
49495
|
-
|
|
49496
|
-
|
|
49497
|
-
|
|
49498
|
-
|
|
49456
|
+
;(async () => {
|
|
49457
|
+
try {
|
|
49458
|
+
log(\`Setting status: \${status}\`)
|
|
49459
|
+
pendingStatusCommand = runFulcrumCommand(['current-task', status, '--path', directory])
|
|
49460
|
+
const res = await pendingStatusCommand
|
|
49461
|
+
pendingStatusCommand = null
|
|
49499
49462
|
|
|
49500
|
-
|
|
49501
|
-
|
|
49502
|
-
|
|
49503
|
-
|
|
49504
|
-
|
|
49505
|
-
|
|
49506
|
-
|
|
49507
|
-
|
|
49508
|
-
}
|
|
49509
|
-
\`\`\`
|
|
49463
|
+
if (res.exitCode !== 0) {
|
|
49464
|
+
log(\`Status update failed: exitCode=\${res.exitCode}, stderr=\${res.stderr}\`)
|
|
49465
|
+
}
|
|
49466
|
+
} catch (e) {
|
|
49467
|
+
log(\`Status update error: \${e}\`)
|
|
49468
|
+
pendingStatusCommand = null
|
|
49469
|
+
}
|
|
49470
|
+
})()
|
|
49471
|
+
}
|
|
49510
49472
|
|
|
49511
|
-
|
|
49473
|
+
const scheduleIdleTransition = () => {
|
|
49474
|
+
cancelPendingIdle()
|
|
49475
|
+
const currentVersion = ++activityVersion
|
|
49512
49476
|
|
|
49513
|
-
|
|
49514
|
-
|
|
49515
|
-
|
|
49516
|
-
|
|
49477
|
+
pendingIdleTimer = setTimeout(() => {
|
|
49478
|
+
if (activityVersion !== currentVersion) {
|
|
49479
|
+
log(
|
|
49480
|
+
\`Stale idle transition (version \${currentVersion} vs \${activityVersion})\`,
|
|
49481
|
+
)
|
|
49482
|
+
return
|
|
49483
|
+
}
|
|
49484
|
+
setStatus("review")
|
|
49485
|
+
}, IDLE_CONFIRMATION_DELAY_MS)
|
|
49517
49486
|
|
|
49518
|
-
|
|
49519
|
-
|
|
49520
|
-
|
|
49487
|
+
log(
|
|
49488
|
+
\`Scheduled idle transition (version \${currentVersion}, delay \${IDLE_CONFIRMATION_DELAY_MS}ms)\`,
|
|
49489
|
+
)
|
|
49490
|
+
}
|
|
49521
49491
|
|
|
49522
|
-
|
|
49523
|
-
|
|
49492
|
+
const recordActivity = (reason: string) => {
|
|
49493
|
+
activityVersion++
|
|
49494
|
+
cancelPendingIdle()
|
|
49495
|
+
log(\`Activity: \${reason} (version now \${activityVersion})\`)
|
|
49496
|
+
}
|
|
49524
49497
|
|
|
49525
|
-
|
|
49526
|
-
|
|
49498
|
+
return {
|
|
49499
|
+
"chat.message": async (_input, output) => {
|
|
49500
|
+
if (!(await checkContext())) return
|
|
49527
49501
|
|
|
49528
|
-
|
|
49529
|
-
|
|
49530
|
-
|
|
49502
|
+
if (output.message.role === "user") {
|
|
49503
|
+
recordActivity("user message")
|
|
49504
|
+
setStatus("in-progress")
|
|
49505
|
+
} else if (output.message.role === "assistant") {
|
|
49506
|
+
recordActivity("assistant message")
|
|
49507
|
+
}
|
|
49508
|
+
},
|
|
49531
49509
|
|
|
49532
|
-
|
|
49510
|
+
event: async ({ event }) => {
|
|
49511
|
+
if (!NOISY_EVENTS.has(event.type)) {
|
|
49512
|
+
log(\`Event: \${event.type}\`)
|
|
49513
|
+
}
|
|
49533
49514
|
|
|
49534
|
-
|
|
49515
|
+
if (!(await checkContext())) return
|
|
49535
49516
|
|
|
49536
|
-
|
|
49517
|
+
const props = (event.properties as Record<string, unknown>) || {}
|
|
49537
49518
|
|
|
49538
|
-
|
|
49519
|
+
if (event.type === "session.created") {
|
|
49520
|
+
const info = (props.info as Record<string, unknown>) || {}
|
|
49521
|
+
const sessionId = info.id as string | undefined
|
|
49522
|
+
const parentId = info.parentID as string | undefined
|
|
49539
49523
|
|
|
49540
|
-
|
|
49524
|
+
if (parentId) {
|
|
49525
|
+
if (sessionId) subagentSessions.add(sessionId)
|
|
49526
|
+
log(\`Subagent session tracked: \${sessionId} (parent: \${parentId})\`)
|
|
49527
|
+
} else if (!mainSessionId && sessionId) {
|
|
49528
|
+
mainSessionId = sessionId
|
|
49529
|
+
log(\`Main session set: \${mainSessionId}\`)
|
|
49530
|
+
}
|
|
49541
49531
|
|
|
49542
|
-
|
|
49532
|
+
recordActivity("session.created")
|
|
49533
|
+
setStatus("in-progress")
|
|
49534
|
+
return
|
|
49535
|
+
}
|
|
49543
49536
|
|
|
49544
|
-
|
|
49537
|
+
const status = props.status as Record<string, unknown> | undefined
|
|
49538
|
+
if (
|
|
49539
|
+
(event.type === "session.status" && status?.type === "busy") ||
|
|
49540
|
+
event.type.startsWith("tool.execute")
|
|
49541
|
+
) {
|
|
49542
|
+
recordActivity(event.type)
|
|
49543
|
+
return
|
|
49544
|
+
}
|
|
49545
49545
|
|
|
49546
|
-
|
|
49546
|
+
if (
|
|
49547
|
+
event.type === "session.idle" ||
|
|
49548
|
+
(event.type === "session.status" && status?.type === "idle")
|
|
49549
|
+
) {
|
|
49550
|
+
const info = (props.info as Record<string, unknown>) || {}
|
|
49551
|
+
const sessionId =
|
|
49552
|
+
(props.sessionID as string) || (info.id as string) || null
|
|
49547
49553
|
|
|
49548
|
-
|
|
49549
|
-
|
|
49550
|
-
|
|
49551
|
-
|
|
49552
|
-
|
|
49553
|
-
|
|
49554
|
-
|
|
49555
|
-
|
|
49554
|
+
if (sessionId && subagentSessions.has(sessionId)) {
|
|
49555
|
+
log(\`Ignoring subagent idle: \${sessionId}\`)
|
|
49556
|
+
return
|
|
49557
|
+
}
|
|
49558
|
+
|
|
49559
|
+
if (mainSessionId && sessionId && sessionId !== mainSessionId) {
|
|
49560
|
+
log(\`Ignoring non-main idle: \${sessionId} (main: \${mainSessionId})\`)
|
|
49561
|
+
return
|
|
49562
|
+
}
|
|
49563
|
+
|
|
49564
|
+
log(\`Main session idle detected: \${sessionId}\`)
|
|
49565
|
+
scheduleIdleTransition()
|
|
49566
|
+
}
|
|
49567
|
+
},
|
|
49568
|
+
}
|
|
49569
|
+
}
|
|
49556
49570
|
`;
|
|
49557
49571
|
|
|
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) {
|
|
49572
|
+
// cli/src/commands/opencode.ts
|
|
49573
|
+
var OPENCODE_DIR = join5(homedir3(), ".opencode");
|
|
49574
|
+
var OPENCODE_CONFIG_PATH = join5(OPENCODE_DIR, "opencode.json");
|
|
49575
|
+
var PLUGIN_DIR2 = join5(homedir3(), ".config", "opencode", "plugin");
|
|
49576
|
+
var PLUGIN_PATH = join5(PLUGIN_DIR2, "fulcrum.ts");
|
|
49577
|
+
var FULCRUM_MCP_CONFIG = {
|
|
49578
|
+
type: "local",
|
|
49579
|
+
command: ["fulcrum", "mcp"],
|
|
49580
|
+
enabled: true
|
|
49581
|
+
};
|
|
49582
|
+
async function handleOpenCodeCommand(action) {
|
|
49572
49583
|
if (action === "install") {
|
|
49573
|
-
await
|
|
49584
|
+
await installOpenCodeIntegration();
|
|
49574
49585
|
return;
|
|
49575
49586
|
}
|
|
49576
49587
|
if (action === "uninstall") {
|
|
49577
|
-
await
|
|
49588
|
+
await uninstallOpenCodeIntegration();
|
|
49578
49589
|
return;
|
|
49579
49590
|
}
|
|
49580
|
-
throw new CliError("INVALID_ACTION", "Unknown action. Usage: fulcrum
|
|
49591
|
+
throw new CliError("INVALID_ACTION", "Unknown action. Usage: fulcrum opencode install | fulcrum opencode uninstall", ExitCodes.INVALID_ARGS);
|
|
49581
49592
|
}
|
|
49582
|
-
async function
|
|
49593
|
+
async function installOpenCodeIntegration() {
|
|
49583
49594
|
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);
|
|
49595
|
+
console.log("Installing OpenCode plugin...");
|
|
49596
|
+
mkdirSync4(PLUGIN_DIR2, { recursive: true });
|
|
49597
|
+
writeFileSync4(PLUGIN_PATH, fulcrum_opencode_default, "utf-8");
|
|
49598
|
+
console.log("\u2713 Installed plugin at " + PLUGIN_PATH);
|
|
49599
|
+
console.log("Configuring MCP server...");
|
|
49600
|
+
const mcpConfigured = addMcpServer();
|
|
49596
49601
|
console.log("");
|
|
49597
|
-
|
|
49602
|
+
if (mcpConfigured) {
|
|
49603
|
+
console.log("Installation complete! Restart OpenCode to apply changes.");
|
|
49604
|
+
} else {
|
|
49605
|
+
console.log("Plugin installed, but MCP configuration was skipped.");
|
|
49606
|
+
console.log("Please add the MCP server manually (see above).");
|
|
49607
|
+
}
|
|
49598
49608
|
} catch (err) {
|
|
49599
|
-
throw new CliError("INSTALL_FAILED", `Failed to install
|
|
49609
|
+
throw new CliError("INSTALL_FAILED", `Failed to install OpenCode integration: ${err instanceof Error ? err.message : String(err)}`, ExitCodes.ERROR);
|
|
49600
49610
|
}
|
|
49601
49611
|
}
|
|
49602
|
-
async function
|
|
49612
|
+
async function uninstallOpenCodeIntegration() {
|
|
49603
49613
|
try {
|
|
49604
|
-
|
|
49605
|
-
|
|
49606
|
-
|
|
49607
|
-
|
|
49608
|
-
console.log("
|
|
49614
|
+
let removedPlugin = false;
|
|
49615
|
+
let removedMcp = false;
|
|
49616
|
+
if (existsSync5(PLUGIN_PATH)) {
|
|
49617
|
+
unlinkSync2(PLUGIN_PATH);
|
|
49618
|
+
console.log("\u2713 Removed plugin from " + PLUGIN_PATH);
|
|
49619
|
+
removedPlugin = true;
|
|
49609
49620
|
} else {
|
|
49610
|
-
console.log("
|
|
49621
|
+
console.log("\u2022 Plugin not found (already removed)");
|
|
49622
|
+
}
|
|
49623
|
+
removedMcp = removeMcpServer();
|
|
49624
|
+
if (!removedPlugin && !removedMcp) {
|
|
49625
|
+
console.log("Nothing to uninstall.");
|
|
49626
|
+
} else {
|
|
49627
|
+
console.log("");
|
|
49628
|
+
console.log("Uninstall complete! Restart OpenCode to apply changes.");
|
|
49611
49629
|
}
|
|
49612
49630
|
} catch (err) {
|
|
49613
|
-
throw new CliError("UNINSTALL_FAILED", `Failed to uninstall
|
|
49631
|
+
throw new CliError("UNINSTALL_FAILED", `Failed to uninstall OpenCode integration: ${err instanceof Error ? err.message : String(err)}`, ExitCodes.ERROR);
|
|
49632
|
+
}
|
|
49633
|
+
}
|
|
49634
|
+
function getMcpObject(config3) {
|
|
49635
|
+
const mcp = config3.mcp;
|
|
49636
|
+
if (mcp && typeof mcp === "object" && !Array.isArray(mcp)) {
|
|
49637
|
+
return mcp;
|
|
49638
|
+
}
|
|
49639
|
+
return {};
|
|
49640
|
+
}
|
|
49641
|
+
function addMcpServer() {
|
|
49642
|
+
mkdirSync4(OPENCODE_DIR, { recursive: true });
|
|
49643
|
+
let config3 = {};
|
|
49644
|
+
if (existsSync5(OPENCODE_CONFIG_PATH)) {
|
|
49645
|
+
try {
|
|
49646
|
+
const content = readFileSync5(OPENCODE_CONFIG_PATH, "utf-8");
|
|
49647
|
+
config3 = JSON.parse(content);
|
|
49648
|
+
} catch {
|
|
49649
|
+
console.log("\u26A0 Could not parse existing opencode.json, skipping MCP configuration");
|
|
49650
|
+
console.log(" Add manually to ~/.opencode/opencode.json:");
|
|
49651
|
+
console.log(' "mcp": { "fulcrum": { "type": "local", "command": ["fulcrum", "mcp"], "enabled": true } }');
|
|
49652
|
+
return false;
|
|
49653
|
+
}
|
|
49654
|
+
}
|
|
49655
|
+
const mcp = getMcpObject(config3);
|
|
49656
|
+
if (mcp.fulcrum) {
|
|
49657
|
+
console.log("\u2022 MCP server already configured, preserving existing configuration");
|
|
49658
|
+
return true;
|
|
49659
|
+
}
|
|
49660
|
+
if (existsSync5(OPENCODE_CONFIG_PATH)) {
|
|
49661
|
+
copyFileSync(OPENCODE_CONFIG_PATH, OPENCODE_CONFIG_PATH + ".backup");
|
|
49662
|
+
}
|
|
49663
|
+
config3.mcp = {
|
|
49664
|
+
...mcp,
|
|
49665
|
+
fulcrum: FULCRUM_MCP_CONFIG
|
|
49666
|
+
};
|
|
49667
|
+
const tempPath = OPENCODE_CONFIG_PATH + ".tmp";
|
|
49668
|
+
try {
|
|
49669
|
+
writeFileSync4(tempPath, JSON.stringify(config3, null, 2), "utf-8");
|
|
49670
|
+
renameSync(tempPath, OPENCODE_CONFIG_PATH);
|
|
49671
|
+
} catch (error46) {
|
|
49672
|
+
try {
|
|
49673
|
+
if (existsSync5(tempPath)) {
|
|
49674
|
+
unlinkSync2(tempPath);
|
|
49675
|
+
}
|
|
49676
|
+
} catch {}
|
|
49677
|
+
throw error46;
|
|
49678
|
+
}
|
|
49679
|
+
console.log("\u2713 Added MCP server to " + OPENCODE_CONFIG_PATH);
|
|
49680
|
+
return true;
|
|
49681
|
+
}
|
|
49682
|
+
function removeMcpServer() {
|
|
49683
|
+
if (!existsSync5(OPENCODE_CONFIG_PATH)) {
|
|
49684
|
+
console.log("\u2022 MCP config not found (already removed)");
|
|
49685
|
+
return false;
|
|
49686
|
+
}
|
|
49687
|
+
let config3;
|
|
49688
|
+
try {
|
|
49689
|
+
const content = readFileSync5(OPENCODE_CONFIG_PATH, "utf-8");
|
|
49690
|
+
config3 = JSON.parse(content);
|
|
49691
|
+
} catch {
|
|
49692
|
+
console.log("\u26A0 Could not parse opencode.json, skipping MCP removal");
|
|
49693
|
+
return false;
|
|
49694
|
+
}
|
|
49695
|
+
const mcp = getMcpObject(config3);
|
|
49696
|
+
if (!mcp.fulcrum) {
|
|
49697
|
+
console.log("\u2022 MCP server not configured (already removed)");
|
|
49698
|
+
return false;
|
|
49699
|
+
}
|
|
49700
|
+
copyFileSync(OPENCODE_CONFIG_PATH, OPENCODE_CONFIG_PATH + ".backup");
|
|
49701
|
+
delete mcp.fulcrum;
|
|
49702
|
+
if (Object.keys(mcp).length === 0) {
|
|
49703
|
+
delete config3.mcp;
|
|
49704
|
+
} else {
|
|
49705
|
+
config3.mcp = mcp;
|
|
49706
|
+
}
|
|
49707
|
+
const tempPath = OPENCODE_CONFIG_PATH + ".tmp";
|
|
49708
|
+
try {
|
|
49709
|
+
writeFileSync4(tempPath, JSON.stringify(config3, null, 2), "utf-8");
|
|
49710
|
+
renameSync(tempPath, OPENCODE_CONFIG_PATH);
|
|
49711
|
+
} catch (error46) {
|
|
49712
|
+
try {
|
|
49713
|
+
if (existsSync5(tempPath)) {
|
|
49714
|
+
unlinkSync2(tempPath);
|
|
49715
|
+
}
|
|
49716
|
+
} catch {}
|
|
49717
|
+
throw error46;
|
|
49614
49718
|
}
|
|
49719
|
+
console.log("\u2713 Removed MCP server from " + OPENCODE_CONFIG_PATH);
|
|
49720
|
+
return true;
|
|
49615
49721
|
}
|
|
49616
49722
|
|
|
49617
49723
|
// cli/src/commands/notifications.ts
|