@phi-code-admin/phi-code 0.57.7 → 0.57.9

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.
@@ -1,335 +1,118 @@
1
1
  /**
2
2
  * Skill Loader Extension - Dynamic skill loading and context injection
3
3
  *
4
- * Automatically discovers and loads skills from ~/.phi/agent/skills/ and .phi/skills/
5
- * directories. Skills are folders containing SKILL.md files with specialized knowledge
6
- * or procedures. When skill-related keywords are detected in user input, the skill
7
- * content is automatically injected into the conversation context.
4
+ * Uses sigma-skills SkillScanner and SkillLoader for skill discovery and matching.
5
+ * Skills are folders containing SKILL.md files with specialized knowledge.
6
+ * When skill-related keywords are detected in user input, the model is notified
7
+ * and can load the full skill content via the `read` tool.
8
+ *
9
+ * Discovery locations (in priority order):
10
+ * 1. .phi/skills/ (project-local, highest priority)
11
+ * 2. ~/.phi/agent/skills/ (global user skills)
12
+ * 3. Bundled skills shipped with the package (lowest priority)
8
13
  *
9
14
  * Features:
10
15
  * - Automatic skill discovery at startup
11
16
  * - Keyword-based skill detection and loading
12
17
  * - /skills command to list available skills
13
- * - Contextual skill injection via ui notifications
14
- *
15
- * Usage:
16
- * 1. Copy to packages/coding-agent/extensions/phi/skill-loader.ts
17
- * 2. Create skill directories with SKILL.md files
18
- * 3. Skills auto-load when relevant keywords are detected
18
+ * - Contextual skill notification via ui.notify
19
19
  */
20
20
 
21
21
  import type { ExtensionAPI, ExtensionContext } from "phi-code";
22
- import { readdir, readFile, access, stat } from "node:fs/promises";
22
+ import { SkillScanner, SkillLoader } from "sigma-skills";
23
+ import type { SkillsConfig } from "sigma-skills";
23
24
  import { join } from "node:path";
24
25
  import { homedir } from "node:os";
25
26
 
26
- interface Skill {
27
- name: string;
28
- path: string;
29
- content: string;
30
- keywords: string[];
31
- description: string;
32
- source: "global" | "local";
33
- }
34
-
35
27
  export default function skillLoaderExtension(pi: ExtensionAPI) {
36
- let availableSkills: Skill[] = [];
37
- const globalSkillsDir = join(homedir(), ".phi", "agent", "skills");
38
- const localSkillsDir = join(process.cwd(), ".phi", "skills");
39
-
40
- /**
41
- * Extract keywords and description from SKILL.md content
42
- */
43
- function parseSkillContent(content: string): { keywords: string[]; description: string } {
44
- const lines = content.split('\n');
45
- const keywords: string[] = [];
46
- let description = "";
28
+ const config: SkillsConfig = {
29
+ globalDir: join(homedir(), ".phi", "agent", "skills"),
30
+ projectDir: join(process.cwd(), ".phi", "skills"),
31
+ bundledDir: join(__dirname, "..", "..", "..", "skills"),
32
+ autoInject: true,
33
+ };
47
34
 
48
- // Look for keywords in frontmatter or explicit sections
49
- let inFrontmatter = false;
50
- let foundKeywords = false;
35
+ const scanner = new SkillScanner(config);
36
+ const loader = new SkillLoader(scanner);
51
37
 
52
- for (const line of lines) {
53
- const trimmed = line.trim();
54
-
55
- // YAML frontmatter
56
- if (trimmed === '---') {
57
- inFrontmatter = !inFrontmatter;
58
- continue;
59
- }
60
-
61
- if (inFrontmatter) {
62
- if (trimmed.startsWith('keywords:') || trimmed.startsWith('tags:')) {
63
- const keywordLine = trimmed.split(':')[1];
64
- if (keywordLine) {
65
- // Parse YAML array or comma-separated
66
- const parsed = keywordLine
67
- .replace(/[\[\]]/g, '')
68
- .split(',')
69
- .map(k => k.trim().toLowerCase())
70
- .filter(k => k.length > 0);
71
- keywords.push(...parsed);
72
- foundKeywords = true;
73
- }
74
- }
75
- if (trimmed.startsWith('description:')) {
76
- description = trimmed.split(':', 2)[1]?.trim() || "";
77
- }
78
- continue;
79
- }
80
-
81
- // Look for explicit keywords section
82
- if (trimmed.toLowerCase().includes('keywords:') || trimmed.toLowerCase().includes('tags:')) {
83
- const keywordText = trimmed.split(':')[1];
84
- if (keywordText) {
85
- const parsed = keywordText
86
- .split(',')
87
- .map(k => k.trim().toLowerCase())
88
- .filter(k => k.length > 0);
89
- keywords.push(...parsed);
90
- foundKeywords = true;
91
- }
92
- continue;
93
- }
94
-
95
- // Use first heading as description if none found
96
- if (!description && trimmed.startsWith('#')) {
97
- description = trimmed.replace(/^#+\s*/, '').trim();
98
- }
99
- }
100
-
101
- // If no explicit keywords, derive from content
102
- if (!foundKeywords) {
103
- const contentLower = content.toLowerCase();
104
- const commonKeywords = [
105
- 'docker', 'kubernetes', 'aws', 'azure', 'gcp', 'terraform',
106
- 'python', 'javascript', 'typescript', 'react', 'node',
107
- 'git', 'github', 'gitlab', 'ci/cd', 'devops',
108
- 'database', 'postgresql', 'mysql', 'mongodb', 'redis',
109
- 'api', 'rest', 'graphql', 'microservices',
110
- 'security', 'auth', 'oauth', 'jwt',
111
- 'test', 'testing', 'unit', 'integration',
112
- 'deploy', 'deployment', 'production'
113
- ];
114
-
115
- for (const keyword of commonKeywords) {
116
- if (contentLower.includes(keyword)) {
117
- keywords.push(keyword);
118
- }
119
- }
120
-
121
- // Also include skill directory name as keyword
122
- const skillName = description.toLowerCase().replace(/\s+/g, '-');
123
- if (skillName) keywords.push(skillName);
124
- }
125
-
126
- return { keywords, description };
127
- }
128
-
129
- /**
130
- * Load skills from a directory
131
- */
132
- async function loadSkillsFromDirectory(directory: string, source: "global" | "local"): Promise<Skill[]> {
133
- const skills: Skill[] = [];
134
-
135
- try {
136
- await access(directory);
137
- const entries = await readdir(directory);
138
-
139
- for (const entry of entries) {
140
- const skillPath = join(directory, entry);
141
-
142
- try {
143
- const skillStat = await stat(skillPath);
144
- if (!skillStat.isDirectory()) continue;
145
-
146
- const skillFilePath = join(skillPath, "SKILL.md");
147
-
148
- try {
149
- await access(skillFilePath);
150
- const content = await readFile(skillFilePath, 'utf-8');
151
- const { keywords, description } = parseSkillContent(content);
152
-
153
- skills.push({
154
- name: entry,
155
- path: skillPath,
156
- content,
157
- keywords,
158
- description: description || entry,
159
- source
160
- });
161
-
162
- } catch {
163
- // No SKILL.md file, skip this directory
164
- }
165
- } catch {
166
- // Can't access directory, skip
167
- }
168
- }
169
- } catch {
170
- // Directory doesn't exist, return empty array
171
- }
38
+ // ─── Input Event: Match skills to user input ─────────────────────
172
39
 
173
- return skills;
174
- }
175
-
176
- /**
177
- * Load all available skills
178
- */
179
- async function loadAllSkills(): Promise<void> {
180
- const globalSkills = await loadSkillsFromDirectory(globalSkillsDir, "global");
181
- const localSkills = await loadSkillsFromDirectory(localSkillsDir, "local");
182
-
183
- availableSkills = [...globalSkills, ...localSkills];
184
-
185
- console.log(`Loaded ${availableSkills.length} skills (${globalSkills.length} global, ${localSkills.length} local)`);
186
- }
187
-
188
- /**
189
- * Find relevant skills for input text
190
- */
191
- function findRelevantSkills(text: string): Skill[] {
192
- const textLower = text.toLowerCase();
193
- const relevantSkills: Array<{ skill: Skill; matchCount: number }> = [];
194
-
195
- for (const skill of availableSkills) {
196
- let matches = 0;
197
-
198
- // Check for keyword matches
199
- for (const keyword of skill.keywords) {
200
- if (textLower.includes(keyword)) {
201
- matches++;
202
- }
203
- }
204
-
205
- // Check for skill name match
206
- if (textLower.includes(skill.name.toLowerCase())) {
207
- matches += 2; // Name match gets higher weight
208
- }
209
-
210
- if (matches > 0) {
211
- relevantSkills.push({ skill, matchCount: matches });
212
- }
213
- }
214
-
215
- // Sort by match count (most relevant first) and return top 3
216
- return relevantSkills
217
- .sort((a, b) => b.matchCount - a.matchCount)
218
- .slice(0, 3)
219
- .map(item => item.skill);
220
- }
221
-
222
- /**
223
- * Input interceptor for skill detection
224
- */
225
40
  pi.on("input", async (event, ctx) => {
226
- // Skip if this is an extension-generated message
227
41
  if (event.source === "extension") {
228
42
  return { action: "continue" };
229
43
  }
230
44
 
231
- // Find relevant skills
232
- const relevantSkills = findRelevantSkills(event.text);
45
+ const matches = loader.findRelevantSkills(event.text);
233
46
 
234
- if (relevantSkills.length > 0) {
235
- // Inject skill content into context
236
- for (const skill of relevantSkills) {
237
- const skillMessage = `Skill Context: ${skill.name}
238
-
239
- ${skill.content}
240
-
241
- ---
242
- This skill was automatically loaded based on your request. Use this knowledge to assist with the task.`;
243
-
244
- // Notify the model about the relevant skill content
245
- // The model can then use the `read` tool to load the full SKILL.md
246
- ctx.ui.notify(`📚 Relevant skill loaded: **${skill.name}**\n${skill.description}\nUse \`read ${skill.path}\` for full content.`, "info");
47
+ if (matches.length > 0) {
48
+ // Notify about top matches (max 3)
49
+ const topMatches = matches.slice(0, 3);
50
+ for (const match of topMatches) {
51
+ ctx.ui.notify(
52
+ `📚 Relevant skill: **${match.skill.name}** — ${match.skill.description}\nUse \`read ${match.skill.path}/SKILL.md\` for full content.`,
53
+ "info"
54
+ );
247
55
  }
248
56
 
249
- // Notify user which skills were loaded
250
- const skillNames = relevantSkills.map(s => s.name).join(", ");
251
- ctx.ui.notify(`🧠 Loaded ${relevantSkills.length} skill(s): ${skillNames}`, "info");
57
+ const skillNames = topMatches.map(m => m.skill.name).join(", ");
58
+ ctx.ui.notify(`🧠 Loaded ${topMatches.length} skill(s): ${skillNames}`, "info");
252
59
  }
253
60
 
254
61
  return { action: "continue" };
255
62
  });
256
63
 
257
- /**
258
- * /skills command - List available skills
259
- */
64
+ // ─── /skills Command ─────────────────────────────────────────────
65
+
260
66
  pi.registerCommand("skills", {
261
- description: "List available skills or show skill details",
262
- getArgumentCompletions: (prefix) => {
263
- const skillNames = availableSkills.map(s => s.name);
264
- const filtered = skillNames.filter(name => name.toLowerCase().startsWith(prefix.toLowerCase()));
265
- return filtered.length > 0 ? filtered.map(name => ({ value: name, label: name })) : null;
266
- },
67
+ description: "List available skills or show details for a specific skill",
267
68
  handler: async (args, ctx) => {
268
- const skillName = args.trim();
69
+ const query = args.trim();
269
70
 
270
- if (!skillName) {
71
+ if (!query) {
271
72
  // List all skills
272
- if (availableSkills.length === 0) {
273
- ctx.ui.notify("No skills found. Create skill directories with SKILL.md files in:\n- ~/.phi/agent/skills/\n- .phi/skills/", "info");
73
+ const skills = loader.listSkills();
74
+
75
+ if (skills.length === 0) {
76
+ ctx.ui.notify(
77
+ "No skills found. Create skill directories with SKILL.md files in:\n" +
78
+ `- \`${config.projectDir}\` (project-local)\n` +
79
+ `- \`${config.globalDir}\` (global)\n` +
80
+ "Or install bundled skills via `/phi-init`.",
81
+ "info"
82
+ );
274
83
  return;
275
84
  }
276
85
 
277
- let message = `🧠 **Available Skills** (${availableSkills.length})\n\n`;
278
-
279
- // Group by source
280
- const globalSkills = availableSkills.filter(s => s.source === "global");
281
- const localSkills = availableSkills.filter(s => s.source === "local");
282
-
283
- if (globalSkills.length > 0) {
284
- message += "**Global Skills:**\n";
285
- globalSkills.forEach(skill => {
286
- const keywords = skill.keywords.slice(0, 3).join(", ");
287
- message += `- **${skill.name}** - ${skill.description}\n`;
288
- message += ` Keywords: ${keywords}\n\n`;
289
- });
290
- }
291
-
292
- if (localSkills.length > 0) {
293
- message += "**Project Skills:**\n";
294
- localSkills.forEach(skill => {
295
- const keywords = skill.keywords.slice(0, 3).join(", ");
296
- message += `- **${skill.name}** - ${skill.description}\n`;
297
- message += ` Keywords: ${keywords}\n\n`;
298
- });
86
+ let message = `**📚 Available Skills (${skills.length}):**\n\n`;
87
+ for (const skill of skills) {
88
+ message += ` **${skill.name}** — ${skill.description}\n`;
89
+ message += ` 📁 \`${skill.path}\`\n`;
299
90
  }
300
-
301
- message += "\nUse `/skills <name>` to view a specific skill.";
302
-
91
+ message += `\nUse \`/skills <name>\` for details.`;
303
92
  ctx.ui.notify(message, "info");
304
-
305
93
  } else {
306
94
  // Show specific skill
307
- const skill = availableSkills.find(s => s.name.toLowerCase() === skillName.toLowerCase());
308
-
309
- if (!skill) {
310
- ctx.ui.notify(`Skill "${skillName}" not found. Use /skills to list available skills.`, "warning");
311
- return;
95
+ const content = loader.getSkillContext(query);
96
+ if (content) {
97
+ const skill = loader.listSkills().find(s => s.name === query);
98
+ ctx.ui.notify(
99
+ `**📚 Skill: ${query}**\n\n` +
100
+ `Path: \`${skill?.path || "unknown"}\`\n` +
101
+ `Keywords: ${skill?.keywords.slice(0, 10).join(", ") || "none"}\n\n` +
102
+ `---\n\n${content.slice(0, 2000)}${content.length > 2000 ? "\n\n... (truncated, use `read` for full content)" : ""}`,
103
+ "info"
104
+ );
105
+ } else {
106
+ ctx.ui.notify(`Skill "${query}" not found. Use \`/skills\` to list available skills.`, "warning");
312
107
  }
313
-
314
- const message = `🧠 **Skill: ${skill.name}**
315
-
316
- **Description:** ${skill.description}
317
- **Source:** ${skill.source}
318
- **Path:** ${skill.path}
319
- **Keywords:** ${skill.keywords.join(", ")}
320
-
321
- **Content Preview:**
322
- ${skill.content.slice(0, 500)}${skill.content.length > 500 ? "..." : ""}`;
323
-
324
- ctx.ui.notify(message, "info");
325
108
  }
326
109
  },
327
110
  });
328
111
 
329
- /**
330
- * Load skills on session start
331
- */
112
+ // ─── Session Start: Load skills ──────────────────────────────────
113
+
332
114
  pi.on("session_start", async (_event, _ctx) => {
333
- await loadAllSkills();
115
+ const skills = loader.listSkills();
116
+ console.log(`[skill-loader] Loaded ${skills.length} skills from 3 locations`);
334
117
  });
335
- }
118
+ }
@@ -1,105 +1,35 @@
1
1
  /**
2
2
  * Smart Router Extension - Intelligent model routing for different task types
3
3
  *
4
- * Analyzes user input keywords and suggests the optimal model:
5
- * - code: implement, create, build, refactor qwen3-coder-plus
6
- * - debug: fix, bug, error, crash → qwen3-max-2026-01-23
7
- * - explore: read, analyze, explain, understand → kimi-k2.5
8
- * - plan: plan, design, architect, spec → qwen3-max-2026-01-23
9
- * - test: test, verify, validate, check → kimi-k2.5
10
- * - review: review, audit, quality, security → qwen3.5-plus
4
+ * Uses sigma-agents SmartRouter for task classification and model recommendation.
5
+ * Analyzes user input keywords and suggests the optimal model per task category.
11
6
  *
12
- * Configuration: ~/.phi/agent/routing.json (same format as config/routing.json)
7
+ * Configuration: ~/.phi/agent/routing.json
13
8
  * Command: /routing — show config, enable/disable, test, reload
14
9
  */
15
10
 
16
11
  import type { ExtensionAPI } from "phi-code";
12
+ import { SmartRouter } from "sigma-agents";
13
+ import type { RoutingConfig, TaskCategory } from "sigma-agents";
17
14
  import { readFile, mkdir, writeFile, access } from "node:fs/promises";
18
15
  import { join } from "node:path";
19
16
  import { homedir } from "node:os";
20
17
 
21
- // ─── Types ───────────────────────────────────────────────────────────────
18
+ // ─── Extension Config ────────────────────────────────────────────────────
22
19
 
23
- interface RouteEntry {
24
- description: string;
25
- keywords: string[];
26
- preferredModel: string;
27
- fallback: string;
28
- agent: string;
29
- }
30
-
31
- interface RoutingConfig {
32
- routes: Record<string, RouteEntry>;
33
- default: { model: string; agent: string | null };
34
- }
35
-
36
- interface FullConfig {
37
- routing: RoutingConfig;
20
+ interface ExtensionConfig {
38
21
  enabled: boolean;
39
22
  notifyOnRecommendation: boolean;
40
23
  }
41
24
 
42
- // ─── Defaults (aligned with config/routing.json and default-models.json) ──
43
-
44
- const DEFAULT_ROUTING: RoutingConfig = {
45
- routes: {
46
- code: {
47
- description: "Code generation, implementation, refactoring",
48
- keywords: ["implement", "create", "build", "refactor", "write", "add", "modify", "update", "generate", "code", "develop", "function", "class"],
49
- preferredModel: "qwen3-coder-plus",
50
- fallback: "qwen3.5-plus",
51
- agent: "code",
52
- },
53
- debug: {
54
- description: "Debugging, fixing, error resolution",
55
- keywords: ["fix", "bug", "error", "debug", "crash", "broken", "failing", "issue", "troubleshoot", "repair", "solve"],
56
- preferredModel: "qwen3-max-2026-01-23",
57
- fallback: "qwen3.5-plus",
58
- agent: "code",
59
- },
60
- explore: {
61
- description: "Code reading, analysis, understanding",
62
- keywords: ["read", "analyze", "explain", "understand", "find", "search", "look", "show", "what", "how", "explore", "examine"],
63
- preferredModel: "kimi-k2.5",
64
- fallback: "glm-4.7",
65
- agent: "explore",
66
- },
67
- plan: {
68
- description: "Architecture, design, planning",
69
- keywords: ["plan", "design", "architect", "spec", "structure", "organize", "strategy", "approach", "roadmap"],
70
- preferredModel: "qwen3-max-2026-01-23",
71
- fallback: "qwen3.5-plus",
72
- agent: "plan",
73
- },
74
- test: {
75
- description: "Testing, validation, verification",
76
- keywords: ["test", "verify", "validate", "check", "assert", "coverage", "unit", "integration", "e2e"],
77
- preferredModel: "kimi-k2.5",
78
- fallback: "glm-4.7",
79
- agent: "test",
80
- },
81
- review: {
82
- description: "Code review, quality assessment",
83
- keywords: ["review", "audit", "quality", "security", "improve", "optimize", "refine", "critique"],
84
- preferredModel: "qwen3.5-plus",
85
- fallback: "qwen3-max-2026-01-23",
86
- agent: "review",
87
- },
88
- },
89
- default: {
90
- model: "qwen3.5-plus",
91
- agent: null,
92
- },
93
- };
94
-
95
25
  // ─── Extension ───────────────────────────────────────────────────────────
96
26
 
97
27
  export default function smartRouterExtension(pi: ExtensionAPI) {
98
28
  const configDir = join(homedir(), ".phi", "agent");
99
29
  const configPath = join(configDir, "routing.json");
100
30
 
101
- let config: FullConfig = {
102
- routing: DEFAULT_ROUTING,
31
+ let router = new SmartRouter(SmartRouter.defaultConfig());
32
+ let extConfig: ExtensionConfig = {
103
33
  enabled: true,
104
34
  notifyOnRecommendation: true,
105
35
  };
@@ -109,99 +39,54 @@ export default function smartRouterExtension(pi: ExtensionAPI) {
109
39
  */
110
40
  async function loadConfig() {
111
41
  try {
112
- await access(configPath);
113
- const text = await readFile(configPath, "utf-8");
114
- const userConfig = JSON.parse(text);
115
-
116
- // Support both flat format (routes at top level) and wrapped format
117
- if (userConfig.routes) {
118
- config.routing = {
119
- routes: { ...DEFAULT_ROUTING.routes, ...userConfig.routes },
120
- default: userConfig.default || DEFAULT_ROUTING.default,
121
- };
122
- }
123
-
124
- if (typeof userConfig.enabled === "boolean") config.enabled = userConfig.enabled;
125
- if (typeof userConfig.notifyOnRecommendation === "boolean") config.notifyOnRecommendation = userConfig.notifyOnRecommendation;
42
+ const config = await SmartRouter.loadConfig(configPath);
43
+ router = new SmartRouter(config);
126
44
  } catch {
127
45
  // No config file — use defaults, and save them for reference
128
46
  try {
129
47
  await mkdir(configDir, { recursive: true });
130
- await writeFile(configPath, JSON.stringify(DEFAULT_ROUTING, null, 2), "utf-8");
48
+ const defaultConfig = SmartRouter.defaultConfig();
49
+ await writeFile(configPath, JSON.stringify(defaultConfig, null, 2), "utf-8");
131
50
  } catch {
132
51
  // Can't write, that's fine
133
52
  }
134
53
  }
135
54
  }
136
55
 
137
- /**
138
- * Analyze input text to classify task type
139
- */
140
- function classifyTask(text: string): { category: string | null; confidence: number; matches: string[]; route: RouteEntry | null } {
141
- const lower = text.toLowerCase();
142
- const results: Array<{ category: string; confidence: number; matches: string[]; route: RouteEntry }> = [];
143
-
144
- for (const [category, route] of Object.entries(config.routing.routes)) {
145
- const matches: string[] = [];
146
-
147
- for (const keyword of route.keywords) {
148
- if (lower.includes(keyword.toLowerCase())) {
149
- matches.push(keyword);
150
- }
151
- }
152
-
153
- if (matches.length > 0) {
154
- // Confidence = weighted match ratio
155
- // More matches = higher confidence, but cap at 95%
156
- const ratio = matches.length / route.keywords.length;
157
- const confidence = Math.min(95, Math.round(ratio * 100 + matches.length * 5));
158
- results.push({ category, confidence, matches, route });
159
- }
160
- }
161
-
162
- if (results.length === 0) {
163
- return { category: null, confidence: 0, matches: [], route: null };
164
- }
165
-
166
- // Highest confidence wins
167
- results.sort((a, b) => b.confidence - a.confidence);
168
- return results[0];
169
- }
170
-
171
- // ─── Input Event ─────────────────────────────────────────────────
172
-
173
56
  /**
174
57
  * Resolve a model name to an available model.
175
58
  * If the preferred model exists in the registry, use it.
176
59
  * Otherwise, fall back to the current model.
177
60
  */
178
61
  function resolveModel(preferredModel: string, ctx: any): string {
179
- // Check if model exists in registry
180
62
  try {
181
63
  const available = ctx.modelRegistry?.getAvailable?.() || [];
182
64
  if (available.some((m: any) => m.id === preferredModel)) {
183
65
  return preferredModel;
184
66
  }
185
- // Model not available — use current model
186
67
  return ctx.model?.id || preferredModel;
187
68
  } catch {
188
69
  return ctx.model?.id || preferredModel;
189
70
  }
190
71
  }
191
72
 
73
+ // ─── Input Event ─────────────────────────────────────────────────
74
+
192
75
  pi.on("input", async (event, ctx) => {
193
- if (!config.enabled || event.source === "extension") {
76
+ if (!extConfig.enabled || event.source === "extension") {
194
77
  return { action: "continue" };
195
78
  }
196
79
 
197
- const result = classifyTask(event.text);
80
+ const recommendation = router.getRecommendation(event.text);
198
81
 
199
- if (result.category && result.confidence >= 25 && result.route) {
200
- if (config.notifyOnRecommendation) {
201
- const resolved = resolveModel(result.route.preferredModel, ctx);
202
- const suffix = resolved !== result.route.preferredModel ? ` (→ ${resolved})` : "";
82
+ if (recommendation.category !== "general") {
83
+ if (extConfig.notifyOnRecommendation) {
84
+ const resolved = resolveModel(recommendation.model, ctx);
85
+ const suffix = resolved !== recommendation.model ? ` (→ ${resolved})` : "";
86
+ const route = (router as any).config?.routes?.[recommendation.category];
87
+ const desc = route?.keywords?.slice(0, 3)?.join(", ") || recommendation.category;
203
88
  ctx.ui.notify(
204
- `🔀 ${result.route.description} → \`${result.route.preferredModel}\`${suffix} (${result.confidence}% | ${result.matches.join(", ")})`,
89
+ `🔀 ${recommendation.category} → \`${recommendation.model}\`${suffix} (${desc})`,
205
90
  "info"
206
91
  );
207
92
  }
@@ -218,17 +103,17 @@ export default function smartRouterExtension(pi: ExtensionAPI) {
218
103
  const arg = args.trim().toLowerCase();
219
104
 
220
105
  if (!arg) {
221
- // Show current config
222
- let output = `**🔀 Smart Router**\n\n`;
223
- output += `Status: ${config.enabled ? "✅ Enabled" : "❌ Disabled"}\n`;
224
- output += `Notifications: ${config.notifyOnRecommendation ? "On" : "Off"}\n\n`;
106
+ const routingConfig = (router as any).config as RoutingConfig;
107
+ let output = `**🔀 Smart Router** (powered by sigma-agents)\n\n`;
108
+ output += `Status: ${extConfig.enabled ? "✅ Enabled" : "❌ Disabled"}\n`;
109
+ output += `Notifications: ${extConfig.notifyOnRecommendation ? "On" : "Off"}\n\n`;
225
110
 
226
111
  output += `**Routes:**\n`;
227
- for (const [cat, route] of Object.entries(config.routing.routes)) {
228
- output += ` **${cat}** → \`${route.preferredModel}\` (fallback: \`${route.fallback}\`) [agent: ${route.agent}]\n`;
112
+ for (const [cat, route] of Object.entries(routingConfig.routes)) {
113
+ output += ` **${cat}** → \`${route.preferredModel}\` (fallback: \`${route.fallback}\`) [agent: ${route.agent || "none"}]\n`;
229
114
  output += ` Keywords: ${route.keywords.slice(0, 6).join(", ")}${route.keywords.length > 6 ? "..." : ""}\n`;
230
115
  }
231
- output += `\n **default** → \`${config.routing.default.model}\`\n`;
116
+ output += `\n **default** → \`${routingConfig.default.model}\`\n`;
232
117
 
233
118
  output += `\nConfig: \`${configPath}\``;
234
119
  output += `\nCommands: \`/routing enable|disable|notify-on|notify-off|reload|test\``;
@@ -239,19 +124,19 @@ export default function smartRouterExtension(pi: ExtensionAPI) {
239
124
 
240
125
  switch (arg) {
241
126
  case "enable":
242
- config.enabled = true;
127
+ extConfig.enabled = true;
243
128
  ctx.ui.notify("✅ Smart routing enabled.", "info");
244
129
  break;
245
130
  case "disable":
246
- config.enabled = false;
131
+ extConfig.enabled = false;
247
132
  ctx.ui.notify("❌ Smart routing disabled.", "info");
248
133
  break;
249
134
  case "notify-on":
250
- config.notifyOnRecommendation = true;
135
+ extConfig.notifyOnRecommendation = true;
251
136
  ctx.ui.notify("🔔 Routing notifications enabled.", "info");
252
137
  break;
253
138
  case "notify-off":
254
- config.notifyOnRecommendation = false;
139
+ extConfig.notifyOnRecommendation = false;
255
140
  ctx.ui.notify("🔕 Routing notifications disabled.", "info");
256
141
  break;
257
142
  case "reload":
@@ -271,10 +156,8 @@ export default function smartRouterExtension(pi: ExtensionAPI) {
271
156
 
272
157
  let output = "**🧪 Routing Test:**\n\n";
273
158
  for (const input of tests) {
274
- const result = classifyTask(input);
275
- const model = result.route?.preferredModel || config.routing.default.model;
276
- const tag = result.category || "default";
277
- output += `"${input}"\n → **${tag}** (${result.confidence}%) → \`${model}\`\n\n`;
159
+ const rec = router.getRecommendation(input);
160
+ output += `"${input}"\n → **${rec.category}** \`${rec.model}\`\n\n`;
278
161
  }
279
162
  ctx.ui.notify(output, "info");
280
163
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phi-code-admin/phi-code",
3
- "version": "0.57.7",
3
+ "version": "0.57.9",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "piConfig": {
@@ -43,10 +43,6 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "@mariozechner/jiti": "^2.6.2",
46
- "phi-code-agent": "^0.56.3",
47
- "phi-code-ai": "^0.56.3",
48
- "phi-code-tui": "^0.56.3",
49
- "sigma-memory": "^0.1.0",
50
46
  "@silvia-odwyer/photon-node": "^0.3.4",
51
47
  "chalk": "^5.5.0",
52
48
  "cli-highlight": "^2.1.11",
@@ -58,7 +54,13 @@
58
54
  "ignore": "^7.0.5",
59
55
  "marked": "^15.0.12",
60
56
  "minimatch": "^10.2.3",
57
+ "phi-code-agent": "^0.56.3",
58
+ "phi-code-ai": "^0.56.3",
59
+ "phi-code-tui": "^0.56.3",
61
60
  "proper-lockfile": "^4.1.2",
61
+ "sigma-agents": "^0.1.1",
62
+ "sigma-memory": "^0.1.0",
63
+ "sigma-skills": "^0.1.0",
62
64
  "strip-ansi": "^7.1.0",
63
65
  "undici": "^7.19.1",
64
66
  "yaml": "^2.8.2"