@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/library.js
CHANGED
|
@@ -1,284 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
// and the three answers already disagree:
|
|
5
|
-
//
|
|
6
|
-
// • the extension builds rich source records (conversationSource / meetingSource /
|
|
7
|
-
// noteSource / briefSource), each with its own title, date and body rules;
|
|
8
|
-
// • the gateway re-derives them in `backup-ingest.js`, whose own comment calls it "a
|
|
9
|
-
// SIMPLIFIED MIRROR of the extension's" — a mirror is a copy that drifts;
|
|
10
|
-
// • a desktop or mobile client would make a third, and three implementations of "what is
|
|
11
|
-
// the title of a meeting" become three different titles for the same meeting.
|
|
12
|
-
//
|
|
13
|
-
// The projection is pure: records in, a normalized record out. No storage, no crypto, no
|
|
14
|
-
// platform. Each host keeps its own persistence (IndexedDB, SQLite, Core Data) and calls
|
|
15
|
-
// this to decide WHAT it is storing — the P8 split the store already uses.
|
|
16
|
-
//
|
|
17
|
-
// THE ID GRAMMAR IS THE CONTRACT. `chat:<id>`, `note:<id>`, `meeting:<id>`, `brief:<id>`.
|
|
18
|
-
// The gateway's warm index, the MCP tools, `find_related`, the omni bar and every citation
|
|
19
|
-
// already speak it; writing it down here is what stops a fourth client inventing
|
|
20
|
-
// `chats/<id>` and silently failing to join.
|
|
21
|
-
//
|
|
22
|
-
// NAMED `normalizeStoredRecord`, NOT `normalizeStoredRecord`, on purpose: `curate.js` already
|
|
23
|
-
// exports the latter and it does the OTHER job — coercing anything into the flat survey
|
|
24
|
-
// shape a curation pass reads. Two functions named the same in one package, one lossy and
|
|
25
|
-
// one lossless, is a trap for whoever imports the wrong one.
|
|
26
|
-
//
|
|
27
|
-
// LOSSY AND LOSSLESS ARE DIFFERENT QUESTIONS, and conflating them is what made the gateway's
|
|
28
|
-
// mirror drift. `toSearchRecord` is deliberately lossy — it is the flat {id,title,type,date,
|
|
29
|
-
// text} the BM25 index wants. `normalizeStoredRecord` is lossless — it is the full record a client
|
|
30
|
-
// stores and round-trips. A client that needs to SHOW a chat must never reconstruct it from
|
|
31
|
-
// a search record.
|
|
1
|
+
class s extends Error{constructor(e){super(e),this.name="LibraryError"}}const m=Object.freeze(["chat","note","meeting","brief"]),l=new Set(m);function u(t){const e=String(t||""),n=e.indexOf(":");if(n<=0)return null;const r=e.slice(0,n),o=e.slice(n+1);return!l.has(r)||!o?null:{kind:r,localId:o}}function S(t,e){if(!l.has(t))throw new s(`unknown record kind: ${t}`);const n=String(e||"");if(!n)throw new s("a record id needs a local id");if(n.includes(":"))throw new s(`local id may not contain ":" — got ${n}`);return`${t}:${n}`}function T(t){return u(t)!==null}const f=48;function p(t,e="Untitled"){const n=String(t||"").split(`
|
|
2
|
+
`).map(r=>r.replace(/^\s*#{1,6}\s*/,"").replace(/[*_`>~]+/g,"").trim()).find(r=>r.length>0);return n?n.length>f?`${n.slice(0,f-1).trimEnd()}…`:n:e}const d=(t,e=0)=>Number.isFinite(Number(t))?Number(t):e,i=t=>typeof t=="string"?t:t==null?"":String(t);function c(t={},{now:e=0}={}){const n=u(t.id);if(!n)throw new s(`not a record id: ${JSON.stringify(t.id)}`);const r=d(t.createdAt,d(t.date,e));return{id:t.id,kind:n.kind,localId:n.localId,title:i(t.title).trim()||x(n.kind,t),tags:y(t.tags),createdAt:r,updatedAt:d(t.updatedAt,r),deletedAt:d(t.deletedAt,0)||0,body:t.body??null,meta:t.meta&&typeof t.meta=="object"?t.meta:{}}}function x(t,e){return t==="note"?p(e?.body?.markdown??e?.body,"Untitled note"):t==="meeting"?"Untitled meeting":t==="brief"?"Untitled brief":"New chat"}function y(t){if(!Array.isArray(t))return[];const e=[];for(const n of t){const r=i(n).trim();r&&!e.includes(r)&&e.push(r)}return e}function I(t){try{return c(t),!0}catch{return!1}}const A={assistant:"Assistant",system:"System",user:"You"};function L(t){const e=c(t);return e.deletedAt?null:{id:e.id,type:e.kind,title:e.title,date:e.updatedAt||e.createdAt,text:h(e)}}function h(t){const e=t.kind?t:c(t),n=`${b(e.kind)}: ${e.title}`,r=e.tags.length?`
|
|
3
|
+
Tags: ${e.tags.join(", ")}`:"";return`${n}${r}
|
|
32
4
|
|
|
33
|
-
|
|
34
|
-
constructor(message) { super(message); this.name = 'LibraryError'; }
|
|
35
|
-
}
|
|
5
|
+
${g(e)}`.trim()}function b(t){return t==="chat"?"CHAT":t==="note"?"NOTE":t==="meeting"?"MEETING":"BRIEF"}function g(t){const e=t.body;if(e==null)return"";if(typeof e=="string")return e;const n=typeof e.text=="string"?e.text:"";if(t.kind==="chat")return(e.messages||[]).filter(o=>o&&o.content).map(o=>`${A[o.role]||"You"}: ${E(o.content)}`).join(`
|
|
36
6
|
|
|
37
|
-
|
|
38
|
-
|
|
7
|
+
`)||n;if(t.kind==="note")return i(e.markdown??e.text);if(t.kind==="meeting"){const r=i(e.notes),o=(e.segments||[]).map(a=>`${i(a.speaker)||"?"}: ${i(a.text)}`).join(`
|
|
8
|
+
`);return[r,o].filter(Boolean).join(`
|
|
39
9
|
|
|
40
|
-
const
|
|
10
|
+
`)||n}if(t.kind==="brief"){const r=(e.claims||[]).map(o=>i(o.text)).filter(Boolean).join(`
|
|
11
|
+
`);return[i(e.summary),r].filter(Boolean).join(`
|
|
41
12
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const colon = s.indexOf(':');
|
|
46
|
-
if (colon <= 0) return null;
|
|
47
|
-
const kind = s.slice(0, colon);
|
|
48
|
-
const localId = s.slice(colon + 1);
|
|
49
|
-
if (!KIND_SET.has(kind) || !localId) return null;
|
|
50
|
-
return { kind, localId };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** The inverse. Throws rather than producing an id nothing else will match. */
|
|
54
|
-
export function makeRecordId(kind, localId) {
|
|
55
|
-
if (!KIND_SET.has(kind)) throw new LibraryError(`unknown record kind: ${kind}`);
|
|
56
|
-
const id = String(localId || '');
|
|
57
|
-
if (!id) throw new LibraryError('a record id needs a local id');
|
|
58
|
-
if (id.includes(':')) throw new LibraryError(`local id may not contain ":" — got ${id}`);
|
|
59
|
-
return `${kind}:${id}`;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function isRecordId(value) {
|
|
63
|
-
return parseRecordId(value) !== null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// --------------------------------------------------------------------------
|
|
67
|
-
// Titles
|
|
68
|
-
// --------------------------------------------------------------------------
|
|
69
|
-
|
|
70
|
-
/** The single length a record title may be — the extension's MAX_TITLE_LEN, shared. */
|
|
71
|
-
export const MAX_TITLE_LEN = 48;
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* First non-empty line, stripped of markdown furniture, clamped.
|
|
75
|
-
*
|
|
76
|
-
* Notes derive their title from the body when the user has not set one, and the meeting
|
|
77
|
-
* autotitler falls back here too. Keeping it in one place is what stops "# Rollback runbook"
|
|
78
|
-
* becoming the title in one client and "Rollback runbook" in another.
|
|
79
|
-
*/
|
|
80
|
-
export function deriveTitle(body, fallback = 'Untitled') {
|
|
81
|
-
const first = String(body || '')
|
|
82
|
-
.split('\n')
|
|
83
|
-
.map((l) => l.replace(/^\s*#{1,6}\s*/, '').replace(/[*_`>~]+/g, '').trim())
|
|
84
|
-
.find((l) => l.length > 0);
|
|
85
|
-
if (!first) return fallback;
|
|
86
|
-
return first.length > MAX_TITLE_LEN ? `${first.slice(0, MAX_TITLE_LEN - 1).trimEnd()}…` : first;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// --------------------------------------------------------------------------
|
|
90
|
-
// Normalization — the lossless shape
|
|
91
|
-
// --------------------------------------------------------------------------
|
|
92
|
-
|
|
93
|
-
const num = (v, dflt = 0) => (Number.isFinite(Number(v)) ? Number(v) : dflt);
|
|
94
|
-
const str = (v) => (typeof v === 'string' ? v : v == null ? '' : String(v));
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* A stored record, whatever client wrote it.
|
|
98
|
-
*
|
|
99
|
-
* `body` is the kind-specific payload and is carried VERBATIM — messages for a chat,
|
|
100
|
-
* segments for a meeting, markdown for a note. This module refuses to flatten it, because
|
|
101
|
-
* flattening is exactly the lossy step that belongs in `toSearchRecord` and nowhere else.
|
|
102
|
-
*/
|
|
103
|
-
export function normalizeStoredRecord(input = {}, { now = 0 } = {}) {
|
|
104
|
-
const parsed = parseRecordId(input.id);
|
|
105
|
-
if (!parsed) throw new LibraryError(`not a record id: ${JSON.stringify(input.id)}`);
|
|
106
|
-
const createdAt = num(input.createdAt, num(input.date, now));
|
|
107
|
-
return {
|
|
108
|
-
id: input.id,
|
|
109
|
-
kind: parsed.kind,
|
|
110
|
-
localId: parsed.localId,
|
|
111
|
-
title: str(input.title).trim() || defaultTitleFor(parsed.kind, input),
|
|
112
|
-
tags: normalizeTagList(input.tags),
|
|
113
|
-
createdAt,
|
|
114
|
-
// A record with no updatedAt sorts by when it was made, not to the bottom of the list.
|
|
115
|
-
updatedAt: num(input.updatedAt, createdAt),
|
|
116
|
-
// Soft delete. A removal has to be REPRESENTABLE or it cannot replicate: a record that
|
|
117
|
-
// is merely absent from one side is indistinguishable from one that side has not seen.
|
|
118
|
-
deletedAt: num(input.deletedAt, 0) || 0,
|
|
119
|
-
body: input.body ?? null,
|
|
120
|
-
meta: input.meta && typeof input.meta === 'object' ? input.meta : {},
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function defaultTitleFor(kind, input) {
|
|
125
|
-
if (kind === 'note') return deriveTitle(input?.body?.markdown ?? input?.body, 'Untitled note');
|
|
126
|
-
if (kind === 'meeting') return 'Untitled meeting';
|
|
127
|
-
if (kind === 'brief') return 'Untitled brief';
|
|
128
|
-
return 'New chat';
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Tags are a shared vocabulary — `tags.js` owns the rules. This is the shallow guard for
|
|
133
|
-
* callers that hand us junk; it deliberately does NOT re-implement normalization, so a
|
|
134
|
-
* client that cares passes tags through `normalizeTags` first.
|
|
135
|
-
*/
|
|
136
|
-
function normalizeTagList(tags) {
|
|
137
|
-
if (!Array.isArray(tags)) return [];
|
|
138
|
-
const out = [];
|
|
139
|
-
for (const t of tags) {
|
|
140
|
-
const s = str(t).trim();
|
|
141
|
-
if (s && !out.includes(s)) out.push(s);
|
|
142
|
-
}
|
|
143
|
-
return out;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export function isValidStoredRecord(rec) {
|
|
147
|
-
try { normalizeStoredRecord(rec); return true; } catch { return false; }
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// --------------------------------------------------------------------------
|
|
151
|
-
// Projection — the lossy shape the search index wants
|
|
152
|
-
// --------------------------------------------------------------------------
|
|
153
|
-
|
|
154
|
-
const ROLE_LABEL = { assistant: 'Assistant', system: 'System', user: 'You' };
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* A record → the flat `{ id, type, title, date, text }` the warm index stores.
|
|
158
|
-
*
|
|
159
|
-
* This is the function the gateway's `backupToRecords` was a copy of. Note what it does NOT
|
|
160
|
-
* do: it never invents an id, never guesses a kind, and never returns a record for something
|
|
161
|
-
* it does not understand — a silent wrong answer in a search index is worse than a gap,
|
|
162
|
-
* because the gap is visible.
|
|
163
|
-
*/
|
|
164
|
-
export function toSearchRecord(record) {
|
|
165
|
-
const rec = normalizeStoredRecord(record);
|
|
166
|
-
if (rec.deletedAt) return null;
|
|
167
|
-
return {
|
|
168
|
-
id: rec.id,
|
|
169
|
-
type: rec.kind,
|
|
170
|
-
title: rec.title,
|
|
171
|
-
date: rec.updatedAt || rec.createdAt,
|
|
172
|
-
text: searchTextFor(rec),
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/** The body of a record as one searchable string. Exported because callers index in batches. */
|
|
177
|
-
export function searchTextFor(record) {
|
|
178
|
-
const rec = record.kind ? record : normalizeStoredRecord(record);
|
|
179
|
-
const head = `${kindLabel(rec.kind)}: ${rec.title}`;
|
|
180
|
-
const tagLine = rec.tags.length ? `\nTags: ${rec.tags.join(', ')}` : '';
|
|
181
|
-
return `${head}${tagLine}\n\n${bodyText(rec)}`.trim();
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function kindLabel(kind) {
|
|
185
|
-
return kind === 'chat' ? 'CHAT' : kind === 'note' ? 'NOTE' : kind === 'meeting' ? 'MEETING' : 'BRIEF';
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function bodyText(rec) {
|
|
189
|
-
const b = rec.body;
|
|
190
|
-
if (b == null) return '';
|
|
191
|
-
if (typeof b === 'string') return b;
|
|
192
|
-
|
|
193
|
-
// A record may arrive FLATTENED rather than structured — the gateway's warm index stores
|
|
194
|
-
// `{ text }` for every kind, with no message list and no markdown. Falling through the
|
|
195
|
-
// kind-specific branches would project it to the empty string, which is worse than it
|
|
196
|
-
// sounds: the record is stored, listed and counted, but is invisible to search and shows
|
|
197
|
-
// an empty preview. So a flat `text` is honoured for any kind, and the structured
|
|
198
|
-
// extraction below wins whenever it actually finds something.
|
|
199
|
-
const flat = typeof b.text === 'string' ? b.text : '';
|
|
200
|
-
|
|
201
|
-
if (rec.kind === 'chat') {
|
|
202
|
-
const turns = (b.messages || [])
|
|
203
|
-
.filter((m) => m && m.content)
|
|
204
|
-
.map((m) => `${ROLE_LABEL[m.role] || 'You'}: ${textOfContent(m.content)}`)
|
|
205
|
-
.join('\n\n');
|
|
206
|
-
return turns || flat;
|
|
207
|
-
}
|
|
208
|
-
if (rec.kind === 'note') return str(b.markdown ?? b.text);
|
|
209
|
-
if (rec.kind === 'meeting') {
|
|
210
|
-
const notes = str(b.notes);
|
|
211
|
-
const segs = (b.segments || [])
|
|
212
|
-
.map((s) => `${str(s.speaker) || '?'}: ${str(s.text)}`)
|
|
213
|
-
.join('\n');
|
|
214
|
-
return [notes, segs].filter(Boolean).join('\n\n') || flat;
|
|
215
|
-
}
|
|
216
|
-
if (rec.kind === 'brief') {
|
|
217
|
-
const claims = (b.claims || []).map((c) => str(c.text)).filter(Boolean).join('\n');
|
|
218
|
-
return [str(b.summary), claims].filter(Boolean).join('\n\n') || flat;
|
|
219
|
-
}
|
|
220
|
-
return flat;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* A message's content may be a string or a multimodal part list. An image part contributes
|
|
225
|
-
* nothing to a text index, and stringifying the object would put `[object Object]` into the
|
|
226
|
-
* corpus — which is not a hypothetical, it is what naive JSON handling does here.
|
|
227
|
-
*/
|
|
228
|
-
function textOfContent(content) {
|
|
229
|
-
if (typeof content === 'string') return content;
|
|
230
|
-
if (!Array.isArray(content)) return '';
|
|
231
|
-
return content
|
|
232
|
-
.map((part) => (typeof part === 'string' ? part : part && part.type === 'text' ? str(part.text) : ''))
|
|
233
|
-
.filter(Boolean)
|
|
234
|
-
.join(' ');
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// --------------------------------------------------------------------------
|
|
238
|
-
// Counting — so every surface reports the same number
|
|
239
|
-
// --------------------------------------------------------------------------
|
|
240
|
-
|
|
241
|
-
/** Words in a note body, by the same rule everywhere. */
|
|
242
|
-
export function wordCount(text) {
|
|
243
|
-
const t = str(text).trim();
|
|
244
|
-
return t ? t.split(/\s+/).length : 0;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Header lines the warm text form carries under its title — `Date: …`, `Platform: meet` —
|
|
249
|
-
* which are metadata, not the preview a row wants. A row already shows the date; a snippet
|
|
250
|
-
* that repeats it says nothing. `You:` / a speaker's name are NOT in this set on purpose:
|
|
251
|
-
* those lines are the content.
|
|
252
|
-
*/
|
|
253
|
-
const META_LINE_RE = /^(Date|Platform|Model|Agent|Tags|Source|Kind|Created|Updated|Started|Ended|Duration|Participants|Attendees|URL|Link):\s/i;
|
|
254
|
-
|
|
255
|
-
/** A short preview for a list row, so a list renders without opening every body. */
|
|
256
|
-
export function snippetOf(text, max = 110) {
|
|
257
|
-
const lines = str(text).split('\n');
|
|
258
|
-
if (lines.length === 1) return lines[0].replace(/[#*_`>~]+/g, '').replace(/\s+/g, ' ').trim().slice(0, max);
|
|
259
|
-
let i = 1; // the first line is the title
|
|
260
|
-
while (i < lines.length && (!lines[i].trim() || META_LINE_RE.test(lines[i].trim()))) i += 1;
|
|
261
|
-
const rest = lines.slice(i).join('\n');
|
|
262
|
-
return rest.replace(/[#*_`>~]+/g, '').replace(/\s+/g, ' ').trim().slice(0, max);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* The index entry a list view renders from — everything needed to draw a row and nothing
|
|
267
|
-
* that requires reading the body a second time.
|
|
268
|
-
*/
|
|
269
|
-
export function toIndexEntry(record) {
|
|
270
|
-
const rec = normalizeStoredRecord(record);
|
|
271
|
-
const text = bodyText(rec);
|
|
272
|
-
return {
|
|
273
|
-
id: rec.id,
|
|
274
|
-
kind: rec.kind,
|
|
275
|
-
title: rec.title,
|
|
276
|
-
tags: rec.tags,
|
|
277
|
-
snippet: snippetOf(text),
|
|
278
|
-
words: wordCount(text),
|
|
279
|
-
chars: text.length,
|
|
280
|
-
createdAt: rec.createdAt,
|
|
281
|
-
updatedAt: rec.updatedAt,
|
|
282
|
-
deletedAt: rec.deletedAt,
|
|
283
|
-
};
|
|
284
|
-
}
|
|
13
|
+
`)||n}return n}function E(t){return typeof t=="string"?t:Array.isArray(t)?t.map(e=>typeof e=="string"?e:e&&e.type==="text"?i(e.text):"").filter(Boolean).join(" "):""}function w(t){const e=i(t).trim();return e?e.split(/\s+/).length:0}const $=/^(Date|Platform|Model|Agent|Tags|Source|Kind|Created|Updated|Started|Ended|Duration|Participants|Attendees|URL|Link):\s/i;function k(t,e=110){const n=i(t).split(`
|
|
14
|
+
`);if(n.length===1)return n[0].replace(/[#*_`>~]+/g,"").replace(/\s+/g," ").trim().slice(0,e);let r=1;for(;r<n.length&&(!n[r].trim()||$.test(n[r].trim()));)r+=1;return n.slice(r).join(`
|
|
15
|
+
`).replace(/[#*_`>~]+/g,"").replace(/\s+/g," ").trim().slice(0,e)}function R(t){const e=c(t),n=g(e);return{id:e.id,kind:e.kind,title:e.title,tags:e.tags,snippet:k(n),words:w(n),chars:n.length,createdAt:e.createdAt,updatedAt:e.updatedAt,deletedAt:e.deletedAt}}export{s as LibraryError,f as MAX_TITLE_LEN,m as RECORD_KINDS,p as deriveTitle,T as isRecordId,I as isValidStoredRecord,S as makeRecordId,c as normalizeStoredRecord,u as parseRecordId,h as searchTextFor,k as snippetOf,R as toIndexEntry,L as toSearchRecord,w as wordCount};
|
package/loop.js
CHANGED
|
@@ -1,132 +1 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// We already run four loops: chat, meeting scribe, watch, and the notes swarm. They were
|
|
4
|
-
// never expressed once; each surface grew its own, and the shared parts were copied. On
|
|
5
|
-
// 2026-08-16 that produced a real bug — `streamChat` opened a turn, one exit path
|
|
6
|
-
// returned above the close, and every finished note reported itself as still running.
|
|
7
|
-
//
|
|
8
|
-
// The fix that day was to route both exits through one helper. The fix here is that
|
|
9
|
-
// there is nothing to route: a loop's `run` receives a context with NO way to open or
|
|
10
|
-
// close a turn. The runner opens before calling it and closes in `finally`, so "forgot to
|
|
11
|
-
// close on one path" is not a mistake a loop author is able to make. That is the
|
|
12
|
-
// difference between a bug fixed and a bug class removed.
|
|
13
|
-
//
|
|
14
|
-
// The runner takes `now`/`newId` rather than reading a clock, matching event.js: this
|
|
15
|
-
// package stays pure so the identical code runs in the extension, gateway and bridge, and
|
|
16
|
-
// so replay is reproducible (order.js never consults wall time; I6 asserts it).
|
|
17
|
-
|
|
18
|
-
export const LOOP_KINDS = Object.freeze(['chat', 'note', 'meeting', 'watch', 'assist', 'suggestion', 'topics', 'other']);
|
|
19
|
-
|
|
20
|
-
export class LoopError extends Error {
|
|
21
|
-
constructor(code, message) { super(message); this.name = 'LoopError'; this.code = code; }
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Declare a loop. A declaration is inert — it names a kind and supplies `run`; the runner
|
|
26
|
-
* supplies everything about when a turn exists.
|
|
27
|
-
*/
|
|
28
|
-
export function defineLoop({ id, kind = 'other', run, background = false }) {
|
|
29
|
-
if (!id) throw new LoopError('BAD_LOOP', 'loop.id required');
|
|
30
|
-
if (typeof run !== 'function') throw new LoopError('BAD_LOOP', `loop '${id}': run required`);
|
|
31
|
-
return Object.freeze({ id, kind, run, background: !!background });
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* @param emit `(type, payload)` — the host maps these to the event schema. Kept as a
|
|
36
|
-
* plain hook so this module has no dependency on the log, matching registry.js.
|
|
37
|
-
* @param decide optional `(guard, request) => decision` — normally `kernel.decide`. A
|
|
38
|
-
* security plugin can refuse a turn before any model call happens, which is
|
|
39
|
-
* the whole reason guards exist below the loop rather than inside it.
|
|
40
|
-
*/
|
|
41
|
-
export function createTurnRunner({ now = () => 0, newId, emit = () => {}, decide = null } = {}) {
|
|
42
|
-
if (typeof newId !== 'function') throw new LoopError('BAD_RUNNER', 'newId required — the runner must not invent identity');
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Run one turn of `loop`. Returns whatever `run` returned.
|
|
46
|
-
*
|
|
47
|
-
* `request.turnId` lets a caller supply identity it already has (the side panel's
|
|
48
|
-
* assistant-message id), so the turn record and the tool events that reference it group
|
|
49
|
-
* into ONE run rather than two — the mismatch that split runs in Activity.
|
|
50
|
-
*/
|
|
51
|
-
async function run(loop, request = {}) {
|
|
52
|
-
if (!loop || typeof loop.run !== 'function') throw new LoopError('BAD_LOOP', 'runner.run needs a loop with run()');
|
|
53
|
-
|
|
54
|
-
const turnId = request.turnId || newId();
|
|
55
|
-
const kind = request.kind || loop.kind || 'other';
|
|
56
|
-
|
|
57
|
-
// Ask BEFORE opening a turn: a denied turn should leave no trace of having started,
|
|
58
|
-
// and a guard that only ran after the model call would be documentation, not a guard.
|
|
59
|
-
if (decide) {
|
|
60
|
-
const d = decide('turn.start', { turnId, kind, loopId: loop.id, ...request });
|
|
61
|
-
if (d && d.allow === false) {
|
|
62
|
-
emit('policy.denied', { turnId, kind, loopId: loop.id, reasons: d.reasons || [] });
|
|
63
|
-
throw new LoopError('DENIED', `turn denied: ${(d.reasons || []).join('; ') || 'policy'}`);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// The turn began when the USER acted, not when the model call did. Everything between
|
|
68
|
-
// — assembling tools, connecting to MCP servers — is time they waited, and a duration
|
|
69
|
-
// that excludes it says 2.6s about a message that took 48. Callers that know the real
|
|
70
|
-
// moment pass it; the rest fall back to now.
|
|
71
|
-
const startedAt = Number.isFinite(request.startedAt) ? request.startedAt : now();
|
|
72
|
-
let closed = false;
|
|
73
|
-
// Facts the loop learns while running — token usage, the model that actually served
|
|
74
|
-
// it — belong on the turn record. The loop may CONTRIBUTE them; it still cannot
|
|
75
|
-
// decide when the turn ends. Handing over the payload is not the same as handing over
|
|
76
|
-
// the lifetime, and only the second one was ever the problem.
|
|
77
|
-
let reported = {};
|
|
78
|
-
// The ONE place a turn closes. Idempotent because a retry path or a double-catch must
|
|
79
|
-
// not be able to write two endings for one turn — a log that can say a turn ended
|
|
80
|
-
// twice cannot be replayed.
|
|
81
|
-
const close = (reason, produced) => {
|
|
82
|
-
if (closed) return;
|
|
83
|
-
closed = true;
|
|
84
|
-
emit('turn.ended', { ...reported, turnId, reason, stepped: !!produced, ms: now() - startedAt, kind });
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
emit('turn.started', {
|
|
88
|
-
turnId, kind, loopId: loop.id,
|
|
89
|
-
agentId: request.agentId || null,
|
|
90
|
-
// WHICH THREAD THIS BELONGS TO. A run on its own is not the unit anyone reasons about —
|
|
91
|
-
// a conversation is, and a meeting or a note can hold many runs (live monitors,
|
|
92
|
-
// summaries, a swarm of agents). Without both halves the log is 1,205 unrelated rows.
|
|
93
|
-
surface: request.surface || null,
|
|
94
|
-
sourceId: request.sourceId || null,
|
|
95
|
-
background: request.background ?? loop.background,
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
let produced = false;
|
|
99
|
-
try {
|
|
100
|
-
// NOTE what this context does NOT contain: no close, no end, no turn handle. A loop
|
|
101
|
-
// cannot leave its own turn open because it was never given the ability to close it.
|
|
102
|
-
const result = await loop.run({
|
|
103
|
-
turnId,
|
|
104
|
-
kind,
|
|
105
|
-
signal: request.signal,
|
|
106
|
-
request,
|
|
107
|
-
/** Report that something reached the user — this is a fact about the turn, not control over it. */
|
|
108
|
-
produced: () => { produced = true; },
|
|
109
|
-
/**
|
|
110
|
-
* Contribute facts to the turn record (tokens, model, cost). Merged into
|
|
111
|
-
* `turn.ended`, and never able to overwrite the fields the runner owns — a loop
|
|
112
|
-
* that could rewrite its own turnId or reason would be holding lifetime again by
|
|
113
|
-
* another name.
|
|
114
|
-
*/
|
|
115
|
-
report: (fields) => { if (fields && typeof fields === 'object') reported = { ...reported, ...fields }; },
|
|
116
|
-
/** Loops emit their own domain events; lifetime is still not theirs. */
|
|
117
|
-
emit: (type, payload) => emit(type, { turnId, ...payload }),
|
|
118
|
-
});
|
|
119
|
-
if (result !== undefined && result !== null && result !== '') produced = true;
|
|
120
|
-
close(request.signal?.aborted ? 'aborted' : 'ok', produced);
|
|
121
|
-
return result;
|
|
122
|
-
} catch (err) {
|
|
123
|
-
// An abort is not a failure: the user asking it to stop and the loop breaking are
|
|
124
|
-
// different facts, and collapsing them makes the log useless for the question people
|
|
125
|
-
// actually ask ("did it fail, or did I stop it?").
|
|
126
|
-
close(request.signal?.aborted ? 'aborted' : 'error', produced);
|
|
127
|
-
throw err;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return { run };
|
|
132
|
-
}
|
|
1
|
+
const I=Object.freeze(["chat","note","meeting","watch","assist","suggestion","topics","other"]);class c extends Error{constructor(i,t){super(t),this.name="LoopError",this.code=i}}function O({id:o,kind:i="other",run:t,background:s=!1}){if(!o)throw new c("BAD_LOOP","loop.id required");if(typeof t!="function")throw new c("BAD_LOOP",`loop '${o}': run required`);return Object.freeze({id:o,kind:i,run:t,background:!!s})}function y({now:o=()=>0,newId:i,emit:t=()=>{},decide:s=null}={}){if(typeof i!="function")throw new c("BAD_RUNNER","newId required — the runner must not invent identity");async function w(e,n={}){if(!e||typeof e.run!="function")throw new c("BAD_LOOP","runner.run needs a loop with run()");const u=n.turnId||i(),a=n.kind||e.kind||"other";if(s){const r=s("turn.start",{turnId:u,kind:a,loopId:e.id,...n});if(r&&r.allow===!1)throw t("policy.denied",{turnId:u,kind:a,loopId:e.id,reasons:r.reasons||[]}),new c("DENIED",`turn denied: ${(r.reasons||[]).join("; ")||"policy"}`)}const g=Number.isFinite(n.startedAt)?n.startedAt:o();let p=!1,l={};const h=(r,d)=>{p||(p=!0,t("turn.ended",{...l,turnId:u,reason:r,stepped:!!d,ms:o()-g,kind:a}))};t("turn.started",{turnId:u,kind:a,loopId:e.id,agentId:n.agentId||null,surface:n.surface||null,sourceId:n.sourceId||null,background:n.background??e.background});let f=!1;try{const r=await e.run({turnId:u,kind:a,signal:n.signal,request:n,produced:()=>{f=!0},report:d=>{d&&typeof d=="object"&&(l={...l,...d})},emit:(d,b)=>t(d,{turnId:u,...b})});return r!=null&&r!==""&&(f=!0),h(n.signal?.aborted?"aborted":"ok",f),r}catch(r){throw h(n.signal?.aborted?"aborted":"error",f),r}}return{run:w}}export{I as LOOP_KINDS,c as LoopError,y as createTurnRunner,O as defineLoop};
|
package/manifest.js
CHANGED
|
@@ -1,107 +1 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// Four registries now exist — adapters, tool groups, search engines, sources — and each is
|
|
4
|
-
// small and correct on its own. What none of them can answer is the question a user asks:
|
|
5
|
-
// "what is running, and can I turn that off?" Answering it in four places would be the
|
|
6
|
-
// duplication that already bit us with the engine list, one level up.
|
|
7
|
-
//
|
|
8
|
-
// So the KERNEL owns the manifest and the registries consult it. This is admission control,
|
|
9
|
-
// not a rewrite: a registry keeps its own shape and simply refuses to offer something the
|
|
10
|
-
// user has disabled. It is also the seam that makes user-contributed plugins possible —
|
|
11
|
-
// once a plugin can arrive from outside, "is this allowed to run" must be asked somewhere
|
|
12
|
-
// that is not the plugin itself.
|
|
13
|
-
//
|
|
14
|
-
// Persistence is INJECTED. Where the toggles live is a platform question (chrome.storage,
|
|
15
|
-
// a file, a database) and the manifest has no business knowing.
|
|
16
|
-
|
|
17
|
-
export class ManifestError extends Error {
|
|
18
|
-
constructor(code, message) { super(message); this.name = 'ManifestError'; this.code = code; }
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** Where a plugin came from. `user` is the one that must never skip a guard. */
|
|
22
|
-
export const SOURCES = Object.freeze(['built-in', 'user']);
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* @param required ids that cannot be disabled, whatever the stored state says. Security is
|
|
26
|
-
* the canonical member: a mandatory plugin the user can switch off is not mandatory,
|
|
27
|
-
* it is a default.
|
|
28
|
-
* @param disabled the user's stored choices — ids they have turned OFF. Stored as the
|
|
29
|
-
* exception rather than the full state on purpose: a plugin added in a later release
|
|
30
|
-
* is then enabled by default without a migration, because absence means "not
|
|
31
|
-
* disabled" rather than "unknown".
|
|
32
|
-
*/
|
|
33
|
-
export function createManifest({ required = ['security'], disabled = [], onChange = null } = {}) {
|
|
34
|
-
const entries = new Map();
|
|
35
|
-
const off = new Set(disabled);
|
|
36
|
-
const req = new Set(required);
|
|
37
|
-
|
|
38
|
-
const notify = () => { if (onChange) onChange([...off].sort()); };
|
|
39
|
-
|
|
40
|
-
return {
|
|
41
|
-
/**
|
|
42
|
-
* Declare something installed. Idempotent, so a registry can register on every build
|
|
43
|
-
* without accumulating duplicates.
|
|
44
|
-
*/
|
|
45
|
-
register({ id, kind, label, source = 'built-in', description = '' }) {
|
|
46
|
-
if (!id) throw new ManifestError('BAD_ENTRY', 'plugin id required');
|
|
47
|
-
if (!SOURCES.includes(source)) throw new ManifestError('BAD_ENTRY', `plugin '${id}': unknown source '${source}'`);
|
|
48
|
-
entries.set(id, { id, kind: kind || 'plugin', label: label || id, source, description });
|
|
49
|
-
return () => entries.delete(id);
|
|
50
|
-
},
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* The question every registry asks. Unknown ids are ENABLED: a registry may consult the
|
|
54
|
-
* manifest before anything has registered, and defaulting to off would make a plugin
|
|
55
|
-
* silently vanish because of a load-order accident.
|
|
56
|
-
*/
|
|
57
|
-
isEnabled(id) {
|
|
58
|
-
if (req.has(id)) return true;
|
|
59
|
-
return !off.has(id);
|
|
60
|
-
},
|
|
61
|
-
|
|
62
|
-
/** Turn something on or off. Required plugins refuse rather than reporting success. */
|
|
63
|
-
setEnabled(id, enabled) {
|
|
64
|
-
if (req.has(id) && !enabled) {
|
|
65
|
-
throw new ManifestError('REQUIRED', `'${id}' is required and cannot be disabled`);
|
|
66
|
-
}
|
|
67
|
-
const was = !off.has(id);
|
|
68
|
-
if (enabled) off.delete(id); else off.add(id);
|
|
69
|
-
if (was !== !!enabled) notify();
|
|
70
|
-
return this.isEnabled(id);
|
|
71
|
-
},
|
|
72
|
-
|
|
73
|
-
/** Everything installed, with its current state — what a settings page renders. */
|
|
74
|
-
list() {
|
|
75
|
-
return [...entries.values()]
|
|
76
|
-
.map((e) => ({ ...e, enabled: this.isEnabled(e.id), required: req.has(e.id) }))
|
|
77
|
-
.sort((a, b) => (a.kind === b.kind ? a.label.localeCompare(b.label) : a.kind.localeCompare(b.kind)));
|
|
78
|
-
},
|
|
79
|
-
|
|
80
|
-
/** Only the ids that are OFF — the shape that persists (see the note above). */
|
|
81
|
-
disabledIds: () => [...off].sort(),
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Adopt state changed elsewhere.
|
|
85
|
-
*
|
|
86
|
-
* A manifest is not a fact read once at startup: the user toggles a plugin in one place
|
|
87
|
-
* (a settings page, another window, another device) and every OTHER place holding a
|
|
88
|
-
* manifest is now wrong. Without this, a toggle appears to do nothing until reload —
|
|
89
|
-
* which reads as the switch being broken.
|
|
90
|
-
*
|
|
91
|
-
* Deliberately does NOT notify: this is the arrival of someone else's change, and
|
|
92
|
-
* echoing it back is how two contexts write over each other forever.
|
|
93
|
-
*/
|
|
94
|
-
sync(ids) {
|
|
95
|
-
const next = new Set(Array.isArray(ids) ? ids : []);
|
|
96
|
-
if (next.size === off.size && [...next].every((id) => off.has(id))) return false;
|
|
97
|
-
off.clear();
|
|
98
|
-
for (const id of next) off.add(id);
|
|
99
|
-
return true;
|
|
100
|
-
},
|
|
101
|
-
|
|
102
|
-
/** Filter a registry's candidates. The one call a registry needs to make. */
|
|
103
|
-
filter(items, idOf = (x) => x?.id) {
|
|
104
|
-
return (items || []).filter((x) => this.isEnabled(idOf(x)));
|
|
105
|
-
},
|
|
106
|
-
};
|
|
107
|
-
}
|
|
1
|
+
class u extends Error{constructor(i,s){super(s),this.name="ManifestError",this.code=i}}const h=Object.freeze(["built-in","user"]);function E({required:c=["security"],disabled:i=[],onChange:s=null}={}){const a=new Map,n=new Set(i),l=new Set(c),f=()=>{s&&s([...n].sort())};return{register({id:e,kind:r,label:t,source:o="built-in",description:d=""}){if(!e)throw new u("BAD_ENTRY","plugin id required");if(!h.includes(o))throw new u("BAD_ENTRY",`plugin '${e}': unknown source '${o}'`);return a.set(e,{id:e,kind:r||"plugin",label:t||e,source:o,description:d}),()=>a.delete(e)},isEnabled(e){return l.has(e)?!0:!n.has(e)},setEnabled(e,r){if(l.has(e)&&!r)throw new u("REQUIRED",`'${e}' is required and cannot be disabled`);const t=!n.has(e);return r?n.delete(e):n.add(e),t!==!!r&&f(),this.isEnabled(e)},list(){return[...a.values()].map(e=>({...e,enabled:this.isEnabled(e.id),required:l.has(e.id)})).sort((e,r)=>e.kind===r.kind?e.label.localeCompare(r.label):e.kind.localeCompare(r.kind))},disabledIds:()=>[...n].sort(),sync(e){const r=new Set(Array.isArray(e)?e:[]);if(r.size===n.size&&[...r].every(t=>n.has(t)))return!1;n.clear();for(const t of r)n.add(t);return!0},filter(e,r=t=>t?.id){return(e||[]).filter(t=>this.isEnabled(r(t)))}}}export{u as ManifestError,h as SOURCES,E as createManifest};
|