@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.
@@ -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
+ }
@@ -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
+ });