@knowsuchagency/fulcrum 3.15.2 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/bin/fulcrum.js +216 -86
- package/package.json +1 -1
- package/server/index.js +1435 -1097
package/README.md
CHANGED
|
@@ -78,7 +78,7 @@ Every hour, your assistant reviews pending events, checks on blocked or overdue
|
|
|
78
78
|
npx @knowsuchagency/fulcrum@latest up
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
Fulcrum will check for dependencies (bun, dtach, AI agent CLI), offer to install any that are missing, and start the server on http://localhost:7777.
|
|
81
|
+
Fulcrum will check for dependencies (bun, dtach, fnox, age, AI agent CLI), offer to install any that are missing, set up encrypted secret storage, and start the server on http://localhost:7777.
|
|
82
82
|
|
|
83
83
|
### Desktop App
|
|
84
84
|
|
|
@@ -216,7 +216,7 @@ A two-tier memory system gives agents both always-on context and on-demand recal
|
|
|
216
216
|
|
|
217
217
|
### System Monitoring
|
|
218
218
|
|
|
219
|
-
Track CPU, memory, and disk usage while your agents work. Jobs is a top-level page (`/jobs`, Cmd+6) for managing systemd (Linux) or launchd (macOS) timers. The Messages tab under Monitoring shows all channel messages (WhatsApp, Discord, Telegram, Slack, Email) with filtering by channel and direction.
|
|
219
|
+
Track CPU, memory, and disk usage while your agents work. Jobs is a top-level page (`/jobs`, Cmd+6) for managing systemd (Linux) or launchd (macOS) timers. The Messages tab under Monitoring shows all channel messages (WhatsApp, Discord, Telegram, Slack, Email) with filtering by channel and direction. The Observer tab tracks every observe-only message processing attempt with circuit breaker status, aggregate stats, and a filterable invocations list.
|
|
220
220
|
|
|
221
221
|

|
|
222
222
|
|
|
@@ -315,6 +315,8 @@ For browser-only access, use Tailscale or Cloudflare Tunnels to expose your serv
|
|
|
315
315
|
<details>
|
|
316
316
|
<summary><strong>Configuration</strong></summary>
|
|
317
317
|
|
|
318
|
+
Sensitive credentials (API keys, tokens, webhook URLs) are encrypted using [fnox](https://github.com/yarlson/fnox) with age encryption. The age key and encrypted secrets live in the fulcrum directory (`age.txt` and `fnox.toml`). Non-sensitive settings are stored in `settings.json`. Existing plain-text secrets are automatically migrated to fnox on server start.
|
|
319
|
+
|
|
318
320
|
Settings are stored in `.fulcrum/settings.json`. The fulcrum directory is resolved in this order:
|
|
319
321
|
|
|
320
322
|
1. `FULCRUM_DIR` environment variable
|
package/bin/fulcrum.js
CHANGED
|
@@ -785,31 +785,31 @@ var init_prompt = __esm(() => {
|
|
|
785
785
|
});
|
|
786
786
|
|
|
787
787
|
// cli/src/utils/server.ts
|
|
788
|
-
import { existsSync,
|
|
788
|
+
import { existsSync, mkdirSync, cpSync } from "fs";
|
|
789
|
+
import { execSync } from "child_process";
|
|
789
790
|
import { join } from "path";
|
|
790
791
|
import { homedir } from "os";
|
|
791
|
-
function getPortFromSettings(settings) {
|
|
792
|
-
if (!settings)
|
|
793
|
-
return null;
|
|
794
|
-
if (settings.server?.port) {
|
|
795
|
-
return settings.server.port;
|
|
796
|
-
}
|
|
797
|
-
if (settings.port) {
|
|
798
|
-
return settings.port;
|
|
799
|
-
}
|
|
800
|
-
return null;
|
|
801
|
-
}
|
|
802
792
|
function expandPath(p) {
|
|
803
793
|
if (p.startsWith("~/")) {
|
|
804
794
|
return join(homedir(), p.slice(2));
|
|
805
795
|
}
|
|
806
796
|
return p;
|
|
807
797
|
}
|
|
808
|
-
function
|
|
798
|
+
function getPortFromFnox(fulcrumDir) {
|
|
799
|
+
const fnoxConfigPath = join(fulcrumDir, "fnox.toml");
|
|
800
|
+
const ageKeyPath = join(fulcrumDir, "age.txt");
|
|
801
|
+
if (!existsSync(fnoxConfigPath) || !existsSync(ageKeyPath))
|
|
802
|
+
return null;
|
|
809
803
|
try {
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
804
|
+
const value = execSync(`fnox get FULCRUM_SERVER_PORT -c "${fnoxConfigPath}" --if-missing ignore`, {
|
|
805
|
+
env: { ...process.env, FNOX_AGE_KEY_FILE: ageKeyPath },
|
|
806
|
+
encoding: "utf-8",
|
|
807
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
808
|
+
}).trim();
|
|
809
|
+
if (value) {
|
|
810
|
+
const port = parseInt(value, 10);
|
|
811
|
+
if (!isNaN(port) && port > 0)
|
|
812
|
+
return port;
|
|
813
813
|
}
|
|
814
814
|
} catch {}
|
|
815
815
|
return null;
|
|
@@ -825,44 +825,39 @@ function discoverServerUrl(urlOverride, portOverride) {
|
|
|
825
825
|
return process.env.FULCRUM_URL;
|
|
826
826
|
}
|
|
827
827
|
if (process.env.FULCRUM_DIR) {
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
const homeSettings = readSettingsFile(globalSettings);
|
|
843
|
-
const homePort = getPortFromSettings(homeSettings);
|
|
844
|
-
if (homePort) {
|
|
845
|
-
return `http://localhost:${homePort}`;
|
|
846
|
-
}
|
|
828
|
+
const port2 = getPortFromFnox(expandPath(process.env.FULCRUM_DIR));
|
|
829
|
+
if (port2)
|
|
830
|
+
return `http://localhost:${port2}`;
|
|
831
|
+
}
|
|
832
|
+
const cwdFulcrum = join(process.cwd(), ".fulcrum");
|
|
833
|
+
if (existsSync(cwdFulcrum)) {
|
|
834
|
+
const port2 = getPortFromFnox(cwdFulcrum);
|
|
835
|
+
if (port2)
|
|
836
|
+
return `http://localhost:${port2}`;
|
|
837
|
+
}
|
|
838
|
+
const globalFulcrum = join(homedir(), ".fulcrum");
|
|
839
|
+
const port = getPortFromFnox(globalFulcrum);
|
|
840
|
+
if (port)
|
|
841
|
+
return `http://localhost:${port}`;
|
|
847
842
|
return `http://localhost:${DEFAULT_PORT}`;
|
|
848
843
|
}
|
|
849
844
|
function updateSettingsPort(port) {
|
|
850
845
|
const fulcrumDir = getFulcrumDir();
|
|
851
|
-
const
|
|
852
|
-
|
|
853
|
-
try {
|
|
854
|
-
if (existsSync(settingsPath)) {
|
|
855
|
-
settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
856
|
-
}
|
|
857
|
-
} catch {}
|
|
858
|
-
if (!settings.server || typeof settings.server !== "object") {
|
|
859
|
-
settings.server = {};
|
|
860
|
-
}
|
|
861
|
-
settings.server.port = port;
|
|
846
|
+
const fnoxConfigPath = join(fulcrumDir, "fnox.toml");
|
|
847
|
+
const ageKeyPath = join(fulcrumDir, "age.txt");
|
|
862
848
|
if (!existsSync(fulcrumDir)) {
|
|
863
849
|
mkdirSync(fulcrumDir, { recursive: true });
|
|
864
850
|
}
|
|
865
|
-
|
|
851
|
+
if (!existsSync(fnoxConfigPath) || !existsSync(ageKeyPath)) {
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
try {
|
|
855
|
+
execSync(`fnox set FULCRUM_SERVER_PORT -p plain -c "${fnoxConfigPath}"`, {
|
|
856
|
+
env: { ...process.env, FNOX_AGE_KEY_FILE: ageKeyPath },
|
|
857
|
+
input: String(port),
|
|
858
|
+
stdio: ["pipe", "ignore", "ignore"]
|
|
859
|
+
});
|
|
860
|
+
} catch {}
|
|
866
861
|
}
|
|
867
862
|
function getFulcrumDir() {
|
|
868
863
|
if (process.env.FULCRUM_DIR) {
|
|
@@ -895,15 +890,6 @@ function migrateFromVibora() {
|
|
|
895
890
|
mkdirSync(fulcrumDir, { recursive: true });
|
|
896
891
|
}
|
|
897
892
|
cpSync(viboraDir, fulcrumDir, { recursive: true });
|
|
898
|
-
const settingsPath = join(fulcrumDir, "settings.json");
|
|
899
|
-
if (existsSync(settingsPath)) {
|
|
900
|
-
try {
|
|
901
|
-
let content = readFileSync(settingsPath, "utf-8");
|
|
902
|
-
content = content.replace(/\.vibora/g, ".fulcrum");
|
|
903
|
-
content = content.replace(/vibora\.(db|log|pid)/g, "fulcrum.$1");
|
|
904
|
-
writeFileSync(settingsPath, content, "utf-8");
|
|
905
|
-
} catch {}
|
|
906
|
-
}
|
|
907
893
|
const oldDbPath = join(fulcrumDir, "vibora.db");
|
|
908
894
|
const newDbPath = join(fulcrumDir, "fulcrum.db");
|
|
909
895
|
if (existsSync(oldDbPath) && !existsSync(newDbPath)) {
|
|
@@ -965,7 +951,7 @@ var init_errors = __esm(() => {
|
|
|
965
951
|
});
|
|
966
952
|
|
|
967
953
|
// cli/src/client.ts
|
|
968
|
-
import { readFileSync
|
|
954
|
+
import { readFileSync } from "fs";
|
|
969
955
|
import { basename } from "path";
|
|
970
956
|
|
|
971
957
|
class FulcrumClient {
|
|
@@ -1228,7 +1214,7 @@ class FulcrumClient {
|
|
|
1228
1214
|
return this.fetch(`/api/tasks/${taskId}/attachments`);
|
|
1229
1215
|
}
|
|
1230
1216
|
async uploadTaskAttachment(taskId, filePath) {
|
|
1231
|
-
const fileContent =
|
|
1217
|
+
const fileContent = readFileSync(filePath);
|
|
1232
1218
|
const filename = basename(filePath);
|
|
1233
1219
|
const formData = new FormData;
|
|
1234
1220
|
const blob = new Blob([fileContent]);
|
|
@@ -1315,7 +1301,7 @@ class FulcrumClient {
|
|
|
1315
1301
|
return this.fetch(`/api/projects/${projectId}/attachments`);
|
|
1316
1302
|
}
|
|
1317
1303
|
async uploadProjectAttachment(projectId, filePath) {
|
|
1318
|
-
const fileContent =
|
|
1304
|
+
const fileContent = readFileSync(filePath);
|
|
1319
1305
|
const filename = basename(filePath);
|
|
1320
1306
|
const formData = new FormData;
|
|
1321
1307
|
const blob = new Blob([fileContent]);
|
|
@@ -46466,7 +46452,7 @@ async function runMcpServer(urlOverride, portOverride) {
|
|
|
46466
46452
|
const client = new FulcrumClient(urlOverride, portOverride);
|
|
46467
46453
|
const server = new McpServer({
|
|
46468
46454
|
name: "fulcrum",
|
|
46469
|
-
version: "
|
|
46455
|
+
version: "4.0.0"
|
|
46470
46456
|
});
|
|
46471
46457
|
registerTools(server, client);
|
|
46472
46458
|
const transport = new StdioServerTransport;
|
|
@@ -48330,9 +48316,9 @@ var configCommand = defineCommand({
|
|
|
48330
48316
|
// cli/src/commands/opencode.ts
|
|
48331
48317
|
import {
|
|
48332
48318
|
mkdirSync as mkdirSync2,
|
|
48333
|
-
writeFileSync
|
|
48319
|
+
writeFileSync,
|
|
48334
48320
|
existsSync as existsSync2,
|
|
48335
|
-
readFileSync as
|
|
48321
|
+
readFileSync as readFileSync2,
|
|
48336
48322
|
unlinkSync,
|
|
48337
48323
|
copyFileSync,
|
|
48338
48324
|
renameSync
|
|
@@ -48650,7 +48636,7 @@ async function installOpenCodeIntegration() {
|
|
|
48650
48636
|
try {
|
|
48651
48637
|
console.log("Installing OpenCode plugin...");
|
|
48652
48638
|
mkdirSync2(PLUGIN_DIR, { recursive: true });
|
|
48653
|
-
|
|
48639
|
+
writeFileSync(PLUGIN_PATH, fulcrum_opencode_default, "utf-8");
|
|
48654
48640
|
console.log("\u2713 Installed plugin at " + PLUGIN_PATH);
|
|
48655
48641
|
console.log("Configuring MCP server...");
|
|
48656
48642
|
const mcpConfigured = addMcpServer();
|
|
@@ -48699,7 +48685,7 @@ function addMcpServer() {
|
|
|
48699
48685
|
let config = {};
|
|
48700
48686
|
if (existsSync2(OPENCODE_CONFIG_PATH)) {
|
|
48701
48687
|
try {
|
|
48702
|
-
const content =
|
|
48688
|
+
const content = readFileSync2(OPENCODE_CONFIG_PATH, "utf-8");
|
|
48703
48689
|
config = JSON.parse(content);
|
|
48704
48690
|
} catch {
|
|
48705
48691
|
console.log("\u26A0 Could not parse existing opencode.json, skipping MCP configuration");
|
|
@@ -48722,7 +48708,7 @@ function addMcpServer() {
|
|
|
48722
48708
|
};
|
|
48723
48709
|
const tempPath = OPENCODE_CONFIG_PATH + ".tmp";
|
|
48724
48710
|
try {
|
|
48725
|
-
|
|
48711
|
+
writeFileSync(tempPath, JSON.stringify(config, null, 2), "utf-8");
|
|
48726
48712
|
renameSync(tempPath, OPENCODE_CONFIG_PATH);
|
|
48727
48713
|
} catch (error) {
|
|
48728
48714
|
try {
|
|
@@ -48742,7 +48728,7 @@ function removeMcpServer() {
|
|
|
48742
48728
|
}
|
|
48743
48729
|
let config;
|
|
48744
48730
|
try {
|
|
48745
|
-
const content =
|
|
48731
|
+
const content = readFileSync2(OPENCODE_CONFIG_PATH, "utf-8");
|
|
48746
48732
|
config = JSON.parse(content);
|
|
48747
48733
|
} catch {
|
|
48748
48734
|
console.log("\u26A0 Could not parse opencode.json, skipping MCP removal");
|
|
@@ -48762,7 +48748,7 @@ function removeMcpServer() {
|
|
|
48762
48748
|
}
|
|
48763
48749
|
const tempPath = OPENCODE_CONFIG_PATH + ".tmp";
|
|
48764
48750
|
try {
|
|
48765
|
-
|
|
48751
|
+
writeFileSync(tempPath, JSON.stringify(config, null, 2), "utf-8");
|
|
48766
48752
|
renameSync(tempPath, OPENCODE_CONFIG_PATH);
|
|
48767
48753
|
} catch (error) {
|
|
48768
48754
|
try {
|
|
@@ -48799,7 +48785,7 @@ var opencodeCommand = defineCommand({
|
|
|
48799
48785
|
|
|
48800
48786
|
// cli/src/commands/claude.ts
|
|
48801
48787
|
import { spawnSync } from "child_process";
|
|
48802
|
-
import { mkdirSync as mkdirSync3, writeFileSync as
|
|
48788
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync2, existsSync as existsSync3, rmSync, readFileSync as readFileSync3 } from "fs";
|
|
48803
48789
|
import { homedir as homedir3 } from "os";
|
|
48804
48790
|
import { dirname, join as join3 } from "path";
|
|
48805
48791
|
init_errors();
|
|
@@ -48815,7 +48801,7 @@ var marketplace_default = `{
|
|
|
48815
48801
|
"name": "fulcrum",
|
|
48816
48802
|
"source": "./",
|
|
48817
48803
|
"description": "Task orchestration for Claude Code",
|
|
48818
|
-
"version": "
|
|
48804
|
+
"version": "4.0.0",
|
|
48819
48805
|
"skills": [
|
|
48820
48806
|
"./skills/fulcrum"
|
|
48821
48807
|
],
|
|
@@ -49335,7 +49321,7 @@ function getInstalledVersion() {
|
|
|
49335
49321
|
return null;
|
|
49336
49322
|
}
|
|
49337
49323
|
try {
|
|
49338
|
-
const installed = JSON.parse(
|
|
49324
|
+
const installed = JSON.parse(readFileSync3(installedMarketplace, "utf-8"));
|
|
49339
49325
|
return installed.plugins?.[0]?.version || null;
|
|
49340
49326
|
} catch {
|
|
49341
49327
|
return null;
|
|
@@ -49371,7 +49357,7 @@ async function installClaudePlugin(options = {}) {
|
|
|
49371
49357
|
for (const file of PLUGIN_FILES) {
|
|
49372
49358
|
const fullPath = join3(MARKETPLACE_DIR, file.path);
|
|
49373
49359
|
mkdirSync3(dirname(fullPath), { recursive: true });
|
|
49374
|
-
|
|
49360
|
+
writeFileSync2(fullPath, file.content, "utf-8");
|
|
49375
49361
|
}
|
|
49376
49362
|
log("\u2713 Created plugin files at " + MARKETPLACE_DIR);
|
|
49377
49363
|
runClaude(["plugin", "marketplace", "remove", MARKETPLACE_NAME]);
|
|
@@ -49658,14 +49644,14 @@ var notifyCommand = defineCommand({
|
|
|
49658
49644
|
|
|
49659
49645
|
// cli/src/commands/up.ts
|
|
49660
49646
|
import { spawn as spawn2 } from "child_process";
|
|
49661
|
-
import { existsSync as
|
|
49662
|
-
import { dirname as dirname3, join as
|
|
49647
|
+
import { existsSync as existsSync6 } from "fs";
|
|
49648
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
49663
49649
|
import { fileURLToPath } from "url";
|
|
49664
49650
|
init_errors();
|
|
49665
49651
|
|
|
49666
49652
|
// cli/src/utils/process.ts
|
|
49667
49653
|
init_server();
|
|
49668
|
-
import { existsSync as existsSync4, readFileSync as
|
|
49654
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2, mkdirSync as mkdirSync4 } from "fs";
|
|
49669
49655
|
import { join as join4, dirname as dirname2 } from "path";
|
|
49670
49656
|
function getPidPath() {
|
|
49671
49657
|
return join4(getFulcrumDir(), "fulcrum.pid");
|
|
@@ -49676,13 +49662,13 @@ function writePid(pid) {
|
|
|
49676
49662
|
if (!existsSync4(dir)) {
|
|
49677
49663
|
mkdirSync4(dir, { recursive: true });
|
|
49678
49664
|
}
|
|
49679
|
-
|
|
49665
|
+
writeFileSync3(pidPath, pid.toString(), "utf-8");
|
|
49680
49666
|
}
|
|
49681
49667
|
function readPid() {
|
|
49682
49668
|
const pidPath = getPidPath();
|
|
49683
49669
|
try {
|
|
49684
49670
|
if (existsSync4(pidPath)) {
|
|
49685
|
-
const content =
|
|
49671
|
+
const content = readFileSync4(pidPath, "utf-8").trim();
|
|
49686
49672
|
const pid = parseInt(content, 10);
|
|
49687
49673
|
return isNaN(pid) ? null : pid;
|
|
49688
49674
|
}
|
|
@@ -49738,7 +49724,7 @@ async function confirm(message) {
|
|
|
49738
49724
|
init_server();
|
|
49739
49725
|
|
|
49740
49726
|
// cli/src/utils/dependencies.ts
|
|
49741
|
-
import { execSync, spawnSync as spawnSync2 } from "child_process";
|
|
49727
|
+
import { execSync as execSync2, spawnSync as spawnSync2 } from "child_process";
|
|
49742
49728
|
var DEPENDENCIES = [
|
|
49743
49729
|
{
|
|
49744
49730
|
name: "bun",
|
|
@@ -49801,13 +49787,34 @@ var DEPENDENCIES = [
|
|
|
49801
49787
|
dnf: "sudo dnf install -y gh",
|
|
49802
49788
|
pacman: "sudo pacman -S --noconfirm github-cli"
|
|
49803
49789
|
}
|
|
49790
|
+
},
|
|
49791
|
+
{
|
|
49792
|
+
name: "fnox",
|
|
49793
|
+
command: "fnox",
|
|
49794
|
+
description: "Encrypted secrets management",
|
|
49795
|
+
required: true,
|
|
49796
|
+
install: {
|
|
49797
|
+
brew: "brew install fnox"
|
|
49798
|
+
}
|
|
49799
|
+
},
|
|
49800
|
+
{
|
|
49801
|
+
name: "age",
|
|
49802
|
+
command: "age-keygen",
|
|
49803
|
+
description: "Age encryption key generation",
|
|
49804
|
+
required: true,
|
|
49805
|
+
install: {
|
|
49806
|
+
brew: "brew install age",
|
|
49807
|
+
apt: "sudo apt install -y age",
|
|
49808
|
+
dnf: "sudo dnf install -y age",
|
|
49809
|
+
pacman: "sudo pacman -S --noconfirm age"
|
|
49810
|
+
}
|
|
49804
49811
|
}
|
|
49805
49812
|
];
|
|
49806
49813
|
function detectPackageManager() {
|
|
49807
49814
|
const managers = ["brew", "apt", "dnf", "pacman"];
|
|
49808
49815
|
for (const pm of managers) {
|
|
49809
49816
|
try {
|
|
49810
|
-
|
|
49817
|
+
execSync2(`which ${pm}`, { stdio: "ignore" });
|
|
49811
49818
|
return pm;
|
|
49812
49819
|
} catch {}
|
|
49813
49820
|
}
|
|
@@ -49815,7 +49822,7 @@ function detectPackageManager() {
|
|
|
49815
49822
|
}
|
|
49816
49823
|
function isCommandInstalled(command) {
|
|
49817
49824
|
try {
|
|
49818
|
-
|
|
49825
|
+
execSync2(`which ${command}`, { stdio: "ignore" });
|
|
49819
49826
|
return true;
|
|
49820
49827
|
} catch {}
|
|
49821
49828
|
return isShellAlias(command);
|
|
@@ -49824,7 +49831,7 @@ function isShellAlias(command) {
|
|
|
49824
49831
|
const shell = process.env.SHELL || "/bin/bash";
|
|
49825
49832
|
const shellName = shell.split("/").pop() || "bash";
|
|
49826
49833
|
try {
|
|
49827
|
-
|
|
49834
|
+
execSync2(`${shellName} -ic "type ${command}" 2>/dev/null`, { stdio: "ignore" });
|
|
49828
49835
|
return true;
|
|
49829
49836
|
} catch {
|
|
49830
49837
|
return false;
|
|
@@ -49832,14 +49839,14 @@ function isShellAlias(command) {
|
|
|
49832
49839
|
}
|
|
49833
49840
|
function getCommandVersion(command) {
|
|
49834
49841
|
try {
|
|
49835
|
-
const output2 =
|
|
49842
|
+
const output2 = execSync2(`${command} --version`, { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] });
|
|
49836
49843
|
return output2.trim().split(`
|
|
49837
49844
|
`)[0];
|
|
49838
49845
|
} catch {}
|
|
49839
49846
|
const shell = process.env.SHELL || "/bin/bash";
|
|
49840
49847
|
const shellName = shell.split("/").pop() || "bash";
|
|
49841
49848
|
try {
|
|
49842
|
-
const output2 =
|
|
49849
|
+
const output2 = execSync2(`${shellName} -ic "${command} --version" 2>/dev/null`, {
|
|
49843
49850
|
encoding: "utf-8",
|
|
49844
49851
|
stdio: ["pipe", "pipe", "ignore"]
|
|
49845
49852
|
});
|
|
@@ -49940,6 +49947,94 @@ function installUv() {
|
|
|
49940
49947
|
return false;
|
|
49941
49948
|
return installDependency(dep);
|
|
49942
49949
|
}
|
|
49950
|
+
function isFnoxInstalled() {
|
|
49951
|
+
return isCommandInstalled("fnox");
|
|
49952
|
+
}
|
|
49953
|
+
function installFnox() {
|
|
49954
|
+
const dep = getDependency("fnox");
|
|
49955
|
+
if (!dep)
|
|
49956
|
+
return false;
|
|
49957
|
+
return installDependency(dep);
|
|
49958
|
+
}
|
|
49959
|
+
function isAgeInstalled() {
|
|
49960
|
+
return isCommandInstalled("age-keygen");
|
|
49961
|
+
}
|
|
49962
|
+
function installAge() {
|
|
49963
|
+
const dep = getDependency("age");
|
|
49964
|
+
if (!dep)
|
|
49965
|
+
return false;
|
|
49966
|
+
return installDependency(dep);
|
|
49967
|
+
}
|
|
49968
|
+
|
|
49969
|
+
// cli/src/utils/fnox-setup.ts
|
|
49970
|
+
init_errors();
|
|
49971
|
+
import { execSync as execSync3 } from "child_process";
|
|
49972
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4, chmodSync } from "fs";
|
|
49973
|
+
import { join as join5 } from "path";
|
|
49974
|
+
function ensureFnoxSetup(fulcrumDir) {
|
|
49975
|
+
const ageKeyPath = join5(fulcrumDir, "age.txt");
|
|
49976
|
+
const fnoxConfigPath = join5(fulcrumDir, "fnox.toml");
|
|
49977
|
+
let publicKey;
|
|
49978
|
+
if (!existsSync5(ageKeyPath)) {
|
|
49979
|
+
console.error("Generating age encryption key...");
|
|
49980
|
+
try {
|
|
49981
|
+
const output2 = execSync3(`age-keygen -o "${ageKeyPath}" 2>&1`, { encoding: "utf-8" });
|
|
49982
|
+
const match = output2.match(/Public key: (age1\S+)/);
|
|
49983
|
+
if (!match) {
|
|
49984
|
+
throw new Error(`Could not parse public key from age-keygen output: ${output2}`);
|
|
49985
|
+
}
|
|
49986
|
+
publicKey = match[1];
|
|
49987
|
+
} catch (err) {
|
|
49988
|
+
throw new CliError("FNOX_SETUP_FAILED", `Failed to generate age key: ${err instanceof Error ? err.message : String(err)}`, ExitCodes.ERROR);
|
|
49989
|
+
}
|
|
49990
|
+
chmodSync(ageKeyPath, 384);
|
|
49991
|
+
console.error("Age encryption key generated.");
|
|
49992
|
+
} else {
|
|
49993
|
+
const content = readFileSync5(ageKeyPath, "utf-8");
|
|
49994
|
+
const match = content.match(/# public key: (age1\S+)/);
|
|
49995
|
+
if (!match) {
|
|
49996
|
+
throw new CliError("FNOX_SETUP_FAILED", `Could not parse public key from existing ${ageKeyPath}`, ExitCodes.ERROR);
|
|
49997
|
+
}
|
|
49998
|
+
publicKey = match[1];
|
|
49999
|
+
}
|
|
50000
|
+
if (!existsSync5(fnoxConfigPath)) {
|
|
50001
|
+
console.error("Creating fnox configuration...");
|
|
50002
|
+
const config = `[providers.plain]
|
|
50003
|
+
type = "plain"
|
|
50004
|
+
|
|
50005
|
+
[providers.age]
|
|
50006
|
+
type = "age"
|
|
50007
|
+
recipients = ["${publicKey}"]
|
|
50008
|
+
`;
|
|
50009
|
+
writeFileSync4(fnoxConfigPath, config, "utf-8");
|
|
50010
|
+
console.error("fnox configuration created.");
|
|
50011
|
+
} else {
|
|
50012
|
+
const existingConfig = readFileSync5(fnoxConfigPath, "utf-8");
|
|
50013
|
+
if (!existingConfig.includes("[providers.plain]")) {
|
|
50014
|
+
const updatedConfig = `[providers.plain]
|
|
50015
|
+
type = "plain"
|
|
50016
|
+
|
|
50017
|
+
${existingConfig}`;
|
|
50018
|
+
writeFileSync4(fnoxConfigPath, updatedConfig, "utf-8");
|
|
50019
|
+
console.error("Added plain provider to fnox configuration.");
|
|
50020
|
+
}
|
|
50021
|
+
}
|
|
50022
|
+
const env2 = { ...process.env, FNOX_AGE_KEY_FILE: ageKeyPath };
|
|
50023
|
+
const fnoxArgs = `-c "${fnoxConfigPath}"`;
|
|
50024
|
+
try {
|
|
50025
|
+
execSync3(`fnox set FULCRUM_SETUP_TEST test_value ${fnoxArgs}`, { env: env2, stdio: "ignore" });
|
|
50026
|
+
const value = execSync3(`fnox get FULCRUM_SETUP_TEST ${fnoxArgs}`, { env: env2, encoding: "utf-8" }).trim();
|
|
50027
|
+
execSync3(`fnox remove FULCRUM_SETUP_TEST ${fnoxArgs}`, { env: env2, stdio: "ignore" });
|
|
50028
|
+
if (value !== "test_value") {
|
|
50029
|
+
throw new Error(`Round-trip test failed: expected "test_value", got "${value}"`);
|
|
50030
|
+
}
|
|
50031
|
+
} catch (err) {
|
|
50032
|
+
throw new CliError("FNOX_SETUP_FAILED", `fnox verification failed: ${err instanceof Error ? err.message : String(err)}
|
|
50033
|
+
` + ` Age key: ${ageKeyPath}
|
|
50034
|
+
` + ` Config: ${fnoxConfigPath}
|
|
50035
|
+
` + ` Ensure fnox and age are properly installed.`, ExitCodes.ERROR);
|
|
50036
|
+
}
|
|
50037
|
+
}
|
|
49943
50038
|
|
|
49944
50039
|
// cli/src/commands/update.ts
|
|
49945
50040
|
import { spawn, spawnSync as spawnSync3 } from "child_process";
|
|
@@ -50019,7 +50114,7 @@ function compareVersions(v1, v2) {
|
|
|
50019
50114
|
var package_default = {
|
|
50020
50115
|
name: "@knowsuchagency/fulcrum",
|
|
50021
50116
|
private: true,
|
|
50022
|
-
version: "
|
|
50117
|
+
version: "4.0.0",
|
|
50023
50118
|
description: "Harness Attention. Orchestrate Agents. Ship.",
|
|
50024
50119
|
license: "PolyForm-Perimeter-1.0.0",
|
|
50025
50120
|
type: "module",
|
|
@@ -50295,7 +50390,7 @@ function getPackageRoot() {
|
|
|
50295
50390
|
const currentFile = fileURLToPath(import.meta.url);
|
|
50296
50391
|
let dir = dirname3(currentFile);
|
|
50297
50392
|
for (let i2 = 0;i2 < 5; i2++) {
|
|
50298
|
-
if (
|
|
50393
|
+
if (existsSync6(join6(dir, "server", "index.js"))) {
|
|
50299
50394
|
return dir;
|
|
50300
50395
|
}
|
|
50301
50396
|
dir = dirname3(dir);
|
|
@@ -50377,6 +50472,38 @@ Found existing Vibora data at ${viboraDir}`);
|
|
|
50377
50472
|
throw new CliError("MISSING_DEPENDENCY", `uv is required. Install manually: ${getInstallCommand(uvDep)}`, ExitCodes.ERROR);
|
|
50378
50473
|
}
|
|
50379
50474
|
}
|
|
50475
|
+
if (!isFnoxInstalled()) {
|
|
50476
|
+
const fnoxDep = getDependency("fnox");
|
|
50477
|
+
const method = getInstallMethod(fnoxDep);
|
|
50478
|
+
console.error("fnox is required for encrypted secrets management but is not installed.");
|
|
50479
|
+
console.error(" fnox encrypts sensitive settings like API keys and tokens.");
|
|
50480
|
+
const shouldInstall = autoYes || await confirm(`Would you like to install fnox via ${method}?`);
|
|
50481
|
+
if (shouldInstall) {
|
|
50482
|
+
const success = installFnox();
|
|
50483
|
+
if (!success) {
|
|
50484
|
+
throw new CliError("INSTALL_FAILED", "Failed to install fnox", ExitCodes.ERROR);
|
|
50485
|
+
}
|
|
50486
|
+
console.error("fnox installed successfully!");
|
|
50487
|
+
} else {
|
|
50488
|
+
throw new CliError("MISSING_DEPENDENCY", `fnox is required. Install manually: ${getInstallCommand(fnoxDep)}`, ExitCodes.ERROR);
|
|
50489
|
+
}
|
|
50490
|
+
}
|
|
50491
|
+
if (!isAgeInstalled()) {
|
|
50492
|
+
const ageDep = getDependency("age");
|
|
50493
|
+
const method = getInstallMethod(ageDep);
|
|
50494
|
+
console.error("age is required for encryption but is not installed.");
|
|
50495
|
+
console.error(" age generates encryption keys used by fnox to encrypt secrets.");
|
|
50496
|
+
const shouldInstall = autoYes || await confirm(`Would you like to install age via ${method}?`);
|
|
50497
|
+
if (shouldInstall) {
|
|
50498
|
+
const success = installAge();
|
|
50499
|
+
if (!success) {
|
|
50500
|
+
throw new CliError("INSTALL_FAILED", "Failed to install age", ExitCodes.ERROR);
|
|
50501
|
+
}
|
|
50502
|
+
console.error("age installed successfully!");
|
|
50503
|
+
} else {
|
|
50504
|
+
throw new CliError("MISSING_DEPENDENCY", `age is required. Install manually: ${getInstallCommand(ageDep)}`, ExitCodes.ERROR);
|
|
50505
|
+
}
|
|
50506
|
+
}
|
|
50380
50507
|
if (isClaudeInstalled() && needsPluginUpdate()) {
|
|
50381
50508
|
console.error("Updating Fulcrum plugin for Claude Code...");
|
|
50382
50509
|
await installClaudePlugin({ silent: true });
|
|
@@ -50409,7 +50536,7 @@ Found existing Vibora data at ${viboraDir}`);
|
|
|
50409
50536
|
}
|
|
50410
50537
|
const host = flags.host ? "0.0.0.0" : "localhost";
|
|
50411
50538
|
const packageRoot = getPackageRoot();
|
|
50412
|
-
const serverPath =
|
|
50539
|
+
const serverPath = join6(packageRoot, "server", "index.js");
|
|
50413
50540
|
const platform2 = process.platform;
|
|
50414
50541
|
const arch = process.arch;
|
|
50415
50542
|
let ptyLibName;
|
|
@@ -50420,8 +50547,9 @@ Found existing Vibora data at ${viboraDir}`);
|
|
|
50420
50547
|
} else {
|
|
50421
50548
|
ptyLibName = arch === "arm64" ? "librust_pty_arm64.so" : "librust_pty.so";
|
|
50422
50549
|
}
|
|
50423
|
-
const ptyLibPath =
|
|
50550
|
+
const ptyLibPath = join6(packageRoot, "lib", ptyLibName);
|
|
50424
50551
|
const fulcrumDir = getFulcrumDir();
|
|
50552
|
+
ensureFnoxSetup(fulcrumDir);
|
|
50425
50553
|
const debug = flags.debug === "true";
|
|
50426
50554
|
console.error(`Starting Fulcrum server${debug ? " (debug mode)" : ""}...`);
|
|
50427
50555
|
const serverProc = spawn2("bun", [serverPath], {
|
|
@@ -50436,6 +50564,8 @@ Found existing Vibora data at ${viboraDir}`);
|
|
|
50436
50564
|
FULCRUM_PACKAGE_ROOT: packageRoot,
|
|
50437
50565
|
FULCRUM_VERSION: package_default.version,
|
|
50438
50566
|
BUN_PTY_LIB: ptyLibPath,
|
|
50567
|
+
FNOX_AGE_KEY_FILE: join6(fulcrumDir, "age.txt"),
|
|
50568
|
+
FULCRUM_FNOX_INSTALLED: "1",
|
|
50439
50569
|
...isClaudeInstalled() && { FULCRUM_CLAUDE_INSTALLED: "1" },
|
|
50440
50570
|
...isOpencodeInstalled() && { FULCRUM_OPENCODE_INSTALLED: "1" },
|
|
50441
50571
|
...debug && { LOG_LEVEL: "debug", DEBUG: "1" }
|