@wiro-ai/wiro-mcp 1.4.0 → 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 (248) hide show
  1. package/README.md +50 -17
  2. package/dist/client.d.ts +25 -6
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +129 -29
  5. package/dist/client.js.map +1 -1
  6. package/dist/instructions.d.ts.map +1 -1
  7. package/dist/instructions.js +24 -17
  8. package/dist/instructions.js.map +1 -1
  9. package/dist/playbooks.d.ts.map +1 -1
  10. package/dist/playbooks.js +61 -48
  11. package/dist/playbooks.js.map +1 -1
  12. package/dist/prompts.d.ts.map +1 -1
  13. package/dist/prompts.js +4 -3
  14. package/dist/prompts.js.map +1 -1
  15. package/dist/server.d.ts +6 -0
  16. package/dist/server.d.ts.map +1 -1
  17. package/dist/server.js +3 -4
  18. package/dist/server.js.map +1 -1
  19. package/dist/tools/apply-agent-skills.d.ts.map +1 -1
  20. package/dist/tools/apply-agent-skills.js +400 -88
  21. package/dist/tools/apply-agent-skills.js.map +1 -1
  22. package/dist/tools/bind-agent-project.d.ts +4 -0
  23. package/dist/tools/bind-agent-project.d.ts.map +1 -0
  24. package/dist/tools/bind-agent-project.js +403 -0
  25. package/dist/tools/bind-agent-project.js.map +1 -0
  26. package/dist/tools/cancel-agent-message.d.ts.map +1 -1
  27. package/dist/tools/cancel-agent-message.js +6 -3
  28. package/dist/tools/cancel-agent-message.js.map +1 -1
  29. package/dist/tools/cancel-task.js +3 -3
  30. package/dist/tools/cancel-task.js.map +1 -1
  31. package/dist/tools/clone-workflow.d.ts.map +1 -1
  32. package/dist/tools/clone-workflow.js +5 -4
  33. package/dist/tools/clone-workflow.js.map +1 -1
  34. package/dist/tools/connect-agent-oauth.d.ts.map +1 -1
  35. package/dist/tools/connect-agent-oauth.js +144 -44
  36. package/dist/tools/connect-agent-oauth.js.map +1 -1
  37. package/dist/tools/control-agent.d.ts.map +1 -1
  38. package/dist/tools/control-agent.js +133 -45
  39. package/dist/tools/control-agent.js.map +1 -1
  40. package/dist/tools/create-workflow.d.ts +9 -6
  41. package/dist/tools/create-workflow.d.ts.map +1 -1
  42. package/dist/tools/create-workflow.js +13 -13
  43. package/dist/tools/create-workflow.js.map +1 -1
  44. package/dist/tools/delete-agent-session.d.ts.map +1 -1
  45. package/dist/tools/delete-agent-session.js +20 -14
  46. package/dist/tools/delete-agent-session.js.map +1 -1
  47. package/dist/tools/delete-agent.d.ts.map +1 -1
  48. package/dist/tools/delete-agent.js +14 -10
  49. package/dist/tools/delete-agent.js.map +1 -1
  50. package/dist/tools/delete-workflow.d.ts.map +1 -1
  51. package/dist/tools/delete-workflow.js +6 -5
  52. package/dist/tools/delete-workflow.js.map +1 -1
  53. package/dist/tools/deploy-agent.d.ts.map +1 -1
  54. package/dist/tools/deploy-agent.js +215 -47
  55. package/dist/tools/deploy-agent.js.map +1 -1
  56. package/dist/tools/disconnect-agent-oauth.d.ts.map +1 -1
  57. package/dist/tools/disconnect-agent-oauth.js +32 -12
  58. package/dist/tools/disconnect-agent-oauth.js.map +1 -1
  59. package/dist/tools/get-account.d.ts.map +1 -1
  60. package/dist/tools/get-account.js +157 -49
  61. package/dist/tools/get-account.js.map +1 -1
  62. package/dist/tools/get-agent-activity.d.ts.map +1 -1
  63. package/dist/tools/get-agent-activity.js +186 -36
  64. package/dist/tools/get-agent-activity.js.map +1 -1
  65. package/dist/tools/get-agent-conversation.d.ts.map +1 -1
  66. package/dist/tools/get-agent-conversation.js +65 -39
  67. package/dist/tools/get-agent-conversation.js.map +1 -1
  68. package/dist/tools/get-agent-message.d.ts.map +1 -1
  69. package/dist/tools/get-agent-message.js +6 -4
  70. package/dist/tools/get-agent-message.js.map +1 -1
  71. package/dist/tools/get-agent-template.d.ts.map +1 -1
  72. package/dist/tools/get-agent-template.js +33 -17
  73. package/dist/tools/get-agent-template.js.map +1 -1
  74. package/dist/tools/get-model-schema.d.ts.map +1 -1
  75. package/dist/tools/get-model-schema.js +7 -5
  76. package/dist/tools/get-model-schema.js.map +1 -1
  77. package/dist/tools/get-my-agent.d.ts.map +1 -1
  78. package/dist/tools/get-my-agent.js +54 -11
  79. package/dist/tools/get-my-agent.js.map +1 -1
  80. package/dist/tools/get-playbook.d.ts.map +1 -1
  81. package/dist/tools/get-playbook.js +3 -2
  82. package/dist/tools/get-playbook.js.map +1 -1
  83. package/dist/tools/get-task-price.d.ts +1 -1
  84. package/dist/tools/get-task-price.d.ts.map +1 -1
  85. package/dist/tools/get-task-price.js +29 -8
  86. package/dist/tools/get-task-price.js.map +1 -1
  87. package/dist/tools/get-task.d.ts.map +1 -1
  88. package/dist/tools/get-task.js +25 -9
  89. package/dist/tools/get-task.js.map +1 -1
  90. package/dist/tools/get-workflow.d.ts.map +1 -1
  91. package/dist/tools/get-workflow.js +46 -21
  92. package/dist/tools/get-workflow.js.map +1 -1
  93. package/dist/tools/index.d.ts +4 -1
  94. package/dist/tools/index.d.ts.map +1 -1
  95. package/dist/tools/index.js +10 -1
  96. package/dist/tools/index.js.map +1 -1
  97. package/dist/tools/kill-task.js +3 -3
  98. package/dist/tools/kill-task.js.map +1 -1
  99. package/dist/tools/list-agent-skills.d.ts.map +1 -1
  100. package/dist/tools/list-agent-skills.js +5 -4
  101. package/dist/tools/list-agent-skills.js.map +1 -1
  102. package/dist/tools/list-agent-templates.js +4 -4
  103. package/dist/tools/list-agent-templates.js.map +1 -1
  104. package/dist/tools/list-my-agents.d.ts.map +1 -1
  105. package/dist/tools/list-my-agents.js +14 -8
  106. package/dist/tools/list-my-agents.js.map +1 -1
  107. package/dist/tools/list-projects.d.ts +4 -0
  108. package/dist/tools/list-projects.d.ts.map +1 -0
  109. package/dist/tools/list-projects.js +141 -0
  110. package/dist/tools/list-projects.js.map +1 -0
  111. package/dist/tools/list-studio-results.js +3 -3
  112. package/dist/tools/list-studio-results.js.map +1 -1
  113. package/dist/tools/list-tasks.js +3 -3
  114. package/dist/tools/list-tasks.js.map +1 -1
  115. package/dist/tools/list-workflow-runs.d.ts.map +1 -1
  116. package/dist/tools/list-workflow-runs.js +6 -4
  117. package/dist/tools/list-workflow-runs.js.map +1 -1
  118. package/dist/tools/list-workflows.d.ts.map +1 -1
  119. package/dist/tools/list-workflows.js +7 -6
  120. package/dist/tools/list-workflows.js.map +1 -1
  121. package/dist/tools/manage-agent-billing.d.ts.map +1 -1
  122. package/dist/tools/manage-agent-billing.js +259 -87
  123. package/dist/tools/manage-agent-billing.js.map +1 -1
  124. package/dist/tools/manage-agent-custom-skill.d.ts.map +1 -1
  125. package/dist/tools/manage-agent-custom-skill.js +91 -21
  126. package/dist/tools/manage-agent-custom-skill.js.map +1 -1
  127. package/dist/tools/recommend-model.js +3 -3
  128. package/dist/tools/recommend-model.js.map +1 -1
  129. package/dist/tools/resume-workflow-run.d.ts.map +1 -1
  130. package/dist/tools/resume-workflow-run.js +3 -2
  131. package/dist/tools/resume-workflow-run.js.map +1 -1
  132. package/dist/tools/run-model.js +5 -5
  133. package/dist/tools/run-model.js.map +1 -1
  134. package/dist/tools/run-workflow.d.ts.map +1 -1
  135. package/dist/tools/run-workflow.js +3 -2
  136. package/dist/tools/run-workflow.js.map +1 -1
  137. package/dist/tools/save-workflow-from-agent.d.ts.map +1 -1
  138. package/dist/tools/save-workflow-from-agent.js +22 -20
  139. package/dist/tools/save-workflow-from-agent.js.map +1 -1
  140. package/dist/tools/schemas.d.ts +150 -127
  141. package/dist/tools/schemas.d.ts.map +1 -1
  142. package/dist/tools/schemas.js +76 -3
  143. package/dist/tools/schemas.js.map +1 -1
  144. package/dist/tools/search-docs.js +3 -3
  145. package/dist/tools/search-docs.js.map +1 -1
  146. package/dist/tools/search-models.d.ts.map +1 -1
  147. package/dist/tools/search-models.js +7 -4
  148. package/dist/tools/search-models.js.map +1 -1
  149. package/dist/tools/send-agent-message.d.ts.map +1 -1
  150. package/dist/tools/send-agent-message.js +32 -11
  151. package/dist/tools/send-agent-message.js.map +1 -1
  152. package/dist/tools/set-agent-credentials.d.ts.map +1 -1
  153. package/dist/tools/set-agent-credentials.js +60 -59
  154. package/dist/tools/set-agent-credentials.js.map +1 -1
  155. package/dist/tools/update-agent.d.ts.map +1 -1
  156. package/dist/tools/update-agent.js +59 -21
  157. package/dist/tools/update-agent.js.map +1 -1
  158. package/dist/tools/update-workflow.d.ts +205 -6
  159. package/dist/tools/update-workflow.d.ts.map +1 -1
  160. package/dist/tools/update-workflow.js +20 -7
  161. package/dist/tools/update-workflow.js.map +1 -1
  162. package/dist/tools/upload-file.d.ts.map +1 -1
  163. package/dist/tools/upload-file.js +13 -9
  164. package/dist/tools/upload-file.js.map +1 -1
  165. package/dist/tools/wait-for-task.d.ts.map +1 -1
  166. package/dist/tools/wait-for-task.js +23 -5
  167. package/dist/tools/wait-for-task.js.map +1 -1
  168. package/dist/tools/wait-for-workflow-run.d.ts +18 -18
  169. package/dist/tools/wait-for-workflow-run.d.ts.map +1 -1
  170. package/dist/tools/wait-for-workflow-run.js +7 -6
  171. package/dist/tools/wait-for-workflow-run.js.map +1 -1
  172. package/dist/types.d.ts +6 -0
  173. package/dist/types.d.ts.map +1 -1
  174. package/dist/types.js.map +1 -1
  175. package/dist/utils/agent-catalog.d.ts +168 -139
  176. package/dist/utils/agent-catalog.d.ts.map +1 -1
  177. package/dist/utils/agent-catalog.js +34 -14
  178. package/dist/utils/agent-catalog.js.map +1 -1
  179. package/dist/utils/agent-config.d.ts +111 -8
  180. package/dist/utils/agent-config.d.ts.map +1 -1
  181. package/dist/utils/agent-config.js +290 -40
  182. package/dist/utils/agent-config.js.map +1 -1
  183. package/dist/utils/agent-message.d.ts +41 -22
  184. package/dist/utils/agent-message.d.ts.map +1 -1
  185. package/dist/utils/agent-message.js +94 -31
  186. package/dist/utils/agent-message.js.map +1 -1
  187. package/dist/utils/agent-targets.d.ts +8 -4
  188. package/dist/utils/agent-targets.d.ts.map +1 -1
  189. package/dist/utils/agent-targets.js +51 -35
  190. package/dist/utils/agent-targets.js.map +1 -1
  191. package/dist/utils/agent-turns.d.ts +31 -24
  192. package/dist/utils/agent-turns.d.ts.map +1 -1
  193. package/dist/utils/agent-turns.js +154 -35
  194. package/dist/utils/agent-turns.js.map +1 -1
  195. package/dist/utils/api.d.ts +6 -0
  196. package/dist/utils/api.d.ts.map +1 -1
  197. package/dist/utils/api.js +22 -1
  198. package/dist/utils/api.js.map +1 -1
  199. package/dist/utils/format.d.ts +27 -1
  200. package/dist/utils/format.d.ts.map +1 -1
  201. package/dist/utils/format.js +429 -82
  202. package/dist/utils/format.js.map +1 -1
  203. package/dist/utils/json-schema-dialect.d.ts +8 -1
  204. package/dist/utils/json-schema-dialect.d.ts.map +1 -1
  205. package/dist/utils/json-schema-dialect.js +27 -1
  206. package/dist/utils/json-schema-dialect.js.map +1 -1
  207. package/dist/utils/media.d.ts +8 -0
  208. package/dist/utils/media.d.ts.map +1 -1
  209. package/dist/utils/media.js +29 -0
  210. package/dist/utils/media.js.map +1 -1
  211. package/dist/utils/poll.d.ts +5 -2
  212. package/dist/utils/poll.d.ts.map +1 -1
  213. package/dist/utils/poll.js +10 -9
  214. package/dist/utils/poll.js.map +1 -1
  215. package/dist/utils/quoted-text.d.ts +28 -0
  216. package/dist/utils/quoted-text.d.ts.map +1 -0
  217. package/dist/utils/quoted-text.js +64 -0
  218. package/dist/utils/quoted-text.js.map +1 -0
  219. package/dist/utils/structured.d.ts +2 -0
  220. package/dist/utils/structured.d.ts.map +1 -1
  221. package/dist/utils/structured.js +15 -2
  222. package/dist/utils/structured.js.map +1 -1
  223. package/dist/utils/studio-runs.d.ts +4 -4
  224. package/dist/utils/studio-runs.d.ts.map +1 -1
  225. package/dist/utils/studio-runs.js +11 -8
  226. package/dist/utils/studio-runs.js.map +1 -1
  227. package/dist/utils/user-agent.d.ts +376 -241
  228. package/dist/utils/user-agent.d.ts.map +1 -1
  229. package/dist/utils/user-agent.js +302 -89
  230. package/dist/utils/user-agent.js.map +1 -1
  231. package/dist/utils/wiro-project.d.ts +45 -0
  232. package/dist/utils/wiro-project.d.ts.map +1 -0
  233. package/dist/utils/wiro-project.js +66 -0
  234. package/dist/utils/wiro-project.js.map +1 -0
  235. package/dist/utils/workflow-agent.d.ts +4 -4
  236. package/dist/utils/workflow-save.d.ts +23 -5
  237. package/dist/utils/workflow-save.d.ts.map +1 -1
  238. package/dist/utils/workflow-save.js +130 -38
  239. package/dist/utils/workflow-save.js.map +1 -1
  240. package/dist/utils/workflow.d.ts +79 -61
  241. package/dist/utils/workflow.d.ts.map +1 -1
  242. package/dist/utils/workflow.js +149 -19
  243. package/dist/utils/workflow.js.map +1 -1
  244. package/dist/utils/workspace.d.ts +64 -0
  245. package/dist/utils/workspace.d.ts.map +1 -0
  246. package/dist/utils/workspace.js +173 -0
  247. package/dist/utils/workspace.js.map +1 -0
  248. package/package.json +6 -4
@@ -1,16 +1,28 @@
1
1
  import { z } from 'zod';
2
- import { asString, errorResult, errorText, requireApiSuccess, toNumber, unixToIso, } from '../utils/api.js';
3
- import { agentStatusLabel, agentStatusOf, checkSkillSet, credentialsNeededFor, describeSkillsApplyError, diffSkills, enabledSkillNames, estimateProratedUsd, formatUsd, isAgentRunning, loadSkillRegistry, newSkillChangeKey, parseSkillChangeKey, planBilling, planSkillChanges, readUserAgent, restartNote, skillChangeDigest, skillPriceTag, workspaceHint, } from '../utils/agent-config.js';
4
- import { outcomeUnknown } from '../utils/user-agent.js';
2
+ import { asString, errorResult, errorText, isPlainObject, requireApiSuccess, toNumber, unixToIso, } from '../utils/api.js';
3
+ import { agentStatusLabel, agentStatusOf, checkSkillSet, credentialsNeededFor, daysLeftText, describeSkillsApplyError, diffSkills, enabledSkillNames, formatUsd, isAgentRunning, loadSkillRegistry, newSkillChangeKey, parseSkillChangeKey, planBilling, planSkillChanges, readPlanChange, readUserAgent, underOneDayLeft, restartNote, skillChangeDigest, skillPriceTag, workspaceHint, } from '../utils/agent-config.js';
4
+ import { asTier, bindProjectStep, formatCredits, outcomeUnknown, toTierPricing, WIRO_PROJECT_CREDENTIAL } from '../utils/user-agent.js';
5
5
  import { DEFAULT_WAIT_SECONDS } from '../utils/poll.js';
6
- import { nextActionSchema } from './schemas.js';
6
+ import { isPlainTeamMember, teamAgentBillingRefused } from '../utils/workspace.js';
7
+ import { nextActionSchema, strictInput } from './schemas.js';
7
8
  const pricingSchema = z.object({
8
9
  currentMonthlyUsd: z.number().nullable(),
9
10
  newMonthlyUsd: z.number().nullable(),
10
11
  deltaMonthlyUsd: z.number().nullable(),
11
- currentMonthlyCredits: z.number().nullable(),
12
- newMonthlyCredits: z.number().nullable(),
12
+ currentMonthlyCredits: z.number().nullable()
13
+ .describe('The monthly credits each renewal grants now.'),
14
+ newMonthlyCredits: z.number().nullable()
15
+ .describe('The monthly credits each renewal grants after the change.'),
13
16
  deltaMonthlyCredits: z.number().nullable(),
17
+ creditsNow: z.number().optional()
18
+ .describe('The credits this period gains now, below 0 when it loses them: the change counts for the time left in the period.'),
19
+ creditsThisPeriod: z.number().optional()
20
+ .describe('The monthly credits this period grants in all after the change.'),
21
+ creditsFromRenewal: z.number().optional()
22
+ .describe('The monthly credits every renewal grants after the change.'),
23
+ periodEnd: z.string().optional()
24
+ .describe('When the current period ends.'),
25
+ note: z.string().optional(),
14
26
  });
15
27
  const outputSchema = {
16
28
  agentGuid: z.string(),
@@ -32,7 +44,12 @@ const outputSchema = {
32
44
  plan: z.enum(['prepaid', 'card', 'none']),
33
45
  subscriptionStatus: z.string().nullable(),
34
46
  wallet: z.enum(['team', 'personal']),
35
- estimatedChargeNowUsd: z.number().nullable().optional(),
47
+ estimatedChargeNowUsd: z.number().nullable().optional()
48
+ .describe('What a wallet plan pays now at list price, as Wiro works it out, before any discount on the wallet; 0 on a card plan, whose difference goes to the card subscription.'),
49
+ creditNowUsd: z.number().nullable().optional()
50
+ .describe('For a drop on a wallet plan, what goes back to the balances that paid; null when it is worked out when the change is applied.'),
51
+ usedCreditsUsd: z.number().nullable().optional()
52
+ .describe('The part of a credit that comes off for credits already used above what the plan keeps.'),
36
53
  chargedNowUsd: z.number().nullable().optional(),
37
54
  note: z.string(),
38
55
  }),
@@ -54,12 +71,89 @@ const outputSchema = {
54
71
  nextAction: nextActionSchema.optional(),
55
72
  };
56
73
  const UNCHANGED_RESTART = 'Nothing changed, so the agent does not restart.';
57
- // A credit for skills turned off goes to the wallet with an end date: it can be
58
- // spent until the plan's current period ends.
59
- const CREDIT_SPEND_BY = 'It can be spent until this period ends.';
74
+ // The tier and the price live with Wiro, and the agent reads neither: only a skill that turns on or off restarts it.
75
+ const TIER_ONLY_RESTART = 'No skill turns on or off, so a change of the tier alone does not restart the agent.';
76
+ function roundCents(usd) {
77
+ return Math.round(usd * 100) / 100;
78
+ }
79
+ /**
80
+ * How long an applied credit lasts, from the parts Wiro says it split into. Each
81
+ * part went back to the balance that paid it and keeps that balance's end date,
82
+ * except the part whose source has ended since (each ...UntilPeriodEndUsd): that
83
+ * part lasts until the plan's current period ends. The two amounts add up to the
84
+ * credit, and parts that come to nothing say nothing.
85
+ */
86
+ function creditTerms(parts) {
87
+ if (!isPlainObject(parts))
88
+ return '';
89
+ const usd = (key) => toNumber(parts[key]) ?? 0;
90
+ const untilEnd = roundCents(usd('couponUntilPeriodEndUsd') + usd('storeUntilPeriodEndUsd') + usd('depositUntilPeriodEndUsd'));
91
+ const keepsEnd = roundCents(roundCents(usd('couponUsd') + usd('storeUsd') + usd('depositUsd')) - untilEnd);
92
+ if (keepsEnd > 0 && untilEnd > 0) {
93
+ return `${formatUsd(keepsEnd)} of it went back to the balances that paid and keeps their end dates, and ${formatUsd(untilEnd)} lasts until this period ends, because the credit that paid it has ended.`;
94
+ }
95
+ if (keepsEnd > 0)
96
+ return 'It went back to the balances that paid and keeps their end dates.';
97
+ return untilEnd > 0 ? 'It lasts until this period ends, because the credit that paid it has ended.' : '';
98
+ }
99
+ /** A count of credits with its sign, such as +97 or -690. */
100
+ function signedCredits(credits) {
101
+ return `${credits > 0 ? '+' : ''}${formatCredits(credits)}`;
102
+ }
103
+ /** The fields of Wiro's plan change that the pricing output carries. */
104
+ function periodFields(change) {
105
+ if (!change)
106
+ return {};
107
+ const periodEnd = unixToIso(change.periodEnd);
108
+ return {
109
+ ...(change.creditsNow !== null ? { creditsNow: change.creditsNow } : {}),
110
+ ...(change.creditsThisPeriod !== null ? { creditsThisPeriod: change.creditsThisPeriod } : {}),
111
+ ...(change.creditsFromRenewal !== null ? { creditsFromRenewal: change.creditsFromRenewal } : {}),
112
+ ...(periodEnd ? { periodEnd } : {}),
113
+ };
114
+ }
115
+ /**
116
+ * What a change does to this period's credits when none come with it: those it takes away end for the
117
+ * time left, and the period keeps the rest. A plan priced again at today's rates can also keep its
118
+ * credits, or gain some, while its price drops.
119
+ */
120
+ function creditsEndNote(skillsOff, creditsNow, creditsThisPeriod) {
121
+ if (creditsNow !== null && creditsNow > 0) {
122
+ return `This period gains ${formatCredits(creditsNow)} credits for the time left${creditsThisPeriod !== null ? `, ${formatCredits(creditsThisPeriod)} in all` : ''}.`;
123
+ }
124
+ if (creditsNow === 0) {
125
+ return creditsThisPeriod !== null ? `This period keeps its ${formatCredits(creditsThisPeriod)} credits.` : 'This period keeps its credits.';
126
+ }
127
+ const keeps = creditsThisPeriod !== null ? `, and this period keeps ${formatCredits(creditsThisPeriod)}` : '';
128
+ if (skillsOff === 0)
129
+ return `The credits the change takes away end now for the time left${keeps}.`;
130
+ return `${skillsOff === 1 ? 'Its' : 'Their'} credits for the time left end now${keeps}.`;
131
+ }
60
132
  function difference(next, current) {
61
133
  return next === null || current === null ? null : Math.round((next - current) * 100) / 100;
62
134
  }
135
+ /**
136
+ * A skill change prices the plan at today's prices for all its skills, so a
137
+ * plan that started at other prices also changes for the skills it already
138
+ * has, and so do its monthly credits. The agent's tiers carry today's prices
139
+ * and credits for those skills. They are the base of the new price, not a price
140
+ * the plan reaches. An agent without an active plan cannot take the change yet,
141
+ * and a move to Pro prices the whole plan at today's Pro prices, which the quote
142
+ * already shows.
143
+ */
144
+ function todayPriceNote(row, billing, currentPrice, currentCredits, tierChange) {
145
+ if (tierChange)
146
+ return null;
147
+ const tier = asTier(row.tier);
148
+ const tiers = toTierPricing(isPlainObject(row.agent) ? row.agent.tiers : undefined);
149
+ const today = tier && tiers ? tiers[tier] : null;
150
+ if (billing.status !== 'active' || currentPrice === null || today === null || !difference(today.priceUsd, currentPrice))
151
+ return null;
152
+ const versus = currentCredits !== null && today.monthlyCredits !== currentCredits
153
+ ? ` with ${formatCredits(today.monthlyCredits)} monthly credits, not its current ${formatUsd(currentPrice)} and ${formatCredits(currentCredits)}`
154
+ : `, not its current ${formatUsd(currentPrice)}`;
155
+ return `Today's ${tier === 'pro' ? 'Pro' : 'Starter'} price for the skills this plan already has is ${formatUsd(today.priceUsd)} a month${versus}. A skill change counts from today's price, and the new price in this quote already includes that.`;
156
+ }
63
157
  /**
64
158
  * The wallet of the agent's workspace, which Wiro charges or credits for a
65
159
  * skill change: the team's for a team agent, else its owner's personal wallet.
@@ -69,51 +163,175 @@ function difference(next, current) {
69
163
  function walletName(billing) {
70
164
  return billing.wallet === 'team' ? 'the team wallet' : 'your personal wallet';
71
165
  }
72
- /** For a cancelled plan, a skill change only covers the days left: nothing renews at the new price. */
166
+ /** When the new monthly credits come: a plan set to end renews only if it is kept, and an agent without a plan gets them once it is subscribed. */
167
+ function creditsWhen(billing) {
168
+ if (billing.plan === 'none')
169
+ return 'a month once it is subscribed';
170
+ return billing.status === 'active' && billing.cancelAtPeriodEnd === true
171
+ ? 'at each renewal after the change, if the plan is kept'
172
+ : 'at each renewal after the change';
173
+ }
174
+ /** When an active plan's next renewal gives the new monthly credits. A card plan whose renewal is not known names it as a condition. */
175
+ function renewalWhen(billing) {
176
+ if (billing.cancelAtPeriodEnd === true)
177
+ return 'from the next renewal, if the plan is kept';
178
+ return billing.cancelAtPeriodEnd === null ? 'from the next renewal, if the plan renews' : 'from the next renewal';
179
+ }
180
+ /** What renewals give after an applied change: a plan that may not renew names it as a condition, as its quote did. */
181
+ function renewalGives(billing, credits) {
182
+ return billing.cancelAtPeriodEnd === false ? `every renewal gives ${formatCredits(credits)}` : `${formatCredits(credits)} ${renewalWhen(billing)}`;
183
+ }
184
+ /** For a cancelled plan, a skill change only covers the time left: nothing renews at the new price. */
73
185
  function cancelledNote(billing) {
74
186
  const end = billing.periodEnd !== null ? unixToIso(billing.periodEnd) : undefined;
75
187
  if (billing.plan === 'card') {
76
188
  return `The plan is set to end${end ? ` after ${end}` : ' with this period'} and does not renew.`;
77
189
  }
78
- return `The plan is cancelled and ends about a day after ${end ?? 'the end of this period'}, so this covers only the days left and the new price is not renewed. Undo the cancellation with manage_agent_billing (renew) to keep the plan.`;
190
+ return `The plan is cancelled and ends about a day after ${end ?? 'the end of this period'}, so this covers only the time left and the new price is not renewed. Undo the cancellation with manage_agent_billing (renew) to keep the plan.`;
191
+ }
192
+ /** A card subscription that ends with this period: the plan is set to end, or it moves to wallet billing. */
193
+ function cardEndsWithPeriod(billing) {
194
+ return billing.plan === 'card' && (billing.cancelAtPeriodEnd === true || billing.switchingToWallet);
195
+ }
196
+ function walletSwitchNote(billing, price) {
197
+ return `When this period ends, the plan moves to wallet billing and renews from ${walletName(billing)}${price !== null ? ` at ${formatUsd(price)} a month` : ''}.`;
198
+ }
199
+ // A card subscription that ends with this period is not renewed by card, so its notes point to no next invoice.
200
+ const CARD_ENDS_QUOTE = 'Nothing is charged now. The card subscription is updated to the new price for the rest of this period.';
201
+ // A card plan whose renewal is not known may also end with this period, so its next invoice is named only for a plan that renews by card.
202
+ const CARD_UNKNOWN_QUOTE = 'Nothing is charged now. The card subscription is updated to the new price, and a plan that renews by card shows the prorated difference on its next invoice.';
203
+ /** Only a wallet plan that has ended comes back with renew; any other plan that is not active takes a new one (manage_agent_billing). */
204
+ function renewsBack(billing) {
205
+ return billing.plan === 'prepaid' && billing.status === 'expired';
206
+ }
207
+ /** What a price drop on a wallet plan gives back now, from Wiro's own plan change. */
208
+ function walletCreditNote(change) {
209
+ const used = change.usedCreditsUsd ?? 0;
210
+ // A credit is at most 1 cent for each credit the change takes away unused, so one that takes none away gives nothing back.
211
+ if (change.creditNowUsd === 0 && used > 0 && change.creditsNow !== null && change.creditsNow >= 0) {
212
+ return 'Nothing comes back: the change takes no credits away.';
213
+ }
214
+ if (change.creditNowUsd === null) {
215
+ return 'What comes back is worked out when the change is applied: what the time left is worth, less 1 cent for each credit already used above what the plan keeps.';
216
+ }
217
+ if (change.creditNowUsd > 0) {
218
+ return `${formatUsd(change.creditNowUsd)} goes back to the balances that paid, and each keeps its own end date.`
219
+ + (used > 0 ? ` ${formatUsd(used)} comes off for credits already used above what the plan keeps.` : '');
220
+ }
221
+ return used > 0
222
+ ? 'Nothing comes back: the credits already used are worth more than the time left.'
223
+ : 'Nothing comes back to the wallet now.';
79
224
  }
80
- function quoteBillingNote(billing, delta, estimate, nextPrice) {
81
- const after = billing.cancelAtPeriodEnd
225
+ /**
226
+ * The quote's billing line. An active plan's money and credits come from the
227
+ * plan change Wiro works out on the plan as it is now (change); without it,
228
+ * the line says what happens without a number.
229
+ */
230
+ function quoteBillingNote(billing, delta, change, nextPrice, skillsOff) {
231
+ // A card plan whose renewal is not known gets the new price without a renewal claim.
232
+ const after = billing.cancelAtPeriodEnd === true
82
233
  ? ` ${cancelledNote(billing)}`
83
- : nextPrice !== null ? ` The plan then renews at ${formatUsd(nextPrice)} a month.` : '';
234
+ : billing.switchingToWallet
235
+ ? ` ${walletSwitchNote(billing, nextPrice)}`
236
+ : nextPrice === null
237
+ ? ''
238
+ : billing.cancelAtPeriodEnd === false
239
+ ? ` The plan then renews at ${formatUsd(nextPrice)} a month.`
240
+ : ` The new monthly price is ${formatUsd(nextPrice)}.`;
84
241
  if (billing.plan === 'none') {
85
242
  return 'The agent has no subscription yet, so nothing is charged now. The new monthly price applies once it is subscribed.';
86
243
  }
87
244
  if (billing.status !== 'active') {
88
- return `The subscription is ${billing.status ?? 'not active'}. Skills can only change on an active subscription, so renew it with manage_agent_billing first.`;
245
+ return renewsBack(billing)
246
+ ? `The subscription is ${billing.status ?? 'not active'}. Skills can only change on an active subscription, so renew it with manage_agent_billing first.`
247
+ : `The subscription is ${billing.status ?? 'not active'}. Skills can only change on an active subscription, so start a new plan with manage_agent_billing (subscribe) first.`;
89
248
  }
90
- if (delta === null) {
249
+ if (delta !== null && Math.abs(delta) < 0.005)
250
+ return 'The monthly price does not change, so nothing is charged.';
251
+ if (delta === null || change === null) {
91
252
  return billing.plan === 'prepaid'
92
- ? `The difference for the rest of this period is taken from, or credited to, ${walletName(billing)} when the change is applied. A credit can be spent until this period ends.${after}`
93
- : `Any prorated difference appears on the next invoice.${after}`;
253
+ ? `The difference for the rest of this period is taken from, or credited to, ${walletName(billing)} when the change is applied. A credit goes back to the balances that paid, and each keeps its own end date.${after}`
254
+ : cardEndsWithPeriod(billing)
255
+ ? `${CARD_ENDS_QUOTE}${after}`
256
+ : billing.cancelAtPeriodEnd === null
257
+ ? `${CARD_UNKNOWN_QUOTE}${after}`
258
+ : `Any prorated difference appears on the next invoice.${after}`;
94
259
  }
95
- if (Math.abs(delta) < 0.005)
96
- return 'The monthly price does not change, so nothing is charged.';
97
- if (billing.plan === 'card') {
98
- return `Nothing is charged now. The card subscription is updated and the prorated difference appears on the next invoice.${after}`;
260
+ const ends = creditsEndNote(skillsOff, change.creditsNow, change.creditsThisPeriod);
261
+ // Credits come with a rise when this period gains them. A plan priced again at today's rates can also
262
+ // lose credits, or keep them, while its price rises.
263
+ const gained = change.creditsNow !== null && change.creditsNow > 0 ? change.creditsNow : null;
264
+ const notGained = change.creditsNow !== null && change.creditsNow <= 0 ? ` ${ends}` : '';
265
+ if (!change.wallet) {
266
+ if (delta > 0) {
267
+ const added = gained !== null ? ` ${formatCredits(gained)} credits come now for the rest of this period.` : notGained;
268
+ if (cardEndsWithPeriod(billing))
269
+ return `${CARD_ENDS_QUOTE}${added}${after}`;
270
+ if (billing.cancelAtPeriodEnd === null)
271
+ return `${CARD_UNKNOWN_QUOTE}${added}${after}`;
272
+ return gained !== null
273
+ ? `Nothing is charged now. The prorated difference appears on the next invoice, and ${formatCredits(gained)} credits come now for the rest of this period.${after}`
274
+ : `Nothing is charged now. The prorated difference appears on the next invoice.${notGained}${after}`;
275
+ }
276
+ if (cardEndsWithPeriod(billing))
277
+ return `${CARD_ENDS_QUOTE} ${ends}${after}`;
278
+ if (billing.cancelAtPeriodEnd === null)
279
+ return `${CARD_UNKNOWN_QUOTE} ${ends}${after}`;
280
+ return `The prorated difference comes off the next invoice. ${ends}${after}`;
99
281
  }
100
- const days = billing.daysRemaining !== null ? ` for the ${billing.daysRemaining} days left in this period` : ' for the rest of this period';
101
282
  if (delta > 0) {
102
- return `About ${formatUsd(estimate ?? 0)} is taken from ${walletName(billing)} now${days}.${after}`;
283
+ const charge = formatUsd(change.chargeNowUsd ?? 0);
284
+ const credits = gained !== null ? `, and ${formatCredits(gained)} credits come with it` : '';
285
+ const days = daysLeftText(change.secondsLeft);
286
+ return underOneDayLeft(change.secondsLeft)
287
+ ? `${charge} is taken from ${walletName(billing)} now at the one-day minimum${credits}.${notGained}${after}`
288
+ : `${charge} is taken from ${walletName(billing)} now for the rest of this period${days ? ` (${days})` : ''}${credits}.${notGained}${after}`;
103
289
  }
104
- return `About ${formatUsd(Math.abs(estimate ?? 0))} is credited back to ${walletName(billing)} now${days}. ${CREDIT_SPEND_BY}${after}`;
290
+ return `${walletCreditNote(change)} ${ends}${after}`;
105
291
  }
106
292
  function chargeNote(billing, body) {
107
293
  const charged = toNumber(body.prepaidWalletDelta);
108
- const cancelled = billing.cancelAtPeriodEnd && billing.status === 'active' ? ` ${cancelledNote(billing)}` : '';
294
+ const used = toNumber(body.usedCreditsUsd) ?? 0;
295
+ const creditsNow = isPlainObject(body.pricing) ? toNumber(body.pricing.creditsNow) : null;
296
+ const cancelled = billing.cancelAtPeriodEnd === true && billing.status === 'active' ? ` ${cancelledNote(billing)}` : '';
109
297
  if (charged !== null && charged > 0) {
110
- return { charged, note: `${formatUsd(charged)} was taken from ${walletName(billing)} for the rest of this period.${cancelled}` };
298
+ const credits = creditsNow !== null && creditsNow > 0 ? `, and ${formatCredits(creditsNow)} credits came with it` : '';
299
+ return { charged, note: `${formatUsd(charged)} was taken from ${walletName(billing)} for the rest of this period${credits}.${cancelled}` };
111
300
  }
112
301
  if (charged !== null && charged < 0) {
113
- return { charged, note: `${formatUsd(Math.abs(charged))} was credited to ${walletName(billing)}. ${CREDIT_SPEND_BY}${cancelled}` };
302
+ const terms = creditTerms(body.prepaidCreditParts);
303
+ const usedNote = used > 0 ? ` ${formatUsd(used)} came off for credits already used above what the plan keeps.` : '';
304
+ return { charged, note: `${formatUsd(Math.abs(charged))} was credited to ${walletName(billing)}.${terms ? ` ${terms}` : ''}${usedNote}${cancelled}` };
305
+ }
306
+ // A change that took no credits away had nothing to credit, and nothing failed.
307
+ if (billing.plan === 'prepaid' && used > 0 && creditsNow !== null && creditsNow >= 0) {
308
+ return { charged: null, note: `Nothing came back: the change took no credits away.${cancelled}` };
309
+ }
310
+ // The credits already used came to more than the time left was worth: no credit, and nothing failed.
311
+ // A part Wiro could not add (prepaidCreditFailedUsd) was due all the same: its warning follows, and the
312
+ // credits used took only what Wiro names.
313
+ if (billing.plan === 'prepaid' && used > 0) {
314
+ return (toNumber(body.prepaidCreditFailedUsd) ?? 0) > 0
315
+ ? { charged: null, note: `Nothing came back now. ${formatUsd(used)} came off for credits already used above what the plan keeps.${cancelled}` }
316
+ : { charged: null, note: `Nothing came back: the credits already used were worth more than the time left.${cancelled}` };
114
317
  }
115
318
  if (body.stripeProrationApplied === true) {
116
- return { charged: null, note: `The card subscription was updated; any prorated difference appears on the next invoice.${cancelled}` };
319
+ const moved = isPlainObject(body.pricing) ? toNumber(body.pricing.deltaUsd) : null;
320
+ if (moved !== null && Math.abs(moved) < 0.005) {
321
+ return { charged: null, note: `The monthly price did not change, so nothing was charged.${cancelled}` };
322
+ }
323
+ const added = moved !== null && moved > 0 && creditsNow !== null && creditsNow > 0
324
+ ? ` ${formatCredits(creditsNow)} credits came now for the rest of this period.`
325
+ : '';
326
+ if (cardEndsWithPeriod(billing)) {
327
+ const price = isPlainObject(body.pricing) ? toNumber(body.pricing.newPriceUsd) : null;
328
+ const switching = billing.switchingToWallet ? ` ${walletSwitchNote(billing, price)}` : '';
329
+ return { charged: null, note: `Nothing was charged now. The card subscription was updated to the new price for the rest of this period.${added}${cancelled}${switching}` };
330
+ }
331
+ if (billing.cancelAtPeriodEnd === null) {
332
+ return { charged: null, note: `Nothing was charged now. The card subscription was updated to the new price, and a plan that renews by card shows the prorated difference on its next invoice.${added}` };
333
+ }
334
+ return { charged: null, note: `The card subscription was updated; any prorated difference appears on the next invoice.${added}${cancelled}` };
117
335
  }
118
336
  if (billing.plan === 'none') {
119
337
  return { charged: null, note: 'Nothing was charged: the agent has no subscription yet.' };
@@ -128,6 +346,21 @@ function appliedBillingNote(billing, body) {
128
346
  .filter((warning) => Boolean(warning));
129
347
  return warnings.length > 0 ? { ...result, note: [result.note, ...warnings].join(' ') } : result;
130
348
  }
349
+ /** The sentence for a change whose skills need a Wiro project. */
350
+ function bindProjectNote(needed) {
351
+ return needed.some(entry => entry.credentials.includes(WIRO_PROJECT_CREDENTIAL))
352
+ ? ' bind_agent_project connects a Wiro project, with no key to paste.'
353
+ : '';
354
+ }
355
+ /** How to give the new skills what they still need. A Wiro project comes first, since it takes no key. */
356
+ function stillNeededSteps(needed) {
357
+ const keys = needed.flatMap(entry => entry.credentials);
358
+ const fields = 'set_agent_credentials or connect_agent_oauth.';
359
+ if (!keys.includes(WIRO_PROJECT_CREDENTIAL))
360
+ return `Set them with ${fields}`;
361
+ const others = keys.some(key => key !== WIRO_PROJECT_CREDENTIAL) ? ` Set the others with ${fields}` : '';
362
+ return `Connect the Wiro project with bind_agent_project, with no key to paste.${others}`;
363
+ }
131
364
  function listLine(label, names) {
132
365
  return names.length > 0 ? `**${label}:** ${names.join(', ')}` : null;
133
366
  }
@@ -149,22 +382,29 @@ export function registerApplyAgentSkills(server, client) {
149
382
  title: 'Change an agent\'s skills',
150
383
  description: 'Turn registry skills on or off for a deployed agent, and optionally move it '
151
384
  + 'from Starter to Pro, as one billed change. Skills set the agent\'s monthly price and monthly credits. '
385
+ + 'On a team agent only a team admin or the person who deployed the agent can change its skills; other '
386
+ + 'members are refused and nothing is charged. '
152
387
  + 'Steps: 1) call without confirm. The tool checks the names, adds the skills they '
153
- + 'depend on, and returns the current and new monthly price, an estimate of what is '
154
- + 'charged now and an idempotency_key. Nothing changes yet. 2) Show the price to the '
388
+ + 'depend on, and returns the current and new monthly price, what Wiro charges or credits now, '
389
+ + 'the credits this period gains or loses, and an idempotency_key. Nothing changes yet. 2) Show the price to the '
155
390
  + 'user. 3) After they approve, call again with confirm: true and that idempotency_key. '
156
- + 'The key is tied to the quoted prices: if the price changed since the quote, nothing '
157
- + 'is applied and a new quote with a new key comes back. Reusing the key after a timeout '
391
+ + 'The key is tied to the quoted prices and, on a prepaid plan, the billing period: if '
392
+ + 'either changed since the quote, nothing is applied and a new quote with a new key comes back. Reusing the key after a timeout '
158
393
  + 'is safe, the change is applied once. Billing: on a prepaid plan the monthly difference '
159
- + 'for the days left in the period is taken from the wallet at once, or credited to it '
160
- + 'when the price drops, and that credit can be spent until the period ends; on a card '
161
- + 'plan it is added to the next invoice. A running agent '
162
- + 'restarts once to load the skills. Newly enabled skills may still need credentials '
163
- + '(set_agent_credentials, connect_agent_oauth) and do not create scheduled tasks '
394
+ + 'for the time left in the period is taken from the wallet at once, and the added credits come '
395
+ + 'for that same time; every renewal gives all of them. When the price drops, what the time left '
396
+ + 'is worth goes back to the balances that paid, less 1 cent for each credit already used above '
397
+ + 'what the plan keeps, and each balance keeps its own end date. A skill that comes off loses its '
398
+ + 'credits for the time left; the ones the time already passed paid for stay until the renewal. '
399
+ + 'On a card plan nothing is charged at once: the card subscription is updated to the new price, '
400
+ + 'a plan that renews by card shows the difference on its next invoice, and the added credits come '
401
+ + 'for the time left. A running agent restarts once when a skill turns on or off; a change of the '
402
+ + 'tier alone leaves it running. Newly enabled skills may still need credentials '
403
+ + '(set_agent_credentials, connect_agent_oauth, bind_agent_project for a Wiro project) and do not create scheduled tasks '
164
404
  + '(manage_agent_custom_skill does). Find skill names with list_agent_skills and the '
165
405
  + 'agent\'s current skills with get_my_agent. The agents playbook (get_playbook) has the '
166
406
  + 'full setup walkthrough.',
167
- inputSchema: {
407
+ inputSchema: strictInput({
168
408
  agent_guid: z.string().min(1).describe('The deployed agent\'s guid, from list_my_agents.'),
169
409
  skills: z.record(z.string(), z.boolean())
170
410
  .describe('Skills to change, as name: true to turn on or name: false to turn off, for example {"int-instagram-post": true}. Registry names only (list_agent_skills). Custom skills and scheduled tasks (cs-...) are managed with manage_agent_custom_skill; channels such as Telegram turn on through set_agent_credentials.'),
@@ -175,12 +415,12 @@ export function registerApplyAgentSkills(server, client) {
175
415
  confirm: z.boolean().default(false)
176
416
  .describe('false (default) only returns the quote. true applies the change and charges it; it needs the idempotency_key from the quote.'),
177
417
  idempotency_key: z.string().min(8).max(128).optional()
178
- .describe('With confirm: true, the key from the quote the user approved. It ties the approval to the quoted prices, and repeating the call with it after a timeout never applies the change twice. Omit it to get a quote.'),
179
- },
418
+ .describe('With confirm: true, the key from the quote the user approved. It ties the approval to the quoted prices and, on a prepaid plan, the billing period, and repeating the call with it after a timeout never applies the change twice. Omit it to get a quote.'),
419
+ }),
180
420
  outputSchema,
181
421
  annotations: {
182
422
  readOnlyHint: false,
183
- destructiveHint: false,
423
+ destructiveHint: true,
184
424
  idempotentHint: true,
185
425
  openWorldHint: true,
186
426
  },
@@ -227,7 +467,9 @@ export function registerApplyAgentSkills(server, client) {
227
467
  alreadySet: diff.alreadySet.filter(name => !plan.addedDependencies.includes(name)),
228
468
  };
229
469
  const credentialsNeeded = credentialsNeededFor(diff.enable, registry, row);
230
- const nothingToDo = Object.keys(diff.changes).length === 0 && !tierChange;
470
+ // A running agent restarts only for a skill that turns on or off.
471
+ const skillsMove = Object.keys(diff.changes).length > 0;
472
+ const nothingToDo = !skillsMove && !tierChange;
231
473
  const base = {
232
474
  agentGuid: agent_guid,
233
475
  changes,
@@ -237,8 +479,8 @@ export function registerApplyAgentSkills(server, client) {
237
479
  };
238
480
  const billingBase = { plan: billing.plan, subscriptionStatus: billing.status, wallet: billing.wallet };
239
481
  // An empty change cannot charge anything. With a key issued for this
240
- // same change, Wiro returns the saved result of an earlier call with
241
- // it, which is how a retry after a timeout learns what happened.
482
+ // same change it is still sent, so a repeated confirm gets the result
483
+ // of the earlier call with that key.
242
484
  if (nothingToDo && !(confirm && givenKey)) {
243
485
  return {
244
486
  content: [{ type: 'text', text: `## No change needed\n\nThe skills${tier ? ' and tier' : ''} are already as requested on agent \`${agent_guid}\`.` }],
@@ -251,8 +493,30 @@ export function registerApplyAgentSkills(server, client) {
251
493
  };
252
494
  }
253
495
  const currentPrice = toNumber(row.monthlypriceusd);
254
- const currentCredits = toNumber(row.monthlycredits);
496
+ // The plan's monthly figure is what each renewal grants. This period's can differ after a change inside it.
497
+ const currentCredits = toNumber(row.renewalcredits) ?? toNumber(row.monthlycredits);
255
498
  const key = idempotency_key?.trim() ?? '';
499
+ // Every team member can read a team agent and price a change, but only a
500
+ // team admin or the person who deployed it can apply one. The credit
501
+ // history check refuses the other members up front; the organization's
502
+ // owner passes it and gets Wiro's answer on confirm.
503
+ if (!nothingToDo && await isPlainTeamMember(client, row, extra.signal)
504
+ && await teamAgentBillingRefused(client, agent_guid, extra.signal)) {
505
+ const message = 'Only a team admin or the person who deployed this agent can change its skills, and this '
506
+ + 'connection acts as a team member without those rights. Nothing was quoted, changed or charged. Ask a '
507
+ + 'team admin or the person who deployed the agent to make the change.';
508
+ return {
509
+ content: [{ type: 'text', text: `## Skill change not possible\n\n${message}` }],
510
+ structuredContent: {
511
+ ...base,
512
+ status: 'refused',
513
+ billing: { ...billingBase, note: message },
514
+ restart: UNCHANGED_RESTART,
515
+ error: { code: 97, message },
516
+ },
517
+ isError: true,
518
+ };
519
+ }
256
520
  if (!nothingToDo) {
257
521
  const preview = requireApiSuccess(await client.post('/UserAgent/PricingPreview', {
258
522
  useragentguid: agent_guid,
@@ -263,19 +527,20 @@ export function registerApplyAgentSkills(server, client) {
263
527
  if (newPrice === null) {
264
528
  throw new Error('Wiro did not return the new monthly price of this change, so it cannot be quoted. Try again later.');
265
529
  }
266
- const priceTag = skillPriceTag(currentPrice, newPrice);
530
+ const priceTag = skillPriceTag(currentPrice, newPrice, billing.plan === 'prepaid' ? billing.periodEnd : null);
267
531
  const approved = confirm && givenKey !== null && givenKey.priceTag === priceTag;
268
532
  if (!approved) {
269
533
  const newCredits = toNumber(preview.totalMonthlyCredits);
270
534
  const delta = difference(newPrice, currentPrice);
271
- const estimate = billing.plan === 'prepaid' && billing.status === 'active' && delta !== null
272
- ? estimateProratedUsd(delta, billing.periodEnd)
273
- : null;
274
- const note = quoteBillingNote(billing, delta, estimate, newPrice);
535
+ // What Wiro works out for this change on the plan as it is now. Only an active plan has one.
536
+ const change = billing.plan !== 'none' && billing.status === 'active' ? readPlanChange(preview.planChange) : null;
537
+ const note = quoteBillingNote(billing, delta, change, newPrice, changes.disable.length);
538
+ const walletDrop = change !== null && change.wallet && delta !== null && delta < 0;
275
539
  const quoteKey = givenKey?.priceTag === priceTag ? key : newSkillChangeKey(digest, priceTag);
276
540
  const priceChanged = confirm && givenKey !== null;
277
541
  const blocked = billing.plan !== 'none' && billing.status !== 'active';
278
- const restart = restartNote(status, 'new skills');
542
+ const restart = skillsMove ? restartNote(status, 'skill change') : TIER_ONLY_RESTART;
543
+ const priceNote = todayPriceNote(row, billing, currentPrice, currentCredits, tierChange);
279
544
  const pricing = {
280
545
  currentMonthlyUsd: currentPrice,
281
546
  newMonthlyUsd: newPrice,
@@ -283,11 +548,13 @@ export function registerApplyAgentSkills(server, client) {
283
548
  currentMonthlyCredits: currentCredits,
284
549
  newMonthlyCredits: newCredits,
285
550
  deltaMonthlyCredits: difference(newCredits, currentCredits),
551
+ ...periodFields(change),
552
+ ...(priceNote ? { note: priceNote } : {}),
286
553
  };
287
554
  const nextAction = blocked
288
555
  ? {
289
556
  tool: 'manage_agent_billing',
290
- arguments: { agent_guid, action: 'renew' },
557
+ arguments: { agent_guid, action: renewsBack(billing) ? 'renew' : 'subscribe' },
291
558
  reason: 'The subscription must be active before skills can change.',
292
559
  }
293
560
  : {
@@ -307,7 +574,7 @@ export function registerApplyAgentSkills(server, client) {
307
574
  const lead = !confirm
308
575
  ? null
309
576
  : priceChanged
310
- ? 'The price changed since the quote, so nothing was applied or charged. This is the new quote with a new idempotency_key; show it to the user and ask again.'
577
+ ? 'The price or the billing period changed since the quote, so nothing was applied or charged. This is the new quote with a new idempotency_key; show it to the user and ask again.'
311
578
  : 'Nothing was applied: confirming needs the idempotency_key of a quote, which ties the approval to the price. This is the quote; show it to the user first.';
312
579
  const lines = [
313
580
  '## Skill change quote',
@@ -320,15 +587,21 @@ export function registerApplyAgentSkills(server, client) {
320
587
  listLine('Already as requested', changes.alreadySet),
321
588
  tierChange ? `**Plan:** ${currentTier ?? 'unknown'} to ${tierChange}` : null,
322
589
  `**Monthly price:** ${currentPrice !== null ? formatUsd(currentPrice) : 'unknown'} now, ${formatUsd(newPrice)} after the change${delta !== null ? ` (${delta >= 0 ? '+' : ''}${formatUsd(delta)})` : ''}`,
323
- newCredits !== null ? `**Monthly credits:** ${currentCredits ?? 'unknown'} now, ${newCredits} after the change` : null,
590
+ priceNote ? `**Price update:** ${priceNote}` : null,
591
+ change?.creditsFromRenewal !== null && change?.creditsFromRenewal !== undefined
592
+ ? `**Monthly credits:** ${currentCredits !== null ? formatCredits(currentCredits) : 'unknown'} now, ${formatCredits(change.creditsFromRenewal)} ${renewalWhen(billing)}`
593
+ : newCredits !== null ? `**Monthly credits:** ${currentCredits !== null ? formatCredits(currentCredits) : 'unknown'} now, ${formatCredits(newCredits)} ${creditsWhen(billing)}` : null,
594
+ change && change.creditsNow !== null && change.creditsThisPeriod !== null
595
+ ? `**This period:** ${signedCredits(change.creditsNow)} credits now, ${formatCredits(change.creditsThisPeriod)} in all until ${unixToIso(change.periodEnd) ?? 'the period ends'}`
596
+ : null,
324
597
  `**Billing:** ${note}`,
325
598
  `**Restart:** ${restart}`,
326
599
  credentialsNeeded.length > 0
327
- ? `**Credentials the new skills need:** ${credentialsNeeded.map(entry => `${entry.skill} (${entry.credentials.join(', ')})`).join('; ')}`
600
+ ? `**Credentials the new skills need:** ${credentialsNeeded.map(entry => `${entry.skill} (${entry.credentials.join(', ')})`).join('; ')}.${bindProjectNote(credentialsNeeded)}`
328
601
  : null,
329
602
  '',
330
603
  blocked
331
- ? 'Renew the subscription with manage_agent_billing before applying this change.'
604
+ ? `${renewsBack(billing) ? 'Renew the subscription' : 'Start a new plan'} with manage_agent_billing before applying this change.`
332
605
  : `Nothing has changed yet. After the user approves, call apply_agent_skills again with confirm: true and idempotency_key \`${quoteKey}\`.`,
333
606
  ].filter((line) => line !== null);
334
607
  return {
@@ -339,7 +612,12 @@ export function registerApplyAgentSkills(server, client) {
339
612
  idempotencyKey: quoteKey,
340
613
  ...(priceChanged ? { priceChanged: true } : {}),
341
614
  pricing,
342
- billing: { ...billingBase, estimatedChargeNowUsd: estimate, note },
615
+ billing: {
616
+ ...billingBase,
617
+ estimatedChargeNowUsd: change ? change.chargeNowUsd : null,
618
+ ...(walletDrop ? { creditNowUsd: change.creditNowUsd, usedCreditsUsd: change.usedCreditsUsd } : {}),
619
+ note,
620
+ },
343
621
  ...(Array.isArray(preview.enabledSkills) ? { enabledSkills: preview.enabledSkills.map(String) } : {}),
344
622
  restart,
345
623
  nextAction,
@@ -362,8 +640,15 @@ export function registerApplyAgentSkills(server, client) {
362
640
  throw error;
363
641
  if (!outcomeUnknown(error)) {
364
642
  const failure = describeSkillsApplyError(error, billing.plan);
643
+ // Code 110 is the whole outcome: the next step reads the agent, and the call is not repeated.
644
+ const failedChange = failure.code === 110;
645
+ const nextAction = failedChange
646
+ ? { tool: 'get_my_agent', arguments: { agent_guid }, reason: 'Check the agent\'s skills and monthly price. Do not repeat the change.' }
647
+ : failure.code === 107
648
+ ? { tool: 'apply_agent_skills', arguments: { agent_guid, skills, ...(tier ? { tier } : {}), add_dependencies }, reason: 'Get a new quote for this change, with a new key.' }
649
+ : undefined;
365
650
  const text = [
366
- '## Skill change not applied',
651
+ failedChange ? '## Skill change failed' : '## Skill change not applied',
367
652
  '',
368
653
  failure.message,
369
654
  failure.guidance ? `\n${failure.guidance}` : '',
@@ -376,7 +661,7 @@ export function registerApplyAgentSkills(server, client) {
376
661
  status: 'refused',
377
662
  idempotencyKey: key,
378
663
  billing: { ...billingBase, note: failure.guidance || failure.message },
379
- restart: UNCHANGED_RESTART,
664
+ restart: failedChange ? 'The agent does not restart for this change.' : UNCHANGED_RESTART,
380
665
  error: {
381
666
  code: failure.code,
382
667
  message: failure.message,
@@ -385,13 +670,17 @@ export function registerApplyAgentSkills(server, client) {
385
670
  ...(failure.requiredUsd !== undefined ? { requiredUsd: failure.requiredUsd } : {}),
386
671
  ...(failure.availableUsd !== undefined ? { availableUsd: failure.availableUsd } : {}),
387
672
  },
673
+ ...(nextAction ? { nextAction } : {}),
388
674
  },
389
675
  isError: true,
390
676
  };
391
677
  }
392
678
  const message = errorText(error);
393
679
  if (sentEmpty) {
394
- const note = `The agent's skills are already as requested. Wiro answered with an error (${message}) when asked for the result of the earlier call with this idempotency_key, so what that call charged is not shown here; get_agent_activity with kind usage shows it.`;
680
+ const note = 'The agent\'s skills already match the request, so the change is in place. Do not repeat it. '
681
+ + (billing.plan === 'prepaid'
682
+ ? `What the earlier call with this idempotency_key took from, or credited to, ${walletName(billing)} is not shown here; get_account with recent_transactions shows it.`
683
+ : 'Any price difference follows the plan\'s usual billing.');
395
684
  return {
396
685
  content: [{ type: 'text', text: `## No change needed\n\n${note}` }],
397
686
  structuredContent: {
@@ -403,34 +692,34 @@ export function registerApplyAgentSkills(server, client) {
403
692
  },
404
693
  };
405
694
  }
406
- // The outcome is open: the change may have been saved and charged,
407
- // so the agent is read again before reporting anything.
695
+ // The agent is read again to see whether the change is in place
696
+ // before anything is reported.
408
697
  const holds = await changeHolds(client, agent_guid, plan.requested, tierChange, extra.signal);
409
698
  if (holds === true) {
410
- const note = `Wiro answered with an error (${message}), but the agent's skills are now as requested, so the change was saved. ${billing.plan === 'prepaid' && billing.status === 'active' ? `The difference for the rest of this period was probably taken from, or credited to, ${walletName(billing)}, and a credit can be spent until this period ends; get_agent_activity with kind usage and get_account show it.` : 'Any price difference follows the plan\'s usual billing.'} Do not repeat the change.`;
699
+ const note = `The agent's skills now match the request, so the change is in place. Do not repeat it. ${billing.plan === 'prepaid' && billing.status === 'active' ? `The difference for the rest of this period was probably taken from, or credited to, ${walletName(billing)}; a credit goes back to the balances that paid. get_account with recent_transactions shows the amount, and get_agent_activity with kind usage the credits it moved.` : 'Any price difference follows the plan\'s usual billing.'}`;
411
700
  return {
412
701
  content: [{ type: 'text', text: [
413
- '## Skills updated (with an error report)',
702
+ '## Skills updated',
414
703
  '',
415
704
  `**Agent:** \`${agent_guid}\``,
416
705
  listLine('Turned on', changes.enable),
417
706
  listLine('Turned off', changes.disable),
418
707
  `**Billing:** ${note}`,
419
- `**Restart:** ${restartNote(status, 'new skills')}`,
708
+ `**Restart:** ${skillsMove ? restartNote(status, 'skill change') : TIER_ONLY_RESTART}`,
420
709
  ].filter((line) => line !== null).join('\n') }],
421
710
  structuredContent: {
422
711
  ...base,
423
712
  status: 'applied',
424
713
  idempotencyKey: key,
425
714
  billing: { ...billingBase, chargedNowUsd: null, note },
426
- restart: restartNote(status, 'new skills'),
715
+ restart: skillsMove ? restartNote(status, 'skill change') : TIER_ONLY_RESTART,
427
716
  },
428
717
  };
429
718
  }
430
719
  const unverified = holds === null;
431
720
  const note = unverified
432
- ? `Wiro answered with an error (${message}), and the agent could not be read to check whether the change was saved. Check get_my_agent before trying again. Repeating this call with the same idempotency_key never applies the change twice.`
433
- : `Wiro answered with an error (${message}) and the agent's skills are unchanged, so the change was not applied. Repeating this call with the same idempotency_key is safe.`;
721
+ ? `The change could not be confirmed (${message}). Check get_my_agent to see whether the skills match the request before trying again; repeating this call with the same idempotency_key never applies the change twice.`
722
+ : `The agent's skills are unchanged, so the change was not applied (${message}). Repeating this call with the same idempotency_key is safe.`;
434
723
  return {
435
724
  content: [{ type: 'text', text: `## Skill change ${unverified ? 'not confirmed' : 'not applied'}\n\n${note}\n\nidempotency_key: \`${key}\`` }],
436
725
  structuredContent: {
@@ -438,17 +727,18 @@ export function registerApplyAgentSkills(server, client) {
438
727
  status: 'refused',
439
728
  idempotencyKey: key,
440
729
  billing: { ...billingBase, note },
441
- restart: unverified ? 'The agent restarts if the change was saved.' : UNCHANGED_RESTART,
730
+ restart: !unverified ? UNCHANGED_RESTART : skillsMove ? 'The agent restarts if the change was saved.' : TIER_ONLY_RESTART,
442
731
  error: { code: null, message },
443
732
  },
444
733
  isError: true,
445
734
  };
446
735
  }
447
- // A reused key gets the saved answer of the call that first used it,
448
- // so the live agent is checked against this change.
736
+ // The live agent is checked against this change before it is reported
737
+ // as applied.
449
738
  const holds = await changeHolds(client, agent_guid, plan.requested, tierChange, extra.signal);
450
739
  if (holds === false) {
451
- const note = 'Wiro answered with the saved result of an earlier call that used this idempotency_key, and the agent\'s skills are not as this change asks, so this change was probably not applied by this call. Check get_my_agent; if the skills are still not as requested, call apply_agent_skills again without idempotency_key to get a new quote.';
740
+ const note = 'The agent\'s skills do not match this change, so it is not in place. Check get_my_agent; if they '
741
+ + 'still do not match the request, call apply_agent_skills again without idempotency_key to get a new quote.';
452
742
  return {
453
743
  content: [{ type: 'text', text: `## Skill change not applied\n\n${note}` }],
454
744
  structuredContent: {
@@ -471,6 +761,13 @@ export function registerApplyAgentSkills(server, client) {
471
761
  // result of the earlier call with this key, not a new charge.
472
762
  const replayed = sentEmpty && !applied.noOp;
473
763
  const serverPricing = applied.pricing ?? {};
764
+ // An agent with no plan has no period and no renewal yet, as its quote says.
765
+ const appliedChange = billing.plan === 'none' ? null : readPlanChange({
766
+ creditsNow: serverPricing.creditsNow,
767
+ creditsThisPeriod: serverPricing.creditsThisPeriod,
768
+ creditsFromRenewal: serverPricing.creditsFromRenewal,
769
+ periodEnd: serverPricing.periodEnd,
770
+ });
474
771
  const pricing = applied.noOp
475
772
  ? undefined
476
773
  : {
@@ -480,30 +777,36 @@ export function registerApplyAgentSkills(server, client) {
480
777
  currentMonthlyCredits: toNumber(serverPricing.previousMonthlyCredits),
481
778
  newMonthlyCredits: toNumber(serverPricing.newMonthlyCredits),
482
779
  deltaMonthlyCredits: toNumber(serverPricing.deltaCredits),
780
+ ...periodFields(appliedChange),
483
781
  };
484
782
  const charge = appliedBillingNote(billing, applied);
783
+ const usedCreditsUsd = toNumber(applied.usedCreditsUsd);
485
784
  const billingNote = replayed
486
785
  ? `This change was already applied by an earlier call with this idempotency_key, and nothing new was charged now. That call's billing: ${charge.note}`
487
786
  : charge.note;
787
+ // Wiro says whether this change restarts a running agent; an answer without it goes by the skills that move.
788
+ const restarts = typeof applied.restartTriggered === 'boolean' ? applied.restartTriggered : skillsMove;
488
789
  const restart = applied.noOp || replayed
489
790
  ? UNCHANGED_RESTART
490
- : restartNote(status, 'new skills');
791
+ : restarts ? restartNote(status, 'skill change') : TIER_ONLY_RESTART;
491
792
  const firstCredential = credentialsNeeded[0]?.credentials[0];
492
793
  const nextAction = applied.noOp || replayed
493
794
  ? undefined
494
- : firstCredential
495
- ? {
496
- tool: 'set_agent_credentials',
497
- arguments: { agent_guid, credential: firstCredential },
498
- reason: `See what ${firstCredential} needs before asking the user for it.`,
499
- }
500
- : running
795
+ : firstCredential === WIRO_PROJECT_CREDENTIAL
796
+ ? bindProjectStep(agent_guid, 'Connect the Wiro project the new skills need.')
797
+ : firstCredential
501
798
  ? {
502
- tool: 'get_my_agent',
503
- arguments: { agent_guid, wait_seconds: DEFAULT_WAIT_SECONDS },
504
- reason: 'Check that the agent is running again with the new skills.',
799
+ tool: 'set_agent_credentials',
800
+ arguments: { agent_guid, credential: firstCredential },
801
+ reason: `See what ${firstCredential} needs before asking the user for it.`,
505
802
  }
506
- : undefined;
803
+ : running && restarts
804
+ ? {
805
+ tool: 'get_my_agent',
806
+ arguments: { agent_guid, wait_seconds: DEFAULT_WAIT_SECONDS },
807
+ reason: 'Check that the agent is running again with the new skills.',
808
+ }
809
+ : undefined;
507
810
  const lines = [
508
811
  applied.noOp ? '## No change needed' : replayed ? '## Already applied' : '## Skills updated',
509
812
  '',
@@ -515,11 +818,14 @@ export function registerApplyAgentSkills(server, client) {
515
818
  pricing?.newMonthlyUsd !== undefined && pricing.newMonthlyUsd !== null
516
819
  ? `**Monthly price:** ${formatUsd(pricing.newMonthlyUsd)}${pricing.currentMonthlyUsd !== null ? ` (was ${formatUsd(pricing.currentMonthlyUsd)})` : ''}`
517
820
  : null,
821
+ pricing && appliedChange?.creditsNow !== null && appliedChange?.creditsNow !== undefined && appliedChange.creditsThisPeriod !== null
822
+ ? `**This period:** ${signedCredits(appliedChange.creditsNow)} credits, ${formatCredits(appliedChange.creditsThisPeriod)} in all until ${unixToIso(appliedChange.periodEnd) ?? 'the period ends'}${appliedChange.creditsFromRenewal !== null ? `; ${renewalGives(billing, appliedChange.creditsFromRenewal)}` : ''}`
823
+ : null,
518
824
  `**Billing:** ${billingNote}`,
519
825
  `**Restart:** ${restart}`,
520
826
  holds === null ? 'The agent could not be read afterwards to confirm the new skills; get_my_agent shows them.' : null,
521
827
  credentialsNeeded.length > 0
522
- ? `**Still needed:** credentials for ${credentialsNeeded.map(entry => `${entry.skill} (${entry.credentials.join(', ')})`).join('; ')}. Set them with set_agent_credentials or connect_agent_oauth.`
828
+ ? `**Still needed:** credentials for ${credentialsNeeded.map(entry => `${entry.skill} (${entry.credentials.join(', ')})`).join('; ')}. ${stillNeededSteps(credentialsNeeded)}`
523
829
  : null,
524
830
  ].filter((line) => line !== null);
525
831
  return {
@@ -529,7 +835,13 @@ export function registerApplyAgentSkills(server, client) {
529
835
  status: applied.noOp ? 'no_change' : 'applied',
530
836
  idempotencyKey: key,
531
837
  ...(pricing ? { pricing } : {}),
532
- billing: { ...billingBase, chargedNowUsd: charge.charged, note: billingNote },
838
+ // The saved answer of an earlier call: its charge is in the note, and this call charged nothing.
839
+ billing: {
840
+ ...billingBase,
841
+ chargedNowUsd: replayed ? 0 : charge.charged,
842
+ ...(usedCreditsUsd !== null && usedCreditsUsd > 0 ? { usedCreditsUsd } : {}),
843
+ note: billingNote,
844
+ },
533
845
  ...(Array.isArray(serverPricing.enabledSkills) ? { enabledSkills: serverPricing.enabledSkills.map(String) } : {}),
534
846
  restart,
535
847
  ...(nextAction ? { nextAction } : {}),