@fieldwork-ai/cli 0.17.2 → 0.23.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 +5 -6
- package/dist/commands/assets.js +2 -2
- package/dist/commands/automations.js +49 -27
- package/dist/commands/connectors.js +24 -4
- package/dist/commands/tables.d.ts +2 -1
- package/dist/commands/tables.js +5 -4
- package/dist/http.js +0 -10
- package/dist/index.js +3 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,11 +16,11 @@ Inside a Fieldwork compute container the CLI is preinstalled and pre-authenticat
|
|
|
16
16
|
Every tool your account can use in chat (platform reads plus your org's connected integrations) is invocable by name. Arguments are one JSON object:
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
fieldwork tool describe
|
|
20
|
-
fieldwork tool run
|
|
19
|
+
fieldwork tool describe kb_grep # description + input schema
|
|
20
|
+
fieldwork tool run kb_grep --args '{"pattern": "replenishment|reorder point", "-i": true}'
|
|
21
21
|
echo '{"database": "Metrics DB", "sql": "select count(*) from orders"}' \
|
|
22
22
|
| fieldwork tool run db_query --args - # stdin for large payloads
|
|
23
|
-
fieldwork tool run
|
|
23
|
+
fieldwork tool run kb_grep --args '{"pattern": "x"}' --arg pattern=override
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
`--arg key=value` overlays a string field; `--arg key:=json` parses the value as JSON. Tool-level errors (strings starting `Error:`) are data and exit 0; transport and auth failures use the exit codes below.
|
|
@@ -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.
|
|
@@ -63,6 +65,3 @@ Source stays in the private app repository under `cli/`. Run `pnpm install --fro
|
|
|
63
65
|
|
|
64
66
|
The terminal chat client is the separate public `@fieldwork-ai/fieldwork-code` package (`fwcode`).
|
|
65
67
|
|
|
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.
|
package/dist/commands/assets.js
CHANGED
|
@@ -155,12 +155,12 @@ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
|
|
|
155
155
|
* a global system, its id for an org-authored one. Mirrors `connectorAddress`
|
|
156
156
|
* in the app; the CLI is its own package and shares no code with it. */
|
|
157
157
|
function dbAddressOf(c) {
|
|
158
|
-
return c.definition_is_global ? c.definition_key : c.
|
|
158
|
+
return c.definition_is_global ? c.definition_key : c.connector_id;
|
|
159
159
|
}
|
|
160
160
|
async function resolveDbConnector(database) {
|
|
161
161
|
const body = await apiJson(`/api/organizations/${requireOrgId()}/db-connectors`);
|
|
162
162
|
const found = UUID_RE.test(database)
|
|
163
|
-
? body.connectors.find((c) => c.
|
|
163
|
+
? body.connectors.find((c) => c.connector_id === database)
|
|
164
164
|
: (body.connectors.find((c) => c.definition_key === database) ??
|
|
165
165
|
body.connectors.find((c) => c.name === database) ??
|
|
166
166
|
body.connectors.find((c) => c.display_name === database));
|
|
@@ -71,27 +71,49 @@ async function followRun(automationId, runId, intervalSeconds, json) {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
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
|
-
});
|
|
92
74
|
const automations = program
|
|
93
75
|
.command("automations")
|
|
94
|
-
.description("
|
|
76
|
+
.description("Create, manage and inspect automations");
|
|
77
|
+
const callTool = async (name, args) => {
|
|
78
|
+
const response = await apiJson("/api/tools/run", {
|
|
79
|
+
method: "POST",
|
|
80
|
+
body: JSON.stringify({ name, args }),
|
|
81
|
+
});
|
|
82
|
+
printData(response.result, true);
|
|
83
|
+
};
|
|
84
|
+
for (const operation of ["publish", "update"]) {
|
|
85
|
+
automations
|
|
86
|
+
.command(operation)
|
|
87
|
+
.description(`${operation} an automation`)
|
|
88
|
+
.requiredOption("--file <path>")
|
|
89
|
+
.action(async (options) => {
|
|
90
|
+
const input = JSON.parse(await readFile(options.file, "utf8"));
|
|
91
|
+
if (operation === "publish" || operation === "update")
|
|
92
|
+
await confirm(`Apply this automation ${operation}?`, getContext().yes);
|
|
93
|
+
await callTool(`automations_${operation}`, input);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
automations
|
|
97
|
+
.command("run <name>")
|
|
98
|
+
.description("Start a real automation run")
|
|
99
|
+
.action(async (name) => {
|
|
100
|
+
await confirm(`Run "${name}" now?`, getContext().yes);
|
|
101
|
+
await callTool("automations_run", { name });
|
|
102
|
+
});
|
|
103
|
+
automations
|
|
104
|
+
.command("examples")
|
|
105
|
+
.description("A working code automation")
|
|
106
|
+
.action(() => callTool("automations_examples", {}));
|
|
107
|
+
for (const operation of ["get-run", "wait"]) {
|
|
108
|
+
automations
|
|
109
|
+
.command(`${operation} <name> <run-id>`)
|
|
110
|
+
.description(operation === "wait" ? "Wait briefly for a run" : "Inspect one run")
|
|
111
|
+
.action((name, runId) => callTool(`automations_${operation.replaceAll("-", "_")}`, {
|
|
112
|
+
name,
|
|
113
|
+
automation_run_id: runId,
|
|
114
|
+
...(operation === "get-run" ? { detail: "full" } : {}),
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
95
117
|
automations
|
|
96
118
|
.command("list")
|
|
97
119
|
.description("List the automations you can see")
|
|
@@ -112,12 +134,8 @@ export function registerAutomationCommands(program) {
|
|
|
112
134
|
});
|
|
113
135
|
automations
|
|
114
136
|
.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
|
-
});
|
|
137
|
+
.description("Read an automation definition, settings and recent runs")
|
|
138
|
+
.action((name) => callTool("automations_get", { name }));
|
|
121
139
|
const setEnabled = (enabled) => async (name) => {
|
|
122
140
|
const { json, yes } = getContext();
|
|
123
141
|
const found = await resolveAutomation(name);
|
|
@@ -171,10 +189,14 @@ export function registerAutomationCommands(program) {
|
|
|
171
189
|
.action(async (name, opts) => {
|
|
172
190
|
const { json } = getContext();
|
|
173
191
|
const found = await resolveAutomation(name);
|
|
174
|
-
const query = opts.run
|
|
192
|
+
const query = opts.run
|
|
193
|
+
? `?run=${encodeURIComponent(opts.run)}`
|
|
194
|
+
: "?limit=1";
|
|
175
195
|
const run = (await fetchRuns(found.automation_id, query))[0];
|
|
176
196
|
if (!run) {
|
|
177
|
-
fail(opts.run
|
|
197
|
+
fail(opts.run
|
|
198
|
+
? `no run ${opts.run} for "${name}"`
|
|
199
|
+
: `"${name}" has no runs yet`, EXIT.notFound);
|
|
178
200
|
}
|
|
179
201
|
printRun(run, json);
|
|
180
202
|
});
|
|
@@ -4,7 +4,7 @@ import { readBundle, writeBundle } from "../bundle.js";
|
|
|
4
4
|
import { apiJson, getContext, requireOrgId } from "../http.js";
|
|
5
5
|
import { EXIT, confirm, fail, note, printData } from "../output.js";
|
|
6
6
|
async function resolveDefinition(key) {
|
|
7
|
-
const body = await apiJson(`/api/organizations/${requireOrgId()}/
|
|
7
|
+
const body = await apiJson(`/api/organizations/${requireOrgId()}/connectors/library?include_drafts=true`);
|
|
8
8
|
const matches = (body.definitions ?? []).filter((d) => d.key === key);
|
|
9
9
|
const definition = matches.find((d) => d.org_private) ?? matches[0];
|
|
10
10
|
if (!definition) {
|
|
@@ -95,7 +95,7 @@ export function writeSecretsFile(out, secrets, opts) {
|
|
|
95
95
|
* a global system, its id for an org-authored one whose key is only unique
|
|
96
96
|
* within the org. Mirrors `connectorAddress` in the app. */
|
|
97
97
|
function addressOf(c) {
|
|
98
|
-
return c.definition_is_global ? c.definition_key : c.
|
|
98
|
+
return c.definition_is_global ? c.definition_key : c.connector_id;
|
|
99
99
|
}
|
|
100
100
|
async function listConnectors() {
|
|
101
101
|
const body = await apiJson(`/api/organizations/${requireOrgId()}/connectors`);
|
|
@@ -179,7 +179,7 @@ export function registerConnectorCommands(program) {
|
|
|
179
179
|
if (opts.actionName)
|
|
180
180
|
query.set("action_name", opts.actionName);
|
|
181
181
|
const suffix = query.size ? `?${query}` : "";
|
|
182
|
-
const body = await apiJson(`/api/organizations/${requireOrgId()}/
|
|
182
|
+
const body = await apiJson(`/api/organizations/${requireOrgId()}/connectors/${definition.connector_id}/source${suffix}`);
|
|
183
183
|
const dir = opts.out ?? key;
|
|
184
184
|
const pulled = writeBundle(dir, body.files);
|
|
185
185
|
if (json) {
|
|
@@ -212,7 +212,7 @@ export function registerConnectorCommands(program) {
|
|
|
212
212
|
fail('the tree\'s connector.json must carry a "key" field', EXIT.usage);
|
|
213
213
|
}
|
|
214
214
|
await confirm(`Publish ${Object.keys(files).length} files to connector "${key}"${opts.actionName ? `, action "${opts.actionName}"` : ""}?`, yes);
|
|
215
|
-
const body = await apiJson(`/api/organizations/${requireOrgId()}/
|
|
215
|
+
const body = await apiJson(`/api/organizations/${requireOrgId()}/connectors/publish`, {
|
|
216
216
|
method: "POST",
|
|
217
217
|
body: JSON.stringify({
|
|
218
218
|
message: opts.message,
|
|
@@ -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()}/connectors/${id}/metadata`), 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()}/connectors/${id}/metadata`;
|
|
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/commands/tables.js
CHANGED
|
@@ -2,18 +2,19 @@ import { readFile } from "node:fs/promises";
|
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { apiJson, requireOrgId, getContext } from "../http.js";
|
|
4
4
|
import { confirm, printData } from "../output.js";
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/** `fieldwork sheets`; also registered, hidden, under its old name `tables`. */
|
|
6
|
+
export function sheetsCommand(name = "sheets") {
|
|
7
|
+
const command = new Command(name).description("Sheets and their editable records");
|
|
7
8
|
const base = () => `/api/organizations/${requireOrgId()}/tables`;
|
|
8
9
|
command
|
|
9
10
|
.command("list")
|
|
10
11
|
.action(async () => printData(await apiJson(base()), true));
|
|
11
12
|
command
|
|
12
13
|
.command("create")
|
|
13
|
-
.requiredOption("--file <path>", "
|
|
14
|
+
.requiredOption("--file <path>", "Sheet definition file")
|
|
14
15
|
.action(async (options) => {
|
|
15
16
|
const body = await readFile(options.file, "utf8");
|
|
16
|
-
await confirm("Create this
|
|
17
|
+
await confirm("Create this sheet?", getContext().yes);
|
|
17
18
|
printData(await apiJson(base(), { method: "POST", body }), true);
|
|
18
19
|
});
|
|
19
20
|
command
|
package/dist/http.js
CHANGED
|
@@ -121,16 +121,6 @@ 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
|
-
}
|
|
134
124
|
const prefix = typeof body?.tool === "string" ? `${body.tool}: ` : "";
|
|
135
125
|
fail([prefix + message, ...details].join("\n"), exitCodeFor(res.status));
|
|
136
126
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { sheetsCommand } from "./commands/tables.js";
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { initContext } from "./http.js";
|
|
@@ -41,7 +41,8 @@ registerAppCommands(program);
|
|
|
41
41
|
registerAutomationCommands(program);
|
|
42
42
|
registerTaskCommands(program);
|
|
43
43
|
registerConnectorCommands(program);
|
|
44
|
-
program.addCommand(
|
|
44
|
+
program.addCommand(sheetsCommand());
|
|
45
|
+
program.addCommand(sheetsCommand("tables"), { hidden: true });
|
|
45
46
|
program.parseAsync(process.argv).catch((err) => {
|
|
46
47
|
process.stderr.write(`fieldwork: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
47
48
|
process.exit(1);
|