@wiro-ai/wiro-mcp 1.4.1 → 1.5.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.
Files changed (226) hide show
  1. package/README.md +38 -15
  2. package/dist/client.d.ts +19 -6
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +44 -27
  5. package/dist/client.js.map +1 -1
  6. package/dist/instructions.d.ts.map +1 -1
  7. package/dist/instructions.js +7 -4
  8. package/dist/instructions.js.map +1 -1
  9. package/dist/playbooks.d.ts.map +1 -1
  10. package/dist/playbooks.js +45 -34
  11. package/dist/playbooks.js.map +1 -1
  12. package/dist/prompts.d.ts.map +1 -1
  13. package/dist/prompts.js +3 -2
  14. package/dist/prompts.js.map +1 -1
  15. package/dist/tools/apply-agent-skills.d.ts.map +1 -1
  16. package/dist/tools/apply-agent-skills.js +343 -87
  17. package/dist/tools/apply-agent-skills.js.map +1 -1
  18. package/dist/tools/bind-agent-project.d.ts +4 -0
  19. package/dist/tools/bind-agent-project.d.ts.map +1 -0
  20. package/dist/tools/bind-agent-project.js +403 -0
  21. package/dist/tools/bind-agent-project.js.map +1 -0
  22. package/dist/tools/cancel-agent-message.d.ts.map +1 -1
  23. package/dist/tools/cancel-agent-message.js +6 -3
  24. package/dist/tools/cancel-agent-message.js.map +1 -1
  25. package/dist/tools/cancel-task.js +3 -3
  26. package/dist/tools/cancel-task.js.map +1 -1
  27. package/dist/tools/clone-workflow.d.ts.map +1 -1
  28. package/dist/tools/clone-workflow.js +5 -4
  29. package/dist/tools/clone-workflow.js.map +1 -1
  30. package/dist/tools/connect-agent-oauth.d.ts.map +1 -1
  31. package/dist/tools/connect-agent-oauth.js +140 -40
  32. package/dist/tools/connect-agent-oauth.js.map +1 -1
  33. package/dist/tools/control-agent.d.ts.map +1 -1
  34. package/dist/tools/control-agent.js +109 -36
  35. package/dist/tools/control-agent.js.map +1 -1
  36. package/dist/tools/create-workflow.d.ts +3 -0
  37. package/dist/tools/create-workflow.d.ts.map +1 -1
  38. package/dist/tools/create-workflow.js +12 -11
  39. package/dist/tools/create-workflow.js.map +1 -1
  40. package/dist/tools/delete-agent-session.d.ts.map +1 -1
  41. package/dist/tools/delete-agent-session.js +18 -12
  42. package/dist/tools/delete-agent-session.js.map +1 -1
  43. package/dist/tools/delete-agent.d.ts.map +1 -1
  44. package/dist/tools/delete-agent.js +12 -9
  45. package/dist/tools/delete-agent.js.map +1 -1
  46. package/dist/tools/delete-workflow.d.ts.map +1 -1
  47. package/dist/tools/delete-workflow.js +5 -4
  48. package/dist/tools/delete-workflow.js.map +1 -1
  49. package/dist/tools/deploy-agent.d.ts.map +1 -1
  50. package/dist/tools/deploy-agent.js +200 -40
  51. package/dist/tools/deploy-agent.js.map +1 -1
  52. package/dist/tools/disconnect-agent-oauth.d.ts.map +1 -1
  53. package/dist/tools/disconnect-agent-oauth.js +32 -12
  54. package/dist/tools/disconnect-agent-oauth.js.map +1 -1
  55. package/dist/tools/get-account.d.ts.map +1 -1
  56. package/dist/tools/get-account.js +148 -42
  57. package/dist/tools/get-account.js.map +1 -1
  58. package/dist/tools/get-agent-activity.d.ts.map +1 -1
  59. package/dist/tools/get-agent-activity.js +185 -35
  60. package/dist/tools/get-agent-activity.js.map +1 -1
  61. package/dist/tools/get-agent-conversation.d.ts.map +1 -1
  62. package/dist/tools/get-agent-conversation.js +57 -35
  63. package/dist/tools/get-agent-conversation.js.map +1 -1
  64. package/dist/tools/get-agent-message.d.ts.map +1 -1
  65. package/dist/tools/get-agent-message.js +6 -4
  66. package/dist/tools/get-agent-message.js.map +1 -1
  67. package/dist/tools/get-agent-template.d.ts.map +1 -1
  68. package/dist/tools/get-agent-template.js +11 -16
  69. package/dist/tools/get-agent-template.js.map +1 -1
  70. package/dist/tools/get-model-schema.js +3 -3
  71. package/dist/tools/get-model-schema.js.map +1 -1
  72. package/dist/tools/get-my-agent.d.ts.map +1 -1
  73. package/dist/tools/get-my-agent.js +11 -7
  74. package/dist/tools/get-my-agent.js.map +1 -1
  75. package/dist/tools/get-playbook.d.ts.map +1 -1
  76. package/dist/tools/get-playbook.js +3 -2
  77. package/dist/tools/get-playbook.js.map +1 -1
  78. package/dist/tools/get-task-price.js +3 -3
  79. package/dist/tools/get-task-price.js.map +1 -1
  80. package/dist/tools/get-task.js +3 -3
  81. package/dist/tools/get-task.js.map +1 -1
  82. package/dist/tools/get-workflow.d.ts.map +1 -1
  83. package/dist/tools/get-workflow.js +38 -19
  84. package/dist/tools/get-workflow.js.map +1 -1
  85. package/dist/tools/index.d.ts +3 -1
  86. package/dist/tools/index.d.ts.map +1 -1
  87. package/dist/tools/index.js +6 -1
  88. package/dist/tools/index.js.map +1 -1
  89. package/dist/tools/kill-task.js +3 -3
  90. package/dist/tools/kill-task.js.map +1 -1
  91. package/dist/tools/list-agent-skills.d.ts.map +1 -1
  92. package/dist/tools/list-agent-skills.js +5 -4
  93. package/dist/tools/list-agent-skills.js.map +1 -1
  94. package/dist/tools/list-agent-templates.js +4 -4
  95. package/dist/tools/list-agent-templates.js.map +1 -1
  96. package/dist/tools/list-my-agents.d.ts.map +1 -1
  97. package/dist/tools/list-my-agents.js +11 -8
  98. package/dist/tools/list-my-agents.js.map +1 -1
  99. package/dist/tools/list-projects.d.ts +4 -0
  100. package/dist/tools/list-projects.d.ts.map +1 -0
  101. package/dist/tools/list-projects.js +141 -0
  102. package/dist/tools/list-projects.js.map +1 -0
  103. package/dist/tools/list-studio-results.js +3 -3
  104. package/dist/tools/list-studio-results.js.map +1 -1
  105. package/dist/tools/list-tasks.js +3 -3
  106. package/dist/tools/list-tasks.js.map +1 -1
  107. package/dist/tools/list-workflow-runs.d.ts.map +1 -1
  108. package/dist/tools/list-workflow-runs.js +6 -4
  109. package/dist/tools/list-workflow-runs.js.map +1 -1
  110. package/dist/tools/list-workflows.d.ts.map +1 -1
  111. package/dist/tools/list-workflows.js +7 -6
  112. package/dist/tools/list-workflows.js.map +1 -1
  113. package/dist/tools/manage-agent-billing.d.ts.map +1 -1
  114. package/dist/tools/manage-agent-billing.js +207 -59
  115. package/dist/tools/manage-agent-billing.js.map +1 -1
  116. package/dist/tools/manage-agent-custom-skill.d.ts.map +1 -1
  117. package/dist/tools/manage-agent-custom-skill.js +88 -12
  118. package/dist/tools/manage-agent-custom-skill.js.map +1 -1
  119. package/dist/tools/recommend-model.js +3 -3
  120. package/dist/tools/recommend-model.js.map +1 -1
  121. package/dist/tools/resume-workflow-run.d.ts.map +1 -1
  122. package/dist/tools/resume-workflow-run.js +3 -2
  123. package/dist/tools/resume-workflow-run.js.map +1 -1
  124. package/dist/tools/run-model.js +3 -3
  125. package/dist/tools/run-model.js.map +1 -1
  126. package/dist/tools/run-workflow.d.ts.map +1 -1
  127. package/dist/tools/run-workflow.js +3 -2
  128. package/dist/tools/run-workflow.js.map +1 -1
  129. package/dist/tools/save-workflow-from-agent.d.ts.map +1 -1
  130. package/dist/tools/save-workflow-from-agent.js +17 -12
  131. package/dist/tools/save-workflow-from-agent.js.map +1 -1
  132. package/dist/tools/schemas.d.ts +32 -26
  133. package/dist/tools/schemas.d.ts.map +1 -1
  134. package/dist/tools/schemas.js +67 -0
  135. package/dist/tools/schemas.js.map +1 -1
  136. package/dist/tools/search-docs.js +3 -3
  137. package/dist/tools/search-docs.js.map +1 -1
  138. package/dist/tools/search-models.js +3 -3
  139. package/dist/tools/search-models.js.map +1 -1
  140. package/dist/tools/send-agent-message.d.ts.map +1 -1
  141. package/dist/tools/send-agent-message.js +30 -9
  142. package/dist/tools/send-agent-message.js.map +1 -1
  143. package/dist/tools/set-agent-credentials.d.ts.map +1 -1
  144. package/dist/tools/set-agent-credentials.js +47 -57
  145. package/dist/tools/set-agent-credentials.js.map +1 -1
  146. package/dist/tools/update-agent.d.ts.map +1 -1
  147. package/dist/tools/update-agent.js +56 -19
  148. package/dist/tools/update-agent.js.map +1 -1
  149. package/dist/tools/update-workflow.d.ts +199 -0
  150. package/dist/tools/update-workflow.d.ts.map +1 -1
  151. package/dist/tools/update-workflow.js +20 -7
  152. package/dist/tools/update-workflow.js.map +1 -1
  153. package/dist/tools/upload-file.js +3 -3
  154. package/dist/tools/upload-file.js.map +1 -1
  155. package/dist/tools/wait-for-task.js +3 -3
  156. package/dist/tools/wait-for-task.js.map +1 -1
  157. package/dist/tools/wait-for-workflow-run.d.ts +3 -3
  158. package/dist/tools/wait-for-workflow-run.d.ts.map +1 -1
  159. package/dist/tools/wait-for-workflow-run.js +7 -6
  160. package/dist/tools/wait-for-workflow-run.js.map +1 -1
  161. package/dist/utils/agent-catalog.d.ts +29 -0
  162. package/dist/utils/agent-catalog.d.ts.map +1 -1
  163. package/dist/utils/agent-catalog.js +28 -7
  164. package/dist/utils/agent-catalog.js.map +1 -1
  165. package/dist/utils/agent-config.d.ts +109 -8
  166. package/dist/utils/agent-config.d.ts.map +1 -1
  167. package/dist/utils/agent-config.js +286 -45
  168. package/dist/utils/agent-config.js.map +1 -1
  169. package/dist/utils/agent-message.d.ts +36 -14
  170. package/dist/utils/agent-message.d.ts.map +1 -1
  171. package/dist/utils/agent-message.js +92 -26
  172. package/dist/utils/agent-message.js.map +1 -1
  173. package/dist/utils/agent-targets.d.ts +7 -3
  174. package/dist/utils/agent-targets.d.ts.map +1 -1
  175. package/dist/utils/agent-targets.js +35 -23
  176. package/dist/utils/agent-targets.js.map +1 -1
  177. package/dist/utils/agent-turns.d.ts +19 -12
  178. package/dist/utils/agent-turns.d.ts.map +1 -1
  179. package/dist/utils/agent-turns.js +97 -15
  180. package/dist/utils/agent-turns.js.map +1 -1
  181. package/dist/utils/api.d.ts +6 -0
  182. package/dist/utils/api.d.ts.map +1 -1
  183. package/dist/utils/api.js +22 -1
  184. package/dist/utils/api.js.map +1 -1
  185. package/dist/utils/format.d.ts.map +1 -1
  186. package/dist/utils/format.js +7 -13
  187. package/dist/utils/format.js.map +1 -1
  188. package/dist/utils/json-schema-dialect.d.ts.map +1 -1
  189. package/dist/utils/json-schema-dialect.js +4 -0
  190. package/dist/utils/json-schema-dialect.js.map +1 -1
  191. package/dist/utils/media.d.ts +8 -0
  192. package/dist/utils/media.d.ts.map +1 -1
  193. package/dist/utils/media.js +29 -0
  194. package/dist/utils/media.js.map +1 -1
  195. package/dist/utils/poll.d.ts +5 -2
  196. package/dist/utils/poll.d.ts.map +1 -1
  197. package/dist/utils/poll.js +10 -9
  198. package/dist/utils/poll.js.map +1 -1
  199. package/dist/utils/quoted-text.d.ts +28 -0
  200. package/dist/utils/quoted-text.d.ts.map +1 -0
  201. package/dist/utils/quoted-text.js +64 -0
  202. package/dist/utils/quoted-text.js.map +1 -0
  203. package/dist/utils/studio-runs.d.ts.map +1 -1
  204. package/dist/utils/studio-runs.js +7 -4
  205. package/dist/utils/studio-runs.js.map +1 -1
  206. package/dist/utils/user-agent.d.ts +101 -25
  207. package/dist/utils/user-agent.d.ts.map +1 -1
  208. package/dist/utils/user-agent.js +160 -42
  209. package/dist/utils/user-agent.js.map +1 -1
  210. package/dist/utils/wiro-project.d.ts +45 -0
  211. package/dist/utils/wiro-project.d.ts.map +1 -0
  212. package/dist/utils/wiro-project.js +66 -0
  213. package/dist/utils/wiro-project.js.map +1 -0
  214. package/dist/utils/workflow-save.d.ts +22 -1
  215. package/dist/utils/workflow-save.d.ts.map +1 -1
  216. package/dist/utils/workflow-save.js +122 -24
  217. package/dist/utils/workflow-save.js.map +1 -1
  218. package/dist/utils/workflow.d.ts +26 -13
  219. package/dist/utils/workflow.d.ts.map +1 -1
  220. package/dist/utils/workflow.js +51 -20
  221. package/dist/utils/workflow.js.map +1 -1
  222. package/dist/utils/workspace.d.ts +30 -4
  223. package/dist/utils/workspace.d.ts.map +1 -1
  224. package/dist/utils/workspace.js +95 -12
  225. package/dist/utils/workspace.js.map +1 -1
  226. 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 { planEndsWithPeriod } from './user-agent.js';
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, planEndsWithPeriod, STARTER_TASK_KEY, WALLET_NOT_READ_NOTE, WIRO_PROJECT_CREDENTIAL } 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(`Agent ${agentGuid} was not found.`);
117
+ throw new Error(AGENT_NOT_FOUND_TEXT);
69
118
  return row;
70
119
  }
71
120
  export function agentStatusOf(row) {
@@ -274,6 +323,71 @@ export function credentialsNeededFor(skills, registry, row) {
274
323
  }))
275
324
  .filter(entry => entry.credentials.length > 0);
276
325
  }
326
+ /** The credential keys the given skills use, main and additional, by the same rule as credentialsNeededFor. */
327
+ export function credentialsUsedBySkills(skills, registry) {
328
+ const used = new Set();
329
+ for (const name of skills) {
330
+ const skill = registry.skills.get(name);
331
+ if (!skill?.requiresCredentials)
332
+ continue;
333
+ for (const key of skill.credentialKeys)
334
+ used.add(key);
335
+ }
336
+ return used;
337
+ }
338
+ /**
339
+ * Why connect_agent_oauth connect refuses a service on this agent, or null when
340
+ * it connects: one of the agent's active skills uses it, or one of its custom
341
+ * skills or scheduled tasks requires it. Wiro keeps such an entry off until the
342
+ * credential is connected, and connecting turns it on, so the entry counts
343
+ * whatever its enabled says. A template agent's setup also waits for the
344
+ * credentials its template requires, whichever skills are on, so those stay
345
+ * connectable.
346
+ */
347
+ export function unusedCredential(row, credential, registry) {
348
+ const used = credentialsUsedBySkills(enabledSkillNames(row), registry);
349
+ for (const entry of customSkillEntries(row)) {
350
+ for (const key of entry.requiresCredentials)
351
+ used.add(key);
352
+ }
353
+ if (used.has(credential) || (!isCustomBuild(row) && credentialGroup(row, credential)?.optional === false))
354
+ return null;
355
+ return {
356
+ services: [...used].flatMap(key => {
357
+ const schema = credentialGroup(row, key)?._schema;
358
+ return isPlainObject(schema) && isPlainObject(schema.oauth_provider)
359
+ ? [`${asString(schema.title) ?? key} (${key})`]
360
+ : [];
361
+ }),
362
+ skills: [...registry.skills.values()]
363
+ .filter(skill => !skill.deprecated && skill.requiresCredentials && skill.credentialKeys[0] === credential)
364
+ .map(skill => skill.name),
365
+ };
366
+ }
367
+ export function turnOnSkillText(title, credential, skills) {
368
+ return skills.length > 0
369
+ ? `To use ${title}, first turn on ${skills.join(' or ')} with apply_agent_skills; list_agent_skills with name "${credential}" shows how it connects.`
370
+ : '';
371
+ }
372
+ /**
373
+ * The same rule for the answers that offer connect (status, and describing or
374
+ * saving a credential), as a note to show instead of that offer. Null when the
375
+ * service connects, and when the skills cannot be read: connect checks again.
376
+ */
377
+ export async function unusedCredentialNote(client, row, credential, title, signal) {
378
+ try {
379
+ const unused = unusedCredential(row, credential, await loadSkillRegistry(client, signal));
380
+ if (!unused)
381
+ return null;
382
+ return [
383
+ `${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.`,
384
+ turnOnSkillText(title, credential, unused.skills),
385
+ ].filter(Boolean).join(' ');
386
+ }
387
+ catch {
388
+ return null;
389
+ }
390
+ }
277
391
  const IDEMPOTENCY_KEY_RE = /^mcp-[0-9a-f]{16}-([0-9a-f]{10})-([0-9a-f]{8})$/;
278
392
  /** Fingerprint of one logical skill change, independent of the agent's current state. */
279
393
  export function skillChangeDigest(agentGuid, requested, tier) {
@@ -286,10 +400,15 @@ export function skillChangeDigest(agentGuid, requested, tier) {
286
400
  function cents(value) {
287
401
  return value === null || !Number.isFinite(value) ? 'none' : String(Math.round(value * 100));
288
402
  }
289
- /** Fingerprint of the monthly price before and after a change, as it was quoted. */
290
- export function skillPriceTag(currentUsd, newUsd) {
403
+ /**
404
+ * Fingerprint of what a quoted charge rests on: the monthly price before and after
405
+ * a change and, for a plan paid from the wallet, the period it is charged in. A
406
+ * renewal keeps the prices and starts a new period, which changes the charge.
407
+ */
408
+ export function skillPriceTag(currentUsd, newUsd, periodEndUnix = null) {
409
+ const prices = `${cents(currentUsd)}:${cents(newUsd)}`;
291
410
  return crypto.createHash('sha256')
292
- .update(`${cents(currentUsd)}:${cents(newUsd)}`)
411
+ .update(periodEndUnix === null ? prices : `${prices}:${periodEndUnix}`)
293
412
  .digest('hex')
294
413
  .slice(0, 8);
295
414
  }
@@ -314,26 +433,45 @@ export function planBilling(row) {
314
433
  const wallet = asString(row.teamguid) ? 'team' : 'personal';
315
434
  const subscription = isPlainObject(row.subscription) ? row.subscription : null;
316
435
  if (!subscription) {
317
- return { plan: 'none', status: null, periodEnd: null, daysRemaining: null, wallet, cancelAtPeriodEnd: false };
436
+ return { plan: 'none', status: null, periodEnd: null, wallet, cancelAtPeriodEnd: false, switchingToWallet: false };
318
437
  }
438
+ const plan = asString(subscription.provider) === 'prepaid' ? 'prepaid' : 'card';
319
439
  return {
320
- plan: asString(subscription.provider) === 'prepaid' ? 'prepaid' : 'card',
440
+ plan,
321
441
  status: asString(subscription.status) ?? null,
322
442
  periodEnd: toNumber(subscription.currentperiodend),
323
- daysRemaining: toNumber(subscription.daysremaining),
324
443
  wallet,
325
444
  cancelAtPeriodEnd: planEndsWithPeriod(subscription, row.stripecancelatperiodend),
445
+ switchingToWallet: plan === 'card' && subscription.nextprovider === 'prepaid',
326
446
  };
327
447
  }
328
- /**
329
- * Wiro charges a prepaid plan the monthly difference for the days left in the
330
- * period. The estimate assumes a 30-day period.
331
- */
332
- export function estimateProratedUsd(deltaUsd, periodEndUnix, nowMs = Date.now(), periodDays = 30) {
333
- if (periodEndUnix === null || !Number.isFinite(deltaUsd))
448
+ const DAY_SECONDS = 86400;
449
+ /** The plan change of a pricing answer, or null when the agent has no active plan or Wiro sent none. */
450
+ export function readPlanChange(value) {
451
+ if (!isPlainObject(value))
334
452
  return null;
335
- const remainingDays = Math.max(0, Math.round((periodEndUnix - nowMs / 1000) / 86400));
336
- return Math.round((deltaUsd / periodDays) * remainingDays * 100) / 100;
453
+ return {
454
+ wallet: asString(value.provider) === 'prepaid',
455
+ periodEnd: toNumber(value.periodEnd),
456
+ secondsLeft: toNumber(value.secondsLeft),
457
+ creditsNow: toNumber(value.creditsNow),
458
+ creditsThisPeriod: toNumber(value.creditsThisPeriod),
459
+ creditsFromRenewal: toNumber(value.creditsFromRenewal),
460
+ chargeNowUsd: toNumber(value.chargeNowUsd),
461
+ creditNowUsd: toNumber(value.creditNowUsd),
462
+ usedCreditsUsd: toNumber(value.usedCreditsUsd),
463
+ };
464
+ }
465
+ /** Whether a rise now pays the one-day minimum: under a day is left of the period. */
466
+ export function underOneDayLeft(secondsLeft) {
467
+ return secondsLeft !== null && secondsLeft < DAY_SECONDS;
468
+ }
469
+ /** The time left of a period in whole days, at least 1, for a sentence such as "(27 days)". */
470
+ export function daysLeftText(secondsLeft) {
471
+ if (secondsLeft === null || secondsLeft < 0)
472
+ return null;
473
+ const days = Math.max(1, Math.round(secondsLeft / DAY_SECONDS));
474
+ return `${days} ${days === 1 ? 'day' : 'days'}`;
337
475
  }
338
476
  const SKILL_PAYMENT_FAILED = 'The payment for this change did not go through, so the change was undone and nothing was charged.';
339
477
  const DEPOSIT_PAGE = 'https://wiro.ai/panel/wallet/deposit/credit-card (for a team project, select that team in the dashboard first)';
@@ -357,7 +495,7 @@ export function describeSkillsApplyError(error, plan) {
357
495
  const message = error instanceof Error ? error.message : String(error);
358
496
  const body = error instanceof ApiResultError ? error.body : {};
359
497
  const codes = error instanceof ApiResultError ? error.codes.map(code => toNumber(code)) : [];
360
- const code = codes.find(candidate => candidate !== null && candidate >= 95 && candidate <= 109) ?? null;
498
+ const code = codes.find(candidate => candidate !== null && candidate >= 95 && candidate <= 110) ?? null;
361
499
  const base = { code, message, retrySameKey: false };
362
500
  switch (code) {
363
501
  case 101: {
@@ -391,10 +529,14 @@ export function describeSkillsApplyError(error, plan) {
391
529
  };
392
530
  }
393
531
  case 104:
532
+ // Two refusals share the code: another change to the agent's plan, or another payment from its wallet.
533
+ // Skill changes, an upgrade to Pro and a wallet plan's renewal take turns on an agent.
394
534
  return {
395
535
  ...base,
396
536
  retrySameKey: true,
397
- guidance: 'Another skill change for this agent is still running. Wait a few seconds, then repeat the call with the same idempotency_key.',
537
+ guidance: /wallet/i.test(message)
538
+ ? '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.'
539
+ : '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
540
  };
399
541
  case 105:
400
542
  return {
@@ -409,19 +551,26 @@ export function describeSkillsApplyError(error, plan) {
409
551
  case 107:
410
552
  return {
411
553
  ...base,
412
- guidance: 'Repeat the call without idempotency_key to start a fresh change.',
554
+ 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
555
  };
414
556
  case 108:
415
557
  return {
416
558
  ...base,
417
559
  retrySameKey: true,
418
- guidance: 'Nothing was changed. Repeat the call with the same idempotency_key.',
560
+ 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
561
  };
420
562
  case 109:
563
+ // Wiro's sentence covers cards and wallets alike. The guidance names the fix for this plan.
421
564
  return {
422
565
  ...base,
566
+ message: 'Payment processing failed for this skill change.',
423
567
  guidance: skillPaymentGuidance(plan),
424
568
  };
569
+ case 110:
570
+ return {
571
+ ...base,
572
+ 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.',
573
+ };
425
574
  case 95:
426
575
  case 96:
427
576
  case 97:
@@ -438,17 +587,54 @@ export function describeSkillsApplyError(error, plan) {
438
587
  guidance: 'The new price could not be computed, so nothing was changed. Check the skill names with list_agent_skills.',
439
588
  };
440
589
  }
590
+ if (/^unknown skill: /i.test(message)) {
591
+ return {
592
+ ...base,
593
+ guidance: 'Nothing was changed or charged. Use names from list_agent_skills.',
594
+ };
595
+ }
596
+ if (isWalletNotRead(error))
597
+ return { ...base, guidance: WALLET_NOT_READ_NOTE };
441
598
  return { ...base, guidance: workspaceHint(message) ?? '' };
442
599
  }
600
+ /**
601
+ * One refusal covers an agent that does not exist and an agent this project may
602
+ * not connect, and the fix for each is different. The agent's own read tells
603
+ * them apart.
604
+ */
605
+ export async function connectionRefusalText(client, agentGuid, message, done, signal) {
606
+ const rule = `can only be ${done} with a project created by the person who deployed the agent: `
607
+ + 'connect this server with such a project, by its API key or a browser sign-in.';
608
+ try {
609
+ await readUserAgent(client, agentGuid, signal);
610
+ }
611
+ catch (error) {
612
+ if (isAgentNotFound(error))
613
+ return AGENT_NOT_FOUND_TEXT;
614
+ return `${message}\n\nCheck the guid with list_my_agents. If the agent is listed, integrations ${rule}`;
615
+ }
616
+ return `${message}\n\nIntegrations ${rule}`;
617
+ }
618
+ /** Wiro's answer when it could not clear a saved connection. Nothing says the connection is gone, and a repeat is safe. */
619
+ export const CONNECTION_NOT_CLEARED = 'The saved connection could not be cleared. Please try again.';
620
+ 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
621
  /** The API names a dashboard concept ("team context"); here the workspace is the connected project's. */
444
622
  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;
623
+ return /switch to the (team|personal)|different team|personal workspace/i.test(message) ? WORKSPACE_HINT : undefined;
448
624
  }
449
625
  // A field's setup help is shown whole: its last steps, such as sharing a
450
626
  // calendar with the service account, are part of the setup.
451
627
  export const FIELD_HELP_MAX = 4_000;
628
+ // The Wiro project field. On the dashboard it is a project picker, and the help
629
+ // it comes with describes that picker, so this line is shown instead.
630
+ export const WIRO_PROJECT_NOTE = 'Connect a Wiro project with bind_agent_project: it takes the project of this connection or '
631
+ + 'one from list_projects, and no key passes through this chat. The user can also pick the project on the agent\'s page in '
632
+ + 'the Wiro dashboard. Saving a project API key with set_agent_credentials stays possible when the user gives one for this, '
633
+ + 'and Wiro adds the project\'s secret by itself. Model runs the agent starts are billed to the wallet of the workspace '
634
+ + 'that owns the project.';
635
+ export function isWiroProjectField(raw) {
636
+ return /^(custom:)?wiro-project-picker$/.test(asString(raw.type) ?? '');
637
+ }
452
638
  export function htmlToText(value, maxLength = 400) {
453
639
  const html = asString(value);
454
640
  if (!html)
@@ -508,7 +694,7 @@ export function parseCredentialField(raw) {
508
694
  ...(toNumber(raw.maxsize) !== null ? { maxSizeKb: toNumber(raw.maxsize) } : {}),
509
695
  ...(asString(raw.item_type) ? { itemType: asString(raw.item_type) } : {}),
510
696
  itemSchema: itemSchemaOf(raw).map(parseCredentialField).filter(field => field.key),
511
- ...(htmlToText(raw.help, FIELD_HELP_MAX) ? { help: htmlToText(raw.help, FIELD_HELP_MAX) } : {}),
697
+ ...(!isWiroProjectField(raw) && htmlToText(raw.help, FIELD_HELP_MAX) ? { help: htmlToText(raw.help, FIELD_HELP_MAX) } : {}),
512
698
  ...(asString(raw.placeholder) ? { placeholder: asString(raw.placeholder) } : {}),
513
699
  };
514
700
  }
@@ -983,7 +1169,8 @@ export function prepareCredentialWrite(inputs, metas) {
983
1169
  const mayClear = !field.required || meta.channel !== null;
984
1170
  if (isEmptyValue(input.value)) {
985
1171
  if (!mayClear) {
986
- prepared.errors.push(`${field.label} is required and cannot be cleared.`);
1172
+ prepared.errors.push(`${field.label} is required and cannot be cleared. To replace the saved value, send the new one. `
1173
+ + 'To end the agent\'s access, revoke it with the service it belongs to.');
987
1174
  continue;
988
1175
  }
989
1176
  const cleared = shape === 'string-array' || shape === 'object-array' ? [] : '';
@@ -1080,6 +1267,38 @@ export function isFilledValue(value) {
1080
1267
  return Object.values(value).some(isFilledValue);
1081
1268
  return true;
1082
1269
  }
1270
+ /**
1271
+ * Whether a field of a saved credential holds what someone entered, counted the
1272
+ * way Wiro counts it for a card's filled state: a list of options counts only an
1273
+ * option that is selected, and a row of a list does not count for its selects
1274
+ * and switches, which come with a default.
1275
+ */
1276
+ export function isCredentialFieldFilled(field, value) {
1277
+ if (field.itemSchema.length === 0 || !Array.isArray(value))
1278
+ return isCredentialValueFilled(value);
1279
+ return value.some(row => {
1280
+ if (!isPlainObject(row))
1281
+ return isCredentialValueFilled(row);
1282
+ return Object.entries(row).some(([key, item]) => {
1283
+ const inner = field.itemSchema.find(entry => entry.key === key);
1284
+ if (!inner)
1285
+ return isCredentialValueFilled(item);
1286
+ if (inner.type === 'select' || inner.type === 'boolean')
1287
+ return false;
1288
+ return isCredentialFieldFilled(inner, item);
1289
+ });
1290
+ });
1291
+ }
1292
+ function isCredentialValueFilled(value) {
1293
+ if (Array.isArray(value) && value.length > 0 && value.every(option => isPlainObject(option) && 'selected' in option)) {
1294
+ return value.some(option => isPlainObject(option) && option.selected === true);
1295
+ }
1296
+ if (Array.isArray(value))
1297
+ return value.some(isCredentialValueFilled);
1298
+ if (isPlainObject(value))
1299
+ return Object.values(value).some(isCredentialValueFilled);
1300
+ return isFilledValue(value);
1301
+ }
1083
1302
  export function credentialGroup(row, key) {
1084
1303
  const groups = isPlainObject(row.credentials) ? row.credentials : null;
1085
1304
  const group = groups ? groups[key] : undefined;
@@ -1091,18 +1310,19 @@ export function credentialStatus(row, key, meta) {
1091
1310
  return null;
1092
1311
  const schema = isPlainObject(group._schema) ? group._schema : {};
1093
1312
  const connected = group._connected === true;
1094
- // A sign-in credential's required fields depend on its mode; connected is the signal there.
1095
- const missingFields = connected || !meta || meta.oauth
1313
+ // A sign-in credential's required fields depend on its mode; connected is the signal there. Filled says every
1314
+ // required field holds a value, a template's own value that is not shown included.
1315
+ const missingFields = connected || !meta || meta.oauth || group._filled === true
1096
1316
  ? []
1097
1317
  : meta.fields
1098
- .filter(field => field.required && isWritableField(field) && !isFilledValue(group[field.key]))
1318
+ .filter(field => field.required && isWritableField(field) && !isCredentialFieldFilled(field, group[field.key]))
1099
1319
  .map(field => field.label);
1100
1320
  return {
1101
1321
  key,
1102
1322
  title: asString(schema.title) ?? meta?.title ?? key,
1103
1323
  connected,
1104
1324
  filled: group._filled === true,
1105
- requiredMissing: group._required_missing === true,
1325
+ requiredMissing: credentialKeysSetupWaitsFor(row).has(key),
1106
1326
  optional: group.optional === true,
1107
1327
  missingFields,
1108
1328
  };
@@ -1158,6 +1378,17 @@ export function isAllowedRedirectUrl(url, agentGuid) {
1158
1378
  export function ownAppRedirectUri(meta) {
1159
1379
  return meta.oauth?.callbackPath ? `${OAUTH_CALLBACK_BASE}${meta.oauth.callbackPath}` : undefined;
1160
1380
  }
1381
+ /** Goes with a list of missing fields that holds app keys, which may be saved already. */
1382
+ export function writeOnlyNote(mode) {
1383
+ return `App keys are not shown once saved. If they are saved already, connect with mode ${mode}.`;
1384
+ }
1385
+ /** The same note before a mode is chosen: it names each usable mode that takes app keys. */
1386
+ export function writeOnlyModesNote(requirements) {
1387
+ const modes = requirements
1388
+ .filter(requirement => requirement.available && requirement.fields.some(field => field.writeOnly))
1389
+ .map(requirement => requirement.mode);
1390
+ return modes.length > 0 ? writeOnlyNote(modes.join(' or ')) : null;
1391
+ }
1161
1392
  function fieldsForMode(meta, mode) {
1162
1393
  if (mode === 'wiro')
1163
1394
  return [];
@@ -1182,11 +1413,15 @@ function fieldsForMode(meta, mode) {
1182
1413
  export function connectionModeRequirements(meta, group) {
1183
1414
  const modes = meta.connectionModes.length > 0 ? meta.connectionModes : ['wiro'];
1184
1415
  return modes.map(mode => {
1185
- const fields = fieldsForMode(meta, mode).map(field => ({
1186
- field: field.key,
1187
- label: field.label,
1188
- isSet: group !== null && isFilledValue(group[field.key]),
1189
- }));
1416
+ const fields = fieldsForMode(meta, mode).map(field => {
1417
+ const isSet = group !== null && isFilledValue(group[field.key]);
1418
+ return {
1419
+ field: field.key,
1420
+ label: field.label,
1421
+ isSet,
1422
+ ...(field.oauthManaged && !isSet ? { writeOnly: true } : {}),
1423
+ };
1424
+ });
1190
1425
  if (mode === 'wiro') {
1191
1426
  return {
1192
1427
  mode,
@@ -1248,6 +1483,16 @@ export function chooseConnectionMode(meta, requirements) {
1248
1483
  });
1249
1484
  return ordered.find(requirement => requirement.fields.every(field => field.isSet))?.mode ?? null;
1250
1485
  }
1486
+ /**
1487
+ * An account in a line of this server's text. The outside service sets its name,
1488
+ * and a service without an account choice gives that name as the id too, so both
1489
+ * are quoted and a repeat is left out.
1490
+ */
1491
+ export function accountLabel(account) {
1492
+ const name = oneLine(account.name);
1493
+ const id = quotedSpan(account.id);
1494
+ return name && name !== oneLine(account.id) ? `${quotedSpan(name)} (${id})` : id;
1495
+ }
1251
1496
  /** Candidate or saved accounts in one shape, whatever the provider calls the fields. */
1252
1497
  export function toAccountRefs(list, picker) {
1253
1498
  if (!Array.isArray(list))
@@ -1342,7 +1587,7 @@ export function buildChainedSelections(next, groups, selections) {
1342
1587
  if (group.errorCode) {
1343
1588
  return {
1344
1589
  skippedParents: [],
1345
- error: `${group.parent.name || group.parent.id} could not be read (${group.errorCode}), so nothing can be chosen for it. Leave it out.`,
1590
+ error: `${accountLabel(group.parent)} could not be read (${group.errorCode}), so nothing can be chosen for it. Leave it out.`,
1346
1591
  };
1347
1592
  }
1348
1593
  const ids = chosen.get(group.parent.id) ?? [];
@@ -1351,7 +1596,7 @@ export function buildChainedSelections(next, groups, selections) {
1351
1596
  if (!item) {
1352
1597
  return {
1353
1598
  skippedParents: [],
1354
- error: `"${itemId}" is not available under ${group.parent.name || group.parent.id}. Available: ${group.items.map(candidate => `${candidate.id} (${candidate.name})`).join(', ') || 'none'}.`,
1599
+ error: `"${itemId}" is not available under ${accountLabel(group.parent)}. Available: ${group.items.map(accountLabel).join(', ') || 'none'}.`,
1355
1600
  };
1356
1601
  }
1357
1602
  if (!ids.includes(item.id))
@@ -1423,6 +1668,8 @@ export function connectionState(status, options) {
1423
1668
  return 'pick_accounts';
1424
1669
  return 'not_connected';
1425
1670
  }
1671
+ /** The entry that holds what an agent posts, sends or changes by itself until the user says yes. */
1672
+ export const APPROVAL_POLICY_KEY = 'cs-approval-policy';
1426
1673
  const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1427
1674
  /**
1428
1675
  * Same rules as the API's key normalisation: cs-cron-x and cs-x stay, cron-x
@@ -1523,7 +1770,7 @@ export function resolveExistingCustomSkillKey(input, existingKeys, kind) {
1523
1770
  return { key: matches[0] };
1524
1771
  if (matches.length > 1)
1525
1772
  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-...) or check get_my_agent.` };
1773
+ 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
1774
  }
1528
1775
  /** A light shape check; the API validates the expression fully. */
1529
1776
  export function looksLikeCron(expression) {
@@ -1619,13 +1866,6 @@ export function humanizeCustomSkillKey(key) {
1619
1866
  body = body.replace(/[-_]+/g, ' ').replace(/\s+/g, ' ').trim();
1620
1867
  return body ? body.charAt(0).toUpperCase() + body.slice(1) : '';
1621
1868
  }
1622
- /** Text in a fence longer than any run of backticks inside it, so it shows as is. */
1623
- export function textBlock(value) {
1624
- let fence = '```';
1625
- while (value.includes(fence))
1626
- fence += '`';
1627
- return [`${fence}text`, value, fence];
1628
- }
1629
1869
  export function customSkillEntries(row) {
1630
1870
  const rows = [
1631
1871
  ...(Array.isArray(row.customskills) ? row.customskills : []),
@@ -1644,6 +1884,7 @@ export function customSkillEntries(row) {
1644
1884
  source,
1645
1885
  userCreated: source === 'user-created' || entry._user_created === true,
1646
1886
  requiresCredentials: stringList(entry.requires_credentials),
1887
+ notWritten: key === STARTER_TASK_KEY && entry._edited === false,
1647
1888
  };
1648
1889
  }).filter(entry => entry.key);
1649
1890
  }