@bubblebrain-ai/bubble 0.0.11 → 0.0.12
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/dist/agent.js +1 -2
- package/dist/feishu/agent-host/run-driver.js +13 -6
- package/dist/feishu/agent-host/runtime-deps.d.ts +2 -2
- package/dist/feishu/router/commands.js +2 -1
- package/dist/feishu/scope/session-binder.js +1 -1
- package/dist/feishu/serve.js +3 -3
- package/dist/main.js +20 -3
- package/dist/prompt/compose.js +3 -3
- package/dist/prompt/environment.js +2 -0
- package/dist/prompt/reminders.js +1 -1
- package/dist/provider-openai-codex.d.ts +8 -1
- package/dist/provider-openai-codex.js +33 -9
- package/dist/provider.d.ts +2 -0
- package/dist/session-title.d.ts +16 -0
- package/dist/session-title.js +134 -0
- package/dist/session-types.d.ts +5 -0
- package/dist/session.d.ts +5 -0
- package/dist/session.js +75 -9
- package/dist/skills/invocation.js +0 -18
- package/dist/skills/registry.d.ts +1 -0
- package/dist/skills/registry.js +2 -0
- package/dist/slash-commands/commands.js +2 -22
- package/dist/slash-commands/registry.js +1 -1
- package/dist/text-display.d.ts +3 -0
- package/dist/text-display.js +25 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +3 -1
- package/dist/tools/skill-search.d.ts +10 -0
- package/dist/tools/skill-search.js +134 -0
- package/dist/tools/skill.js +1 -4
- package/dist/tui-ink/app.js +54 -65
- package/dist/tui-ink/input-box.d.ts +22 -1
- package/dist/tui-ink/input-box.js +105 -11
- package/dist/tui-ink/message-list.js +3 -2
- package/dist/tui-ink/model-picker.d.ts +18 -0
- package/dist/tui-ink/model-picker.js +80 -23
- package/dist/tui-ink/session-picker.js +5 -7
- package/dist/tui-ink/theme.js +2 -2
- package/package.json +1 -1
package/dist/session.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Session Manager - Append-only JSONL persistence over a structured session log.
|
|
3
3
|
*/
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
4
5
|
import { mkdirSync, appendFileSync, existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
5
6
|
import { basename, dirname, join } from "node:path";
|
|
6
7
|
import { getBubbleHome } from "./bubble-home.js";
|
|
7
8
|
import { compactSessionEntries } from "./context/compact.js";
|
|
8
9
|
import { SessionLog } from "./session-log.js";
|
|
10
|
+
import { normalizeSingleLine, truncateVisual } from "./text-display.js";
|
|
11
|
+
import { deterministicTitleFromUserContent } from "./session-title.js";
|
|
9
12
|
const AUTO_COMPACT_ENTRY_THRESHOLD = 180;
|
|
10
13
|
const AUTO_COMPACT_KEEP_RECENT_TURNS = 3;
|
|
11
14
|
export class SessionManager {
|
|
@@ -107,10 +110,28 @@ export class SessionManager {
|
|
|
107
110
|
getMetadata() {
|
|
108
111
|
return this.log.getMetadata();
|
|
109
112
|
}
|
|
113
|
+
getOrCreatePromptCacheKey() {
|
|
114
|
+
const existing = this.log.getMetadata().promptCacheKey;
|
|
115
|
+
if (existing)
|
|
116
|
+
return existing;
|
|
117
|
+
const promptCacheKey = randomUUID();
|
|
118
|
+
this.updateMetadata({ promptCacheKey });
|
|
119
|
+
return promptCacheKey;
|
|
120
|
+
}
|
|
110
121
|
setMetadata(metadata) {
|
|
111
122
|
const nextEntries = this.log.setMetadata(metadata);
|
|
112
123
|
this.rewrite(nextEntries);
|
|
113
124
|
}
|
|
125
|
+
updateMetadata(patch) {
|
|
126
|
+
this.setMetadata({
|
|
127
|
+
...this.log.getMetadata(),
|
|
128
|
+
...dropUndefined(patch),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
clearTitleMetadata() {
|
|
132
|
+
const { title: _title, titleSource: _titleSource, titleUpdatedAt: _titleUpdatedAt, titleUserMessageId: _titleUserMessageId, ...metadata } = this.log.getMetadata();
|
|
133
|
+
this.setMetadata(metadata);
|
|
134
|
+
}
|
|
114
135
|
appendMessage(message) {
|
|
115
136
|
const entries = this.log.appendMessage(message);
|
|
116
137
|
this.persist(entries);
|
|
@@ -192,21 +213,23 @@ function summarizeSessionFile(file, cwdDir) {
|
|
|
192
213
|
const log = new SessionLog();
|
|
193
214
|
log.load(lines);
|
|
194
215
|
const metadata = log.getMetadata();
|
|
216
|
+
const entries = log.list();
|
|
195
217
|
const messages = log.toMessages();
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
firstUserText
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const snippet = firstUserText.trim().replace(/\s+/g, " ").slice(0, 80);
|
|
218
|
+
const firstUserEntry = firstUserEntryAfterLatestClear(entries);
|
|
219
|
+
const firstUserText = firstUserEntry ? messageText(firstUserEntry.message) : "";
|
|
220
|
+
const preview = firstUserText
|
|
221
|
+
? sessionPreviewFromText(firstUserText)
|
|
222
|
+
: (messages.length > 0 ? "No user message" : "No messages");
|
|
223
|
+
const title = usableStoredTitle(metadata, entries)
|
|
224
|
+
?? (firstUserEntry ? deterministicTitleFromUserContent(firstUserEntry.message.content) : (messages.length > 0 ? "Assistant-only session" : "Empty session"));
|
|
204
225
|
return {
|
|
205
226
|
file,
|
|
206
227
|
name: basename(file).replace(/\.jsonl$/, ""),
|
|
207
228
|
cwd: metadata.cwd,
|
|
208
229
|
cwdLabel: metadata.cwd ?? decodeCwdDir(cwdDir),
|
|
209
|
-
|
|
230
|
+
title,
|
|
231
|
+
preview,
|
|
232
|
+
firstUserMessage: preview,
|
|
210
233
|
messageCount: messages.length,
|
|
211
234
|
mtime: stat.mtimeMs,
|
|
212
235
|
};
|
|
@@ -219,3 +242,46 @@ function decodeCwdDir(safe) {
|
|
|
219
242
|
return "/" + safe.slice(1).replace(/_/g, "/");
|
|
220
243
|
return safe.replace(/_/g, "/");
|
|
221
244
|
}
|
|
245
|
+
function dropUndefined(value) {
|
|
246
|
+
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
|
|
247
|
+
}
|
|
248
|
+
function firstUserEntryAfterLatestClear(entries) {
|
|
249
|
+
const startIndex = latestClearIndex(entries) + 1;
|
|
250
|
+
for (let i = startIndex; i < entries.length; i++) {
|
|
251
|
+
const entry = entries[i];
|
|
252
|
+
if (entry.type === "user_message")
|
|
253
|
+
return entry;
|
|
254
|
+
}
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
function latestClearIndex(entries) {
|
|
258
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
259
|
+
const entry = entries[i];
|
|
260
|
+
if (entry.type === "marker" && entry.kind === "conversation_clear")
|
|
261
|
+
return i;
|
|
262
|
+
}
|
|
263
|
+
return -1;
|
|
264
|
+
}
|
|
265
|
+
function usableStoredTitle(metadata, entries) {
|
|
266
|
+
const title = normalizeSingleLine(metadata.title ?? "");
|
|
267
|
+
if (!title)
|
|
268
|
+
return undefined;
|
|
269
|
+
if (!metadata.titleUserMessageId)
|
|
270
|
+
return title;
|
|
271
|
+
const anchorIndex = entries.findIndex((entry) => entry.id === metadata.titleUserMessageId);
|
|
272
|
+
if (anchorIndex < 0)
|
|
273
|
+
return undefined;
|
|
274
|
+
if (anchorIndex <= latestClearIndex(entries))
|
|
275
|
+
return undefined;
|
|
276
|
+
return title;
|
|
277
|
+
}
|
|
278
|
+
function messageText(message) {
|
|
279
|
+
if (message.role !== "user")
|
|
280
|
+
return "";
|
|
281
|
+
if (typeof message.content === "string")
|
|
282
|
+
return message.content;
|
|
283
|
+
return message.content.map((part) => part.type === "text" ? part.text : "").join("\n");
|
|
284
|
+
}
|
|
285
|
+
function sessionPreviewFromText(text) {
|
|
286
|
+
return truncateVisual(normalizeSingleLine(text), 100) || "No user message";
|
|
287
|
+
}
|
|
@@ -5,24 +5,6 @@ export function parseSkillInvocation(input, registry) {
|
|
|
5
5
|
const withoutSlash = trimmed.slice(1).trim();
|
|
6
6
|
if (!withoutSlash)
|
|
7
7
|
return undefined;
|
|
8
|
-
if (withoutSlash.startsWith("skill ")) {
|
|
9
|
-
const rest = withoutSlash.slice("skill ".length).trim();
|
|
10
|
-
const firstSpace = rest.indexOf(" ");
|
|
11
|
-
if (firstSpace === -1)
|
|
12
|
-
return undefined;
|
|
13
|
-
const skillName = rest.slice(0, firstSpace).trim();
|
|
14
|
-
const task = rest.slice(firstSpace + 1).trim();
|
|
15
|
-
if (!skillName || !task)
|
|
16
|
-
return undefined;
|
|
17
|
-
const skill = registry.get(skillName);
|
|
18
|
-
if (!skill)
|
|
19
|
-
return undefined;
|
|
20
|
-
return {
|
|
21
|
-
skill,
|
|
22
|
-
task,
|
|
23
|
-
actualPrompt: buildSkillExecutionPrompt(skill, task),
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
8
|
const firstSpace = withoutSlash.indexOf(" ");
|
|
27
9
|
if (firstSpace === -1)
|
|
28
10
|
return undefined;
|
package/dist/skills/registry.js
CHANGED
|
@@ -9,9 +9,11 @@ export class SkillRegistry {
|
|
|
9
9
|
const cwd = options.cwd ?? process.cwd();
|
|
10
10
|
const bubbleHome = options.bubbleHome ?? getBubbleHome();
|
|
11
11
|
const agentsHome = options.agentsHome ?? join(homedir(), ".agents");
|
|
12
|
+
const claudeHome = options.claudeHome ?? join(homedir(), ".claude");
|
|
12
13
|
const roots = [
|
|
13
14
|
{ path: join(bubbleHome, "skills"), source: "user" },
|
|
14
15
|
{ path: join(agentsHome, "skills"), source: "user" },
|
|
16
|
+
{ path: join(claudeHome, "skills"), source: "user" },
|
|
15
17
|
{ path: join(cwd, ".bubble", "skills"), source: "project" },
|
|
16
18
|
...(options.skillPaths ?? []).map((path) => ({ path, source: "configured" })),
|
|
17
19
|
];
|
|
@@ -6,7 +6,6 @@ import { parseRule } from "../permissions/rule.js";
|
|
|
6
6
|
import { encodeModel, decodeModel, displayModel, BUILTIN_PROVIDERS, isUserVisibleProvider } from "../provider-registry.js";
|
|
7
7
|
import { getAvailableThinkingLevels, normalizeThinkingLevel } from "../provider-transform.js";
|
|
8
8
|
import { buildSystemPrompt } from "../system-prompt.js";
|
|
9
|
-
import { formatLoadedSkill } from "../tools/skill.js";
|
|
10
9
|
import { isThinkingLevel } from "../variant/thinking-level.js";
|
|
11
10
|
import { buildMemoryPrompt, getMemoryStatus, isMemoryDisabled, resetMemory, searchMemory, } from "../memory/index.js";
|
|
12
11
|
import { feishuCommand } from "./feishu.js";
|
|
@@ -55,7 +54,7 @@ function persistSelectedModel(model, ctx) {
|
|
|
55
54
|
userConfig.setDefaultThinkingLevel(ctx.agent.thinking);
|
|
56
55
|
userConfig.pushRecentModel(model);
|
|
57
56
|
if (ctx.sessionManager) {
|
|
58
|
-
ctx.sessionManager.
|
|
57
|
+
ctx.sessionManager.updateMetadata({ model, thinkingLevel: ctx.agent.thinking, reasoningEffort: ctx.agent.thinking });
|
|
59
58
|
ctx.sessionManager.appendMarker("model_switch", model);
|
|
60
59
|
}
|
|
61
60
|
}
|
|
@@ -68,7 +67,6 @@ function syncSystemPrompt(ctx, model) {
|
|
|
68
67
|
configuredModelId: model,
|
|
69
68
|
thinkingLevel: ctx.agent.thinking,
|
|
70
69
|
workingDir: ctx.cwd,
|
|
71
|
-
skills: ctx.skillRegistry.summaries(),
|
|
72
70
|
memoryPrompt: buildMemoryPrompt(ctx.cwd),
|
|
73
71
|
}));
|
|
74
72
|
}
|
|
@@ -257,25 +255,6 @@ const builtinSlashCommandEntries = [
|
|
|
257
255
|
ctx.openPicker("skill");
|
|
258
256
|
},
|
|
259
257
|
},
|
|
260
|
-
{
|
|
261
|
-
name: "skill",
|
|
262
|
-
description: "Load a skill explicitly. Usage: /skill <name>",
|
|
263
|
-
async handler(args, ctx) {
|
|
264
|
-
const name = args.trim();
|
|
265
|
-
if (!name) {
|
|
266
|
-
return "Usage: /skill <name>";
|
|
267
|
-
}
|
|
268
|
-
const skill = ctx.skillRegistry.get(name);
|
|
269
|
-
if (!skill) {
|
|
270
|
-
const available = ctx.skillRegistry.summaries().map((item) => item.name).join(", ");
|
|
271
|
-
return available
|
|
272
|
-
? `Unknown skill "${name}". Available skills: ${available}`
|
|
273
|
-
: `Unknown skill "${name}". No skills are currently available.`;
|
|
274
|
-
}
|
|
275
|
-
ctx.sessionManager?.appendMarker("skill_activated", skill.meta.name);
|
|
276
|
-
return formatLoadedSkill(skill);
|
|
277
|
-
},
|
|
278
|
-
},
|
|
279
258
|
{
|
|
280
259
|
name: "help",
|
|
281
260
|
description: "Show available slash commands",
|
|
@@ -339,6 +318,7 @@ const builtinSlashCommandEntries = [
|
|
|
339
318
|
async handler(args, ctx) {
|
|
340
319
|
ctx.agent.messages = ctx.agent.messages.filter((m) => m.role === "system" || m.role === "meta");
|
|
341
320
|
ctx.sessionManager?.appendMarker("conversation_clear", "");
|
|
321
|
+
ctx.sessionManager?.clearTitleMetadata?.();
|
|
342
322
|
if (ctx.agent.getTodos().length > 0) {
|
|
343
323
|
ctx.agent.setTodos([]);
|
|
344
324
|
}
|
|
@@ -48,7 +48,7 @@ export class SlashCommandRegistry {
|
|
|
48
48
|
if (skill) {
|
|
49
49
|
return {
|
|
50
50
|
handled: true,
|
|
51
|
-
result: `Skill "${skill.meta.name}": ${skill.meta.description}\nUse /${skill.meta.name} <your request> to run with this skill, or /
|
|
51
|
+
result: `Skill "${skill.meta.name}": ${skill.meta.description}\nUse /${skill.meta.name} <your request> to run with this skill, or /skills to choose from the picker.`,
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
return {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import stringWidth from "string-width";
|
|
2
|
+
export function normalizeSingleLine(text) {
|
|
3
|
+
return text.replace(/\s+/g, " ").trim();
|
|
4
|
+
}
|
|
5
|
+
export function truncateVisual(text, maxWidth) {
|
|
6
|
+
if (maxWidth <= 0)
|
|
7
|
+
return "";
|
|
8
|
+
if (stringWidth(text) <= maxWidth)
|
|
9
|
+
return text;
|
|
10
|
+
if (maxWidth === 1)
|
|
11
|
+
return "…";
|
|
12
|
+
let out = "";
|
|
13
|
+
let width = 0;
|
|
14
|
+
for (const ch of text) {
|
|
15
|
+
const chWidth = stringWidth(ch);
|
|
16
|
+
if (width + chWidth > maxWidth - 1)
|
|
17
|
+
break;
|
|
18
|
+
out += ch;
|
|
19
|
+
width += chWidth;
|
|
20
|
+
}
|
|
21
|
+
return `${out}…`;
|
|
22
|
+
}
|
|
23
|
+
export function padVisual(text, width) {
|
|
24
|
+
return `${text}${" ".repeat(Math.max(0, width - stringWidth(text)))}`;
|
|
25
|
+
}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export { createLspTool } from "./lsp.js";
|
|
|
11
11
|
export { createWebFetchTool } from "./web-fetch.js";
|
|
12
12
|
export { createWebSearchTool } from "./web-search.js";
|
|
13
13
|
export { createSkillTool } from "./skill.js";
|
|
14
|
+
export { createSkillSearchTool } from "./skill-search.js";
|
|
14
15
|
export { createAgentLifecycleTools, createCloseAgentTool, createSendInputTool, createSpawnAgentTool, createWaitAgentTool } from "./agent-lifecycle.js";
|
|
15
16
|
export { createTodoTool, type TodoStore } from "./todo.js";
|
|
16
17
|
export { createExitPlanModeTool, type PlanController } from "./exit-plan-mode.js";
|
package/dist/tools/index.js
CHANGED
|
@@ -11,6 +11,7 @@ export { createLspTool } from "./lsp.js";
|
|
|
11
11
|
export { createWebFetchTool } from "./web-fetch.js";
|
|
12
12
|
export { createWebSearchTool } from "./web-search.js";
|
|
13
13
|
export { createSkillTool } from "./skill.js";
|
|
14
|
+
export { createSkillSearchTool } from "./skill-search.js";
|
|
14
15
|
export { createAgentLifecycleTools, createCloseAgentTool, createSendInputTool, createSpawnAgentTool, createWaitAgentTool } from "./agent-lifecycle.js";
|
|
15
16
|
export { createTodoTool } from "./todo.js";
|
|
16
17
|
export { createExitPlanModeTool } from "./exit-plan-mode.js";
|
|
@@ -26,6 +27,7 @@ import { getLspService } from "../lsp/index.js";
|
|
|
26
27
|
import { createLspTool } from "./lsp.js";
|
|
27
28
|
import { createReadTool } from "./read.js";
|
|
28
29
|
import { createSkillTool } from "./skill.js";
|
|
30
|
+
import { createSkillSearchTool } from "./skill-search.js";
|
|
29
31
|
import { createAgentLifecycleTools } from "./agent-lifecycle.js";
|
|
30
32
|
import { createTodoTool } from "./todo.js";
|
|
31
33
|
import { createToolSearchTool } from "./tool-search.js";
|
|
@@ -53,7 +55,7 @@ export function createAllTools(cwd, skillRegistry, options = {}) {
|
|
|
53
55
|
createMemoryReadSummaryTool(cwd),
|
|
54
56
|
...createAgentLifecycleTools(),
|
|
55
57
|
...(options.questionController ? [createQuestionTool(options.questionController)] : []),
|
|
56
|
-
...(skillRegistry ? [createSkillTool(skillRegistry)] : []),
|
|
58
|
+
...(skillRegistry ? [createSkillSearchTool(skillRegistry), createSkillTool(skillRegistry)] : []),
|
|
57
59
|
...(options.todoStore ? [createTodoTool(options.todoStore)] : []),
|
|
58
60
|
...(options.planController ? [createExitPlanModeTool(options.planController)] : []),
|
|
59
61
|
...(options.toolSearchController ? [createToolSearchTool(options.toolSearchController)] : []),
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SkillRegistry } from "../skills/registry.js";
|
|
2
|
+
import type { SkillSummary } from "../skills/types.js";
|
|
3
|
+
import type { ToolRegistryEntry } from "../types.js";
|
|
4
|
+
interface SkillSearchMatch {
|
|
5
|
+
skill: SkillSummary;
|
|
6
|
+
score: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function createSkillSearchTool(registry: SkillRegistry): ToolRegistryEntry;
|
|
9
|
+
export declare function searchSkillSummaries(skills: SkillSummary[], query: string): SkillSearchMatch[];
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const DEFAULT_MAX_RESULTS = 8;
|
|
2
|
+
const MAX_RESULTS = 25;
|
|
3
|
+
const SOURCE_PRIORITY = {
|
|
4
|
+
project: 0,
|
|
5
|
+
configured: 1,
|
|
6
|
+
user: 2,
|
|
7
|
+
};
|
|
8
|
+
export function createSkillSearchTool(registry) {
|
|
9
|
+
return {
|
|
10
|
+
name: "skill_search",
|
|
11
|
+
readOnly: true,
|
|
12
|
+
effect: "read",
|
|
13
|
+
description: "Search available skills by name, description, tags, and source. Use this before loading a skill when a task may match a specialized workflow.",
|
|
14
|
+
parameters: {
|
|
15
|
+
type: "object",
|
|
16
|
+
properties: {
|
|
17
|
+
query: {
|
|
18
|
+
type: "string",
|
|
19
|
+
description: "Search terms describing the desired skill or workflow.",
|
|
20
|
+
},
|
|
21
|
+
max_results: {
|
|
22
|
+
type: "number",
|
|
23
|
+
description: `Maximum number of matches to return (default ${DEFAULT_MAX_RESULTS}, max ${MAX_RESULTS}).`,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
required: ["query"],
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
},
|
|
29
|
+
async execute(args) {
|
|
30
|
+
const query = typeof args.query === "string" ? args.query.trim() : "";
|
|
31
|
+
const maxResults = typeof args.max_results === "number" && args.max_results > 0
|
|
32
|
+
? Math.min(Math.floor(args.max_results), MAX_RESULTS)
|
|
33
|
+
: DEFAULT_MAX_RESULTS;
|
|
34
|
+
const skills = registry.summaries();
|
|
35
|
+
if (skills.length === 0) {
|
|
36
|
+
return { content: "No skills are currently available." };
|
|
37
|
+
}
|
|
38
|
+
const matches = searchSkillSummaries(skills, query).slice(0, maxResults);
|
|
39
|
+
if (matches.length === 0) {
|
|
40
|
+
return {
|
|
41
|
+
content: `No skills matched "${query}". Try broader terms or use /skills to browse all skills manually.`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
content: formatSkillSearchResults(matches, skills.length, query),
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export function searchSkillSummaries(skills, query) {
|
|
51
|
+
const terms = normalizeTerms(query);
|
|
52
|
+
const scored = [];
|
|
53
|
+
for (const skill of skills) {
|
|
54
|
+
const score = scoreSkill(skill, terms, query);
|
|
55
|
+
if (score > 0 || terms.length === 0) {
|
|
56
|
+
scored.push({ skill, score });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
scored.sort((a, b) => {
|
|
60
|
+
if (b.score !== a.score)
|
|
61
|
+
return b.score - a.score;
|
|
62
|
+
const ap = SOURCE_PRIORITY[a.skill.source ?? "user"] ?? 3;
|
|
63
|
+
const bp = SOURCE_PRIORITY[b.skill.source ?? "user"] ?? 3;
|
|
64
|
+
if (ap !== bp)
|
|
65
|
+
return ap - bp;
|
|
66
|
+
return a.skill.name.localeCompare(b.skill.name);
|
|
67
|
+
});
|
|
68
|
+
return scored;
|
|
69
|
+
}
|
|
70
|
+
function scoreSkill(skill, terms, rawQuery) {
|
|
71
|
+
const name = skill.name.toLowerCase();
|
|
72
|
+
const desc = (skill.description ?? "").toLowerCase();
|
|
73
|
+
const tags = (skill.tags ?? []).map((tag) => tag.toLowerCase());
|
|
74
|
+
const source = skill.source ?? "user";
|
|
75
|
+
const sourceBonus = source === "project" ? 4 : source === "configured" ? 2 : 0;
|
|
76
|
+
const query = rawQuery.trim().toLowerCase();
|
|
77
|
+
if (terms.length === 0)
|
|
78
|
+
return 1 + sourceBonus;
|
|
79
|
+
let score = 0;
|
|
80
|
+
if (name === query)
|
|
81
|
+
score += 80;
|
|
82
|
+
if (name.includes(query) && query.length > 0)
|
|
83
|
+
score += 30;
|
|
84
|
+
for (const term of terms) {
|
|
85
|
+
if (name === term)
|
|
86
|
+
score += 30;
|
|
87
|
+
else if (name.includes(term))
|
|
88
|
+
score += 12;
|
|
89
|
+
if (tags.some((tag) => tag === term))
|
|
90
|
+
score += 10;
|
|
91
|
+
else if (tags.some((tag) => tag.includes(term)))
|
|
92
|
+
score += 6;
|
|
93
|
+
if (desc.includes(term))
|
|
94
|
+
score += 3;
|
|
95
|
+
if (source.includes(term))
|
|
96
|
+
score += 2;
|
|
97
|
+
}
|
|
98
|
+
return score > 0 ? score + sourceBonus : 0;
|
|
99
|
+
}
|
|
100
|
+
function normalizeTerms(query) {
|
|
101
|
+
const rawTerms = query
|
|
102
|
+
.toLowerCase()
|
|
103
|
+
.split(/[^a-z0-9_\-\u3000-\u9fff]+/i)
|
|
104
|
+
.map((term) => term.trim())
|
|
105
|
+
.filter(Boolean);
|
|
106
|
+
const terms = new Set();
|
|
107
|
+
for (const term of rawTerms) {
|
|
108
|
+
terms.add(term);
|
|
109
|
+
const chars = Array.from(term);
|
|
110
|
+
if (chars.some(isCjkChar) && chars.length > 2) {
|
|
111
|
+
for (let i = 0; i < chars.length - 1; i++) {
|
|
112
|
+
terms.add(`${chars[i]}${chars[i + 1]}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return [...terms];
|
|
117
|
+
}
|
|
118
|
+
function isCjkChar(ch) {
|
|
119
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
120
|
+
return code >= 0x3000 && code <= 0x9fff;
|
|
121
|
+
}
|
|
122
|
+
function formatSkillSearchResults(matches, total, query) {
|
|
123
|
+
const lines = [
|
|
124
|
+
query ? `Skill search results for "${query}" (${matches.length} of ${total}):` : `Available skills (${matches.length} of ${total}):`,
|
|
125
|
+
];
|
|
126
|
+
for (const { skill } of matches) {
|
|
127
|
+
const tags = skill.tags && skill.tags.length > 0 ? ` [tags: ${skill.tags.join(", ")}]` : "";
|
|
128
|
+
const source = skill.source ? ` (${skill.source})` : "";
|
|
129
|
+
lines.push(`- ${skill.name}${source}: ${skill.description}${tags}`);
|
|
130
|
+
}
|
|
131
|
+
lines.push("");
|
|
132
|
+
lines.push("Call skill with the exact name to load a selected skill.");
|
|
133
|
+
return lines.join("\n");
|
|
134
|
+
}
|
package/dist/tools/skill.js
CHANGED
|
@@ -38,11 +38,8 @@ export function createSkillTool(registry) {
|
|
|
38
38
|
}
|
|
39
39
|
const skill = registry.get(name);
|
|
40
40
|
if (!skill) {
|
|
41
|
-
const available = registry.summaries().map((item) => item.name).join(", ");
|
|
42
41
|
return {
|
|
43
|
-
content: available
|
|
44
|
-
? `Error: Unknown skill "${name}". Available skills: ${available}`
|
|
45
|
-
: `Error: Unknown skill "${name}". No skills are currently available.`,
|
|
42
|
+
content: `Error: Unknown skill "${name}". Use skill_search to find available skills, then retry with the exact skill name.`,
|
|
46
43
|
isError: true,
|
|
47
44
|
};
|
|
48
45
|
}
|