@chatpanel/events 0.102.0 → 0.104.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 (139) hide show
  1. package/adapters.js +1 -83
  2. package/adaptive-tool-policy.js +1 -45
  3. package/agent.js +2 -255
  4. package/attribution.js +1 -132
  5. package/backup-envelope.js +1 -221
  6. package/board-tool.js +2 -165
  7. package/budget.js +1 -116
  8. package/capability.js +1 -126
  9. package/citations.js +5 -127
  10. package/client-prefs.js +1 -148
  11. package/context-attachments.js +8 -144
  12. package/cowriter-router.js +1 -83
  13. package/cowriter-writer.js +19 -193
  14. package/cowriter.js +1 -190
  15. package/curate.js +3 -509
  16. package/distance.js +1 -124
  17. package/engine.js +1 -131
  18. package/entitlement.js +1 -332
  19. package/entity.js +1 -354
  20. package/event.js +1 -175
  21. package/extraction.js +4 -293
  22. package/failover.js +1 -93
  23. package/find-tool.js +1 -66
  24. package/flowchart.js +2 -644
  25. package/gate.js +1 -74
  26. package/harness.js +4 -101
  27. package/index.js +1 -345
  28. package/invariants.js +1 -174
  29. package/job.js +3 -147
  30. package/kernel.js +1 -255
  31. package/knowledge-derive.js +1 -267
  32. package/knowledge.js +3 -230
  33. package/library.js +11 -280
  34. package/loop.js +1 -132
  35. package/manifest.js +1 -107
  36. package/markdown-authoring.js +11 -288
  37. package/markdown-render.js +0 -0
  38. package/mcp-client.js +8 -358
  39. package/mcp-dispatch.js +1 -52
  40. package/mcp-errors.js +1 -106
  41. package/mcp-manager.js +1 -127
  42. package/media-transcript.js +8 -531
  43. package/meeting-analyzers.js +1 -83
  44. package/meeting-insights.js +7 -164
  45. package/meeting-shape.js +1 -282
  46. package/meeting-text.js +5 -94
  47. package/memory.js +2 -638
  48. package/model-candidates.js +1 -358
  49. package/model-health.js +1 -182
  50. package/model-ledger.js +1 -228
  51. package/model-picker.js +1 -188
  52. package/note-actions.js +15 -246
  53. package/note-graph.js +1 -158
  54. package/note-links.js +1 -68
  55. package/note-mentions.js +8 -114
  56. package/note-plan.js +9 -107
  57. package/note-research.js +1 -135
  58. package/observability.js +1 -119
  59. package/omni.js +1 -125
  60. package/order.js +1 -78
  61. package/package.json +4 -5
  62. package/pdf-layout.js +7 -253
  63. package/policy.js +1 -0
  64. package/project-run.js +20 -444
  65. package/project.js +1 -170
  66. package/promotion.js +1 -171
  67. package/queue.js +1 -98
  68. package/reach.js +1 -23
  69. package/recipe-tool.js +2 -161
  70. package/recipe.js +1 -264
  71. package/record-list.js +2 -110
  72. package/recruit.js +3 -456
  73. package/redaction-tokens.js +1 -61
  74. package/ref.js +1 -55
  75. package/registry.js +1 -240
  76. package/route-graph.js +1 -115
  77. package/route-strategies.js +2 -232
  78. package/router.js +2 -880
  79. package/rrf.js +1 -89
  80. package/rules.js +1 -142
  81. package/schedule.js +1 -663
  82. package/scm-connection.js +1 -180
  83. package/scopes.js +1 -12
  84. package/scorecard.js +1 -335
  85. package/search-engines.js +1 -81
  86. package/skill-manifest.js +1 -330
  87. package/skill-scan.js +3 -209
  88. package/skill-sources.js +1 -162
  89. package/skill-vars.js +2 -252
  90. package/slash-commands.js +1 -158
  91. package/source-gate.js +1 -92
  92. package/sources-retrieval.js +5 -186
  93. package/sources.js +1 -273
  94. package/store.js +1 -171
  95. package/structured.js +4 -901
  96. package/subject-kinds.js +1 -5
  97. package/subject-name.js +1 -96
  98. package/sync-plan.js +1 -213
  99. package/synthesis.js +3 -123
  100. package/tags.js +1 -203
  101. package/team-board.js +6 -337
  102. package/team-cache.js +2 -63
  103. package/team-observe.js +1 -118
  104. package/team-org.js +1 -442
  105. package/team-plan.js +4 -131
  106. package/team-record.js +1 -225
  107. package/team-run.js +9 -652
  108. package/team-subtask.js +2 -191
  109. package/team-task.js +2 -124
  110. package/team-tool.js +2 -196
  111. package/team-trail.js +2 -73
  112. package/team-worklog.js +2 -149
  113. package/team.js +1 -302
  114. package/text-search.js +1 -155
  115. package/theme.js +17 -154
  116. package/titles.js +4 -285
  117. package/tool-discovery.js +1 -87
  118. package/tool-dispatch.js +5 -230
  119. package/tool-groups.js +1 -81
  120. package/tool-hints.js +10 -185
  121. package/tool-loop-guard.js +4 -182
  122. package/tool-need.js +2 -124
  123. package/tool-result.js +2 -438
  124. package/tool-round.js +0 -0
  125. package/tool-schema.js +1 -155
  126. package/tool-traits.js +1 -182
  127. package/toolset.js +2 -88
  128. package/trajectory.js +1 -530
  129. package/turn-loop.js +4 -440
  130. package/upcast.js +1 -37
  131. package/vault.js +2 -250
  132. package/view.js +1 -88
  133. package/voice-intents.js +2 -1488
  134. package/voice-speaker.js +1 -98
  135. package/weather-tool.js +1 -65
  136. package/weather.js +2 -211
  137. package/web-search-tool.js +14 -102
  138. package/web-search.js +1 -160
  139. package/widget.js +1 -153
package/engine.js CHANGED
@@ -1,131 +1 @@
1
- // An ENGINE what actually runs an agent's turns as a declaration, and its one-line label.
2
- //
3
- // The model is a variable, not a constant (architecture-pillars.md §13). An agent card names
4
- // the engine it runs on, and there are four ways to say it:
5
- //
6
- // { kind: 'model', providerId?, model } an endpoint the client calls; `providerId` is
7
- // the endpoint / gateway destination, because the
8
- // same model at two providers is two engines
9
- // { kind: 'harness', harnessId, model? } a CLI coding agent the bridge runs (Claude
10
- // Code, Codex, …) — ChatPanel delegates a whole
11
- // task to it; `model` is what it was asked to run
12
- // { kind: 'auto', policy } the recruiter picks, by policy, from the
13
- // engine cards (§13.4) — the default
14
- // { kind: 'assistant' } the built-in Assistant: whatever model the chat
15
- // is on right now (`engineOf` resolves it)
16
- //
17
- // The RECORD keeps a flatter shape — `{ kind: 'model'|'harness', id, model? }`, see
18
- // scorecard.js `normalizeEngine` — because a record says what DID run, and `auto` and
19
- // `assistant` never run anything themselves. `engineRef` maps a spec to that shape once a
20
- // choice was made, so the scorecard's `byEngine` and the model ledger key the same way.
21
- //
22
- // Pure, dependency-free; team.js and agent.js both import from here, never from each other.
23
-
24
- export const ENGINE_KINDS = Object.freeze(['model', 'harness', 'auto', 'assistant']);
25
- export const ROUTE_PREFERS = Object.freeze(['cheapest-that-clears', 'best-quality', 'fastest', 'balanced']);
26
- export const HARNESS_ID_RE = /^[a-zA-Z0-9_.:@+-]{1,120}$/;
27
-
28
- const isRecord = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
29
- const str = (v, n = 200) => (v == null || v === '' ? undefined : String(v).trim().slice(0, n) || undefined);
30
- const num = (v) => { const n = Number(v); return v === '' || v == null || !Number.isFinite(n) ? undefined : n; };
31
- const refs = (xs) => (Array.isArray(xs) ? [...new Set(xs.map((x) => str(typeof x === 'string' ? x : engineKeyOf(x), 200)).filter(Boolean))] : undefined);
32
-
33
- /** A routing policy, normalized: an unknown preference is `balanced`; floors and ceilings are numbers or absent. */
34
- export function normalizePolicy(p) {
35
- const src = isRecord(p) ? p : {};
36
- const floor = {}; const ceiling = {};
37
- const q = num(src.floor?.quality); if (q !== undefined) floor.quality = Math.max(0, Math.min(1, q));
38
- const av = num(src.floor?.availability); if (av !== undefined) floor.availability = Math.max(0, Math.min(1, av));
39
- const c = num(src.ceiling?.costPerTask); if (c !== undefined && c >= 0) ceiling.costPerTask = c;
40
- const l = num(src.ceiling?.latencyMs); if (l !== undefined && l >= 0) ceiling.latencyMs = Math.round(l);
41
- const allow = refs(src.allow); const deny = refs(src.deny);
42
- return {
43
- prefer: ROUTE_PREFERS.includes(src.prefer) ? src.prefer : 'balanced',
44
- ...(Object.keys(floor).length ? { floor } : {}),
45
- ...(Object.keys(ceiling).length ? { ceiling } : {}),
46
- ...(allow?.length ? { allow } : {}),
47
- ...(deny?.length ? { deny } : {}),
48
- };
49
- }
50
-
51
- /**
52
- * An engine spec as stored. A string is read the obvious way — `assistant`, `auto`, a
53
- * `harness:<id>` / `model:<id>` prefix, or a bare model id — because a person types these
54
- * and a model proposes them in prose. Anything unreadable is `auto`, the honest default.
55
- */
56
- export function normalizeEngineSpec(e) {
57
- if (e == null || e === '') return { kind: 'auto', policy: normalizePolicy() };
58
- if (typeof e === 'string') {
59
- const s = e.trim();
60
- if (s === 'assistant' || s === 'auto') return s === 'assistant' ? { kind: 'assistant' } : { kind: 'auto', policy: normalizePolicy() };
61
- const m = /^(model|harness):(.+)$/.exec(s);
62
- if (m) return m[1] === 'harness' ? { kind: 'harness', harnessId: m[2].trim() } : { kind: 'model', model: m[2].trim() };
63
- return { kind: 'model', model: s };
64
- }
65
- if (!isRecord(e)) return { kind: 'auto', policy: normalizePolicy() };
66
- const kind = ENGINE_KINDS.includes(e.kind) ? e.kind : (e.harnessId ? 'harness' : e.model ? 'model' : e.policy ? 'auto' : 'auto');
67
- if (kind === 'assistant') return { kind };
68
- if (kind === 'auto') return { kind, policy: normalizePolicy(e.policy) };
69
- if (kind === 'harness') {
70
- const harnessId = str(e.harnessId || e.id, 120);
71
- if (!harnessId) return { kind: 'auto', policy: normalizePolicy() };
72
- const model = str(e.model, 200);
73
- return { kind, harnessId, ...(model ? { model } : {}) };
74
- }
75
- const model = str(e.model || e.id, 200);
76
- if (!model) return { kind: 'auto', policy: normalizePolicy() };
77
- const providerId = str(e.providerId || e.destination || e.endpointId, 120);
78
- return { kind: 'model', ...(providerId ? { providerId } : {}), model };
79
- }
80
-
81
- /** Is this a spec a validator should accept? Returns the errors, with a prefix. */
82
- export function validateEngineSpec(e, where = 'engine') {
83
- const errors = [];
84
- if (e == null || e === '') return errors;
85
- if (typeof e === 'string') return errors; // every string reads as something
86
- if (!isRecord(e)) return [`${where}: a string or an object`];
87
- if (e.kind !== undefined && !ENGINE_KINDS.includes(e.kind)) errors.push(`${where}.kind: one of ${ENGINE_KINDS.join(', ')}`);
88
- if (e.kind === 'harness' && !str(e.harnessId || e.id)) errors.push(`${where}.harnessId: which harness`);
89
- if (e.kind === 'harness' && str(e.harnessId || e.id) && !HARNESS_ID_RE.test(String(e.harnessId || e.id).trim())) errors.push(`${where}.harnessId: a short identifier`);
90
- if (e.kind === 'model' && !str(e.model || e.id)) errors.push(`${where}.model: which model`);
91
- if (e.kind === 'auto' && e.policy !== undefined && !isRecord(e.policy)) errors.push(`${where}.policy: an object`);
92
- if (e.kind === 'auto' && isRecord(e.policy) && e.policy.prefer !== undefined && !ROUTE_PREFERS.includes(e.policy.prefer)) errors.push(`${where}.policy.prefer: one of ${ROUTE_PREFERS.join(', ')}`);
93
- return errors;
94
- }
95
-
96
- /**
97
- * The record's shape for a spec that names something concrete — `{ kind, id, model? }`,
98
- * the same fields scorecard.js keys `byEngine` on and the model ledger is keyed by. `auto`
99
- * and `assistant` have no ref: nothing ran yet.
100
- */
101
- export function engineRef(spec) {
102
- const s = normalizeEngineSpec(spec);
103
- if (s.kind === 'harness') return { kind: 'harness', id: s.harnessId, ...(s.model ? { model: s.model } : {}) };
104
- if (s.kind === 'model') return s.providerId ? { kind: 'model', id: s.providerId, model: s.model } : { kind: 'model', id: s.model };
105
- return null;
106
- }
107
-
108
- /** The ledger key of a spec, or null when it names nothing concrete. Same key as `engineKey` in scorecard.js. */
109
- export function engineKeyOf(spec) {
110
- const r = engineRef(spec);
111
- return r ? `${r.kind}:${r.id}${r.model && r.model !== r.id ? `/${r.model}` : ''}` : null;
112
- }
113
-
114
- /** One phrase a person reads on a card: "Claude Code", "gpt-4o at openrouter", "auto · cheapest that clears", "the chat's model". */
115
- export function describeEngine(spec, { harnessName = (id) => id, providerName = (id) => id } = {}) {
116
- const s = normalizeEngineSpec(spec);
117
- if (s.kind === 'assistant') return 'the chat’s model';
118
- if (s.kind === 'auto') return `auto · ${s.policy.prefer.replace(/-/g, ' ')}`;
119
- if (s.kind === 'harness') return `${harnessName(s.harnessId)}${s.model ? ` (${s.model})` : ''}`;
120
- return `${s.model}${s.providerId ? ` at ${providerName(s.providerId)}` : ''}`;
121
- }
122
-
123
- /**
124
- * The role tier today's appointers understand (`cheap` / `balanced` / `strong`) for a spec:
125
- * the bridge to `prefer` until the recruiter routes by card (§13.4, step 5).
126
- */
127
- export function tierOf(spec) {
128
- const s = normalizeEngineSpec(spec);
129
- if (s.kind !== 'auto') return 'balanced';
130
- return { 'best-quality': 'strong', 'cheapest-that-clears': 'cheap', fastest: 'cheap', balanced: 'balanced' }[s.policy.prefer] || 'balanced';
131
- }
1
+ const f=Object.freeze(["model","harness","auto","assistant"]),p=Object.freeze(["cheapest-that-clears","best-quality","fastest","balanced"]),$=/^[a-zA-Z0-9_.:@+-]{1,120}$/,a=n=>n!==null&&typeof n=="object"&&!Array.isArray(n),s=(n,i=200)=>n==null||n===""?void 0:String(n).trim().slice(0,i)||void 0,l=n=>{const i=Number(n);return n===""||n==null||!Number.isFinite(i)?void 0:i},k=n=>Array.isArray(n)?[...new Set(n.map(i=>s(typeof i=="string"?i:I(i),200)).filter(Boolean))]:void 0;function d(n){const i=a(n)?n:{},t={},e={},r=l(i.floor?.quality);r!==void 0&&(t.quality=Math.max(0,Math.min(1,r)));const o=l(i.floor?.availability);o!==void 0&&(t.availability=Math.max(0,Math.min(1,o)));const c=l(i.ceiling?.costPerTask);c!==void 0&&c>=0&&(e.costPerTask=c);const u=l(i.ceiling?.latencyMs);u!==void 0&&u>=0&&(e.latencyMs=Math.round(u));const h=k(i.allow),y=k(i.deny);return{prefer:p.includes(i.prefer)?i.prefer:"balanced",...Object.keys(t).length?{floor:t}:{},...Object.keys(e).length?{ceiling:e}:{},...h?.length?{allow:h}:{},...y?.length?{deny:y}:{}}}function m(n){if(n==null||n==="")return{kind:"auto",policy:d()};if(typeof n=="string"){const r=n.trim();if(r==="assistant"||r==="auto")return r==="assistant"?{kind:"assistant"}:{kind:"auto",policy:d()};const o=/^(model|harness):(.+)$/.exec(r);return o?o[1]==="harness"?{kind:"harness",harnessId:o[2].trim()}:{kind:"model",model:o[2].trim()}:{kind:"model",model:r}}if(!a(n))return{kind:"auto",policy:d()};const i=f.includes(n.kind)?n.kind:n.harnessId?"harness":n.model?"model":(n.policy,"auto");if(i==="assistant")return{kind:i};if(i==="auto")return{kind:i,policy:d(n.policy)};if(i==="harness"){const r=s(n.harnessId||n.id,120);if(!r)return{kind:"auto",policy:d()};const o=s(n.model,200);return{kind:i,harnessId:r,...o?{model:o}:{}}}const t=s(n.model||n.id,200);if(!t)return{kind:"auto",policy:d()};const e=s(n.providerId||n.destination||n.endpointId,120);return{kind:"model",...e?{providerId:e}:{},model:t}}function b(n,i="engine"){const t=[];return n==null||n===""||typeof n=="string"?t:a(n)?(n.kind!==void 0&&!f.includes(n.kind)&&t.push(`${i}.kind: one of ${f.join(", ")}`),n.kind==="harness"&&!s(n.harnessId||n.id)&&t.push(`${i}.harnessId: which harness`),n.kind==="harness"&&s(n.harnessId||n.id)&&!$.test(String(n.harnessId||n.id).trim())&&t.push(`${i}.harnessId: a short identifier`),n.kind==="model"&&!s(n.model||n.id)&&t.push(`${i}.model: which model`),n.kind==="auto"&&n.policy!==void 0&&!a(n.policy)&&t.push(`${i}.policy: an object`),n.kind==="auto"&&a(n.policy)&&n.policy.prefer!==void 0&&!p.includes(n.policy.prefer)&&t.push(`${i}.policy.prefer: one of ${p.join(", ")}`),t):[`${i}: a string or an object`]}function g(n){const i=m(n);return i.kind==="harness"?{kind:"harness",id:i.harnessId,...i.model?{model:i.model}:{}}:i.kind==="model"?i.providerId?{kind:"model",id:i.providerId,model:i.model}:{kind:"model",id:i.model}:null}function I(n){const i=g(n);return i?`${i.kind}:${i.id}${i.model&&i.model!==i.id?`/${i.model}`:""}`:null}function x(n,{harnessName:i=e=>e,providerName:t=e=>e}={}){const e=m(n);return e.kind==="assistant"?"the chat’s model":e.kind==="auto"?`auto · ${e.policy.prefer.replace(/-/g," ")}`:e.kind==="harness"?`${i(e.harnessId)}${e.model?` (${e.model})`:""}`:`${e.model}${e.providerId?` at ${t(e.providerId)}`:""}`}function E(n){const i=m(n);return i.kind!=="auto"?"balanced":{"best-quality":"strong","cheapest-that-clears":"cheap",fastest:"cheap",balanced:"balanced"}[i.policy.prefer]||"balanced"}export{f as ENGINE_KINDS,$ as HARNESS_ID_RE,p as ROUTE_PREFERS,x as describeEngine,I as engineKeyOf,g as engineRef,m as normalizeEngineSpec,d as normalizePolicy,E as tierOf,b as validateEngineSpec};
package/entitlement.js CHANGED
@@ -1,332 +1 @@
1
- // PLANS, GATES AND THE SIGNED ENTITLEMENTone definition, every client.
2
- //
3
- // WHY THIS IS SHARED, and it is the sharpest example in the package. "Is this user Pro" is
4
- // currently answered in four places: the extension (`js/license.js`), the bridge
5
- // (`src/entitlement.js`), the gateway (`src/entitlement.js`) and now a desktop app. Three of
6
- // those already carry their own copy of the SAME public key, which is why `CLAUDE.md` has to
7
- // warn that rotating the signing key means editing every client by hand.
8
- //
9
- // Duplicating a feature matrix is worse than duplicating a helper: it does not fail loudly,
10
- // it fails as "Pro works in the browser but not on the desktop", which reads to the person
11
- // paying for it as the product being broken. Defining it once makes the two clients agree
12
- // BY CONSTRUCTION rather than by diligence.
13
- //
14
- // WHAT IS STILL THE CLIENT'S JOB (P8, as everywhere else): storing the licence, minting and
15
- // keeping an install id, opening a browser for checkout, and counting local usage against
16
- // the free limits. Those need storage and a platform. Everything here is pure.
17
- //
18
- // THE PRIVATE KEY IS NOT HERE AND NEVER WILL BE. The worker holds it; this file holds only
19
- // the public half, which is what lets every client verify OFFLINE — chat traffic never
20
- // touches the licence server, and a client that cannot reach the network keeps working.
21
-
22
- export class EntitlementError extends Error {
23
- constructor(message) { super(message); this.name = 'EntitlementError'; }
24
- }
25
-
26
- export const PLANS = Object.freeze(['free', 'pro', 'team']);
27
- const RANK = Object.freeze({ free: 0, pro: 1, team: 2 });
28
-
29
- /** Where the licence server lives. One base; the endpoints derive from it. */
30
- export const API_BASE = 'https://api.chatpanel.net';
31
- export const ENDPOINTS = Object.freeze({
32
- verify: `${API_BASE}/license/verify`,
33
- entitlement: `${API_BASE}/entitlement`,
34
- claim: `${API_BASE}/entitlement/claim`,
35
- release: `${API_BASE}/entitlement/release`,
36
- restore: `${API_BASE}/restore`,
37
- });
38
-
39
- /**
40
- * The PUBLIC half of the entitlement signing key.
41
- *
42
- * Byte-for-byte the key the extension and bridge already embed. If it is ever rotated it is
43
- * rotated HERE, and every client that takes this package inherits it on its next sync —
44
- * which is the entire reason this module exists.
45
- */
46
- export const ENTITLEMENT_PUBLIC_JWK = Object.freeze({
47
- kty: 'EC',
48
- crv: 'P-256',
49
- x: 'CmgKLC4e3xDMvwhbjVqF7jbDe1JhC1KKQi8JN3qVX_4',
50
- y: 'r40l6fQiyCcJYqW-SvB4VoSyn4F36yhSt82ZAOSo78E',
51
- });
52
-
53
- /** Where "Upgrade" sends people. A stable URL, so pricing can change without a release. */
54
- export const UPGRADE_URL = 'https://chatpanel.net/#pricing';
55
-
56
- export function checkoutUrl(plan = 'pro', installId = '', client = '') {
57
- const u = new URL('https://chatpanel.net/');
58
- if (installId) u.searchParams.set('install_id', installId);
59
- if (plan) u.searchParams.set('plan', plan);
60
- // Which client sent them, so a desktop purchase can be attributed and, later, seated
61
- // differently from a browser one. Additive: the site ignores what it does not read.
62
- if (client) u.searchParams.set('client', client);
63
- u.hash = 'pricing';
64
- return u.toString();
65
- }
66
-
67
- // --------------------------------------------------------------------------
68
- // The matrix
69
- // --------------------------------------------------------------------------
70
-
71
- /**
72
- * Every gated feature → the minimum plan that unlocks it. Anything absent is free.
73
- *
74
- * Features a given client cannot perform are still listed: the desktop cannot capture a
75
- * meeting, but it can READ one, and `liveMeetings` gates the dashboard and history search
76
- * as well as capture. A client omitting a key it "does not need" is how the two drift.
77
- */
78
- export const FEATURE_TIER = Object.freeze({
79
- localAgents: 'free',
80
- byoModels: 'free',
81
- urlContext: 'free',
82
- liveMeetings: 'free',
83
-
84
- multiTab: 'pro',
85
- unlimitedAgents: 'pro',
86
- customSkills: 'pro',
87
- customAgents: 'pro',
88
- advancedAgent: 'pro',
89
- unlimitedNotes: 'pro',
90
- unlimitedMeetings: 'pro',
91
- structuredInsert: 'pro',
92
- exportChats: 'pro',
93
- autoBackup: 'pro',
94
- promptLibrary: 'pro',
95
- fileAttachments: 'pro',
96
- watch: 'pro',
97
-
98
- cloudSync: 'team',
99
- sharedLibrary: 'team',
100
- hostedBridge: 'team',
101
- sso: 'team',
102
- });
103
-
104
- export const PRO_FEATURES = Object.freeze({
105
- unlimitedNotes: 'Unlimited notes — Free keeps your first 10',
106
- unlimitedMeetings: 'Unlimited meetings — Free keeps your first 10',
107
- multiTab: 'Attach several tabs at once',
108
- unlimitedAgents: 'Unlimited custom agents',
109
- customSkills: 'Create & edit your own skills',
110
- advancedAgent: 'Per-agent system prompts & working directories',
111
- structuredInsert: 'Clean diagrams on Excalidraw, draw.io & tldraw — shapes placed as data, not pixel-drawn',
112
- exportChats: 'Export conversations as Markdown',
113
- autoBackup: 'Automatic daily backup of all your data to disk',
114
- watch: 'Watch a page & act on changes — the agent reacts as the page updates',
115
- });
116
-
117
- export const TEAM_FEATURES = Object.freeze({
118
- cloudSync: 'Sync chats across your devices',
119
- sharedLibrary: 'Shared team agents & skills',
120
- hostedBridge: 'Hosted agents — no local bridge to run',
121
- sso: 'SSO & admin controls',
122
- });
123
-
124
- /** Free-tier ceilings. LIFETIME counts where noted — see the anti-cheat note in each client. */
125
- export const FREE_LIMITS = Object.freeze({
126
- notes: 10,
127
- meetings: 10,
128
- apiEndpoints: 1,
129
- bridgeAgents: 1,
130
- customAgents: 1,
131
- attachmentsPerMessage: 1,
132
- mcpServers: 1,
133
- gatewayDestinations: 1,
134
- webSearchEngines: 3,
135
- webSearchesPerDay: 50,
136
- fullRedactions: 25,
137
- });
138
-
139
- // --------------------------------------------------------------------------
140
- // Resolution
141
- // --------------------------------------------------------------------------
142
-
143
- /**
144
- * The plan actually in force.
145
- *
146
- * An expired licence is `free`, checked against an injected `now` so a client cannot be
147
- * tested only at the moment it happens to be run.
148
- */
149
- export function planOf(license, now = Date.now()) {
150
- if (!license || !PLANS.includes(license.plan)) return 'free';
151
- if (license.expiresAt && now > license.expiresAt) return 'free';
152
- return license.plan;
153
- }
154
-
155
- export function planLabel(license, now = Date.now()) {
156
- return { free: 'Free', pro: 'Pro', team: 'Team' }[planOf(license, now)];
157
- }
158
-
159
- export function isPro(license, now = Date.now()) {
160
- return RANK[planOf(license, now)] >= RANK.pro;
161
- }
162
-
163
- export function isTeam(license, now = Date.now()) {
164
- return planOf(license, now) === 'team';
165
- }
166
-
167
- /** Gate a feature. `count` expresses "this would be my Nth", for the counted allowances. */
168
- export function can(license, feature, count = 0, now = Date.now()) {
169
- const plan = planOf(license, now);
170
- if (feature === 'unlimitedAgents' && RANK[plan] < RANK.pro) {
171
- return count < FREE_LIMITS.customAgents;
172
- }
173
- const need = FEATURE_TIER[feature] || 'free';
174
- return RANK[plan] >= RANK[need];
175
- }
176
-
177
- /** The minimum plan a feature needs — for an upgrade prompt that names the right tier. */
178
- export function tierFor(feature) {
179
- return FEATURE_TIER[feature] || 'free';
180
- }
181
-
182
- /**
183
- * Is this the Nth use of a lifetime-capped free allowance?
184
- *
185
- * The count is MONOTONIC — "how many have ever been created", not "how many exist now" —
186
- * because a cap on the current count is lifted by deleting things, which is not a limit.
187
- */
188
- export function withinFreeLimit(license, key, everCount, now = Date.now()) {
189
- if (isPro(license, now)) return true;
190
- const cap = FREE_LIMITS[key];
191
- return typeof cap !== 'number' || everCount < cap;
192
- }
193
-
194
- // --------------------------------------------------------------------------
195
- // The signed token
196
- // --------------------------------------------------------------------------
197
-
198
- function b64urlToBytes(s) {
199
- const pad = '='.repeat((4 - (String(s).length % 4)) % 4);
200
- const b64 = String(s).replace(/-/g, '+').replace(/_/g, '/') + pad;
201
- if (typeof Buffer !== 'undefined') return new Uint8Array(Buffer.from(b64, 'base64'));
202
- const bin = atob(b64);
203
- const out = new Uint8Array(bin.length);
204
- for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
205
- return out;
206
- }
207
-
208
- function decodePayload(head) {
209
- try {
210
- return JSON.parse(new TextDecoder().decode(b64urlToBytes(head)));
211
- } catch {
212
- return null;
213
- }
214
- }
215
-
216
- let _keyPromise = null;
217
-
218
- async function verifyKey(subtle) {
219
- const s = subtle || globalThis.crypto?.subtle;
220
- if (!s) throw new EntitlementError('no WebCrypto available on this runtime');
221
- if (subtle) {
222
- // An injected implementation must not be memoised into the shared slot, or a test that
223
- // passes one would poison every later call in the process.
224
- return s.importKey('jwk', ENTITLEMENT_PUBLIC_JWK, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify']);
225
- }
226
- if (!_keyPromise) {
227
- _keyPromise = s.importKey('jwk', ENTITLEMENT_PUBLIC_JWK, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify']);
228
- }
229
- return _keyPromise;
230
- }
231
-
232
- /**
233
- * The payload field names the WORKER actually signs. Not negotiable, and not guessable:
234
- * `server/src/worker.js` grant() signs `{ typ:'ent', install_id, plan, sub, iat, exp }`,
235
- * and the extension checks `p.typ !== 'ent' || p.install_id !== installId`.
236
- *
237
- * Written down here because getting them wrong fails in the worst possible way: the
238
- * signature verifies, the token is genuine, and the binding check reads `undefined` — so
239
- * every real token is rejected and the user is told their licence is invalid. That is
240
- * exactly what happened when this module first read `payload.install`.
241
- */
242
- export const TOKEN_TYPE = 'ent';
243
-
244
- /**
245
- * Verify a server entitlement token and return its payload, or `null`.
246
- *
247
- * Four checks, and all four matter: the SIGNATURE (it came from the worker), the TYPE (a
248
- * `claim` token is also signed by the same key and must not be accepted as an entitlement),
249
- * the INSTALL BINDING (issued to this device, so a token copied from a forum grants
250
- * nothing), and EXPIRY. Returning null rather than throwing is deliberate — a caller
251
- * resolving a licence at startup must degrade to free, not crash the app.
252
- */
253
- export async function verifyEntitlement(token, installId, { subtle = null, now = Date.now() } = {}) {
254
- if (!token || typeof token !== 'string' || token.indexOf('.') < 0) return null;
255
- const [head, sig] = token.split('.');
256
- if (!head || !sig) return null;
257
-
258
- let ok = false;
259
- try {
260
- const s = subtle || globalThis.crypto?.subtle;
261
- ok = await s.verify(
262
- { name: 'ECDSA', hash: 'SHA-256' },
263
- await verifyKey(subtle),
264
- b64urlToBytes(sig),
265
- new TextEncoder().encode(head),
266
- );
267
- } catch {
268
- return null;
269
- }
270
- if (!ok) return null;
271
-
272
- const payload = decodePayload(head);
273
- if (!payload) return null;
274
- // The same key signs `claim` and `restore` tokens. Accepting one of those as an
275
- // entitlement would turn a portable, install-independent token into a licence.
276
- if (payload.typ !== TOKEN_TYPE) return null;
277
- if (payload.exp && now > Number(payload.exp)) return null;
278
- // `install_id` binds the token to one device. A token with no binding is refused rather
279
- // than treated as universal — "unbound" must never be the permissive case.
280
- if (!payload.install_id || (installId && payload.install_id !== installId)) return null;
281
- if (!PLANS.includes(payload.plan)) return null;
282
- return payload;
283
- }
284
-
285
- /**
286
- * A verified payload → the licence record a client stores.
287
- *
288
- * TWO EXPIRIES, AND CONFLATING THEM COSTS SOMEONE THEIR PRO.
289
- *
290
- * · `tokenExp` is the signed token's TTL — about a week. It exists so a revoked device
291
- * stops working without the server having to reach it.
292
- * · `expiresAt` is when the SUBSCRIPTION ends, which the server returns beside the token.
293
- *
294
- * `planOf` lapses a licence at `expiresAt`. Setting that from the token's TTL means anyone
295
- * offline for longer than the TTL silently drops to Free while still paying — which is
296
- * exactly the bug this signature now prevents by taking the subscription's date separately.
297
- * With no server date the licence does not expire locally; the next successful check is
298
- * what corrects it, and that is the safer direction to be wrong in.
299
- */
300
- export function licenseFromPayload(payload, { at = Date.now(), expiresAt = 0 } = {}) {
301
- if (!payload) return { plan: 'free' };
302
- return {
303
- plan: payload.plan,
304
- expiresAt: Number(expiresAt) || 0,
305
- tokenExp: Number(payload.exp) || 0,
306
- installId: payload.install_id || '',
307
- sub: payload.sub || null,
308
- checkedAt: at,
309
- source: 'entitlement',
310
- };
311
- }
312
-
313
- /**
314
- * Should this client re-check with the server yet?
315
- *
316
- * Offline verification is the point, so the cadence is generous — but a licence that has
317
- * NEVER been checked, or one whose expiry is close, is worth a call. Pure so the schedule is
318
- * testable without waiting a day.
319
- */
320
- export const RECHECK_INTERVAL_MS = 12 * 60 * 60 * 1000;
321
-
322
- export function needsRecheck(license, { now = Date.now(), interval = RECHECK_INTERVAL_MS } = {}) {
323
- if (!license || !license.checkedAt) return true;
324
- if (now - license.checkedAt > interval) return true;
325
- // Inside the last day of either deadline, check eagerly: a renewal lands near the
326
- // subscription date, and the token has to be replaced before ITS ttl runs out or the
327
- // client is left holding something it can no longer prove.
328
- const soon = 24 * 60 * 60 * 1000;
329
- if (license.expiresAt && license.expiresAt - now < soon) return true;
330
- if (license.tokenExp && license.tokenExp - now < soon) return true;
331
- return false;
332
- }
1
+ class A extends Error{constructor(t){super(t),this.name="EntitlementError"}}const d=Object.freeze(["free","pro","team"]),s=Object.freeze({free:0,pro:1,team:2}),l="https://api.chatpanel.net",T=Object.freeze({verify:`${l}/license/verify`,entitlement:`${l}/entitlement`,claim:`${l}/entitlement/claim`,release:`${l}/entitlement/release`,restore:`${l}/restore`}),m=Object.freeze({kty:"EC",crv:"P-256",x:"CmgKLC4e3xDMvwhbjVqF7jbDe1JhC1KKQi8JN3qVX_4",y:"r40l6fQiyCcJYqW-SvB4VoSyn4F36yhSt82ZAOSo78E"}),v="https://chatpanel.net/#pricing";function k(e="pro",t="",n=""){const r=new URL("https://chatpanel.net/");return t&&r.searchParams.set("install_id",t),e&&r.searchParams.set("plan",e),n&&r.searchParams.set("client",n),r.hash="pricing",r.toString()}const h=Object.freeze({localAgents:"free",byoModels:"free",urlContext:"free",liveMeetings:"free",multiTab:"pro",unlimitedAgents:"pro",customSkills:"pro",customAgents:"pro",advancedAgent:"pro",unlimitedNotes:"pro",unlimitedMeetings:"pro",structuredInsert:"pro",exportChats:"pro",autoBackup:"pro",promptLibrary:"pro",fileAttachments:"pro",watch:"pro",cloudSync:"team",sharedLibrary:"team",hostedBridge:"team",sso:"team"}),C=Object.freeze({unlimitedNotes:"Unlimited notes Free keeps your first 10",unlimitedMeetings:"Unlimited meetings — Free keeps your first 10",multiTab:"Attach several tabs at once",unlimitedAgents:"Unlimited custom agents",customSkills:"Create & edit your own skills",advancedAgent:"Per-agent system prompts & working directories",structuredInsert:"Clean diagrams on Excalidraw, draw.io & tldraw — shapes placed as data, not pixel-drawn",exportChats:"Export conversations as Markdown",autoBackup:"Automatic daily backup of all your data to disk",watch:"Watch a page & act on changes — the agent reacts as the page updates"}),D=Object.freeze({cloudSync:"Sync chats across your devices",sharedLibrary:"Shared team agents & skills",hostedBridge:"Hosted agents — no local bridge to run",sso:"SSO & admin controls"}),y=Object.freeze({notes:10,meetings:10,apiEndpoints:1,bridgeAgents:1,customAgents:1,attachmentsPerMessage:1,mcpServers:1,gatewayDestinations:1,webSearchEngines:3,webSearchesPerDay:50,fullRedactions:25});function c(e,t=Date.now()){return!e||!d.includes(e.plan)||e.expiresAt&&t>e.expiresAt?"free":e.plan}function P(e,t=Date.now()){return{free:"Free",pro:"Pro",team:"Team"}[c(e,t)]}function b(e,t=Date.now()){return s[c(e,t)]>=s.pro}function _(e,t=Date.now()){return c(e,t)==="team"}function O(e,t,n=0,r=Date.now()){const o=c(e,r);if(t==="unlimitedAgents"&&s[o]<s.pro)return n<y.customAgents;const i=h[t]||"free";return s[o]>=s[i]}function N(e){return h[e]||"free"}function R(e,t,n,r=Date.now()){if(b(e,r))return!0;const o=y[t];return typeof o!="number"||n<o}function p(e){const t="=".repeat((4-String(e).length%4)%4),n=String(e).replace(/-/g,"+").replace(/_/g,"/")+t;if(typeof Buffer<"u")return new Uint8Array(Buffer.from(n,"base64"));const r=atob(n),o=new Uint8Array(r.length);for(let i=0;i<r.length;i++)o[i]=r.charCodeAt(i);return o}function g(e){try{return JSON.parse(new TextDecoder().decode(p(e)))}catch{return null}}let f=null;async function x(e){const t=e||globalThis.crypto?.subtle;if(!t)throw new A("no WebCrypto available on this runtime");return e?t.importKey("jwk",m,{name:"ECDSA",namedCurve:"P-256"},!1,["verify"]):(f||(f=t.importKey("jwk",m,{name:"ECDSA",namedCurve:"P-256"},!1,["verify"])),f)}const w="ent";async function L(e,{typ:t=null,subtle:n=null,now:r=Date.now()}={}){if(!e||typeof e!="string"||e.indexOf(".")<0)return null;const[o,i]=e.split(".");if(!o||!i)return null;let u=!1;try{u=await(n||globalThis.crypto?.subtle).verify({name:"ECDSA",hash:"SHA-256"},await x(n),p(i),new TextEncoder().encode(o))}catch{return null}if(!u)return null;const a=g(o);return!a||t&&a.typ!==t||a.exp&&r>Number(a.exp)?null:a}async function U(e,t,{subtle:n=null,now:r=Date.now()}={}){if(!e||typeof e!="string"||e.indexOf(".")<0)return null;const[o,i]=e.split(".");if(!o||!i)return null;let u=!1;try{u=await(n||globalThis.crypto?.subtle).verify({name:"ECDSA",hash:"SHA-256"},await x(n),p(i),new TextEncoder().encode(o))}catch{return null}if(!u)return null;const a=g(o);return!a||a.typ!==w||a.exp&&r>Number(a.exp)||!a.install_id||t&&a.install_id!==t||!d.includes(a.plan)?null:a}function j(e,{at:t=Date.now(),expiresAt:n=0}={}){return e?{plan:e.plan,expiresAt:Number(n)||0,tokenExp:Number(e.exp)||0,installId:e.install_id||"",sub:e.sub||null,checkedAt:t,source:"entitlement"}:{plan:"free"}}const S=720*60*1e3;function F(e,{now:t=Date.now(),interval:n=S}={}){if(!e||!e.checkedAt||t-e.checkedAt>n)return!0;const r=1440*60*1e3;return!!(e.expiresAt&&e.expiresAt-t<r||e.tokenExp&&e.tokenExp-t<r)}export{l as API_BASE,T as ENDPOINTS,m as ENTITLEMENT_PUBLIC_JWK,A as EntitlementError,h as FEATURE_TIER,y as FREE_LIMITS,d as PLANS,C as PRO_FEATURES,S as RECHECK_INTERVAL_MS,D as TEAM_FEATURES,w as TOKEN_TYPE,v as UPGRADE_URL,O as can,k as checkoutUrl,b as isPro,_ as isTeam,j as licenseFromPayload,F as needsRecheck,P as planLabel,c as planOf,N as tierFor,U as verifyEntitlement,L as verifySignedToken,R as withinFreeLimit};