@fieldwork-ai/cli 0.17.1 → 0.19.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 +6 -0
- package/dist/commands/automations.js +66 -10
- package/dist/commands/connectors.js +20 -0
- package/dist/http.js +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,6 +43,8 @@ Mutations prompt for confirmation on a TTY; pass `--yes` to skip (required in no
|
|
|
43
43
|
|
|
44
44
|
The connector development loop end to end: `connectors pull acme_erp` → edit the adapter → `connectors publish acme_erp --message "…" --base-revision N` (a new key creates the definition from the tree's `connector.json`) → `connectors connect acme_erp --spec spec.json` → `connectors secrets set <id>` → `automations run-now <sync> --follow` to watch the logs.
|
|
45
45
|
|
|
46
|
+
Publishing initializes a new connector from `connector.json`, but subsequent action revisions leave `connector_definitions` metadata unchanged. To edit its display name, description, branding, or REST secret-name hints, use `fieldwork connectors metadata <definition-id>`, then `fieldwork connectors update-metadata <definition-id> --changes changes.json --expected-updated-at '<exact updated_at from the read>'`. The update requires connector edit access and rejects stale reads; credential values are managed separately through `secrets`.
|
|
47
|
+
|
|
46
48
|
## Auth and configuration
|
|
47
49
|
|
|
48
50
|
Credential resolution order: the injected container file (`~/.fieldwork/credentials`) → `FIELDWORK_TOKEN` / `FIELDWORK_API_URL` environment variables → the `fieldwork login` config (`~/.config/fieldwork/config.json`). One token binds one organization; `fieldwork orgs use` re-runs the browser flow to switch. Revoke a login anytime in Fieldwork under Settings → Connected apps.
|
|
@@ -62,3 +64,7 @@ claude mcp add --transport http fieldwork https://app.getfieldwork.ai/mcp
|
|
|
62
64
|
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
65
|
|
|
64
66
|
The terminal chat client is the separate public `@fieldwork-ai/fieldwork-code` package (`fwcode`).
|
|
67
|
+
|
|
68
|
+
## Graph authoring diagnostics
|
|
69
|
+
|
|
70
|
+
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.
|
|
@@ -71,7 +71,9 @@ async function followRun(automationId, runId, intervalSeconds, json) {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
export function registerAutomationCommands(program) {
|
|
74
|
-
const graph = program
|
|
74
|
+
const graph = program
|
|
75
|
+
.command("automation-graph", { hidden: true })
|
|
76
|
+
.description("Publish and inspect automation graphs");
|
|
75
77
|
graph.command("get <automation-id>").action(async (id) => printData(await apiJson(`/api/organizations/${requireOrgId()}/automations/${encodeURIComponent(id)}/graph`), true));
|
|
76
78
|
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
79
|
graph.command("configure <automation-id>").requiredOption("--file <path>").action(async (id, options) => {
|
|
@@ -91,7 +93,61 @@ export function registerAutomationCommands(program) {
|
|
|
91
93
|
});
|
|
92
94
|
const automations = program
|
|
93
95
|
.command("automations")
|
|
94
|
-
.description("
|
|
96
|
+
.description("Create, manage and inspect automations");
|
|
97
|
+
const callTool = async (name, args) => {
|
|
98
|
+
const response = await apiJson("/api/tools/run", {
|
|
99
|
+
method: "POST",
|
|
100
|
+
body: JSON.stringify({ name, args }),
|
|
101
|
+
});
|
|
102
|
+
printData(response.result, true);
|
|
103
|
+
};
|
|
104
|
+
for (const operation of ["publish", "validate", "test", "update"]) {
|
|
105
|
+
automations
|
|
106
|
+
.command(operation)
|
|
107
|
+
.description(`${operation} an automation`)
|
|
108
|
+
.requiredOption("--file <path>")
|
|
109
|
+
.action(async (options) => {
|
|
110
|
+
const input = JSON.parse(await readFile(options.file, "utf8"));
|
|
111
|
+
if (operation === "publish" || operation === "update")
|
|
112
|
+
await confirm(`Apply this automation ${operation}?`, getContext().yes);
|
|
113
|
+
await callTool(`automations_${operation}`, input);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
automations
|
|
117
|
+
.command("run <name>")
|
|
118
|
+
.description("Start a real automation run")
|
|
119
|
+
.option("--input <path>", "File containing the run input")
|
|
120
|
+
.action(async (name, options) => {
|
|
121
|
+
const input = options.input
|
|
122
|
+
? JSON.parse(await readFile(options.input, "utf8"))
|
|
123
|
+
: undefined;
|
|
124
|
+
await confirm(`Run "${name}" now?`, getContext().yes);
|
|
125
|
+
await callTool("automations_run", { name, input });
|
|
126
|
+
});
|
|
127
|
+
automations
|
|
128
|
+
.command("examples")
|
|
129
|
+
.description("Working automation definitions and tests")
|
|
130
|
+
.action(() => callTool("automations_examples", {}));
|
|
131
|
+
for (const operation of ["get-run", "wait"]) {
|
|
132
|
+
automations
|
|
133
|
+
.command(`${operation} <name> <run-id>`)
|
|
134
|
+
.description(operation === "wait" ? "Wait briefly for a run" : "Inspect one run")
|
|
135
|
+
.action((name, runId) => callTool(`automations_${operation.replaceAll("-", "_")}`, {
|
|
136
|
+
name,
|
|
137
|
+
automation_run_id: runId,
|
|
138
|
+
...(operation === "get-run" ? { detail: "full" } : {}),
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
automations
|
|
142
|
+
.command("control <name> <run-id>")
|
|
143
|
+
.description("Cancel, retry, or reconcile a run")
|
|
144
|
+
.requiredOption("--file <path>")
|
|
145
|
+
.action(async (name, runId, options) => {
|
|
146
|
+
const found = await resolveAutomation(name);
|
|
147
|
+
const body = await readFile(options.file, "utf8");
|
|
148
|
+
await confirm("Apply this run control? Verify external results before reconciliation.", getContext().yes);
|
|
149
|
+
printData(await apiJson(`/api/organizations/${requireOrgId()}/automations/${found.automation_id}/runs/${encodeURIComponent(runId)}/steps`, { method: "POST", body }), true);
|
|
150
|
+
});
|
|
95
151
|
automations
|
|
96
152
|
.command("list")
|
|
97
153
|
.description("List the automations you can see")
|
|
@@ -112,12 +168,8 @@ export function registerAutomationCommands(program) {
|
|
|
112
168
|
});
|
|
113
169
|
automations
|
|
114
170
|
.command("get <name>")
|
|
115
|
-
.description("
|
|
116
|
-
.action(
|
|
117
|
-
const found = await resolveAutomation(name);
|
|
118
|
-
const body = await apiJson(`/api/organizations/${requireOrgId()}/automations/${found.automation_id}`);
|
|
119
|
-
printData(body, getContext().json);
|
|
120
|
-
});
|
|
171
|
+
.description("Read an automation definition, settings and recent runs")
|
|
172
|
+
.action((name) => callTool("automations_get", { name }));
|
|
121
173
|
const setEnabled = (enabled) => async (name) => {
|
|
122
174
|
const { json, yes } = getContext();
|
|
123
175
|
const found = await resolveAutomation(name);
|
|
@@ -171,10 +223,14 @@ export function registerAutomationCommands(program) {
|
|
|
171
223
|
.action(async (name, opts) => {
|
|
172
224
|
const { json } = getContext();
|
|
173
225
|
const found = await resolveAutomation(name);
|
|
174
|
-
const query = opts.run
|
|
226
|
+
const query = opts.run
|
|
227
|
+
? `?run=${encodeURIComponent(opts.run)}`
|
|
228
|
+
: "?limit=1";
|
|
175
229
|
const run = (await fetchRuns(found.automation_id, query))[0];
|
|
176
230
|
if (!run) {
|
|
177
|
-
fail(opts.run
|
|
231
|
+
fail(opts.run
|
|
232
|
+
? `no run ${opts.run} for "${name}"`
|
|
233
|
+
: `"${name}" has no runs yet`, EXIT.notFound);
|
|
178
234
|
}
|
|
179
235
|
printRun(run, json);
|
|
180
236
|
});
|
|
@@ -223,6 +223,26 @@ export function registerConnectorCommands(program) {
|
|
|
223
223
|
});
|
|
224
224
|
printData(body, json);
|
|
225
225
|
});
|
|
226
|
+
connectors.command("metadata <definition-id>")
|
|
227
|
+
.description("Read an org-private connector definition's editable metadata")
|
|
228
|
+
.action(async (id) => {
|
|
229
|
+
printData(await apiJson(`/api/organizations/${requireOrgId()}/connector-definitions/${id}`), getContext().json);
|
|
230
|
+
});
|
|
231
|
+
connectors.command("update-metadata <definition-id>")
|
|
232
|
+
.description("Edit connector metadata separately from action code")
|
|
233
|
+
.requiredOption("--changes <json-file>", "metadata fields to change")
|
|
234
|
+
.requiredOption("--expected-updated-at <timestamp>", "exact updated_at from metadata read")
|
|
235
|
+
.action(async (id, opts) => {
|
|
236
|
+
const url = `/api/organizations/${requireOrgId()}/connector-definitions/${id}`;
|
|
237
|
+
const before = await apiJson(url);
|
|
238
|
+
if (before.updated_at !== opts.expectedUpdatedAt)
|
|
239
|
+
fail("Connector metadata changed; read it again before updating", EXIT.conflict);
|
|
240
|
+
const changes = JSON.parse(readFileSync(opts.changes, "utf8"));
|
|
241
|
+
await confirm(`Update connector metadata? Before: ${JSON.stringify(before.metadata)} Changes: ${JSON.stringify(changes)}`, getContext().yes);
|
|
242
|
+
printData(await apiJson(url, {
|
|
243
|
+
method: "PATCH", body: JSON.stringify({ expected_updated_at: opts.expectedUpdatedAt, changes }),
|
|
244
|
+
}), getContext().json);
|
|
245
|
+
});
|
|
226
246
|
const secrets = connectors
|
|
227
247
|
.command("secrets")
|
|
228
248
|
.description("Connector credentials (values never transit argv or stdout)");
|
package/dist/http.js
CHANGED
|
@@ -121,6 +121,16 @@ export async function apiJson(pathname, init = {}) {
|
|
|
121
121
|
const { path, message } = issue;
|
|
122
122
|
return typeof message === "string" ? [`${Array.isArray(path) ? path.join(".") + ": " : ""}${message}`] : [];
|
|
123
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
|
+
}
|
|
124
134
|
const prefix = typeof body?.tool === "string" ? `${body.tool}: ` : "";
|
|
125
135
|
fail([prefix + message, ...details].join("\n"), exitCodeFor(res.status));
|
|
126
136
|
}
|