@chatpanel/events 0.102.0 → 0.104.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/adapters.js +1 -83
  2. package/adaptive-tool-policy.js +1 -45
  3. package/agent.js +2 -255
  4. package/attribution.js +1 -132
  5. package/backup-envelope.js +1 -221
  6. package/board-tool.js +2 -165
  7. package/budget.js +1 -116
  8. package/capability.js +1 -126
  9. package/citations.js +5 -127
  10. package/client-prefs.js +1 -148
  11. package/context-attachments.js +8 -144
  12. package/cowriter-router.js +1 -83
  13. package/cowriter-writer.js +19 -193
  14. package/cowriter.js +1 -190
  15. package/curate.js +3 -509
  16. package/distance.js +1 -124
  17. package/engine.js +1 -131
  18. package/entitlement.js +1 -332
  19. package/entity.js +1 -354
  20. package/event.js +1 -175
  21. package/extraction.js +4 -293
  22. package/failover.js +1 -93
  23. package/find-tool.js +1 -66
  24. package/flowchart.js +2 -644
  25. package/gate.js +1 -74
  26. package/harness.js +4 -101
  27. package/index.js +1 -345
  28. package/invariants.js +1 -174
  29. package/job.js +3 -147
  30. package/kernel.js +1 -255
  31. package/knowledge-derive.js +1 -267
  32. package/knowledge.js +3 -230
  33. package/library.js +11 -280
  34. package/loop.js +1 -132
  35. package/manifest.js +1 -107
  36. package/markdown-authoring.js +11 -288
  37. package/markdown-render.js +0 -0
  38. package/mcp-client.js +8 -358
  39. package/mcp-dispatch.js +1 -52
  40. package/mcp-errors.js +1 -106
  41. package/mcp-manager.js +1 -127
  42. package/media-transcript.js +8 -531
  43. package/meeting-analyzers.js +1 -83
  44. package/meeting-insights.js +7 -164
  45. package/meeting-shape.js +1 -282
  46. package/meeting-text.js +5 -94
  47. package/memory.js +2 -638
  48. package/model-candidates.js +1 -358
  49. package/model-health.js +1 -182
  50. package/model-ledger.js +1 -228
  51. package/model-picker.js +1 -188
  52. package/note-actions.js +15 -246
  53. package/note-graph.js +1 -158
  54. package/note-links.js +1 -68
  55. package/note-mentions.js +8 -114
  56. package/note-plan.js +9 -107
  57. package/note-research.js +1 -135
  58. package/observability.js +1 -119
  59. package/omni.js +1 -125
  60. package/order.js +1 -78
  61. package/package.json +4 -5
  62. package/pdf-layout.js +7 -253
  63. package/policy.js +1 -0
  64. package/project-run.js +20 -444
  65. package/project.js +1 -170
  66. package/promotion.js +1 -171
  67. package/queue.js +1 -98
  68. package/reach.js +1 -23
  69. package/recipe-tool.js +2 -161
  70. package/recipe.js +1 -264
  71. package/record-list.js +2 -110
  72. package/recruit.js +3 -456
  73. package/redaction-tokens.js +1 -61
  74. package/ref.js +1 -55
  75. package/registry.js +1 -240
  76. package/route-graph.js +1 -115
  77. package/route-strategies.js +2 -232
  78. package/router.js +2 -880
  79. package/rrf.js +1 -89
  80. package/rules.js +1 -142
  81. package/schedule.js +1 -663
  82. package/scm-connection.js +1 -180
  83. package/scopes.js +1 -12
  84. package/scorecard.js +1 -335
  85. package/search-engines.js +1 -81
  86. package/skill-manifest.js +1 -330
  87. package/skill-scan.js +3 -209
  88. package/skill-sources.js +1 -162
  89. package/skill-vars.js +2 -252
  90. package/slash-commands.js +1 -158
  91. package/source-gate.js +1 -92
  92. package/sources-retrieval.js +5 -186
  93. package/sources.js +1 -273
  94. package/store.js +1 -171
  95. package/structured.js +4 -901
  96. package/subject-kinds.js +1 -5
  97. package/subject-name.js +1 -96
  98. package/sync-plan.js +1 -213
  99. package/synthesis.js +3 -123
  100. package/tags.js +1 -203
  101. package/team-board.js +6 -337
  102. package/team-cache.js +2 -63
  103. package/team-observe.js +1 -118
  104. package/team-org.js +1 -442
  105. package/team-plan.js +4 -131
  106. package/team-record.js +1 -225
  107. package/team-run.js +9 -652
  108. package/team-subtask.js +2 -191
  109. package/team-task.js +2 -124
  110. package/team-tool.js +2 -196
  111. package/team-trail.js +2 -73
  112. package/team-worklog.js +2 -149
  113. package/team.js +1 -302
  114. package/text-search.js +1 -155
  115. package/theme.js +17 -154
  116. package/titles.js +4 -285
  117. package/tool-discovery.js +1 -87
  118. package/tool-dispatch.js +5 -230
  119. package/tool-groups.js +1 -81
  120. package/tool-hints.js +10 -185
  121. package/tool-loop-guard.js +4 -182
  122. package/tool-need.js +2 -124
  123. package/tool-result.js +2 -438
  124. package/tool-round.js +0 -0
  125. package/tool-schema.js +1 -155
  126. package/tool-traits.js +1 -182
  127. package/toolset.js +2 -88
  128. package/trajectory.js +1 -530
  129. package/turn-loop.js +4 -440
  130. package/upcast.js +1 -37
  131. package/vault.js +2 -250
  132. package/view.js +1 -88
  133. package/voice-intents.js +2 -1488
  134. package/voice-speaker.js +1 -98
  135. package/weather-tool.js +1 -65
  136. package/weather.js +2 -211
  137. package/web-search-tool.js +14 -102
  138. package/web-search.js +1 -160
  139. package/widget.js +1 -153
package/note-actions.js CHANGED
@@ -1,252 +1,21 @@
1
- // AI writing in a note — what the model is asked, and where its answer lands.
2
- //
3
- // A note has three ways to ask a model to write: a NAMED ACTION on the selection or the whole
4
- // note ("Continue writing", "Summarize", "Turn into tasks", "Improve writing"), an
5
- // `@command instruction` typed on a line ("@table the G7 by population"), and a `/` palette
6
- // that offers the actions at the caret. Every client has all three, and each has two halves
7
- // that must agree: the PROMPT the model receives and the FRAME the output is written into —
8
- // head, output, tail. Ask for a summary and land it at the caret, or ask for a checklist and
9
- // replace the whole note with it, and the feature is wrong in a way no prompt fixes.
10
- //
11
- // These lived inside the extension's `notes.js` — five thousand lines, DOM-bound — and the
12
- // desktop was about to type them a second time. Nothing here needs a window: the specs are
13
- // data, the framing is body + selection in and three strings out, the triggers are text +
14
- // caret in and a range out. So they are here, and the clients render the menu around them.
15
- //
16
- // THE CLIENT STILL OWNS: the model call (through its own streaming path), the editor write,
17
- // attribution and versioning, and any tools it arms. `NOTE_COMMANDS[].tools` says a command
18
- // WANTS live data; whether a target can fetch it is the client's to know.
1
+ const h=Object.freeze({continue:Object.freeze({label:"Continue writing",hint:"draft inline from here",system:"You are a writing assistant continuing the user's note. Match their voice, tone and markdown style, and continue naturally from where the text stops. Output ONLY the continuation no preamble, no repeating prior text.",maxTokens:700,needsSelection:!1}),summarize:Object.freeze({label:"Summarize",hint:"a tight summary",system:"Summarize the text concisely in GitHub-flavored markdown — a few tight bullets or a short paragraph. Output ONLY the summary.",maxTokens:500,needsSelection:!1}),tasks:Object.freeze({label:"Turn into tasks",hint:"selection → checklist",system:'Convert the text into a GitHub-flavored markdown checklist: one actionable item per line as "- [ ] item". Output ONLY the checklist.',maxTokens:700,needsSelection:!0}),improve:Object.freeze({label:"Improve writing",hint:"rewrite clearer",system:"Rewrite the text to be clearer and more concise while preserving its meaning, tone and markdown formatting. Output ONLY the rewritten text.",maxTokens:1e3,needsSelection:!1})}),w=.5,p=Object.freeze(["continue","summarize","tasks","improve"]),x=e=>h[e]?.label||String(e||"");function N(e,n,r=0,s=r){if(!h[e])return{error:"unknown"};const t=String(n??""),a=f(r,0,t.length),l=f(s,a,t.length),i=t.slice(a,l);let c,u,m;if(e==="continue")c=t,u=t+(t&&!/\s$/.test(t)?`
19
2
 
20
- /**
21
- * The named actions, keyed the way the menus and the `/` palette refer to them.
22
- *
23
- * `system` is the whole instruction; the user turn is the target text alone. Every prompt
24
- * ends in "Output ONLY …" because the answer is written INTO the note — a "Sure! Here is
25
- * your summary:" preamble would land in the document verbatim.
26
- */
27
- export const NOTE_ACTIONS = Object.freeze({
28
- continue: Object.freeze({
29
- label: 'Continue writing',
30
- hint: 'draft inline from here',
31
- system: "You are a writing assistant continuing the user's note. Match their voice, tone and markdown style, and continue naturally from where the text stops. Output ONLY the continuation — no preamble, no repeating prior text.",
32
- maxTokens: 700,
33
- needsSelection: false,
34
- }),
35
- summarize: Object.freeze({
36
- label: 'Summarize',
37
- hint: 'a tight summary',
38
- system: 'Summarize the text concisely in GitHub-flavored markdown — a few tight bullets or a short paragraph. Output ONLY the summary.',
39
- maxTokens: 500,
40
- needsSelection: false,
41
- }),
42
- tasks: Object.freeze({
43
- label: 'Turn into tasks',
44
- hint: 'selection → checklist',
45
- system: 'Convert the text into a GitHub-flavored markdown checklist: one actionable item per line as "- [ ] item". Output ONLY the checklist.',
46
- maxTokens: 700,
47
- needsSelection: true,
48
- }),
49
- improve: Object.freeze({
50
- label: 'Improve writing',
51
- hint: 'rewrite clearer',
52
- system: 'Rewrite the text to be clearer and more concise while preserving its meaning, tone and markdown formatting. Output ONLY the rewritten text.',
53
- maxTokens: 1000,
54
- needsSelection: false,
55
- }),
56
- });
3
+ `:""),m="";else if(e==="summarize")c=i||t,u=`${t.replace(/\s+$/,"")}
57
4
 
58
- /** The sampling every action uses. Low enough to stay on-task, high enough to write prose. */
59
- export const NOTE_ACTION_TEMPERATURE = 0.5;
5
+ ## Summary
60
6
 
61
- /** The order the `/` palette and the Agent menu list them in. */
62
- export const NOTE_ACTION_ORDER = Object.freeze(['continue', 'summarize', 'tasks', 'improve']);
7
+ `,m="";else if(e==="tasks"){if(!i.trim())return{error:"select"};c=i,u=t.slice(0,a),m=t.slice(l)}else c=i||t,u=i?t.slice(0,a):"",m=i?t.slice(l):"";return c.trim()?{target:c,head:u,tail:m}:{error:"empty"}}const y=Object.freeze({select:"Select some text first",empty:"Nothing to work with yet",unknown:"Unknown action"}),d=Object.freeze([Object.freeze({cmd:"insert",label:"Insert",hint:"generate or fetch, then insert",tools:!0,system:"You insert content into the user's note. Follow the instruction. If it needs current, live, or web data, USE the web_search tool to fetch it — never guess or use stale knowledge. Output ONLY the content to insert as clean GitHub-flavored markdown — no preamble, no closing remarks."}),Object.freeze({cmd:"table",label:"Table",hint:"produce a markdown table",tools:!0,system:"Produce the requested data as a GitHub-flavored markdown table. If it needs live/current data, USE the web_search tool. Output ONLY the table."}),Object.freeze({cmd:"list",label:"List",hint:"produce a bullet/task list",tools:!0,system:"Produce the requested content as a GitHub-flavored markdown list (use - [ ] for actionable tasks). Use web_search for live data. Output ONLY the list."}),Object.freeze({cmd:"summarize",label:"Summarize",hint:"summarize a topic",tools:!1,system:"Summarize the requested topic concisely in markdown. Output ONLY the summary."}),Object.freeze({cmd:"translate",label:"Translate",hint:"translate to a language",tools:!1,system:"Translate the requested text to the requested language, preserving markdown. Output ONLY the translation."})]),T=.4,k=1800,b=new RegExp(`@(${d.map(e=>e.cmd).join("|")})\\b[ \\t]*(.*)$`,"i");function S(e,n){const r=String(e??""),s=f(n,0,r.length),o=r.lastIndexOf(`
8
+ `,s-1)+1;let t=r.indexOf(`
9
+ `,s);t<0&&(t=r.length);const l=r.slice(o,t).match(b);if(!l)return null;const i=d.find(u=>u.cmd===l[1].toLowerCase()),c=(l[2]||"").trim();return!i||!c?null:{spec:i,instruction:c,start:o+l.index,end:t}}function E(e,n,r,{hasSelection:s=!1}={}){if(s)return null;const o=String(e??""),t=f(n,0,o.length),a=o.slice(o.lastIndexOf(`
10
+ `,t-1)+1,t),l=g(String(r||"/")),i=a.match(new RegExp(`(?:^|\\s)${l}(\\w*)$`));return i?{word:i[1],start:t-i[1].length,end:t}:null}function z(e,n){const r=String(n||"").trim().toLowerCase(),s=Array.isArray(e)?e:[];return r?s.filter(o=>String(o.key||"").toLowerCase().startsWith(r)||String(o.label||"").toLowerCase().includes(r)):s}function _(e=p){return e.filter(n=>h[n]).map(n=>({key:n,label:h[n].label,hint:h[n].hint}))}const O=4e3;function v(e,n,{title:r="",max:s=O}={}){const o=`${e??""}
11
+ ${n??""}`.replace(/\n{3,}/g,`
63
12
 
64
- /** The label for an action key, or the key itself for one this build does not know. */
65
- export const noteActionLabel = (kind) => NOTE_ACTIONS[kind]?.label || String(kind || '');
13
+ `).trim();if(!o)return"";const t=o.length>s?`${o.slice(0,s)}
14
+ …(note truncated)`:o,a=String(r||"").trim();return`The note I'm editing${a?` (title: "${a}")`:""} is below. Resolve any reference in my instruction — "this meeting", "above", a date, a name, a [[wikilink]] or a URL (a meeting link's #id identifies that exact meeting) — against THIS note, not a guess. If a tool lets you fetch something referenced here by id, use that id.
66
15
 
67
- /**
68
- * Where an action's output lands.
69
- *
70
- * Returns `{ target, head, tail }`: the text the model is given, and what sits before and
71
- * after its answer in the finished note. Or `{ error }` — one of the two ways an action
72
- * cannot run — so a client shows the reason rather than sending an empty prompt and reading
73
- * the model's confusion back into the document.
74
- *
75
- * continue — the whole note in, the answer appended after a blank line;
76
- * summarize — the selection (or the note) in, a `## Summary` section appended at the end;
77
- * tasks — the selection in, the checklist REPLACES it; needs a selection, because a
78
- * checklist of the entire note replacing the entire note is never what was meant;
79
- * improve — the selection in and replaced; with no selection the whole note is rewritten.
80
- *
81
- * `error` is 'select' (this action needs a selection) or 'empty' (nothing to work with).
82
- */
83
- export function frameNoteAction(kind, body, selStart = 0, selEnd = selStart) {
84
- const spec = NOTE_ACTIONS[kind];
85
- if (!spec) return { error: 'unknown' };
86
- const text = String(body ?? '');
87
- const s0 = clamp(selStart, 0, text.length);
88
- const s1 = clamp(selEnd, s0, text.length);
89
- const sel = text.slice(s0, s1);
16
+ """
17
+ ${t}
18
+ """`}function A(e,n){const r=String(e??"");return n?`${n}
90
19
 
91
- let target;
92
- let head;
93
- let tail;
94
- if (kind === 'continue') {
95
- target = text;
96
- head = text + (text && !/\s$/.test(text) ? '\n\n' : '');
97
- tail = '';
98
- } else if (kind === 'summarize') {
99
- target = sel || text;
100
- head = `${text.replace(/\s+$/, '')}\n\n## Summary\n\n`;
101
- tail = '';
102
- } else if (kind === 'tasks') {
103
- if (!sel.trim()) return { error: 'select' };
104
- target = sel;
105
- head = text.slice(0, s0);
106
- tail = text.slice(s1);
107
- } else {
108
- target = sel || text;
109
- head = sel ? text.slice(0, s0) : '';
110
- tail = sel ? text.slice(s1) : '';
111
- }
112
- if (!target.trim()) return { error: 'empty' };
113
- return { target, head, tail };
114
- }
115
-
116
- /** What a client should tell the user for each `frameNoteAction` error. */
117
- export const NOTE_ACTION_ERRORS = Object.freeze({
118
- select: 'Select some text first',
119
- empty: 'Nothing to work with yet',
120
- unknown: 'Unknown action',
121
- });
122
-
123
- /**
124
- * The `@command instruction` grammar — a line the user writes, and the model replaces.
125
- *
126
- * `tools: true` means the command is about live data and wants a web search armed. A target
127
- * that cannot search (an HTTP model with no tools) still answers; the prompt tells it not to
128
- * guess, and a client that knows its target cannot fetch may say so.
129
- */
130
- export const NOTE_COMMANDS = Object.freeze([
131
- Object.freeze({ cmd: 'insert', label: 'Insert', hint: 'generate or fetch, then insert', tools: true, system: 'You insert content into the user\'s note. Follow the instruction. If it needs current, live, or web data, USE the web_search tool to fetch it — never guess or use stale knowledge. Output ONLY the content to insert as clean GitHub-flavored markdown — no preamble, no closing remarks.' }),
132
- Object.freeze({ cmd: 'table', label: 'Table', hint: 'produce a markdown table', tools: true, system: 'Produce the requested data as a GitHub-flavored markdown table. If it needs live/current data, USE the web_search tool. Output ONLY the table.' }),
133
- Object.freeze({ cmd: 'list', label: 'List', hint: 'produce a bullet/task list', tools: true, system: 'Produce the requested content as a GitHub-flavored markdown list (use - [ ] for actionable tasks). Use web_search for live data. Output ONLY the list.' }),
134
- Object.freeze({ cmd: 'summarize', label: 'Summarize', hint: 'summarize a topic', tools: false, system: 'Summarize the requested topic concisely in markdown. Output ONLY the summary.' }),
135
- Object.freeze({ cmd: 'translate', label: 'Translate', hint: 'translate to a language', tools: false, system: 'Translate the requested text to the requested language, preserving markdown. Output ONLY the translation.' }),
136
- ]);
137
-
138
- /** The sampling a command runs at — tighter than an action, because it is following orders. */
139
- export const NOTE_COMMAND_TEMPERATURE = 0.4;
140
- export const NOTE_COMMAND_MAX_TOKENS = 1800;
141
-
142
- // Matches `@command …` ANYWHERE on the line, not only at its start, so it works mid-sentence.
143
- const NOTE_CMD_RE = new RegExp(`@(${NOTE_COMMANDS.map((c) => c.cmd).join('|')})\\b[ \\t]*(.*)$`, 'i');
144
-
145
- /**
146
- * The runnable `@command instruction` on the line the caret is on, or null.
147
- *
148
- * Returns `{ spec, instruction, start, end }` where [start, end) is the span to replace —
149
- * from the `@` to the end of the line, so text BEFORE the command on that line survives.
150
- * A command with no instruction is not runnable: "@table" alone is the user still typing.
151
- */
152
- export function commandLineAt(text, pos) {
153
- const v = String(text ?? '');
154
- const p = clamp(pos, 0, v.length);
155
- const lineStart = v.lastIndexOf('\n', p - 1) + 1;
156
- let end = v.indexOf('\n', p);
157
- if (end < 0) end = v.length;
158
- const line = v.slice(lineStart, end);
159
- const m = line.match(NOTE_CMD_RE);
160
- if (!m) return null;
161
- const spec = NOTE_COMMANDS.find((c) => c.cmd === m[1].toLowerCase());
162
- const instruction = (m[2] || '').trim();
163
- if (!spec || !instruction) return null;
164
- return { spec, instruction, start: lineStart + m.index, end };
165
- }
166
-
167
- /**
168
- * The `<trigger>word` being typed at the caret — `/` for the palette, `@` for commands and
169
- * mentions — or null. The trigger must open a token: at the start of the line or after
170
- * whitespace, so `a/b` and `alex@example.com` never pop a menu. A `#` trigger with a space
171
- * after it is a markdown heading, which `\w*` already refuses.
172
- *
173
- * Returns `{ word, start, end }` where [start, end) is the WORD (not the trigger), which is
174
- * what a client replaces when an item is picked. `hasSelection` suppresses it: a palette over
175
- * a selection is a different gesture from a palette at a caret.
176
- */
177
- export function triggerQueryAt(text, pos, trigger, { hasSelection = false } = {}) {
178
- if (hasSelection) return null;
179
- const v = String(text ?? '');
180
- const p = clamp(pos, 0, v.length);
181
- const line = v.slice(v.lastIndexOf('\n', p - 1) + 1, p);
182
- const t = escapeRe(String(trigger || '/'));
183
- const m = line.match(new RegExp(`(?:^|\\s)${t}(\\w*)$`));
184
- if (!m) return null;
185
- return { word: m[1], start: p - m[1].length, end: p };
186
- }
187
-
188
- /**
189
- * Filter the `/` palette by what has been typed after the slash: a prefix of the key or a
190
- * substring of the label, case-insensitively. An empty word keeps everything, in order.
191
- */
192
- export function filterNoteActions(items, word) {
193
- const w = String(word || '').trim().toLowerCase();
194
- const list = Array.isArray(items) ? items : [];
195
- if (!w) return list;
196
- return list.filter((it) => (
197
- String(it.key || '').toLowerCase().startsWith(w)
198
- || String(it.label || '').toLowerCase().includes(w)
199
- ));
200
- }
201
-
202
- /**
203
- * The palette items for the actions this build lists, in order — `{ key, label, hint }`
204
- * with no runner, so a client maps keys to its own handlers and drops any it cannot run.
205
- */
206
- export function noteActionItems(order = NOTE_ACTION_ORDER) {
207
- return order
208
- .filter((k) => NOTE_ACTIONS[k])
209
- .map((k) => ({ key: k, label: NOTE_ACTIONS[k].label, hint: NOTE_ACTIONS[k].hint }));
210
- }
211
-
212
- /** How much of the note rides along with a command — bounds the tokens, not the meaning. */
213
- export const NOTE_CONTEXT_MAX_CHARS = 4000;
214
-
215
- /**
216
- * The note itself (minus the command line) as grounding for an `@command` or `@[Agent]` task.
217
- *
218
- * "Summarize above", "action items for this meeting", "translate that" — the instruction
219
- * points at the document it sits in, and a model given only the instruction answers that
220
- * there is nothing to summarize. So the note travels with the task: everything before the
221
- * line (`head`) and after it (`tail`), the title when there is one, and a preface telling
222
- * the model to resolve references — a date, a [[wikilink]], a meeting link's #id — against
223
- * THIS text rather than free-searching history for a guess.
224
- *
225
- * Capped at `max` characters so a long note cannot blow the budget; the client's redaction
226
- * harness sees it like any other user turn. An empty note yields '' — there is nothing to
227
- * ground in, and a frame around nothing would only cost tokens.
228
- */
229
- export function noteCommandContext(head, tail, { title = '', max = NOTE_CONTEXT_MAX_CHARS } = {}) {
230
- const body = `${head ?? ''}\n${tail ?? ''}`.replace(/\n{3,}/g, '\n\n').trim();
231
- if (!body) return '';
232
- const clipped = body.length > max ? `${body.slice(0, max)}\n…(note truncated)` : body;
233
- const t = String(title || '').trim();
234
- return `The note I'm editing${t ? ` (title: "${t}")` : ''} is below. Resolve any reference in my instruction — "this meeting", "above", a date, a name, a [[wikilink]] or a URL (a meeting link's #id identifies that exact meeting) — against THIS note, not a guess. If a tool lets you fetch something referenced here by id, use that id.\n\n"""\n${clipped}\n"""`;
235
- }
236
-
237
- /**
238
- * The user turn for a command: the grounding (when there is any) and then the instruction,
239
- * separated so the model cannot mistake the note for the ask. One function, because the
240
- * separator is part of the contract — a client that joined them with a different rule
241
- * would get a different answer from the same note.
242
- */
243
- export function groundedInstruction(instruction, context) {
244
- const ask = String(instruction ?? '');
245
- return context ? `${context}\n\n---\nInstruction: ${ask}` : ask;
246
- }
247
-
248
- function clamp(n, lo, hi) {
249
- const x = Number.isFinite(n) ? n : lo;
250
- return Math.max(lo, Math.min(hi, x));
251
- }
252
- const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
20
+ ---
21
+ Instruction: ${r}`:r}function f(e,n,r){const s=Number.isFinite(e)?e:n;return Math.max(n,Math.min(r,s))}const g=e=>e.replace(/[.*+?^${}()|[\]\\/]/g,"\\$&");export{h as NOTE_ACTIONS,y as NOTE_ACTION_ERRORS,p as NOTE_ACTION_ORDER,w as NOTE_ACTION_TEMPERATURE,d as NOTE_COMMANDS,k as NOTE_COMMAND_MAX_TOKENS,T as NOTE_COMMAND_TEMPERATURE,O as NOTE_CONTEXT_MAX_CHARS,S as commandLineAt,z as filterNoteActions,N as frameNoteAction,A as groundedInstruction,_ as noteActionItems,x as noteActionLabel,v as noteCommandContext,E as triggerQueryAt};
package/note-graph.js CHANGED
@@ -1,158 +1 @@
1
- // The shape of a corpus: what links to what, and what it is all about.
2
- //
3
- // A note graph drawn from `[[wikilinks]]` alone is mostly dust — people link deliberately and
4
- // rarely, so the picture is a handful of pairs and a hundred lonely dots, which tells you
5
- // nothing you did not already know. The connective tissue is what notes are ABOUT: a topic
6
- // shared by two or more notes becomes a node of its own, and the notes hang off it.
7
- //
8
- // That is the whole idea, and it is pure: an index of `{ id, title, topics, tags, links }` in,
9
- // nodes and links out. No DOM, no layout, no physics — a client draws it with SVG, canvas, or
10
- // a native view, and all of them get the same graph. The extension's `graph-view.js` keeps the
11
- // drawing; this is the model underneath it.
12
- //
13
- // TOPICS ON A SINGLE NOTE ARE DROPPED. A topic that touches one note connects nothing, and
14
- // drawing it adds a labelled node with exactly one edge — clutter that makes the real clusters
15
- // harder to see. The threshold is the difference between a graph and a hairball.
16
-
17
- /** How many nodes are worth drawing before a view should start trimming. */
18
- export const MAX_GRAPH_NODES = 140;
19
-
20
- const lc = (s) => String(s || '').trim().toLowerCase();
21
-
22
- /**
23
- * Build the note graph.
24
- *
25
- * @param {Array} index `{ id, title, topics?, tags?, links? }` — the lightweight list, not
26
- * the bodies. `links` are wikilink TITLES, resolved here against the
27
- * index's own titles, so a link to a page that does not exist yet draws
28
- * nothing rather than a dangling node.
29
- * @param {object} [opts]
30
- * @param {boolean} [opts.topics=true] include topic hubs
31
- * @param {number} [opts.minShared=2] how many notes a topic needs before it earns a node
32
- * @returns {{nodes: Array, links: Array}}
33
- */
34
- export function buildNoteGraph(index, { topics = true, minShared = 2 } = {}) {
35
- const list = Array.isArray(index) ? index.filter(Boolean) : [];
36
- const nodes = list.map((n) => ({ id: n.id, label: n.title || 'Untitled note', type: 'note' }));
37
- const links = [];
38
- const seen = new Set();
39
- const addEdge = (s, t, kind) => {
40
- if (!s || !t || s === t) return;
41
- const key = s < t ? `${s}|${t}` : `${t}|${s}`;
42
- if (seen.has(key)) return;
43
- seen.add(key);
44
- links.push({ s, t, kind });
45
- };
46
-
47
- // Explicit wikilinks first — a deliberate connection outranks an inferred one, and adding
48
- // them first means the dedupe above can never drop one in favour of a topic edge.
49
- const byTitle = new Map(list.map((n) => [lc(n.title), n.id]));
50
- for (const n of list) {
51
- for (const name of n.links || []) {
52
- const target = byTitle.get(lc(name));
53
- if (target) addEdge(n.id, target, 'link');
54
- }
55
- }
56
-
57
- if (topics) {
58
- const hubs = new Map(); // lowercased topic → { label, ids[] }
59
- for (const n of list) {
60
- // Tags are the user's own topics and count the same: a corpus where somebody tags by
61
- // hand should not draw a sparser graph than one where a model extracted the topics.
62
- for (const raw of [...(n.topics || []), ...(n.tags || [])]) {
63
- const label = String(raw || '').trim();
64
- if (!label) continue;
65
- const key = lc(label);
66
- const e = hubs.get(key) || { label, ids: [] };
67
- if (!e.ids.includes(n.id)) e.ids.push(n.id);
68
- hubs.set(key, e);
69
- }
70
- }
71
- for (const [key, { label, ids }] of hubs) {
72
- if (ids.length < minShared) continue;
73
- const tid = `topic:${key}`;
74
- nodes.push({ id: tid, label, type: 'topic' });
75
- for (const id of ids) addEdge(id, tid, 'topic');
76
- }
77
- }
78
-
79
- return { nodes, links };
80
- }
81
-
82
- /**
83
- * The neighbourhood around one node — BFS out `hops`, keeping only links whose endpoints both
84
- * survive.
85
- *
86
- * A whole-corpus graph answers "what does my thinking look like"; this answers "what is near
87
- * THIS note", which is the question someone reading a note actually has. Topic hubs count as
88
- * a hop, so `hops: 2` reaches the sibling notes that share a topic with this one — which is
89
- * the entire point of having hubs at all.
90
- */
91
- export function egoGraph(full, id, hops = 2) {
92
- const nodes = full?.nodes || [];
93
- const links = full?.links || [];
94
- const byId = new Map(nodes.map((n) => [n.id, n]));
95
- if (!byId.has(id)) return { nodes: [], links: [] };
96
- const adj = new Map();
97
- for (const l of links) {
98
- if (!adj.has(l.s)) adj.set(l.s, new Set());
99
- if (!adj.has(l.t)) adj.set(l.t, new Set());
100
- adj.get(l.s).add(l.t);
101
- adj.get(l.t).add(l.s);
102
- }
103
- const inSet = new Set([id]);
104
- let frontier = new Set([id]);
105
- for (let h = 0; h < hops; h++) {
106
- const next = new Set();
107
- for (const cur of frontier) {
108
- for (const nb of adj.get(cur) || []) if (!inSet.has(nb)) { inSet.add(nb); next.add(nb); }
109
- }
110
- frontier = next;
111
- }
112
- return {
113
- nodes: [...inSet].map((nid) => {
114
- const m = byId.get(nid);
115
- return { id: nid, label: m?.label || 'Untitled note', type: m?.type || 'note', focus: nid === id };
116
- }),
117
- links: links.filter((l) => inSet.has(l.s) && inSet.has(l.t)),
118
- };
119
- }
120
-
121
- /**
122
- * Trim a graph to the most connected `max` nodes, keeping `keep` whatever its degree.
123
- *
124
- * A corpus of two thousand notes draws as a grey disc: every renderer slows to a crawl and
125
- * nothing is legible anyway. Degree is the right thing to cut on — a node nothing connects to
126
- * carries no information in a picture whose entire subject is connection — and the focused
127
- * node is exempt, because a view of "around this note" that dropped the note would be absurd.
128
- */
129
- export function trimGraph(graph, max = MAX_GRAPH_NODES, keep = '') {
130
- const nodes = graph?.nodes || [];
131
- const links = graph?.links || [];
132
- if (nodes.length <= max) return { nodes, links };
133
- const degree = new Map();
134
- for (const l of links) {
135
- degree.set(l.s, (degree.get(l.s) || 0) + 1);
136
- degree.set(l.t, (degree.get(l.t) || 0) + 1);
137
- }
138
- const ranked = [...nodes].sort((a, b) => {
139
- if (a.id === keep) return -1;
140
- if (b.id === keep) return 1;
141
- return (degree.get(b.id) || 0) - (degree.get(a.id) || 0);
142
- }).slice(0, max);
143
- const inSet = new Set(ranked.map((n) => n.id));
144
- return { nodes: ranked, links: links.filter((l) => inSet.has(l.s) && inSet.has(l.t)) };
145
- }
146
-
147
- /**
148
- * How many notes, links and topic hubs — the one-line summary a pane shows above the picture,
149
- * so a graph that looks empty says whether it IS empty or merely unconnected.
150
- */
151
- export function graphStats(graph) {
152
- const nodes = graph?.nodes || [];
153
- return {
154
- notes: nodes.filter((n) => n.type !== 'topic').length,
155
- topics: nodes.filter((n) => n.type === 'topic').length,
156
- links: (graph?.links || []).length,
157
- };
158
- }
1
+ const k=140,h=o=>String(o||"").trim().toLowerCase();function w(o,{topics:r=!0,minShared:f=2}={}){const l=Array.isArray(o)?o.filter(Boolean):[],p=l.map(t=>({id:t.id,label:t.title||"Untitled note",type:"note"})),c=[],s=new Set,d=(t,e,a)=>{if(!t||!e||t===e)return;const i=t<e?`${t}|${e}`:`${e}|${t}`;s.has(i)||(s.add(i),c.push({s:t,t:e,kind:a}))},n=new Map(l.map(t=>[h(t.title),t.id]));for(const t of l)for(const e of t.links||[]){const a=n.get(h(e));a&&d(t.id,a,"link")}if(r){const t=new Map;for(const e of l)for(const a of[...e.topics||[],...e.tags||[]]){const i=String(a||"").trim();if(!i)continue;const u=h(i),g=t.get(u)||{label:i,ids:[]};g.ids.includes(e.id)||g.ids.push(e.id),t.set(u,g)}for(const[e,{label:a,ids:i}]of t){if(i.length<f)continue;const u=`topic:${e}`;p.push({id:u,label:a,type:"topic"});for(const g of i)d(g,u,"topic")}}return{nodes:p,links:c}}function S(o,r,f=2){const l=o?.nodes||[],p=o?.links||[],c=new Map(l.map(t=>[t.id,t]));if(!c.has(r))return{nodes:[],links:[]};const s=new Map;for(const t of p)s.has(t.s)||s.set(t.s,new Set),s.has(t.t)||s.set(t.t,new Set),s.get(t.s).add(t.t),s.get(t.t).add(t.s);const d=new Set([r]);let n=new Set([r]);for(let t=0;t<f;t++){const e=new Set;for(const a of n)for(const i of s.get(a)||[])d.has(i)||(d.add(i),e.add(i));n=e}return{nodes:[...d].map(t=>{const e=c.get(t);return{id:t,label:e?.label||"Untitled note",type:e?.type||"note",focus:t===r}}),links:p.filter(t=>d.has(t.s)&&d.has(t.t))}}function y(o,r=140,f=""){const l=o?.nodes||[],p=o?.links||[];if(l.length<=r)return{nodes:l,links:p};const c=new Map;for(const n of p)c.set(n.s,(c.get(n.s)||0)+1),c.set(n.t,(c.get(n.t)||0)+1);const s=[...l].sort((n,t)=>n.id===f?-1:t.id===f?1:(c.get(t.id)||0)-(c.get(n.id)||0)).slice(0,r),d=new Set(s.map(n=>n.id));return{nodes:s,links:p.filter(n=>d.has(n.s)&&d.has(n.t))}}function b(o){const r=o?.nodes||[];return{notes:r.filter(f=>f.type!=="topic").length,topics:r.filter(f=>f.type==="topic").length,links:(o?.links||[]).length}}export{k as MAX_GRAPH_NODES,w as buildNoteGraph,S as egoGraph,b as graphStats,y as trimGraph};
package/note-links.js CHANGED
@@ -1,68 +1 @@
1
- // Linking one note to another: the `[[` picker's grammar.
2
- //
3
- // Split from `note-mentions.js` for a reason that is not taxonomy. The mention grammar is
4
- // asked a question by a KEYSTROKE handler — "is this line a delegation?" — which cannot await
5
- // a module, so it must already be loaded; the extension's Notes page therefore carries it on
6
- // its first-paint graph. Link picking happens inside an autocomplete popup, which is async by
7
- // nature and can load what it needs when the user types the second bracket.
8
- //
9
- // Keeping them in one file put both on that graph and pushed the extension's Notes first
10
- // paint 1.6 KB over its budget. The guard caught it, and this is the shape that answer takes:
11
- // a module is the unit of loading, so "needed synchronously" and "needed on demand" are not
12
- // allowed to share one.
13
-
14
- /**
15
- * The `[[…` link query under the caret, or null — `{ query, start, end }`, where [start, end)
16
- * is the text typed AFTER the opening brackets, which is what a pick replaces.
17
- *
18
- * Deliberately not a `triggerQueryAt` trigger: `[[` is two characters, the query may contain
19
- * spaces (page titles do), and it ends at a `]]` rather than at whitespace. A closed link is
20
- * not a query — once `]]` is typed the user is past it — and neither is one carrying a newline
21
- * or another bracket, which mean the `[[` above belongs to something else entirely.
22
- */
23
- export function wikiQueryAt(text, pos, { hasSelection = false } = {}) {
24
- if (hasSelection) return null;
25
- const v = String(text ?? '');
26
- const p = Math.max(0, Math.min(Number.isFinite(pos) ? pos : 0, v.length));
27
- const upto = v.slice(0, p);
28
- const open = upto.lastIndexOf('[[');
29
- if (open < 0) return null;
30
- const between = upto.slice(open + 2);
31
- if (/[[\]\n]/.test(between)) return null;
32
- return { query: between, start: open + 2, end: p };
33
- }
34
-
35
- /**
36
- * Rank link targets for what has been typed.
37
- *
38
- * Prefix matches first — someone typing "Roll" means a page whose name STARTS that way, and
39
- * burying it under a note that mentions "rollback" in the middle of its title is the ranking
40
- * being clever at the user's expense. Then substring, each group alphabetical so the list is
41
- * stable between keystrokes.
42
- *
43
- * DEDUPED BY TITLE, because a `[[link]]` addresses a title and nothing else: two records
44
- * called "Atlas" are one destination, and offering the name twice asks the user to choose
45
- * between two things they cannot tell apart. The first of a title wins, which — given the
46
- * caller passes its index in its own order — is the one it considers canonical.
47
- */
48
- export function rankLinkTargets(targets, query, limit = 8) {
49
- const q = String(query || '').trim().toLowerCase();
50
- const byTitleSeen = new Set();
51
- const list = (Array.isArray(targets) ? targets : []).filter((t) => {
52
- if (!t || !t.title) return false;
53
- const key = String(t.title).trim().toLowerCase();
54
- if (byTitleSeen.has(key)) return false;
55
- byTitleSeen.add(key);
56
- return true;
57
- });
58
- const byTitle = (a, b) => String(a.title).localeCompare(String(b.title));
59
- if (!q) return [...list].sort(byTitle).slice(0, limit);
60
- const starts = [];
61
- const has = [];
62
- for (const t of list) {
63
- const title = String(t.title).toLowerCase();
64
- if (title.startsWith(q)) starts.push(t);
65
- else if (title.includes(q)) has.push(t);
66
- }
67
- return [...starts.sort(byTitle), ...has.sort(byTitle)].slice(0, limit);
68
- }
1
+ function f(l,c,{hasSelection:u=!1}={}){if(u)return null;const r=String(l??""),s=Math.max(0,Math.min(Number.isFinite(c)?c:0,r.length)),i=r.slice(0,s),e=i.lastIndexOf("[[");if(e<0)return null;const o=i.slice(e+2);return/[[\]\n]/.test(o)?null:{query:o,start:e+2,end:s}}function h(l,c,u=8){const r=String(c||"").trim().toLowerCase(),s=new Set,i=(Array.isArray(l)?l:[]).filter(t=>{if(!t||!t.title)return!1;const n=String(t.title).trim().toLowerCase();return s.has(n)?!1:(s.add(n),!0)}),e=(t,n)=>String(t.title).localeCompare(String(n.title));if(!r)return[...i].sort(e).slice(0,u);const o=[],a=[];for(const t of i){const n=String(t.title).toLowerCase();n.startsWith(r)?o.push(t):n.includes(r)&&a.push(t)}return[...o.sort(e),...a.sort(e)].slice(0,u)}export{h as rankLinkTargets,f as wikiQueryAt};
package/note-mentions.js CHANGED
@@ -1,117 +1,11 @@
1
- // Delegation inside a note: `@[Agent Name] task` and `#[Skill Name]`.
2
- //
3
- // A note is not only a document a model writes INTO it is a place to hand a job to a named
4
- // agent and scope it to a saved skill. Both are written as tokens in the text, which means
5
- // both are grammar: something has to say where the token ends and the instruction begins, and
6
- // two clients answering that differently is one feature behaving as two.
7
- //
8
- // These were `notes-util.js` in the extension — already pure, already tested, and already the
9
- // right shape; they simply lived where only one client could reach them. The desktop needs
10
- // exactly the same answers, and a mobile client will too.
11
- //
12
- // WHAT IS NOT HERE: running the agent, arming its tools, and streaming into the editor. Those
13
- // need a target resolver, a transport and a document — all platform. What is here is the
14
- // reading of the line and the resolving of a name against a list the caller supplies.
1
+ function a(i){const e=String(i||""),n=e.match(/@\[([^\]\n]+)\]/);if(!n)return{name:"",task:""};const t=n[1].trim(),s=`${e.slice(0,n.index)} ${e.slice(n.index+n[0].length)}`.replace(/\s+/g," ").trim();return{name:t,task:s}}function l(i,e){const n=String(i??""),t=Math.max(0,Math.min(Number.isFinite(e)?e:0,n.length)),s=n.lastIndexOf(`
2
+ `,t-1)+1;let o=n.indexOf(`
3
+ `,t);o<0&&(o=n.length);const{name:r,task:c}=a(n.slice(s,o));return!r||!c?null:{name:r,task:c,start:s,end:o}}function u(i){const e=String(i||""),n=e.match(/#\[([^\]\n]+)\]/);return n?{name:n[1].trim(),text:e.replace(n[0],"").replace(/[ \t]{2,}/g," ").trim()}:{name:"",text:e.trim()}}function m(i,e){const n=String(i||""),t=String(e||"");return/\{\{\s*input[^}]*\}\}/i.test(n)?n.replace(/\{\{\s*input[^}]*\}\}/gi,t):n?t?`${n}
15
4
 
16
- /**
17
- * Pull an `@[Agent Name] task` mention out of one line.
18
- *
19
- * The instruction may sit BEFORE or AFTER the token — "Update the plan @[Codex]" and
20
- * "@[Codex] update the plan" are the same request — so the task is the whole line minus the
21
- * token. Brackets are what let an agent's name contain spaces.
22
- *
23
- * Returns `{ name, task }`, both `''` when the line carries no runnable mention. A mention
24
- * with no task is not runnable: `@[Codex]` alone is someone still typing.
25
- */
26
- export function parseAgentMention(line) {
27
- const s = String(line || '');
28
- const m = s.match(/@\[([^\]\n]+)\]/);
29
- if (!m) return { name: '', task: '' };
30
- const name = m[1].trim();
31
- const task = (`${s.slice(0, m.index)} ${s.slice(m.index + m[0].length)}`).replace(/\s+/g, ' ').trim();
32
- return { name, task };
33
- }
5
+ ${t}`:n:t}function f(i,e){const n=String(e||"").trim().toLowerCase();if(!n)return null;const t=Array.isArray(i)?i:[],s=o=>String(o?.name||o?.title||"").toLowerCase();return t.find(o=>s(o)===n)||t.find(o=>s(o).includes(n))||null}function g(i,e){const n=String(e||"").trim().toLowerCase();if(!n)return null;const t=Array.isArray(i)?i:[],s=r=>String(r?.name||"").toLowerCase(),o=r=>String(r?.model||r?.bridgeAgent||r?.id||"").toLowerCase();return t.find(r=>s(r)===n)||t.find(r=>o(r)===n)||t.find(r=>s(r)&&s(r).includes(n))||t.find(r=>o(r).includes(n))||null}function d(i,e){return`${String(e||"").split(`
6
+ `).map(t=>`> ${t}`).join(`
7
+ `)}
34
8
 
35
- /**
36
- * The runnable `@[Agent] task` on the caret's line, or null — `{ name, task, start, end }`
37
- * where [start, end) is the WHOLE line, because the Q&A replaces the line it was written on.
38
- */
39
- export function agentMentionAt(text, pos) {
40
- const v = String(text ?? '');
41
- const p = Math.max(0, Math.min(Number.isFinite(pos) ? pos : 0, v.length));
42
- const start = v.lastIndexOf('\n', p - 1) + 1;
43
- let end = v.indexOf('\n', p);
44
- if (end < 0) end = v.length;
45
- const { name, task } = parseAgentMention(v.slice(start, end));
46
- if (!name || !task) return null;
47
- return { name, task, start, end };
48
- }
9
+ **${String(i||"Agent")}:**
49
10
 
50
- /**
51
- * Pull a `#[Skill Name]` out of an instruction. Returns the name (or `''`) and the
52
- * instruction with the token removed — the token is addressing, not content, and leaving it
53
- * in means the model is asked to do something with the literal text "#[Weekly review]".
54
- */
55
- export function parseSkillMention(instruction) {
56
- const s = String(instruction || '');
57
- const m = s.match(/#\[([^\]\n]+)\]/);
58
- if (!m) return { name: '', text: s.trim() };
59
- return { name: m[1].trim(), text: s.replace(m[0], '').replace(/[ \t]{2,}/g, ' ').trim() };
60
- }
61
-
62
- /**
63
- * Merge a skill's saved prompt with the user's task.
64
- *
65
- * `{{input}}` placeholders are substituted when the author wrote any — a skill that says
66
- * "Summarize {{input}} for an exec" wants the task in the middle, not stapled underneath.
67
- * With no placeholder the task is appended, which is what an instruction-style skill expects.
68
- */
69
- export function mergeSkillPrompt(prompt, task) {
70
- const p = String(prompt || '');
71
- const t = String(task || '');
72
- if (/\{\{\s*input[^}]*\}\}/i.test(p)) return p.replace(/\{\{\s*input[^}]*\}\}/gi, t);
73
- return p ? (t ? `${p}\n\n${t}` : p) : t;
74
- }
75
-
76
- /** Resolve a skill by display name — exact first, then contains. */
77
- export function findSkillByName(skills, name) {
78
- const q = String(name || '').trim().toLowerCase();
79
- if (!q) return null;
80
- const list = Array.isArray(skills) ? skills : [];
81
- const of = (s) => String(s?.name || s?.title || '').toLowerCase();
82
- return list.find((s) => of(s) === q) || list.find((s) => of(s).includes(q)) || null;
83
- }
84
-
85
- /**
86
- * Resolve a mentioned TARGET by name against whatever list the client has.
87
- *
88
- * Exact name, then a contains match, then the underlying model or agent id — so `@[Claude]`
89
- * finds "Claude Code", and `@[codex]` finds a target named something else that runs `codex`.
90
- * The entries are duck-typed (`name`, `model`, `bridgeAgent`, or a bare `id`) because the
91
- * extension holds configured endpoints and agents while the desktop holds the gateway's model
92
- * list, and neither should have to reshape its data to ask this question.
93
- */
94
- export function findTargetByName(targets, name) {
95
- const q = String(name || '').trim().toLowerCase();
96
- if (!q) return null;
97
- const list = Array.isArray(targets) ? targets : [];
98
- const nameOf = (t) => String(t?.name || '').toLowerCase();
99
- const idOf = (t) => String(t?.model || t?.bridgeAgent || t?.id || '').toLowerCase();
100
- return list.find((t) => nameOf(t) === q)
101
- || list.find((t) => idOf(t) === q)
102
- || list.find((t) => nameOf(t) && nameOf(t).includes(q))
103
- || list.find((t) => idOf(t).includes(q))
104
- || null;
105
- }
106
-
107
- /**
108
- * How a delegated answer is written into the note.
109
- *
110
- * The question is quoted and the agent named above its reply, so a note that collected three
111
- * delegations reads as a transcript rather than as three anonymous blocks of prose. Returned
112
- * separately from the answer because the client streams INTO the gap between them.
113
- */
114
- export function mentionAnswerPrefix(name, task) {
115
- const quoted = String(task || '').split('\n').map((l) => `> ${l}`).join('\n');
116
- return `${quoted}\n\n**${String(name || 'Agent')}:**\n\n`;
117
- }
11
+ `}export{l as agentMentionAt,f as findSkillByName,g as findTargetByName,d as mentionAnswerPrefix,m as mergeSkillPrompt,a as parseAgentMention,u as parseSkillMention};