@hienlh/ppm 0.9.80 → 0.9.82
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/.opencode/.env.example +98 -0
- package/.opencode/skills/ads-management/scripts/.env.example +13 -0
- package/.opencode/skills/ai-multimodal/.env.example +230 -0
- package/.opencode/skills/cip-design/.env.example +6 -0
- package/.opencode/skills/devops/.env.example +76 -0
- package/.opencode/skills/docs-seeker/.env.example +15 -0
- package/.opencode/skills/elevenlabs/.env.example +3 -0
- package/.opencode/skills/marketing-dashboard/.env.example +15 -0
- package/.opencode/skills/marketing-dashboard/app/.env.example +2 -0
- package/.opencode/skills/marketing-dashboard/server/.env.example +2 -0
- package/.opencode/skills/mcp-management/scripts/dist/analyze-tools.js +70 -0
- package/.opencode/skills/mcp-management/scripts/dist/cli.js +160 -0
- package/.opencode/skills/mcp-management/scripts/dist/mcp-client.js +183 -0
- package/.opencode/skills/payment-integration/scripts/.env.example +20 -0
- package/.opencode/skills/sequential-thinking/.env.example +8 -0
- package/.repomixignore +22 -0
- package/AGENTS.md +62 -0
- package/CHANGELOG.md +17 -0
- package/CLAUDE.md +12 -0
- package/assets/skills/ppm-guide/SKILL.md +61 -0
- package/bun.lock +9 -1
- package/dist/web/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/web/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/web/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/web/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/web/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/web/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/web/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/web/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/web/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/web/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/web/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/web/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/web/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/web/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/web/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/web/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/web/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/web/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/web/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/web/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/web/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/web/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/web/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/web/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/web/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/web/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/web/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/web/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/web/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/web/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/web/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/web/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/web/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/web/assets/chat-tab-bS86TsT5.js +10 -0
- package/dist/web/assets/{code-editor-BFe-hnpF.js → code-editor-BaNaQ33b.js} +1 -1
- package/dist/web/assets/{database-viewer-BeY2V5QI.js → database-viewer-C5MVw8cJ.js} +1 -1
- package/dist/web/assets/{diff-viewer-D6xzs8PP.js → diff-viewer-CUbFMWVo.js} +1 -1
- package/dist/web/assets/{extension-webview-Cd1XYFXO.js → extension-webview-CwGufYEP.js} +1 -1
- package/dist/web/assets/{git-graph-D2XXpiMQ.js → git-graph-BD7A7MLo.js} +1 -1
- package/dist/web/assets/index-BYXjCNlK.css +2 -0
- package/dist/web/assets/index-CpzkPHOC.js +30 -0
- package/dist/web/assets/keybindings-store-DsaANvBz.js +1 -0
- package/dist/web/assets/markdown-renderer-C19IsITh.js +326 -0
- package/dist/web/assets/{port-forwarding-tab-B5rj_I66.js → port-forwarding-tab-BF79F1iL.js} +1 -1
- package/dist/web/assets/{postgres-viewer-DnlqzOnm.js → postgres-viewer-_nYiO_wp.js} +1 -1
- package/dist/web/assets/{settings-tab-CNZpuPD3.js → settings-tab-C1SQMbSu.js} +1 -1
- package/dist/web/assets/{sql-query-editor-Df2kzbPj.js → sql-query-editor-6OFvxxuN.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-Cj1G70z4.js → sqlite-viewer-SNVYFXvB.js} +1 -1
- package/dist/web/assets/{terminal-tab-Dv9A7Xe2.js → terminal-tab-BJEkmrDt.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-CPfIEo8t.js → use-monaco-theme-r8FzlCWr.js} +1 -1
- package/dist/web/index.html +2 -2
- package/dist/web/sw.js +1 -1
- package/docs/codebase-summary.md +78 -0
- package/docs/project-changelog.md +29 -0
- package/docs/system-architecture.md +2 -0
- package/package.json +5 -2
- package/release-manifest.json +15784 -0
- package/scripts/check-ppm-dir-usage.sh +21 -0
- package/scripts/generate-ppm-guide.ts +92 -0
- package/src/cli/commands/init.ts +2 -1
- package/src/cli/commands/logs.ts +11 -11
- package/src/cli/commands/report.ts +3 -2
- package/src/cli/commands/restart.ts +22 -23
- package/src/cli/commands/skills-cmd.ts +123 -0
- package/src/cli/commands/status.ts +7 -8
- package/src/cli/commands/stop.ts +18 -19
- package/src/index.ts +3 -0
- package/src/lib/account-crypto.ts +12 -7
- package/src/providers/claude-agent-sdk.ts +42 -11
- package/src/server/index.ts +8 -8
- package/src/server/routes/chat.ts +4 -2
- package/src/server/routes/upgrade.ts +3 -5
- package/src/server/ws/chat.ts +31 -0
- package/src/services/cloud-ws.service.ts +6 -3
- package/src/services/cloud.service.ts +20 -19
- package/src/services/cloudflared.service.ts +13 -13
- package/src/services/config.service.ts +5 -7
- package/src/services/db.service.ts +5 -6
- package/src/services/extension-rpc-handlers.ts +2 -2
- package/src/services/extension.service.ts +9 -12
- package/src/services/ppm-dir.ts +14 -0
- package/src/services/slash-discovery/builtin-commands.ts +53 -0
- package/src/services/slash-discovery/builtin-handlers.ts +65 -0
- package/src/services/slash-discovery/definition-source.ts +27 -0
- package/src/services/slash-discovery/discover-skill-roots.ts +128 -0
- package/src/services/slash-discovery/fuzzy-search.ts +76 -0
- package/src/services/slash-discovery/index.ts +42 -0
- package/src/services/slash-discovery/resolve-overrides.ts +41 -0
- package/src/services/slash-discovery/skill-loader.ts +156 -0
- package/src/services/slash-discovery/types.ts +51 -0
- package/src/services/slash-items.service.ts +4 -182
- package/src/services/supervisor-state.ts +14 -15
- package/src/services/supervisor-stopped-page.ts +2 -4
- package/src/services/supervisor.ts +15 -15
- package/src/services/tunnel.service.ts +22 -5
- package/src/services/upgrade.service.ts +2 -3
- package/src/types/chat.ts +3 -1
- package/src/web/components/chat/chat-history-bar.tsx +2 -15
- package/src/web/components/chat/chat-tab.tsx +5 -2
- package/src/web/components/chat/message-input.tsx +48 -6
- package/src/web/components/chat/message-list.tsx +19 -5
- package/src/web/components/chat/slash-command-picker.tsx +21 -12
- package/src/web/components/layout/mobile-nav.tsx +47 -21
- package/src/web/components/layout/panel-layout.tsx +11 -0
- package/src/web/components/layout/upgrade-banner.tsx +48 -2
- package/src/web/components/shared/markdown-renderer.tsx +5 -2
- package/src/web/hooks/use-chat.ts +33 -1
- package/src/web/main.tsx +1 -0
- package/src/web/stores/panel-store.ts +25 -1
- package/src/web/styles/globals.css +14 -0
- package/dist/web/assets/chat-tab-CmSLt4tg.js +0 -10
- package/dist/web/assets/index-BtwsLrdT.css +0 -2
- package/dist/web/assets/index-D6_wwsL_.js +0 -30
- package/dist/web/assets/keybindings-store-C8ryKudw.js +0 -1
- package/dist/web/assets/markdown-renderer-xYMhd9cE.js +0 -69
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
|
|
4
|
+
let _dir: string | undefined;
|
|
5
|
+
|
|
6
|
+
/** Centralized PPM directory resolution. Respects PPM_HOME env var for test isolation. */
|
|
7
|
+
export function getPpmDir(): string {
|
|
8
|
+
return (_dir ??= resolve(process.env.PPM_HOME || resolve(homedir(), ".ppm")));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Reset cached dir (for test teardown if needed) */
|
|
12
|
+
export function _resetPpmDir(): void {
|
|
13
|
+
_dir = undefined;
|
|
14
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { SlashItem } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
export interface BuiltinSlashCommand {
|
|
4
|
+
name: string;
|
|
5
|
+
summary: string;
|
|
6
|
+
category: "session" | "tools" | "config";
|
|
7
|
+
argumentHint?: string;
|
|
8
|
+
aliases?: string[];
|
|
9
|
+
/** "ppm" = PPM intercepts and executes, "sdk" = passed through to Claude SDK */
|
|
10
|
+
handler: "ppm" | "sdk";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Static registry of built-in slash commands */
|
|
14
|
+
const BUILTIN_COMMANDS: BuiltinSlashCommand[] = [
|
|
15
|
+
// PPM-executed commands (intercepted before SDK)
|
|
16
|
+
{ name: "skills", summary: "List available skills and their sources", category: "tools", aliases: ["sk"], handler: "ppm" },
|
|
17
|
+
{ name: "version", summary: "Show PPM and SDK version info", category: "session", handler: "ppm" },
|
|
18
|
+
// SDK-passthrough commands (picker hints, SDK handles execution)
|
|
19
|
+
{ name: "help", summary: "Show available commands and skills", category: "session", handler: "sdk" },
|
|
20
|
+
{ name: "status", summary: "Show session status and context usage", category: "session", handler: "sdk" },
|
|
21
|
+
{ name: "cost", summary: "Show token usage and estimated cost", category: "session", handler: "sdk" },
|
|
22
|
+
{ name: "compact", summary: "Compact conversation to reduce context", category: "session", handler: "sdk" },
|
|
23
|
+
{ name: "model", summary: "View or change the AI model", category: "config", argumentHint: "[model-name]", handler: "sdk" },
|
|
24
|
+
{ name: "config", summary: "View or modify configuration", category: "config", handler: "sdk" },
|
|
25
|
+
{ name: "memory", summary: "View or edit AI memory", category: "config", handler: "sdk" },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/** Convert builtin commands to SlashItem[] for the picker */
|
|
29
|
+
export function getBuiltinSlashItems(): SlashItem[] {
|
|
30
|
+
return BUILTIN_COMMANDS.map((cmd) => ({
|
|
31
|
+
type: "builtin" as const,
|
|
32
|
+
name: cmd.name,
|
|
33
|
+
description: cmd.summary,
|
|
34
|
+
argumentHint: cmd.argumentHint,
|
|
35
|
+
scope: "bundled" as const,
|
|
36
|
+
category: cmd.category,
|
|
37
|
+
aliases: cmd.aliases,
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Look up a builtin command by name (or alias) */
|
|
42
|
+
export function getBuiltinByName(name: string): BuiltinSlashCommand | undefined {
|
|
43
|
+
const lower = name.toLowerCase();
|
|
44
|
+
return BUILTIN_COMMANDS.find(
|
|
45
|
+
(cmd) => cmd.name === lower || cmd.aliases?.includes(lower),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Check if a command name is PPM-handled (not passed through to SDK) */
|
|
50
|
+
export function isPpmHandled(name: string): boolean {
|
|
51
|
+
const cmd = getBuiltinByName(name);
|
|
52
|
+
return cmd?.handler === "ppm";
|
|
53
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { VERSION } from "../../version.ts";
|
|
2
|
+
import { getBuiltinByName } from "./builtin-commands.ts";
|
|
3
|
+
import { discoverSkillRoots } from "./discover-skill-roots.ts";
|
|
4
|
+
import { loadItemsFromRoots } from "./skill-loader.ts";
|
|
5
|
+
import { resolveOverrides } from "./resolve-overrides.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Handle the /skills built-in command.
|
|
9
|
+
* Returns a formatted text listing of discovered skills.
|
|
10
|
+
*/
|
|
11
|
+
function handleSkills(projectPath: string): string {
|
|
12
|
+
// Call pipeline directly to avoid circular dependency with index.ts
|
|
13
|
+
const roots = discoverSkillRoots(projectPath);
|
|
14
|
+
const rawItems = loadItemsFromRoots(roots);
|
|
15
|
+
const result = resolveOverrides(rawItems, roots);
|
|
16
|
+
|
|
17
|
+
const lines: string[] = ["**Discovered Skills & Commands**\n"];
|
|
18
|
+
|
|
19
|
+
if (result.roots.length > 0) {
|
|
20
|
+
lines.push("Roots:");
|
|
21
|
+
for (const root of result.roots) {
|
|
22
|
+
lines.push(` ${root.path} (${root.source})`);
|
|
23
|
+
}
|
|
24
|
+
lines.push("");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const skills = result.active.filter((i) => i.type === "skill");
|
|
28
|
+
const commands = result.active.filter((i) => i.type === "command");
|
|
29
|
+
|
|
30
|
+
lines.push(`${skills.length} skills, ${commands.length} commands (${result.shadowed.length} shadowed)\n`);
|
|
31
|
+
|
|
32
|
+
for (const item of result.active) {
|
|
33
|
+
if (item.type === "builtin") continue;
|
|
34
|
+
lines.push(` /${item.name} [${item.type}] ${item.source} — ${item.description || "(no description)"}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (result.shadowed.length > 0) {
|
|
38
|
+
lines.push("\nShadowed:");
|
|
39
|
+
for (const item of result.shadowed) {
|
|
40
|
+
lines.push(` /${item.name} [${item.type}] ${item.source} ← shadowed by ${item.shadowedBy.source}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return lines.join("\n");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Handle the /version built-in command */
|
|
48
|
+
function handleVersion(): string {
|
|
49
|
+
return `PPM v${VERSION}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Execute a PPM-handled built-in command.
|
|
54
|
+
* Returns the response text, or null if not a PPM-handled command.
|
|
55
|
+
*/
|
|
56
|
+
export function executeBuiltin(name: string, projectPath: string): string | null {
|
|
57
|
+
const cmd = getBuiltinByName(name);
|
|
58
|
+
if (!cmd || cmd.handler !== "ppm") return null;
|
|
59
|
+
|
|
60
|
+
switch (cmd.name) {
|
|
61
|
+
case "skills": return handleSkills(projectPath);
|
|
62
|
+
case "version": return handleVersion();
|
|
63
|
+
default: return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { DefinitionSource, SlashItemScope } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
/** Numeric priority — lower = higher priority */
|
|
4
|
+
export const SOURCE_PRIORITY: Record<DefinitionSource, number> = {
|
|
5
|
+
"project-ppm": 0,
|
|
6
|
+
"project-claw": 1,
|
|
7
|
+
"project-codex": 2,
|
|
8
|
+
"project-claude": 3,
|
|
9
|
+
"env-var": 4,
|
|
10
|
+
"user-ppm": 5,
|
|
11
|
+
"user-claw": 6,
|
|
12
|
+
"user-codex": 7,
|
|
13
|
+
"user-claude": 8,
|
|
14
|
+
"bundled": 9,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** Compare two sources by priority (for sorting highest-priority first) */
|
|
18
|
+
export function compareSourcePriority(a: DefinitionSource, b: DefinitionSource): number {
|
|
19
|
+
return SOURCE_PRIORITY[a] - SOURCE_PRIORITY[b];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Map a DefinitionSource to the user-facing scope label */
|
|
23
|
+
export function sourceToScope(source: DefinitionSource): SlashItemScope {
|
|
24
|
+
if (source === "bundled") return "bundled";
|
|
25
|
+
if (source.startsWith("project-") || source === "env-var") return "project";
|
|
26
|
+
return "user";
|
|
27
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { resolve, dirname } from "node:path";
|
|
2
|
+
import { existsSync, statSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import type { SkillRoot, DefinitionSource, ItemOrigin } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
/** Tool ecosystem prefixes mapped to their DefinitionSource for project-level roots */
|
|
8
|
+
const PROJECT_ECOSYSTEMS: Array<{ dir: string; source: DefinitionSource }> = [
|
|
9
|
+
{ dir: ".ppm", source: "project-ppm" },
|
|
10
|
+
{ dir: ".claw", source: "project-claw" },
|
|
11
|
+
{ dir: ".codex", source: "project-codex" },
|
|
12
|
+
{ dir: ".claude", source: "project-claude" },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
/** User-global ecosystem roots (same prefixes, different source) */
|
|
16
|
+
const USER_ECOSYSTEMS: Array<{ dir: string; source: DefinitionSource }> = [
|
|
17
|
+
{ dir: ".ppm", source: "user-ppm" },
|
|
18
|
+
{ dir: ".claw", source: "user-claw" },
|
|
19
|
+
{ dir: ".codex", source: "user-codex" },
|
|
20
|
+
{ dir: ".claude", source: "user-claude" },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const ORIGINS: ItemOrigin[] = ["skills", "commands"];
|
|
24
|
+
|
|
25
|
+
/** Resolve PPM package root for bundled skills */
|
|
26
|
+
const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
|
|
27
|
+
const BUNDLED_SKILLS_DIR = resolve(PKG_ROOT, "assets/skills");
|
|
28
|
+
|
|
29
|
+
/** Check if a path is a readable directory */
|
|
30
|
+
function isDir(p: string): boolean {
|
|
31
|
+
try { return statSync(p).isDirectory(); } catch { return false; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Push root if the directory exists and hasn't been seen yet */
|
|
35
|
+
function addRoot(
|
|
36
|
+
roots: SkillRoot[],
|
|
37
|
+
seen: Set<string>,
|
|
38
|
+
basePath: string,
|
|
39
|
+
origin: ItemOrigin,
|
|
40
|
+
source: DefinitionSource,
|
|
41
|
+
): void {
|
|
42
|
+
const full = resolve(basePath, origin);
|
|
43
|
+
if (!isDir(full)) return;
|
|
44
|
+
const resolved = resolve(full);
|
|
45
|
+
if (seen.has(resolved)) return;
|
|
46
|
+
seen.add(resolved);
|
|
47
|
+
roots.push({ path: resolved, source, origin });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Walk ancestor directories from projectPath upward, stopping at git root.
|
|
52
|
+
* At each level, check for ecosystem skill/command directories.
|
|
53
|
+
*/
|
|
54
|
+
function walkAncestors(projectPath: string, roots: SkillRoot[], seen: Set<string>): void {
|
|
55
|
+
let current = resolve(projectPath);
|
|
56
|
+
const root = (current.startsWith("/") ? "/" : current.slice(0, 3)); // unix root or Windows drive
|
|
57
|
+
|
|
58
|
+
while (current !== root) {
|
|
59
|
+
for (const eco of PROJECT_ECOSYSTEMS) {
|
|
60
|
+
const base = resolve(current, eco.dir);
|
|
61
|
+
for (const origin of ORIGINS) {
|
|
62
|
+
addRoot(roots, seen, base, origin, eco.source);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Stop at git root boundary
|
|
66
|
+
if (isDir(resolve(current, ".git"))) break;
|
|
67
|
+
const parent = dirname(current);
|
|
68
|
+
if (parent === current) break;
|
|
69
|
+
current = parent;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Check environment variable paths for additional roots */
|
|
74
|
+
function checkEnvVars(roots: SkillRoot[], seen: Set<string>): void {
|
|
75
|
+
const ppmSkillsDir = process.env.PPM_SKILLS_DIR;
|
|
76
|
+
if (ppmSkillsDir && isDir(ppmSkillsDir)) {
|
|
77
|
+
const resolved = resolve(ppmSkillsDir);
|
|
78
|
+
if (!seen.has(resolved)) {
|
|
79
|
+
seen.add(resolved);
|
|
80
|
+
roots.push({ path: resolved, source: "env-var", origin: "skills" });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR;
|
|
85
|
+
if (claudeConfigDir) {
|
|
86
|
+
for (const origin of ORIGINS) {
|
|
87
|
+
addRoot(roots, seen, claudeConfigDir, origin, "env-var");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Add user-global roots (~/.ppm, ~/.claw, ~/.codex, ~/.claude) */
|
|
93
|
+
function addUserGlobalRoots(roots: SkillRoot[], seen: Set<string>): void {
|
|
94
|
+
const home = homedir();
|
|
95
|
+
for (const eco of USER_ECOSYSTEMS) {
|
|
96
|
+
const base = resolve(home, eco.dir);
|
|
97
|
+
for (const origin of ORIGINS) {
|
|
98
|
+
addRoot(roots, seen, base, origin, eco.source);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Add bundled skills root (shipped with PPM package) */
|
|
104
|
+
function addBundledRoot(roots: SkillRoot[], seen: Set<string>): void {
|
|
105
|
+
if (isDir(BUNDLED_SKILLS_DIR)) {
|
|
106
|
+
const resolved = resolve(BUNDLED_SKILLS_DIR);
|
|
107
|
+
if (!seen.has(resolved)) {
|
|
108
|
+
seen.add(resolved);
|
|
109
|
+
roots.push({ path: resolved, source: "bundled", origin: "skills" });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Discover all skill/command roots for a project.
|
|
116
|
+
* Returns ordered array (highest priority first).
|
|
117
|
+
*/
|
|
118
|
+
export function discoverSkillRoots(projectPath: string): SkillRoot[] {
|
|
119
|
+
const roots: SkillRoot[] = [];
|
|
120
|
+
const seen = new Set<string>();
|
|
121
|
+
|
|
122
|
+
walkAncestors(projectPath, roots, seen);
|
|
123
|
+
checkEnvVars(roots, seen);
|
|
124
|
+
addUserGlobalRoots(roots, seen);
|
|
125
|
+
addBundledRoot(roots, seen);
|
|
126
|
+
|
|
127
|
+
return roots;
|
|
128
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { SlashItem } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
/** Iterative Levenshtein distance (single-row DP) */
|
|
4
|
+
export function levenshtein(a: string, b: string): number {
|
|
5
|
+
if (a === b) return 0;
|
|
6
|
+
if (a.length === 0) return b.length;
|
|
7
|
+
if (b.length === 0) return a.length;
|
|
8
|
+
|
|
9
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
10
|
+
let curr = new Array<number>(b.length + 1);
|
|
11
|
+
|
|
12
|
+
for (let i = 1; i <= a.length; i++) {
|
|
13
|
+
curr[0] = i;
|
|
14
|
+
for (let j = 1; j <= b.length; j++) {
|
|
15
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
16
|
+
curr[j] = Math.min(
|
|
17
|
+
curr[j - 1]! + 1, // insertion
|
|
18
|
+
prev[j]! + 1, // deletion
|
|
19
|
+
prev[j - 1]! + cost, // substitution
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
[prev, curr] = [curr, prev];
|
|
23
|
+
}
|
|
24
|
+
return prev[b.length]!;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface FuzzyScore { rank: number; distance: number }
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Score a query against a candidate string.
|
|
31
|
+
* Returns null if no reasonable match. Rank: 0=prefix, 1=contains, 2=fuzzy.
|
|
32
|
+
*/
|
|
33
|
+
export function scoreFuzzy(query: string, candidate: string): FuzzyScore | null {
|
|
34
|
+
const lq = query.toLowerCase();
|
|
35
|
+
const lc = candidate.toLowerCase();
|
|
36
|
+
|
|
37
|
+
if (lc.startsWith(lq)) return { rank: 0, distance: 0 };
|
|
38
|
+
if (lc.includes(lq)) return { rank: 1, distance: lc.indexOf(lq) };
|
|
39
|
+
|
|
40
|
+
const maxDist = Math.max(Math.floor(lq.length * 0.4), 2);
|
|
41
|
+
const dist = levenshtein(lq, lc.slice(0, lq.length + maxDist));
|
|
42
|
+
if (dist <= maxDist) return { rank: 2, distance: dist };
|
|
43
|
+
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Search slash items by query with fuzzy matching.
|
|
49
|
+
* Returns ranked results (best match first), truncated to limit.
|
|
50
|
+
*/
|
|
51
|
+
export function searchSlashItems(
|
|
52
|
+
items: SlashItem[],
|
|
53
|
+
query: string,
|
|
54
|
+
limit = 20,
|
|
55
|
+
): SlashItem[] {
|
|
56
|
+
if (!query) return items;
|
|
57
|
+
// Cap query length to prevent quadratic blowup in Levenshtein
|
|
58
|
+
query = query.slice(0, 50);
|
|
59
|
+
|
|
60
|
+
const scored: Array<{ item: SlashItem; rank: number; distance: number }> = [];
|
|
61
|
+
|
|
62
|
+
for (const item of items) {
|
|
63
|
+
// Score against name and description, keep best
|
|
64
|
+
const nameScore = scoreFuzzy(query, item.name);
|
|
65
|
+
const descScore = scoreFuzzy(query, item.description);
|
|
66
|
+
const best = [nameScore, descScore]
|
|
67
|
+
.filter((s): s is FuzzyScore => s !== null)
|
|
68
|
+
.sort((a, b) => a.rank - b.rank || a.distance - b.distance)[0];
|
|
69
|
+
|
|
70
|
+
if (best) scored.push({ item, rank: best.rank, distance: best.distance });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
scored.sort((a, b) => a.rank - b.rank || a.distance - b.distance || a.item.name.localeCompare(b.item.name));
|
|
74
|
+
|
|
75
|
+
return scored.slice(0, limit).map((s) => s.item);
|
|
76
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { discoverSkillRoots } from "./discover-skill-roots.ts";
|
|
2
|
+
import { loadItemsFromRoots } from "./skill-loader.ts";
|
|
3
|
+
import { resolveOverrides } from "./resolve-overrides.ts";
|
|
4
|
+
import { getBuiltinSlashItems } from "./builtin-commands.ts";
|
|
5
|
+
import type { SlashItem, SlashItemWithSource, DiscoveryResult } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
export { searchSlashItems } from "./fuzzy-search.ts";
|
|
8
|
+
export { isPpmHandled, getBuiltinByName } from "./builtin-commands.ts";
|
|
9
|
+
export { executeBuiltin } from "./builtin-handlers.ts";
|
|
10
|
+
export type { SlashItem, SlashItemWithSource, ShadowedItem, DiscoveryResult, SkillRoot, DefinitionSource } from "./types.ts";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Full discovery pipeline: roots → load → resolve overrides → prepend builtins.
|
|
14
|
+
* Returns active items, shadowed items, and discovered roots.
|
|
15
|
+
*/
|
|
16
|
+
export function listSlashItemsDetailed(projectPath: string): DiscoveryResult {
|
|
17
|
+
const roots = discoverSkillRoots(projectPath);
|
|
18
|
+
const rawItems = loadItemsFromRoots(roots);
|
|
19
|
+
const result = resolveOverrides(rawItems, roots);
|
|
20
|
+
|
|
21
|
+
// Prepend builtins (not subject to shadowing — unique type namespace)
|
|
22
|
+
const builtinItems: SlashItemWithSource[] = getBuiltinSlashItems().map((item) => ({
|
|
23
|
+
...item,
|
|
24
|
+
source: "bundled" as const,
|
|
25
|
+
rootPath: "",
|
|
26
|
+
filePath: "",
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
...result,
|
|
31
|
+
active: [...builtinItems, ...result.active],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Backward-compatible: returns flat list of active items (no source metadata).
|
|
37
|
+
* Same signature as the original `listSlashItems()`.
|
|
38
|
+
*/
|
|
39
|
+
export function listSlashItems(projectPath: string): SlashItem[] {
|
|
40
|
+
const { active } = listSlashItemsDetailed(projectPath);
|
|
41
|
+
return active.map(({ source, rootPath, filePath, ...item }) => item);
|
|
42
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { compareSourcePriority } from "./definition-source.ts";
|
|
2
|
+
import type { SlashItemWithSource, ShadowedItem, DiscoveryResult, SkillRoot } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolve override/shadowing conflicts among discovered items.
|
|
6
|
+
* Groups by `type:name` key. Highest-priority source wins; others become shadowed.
|
|
7
|
+
*/
|
|
8
|
+
export function resolveOverrides(
|
|
9
|
+
items: SlashItemWithSource[],
|
|
10
|
+
roots: SkillRoot[],
|
|
11
|
+
): DiscoveryResult {
|
|
12
|
+
const groups = new Map<string, SlashItemWithSource[]>();
|
|
13
|
+
|
|
14
|
+
for (const item of items) {
|
|
15
|
+
const key = `${item.type}:${item.name}`;
|
|
16
|
+
const group = groups.get(key);
|
|
17
|
+
if (group) group.push(item);
|
|
18
|
+
else groups.set(key, [item]);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const active: SlashItemWithSource[] = [];
|
|
22
|
+
const shadowed: ShadowedItem[] = [];
|
|
23
|
+
|
|
24
|
+
for (const group of groups.values()) {
|
|
25
|
+
// Sort by source priority (lowest numeric = highest priority)
|
|
26
|
+
group.sort((a, b) => compareSourcePriority(a.source, b.source));
|
|
27
|
+
|
|
28
|
+
const winner = group[0]!;
|
|
29
|
+
active.push(winner);
|
|
30
|
+
|
|
31
|
+
// Rest are shadowed
|
|
32
|
+
for (let i = 1; i < group.length; i++) {
|
|
33
|
+
shadowed.push({
|
|
34
|
+
...group[i]!,
|
|
35
|
+
shadowedBy: { name: winner.name, source: winner.source },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { active, shadowed, roots };
|
|
41
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { resolve, basename, relative, sep } from "node:path";
|
|
2
|
+
import { readdirSync, readFileSync, existsSync, statSync } from "node:fs";
|
|
3
|
+
import yaml from "js-yaml";
|
|
4
|
+
import { sourceToScope } from "./definition-source.ts";
|
|
5
|
+
import type { SkillRoot, SlashItemWithSource } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
/** Safely coerce a frontmatter value to string */
|
|
8
|
+
function str(val: unknown): string | undefined {
|
|
9
|
+
if (typeof val === "string") return val;
|
|
10
|
+
if (typeof val === "number" || typeof val === "boolean") return String(val);
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Parse YAML frontmatter from a Markdown file */
|
|
15
|
+
function parseFrontmatter(content: string): { meta: Record<string, unknown>; body: string } {
|
|
16
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
17
|
+
if (!match || !match[1]) return { meta: {}, body: content };
|
|
18
|
+
try {
|
|
19
|
+
// maxAliasCount prevents YAML bomb DoS (supported in js-yaml >=4.1, not in @types)
|
|
20
|
+
const meta = yaml.load(match[1], { maxAliasCount: 100 } as yaml.LoadOptions) as Record<string, unknown>;
|
|
21
|
+
return { meta: meta ?? {}, body: content.slice(match[0]!.length).trim() };
|
|
22
|
+
} catch {
|
|
23
|
+
return { meta: {}, body: content };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Recursively walk a directory, calling visitor for every file. Tracks visited paths to prevent symlink cycles and enforces root boundary. */
|
|
28
|
+
function walkDir(dir: string, visitor: (filePath: string) => void, visited = new Set<string>(), rootBoundary?: string): void {
|
|
29
|
+
const resolved = resolve(dir);
|
|
30
|
+
if (visited.has(resolved)) return;
|
|
31
|
+
// Prevent symlink escape: resolved path must stay within root boundary
|
|
32
|
+
if (rootBoundary && resolved !== rootBoundary && !resolved.startsWith(rootBoundary + "/")) return;
|
|
33
|
+
visited.add(resolved);
|
|
34
|
+
let entries: string[];
|
|
35
|
+
try { entries = readdirSync(dir); } catch { return; }
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
const full = resolve(dir, entry);
|
|
38
|
+
try {
|
|
39
|
+
const stat = statSync(full);
|
|
40
|
+
if (stat.isDirectory()) walkDir(full, visitor, visited, rootBoundary);
|
|
41
|
+
else if (stat.isFile()) visitor(full);
|
|
42
|
+
} catch { /* skip */ }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Collect commands from a root with origin "commands" */
|
|
47
|
+
function loadCommands(root: SkillRoot): SlashItemWithSource[] {
|
|
48
|
+
const items: SlashItemWithSource[] = [];
|
|
49
|
+
if (!existsSync(root.path)) return items;
|
|
50
|
+
const scope = sourceToScope(root.source);
|
|
51
|
+
const boundary = resolve(root.path);
|
|
52
|
+
|
|
53
|
+
walkDir(root.path, (filePath) => {
|
|
54
|
+
if (!filePath.endsWith(".md")) return;
|
|
55
|
+
try {
|
|
56
|
+
const content = readFileSync(filePath, "utf-8");
|
|
57
|
+
const { meta } = parseFrontmatter(content);
|
|
58
|
+
const rel = relative(root.path, filePath);
|
|
59
|
+
const name = rel.replace(/\.md$/, "").split(sep).join("/");
|
|
60
|
+
items.push({
|
|
61
|
+
type: "command",
|
|
62
|
+
name: str(meta.name) ?? name,
|
|
63
|
+
description: str(meta.description) ?? "",
|
|
64
|
+
argumentHint: str(meta["argument-hint"]),
|
|
65
|
+
scope,
|
|
66
|
+
source: root.source,
|
|
67
|
+
rootPath: root.path,
|
|
68
|
+
filePath,
|
|
69
|
+
});
|
|
70
|
+
} catch { /* skip */ }
|
|
71
|
+
}, new Set(), boundary);
|
|
72
|
+
return items;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Collect skills from a root with origin "skills".
|
|
77
|
+
* User-global roots use strict mode (SKILL.md only).
|
|
78
|
+
* Project/env roots use relaxed mode (also loose .md files).
|
|
79
|
+
*/
|
|
80
|
+
function loadSkills(root: SkillRoot): SlashItemWithSource[] {
|
|
81
|
+
const items: SlashItemWithSource[] = [];
|
|
82
|
+
if (!existsSync(root.path)) return items;
|
|
83
|
+
const scope = sourceToScope(root.source);
|
|
84
|
+
const strictMode = scope === "user" || scope === "bundled";
|
|
85
|
+
const dirsWithSkillMd = new Set<string>();
|
|
86
|
+
|
|
87
|
+
const boundary = resolve(root.path);
|
|
88
|
+
|
|
89
|
+
// Pass 1: SKILL.md directory-based skills
|
|
90
|
+
walkDir(root.path, (filePath) => {
|
|
91
|
+
if (basename(filePath) !== "SKILL.md") return;
|
|
92
|
+
try {
|
|
93
|
+
const content = readFileSync(filePath, "utf-8");
|
|
94
|
+
const { meta } = parseFrontmatter(content);
|
|
95
|
+
const skillDir = resolve(filePath, "..");
|
|
96
|
+
dirsWithSkillMd.add(skillDir);
|
|
97
|
+
const rel = relative(root.path, skillDir);
|
|
98
|
+
const pathName = rel.split(sep).join("/");
|
|
99
|
+
const name = str(meta.name) ?? pathName;
|
|
100
|
+
if (!name) return;
|
|
101
|
+
items.push({
|
|
102
|
+
type: "skill",
|
|
103
|
+
name,
|
|
104
|
+
description: str(meta.description) ?? "",
|
|
105
|
+
argumentHint: str(meta["argument-hint"]),
|
|
106
|
+
scope,
|
|
107
|
+
source: root.source,
|
|
108
|
+
rootPath: root.path,
|
|
109
|
+
filePath,
|
|
110
|
+
});
|
|
111
|
+
} catch { /* skip */ }
|
|
112
|
+
}, new Set(), boundary);
|
|
113
|
+
|
|
114
|
+
// Pass 2 (relaxed mode only): loose .md files not inside a SKILL.md directory
|
|
115
|
+
if (!strictMode) {
|
|
116
|
+
walkDir(root.path, (filePath) => {
|
|
117
|
+
if (!filePath.endsWith(".md") || basename(filePath) === "SKILL.md") return;
|
|
118
|
+
const dir = resolve(filePath, "..");
|
|
119
|
+
let ancestor = dir;
|
|
120
|
+
while ((ancestor + "/").startsWith(boundary + "/") && ancestor !== boundary) {
|
|
121
|
+
if (dirsWithSkillMd.has(ancestor)) return;
|
|
122
|
+
ancestor = resolve(ancestor, "..");
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const content = readFileSync(filePath, "utf-8");
|
|
126
|
+
const { meta } = parseFrontmatter(content);
|
|
127
|
+
const rel = relative(root.path, filePath);
|
|
128
|
+
const pathName = rel.replace(/\.md$/, "").split(sep).join("/");
|
|
129
|
+
const name = str(meta.name) ?? pathName;
|
|
130
|
+
if (!name) return;
|
|
131
|
+
items.push({
|
|
132
|
+
type: "skill",
|
|
133
|
+
name,
|
|
134
|
+
description: str(meta.description) ?? "",
|
|
135
|
+
argumentHint: str(meta["argument-hint"]),
|
|
136
|
+
scope,
|
|
137
|
+
source: root.source,
|
|
138
|
+
rootPath: root.path,
|
|
139
|
+
filePath,
|
|
140
|
+
});
|
|
141
|
+
} catch { /* skip */ }
|
|
142
|
+
}, new Set(), boundary);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return items;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Load all slash items from a single root */
|
|
149
|
+
export function loadItemsFromRoot(root: SkillRoot): SlashItemWithSource[] {
|
|
150
|
+
return root.origin === "commands" ? loadCommands(root) : loadSkills(root);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Load items from all roots */
|
|
154
|
+
export function loadItemsFromRoots(roots: SkillRoot[]): SlashItemWithSource[] {
|
|
155
|
+
return roots.flatMap(loadItemsFromRoot);
|
|
156
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Priority ranking — lower numeric value = higher priority */
|
|
2
|
+
export type DefinitionSource =
|
|
3
|
+
| "project-ppm" // 0 — .ppm/ in project tree
|
|
4
|
+
| "project-claw" // 1 — .claw/ in project tree
|
|
5
|
+
| "project-codex" // 2 — .codex/ in project tree
|
|
6
|
+
| "project-claude" // 3 — .claude/ in project tree
|
|
7
|
+
| "env-var" // 4 — $PPM_SKILLS_DIR or $CLAUDE_CONFIG_DIR
|
|
8
|
+
| "user-ppm" // 5 — ~/.ppm/
|
|
9
|
+
| "user-claw" // 6 — ~/.claw/
|
|
10
|
+
| "user-codex" // 7 — ~/.codex/
|
|
11
|
+
| "user-claude" // 8 — ~/.claude/
|
|
12
|
+
| "bundled"; // 9 — shipped with PPM package
|
|
13
|
+
|
|
14
|
+
export type SlashItemType = "skill" | "command" | "builtin";
|
|
15
|
+
export type SlashItemScope = "project" | "user" | "bundled";
|
|
16
|
+
export type ItemOrigin = "skills" | "commands";
|
|
17
|
+
|
|
18
|
+
export interface SlashItem {
|
|
19
|
+
type: SlashItemType;
|
|
20
|
+
/** Slash name, e.g. "review", "devops/deploy", "ck:research" */
|
|
21
|
+
name: string;
|
|
22
|
+
description: string;
|
|
23
|
+
argumentHint?: string;
|
|
24
|
+
/** Where the item comes from */
|
|
25
|
+
scope: SlashItemScope;
|
|
26
|
+
category?: string;
|
|
27
|
+
aliases?: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SkillRoot {
|
|
31
|
+
path: string; // Resolved absolute path
|
|
32
|
+
source: DefinitionSource;
|
|
33
|
+
origin: ItemOrigin;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Extends SlashItem with source metadata */
|
|
37
|
+
export interface SlashItemWithSource extends SlashItem {
|
|
38
|
+
source: DefinitionSource;
|
|
39
|
+
rootPath: string;
|
|
40
|
+
filePath: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ShadowedItem extends SlashItemWithSource {
|
|
44
|
+
shadowedBy: { name: string; source: DefinitionSource };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface DiscoveryResult {
|
|
48
|
+
active: SlashItemWithSource[];
|
|
49
|
+
shadowed: ShadowedItem[];
|
|
50
|
+
roots: SkillRoot[];
|
|
51
|
+
}
|