@chatpanel/events 0.101.0 → 0.103.1

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 (138) 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 +1 -4
  62. package/pdf-layout.js +7 -253
  63. package/project-run.js +20 -444
  64. package/project.js +1 -170
  65. package/promotion.js +1 -171
  66. package/queue.js +1 -98
  67. package/reach.js +1 -23
  68. package/recipe-tool.js +2 -161
  69. package/recipe.js +1 -264
  70. package/record-list.js +2 -110
  71. package/recruit.js +3 -456
  72. package/redaction-tokens.js +1 -61
  73. package/ref.js +1 -55
  74. package/registry.js +1 -240
  75. package/route-graph.js +1 -115
  76. package/route-strategies.js +2 -232
  77. package/router.js +2 -880
  78. package/rrf.js +1 -89
  79. package/rules.js +1 -142
  80. package/schedule.js +1 -663
  81. package/scm-connection.js +1 -180
  82. package/scopes.js +1 -12
  83. package/scorecard.js +1 -335
  84. package/search-engines.js +1 -81
  85. package/skill-manifest.js +1 -330
  86. package/skill-scan.js +3 -209
  87. package/skill-sources.js +1 -162
  88. package/skill-vars.js +2 -252
  89. package/slash-commands.js +1 -158
  90. package/source-gate.js +1 -92
  91. package/sources-retrieval.js +5 -186
  92. package/sources.js +1 -273
  93. package/store.js +1 -171
  94. package/structured.js +4 -901
  95. package/subject-kinds.js +1 -5
  96. package/subject-name.js +1 -96
  97. package/sync-plan.js +1 -213
  98. package/synthesis.js +3 -123
  99. package/tags.js +1 -203
  100. package/team-board.js +6 -337
  101. package/team-cache.js +2 -63
  102. package/team-observe.js +1 -118
  103. package/team-org.js +1 -348
  104. package/team-plan.js +4 -131
  105. package/team-record.js +1 -225
  106. package/team-run.js +9 -652
  107. package/team-subtask.js +2 -191
  108. package/team-task.js +2 -124
  109. package/team-tool.js +2 -196
  110. package/team-trail.js +2 -73
  111. package/team-worklog.js +2 -149
  112. package/team.js +1 -302
  113. package/text-search.js +1 -155
  114. package/theme.js +17 -154
  115. package/titles.js +4 -285
  116. package/tool-discovery.js +1 -87
  117. package/tool-dispatch.js +5 -230
  118. package/tool-groups.js +1 -81
  119. package/tool-hints.js +10 -185
  120. package/tool-loop-guard.js +4 -182
  121. package/tool-need.js +2 -124
  122. package/tool-result.js +2 -438
  123. package/tool-round.js +0 -0
  124. package/tool-schema.js +1 -155
  125. package/tool-traits.js +1 -182
  126. package/toolset.js +2 -88
  127. package/trajectory.js +1 -530
  128. package/turn-loop.js +4 -440
  129. package/upcast.js +1 -37
  130. package/vault.js +2 -250
  131. package/view.js +1 -88
  132. package/voice-intents.js +2 -1488
  133. package/voice-speaker.js +1 -98
  134. package/weather-tool.js +1 -65
  135. package/weather.js +2 -211
  136. package/web-search-tool.js +14 -102
  137. package/web-search.js +1 -160
  138. package/widget.js +1 -153
package/adapters.js CHANGED
@@ -1,83 +1 @@
1
- // Surface adapters the plugin contract for "this app is driven better by its own data
2
- // format than by pointer automation".
3
- //
4
- // Excalidraw is drawn by writing scene elements, a spreadsheet is filled by addressing
5
- // cells, a diagram tool by inserting nodes. Each of those is the SAME shape of thing:
6
- // recognise a page, offer a tool, say how to use it, execute it. That was hardcoded as a
7
- // list inside one 1,200-line module, which meant every new app edited a shared file and
8
- // nothing outside the extension could offer one at all.
9
- //
10
- // As a plugin contract instead (P15): an adapter is declared, registered with the kernel,
11
- // and discovered. The desktop and mobile clients get the same registry; a user or a skill
12
- // can eventually contribute one without touching this code.
13
- //
14
- // WHAT IS SHARED IS THE CONTRACT, NOT THE ADAPTER. Recognition and selection are pure and
15
- // live here; the execution of any real adapter needs a platform (chrome.scripting, CDP)
16
- // and therefore lives in the client, injected at registration.
17
-
18
- export class AdapterError extends Error {
19
- constructor(code, message) { super(message); this.name = 'AdapterError'; this.code = code; }
20
- }
21
-
22
- /**
23
- * Declare an adapter.
24
- *
25
- * @param matches (url, caps) => boolean. Given the URL and whatever capability probe the
26
- * host performed, so an adapter can recognise a self-hosted or embedded
27
- * instance rather than only a known hostname — the reason a hostname table
28
- * was rejected in the first place.
29
- * @param priority higher wins when two adapters match. Ties break on registration order,
30
- * so the answer is stable rather than dependent on activation timing.
31
- */
32
- export function defineAdapter({ id, label, matches, toolSpecs, guidance, execute, priority = 0 }) {
33
- if (!id) throw new AdapterError('BAD_ADAPTER', 'adapter.id required');
34
- if (typeof matches !== 'function') throw new AdapterError('BAD_ADAPTER', `adapter '${id}': matches required`);
35
- if (typeof execute !== 'function') throw new AdapterError('BAD_ADAPTER', `adapter '${id}': execute required`);
36
- return Object.freeze({
37
- id,
38
- label: label || id,
39
- matches,
40
- priority,
41
- toolSpecs: typeof toolSpecs === 'function' ? toolSpecs : () => [],
42
- guidance: typeof guidance === 'function' ? guidance : () => '',
43
- execute,
44
- });
45
- }
46
-
47
- /**
48
- * The registry a host binds adapters into.
49
- *
50
- * Deliberately independent of the kernel: a plugin registers through the kernel and calls
51
- * `add` from its activate, but a host with no kernel yet can still use this. Coupling them
52
- * would make the plugin model a prerequisite for a feature rather than a way to build it.
53
- */
54
- export function createAdapterRegistry() {
55
- const adapters = [];
56
- return {
57
- /** Register an adapter. Returns its remover, so registration is revertible (P15). */
58
- add(adapter) {
59
- adapters.push(adapter);
60
- return () => {
61
- const i = adapters.indexOf(adapter);
62
- if (i >= 0) adapters.splice(i, 1);
63
- };
64
- },
65
-
66
- list: () => [...adapters],
67
-
68
- /**
69
- * The adapter for a page, or null.
70
- *
71
- * A matcher that throws is treated as "no match" rather than taking the page down: one
72
- * badly-written adapter must not stop the others being offered, which is the same
73
- * isolation rule the source registry follows.
74
- */
75
- for(url, caps = {}) {
76
- const hits = adapters.filter((a) => {
77
- try { return !!a.matches(url, caps); } catch { return false; }
78
- });
79
- if (!hits.length) return null;
80
- return hits.sort((a, b) => b.priority - a.priority)[0];
81
- },
82
- };
83
- }
1
+ class f extends Error{constructor(t,e){super(e),this.name="AdapterError",this.code=t}}function c({id:r,label:t,matches:e,toolSpecs:o,guidance:n,execute:i,priority:u=0}){if(!r)throw new f("BAD_ADAPTER","adapter.id required");if(typeof e!="function")throw new f("BAD_ADAPTER",`adapter '${r}': matches required`);if(typeof i!="function")throw new f("BAD_ADAPTER",`adapter '${r}': execute required`);return Object.freeze({id:r,label:t||r,matches:e,priority:u,toolSpecs:typeof o=="function"?o:()=>[],guidance:typeof n=="function"?n:()=>"",execute:i})}function p(){const r=[];return{add(t){return r.push(t),()=>{const e=r.indexOf(t);e>=0&&r.splice(e,1)}},list:()=>[...r],for(t,e={}){const o=r.filter(n=>{try{return!!n.matches(t,e)}catch{return!1}});return o.length?o.sort((n,i)=>i.priority-n.priority)[0]:null}}}export{f as AdapterError,p as createAdapterRegistry,c as defineAdapter};
@@ -1,45 +1 @@
1
- const INVALID_PARAMS_RE = /\b(?:invalid request parameters|-32602)\b/i;
2
-
3
- export function resultText(result) {
4
- if (typeof result === 'string') return result;
5
- if (!result || typeof result !== 'object') return '';
6
- if (typeof result.text === 'string') return result.text;
7
- try {
8
- return JSON.stringify(result);
9
- } catch {
10
- return String(result);
11
- }
12
- }
13
-
14
- export function isInvalidToolParametersResult(result) {
15
- return INVALID_PARAMS_RE.test(resultText(result));
16
- }
17
-
18
- export function adaptiveToolRetryHint(name) {
19
- const tool = String(name || 'this tool');
20
- return [
21
- `Do not retry ${tool} with the same arguments.`,
22
- 'Match the user request to the tool domain first: Hacker News requests should use Hacker News tools, Confluence requests should use Confluence tools, and Jira requests should use Jira tools.',
23
- 'If the right tool is unavailable, answer with the exact tool error instead of trying unrelated tools.',
24
- ].join(' ');
25
- }
26
-
27
- export function createAdaptiveToolPolicy() {
28
- const suppressed = new Set();
29
-
30
- return {
31
- isSuppressed(name) {
32
- return suppressed.has(String(name || ''));
33
- },
34
- recordResult(name, result) {
35
- const toolName = String(name || '');
36
- if (toolName && isInvalidToolParametersResult(result)) suppressed.add(toolName);
37
- },
38
- filterOpenAITools(specs = []) {
39
- return (specs || []).filter((spec) => !suppressed.has(spec?.function?.name));
40
- },
41
- filterAnthropicTools(specs = []) {
42
- return (specs || []).filter((spec) => !suppressed.has(spec?.name));
43
- },
44
- };
45
- }
1
+ const n=/\b(?:invalid request parameters|-32602)\b/i;function s(t){if(typeof t=="string")return t;if(!t||typeof t!="object")return"";if(typeof t.text=="string")return t.text;try{return JSON.stringify(t)}catch{return String(t)}}function i(t){return n.test(s(t))}function a(t){return[`Do not retry ${String(t||"this tool")} with the same arguments.`,"Match the user request to the tool domain first: Hacker News requests should use Hacker News tools, Confluence requests should use Confluence tools, and Jira requests should use Jira tools.","If the right tool is unavailable, answer with the exact tool error instead of trying unrelated tools."].join(" ")}function u(){const t=new Set;return{isSuppressed(e){return t.has(String(e||""))},recordResult(e,o){const r=String(e||"");r&&i(o)&&t.add(r)},filterOpenAITools(e=[]){return(e||[]).filter(o=>!t.has(o?.function?.name))},filterAnthropicTools(e=[]){return(e||[]).filter(o=>!t.has(o?.name))}}}export{a as adaptiveToolRetryHint,u as createAdaptiveToolPolicy,i as isInvalidToolParametersResult,s as resultText};
package/agent.js CHANGED
@@ -1,256 +1,3 @@
1
- // An AGENT, as data — the pool an org recruits from. Nothing runs here.
2
- //
3
- // A team's roles were inlined: each carried its own prompt, tier and grants, so the same
4
- // "researcher" existed once per team and an edit landed in one of them. An agent is the role
5
- // definition promoted out of the team (F8 §8 A1, architecture-pillars.md §9): a persistent
6
- // identity — name, purpose, prompt, skills, grants, egress class, ENGINE, memory namespace —
7
- // with a scorecard the runner writes and the store attests (scorecard.js). One agent stands
8
- // in many teams; a team's role says `agent: <id>` and `resolveTeam` fills the role from the
9
- // pool at run time, so the runner (team-run.js) is unchanged.
10
- //
11
- // The engine is a field on the card, never a nav item: `model` / `harness` / `auto` /
12
- // `assistant` (engine.js). The built-in ASSISTANT is the agent behind every plain chat: its
13
- // engine is whichever model the chat is on, which is why `engineOf` takes the chat's model.
14
- //
15
- // Trust is derived, never declared (the team.js rule): `builtin` only when the host says so.
16
- // Grants are the team's vocabulary (team.js GRANT_RE), including the work grants a harness
17
- // engine may hold; `page` is never grantable. The pool is shared through the client-prefs
18
- // document (`agents` section) like teams, skills and recipes.
1
+ import{normalizeGrants as k,GRANT_RE as E,TeamError as A,normalizeTeam as x,slugTeamName as I}from"./team.js";import{normalizeEngineSpec as l,validateEngineSpec as N,describeEngine as O,engineRef as $,tierOf as B}from"./engine.js";const _=/^[a-z][a-z0-9_-]{0,63}$/i,u=Object.freeze(["jobs","meetings","notes"]),m=Object.freeze(["redacted","delegated"]),j=32,g="assistant";class z extends Error{constructor(r,t){super(t),this.name="AgentError",this.code=r}}const d=e=>e!==null&&typeof e=="object"&&!Array.isArray(e),h=(e,r,t)=>[...new Set((Array.isArray(e)?e:typeof e=="string"?e.split(/[,\s]+/):[]).map(o=>String(o||"").trim().slice(0,r)).filter(Boolean))].slice(0,t);function S(e){const r=[];if(!d(e))return{ok:!1,errors:["agent must be an object"]};_.test(String(e.id||""))||r.push("id: a short identifier (letters, digits, _ -)"),String(e.name||e.id||"").trim()||r.push("name: what to call it"),!String(e.prompt||"").trim()&&String(e.id)!==g&&r.push("prompt: what this agent does"),r.push(...N(e.engine,"engine"));const t=(Array.isArray(e.grants)?e.grants:[]).filter(o=>!E.test(String(o)));return t.length&&r.push(`grants: not grantable: ${t.join(", ")}${t.some(o=>/^page/.test(String(o)))?" (a tab is one person's; an agent may not act on it)":""}`),e.egress!==void 0&&e.egress!==null&&!m.includes(e.egress)&&r.push(`egress: one of ${m.join(", ")}`),e.appliesTo!==void 0&&(!Array.isArray(e.appliesTo)||e.appliesTo.some(o=>!u.includes(o)))&&r.push(`appliesTo: a list of ${u.join(", ")}`),e.skills!==void 0&&!Array.isArray(e.skills)&&typeof e.skills!="string"&&r.push("skills: a list of skill names"),{ok:r.length===0,errors:r}}function f(e,{builtin:r=!1}={}){const t=S(e);if(!t.ok)throw new z("INVALID",t.errors.join("; "));const o=String(e.id);return{id:o,name:String(e.name||o).trim().slice(0,60),purpose:String(e.purpose||"").trim().slice(0,300),prompt:String(e.prompt||"").trim().slice(0,8e3),skills:h(e.skills,80,j),grants:k(e.grants),engine:l(e.engine),...m.includes(e.egress)?{egress:e.egress}:{},appliesTo:Array.isArray(e.appliesTo)&&e.appliesTo.length?[...new Set(e.appliesTo.filter(i=>u.includes(i)))]:["jobs"],memoryScope:String(e.memoryScope||"").trim().slice(0,120)||`agent:${o}`,...e.workdir?{workdir:String(e.workdir).trim().slice(0,400)}:{},createdBy:String(e.createdBy||"person").slice(0,80),enabled:e.enabled!==!1,...e.origin&&d(e.origin)?{origin:{...e.origin}}:{},...r?{builtin:!0}:{},...e.createdAt?{createdAt:e.createdAt}:{}}}function Y(e,r){return Object.freeze(f(e,r))}function D({grants:e=["data","web","history","mcp"]}={}){return Y({id:g,name:"Assistant",purpose:"The chat itself: answers, uses the tools you connected, remembers what you tell it.",prompt:"",engine:"assistant",grants:e,appliesTo:["jobs","meetings","notes"],createdBy:"chatpanel"},{builtin:!0})}function P(e,{chatModel:r=null}={}){const t=e||{};if(t.engine!==void 0&&t.engine!==null){const i=l(t.engine);return i.kind!=="assistant"?i:r?l(typeof r=="string"?r:{kind:"model",providerId:r.providerId,model:r.model||r.id}):{kind:"auto",policy:{prefer:"balanced"}}}return t.model?l({kind:"model",model:t.model}):{kind:"auto",policy:{prefer:{cheap:"cheapest-that-clears",strong:"best-quality",balanced:"balanced"}[t.prefer]||"balanced"}}}function C(e,r){const t=e||{};return`${t.name||t.id} ${O(t.engine,r)} · tools: ${(t.grants||["none"]).join(", ")}${t.skills?.length?` · skills: ${t.skills.join(", ")}`:""}`}function L(e){return I(e)}function J(e,r=[],{chatModel:t=null,targetFor:o=null}={}){const i=x(e),p=new Map((Array.isArray(r)?r:[]).filter(n=>n&&n.id).map(n=>[String(n.id),n])),b={},T=i.roles.map(n=>{if(!n.agent)return n;const c=p.get(n.agent)||(n.agent===g?D():null);if(!c)throw new A("NO_AGENT",`role "${n.id}" names agent "${n.agent}", which is not in the pool`);const s=f(c,{builtin:!!c.builtin});if(s.enabled===!1)throw new A("NO_AGENT",`agent "${s.id}" is disabled`);b[n.id]=s;const a=P(n.engine?{engine:n.engine}:s,{chatModel:t}),y=$(a)?o?o(a,s):a.kind==="harness"?a.harnessId:a.model:null,v=Array.isArray(n.grants)?s.grants.includes("none")?["none"]:k(n.grants.filter(w=>s.grants.includes(w)||w.startsWith("mcp:")&&s.grants.includes("mcp"))):s.grants,R=[s.prompt,n.prompt?`In this team: ${n.prompt}`:""].filter(Boolean).join(`
19
2
 
20
- import { normalizeGrants, GRANT_RE, TeamError, normalizeTeam, slugTeamName } from './team.js';
21
- import { normalizeEngineSpec, validateEngineSpec, describeEngine, engineRef, tierOf } from './engine.js';
22
-
23
- export const AGENT_ID_RE = /^[a-z][a-z0-9_-]{0,63}$/i;
24
- export const APPLIES_TO = Object.freeze(['jobs', 'meetings', 'notes']);
25
- export const EGRESS_CLASSES = Object.freeze(['redacted', 'delegated']);
26
- export const MAX_SKILLS = 32;
27
- export const ASSISTANT_ID = 'assistant';
28
-
29
- export class AgentError extends Error {
30
- constructor(code, message) { super(message); this.name = 'AgentError'; this.code = code; }
31
- }
32
-
33
- const isRecord = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
34
- const list = (xs, n, max) => [...new Set((Array.isArray(xs) ? xs : typeof xs === 'string' ? xs.split(/[,\s]+/) : []).map((x) => String(x || '').trim().slice(0, n)).filter(Boolean))].slice(0, max);
35
-
36
- export function validateAgent(agent) {
37
- const errors = [];
38
- if (!isRecord(agent)) return { ok: false, errors: ['agent must be an object'] };
39
- if (!AGENT_ID_RE.test(String(agent.id || ''))) errors.push('id: a short identifier (letters, digits, _ -)');
40
- if (!String(agent.name || agent.id || '').trim()) errors.push('name: what to call it');
41
- if (!String(agent.prompt || '').trim() && String(agent.id) !== ASSISTANT_ID) errors.push('prompt: what this agent does');
42
- errors.push(...validateEngineSpec(agent.engine, 'engine'));
43
- const bad = (Array.isArray(agent.grants) ? agent.grants : []).filter((g) => !GRANT_RE.test(String(g)));
44
- if (bad.length) errors.push(`grants: not grantable: ${bad.join(', ')}${bad.some((g) => /^page/.test(String(g))) ? ' (a tab is one person\'s; an agent may not act on it)' : ''}`);
45
- if (agent.egress !== undefined && agent.egress !== null && !EGRESS_CLASSES.includes(agent.egress)) errors.push(`egress: one of ${EGRESS_CLASSES.join(', ')}`);
46
- if (agent.appliesTo !== undefined && (!Array.isArray(agent.appliesTo) || agent.appliesTo.some((a) => !APPLIES_TO.includes(a)))) errors.push(`appliesTo: a list of ${APPLIES_TO.join(', ')}`);
47
- if (agent.skills !== undefined && !Array.isArray(agent.skills) && typeof agent.skills !== 'string') errors.push('skills: a list of skill names');
48
- return { ok: errors.length === 0, errors };
49
- }
50
-
51
- /**
52
- * The stored form. Defaults filled, grants normalized, engine normalized, trust derived. The
53
- * memory namespace defaults to the agent's own (`agent:<id>`); a team may point several
54
- * agents at one shared namespace by naming it.
55
- */
56
- export function normalizeAgent(agent, { builtin = false } = {}) {
57
- const v = validateAgent(agent);
58
- if (!v.ok) throw new AgentError('INVALID', v.errors.join('; '));
59
- const id = String(agent.id);
60
- return {
61
- id,
62
- name: String(agent.name || id).trim().slice(0, 60),
63
- purpose: String(agent.purpose || '').trim().slice(0, 300),
64
- prompt: String(agent.prompt || '').trim().slice(0, 8000),
65
- skills: list(agent.skills, 80, MAX_SKILLS),
66
- grants: normalizeGrants(agent.grants),
67
- engine: normalizeEngineSpec(agent.engine),
68
- ...(EGRESS_CLASSES.includes(agent.egress) ? { egress: agent.egress } : {}),
69
- appliesTo: Array.isArray(agent.appliesTo) && agent.appliesTo.length ? [...new Set(agent.appliesTo.filter((a) => APPLIES_TO.includes(a)))] : ['jobs'],
70
- memoryScope: String(agent.memoryScope || '').trim().slice(0, 120) || `agent:${id}`,
71
- ...(agent.workdir ? { workdir: String(agent.workdir).trim().slice(0, 400) } : {}),
72
- createdBy: String(agent.createdBy || 'person').slice(0, 80),
73
- enabled: agent.enabled !== false,
74
- ...(agent.origin && isRecord(agent.origin) ? { origin: { ...agent.origin } } : {}),
75
- ...(builtin ? { builtin: true } : {}),
76
- ...(agent.createdAt ? { createdAt: agent.createdAt } : {}),
77
- };
78
- }
79
-
80
- export function defineAgent(agent, opts) { return Object.freeze(normalizeAgent(agent, opts)); }
81
-
82
- /**
83
- * The built-in Assistant — the agent behind every plain chat. Its engine is the chat's model
84
- * (`engineOf` resolves it); its grants are whatever the chat has; it applies everywhere.
85
- */
86
- export function assistantAgent({ grants = ['data', 'web', 'history', 'mcp'] } = {}) {
87
- return defineAgent({
88
- id: ASSISTANT_ID, name: 'Assistant', purpose: 'The chat itself: answers, uses the tools you connected, remembers what you tell it.',
89
- prompt: '', engine: 'assistant', grants, appliesTo: ['jobs', 'meetings', 'notes'], createdBy: 'chatpanel',
90
- }, { builtin: true });
91
- }
92
-
93
- /**
94
- * WHAT RUNS this agent's turns, resolved: the Assistant's engine is the chat's model (the
95
- * host passes `chatModel` as `{ providerId?, model }` or a string), which is why the
96
- * Assistant has no engine of its own. Everything else returns its normalized spec. A legacy
97
- * role (no `engine`) reads as `model` when it pins one, else `auto` at its tier.
98
- */
99
- export function engineOf(agentOrRole, { chatModel = null } = {}) {
100
- const a = agentOrRole || {};
101
- if (a.engine !== undefined && a.engine !== null) {
102
- const s = normalizeEngineSpec(a.engine);
103
- if (s.kind !== 'assistant') return s;
104
- if (chatModel) return normalizeEngineSpec(typeof chatModel === 'string' ? chatModel : { kind: 'model', providerId: chatModel.providerId, model: chatModel.model || chatModel.id });
105
- return { kind: 'auto', policy: { prefer: 'balanced' } };
106
- }
107
- if (a.model) return normalizeEngineSpec({ kind: 'model', model: a.model });
108
- const prefer = { cheap: 'cheapest-that-clears', strong: 'best-quality', balanced: 'balanced' }[a.prefer] || 'balanced';
109
- return { kind: 'auto', policy: { prefer } };
110
- }
111
-
112
- /** The capability strip's first column (§8): what kind of thing this agent is, in a word. */
113
- export function describeAgent(agent, opts) {
114
- const a = agent || {};
115
- return `${a.name || a.id} — ${describeEngine(a.engine, opts)} · tools: ${(a.grants || ['none']).join(', ')}${a.skills?.length ? ` · skills: ${a.skills.join(', ')}` : ''}`;
116
- }
117
-
118
- export function slugAgentId(name) { return slugTeamName(name); }
119
-
120
- /**
121
- * A team as the runner needs it: every role that says `agent` is filled from the pool —
122
- * prompt, grants, skills, engine, working directory — and the role's own fields narrow it.
123
- *
124
- * • prompt the agent's, then the role's ("In this team: …") when the role adds one
125
- * • grants the agent's, narrowed to the role's when the role lists any (a team may not
126
- * widen an agent; it may lend less)
127
- * • engine the role's override when it has one, else the agent's, with the Assistant
128
- * resolved to `chatModel`; `prefer` follows for today's appointers
129
- * • model what `callModel` is handed — `targetFor(engine)` when the host maps engines
130
- * to its target ids (the extension's endpoint ids, the desktop's gateway ids),
131
- * else the harness id or the model name
132
- *
133
- * A role naming an agent not in the pool throws NO_AGENT: a team is not run with a hole in
134
- * it. Returns the normalized team plus `agents` — the resolved cards, by role id.
135
- */
136
- export function resolveTeam(team, pool = [], { chatModel = null, targetFor = null } = {}) {
137
- const t = normalizeTeam(team);
138
- const byId = new Map((Array.isArray(pool) ? pool : []).filter((a) => a && a.id).map((a) => [String(a.id), a]));
139
- const agents = {};
140
- const roles = t.roles.map((r) => {
141
- if (!r.agent) return r;
142
- const raw = byId.get(r.agent) || (r.agent === ASSISTANT_ID ? assistantAgent() : null);
143
- if (!raw) throw new TeamError('NO_AGENT', `role "${r.id}" names agent "${r.agent}", which is not in the pool`);
144
- const a = normalizeAgent(raw, { builtin: !!raw.builtin });
145
- if (a.enabled === false) throw new TeamError('NO_AGENT', `agent "${a.id}" is disabled`);
146
- agents[r.id] = a;
147
- const engine = engineOf(r.engine ? { engine: r.engine } : a, { chatModel });
148
- const ref = engineRef(engine);
149
- const target = ref ? (targetFor ? targetFor(engine, a) : (engine.kind === 'harness' ? engine.harnessId : engine.model)) : null;
150
- const grants = !Array.isArray(r.grants) ? a.grants
151
- : a.grants.includes('none') ? ['none']
152
- : normalizeGrants(r.grants.filter((g) => a.grants.includes(g) || (g.startsWith('mcp:') && a.grants.includes('mcp'))));
153
- const prompt = [a.prompt, r.prompt ? `In this team: ${r.prompt}` : ''].filter(Boolean).join('\n\n').slice(0, 8000);
154
- return {
155
- ...r,
156
- name: r.name === r.id ? a.name : r.name,
157
- prompt,
158
- grants,
159
- engine,
160
- prefer: engine.kind === 'auto' ? tierOf(engine) : r.prefer,
161
- ...(target ? { model: r.model || target } : {}),
162
- ...(a.skills.length ? { skills: [...a.skills] } : {}),
163
- ...(a.workdir ? { workdir: a.workdir } : {}),
164
- ...(a.egress ? { egress: a.egress } : {}),
165
- ...(a.memoryScope ? { memoryScope: a.memoryScope } : {}),
166
- };
167
- });
168
- return { ...t, roles, agents };
169
- }
170
-
171
- // ── The standing org and the editor's form ────────────────────────────────────────────────
172
- //
173
- // "ChatPanel Engineering" (architecture-pillars.md §12.2) — the first standing agents,
174
- // offered as starters the way `research` and `review` are for teams. Every Implementer is
175
- // this ONE card with a different working directory: the org repo's `agents/*.json` (§14.3)
176
- // is where a company keeps one per repo. Engines are `auto` / `harness:<id>` so a starter
177
- // does not name a provider a person may not have; the Harness engines say `claude` because
178
- // that is the bridge's id for Claude Code and the pilot's choice — change it on the card.
179
-
180
- export const STARTER_AGENTS = Object.freeze([
181
- // The EXECUTIVE holds a goal (project-run.js): it posts the jobs, recruits for each from
182
- // the pool, runs the recruited as a team, reads what came back, posts the follow-ups, asks
183
- // the stakeholder where the gate says a person decides, and closes when done-when holds.
184
- // A person is the stakeholder by default; this is the manager they delegate the running to.
185
- { id: 'executive', name: 'Executive', purpose: 'Runs a project: posts the jobs for the goal, recruits from the pool, reads the results, posts follow-ups, asks before spending or changing scope, closes when done-when holds.',
186
- prompt: 'You are the Executive. You hold one goal and its done-when. Break the goal into jobs a stranger could act on, each naming the skills and tools it needs; prefer jobs that run at the same time and use dependsOn only when one truly needs another\'s result. Read every result against done-when as written — say it holds only when it does on the results as they are, not as they could be. Post follow-up jobs only for what would move done-when. Say plainly what was not found. You never do a job yourself and never create an agent, tool or skill without a person\'s approval.',
187
- skills: ['planning', 'management'], grants: ['none'], engine: { kind: 'auto', policy: { prefer: 'best-quality' } }, appliesTo: ['jobs'] },
188
- { id: 'architect', name: 'Architect', purpose: 'Reads the docs and the repos; writes the project page and the jobs.',
189
- prompt: 'You are the Architect. Read the feature doc, ROADMAP.md, naming-revamp.md and architecture-pillars.md before deciding anything. Write the project page (goal, done-when, budget) and post one job per repo that must change, saying which repo and what the guard is. Propose a new agent type only when no one in the pool fits. Never run a shell.',
190
- skills: [], grants: ['data', 'history'], engine: { kind: 'auto', policy: { prefer: 'best-quality' } }, appliesTo: ['jobs'] },
191
- { id: 'implementer', name: 'Implementer', purpose: 'Builds one job on a branch in one repo; runs the guard; posts on the thread. Never pushes to main, never publishes.',
192
- prompt: 'You are an Implementer. Work only in the repository you were given, on the branch named for this job. Read the job thread first. Make the change, run the repository\'s guard (tools/test-*.mjs or npm test) until it passes, commit with a message that says what and why, and post a summary with the diff stat on the job thread. Do not push to main, do not publish, do not touch another repository.',
193
- skills: [], grants: ['shell', 'fs:write', 'scm:read', 'scm:push', 'scm:pr'], engine: { kind: 'harness', harnessId: 'claude' }, appliesTo: ['jobs'] },
194
- { id: 'reviewer', name: 'Reviewer', purpose: 'Reads the branch diff; posts findings on the thread. Approve or reject is a person\'s.',
195
- prompt: 'You are the Reviewer. Read the diff of the job\'s branch against its base. Post each finding as a reply on the job thread: what, where (file:line), why it matters, what to do instead. Do not edit files. Do not approve or reject — say what you found and let a person decide.',
196
- skills: ['review'], grants: ['shell', 'scm:read'], engine: { kind: 'harness', harnessId: 'claude' }, appliesTo: ['jobs'] },
197
- { id: 'tester', name: 'Tester', purpose: 'Runs the repository\'s guard on the branch; reports the result. No writes.',
198
- prompt: 'You are the Tester. Check out the job\'s branch in its worktree and run the repository\'s guard (tools/test-*.mjs, npm test, the build). Report pass/fail with the failing output verbatim. Do not change any file.',
199
- skills: [], grants: ['shell', 'scm:read'], engine: { kind: 'harness', harnessId: 'claude' }, appliesTo: ['jobs'] },
200
- { id: 'librarian', name: 'Librarian', purpose: 'Before every job: what was already done for this, and where.',
201
- prompt: 'You are the Librarian. Before a job starts, search the runs, boards, briefs and docs for work that already covers it. Reply on the job thread with "already done in …" pointers and what could be reused or extended. Never do the job yourself.',
202
- skills: [], grants: ['data', 'history'], engine: { kind: 'auto', policy: { prefer: 'cheapest-that-clears' } }, appliesTo: ['jobs'] },
203
- { id: 'scribe', name: 'Scribe', purpose: 'Writes what the run taught into the roadmap and the status row — proposed, a person lands it.',
204
- prompt: 'You are the Scribe. From the run\'s board and scorecards, write the "what this run taught" paragraph for the feature doc, the ROADMAP.md entry and the IMPLEMENTATION-STATUS.md row. Post them as a proposal on the thread; a person lands them. Say plainly what did not work.',
205
- skills: [], grants: ['data', 'history'], engine: { kind: 'auto', policy: { prefer: 'balanced' } }, appliesTo: ['jobs', 'notes'] },
206
- { id: 'release', name: 'Release', purpose: 'Version bump and changelog on the merged branch; asks before publish or push.',
207
- prompt: 'You are Release. On the merged branch: bump the version, write the changelog line, run the guard. Before `npm publish` or any `git push`, stop and ask on the thread — a person answers. Never publish or push without that answer.',
208
- skills: [], grants: ['shell', 'fs:write', 'scm:read', 'scm:push'], engine: { kind: 'harness', harnessId: 'claude' }, appliesTo: ['jobs'] },
209
- ]);
210
-
211
- /** Fresh copies — a starter is a template, never the stored record. */
212
- export function starterAgents() {
213
- return STARTER_AGENTS.map((a) => JSON.parse(JSON.stringify(a)));
214
- }
215
-
216
- /** A blank agent for the editor. */
217
- export function blankAgent() {
218
- return { id: '', name: '', purpose: '', prompt: '', skills: [], grants: ['none'], engine: { kind: 'auto', policy: { prefer: 'balanced' } }, appliesTo: ['jobs'], memoryScope: '' };
219
- }
220
-
221
- /**
222
- * The editor's form → an agent, or the errors. Grants and skills come as text ("web, data",
223
- * "review, graphify"); the engine as the form's own shape (`{ kind, model, providerId,
224
- * harnessId, prefer }`) or a string; a blank id is slugged from the name.
225
- */
226
- export function agentFromForm(form) {
227
- const f = form || {};
228
- const id = String(f.id || '').trim() || slugAgentId(f.name);
229
- const engine = isRecord(f.engine)
230
- ? (f.engine.kind === 'auto' ? { kind: 'auto', policy: { prefer: f.engine.prefer || f.engine.policy?.prefer || 'balanced', ...(f.engine.policy || {}) } } : f.engine)
231
- : f.engine;
232
- const agent = {
233
- id,
234
- name: String(f.name || '').trim() || id,
235
- purpose: String(f.purpose || '').trim(),
236
- prompt: String(f.prompt || ''),
237
- skills: list(f.skills, 80, MAX_SKILLS),
238
- grants: list(f.grants, 80, 32).length ? list(f.grants, 80, 32) : ['none'],
239
- engine,
240
- ...(f.egress ? { egress: f.egress } : {}),
241
- appliesTo: Array.isArray(f.appliesTo) && f.appliesTo.length ? f.appliesTo : ['jobs'],
242
- ...(f.memoryScope ? { memoryScope: f.memoryScope } : {}),
243
- ...(f.workdir ? { workdir: f.workdir } : {}),
244
- createdBy: f.createdBy || 'person',
245
- enabled: f.enabled !== false,
246
- ...(f.origin && isRecord(f.origin) ? { origin: f.origin } : {}),
247
- ...(f.createdAt ? { createdAt: f.createdAt } : {}),
248
- };
249
- const v = validateAgent(agent);
250
- return v.ok ? { ok: true, agent: normalizeAgent(agent) } : { ok: false, errors: v.errors };
251
- }
252
-
253
- /** Which agents apply to jobs — the pool a job board recruits from. */
254
- export function poolFor(agents, surface = 'jobs') {
255
- return (Array.isArray(agents) ? agents : []).filter((a) => a && a.enabled !== false && (Array.isArray(a.appliesTo) ? a.appliesTo : ['jobs']).includes(surface));
256
- }
3
+ `).slice(0,8e3);return{...n,name:n.name===n.id?s.name:n.name,prompt:R,grants:v,engine:a,prefer:a.kind==="auto"?B(a):n.prefer,...y?{model:n.model||y}:{},...s.skills.length?{skills:[...s.skills]}:{},...s.workdir?{workdir:s.workdir}:{},...s.egress?{egress:s.egress}:{},...s.memoryScope?{memoryScope:s.memoryScope}:{}}});return{...i,roles:T,agents:b}}const G=Object.freeze([{id:"executive",name:"Executive",purpose:"Runs a project: posts the jobs for the goal, recruits from the pool, reads the results, posts follow-ups, asks before spending or changing scope, closes when done-when holds.",prompt:"You are the Executive. You hold one goal and its done-when. Break the goal into jobs a stranger could act on, each naming the skills and tools it needs; prefer jobs that run at the same time and use dependsOn only when one truly needs another's result. Read every result against done-when as written — say it holds only when it does on the results as they are, not as they could be. Post follow-up jobs only for what would move done-when. Say plainly what was not found. You never do a job yourself and never create an agent, tool or skill without a person's approval.",skills:["planning","management"],grants:["none"],engine:{kind:"auto",policy:{prefer:"best-quality"}},appliesTo:["jobs"]},{id:"architect",name:"Architect",purpose:"Reads the docs and the repos; writes the project page and the jobs.",prompt:"You are the Architect. Read the feature doc, ROADMAP.md, naming-revamp.md and architecture-pillars.md before deciding anything. Write the project page (goal, done-when, budget) and post one job per repo that must change, saying which repo and what the guard is. Propose a new agent type only when no one in the pool fits. Never run a shell.",skills:[],grants:["data","history"],engine:{kind:"auto",policy:{prefer:"best-quality"}},appliesTo:["jobs"]},{id:"implementer",name:"Implementer",purpose:"Builds one job on a branch in one repo; runs the guard; posts on the thread. Never pushes to main, never publishes.",prompt:"You are an Implementer. Work only in the repository you were given, on the branch named for this job. Read the job thread first. Make the change, run the repository's guard (tools/test-*.mjs or npm test) until it passes, commit with a message that says what and why, and post a summary with the diff stat on the job thread. Do not push to main, do not publish, do not touch another repository.",skills:[],grants:["shell","fs:write","scm:read","scm:push","scm:pr"],engine:{kind:"harness",harnessId:"claude"},appliesTo:["jobs"]},{id:"reviewer",name:"Reviewer",purpose:"Reads the branch diff; posts findings on the thread. Approve or reject is a person's.",prompt:"You are the Reviewer. Read the diff of the job's branch against its base. Post each finding as a reply on the job thread: what, where (file:line), why it matters, what to do instead. Do not edit files. Do not approve or reject — say what you found and let a person decide.",skills:["review"],grants:["shell","scm:read"],engine:{kind:"harness",harnessId:"claude"},appliesTo:["jobs"]},{id:"tester",name:"Tester",purpose:"Runs the repository's guard on the branch; reports the result. No writes.",prompt:"You are the Tester. Check out the job's branch in its worktree and run the repository's guard (tools/test-*.mjs, npm test, the build). Report pass/fail with the failing output verbatim. Do not change any file.",skills:[],grants:["shell","scm:read"],engine:{kind:"harness",harnessId:"claude"},appliesTo:["jobs"]},{id:"librarian",name:"Librarian",purpose:"Before every job: what was already done for this, and where.",prompt:'You are the Librarian. Before a job starts, search the runs, boards, briefs and docs for work that already covers it. Reply on the job thread with "already done in …" pointers and what could be reused or extended. Never do the job yourself.',skills:[],grants:["data","history"],engine:{kind:"auto",policy:{prefer:"cheapest-that-clears"}},appliesTo:["jobs"]},{id:"scribe",name:"Scribe",purpose:"Writes what the run taught into the roadmap and the status row — proposed, a person lands it.",prompt:`You are the Scribe. From the run's board and scorecards, write the "what this run taught" paragraph for the feature doc, the ROADMAP.md entry and the IMPLEMENTATION-STATUS.md row. Post them as a proposal on the thread; a person lands them. Say plainly what did not work.`,skills:[],grants:["data","history"],engine:{kind:"auto",policy:{prefer:"balanced"}},appliesTo:["jobs","notes"]},{id:"release",name:"Release",purpose:"Version bump and changelog on the merged branch; asks before publish or push.",prompt:"You are Release. On the merged branch: bump the version, write the changelog line, run the guard. Before `npm publish` or any `git push`, stop and ask on the thread — a person answers. Never publish or push without that answer.",skills:[],grants:["shell","fs:write","scm:read","scm:push"],engine:{kind:"harness",harnessId:"claude"},appliesTo:["jobs"]}]);function M(){return G.map(e=>JSON.parse(JSON.stringify(e)))}function V(){return{id:"",name:"",purpose:"",prompt:"",skills:[],grants:["none"],engine:{kind:"auto",policy:{prefer:"balanced"}},appliesTo:["jobs"],memoryScope:""}}function K(e){const r=e||{},t=String(r.id||"").trim()||L(r.name),o=d(r.engine)&&r.engine.kind==="auto"?{kind:"auto",policy:{prefer:r.engine.prefer||r.engine.policy?.prefer||"balanced",...r.engine.policy||{}}}:r.engine,i={id:t,name:String(r.name||"").trim()||t,purpose:String(r.purpose||"").trim(),prompt:String(r.prompt||""),skills:h(r.skills,80,j),grants:h(r.grants,80,32).length?h(r.grants,80,32):["none"],engine:o,...r.egress?{egress:r.egress}:{},appliesTo:Array.isArray(r.appliesTo)&&r.appliesTo.length?r.appliesTo:["jobs"],...r.memoryScope?{memoryScope:r.memoryScope}:{},...r.workdir?{workdir:r.workdir}:{},createdBy:r.createdBy||"person",enabled:r.enabled!==!1,...r.origin&&d(r.origin)?{origin:r.origin}:{},...r.createdAt?{createdAt:r.createdAt}:{}},p=S(i);return p.ok?{ok:!0,agent:f(i)}:{ok:!1,errors:p.errors}}function U(e,r="jobs"){return(Array.isArray(e)?e:[]).filter(t=>t&&t.enabled!==!1&&(Array.isArray(t.appliesTo)?t.appliesTo:["jobs"]).includes(r))}export{_ as AGENT_ID_RE,u as APPLIES_TO,g as ASSISTANT_ID,z as AgentError,m as EGRESS_CLASSES,j as MAX_SKILLS,G as STARTER_AGENTS,K as agentFromForm,D as assistantAgent,V as blankAgent,Y as defineAgent,C as describeAgent,P as engineOf,f as normalizeAgent,U as poolFor,J as resolveTeam,L as slugAgentId,M as starterAgents,S as validateAgent};
package/attribution.js CHANGED
@@ -1,132 +1 @@
1
- // AUTHORSHIP who wrote which run of a document, and the versions you can go back to.
2
- //
3
- // Moved here from the extension's `notes-provenance.js` unchanged in behaviour. It was
4
- // always platform-free: a run-list `[{ len, author, at }]` that sums to the body length and
5
- // shifts naturally as text is inserted or deleted, with no absolute offsets to fix up. The
6
- // desktop needed exactly the same answers, and a second implementation of "who wrote this"
7
- // would have disagreed with the first on precisely the edits that matter.
8
- //
9
- // WHY RUNS AND NOT OFFSETS. An offset-based ledger has to be repaired after every edit, and
10
- // the repair is where the bugs live. A run-list is repaired BY the edit: replacing [s,e)
11
- // with n characters is a splice, and everything after it moves without being touched.
12
- //
13
- // The versioning half is new here and belongs beside it, because a version snapshot carries
14
- // its ledger — restoring a body without its attribution would silently reattribute an
15
- // agent's paragraphs to the person who pressed Restore.
16
-
17
- export const HUMAN = 'You';
18
-
19
- export function blankAttribution(len, author = HUMAN, at = 0) {
20
- return len > 0 ? [{ len, author, at }] : [];
21
- }
22
-
23
- // The minimal replaced range: [start,end) of `prev` became `insLen` new chars in `next`.
24
- export function diffRange(prev, next) {
25
- const max = Math.min(prev.length, next.length);
26
- let s = 0; while (s < max && prev[s] === next[s]) s++;
27
- let e = 0; while (e < max - s && prev[prev.length - 1 - e] === next[next.length - 1 - e]) e++;
28
- return { start: s, end: prev.length - e, insLen: next.length - s - e };
29
- }
30
-
31
- export function mergeRuns(runs) {
32
- const out = [];
33
- for (const r of runs) {
34
- if (!r.len) continue;
35
- const last = out[out.length - 1];
36
- if (last && last.author === r.author && last.at === r.at) last.len += r.len;
37
- else out.push({ len: r.len, author: r.author, at: r.at });
38
- }
39
- return out;
40
- }
41
-
42
- export function spliceAttribution(runs, start, end, insLen, author, at) {
43
- const before = [], after = [];
44
- let pos = 0;
45
- for (const r of runs) {
46
- const rStart = pos, rEnd = pos + r.len;
47
- if (rEnd <= start) before.push(r);
48
- else if (rStart >= end) after.push(r);
49
- else {
50
- if (rStart < start) before.push({ len: start - rStart, author: r.author, at: r.at });
51
- if (rEnd > end) after.push({ len: rEnd - end, author: r.author, at: r.at });
52
- }
53
- pos = rEnd;
54
- }
55
- return mergeRuns([...before, ...(insLen ? [{ len: insLen, author, at }] : []), ...after]);
56
- }
57
-
58
- // Attribute the diff prev→next to `author`. Returns the updated run-list (unchanged
59
- // reference-wise only when there was no change).
60
- export function applyAttribution(runs, prev, next, author, at) {
61
- const cur = Array.isArray(runs) && runs.length ? runs : blankAttribution(prev.length);
62
- const { start, end, insLen } = diffRange(prev, next);
63
- if (start === end && !insLen) return cur; // no change
64
- return spliceAttribution(cur, start, end, insLen, author, at);
65
- }
66
-
67
- export function attributionSummary(runs) {
68
- const by = new Map();
69
- let total = 0;
70
- for (const r of runs || []) { by.set(r.author, (by.get(r.author) || 0) + r.len); total += r.len; }
71
- return {
72
- by: [...by.entries()].map(([author, chars]) => ({ author, chars })).sort((a, b) => b.chars - a.chars),
73
- total,
74
- };
75
- }
76
-
77
- // Adopt a stored ledger only if it still matches the body length (a note edited by an
78
- // older build, or imported, won't have one) — otherwise seed the whole body as You.
79
- export function normalizeAttribution(runs, bodyLen, at) {
80
- if (Array.isArray(runs) && runs.length && runs.reduce((n, r) => n + (r.len || 0), 0) === bodyLen) return mergeRuns(runs);
81
- return blankAttribution(bodyLen, HUMAN, at);
82
- }
83
-
84
- // ---------------------------------------------------------------------------
85
- // Versions — the snapshots you can go back to
86
- // ---------------------------------------------------------------------------
87
-
88
- /** Forty is what the extension keeps. Enough to undo an afternoon, bounded enough to store. */
89
- export const MAX_VERSIONS = 40;
90
-
91
- /**
92
- * Append a snapshot of `body` and its ledger, and return the new list.
93
- *
94
- * Two rules, both about not filling the list with noise:
95
- * · a body identical to the newest snapshot is not a new version;
96
- * · a ledger that does not sum to this body's length is not this body's ledger, so it is
97
- * seeded blank rather than carried across — a mismatched ledger would make a later
98
- * restore attribute the wrong spans to the wrong authors.
99
- */
100
- export function pushVersion(versions, { body, attribution = null, by = HUMAN, label = '', at = Date.now() } = {}) {
101
- const list = Array.isArray(versions) ? versions : [];
102
- const text = String(body ?? '');
103
- const last = list[list.length - 1];
104
- if (last && last.body === text) return list;
105
- const ledger = (Array.isArray(attribution) && attribution.reduce((n, r) => n + (r.len || 0), 0) === text.length)
106
- ? attribution
107
- : blankAttribution(text.length, by, at);
108
- return [...list, { body: text, attribution: ledger, at, by, label: label || by }].slice(-MAX_VERSIONS);
109
- }
110
-
111
- /**
112
- * Restore version `index`, keeping the current draft as a snapshot first so the restore is
113
- * itself undoable.
114
- *
115
- * The guard against re-snapshotting matters more than it looks: flipping between two
116
- * versions (A→B→A→B) would otherwise push an identical "Before restore" row every time and
117
- * push the version you were trying to reach off the end of a bounded list.
118
- */
119
- export function restoreVersion(versions, index, { currentBody, currentAttribution = null, at = Date.now(), label = 'Before restore' } = {}) {
120
- const list = Array.isArray(versions) ? versions : [];
121
- const target = list[index];
122
- if (!target) return null;
123
- const draft = String(currentBody ?? '');
124
- const next = list.some((v) => v.body === draft)
125
- ? list
126
- : pushVersion(list, { body: draft, attribution: currentAttribution, by: HUMAN, label, at });
127
- return {
128
- body: target.body,
129
- attribution: normalizeAttribution(target.attribution, target.body.length, target.at || at),
130
- versions: next,
131
- };
132
- }
1
+ const x="You";function f(r,n="You",e=0){return r>0?[{len:r,author:n,at:e}]:[]}function A(r,n){const e=Math.min(r.length,n.length);let t=0;for(;t<e&&r[t]===n[t];)t++;let o=0;for(;o<e-t&&r[r.length-1-o]===n[n.length-1-o];)o++;return{start:t,end:r.length-o,insLen:n.length-t-o}}function g(r){const n=[];for(const e of r){if(!e.len)continue;const t=n[n.length-1];t&&t.author===e.author&&t.at===e.at?t.len+=e.len:n.push({len:e.len,author:e.author,at:e.at})}return n}function y(r,n,e,t,o,s){const i=[],l=[];let a=0;for(const u of r){const c=a,h=a+u.len;h<=n?i.push(u):c>=e?l.push(u):(c<n&&i.push({len:n-c,author:u.author,at:u.at}),h>e&&l.push({len:h-e,author:u.author,at:u.at})),a=h}return g([...i,...t?[{len:t,author:o,at:s}]:[],...l])}function m(r,n,e,t,o){const s=Array.isArray(r)&&r.length?r:f(n.length),{start:i,end:l,insLen:a}=A(n,e);return i===l&&!a?s:y(s,i,l,a,t,o)}function M(r){const n=new Map;let e=0;for(const t of r||[])n.set(t.author,(n.get(t.author)||0)+t.len),e+=t.len;return{by:[...n.entries()].map(([t,o])=>({author:t,chars:o})).sort((t,o)=>o.chars-t.chars),total:e}}function p(r,n,e){return Array.isArray(r)&&r.length&&r.reduce((t,o)=>t+(o.len||0),0)===n?g(r):f(n,"You",e)}const b=40;function d(r,{body:n,attribution:e=null,by:t="You",label:o="",at:s=Date.now()}={}){const i=Array.isArray(r)?r:[],l=String(n??""),a=i[i.length-1];if(a&&a.body===l)return i;const u=Array.isArray(e)&&e.reduce((c,h)=>c+(h.len||0),0)===l.length?e:f(l.length,t,s);return[...i,{body:l,attribution:u,at:s,by:t,label:o||t}].slice(-b)}function N(r,n,{currentBody:e,currentAttribution:t=null,at:o=Date.now(),label:s="Before restore"}={}){const i=Array.isArray(r)?r:[],l=i[n];if(!l)return null;const a=String(e??""),u=i.some(c=>c.body===a)?i:d(i,{body:a,attribution:t,by:"You",label:s,at:o});return{body:l.body,attribution:p(l.attribution,l.body.length,l.at||o),versions:u}}export{x as HUMAN,b as MAX_VERSIONS,m as applyAttribution,M as attributionSummary,f as blankAttribution,A as diffRange,g as mergeRuns,p as normalizeAttribution,d as pushVersion,N as restoreVersion,y as spliceAttribution};