@chatpanel/events 0.102.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.
- package/adapters.js +1 -83
- package/adaptive-tool-policy.js +1 -45
- package/agent.js +2 -255
- package/attribution.js +1 -132
- package/backup-envelope.js +1 -221
- package/board-tool.js +2 -165
- package/budget.js +1 -116
- package/capability.js +1 -126
- package/citations.js +5 -127
- package/client-prefs.js +1 -148
- package/context-attachments.js +8 -144
- package/cowriter-router.js +1 -83
- package/cowriter-writer.js +19 -193
- package/cowriter.js +1 -190
- package/curate.js +3 -509
- package/distance.js +1 -124
- package/engine.js +1 -131
- package/entitlement.js +1 -332
- package/entity.js +1 -354
- package/event.js +1 -175
- package/extraction.js +4 -293
- package/failover.js +1 -93
- package/find-tool.js +1 -66
- package/flowchart.js +2 -644
- package/gate.js +1 -74
- package/harness.js +4 -101
- package/index.js +1 -345
- package/invariants.js +1 -174
- package/job.js +3 -147
- package/kernel.js +1 -255
- package/knowledge-derive.js +1 -267
- package/knowledge.js +3 -230
- package/library.js +11 -280
- package/loop.js +1 -132
- package/manifest.js +1 -107
- package/markdown-authoring.js +11 -288
- package/markdown-render.js +0 -0
- package/mcp-client.js +8 -358
- package/mcp-dispatch.js +1 -52
- package/mcp-errors.js +1 -106
- package/mcp-manager.js +1 -127
- package/media-transcript.js +8 -531
- package/meeting-analyzers.js +1 -83
- package/meeting-insights.js +7 -164
- package/meeting-shape.js +1 -282
- package/meeting-text.js +5 -94
- package/memory.js +2 -638
- package/model-candidates.js +1 -358
- package/model-health.js +1 -182
- package/model-ledger.js +1 -228
- package/model-picker.js +1 -188
- package/note-actions.js +15 -246
- package/note-graph.js +1 -158
- package/note-links.js +1 -68
- package/note-mentions.js +8 -114
- package/note-plan.js +9 -107
- package/note-research.js +1 -135
- package/observability.js +1 -119
- package/omni.js +1 -125
- package/order.js +1 -78
- package/package.json +1 -4
- package/pdf-layout.js +7 -253
- package/project-run.js +20 -444
- package/project.js +1 -170
- package/promotion.js +1 -171
- package/queue.js +1 -98
- package/reach.js +1 -23
- package/recipe-tool.js +2 -161
- package/recipe.js +1 -264
- package/record-list.js +2 -110
- package/recruit.js +3 -456
- package/redaction-tokens.js +1 -61
- package/ref.js +1 -55
- package/registry.js +1 -240
- package/route-graph.js +1 -115
- package/route-strategies.js +2 -232
- package/router.js +2 -880
- package/rrf.js +1 -89
- package/rules.js +1 -142
- package/schedule.js +1 -663
- package/scm-connection.js +1 -180
- package/scopes.js +1 -12
- package/scorecard.js +1 -335
- package/search-engines.js +1 -81
- package/skill-manifest.js +1 -330
- package/skill-scan.js +3 -209
- package/skill-sources.js +1 -162
- package/skill-vars.js +2 -252
- package/slash-commands.js +1 -158
- package/source-gate.js +1 -92
- package/sources-retrieval.js +5 -186
- package/sources.js +1 -273
- package/store.js +1 -171
- package/structured.js +4 -901
- package/subject-kinds.js +1 -5
- package/subject-name.js +1 -96
- package/sync-plan.js +1 -213
- package/synthesis.js +3 -123
- package/tags.js +1 -203
- package/team-board.js +6 -337
- package/team-cache.js +2 -63
- package/team-observe.js +1 -118
- package/team-org.js +1 -442
- package/team-plan.js +4 -131
- package/team-record.js +1 -225
- package/team-run.js +9 -652
- package/team-subtask.js +2 -191
- package/team-task.js +2 -124
- package/team-tool.js +2 -196
- package/team-trail.js +2 -73
- package/team-worklog.js +2 -149
- package/team.js +1 -302
- package/text-search.js +1 -155
- package/theme.js +17 -154
- package/titles.js +4 -285
- package/tool-discovery.js +1 -87
- package/tool-dispatch.js +5 -230
- package/tool-groups.js +1 -81
- package/tool-hints.js +10 -185
- package/tool-loop-guard.js +4 -182
- package/tool-need.js +2 -124
- package/tool-result.js +2 -438
- package/tool-round.js +0 -0
- package/tool-schema.js +1 -155
- package/tool-traits.js +1 -182
- package/toolset.js +2 -88
- package/trajectory.js +1 -530
- package/turn-loop.js +4 -440
- package/upcast.js +1 -37
- package/vault.js +2 -250
- package/view.js +1 -88
- package/voice-intents.js +2 -1488
- package/voice-speaker.js +1 -98
- package/weather-tool.js +1 -65
- package/weather.js +2 -211
- package/web-search-tool.js +14 -102
- package/web-search.js +1 -160
- package/widget.js +1 -153
package/cowriter-router.js
CHANGED
|
@@ -1,83 +1 @@
|
|
|
1
|
-
|
|
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};
|
package/cowriter-writer.js
CHANGED
|
@@ -1,199 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
8
|
+
`:"")+t}function w(r){const n=String(r??"");return n.endsWith(`
|
|
15
9
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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};
|