@selesai/code 0.13.14 → 0.13.16
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/CHANGELOG.md +16 -0
- package/dist/core/agent-session-auto-handoff.test.js +2 -0
- package/dist/core/agent-session-length-continuation.test.js +2 -0
- package/dist/core/agent-session.js +18 -1
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/loader.js +10 -0
- package/dist/core/extensions/runner.js +2 -0
- package/dist/core/extensions/types.d.ts +41 -1
- package/dist/core/resource-loader.d.ts +17 -0
- package/dist/core/resource-loader.js +8 -0
- package/dist/extensions/capability-gateway/catalog.ts +150 -0
- package/dist/extensions/capability-gateway/index.test.ts +128 -0
- package/dist/extensions/capability-gateway/index.ts +329 -0
- package/dist/extensions/capability-gateway/integration.test.ts +238 -0
- package/dist/extensions/capability-gateway/package.json +15 -0
- package/dist/extensions/package.json +1 -0
- package/dist/extensions/pi-web-agent/src/backends/config.ts +1 -0
- package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +5 -3
- package/dist/extensions/pi-web-agent/src/search/tokenin.ts +7 -3
- package/dist/extensions/pi-web-agent/src/types.ts +1 -1
- package/dist/extensions/pi-zentui/extensions/zentui/working-line.ts +9 -4
- package/dist/extensions/pi-zentui/test/working-line.test.ts +26 -0
- package/dist/index.d.ts +1 -1
- package/dist/skills/to-prd/SKILL.md +73 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@selesai/code` will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.13.16] - 2026-09-06
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **Capability gateway inline-skill routing.** `$skill` references expanded by inline skills are no longer re-recommended through `capability_skill_show`; embedded `<skill>` blocks are excluded from prompt routing.
|
|
9
|
+
- **TokenIn search backend URL.** A custom `baseUrl` ending in `/v1` no longer produces a doubled `/v1/v1/search/…` path, and accounts with a blank `apiKey` are now treated as unconfigured instead of triggering doomed requests.
|
|
10
|
+
- **TokenIn search fallback typing.** Search fallback metadata can now reference any provider, matching actual failover behavior.
|
|
11
|
+
|
|
12
|
+
## [0.13.15] - 2026-09-06
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- **Progressive capability gateway.** Optional extension tools and skills now stay out of the default model context while remaining discoverable through `capability_catalog`, `capability_discover`, and `capability_skill_show`. Built-in tools are unchanged, and the gateway can be disabled with `SELESAI_CAPABILITY_GATEWAY=0`.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **Capability catalog metadata.** Disabled skills now recover their frontmatter descriptions, and catalog results are no longer capped at 20 entries.
|
|
19
|
+
- **Zentui stale-session updates.** Timer-driven working-line updates now fail open when a captured extension context becomes stale after session replacement or reload.
|
|
20
|
+
|
|
5
21
|
## [0.13.13] - 2026-09-04
|
|
6
22
|
|
|
7
23
|
### Added
|
|
@@ -19,6 +19,8 @@ function createMockSession({ enabled, threshold = 128_000, tokens, mode = "tui",
|
|
|
19
19
|
const resourceLoader = {
|
|
20
20
|
getExtensions: () => ({ extensions: [], errors: [], runtime: { flagValues: new Map(), pendingProviderRegistrations: [], pendingNativeProviderRegistrations: [], assertActive: () => { } } }),
|
|
21
21
|
getSkills: () => ({ skills: [], diagnostics: [] }),
|
|
22
|
+
getPromptSkills: () => [],
|
|
23
|
+
setSkillsIndexFilter: () => { },
|
|
22
24
|
getPrompts: () => ({ prompts: [], diagnostics: [] }),
|
|
23
25
|
getThemes: () => ({ themes: [], diagnostics: [] }),
|
|
24
26
|
getAgents: () => ({ agents: [], diagnostics: [] }),
|
|
@@ -14,6 +14,8 @@ function createMockSession() {
|
|
|
14
14
|
const resourceLoader = {
|
|
15
15
|
getExtensions: () => ({ extensions: [], errors: [], runtime: { flagValues: new Map(), pendingProviderRegistrations: [], pendingNativeProviderRegistrations: [], assertActive: () => { } } }),
|
|
16
16
|
getSkills: () => ({ skills: [], diagnostics: [] }),
|
|
17
|
+
getPromptSkills: () => [],
|
|
18
|
+
setSkillsIndexFilter: () => { },
|
|
17
19
|
getPrompts: () => ({ prompts: [], diagnostics: [] }),
|
|
18
20
|
getThemes: () => ({ themes: [], diagnostics: [] }),
|
|
19
21
|
getAgents: () => ({ agents: [], diagnostics: [] }),
|
|
@@ -711,6 +711,7 @@ export class AgentSession {
|
|
|
711
711
|
description: definition.description,
|
|
712
712
|
parameters: definition.parameters,
|
|
713
713
|
promptGuidelines: definition.promptGuidelines,
|
|
714
|
+
discovery: definition.discovery,
|
|
714
715
|
sourceInfo,
|
|
715
716
|
}));
|
|
716
717
|
}
|
|
@@ -819,7 +820,7 @@ export class AgentSession {
|
|
|
819
820
|
const loaderSystemPrompt = this._resourceLoader.getSystemPrompt();
|
|
820
821
|
const loaderAppendSystemPrompt = this._resourceLoader.getAppendSystemPrompt();
|
|
821
822
|
const appendSystemPrompt = loaderAppendSystemPrompt.length > 0 ? loaderAppendSystemPrompt.join("\n\n") : undefined;
|
|
822
|
-
const loadedSkills = this._resourceLoader.
|
|
823
|
+
const loadedSkills = this._resourceLoader.getPromptSkills();
|
|
823
824
|
const loadedAgents = this._resourceLoader.getAgents().agents;
|
|
824
825
|
const loadedContextFiles = this._resourceLoader.getAgentsFiles().agentsFiles;
|
|
825
826
|
this._baseSystemPromptOptions = {
|
|
@@ -2249,6 +2250,22 @@ export class AgentSession {
|
|
|
2249
2250
|
getActiveTools: () => this.getActiveToolNames(),
|
|
2250
2251
|
getAllTools: () => this.getAllTools(),
|
|
2251
2252
|
setActiveTools: (toolNames) => this.setActiveToolsByName(toolNames),
|
|
2253
|
+
getResolvedSkills: () => this._resourceLoader.getResolvedSkills().map((resource) => {
|
|
2254
|
+
const skill = this._resourceLoader.getSkills().skills.find((s) => s.filePath === resource.path);
|
|
2255
|
+
return {
|
|
2256
|
+
name: skill?.name ?? basename(dirname(resource.path)),
|
|
2257
|
+
description: skill?.description ?? "",
|
|
2258
|
+
category: skill?.category,
|
|
2259
|
+
filePath: resource.path,
|
|
2260
|
+
scope: resource.metadata.scope === "project" ? "project" : "user",
|
|
2261
|
+
disableModelInvocation: skill?.disableModelInvocation ?? false,
|
|
2262
|
+
};
|
|
2263
|
+
}),
|
|
2264
|
+
setSkillsIndexFilter: (filter) => {
|
|
2265
|
+
this._resourceLoader.setSkillsIndexFilter(filter);
|
|
2266
|
+
this._baseSystemPrompt = this._rebuildSystemPrompt(this.getActiveToolNames());
|
|
2267
|
+
this.agent.state.systemPrompt = this._systemPromptOverride ?? this._baseSystemPrompt;
|
|
2268
|
+
},
|
|
2252
2269
|
refreshTools: () => this._refreshToolRegistry(),
|
|
2253
2270
|
getCommands,
|
|
2254
2271
|
setModel: async (model) => {
|
|
@@ -6,6 +6,6 @@ export type { SourceInfo } from "../source-info.ts";
|
|
|
6
6
|
export { createExtensionRuntime, discoverAndLoadExtensions, loadExtensionFromFactory, loadExtensions, } from "./loader.ts";
|
|
7
7
|
export type { ExtensionErrorListener, ForkHandler, NavigateTreeHandler, NewSessionHandler, ShutdownHandler, SwitchSessionHandler, } from "./runner.ts";
|
|
8
8
|
export { ExtensionRunner } from "./runner.ts";
|
|
9
|
-
export type { AfterProviderResponseEvent, AgentEndEvent, AgentSettledEvent, AgentStartEvent, AgentToolResult, AgentToolUpdateCallback, AppendEntryHandler, AppKeybinding, AutocompleteProviderFactory, BashToolCallEvent, BashToolResultEvent, BeforeAgentStartEvent, BeforeAgentStartEventResult, BeforeProviderHeadersEvent, BeforeProviderRequestEvent, BeforeProviderRequestEventResult, BuildSystemPromptOptions, CompactOptions, ContextEvent, ContextEventResult, ContextUsage, CustomToolCallEvent, CustomToolResultEvent, EditorFactory, EditToolCallEvent, EditToolResultEvent, EntryRenderer, EntryRenderOptions, ExecOptions, ExecResult, Extension, ExtensionActions, ExtensionAPI, ExtensionCommandContext, ExtensionCommandContextActions, ExtensionContext, ExtensionContextActions, ExtensionError, ExtensionEvent, ExtensionFactory, ExtensionFlag, ExtensionHandler, ExtensionMode, ExtensionRuntime, ExtensionShortcut, ExtensionUIContext, ExtensionUIDialogOptions, ExtensionWidgetOptions, FindToolCallEvent, FindToolResultEvent, GetActiveToolsHandler, GetAllToolsHandler, GetCommandsHandler, GetThinkingLevelHandler, GrepToolCallEvent, GrepToolResultEvent, InlineExtension, InputEvent, InputEventResult, InputSource, KeybindingsManager, LoadExtensionsResult, LsToolCallEvent, LsToolResultEvent, MessageEndEvent, MessageRenderer, MessageRenderOptions, MessageStartEvent, MessageUpdateEvent, ModelSelectEvent, ModelSelectSource, ProjectTrustContext, ProjectTrustEvent, ProjectTrustEventDecision, ProjectTrustEventResult, ProjectTrustHandler, ProviderConfig, ProviderModelConfig, ReadToolCallEvent, ReadToolResultEvent, RegisteredCommand, RegisteredTool, ReplacedSessionContext, ResolvedCommand, ResourcesDiscoverEvent, ResourcesDiscoverResult, SendMessageHandler, SendUserMessageHandler, SessionBeforeCompactEvent, SessionBeforeCompactResult, SessionBeforeForkEvent, SessionBeforeForkResult, SessionBeforeSwitchEvent, SessionBeforeSwitchResult, SessionBeforeTreeEvent, SessionBeforeTreeResult, SessionCompactEvent, SessionEvent, SessionInfoChangedEvent, SessionShutdownEvent, SessionStartEvent, SessionTreeEvent, SetActiveToolsHandler, SetLabelHandler, SetModelHandler, SetThinkingLevelHandler, TerminalInputHandler, ToolCallEvent, ToolCallEventResult, ToolDefinition, ToolExecutionEndEvent, ToolExecutionMode, ToolExecutionStartEvent, ToolExecutionUpdateEvent, ToolInfo, ToolRenderResultOptions, ToolResultEvent, ToolResultEventResult, TreePreparation, TurnEndEvent, TurnStartEvent, UIPromptEndEvent, UIPromptKind, UIPromptStartEvent, UserBashEvent, UserBashEventResult, WidgetPlacement, WorkingIndicatorOptions, WriteToolCallEvent, WriteToolResultEvent, } from "./types.ts";
|
|
9
|
+
export type { AfterProviderResponseEvent, AgentEndEvent, AgentSettledEvent, AgentStartEvent, AgentToolResult, AgentToolUpdateCallback, AppendEntryHandler, AppKeybinding, AutocompleteProviderFactory, BashToolCallEvent, BashToolResultEvent, BeforeAgentStartEvent, BeforeAgentStartEventResult, BeforeProviderHeadersEvent, BeforeProviderRequestEvent, BeforeProviderRequestEventResult, BuildSystemPromptOptions, CompactOptions, ContextEvent, ContextEventResult, ContextUsage, CustomToolCallEvent, CustomToolResultEvent, EditorFactory, EditToolCallEvent, EditToolResultEvent, EntryRenderer, EntryRenderOptions, ExecOptions, ExecResult, Extension, ExtensionActions, ExtensionAPI, ExtensionCommandContext, ExtensionCommandContextActions, ExtensionContext, ExtensionContextActions, ExtensionError, ExtensionEvent, ExtensionFactory, ExtensionFlag, ExtensionHandler, ExtensionMode, ExtensionRuntime, ExtensionShortcut, ExtensionUIContext, ExtensionUIDialogOptions, ExtensionWidgetOptions, FindToolCallEvent, FindToolResultEvent, GetActiveToolsHandler, GetAllToolsHandler, GetCommandsHandler, GetThinkingLevelHandler, GrepToolCallEvent, GrepToolResultEvent, InlineExtension, InputEvent, InputEventResult, InputSource, KeybindingsManager, LoadExtensionsResult, LsToolCallEvent, LsToolResultEvent, MessageEndEvent, MessageRenderer, MessageRenderOptions, MessageStartEvent, MessageUpdateEvent, ModelSelectEvent, ModelSelectSource, ProjectTrustContext, ProjectTrustEvent, ProjectTrustEventDecision, ProjectTrustEventResult, ProjectTrustHandler, ProviderConfig, ProviderModelConfig, ReadToolCallEvent, ReadToolResultEvent, RegisteredCommand, RegisteredTool, ReplacedSessionContext, ResolvedCommand, ResolvedSkillInfo, ResourcesDiscoverEvent, ResourcesDiscoverResult, SendMessageHandler, SendUserMessageHandler, SessionBeforeCompactEvent, SessionBeforeCompactResult, SessionBeforeForkEvent, SessionBeforeForkResult, SessionBeforeSwitchEvent, SessionBeforeSwitchResult, SessionBeforeTreeEvent, SessionBeforeTreeResult, SessionCompactEvent, SessionEvent, SessionInfoChangedEvent, SessionShutdownEvent, SessionStartEvent, SessionTreeEvent, SetActiveToolsHandler, SetLabelHandler, SetModelHandler, SetThinkingLevelHandler, TerminalInputHandler, ToolCallEvent, ToolCallEventResult, ToolDefinition, ToolExecutionEndEvent, ToolExecutionMode, ToolExecutionStartEvent, ToolExecutionUpdateEvent, ToolInfo, ToolRenderResultOptions, ToolResultEvent, ToolResultEventResult, TreePreparation, TurnEndEvent, TurnStartEvent, UIPromptEndEvent, UIPromptKind, UIPromptStartEvent, UserBashEvent, UserBashEventResult, WidgetPlacement, WorkingIndicatorOptions, WriteToolCallEvent, WriteToolResultEvent, } from "./types.ts";
|
|
10
10
|
export { defineTool, isBashToolResult, isEditToolResult, isFindToolResult, isGrepToolResult, isLsToolResult, isReadToolResult, isToolCallEventType, isWriteToolResult, } from "./types.ts";
|
|
11
11
|
export { wrapRegisteredTool, wrapRegisteredTools } from "./wrapper.ts";
|
|
@@ -150,6 +150,8 @@ export function createExtensionRuntime() {
|
|
|
150
150
|
getActiveTools: notInitialized,
|
|
151
151
|
getAllTools: notInitialized,
|
|
152
152
|
setActiveTools: notInitialized,
|
|
153
|
+
getResolvedSkills: notInitialized,
|
|
154
|
+
setSkillsIndexFilter: notInitialized,
|
|
153
155
|
// registerTool() is valid during extension load; refresh is only needed post-bind.
|
|
154
156
|
refreshTools: () => { },
|
|
155
157
|
getCommands: notInitialized,
|
|
@@ -291,6 +293,14 @@ function createExtensionAPI(extension, runtime, cwd, eventBus) {
|
|
|
291
293
|
assertActive();
|
|
292
294
|
return runtime.getActiveTools();
|
|
293
295
|
},
|
|
296
|
+
getResolvedSkills() {
|
|
297
|
+
assertActive();
|
|
298
|
+
return runtime.getResolvedSkills();
|
|
299
|
+
},
|
|
300
|
+
setSkillsIndexFilter(filter) {
|
|
301
|
+
assertActive();
|
|
302
|
+
runtime.setSkillsIndexFilter(filter);
|
|
303
|
+
},
|
|
294
304
|
getAllTools() {
|
|
295
305
|
assertActive();
|
|
296
306
|
return runtime.getAllTools();
|
|
@@ -168,6 +168,8 @@ export class ExtensionRunner {
|
|
|
168
168
|
this.runtime.getActiveTools = actions.getActiveTools;
|
|
169
169
|
this.runtime.getAllTools = actions.getAllTools;
|
|
170
170
|
this.runtime.setActiveTools = actions.setActiveTools;
|
|
171
|
+
this.runtime.getResolvedSkills = actions.getResolvedSkills;
|
|
172
|
+
this.runtime.setSkillsIndexFilter = actions.setSkillsIndexFilter;
|
|
171
173
|
this.runtime.refreshTools = actions.refreshTools;
|
|
172
174
|
this.runtime.getCommands = actions.getCommands;
|
|
173
175
|
this.runtime.setModel = actions.setModel;
|
|
@@ -23,6 +23,7 @@ import type { ModelRegistry } from "../model-registry.ts";
|
|
|
23
23
|
import type { ScopedModel } from "../model-resolver.ts";
|
|
24
24
|
import type { BranchSummaryEntry, CompactionEntry, CustomEntry, ReadonlySessionManager, SessionEntry, SessionManager } from "../session-manager.ts";
|
|
25
25
|
import type { SlashCommandInfo } from "../slash-commands.ts";
|
|
26
|
+
import type { Skill } from "../skills.ts";
|
|
26
27
|
import type { SourceInfo } from "../source-info.ts";
|
|
27
28
|
import type { BuildSystemPromptOptions } from "../system-prompt.ts";
|
|
28
29
|
import type { BashOperations } from "../tools/bash.ts";
|
|
@@ -352,6 +353,20 @@ export interface ToolDefinition<TParams extends TSchema = TSchema, TDetails = un
|
|
|
352
353
|
description: string;
|
|
353
354
|
/** Optional one-line snippet for the Available tools section in the default system prompt. Custom tools are omitted from that section when this is not provided. */
|
|
354
355
|
promptSnippet?: string;
|
|
356
|
+
/**
|
|
357
|
+
* Optional discovery metadata for the capability gateway. When present, the
|
|
358
|
+
* tool is eligible for catalog/discovery; when absent, a conservative
|
|
359
|
+
* name/first-sentence fallback summary is used. Never contains the full
|
|
360
|
+
* tool description or schema.
|
|
361
|
+
*/
|
|
362
|
+
discovery?: {
|
|
363
|
+
/** Short one-line purpose shown in the catalog. */
|
|
364
|
+
summary?: string;
|
|
365
|
+
/** Alternative names the router can match. */
|
|
366
|
+
aliases?: string[];
|
|
367
|
+
/** Stable category label. */
|
|
368
|
+
category?: string;
|
|
369
|
+
};
|
|
355
370
|
/** Optional guideline bullets appended to the default system prompt Guidelines section when this tool is active. */
|
|
356
371
|
promptGuidelines?: string[];
|
|
357
372
|
/** Parameter schema (TypeBox) */
|
|
@@ -972,6 +987,20 @@ export interface ExtensionAPI {
|
|
|
972
987
|
exec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;
|
|
973
988
|
/** Get the list of currently active tool names. */
|
|
974
989
|
getActiveTools(): string[];
|
|
990
|
+
/**
|
|
991
|
+
* Get the resolved, trust-filtered skill catalog (enabled skills with
|
|
992
|
+
* metadata). Skills hidden from model invocation are flagged so callers
|
|
993
|
+
* can exclude them from automatic discovery.
|
|
994
|
+
*/
|
|
995
|
+
getResolvedSkills(): ResolvedSkillInfo[];
|
|
996
|
+
/**
|
|
997
|
+
* Replace the skill view rendered into the system-prompt skill index.
|
|
998
|
+
* The filter receives the default eligible view (trust-filtered,
|
|
999
|
+
* model-invocation-eligible) and returns the view to render. Passing
|
|
1000
|
+
* undefined restores the default. The resolved skill catalog is never
|
|
1001
|
+
* mutated; explicit inline skill invocation is unaffected.
|
|
1002
|
+
*/
|
|
1003
|
+
setSkillsIndexFilter(filter: ((skills: Skill[]) => Skill[]) | undefined): void;
|
|
975
1004
|
/** Get all configured tools with parameter schema, prompt guidelines, and source metadata. */
|
|
976
1005
|
getAllTools(): ToolInfo[];
|
|
977
1006
|
/** Set the active tools by name. */
|
|
@@ -1161,9 +1190,18 @@ export type SetSessionNameHandler = (name: string) => void;
|
|
|
1161
1190
|
export type GetSessionNameHandler = () => string | undefined;
|
|
1162
1191
|
export type GetActiveToolsHandler = () => string[];
|
|
1163
1192
|
/** Tool info with name, description, parameter schema, prompt guidelines, and source metadata. */
|
|
1164
|
-
export type ToolInfo = Pick<ToolDefinition, "name" | "description" | "parameters" | "promptGuidelines"> & {
|
|
1193
|
+
export type ToolInfo = Pick<ToolDefinition, "name" | "description" | "parameters" | "promptGuidelines" | "discovery"> & {
|
|
1165
1194
|
sourceInfo: SourceInfo;
|
|
1166
1195
|
};
|
|
1196
|
+
/** A resolved, trust-filtered skill in the extension-facing catalog. */
|
|
1197
|
+
export interface ResolvedSkillInfo {
|
|
1198
|
+
name: string;
|
|
1199
|
+
description: string;
|
|
1200
|
+
category?: string;
|
|
1201
|
+
filePath: string;
|
|
1202
|
+
scope: "user" | "project" | "temporary";
|
|
1203
|
+
disableModelInvocation: boolean;
|
|
1204
|
+
}
|
|
1167
1205
|
export type GetAllToolsHandler = () => ToolInfo[];
|
|
1168
1206
|
export type GetCommandsHandler = () => SlashCommandInfo[];
|
|
1169
1207
|
export type SetActiveToolsHandler = (toolNames: string[]) => void;
|
|
@@ -1217,6 +1255,8 @@ export interface ExtensionActions {
|
|
|
1217
1255
|
getActiveTools: GetActiveToolsHandler;
|
|
1218
1256
|
getAllTools: GetAllToolsHandler;
|
|
1219
1257
|
setActiveTools: SetActiveToolsHandler;
|
|
1258
|
+
getResolvedSkills: () => ResolvedSkillInfo[];
|
|
1259
|
+
setSkillsIndexFilter: (filter: ((skills: Skill[]) => Skill[]) | undefined) => void;
|
|
1220
1260
|
refreshTools: RefreshToolsHandler;
|
|
1221
1261
|
getCommands: GetCommandsHandler;
|
|
1222
1262
|
setModel: SetModelHandler;
|
|
@@ -35,6 +35,20 @@ export interface ResourceLoader {
|
|
|
35
35
|
diagnostics: ResourceDiagnostic[];
|
|
36
36
|
};
|
|
37
37
|
getResolvedSkills(): ResolvedResource[];
|
|
38
|
+
/**
|
|
39
|
+
* Skills eligible for the system-prompt skill index (trust-filtered,
|
|
40
|
+
* model-invocation-eligible). Extensions may replace this view via
|
|
41
|
+
* setSkillsIndexFilter to implement progressive disclosure without
|
|
42
|
+
* touching the resolved catalog.
|
|
43
|
+
*/
|
|
44
|
+
getPromptSkills(): Skill[];
|
|
45
|
+
/**
|
|
46
|
+
* Replace the skill view used when rendering the system-prompt skill index.
|
|
47
|
+
* The filter receives the default eligible view and returns the view to
|
|
48
|
+
* render. Passing undefined restores the default. The resolved catalog
|
|
49
|
+
* (getSkills/getResolvedSkills) is never mutated.
|
|
50
|
+
*/
|
|
51
|
+
setSkillsIndexFilter(filter: ((skills: Skill[]) => Skill[]) | undefined): void;
|
|
38
52
|
getPrompts(): {
|
|
39
53
|
prompts: PromptTemplate[];
|
|
40
54
|
diagnostics: ResourceDiagnostic[];
|
|
@@ -166,6 +180,7 @@ export declare class DefaultResourceLoader implements ResourceLoader {
|
|
|
166
180
|
private skills;
|
|
167
181
|
private skillDiagnostics;
|
|
168
182
|
private resolvedSkills;
|
|
183
|
+
private skillsIndexFilter;
|
|
169
184
|
private agents;
|
|
170
185
|
private agentDiagnostics;
|
|
171
186
|
private prompts;
|
|
@@ -193,6 +208,8 @@ export declare class DefaultResourceLoader implements ResourceLoader {
|
|
|
193
208
|
diagnostics: ResourceDiagnostic[];
|
|
194
209
|
};
|
|
195
210
|
getResolvedSkills(): ResolvedResource[];
|
|
211
|
+
getPromptSkills(): Skill[];
|
|
212
|
+
setSkillsIndexFilter(filter: ((skills: Skill[]) => Skill[]) | undefined): void;
|
|
196
213
|
getPrompts(): {
|
|
197
214
|
prompts: PromptTemplate[];
|
|
198
215
|
diagnostics: ResourceDiagnostic[];
|
|
@@ -141,6 +141,7 @@ export class DefaultResourceLoader {
|
|
|
141
141
|
skills;
|
|
142
142
|
skillDiagnostics;
|
|
143
143
|
resolvedSkills = [];
|
|
144
|
+
skillsIndexFilter;
|
|
144
145
|
agents;
|
|
145
146
|
agentDiagnostics;
|
|
146
147
|
prompts;
|
|
@@ -226,6 +227,13 @@ export class DefaultResourceLoader {
|
|
|
226
227
|
getResolvedSkills() {
|
|
227
228
|
return this.resolvedSkills;
|
|
228
229
|
}
|
|
230
|
+
getPromptSkills() {
|
|
231
|
+
const eligible = this.skills.filter((skill) => !skill.disableModelInvocation);
|
|
232
|
+
return this.skillsIndexFilter ? this.skillsIndexFilter(eligible) : eligible;
|
|
233
|
+
}
|
|
234
|
+
setSkillsIndexFilter(filter) {
|
|
235
|
+
this.skillsIndexFilter = filter;
|
|
236
|
+
}
|
|
229
237
|
getPrompts() {
|
|
230
238
|
return { prompts: this.prompts, diagnostics: this.promptDiagnostics };
|
|
231
239
|
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability gateway catalog and deterministic router.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions: no host imports, fully unit-testable. The router uses
|
|
5
|
+
* deterministic local scoring (name/alias matches above token overlap) and
|
|
6
|
+
* never calls a model.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
|
|
11
|
+
import { parseFrontmatter, type ResolvedSkillInfo, type ToolInfo } from "@selesai/code";
|
|
12
|
+
|
|
13
|
+
export interface CatalogEntry {
|
|
14
|
+
name: string;
|
|
15
|
+
kind: "tool" | "skill";
|
|
16
|
+
summary: string;
|
|
17
|
+
aliases: string[];
|
|
18
|
+
category?: string;
|
|
19
|
+
eligible: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Built-in tools are never mediated by the gateway. */
|
|
23
|
+
export const BUILTIN_TOOL_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls"]);
|
|
24
|
+
|
|
25
|
+
/** First sentence of a description, used as the conservative fallback summary. */
|
|
26
|
+
export function firstSentence(text: string): string {
|
|
27
|
+
const trimmed = text.trim();
|
|
28
|
+
const end = trimmed.search(/[.!?](?:\s|$)/);
|
|
29
|
+
return (end === -1 ? trimmed : trimmed.slice(0, end + 1)).trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Compact summary for a tool: explicit discovery metadata wins, then first sentence, then snippet. */
|
|
33
|
+
export function toolSummary(tool: ToolInfo): string {
|
|
34
|
+
const explicit = tool.discovery?.summary?.trim();
|
|
35
|
+
if (explicit) return explicit;
|
|
36
|
+
const sentence = firstSentence(tool.description);
|
|
37
|
+
if (sentence) return sentence;
|
|
38
|
+
return tool.promptSnippet?.trim() || tool.name;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function buildToolCatalog(tools: ToolInfo[], gatewayToolNames: Set<string>): CatalogEntry[] {
|
|
42
|
+
return tools
|
|
43
|
+
.filter((tool) => !BUILTIN_TOOL_NAMES.has(tool.name) && !gatewayToolNames.has(tool.name))
|
|
44
|
+
.map((tool) => ({
|
|
45
|
+
name: tool.name,
|
|
46
|
+
kind: "tool" as const,
|
|
47
|
+
summary: toolSummary(tool),
|
|
48
|
+
aliases: tool.discovery?.aliases ?? [],
|
|
49
|
+
category: tool.discovery?.category,
|
|
50
|
+
eligible: true,
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildSkillCatalog(skills: ResolvedSkillInfo[]): CatalogEntry[] {
|
|
55
|
+
return skills
|
|
56
|
+
.filter((skill) => !skill.disableModelInvocation)
|
|
57
|
+
.map((skill) => ({
|
|
58
|
+
name: skill.name,
|
|
59
|
+
kind: "skill" as const,
|
|
60
|
+
summary: skill.description || skillSummaryFromFile(skill.filePath),
|
|
61
|
+
aliases: [],
|
|
62
|
+
category: skill.category,
|
|
63
|
+
eligible: true,
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Disabled skills resolve as paths but never parse; read the frontmatter description instead.
|
|
68
|
+
function skillSummaryFromFile(filePath: string): string {
|
|
69
|
+
try {
|
|
70
|
+
const { frontmatter } = parseFrontmatter<{ description?: string }>(readFileSync(filePath, "utf-8"));
|
|
71
|
+
return firstSentence(frontmatter.description ?? "");
|
|
72
|
+
} catch {
|
|
73
|
+
return "";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function normalizeQuery(text: string): string {
|
|
78
|
+
return text
|
|
79
|
+
.toLowerCase()
|
|
80
|
+
.replace(/[^a-z0-9\s-]/g, " ")
|
|
81
|
+
.replace(/\s+/g, " ")
|
|
82
|
+
.trim();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function tokenize(text: string): string[] {
|
|
86
|
+
return normalizeQuery(text).split(/\s+/).filter(Boolean);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Words that carry no routing signal. */
|
|
90
|
+
const STOPWORDS = new Set([
|
|
91
|
+
"the", "a", "an", "to", "of", "for", "with", "and", "or", "use", "using", "me", "my", "i",
|
|
92
|
+
"please", "help", "can", "you", "do", "does", "is", "are", "on", "in", "at", "by", "from",
|
|
93
|
+
"this", "that", "it", "its", "want", "need", "run", "call", "invoke", "find", "show", "list",
|
|
94
|
+
"get", "load", "activate", "discover", "capability", "tool", "skill", "the", "a",
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
export interface RouteResult {
|
|
98
|
+
action: "activate" | "recommend" | "hint" | "none";
|
|
99
|
+
entry?: CatalogEntry;
|
|
100
|
+
candidates?: CatalogEntry[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Deterministic routing over the compact catalog.
|
|
105
|
+
*
|
|
106
|
+
* - Exact name/alias match scores 3; prefix match scores 2; each content token
|
|
107
|
+
* shared with the name/alias or summary adds 1.
|
|
108
|
+
* - A unique tool with score >= 3 is auto-activated.
|
|
109
|
+
* - A unique skill with score >= 3 is recommended (never auto-loaded).
|
|
110
|
+
* - Score 2 or a tie at the top produces a catalog-discovery hint.
|
|
111
|
+
* - No signal produces no hint at all.
|
|
112
|
+
*/
|
|
113
|
+
export function route(query: string, catalog: CatalogEntry[]): RouteResult {
|
|
114
|
+
const q = normalizeQuery(query);
|
|
115
|
+
if (!q) return { action: "none" };
|
|
116
|
+
const contentTokens = tokenize(q).filter((token) => !STOPWORDS.has(token));
|
|
117
|
+
if (contentTokens.length === 0) return { action: "none" };
|
|
118
|
+
|
|
119
|
+
const scored = catalog.map((entry) => {
|
|
120
|
+
const names = [entry.name, ...entry.aliases].map(normalizeQuery);
|
|
121
|
+
const nameTokens = new Set(names.flatMap((name) => name.split(/\s+/)));
|
|
122
|
+
const summaryTokens = new Set(tokenize(entry.summary));
|
|
123
|
+
let score = 0;
|
|
124
|
+
for (const name of names) {
|
|
125
|
+
if (name === q) score = Math.max(score, 3);
|
|
126
|
+
else if (name.startsWith(q) || q.startsWith(name)) score = Math.max(score, 2);
|
|
127
|
+
}
|
|
128
|
+
for (const token of contentTokens) {
|
|
129
|
+
if (nameTokens.has(token)) score += 3;
|
|
130
|
+
else if (token.length >= 3 && [...nameTokens].some((nameToken) => nameToken.startsWith(token))) score += 2;
|
|
131
|
+
if (summaryTokens.has(token)) score += 1;
|
|
132
|
+
}
|
|
133
|
+
return { entry, score };
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const best = Math.max(0, ...scored.map((s) => s.score));
|
|
137
|
+
if (best === 0) return { action: "none" };
|
|
138
|
+
const top = scored.filter((s) => s.score === best);
|
|
139
|
+
if (top.length > 1) {
|
|
140
|
+
return { action: "hint", candidates: top.map((s) => s.entry) };
|
|
141
|
+
}
|
|
142
|
+
const winner = top[0]!.entry;
|
|
143
|
+
if (best >= 3) {
|
|
144
|
+
return { action: winner.kind === "tool" ? "activate" : "recommend", entry: winner };
|
|
145
|
+
}
|
|
146
|
+
if (best >= 2) {
|
|
147
|
+
return { action: "hint", candidates: [winner] };
|
|
148
|
+
}
|
|
149
|
+
return { action: "none" };
|
|
150
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import type { ResolvedSkillInfo, ToolInfo } from "@selesai/code";
|
|
7
|
+
import {
|
|
8
|
+
buildSkillCatalog,
|
|
9
|
+
buildToolCatalog,
|
|
10
|
+
firstSentence,
|
|
11
|
+
route,
|
|
12
|
+
toolSummary,
|
|
13
|
+
type CatalogEntry,
|
|
14
|
+
} from "./catalog.ts";
|
|
15
|
+
|
|
16
|
+
const tool = (overrides: Partial<ToolInfo>): ToolInfo =>
|
|
17
|
+
({
|
|
18
|
+
name: "grep_app_search",
|
|
19
|
+
description: "Search public GitHub code through grep.app. Returns one page.",
|
|
20
|
+
parameters: {} as never,
|
|
21
|
+
sourceInfo: { path: "/ext/index.ts", source: "local", scope: "user", origin: "top-level" },
|
|
22
|
+
...overrides,
|
|
23
|
+
}) as ToolInfo;
|
|
24
|
+
|
|
25
|
+
const skill = (overrides: Partial<ResolvedSkillInfo>): ResolvedSkillInfo => ({
|
|
26
|
+
name: "research",
|
|
27
|
+
description: "Investigate a question against high-trust primary sources.",
|
|
28
|
+
filePath: "/skills/research/SKILL.md",
|
|
29
|
+
scope: "user",
|
|
30
|
+
disableModelInvocation: false,
|
|
31
|
+
...overrides,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("catalog metadata", () => {
|
|
35
|
+
it("uses explicit discovery summary over the description first sentence", () => {
|
|
36
|
+
const t = tool({ discovery: { summary: "Search public GitHub code" } });
|
|
37
|
+
expect(toolSummary(t)).toBe("Search public GitHub code");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("falls back to the first sentence of the description", () => {
|
|
41
|
+
expect(toolSummary(tool({}))).toBe("Search public GitHub code through grep.app.");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("firstSentence handles short text without punctuation", () => {
|
|
45
|
+
expect(firstSentence("no punctuation here")).toBe("no punctuation here");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("builds a tool catalog excluding built-ins and gateway tools", () => {
|
|
49
|
+
const tools = [
|
|
50
|
+
tool({ name: "read" }),
|
|
51
|
+
tool({ name: "capability_catalog" }),
|
|
52
|
+
tool({ name: "grep_app_search", discovery: { aliases: ["github-search"], category: "web" } }),
|
|
53
|
+
];
|
|
54
|
+
const entries = buildToolCatalog(tools, new Set(["capability_catalog"]));
|
|
55
|
+
expect(entries.map((e) => e.name)).toEqual(["grep_app_search"]);
|
|
56
|
+
expect(entries[0]!.aliases).toEqual(["github-search"]);
|
|
57
|
+
expect(entries[0]!.category).toBe("web");
|
|
58
|
+
expect(entries[0]!.kind).toBe("tool");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("builds a skill catalog excluding disable-model-invocation skills", () => {
|
|
62
|
+
const entries = buildSkillCatalog([
|
|
63
|
+
skill({ name: "research" }),
|
|
64
|
+
skill({ name: "secret", disableModelInvocation: true }),
|
|
65
|
+
]);
|
|
66
|
+
expect(entries.map((e) => e.name)).toEqual(["research"]);
|
|
67
|
+
expect(entries[0]!.kind).toBe("skill");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("falls back to the file frontmatter description when the resolved description is empty", () => {
|
|
71
|
+
const dir = mkdtempSync(join(tmpdir(), "gw-skill-"));
|
|
72
|
+
const filePath = join(dir, "SKILL.md");
|
|
73
|
+
writeFileSync(
|
|
74
|
+
filePath,
|
|
75
|
+
"---\nname: brandkit\ndescription: Premium brand-kit image generation skill for creating high-end brand-guidelines boards.\n---\nbody\n",
|
|
76
|
+
);
|
|
77
|
+
try {
|
|
78
|
+
const entries = buildSkillCatalog([skill({ name: "brandkit", description: "", filePath })]);
|
|
79
|
+
expect(entries[0]!.summary).toBe("Premium brand-kit image generation skill for creating high-end brand-guidelines boards.");
|
|
80
|
+
} finally {
|
|
81
|
+
rmSync(dir, { recursive: true, force: true });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("deterministic routing", () => {
|
|
87
|
+
const catalog: CatalogEntry[] = [
|
|
88
|
+
{ name: "grep_app_search", kind: "tool", summary: "Search public GitHub code", aliases: ["github-search"], category: "web" },
|
|
89
|
+
{ name: "grep_app_fetch", kind: "tool", summary: "Fetch a public GitHub file", aliases: [], category: "web" },
|
|
90
|
+
{ name: "research", kind: "skill", summary: "Investigate a question against primary sources", aliases: [], category: "research" },
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
it("auto-activates a unique high-confidence tool match", () => {
|
|
94
|
+
const result = route("search github code with grep_app_search", catalog);
|
|
95
|
+
expect(result.action).toBe("activate");
|
|
96
|
+
expect(result.entry?.name).toBe("grep_app_search");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("matches by alias", () => {
|
|
100
|
+
const result = route("use github-search", catalog);
|
|
101
|
+
expect(result.action).toBe("activate");
|
|
102
|
+
expect(result.entry?.name).toBe("grep_app_search");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("recommends a unique high-confidence skill without auto-loading", () => {
|
|
106
|
+
const result = route("do research on this topic", catalog);
|
|
107
|
+
expect(result.action).toBe("recommend");
|
|
108
|
+
expect(result.entry?.name).toBe("research");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("returns a hint for ambiguous matches", () => {
|
|
112
|
+
const result = route("grep app", catalog);
|
|
113
|
+
expect(result.action).toBe("hint");
|
|
114
|
+
expect(result.candidates?.length).toBeGreaterThan(1);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("returns none for unrelated prompts", () => {
|
|
118
|
+
expect(route("refactor the auth module", catalog).action).toBe("none");
|
|
119
|
+
expect(route("", catalog).action).toBe("none");
|
|
120
|
+
expect(route(" ", catalog).action).toBe("none");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("is deterministic for the same input", () => {
|
|
124
|
+
const a = route("search github code", catalog);
|
|
125
|
+
const b = route("search github code", catalog);
|
|
126
|
+
expect(a).toEqual(b);
|
|
127
|
+
});
|
|
128
|
+
});
|