@worca/app 1.5.0 → 1.6.0-rc.2

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 (101) hide show
  1. package/README.md +8 -2
  2. package/docker/compose.broker.yml +79 -0
  3. package/docker/compose.isolation.yml +40 -0
  4. package/package.json +2 -1
  5. package/src/broker/config.mjs +200 -0
  6. package/src/broker/copilot.mjs +124 -0
  7. package/src/broker/limits.mjs +88 -0
  8. package/src/broker/main.mjs +129 -0
  9. package/src/broker/scrub.mjs +45 -0
  10. package/src/broker/service.mjs +558 -0
  11. package/src/broker/slots.mjs +180 -0
  12. package/src/broker/store.mjs +171 -0
  13. package/src/broker/tokens.mjs +116 -0
  14. package/src/broker/ui/page.css +54 -0
  15. package/src/broker/ui/page.html +25 -0
  16. package/src/broker/ui/page.mjs +202 -0
  17. package/src/broker/ui-server.mjs +217 -0
  18. package/src/broker/usage.mjs +108 -0
  19. package/src/broker/vault.mjs +37 -0
  20. package/src/cli/models.mjs +46 -14
  21. package/src/cli/render.mjs +21 -0
  22. package/src/cli/runs.mjs +296 -0
  23. package/src/cli/worca-cc.mjs +14 -2
  24. package/src/core/agent-pool.mjs +76 -0
  25. package/src/core/artifacts.mjs +40 -8
  26. package/src/core/ask/events.mjs +11 -0
  27. package/src/core/ask/html-text.mjs +113 -0
  28. package/src/core/ask/limits.mjs +5 -0
  29. package/src/core/ask/mcp-stdio.mjs +74 -20
  30. package/src/core/ask/prompt.mjs +25 -6
  31. package/src/core/ask/spawn.mjs +55 -5
  32. package/src/core/ask/store.mjs +1 -1
  33. package/src/core/ask/tools.mjs +66 -1
  34. package/src/core/ask/turn.mjs +56 -3
  35. package/src/core/ask/web-access.mjs +26 -0
  36. package/src/core/ask/web-deps.mjs +56 -0
  37. package/src/core/ask/web-fetch.mjs +271 -0
  38. package/src/core/ask/web-proposal.mjs +76 -0
  39. package/src/core/auto/classify.mjs +20 -8
  40. package/src/core/auto/runnable.mjs +28 -0
  41. package/src/core/billing.mjs +53 -0
  42. package/src/core/bridge/errors.mjs +77 -5
  43. package/src/core/bridge/openrouter.mjs +59 -0
  44. package/src/core/bridge/provider-ops.mjs +202 -10
  45. package/src/core/bridge/providers/endpoint.mjs +112 -7
  46. package/src/core/bridge/registry.mjs +13 -0
  47. package/src/core/bridge/server.mjs +16 -3
  48. package/src/core/bridge/telemetry.mjs +44 -12
  49. package/src/core/bridge/translate/common.mjs +31 -0
  50. package/src/core/bridge/translate/request.mjs +4 -1
  51. package/src/core/bridge/translate/response.mjs +3 -0
  52. package/src/core/bridge/translate/schema-keywords.mjs +75 -0
  53. package/src/core/bridge/translate/stream.mjs +50 -4
  54. package/src/core/bridge/upstream.mjs +102 -17
  55. package/src/core/broker-boot.mjs +57 -0
  56. package/src/core/broker-client.mjs +206 -0
  57. package/src/core/broker-guard.mjs +112 -0
  58. package/src/core/broker-routing.mjs +138 -0
  59. package/src/core/claude-auth.mjs +29 -0
  60. package/src/core/claude-runner.mjs +200 -9
  61. package/src/core/config.mjs +9 -12
  62. package/src/core/failure-policy.mjs +4 -2
  63. package/src/core/git-info.mjs +49 -5
  64. package/src/core/github-credentials.mjs +44 -1
  65. package/src/core/graph/script-runner.mjs +3 -1
  66. package/src/core/list-prices.mjs +29 -0
  67. package/src/core/mcp-secrets.mjs +80 -0
  68. package/src/core/metrics/sync.mjs +2 -1
  69. package/src/core/model-env.mjs +72 -0
  70. package/src/core/model-test.mjs +17 -5
  71. package/src/core/onboarding.mjs +12 -6
  72. package/src/core/openrouter-free.mjs +159 -0
  73. package/src/core/orchestrator.mjs +100 -12
  74. package/src/core/policy/effective.mjs +18 -1
  75. package/src/core/policy/local.mjs +4 -1
  76. package/src/core/policy/registry.mjs +12 -2
  77. package/src/core/preflight.mjs +116 -0
  78. package/src/core/recoverable-error.mjs +95 -0
  79. package/src/core/recovery-backoff.mjs +84 -0
  80. package/src/core/redact.mjs +25 -0
  81. package/src/core/run-context.mjs +6 -0
  82. package/src/core/run-harness.mjs +112 -30
  83. package/src/core/run-report.mjs +2 -1
  84. package/src/core/settings.mjs +90 -5
  85. package/src/core/title.mjs +7 -2
  86. package/src/core/web-allowlist.mjs +95 -0
  87. package/ui/public/app.js +271 -54
  88. package/ui/public/ask-model.mjs +1 -0
  89. package/ui/public/ask-panel.mjs +160 -23
  90. package/ui/public/bridge-view.mjs +216 -9
  91. package/ui/public/chat-settings-view.mjs +52 -1
  92. package/ui/public/credential-badges.mjs +63 -0
  93. package/ui/public/credentials-view.mjs +57 -0
  94. package/ui/public/index.html +303 -244
  95. package/ui/public/models-view.mjs +19 -1
  96. package/ui/public/openrouter-free-view.mjs +118 -0
  97. package/ui/public/stats-view.mjs +56 -0
  98. package/ui/public/style.css +110 -54
  99. package/ui/public/team-policy-view.mjs +2 -1
  100. package/ui/public/ws-seq.mjs +24 -0
  101. package/ui/server.mjs +319 -23
@@ -8,6 +8,7 @@
8
8
  // delegated listener on the list container.
9
9
 
10
10
  import { bridgedBadge, needsSignInPill, degradationLine, renderConnectionSection, collectConnection, applyConnectionMode } from './bridge-view.mjs';
11
+ import { credentialBadge } from './credential-badges.mjs';
11
12
 
12
13
  function h(doc, tag, cls, text) {
13
14
  const n = doc.createElement(tag);
@@ -17,6 +18,15 @@ function h(doc, tag, cls, text) {
17
18
  }
18
19
 
19
20
  /** "medium · high" or "all efforts" (the full set carries no signal). */
21
+ /** Credential broker: "your key / no key" for the signed-in person, after a model's name. */
22
+ function keyBadge(parent, m, doc) {
23
+ const b = credentialBadge(m.id);
24
+ if (!b) return;
25
+ const el = h(doc, 'span', `badge ${b.tone} mv-key-badge`, b.text);
26
+ el.title = b.title;
27
+ parent.appendChild(el);
28
+ }
29
+
20
30
  export function effortsSummary(efforts, allEfforts) {
21
31
  const list = Array.isArray(efforts) ? efforts : [];
22
32
  if (!list.length || (allEfforts && list.length === allEfforts.length)) return 'all efforts';
@@ -112,7 +122,10 @@ export function renderModelsList({ globals = [], legacy = [], plugins = [], poli
112
122
  const btn = h(doc, 'button', 'mv-sec-toggle');
113
123
  btn.type = 'button';
114
124
  btn.dataset.section = key;
115
- btn.appendChild(h(doc, 'span', 'mv-sec-caret', '▾'));
125
+ // The shared disclosure chevron (style.css .adv-chev), turned down while the group is open.
126
+ const caret = h(doc, 'span', 'mv-sec-caret adv-chev');
127
+ caret.setAttribute('aria-hidden', 'true');
128
+ btn.appendChild(caret);
116
129
  btn.appendChild(h(doc, 'h3', 'mv-section-title', title));
117
130
  btn.appendChild(h(doc, 'span', 'mv-sec-count', ''));
118
131
  head.appendChild(btn);
@@ -185,6 +198,7 @@ export function renderModelsList({ globals = [], legacy = [], plugins = [], poli
185
198
  const body = h(doc, 'div', 'mv-body');
186
199
  const head = h(doc, 'div', 'mv-head');
187
200
  head.appendChild(h(doc, 'b', 'mv-name', m.label || m.id));
201
+ keyBadge(head, m, doc);
188
202
  if (predefLc.has(m.id.toLowerCase())) head.appendChild(h(doc, 'span', 'badge violet mv-shadow', 'overrides built-in'));
189
203
  else if (pluginLc.has(m.id.toLowerCase())) head.appendChild(h(doc, 'span', 'badge violet mv-shadow', 'overrides plugin'));
190
204
  const rb = routedBadge(m);
@@ -238,6 +252,7 @@ export function renderModelsList({ globals = [], legacy = [], plugins = [], poli
238
252
  const body = h(doc, 'div', 'mv-body');
239
253
  const head = h(doc, 'div', 'mv-head');
240
254
  head.appendChild(h(doc, 'b', 'mv-name', m.label || m.id));
255
+ keyBadge(head, m, doc);
241
256
  head.appendChild(h(doc, 'span', 'badge waiting mv-origin', 'project (legacy)'));
242
257
  body.appendChild(head);
243
258
  body.appendChild(h(doc, 'small', 'mv-summary hint', m.id));
@@ -262,6 +277,7 @@ export function renderModelsList({ globals = [], legacy = [], plugins = [], poli
262
277
  const body = h(doc, 'div', 'mv-body');
263
278
  const head = h(doc, 'div', 'mv-head');
264
279
  head.appendChild(h(doc, 'b', 'mv-name', m.label || m.id));
280
+ keyBadge(head, m, doc);
265
281
  head.appendChild(h(doc, 'span', 'badge waiting mv-origin', `plugin: ${m.plugin}`));
266
282
  if (globalLc.has(m.id.toLowerCase())) head.appendChild(h(doc, 'span', 'badge violet mv-shadowed', 'overridden by your copy'));
267
283
  const prb = routedBadge(m);
@@ -311,6 +327,7 @@ export function renderModelsList({ globals = [], legacy = [], plugins = [], poli
311
327
  const body = h(doc, 'div', 'mv-body');
312
328
  const head = h(doc, 'div', 'mv-head');
313
329
  head.appendChild(h(doc, 'b', 'mv-name', m.label || m.id));
330
+ keyBadge(head, m, doc);
314
331
  const badge = h(doc, 'span', 'badge blue mv-origin', 'policy');
315
332
  badge.title = `Team policy on ${m.home}`;
316
333
  head.appendChild(badge);
@@ -339,6 +356,7 @@ export function renderModelsList({ globals = [], legacy = [], plugins = [], poli
339
356
  const row = h(doc, 'div', 'mv-builtin');
340
357
  row.dataset.id = m.id;
341
358
  row.appendChild(h(doc, 'b', 'mv-name', m.label));
359
+ keyBadge(row, m, doc);
342
360
  if (globalLc.has(m.id.toLowerCase()) || pluginLc.has(m.id.toLowerCase())) {
343
361
  row.appendChild(h(doc, 'span', 'badge violet mv-shadowed', 'overridden'));
344
362
  }
@@ -0,0 +1,118 @@
1
+ // ui/public/openrouter-free-view.mjs
2
+ // OpenRouter's daily allowance of `:free` requests (src/core/openrouter-free.mjs), where it
3
+ // comes up: the sidebar line under the spend block, a run's cost pill, the new-run form's
4
+ // warning and the Providers card. Pure helpers plus one renderer; app.js fetches
5
+ // /api/openrouter/free-daily and mounts the result.
6
+
7
+ /** Below this share of the day's allowance the line turns amber. */
8
+ export const FREE_LOW_AT = 0.1;
9
+ /** A run's typical free requests when this install has no history yet (a small pipeline run). */
10
+ export const FREE_RUN_DEFAULT = 90;
11
+
12
+ /** "3h 12m" / "12m" / "under a minute". Pure. */
13
+ export function untilText(ms) {
14
+ const min = Math.floor(Math.max(0, ms) / 60_000);
15
+ if (min < 1) return 'under a minute';
16
+ const h = Math.floor(min / 60);
17
+ return h ? `${h}h ${min % 60}m` : `${min}m`;
18
+ }
19
+
20
+ /** "00:00 UTC". Pure. */
21
+ export function utcClock(iso) {
22
+ const t = Date.parse(iso || '');
23
+ return Number.isFinite(t) ? `${new Date(t).toISOString().slice(11, 16)} UTC` : '';
24
+ }
25
+
26
+ /** 'ok' | 'low' | 'out' for a known status. Pure. */
27
+ export function freeLevel(s) {
28
+ if (!s || !s.known || !(s.limit > 0)) return 'ok';
29
+ if (s.remaining <= 0) return 'out';
30
+ return s.remaining / s.limit < FREE_LOW_AT ? 'low' : 'ok';
31
+ }
32
+
33
+ /** The requests a run spent on OpenRouter `:free` models (its steps' bridgeFreeCalls). Pure. */
34
+ export function runFreeRequests(steps) {
35
+ return (Array.isArray(steps) ? steps : []).reduce((n, s) => n + (Number.isFinite(s?.bridgeFreeCalls) ? s.bridgeFreeCalls : 0), 0);
36
+ }
37
+
38
+ /** " · 87 free requests" for a cost pill, '' when the run used none. Pure. */
39
+ export function freeRequestsSuffix(steps) {
40
+ const n = runFreeRequests(steps);
41
+ return n > 0 ? ` · ${n} free request${n === 1 ? '' : 's'}` : '';
42
+ }
43
+
44
+ /**
45
+ * What a run usually takes on this install: the median of the last `max` finished runs that
46
+ * used free requests, else FREE_RUN_DEFAULT. Pure.
47
+ */
48
+ export function typicalFreeRun(runs, { max = 10 } = {}) {
49
+ const done = new Set(['completed', 'failed', 'stopped']);
50
+ const counts = (Array.isArray(runs) ? runs : [])
51
+ .filter((r) => r && done.has(r.status))
52
+ .map((r) => runFreeRequests(r.steps))
53
+ .filter((n) => n > 0)
54
+ .slice(0, max)
55
+ .sort((a, b) => a - b);
56
+ if (!counts.length) return FREE_RUN_DEFAULT;
57
+ const mid = Math.floor(counts.length / 2);
58
+ return counts.length % 2 ? counts[mid] : Math.round((counts[mid - 1] + counts[mid]) / 2);
59
+ }
60
+
61
+ /**
62
+ * The new-run form's warning when a chosen model is `:free` and fewer requests are left than a
63
+ * run usually takes; null otherwise. Pure.
64
+ */
65
+ export function newRunFreeWarning(s, { typical = FREE_RUN_DEFAULT, usesFree = false, now = Date.now() } = {}) {
66
+ if (!usesFree || !s || !s.known || s.remaining >= typical) return null;
67
+ const reset = Date.parse(s.resetAt || '');
68
+ const when = Number.isFinite(reset) ? ` (resets ${utcClock(s.resetAt)}, in ${untilText(reset - now)})` : '';
69
+ if (s.remaining <= 0) return `No OpenRouter free requests left today${when}: a free model would pause at its first call. Pick a paid model, or start after the reset.`;
70
+ return `${s.remaining} OpenRouter free request${s.remaining === 1 ? '' : 's'} left today${when}; a run here usually needs ~${typical}.`;
71
+ }
72
+
73
+ /** The Providers card's line about the allowance, '' when unknown. Pure. */
74
+ export function providerFreeLine(s, { now = Date.now() } = {}) {
75
+ if (!s || !s.known) return '';
76
+ const reset = Date.parse(s.resetAt || '');
77
+ const when = Number.isFinite(reset) ? ` · resets ${utcClock(s.resetAt)} (in ${untilText(reset - now)})` : '';
78
+ return `Free-model requests today: ${s.remaining} of ${s.limit} left${when}.`;
79
+ }
80
+
81
+ /**
82
+ * The sidebar block: "OpenRouter free · 941 / 1000 today", a meter of what is left, amber
83
+ * below 10%, red at 0, the reset on hover. null when there is nothing to show.
84
+ */
85
+ export function renderFreeDaily(s, { doc = globalThis.document, now = Date.now() } = {}) {
86
+ if (!s || !s.enabled || !s.known) return null;
87
+ const level = freeLevel(s);
88
+ const btn = doc.createElement('button');
89
+ btn.type = 'button';
90
+ btn.className = `spend-ind free-ind${level === 'low' ? ' warn' : level === 'out' ? ' over' : ''}`;
91
+ const reset = Date.parse(s.resetAt || '');
92
+ btn.title = `OpenRouter free-model requests left today: ${s.remaining} of ${s.limit}` +
93
+ (Number.isFinite(reset) ? ` · resets ${utcClock(s.resetAt)}, in ${untilText(reset - now)}` : '') +
94
+ ' — every model call on a :free model is one request';
95
+ const row = doc.createElement('span');
96
+ row.className = 'spend-ind-row';
97
+ const label = doc.createElement('span');
98
+ label.className = 'spend-ind-label';
99
+ label.textContent = 'OpenRouter free today';
100
+ const amt = doc.createElement('span');
101
+ amt.className = 'spend-ind-amt mono';
102
+ amt.textContent = `${s.remaining} / ${s.limit}`;
103
+ row.append(label, amt);
104
+ const meter = doc.createElement('span');
105
+ meter.className = 'spend-ind-meter';
106
+ const fill = doc.createElement('span');
107
+ fill.className = 'spend-ind-meter-fill';
108
+ fill.style.width = `${s.limit > 0 ? Math.max(0, Math.min(100, (s.remaining / s.limit) * 100)) : 0}%`;
109
+ meter.appendChild(fill);
110
+ btn.append(row, meter);
111
+ if (level === 'out') {
112
+ const sub = doc.createElement('small');
113
+ sub.className = 'spend-ind-sub';
114
+ sub.textContent = 'used up · free models pause until the reset';
115
+ btn.appendChild(sub);
116
+ }
117
+ return btn;
118
+ }
@@ -678,6 +678,57 @@ export function renderRunsChart(spec, { doc = globalThis.document, fmt = DEFAULT
678
678
  }
679
679
 
680
680
  /** Full Statistics body: KPI row + the two chart cards (or empty notes). */
681
+ const SLOT_NAMES = { anthropic: 'Anthropic', openai: 'OpenAI', openrouter: 'OpenRouter', copilot: 'Copilot', local: 'Local' };
682
+ const compactCount = (n) => (n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e3 ? `${(n / 1e3).toFixed(1)}k` : String(n));
683
+
684
+ /**
685
+ * "By person" (credential broker, docs/credential-broker.md): model spend per person in the
686
+ * stats window, as the broker metered it — every call is charged to whoever caused it.
687
+ * @param {{people?:object[], error?:string}} byPerson
688
+ */
689
+ export function renderPeopleCard(byPerson, rangeLabel, { doc = globalThis.document, fmt = DEFAULT_FMT } = {}) {
690
+ const card = chartCard(doc, 'By person', rangeLabel);
691
+ card.classList.add('people-card');
692
+ if (byPerson?.error) {
693
+ card.appendChild(h(doc, 'p', 'hint err', `The credential broker didn't answer: ${byPerson.error}`));
694
+ return card;
695
+ }
696
+ const people = byPerson?.people || [];
697
+ if (!people.length) {
698
+ card.appendChild(h(doc, 'p', 'chart-empty hint', 'No model calls in this period.'));
699
+ return card;
700
+ }
701
+ const top = Math.max(...people.map((p) => p.usd), 0);
702
+ const total = people.reduce((a, p) => a + p.usd, 0);
703
+ const table = h(doc, 'table', 'people-table');
704
+ const head = h(doc, 'tr', null);
705
+ for (const t of ['Person', 'Spend', 'Requests', 'Tokens in / out', 'Providers']) head.appendChild(h(doc, 'th', null, t));
706
+ const thead = h(doc, 'thead', null); thead.appendChild(head); table.appendChild(thead);
707
+ const tbody = h(doc, 'tbody', null);
708
+ for (const p of people) {
709
+ const tr = h(doc, 'tr', null);
710
+ tr.appendChild(h(doc, 'td', 'people-name', p.person === 'local' ? 'No signed-in person' : p.person));
711
+ const spend = h(doc, 'td', 'people-spend');
712
+ const bar = h(doc, 'span', 'people-bar');
713
+ const fill = h(doc, 'span', 'people-bar-fill');
714
+ fill.style.width = `${top > 0 ? Math.max(2, Math.round((p.usd / top) * 100)) : 0}%`;
715
+ bar.appendChild(fill);
716
+ spend.appendChild(h(doc, 'span', 'people-usd', fmt.usd(p.usd)));
717
+ spend.appendChild(bar);
718
+ tr.appendChild(spend);
719
+ tr.appendChild(h(doc, 'td', 'num', compactCount(p.requests)));
720
+ tr.appendChild(h(doc, 'td', 'num', `${compactCount(p.inputTokens + (p.cacheReadTokens || 0))} / ${compactCount(p.outputTokens)}`));
721
+ tr.appendChild(h(doc, 'td', 'people-slots', [...new Set(p.slots.map((s) => `${SLOT_NAMES[s.slot] || s.slot}${s.plan === 'subscription' ? ' (subscription)' : ''}`))].join(', ')));
722
+ tbody.appendChild(tr);
723
+ }
724
+ table.appendChild(tbody);
725
+ const scroll = h(doc, 'div', 'people-scroll');
726
+ scroll.appendChild(table);
727
+ card.appendChild(scroll);
728
+ card.appendChild(h(doc, 'small', 'hint', `${fmt.usd(total)} in total, metered by the credential broker. Each call is charged to the person whose action caused it.`));
729
+ return card;
730
+ }
731
+
681
732
  export function renderStatsBody(model, opts = {}) {
682
733
  const { doc = globalThis.document } = opts;
683
734
  const wrap = h(doc, 'div', null);
@@ -706,5 +757,10 @@ export function renderStatsBody(model, opts = {}) {
706
757
  currentBucketStartMs, rangeLabel }, opts));
707
758
  }
708
759
  wrap.appendChild(grid);
760
+ if (model.byPerson) {
761
+ const people = h(doc, 'div', 'charts-grid people-grid');
762
+ people.appendChild(renderPeopleCard(model.byPerson, rangeLabel, opts));
763
+ wrap.appendChild(people);
764
+ }
709
765
  return wrap;
710
766
  }