@knowsuchagency/fulcrum 4.1.0 → 4.1.2

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 CHANGED
@@ -270,6 +270,7 @@ Both plugins include an MCP server with 100+ tools:
270
270
  | **Memory** | Read/update master memory file; store ephemeral knowledge with tags |
271
271
  | **Calendar** | Manage CalDAV accounts, sync calendars, configure event copy rules |
272
272
  | **Gmail** | List Google accounts, manage Gmail drafts, send emails |
273
+ | **Jobs** | List, create, update, delete, enable/disable, and run systemd timers and launchd jobs |
273
274
  | **Assistant** | Send messages via channels (WhatsApp, Discord, Telegram, Slack, Gmail); query sweep history |
274
275
 
275
276
  Use `search_tools` to discover available tools by keyword or category.
@@ -315,9 +316,9 @@ For browser-only access, use Tailscale or Cloudflare Tunnels to expose your serv
315
316
  <details>
316
317
  <summary><strong>Configuration</strong></summary>
317
318
 
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
+ All configuration is managed by [fnox](https://github.com/yarlson/fnox) a single `fnox.toml` file stores both plain and encrypted settings. Sensitive credentials (API keys, tokens, webhook URLs) are encrypted with age; the age key (`age.txt`) lives alongside `fnox.toml` in the fulcrum directory. Existing `settings.json` files are automatically migrated to fnox on server start.
319
320
 
320
- Settings are stored in `.fulcrum/settings.json`. The fulcrum directory is resolved in this order:
321
+ The fulcrum directory is resolved in this order:
321
322
 
322
323
  1. `FULCRUM_DIR` environment variable
323
324
  2. `.fulcrum` in current working directory
package/bin/fulcrum.js CHANGED
@@ -1670,6 +1670,65 @@ class FulcrumClient {
1670
1670
  body: JSON.stringify({ heading, content })
1671
1671
  });
1672
1672
  }
1673
+ async listJobs(scope) {
1674
+ const params = new URLSearchParams;
1675
+ if (scope)
1676
+ params.set("scope", scope);
1677
+ const query = params.toString() ? `?${params.toString()}` : "";
1678
+ return this.fetch(`/api/jobs${query}`);
1679
+ }
1680
+ async getJob(name, scope) {
1681
+ const params = new URLSearchParams;
1682
+ if (scope)
1683
+ params.set("scope", scope);
1684
+ const query = params.toString() ? `?${params.toString()}` : "";
1685
+ return this.fetch(`/api/jobs/${encodeURIComponent(name)}${query}`);
1686
+ }
1687
+ async getJobLogs(name, scope, lines) {
1688
+ const params = new URLSearchParams;
1689
+ if (scope)
1690
+ params.set("scope", scope);
1691
+ if (lines)
1692
+ params.set("lines", String(lines));
1693
+ const query = params.toString() ? `?${params.toString()}` : "";
1694
+ return this.fetch(`/api/jobs/${encodeURIComponent(name)}/logs${query}`);
1695
+ }
1696
+ async createJob(data) {
1697
+ return this.fetch("/api/jobs", {
1698
+ method: "POST",
1699
+ body: JSON.stringify(data)
1700
+ });
1701
+ }
1702
+ async updateJob(name, updates) {
1703
+ return this.fetch(`/api/jobs/${encodeURIComponent(name)}`, {
1704
+ method: "PATCH",
1705
+ body: JSON.stringify(updates)
1706
+ });
1707
+ }
1708
+ async deleteJob(name) {
1709
+ return this.fetch(`/api/jobs/${encodeURIComponent(name)}`, { method: "DELETE" });
1710
+ }
1711
+ async enableJob(name, scope) {
1712
+ const params = new URLSearchParams;
1713
+ if (scope)
1714
+ params.set("scope", scope);
1715
+ const query = params.toString() ? `?${params.toString()}` : "";
1716
+ return this.fetch(`/api/jobs/${encodeURIComponent(name)}/enable${query}`, { method: "POST" });
1717
+ }
1718
+ async disableJob(name, scope) {
1719
+ const params = new URLSearchParams;
1720
+ if (scope)
1721
+ params.set("scope", scope);
1722
+ const query = params.toString() ? `?${params.toString()}` : "";
1723
+ return this.fetch(`/api/jobs/${encodeURIComponent(name)}/disable${query}`, { method: "POST" });
1724
+ }
1725
+ async runJobNow(name, scope) {
1726
+ const params = new URLSearchParams;
1727
+ if (scope)
1728
+ params.set("scope", scope);
1729
+ const query = params.toString() ? `?${params.toString()}` : "";
1730
+ return this.fetch(`/api/jobs/${encodeURIComponent(name)}/run${query}`, { method: "POST" });
1731
+ }
1673
1732
  async search(input) {
1674
1733
  const params = new URLSearchParams({ q: input.query });
1675
1734
  if (input.entities?.length)
@@ -44413,6 +44472,69 @@ var init_registry = __esm(() => {
44413
44472
  category: "email",
44414
44473
  keywords: ["gmail", "draft", "delete", "remove", "email", "google"],
44415
44474
  defer_loading: true
44475
+ },
44476
+ {
44477
+ name: "list_jobs",
44478
+ description: "List scheduled jobs (systemd timers on Linux, launchd on macOS)",
44479
+ category: "jobs",
44480
+ keywords: ["job", "timer", "schedule", "cron", "systemd", "launchd", "list"],
44481
+ defer_loading: true
44482
+ },
44483
+ {
44484
+ name: "get_job",
44485
+ description: "Get details of a scheduled job including schedule, command, and execution stats",
44486
+ category: "jobs",
44487
+ keywords: ["job", "timer", "schedule", "details", "get"],
44488
+ defer_loading: true
44489
+ },
44490
+ {
44491
+ name: "get_job_logs",
44492
+ description: "Get execution logs for a scheduled job",
44493
+ category: "jobs",
44494
+ keywords: ["job", "timer", "logs", "output", "journalctl"],
44495
+ defer_loading: true
44496
+ },
44497
+ {
44498
+ name: "create_job",
44499
+ description: "Create a new scheduled job (Linux systemd only)",
44500
+ category: "jobs",
44501
+ keywords: ["job", "timer", "schedule", "create", "new", "systemd"],
44502
+ defer_loading: true
44503
+ },
44504
+ {
44505
+ name: "update_job",
44506
+ description: "Update a scheduled job (Linux systemd only)",
44507
+ category: "jobs",
44508
+ keywords: ["job", "timer", "schedule", "update", "edit", "modify"],
44509
+ defer_loading: true
44510
+ },
44511
+ {
44512
+ name: "delete_job",
44513
+ description: "Delete a scheduled job (Linux systemd user jobs only)",
44514
+ category: "jobs",
44515
+ keywords: ["job", "timer", "schedule", "delete", "remove"],
44516
+ defer_loading: true
44517
+ },
44518
+ {
44519
+ name: "enable_job",
44520
+ description: "Enable a scheduled job so it runs on schedule",
44521
+ category: "jobs",
44522
+ keywords: ["job", "timer", "enable", "start", "activate"],
44523
+ defer_loading: true
44524
+ },
44525
+ {
44526
+ name: "disable_job",
44527
+ description: "Disable a scheduled job so it stops running",
44528
+ category: "jobs",
44529
+ keywords: ["job", "timer", "disable", "stop", "deactivate"],
44530
+ defer_loading: true
44531
+ },
44532
+ {
44533
+ name: "run_job_now",
44534
+ description: "Trigger immediate execution of a scheduled job",
44535
+ category: "jobs",
44536
+ keywords: ["job", "timer", "run", "trigger", "execute", "now"],
44537
+ defer_loading: true
44416
44538
  }
44417
44539
  ];
44418
44540
  });
@@ -46395,6 +46517,138 @@ var init_search = __esm(() => {
46395
46517
  EntityTypeSchema = exports_external.enum(["tasks", "projects", "messages", "events", "memories", "conversations", "gmail"]);
46396
46518
  });
46397
46519
 
46520
+ // cli/src/mcp/tools/jobs.ts
46521
+ var JobScopeSchema, registerJobTools = (server, client) => {
46522
+ server.tool("list_jobs", "List scheduled jobs (systemd timers on Linux, launchd on macOS)", {
46523
+ scope: exports_external.optional(JobScopeSchema).describe("Filter by scope: all, user, or system (default: all)")
46524
+ }, async ({ scope }) => {
46525
+ try {
46526
+ const jobs = await client.listJobs(scope);
46527
+ return formatSuccess(jobs);
46528
+ } catch (err) {
46529
+ return handleToolError(err);
46530
+ }
46531
+ });
46532
+ server.tool("get_job", "Get details of a scheduled job including schedule, command, execution stats, and unit file contents", {
46533
+ name: exports_external.string().describe("Job/timer name"),
46534
+ scope: exports_external.optional(exports_external.enum(["user", "system"])).describe("Job scope (default: user)")
46535
+ }, async ({ name, scope }) => {
46536
+ try {
46537
+ const job = await client.getJob(name, scope);
46538
+ return formatSuccess(job);
46539
+ } catch (err) {
46540
+ return handleToolError(err);
46541
+ }
46542
+ });
46543
+ server.tool("get_job_logs", "Get execution logs for a scheduled job (from journalctl)", {
46544
+ name: exports_external.string().describe("Job/timer name"),
46545
+ scope: exports_external.optional(exports_external.enum(["user", "system"])).describe("Job scope (default: user)"),
46546
+ lines: exports_external.optional(exports_external.number()).describe("Number of log lines to return (default: 100)")
46547
+ }, async ({ name, scope, lines }) => {
46548
+ try {
46549
+ const result = await client.getJobLogs(name, scope, lines);
46550
+ return formatSuccess(result);
46551
+ } catch (err) {
46552
+ return handleToolError(err);
46553
+ }
46554
+ });
46555
+ server.tool("create_job", "Create a new scheduled job (Linux systemd only). Creates a .timer and .service unit file.", {
46556
+ name: exports_external.string().describe("Job name (alphanumeric, hyphens, underscores only)"),
46557
+ description: exports_external.string().describe("Human-readable description"),
46558
+ schedule: exports_external.string().describe('systemd OnCalendar schedule (e.g., "daily", "*-*-* 09:00:00", "Mon..Fri 09:00")'),
46559
+ command: exports_external.string().describe("Command to execute"),
46560
+ workingDirectory: exports_external.optional(exports_external.string()).describe("Working directory for the command"),
46561
+ environment: exports_external.optional(exports_external.record(exports_external.string())).describe("Environment variables as key-value pairs"),
46562
+ persistent: exports_external.optional(exports_external.boolean()).describe("Run missed executions on next boot (default: true)")
46563
+ }, async ({ name, description, schedule, command, workingDirectory, environment, persistent }) => {
46564
+ try {
46565
+ const result = await client.createJob({
46566
+ name,
46567
+ description,
46568
+ schedule,
46569
+ command,
46570
+ workingDirectory,
46571
+ environment,
46572
+ persistent
46573
+ });
46574
+ return formatSuccess(result);
46575
+ } catch (err) {
46576
+ return handleToolError(err);
46577
+ }
46578
+ });
46579
+ server.tool("update_job", "Update a scheduled job (Linux systemd only)", {
46580
+ name: exports_external.string().describe("Job name to update"),
46581
+ description: exports_external.optional(exports_external.string()).describe("New description"),
46582
+ schedule: exports_external.optional(exports_external.string()).describe("New schedule"),
46583
+ command: exports_external.optional(exports_external.string()).describe("New command"),
46584
+ workingDirectory: exports_external.optional(exports_external.string()).describe("New working directory"),
46585
+ environment: exports_external.optional(exports_external.record(exports_external.string())).describe("New environment variables"),
46586
+ persistent: exports_external.optional(exports_external.boolean()).describe("Run missed executions on next boot")
46587
+ }, async ({ name, description, schedule, command, workingDirectory, environment, persistent }) => {
46588
+ try {
46589
+ const result = await client.updateJob(name, {
46590
+ description,
46591
+ schedule,
46592
+ command,
46593
+ workingDirectory,
46594
+ environment,
46595
+ persistent
46596
+ });
46597
+ return formatSuccess(result);
46598
+ } catch (err) {
46599
+ return handleToolError(err);
46600
+ }
46601
+ });
46602
+ server.tool("delete_job", "Delete a scheduled job (Linux systemd user jobs only)", {
46603
+ name: exports_external.string().describe("Job name to delete")
46604
+ }, async ({ name }) => {
46605
+ try {
46606
+ const result = await client.deleteJob(name);
46607
+ return formatSuccess(result);
46608
+ } catch (err) {
46609
+ return handleToolError(err);
46610
+ }
46611
+ });
46612
+ server.tool("enable_job", "Enable a scheduled job's timer so it runs on schedule", {
46613
+ name: exports_external.string().describe("Job name"),
46614
+ scope: exports_external.optional(exports_external.enum(["user", "system"])).describe("Job scope (default: user)")
46615
+ }, async ({ name, scope }) => {
46616
+ try {
46617
+ const result = await client.enableJob(name, scope);
46618
+ return formatSuccess(result);
46619
+ } catch (err) {
46620
+ return handleToolError(err);
46621
+ }
46622
+ });
46623
+ server.tool("disable_job", "Disable a scheduled job's timer so it stops running", {
46624
+ name: exports_external.string().describe("Job name"),
46625
+ scope: exports_external.optional(exports_external.enum(["user", "system"])).describe("Job scope (default: user)")
46626
+ }, async ({ name, scope }) => {
46627
+ try {
46628
+ const result = await client.disableJob(name, scope);
46629
+ return formatSuccess(result);
46630
+ } catch (err) {
46631
+ return handleToolError(err);
46632
+ }
46633
+ });
46634
+ server.tool("run_job_now", "Trigger immediate execution of a scheduled job", {
46635
+ name: exports_external.string().describe("Job name"),
46636
+ scope: exports_external.optional(exports_external.enum(["user", "system"])).describe("Job scope (default: user)")
46637
+ }, async ({ name, scope }) => {
46638
+ try {
46639
+ const result = await client.runJobNow(name, scope);
46640
+ return formatSuccess(result);
46641
+ } catch (err) {
46642
+ return handleToolError(err);
46643
+ }
46644
+ });
46645
+ };
46646
+ var init_jobs = __esm(() => {
46647
+ init_zod2();
46648
+ init_utils();
46649
+ JobScopeSchema = exports_external.enum(["all", "user", "system"]);
46650
+ });
46651
+
46398
46652
  // cli/src/mcp/tools/index.ts
46399
46653
  function registerTools(server, client) {
46400
46654
  registerCoreTools(server, client);
@@ -46415,6 +46669,7 @@ function registerTools(server, client) {
46415
46669
  registerMemoryTools(server, client);
46416
46670
  registerMemoryFileTools(server, client);
46417
46671
  registerSearchTools(server, client);
46672
+ registerJobTools(server, client);
46418
46673
  }
46419
46674
  var init_tools = __esm(() => {
46420
46675
  init_core5();
@@ -46435,6 +46690,7 @@ var init_tools = __esm(() => {
46435
46690
  init_memory_file();
46436
46691
  init_messaging();
46437
46692
  init_search();
46693
+ init_jobs();
46438
46694
  init_types4();
46439
46695
  });
46440
46696
 
@@ -46453,7 +46709,7 @@ async function runMcpServer(urlOverride, portOverride) {
46453
46709
  const client = new FulcrumClient(urlOverride, portOverride);
46454
46710
  const server = new McpServer({
46455
46711
  name: "fulcrum",
46456
- version: "4.1.0"
46712
+ version: "4.1.2"
46457
46713
  });
46458
46714
  registerTools(server, client);
46459
46715
  const transport = new StdioServerTransport;
@@ -48802,7 +49058,7 @@ var marketplace_default = `{
48802
49058
  "name": "fulcrum",
48803
49059
  "source": "./",
48804
49060
  "description": "Task orchestration for Claude Code",
48805
- "version": "4.1.0",
49061
+ "version": "4.1.2",
48806
49062
  "skills": [
48807
49063
  "./skills/fulcrum"
48808
49064
  ],
@@ -50115,7 +50371,7 @@ function compareVersions(v1, v2) {
50115
50371
  var package_default = {
50116
50372
  name: "@knowsuchagency/fulcrum",
50117
50373
  private: true,
50118
- version: "4.1.0",
50374
+ version: "4.1.2",
50119
50375
  description: "Harness Attention. Orchestrate Agents. Ship.",
50120
50376
  license: "PolyForm-Perimeter-1.0.0",
50121
50377
  type: "module",
Binary file