@chatpanel/events 0.70.0 → 0.71.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/index.js CHANGED
@@ -201,7 +201,8 @@ export {
201
201
  } from './memory.js';
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
- export { SKILL_VARS, SKILL_VAR_NAMES, skillVar, skillVarPattern, parseSkillVars, lintSkillPrompt, suggestSkillVar, substituteSkillVars, skillVarGuidance, SkillVarError } from './skill-vars.js';
204
+ export { SKILL_VARS, SKILL_VAR_NAMES, skillVar, skillVarPattern, parseSkillVars, lintSkillPrompt, suggestSkillVar, substituteSkillVars, expandSkillPrompt, skillVarGuidance, SkillVarError } from './skill-vars.js';
205
+ export { SLASH_TYPING_RE, enabledSkills, slashCommandItems, matchSlashSkill, matchSlashRecipe, recipeInvocationText, slashCommandInsert, skillInvocationOf, skillInvocationLabel } from './slash-commands.js';
205
206
  export { outlineOf, parseListItem, continueList, indentSelection, toggleWrap, toggleLinePrefix, toggleTask, toggleLink, docStats, selectionStats } from './markdown-authoring.js';
206
207
  export {
207
208
  MAX_TAG_LENGTH, MAX_TAGS, normalizeTag, normalizeTags, hasTag, addTag, removeTag, toggleTag,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chatpanel/events",
3
- "version": "0.70.0",
3
+ "version": "0.71.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",
@@ -67,6 +67,7 @@
67
67
  "./skill-scan.js": "./skill-scan.js",
68
68
  "./skill-sources.js": "./skill-sources.js",
69
69
  "./skill-vars.js": "./skill-vars.js",
70
+ "./slash-commands.js": "./slash-commands.js",
70
71
  "./sources-retrieval.js": "./sources-retrieval.js",
71
72
  "./sources.js": "./sources.js",
72
73
  "./store.js": "./store.js",
@@ -179,6 +180,7 @@
179
180
  "skill-scan.js",
180
181
  "skill-sources.js",
181
182
  "skill-vars.js",
183
+ "slash-commands.js",
182
184
  "sources-retrieval.js",
183
185
  "sources.js",
184
186
  "store.js",
package/skill-vars.js CHANGED
@@ -224,6 +224,22 @@ export async function substituteSkillVars(text, { args = '', resolvers = {} } =
224
224
  return { text: out, filled, empty, unknown: lint.unknown };
225
225
  }
226
226
 
227
+ /**
228
+ * The prompt a `/command args` sends.
229
+ *
230
+ * The typed args fill `{{input}}` when the author left a slot for them, and are appended
231
+ * after the prompt when not — never both, which is how "/fix this sentence" once landed
232
+ * in the prompt twice. Everything else `substituteSkillVars` reports (an empty selection,
233
+ * an invented placeholder) comes back untouched, so the caller can say so out loud.
234
+ */
235
+ export async function expandSkillPrompt(prompt, { args = '', resolvers = {} } = {}) {
236
+ const src = String(prompt || '');
237
+ const a = String(args || '').trim();
238
+ const inline = lintSkillPrompt(src).hasInput;
239
+ const body = src + (!inline && a ? `\n\n${a}` : '');
240
+ return substituteSkillVars(body, { args: a, resolvers });
241
+ }
242
+
227
243
  /**
228
244
  * The sentence prompt-assist needs so the model stops inventing placeholders.
229
245
  * Generated from SKILL_VARS so a variable added here reaches the assist prompt
@@ -0,0 +1,133 @@
1
+ // slash-commands.js — the `/command` grammar of a chat composer, declared ONCE.
2
+ //
3
+ // A skill, a saved recipe and a client's built-in command all answer to a slash. Which
4
+ // ones exist, how a half-typed prefix is matched, what a chosen item inserts, and how a
5
+ // sent run is LABELLED all lived in the extension's side panel, and the desktop had to copy
6
+ // them the day its composer wanted `/summarize` too. This module is that copy, made the
7
+ // original: a third client (mobile, a channel bot) inherits the grammar instead of
8
+ // re-deriving it.
9
+ //
10
+ // What is NOT here is deliberate:
11
+ // • the BUILT-IN list — `/search`, `/history`, `/monitor` — is what a client can DO,
12
+ // and a desktop with no live meeting has no `/tldr`. Each client passes its own.
13
+ // • the prompt expansion — `expandSkillPrompt` in skill-vars.js, so a composer that only
14
+ // needs the menu never pays for the variable layer.
15
+ // • the entitlement — `skillsAllowed` is decided by the caller against its licence.
16
+
17
+ function normalizePrefix(prefix) {
18
+ return String(prefix || '')
19
+ .toLowerCase()
20
+ .replace(/^\//, '')
21
+ .replace(/\s+/g, ' ');
22
+ }
23
+
24
+ function skillItem(skill) {
25
+ return {
26
+ type: 'skill',
27
+ command: skill.command || '',
28
+ icon: skill.icon || '🎓',
29
+ description: skill.description || skill.name || '',
30
+ skill,
31
+ };
32
+ }
33
+
34
+ // A saved recipe is a /command too. It is not a skill: nothing is expanded into a prompt.
35
+ // The line becomes a plain request to run it, and the `recipe` tool does the rest.
36
+ function recipeItem(recipe) {
37
+ return { type: 'recipe', command: recipe.name || '', icon: '🧩', description: recipe.description || 'Saved recipe', recipe };
38
+ }
39
+
40
+ /** Skills that are switched on. Absence of the flag means enabled (older records have none). */
41
+ export function enabledSkills(skills) {
42
+ return (Array.isArray(skills) ? skills : []).filter((s) => !!s && s.enabled !== false);
43
+ }
44
+
45
+ /**
46
+ * The items a composer offers for what has been typed so far.
47
+ *
48
+ * @param builtins the client's own commands — `{ command, icon, description, feature? }`.
49
+ * One with a `feature` is shown LOCKED when `features[feature]` is
50
+ * false: a Free user should still discover what Pro unlocks.
51
+ * @param skills the user's skills; only enabled ones with a command are offered
52
+ * @param recipes saved recipes; enabled ones by name
53
+ * @param prefix what follows the slash, possibly with a subcommand ("history m")
54
+ * @param skillsAllowed whether skills are offered at all (Pro on the extension)
55
+ * @param features `{ liveMeetings: true }` — what the licence unlocks
56
+ */
57
+ export function slashCommandItems({
58
+ builtins = [],
59
+ skills = [],
60
+ recipes = [],
61
+ prefix = '',
62
+ skillsAllowed = false,
63
+ features = {},
64
+ } = {}) {
65
+ const normalized = normalizePrefix(prefix);
66
+ const own = (Array.isArray(builtins) ? builtins : []).map((item) => ({
67
+ type: 'builtin',
68
+ ...item,
69
+ locked: !!item.feature && !features[item.feature],
70
+ }));
71
+ const skillItems = skillsAllowed ? enabledSkills(skills).map(skillItem) : [];
72
+ const recipeItems = (recipes || []).filter((r) => r && r.enabled !== false && r.name).map(recipeItem);
73
+ return [...own, ...skillItems, ...recipeItems]
74
+ .filter((item) => item.command && item.command.toLowerCase().startsWith(normalized))
75
+ .slice(0, 12);
76
+ }
77
+
78
+ /** Is the composer's text a slash command still being typed — the moment to show the menu? */
79
+ export const SLASH_TYPING_RE = /^\/([a-z0-9_-]*(?:\s+[a-z0-9_-]*)?)$/i;
80
+
81
+ /** "/summarize the thread" → the enabled skill whose command is `summarize`, and the rest. */
82
+ export function matchSlashSkill(text, skills = []) {
83
+ const m = /^\/([a-z0-9_-]+)\s*([\s\S]*)$/i.exec(String(text || ''));
84
+ if (!m) return null;
85
+ const skill = enabledSkills(skills).find((s) => String(s.command || '').toLowerCase() === m[1].toLowerCase());
86
+ return skill ? { skill, args: m[2].trim() } : null;
87
+ }
88
+
89
+ /** "/open_bug Crash on start" → the recipe, and the rest of the line as its input. */
90
+ export function matchSlashRecipe(text, recipes = []) {
91
+ const m = /^\/([a-z0-9_-]+)\s*([\s\S]*)$/i.exec(String(text || ''));
92
+ if (!m) return null;
93
+ const recipe = (recipes || []).find((r) => r && r.enabled !== false && String(r.name || '').toLowerCase() === m[1].toLowerCase());
94
+ return recipe ? { recipe, args: m[2].trim() } : null;
95
+ }
96
+
97
+ /** What the model receives for a recipe command: a request, not a prompt expansion. */
98
+ export function recipeInvocationText(recipe, args = '') {
99
+ const a = String(args || '').trim();
100
+ return `Run the saved recipe "${recipe.name}"${a ? ` with this input: ${a}` : ''}. Use the recipe tool; if a parameter is missing, ask for it.`;
101
+ }
102
+
103
+ export function slashCommandInsert(item) {
104
+ return item?.command ? `/${item.command} ` : '/';
105
+ }
106
+
107
+ /**
108
+ * How a skill invocation should READ once it has been sent.
109
+ *
110
+ * The model must receive the whole prompt — that is what a skill IS. The user's own bubble
111
+ * echoing it back is a separate question with a different answer: a screen of instructions
112
+ * they wrote themselves buries the thread the skill was asked to read. So the send keeps
113
+ * the expansion as the message CONTENT (model, exports and memory capture see exactly what
114
+ * they always saw) and carries this beside it, for display only.
115
+ *
116
+ * Null for anything that is not a command, so callers attach it unconditionally and older
117
+ * messages — which have none — keep rendering as they did.
118
+ */
119
+ export function skillInvocationOf(skill, args = '') {
120
+ if (!skill?.command) return null;
121
+ return {
122
+ command: skill.command,
123
+ args: String(args || '').trim(),
124
+ name: skill.name || '',
125
+ icon: skill.icon || '🎓',
126
+ };
127
+ }
128
+
129
+ /** The one line an invocation is worth: `/command args`, exactly as typed. */
130
+ export function skillInvocationLabel(inv) {
131
+ if (!inv?.command) return '';
132
+ return `/${inv.command}${inv.args ? ` ${inv.args}` : ''}`;
133
+ }