@fieldwork-ai/cli 0.16.0 → 0.17.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
@@ -62,3 +62,7 @@ claude mcp add --transport http fieldwork https://app.getfieldwork.ai/mcp
62
62
  Source stays in the private app repository under `cli/`. Run `pnpm install --frozen-lockfile`, `pnpm build`, and `pnpm test` here. Publish the compiled package with `npm publish --access public`; the package includes `dist/` and npm metadata, without TypeScript source or source maps. `scripts/pack-cli.sh` builds and packs this same package into the compute image without depending on an npm release.
63
63
 
64
64
  The terminal chat client is the separate public `@fieldwork-ai/fieldwork-code` package (`fwcode`).
65
+
66
+ ## Graph authoring diagnostics
67
+
68
+ Use `fieldwork tool run automations_graph_examples --json` for executable examples and the coordinated code/graph publication format. `automations_graph_validate` takes the publication input without committing it. `automations_graph_test` uses isolated editable-table fixtures and captured notifications; it does not execute authored code or send email. `automations_graph_wait` returns bounded, compact progress, and `automations_graph_trace` accepts `detail: "full"` when saved payloads are needed. Contract failures include source/destination paths, required coordinated changes and retryability on stderr with a nonzero exit code.
@@ -1,6 +1,7 @@
1
+ import { readFile } from "node:fs/promises";
1
2
  import { api, apiJson, getContext, requireOrgId } from "../http.js";
2
3
  import { EXIT, confirm, exitCodeFor, fail, note, printData } from "../output.js";
3
- const TERMINAL_STATUSES = new Set(["succeeded", "failed"]);
4
+ const TERMINAL_STATUSES = new Set(["succeeded", "failed", "cancelled", "needs_attention"]);
4
5
  async function resolveAutomation(name) {
5
6
  const body = await apiJson(`/api/organizations/${requireOrgId()}/automations?name=${encodeURIComponent(name)}`);
6
7
  const automation = body.automations?.[0];
@@ -70,6 +71,24 @@ async function followRun(automationId, runId, intervalSeconds, json) {
70
71
  }
71
72
  }
72
73
  export function registerAutomationCommands(program) {
74
+ const graph = program.command("automation-graph").description("Publish and inspect automation graphs");
75
+ graph.command("get <automation-id>").action(async (id) => printData(await apiJson(`/api/organizations/${requireOrgId()}/automations/${encodeURIComponent(id)}/graph`), true));
76
+ graph.command("trace <automation-id> <run-id>").action(async (id, runId) => printData(await apiJson(`/api/organizations/${requireOrgId()}/automations/${encodeURIComponent(id)}/runs/${encodeURIComponent(runId)}/steps`), true));
77
+ graph.command("configure <automation-id>").requiredOption("--file <path>").action(async (id, options) => {
78
+ const body = await readFile(options.file, "utf8");
79
+ await confirm("Apply these triggers and enabled state?", getContext().yes);
80
+ printData(await apiJson(`/api/organizations/${requireOrgId()}/automations/${encodeURIComponent(id)}/graph`, { method: "PATCH", body }), true);
81
+ });
82
+ graph.command("control <automation-id> <run-id>").requiredOption("--file <path>", "Cancel, retry, or confirmed reconciliation").action(async (id, runId, options) => {
83
+ const body = await readFile(options.file, "utf8");
84
+ await confirm("Apply this run control? Confirm external results before reconciliation.", getContext().yes);
85
+ printData(await apiJson(`/api/organizations/${requireOrgId()}/automations/${encodeURIComponent(id)}/runs/${encodeURIComponent(runId)}/steps`, { method: "POST", body }), true);
86
+ });
87
+ graph.command("publish").requiredOption("--file <path>").action(async (options) => {
88
+ const body = await readFile(options.file, "utf8");
89
+ await confirm("Publish this graph and delegate its listed resource access?", getContext().yes);
90
+ printData(await apiJson(`/api/organizations/${requireOrgId()}/automations/graphs`, { method: "POST", body }), true);
91
+ });
73
92
  const automations = program
74
93
  .command("automations")
75
94
  .description("Standing instructions that run unattended (schedules, webhooks)");
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function tablesCommand(): Command;
@@ -0,0 +1,41 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { Command } from "commander";
3
+ import { apiJson, requireOrgId, getContext } from "../http.js";
4
+ import { confirm, printData } from "../output.js";
5
+ export function tablesCommand() {
6
+ const command = new Command("tables").description("Fieldwork Tables and editable records");
7
+ const base = () => `/api/organizations/${requireOrgId()}/tables`;
8
+ command
9
+ .command("list")
10
+ .action(async () => printData(await apiJson(base()), true));
11
+ command
12
+ .command("create")
13
+ .requiredOption("--file <path>", "Table definition file")
14
+ .action(async (options) => {
15
+ const body = await readFile(options.file, "utf8");
16
+ await confirm("Create this Fieldwork Table?", getContext().yes);
17
+ printData(await apiJson(base(), { method: "POST", body }), true);
18
+ });
19
+ command
20
+ .command("records <table-id>")
21
+ .option("--after <id>")
22
+ .option("--limit <number>", "Page size", "50")
23
+ .action(async (tableId, options) => {
24
+ const query = new URLSearchParams({ limit: options.limit });
25
+ if (options.after)
26
+ query.set("after", options.after);
27
+ printData(await apiJson(`${base()}/${encodeURIComponent(tableId)}/records?${query}`), true);
28
+ });
29
+ command
30
+ .command("mutate <table-id>")
31
+ .requiredOption("--file <path>", "Record mutation file")
32
+ .action(async (tableId, options) => {
33
+ const body = await readFile(options.file, "utf8");
34
+ await confirm("Apply this record mutation?", getContext().yes);
35
+ printData(await apiJson(`${base()}/${encodeURIComponent(tableId)}/records`, {
36
+ method: "POST",
37
+ body,
38
+ }), true);
39
+ });
40
+ return command;
41
+ }
package/dist/http.js CHANGED
@@ -114,7 +114,25 @@ export async function apiJson(pathname, init = {}) {
114
114
  const res = await api(pathname, init);
115
115
  const body = (await res.json().catch(() => null));
116
116
  if (!res.ok) {
117
- fail(body?.error ?? `request failed (${res.status})`, exitCodeFor(res.status));
117
+ const message = body?.error ?? `request failed (${res.status})`;
118
+ const details = Array.isArray(body?.details) ? body.details.flatMap((issue) => {
119
+ if (!issue || typeof issue !== "object")
120
+ return [];
121
+ const { path, message } = issue;
122
+ return typeof message === "string" ? [`${Array.isArray(path) ? path.join(".") + ": " : ""}${message}`] : [];
123
+ }) : [];
124
+ if (body?.code === "invalid_graph" && body.details && typeof body.details === "object" && !Array.isArray(body.details)) {
125
+ const diagnostic = body.details;
126
+ for (const key of ["action_definition_id", "automation_id", "automation_revision_id", "node_id", "source", "destination", "input", "output", "source_schema", "target_schema", "effect_changed", "credential_changed", "required_action"]) {
127
+ const value = diagnostic[key];
128
+ if (value !== undefined && value !== null)
129
+ details.push(`${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`);
130
+ }
131
+ if (typeof body.retryable === "boolean")
132
+ details.push(`retryable: ${body.retryable}`);
133
+ }
134
+ const prefix = typeof body?.tool === "string" ? `${body.tool}: ` : "";
135
+ fail([prefix + message, ...details].join("\n"), exitCodeFor(res.status));
118
136
  }
119
137
  return body;
120
138
  }
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { tablesCommand } from "./commands/tables.js";
2
3
  import { Command } from "commander";
3
4
  import { createRequire } from "node:module";
4
5
  import { initContext } from "./http.js";
@@ -40,6 +41,7 @@ registerAppCommands(program);
40
41
  registerAutomationCommands(program);
41
42
  registerTaskCommands(program);
42
43
  registerConnectorCommands(program);
44
+ program.addCommand(tablesCommand());
43
45
  program.parseAsync(process.argv).catch((err) => {
44
46
  process.stderr.write(`fieldwork: ${err instanceof Error ? err.message : String(err)}\n`);
45
47
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fieldwork-ai/cli",
3
- "version": "0.16.0",
3
+ "version": "0.17.2",
4
4
  "description": "Fieldwork asset-management CLI",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -13,6 +13,12 @@
13
13
  "engines": {
14
14
  "node": ">=24"
15
15
  },
16
+ "scripts": {
17
+ "build": "node -e \"require('node:fs').rmSync('dist', {recursive:true,force:true})\" && tsc",
18
+ "prepack": "pnpm build",
19
+ "dev": "tsx src/index.ts",
20
+ "test": "vitest run"
21
+ },
16
22
  "dependencies": {
17
23
  "commander": "^14.0.2"
18
24
  },
@@ -30,10 +36,5 @@
30
36
  "exports": {
31
37
  "./package.json": "./package.json",
32
38
  "./*": "./dist/*.js"
33
- },
34
- "scripts": {
35
- "build": "node -e \"require('node:fs').rmSync('dist', {recursive:true,force:true})\" && tsc",
36
- "dev": "tsx src/index.ts",
37
- "test": "vitest run"
38
39
  }
39
- }
40
+ }