@gpambrozio/paseo-skills 0.1.2
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 +53 -0
- package/LICENSE +21 -0
- package/README.md +98 -0
- package/client/panel.tsx +542 -0
- package/client/pill.tsx +116 -0
- package/client/skills-query.tsx +29 -0
- package/index.client.tsx +27 -0
- package/index.server.ts +10 -0
- package/package.json +44 -0
- package/paseo-plugin.json +7 -0
- package/server/resolve/claude.ts +101 -0
- package/server/resolve/codex.ts +50 -0
- package/server/resolve/frontmatter.ts +88 -0
- package/server/resolve/repo-root.ts +43 -0
- package/server/resolve/reported.ts +80 -0
- package/server/resolve/skill-directory.ts +93 -0
- package/server/resolve/skill-entry.ts +39 -0
- package/server/sdk-types.ts +24 -0
- package/server/skills.ts +152 -0
- package/shared/skills.ts +67 -0
package/server/skills.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import type { PluginHandlerContext } from "@getpaseo/plugin/server";
|
|
6
|
+
|
|
7
|
+
import { parseFrontmatter } from "./resolve/frontmatter";
|
|
8
|
+
import { resolveClaudeSkills } from "./resolve/claude";
|
|
9
|
+
import { resolveCodexSkills } from "./resolve/codex";
|
|
10
|
+
import { selectReported, supportsCommands } from "./resolve/reported";
|
|
11
|
+
import type { ReportedSkill } from "./resolve/reported";
|
|
12
|
+
import type { SkillEntry } from "./resolve/skill-entry";
|
|
13
|
+
|
|
14
|
+
export interface SkillRoots {
|
|
15
|
+
claudeHome: string;
|
|
16
|
+
codexHome: string;
|
|
17
|
+
agentsHome: string;
|
|
18
|
+
adminSkillsDir: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* `~/.agents` has no environment override — Codex documents the literal path.
|
|
23
|
+
* `/etc/codex/skills` is a constant for the same reason; on a machine without
|
|
24
|
+
* one it reads as an absent directory, which is the normal case.
|
|
25
|
+
*/
|
|
26
|
+
export function defaultSkillRoots(env: NodeJS.ProcessEnv = process.env): SkillRoots {
|
|
27
|
+
const home = os.homedir();
|
|
28
|
+
return {
|
|
29
|
+
claudeHome: path.join(home, ".claude"),
|
|
30
|
+
codexHome: env.CODEX_HOME ?? path.join(home, ".codex"),
|
|
31
|
+
agentsHome: path.join(home, ".agents"),
|
|
32
|
+
adminSkillsDir: path.join(path.sep, "etc", "codex", "skills"),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface ResolvedAgent {
|
|
37
|
+
provider: string;
|
|
38
|
+
cwd: string;
|
|
39
|
+
handle: unknown;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function loadAgent(agentId: string, context: PluginHandlerContext): Promise<ResolvedAgent> {
|
|
43
|
+
const handle = context.paseo.agents.ref(agentId);
|
|
44
|
+
const refreshed = await handle.refresh();
|
|
45
|
+
const agent = refreshed?.agent ?? handle.current();
|
|
46
|
+
if (!agent) {
|
|
47
|
+
throw new Error(`Agent not found: ${agentId}`);
|
|
48
|
+
}
|
|
49
|
+
return { provider: agent.provider, cwd: agent.cwd, handle };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface ReportedSkills {
|
|
53
|
+
available: boolean;
|
|
54
|
+
error: string | null;
|
|
55
|
+
skills: ReportedSkill[];
|
|
56
|
+
commands: ReportedSkill[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const UNAVAILABLE: ReportedSkills = {
|
|
60
|
+
available: false,
|
|
61
|
+
error: null,
|
|
62
|
+
skills: [],
|
|
63
|
+
commands: [],
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Asks the live session what it can run. A failure here never fails the whole
|
|
68
|
+
* list: filesystem discovery already succeeded, and losing it because the
|
|
69
|
+
* session could not answer would be a worse outcome than a missing section. The
|
|
70
|
+
* error travels with the section so the panel can say why it is empty.
|
|
71
|
+
*/
|
|
72
|
+
async function loadReportedSkills(
|
|
73
|
+
agent: ResolvedAgent,
|
|
74
|
+
discovered: SkillEntry[],
|
|
75
|
+
): Promise<ReportedSkills> {
|
|
76
|
+
if (!supportsCommands(agent.handle)) return UNAVAILABLE;
|
|
77
|
+
const discoveredNames = discovered.map((entry) => entry.name);
|
|
78
|
+
try {
|
|
79
|
+
const result = await agent.handle.commands();
|
|
80
|
+
return {
|
|
81
|
+
available: true,
|
|
82
|
+
error: result.error,
|
|
83
|
+
...selectReported(result.commands, discoveredNames),
|
|
84
|
+
};
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return {
|
|
87
|
+
available: true,
|
|
88
|
+
error: error instanceof Error ? error.message : String(error),
|
|
89
|
+
skills: [],
|
|
90
|
+
commands: [],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function resolveForAgent(agent: ResolvedAgent, roots: SkillRoots): Promise<SkillEntry[]> {
|
|
96
|
+
if (agent.provider === "claude") {
|
|
97
|
+
return resolveClaudeSkills({ cwd: agent.cwd, claudeHome: roots.claudeHome });
|
|
98
|
+
}
|
|
99
|
+
if (agent.provider === "codex") {
|
|
100
|
+
return resolveCodexSkills({
|
|
101
|
+
cwd: agent.cwd,
|
|
102
|
+
codexHome: roots.codexHome,
|
|
103
|
+
agentsHome: roots.agentsHome,
|
|
104
|
+
adminSkillsDir: roots.adminSkillsDir,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Only Claude and Codex have documented skill directories to walk. Every other
|
|
112
|
+
* provider still reaches the panel through what its session reports.
|
|
113
|
+
*/
|
|
114
|
+
function scansSkillFiles(provider: string): boolean {
|
|
115
|
+
return provider === "claude" || provider === "codex";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function createListSkillsHandler(roots: SkillRoots = defaultSkillRoots()) {
|
|
119
|
+
return async (input: { agentId: string }, context: PluginHandlerContext) => {
|
|
120
|
+
const agent = await loadAgent(input.agentId, context);
|
|
121
|
+
const skills = await resolveForAgent(agent, roots);
|
|
122
|
+
return {
|
|
123
|
+
provider: agent.provider,
|
|
124
|
+
scanned: scansSkillFiles(agent.provider),
|
|
125
|
+
cwd: agent.cwd,
|
|
126
|
+
skills,
|
|
127
|
+
reported: await loadReportedSkills(agent, skills),
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Takes a skill id, never a path. Discovery runs again and the id is looked up
|
|
134
|
+
* in its result, so the only readable files are ones discovery already found.
|
|
135
|
+
*/
|
|
136
|
+
export function createReadSkillHandler(roots: SkillRoots = defaultSkillRoots()) {
|
|
137
|
+
return async (input: { agentId: string; skillId: string }, context: PluginHandlerContext) => {
|
|
138
|
+
const agent = await loadAgent(input.agentId, context);
|
|
139
|
+
const skills = await resolveForAgent(agent, roots);
|
|
140
|
+
const skill = skills.find((entry) => entry.id === input.skillId);
|
|
141
|
+
if (!skill) {
|
|
142
|
+
throw new Error(`Skill not available: ${input.skillId}`);
|
|
143
|
+
}
|
|
144
|
+
const raw = await readFile(skill.path, "utf8");
|
|
145
|
+
return {
|
|
146
|
+
name: skill.name,
|
|
147
|
+
description: skill.description,
|
|
148
|
+
path: skill.path,
|
|
149
|
+
body: parseFrontmatter(raw).body,
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
}
|
package/shared/skills.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { defineRpc } from "@getpaseo/plugin";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const SkillSourceSchema = z.object({
|
|
5
|
+
// Must stay in step with SkillSourceKind in resolve/skill-entry.ts — a
|
|
6
|
+
// mismatch fails zod validation at runtime, not at compile time.
|
|
7
|
+
kind: z.enum(["project", "repo", "personal", "admin", "plugin"]),
|
|
8
|
+
label: z.string(),
|
|
9
|
+
dir: z.string(),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const SkillEntrySchema = z.object({
|
|
13
|
+
id: z.string(),
|
|
14
|
+
name: z.string(),
|
|
15
|
+
description: z.string(),
|
|
16
|
+
source: SkillSourceSchema,
|
|
17
|
+
path: z.string(),
|
|
18
|
+
userInvocable: z.boolean(),
|
|
19
|
+
status: z.enum(["discovered"]),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const ReportedSkillSchema = z.object({
|
|
23
|
+
name: z.string(),
|
|
24
|
+
description: z.string(),
|
|
25
|
+
argumentHint: z.string(),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* What the live session says it can run, minus everything discovery already
|
|
30
|
+
* found, split on the provider's own `kind`. `available: false` means the daemon
|
|
31
|
+
* predates `agent.commands()`, which is not an error — the panel simply omits
|
|
32
|
+
* both sections.
|
|
33
|
+
*/
|
|
34
|
+
export const ReportedSkillsSchema = z.object({
|
|
35
|
+
available: z.boolean(),
|
|
36
|
+
error: z.string().nullable(),
|
|
37
|
+
skills: z.array(ReportedSkillSchema),
|
|
38
|
+
commands: z.array(ReportedSkillSchema),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export const listSkills = defineRpc({
|
|
42
|
+
name: "skills.list",
|
|
43
|
+
input: z.object({ agentId: z.string() }),
|
|
44
|
+
output: z.object({
|
|
45
|
+
provider: z.string(),
|
|
46
|
+
/**
|
|
47
|
+
* Whether filesystem discovery ran for this provider — not whether the panel
|
|
48
|
+
* has anything to show. Every provider that answers `agent.commands()` gets a
|
|
49
|
+
* reported section regardless.
|
|
50
|
+
*/
|
|
51
|
+
scanned: z.boolean(),
|
|
52
|
+
cwd: z.string().nullable(),
|
|
53
|
+
skills: z.array(SkillEntrySchema),
|
|
54
|
+
reported: ReportedSkillsSchema,
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export const readSkill = defineRpc({
|
|
59
|
+
name: "skills.read",
|
|
60
|
+
input: z.object({ agentId: z.string(), skillId: z.string() }),
|
|
61
|
+
output: z.object({
|
|
62
|
+
name: z.string(),
|
|
63
|
+
description: z.string(),
|
|
64
|
+
path: z.string(),
|
|
65
|
+
body: z.string(),
|
|
66
|
+
}),
|
|
67
|
+
});
|