dexto 1.14.4 → 2.0.0-beta.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/dist/cli/auth/billing.d.ts +0 -6
- package/dist/cli/auth/billing.d.ts.map +1 -1
- package/dist/cli/auth/billing.js +0 -23
- package/dist/cli/auth/constants.d.ts +3 -31
- package/dist/cli/auth/constants.d.ts.map +1 -1
- package/dist/cli/auth/constants.js +9 -33
- package/dist/cli/auth/index.d.ts +2 -9
- package/dist/cli/auth/index.d.ts.map +1 -1
- package/dist/cli/auth/index.js +2 -7
- package/dist/cli/auth/sign-in-runtime.d.ts +12 -0
- package/dist/cli/auth/sign-in-runtime.d.ts.map +1 -0
- package/dist/cli/auth/sign-in-runtime.js +59 -0
- package/dist/cli/cloud/app-scopes.d.ts +4 -0
- package/dist/cli/cloud/app-scopes.d.ts.map +1 -0
- package/dist/cli/cloud/app-scopes.js +19 -0
- package/dist/cli/cloud/command.d.ts +143 -0
- package/dist/cli/cloud/command.d.ts.map +1 -0
- package/dist/cli/cloud/command.js +202 -0
- package/dist/cli/cloud/commands/account.d.ts +11 -0
- package/dist/cli/cloud/commands/account.d.ts.map +1 -0
- package/dist/cli/cloud/commands/account.js +128 -0
- package/dist/cli/cloud/commands/agents.d.ts +3 -0
- package/dist/cli/cloud/commands/agents.d.ts.map +1 -0
- package/dist/cli/cloud/commands/agents.js +524 -0
- package/dist/cli/cloud/commands/api.d.ts +3 -0
- package/dist/cli/cloud/commands/api.d.ts.map +1 -0
- package/dist/cli/cloud/commands/api.js +26 -0
- package/dist/cli/cloud/commands/app-files.d.ts +29 -0
- package/dist/cli/cloud/commands/app-files.d.ts.map +1 -0
- package/dist/cli/cloud/commands/app-files.js +72 -0
- package/dist/cli/cloud/commands/automations.d.ts +3 -0
- package/dist/cli/cloud/commands/automations.d.ts.map +1 -0
- package/dist/cli/cloud/commands/automations.js +232 -0
- package/dist/cli/cloud/commands/chats.d.ts +3 -0
- package/dist/cli/cloud/commands/chats.d.ts.map +1 -0
- package/dist/cli/cloud/commands/chats.js +189 -0
- package/dist/cli/cloud/commands/data-deletions.d.ts +3 -0
- package/dist/cli/cloud/commands/data-deletions.d.ts.map +1 -0
- package/dist/cli/cloud/commands/data-deletions.js +54 -0
- package/dist/cli/cloud/commands/deploys.d.ts +10 -0
- package/dist/cli/cloud/commands/deploys.d.ts.map +1 -0
- package/dist/cli/cloud/commands/deploys.js +223 -0
- package/dist/cli/cloud/commands/drive.d.ts +12 -0
- package/dist/cli/cloud/commands/drive.d.ts.map +1 -0
- package/dist/cli/cloud/commands/drive.js +320 -0
- package/dist/cli/cloud/commands/files.d.ts +24 -0
- package/dist/cli/cloud/commands/files.d.ts.map +1 -0
- package/dist/cli/cloud/commands/files.js +282 -0
- package/dist/cli/cloud/commands/find.d.ts +3 -0
- package/dist/cli/cloud/commands/find.d.ts.map +1 -0
- package/dist/cli/cloud/commands/find.js +63 -0
- package/dist/cli/cloud/commands/models.d.ts +3 -0
- package/dist/cli/cloud/commands/models.d.ts.map +1 -0
- package/dist/cli/cloud/commands/models.js +248 -0
- package/dist/cli/cloud/commands/profile.d.ts +3 -0
- package/dist/cli/cloud/commands/profile.d.ts.map +1 -0
- package/dist/cli/cloud/commands/profile.js +119 -0
- package/dist/cli/cloud/commands/projects.d.ts +5 -0
- package/dist/cli/cloud/commands/projects.d.ts.map +1 -0
- package/dist/cli/cloud/commands/projects.js +66 -0
- package/dist/cli/cloud/commands/publish.d.ts +3 -0
- package/dist/cli/cloud/commands/publish.d.ts.map +1 -0
- package/dist/cli/cloud/commands/publish.js +138 -0
- package/dist/cli/cloud/commands/sandboxes.d.ts +3 -0
- package/dist/cli/cloud/commands/sandboxes.d.ts.map +1 -0
- package/dist/cli/cloud/commands/sandboxes.js +265 -0
- package/dist/cli/cloud/commands/secrets.d.ts +3 -0
- package/dist/cli/cloud/commands/secrets.d.ts.map +1 -0
- package/dist/cli/cloud/commands/secrets.js +110 -0
- package/dist/cli/cloud/commands/skills.d.ts +3 -0
- package/dist/cli/cloud/commands/skills.d.ts.map +1 -0
- package/dist/cli/cloud/commands/skills.js +202 -0
- package/dist/cli/cloud/commands/web.d.ts +3 -0
- package/dist/cli/cloud/commands/web.d.ts.map +1 -0
- package/dist/cli/cloud/commands/web.js +53 -0
- package/dist/cli/cloud/commands/workspace-apps.d.ts +58 -0
- package/dist/cli/cloud/commands/workspace-apps.d.ts.map +1 -0
- package/dist/cli/cloud/commands/workspace-apps.js +656 -0
- package/dist/cli/cloud/commands/workspace.d.ts +36 -0
- package/dist/cli/cloud/commands/workspace.d.ts.map +1 -0
- package/dist/cli/cloud/commands/workspace.js +279 -0
- package/dist/cli/cloud/connection.d.ts +33 -0
- package/dist/cli/cloud/connection.d.ts.map +1 -0
- package/dist/cli/cloud/connection.js +38 -0
- package/dist/cli/cloud/events.d.ts +63 -0
- package/dist/cli/cloud/events.d.ts.map +1 -0
- package/dist/cli/cloud/events.js +72 -0
- package/dist/cli/cloud/host.d.ts +9 -0
- package/dist/cli/cloud/host.d.ts.map +1 -0
- package/dist/cli/cloud/host.js +105 -0
- package/dist/cli/cloud/local-chats.d.ts +73 -0
- package/dist/cli/cloud/local-chats.d.ts.map +1 -0
- package/dist/cli/cloud/local-chats.js +441 -0
- package/dist/cli/cloud/output.d.ts +34 -0
- package/dist/cli/cloud/output.d.ts.map +1 -0
- package/dist/cli/cloud/output.js +76 -0
- package/dist/cli/cloud/run.d.ts +26 -0
- package/dist/cli/cloud/run.d.ts.map +1 -0
- package/dist/cli/cloud/run.js +444 -0
- package/dist/cli/cloud/runtime.d.ts +33 -0
- package/dist/cli/cloud/runtime.d.ts.map +1 -0
- package/dist/cli/cloud/runtime.js +86 -0
- package/dist/cli/cloud/tar.d.ts +9 -0
- package/dist/cli/cloud/tar.d.ts.map +1 -0
- package/dist/cli/cloud/tar.js +69 -0
- package/dist/cli/cloud/transfer.d.ts +13 -0
- package/dist/cli/cloud/transfer.d.ts.map +1 -0
- package/dist/cli/cloud/transfer.js +35 -0
- package/dist/cli/cloud/v1.d.ts +19 -0
- package/dist/cli/cloud/v1.d.ts.map +1 -0
- package/dist/cli/cloud/v1.js +65 -0
- package/dist/cli/cloud/zip.d.ts +8 -0
- package/dist/cli/cloud/zip.d.ts.map +1 -0
- package/dist/cli/cloud/zip.js +66 -0
- package/dist/cli/commands/auth/sign-in-commands.d.ts +62 -0
- package/dist/cli/commands/auth/sign-in-commands.d.ts.map +1 -0
- package/dist/cli/commands/auth/sign-in-commands.js +247 -0
- package/dist/cli/commands/auth/sign-in-context.d.ts +16 -0
- package/dist/cli/commands/auth/sign-in-context.d.ts.map +1 -0
- package/dist/cli/commands/auth/sign-in-context.js +68 -0
- package/dist/cli/commands/cloud/register.d.ts +5 -0
- package/dist/cli/commands/cloud/register.d.ts.map +1 -1
- package/dist/cli/commands/cloud/register.js +42 -24
- package/dist/cli/commands/create-app.js +5 -6
- package/dist/cli/commands/create-image.d.ts.map +1 -1
- package/dist/cli/commands/create-image.js +9 -10
- package/dist/cli/commands/index.d.ts +0 -2
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +0 -2
- package/dist/cli/commands/init.d.ts +0 -2
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +5 -26
- package/dist/cli/commands/run/headless.js +2 -2
- package/dist/cli/commands/run/output.d.ts +29 -10
- package/dist/cli/commands/run/output.d.ts.map +1 -1
- package/dist/cli/commands/run/output.js +44 -22
- package/dist/cli/commands/run/register.d.ts.map +1 -1
- package/dist/cli/commands/run/register.js +21 -9
- package/dist/cli/commands/session/register.d.ts.map +1 -1
- package/dist/cli/commands/session/register.js +51 -21
- package/dist/cli/commands/session-commands.js +1 -1
- package/dist/cli/commands/setup.js +17 -14
- package/dist/cli/help-json.d.ts +22 -0
- package/dist/cli/help-json.d.ts.map +1 -0
- package/dist/cli/help-json.js +20 -0
- package/dist/cli/modes/cli.d.ts.map +1 -1
- package/dist/cli/modes/cli.js +42 -9
- package/dist/cli/modes/server.js +8 -8
- package/dist/cli/modes/web.js +3 -3
- package/dist/cli/utils/dexto-auth-check.d.ts +1 -1
- package/dist/cli/utils/dexto-auth-check.d.ts.map +1 -1
- package/dist/cli/utils/dexto-auth-check.js +4 -5
- package/dist/cli/utils/dexto-setup.d.ts +4 -5
- package/dist/cli/utils/dexto-setup.d.ts.map +1 -1
- package/dist/cli/utils/dexto-setup.js +17 -14
- package/dist/cli/utils/provider-setup.js +1 -1
- package/dist/cli/utils/scaffolding-utils.d.ts +9 -3
- package/dist/cli/utils/scaffolding-utils.d.ts.map +1 -1
- package/dist/cli/utils/scaffolding-utils.js +48 -40
- package/dist/cli/utils/version-check.d.ts.map +1 -1
- package/dist/cli/utils/version-check.js +2 -1
- package/dist/index-main.js +33 -65
- package/package.json +8 -7
- package/dist/cli/auth/api-client.d.ts +0 -125
- package/dist/cli/auth/api-client.d.ts.map +0 -1
- package/dist/cli/auth/api-client.js +0 -585
- package/dist/cli/auth/device.d.ts +0 -14
- package/dist/cli/auth/device.d.ts.map +0 -1
- package/dist/cli/auth/device.js +0 -84
- package/dist/cli/auth/dexto-api-key.d.ts +0 -34
- package/dist/cli/auth/dexto-api-key.d.ts.map +0 -1
- package/dist/cli/auth/dexto-api-key.js +0 -160
- package/dist/cli/auth/login-persistence.d.ts +0 -15
- package/dist/cli/auth/login-persistence.d.ts.map +0 -1
- package/dist/cli/auth/login-persistence.js +0 -72
- package/dist/cli/auth/oauth.d.ts +0 -39
- package/dist/cli/auth/oauth.d.ts.map +0 -1
- package/dist/cli/auth/oauth.js +0 -434
- package/dist/cli/auth/service.d.ts +0 -24
- package/dist/cli/auth/service.d.ts.map +0 -1
- package/dist/cli/auth/service.js +0 -161
- package/dist/cli/auth/types.d.ts +0 -11
- package/dist/cli/auth/types.d.ts.map +0 -1
- package/dist/cli/auth/types.js +0 -1
- package/dist/cli/cloud-chat.d.ts +0 -18
- package/dist/cli/cloud-chat.d.ts.map +0 -1
- package/dist/cli/cloud-chat.js +0 -734
- package/dist/cli/commands/auth/index.d.ts +0 -4
- package/dist/cli/commands/auth/index.d.ts.map +0 -1
- package/dist/cli/commands/auth/index.js +0 -4
- package/dist/cli/commands/auth/login.d.ts +0 -12
- package/dist/cli/commands/auth/login.d.ts.map +0 -1
- package/dist/cli/commands/auth/login.js +0 -153
- package/dist/cli/commands/auth/logout.d.ts +0 -5
- package/dist/cli/commands/auth/logout.d.ts.map +0 -1
- package/dist/cli/commands/auth/logout.js +0 -81
- package/dist/cli/commands/auth/register.d.ts +0 -3
- package/dist/cli/commands/auth/register.d.ts.map +0 -1
- package/dist/cli/commands/auth/register.js +0 -96
- package/dist/cli/commands/auth/status.d.ts +0 -2
- package/dist/cli/commands/auth/status.d.ts.map +0 -1
- package/dist/cli/commands/auth/status.js +0 -22
- package/dist/cli/commands/billing/index.d.ts +0 -2
- package/dist/cli/commands/billing/index.d.ts.map +0 -1
- package/dist/cli/commands/billing/index.js +0 -2
- package/dist/cli/commands/billing/register.d.ts +0 -3
- package/dist/cli/commands/billing/register.d.ts.map +0 -1
- package/dist/cli/commands/billing/register.js +0 -20
- package/dist/cli/commands/billing/status.d.ts +0 -8
- package/dist/cli/commands/billing/status.d.ts.map +0 -1
- package/dist/cli/commands/billing/status.js +0 -71
- package/dist/cli/commands/cloud/discovery.d.ts +0 -17
- package/dist/cli/commands/cloud/discovery.d.ts.map +0 -1
- package/dist/cli/commands/cloud/discovery.js +0 -88
- package/dist/cli/commands/deploy/client.d.ts +0 -113
- package/dist/cli/commands/deploy/client.d.ts.map +0 -1
- package/dist/cli/commands/deploy/client.js +0 -519
- package/dist/cli/commands/deploy/config.d.ts +0 -37
- package/dist/cli/commands/deploy/config.d.ts.map +0 -1
- package/dist/cli/commands/deploy/config.js +0 -144
- package/dist/cli/commands/deploy/entry-agent.d.ts +0 -3
- package/dist/cli/commands/deploy/entry-agent.d.ts.map +0 -1
- package/dist/cli/commands/deploy/entry-agent.js +0 -70
- package/dist/cli/commands/deploy/index.d.ts +0 -12
- package/dist/cli/commands/deploy/index.d.ts.map +0 -1
- package/dist/cli/commands/deploy/index.js +0 -370
- package/dist/cli/commands/deploy/links.d.ts +0 -3
- package/dist/cli/commands/deploy/links.d.ts.map +0 -1
- package/dist/cli/commands/deploy/links.js +0 -53
- package/dist/cli/commands/deploy/register.d.ts +0 -6
- package/dist/cli/commands/deploy/register.d.ts.map +0 -1
- package/dist/cli/commands/deploy/register.js +0 -128
- package/dist/cli/commands/deploy/snapshot.d.ts +0 -12
- package/dist/cli/commands/deploy/snapshot.d.ts.map +0 -1
- package/dist/cli/commands/deploy/snapshot.js +0 -76
- package/dist/cli/commands/deploy/state.d.ts +0 -13
- package/dist/cli/commands/deploy/state.d.ts.map +0 -1
- package/dist/cli/commands/deploy/state.js +0 -122
- package/dist/cli/commands/span/register.d.ts +0 -6
- package/dist/cli/commands/span/register.d.ts.map +0 -1
- package/dist/cli/commands/span/register.js +0 -25
- package/dist/cli/commands/trace/client.d.ts +0 -98
- package/dist/cli/commands/trace/client.d.ts.map +0 -1
- package/dist/cli/commands/trace/client.js +0 -150
- package/dist/cli/commands/trace/format.d.ts +0 -5
- package/dist/cli/commands/trace/format.d.ts.map +0 -1
- package/dist/cli/commands/trace/format.js +0 -58
- package/dist/cli/commands/trace/index.d.ts +0 -21
- package/dist/cli/commands/trace/index.d.ts.map +0 -1
- package/dist/cli/commands/trace/index.js +0 -49
- package/dist/cli/commands/trace/register.d.ts +0 -6
- package/dist/cli/commands/trace/register.d.ts.map +0 -1
- package/dist/cli/commands/trace/register.js +0 -71
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
// `workspace <app> <operation>`: the workspace's operations grouped by the app that provides them.
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { isJsonObject, MissingScopeFailure, shellWord, UsageError, } from '../command.js';
|
|
5
|
+
import { readAppScopes } from '../app-scopes.js';
|
|
6
|
+
import { attachFiles, fileFieldOf, isLocalFile } from './app-files.js';
|
|
7
|
+
import { agreeToCall, ASKING_LEVELS, callWorkspaceTool, listWorkspaceTools, readWorkspaceTool, } from './workspace.js';
|
|
8
|
+
/**
|
|
9
|
+
* The apps the sign-in reaches: those whose manifests provide its tools, and the Apps whose
|
|
10
|
+
* actions it can run. Tools a server lists without a provider stay under `workspace call`.
|
|
11
|
+
*/
|
|
12
|
+
export async function listWorkspaceApps(context) {
|
|
13
|
+
const tools = await listWorkspaceTools(context);
|
|
14
|
+
const apps = new Map();
|
|
15
|
+
for (const tool of tools) {
|
|
16
|
+
const provider = providerOf(tool);
|
|
17
|
+
if (provider === null)
|
|
18
|
+
continue;
|
|
19
|
+
const app = apps.get(provider.id) ?? { ...provider, actions: 0, tools: 0 };
|
|
20
|
+
apps.set(provider.id, { ...app, tools: app.tools + 1 });
|
|
21
|
+
}
|
|
22
|
+
for (const listed of await listActionApps(context, tools)) {
|
|
23
|
+
const app = apps.get(listed.id) ?? {
|
|
24
|
+
actions: 0,
|
|
25
|
+
id: listed.id,
|
|
26
|
+
kind: 'app',
|
|
27
|
+
tools: 0,
|
|
28
|
+
};
|
|
29
|
+
apps.set(listed.id, { ...app, actions: listed.actions });
|
|
30
|
+
}
|
|
31
|
+
return [...apps.values()]
|
|
32
|
+
.filter((app) => app.tools + app.actions > 0)
|
|
33
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
34
|
+
}
|
|
35
|
+
/** Runs `<app> [operation] [action] [--field value…]`; `commands` are the CLI's own for the app. */
|
|
36
|
+
export async function runWorkspaceApp(context, args, help, commands) {
|
|
37
|
+
const [appId, operationName, ...rest] = args;
|
|
38
|
+
if (appId === undefined)
|
|
39
|
+
throw new UsageError('Name an app, such as dexto workspace drive.');
|
|
40
|
+
const operations = await appOperations(context, await listWorkspaceTools(context), appId);
|
|
41
|
+
if (operations.length === 0 && commands.length === 0) {
|
|
42
|
+
await requireOperationScopes(context, appId, operationName);
|
|
43
|
+
throw new UsageError(`Unknown command or app: ${appId}. List your workspace's apps with dexto workspace.`);
|
|
44
|
+
}
|
|
45
|
+
if (operationName === undefined || operationName.startsWith('-')) {
|
|
46
|
+
printOperations(context, appId, operations, commands);
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
const operation = operations.find((each) => each.name === operationName);
|
|
50
|
+
if (operation === undefined) {
|
|
51
|
+
await requireOperationScopes(context, appId, operationName);
|
|
52
|
+
throw new UsageError(`${appId} has no operation ${operationName}. Its operations: ${[...operations, ...commands].map((each) => each.name).join(', ')}.`);
|
|
53
|
+
}
|
|
54
|
+
if (help) {
|
|
55
|
+
printOperationHelp(context, appId, operation);
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
// An App action's dispatcher refuses one scope at a time, so ask for all it takes up front.
|
|
59
|
+
if (operation.call.kind === 'action')
|
|
60
|
+
await requireOperationScopes(context, appId, operationName);
|
|
61
|
+
const { attachFiles: attach, input, yes, } = await operationInput(context, operation, rest, appId);
|
|
62
|
+
const agreed = await agreeToCall(context, {
|
|
63
|
+
level: callLevel(operation, input),
|
|
64
|
+
schema: operation.confirmSchema,
|
|
65
|
+
title: operation.title,
|
|
66
|
+
}, yes);
|
|
67
|
+
// Local files are uploaded only once the person has agreed, so declining leaves none behind.
|
|
68
|
+
await attach();
|
|
69
|
+
return operation.call.kind === 'tool'
|
|
70
|
+
? await callWorkspaceTool(context, operation.call.name, {
|
|
71
|
+
...input,
|
|
72
|
+
...(operation.call.action === null ? {} : { action: operation.call.action }),
|
|
73
|
+
...agreed,
|
|
74
|
+
})
|
|
75
|
+
: await callWorkspaceTool(context, 'dexto_apps_action', {
|
|
76
|
+
appId,
|
|
77
|
+
arguments: input,
|
|
78
|
+
toolName: operation.call.toolName,
|
|
79
|
+
...agreed,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* A sign-in sees only the operations its scopes allow, so one it can't see may need more: refuses
|
|
84
|
+
* with the scopes the named operation takes that the sign-in lacks, or, with none named, those of
|
|
85
|
+
* the app's operations. The server leaves out any that can spend, and a name no operation has
|
|
86
|
+
* asks for nothing.
|
|
87
|
+
*/
|
|
88
|
+
async function requireOperationScopes(context, appId, operationName) {
|
|
89
|
+
const connection = await context.connection();
|
|
90
|
+
if (connection.kind !== 'oauth')
|
|
91
|
+
return;
|
|
92
|
+
const operations = Object.entries((await readAppScopes(context.runtime, connection.login.appUrl)).get(appId) ?? {});
|
|
93
|
+
const needed = operationName === undefined
|
|
94
|
+
? operations.flatMap(([, scopes]) => scopes)
|
|
95
|
+
: (operations.find(([name]) => toolOperationName(name, appId) === operationName ||
|
|
96
|
+
actionOperationName(name, appId) === operationName)?.[1] ?? []);
|
|
97
|
+
const missing = [...new Set(needed)].filter((scope) => !connection.login.scopes.includes(scope));
|
|
98
|
+
if (missing.length > 0)
|
|
99
|
+
throw new MissingScopeFailure(context, missing.join(','), `Your sign-in can't use ${operationName === undefined ? appId : `${appId} ${operationName}`} yet: it needs ${missing.join(', ')}.`);
|
|
100
|
+
}
|
|
101
|
+
/** Refuses at once when an OAuth sign-in lacks any of `scopes`, naming the sign-in that has them. */
|
|
102
|
+
async function requireScopes(context, scopes, what) {
|
|
103
|
+
const connection = await context.connection();
|
|
104
|
+
if (connection.kind !== 'oauth')
|
|
105
|
+
return;
|
|
106
|
+
const missing = scopes.filter((scope) => !connection.login.scopes.includes(scope));
|
|
107
|
+
if (missing.length > 0)
|
|
108
|
+
throw new MissingScopeFailure(context, missing.join(','), `Your sign-in can't ${what} yet: it needs ${missing.join(', ')}.`);
|
|
109
|
+
}
|
|
110
|
+
/** An app's tools and its headless App actions, each under a short name unique in the app. */
|
|
111
|
+
async function appOperations(context, tools, appId) {
|
|
112
|
+
const fromTools = tools
|
|
113
|
+
.filter((tool) => providerOf(tool)?.id === appId)
|
|
114
|
+
.flatMap((tool) => {
|
|
115
|
+
const actionLevels = actionLevelsOf(tool);
|
|
116
|
+
const operation = {
|
|
117
|
+
call: { action: null, actionLevels, kind: 'tool', name: tool.name },
|
|
118
|
+
confirmSchema: tool.inputSchema,
|
|
119
|
+
description: tool.description ?? '',
|
|
120
|
+
inputSchema: tool.inputSchema,
|
|
121
|
+
level: levelOf(tool),
|
|
122
|
+
name: toolOperationName(tool.name, appId),
|
|
123
|
+
title: tool.title ?? tool.name,
|
|
124
|
+
};
|
|
125
|
+
const actions = actionValues(tool.inputSchema);
|
|
126
|
+
// `drive list-items`, not `drive drive list-items`.
|
|
127
|
+
if (!appPrefixes(appId).includes(tool.name.replace(/^dexto_/u, '')) ||
|
|
128
|
+
actions.length === 0)
|
|
129
|
+
return [operation];
|
|
130
|
+
return actions.map((action) => {
|
|
131
|
+
return {
|
|
132
|
+
...operation,
|
|
133
|
+
call: { action, actionLevels, kind: 'tool', name: tool.name },
|
|
134
|
+
inputSchema: actionVariant(tool.inputSchema, action),
|
|
135
|
+
level: actionLevels[action] ?? operation.level,
|
|
136
|
+
name: kebab(action),
|
|
137
|
+
title: `${operation.title}: ${kebab(action).replaceAll('-', ' ')}`,
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
const runsActions = tools.find((tool) => tool.name === 'dexto_apps_action');
|
|
142
|
+
const fromActions = (await appActions(context, tools, appId)).map((action) => ({
|
|
143
|
+
call: { kind: 'action', toolName: action.toolName },
|
|
144
|
+
confirmSchema: runsActions?.inputSchema ?? null,
|
|
145
|
+
description: action.description,
|
|
146
|
+
inputSchema: action.inputSchema,
|
|
147
|
+
level: action.level,
|
|
148
|
+
name: actionOperationName(action.toolName, appId),
|
|
149
|
+
title: action.title,
|
|
150
|
+
}));
|
|
151
|
+
return uniqueNames([...fromTools, ...fromActions]);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The CLI command that makes a call: `dexto workspace <app> <operation> --field value`, or
|
|
155
|
+
* `workspace call` when no app operation does. A value written `<type>` is left for the caller to
|
|
156
|
+
* fill in. `operationsByApp` keeps each app's operations across calls.
|
|
157
|
+
*/
|
|
158
|
+
export async function commandForCall(context, tools, call, operationsByApp) {
|
|
159
|
+
const operationsOf = async (appId) => {
|
|
160
|
+
const known = operationsByApp.get(appId);
|
|
161
|
+
if (known !== undefined)
|
|
162
|
+
return known;
|
|
163
|
+
const listed = await appOperations(context, tools, appId);
|
|
164
|
+
operationsByApp.set(appId, listed);
|
|
165
|
+
return listed;
|
|
166
|
+
};
|
|
167
|
+
const words = await (async () => {
|
|
168
|
+
if (call.tool === 'dexto_apps_action') {
|
|
169
|
+
const { appId, toolName } = call.arguments;
|
|
170
|
+
if (typeof appId !== 'string' || typeof toolName !== 'string')
|
|
171
|
+
return null;
|
|
172
|
+
const operation = (await operationsOf(appId)).find((each) => each.call.kind === 'action' && each.call.toolName === toolName);
|
|
173
|
+
const input = isJsonObject(call.arguments.arguments) ? call.arguments.arguments : {};
|
|
174
|
+
return operation === undefined ? null : [appId, operation.name, ...flagWords(input)];
|
|
175
|
+
}
|
|
176
|
+
const tool = tools.find((each) => each.name === call.tool);
|
|
177
|
+
const appId = tool === undefined ? null : (providerOf(tool)?.id ?? null);
|
|
178
|
+
if (appId === null)
|
|
179
|
+
return null;
|
|
180
|
+
const action = typeof call.arguments.action === 'string' ? call.arguments.action : null;
|
|
181
|
+
const operation = (await operationsOf(appId)).find((each) => each.call.kind === 'tool' &&
|
|
182
|
+
each.call.name === call.tool &&
|
|
183
|
+
(each.call.action === null || each.call.action === action));
|
|
184
|
+
if (operation === undefined || operation.call.kind !== 'tool')
|
|
185
|
+
return null;
|
|
186
|
+
const input = Object.fromEntries(Object.entries(call.arguments).filter(([name]) => name !== 'action' || action === null));
|
|
187
|
+
return [
|
|
188
|
+
appId,
|
|
189
|
+
operation.name,
|
|
190
|
+
...(operation.call.action === null && action !== null ? [kebab(action)] : []),
|
|
191
|
+
...flagWords(input),
|
|
192
|
+
];
|
|
193
|
+
})();
|
|
194
|
+
return words === null
|
|
195
|
+
? ['dexto', 'workspace', 'call', call.tool, '--data', JSON.stringify(call.arguments)]
|
|
196
|
+
.map(shellWord)
|
|
197
|
+
.join(' ')
|
|
198
|
+
: ['dexto', 'workspace', ...words].map(commandWord).join(' ');
|
|
199
|
+
}
|
|
200
|
+
/** Each input field as `--field value`; objects and lists as JSON. */
|
|
201
|
+
function flagWords(input) {
|
|
202
|
+
return Object.entries(input).flatMap(([name, value]) => [
|
|
203
|
+
`--${kebab(name)}`,
|
|
204
|
+
typeof value === 'string' ? value : JSON.stringify(value),
|
|
205
|
+
]);
|
|
206
|
+
}
|
|
207
|
+
/** A placeholder such as `<string>` stays readable; any other word is quoted as a shell needs. */
|
|
208
|
+
function commandWord(word) {
|
|
209
|
+
return /^<[\w ?|:-]+>$/u.test(word) ? word : shellWord(word);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Keeps names unique in an app. Tools keep their short names, falling back to their full names
|
|
213
|
+
* when two tools clash; an App action whose name is taken becomes `app-<name>`. Anything still
|
|
214
|
+
* clashing is numbered.
|
|
215
|
+
*/
|
|
216
|
+
function uniqueNames(operations) {
|
|
217
|
+
const named = (name, kind) => operations.filter((each) => each.name === name && (kind === undefined || each.call.kind === kind)).length;
|
|
218
|
+
const renamed = operations.map((operation) => {
|
|
219
|
+
if (operation.call.kind === 'action')
|
|
220
|
+
return named(operation.name) > 1
|
|
221
|
+
? { ...operation, name: `app-${operation.name}` }
|
|
222
|
+
: operation;
|
|
223
|
+
if (named(operation.name, 'tool') === 1)
|
|
224
|
+
return operation;
|
|
225
|
+
const full = [operation.call.name.replace(/^dexto_/u, ''), operation.call.action];
|
|
226
|
+
return { ...operation, name: kebab(full.filter((part) => part !== null).join('_')) };
|
|
227
|
+
});
|
|
228
|
+
const seen = new Map();
|
|
229
|
+
return renamed.map((operation) => {
|
|
230
|
+
const count = (seen.get(operation.name) ?? 0) + 1;
|
|
231
|
+
seen.set(operation.name, count);
|
|
232
|
+
return count === 1 ? operation : { ...operation, name: `${operation.name}-${count}` };
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
/** `dexto_chat_start` in `chat` is `start`; `dexto_gpu_sandboxes_list` in `gpus` is `sandboxes-list`. */
|
|
236
|
+
function toolOperationName(tool, appId) {
|
|
237
|
+
const bare = tool.replace(/^dexto_/u, '');
|
|
238
|
+
for (const prefix of appPrefixes(appId)) {
|
|
239
|
+
if (bare.startsWith(`${prefix}_`))
|
|
240
|
+
return kebab(bare.slice(prefix.length + 1));
|
|
241
|
+
}
|
|
242
|
+
return kebab(bare);
|
|
243
|
+
}
|
|
244
|
+
/** How tool names start for an app: `capabilities`, `capability`; `gpus`, `gpu`. */
|
|
245
|
+
function appPrefixes(appId) {
|
|
246
|
+
const name = appId.replaceAll('-', '_');
|
|
247
|
+
return [name, name.replace(/ies$/u, 'y'), name.replace(/s$/u, '')];
|
|
248
|
+
}
|
|
249
|
+
/** `drive.listItems` in `drive` is `list-items`. */
|
|
250
|
+
function actionOperationName(toolName, appId) {
|
|
251
|
+
return kebab(toolName.startsWith(`${appId}.`) ? toolName.slice(appId.length + 1) : toolName);
|
|
252
|
+
}
|
|
253
|
+
function kebab(value) {
|
|
254
|
+
return value
|
|
255
|
+
.replaceAll(/([a-z0-9])([A-Z])/gu, '$1-$2')
|
|
256
|
+
.replaceAll(/[_.\s]+/gu, '-')
|
|
257
|
+
.toLowerCase();
|
|
258
|
+
}
|
|
259
|
+
function providerOf(tool) {
|
|
260
|
+
const id = tool._meta?.['dexto/app'];
|
|
261
|
+
const kind = tool._meta?.['dexto/app_kind'];
|
|
262
|
+
return typeof id === 'string' ? { id, kind: typeof kind === 'string' ? kind : 'app' } : null;
|
|
263
|
+
}
|
|
264
|
+
function levelOf(tool) {
|
|
265
|
+
const level = tool._meta?.['dexto/level'];
|
|
266
|
+
return typeof level === 'string' ? level : 'act';
|
|
267
|
+
}
|
|
268
|
+
function actionLevelsOf(tool) {
|
|
269
|
+
const levels = tool._meta?.['dexto/action_levels'];
|
|
270
|
+
if (!isJsonObject(levels))
|
|
271
|
+
return {};
|
|
272
|
+
return Object.fromEntries(Object.entries(levels).flatMap(([action, level]) => typeof level === 'string' ? [[action, level]] : []));
|
|
273
|
+
}
|
|
274
|
+
/** A call's level: its action's own, as `chat computer read` reads, else the operation's. */
|
|
275
|
+
function callLevel(operation, input) {
|
|
276
|
+
if (operation.call.kind !== 'tool' || typeof input.action !== 'string')
|
|
277
|
+
return operation.level;
|
|
278
|
+
return operation.call.actionLevels[input.action] ?? operation.level;
|
|
279
|
+
}
|
|
280
|
+
/** The Apps this sign-in can see, and how many actions each declares, headless or not. */
|
|
281
|
+
async function listActionApps(context, tools) {
|
|
282
|
+
if (!tools.some((tool) => tool.name === 'dexto_apps_list'))
|
|
283
|
+
return [];
|
|
284
|
+
const listed = await readWorkspaceTool(context, 'dexto_apps_list', {});
|
|
285
|
+
if (!isJsonObject(listed) || !Array.isArray(listed.apps))
|
|
286
|
+
return [];
|
|
287
|
+
return listed.apps.flatMap((app) => isJsonObject(app) && typeof app.id === 'string' && Array.isArray(app.actionsAvailable)
|
|
288
|
+
? [{ actions: app.actionsAvailable.length, id: app.id }]
|
|
289
|
+
: []);
|
|
290
|
+
}
|
|
291
|
+
/** One App's actions that run without a live UI, from `dexto_app_get`. */
|
|
292
|
+
async function appActions(context, tools, appId) {
|
|
293
|
+
if (!tools.some((tool) => tool.name === 'dexto_app_get'))
|
|
294
|
+
return [];
|
|
295
|
+
const app = await readWorkspaceTool(context, 'dexto_app_get', { appId });
|
|
296
|
+
if (!isJsonObject(app) || !Array.isArray(app.actions))
|
|
297
|
+
return [];
|
|
298
|
+
return app.actions.flatMap((action) => isJsonObject(action) &&
|
|
299
|
+
typeof action.toolName === 'string' &&
|
|
300
|
+
action.execution === 'headless'
|
|
301
|
+
? [
|
|
302
|
+
{
|
|
303
|
+
description: typeof action.description === 'string' ? action.description : '',
|
|
304
|
+
inputSchema: action.inputSchema,
|
|
305
|
+
level: typeof action.level === 'string' ? action.level : 'act',
|
|
306
|
+
title: typeof action.title === 'string' ? action.title : action.toolName,
|
|
307
|
+
toolName: action.toolName,
|
|
308
|
+
},
|
|
309
|
+
]
|
|
310
|
+
: []);
|
|
311
|
+
}
|
|
312
|
+
/** The app's operations, then the CLI's own commands for it; as JSON, `commands` beside `data`. */
|
|
313
|
+
function printOperations(context, appId, operations, commands) {
|
|
314
|
+
if (context.output.json)
|
|
315
|
+
return context.output.record({ commands, data: operations });
|
|
316
|
+
if (operations.length > 0)
|
|
317
|
+
printOperationTable(context, appId, operations);
|
|
318
|
+
if (commands.length === 0)
|
|
319
|
+
return;
|
|
320
|
+
const width = Math.max(...commands.map((command) => command.usage.length));
|
|
321
|
+
context.runtime.writeOutput(`${operations.length > 0 ? '\n' : ''}Commands:\n${commands
|
|
322
|
+
.map((command) => ` ${command.usage.padEnd(width)} ${command.summary}`)
|
|
323
|
+
.join('\n')}\n`);
|
|
324
|
+
}
|
|
325
|
+
function printOperationTable(context, appId, operations) {
|
|
326
|
+
context.output.list({ data: operations }, [
|
|
327
|
+
{ header: 'operation', value: (operation) => operation.name },
|
|
328
|
+
{ header: 'level', value: (operation) => operation.level },
|
|
329
|
+
{
|
|
330
|
+
header: 'asks',
|
|
331
|
+
value: (operation) => (ASKING_LEVELS.has(operation.level) ? 'yes' : ''),
|
|
332
|
+
},
|
|
333
|
+
{ header: 'title', value: (operation) => operation.title },
|
|
334
|
+
]);
|
|
335
|
+
if (!context.output.json)
|
|
336
|
+
context.output.note(`Run one with dexto workspace ${appId} <operation> --field value; --help on it shows its input.`);
|
|
337
|
+
}
|
|
338
|
+
function printOperationHelp(context, appId, operation) {
|
|
339
|
+
if (context.output.json)
|
|
340
|
+
return context.output.record(operation);
|
|
341
|
+
const fields = schemaFields(operation.inputSchema);
|
|
342
|
+
const actions = openActions(operation);
|
|
343
|
+
const lines = [
|
|
344
|
+
`Usage: dexto workspace ${appId} ${operation.name}${actions.length > 0 ? ' <action>' : ''} [--field value…] [--data JSON]${ASKING_LEVELS.has(operation.level) ? ' [--yes]' : ''}`,
|
|
345
|
+
'',
|
|
346
|
+
operation.title,
|
|
347
|
+
...(operation.description.length > 0 ? ['', operation.description] : []),
|
|
348
|
+
'',
|
|
349
|
+
`Level: ${operation.level}${ASKING_LEVELS.has(operation.level) ? ' (asks first: pass --yes once your person has agreed)' : ''}`,
|
|
350
|
+
...(actions.length > 0 ? ['', `Actions: ${actions.map(kebab).join(', ')}`] : []),
|
|
351
|
+
...(fields.length > 0
|
|
352
|
+
? [
|
|
353
|
+
'',
|
|
354
|
+
'Input:',
|
|
355
|
+
...fields.map((field) => ` --${kebab(field.name)}${REQUEST_ID_FIELDS.has(field.name) ? ' (made up when left out)' : field.required ? ' (required)' : ''} ${field.file === null ? field.type : `file or link${field.file.list ? ', repeatable' : ''}`}${field.description.length > 0 ? ` ${field.description}` : ''}`),
|
|
356
|
+
'',
|
|
357
|
+
'Any field also reads a file, or stdin for -, as --<field>-file PATH.',
|
|
358
|
+
...(fields.some((field) => field.file !== null)
|
|
359
|
+
? [
|
|
360
|
+
'A file or link field takes a local path, which is uploaded into the app, or an https link.',
|
|
361
|
+
]
|
|
362
|
+
: []),
|
|
363
|
+
]
|
|
364
|
+
: []),
|
|
365
|
+
];
|
|
366
|
+
context.runtime.writeOutput(`${lines.join('\n')}\n`);
|
|
367
|
+
}
|
|
368
|
+
/** The input object's fields, across the variants of a schema that offers several. */
|
|
369
|
+
function schemaFields(schema) {
|
|
370
|
+
const fields = new Map();
|
|
371
|
+
for (const variant of schemaVariants(schema)) {
|
|
372
|
+
const required = new Set(Array.isArray(variant.required)
|
|
373
|
+
? variant.required.filter((each) => typeof each === 'string')
|
|
374
|
+
: []);
|
|
375
|
+
const properties = isJsonObject(variant.properties) ? variant.properties : {};
|
|
376
|
+
for (const [name, property] of Object.entries(properties)) {
|
|
377
|
+
// A word picks `action`, and `--yes` sends `confirm`: neither is a field to type.
|
|
378
|
+
if (fields.has(name) || name === 'action' || name === 'confirm')
|
|
379
|
+
continue;
|
|
380
|
+
fields.set(name, {
|
|
381
|
+
description: isJsonObject(property) && typeof property.description === 'string'
|
|
382
|
+
? property.description
|
|
383
|
+
: '',
|
|
384
|
+
file: isJsonObject(property) ? fileFieldOf(property) : null,
|
|
385
|
+
list: isJsonObject(property) && takesStringList(property),
|
|
386
|
+
name,
|
|
387
|
+
required: required.has(name) && schemaVariants(schema).length === 1,
|
|
388
|
+
type: isJsonObject(property) ? propertyType(property) : 'value',
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return [...fields.values()];
|
|
393
|
+
}
|
|
394
|
+
/** Fields that only make a retry safe, which the CLI makes up when they're left out. */
|
|
395
|
+
const REQUEST_ID_FIELDS = new Set([
|
|
396
|
+
'idempotencyKey',
|
|
397
|
+
'idempotency_key',
|
|
398
|
+
'requestId',
|
|
399
|
+
'request_id',
|
|
400
|
+
]);
|
|
401
|
+
/**
|
|
402
|
+
* The required fields an input leaves out, by the schema that applies: the operation's own, or the
|
|
403
|
+
* variant of the action picked. `action` and `confirm` are the CLI's to set.
|
|
404
|
+
*/
|
|
405
|
+
function missingFields(operation, input) {
|
|
406
|
+
const variants = schemaVariants(operation.inputSchema);
|
|
407
|
+
const variant = variants.length === 1
|
|
408
|
+
? variants[0]
|
|
409
|
+
: variants.find((each) => {
|
|
410
|
+
const action = isJsonObject(each.properties) ? each.properties.action : null;
|
|
411
|
+
return isJsonObject(action) && action.const === input.action;
|
|
412
|
+
});
|
|
413
|
+
const required = Array.isArray(variant?.required)
|
|
414
|
+
? variant.required.filter((each) => typeof each === 'string')
|
|
415
|
+
: [];
|
|
416
|
+
return required.filter((name) => name !== 'action' && name !== 'confirm' && !(name in input));
|
|
417
|
+
}
|
|
418
|
+
function schemaVariants(schema) {
|
|
419
|
+
if (!isJsonObject(schema))
|
|
420
|
+
return [];
|
|
421
|
+
const variants = Array.isArray(schema.anyOf)
|
|
422
|
+
? schema.anyOf
|
|
423
|
+
: Array.isArray(schema.oneOf)
|
|
424
|
+
? schema.oneOf
|
|
425
|
+
: [schema];
|
|
426
|
+
// Variants beside the schema's own properties only narrow them: each one is all of them.
|
|
427
|
+
const own = schema.properties;
|
|
428
|
+
if (variants[0] === schema || !isJsonObject(own))
|
|
429
|
+
return variants.filter(isJsonObject);
|
|
430
|
+
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
431
|
+
return variants.filter(isJsonObject).map((variant) => ({
|
|
432
|
+
...variant,
|
|
433
|
+
properties: {
|
|
434
|
+
...own,
|
|
435
|
+
...(isJsonObject(variant.properties) ? variant.properties : {}),
|
|
436
|
+
},
|
|
437
|
+
required: [...required, ...(Array.isArray(variant.required) ? variant.required : [])],
|
|
438
|
+
}));
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* `string`, `folder|recent` for a choice, `string,…` for a list of strings, or `string or null` for
|
|
442
|
+
* a field that takes either.
|
|
443
|
+
*/
|
|
444
|
+
function propertyType(property) {
|
|
445
|
+
if (Array.isArray(property.enum))
|
|
446
|
+
return property.enum.map(String).join('|');
|
|
447
|
+
if (takesStringList(property) && !Array.isArray(property.anyOf))
|
|
448
|
+
return 'string,…';
|
|
449
|
+
if (typeof property.const === 'string')
|
|
450
|
+
return property.const;
|
|
451
|
+
if (Array.isArray(property.anyOf))
|
|
452
|
+
return property.anyOf
|
|
453
|
+
.map((each) => (isJsonObject(each) ? propertyType(each) : 'value'))
|
|
454
|
+
.join(' or ');
|
|
455
|
+
return typeof property.type === 'string' ? property.type : 'value';
|
|
456
|
+
}
|
|
457
|
+
/** An array of strings, or a choice of types that includes one. */
|
|
458
|
+
function takesStringList(property) {
|
|
459
|
+
if (Array.isArray(property.anyOf))
|
|
460
|
+
return property.anyOf.some((each) => isJsonObject(each) && takesStringList(each));
|
|
461
|
+
return (property.type === 'array' &&
|
|
462
|
+
isJsonObject(property.items) &&
|
|
463
|
+
property.items.type === 'string');
|
|
464
|
+
}
|
|
465
|
+
/** The actions left to pick: none where the operation is one action of its tool. */
|
|
466
|
+
function openActions(operation) {
|
|
467
|
+
return operation.call.kind === 'tool' && operation.call.action !== null
|
|
468
|
+
? []
|
|
469
|
+
: actionValues(operation.inputSchema);
|
|
470
|
+
}
|
|
471
|
+
/** Whether the input takes fields its schema doesn't name; a closed object takes none. */
|
|
472
|
+
function takesOtherFields(schema) {
|
|
473
|
+
const variants = schemaVariants(schema);
|
|
474
|
+
return (variants.length === 0 || variants.some((variant) => variant.additionalProperties !== false));
|
|
475
|
+
}
|
|
476
|
+
/** The `action` values a tool that takes one action at a time accepts. */
|
|
477
|
+
function actionValues(schema) {
|
|
478
|
+
const values = [];
|
|
479
|
+
for (const variant of schemaVariants(schema)) {
|
|
480
|
+
const action = isJsonObject(variant.properties) ? variant.properties.action : undefined;
|
|
481
|
+
if (!isJsonObject(action))
|
|
482
|
+
continue;
|
|
483
|
+
if (typeof action.const === 'string')
|
|
484
|
+
values.push(action.const);
|
|
485
|
+
if (Array.isArray(action.enum))
|
|
486
|
+
values.push(...action.enum.filter((each) => typeof each === 'string'));
|
|
487
|
+
}
|
|
488
|
+
return [...new Set(values)];
|
|
489
|
+
}
|
|
490
|
+
/** The variant of a tool's schema that takes this action, or the whole schema when it has one. */
|
|
491
|
+
function actionVariant(schema, action) {
|
|
492
|
+
const variants = schemaVariants(schema);
|
|
493
|
+
return (variants.find((variant) => {
|
|
494
|
+
const property = isJsonObject(variant.properties) ? variant.properties.action : null;
|
|
495
|
+
return isJsonObject(property) && property.const === action;
|
|
496
|
+
}) ?? schema);
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* The operation's input: `--data` or `--file` first, then each `--field value`, typed by the
|
|
500
|
+
* field's schema. `--<field>-file <path|->` reads a field from a file or stdin, unless a field has
|
|
501
|
+
* that whole name; a list of strings takes `a,b` or the flag repeated. A word after the operation
|
|
502
|
+
* picks the action of a tool that takes one, and `--yes` agrees to an operation that asks first.
|
|
503
|
+
*/
|
|
504
|
+
async function operationInput(context, operation, args, appId) {
|
|
505
|
+
const input = {};
|
|
506
|
+
let yes = false;
|
|
507
|
+
const fields = schemaFields(operation.inputSchema);
|
|
508
|
+
const actions = openActions(operation);
|
|
509
|
+
const lists = new Map();
|
|
510
|
+
const files = new Map();
|
|
511
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
512
|
+
const token = args[index] ?? '';
|
|
513
|
+
if (token === '--yes') {
|
|
514
|
+
yes = true;
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
if (!token.startsWith('--')) {
|
|
518
|
+
const action = actions.find((each) => kebab(each) === kebab(token));
|
|
519
|
+
if (action === undefined || 'action' in input)
|
|
520
|
+
throw new UsageError(actions.length === 0
|
|
521
|
+
? `Unexpected ${token}. Give the input as --field value.`
|
|
522
|
+
: `Unknown action ${token}. Actions: ${actions.map(kebab).join(', ')}.`);
|
|
523
|
+
input.action = action;
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
const [flag, inline] = token.includes('=')
|
|
527
|
+
? [token.slice(2, token.indexOf('=')), token.slice(token.indexOf('=') + 1)]
|
|
528
|
+
: [token.slice(2), null];
|
|
529
|
+
const next = args[index + 1];
|
|
530
|
+
const value = inline ?? (next !== undefined && !next.startsWith('--') ? next : null);
|
|
531
|
+
if (inline === null && value !== null)
|
|
532
|
+
index += 1;
|
|
533
|
+
if (flag === 'data' || flag === 'file') {
|
|
534
|
+
if (value === null)
|
|
535
|
+
throw new UsageError(`--${flag} needs a value.`);
|
|
536
|
+
const text = flag === 'file' ? await readText(context, value) : value;
|
|
537
|
+
Object.assign(input, parseObject(text, `--${flag}`));
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
const field = fields.find((each) => kebab(each.name) === kebab(flag)) ?? null;
|
|
541
|
+
const fromFile = field === null && flag.endsWith('-file')
|
|
542
|
+
? (fields.find((each) => kebab(each.name) === kebab(flag.slice(0, -5))) ?? null)
|
|
543
|
+
: null;
|
|
544
|
+
if (fromFile !== null) {
|
|
545
|
+
if (value === null)
|
|
546
|
+
throw new UsageError(`--${flag} needs a path, or - for stdin.`);
|
|
547
|
+
const text = await readText(context, value);
|
|
548
|
+
// A string field takes the file's text as it is, a list its lines or commas, and any
|
|
549
|
+
// other field reads it as JSON.
|
|
550
|
+
input[fromFile.name] = fromFile.list
|
|
551
|
+
? listItems(text.trim(), flag)
|
|
552
|
+
: fromFile.type.split(' or ').includes('string')
|
|
553
|
+
? text
|
|
554
|
+
: typedValue(text, fromFile.type, flag);
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
if (field === null && !takesOtherFields(operation.inputSchema))
|
|
558
|
+
throw new UsageError(`${operation.name} takes no --${flag}. Its input: ${fields.map((each) => `--${kebab(each.name)}`).join(', ') || 'none'}.`);
|
|
559
|
+
const name = field?.name ?? camel(flag);
|
|
560
|
+
if (field !== null && field.file !== null && operation.call.kind === 'action') {
|
|
561
|
+
if (value === null)
|
|
562
|
+
throw new UsageError(`--${flag} needs a file's path or a link.`);
|
|
563
|
+
// JSON gives the field's value as it is; a path or a link is one file.
|
|
564
|
+
const given = /^[[{]/u.test(value) ? typedValue(value, 'array', flag) : value;
|
|
565
|
+
files.set(field, [
|
|
566
|
+
...(files.get(field) ?? []),
|
|
567
|
+
...(Array.isArray(given) ? given : [given]),
|
|
568
|
+
]);
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
if (value === null)
|
|
572
|
+
input[name] = true;
|
|
573
|
+
else if (field?.list === true) {
|
|
574
|
+
const items = [...(lists.get(name) ?? []), ...listItems(value, flag)];
|
|
575
|
+
lists.set(name, items);
|
|
576
|
+
input[name] = items;
|
|
577
|
+
}
|
|
578
|
+
else
|
|
579
|
+
input[name] = typedValue(value, field?.type ?? 'value', flag);
|
|
580
|
+
}
|
|
581
|
+
// Files are uploaded only once the rest of the input is complete.
|
|
582
|
+
const named = {
|
|
583
|
+
...input,
|
|
584
|
+
...Object.fromEntries([...files.keys()].map((field) => [field.name, null])),
|
|
585
|
+
};
|
|
586
|
+
for (const name of missingFields(operation, named)) {
|
|
587
|
+
if (!REQUEST_ID_FIELDS.has(name))
|
|
588
|
+
throw new UsageError(`${operation.name} needs --${kebab(name)}. Add --help for its input.`);
|
|
589
|
+
// A request id only makes a retry safe: one is made up, and said, so it can be reused.
|
|
590
|
+
input[name] = randomUUID();
|
|
591
|
+
context.output.note(`Using --${kebab(name)} ${String(input[name])}; pass the same one to retry without doing it twice.`);
|
|
592
|
+
}
|
|
593
|
+
for (const [field, values] of files) {
|
|
594
|
+
if (field.file?.list === false && values.length > 1)
|
|
595
|
+
throw new UsageError(`--${kebab(field.name)} takes one file.`);
|
|
596
|
+
}
|
|
597
|
+
// A local file is uploaded before the action runs: check the sign-in can do both first, so a
|
|
598
|
+
// refusal leaves no upload behind.
|
|
599
|
+
if ([...files].some(([field, values]) => field.file !== null && values.some(isLocalFile)))
|
|
600
|
+
await requireScopes(context, ['drive:write', 'apps:execute'], `attach files to ${appId}`);
|
|
601
|
+
const attach = async () => {
|
|
602
|
+
for (const [field, values] of files) {
|
|
603
|
+
if (field.file === null)
|
|
604
|
+
continue;
|
|
605
|
+
const attached = await attachFiles(context, appId, field.file, values);
|
|
606
|
+
input[field.name] = field.file.list ? attached : attached[0];
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
return { attachFiles: attach, input, yes };
|
|
610
|
+
}
|
|
611
|
+
/** A file's text, or stdin's for `-`. */
|
|
612
|
+
async function readText(context, path) {
|
|
613
|
+
if (path === '-')
|
|
614
|
+
return await context.runtime.readStdin();
|
|
615
|
+
return await readFile(path, 'utf8').catch((error) => {
|
|
616
|
+
throw new UsageError(`Can't read ${path}: ${error instanceof Error ? error.message : 'unknown error'}.`);
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
/** A list's items: a JSON array as it is, otherwise the words between commas or lines. */
|
|
620
|
+
function listItems(value, flag) {
|
|
621
|
+
if (!value.startsWith('['))
|
|
622
|
+
return value.split(/[,\n]/u).flatMap((item) => item.trim() || []);
|
|
623
|
+
const parsed = typedValue(value, 'array', flag);
|
|
624
|
+
if (!Array.isArray(parsed))
|
|
625
|
+
throw new UsageError(`--${flag} isn't a JSON array.`);
|
|
626
|
+
return parsed;
|
|
627
|
+
}
|
|
628
|
+
function parseObject(text, source) {
|
|
629
|
+
let parsed;
|
|
630
|
+
try {
|
|
631
|
+
parsed = JSON.parse(text);
|
|
632
|
+
}
|
|
633
|
+
catch {
|
|
634
|
+
throw new UsageError(`${source} isn't valid JSON.`);
|
|
635
|
+
}
|
|
636
|
+
if (!isJsonObject(parsed))
|
|
637
|
+
throw new UsageError(`${source} must be a JSON object.`);
|
|
638
|
+
return parsed;
|
|
639
|
+
}
|
|
640
|
+
/** A flag's value as its field's type: a string field keeps the text; JSON for the rest. */
|
|
641
|
+
function typedValue(value, type, flag) {
|
|
642
|
+
const types = type.split(' or ');
|
|
643
|
+
if (types.some((each) => each === 'string' || each.includes('|')))
|
|
644
|
+
return value === 'null' && types.includes('null') ? null : value;
|
|
645
|
+
try {
|
|
646
|
+
return JSON.parse(value);
|
|
647
|
+
}
|
|
648
|
+
catch {
|
|
649
|
+
if (/^[[{"]/u.test(value))
|
|
650
|
+
throw new UsageError(`--${flag} isn't valid JSON.`);
|
|
651
|
+
return value;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
function camel(value) {
|
|
655
|
+
return value.replaceAll(/-([a-z0-9])/gu, (_, letter) => letter.toUpperCase());
|
|
656
|
+
}
|