@lumea-labs/skills-polpo 0.1.0

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/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # @lumea-labs/skills-polpo
2
+
3
+ Tier 2 wiring of @lumea-labs/skills to the Polpo skills API.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @lumea-labs/skills-polpo
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```tsx
14
+ import { /* … */ } from "@lumea-labs/skills-polpo";
15
+ ```
16
+
17
+ See the [Lumea Agents repo](https://github.com/Lumea-Technologies/lumea-agents/tree/main/packages/skills-polpo) for the full source and a playground example.
18
+
19
+ ## License
20
+
21
+ Apache-2.0
@@ -0,0 +1,6 @@
1
+ export { UsePolpoSkillsAdapterReturn, usePolpoSkillsAdapter } from './use-polpo-skills-adapter.js';
2
+ export { PolpoSkills, PolpoSkillsProps } from './polpo-skills.js';
3
+ export { mapLoadedSkill, mapPolpoSkill } from './map-polpo-skill.js';
4
+ import '@lumea-labs/skills';
5
+ import 'react';
6
+ import '@polpo-ai/sdk';
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ import {
2
+ usePolpoSkillsAdapter
3
+ } from "./use-polpo-skills-adapter";
4
+ import { PolpoSkills } from "./polpo-skills";
5
+ import { mapPolpoSkill, mapLoadedSkill } from "./map-polpo-skill";
6
+ export {
7
+ PolpoSkills,
8
+ mapLoadedSkill,
9
+ mapPolpoSkill,
10
+ usePolpoSkillsAdapter
11
+ };
@@ -0,0 +1,7 @@
1
+ import { LoadedSkill, SkillWithAssignment, SkillInfo } from '@polpo-ai/sdk';
2
+ import { Skill } from '@lumea-labs/skills';
3
+
4
+ declare function mapPolpoSkill(s: SkillWithAssignment | SkillInfo): Skill;
5
+ declare function mapLoadedSkill(loaded: LoadedSkill, base?: Skill): Skill;
6
+
7
+ export { mapLoadedSkill, mapPolpoSkill };
@@ -0,0 +1,34 @@
1
+ function mapScope(source) {
2
+ switch (source) {
3
+ case "project":
4
+ return "project";
5
+ case "global":
6
+ case "home":
7
+ return "personal";
8
+ case "polpo":
9
+ case "claude":
10
+ return "plugin";
11
+ }
12
+ }
13
+ function mapPolpoSkill(s) {
14
+ const tags = s.tags ?? [];
15
+ const assignedTo = "assignedTo" in s && Array.isArray(s.assignedTo) ? s.assignedTo : void 0;
16
+ return {
17
+ id: s.name,
18
+ name: s.name,
19
+ description: s.description,
20
+ tags: s.category ? [s.category, ...tags] : tags,
21
+ installPath: s.path,
22
+ installed: true,
23
+ enabled: assignedTo ? assignedTo.length > 0 : void 0,
24
+ scope: mapScope(s.source)
25
+ };
26
+ }
27
+ function mapLoadedSkill(loaded, base) {
28
+ const mapped = base ?? mapPolpoSkill(loaded);
29
+ return { ...mapped, readme: loaded.content };
30
+ }
31
+ export {
32
+ mapLoadedSkill,
33
+ mapPolpoSkill
34
+ };
@@ -0,0 +1,30 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { SkillsLabels, SkillsQuery } from '@lumea-labs/skills';
4
+
5
+ interface PolpoSkillsProps {
6
+ labels?: Partial<SkillsLabels>;
7
+ initialQuery?: SkillsQuery;
8
+ autoLoad?: boolean;
9
+ autoLoadInstalled?: boolean;
10
+ children: ReactNode;
11
+ }
12
+ /**
13
+ * Convenience wrapper: builds a Polpo-wired `SkillsAdapter` and mounts
14
+ * `<SkillsProvider>` with it. Use this when you don't need the adapter
15
+ * outside the tree — otherwise call `usePolpoSkillsAdapter()` and wire
16
+ * `<SkillsProvider>` yourself.
17
+ *
18
+ * <PolpoSkills>
19
+ * <SkillsSearchBar />
20
+ * <SkillsList onOpenSkill={(s) => router.push(`/skills/${s.id}`)} />
21
+ * </PolpoSkills>
22
+ *
23
+ * Internally a `<PolpoSkillsSync>` mounts inside the provider and
24
+ * republishes a refresh whenever Polpo's `useSkills` data changes —
25
+ * the provider only auto-loads once, so without this the cached list
26
+ * stays empty if Polpo's fetch resolves after the first render.
27
+ */
28
+ declare function PolpoSkills({ labels, initialQuery, autoLoad, autoLoadInstalled, children, }: PolpoSkillsProps): react.JSX.Element;
29
+
30
+ export { PolpoSkills, type PolpoSkillsProps };
@@ -0,0 +1,43 @@
1
+ "use client";
2
+ import { useEffect } from "react";
3
+ import {
4
+ SkillsProvider,
5
+ useSkills as useSkillsPackage
6
+ } from "@lumea-labs/skills";
7
+ import { usePolpoSkillsAdapter } from "./use-polpo-skills-adapter";
8
+ function PolpoSkills({
9
+ labels,
10
+ initialQuery,
11
+ autoLoad,
12
+ autoLoadInstalled,
13
+ children
14
+ }) {
15
+ const { adapter, skills } = usePolpoSkillsAdapter();
16
+ return /* @__PURE__ */ React.createElement(
17
+ SkillsProvider,
18
+ {
19
+ adapter,
20
+ labels,
21
+ initialQuery,
22
+ autoLoad,
23
+ autoLoadInstalled
24
+ },
25
+ /* @__PURE__ */ React.createElement(PolpoSkillsSync, { polpoSkills: skills }),
26
+ children
27
+ );
28
+ }
29
+ function PolpoSkillsSync({ polpoSkills }) {
30
+ const { actions } = useSkillsPackage();
31
+ const key = polpoSkills.map((s) => s.id).join("|");
32
+ useEffect(() => {
33
+ const t = setTimeout(() => {
34
+ void actions.refresh();
35
+ void actions.refreshInstalled();
36
+ }, 0);
37
+ return () => clearTimeout(t);
38
+ }, [key]);
39
+ return null;
40
+ }
41
+ export {
42
+ PolpoSkills
43
+ };
@@ -0,0 +1,53 @@
1
+ import { SkillsAdapter, Skill } from '@lumea-labs/skills';
2
+
3
+ interface UsePolpoSkillsAdapterReturn {
4
+ adapter: SkillsAdapter;
5
+ skills: Skill[];
6
+ isLoading: boolean;
7
+ error: Error | null;
8
+ refetch: () => Promise<void>;
9
+ /** Pending flags for the currently in-flight mutation, surfaced so
10
+ * consumers can disable buttons without re-deriving them. */
11
+ isCreating: boolean;
12
+ isInstalling: boolean;
13
+ isDeleting: boolean;
14
+ /** Polpo-specific assignment ops — not part of the
15
+ * `@lumea/skills` adapter contract because that package doesn't
16
+ * yet model agent-level assignments. Surface them here so
17
+ * consumers (e.g. an agent detail page) can wire UI on top. */
18
+ assignSkill: (skillName: string, agentName: string) => Promise<void>;
19
+ unassignSkill: (skillName: string, agentName: string) => Promise<void>;
20
+ isAssigning: boolean;
21
+ isUnassigning: boolean;
22
+ }
23
+ /**
24
+ * Tier-2 wiring: build a `SkillsAdapter` from `@polpo-ai/react`'s
25
+ * `useSkills` hook + the `PolpoClient` (for content fetching).
26
+ *
27
+ * The mapping is opinionated:
28
+ *
29
+ * - `listSkills` and `listInstalled` both return Polpo's local pool.
30
+ * There is no separate "registry" surface — Polpo doesn't expose
31
+ * one — so the browse view shows installed skills filtered by
32
+ * `query.search` / `query.tags`.
33
+ *
34
+ * - `installSkill(skill)` reads the source from
35
+ * `skill.repositoryUrl ?? skill.repository ?? skill.installPath`
36
+ * and forwards to Polpo's `installSkills(source)`. Skills coming
37
+ * out of `listSkills` set `installPath` so re-installing is a
38
+ * no-op (`force: true` would be the consumer's call).
39
+ *
40
+ * - `getSkill(id)` does a deep fetch via
41
+ * `client.getSkillContent(name)` to populate `readme`.
42
+ *
43
+ * - `createSkill(input)` maps the lumea wizard shape to Polpo's
44
+ * `CreateSkillRequest`. `frontmatter.allowedTools` flows through;
45
+ * `scope` is dropped (Polpo decides where to write).
46
+ *
47
+ * - `toggleSkill` is intentionally not implemented — Polpo's enable
48
+ * state is per-agent assignment, not per-skill. Use the dedicated
49
+ * `assignSkill` / `unassignSkill` returned alongside the adapter.
50
+ */
51
+ declare function usePolpoSkillsAdapter(): UsePolpoSkillsAdapterReturn;
52
+
53
+ export { type UsePolpoSkillsAdapterReturn, usePolpoSkillsAdapter };
@@ -0,0 +1,185 @@
1
+ "use client";
2
+ import { useCallback, useMemo } from "react";
3
+ import { usePolpo, useSkills } from "@polpo-ai/react";
4
+ import { mapLoadedSkill, mapPolpoSkill } from "./map-polpo-skill";
5
+ function usePolpoSkillsAdapter() {
6
+ const { client } = usePolpo();
7
+ const {
8
+ skills: polpoSkills,
9
+ isLoading,
10
+ error,
11
+ refetch,
12
+ createSkill: polpoCreate,
13
+ isCreating,
14
+ installSkills,
15
+ isInstalling,
16
+ deleteSkill,
17
+ isDeleting,
18
+ assignSkill: polpoAssign,
19
+ unassignSkill: polpoUnassign,
20
+ isAssigning,
21
+ isUnassigning
22
+ } = useSkills();
23
+ const skills = useMemo(
24
+ () => polpoSkills.map(mapPolpoSkill),
25
+ [polpoSkills]
26
+ );
27
+ const matchesQuery = useCallback(
28
+ (skill, q) => {
29
+ if (!q) return true;
30
+ if (q.search) {
31
+ const needle = q.search.toLowerCase();
32
+ const hay = `${skill.name} ${skill.description ?? ""}`.toLowerCase();
33
+ if (!hay.includes(needle)) return false;
34
+ }
35
+ if (q.tags && q.tags.length > 0) {
36
+ const have = new Set((skill.tags ?? []).map((t) => t.toLowerCase()));
37
+ const want = q.tags.map((t) => t.toLowerCase());
38
+ if (!want.every((t) => have.has(t))) return false;
39
+ }
40
+ if (q.installedOnly && !skill.installed) return false;
41
+ return true;
42
+ },
43
+ []
44
+ );
45
+ const sortSkills = useCallback(
46
+ (list, sort) => {
47
+ const out = [...list];
48
+ switch (sort) {
49
+ case "alphabetical":
50
+ out.sort((a, b) => a.name.localeCompare(b.name));
51
+ break;
52
+ case "popular":
53
+ case "trending":
54
+ out.sort(
55
+ (a, b) => (b.installs ?? 0) - (a.installs ?? 0) || a.name.localeCompare(b.name)
56
+ );
57
+ break;
58
+ case "recent":
59
+ out.sort(
60
+ (a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? "")
61
+ );
62
+ break;
63
+ default:
64
+ break;
65
+ }
66
+ return out;
67
+ },
68
+ []
69
+ );
70
+ const listSkills = useCallback(
71
+ async (q) => {
72
+ const filtered = skills.filter((s) => matchesQuery(s, q));
73
+ const sorted = sortSkills(filtered, q?.sort);
74
+ const offset = q?.offset ?? 0;
75
+ const limit = q?.limit ?? sorted.length;
76
+ return sorted.slice(offset, offset + limit);
77
+ },
78
+ [skills, matchesQuery, sortSkills]
79
+ );
80
+ const listInstalled = useCallback(async () => {
81
+ return skills;
82
+ }, [skills]);
83
+ const getSkill = useCallback(
84
+ async (id) => {
85
+ const base = skills.find((s) => s.id === id);
86
+ try {
87
+ const [loaded, listing] = await Promise.all([
88
+ client.getSkillContent(id),
89
+ // The skill folder lives at `installPath` (Polpo's `path`).
90
+ // Listing it gives us the file tree for `<SkillFileTree>`.
91
+ base?.installPath ? client.listFiles(base.installPath).catch(() => null) : Promise.resolve(null)
92
+ ]);
93
+ const files = listing?.entries?.map((e) => ({
94
+ path: e.name,
95
+ type: e.type,
96
+ size: e.size
97
+ })) ?? [];
98
+ return { ...mapLoadedSkill(loaded, base), files };
99
+ } catch {
100
+ return base ?? null;
101
+ }
102
+ },
103
+ [skills, client]
104
+ );
105
+ const installSkill = useCallback(
106
+ async (skill) => {
107
+ const source = skill.repositoryUrl ?? skill.repository ?? skill.installPath;
108
+ if (!source) {
109
+ throw new Error(
110
+ `Cannot install skill "${skill.id}" \u2014 no source (repositoryUrl, repository or installPath).`
111
+ );
112
+ }
113
+ await installSkills(source, { skillNames: [skill.name] });
114
+ },
115
+ [installSkills]
116
+ );
117
+ const uninstallSkill = useCallback(
118
+ async (id) => {
119
+ await deleteSkill(id);
120
+ },
121
+ [deleteSkill]
122
+ );
123
+ const createSkill = useCallback(
124
+ async (input) => {
125
+ const created = await polpoCreate({
126
+ name: input.name,
127
+ description: input.description,
128
+ content: input.body,
129
+ allowedTools: input.frontmatter.allowedTools
130
+ });
131
+ const fresh = skills.find((s) => s.name === created.name) ?? {
132
+ id: created.name,
133
+ name: created.name,
134
+ description: input.description,
135
+ installed: true,
136
+ installPath: created.path,
137
+ scope: input.scope,
138
+ tags: [],
139
+ frontmatter: input.frontmatter
140
+ };
141
+ return fresh;
142
+ },
143
+ [polpoCreate, skills]
144
+ );
145
+ const adapter = useMemo(
146
+ () => ({
147
+ listSkills,
148
+ listInstalled,
149
+ getSkill,
150
+ installSkill,
151
+ uninstallSkill,
152
+ createSkill
153
+ }),
154
+ [listSkills, listInstalled, getSkill, installSkill, uninstallSkill, createSkill]
155
+ );
156
+ const assignSkill = useCallback(
157
+ async (skillName, agentName) => {
158
+ await polpoAssign(skillName, agentName);
159
+ },
160
+ [polpoAssign]
161
+ );
162
+ const unassignSkill = useCallback(
163
+ async (skillName, agentName) => {
164
+ await polpoUnassign(skillName, agentName);
165
+ },
166
+ [polpoUnassign]
167
+ );
168
+ return {
169
+ adapter,
170
+ skills,
171
+ isLoading,
172
+ error,
173
+ refetch,
174
+ isCreating,
175
+ isInstalling,
176
+ isDeleting,
177
+ assignSkill,
178
+ unassignSkill,
179
+ isAssigning,
180
+ isUnassigning
181
+ };
182
+ }
183
+ export {
184
+ usePolpoSkillsAdapter
185
+ };
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@lumea-labs/skills-polpo",
3
+ "version": "0.1.0",
4
+ "description": "Tier 2 wiring of @lumea-labs/skills to the Polpo skills API.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "sideEffects": false,
9
+ "peerDependencies": {
10
+ "react": ">=19",
11
+ "lucide-react": "*",
12
+ "@lumea-labs/skills": "*",
13
+ "@polpo-ai/react": ">=0.6.0",
14
+ "@polpo-ai/sdk": ">=0.6.0"
15
+ },
16
+ "license": "Apache-2.0",
17
+ "module": "./dist/index.js",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js"
22
+ }
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "README.md"
27
+ ],
28
+ "scripts": {
29
+ "build": "tsup",
30
+ "build:watch": "tsup --watch",
31
+ "clean": "rm -rf dist"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/Lumea-Technologies/lumea-agents.git",
39
+ "directory": "packages/skills-polpo"
40
+ },
41
+ "homepage": "https://github.com/Lumea-Technologies/lumea-agents/tree/main/packages/skills-polpo"
42
+ }