@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
@@ -1,83 +1 @@
1
- // Who does which job: appointing a model to each role of the co-writer team.
2
- //
3
- // A co-writer is not one model. Proofreading runs constantly and must be cheap; drafting
4
- // ahead runs rarely and should be the best thing available; research sits between them. Using
5
- // one model for all three means either paying strong-model prices to catch "the the", or
6
- // asking a small model to write prose. So each ROLE states a preference and this appoints the
7
- // nearest candidate to it.
8
- //
9
- // Pure and dependency-free on purpose — no settings, no license, no DOM. The caller
10
- // normalizes whatever roster it holds (the extension's configured endpoints and agents, the
11
- // desktop's gateway model list) into candidates; this only decides. That is what makes the
12
- // same appointment logic available to a gateway or a bridge that wanted to offer it.
13
- //
14
- // candidate: { id, name, kind, model, tier?, subagents?, usable? }
15
- // role: { id, prefer: 'cheap' | 'balanced' | 'strong' }
16
-
17
- const TIER_RANK = { cheap: 0, balanced: 1, strong: 2 };
18
-
19
- // Infer a capability tier from a model id (best-effort, provider-agnostic).
20
- export function classifyModel(model = '') {
21
- const m = String(model).toLowerCase();
22
- if (/haiku|mini|flash|nano|lite|small|instant|\b[1-9]b\b|8b|7b|3b/.test(m)) return 'cheap';
23
- if (/opus|ultra|o1|o3|405b|70b|72b|large|gpt-4(?!o)|gpt-5/.test(m)) return 'strong';
24
- if (/sonnet|gpt-4o|mixtral|medium|32b|command-r/.test(m)) return 'balanced';
25
- return 'balanced'; // unknown → treat as mid so it's never wrongly picked as "cheapest"
26
- }
27
-
28
- // Native-subagent capable = bridge CLIs that orchestrate their own subagents.
29
- export function supportsSubagents(candidate) {
30
- return candidate?.kind === 'bridge' && /^(claude|codex)$/i.test(candidate.bridgeAgent || candidate.model || '');
31
- }
32
-
33
- function withTierAndMode(c) {
34
- const tier = c.tier || classifyModel(c.model);
35
- return { ...c, tier, mode: (c.subagents ?? supportsSubagents(c)) ? 'subagent' : 'api' };
36
- }
37
-
38
- // Appoint one role → the best available candidate (or null if none usable).
39
- export function appoint(role, candidates, { overrides = {}, exclude = null } = {}) {
40
- // `exclude` — ids (or model names) that failed this run: the next appointment is the
41
- // nearest tier among what is left, which is what a person would do by hand.
42
- const out = exclude ? new Set(exclude) : null;
43
- const usable = (candidates || []).filter((c) => c && c.usable !== false && c.model && !(out && (out.has(c.id) || out.has(c.model))));
44
- if (!usable.length) return null;
45
- const ovId = overrides[role.id];
46
- if (ovId) {
47
- const m = usable.find((c) => c.id === ovId);
48
- if (m) return withTierAndMode(m);
49
- }
50
- const want = TIER_RANK[role.prefer] ?? 1;
51
- // Ties go to ROSTER ORDER, not to the alphabet: the host lists what it trusts first (the
52
- // model the person is already chatting with, installed agents), and over a gateway that
53
- // lists eight hundred models the alphabet picks a provider nobody has used.
54
- const best = usable
55
- .map((c, i) => ({ c: withTierAndMode(c), d: Math.abs((TIER_RANK[classifyModel(c.model)] ?? 1) - want), i }))
56
- .sort((a, b) => a.d - b.d || a.i - b.i)[0];
57
- return best.c;
58
- }
59
-
60
- // Appoint a whole team → { [roleId]: appointment | null }.
61
- export function routeTeam(roles, candidates, opts = {}) {
62
- const out = {};
63
- for (const role of roles || []) out[role.id] = appoint(role, candidates, opts);
64
- return out;
65
- }
66
-
67
- /**
68
- * The team, and what each member is for.
69
- *
70
- * Here rather than in a client because a role is a CONTRACT — "the editor proofreads as you
71
- * type, cheaply, constantly" — and a second client inventing its own list is a client whose
72
- * Writer is not the same job as everyone else's. The icons and descriptions travel with it so
73
- * two products describe the team in one voice.
74
- */
75
- export const SWARM_ROLES = Object.freeze([
76
- Object.freeze({ id: 'editor', prefer: 'cheap', icon: '\u270d\ufe0f', name: 'Editor', desc: 'Proofreads as you type' }),
77
- Object.freeze({ id: 'researcher', prefer: 'balanced', icon: '\ud83d\udd0e', name: 'Researcher', desc: 'Finds related material' }),
78
- Object.freeze({ id: 'writer', prefer: 'strong', icon: '\u2728', name: 'Writer', desc: 'Drafts ahead and rewrites' }),
79
- Object.freeze({ id: 'factcheck', prefer: 'strong', icon: '\u26a0\ufe0f', name: 'Fact-checker', desc: 'Flags shaky claims' }),
80
- ]);
81
-
82
- /** A role by id, or null — so a stored override naming a role this build dropped is ignored. */
83
- export const roleById = (id) => SWARM_ROLES.find((r) => r.id === id) || null;
1
+ const c={cheap:0,balanced:1,strong:2};function u(e=""){const r=String(e).toLowerCase();return/haiku|mini|flash|nano|lite|small|instant|\b[1-9]b\b|8b|7b|3b/.test(r)?"cheap":/opus|ultra|o1|o3|405b|70b|72b|large|gpt-4(?!o)|gpt-5/.test(r)?"strong":(/sonnet|gpt-4o|mixtral|medium|32b|command-r/.test(r),"balanced")}function b(e){return e?.kind==="bridge"&&/^(claude|codex)$/i.test(e.bridgeAgent||e.model||"")}function f(e){const r=e.tier||u(e.model);return{...e,tier:r,mode:e.subagents??b(e)?"subagent":"api"}}function p(e,r,{overrides:a={},exclude:s=null}={}){const n=s?new Set(s):null,i=(r||[]).filter(t=>t&&t.usable!==!1&&t.model&&!(n&&(n.has(t.id)||n.has(t.model))));if(!i.length)return null;const d=a[e.id];if(d){const t=i.find(o=>o.id===d);if(t)return f(t)}const l=c[e.prefer]??1;return i.map((t,o)=>({c:f(t),d:Math.abs((c[u(t.model)]??1)-l),i:o})).sort((t,o)=>t.d-o.d||t.i-o.i)[0].c}function h(e,r,a={}){const s={};for(const n of e||[])s[n.id]=p(n,r,a);return s}const m=Object.freeze([Object.freeze({id:"editor",prefer:"cheap",icon:"✍️",name:"Editor",desc:"Proofreads as you type"}),Object.freeze({id:"researcher",prefer:"balanced",icon:"🔎",name:"Researcher",desc:"Finds related material"}),Object.freeze({id:"writer",prefer:"strong",icon:"✨",name:"Writer",desc:"Drafts ahead and rewrites"}),Object.freeze({id:"factcheck",prefer:"strong",icon:"⚠️",name:"Fact-checker",desc:"Flags shaky claims"})]),x=e=>m.find(r=>r.id===e)||null;export{m as SWARM_ROLES,p as appoint,u as classifyModel,x as roleById,h as routeTeam,b as supportsSubagents};
@@ -1,199 +1,25 @@
1
- // cowriter-writer.js — the WRITER's half of the co-writer: when it may draft, what it is
2
- // asked, and how much it may spend.
3
- //
4
- // The Editor's half (lint, diff, the copy-editor prompt) is cowriter.js. This is the member
5
- // that WRITES a continuation from the caret on ⌘↵, a section under a heading you paused on,
6
- // the next line toward a goal, or the result of an instruction typed into the note — and it
7
- // lived as a dozen functions inside the extension's notes.js until the desktop needed the
8
- // same member. Everything here is a decision or a string; the ghost text, the keys and the
9
- // stream are the client's.
10
- //
11
- // The standing rule of this member: IT OFFERS, THE USER ACCEPTS. Every draft is a suggestion
12
- // with an accept and a reject, and nothing here writes into a document.
1
+ function c(r,n){const e=String(r??""),t=Math.max(0,Math.min(n??0,e.length)),o=e.lastIndexOf(`
2
+ `,t-1)+1,s=e.indexOf(`
3
+ `,t),i=s<0?e.length:s;return{text:e.slice(o,i),start:o,end:i}}function m(r,n){const e=String(r??""),t=c(e,n),o=t.text;if(/^\s*([-*]|\d+\.)\s*(\[ \]\s*)?$/.test(o))return{kind:"item",at:t.end,label:"this item"};if(/\b(TODO|TK|TBD)\b:?\s*$/i.test(o))return{kind:"todo",at:t.end,label:"this to-do"};if(/^#{1,6}\s+\S/.test(o)){const s=e.slice(t.end).replace(/^\n/,"").split(`
4
+ `,1)[0]||"";if(!s.trim()||/^#{1,6}\s/.test(s))return{kind:"section",at:t.end,label:"this section"}}if(!o.trim()){const s=e.slice(0,t.start).replace(/\n$/,""),i=s.slice(s.lastIndexOf(`
5
+ `)+1);if(/^#{1,6}\s+\S/.test(i))return{kind:"section",at:t.start,label:"this section"}}return null}const u=/^\s*(?:please\s+|can you\s+|now\s+)?(summari[sz]e|recap|tl;?dr|rewrite|re-?write|reword|rephrase|expand|elaborate|continue|list|enumerate|outline|draft|write|compose|generate|create|add|explain|describe|define|compare|contrast|shorten|condense|tighten|simplify|translate|convert|turn\s+.+\s+into|make\s+(?:this|it|these|a\b)|bullet|brainstorm|suggest|proofread|polish|improve)\b/i;function g(r,n){const e=c(r,n),t=e.text.trim();return t.length<6||/^[@/]/.test(t)||/@\[[^\]]+\]/.test(t)||!u.test(t)?null:{text:t,start:e.start,end:e.end}}const p=24;function x({text:r,caret:n,lastLen:e=-1,minNew:t=p}={}){const o=String(r??""),s=Math.max(0,Math.min(n??0,o.length));return!(s!==o.length&&o[s]!==`
6
+ `||o.slice(0,s).trim().length<24||e>=0&&o.length<=e+t)}function b({capPerMin:r=20,now:n=Date.now}={}){let e=[];const t=()=>{const o=n();e=e.filter(s=>o-s<6e4)};return{cap:r,ok(){return t(),e.length<r},spend(){e.push(n())},used(){return t(),e.length}}}function l(r,n=""){const e=String(r??""),t=e.length>1600?`…${e.slice(-1600)}`:e,o=String(n||"").trim();return(o?`# ${o}
13
7
 
14
- // ── What the cursor line affords ─────────────────────────────────────────────────
8
+ `:"")+t}function w(r){const n=String(r??"");return n.endsWith(`
15
9
 
16
- /** The line the caret is on, as `{ text, start, end }` — `end` excludes the newline. */
17
- export function lineAt(text, caret) {
18
- const src = String(text ?? '');
19
- const pos = Math.max(0, Math.min(caret ?? 0, src.length));
20
- const start = src.lastIndexOf('\n', pos - 1) + 1;
21
- const nl = src.indexOf('\n', pos);
22
- const end = nl < 0 ? src.length : nl;
23
- return { text: src.slice(start, end), start, end };
24
- }
10
+ `)?"":n.endsWith(`
11
+ `)?`
12
+ `:`
25
13
 
26
- /**
27
- * What the cursor line affords the Writer: an empty outline item, a TODO marker, or a
28
- * heading whose section has no body yet → `{ kind, at, label }`, or null. Detecting the
29
- * spot is free; drafting it is a model call, and only happens on a nudge, ⌘↵ or Focus.
30
- */
31
- export function writerAffordance(text, caret) {
32
- const v = String(text ?? '');
33
- const line = lineAt(v, caret);
34
- const t = line.text;
35
- if (/^\s*([-*]|\d+\.)\s*(\[ \]\s*)?$/.test(t)) return { kind: 'item', at: line.end, label: 'this item' };
36
- if (/\b(TODO|TK|TBD)\b:?\s*$/i.test(t)) return { kind: 'todo', at: line.end, label: 'this to-do' };
37
- if (/^#{1,6}\s+\S/.test(t)) { // a heading whose section has no body yet
38
- const next = v.slice(line.end).replace(/^\n/, '').split('\n', 1)[0] || '';
39
- if (!next.trim() || /^#{1,6}\s/.test(next)) return { kind: 'section', at: line.end, label: 'this section' };
40
- }
41
- if (!t.trim()) { // a blank line directly under a heading
42
- const before = v.slice(0, line.start).replace(/\n$/, '');
43
- const prev = before.slice(before.lastIndexOf('\n') + 1);
44
- if (/^#{1,6}\s+\S/.test(prev)) return { kind: 'section', at: line.start, label: 'this section' };
45
- }
46
- return null;
47
- }
14
+ `}function f(r=[],{limit:n=5}={}){const e=(Array.isArray(r)?r:[]).filter(t=>t&&t.title).slice(0,n);return e.length?`
48
15
 
49
- /**
50
- * An imperative line the user wants ACTED on — "summarize the above in 3 sentences", "list
51
- * the key risks", "rewrite this as bullets" as distinct from prose to continue. A
52
- * conservative verb-led match, never an @mention or a /command line (those run elsewhere).
53
- */
54
- export const INSTRUCTION_RE = /^\s*(?:please\s+|can you\s+|now\s+)?(summari[sz]e|recap|tl;?dr|rewrite|re-?write|reword|rephrase|expand|elaborate|continue|list|enumerate|outline|draft|write|compose|generate|create|add|explain|describe|define|compare|contrast|shorten|condense|tighten|simplify|translate|convert|turn\s+.+\s+into|make\s+(?:this|it|these|a\b)|bullet|brainstorm|suggest|proofread|polish|improve)\b/i;
16
+ Related material you may draw on (only if genuinely useful — cite as [[title]] or [text](url)):
17
+ `+e.map(t=>`- ${t.title}${t.snippet?`${t.snippet}`:""}`).join(`
18
+ `):""}function O({before:r,title:n="",instruction:e="",contextBefore:t=null,intent:o="",cards:s=[]}={}){const i=f(s),a=String(o||"").trim();return e?{system:`You are executing an instruction inside the user's note. Use the note as context and do EXACTLY what the instruction says. Match the note's voice, tone, and markdown style. Output ONLY the resulting markdown to insert — no preamble, no restating the instruction, no meta commentary.${a?` The note's goal: ${a}.`:""}${i}`,user:`NOTE SO FAR:
19
+ ${l(t??r,n)}
55
20
 
56
- export function instructionOnLine(text, caret) {
57
- const line = lineAt(text, caret);
58
- const t = line.text.trim();
59
- if (t.length < 6) return null;
60
- if (/^[@/]/.test(t) || /@\[[^\]]+\]/.test(t)) return null;
61
- if (!INSTRUCTION_RE.test(t)) return null;
62
- return { text: t, start: line.start, end: line.end };
63
- }
21
+ INSTRUCTION (do exactly this, output only the result):
22
+ ${e}`,maxTokens:600,temperature:.4}:{system:`${a?`The note's goal (guide your writing toward it): ${a}.
64
23
 
65
- // ── Goal-drive ───────────────────────────────────────────────────────────────────
66
-
67
- /** Chars of the user's OWN writing required between two automatic drafts. */
68
- export const GOAL_MIN_NEW = 24;
69
-
70
- /**
71
- * May the goal draft the next line now?
72
- *
73
- * It must NOT loop: without this guard it re-fires on every pause (each Enter changes the
74
- * length) and re-drafts near-duplicates. So it fires at most once per burst of the user's
75
- * own writing — the document must have grown by `minNew` chars since the last draft, the
76
- * caret must sit at a line end, and there must be real context to continue. `lastLen` is
77
- * the body length at the last draft (-1 = armed: fire once there is context); the caller
78
- * re-baselines it when a draft is accepted, so an accepted line does not trigger the next.
79
- */
80
- export function goalDraftAllowed({ text, caret, lastLen = -1, minNew = GOAL_MIN_NEW } = {}) {
81
- const v = String(text ?? '');
82
- const from = Math.max(0, Math.min(caret ?? 0, v.length));
83
- if (from !== v.length && v[from] !== '\n') return false;
84
- if (v.slice(0, from).trim().length < 24) return false;
85
- if (lastLen >= 0 && v.length <= lastLen + minNew) return false;
86
- return true;
87
- }
88
-
89
- // ── Spend ────────────────────────────────────────────────────────────────────────
90
-
91
- /**
92
- * A rolling per-minute cap on MODEL calls the team makes on its own. Free work — the
93
- * deterministic Editor pass, retrieval-only research — never counts; over the cap the
94
- * spending members skip until the window clears. Visible, so "why did it stop" has an answer.
95
- */
96
- export function createSpendMeter({ capPerMin = 20, now = Date.now } = {}) {
97
- let calls = [];
98
- const prune = () => { const t = now(); calls = calls.filter((c) => t - c < 60_000); };
99
- return {
100
- cap: capPerMin,
101
- ok() { prune(); return calls.length < capPerMin; },
102
- spend() { calls.push(now()); },
103
- used() { prune(); return calls.length; },
104
- };
105
- }
106
-
107
- // ── Prompts ──────────────────────────────────────────────────────────────────────
108
-
109
- /** The tail of the note the Writer continues from — capped, with the title as context. */
110
- export function writerTail(before, title = '') {
111
- const b = String(before ?? '');
112
- const tail = b.length > 1600 ? `…${b.slice(-1600)}` : b;
113
- const t = String(title || '').trim();
114
- return (t ? `# ${t}\n\n` : '') + tail;
115
- }
116
-
117
- /** Where an instruction's result goes: on its own line under the instruction. */
118
- export function draftSeparator(before) {
119
- const b = String(before ?? '');
120
- return b.endsWith('\n\n') ? '' : b.endsWith('\n') ? '\n' : '\n\n';
121
- }
122
-
123
- /** The Researcher's shelf, as the lines the Writer may draw on — a handoff, not a dump. */
124
- export function groundingBlock(cards = [], { limit = 5 } = {}) {
125
- const list = (Array.isArray(cards) ? cards : []).filter((c) => c && c.title).slice(0, limit);
126
- if (!list.length) return '';
127
- return '\n\nRelated material you may draw on (only if genuinely useful — cite as [[title]] or [text](url)):\n'
128
- + list.map((c) => `- ${c.title}${c.snippet ? ` — ${c.snippet}` : ''}`).join('\n');
129
- }
130
-
131
- /**
132
- * The Writer's request: a continuation from where the note stops, or an instruction executed
133
- * over the note above it. -> { system, user, maxTokens, temperature }
134
- */
135
- export function writerRequest({ before, title = '', instruction = '', contextBefore = null, intent = '', cards = [] } = {}) {
136
- const grounding = groundingBlock(cards);
137
- const goal = String(intent || '').trim();
138
- if (instruction) {
139
- return {
140
- system: `You are executing an instruction inside the user's note. Use the note as context and do EXACTLY what the instruction says. Match the note's voice, tone, and markdown style. Output ONLY the resulting markdown to insert — no preamble, no restating the instruction, no meta commentary.${goal ? ` The note's goal: ${goal}.` : ''}${grounding}`,
141
- user: `NOTE SO FAR:\n${writerTail(contextBefore ?? before, title)}\n\nINSTRUCTION (do exactly this, output only the result):\n${instruction}`,
142
- maxTokens: 600,
143
- temperature: 0.4,
144
- };
145
- }
146
- return {
147
- system: `${goal ? `The note's goal (guide your writing toward it): ${goal}.\n\n` : ''}You continue the user's note from where it stops. Match their voice, tone, and markdown style exactly. Write only the NEXT one or two sentences (or finish the current one) — concise, natural, no preamble, no repetition of prior text, no meta commentary. Output ONLY the continuation.${grounding}`,
148
- user: writerTail(before, title),
149
- maxTokens: 220,
150
- temperature: 0.6,
151
- };
152
- }
153
-
154
- /** Inline autocomplete: a few words, at most one sentence, at the very end of the note. */
155
- export const AUTOCOMPLETE_SYSTEM = 'You are an inline writing autocomplete. Continue the note from EXACTLY where it stops with a SHORT continuation — a few words up to one sentence. Match the voice and markdown. Output ONLY the text to append: no quotes, no preamble, no repetition of prior text. If nothing sensible follows, output nothing.';
156
- export const AUTOCOMPLETE_MAX_TOKENS = 48;
157
- export const AUTOCOMPLETE_TEMPERATURE = 0.1;
158
-
159
- /** Keep a completion short: the first line, and at most the first sentence of it. */
160
- export function clipCompletion(s) {
161
- let t = String(s ?? '').replace(/^\s+/, '');
162
- const nl = t.indexOf('\n');
163
- if (nl >= 0) t = t.slice(0, nl);
164
- const m = t.match(/^.*?[.!?](\s|$)/);
165
- if (m) t = m[0];
166
- return t.replace(/\s+$/, '');
167
- }
168
-
169
- // ── The switches, in one voice ───────────────────────────────────────────────────
170
-
171
- /** The two gears — and what each DOES to your text, which is the question. */
172
- export const GEARS = Object.freeze([
173
- Object.freeze({ id: 'ambient', name: 'Suggest', note: 'Nothing is written for you. Typo fixes, links and research collect quietly in the Co-writer tab, and you decide what to take. Press ⌘↵ any time to draft ahead.' }),
174
- Object.freeze({ id: 'focus', name: 'Write with me', note: 'The team writes alongside you: it drafts a section when you pause on a heading, and every draft is still accept-or-reject. The per-minute spend cap still applies.' }),
175
- ]);
176
-
177
- /** The opt-ins. Each is a model call the team may make WITHOUT being asked, so each is off. */
178
- export const WRITER_PREFS = Object.freeze([
179
- Object.freeze({ k: 'revealFixes', label: 'Show fixes as they land', desc: 'Open the Co-writer tab when a typo fix or a draft is ready — no hunting for the badge.', spends: false }),
180
- Object.freeze({ k: 'actOnInstructions', label: 'Act on instruction lines', desc: 'A line like “summarize the above in 3 sentences” becomes a task the Writer drafts (accept/reject).', spends: true }),
181
- Object.freeze({ k: 'goalDrive', label: 'Let the goal keep writing', desc: 'With a goal set, draft the next line toward it whenever you pause at a line end. Always accept/reject; respects the spend cap.', spends: true }),
182
- Object.freeze({ k: 'autocomplete', label: 'Autocomplete as I type', desc: 'A short ghost continuation at the end of the note after a pause — one small call per pause, Tab keeps it. Counts against the spend cap.', spends: true }),
183
- ]);
184
-
185
- export const WRITER_PREF_DEFAULTS = Object.freeze({ gear: 'ambient', revealFixes: false, actOnInstructions: false, goalDrive: false, autocomplete: false });
186
-
187
- /** A stored prefs object, with unknown keys dropped and missing ones defaulted. */
188
- export function normalizeWriterPrefs(stored) {
189
- const s = stored && typeof stored === 'object' ? stored : {};
190
- const out = { ...WRITER_PREF_DEFAULTS };
191
- out.gear = s.gear === 'focus' ? 'focus' : 'ambient';
192
- for (const p of WRITER_PREFS) out[p.k] = s[p.k] === true;
193
- return out;
194
- }
195
-
196
- /** A note's goal, as stored: one line, capped. */
197
- export function normalizeIntent(v) {
198
- return String(v || '').replace(/\s+/g, ' ').trim().slice(0, 200);
199
- }
24
+ `:""}You continue the user's note from where it stops. Match their voice, tone, and markdown style exactly. Write only the NEXT one or two sentences (or finish the current one) — concise, natural, no preamble, no repetition of prior text, no meta commentary. Output ONLY the continuation.${i}`,user:l(r,n),maxTokens:220,temperature:.6}}const y="You are an inline writing autocomplete. Continue the note from EXACTLY where it stops with a SHORT continuation — a few words up to one sentence. Match the voice and markdown. Output ONLY the text to append: no quotes, no preamble, no repetition of prior text. If nothing sensible follows, output nothing.",T=48,k=.1;function S(r){let n=String(r??"").replace(/^\s+/,"");const e=n.indexOf(`
25
+ `);e>=0&&(n=n.slice(0,e));const t=n.match(/^.*?[.!?](\s|$)/);return t&&(n=t[0]),n.replace(/\s+$/,"")}const E=Object.freeze([Object.freeze({id:"ambient",name:"Suggest",note:"Nothing is written for you. Typo fixes, links and research collect quietly in the Co-writer tab, and you decide what to take. Press ⌘↵ any time to draft ahead."}),Object.freeze({id:"focus",name:"Write with me",note:"The team writes alongside you: it drafts a section when you pause on a heading, and every draft is still accept-or-reject. The per-minute spend cap still applies."})]),d=Object.freeze([Object.freeze({k:"revealFixes",label:"Show fixes as they land",desc:"Open the Co-writer tab when a typo fix or a draft is ready — no hunting for the badge.",spends:!1}),Object.freeze({k:"actOnInstructions",label:"Act on instruction lines",desc:"A line like “summarize the above in 3 sentences” becomes a task the Writer drafts (accept/reject).",spends:!0}),Object.freeze({k:"goalDrive",label:"Let the goal keep writing",desc:"With a goal set, draft the next line toward it whenever you pause at a line end. Always accept/reject; respects the spend cap.",spends:!0}),Object.freeze({k:"autocomplete",label:"Autocomplete as I type",desc:"A short ghost continuation at the end of the note after a pause — one small call per pause, Tab keeps it. Counts against the spend cap.",spends:!0})]),h=Object.freeze({gear:"ambient",revealFixes:!1,actOnInstructions:!1,goalDrive:!1,autocomplete:!1});function A(r){const n=r&&typeof r=="object"?r:{},e={...h};e.gear=n.gear==="focus"?"focus":"ambient";for(const t of d)e[t.k]=n[t.k]===!0;return e}function $(r){return String(r||"").replace(/\s+/g," ").trim().slice(0,200)}export{T as AUTOCOMPLETE_MAX_TOKENS,y as AUTOCOMPLETE_SYSTEM,k as AUTOCOMPLETE_TEMPERATURE,E as GEARS,p as GOAL_MIN_NEW,u as INSTRUCTION_RE,d as WRITER_PREFS,h as WRITER_PREF_DEFAULTS,S as clipCompletion,b as createSpendMeter,w as draftSeparator,x as goalDraftAllowed,f as groundingBlock,g as instructionOnLine,c as lineAt,$ as normalizeIntent,A as normalizeWriterPrefs,m as writerAffordance,O as writerRequest,l as writerTail};
package/cowriter.js CHANGED
@@ -1,190 +1 @@
1
- // The co-writer's two deterministic halves: what is mechanically wrong with a paragraph, and
2
- // what the SMALLEST change is that fixes a sentence.
3
- //
4
- // A co-writer that replaces a paragraph with its own version is a rewriter, and people do not
5
- // want their prose rewritten — they want the doubled word caught. Everything here exists to
6
- // keep a suggestion small enough to accept with one click and small enough to be obviously
7
- // right when you look at it.
8
- //
9
- // THE DETERMINISTIC PASS RUNS FIRST, AND OFTEN ENDS IT. `lintText` catches the mechanical
10
- // mistakes — doubled words, double spaces, a space before a comma, the lone lowercase "i" —
11
- // for free, so a token is spent only on text that is already mechanically clean. That order
12
- // is the difference between a co-writer that idles at zero cost and one that bills for
13
- // noticing "the the".
14
- //
15
- // Both halves were `cowriter-lint.js` and `cowriter-diff.js` in the extension, written pure
16
- // and dependency-free from the start and explicitly noted there as portable. They are here so
17
- // the second and third clients inherit them rather than copy them.
18
-
19
- // ── the deterministic pass ──────────────────────────────────────────────────────────────
20
-
21
- /**
22
- * Mechanical mistakes in `text`, as minimal edits.
23
- *
24
- * Returns `[{ start, end, before, after }]` — the same shape `wordDiff` produces, so a client
25
- * renders a lint fix and a model fix with one component — non-overlapping and left-to-right.
26
- */
27
- export function lintText(text = '') {
28
- const src = String(text);
29
- const raw = [];
30
- const add = (start, end, after) => {
31
- if (after !== src.slice(start, end)) raw.push({ start, end, before: src.slice(start, end), after });
32
- };
33
- let m;
34
-
35
- // 1) doubled word: "the the" → "the" (case-insensitive, same word).
36
- const dup = /\b(\w+)(\s+)\1\b/gi;
37
- while ((m = dup.exec(src))) add(m.index, m.index + m[0].length, m[1]);
38
-
39
- // 2) a run of 2+ spaces between visible chars → a single space.
40
- const runs = /(\S)( {2,})(\S)/g;
41
- while ((m = runs.exec(src))) { const s = m.index + 1; add(s, s + m[2].length, ' '); runs.lastIndex = s + 1; }
42
-
43
- // 3) whitespace before sentence punctuation: "word ," → "word,".
44
- const sp = /(\S)(\s+)([,.;:!?])/g;
45
- while ((m = sp.exec(src))) add(m.index + 1, m.index + m[0].length, m[3]);
46
-
47
- // 4) standalone lowercase "i" → "I" (skipping the "i.e." abbreviation).
48
- const iRe = /(^|[ \t(])i(?=[ \t.,;:!?)]|$)/g;
49
- while ((m = iRe.exec(src))) {
50
- const at = m.index + m[1].length;
51
- if (src[at + 1] === '.' && /[a-z]/i.test(src[at + 2] || '')) continue; // i.e., i.g.
52
- add(at, at + 1, 'I');
53
- }
54
-
55
- // Sort left-to-right and drop any edit overlapping one already kept: two fixes over the
56
- // same characters cannot both be applied, and applying one invalidates the other's offsets.
57
- raw.sort((a, b) => a.start - b.start || a.end - b.end);
58
- const out = [];
59
- let lastEnd = -1;
60
- for (const e of raw) {
61
- if (e.start < lastEnd) continue;
62
- out.push(e);
63
- lastEnd = e.end;
64
- }
65
- return out;
66
- }
67
-
68
- // ── the minimal diff ────────────────────────────────────────────────────────────────────
69
-
70
- /** Non-whitespace tokens with their char offsets in the source string. */
71
- function words(text) {
72
- const out = [];
73
- const re = /\S+/g;
74
- let m;
75
- while ((m = re.exec(text))) out.push({ w: m[0], start: m.index, end: m.index + m[0].length });
76
- return out;
77
- }
78
-
79
- /** Matched index pairs between two word arrays (longest common subsequence). */
80
- function lcsPairs(a, b) {
81
- const n = a.length;
82
- const m = b.length;
83
- const dp = Array.from({ length: n + 1 }, () => new Int32Array(m + 1));
84
- for (let i = n - 1; i >= 0; i--) {
85
- for (let j = m - 1; j >= 0; j--) {
86
- dp[i][j] = a[i].w === b[j].w ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
87
- }
88
- }
89
- const pairs = [];
90
- let i = 0;
91
- let j = 0;
92
- while (i < n && j < m) {
93
- if (a[i].w === b[j].w) { pairs.push([i, j]); i++; j++; }
94
- else if (dp[i + 1][j] >= dp[i][j + 1]) i++;
95
- else j++;
96
- }
97
- return pairs;
98
- }
99
-
100
- /**
101
- * Minimal edits turning `original` into `corrected`, with offsets into `original`.
102
- *
103
- * Word-run replacements, insertions (start === end, before '') and deletions (after ''). The
104
- * whitespace handling is the fiddly half and the reason this is one tested function rather
105
- * than an idea each client implements: an insertion carries a space so the new word is not
106
- * glued to its neighbour, and a deletion absorbs one adjacent space so it leaves neither a
107
- * double space nor a leading one.
108
- */
109
- export function wordDiff(original, corrected) {
110
- const A = words(original);
111
- const B = words(corrected);
112
- const matches = lcsPairs(A, B);
113
- const edits = [];
114
- const push = (aFrom, aTo, bFrom, bTo) => {
115
- if (aFrom === aTo && bFrom === bTo) return;
116
- const insWords = B.slice(bFrom, bTo).map((x) => x.w);
117
- let start;
118
- let end;
119
- let after;
120
- if (aFrom === aTo) {
121
- if (aFrom < A.length) { start = A[aFrom].start; end = start; after = `${insWords.join(' ')} `; }
122
- else { start = A.length ? A[A.length - 1].end : 0; end = start; after = `${A.length ? ' ' : ''}${insWords.join(' ')}`; }
123
- } else if (bFrom === bTo) {
124
- start = A[aFrom].start;
125
- if (aTo < A.length) end = A[aTo].start;
126
- else { end = A[aTo - 1].end; if (aFrom > 0) start = A[aFrom - 1].end; }
127
- after = '';
128
- } else {
129
- start = A[aFrom].start;
130
- end = A[aTo - 1].end;
131
- after = insWords.join(' ');
132
- }
133
- edits.push({ start, end, before: original.slice(start, end), after });
134
- };
135
- let ai = 0;
136
- let bi = 0;
137
- for (const [am, bm] of matches) {
138
- if (am > ai || bm > bi) push(ai, am, bi, bm);
139
- ai = am + 1;
140
- bi = bm + 1;
141
- }
142
- if (ai < A.length || bi < B.length) push(ai, A.length, bi, B.length);
143
- return edits;
144
- }
145
-
146
- /**
147
- * Keep only SMALL corrections — a typo, a comma, a missing "the".
148
- *
149
- * Without this the co-writer restructures prose: ask a model to "fix the mistakes" in a
150
- * paragraph and it will happily return a better paragraph, which arrives here as one enormous
151
- * edit and is offered as a one-click "fix". A suggestion nobody can check at a glance is not
152
- * a suggestion, so anything large is dropped rather than shown.
153
- */
154
- export function filterTypoEdits(edits, { maxWords = 5, maxLen = 48 } = {}) {
155
- return (edits || []).filter((e) => {
156
- if (e.before === e.after) return false;
157
- if (e.before.length > maxLen || e.after.length > maxLen) return false;
158
- const bw = e.before ? e.before.split(/\s+/).length : 0;
159
- const aw = e.after ? e.after.split(/\s+/).length : 0;
160
- if (Math.max(bw, aw) > maxWords) return false;
161
- // A pure insertion is only "small" if it is a word or two (a missing "the", a comma).
162
- if (!e.before && aw > 2) return false;
163
- return true;
164
- });
165
- }
166
-
167
- /** Stable identity for a suggestion, so a fix the user dismissed is not offered again. */
168
- export function editKey(edit) {
169
- return `${edit.before}␟${edit.after}`;
170
- }
171
-
172
- /** Apply non-overlapping edits right-to-left, so earlier offsets stay valid as it goes. */
173
- export function applyEdits(text, edits) {
174
- let out = String(text);
175
- for (const e of [...(edits || [])].sort((a, b) => b.start - a.start)) {
176
- out = out.slice(0, e.start) + e.after + out.slice(e.end);
177
- }
178
- return out;
179
- }
180
-
181
- /**
182
- * The instruction a co-writing model is given, and the guardrail inside it.
183
- *
184
- * It is told to return the corrected text and NOTHING else, because the answer is diffed
185
- * against the original rather than read: a preamble becomes a spurious insertion at offset 0,
186
- * offered to the user as a "fix" that pastes "Sure, here is the corrected text:" into their
187
- * note.
188
- */
189
- export const COWRITER_SYSTEM = 'You are a meticulous copy editor. Fix ONLY spelling, grammar, punctuation and obvious word mistakes in the text. Do NOT rewrite, rephrase, restructure, shorten or improve the style, and do not add or remove content. Preserve the markdown exactly. Output ONLY the corrected text.';
190
- export const COWRITER_TEMPERATURE = 0;
1
+ function S(o=""){const i=String(o),t=[],e=(r,d,p)=>{p!==i.slice(r,d)&&t.push({start:r,end:d,before:i.slice(r,d),after:p})};let n;const h=/\b(\w+)(\s+)\1\b/gi;for(;n=h.exec(i);)e(n.index,n.index+n[0].length,n[1]);const f=/(\S)( {2,})(\S)/g;for(;n=f.exec(i);){const r=n.index+1;e(r,r+n[2].length," "),f.lastIndex=r+1}const l=/(\S)(\s+)([,.;:!?])/g;for(;n=l.exec(i);)e(n.index+1,n.index+n[0].length,n[3]);const c=/(^|[ \t(])i(?=[ \t.,;:!?)]|$)/g;for(;n=c.exec(i);){const r=n.index+n[1].length;i[r+1]==="."&&/[a-z]/i.test(i[r+2]||"")||e(r,r+1,"I")}t.sort((r,d)=>r.start-d.start||r.end-d.end);const s=[];let u=-1;for(const r of t)r.start<u||(s.push(r),u=r.end);return s}function w(o){const i=[],t=/\S+/g;let e;for(;e=t.exec(o);)i.push({w:e[0],start:e.index,end:e.index+e[0].length});return i}function y(o,i){const t=o.length,e=i.length,n=Array.from({length:t+1},()=>new Int32Array(e+1));for(let c=t-1;c>=0;c--)for(let s=e-1;s>=0;s--)n[c][s]=o[c].w===i[s].w?n[c+1][s+1]+1:Math.max(n[c+1][s],n[c][s+1]);const h=[];let f=0,l=0;for(;f<t&&l<e;)o[f].w===i[l].w?(h.push([f,l]),f++,l++):n[f+1][l]>=n[f][l+1]?f++:l++;return h}function b(o,i){const t=w(o),e=w(i),n=y(t,e),h=[],f=(s,u,r,d)=>{if(s===u&&r===d)return;const p=e.slice(r,d).map(E=>E.w);let a,g,x;s===u?s<t.length?(a=t[s].start,g=a,x=`${p.join(" ")} `):(a=t.length?t[t.length-1].end:0,g=a,x=`${t.length?" ":""}${p.join(" ")}`):r===d?(a=t[s].start,u<t.length?g=t[u].start:(g=t[u-1].end,s>0&&(a=t[s-1].end)),x=""):(a=t[s].start,g=t[u-1].end,x=p.join(" ")),h.push({start:a,end:g,before:o.slice(a,g),after:x})};let l=0,c=0;for(const[s,u]of n)(s>l||u>c)&&f(l,s,c,u),l=s+1,c=u+1;return(l<t.length||c<e.length)&&f(l,t.length,c,e.length),h}function m(o,{maxWords:i=5,maxLen:t=48}={}){return(o||[]).filter(e=>{if(e.before===e.after||e.before.length>t||e.after.length>t)return!1;const n=e.before?e.before.split(/\s+/).length:0,h=e.after?e.after.split(/\s+/).length:0;return!(Math.max(n,h)>i||!e.before&&h>2)})}function R(o){return`${o.before}␟${o.after}`}function O(o,i){let t=String(o);for(const e of[...i||[]].sort((n,h)=>h.start-n.start))t=t.slice(0,e.start)+e.after+t.slice(e.end);return t}const $="You are a meticulous copy editor. Fix ONLY spelling, grammar, punctuation and obvious word mistakes in the text. Do NOT rewrite, rephrase, restructure, shorten or improve the style, and do not add or remove content. Preserve the markdown exactly. Output ONLY the corrected text.",j=0;export{$ as COWRITER_SYSTEM,j as COWRITER_TEMPERATURE,O as applyEdits,R as editKey,m as filterTypoEdits,S as lintText,b as wordDiff};