@chatpanel/events 0.71.0 → 0.72.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.
- package/cowriter-writer.js +199 -0
- package/index.js +1 -0
- package/package.json +3 -1
|
@@ -0,0 +1,199 @@
|
|
|
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.
|
|
13
|
+
|
|
14
|
+
// ── What the cursor line affords ─────────────────────────────────────────────────
|
|
15
|
+
|
|
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
|
+
}
|
|
25
|
+
|
|
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
|
+
}
|
|
48
|
+
|
|
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;
|
|
55
|
+
|
|
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
|
+
}
|
|
64
|
+
|
|
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
|
+
}
|
package/index.js
CHANGED
|
@@ -202,6 +202,7 @@ export {
|
|
|
202
202
|
export { SOURCE_TRUST, SkillSourceError, defineSkillSource, createSkillSourceRegistry } from './skill-sources.js';
|
|
203
203
|
export { SKILL_MANIFEST_VERSION, SKILL_CONTEXTS, SKILL_HISTORY_SCOPES, SKILL_MCP_MODES, SKILL_TRUST, SKILL_FILE_KINDS, SKILL_UPCASTERS, SkillManifestError, isSafeSkillPath, originOf, trustOf, skillFiles, needsBridge, declaredAccess, originLabel, sameSkillOrigin, skillIsStale, validateSkill, upcastSkill, upcastSkills, normalizeSkill } from './skill-manifest.js';
|
|
204
204
|
export { SKILL_VARS, SKILL_VAR_NAMES, skillVar, skillVarPattern, parseSkillVars, lintSkillPrompt, suggestSkillVar, substituteSkillVars, expandSkillPrompt, skillVarGuidance, SkillVarError } from './skill-vars.js';
|
|
205
|
+
export { lineAt, writerAffordance, INSTRUCTION_RE, instructionOnLine, GOAL_MIN_NEW, goalDraftAllowed, createSpendMeter, writerTail, draftSeparator, groundingBlock, writerRequest, AUTOCOMPLETE_SYSTEM, AUTOCOMPLETE_MAX_TOKENS, AUTOCOMPLETE_TEMPERATURE, clipCompletion, GEARS, WRITER_PREFS, WRITER_PREF_DEFAULTS, normalizeWriterPrefs, normalizeIntent } from './cowriter-writer.js';
|
|
205
206
|
export { SLASH_TYPING_RE, enabledSkills, slashCommandItems, matchSlashSkill, matchSlashRecipe, recipeInvocationText, slashCommandInsert, skillInvocationOf, skillInvocationLabel } from './slash-commands.js';
|
|
206
207
|
export { outlineOf, parseListItem, continueList, indentSelection, toggleWrap, toggleLinePrefix, toggleTask, toggleLink, docStats, selectionStats } from './markdown-authoring.js';
|
|
207
208
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/events",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.72.0",
|
|
4
4
|
"description": "The canonical ChatPanel event-log and capability contracts \u2014 typed durable facts, clock-free deterministic linearization, schema upcasting, and the invariants the replay harness asserts. Pure, dependency-free ESM shared by the ChatPanel extension, gateway and bridge.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"./citations.js": "./citations.js",
|
|
14
14
|
"./cowriter-router.js": "./cowriter-router.js",
|
|
15
15
|
"./cowriter.js": "./cowriter.js",
|
|
16
|
+
"./cowriter-writer.js": "./cowriter-writer.js",
|
|
16
17
|
"./curate.js": "./curate.js",
|
|
17
18
|
"./distance.js": "./distance.js",
|
|
18
19
|
"./entitlement.js": "./entitlement.js",
|
|
@@ -122,6 +123,7 @@
|
|
|
122
123
|
"context-attachments.js",
|
|
123
124
|
"cowriter-router.js",
|
|
124
125
|
"cowriter.js",
|
|
126
|
+
"cowriter-writer.js",
|
|
125
127
|
"curate.js",
|
|
126
128
|
"distance.js",
|
|
127
129
|
"entitlement.js",
|