@wiro-ai/wiro-mcp 1.4.1 → 1.5.1
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 +38 -15
- package/dist/client.d.ts +19 -6
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +44 -27
- package/dist/client.js.map +1 -1
- package/dist/instructions.d.ts.map +1 -1
- package/dist/instructions.js +7 -4
- package/dist/instructions.js.map +1 -1
- package/dist/playbooks.d.ts.map +1 -1
- package/dist/playbooks.js +46 -35
- package/dist/playbooks.js.map +1 -1
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +3 -2
- package/dist/prompts.js.map +1 -1
- package/dist/tools/apply-agent-skills.d.ts.map +1 -1
- package/dist/tools/apply-agent-skills.js +458 -109
- package/dist/tools/apply-agent-skills.js.map +1 -1
- package/dist/tools/bind-agent-project.d.ts +4 -0
- package/dist/tools/bind-agent-project.d.ts.map +1 -0
- package/dist/tools/bind-agent-project.js +405 -0
- package/dist/tools/bind-agent-project.js.map +1 -0
- package/dist/tools/cancel-agent-message.d.ts.map +1 -1
- package/dist/tools/cancel-agent-message.js +6 -3
- package/dist/tools/cancel-agent-message.js.map +1 -1
- package/dist/tools/cancel-task.js +3 -3
- package/dist/tools/cancel-task.js.map +1 -1
- package/dist/tools/clone-workflow.d.ts.map +1 -1
- package/dist/tools/clone-workflow.js +5 -4
- package/dist/tools/clone-workflow.js.map +1 -1
- package/dist/tools/connect-agent-oauth.d.ts.map +1 -1
- package/dist/tools/connect-agent-oauth.js +161 -40
- package/dist/tools/connect-agent-oauth.js.map +1 -1
- package/dist/tools/control-agent.d.ts.map +1 -1
- package/dist/tools/control-agent.js +109 -36
- package/dist/tools/control-agent.js.map +1 -1
- package/dist/tools/create-workflow.d.ts +3 -0
- package/dist/tools/create-workflow.d.ts.map +1 -1
- package/dist/tools/create-workflow.js +12 -11
- package/dist/tools/create-workflow.js.map +1 -1
- package/dist/tools/delete-agent-session.d.ts.map +1 -1
- package/dist/tools/delete-agent-session.js +18 -12
- package/dist/tools/delete-agent-session.js.map +1 -1
- package/dist/tools/delete-agent.d.ts.map +1 -1
- package/dist/tools/delete-agent.js +12 -9
- package/dist/tools/delete-agent.js.map +1 -1
- package/dist/tools/delete-workflow.d.ts.map +1 -1
- package/dist/tools/delete-workflow.js +5 -4
- package/dist/tools/delete-workflow.js.map +1 -1
- package/dist/tools/deploy-agent.d.ts.map +1 -1
- package/dist/tools/deploy-agent.js +239 -46
- package/dist/tools/deploy-agent.js.map +1 -1
- package/dist/tools/disconnect-agent-oauth.d.ts.map +1 -1
- package/dist/tools/disconnect-agent-oauth.js +32 -12
- package/dist/tools/disconnect-agent-oauth.js.map +1 -1
- package/dist/tools/get-account.d.ts.map +1 -1
- package/dist/tools/get-account.js +148 -42
- package/dist/tools/get-account.js.map +1 -1
- package/dist/tools/get-agent-activity.d.ts.map +1 -1
- package/dist/tools/get-agent-activity.js +185 -35
- package/dist/tools/get-agent-activity.js.map +1 -1
- package/dist/tools/get-agent-conversation.d.ts.map +1 -1
- package/dist/tools/get-agent-conversation.js +57 -35
- package/dist/tools/get-agent-conversation.js.map +1 -1
- package/dist/tools/get-agent-message.d.ts.map +1 -1
- package/dist/tools/get-agent-message.js +6 -4
- package/dist/tools/get-agent-message.js.map +1 -1
- package/dist/tools/get-agent-template.d.ts.map +1 -1
- package/dist/tools/get-agent-template.js +13 -17
- package/dist/tools/get-agent-template.js.map +1 -1
- package/dist/tools/get-model-schema.js +3 -3
- package/dist/tools/get-model-schema.js.map +1 -1
- package/dist/tools/get-my-agent.d.ts.map +1 -1
- package/dist/tools/get-my-agent.js +13 -8
- package/dist/tools/get-my-agent.js.map +1 -1
- package/dist/tools/get-playbook.d.ts.map +1 -1
- package/dist/tools/get-playbook.js +3 -2
- package/dist/tools/get-playbook.js.map +1 -1
- package/dist/tools/get-task-price.js +3 -3
- package/dist/tools/get-task-price.js.map +1 -1
- package/dist/tools/get-task.js +3 -3
- package/dist/tools/get-task.js.map +1 -1
- package/dist/tools/get-workflow.d.ts.map +1 -1
- package/dist/tools/get-workflow.js +38 -19
- package/dist/tools/get-workflow.js.map +1 -1
- package/dist/tools/index.d.ts +3 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +6 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/kill-task.js +3 -3
- package/dist/tools/kill-task.js.map +1 -1
- package/dist/tools/list-agent-skills.d.ts.map +1 -1
- package/dist/tools/list-agent-skills.js +11 -8
- package/dist/tools/list-agent-skills.js.map +1 -1
- package/dist/tools/list-agent-templates.js +4 -4
- package/dist/tools/list-agent-templates.js.map +1 -1
- package/dist/tools/list-my-agents.d.ts.map +1 -1
- package/dist/tools/list-my-agents.js +11 -8
- package/dist/tools/list-my-agents.js.map +1 -1
- package/dist/tools/list-projects.d.ts +4 -0
- package/dist/tools/list-projects.d.ts.map +1 -0
- package/dist/tools/list-projects.js +141 -0
- package/dist/tools/list-projects.js.map +1 -0
- package/dist/tools/list-studio-results.js +3 -3
- package/dist/tools/list-studio-results.js.map +1 -1
- package/dist/tools/list-tasks.js +3 -3
- package/dist/tools/list-tasks.js.map +1 -1
- package/dist/tools/list-workflow-runs.d.ts.map +1 -1
- package/dist/tools/list-workflow-runs.js +6 -4
- package/dist/tools/list-workflow-runs.js.map +1 -1
- package/dist/tools/list-workflows.d.ts.map +1 -1
- package/dist/tools/list-workflows.js +7 -6
- package/dist/tools/list-workflows.js.map +1 -1
- package/dist/tools/manage-agent-billing.d.ts.map +1 -1
- package/dist/tools/manage-agent-billing.js +207 -59
- package/dist/tools/manage-agent-billing.js.map +1 -1
- package/dist/tools/manage-agent-custom-skill.d.ts.map +1 -1
- package/dist/tools/manage-agent-custom-skill.js +119 -15
- package/dist/tools/manage-agent-custom-skill.js.map +1 -1
- package/dist/tools/recommend-model.js +3 -3
- package/dist/tools/recommend-model.js.map +1 -1
- package/dist/tools/resume-workflow-run.d.ts.map +1 -1
- package/dist/tools/resume-workflow-run.js +3 -2
- package/dist/tools/resume-workflow-run.js.map +1 -1
- package/dist/tools/run-model.js +3 -3
- package/dist/tools/run-model.js.map +1 -1
- package/dist/tools/run-workflow.d.ts.map +1 -1
- package/dist/tools/run-workflow.js +3 -2
- package/dist/tools/run-workflow.js.map +1 -1
- package/dist/tools/save-workflow-from-agent.d.ts.map +1 -1
- package/dist/tools/save-workflow-from-agent.js +17 -12
- package/dist/tools/save-workflow-from-agent.js.map +1 -1
- package/dist/tools/schemas.d.ts +32 -26
- package/dist/tools/schemas.d.ts.map +1 -1
- package/dist/tools/schemas.js +67 -0
- package/dist/tools/schemas.js.map +1 -1
- package/dist/tools/search-docs.js +3 -3
- package/dist/tools/search-docs.js.map +1 -1
- package/dist/tools/search-models.js +3 -3
- package/dist/tools/search-models.js.map +1 -1
- package/dist/tools/send-agent-message.d.ts.map +1 -1
- package/dist/tools/send-agent-message.js +30 -9
- package/dist/tools/send-agent-message.js.map +1 -1
- package/dist/tools/set-agent-credentials.d.ts.map +1 -1
- package/dist/tools/set-agent-credentials.js +55 -61
- package/dist/tools/set-agent-credentials.js.map +1 -1
- package/dist/tools/update-agent.d.ts.map +1 -1
- package/dist/tools/update-agent.js +59 -20
- package/dist/tools/update-agent.js.map +1 -1
- package/dist/tools/update-workflow.d.ts +199 -0
- package/dist/tools/update-workflow.d.ts.map +1 -1
- package/dist/tools/update-workflow.js +20 -7
- package/dist/tools/update-workflow.js.map +1 -1
- package/dist/tools/upload-file.js +3 -3
- package/dist/tools/upload-file.js.map +1 -1
- package/dist/tools/wait-for-task.js +3 -3
- package/dist/tools/wait-for-task.js.map +1 -1
- package/dist/tools/wait-for-workflow-run.d.ts +3 -3
- package/dist/tools/wait-for-workflow-run.d.ts.map +1 -1
- package/dist/tools/wait-for-workflow-run.js +7 -6
- package/dist/tools/wait-for-workflow-run.js.map +1 -1
- package/dist/utils/agent-catalog.d.ts +59 -0
- package/dist/utils/agent-catalog.d.ts.map +1 -1
- package/dist/utils/agent-catalog.js +28 -7
- package/dist/utils/agent-catalog.js.map +1 -1
- package/dist/utils/agent-config.d.ts +172 -9
- package/dist/utils/agent-config.d.ts.map +1 -1
- package/dist/utils/agent-config.js +416 -60
- package/dist/utils/agent-config.js.map +1 -1
- package/dist/utils/agent-message.d.ts +36 -14
- package/dist/utils/agent-message.d.ts.map +1 -1
- package/dist/utils/agent-message.js +92 -26
- package/dist/utils/agent-message.js.map +1 -1
- package/dist/utils/agent-targets.d.ts +7 -3
- package/dist/utils/agent-targets.d.ts.map +1 -1
- package/dist/utils/agent-targets.js +35 -23
- package/dist/utils/agent-targets.js.map +1 -1
- package/dist/utils/agent-turns.d.ts +19 -12
- package/dist/utils/agent-turns.d.ts.map +1 -1
- package/dist/utils/agent-turns.js +97 -15
- package/dist/utils/agent-turns.js.map +1 -1
- package/dist/utils/api.d.ts +6 -0
- package/dist/utils/api.d.ts.map +1 -1
- package/dist/utils/api.js +22 -1
- package/dist/utils/api.js.map +1 -1
- package/dist/utils/format.d.ts.map +1 -1
- package/dist/utils/format.js +7 -13
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/json-schema-dialect.d.ts.map +1 -1
- package/dist/utils/json-schema-dialect.js +4 -0
- package/dist/utils/json-schema-dialect.js.map +1 -1
- package/dist/utils/media.d.ts +8 -0
- package/dist/utils/media.d.ts.map +1 -1
- package/dist/utils/media.js +29 -0
- package/dist/utils/media.js.map +1 -1
- package/dist/utils/poll.d.ts +5 -2
- package/dist/utils/poll.d.ts.map +1 -1
- package/dist/utils/poll.js +10 -9
- package/dist/utils/poll.js.map +1 -1
- package/dist/utils/quoted-text.d.ts +28 -0
- package/dist/utils/quoted-text.d.ts.map +1 -0
- package/dist/utils/quoted-text.js +64 -0
- package/dist/utils/quoted-text.js.map +1 -0
- package/dist/utils/studio-runs.d.ts.map +1 -1
- package/dist/utils/studio-runs.js +7 -4
- package/dist/utils/studio-runs.js.map +1 -1
- package/dist/utils/user-agent.d.ts +197 -29
- package/dist/utils/user-agent.d.ts.map +1 -1
- package/dist/utils/user-agent.js +314 -48
- package/dist/utils/user-agent.js.map +1 -1
- package/dist/utils/wiro-project.d.ts +45 -0
- package/dist/utils/wiro-project.d.ts.map +1 -0
- package/dist/utils/wiro-project.js +66 -0
- package/dist/utils/wiro-project.js.map +1 -0
- package/dist/utils/workflow-save.d.ts +22 -1
- package/dist/utils/workflow-save.d.ts.map +1 -1
- package/dist/utils/workflow-save.js +122 -24
- package/dist/utils/workflow-save.js.map +1 -1
- package/dist/utils/workflow.d.ts +26 -13
- package/dist/utils/workflow.d.ts.map +1 -1
- package/dist/utils/workflow.js +51 -20
- package/dist/utils/workflow.js.map +1 -1
- package/dist/utils/workspace.d.ts +30 -4
- package/dist/utils/workspace.d.ts.map +1 -1
- package/dist/utils/workspace.js +95 -12
- package/dist/utils/workspace.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
|
-
import { ApiResultError, asString, hasErrorCode, isPlainObject, requireApiSuccess, toNumber, truncate, unixToIso, } from './api.js';
|
|
3
|
-
import {
|
|
2
|
+
import { AGENT_NOT_FOUND_TEXT, ApiResultError, asString, hasErrorCode, isAgentNotFound, isPlainObject, requireApiSuccess, toNumber, truncate, unixToIso, } from './api.js';
|
|
3
|
+
import { oneLine, quotedSpan } from './quoted-text.js';
|
|
4
|
+
import { credentialKeysSetupWaitsFor, isWalletNotRead, nameList, planEndsWithPeriod, skillLabel, STARTER_TASK_KEY, toSkillGroups, WALLET_NOT_READ_NOTE, WIRO_PROJECT_CREDENTIAL, wordsFromKey, } from './user-agent.js';
|
|
4
5
|
// Helpers for configuring a deployed agent: its registry skills, credentials,
|
|
5
6
|
// sign-in connections and custom skills. The pure functions mirror the server
|
|
6
7
|
// rules closely so a bad request is caught before anything is written.
|
|
@@ -61,11 +62,59 @@ export function restartNote(status, what = 'change') {
|
|
|
61
62
|
return `The ${what} takes effect the next time the agent starts.`;
|
|
62
63
|
}
|
|
63
64
|
}
|
|
65
|
+
export function keyList(keys) {
|
|
66
|
+
return keys.length > 1 ? `${keys.slice(0, -1).join(', ')} and ${keys[keys.length - 1]}` : keys.join('');
|
|
67
|
+
}
|
|
68
|
+
function afterWriteNote(status) {
|
|
69
|
+
switch (status) {
|
|
70
|
+
case 1:
|
|
71
|
+
return 'The agent restarts to load the new values, which takes about a minute.';
|
|
72
|
+
case 2:
|
|
73
|
+
case 3:
|
|
74
|
+
return 'The agent is starting and loads the new values.';
|
|
75
|
+
case 4:
|
|
76
|
+
// A save that changes a value sets a running agent stopping before Wiro answers.
|
|
77
|
+
return 'The agent kept running: a save that matches what is stored does not restart it. If it should load new values now, control_agent with action restart does it.';
|
|
78
|
+
case 6:
|
|
79
|
+
return 'The agent still needs setup before it can start; get_my_agent shows what is missing.';
|
|
80
|
+
case 5:
|
|
81
|
+
return 'The agent stopped with an error; the values are used when it starts.';
|
|
82
|
+
case null:
|
|
83
|
+
return 'Check the agent with get_my_agent.';
|
|
84
|
+
default:
|
|
85
|
+
return `The agent is ${agentStatusLabel(status)}; the values are used when it starts.`;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** What the setup still waits for and the tool that gives each one: a Wiro project, a sign-in or saved fields. */
|
|
89
|
+
export function setupStillMissingNote(missing) {
|
|
90
|
+
if (missing.length === 0)
|
|
91
|
+
return null;
|
|
92
|
+
const titles = (items) => items.map(item => item.title);
|
|
93
|
+
const isProject = (item) => item.key === WIRO_PROJECT_CREDENTIAL;
|
|
94
|
+
const steps = [
|
|
95
|
+
{ verb: 'Set', names: titles(missing.filter(item => !item.oauth && !isProject(item))), tool: 'set_agent_credentials' },
|
|
96
|
+
{ verb: 'Connect', names: titles(missing.filter(item => item.oauth && !isProject(item))), tool: 'connect_agent_oauth' },
|
|
97
|
+
{ verb: 'Connect', names: titles(missing.filter(isProject)), tool: 'bind_agent_project' },
|
|
98
|
+
].filter(step => step.names.length > 0);
|
|
99
|
+
const first = steps[0];
|
|
100
|
+
const how = steps.length === 1 && first
|
|
101
|
+
? `${first.verb} ${first.names.length > 1 ? 'them' : 'it'} with ${first.tool}`
|
|
102
|
+
: steps.map((step, index) => `${index === 0 ? step.verb : step.verb.toLowerCase()} ${keyList(step.names)} with ${step.tool}`).join(', ');
|
|
103
|
+
return `Setup is not finished: still missing ${keyList(titles(missing))}. ${how}, then check with get_my_agent.`;
|
|
104
|
+
}
|
|
105
|
+
/** When the agent loads the values, and the credentials its setup still waits for, at any status. */
|
|
106
|
+
export function afterWriteNotes(status, missing) {
|
|
107
|
+
const setup = setupStillMissingNote(missing);
|
|
108
|
+
if (!setup)
|
|
109
|
+
return [afterWriteNote(status)];
|
|
110
|
+
// At status 6 the general note says the same without the names.
|
|
111
|
+
return status === 6 ? [setup] : [afterWriteNote(status), setup];
|
|
112
|
+
}
|
|
64
113
|
export async function readUserAgent(client, agentGuid, signal) {
|
|
65
114
|
const body = requireApiSuccess(await client.post('/UserAgent/Detail', { guid: agentGuid }, signal), 'Could not read the agent.');
|
|
66
115
|
const row = Array.isArray(body.useragents) ? body.useragents[0] : undefined;
|
|
67
116
|
if (!isPlainObject(row))
|
|
68
|
-
throw new Error(
|
|
117
|
+
throw new Error(AGENT_NOT_FOUND_TEXT);
|
|
69
118
|
return row;
|
|
70
119
|
}
|
|
71
120
|
export function agentStatusOf(row) {
|
|
@@ -138,9 +187,34 @@ export function parseSkillRegistry(body) {
|
|
|
138
187
|
}
|
|
139
188
|
return { skills, channels };
|
|
140
189
|
}
|
|
190
|
+
/** The catalog title of every skill the registry lists that has one, for skillLabel. */
|
|
191
|
+
export function registryTitles(registry) {
|
|
192
|
+
return new Map([...registry.skills.values()].filter(skill => skill.title !== skill.name).map(skill => [skill.name, skill.title]));
|
|
193
|
+
}
|
|
141
194
|
export async function loadSkillRegistry(client, signal) {
|
|
142
195
|
return cachedRead(client, 'skills', async () => parseSkillRegistry(requireApiSuccess(await client.post('/Skills/List', {}, signal), 'Could not read the skill list.')));
|
|
143
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* The catalog titles of these skills, so a text names them as people see them. The catalog is read only when
|
|
199
|
+
* there is a name; the map is empty when there is none or the catalog cannot be read.
|
|
200
|
+
*/
|
|
201
|
+
export async function skillTitles(client, names, signal) {
|
|
202
|
+
const titles = new Map();
|
|
203
|
+
const wanted = new Set(names);
|
|
204
|
+
if (wanted.size === 0)
|
|
205
|
+
return titles;
|
|
206
|
+
const registry = await loadSkillRegistry(client, signal).catch(() => null);
|
|
207
|
+
for (const name of wanted) {
|
|
208
|
+
const title = registry?.skills.get(name)?.title;
|
|
209
|
+
if (title && title !== name)
|
|
210
|
+
titles.set(name, title);
|
|
211
|
+
}
|
|
212
|
+
return titles;
|
|
213
|
+
}
|
|
214
|
+
/** The catalog titles of the skills these scheduled tasks are paused for (skillTitles). */
|
|
215
|
+
export async function pausedSkillTitles(client, tasks, signal) {
|
|
216
|
+
return skillTitles(client, tasks.filter(task => task.skillOff).flatMap(task => (task.offSkills ?? []).flat()), signal);
|
|
217
|
+
}
|
|
144
218
|
function suggestSkills(name, registry) {
|
|
145
219
|
const tokens = new Set(name.toLowerCase().split(/[^a-z0-9]+/).filter(token => token.length > 2));
|
|
146
220
|
return [...registry.skills.keys()]
|
|
@@ -162,6 +236,8 @@ function suggestSkills(name, registry) {
|
|
|
162
236
|
export function planSkillChanges(input, registry, addDependencies) {
|
|
163
237
|
const errors = new Set();
|
|
164
238
|
const requested = {};
|
|
239
|
+
const titles = registryTitles(registry);
|
|
240
|
+
const label = (name) => skillLabel(name, titles);
|
|
165
241
|
for (const [rawName, enabled] of Object.entries(input)) {
|
|
166
242
|
const name = rawName.trim();
|
|
167
243
|
if (!name) {
|
|
@@ -180,11 +256,11 @@ export function planSkillChanges(input, registry, addDependencies) {
|
|
|
180
256
|
const skill = registry.skills.get(name);
|
|
181
257
|
if (!skill) {
|
|
182
258
|
const suggestions = suggestSkills(name, registry);
|
|
183
|
-
errors.add(`Unknown skill "${name}".${suggestions.length ? ` Did you mean ${suggestions.join(', ')}?` : ''} Use list_agent_skills for the full list.`);
|
|
259
|
+
errors.add(`Unknown skill "${name}".${suggestions.length ? ` Did you mean ${suggestions.map(label).join(', ')}?` : ''} Use list_agent_skills for the full list.`);
|
|
184
260
|
continue;
|
|
185
261
|
}
|
|
186
262
|
if (enabled && skill.deprecated) {
|
|
187
|
-
errors.add(`${name} is retired and cannot be turned on${skill.replacement ? `. Use ${skill.replacement} instead` : ''}.`);
|
|
263
|
+
errors.add(`${label(name)} is retired and cannot be turned on${skill.replacement ? `. Use ${label(skill.replacement)} instead` : ''}.`);
|
|
188
264
|
continue;
|
|
189
265
|
}
|
|
190
266
|
requested[name] = enabled;
|
|
@@ -196,7 +272,7 @@ export function planSkillChanges(input, registry, addDependencies) {
|
|
|
196
272
|
const name = queue.shift();
|
|
197
273
|
for (const dependency of registry.skills.get(name)?.dependsOn ?? []) {
|
|
198
274
|
if (requested[dependency] === false) {
|
|
199
|
-
errors.add(`${name} needs ${dependency}, which this change turns off. Keep ${dependency} on, or leave ${name} off.`);
|
|
275
|
+
errors.add(`${label(name)} needs ${label(dependency)}, which this change turns off. Keep ${label(dependency)} on, or leave ${label(name)} off.`);
|
|
200
276
|
continue;
|
|
201
277
|
}
|
|
202
278
|
if (requested[dependency] === true || !registry.skills.has(dependency))
|
|
@@ -255,16 +331,7 @@ export function checkSkillSet(enabledNow, changes, registry) {
|
|
|
255
331
|
}
|
|
256
332
|
/** Credential keys the newly enabled skills read, skipping the ones the agent already has. */
|
|
257
333
|
export function credentialsNeededFor(skills, registry, row) {
|
|
258
|
-
const
|
|
259
|
-
// An OAuth credential is ready only once connected: a saved shop domain or
|
|
260
|
-
// own-app key alone does not let the agent sign in.
|
|
261
|
-
const ready = (key) => {
|
|
262
|
-
const group = groups[key];
|
|
263
|
-
if (!isPlainObject(group))
|
|
264
|
-
return false;
|
|
265
|
-
const oauth = isPlainObject(group._schema) && isPlainObject(group._schema.oauth_provider);
|
|
266
|
-
return group._connected === true || (!oauth && group._filled === true);
|
|
267
|
-
};
|
|
334
|
+
const ready = (key) => (row ? credentialReady(row, key) : false);
|
|
268
335
|
return skills
|
|
269
336
|
.map(skill => ({
|
|
270
337
|
skill,
|
|
@@ -274,6 +341,73 @@ export function credentialsNeededFor(skills, registry, row) {
|
|
|
274
341
|
}))
|
|
275
342
|
.filter(entry => entry.credentials.length > 0);
|
|
276
343
|
}
|
|
344
|
+
/** The credential keys the given skills use, main and additional, by the same rule as credentialsNeededFor. */
|
|
345
|
+
export function credentialsUsedBySkills(skills, registry) {
|
|
346
|
+
const used = new Set();
|
|
347
|
+
for (const name of skills) {
|
|
348
|
+
const skill = registry.skills.get(name);
|
|
349
|
+
if (!skill?.requiresCredentials)
|
|
350
|
+
continue;
|
|
351
|
+
for (const key of skill.credentialKeys)
|
|
352
|
+
used.add(key);
|
|
353
|
+
}
|
|
354
|
+
return used;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Why connect_agent_oauth connect refuses a service on this agent, or null when
|
|
358
|
+
* it connects: one of the agent's active skills uses it, or one of its custom
|
|
359
|
+
* skills or scheduled tasks requires it (a skill's scheduled task only when it
|
|
360
|
+
* can use that credential now). Wiro keeps such an entry off until the
|
|
361
|
+
* credential is connected, and connecting turns it on, so the entry counts
|
|
362
|
+
* whatever its enabled says. A template agent's setup also waits for the
|
|
363
|
+
* credentials its template requires, whichever skills are on, so those stay
|
|
364
|
+
* connectable.
|
|
365
|
+
*/
|
|
366
|
+
export function unusedCredential(row, credential, registry) {
|
|
367
|
+
const used = credentialsUsedBySkills(enabledSkillNames(row), registry);
|
|
368
|
+
// A skill's scheduled task counts only the credentials it can use now; a task of the user's own, the ones its row lists.
|
|
369
|
+
for (const entry of customSkillEntries(row)) {
|
|
370
|
+
for (const key of entry.usableCredentials ?? entry.requiresCredentials)
|
|
371
|
+
used.add(key);
|
|
372
|
+
}
|
|
373
|
+
if (used.has(credential) || (!isCustomBuild(row) && credentialGroup(row, credential)?.optional === false))
|
|
374
|
+
return null;
|
|
375
|
+
return {
|
|
376
|
+
services: [...used].flatMap(key => {
|
|
377
|
+
const schema = credentialGroup(row, key)?._schema;
|
|
378
|
+
return isPlainObject(schema) && isPlainObject(schema.oauth_provider)
|
|
379
|
+
? [`${asString(schema.title) ?? wordsFromKey(key)} (\`${key}\`)`]
|
|
380
|
+
: [];
|
|
381
|
+
}),
|
|
382
|
+
skills: [...registry.skills.values()]
|
|
383
|
+
.filter(skill => !skill.deprecated && skill.requiresCredentials && skill.credentialKeys[0] === credential)
|
|
384
|
+
.map(skill => skillLabel(skill.name, registryTitles(registry))),
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
export function turnOnSkillText(title, credential, skills) {
|
|
388
|
+
return skills.length > 0
|
|
389
|
+
? `To use ${title}, first turn on ${skills.join(' or ')} with apply_agent_skills; list_agent_skills with name "${credential}" shows how it connects.`
|
|
390
|
+
: '';
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* The same rule for the answers that offer connect (status, and describing or
|
|
394
|
+
* saving a credential), as a note to show instead of that offer. Null when the
|
|
395
|
+
* service connects, and when the skills cannot be read: connect checks again.
|
|
396
|
+
*/
|
|
397
|
+
export async function unusedCredentialNote(client, row, credential, title, signal) {
|
|
398
|
+
try {
|
|
399
|
+
const unused = unusedCredential(row, credential, await loadSkillRegistry(client, signal));
|
|
400
|
+
if (!unused)
|
|
401
|
+
return null;
|
|
402
|
+
return [
|
|
403
|
+
`${title} is not used by any of this agent's active skills, custom skills or scheduled tasks, so connect_agent_oauth refuses it for now.`,
|
|
404
|
+
turnOnSkillText(title, credential, unused.skills),
|
|
405
|
+
].filter(Boolean).join(' ');
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
277
411
|
const IDEMPOTENCY_KEY_RE = /^mcp-[0-9a-f]{16}-([0-9a-f]{10})-([0-9a-f]{8})$/;
|
|
278
412
|
/** Fingerprint of one logical skill change, independent of the agent's current state. */
|
|
279
413
|
export function skillChangeDigest(agentGuid, requested, tier) {
|
|
@@ -286,10 +420,15 @@ export function skillChangeDigest(agentGuid, requested, tier) {
|
|
|
286
420
|
function cents(value) {
|
|
287
421
|
return value === null || !Number.isFinite(value) ? 'none' : String(Math.round(value * 100));
|
|
288
422
|
}
|
|
289
|
-
/**
|
|
290
|
-
|
|
423
|
+
/**
|
|
424
|
+
* Fingerprint of what a quoted charge rests on: the monthly price before and after
|
|
425
|
+
* a change and, for a plan paid from the wallet, the period it is charged in. A
|
|
426
|
+
* renewal keeps the prices and starts a new period, which changes the charge.
|
|
427
|
+
*/
|
|
428
|
+
export function skillPriceTag(currentUsd, newUsd, periodEndUnix = null) {
|
|
429
|
+
const prices = `${cents(currentUsd)}:${cents(newUsd)}`;
|
|
291
430
|
return crypto.createHash('sha256')
|
|
292
|
-
.update(`${
|
|
431
|
+
.update(periodEndUnix === null ? prices : `${prices}:${periodEndUnix}`)
|
|
293
432
|
.digest('hex')
|
|
294
433
|
.slice(0, 8);
|
|
295
434
|
}
|
|
@@ -314,26 +453,45 @@ export function planBilling(row) {
|
|
|
314
453
|
const wallet = asString(row.teamguid) ? 'team' : 'personal';
|
|
315
454
|
const subscription = isPlainObject(row.subscription) ? row.subscription : null;
|
|
316
455
|
if (!subscription) {
|
|
317
|
-
return { plan: 'none', status: null, periodEnd: null,
|
|
456
|
+
return { plan: 'none', status: null, periodEnd: null, wallet, cancelAtPeriodEnd: false, switchingToWallet: false };
|
|
318
457
|
}
|
|
458
|
+
const plan = asString(subscription.provider) === 'prepaid' ? 'prepaid' : 'card';
|
|
319
459
|
return {
|
|
320
|
-
plan
|
|
460
|
+
plan,
|
|
321
461
|
status: asString(subscription.status) ?? null,
|
|
322
462
|
periodEnd: toNumber(subscription.currentperiodend),
|
|
323
|
-
daysRemaining: toNumber(subscription.daysremaining),
|
|
324
463
|
wallet,
|
|
325
464
|
cancelAtPeriodEnd: planEndsWithPeriod(subscription, row.stripecancelatperiodend),
|
|
465
|
+
switchingToWallet: plan === 'card' && subscription.nextprovider === 'prepaid',
|
|
326
466
|
};
|
|
327
467
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
export function estimateProratedUsd(deltaUsd, periodEndUnix, nowMs = Date.now(), periodDays = 30) {
|
|
333
|
-
if (periodEndUnix === null || !Number.isFinite(deltaUsd))
|
|
468
|
+
const DAY_SECONDS = 86400;
|
|
469
|
+
/** The plan change of a pricing answer, or null when the agent has no active plan or Wiro sent none. */
|
|
470
|
+
export function readPlanChange(value) {
|
|
471
|
+
if (!isPlainObject(value))
|
|
334
472
|
return null;
|
|
335
|
-
|
|
336
|
-
|
|
473
|
+
return {
|
|
474
|
+
wallet: asString(value.provider) === 'prepaid',
|
|
475
|
+
periodEnd: toNumber(value.periodEnd),
|
|
476
|
+
secondsLeft: toNumber(value.secondsLeft),
|
|
477
|
+
creditsNow: toNumber(value.creditsNow),
|
|
478
|
+
creditsThisPeriod: toNumber(value.creditsThisPeriod),
|
|
479
|
+
creditsFromRenewal: toNumber(value.creditsFromRenewal),
|
|
480
|
+
chargeNowUsd: toNumber(value.chargeNowUsd),
|
|
481
|
+
creditNowUsd: toNumber(value.creditNowUsd),
|
|
482
|
+
usedCreditsUsd: toNumber(value.usedCreditsUsd),
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
/** Whether a rise now pays the one-day minimum: under a day is left of the period. */
|
|
486
|
+
export function underOneDayLeft(secondsLeft) {
|
|
487
|
+
return secondsLeft !== null && secondsLeft < DAY_SECONDS;
|
|
488
|
+
}
|
|
489
|
+
/** The time left of a period in whole days, at least 1, for a sentence such as "(27 days)". */
|
|
490
|
+
export function daysLeftText(secondsLeft) {
|
|
491
|
+
if (secondsLeft === null || secondsLeft < 0)
|
|
492
|
+
return null;
|
|
493
|
+
const days = Math.max(1, Math.round(secondsLeft / DAY_SECONDS));
|
|
494
|
+
return `${days} ${days === 1 ? 'day' : 'days'}`;
|
|
337
495
|
}
|
|
338
496
|
const SKILL_PAYMENT_FAILED = 'The payment for this change did not go through, so the change was undone and nothing was charged.';
|
|
339
497
|
const DEPOSIT_PAGE = 'https://wiro.ai/panel/wallet/deposit/credit-card (for a team project, select that team in the dashboard first)';
|
|
@@ -353,11 +511,11 @@ function skillPaymentGuidance(plan) {
|
|
|
353
511
|
* Turns a refused SkillsApply into an explanation and the step that fixes it.
|
|
354
512
|
* With the agent's plan, a payment failure names only the fix for that plan.
|
|
355
513
|
*/
|
|
356
|
-
export function describeSkillsApplyError(error, plan) {
|
|
514
|
+
export function describeSkillsApplyError(error, plan, titles) {
|
|
357
515
|
const message = error instanceof Error ? error.message : String(error);
|
|
358
516
|
const body = error instanceof ApiResultError ? error.body : {};
|
|
359
517
|
const codes = error instanceof ApiResultError ? error.codes.map(code => toNumber(code)) : [];
|
|
360
|
-
const code = codes.find(candidate => candidate !== null && candidate >= 95 && candidate <=
|
|
518
|
+
const code = codes.find(candidate => candidate !== null && candidate >= 95 && candidate <= 110) ?? null;
|
|
361
519
|
const base = { code, message, retrySameKey: false };
|
|
362
520
|
switch (code) {
|
|
363
521
|
case 101: {
|
|
@@ -367,7 +525,7 @@ export function describeSkillsApplyError(error, plan) {
|
|
|
367
525
|
return {
|
|
368
526
|
...base,
|
|
369
527
|
deps,
|
|
370
|
-
guidance: `Some skills need others that would be off: ${deps.map(dep => `${dep.skill} needs ${dep.requires}`).join('; ')}. Turn the needed skills on in the same change (add_dependencies does this for skills you turn on), or keep them on when you turn other skills off.`,
|
|
528
|
+
guidance: `Some skills need others that would be off: ${deps.map(dep => `${skillLabel(dep.skill, titles)} needs ${skillLabel(dep.requires, titles)}`).join('; ')}. Turn the needed skills on in the same change (add_dependencies does this for skills you turn on), or keep them on when you turn other skills off.`,
|
|
371
529
|
};
|
|
372
530
|
}
|
|
373
531
|
case 102: {
|
|
@@ -377,7 +535,7 @@ export function describeSkillsApplyError(error, plan) {
|
|
|
377
535
|
return {
|
|
378
536
|
...base,
|
|
379
537
|
conflicts,
|
|
380
|
-
guidance: `These skills cannot be on together: ${conflicts.map(pair => `${pair.a} and ${pair.b}`).join('; ')}. Keep only one of each pair.`,
|
|
538
|
+
guidance: `These skills cannot be on together: ${conflicts.map(pair => `${skillLabel(pair.a, titles)} and ${skillLabel(pair.b, titles)}`).join('; ')}. Keep only one of each pair.`,
|
|
381
539
|
};
|
|
382
540
|
}
|
|
383
541
|
case 103: {
|
|
@@ -391,10 +549,14 @@ export function describeSkillsApplyError(error, plan) {
|
|
|
391
549
|
};
|
|
392
550
|
}
|
|
393
551
|
case 104:
|
|
552
|
+
// Two refusals share the code: another change to the agent's plan, or another payment from its wallet.
|
|
553
|
+
// Skill changes, an upgrade to Pro and a wallet plan's renewal take turns on an agent.
|
|
394
554
|
return {
|
|
395
555
|
...base,
|
|
396
556
|
retrySameKey: true,
|
|
397
|
-
guidance:
|
|
557
|
+
guidance: /wallet/i.test(message)
|
|
558
|
+
? 'Another payment from the same wallet was going through, so this change was not made and nothing was charged. It is not a low balance, and no funds need to be added. Offer to try again in a minute: repeat the call with the same idempotency_key.'
|
|
559
|
+
: 'Another change to this agent\'s skills or plan, such as an upgrade or a renewal, is still going through, and one runs at a time. Nothing was changed or charged by this call. Wait a few seconds, then repeat the call with the same idempotency_key. If it keeps saying so, it clears within 15 minutes.',
|
|
398
560
|
};
|
|
399
561
|
case 105:
|
|
400
562
|
return {
|
|
@@ -409,19 +571,26 @@ export function describeSkillsApplyError(error, plan) {
|
|
|
409
571
|
case 107:
|
|
410
572
|
return {
|
|
411
573
|
...base,
|
|
412
|
-
guidance: '
|
|
574
|
+
guidance: 'This idempotency_key cannot be used for this call, and nothing was changed or charged by it. Get a new quote: repeat the call without idempotency_key, show the quote to the user, and confirm with the key it gives.',
|
|
413
575
|
};
|
|
414
576
|
case 108:
|
|
415
577
|
return {
|
|
416
578
|
...base,
|
|
417
579
|
retrySameKey: true,
|
|
418
|
-
guidance: 'Nothing was changed.
|
|
580
|
+
guidance: 'Wiro could not read or save what this change needs just now, and the wallet balance can be part of that. It is not a low balance, so no funds need to be added. Nothing was changed or charged. Try again: repeat the call with the same idempotency_key.',
|
|
419
581
|
};
|
|
420
582
|
case 109:
|
|
583
|
+
// Wiro's sentence covers cards and wallets alike. The guidance names the fix for this plan.
|
|
421
584
|
return {
|
|
422
585
|
...base,
|
|
586
|
+
message: 'Payment processing failed for this skill change.',
|
|
423
587
|
guidance: skillPaymentGuidance(plan),
|
|
424
588
|
};
|
|
589
|
+
case 110:
|
|
590
|
+
return {
|
|
591
|
+
...base,
|
|
592
|
+
guidance: 'Do not repeat this change. Check the agent\'s skills and monthly price with get_my_agent and the wallet with get_account (recent_transactions) before changing anything, and contact Wiro support if they are not what you expect.',
|
|
593
|
+
};
|
|
425
594
|
case 95:
|
|
426
595
|
case 96:
|
|
427
596
|
case 97:
|
|
@@ -438,17 +607,54 @@ export function describeSkillsApplyError(error, plan) {
|
|
|
438
607
|
guidance: 'The new price could not be computed, so nothing was changed. Check the skill names with list_agent_skills.',
|
|
439
608
|
};
|
|
440
609
|
}
|
|
610
|
+
if (/^unknown skill: /i.test(message)) {
|
|
611
|
+
return {
|
|
612
|
+
...base,
|
|
613
|
+
guidance: 'Nothing was changed or charged. Use names from list_agent_skills.',
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
if (isWalletNotRead(error))
|
|
617
|
+
return { ...base, guidance: WALLET_NOT_READ_NOTE };
|
|
441
618
|
return { ...base, guidance: workspaceHint(message) ?? '' };
|
|
442
619
|
}
|
|
620
|
+
/**
|
|
621
|
+
* One refusal covers an agent that does not exist and an agent this project may
|
|
622
|
+
* not connect, and the fix for each is different. The agent's own read tells
|
|
623
|
+
* them apart.
|
|
624
|
+
*/
|
|
625
|
+
export async function connectionRefusalText(client, agentGuid, message, done, signal) {
|
|
626
|
+
const rule = `can only be ${done} with a project created by the person who deployed the agent: `
|
|
627
|
+
+ 'connect this server with such a project, by its API key or a browser sign-in.';
|
|
628
|
+
try {
|
|
629
|
+
await readUserAgent(client, agentGuid, signal);
|
|
630
|
+
}
|
|
631
|
+
catch (error) {
|
|
632
|
+
if (isAgentNotFound(error))
|
|
633
|
+
return AGENT_NOT_FOUND_TEXT;
|
|
634
|
+
return `${message}\n\nCheck the guid with list_my_agents. If the agent is listed, integrations ${rule}`;
|
|
635
|
+
}
|
|
636
|
+
return `${message}\n\nIntegrations ${rule}`;
|
|
637
|
+
}
|
|
638
|
+
/** Wiro's answer when it could not clear a saved connection. Nothing says the connection is gone, and a repeat is safe. */
|
|
639
|
+
export const CONNECTION_NOT_CLEARED = 'The saved connection could not be cleared. Please try again.';
|
|
640
|
+
export const WORKSPACE_HINT = 'Connect with a project in the workspace (personal or team) that owns this agent, by its API key or a browser sign-in.';
|
|
443
641
|
/** The API names a dashboard concept ("team context"); here the workspace is the connected project's. */
|
|
444
642
|
export function workspaceHint(message) {
|
|
445
|
-
return /switch to the (team|personal)|different team|personal workspace/i.test(message)
|
|
446
|
-
? 'Connect with a project in the workspace (personal or team) that owns this agent, by its API key or a browser sign-in.'
|
|
447
|
-
: undefined;
|
|
643
|
+
return /switch to the (team|personal)|different team|personal workspace/i.test(message) ? WORKSPACE_HINT : undefined;
|
|
448
644
|
}
|
|
449
645
|
// A field's setup help is shown whole: its last steps, such as sharing a
|
|
450
646
|
// calendar with the service account, are part of the setup.
|
|
451
647
|
export const FIELD_HELP_MAX = 4_000;
|
|
648
|
+
// The Wiro project field. On the dashboard it is a project picker, and the help
|
|
649
|
+
// it comes with describes that picker, so this line is shown instead.
|
|
650
|
+
export const WIRO_PROJECT_NOTE = 'Connect a Wiro project with bind_agent_project: it takes the project of this connection or '
|
|
651
|
+
+ 'one from list_projects, and no key passes through this chat. The user can also pick the project on the agent\'s page in '
|
|
652
|
+
+ 'the Wiro dashboard. Saving a project API key with set_agent_credentials stays possible when the user gives one for this, '
|
|
653
|
+
+ 'and Wiro adds the project\'s secret by itself. Model runs the agent starts are billed to the wallet of the workspace '
|
|
654
|
+
+ 'that owns the project.';
|
|
655
|
+
export function isWiroProjectField(raw) {
|
|
656
|
+
return /^(custom:)?wiro-project-picker$/.test(asString(raw.type) ?? '');
|
|
657
|
+
}
|
|
452
658
|
export function htmlToText(value, maxLength = 400) {
|
|
453
659
|
const html = asString(value);
|
|
454
660
|
if (!html)
|
|
@@ -508,7 +714,7 @@ export function parseCredentialField(raw) {
|
|
|
508
714
|
...(toNumber(raw.maxsize) !== null ? { maxSizeKb: toNumber(raw.maxsize) } : {}),
|
|
509
715
|
...(asString(raw.item_type) ? { itemType: asString(raw.item_type) } : {}),
|
|
510
716
|
itemSchema: itemSchemaOf(raw).map(parseCredentialField).filter(field => field.key),
|
|
511
|
-
...(htmlToText(raw.help, FIELD_HELP_MAX) ? { help: htmlToText(raw.help, FIELD_HELP_MAX) } : {}),
|
|
717
|
+
...(!isWiroProjectField(raw) && htmlToText(raw.help, FIELD_HELP_MAX) ? { help: htmlToText(raw.help, FIELD_HELP_MAX) } : {}),
|
|
512
718
|
...(asString(raw.placeholder) ? { placeholder: asString(raw.placeholder) } : {}),
|
|
513
719
|
};
|
|
514
720
|
}
|
|
@@ -983,7 +1189,8 @@ export function prepareCredentialWrite(inputs, metas) {
|
|
|
983
1189
|
const mayClear = !field.required || meta.channel !== null;
|
|
984
1190
|
if (isEmptyValue(input.value)) {
|
|
985
1191
|
if (!mayClear) {
|
|
986
|
-
prepared.errors.push(`${field.label} is required and cannot be cleared
|
|
1192
|
+
prepared.errors.push(`${field.label} is required and cannot be cleared. To replace the saved value, send the new one. `
|
|
1193
|
+
+ 'To end the agent\'s access, revoke it with the service it belongs to.');
|
|
987
1194
|
continue;
|
|
988
1195
|
}
|
|
989
1196
|
const cleared = shape === 'string-array' || shape === 'object-array' ? [] : '';
|
|
@@ -1080,29 +1287,73 @@ export function isFilledValue(value) {
|
|
|
1080
1287
|
return Object.values(value).some(isFilledValue);
|
|
1081
1288
|
return true;
|
|
1082
1289
|
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Whether a field of a saved credential holds what someone entered, counted the
|
|
1292
|
+
* way Wiro counts it for a card's filled state: a list of options counts only an
|
|
1293
|
+
* option that is selected, and a row of a list does not count for its selects
|
|
1294
|
+
* and switches, which come with a default.
|
|
1295
|
+
*/
|
|
1296
|
+
export function isCredentialFieldFilled(field, value) {
|
|
1297
|
+
if (field.itemSchema.length === 0 || !Array.isArray(value))
|
|
1298
|
+
return isCredentialValueFilled(value);
|
|
1299
|
+
return value.some(row => {
|
|
1300
|
+
if (!isPlainObject(row))
|
|
1301
|
+
return isCredentialValueFilled(row);
|
|
1302
|
+
return Object.entries(row).some(([key, item]) => {
|
|
1303
|
+
const inner = field.itemSchema.find(entry => entry.key === key);
|
|
1304
|
+
if (!inner)
|
|
1305
|
+
return isCredentialValueFilled(item);
|
|
1306
|
+
if (inner.type === 'select' || inner.type === 'boolean')
|
|
1307
|
+
return false;
|
|
1308
|
+
return isCredentialFieldFilled(inner, item);
|
|
1309
|
+
});
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
function isCredentialValueFilled(value) {
|
|
1313
|
+
if (Array.isArray(value) && value.length > 0 && value.every(option => isPlainObject(option) && 'selected' in option)) {
|
|
1314
|
+
return value.some(option => isPlainObject(option) && option.selected === true);
|
|
1315
|
+
}
|
|
1316
|
+
if (Array.isArray(value))
|
|
1317
|
+
return value.some(isCredentialValueFilled);
|
|
1318
|
+
if (isPlainObject(value))
|
|
1319
|
+
return Object.values(value).some(isCredentialValueFilled);
|
|
1320
|
+
return isFilledValue(value);
|
|
1321
|
+
}
|
|
1083
1322
|
export function credentialGroup(row, key) {
|
|
1084
1323
|
const groups = isPlainObject(row.credentials) ? row.credentials : null;
|
|
1085
1324
|
const group = groups ? groups[key] : undefined;
|
|
1086
1325
|
return isPlainObject(group) ? group : null;
|
|
1087
1326
|
}
|
|
1327
|
+
/**
|
|
1328
|
+
* Whether the agent can use a credential now. An OAuth credential is ready only once connected: a saved shop
|
|
1329
|
+
* domain or own-app key alone does not let the agent sign in.
|
|
1330
|
+
*/
|
|
1331
|
+
export function credentialReady(row, key) {
|
|
1332
|
+
const group = credentialGroup(row, key);
|
|
1333
|
+
if (!group)
|
|
1334
|
+
return false;
|
|
1335
|
+
const oauth = isPlainObject(group._schema) && isPlainObject(group._schema.oauth_provider);
|
|
1336
|
+
return group._connected === true || (!oauth && group._filled === true);
|
|
1337
|
+
}
|
|
1088
1338
|
export function credentialStatus(row, key, meta) {
|
|
1089
1339
|
const group = credentialGroup(row, key);
|
|
1090
1340
|
if (!group)
|
|
1091
1341
|
return null;
|
|
1092
1342
|
const schema = isPlainObject(group._schema) ? group._schema : {};
|
|
1093
1343
|
const connected = group._connected === true;
|
|
1094
|
-
// A sign-in credential's required fields depend on its mode; connected is the signal there.
|
|
1095
|
-
|
|
1344
|
+
// A sign-in credential's required fields depend on its mode; connected is the signal there. Filled says every
|
|
1345
|
+
// required field holds a value, a template's own value that is not shown included.
|
|
1346
|
+
const missingFields = connected || !meta || meta.oauth || group._filled === true
|
|
1096
1347
|
? []
|
|
1097
1348
|
: meta.fields
|
|
1098
|
-
.filter(field => field.required && isWritableField(field) && !
|
|
1349
|
+
.filter(field => field.required && isWritableField(field) && !isCredentialFieldFilled(field, group[field.key]))
|
|
1099
1350
|
.map(field => field.label);
|
|
1100
1351
|
return {
|
|
1101
1352
|
key,
|
|
1102
1353
|
title: asString(schema.title) ?? meta?.title ?? key,
|
|
1103
1354
|
connected,
|
|
1104
1355
|
filled: group._filled === true,
|
|
1105
|
-
requiredMissing:
|
|
1356
|
+
requiredMissing: credentialKeysSetupWaitsFor(row).has(key),
|
|
1106
1357
|
optional: group.optional === true,
|
|
1107
1358
|
missingFields,
|
|
1108
1359
|
};
|
|
@@ -1158,6 +1409,17 @@ export function isAllowedRedirectUrl(url, agentGuid) {
|
|
|
1158
1409
|
export function ownAppRedirectUri(meta) {
|
|
1159
1410
|
return meta.oauth?.callbackPath ? `${OAUTH_CALLBACK_BASE}${meta.oauth.callbackPath}` : undefined;
|
|
1160
1411
|
}
|
|
1412
|
+
/** Goes with a list of missing fields that holds app keys, which may be saved already. */
|
|
1413
|
+
export function writeOnlyNote(mode) {
|
|
1414
|
+
return `App keys are not shown once saved. If they are saved already, connect with mode ${mode}.`;
|
|
1415
|
+
}
|
|
1416
|
+
/** The same note before a mode is chosen: it names each usable mode that takes app keys. */
|
|
1417
|
+
export function writeOnlyModesNote(requirements) {
|
|
1418
|
+
const modes = requirements
|
|
1419
|
+
.filter(requirement => requirement.available && requirement.fields.some(field => field.writeOnly))
|
|
1420
|
+
.map(requirement => requirement.mode);
|
|
1421
|
+
return modes.length > 0 ? writeOnlyNote(modes.join(' or ')) : null;
|
|
1422
|
+
}
|
|
1161
1423
|
function fieldsForMode(meta, mode) {
|
|
1162
1424
|
if (mode === 'wiro')
|
|
1163
1425
|
return [];
|
|
@@ -1182,11 +1444,15 @@ function fieldsForMode(meta, mode) {
|
|
|
1182
1444
|
export function connectionModeRequirements(meta, group) {
|
|
1183
1445
|
const modes = meta.connectionModes.length > 0 ? meta.connectionModes : ['wiro'];
|
|
1184
1446
|
return modes.map(mode => {
|
|
1185
|
-
const fields = fieldsForMode(meta, mode).map(field =>
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1447
|
+
const fields = fieldsForMode(meta, mode).map(field => {
|
|
1448
|
+
const isSet = group !== null && isFilledValue(group[field.key]);
|
|
1449
|
+
return {
|
|
1450
|
+
field: field.key,
|
|
1451
|
+
label: field.label,
|
|
1452
|
+
isSet,
|
|
1453
|
+
...(field.oauthManaged && !isSet ? { writeOnly: true } : {}),
|
|
1454
|
+
};
|
|
1455
|
+
});
|
|
1190
1456
|
if (mode === 'wiro') {
|
|
1191
1457
|
return {
|
|
1192
1458
|
mode,
|
|
@@ -1248,6 +1514,16 @@ export function chooseConnectionMode(meta, requirements) {
|
|
|
1248
1514
|
});
|
|
1249
1515
|
return ordered.find(requirement => requirement.fields.every(field => field.isSet))?.mode ?? null;
|
|
1250
1516
|
}
|
|
1517
|
+
/**
|
|
1518
|
+
* An account in a line of this server's text. The outside service sets its name,
|
|
1519
|
+
* and a service without an account choice gives that name as the id too, so both
|
|
1520
|
+
* are quoted and a repeat is left out.
|
|
1521
|
+
*/
|
|
1522
|
+
export function accountLabel(account) {
|
|
1523
|
+
const name = oneLine(account.name);
|
|
1524
|
+
const id = quotedSpan(account.id);
|
|
1525
|
+
return name && name !== oneLine(account.id) ? `${quotedSpan(name)} (${id})` : id;
|
|
1526
|
+
}
|
|
1251
1527
|
/** Candidate or saved accounts in one shape, whatever the provider calls the fields. */
|
|
1252
1528
|
export function toAccountRefs(list, picker) {
|
|
1253
1529
|
if (!Array.isArray(list))
|
|
@@ -1342,7 +1618,7 @@ export function buildChainedSelections(next, groups, selections) {
|
|
|
1342
1618
|
if (group.errorCode) {
|
|
1343
1619
|
return {
|
|
1344
1620
|
skippedParents: [],
|
|
1345
|
-
error: `${group.parent
|
|
1621
|
+
error: `${accountLabel(group.parent)} could not be read (${group.errorCode}), so nothing can be chosen for it. Leave it out.`,
|
|
1346
1622
|
};
|
|
1347
1623
|
}
|
|
1348
1624
|
const ids = chosen.get(group.parent.id) ?? [];
|
|
@@ -1351,7 +1627,7 @@ export function buildChainedSelections(next, groups, selections) {
|
|
|
1351
1627
|
if (!item) {
|
|
1352
1628
|
return {
|
|
1353
1629
|
skippedParents: [],
|
|
1354
|
-
error: `"${itemId}" is not available under ${group.parent
|
|
1630
|
+
error: `"${itemId}" is not available under ${accountLabel(group.parent)}. Available: ${group.items.map(accountLabel).join(', ') || 'none'}.`,
|
|
1355
1631
|
};
|
|
1356
1632
|
}
|
|
1357
1633
|
if (!ids.includes(item.id))
|
|
@@ -1423,6 +1699,8 @@ export function connectionState(status, options) {
|
|
|
1423
1699
|
return 'pick_accounts';
|
|
1424
1700
|
return 'not_connected';
|
|
1425
1701
|
}
|
|
1702
|
+
/** The entry that holds what an agent posts, sends or changes by itself until the user says yes. */
|
|
1703
|
+
export const APPROVAL_POLICY_KEY = 'cs-approval-policy';
|
|
1426
1704
|
const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1427
1705
|
/**
|
|
1428
1706
|
* Same rules as the API's key normalisation: cs-cron-x and cs-x stay, cron-x
|
|
@@ -1523,7 +1801,7 @@ export function resolveExistingCustomSkillKey(input, existingKeys, kind) {
|
|
|
1523
1801
|
return { key: matches[0] };
|
|
1524
1802
|
if (matches.length > 1)
|
|
1525
1803
|
return { error: `Both ${asSkill} and ${asTask} exist. Pass the full key.` };
|
|
1526
|
-
return { error: `This agent has no custom skill or scheduled task named "${input}". Pass the full key (cs-... or cs-cron-...)
|
|
1804
|
+
return { error: `This agent has no custom skill or scheduled task named "${input}". Pass the full key (cs-... or cs-cron-...). get_my_agent without entry lists its keys.` };
|
|
1527
1805
|
}
|
|
1528
1806
|
/** A light shape check; the API validates the expression fully. */
|
|
1529
1807
|
export function looksLikeCron(expression) {
|
|
@@ -1619,12 +1897,9 @@ export function humanizeCustomSkillKey(key) {
|
|
|
1619
1897
|
body = body.replace(/[-_]+/g, ' ').replace(/\s+/g, ' ').trim();
|
|
1620
1898
|
return body ? body.charAt(0).toUpperCase() + body.slice(1) : '';
|
|
1621
1899
|
}
|
|
1622
|
-
/**
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
while (value.includes(fence))
|
|
1626
|
-
fence += '`';
|
|
1627
|
-
return [`${fence}text`, value, fence];
|
|
1900
|
+
/** Wiro's _if_connected: each credential key with the lists of skills a task would still wait for. */
|
|
1901
|
+
function skillGroupsByKey(value) {
|
|
1902
|
+
return Object.fromEntries(Object.entries(value).filter(([, groups]) => Array.isArray(groups)).map(([key, groups]) => [key, toSkillGroups(groups)]));
|
|
1628
1903
|
}
|
|
1629
1904
|
export function customSkillEntries(row) {
|
|
1630
1905
|
const rows = [
|
|
@@ -1644,9 +1919,90 @@ export function customSkillEntries(row) {
|
|
|
1644
1919
|
source,
|
|
1645
1920
|
userCreated: source === 'user-created' || entry._user_created === true,
|
|
1646
1921
|
requiresCredentials: stringList(entry.requires_credentials),
|
|
1922
|
+
notWritten: key === STARTER_TASK_KEY && entry._edited === false,
|
|
1923
|
+
ownerSkills: stringList(entry._owner_skills),
|
|
1924
|
+
skillOff: entry._skill_off === true,
|
|
1925
|
+
offSkills: toSkillGroups(entry._off_skills),
|
|
1926
|
+
...(Array.isArray(entry._usable_credentials) ? { usableCredentials: stringList(entry._usable_credentials) } : {}),
|
|
1927
|
+
...(isPlainObject(entry._if_connected) ? { ifConnected: skillGroupsByKey(entry._if_connected) } : {}),
|
|
1647
1928
|
};
|
|
1648
1929
|
}).filter(entry => entry.key);
|
|
1649
1930
|
}
|
|
1931
|
+
/**
|
|
1932
|
+
* What connecting a credential does to the entries that need it. Saving it turns on an entry that is off only
|
|
1933
|
+
* when the entry's own list names it. For a skill's scheduled task Wiro says what it would still wait for once
|
|
1934
|
+
* that credential is connected: such a task turns on and runs (on), or turns on and stays paused while those
|
|
1935
|
+
* skills are off (paused). A scheduled task that is on already starts running when only this connection held
|
|
1936
|
+
* it back (starts), or stays paused while those skills are off (waiting).
|
|
1937
|
+
*/
|
|
1938
|
+
export function entriesTurnedOnBy(row, credential) {
|
|
1939
|
+
const on = [];
|
|
1940
|
+
const paused = [];
|
|
1941
|
+
const starts = [];
|
|
1942
|
+
const waiting = [];
|
|
1943
|
+
for (const entry of customSkillEntries(row)) {
|
|
1944
|
+
const groups = entry.ifConnected?.[credential];
|
|
1945
|
+
if (entry.enabled) {
|
|
1946
|
+
if (groups && groups.length > 0)
|
|
1947
|
+
waiting.push({ key: entry.key, groups });
|
|
1948
|
+
else if (startsOnConnect(row, entry, credential))
|
|
1949
|
+
starts.push(entry);
|
|
1950
|
+
continue;
|
|
1951
|
+
}
|
|
1952
|
+
if (!entry.requiresCredentials.includes(credential))
|
|
1953
|
+
continue;
|
|
1954
|
+
if (!entry.ifConnected || (groups !== undefined && groups.length === 0))
|
|
1955
|
+
on.push(entry);
|
|
1956
|
+
else if (groups !== undefined)
|
|
1957
|
+
paused.push({ key: entry.key, groups });
|
|
1958
|
+
}
|
|
1959
|
+
return { on, paused, starts, waiting };
|
|
1960
|
+
}
|
|
1961
|
+
/**
|
|
1962
|
+
* Whether a scheduled task that is on runs once this credential is connected, and not before. A skill's task
|
|
1963
|
+
* runs then when Wiro says it needs nothing more ([]), and does not run now while it is paused for skills or
|
|
1964
|
+
* none of the connections it can use is ready. A task with no skill gate runs while any credential its list
|
|
1965
|
+
* names is ready. The starter task does not run until its text is written.
|
|
1966
|
+
*/
|
|
1967
|
+
function startsOnConnect(row, entry, credential) {
|
|
1968
|
+
if (entry.kind !== 'scheduled_task' || entry.notWritten)
|
|
1969
|
+
return false;
|
|
1970
|
+
if (entry.ifConnected) {
|
|
1971
|
+
const usable = entry.usableCredentials ?? [];
|
|
1972
|
+
return entry.ifConnected[credential]?.length === 0
|
|
1973
|
+
&& (entry.skillOff === true || (usable.length > 0 && !usable.some(key => credentialReady(row, key))));
|
|
1974
|
+
}
|
|
1975
|
+
return entry.requiresCredentials.includes(credential) && !entry.requiresCredentials.some(key => credentialReady(row, key));
|
|
1976
|
+
}
|
|
1977
|
+
/**
|
|
1978
|
+
* The connection a scheduled task needs, as a sentence: the credentials it can use (a skill's task) or the
|
|
1979
|
+
* ones its row lists (a task of the user's own), by title where the agent's credential cards have one.
|
|
1980
|
+
*/
|
|
1981
|
+
export function connectionNeededText(row, entry) {
|
|
1982
|
+
const keys = entry.usableCredentials ?? entry.requiresCredentials;
|
|
1983
|
+
if (keys.length === 0)
|
|
1984
|
+
return '';
|
|
1985
|
+
const labels = keys.map(key => credentialLabel(row, key));
|
|
1986
|
+
return `It only runs while ${labels.length > 1 ? 'one of ' : ''}${nameList(labels, 'or')} is connected.`;
|
|
1987
|
+
}
|
|
1988
|
+
/**
|
|
1989
|
+
* A credential as people see it, with its key: Google Ads (`google-ads`). Its title comes from the agent's card
|
|
1990
|
+
* for it, or from titles (the credential catalog), or is made from the key.
|
|
1991
|
+
*/
|
|
1992
|
+
export function credentialLabel(row, key, titles) {
|
|
1993
|
+
const schema = credentialGroup(row, key)?._schema;
|
|
1994
|
+
const title = isPlainObject(schema) ? asString(schema.title) : undefined;
|
|
1995
|
+
return `${title ?? titles?.get(key) ?? wordsFromKey(key)} (\`${key}\`)`;
|
|
1996
|
+
}
|
|
1997
|
+
/** The catalog titles of these credentials, read through the cached credential details; a key it cannot read is left out. */
|
|
1998
|
+
export async function credentialTitles(client, keys, signal) {
|
|
1999
|
+
const unique = [...new Set(keys)];
|
|
2000
|
+
const metas = await Promise.all(unique.map(key => loadCredentialMeta(client, key, signal).catch(() => null)));
|
|
2001
|
+
return new Map(unique.flatMap((key, index) => {
|
|
2002
|
+
const title = metas[index]?.title;
|
|
2003
|
+
return title && title !== key ? [[key, title]] : [];
|
|
2004
|
+
}));
|
|
2005
|
+
}
|
|
1650
2006
|
/** Readable text for the API's custom skill error codes. */
|
|
1651
2007
|
export function customSkillErrorMessage(message) {
|
|
1652
2008
|
if (/invalid-cron-expression|invalid cron/i.test(message)) {
|