toolroll 0.9.54 → 0.9.56

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 (350) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +29 -12
  3. package/dist/action-ledger.d.ts +2 -4
  4. package/dist/action-ledger.js +2 -10
  5. package/dist/agent-onboard.js +4 -1
  6. package/dist/approval-policy.d.ts +5 -5
  7. package/dist/approval-policy.js +4 -4
  8. package/dist/approval-rules-ui.d.ts +3 -2
  9. package/dist/approval-rules-ui.js +17 -22
  10. package/dist/assignment-ui.d.ts +3 -2
  11. package/dist/assignment-ui.js +4 -11
  12. package/dist/backup-ui.d.ts +2 -1
  13. package/dist/backup-ui.js +16 -27
  14. package/dist/brand-mark.d.ts +3 -2
  15. package/dist/brand-mark.js +4 -4
  16. package/dist/browser/workspace.css +1 -1
  17. package/dist/browser/workspace.js +25 -25
  18. package/dist/browser-shell.d.ts +2 -1
  19. package/dist/browser-shell.js +9 -9
  20. package/dist/browser-workspace.d.ts +14 -15
  21. package/dist/browser-workspace.js +0 -3
  22. package/dist/budget-alerts.js +3 -3
  23. package/dist/chat-actions.d.ts +23 -18
  24. package/dist/chat-actions.js +110 -88
  25. package/dist/chat-ask.d.ts +2 -2
  26. package/dist/chat-ask.js +5 -5
  27. package/dist/chat-channel.d.ts +15 -15
  28. package/dist/chat-channel.js +20 -20
  29. package/dist/chat-controls.d.ts +0 -4
  30. package/dist/chat-controls.js +0 -1
  31. package/dist/chat-core.js +1 -1
  32. package/dist/chat-delivery-state.js +2 -2
  33. package/dist/chat-delivery.d.ts +2 -2
  34. package/dist/chat-delivery.js +27 -34
  35. package/dist/chat-flow.js +3 -3
  36. package/dist/chat-messages.d.ts +1 -1
  37. package/dist/chat-messages.js +4 -4
  38. package/dist/chat-migration.d.ts +1 -1
  39. package/dist/chat-migration.js +11 -11
  40. package/dist/chat-polish.d.ts +5 -4
  41. package/dist/chat-polish.js +9 -10
  42. package/dist/chat-rooms.js +3 -3
  43. package/dist/chat-warmth.d.ts +2 -2
  44. package/dist/check-levels-ui.d.ts +2 -1
  45. package/dist/check-levels-ui.js +26 -53
  46. package/dist/cli.d.ts +3 -1
  47. package/dist/cli.js +15 -10
  48. package/dist/coding-shipping-ui.d.ts +2 -1
  49. package/dist/coding-shipping-ui.js +6 -6
  50. package/dist/coding-ui.d.ts +3 -1
  51. package/dist/coding-ui.js +30 -29
  52. package/dist/contracts/chat-actions.d.ts +32 -27
  53. package/dist/contracts/chat-actions.js +29 -11
  54. package/dist/contracts/chat-content.d.ts +1 -1
  55. package/dist/contracts/chat-content.js +1 -1
  56. package/dist/contracts/chat-tables.js +4 -4
  57. package/dist/contracts/cli.d.ts +1 -3
  58. package/dist/contracts/console-api.d.ts +7 -7
  59. package/dist/contracts/console-api.js +13 -12
  60. package/dist/contracts/flow-propose.d.ts +3 -3
  61. package/dist/contracts/flow.d.ts +132 -125
  62. package/dist/contracts/flow.js +27 -12
  63. package/dist/contracts/health.d.ts +1 -3
  64. package/dist/contracts/lead-context.d.ts +1 -1
  65. package/dist/contracts/lead-context.js +2 -2
  66. package/dist/contracts/lead-tools.d.ts +20 -18
  67. package/dist/contracts/lead-tools.js +15 -15
  68. package/dist/contracts/stage-output.d.ts +1 -1
  69. package/dist/contracts/stage-output.js +1 -1
  70. package/dist/contracts/store-json.d.ts +23 -22
  71. package/dist/contracts/store-json.js +17 -17
  72. package/dist/contracts/{teammate-turn.d.ts → subagent-turn.d.ts} +5 -5
  73. package/dist/contracts/{teammate-turn.js → subagent-turn.js} +7 -7
  74. package/dist/control-ui.d.ts +5 -4
  75. package/dist/control-ui.js +9 -34
  76. package/dist/converse.d.ts +23 -23
  77. package/dist/converse.js +28 -28
  78. package/dist/coordinator-proposals.d.ts +2 -2
  79. package/dist/coordinator-proposals.js +1 -1
  80. package/dist/coordinator.js +1 -1
  81. package/dist/credentials-ui.d.ts +5 -4
  82. package/dist/credentials-ui.js +23 -24
  83. package/dist/demo-chat.d.ts +3 -3
  84. package/dist/demo-chat.js +29 -76
  85. package/dist/deprecations.d.ts +12 -0
  86. package/dist/deprecations.js +20 -0
  87. package/dist/desktop-update-gate.js +8 -4
  88. package/dist/desktop-update.js +6 -1
  89. package/dist/discord-chat.js +6 -6
  90. package/dist/discord-settings.d.ts +3 -2
  91. package/dist/discord-settings.js +17 -42
  92. package/dist/dispatch.d.ts +1 -1
  93. package/dist/dispatch.js +1 -1
  94. package/dist/envelope.js +10 -1
  95. package/dist/evidence-pack.d.ts +2 -1
  96. package/dist/evidence-pack.js +45 -49
  97. package/dist/export-ui.d.ts +7 -2
  98. package/dist/export-ui.js +4 -10
  99. package/dist/export.js +7 -7
  100. package/dist/flow-engine.d.ts +2 -2
  101. package/dist/flow-engine.js +21 -21
  102. package/dist/flow-gallery-ui.d.ts +4 -3
  103. package/dist/flow-gallery-ui.js +28 -43
  104. package/dist/flow-insights.d.ts +1 -1
  105. package/dist/flow-share.js +13 -8
  106. package/dist/flow-starters-ui.d.ts +2 -1
  107. package/dist/flow-starters-ui.js +12 -15
  108. package/dist/flow-steps.d.ts +5 -5
  109. package/dist/flow-steps.js +19 -19
  110. package/dist/flow-triggers.d.ts +2 -1
  111. package/dist/flow-triggers.js +17 -15
  112. package/dist/flows-cli.js +1 -1
  113. package/dist/flows-ui.d.ts +4 -3
  114. package/dist/flows-ui.js +38 -49
  115. package/dist/flows.js +21 -20
  116. package/dist/guides.js +112 -6
  117. package/dist/html.d.ts +76 -0
  118. package/dist/html.js +173 -0
  119. package/dist/integrations-ui.d.ts +2 -1
  120. package/dist/integrations-ui.js +19 -23
  121. package/dist/invoke.js +1 -1
  122. package/dist/kits-ui.d.ts +5 -4
  123. package/dist/kits-ui.js +22 -24
  124. package/dist/kits.d.ts +12 -12
  125. package/dist/kits.js +31 -31
  126. package/dist/knowledge-ui.d.ts +6 -5
  127. package/dist/knowledge-ui.js +26 -25
  128. package/dist/{mate-cli.d.ts → lead-cli.d.ts} +11 -11
  129. package/dist/{mate-cli.js → lead-cli.js} +38 -38
  130. package/dist/lead-commitments.js +4 -4
  131. package/dist/lead-context.d.ts +12 -2
  132. package/dist/lead-context.js +15 -17
  133. package/dist/{mate-contract.d.ts → lead-contract.d.ts} +3 -3
  134. package/dist/{mate-contract.js → lead-contract.js} +7 -6
  135. package/dist/{mate-doors.d.ts → lead-doors.d.ts} +6 -6
  136. package/dist/{mate-doors.js → lead-doors.js} +24 -24
  137. package/dist/lead-follow.d.ts +5 -5
  138. package/dist/lead-follow.js +9 -9
  139. package/dist/lead-people.d.ts +5 -5
  140. package/dist/lead-people.js +14 -14
  141. package/dist/{mate-progress.d.ts → lead-progress.d.ts} +9 -9
  142. package/dist/{mate-progress.js → lead-progress.js} +3 -3
  143. package/dist/{mate-tools.d.ts → lead-tools.d.ts} +19 -19
  144. package/dist/{mate-tools.js → lead-tools.js} +109 -101
  145. package/dist/{mate.d.ts → lead.d.ts} +27 -27
  146. package/dist/{mate.js → lead.js} +102 -102
  147. package/dist/ledger-view.d.ts +2 -1
  148. package/dist/ledger-view.js +27 -29
  149. package/dist/limits-ui.d.ts +3 -2
  150. package/dist/limits-ui.js +8 -8
  151. package/dist/live-bus.d.ts +50 -8
  152. package/dist/live-bus.js +133 -50
  153. package/dist/mcp-connect.d.ts +1 -1
  154. package/dist/mcp-connect.js +3 -3
  155. package/dist/mcp-core.js +1 -1
  156. package/dist/mcp-oauth.js +35 -36
  157. package/dist/memory-pass.d.ts +2 -2
  158. package/dist/memory-pass.js +7 -7
  159. package/dist/models-ui.d.ts +2 -1
  160. package/dist/models-ui.js +25 -35
  161. package/dist/monitoring-ui.d.ts +4 -3
  162. package/dist/monitoring-ui.js +13 -26
  163. package/dist/notification-rows.d.ts +2 -2
  164. package/dist/openrouter-models.d.ts +2 -1
  165. package/dist/openrouter-models.js +5 -9
  166. package/dist/operate.d.ts +3 -3
  167. package/dist/operate.js +46 -41
  168. package/dist/people-audit-ui.d.ts +2 -1
  169. package/dist/people-audit-ui.js +13 -13
  170. package/dist/policy-ui.d.ts +3 -2
  171. package/dist/policy-ui.js +17 -23
  172. package/dist/policy.d.ts +1 -1
  173. package/dist/principal.d.ts +1 -1
  174. package/dist/principal.js +2 -2
  175. package/dist/project-delete-ui.d.ts +4 -3
  176. package/dist/project-delete-ui.js +16 -28
  177. package/dist/project-delete.d.ts +1 -1
  178. package/dist/project-delete.js +26 -26
  179. package/dist/project-memory.js +1 -1
  180. package/dist/project-tools.d.ts +1 -1
  181. package/dist/project-tools.js +1 -1
  182. package/dist/pull-request-ui.d.ts +2 -1
  183. package/dist/pull-request-ui.js +17 -32
  184. package/dist/recipe-ui.d.ts +8 -7
  185. package/dist/recipe-ui.js +39 -32
  186. package/dist/release-adapters.d.ts +60 -0
  187. package/dist/release-adapters.js +435 -0
  188. package/dist/release-cli.d.ts +24 -0
  189. package/dist/release-cli.js +181 -0
  190. package/dist/release-gate.d.ts +80 -0
  191. package/dist/release-gate.js +150 -0
  192. package/dist/release-links.d.ts +29 -0
  193. package/dist/release-links.js +102 -0
  194. package/dist/release.d.ts +331 -0
  195. package/dist/release.js +550 -0
  196. package/dist/reply-shape.d.ts +2 -1
  197. package/dist/reply-shape.js +5 -5
  198. package/dist/repository-context-ui.d.ts +2 -1
  199. package/dist/repository-context-ui.js +2 -7
  200. package/dist/request-budget-ui.d.ts +2 -1
  201. package/dist/request-budget-ui.js +16 -16
  202. package/dist/result-review.d.ts +2 -3
  203. package/dist/result-review.js +3 -9
  204. package/dist/retention-ui.d.ts +7 -1
  205. package/dist/retention-ui.js +7 -11
  206. package/dist/retention.js +4 -4
  207. package/dist/scope.d.ts +2 -2
  208. package/dist/scout-report.d.ts +1 -1
  209. package/dist/scout-report.js +1 -1
  210. package/dist/serve.d.ts +11 -2
  211. package/dist/serve.js +105 -99
  212. package/dist/server/chat.d.ts +7 -10
  213. package/dist/server/chat.js +984 -1021
  214. package/dist/server/chrome.d.ts +291 -0
  215. package/dist/server/chrome.js +747 -0
  216. package/dist/server/console.css +2819 -0
  217. package/dist/server/flows.d.ts +2 -6
  218. package/dist/server/flows.js +854 -773
  219. package/dist/server/guards.d.ts +1 -1
  220. package/dist/server/guards.js +8 -5
  221. package/dist/server/handler-context.d.ts +1 -1
  222. package/dist/server/handler-context.js +1 -1
  223. package/dist/server/handler-registry.d.ts +8 -6
  224. package/dist/server/handler-registry.js +14 -2
  225. package/dist/server/http.d.ts +274 -0
  226. package/dist/server/http.js +239 -0
  227. package/dist/server/live.d.ts +34 -0
  228. package/dist/server/live.js +309 -0
  229. package/dist/server/pages.d.ts +2 -6
  230. package/dist/server/pages.js +808 -789
  231. package/dist/server/people-tokens.d.ts +1 -6
  232. package/dist/server/people-tokens.js +581 -573
  233. package/dist/server/remote.d.ts +4 -7
  234. package/dist/server/remote.js +39 -70
  235. package/dist/server/render-chat.d.ts +402 -0
  236. package/dist/server/render-chat.js +927 -0
  237. package/dist/server/render-pages.d.ts +535 -0
  238. package/dist/server/render-pages.js +1277 -0
  239. package/dist/server/render-people.d.ts +32 -0
  240. package/dist/server/render-people.js +121 -0
  241. package/dist/server/render-results.d.ts +744 -0
  242. package/dist/server/render-results.js +2237 -0
  243. package/dist/server/render-settings.d.ts +154 -0
  244. package/dist/server/render-settings.js +580 -0
  245. package/dist/server/render-tasks.d.ts +632 -0
  246. package/dist/server/render-tasks.js +2345 -0
  247. package/dist/server/request-context.d.ts +46 -0
  248. package/dist/server/request-context.js +17 -0
  249. package/dist/server/route-table.d.ts +25 -3
  250. package/dist/server/route-table.js +167 -77
  251. package/dist/server/runtime.d.ts +28 -20
  252. package/dist/server/runtime.js +7 -1
  253. package/dist/server/session.d.ts +138 -0
  254. package/dist/server/session.js +94 -0
  255. package/dist/server/settings.d.ts +1 -5
  256. package/dist/server/settings.js +2186 -2121
  257. package/dist/server/task-views.d.ts +11 -4
  258. package/dist/server/task-views.js +26 -22
  259. package/dist/server/tasks.d.ts +9 -9
  260. package/dist/server/tasks.js +1489 -1420
  261. package/dist/server-telemetry.d.ts +4 -3
  262. package/dist/server-telemetry.js +4 -8
  263. package/dist/setup-guide.d.ts +2 -2
  264. package/dist/skills-ui.d.ts +4 -3
  265. package/dist/skills-ui.js +14 -17
  266. package/dist/skills.d.ts +1 -1
  267. package/dist/skills.js +2 -1
  268. package/dist/slack-chat.d.ts +2 -2
  269. package/dist/slack-chat.js +6 -6
  270. package/dist/slack-settings.d.ts +3 -2
  271. package/dist/slack-settings.js +17 -42
  272. package/dist/spend-ui.d.ts +5 -4
  273. package/dist/spend-ui.js +17 -40
  274. package/dist/spend.d.ts +14 -14
  275. package/dist/spend.js +31 -31
  276. package/dist/sso-ui.d.ts +3 -2
  277. package/dist/sso-ui.js +26 -27
  278. package/dist/storage-ui.d.ts +2 -1
  279. package/dist/storage-ui.js +14 -33
  280. package/dist/store-baseline.d.ts +19 -0
  281. package/dist/store-baseline.js +2873 -0
  282. package/dist/store.d.ts +197 -249
  283. package/dist/store.js +747 -3084
  284. package/dist/subagent-admin.d.ts +53 -0
  285. package/dist/{teammate-admin.js → subagent-admin.js} +37 -37
  286. package/dist/{teammate-desk.d.ts → subagent-desk.d.ts} +22 -23
  287. package/dist/{teammate-desk.js → subagent-desk.js} +26 -35
  288. package/dist/{teammate-memory.d.ts → subagent-memory.d.ts} +17 -17
  289. package/dist/{teammate-memory.js → subagent-memory.js} +41 -41
  290. package/dist/{teammate-question.d.ts → subagent-question.d.ts} +6 -6
  291. package/dist/{teammate-question.js → subagent-question.js} +17 -17
  292. package/dist/{teammate-tools.d.ts → subagent-tools.d.ts} +18 -18
  293. package/dist/{teammate-tools.js → subagent-tools.js} +36 -36
  294. package/dist/{teammate-week.d.ts → subagent-week.d.ts} +10 -10
  295. package/dist/{teammate-week.js → subagent-week.js} +23 -23
  296. package/dist/{teammate-work.d.ts → subagent-work.d.ts} +21 -21
  297. package/dist/{teammate-work.js → subagent-work.js} +50 -50
  298. package/dist/subagents-ui.d.ts +22 -0
  299. package/dist/subagents-ui.js +137 -0
  300. package/dist/{teammates.d.ts → subagents.d.ts} +11 -11
  301. package/dist/{teammates.js → subagents.js} +21 -21
  302. package/dist/subscription-chat.d.ts +13 -13
  303. package/dist/subscription-chat.js +19 -19
  304. package/dist/surface.d.ts +19 -2
  305. package/dist/surface.js +20 -33
  306. package/dist/task-composer.d.ts +2 -1
  307. package/dist/task-composer.js +7 -7
  308. package/dist/task-status.d.ts +4 -3
  309. package/dist/task-status.js +15 -16
  310. package/dist/team-cli.d.ts +1 -0
  311. package/dist/team-cli.js +14 -2
  312. package/dist/team-http.d.ts +5 -9
  313. package/dist/team-http.js +5 -63
  314. package/dist/team-leads.js +13 -13
  315. package/dist/team-runtime.d.ts +2 -2
  316. package/dist/team-runtime.js +8 -8
  317. package/dist/team-ui.d.ts +2 -1
  318. package/dist/team-ui.js +2 -5
  319. package/dist/teams-chat.js +6 -6
  320. package/dist/teams-settings.d.ts +3 -2
  321. package/dist/teams-settings.js +20 -31
  322. package/dist/telegram-flow.d.ts +3 -3
  323. package/dist/telegram-flow.js +3 -3
  324. package/dist/{telegram-mate.d.ts → telegram-lead.d.ts} +5 -5
  325. package/dist/{telegram-mate.js → telegram-lead.js} +27 -27
  326. package/dist/telegram-settings.d.ts +3 -2
  327. package/dist/telegram-settings.js +14 -25
  328. package/dist/telegram-status.js +1 -1
  329. package/dist/telegram-team.d.ts +1 -1
  330. package/dist/telegram-team.js +5 -5
  331. package/dist/telegram.d.ts +1 -1
  332. package/dist/telegram.js +9 -30
  333. package/dist/text-limits.d.ts +18 -18
  334. package/dist/text-limits.js +18 -18
  335. package/dist/toolroll-update-ui.d.ts +3 -2
  336. package/dist/toolroll-update-ui.js +35 -39
  337. package/dist/toolroll-update.d.ts +8 -0
  338. package/dist/toolroll-update.js +45 -8
  339. package/dist/tools-ui.d.ts +5 -2
  340. package/dist/tools-ui.js +31 -31
  341. package/dist/when-html.d.ts +11 -3
  342. package/dist/when-html.js +5 -6
  343. package/dist/workspace-ui.d.ts +4 -2
  344. package/dist/workspace-ui.js +10 -10
  345. package/package.json +1 -2
  346. package/dist/server/shared.d.ts +0 -3151
  347. package/dist/server/shared.js +0 -12001
  348. package/dist/teammate-admin.d.ts +0 -53
  349. package/dist/teammates-ui.d.ts +0 -20
  350. package/dist/teammates-ui.js +0 -166
package/dist/spend.d.ts CHANGED
@@ -7,8 +7,8 @@
7
7
  * Models), or at the provider's highest listed price when the model isn't
8
8
  * listed; work with no tokens and no cost is "unpriced" and said so.
9
9
  *
10
- * Spend counts toward a project (a run's task, a teammate's, a project chat),
11
- * a person (who filed the task, or had the chat), a teammate (its turns, and
10
+ * Spend counts toward a project (a run's task, a subagent's, a project chat),
11
+ * a person (who filed the task, or had the chat), a subagent (its turns, and
12
12
  * tasks it filed), and the whole installation. Budgets are per calendar
13
13
  * month (UTC).
14
14
  */
@@ -30,7 +30,7 @@ export declare function claudeBillingFrom(seen: {
30
30
  model: string | null;
31
31
  planWindows: boolean;
32
32
  }): Billing | null;
33
- /** How Claude bills on this computer when Toolroll gives it no key (teammate turns, drafts): as its keyless runs
33
+ /** How Claude bills on this computer when Toolroll gives it no key (subagent turns, drafts): as its keyless runs
34
34
  * were last seen, and its plan until one has been. */
35
35
  export declare function claudeMachineBilling(db: Database): Billing;
36
36
  /** Whether API work on this provider can be priced: it reports its cost, or the catalogue has its prices. */
@@ -60,8 +60,8 @@ export declare function priceWork(db: Database, work: {
60
60
  source: PriceSource;
61
61
  price: Price | null;
62
62
  };
63
- export declare const SPEND_SCHEMA = "\nCREATE TABLE IF NOT EXISTS run_spend (\n run INTEGER PRIMARY KEY,\n microusd INTEGER,\n source TEXT NOT NULL CHECK (source IN ('reported', 'estimated', 'unpriced', 'subscription')),\n billing TEXT NOT NULL CHECK (billing IN ('subscription', 'api-key')),\n -- 1 once the run's own evidence set it (a key it was given, what its CLI said); a later turn can only make it a key.\n billing_fixed INTEGER NOT NULL DEFAULT 0,\n price_model TEXT,\n input_usd REAL,\n output_usd REAL,\n priced_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS budget (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n scope_kind TEXT NOT NULL CHECK (scope_kind IN ('installation', 'project', 'person', 'teammate')),\n scope_key TEXT NOT NULL,\n limit_microusd INTEGER NOT NULL CHECK (limit_microusd > 0),\n hard_stop INTEGER NOT NULL DEFAULT 1,\n created_by TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_by TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n removed_by TEXT,\n removed_at TEXT\n);\nCREATE UNIQUE INDEX IF NOT EXISTS budget_live ON budget (scope_kind, scope_key) WHERE removed_at IS NULL;\nCREATE TABLE IF NOT EXISTS provider_limit (\n provider TEXT NOT NULL,\n window TEXT NOT NULL,\n used_percent REAL NOT NULL,\n window_minutes INTEGER,\n resets_at TEXT,\n reached INTEGER NOT NULL DEFAULT 0,\n plan TEXT,\n observed_at TEXT NOT NULL,\n PRIMARY KEY (provider, window)\n);\n-- Spend that isn't a run, a teammate turn or a chat: a flow's Claude draft.\nCREATE TABLE IF NOT EXISTS side_spend (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n kind TEXT NOT NULL CHECK (kind IN ('draft')),\n repo TEXT,\n provider TEXT NOT NULL,\n model TEXT,\n cost_usd REAL,\n billing TEXT NOT NULL CHECK (billing IN ('subscription', 'api-key')),\n at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS side_spend_at ON side_spend (at);\nCREATE TABLE IF NOT EXISTS provider_account (\n provider TEXT PRIMARY KEY,\n billing TEXT NOT NULL CHECK (billing IN ('subscription', 'api-key')),\n observed_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS chat_turn_created ON chat_turn (created_at);\nCREATE INDEX IF NOT EXISTS mate_turn_created ON mate_turn (created_at);\nCREATE INDEX IF NOT EXISTS teammate_turn_at ON teammate_turn (at);\n";
64
- export type BudgetScope = "installation" | "project" | "person" | "teammate";
63
+ export declare const SPEND_SCHEMA = "\nCREATE TABLE IF NOT EXISTS run_spend (\n run INTEGER PRIMARY KEY,\n microusd INTEGER,\n source TEXT NOT NULL CHECK (source IN ('reported', 'estimated', 'unpriced', 'subscription')),\n billing TEXT NOT NULL CHECK (billing IN ('subscription', 'api-key')),\n -- 1 once the run's own evidence set it (a key it was given, what its CLI said); a later turn can only make it a key.\n billing_fixed INTEGER NOT NULL DEFAULT 0,\n price_model TEXT,\n input_usd REAL,\n output_usd REAL,\n priced_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS budget (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n scope_kind TEXT NOT NULL CHECK (scope_kind IN ('installation', 'project', 'person', 'subagent')),\n scope_key TEXT NOT NULL,\n limit_microusd INTEGER NOT NULL CHECK (limit_microusd > 0),\n hard_stop INTEGER NOT NULL DEFAULT 1,\n created_by TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_by TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n removed_by TEXT,\n removed_at TEXT\n);\nCREATE UNIQUE INDEX IF NOT EXISTS budget_live ON budget (scope_kind, scope_key) WHERE removed_at IS NULL;\nCREATE TABLE IF NOT EXISTS provider_limit (\n provider TEXT NOT NULL,\n window TEXT NOT NULL,\n used_percent REAL NOT NULL,\n window_minutes INTEGER,\n resets_at TEXT,\n reached INTEGER NOT NULL DEFAULT 0,\n plan TEXT,\n observed_at TEXT NOT NULL,\n PRIMARY KEY (provider, window)\n);\n-- Spend that isn't a run, a subagent turn or a chat: a flow's Claude draft.\nCREATE TABLE IF NOT EXISTS side_spend (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n kind TEXT NOT NULL CHECK (kind IN ('draft')),\n repo TEXT,\n provider TEXT NOT NULL,\n model TEXT,\n cost_usd REAL,\n billing TEXT NOT NULL CHECK (billing IN ('subscription', 'api-key')),\n at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS side_spend_at ON side_spend (at);\nCREATE TABLE IF NOT EXISTS provider_account (\n provider TEXT PRIMARY KEY,\n billing TEXT NOT NULL CHECK (billing IN ('subscription', 'api-key')),\n observed_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS chat_turn_created ON chat_turn (created_at);\nCREATE INDEX IF NOT EXISTS mate_turn_created ON mate_turn (created_at);\nCREATE INDEX IF NOT EXISTS subagent_turn_at ON subagent_turn (at);\n";
64
+ export type BudgetScope = "installation" | "project" | "person" | "subagent";
65
65
  export type Budget = {
66
66
  id: number;
67
67
  scope: BudgetScope;
@@ -74,12 +74,12 @@ export type Budget = {
74
74
  /** One piece of spend, whatever made it, with what it counts toward. */
75
75
  export type SpendItem = {
76
76
  at: string;
77
- kind: "run" | "teammate" | "chat" | "sort" | "draft";
77
+ kind: "run" | "subagent" | "chat" | "sort" | "draft";
78
78
  microusd: number | null;
79
79
  source: PriceSource;
80
80
  project: string | null;
81
81
  person: string | null;
82
- teammate: number | null;
82
+ subagent: number | null;
83
83
  provider: string;
84
84
  model: string | null;
85
85
  tokensIn: number | null;
@@ -100,9 +100,9 @@ export declare function monthNamed(name: string | null): {
100
100
  to: string;
101
101
  name: string;
102
102
  } | null;
103
- /** Every teammate's name by id (its handle when its soul file names none). */
104
- export declare function teammateNames(db: Database): Map<number, string>;
105
- export declare function teammateFilers(db: Database): (repo: string | null, filedBy: string | null, kind: string | null) => number | null;
103
+ /** Every subagent's name by id (its handle when its soul file names none). */
104
+ export declare function subagentNames(db: Database): Map<number, string>;
105
+ export declare function subagentFilers(db: Database): (repo: string | null, filedBy: string | null, kind: string | null) => number | null;
106
106
  /** Who a task's work counts toward: its own filer, or, for a revision filed by automation (an automatic repair), the
107
107
  * filer of the task it revises, up its ancestry (bounded). */
108
108
  export declare function filersOf(db: Database): (taskRef: number) => {
@@ -110,7 +110,7 @@ export declare function filersOf(db: Database): (taskRef: number) => {
110
110
  filedBy: string | null;
111
111
  kind: string | null;
112
112
  };
113
- /** Every piece of spend in [from, to): runs (by when they started), teammate turns and chat turns. `modeOf` says how
113
+ /** Every piece of spend in [from, to): runs (by when they started), subagent turns and chat turns. `modeOf` says how
114
114
  * a provider bills when a record doesn't (older rows): its current login or key. */
115
115
  export declare function spendItems(db: Database, from: string, to: string, modeOf?: (provider: string) => Billing): SpendItem[];
116
116
  /** Whether a piece of spend counts toward a budget. */
@@ -134,9 +134,9 @@ export type BudgetHold = {
134
134
  remainingMicrousd: number | null;
135
135
  };
136
136
  /** Why work waits on a budget, in words. */
137
- export declare function budgetHoldWords(hold: BudgetHold, month: string, teammateName?: string): string;
137
+ export declare function budgetHoldWords(hold: BudgetHold, month: string, subagentName?: string): string;
138
138
  /** Each budget's month so far: what counted toward it, and how many pieces of work had no price. */
139
139
  export declare function budgetStates(budgets: readonly Budget[], items: readonly SpendItem[]): BudgetState[];
140
- /** A budget in words: "The shop project's", "alex's", "Maya's (teammate)", "The whole installation's". */
141
- export declare function budgetLabel(budget: Pick<Budget, "scope" | "key">, teammateName?: string): string;
140
+ /** A budget in words: "The shop project's", "alex's", "Maya's (subagent)", "The whole installation's". */
141
+ export declare function budgetLabel(budget: Pick<Budget, "scope" | "key">, subagentName?: string): string;
142
142
  export declare const usd: (microusd: number | null) => string;
package/dist/spend.js CHANGED
@@ -30,7 +30,7 @@ export function claudeBillingFrom(seen) {
30
30
  return "subscription";
31
31
  return seen.keySource === "none" ? "api-key" : null;
32
32
  }
33
- /** How Claude bills on this computer when Toolroll gives it no key (teammate turns, drafts): as its keyless runs
33
+ /** How Claude bills on this computer when Toolroll gives it no key (subagent turns, drafts): as its keyless runs
34
34
  * were last seen, and its plan until one has been. */
35
35
  export function claudeMachineBilling(db) {
36
36
  return seenBilling(db, "claude") ?? "subscription";
@@ -100,7 +100,7 @@ CREATE TABLE IF NOT EXISTS run_spend (
100
100
  );
101
101
  CREATE TABLE IF NOT EXISTS budget (
102
102
  id INTEGER PRIMARY KEY AUTOINCREMENT,
103
- scope_kind TEXT NOT NULL CHECK (scope_kind IN ('installation', 'project', 'person', 'teammate')),
103
+ scope_kind TEXT NOT NULL CHECK (scope_kind IN ('installation', 'project', 'person', 'subagent')),
104
104
  scope_key TEXT NOT NULL,
105
105
  limit_microusd INTEGER NOT NULL CHECK (limit_microusd > 0),
106
106
  hard_stop INTEGER NOT NULL DEFAULT 1,
@@ -123,7 +123,7 @@ CREATE TABLE IF NOT EXISTS provider_limit (
123
123
  observed_at TEXT NOT NULL,
124
124
  PRIMARY KEY (provider, window)
125
125
  );
126
- -- Spend that isn't a run, a teammate turn or a chat: a flow's Claude draft.
126
+ -- Spend that isn't a run, a subagent turn or a chat: a flow's Claude draft.
127
127
  CREATE TABLE IF NOT EXISTS side_spend (
128
128
  id INTEGER PRIMARY KEY AUTOINCREMENT,
129
129
  kind TEXT NOT NULL CHECK (kind IN ('draft')),
@@ -142,7 +142,7 @@ CREATE TABLE IF NOT EXISTS provider_account (
142
142
  );
143
143
  CREATE INDEX IF NOT EXISTS chat_turn_created ON chat_turn (created_at);
144
144
  CREATE INDEX IF NOT EXISTS mate_turn_created ON mate_turn (created_at);
145
- CREATE INDEX IF NOT EXISTS teammate_turn_at ON teammate_turn (at);
145
+ CREATE INDEX IF NOT EXISTS subagent_turn_at ON subagent_turn (at);
146
146
  `;
147
147
  /** The calendar month (UTC) `at` falls in: [start, end) as ISO strings, and its name (YYYY-MM). */
148
148
  export function monthOf(at) {
@@ -156,25 +156,25 @@ export function monthNamed(name) {
156
156
  return null;
157
157
  return monthOf(new Date(`${name}-01T00:00:00.000Z`));
158
158
  }
159
- /** The teammate behind a filing: tasks a teammate files are filed as its name ("Maya (AI)", from its soul file), in
160
- * its project. A later teammate with the same name and project wins, as it would read on the card. */
161
- /** A teammate's name as its soul file gives it ("Maya"), or null. */
159
+ /** The subagent behind a filing: tasks a subagent files are filed as its name ("Maya (AI)", from its soul file), in
160
+ * its project. A later subagent with the same name and project wins, as it would read on the card. */
161
+ /** A subagent's name as its soul file gives it ("Maya"), or null. */
162
162
  const soulName = (soul) => {
163
163
  const front = /^---\s*\n([\s\S]*?)\n---/.exec(String(soul ?? ""));
164
164
  return front === null ? null : /^name:\s*(.+?)\s*$/m.exec(front[1])?.[1] ?? null;
165
165
  };
166
- /** Every teammate's name by id (its handle when its soul file names none). */
167
- export function teammateNames(db) {
168
- return new Map(db.prepare("SELECT id, handle, soul FROM teammate").all().map(row => [Number(row["id"]), soulName(row["soul"]) ?? String(row["handle"])]));
166
+ /** Every subagent's name by id (its handle when its soul file names none). */
167
+ export function subagentNames(db) {
168
+ return new Map(db.prepare("SELECT id, handle, soul FROM subagent").all().map(row => [Number(row["id"]), soulName(row["soul"]) ?? String(row["handle"])]));
169
169
  }
170
- export function teammateFilers(db) {
170
+ export function subagentFilers(db) {
171
171
  const byName = new Map();
172
- for (const row of db.prepare("SELECT id, repo, handle, soul FROM teammate ORDER BY id").all()) {
172
+ for (const row of db.prepare("SELECT id, repo, handle, soul FROM subagent ORDER BY id").all()) {
173
173
  const name = soulName(row["soul"]);
174
174
  for (const one of [String(row["handle"]), ...(name === null ? [] : [name])])
175
175
  byName.set(`${String(row["repo"])}\n${one} (AI)`, Number(row["id"]));
176
176
  }
177
- return (repo, filedBy, kind) => kind !== "teammate" || repo === null || filedBy === null ? null : byName.get(`${repo}\n${filedBy}`) ?? null;
177
+ return (repo, filedBy, kind) => kind !== "subagent" || repo === null || filedBy === null ? null : byName.get(`${repo}\n${filedBy}`) ?? null;
178
178
  }
179
179
  /** Who a task's work counts toward: its own filer, or, for a revision filed by automation (an automatic repair), the
180
180
  * filer of the task it revises, up its ancestry (bounded). */
@@ -198,10 +198,10 @@ export function filersOf(db) {
198
198
  return found;
199
199
  };
200
200
  }
201
- /** Every piece of spend in [from, to): runs (by when they started), teammate turns and chat turns. `modeOf` says how
201
+ /** Every piece of spend in [from, to): runs (by when they started), subagent turns and chat turns. `modeOf` says how
202
202
  * a provider bills when a record doesn't (older rows): its current login or key. */
203
203
  export function spendItems(db, from, to, modeOf = provider => billingOf(provider, db)) {
204
- const teammateOf = teammateFilers(db);
204
+ const subagentOf = subagentFilers(db);
205
205
  const filerOf = filersOf(db);
206
206
  const text = (value) => value == null ? null : String(value);
207
207
  const count = (value) => value == null ? null : Number(value);
@@ -224,19 +224,19 @@ export function spendItems(db, from, to, modeOf = provider => billingOf(provider
224
224
  source: settled ? String(row["source"]) : priced.source,
225
225
  project: text(row["repo"]),
226
226
  person: kind === "person" || kind === "coordinator" ? filer.filedBy : null,
227
- teammate: teammateOf(filer.repo, filer.filedBy, kind), provider, model: text(row["model"]),
227
+ subagent: subagentOf(filer.repo, filer.filedBy, kind), provider, model: text(row["model"]),
228
228
  tokensIn: count(row["tokens_in"]), tokensOut: count(row["tokens_out"]),
229
229
  taskId: String(row["task"]), runId: Number(row["id"]), authMode: billing,
230
230
  };
231
231
  });
232
- // A teammate's own turns get no key from Toolroll (teammates.ts claudeTurnRunner): they bill as this computer's
232
+ // A subagent's own turns get no key from Toolroll (subagents.ts claudeTurnRunner): they bill as this computer's
233
233
  // Claude sign-in was seen billing when the turn ran (a plan, or a Console login, key helper or gateway).
234
- for (const row of db.prepare(`SELECT t.at, t.teammate, t.model, t.cost_usd, t.tokens_in, t.tokens_out, t.billing, m.repo FROM teammate_turn t JOIN teammate m ON m.id = t.teammate
234
+ for (const row of db.prepare(`SELECT t.at, t.subagent, t.model, t.cost_usd, t.tokens_in, t.tokens_out, t.billing, m.repo FROM subagent_turn t JOIN subagent m ON m.id = t.subagent
235
235
  WHERE t.at >= ? AND t.at < ? ORDER BY t.id`).all(from, to)) {
236
236
  const billing = row["billing"] === "api-key" ? "api-key" : "subscription";
237
237
  const priced = priceWork(db, { provider: "claude", model: text(row["model"]), costUsd: count(row["cost_usd"]), tokensIn: count(row["tokens_in"]), tokensOut: count(row["tokens_out"]), billing });
238
- items.push({ at: String(row["at"]), kind: "teammate", microusd: priced.microusd, source: priced.source, project: String(row["repo"]), person: null,
239
- teammate: Number(row["teammate"]), provider: "claude", model: text(row["model"]),
238
+ items.push({ at: String(row["at"]), kind: "subagent", microusd: priced.microusd, source: priced.source, project: String(row["repo"]), person: null,
239
+ subagent: Number(row["subagent"]), provider: "claude", model: text(row["model"]),
240
240
  tokensIn: count(row["tokens_in"]), tokensOut: count(row["tokens_out"]), taskId: null, runId: null, authMode: billing });
241
241
  }
242
242
  // A chat turn that mirrors a project chat's turn is that turn: counted once, as the project chat's. A task's chat
@@ -246,19 +246,19 @@ export function spendItems(db, from, to, modeOf = provider => billingOf(provider
246
246
  UNION ALL SELECT t.created_at, t.approver, COALESCE((SELECT c.provider FROM chat_turn c WHERE c.mate_turn = t.id ORDER BY c.id LIMIT 1), 'lead'),
247
247
  (SELECT c.model FROM chat_turn c WHERE c.mate_turn = t.id ORDER BY c.id LIMIT 1), t.settled_microusd, t.tokens_in, t.tokens_out,
248
248
  CASE th.scope_kind WHEN 'project' THEN th.scope_key WHEN 'task' THEN (SELECT r.repo FROM task_ref r WHERE r.external_id = th.scope_key ORDER BY r.id DESC LIMIT 1) END
249
- FROM mate_turn t LEFT JOIN mate_thread th ON th.id = t.thread WHERE t.created_at >= ? AND t.created_at < ?`).all(from, to, from, to)) {
249
+ FROM mate_turn t LEFT JOIN lead_thread th ON th.id = t.thread WHERE t.created_at >= ? AND t.created_at < ?`).all(from, to, from, to)) {
250
250
  const provider = String(row["provider"]);
251
251
  const subscription = provider.endsWith("-subscription") && modeOf(provider) === "subscription";
252
252
  const settled = count(row["settled_microusd"]);
253
253
  items.push({ at: String(row["at"]), kind: "chat", microusd: subscription ? 0 : settled, source: subscription ? "subscription" : settled === null ? "unpriced" : "reported",
254
- project: text(row["project"]), person: text(row["approver"]), teammate: null, provider, model: text(row["model"]),
254
+ project: text(row["project"]), person: text(row["approver"]), subagent: null, provider, model: text(row["model"]),
255
255
  tokensIn: count(row["tokens_in"]), tokensOut: count(row["tokens_out"]), taskId: null, runId: null, authMode: subscription ? "subscription" : "api-key" });
256
256
  }
257
- // A flow's Claude drafts: like a teammate's turns, billed as the sign-in was.
257
+ // A flow's Claude drafts: like a subagent's turns, billed as the sign-in was.
258
258
  for (const row of db.prepare("SELECT at, kind, repo, provider, model, cost_usd, billing FROM side_spend WHERE at >= ? AND at < ? ORDER BY id").all(from, to)) {
259
259
  const billing = row["billing"] === "api-key" ? "api-key" : "subscription";
260
260
  const priced = priceWork(db, { provider: String(row["provider"]), model: text(row["model"]), costUsd: count(row["cost_usd"]), tokensIn: null, tokensOut: null, billing });
261
- items.push({ at: String(row["at"]), kind: "draft", microusd: priced.microusd, source: priced.source, project: text(row["repo"]), person: null, teammate: null,
261
+ items.push({ at: String(row["at"]), kind: "draft", microusd: priced.microusd, source: priced.source, project: text(row["repo"]), person: null, subagent: null,
262
262
  provider: String(row["provider"]), model: text(row["model"]), tokensIn: null, tokensOut: null, taskId: null, runId: null, authMode: billing });
263
263
  }
264
264
  // A flow's Sort zone asks Jev on OpenRouter (the key's credit): what it cost is kept with its answer.
@@ -267,7 +267,7 @@ export function spendItems(db, from, to, modeOf = provider => billingOf(provider
267
267
  WHERE fs.kind = 'sort' AND fs.decision_json IS NOT NULL AND fs.started_at >= ? AND fs.started_at < ? ORDER BY fs.started_at`).all(from, to)) {
268
268
  const cost = typeof row["cost"] === "number" && Number.isFinite(row["cost"]) && row["cost"] >= 0 ? row["cost"] : null;
269
269
  items.push({ at: String(row["at"]), kind: "sort", microusd: cost === null ? null : Math.round(cost * 1_000_000), source: cost === null ? "unpriced" : "reported",
270
- project: text(row["repo"]), person: null, teammate: null, provider: "openrouter", model: text(row["model"]), tokensIn: null, tokensOut: null, taskId: null, runId: null, authMode: "api-key" });
270
+ project: text(row["repo"]), person: null, subagent: null, provider: "openrouter", model: text(row["model"]), tokensIn: null, tokensOut: null, taskId: null, runId: null, authMode: "api-key" });
271
271
  }
272
272
  return items;
273
273
  }
@@ -279,14 +279,14 @@ export function countsToward(item, budget) {
279
279
  return item.project === budget.key;
280
280
  if (budget.scope === "person")
281
281
  return item.person === budget.key;
282
- return item.teammate !== null && String(item.teammate) === budget.key;
282
+ return item.subagent !== null && String(item.subagent) === budget.key;
283
283
  }
284
284
  const PROVIDER_WORDS = { claude: "Claude", codex: "Codex", gemini: "Gemini", openrouter: "OpenRouter" };
285
285
  /** Why work waits on a budget, in words. */
286
- export function budgetHoldWords(hold, month, teammateName) {
286
+ export function budgetHoldWords(hold, month, subagentName) {
287
287
  if (hold.over === null)
288
288
  return "";
289
- const label = budgetLabel(hold.over, teammateName);
289
+ const label = budgetLabel(hold.over, subagentName);
290
290
  return hold.why === "unpriced"
291
291
  ? `${label} budget can't price ${PROVIDER_WORDS[hold.unpricedProvider ?? ""] ?? hold.unpricedProvider} work on a key yet. Open Settings → Models to load prices`
292
292
  : `${label} budget is used up for ${month}`;
@@ -300,14 +300,14 @@ export function budgetStates(budgets, items) {
300
300
  percent: Math.floor((spent / budget.limitMicrousd) * 100) };
301
301
  });
302
302
  }
303
- /** A budget in words: "The shop project's", "alex's", "Maya's (teammate)", "The whole installation's". */
304
- export function budgetLabel(budget, teammateName) {
303
+ /** A budget in words: "The shop project's", "alex's", "Maya's (subagent)", "The whole installation's". */
304
+ export function budgetLabel(budget, subagentName) {
305
305
  if (budget.scope === "installation")
306
306
  return "The whole installation's";
307
307
  if (budget.scope === "project")
308
308
  return `The ${budget.key.split("/").filter(Boolean).pop() ?? budget.key} project's`;
309
309
  if (budget.scope === "person")
310
310
  return `${budget.key}'s`;
311
- return `${teammateName ?? `Teammate ${budget.key}`}'s`;
311
+ return `${subagentName ?? `Subagent ${budget.key}`}'s`;
312
312
  }
313
313
  export const usd = (microusd) => microusd === null ? "unpriced" : `$${(microusd / 1_000_000).toFixed(microusd !== 0 && Math.abs(microusd) < 10_000_000 ? 2 : 0)}`;
package/dist/sso-ui.d.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  * operator's page; changes take their password.
5
5
  */
6
6
  import type { OidcSettings } from "./oidc.js";
7
+ import { type Html } from "./html.js";
7
8
  export declare const SSO_CSS: string;
8
9
  export type SsoSettingsView = {
9
10
  settings: OidcSettings | null;
@@ -16,7 +17,7 @@ export type SsoSettingsView = {
16
17
  /** Whether the person looking is linked to the provider already. */
17
18
  linked: boolean;
18
19
  };
19
- export declare function ssoSettingsHtml(view: SsoSettingsView, csrf: string, notice: {
20
+ export declare function ssoSettingsHtml(view: SsoSettingsView, notice: {
20
21
  said?: string | null;
21
22
  problem?: string | null;
22
- }): string;
23
+ }): Html;
package/dist/sso-ui.js CHANGED
@@ -1,39 +1,38 @@
1
- const e = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
1
+ import { html, postForm } from "./html.js";
2
2
  export const SSO_CSS = `.sign-in{max-width:780px;min-width:0}.sign-in form{display:grid;gap:12px;margin:0}.sign-in label{display:grid;gap:6px}.sign-in input,.sign-in select{box-sizing:border-box;width:100%;max-width:100%;min-width:0}` +
3
3
  `.sign-in .card{padding:16px 18px;margin:12px 0}.sign-in .card h2{font-size:1.05rem;margin:0 0 6px}.sign-in .redirect code{user-select:all;overflow-wrap:anywhere}.sign-in button{justify-self:start;min-height:44px}` +
4
4
  `.sign-in .rules{display:grid;gap:8px}.sign-in .rule{display:grid;grid-template-columns:minmax(0,2fr) minmax(0,1fr) minmax(0,2fr);gap:8px;align-items:start}.sign-in .rule select[multiple]{min-height:5.5rem}` +
5
5
  `.sign-in .row{display:flex;gap:12px;flex-wrap:wrap;align-items:center}.sign-in fieldset{border:0;padding:0;margin:0;display:grid;gap:6px}.sign-in fieldset label{display:flex;gap:8px;align-items:center}.sign-in fieldset input{width:auto}` +
6
6
  `@media(max-width:600px){.sign-in .rule{grid-template-columns:1fr}.sign-in input,.sign-in select{font-size:16px}}`;
7
- const password = '<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>';
7
+ const password = html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>`;
8
8
  function ruleRow(index, rule, projects) {
9
9
  const chosen = rule === null ? [] : rule.projects === "all" ? ["all"] : rule.projects;
10
- return `<div class="rule"><label>Group<input type="text" name="group-${index}" value="${e(rule?.group ?? "")}" placeholder="${index === 0 ? "for example: engineering" : ""}" autocomplete="off"></label>` +
11
- `<label>Role<select name="role-${index}"><option value="operator"${rule?.role === "operator" ? " selected" : ""}>Operator</option><option value="viewer"${rule?.role === "viewer" ? " selected" : ""}>Viewer</option></select></label>` +
12
- `<label>Projects<select name="projects-${index}" multiple><option value="all"${chosen.includes("all") ? " selected" : ""}>All projects</option>${projects.map(one => `<option value="${e(one.path)}"${chosen.includes(one.path) ? " selected" : ""}>${e(one.name)}</option>`).join("")}</select></label></div>`;
10
+ return html `<div class="rule"><label>Group<input type="text" name="group-${index}" value="${rule?.group ?? ""}" placeholder="${index === 0 ? "for example: engineering" : ""}" autocomplete="off"></label>\
11
+ <label>Role<select name="role-${index}"><option value="operator"${rule?.role === "operator" ? " selected" : ""}>Operator</option><option value="viewer"${rule?.role === "viewer" ? " selected" : ""}>Viewer</option></select></label>\
12
+ <label>Projects<select name="projects-${index}" multiple><option value="all"${chosen.includes("all") ? " selected" : ""}>All projects</option>${projects.map(one => html `<option value="${one.path}"${chosen.includes(one.path) ? " selected" : ""}>${one.name}</option>`)}</select></label></div>`;
13
13
  }
14
- export function ssoSettingsHtml(view, csrf, notice) {
14
+ export function ssoSettingsHtml(view, notice) {
15
15
  const s = view.settings;
16
- const hidden = (fields) => Object.entries(fields).map(([k, v]) => `<input type="hidden" name="${e(k)}" value="${e(v)}">`).join("");
17
- const rows = [...(s?.rules ?? []), null, null].slice(0, Math.max(3, (s?.rules.length ?? 0) + 1)).map((rule, index) => ruleRow(index, rule, view.projects)).join("");
18
- const note = notice.problem ? `<p class="problem" role="alert">${e(notice.problem)}</p>` : notice.said ? `<p role="status">${e(notice.said)}</p>` : "";
16
+ const rows = [...(s?.rules ?? []), null, null].slice(0, Math.max(3, (s?.rules.length ?? 0) + 1)).map((rule, index) => ruleRow(index, rule, view.projects));
17
+ const note = notice.problem ? html `<p class="problem" role="alert">${notice.problem}</p>` : notice.said ? html `<p role="status">${notice.said}</p>` : "";
19
18
  const redirect = view.redirect === null
20
- ? `<p class="meta">Open this page from the address people use (your https address) to see the redirect address to register.</p>`
21
- : `<p class="redirect">Register this redirect address with the provider: <code>${e(view.redirect)}</code></p>`;
22
- const form = `<form method="post" action="/settings/sign-in">${hidden({ csrf, action: "save" })}` +
23
- `<label>Provider address (issuer)<input type="url" name="issuer" value="${e(s?.issuer ?? "")}" placeholder="https://acme.okta.com/oauth2/default" required inputmode="url" autocomplete="off"></label>` +
24
- `<label>Client ID<input type="text" name="client-id" value="${e(s?.clientId ?? "")}" required autocomplete="off"></label>` +
25
- `<label>Client secret<input type="password" name="client-secret" autocomplete="off" placeholder="${s?.clientSecret ? "Saved. Leave blank to keep it" : "Leave blank for a public client"}"></label>` +
26
- `<details><summary>More</summary><label>Button says “Sign in with …”<input type="text" name="label" value="${e(s?.label ?? "")}" placeholder="Okta"></label>` +
27
- `<label>Scopes<input type="text" name="scopes" value="${e(s?.scopes ?? "openid email profile")}"></label><label>Groups claim<input type="text" name="groups-claim" value="${e(s?.groupsClaim ?? "groups")}"></label></details>` +
28
- `<h2>Who may sign in</h2><p class="meta">A person's first matching group sets their role and projects, again at every sign-in. No match, no entry. Use * for everyone.</p><div class="rules">${rows}</div>` +
29
- `<fieldset><legend>Passwords</legend><label><input type="radio" name="passwords" value="everyone"${s?.passwords !== "operators" ? " checked" : ""}> Everyone may still use a password</label>` +
30
- `<label><input type="radio" name="passwords" value="operators"${s?.passwords === "operators" ? " checked" : ""}> Only instance operators (a way in if the provider is down)</label></fieldset>` +
31
- `${password}<button>${s === null ? "Turn on sign-in with the provider" : "Save"}</button></form>`;
32
- const test = s === null ? "" : `<form method="post" action="/settings/sign-in" class="row">${hidden({ csrf, action: "test" })}<button class="secondary">Check the provider</button></form>`;
19
+ ? html `<p class="meta">Open this page from the address people use (your https address) to see the redirect address to register.</p>`
20
+ : html `<p class="redirect">Register this redirect address with the provider: <code>${view.redirect}</code></p>`;
21
+ const form = postForm("/settings/sign-in", html `\
22
+ <label>Provider address (issuer)<input type="url" name="issuer" value="${s?.issuer ?? ""}" placeholder="https://acme.okta.com/oauth2/default" required inputmode="url" autocomplete="off"></label>\
23
+ <label>Client ID<input type="text" name="client-id" value="${s?.clientId ?? ""}" required autocomplete="off"></label>\
24
+ <label>Client secret<input type="password" name="client-secret" autocomplete="off" placeholder="${s?.clientSecret ? "Saved. Leave blank to keep it" : "Leave blank for a public client"}"></label>\
25
+ <details><summary>More</summary><label>Button says “Sign in with …”<input type="text" name="label" value="${s?.label ?? ""}" placeholder="Okta"></label>\
26
+ <label>Scopes<input type="text" name="scopes" value="${s?.scopes ?? "openid email profile"}"></label><label>Groups claim<input type="text" name="groups-claim" value="${s?.groupsClaim ?? "groups"}"></label></details>\
27
+ <h2>Who may sign in</h2><p class="meta">A person's first matching group sets their role and projects, again at every sign-in. No match, no entry. Use * for everyone.</p><div class="rules">${rows}</div>\
28
+ <fieldset><legend>Passwords</legend><label><input type="radio" name="passwords" value="everyone"${s?.passwords !== "operators" ? " checked" : ""}> Everyone may still use a password</label>\
29
+ <label><input type="radio" name="passwords" value="operators"${s?.passwords === "operators" ? " checked" : ""}> Only instance operators (a way in if the provider is down)</label></fieldset>\
30
+ ${password}<button>${s === null ? "Turn on sign-in with the provider" : "Save"}</button>`, { hidden: { action: "save" } });
31
+ const test = s === null ? "" : postForm("/settings/sign-in", html `<button class="secondary">Check the provider</button>`, { attrs: { class: "row" }, hidden: { action: "test" } });
33
32
  const link = s === null ? "" : view.linked
34
- ? `<p class="meta">You sign in with ${e(s.label)}.</p>`
35
- : `<section class="card"><h2>Your account</h2><p>Sign in with ${e(s.label)} as this account from now on.</p><p><a class="button-link" href="/login/sso?link=1">Link to ${e(s.label)}</a></p></section>`;
36
- const off = s === null ? "" : `<details class="card"><summary>Turn off</summary><form method="post" action="/settings/sign-in">${hidden({ csrf, action: "remove" })}<p>People who signed in with ${e(s.label)} can't sign in until it's on again, unless they have a password.</p>${password}<button class="danger">Turn off sign-in with ${e(s.label)}</button></form></details>`;
37
- return `<section class="sign-in">${note}<p>People sign in with your organisation's identity provider (Okta, Microsoft Entra, Google, anything that speaks OpenID Connect), and its groups decide what they may do.</p>` +
38
- `${redirect}<section class="card">${form}</section>${test}${link}${off}</section>`;
33
+ ? html `<p class="meta">You sign in with ${s.label}.</p>`
34
+ : html `<section class="card"><h2>Your account</h2><p>Sign in with ${s.label} as this account from now on.</p><p><a class="button-link" href="/login/sso?link=1">Link to ${s.label}</a></p></section>`;
35
+ const off = s === null ? "" : html `<details class="card"><summary>Turn off</summary>${postForm("/settings/sign-in", html `<p>People who signed in with ${s.label} can't sign in until it's on again, unless they have a password.</p>${password}<button class="danger">Turn off sign-in with ${s.label}</button>`, { hidden: { action: "remove" } })}</details>`;
36
+ return html `<section class="sign-in">${note}<p>People sign in with your organisation's identity provider (Okta, Microsoft Entra, Google, anything that speaks OpenID Connect), and its groups decide what they may do.</p>\
37
+ ${redirect}<section class="card">${form}</section>${test}${link}${off}</section>`;
39
38
  }
@@ -1,3 +1,4 @@
1
+ import { type Html } from "./html.js";
1
2
  import { type CheckoutPlan } from "./checkout-cleanup.js";
2
3
  import { type SweepRecord } from "./storage-sweep.js";
3
4
  export declare const STORAGE_CSS: string;
@@ -12,4 +13,4 @@ export type StorageView = {
12
13
  export declare function storageHtml(view: StorageView, notice: {
13
14
  said?: string | null;
14
15
  problem?: string | null;
15
- }): string;
16
+ }): Html;
@@ -6,10 +6,10 @@
6
6
  */
7
7
  import { homedir } from "node:os";
8
8
  import { basename } from "node:path";
9
+ import { html, postForm } from "./html.js";
9
10
  import { previewDigest, whyWords } from "./checkout-cleanup.js";
10
11
  import { CLEANUP_CHOICES, bytesWords } from "./storage.js";
11
12
  import { sweepDetails, sweepWords } from "./storage-sweep.js";
12
- const e = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
13
13
  const plural = (count, one, many = `${one}s`) => `${count} ${count === 1 ? one : many}`;
14
14
  export const STORAGE_CSS = `.storage{max-width:720px;min-width:0;overflow-wrap:anywhere}.storage h2{font-size:1.0625rem;margin:24px 0 4px}` +
15
15
  `.storage .storage-state{display:grid;gap:12px}.storage .storage-state h2{margin:0}.storage .storage-state p{margin:0}` +
@@ -45,49 +45,30 @@ function whenWords(at) {
45
45
  function sweepHtml(sweep) {
46
46
  const last = sweep.last;
47
47
  if (last === null)
48
- return `<p class="meta" data-last-sweep="">${sweep.off ? "Automatic sweep is off." : "Toolroll sweeps leftovers once a day. It hasn't run yet."}</p>`;
49
- const who = last.source === "manual" ? `Cleaned up by ${e(last.actor)}` : "Swept";
48
+ return html `<p class="meta" data-last-sweep="">${sweep.off ? "Automatic sweep is off." : html `Toolroll sweeps leftovers once a day. It hasn't run yet.`}</p>`;
49
+ const who = last.source === "manual" ? `Cleaned up by ${last.actor}` : "Swept";
50
50
  const details = sweepDetails(last.parts);
51
51
  const problem = last.parts.some(one => one.failed > 0 || one.problem !== undefined);
52
- return `<div class="sweep" data-last-sweep="${e(last.at)}"><p${problem ? ' class="problem"' : ""}>${who} <time datetime="${e(last.at)}">${e(whenWords(last.at))}</time>. ${e(sweepWords(last.parts))}</p>` +
53
- (details.length === 0 ? "" : `<details class="sweep-details"><summary>Details</summary><ul>${details.map(line => `<li>${e(homeAsTilde(line))}</li>`).join("")}</ul></details>`) + `</div>`;
52
+ return html `<div class="sweep" data-last-sweep="${last.at}"><p${problem ? html ` class="problem"` : ""}>${who} <time datetime="${last.at}">${whenWords(last.at)}</time>. ${sweepWords(last.parts)}</p>${details.length === 0 ? "" : html `<details class="sweep-details"><summary>Details</summary><ul>${details.map(line => html `<li>${homeAsTilde(line)}</li>`)}</ul></details>`}</div>`;
54
53
  }
55
- function item(one, csrf) {
54
+ function item(one) {
56
55
  const discard = one.why === "has changes"
57
- ? `<details class="discard"><summary>Discard</summary><form method="post" action="/settings/storage/discard"><input type="hidden" name="csrf" value="${e(csrf)}"><input type="hidden" name="path" value="${e(one.path)}">` +
58
- `<p class="meta">Throws away this checkout and its uncommitted changes. Its branch stays.</p>` +
59
- `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label><button type="submit" class="danger">Discard changes</button></form></details>`
56
+ ? html `<details class="discard"><summary>Discard</summary>${postForm("/settings/storage/discard", html `<p class="meta">Throws away this checkout and its uncommitted changes. Its branch stays.</p><label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label><button type="submit" class="danger">Discard changes</button>`, { hidden: { path: one.path } })}</details>`
60
57
  : "";
61
58
  // The task it was for, then where it is (the home folder as ~), then why it stays.
62
59
  const home = homedir();
63
60
  const where = home !== "" && one.path.startsWith(`${home}/`) ? `~${one.path.slice(home.length)}` : one.path;
64
- return `<li data-checkout="${e(one.path)}"><span class="name">${e(one.taskId ?? basename(one.path))}</span><span class="size">${e(bytesWords(one.bytes))}</span>` +
65
- `<span class="path">${e(where)}</span>` +
66
- (one.why === null ? "" : `<span class="why">${e(whyWords(one)[0].toUpperCase() + whyWords(one).slice(1))}</span>`) + discard + `</li>`;
61
+ return html `<li data-checkout="${one.path}"><span class="name">${one.taskId ?? basename(one.path)}</span><span class="size">${bytesWords(one.bytes)}</span><span class="path">${where}</span>${one.why === null ? "" : html `<span class="why">${whyWords(one)[0].toUpperCase() + whyWords(one).slice(1)}</span>`}${discard}</li>`;
67
62
  }
68
63
  export function storageHtml(view, notice) {
69
- const { plan, csrf } = view;
70
- const note = notice.problem ? `<p class="problem" role="alert">${e(notice.problem)}</p>` : notice.said ? `<p role="status">${e(notice.said)}</p>` : "";
71
- const hidden = `<input type="hidden" name="csrf" value="${e(csrf)}">`;
64
+ const { plan } = view;
65
+ const note = notice.problem ? html `<p class="problem" role="alert">${notice.problem}</p>` : notice.said ? html `<p role="status">${notice.said}</p>` : "";
72
66
  const facts = [plural(plan.count, "checkout"), `${plan.waitingReview} waiting for review`, `${plan.withChanges} kept for their changes`].join(" · ");
73
67
  const frees = plan.go.length === 0 ? "Nothing to clean up now." : `Cleaning up now frees about ${bytesWords(plan.freeBytes)}.`;
74
68
  const remove = plan.go.length === 0 ? "" :
75
- `<form method="post" action="/settings/storage/clean" class="remove">${hidden}<input type="hidden" name="preview" value="${e(previewDigest(plan))}">` +
76
- `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>` +
77
- `<button type="submit">Remove ${plural(plan.go.length, "checkout")}</button></form>`;
78
- const preview = `<details class="clean-up" data-clean-up${notice.problem?.startsWith("Enter your Toolroll password to clean") ? " open" : ""}><summary>Clean up</summary><div class="preview">` +
79
- (plan.go.length === 0 ? `<p>Nothing to remove now.</p>` :
80
- `<h3>Removes ${plural(plan.go.length, "checkout")}, about ${e(bytesWords(plan.freeBytes))}</h3><p class="meta">Their branches stay, so nothing committed is lost.</p>` +
81
- `<ul class="checkouts" data-goes>${plan.go.map(one => item(one, csrf)).join("")}</ul>`) +
82
- (plan.stay.length === 0 ? "" : `<h3>Stays (${plan.stay.length})</h3><ul class="checkouts" data-stays>${plan.stay.map(one => item(one, csrf)).join("")}</ul>`) +
83
- remove + `</div></details>`;
84
- const options = CLEANUP_CHOICES.map(one => `<option value="${one.value}"${one.value === plan.cleanup ? " selected" : ""}>${e(one.label)}</option>`).join("");
85
- return `<section class="storage">${note}` +
86
- `<div class="card storage-state"><h2 data-checkout-bytes="${plan.totalBytes}">Checkouts use ${e(bytesWords(plan.totalBytes))}</h2><p class="meta">${e(facts)}</p>` +
87
- `<p data-clean-bytes="${plan.freeBytes}">${e(frees)}</p>${view.sweep === undefined ? "" : sweepHtml(view.sweep)}${preview}</div>` +
88
- `<h2>Automatic cleanup</h2>` +
89
- `<form method="post" action="/settings/storage" class="setting">${hidden}` +
90
- `<label>Remove a finished task's clean checkout<select name="cleanup">${options}</select></label>` +
91
- `<p class="meta">Its branch always stays. Checkouts with changes, commits on no branch, in use, or waiting for review are kept.</p>` +
92
- `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label><button type="submit">Save</button></form></section>`;
69
+ postForm("/settings/storage/clean", html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label><button type="submit">Remove ${plural(plan.go.length, "checkout")}</button>`, { attrs: { class: "remove" }, hidden: { preview: previewDigest(plan) } });
70
+ const preview = html `<details class="clean-up" data-clean-up${notice.problem?.startsWith("Enter your Toolroll password to clean") ? html ` open` : ""}><summary>Clean up</summary><div class="preview">${plan.go.length === 0 ? html `<p>Nothing to remove now.</p>` :
71
+ html `<h3>Removes ${plural(plan.go.length, "checkout")}, about ${bytesWords(plan.freeBytes)}</h3><p class="meta">Their branches stay, so nothing committed is lost.</p><ul class="checkouts" data-goes>${plan.go.map(one => item(one))}</ul>`}${plan.stay.length === 0 ? "" : html `<h3>Stays (${plan.stay.length})</h3><ul class="checkouts" data-stays>${plan.stay.map(one => item(one))}</ul>`}${remove}</div></details>`;
72
+ const options = CLEANUP_CHOICES.map(one => html `<option value="${one.value}"${one.value === plan.cleanup ? html ` selected` : ""}>${one.label}</option>`);
73
+ return html `<section class="storage">${note}<div class="card storage-state"><h2 data-checkout-bytes="${plan.totalBytes}">Checkouts use ${bytesWords(plan.totalBytes)}</h2><p class="meta">${facts}</p><p data-clean-bytes="${plan.freeBytes}">${frees}</p>${view.sweep === undefined ? "" : sweepHtml(view.sweep)}${preview}</div><h2>Automatic cleanup</h2>${postForm("/settings/storage", html `<label>Remove a finished task's clean checkout<select name="cleanup">${options}</select></label><p class="meta">Its branch always stays. Checkouts with changes, commits on no branch, in use, or waiting for review are kept.</p><label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label><button type="submit">Save</button>`, { attrs: { class: "setting" } })}</section>`;
93
74
  }