@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,27 @@
1
+ import type { PluginClientContext } from "@getpaseo/plugin/client";
2
+
3
+ import { SkillsPanel } from "./client/panel";
4
+ import { contributePills } from "./client/pill";
5
+
6
+ export default function contribute(client: PluginClientContext) {
7
+ client.addWorkspacePanel({
8
+ id: "skills",
9
+ title: "Skills",
10
+ icon: "Sparkles",
11
+ context: "agent",
12
+ Component: SkillsPanel,
13
+ });
14
+ client.addCommandCenterItem({
15
+ id: "open-skills",
16
+ title: "Skills",
17
+ icon: "Sparkles",
18
+ keywords: ["skill", "skills", "agent skills"],
19
+ context: "agent",
20
+ onSelect: ({ openPanel }) => {
21
+ openPanel("skills");
22
+ },
23
+ });
24
+ // The pill reaches the panel from the composer; the Command Center item is
25
+ // the keyboard path to the same panel.
26
+ return contributePills(client);
27
+ }
@@ -0,0 +1,10 @@
1
+ import type { PluginServerContext } from "@getpaseo/plugin/server";
2
+
3
+ import { createListSkillsHandler, createReadSkillHandler } from "./server/skills";
4
+ import { listSkills, readSkill } from "./shared/skills";
5
+
6
+ export default function contribute(server: PluginServerContext) {
7
+ server.handle(listSkills, createListSkillsHandler());
8
+ server.handle(readSkill, createReadSkillHandler());
9
+ return () => {};
10
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@gpambrozio/paseo-skills",
3
+ "version": "0.1.2",
4
+ "description": "Paseo plugin: lists the skills an agent can use, shows where each comes from, renders its SKILL.md, and invokes it",
5
+ "license": "MIT",
6
+ "homepage": "https://github.com/gpambrozio/paseo-plugins/tree/main/skills",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/gpambrozio/paseo-plugins.git",
10
+ "directory": "skills"
11
+ },
12
+ "type": "module",
13
+ "files": [
14
+ "paseo-plugin.json",
15
+ "index.client.tsx",
16
+ "index.server.ts",
17
+ "client/",
18
+ "server/",
19
+ "shared/",
20
+ "CHANGELOG.md",
21
+ "!**/*.test.ts"
22
+ ],
23
+ "publishConfig": {
24
+ "access": "public",
25
+ "registry": "https://registry.npmjs.org"
26
+ },
27
+ "scripts": {
28
+ "typecheck": "tsc --noEmit",
29
+ "test": "vitest run --passWithNoTests"
30
+ },
31
+ "devDependencies": {
32
+ "@getpaseo/client": "0.8.0",
33
+ "@getpaseo/plugin": "0.8.0",
34
+ "@getpaseo/protocol": "0.8.0",
35
+ "@tanstack/react-query": "^5.102.8",
36
+ "@types/node": "^26.5.0",
37
+ "@types/react": "~19.2.0",
38
+ "react": "19.1.0",
39
+ "react-native": "0.81.5",
40
+ "typescript": "^7.0.2",
41
+ "vitest": "^5.0.0",
42
+ "zod": "^4.5.4"
43
+ }
44
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "id": "skills",
3
+ "description": "Lists the skills an agent can use, renders each SKILL.md, and invokes it",
4
+ "requirements": {
5
+ "paseo": ">=0.8.0"
6
+ }
7
+ }
@@ -0,0 +1,101 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { dirsUpToRepoRoot } from "./repo-root";
5
+ import { readSkillCandidates, type SkillDirectoryCandidate } from "./skill-directory";
6
+ import type { SkillEntry } from "./skill-entry";
7
+
8
+ export interface ClaudeResolveOptions {
9
+ cwd: string;
10
+ claudeHome: string;
11
+ }
12
+
13
+ interface InstalledPluginEntry {
14
+ scope?: string;
15
+ projectPath?: string;
16
+ installPath?: string;
17
+ }
18
+
19
+ function isInside(child: string, parent: string): boolean {
20
+ const normalizedChild = path.resolve(child);
21
+ const normalizedParent = path.resolve(parent);
22
+ return (
23
+ normalizedChild === normalizedParent ||
24
+ normalizedChild.startsWith(`${normalizedParent}${path.sep}`)
25
+ );
26
+ }
27
+
28
+ /**
29
+ * The plugin cache keeps every version ever fetched, so the directory listing
30
+ * cannot tell you what is live. installed_plugins.json can: each entry names the
31
+ * exact installPath in use. An entry with a projectPath applies only inside that
32
+ * directory, whatever its `scope` string says — real manifests carry
33
+ * `scope: "local"` entries that are per-project. An entry with no projectPath
34
+ * applies everywhere.
35
+ */
36
+ async function readInstalledPluginDirs(
37
+ claudeHome: string,
38
+ cwd: string,
39
+ ): Promise<Array<{ pluginName: string; dir: string }>> {
40
+ const manifestPath = path.join(claudeHome, "plugins", "installed_plugins.json");
41
+ let parsed: unknown;
42
+ try {
43
+ parsed = JSON.parse(await readFile(manifestPath, "utf8"));
44
+ } catch {
45
+ return [];
46
+ }
47
+
48
+ const plugins = (parsed as { plugins?: Record<string, unknown> } | null)?.plugins;
49
+ if (!plugins || typeof plugins !== "object") return [];
50
+
51
+ const results: Array<{ pluginName: string; dir: string }> = [];
52
+ for (const [key, value] of Object.entries(plugins)) {
53
+ if (!Array.isArray(value)) continue;
54
+ const pluginName = key.split("@")[0] ?? key;
55
+ for (const entry of value as InstalledPluginEntry[]) {
56
+ if (!entry || typeof entry.installPath !== "string") continue;
57
+ // A projectPath scopes the entry to that directory regardless of what
58
+ // `scope` says — real manifests carry `scope: "local"` entries that are
59
+ // per-project. An entry with no projectPath applies everywhere.
60
+ if (typeof entry.projectPath === "string" && !isInside(cwd, entry.projectPath)) continue;
61
+ results.push({ pluginName, dir: path.join(entry.installPath, "skills") });
62
+ }
63
+ }
64
+ return results;
65
+ }
66
+
67
+ /**
68
+ * Precedence: the working directory, then each directory up to the repository
69
+ * root, then personal, then plugins. Claude loads `.claude/skills` from every
70
+ * one of those ancestors, so an agent working in a subdirectory still sees the
71
+ * skills checked in at the repo root. Plugin skills are namespaced
72
+ * `plugin:skill`, so in practice they never collide with the rest.
73
+ */
74
+ export async function resolveClaudeSkills(options: ClaudeResolveOptions): Promise<SkillEntry[]> {
75
+ const [pluginDirs, dirs] = await Promise.all([
76
+ readInstalledPluginDirs(options.claudeHome, options.cwd),
77
+ dirsUpToRepoRoot(options.cwd),
78
+ ]);
79
+
80
+ const candidates: SkillDirectoryCandidate[] = dirs.map((dir, index) => ({
81
+ dir: path.join(dir, ".claude", "skills"),
82
+ kind: index === 0 ? "project" : "repo",
83
+ label: index === 0 ? "Project" : "Repository",
84
+ }));
85
+
86
+ candidates.push({
87
+ dir: path.join(options.claudeHome, "skills"),
88
+ kind: "personal",
89
+ label: "Personal",
90
+ });
91
+ for (const { pluginName, dir } of pluginDirs) {
92
+ candidates.push({
93
+ dir,
94
+ kind: "plugin",
95
+ label: pluginName,
96
+ nameFor: (name) => `${pluginName}:${name}`,
97
+ });
98
+ }
99
+
100
+ return readSkillCandidates(candidates);
101
+ }
@@ -0,0 +1,50 @@
1
+ import path from "node:path";
2
+
3
+ import { dirsUpToRepoRoot } from "./repo-root";
4
+ import { readSkillCandidates, type SkillDirectoryCandidate } from "./skill-directory";
5
+ import type { SkillEntry } from "./skill-entry";
6
+
7
+ export interface CodexResolveOptions {
8
+ cwd: string;
9
+ /** `$CODEX_HOME` or `~/.codex`. Only the legacy `skills` directory is read from it. */
10
+ codexHome: string;
11
+ /** `~/.agents`, the documented home for user-scoped skills. */
12
+ agentsHome: string;
13
+ /** `/etc/codex/skills`, the admin scope. */
14
+ adminSkillsDir: string;
15
+ }
16
+
17
+ /**
18
+ * Follows Codex's documented search path: `.agents/skills` in every directory
19
+ * from cwd up to the repository root, then `$HOME/.agents/skills`, then
20
+ * `/etc/codex/skills`.
21
+ *
22
+ * `.codex/skills` is read alongside each `.agents/skills` and at `$CODEX_HOME`
23
+ * because that is where Paseo's own orchestration sync writes and where older
24
+ * Codex builds looked. It sits second within each scope, so when a name lives
25
+ * in both the documented directory wins.
26
+ *
27
+ * Codex's bundled system skills have no path on disk and stay invisible here.
28
+ */
29
+ export async function resolveCodexSkills(options: CodexResolveOptions): Promise<SkillEntry[]> {
30
+ const dirs = await dirsUpToRepoRoot(options.cwd);
31
+
32
+ const candidates: SkillDirectoryCandidate[] = dirs.flatMap((dir, index) => {
33
+ const scope =
34
+ index === 0
35
+ ? { kind: "project" as const, label: "Project" }
36
+ : { kind: "repo" as const, label: "Repository" };
37
+ return [
38
+ { dir: path.join(dir, ".agents", "skills"), ...scope },
39
+ { dir: path.join(dir, ".codex", "skills"), ...scope },
40
+ ];
41
+ });
42
+
43
+ candidates.push(
44
+ { dir: path.join(options.agentsHome, "skills"), kind: "personal", label: "Personal" },
45
+ { dir: path.join(options.codexHome, "skills"), kind: "personal", label: "Personal" },
46
+ { dir: options.adminSkillsDir, kind: "admin", label: "Admin" },
47
+ );
48
+
49
+ return readSkillCandidates(candidates);
50
+ }
@@ -0,0 +1,88 @@
1
+ export interface ParsedSkillDocument {
2
+ frontmatter: Record<string, string>;
3
+ body: string;
4
+ }
5
+
6
+ function unquote(value: string): string {
7
+ if (value.length >= 2) {
8
+ const first = value[0];
9
+ const last = value[value.length - 1];
10
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
11
+ return value.slice(1, -1);
12
+ }
13
+ }
14
+ return value;
15
+ }
16
+
17
+ function isIndented(line: string): boolean {
18
+ return /^[ \t]/.test(line);
19
+ }
20
+
21
+ /**
22
+ * Reads the leading `---` block of a SKILL.md.
23
+ *
24
+ * Supports the flat `key: value` pairs most skills use, plus `>` folded and `|`
25
+ * literal block scalars, which real skills do use for long descriptions. Lines
26
+ * that are indented belong to a nested structure, not the top level: they are
27
+ * skipped rather than trimmed into a top-level key, so a nested `metadata.name`
28
+ * cannot overwrite the skill's own name.
29
+ *
30
+ * A file whose fence is missing or unterminated is reported as all body with no
31
+ * frontmatter, which callers treat as "not a skill".
32
+ */
33
+ export function parseFrontmatter(raw: string): ParsedSkillDocument {
34
+ const normalized = raw.replace(/\r\n/g, "\n");
35
+ if (!normalized.startsWith("---\n")) {
36
+ return { frontmatter: {}, body: raw };
37
+ }
38
+
39
+ const lines = normalized.split("\n");
40
+ let closingIndex = -1;
41
+ for (let index = 1; index < lines.length; index += 1) {
42
+ if (lines[index]!.trim() === "---") {
43
+ closingIndex = index;
44
+ break;
45
+ }
46
+ }
47
+ if (closingIndex === -1) {
48
+ return { frontmatter: {}, body: raw };
49
+ }
50
+
51
+ const frontmatter: Record<string, string> = {};
52
+ let index = 1;
53
+ while (index < closingIndex) {
54
+ const line = lines[index]!;
55
+ index += 1;
56
+ if (isIndented(line)) continue;
57
+
58
+ const separator = line.indexOf(":");
59
+ if (separator === -1) continue;
60
+ const key = line.slice(0, separator).trim();
61
+ if (key.length === 0) continue;
62
+ const rest = line.slice(separator + 1).trim();
63
+
64
+ if (/^[>|][-+]?$/.test(rest)) {
65
+ const continuation: string[] = [];
66
+ while (index < closingIndex && (isIndented(lines[index]!) || lines[index]!.trim() === "")) {
67
+ continuation.push(lines[index]!.trim());
68
+ index += 1;
69
+ }
70
+ while (continuation.length > 0 && continuation[continuation.length - 1] === "") {
71
+ continuation.pop();
72
+ }
73
+ frontmatter[key] =
74
+ rest[0] === ">"
75
+ ? continuation.filter((entry) => entry !== "").join(" ")
76
+ : continuation.join("\n");
77
+ continue;
78
+ }
79
+
80
+ frontmatter[key] = unquote(rest);
81
+ }
82
+
83
+ const body = lines
84
+ .slice(closingIndex + 1)
85
+ .join("\n")
86
+ .replace(/^\n+/, "");
87
+ return { frontmatter, body };
88
+ }
@@ -0,0 +1,43 @@
1
+ import { stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * Walks up from cwd looking for a `.git` entry. Matches a file as well as a
6
+ * directory so worktrees and submodules resolve. Stops at the filesystem root.
7
+ */
8
+ export async function findRepoRoot(cwd: string): Promise<string | null> {
9
+ let current = path.resolve(cwd);
10
+ for (;;) {
11
+ try {
12
+ await stat(path.join(current, ".git"));
13
+ return current;
14
+ } catch {
15
+ const parent = path.dirname(current);
16
+ if (parent === current) return null;
17
+ current = parent;
18
+ }
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Every directory from cwd up to the repository root, cwd first. Both providers
24
+ * scan their skills directory in each of these, not only in cwd — a skill
25
+ * checked in at the repo root is available to an agent working in a
26
+ * subdirectory. Outside a repository the walk has nowhere to stop, so it yields
27
+ * cwd alone rather than climbing to the filesystem root.
28
+ */
29
+ export async function dirsUpToRepoRoot(cwd: string): Promise<string[]> {
30
+ const start = path.resolve(cwd);
31
+ const repoRoot = await findRepoRoot(start);
32
+ if (!repoRoot || repoRoot === start) return [start];
33
+
34
+ const dirs = [start];
35
+ let current = start;
36
+ while (current !== repoRoot) {
37
+ const parent = path.dirname(current);
38
+ if (parent === current) break;
39
+ dirs.push(parent);
40
+ current = parent;
41
+ }
42
+ return dirs;
43
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The session's own view of what it can run, as reported by the provider rather
3
+ * than found on disk. This is the only way to see skills bundled inside an agent
4
+ * binary, which live on no scannable path.
5
+ *
6
+ * Deliberately free of Node imports and of any `@getpaseo/client` type, so it
7
+ * compiles into either bundle and does not depend on the daemon's SDK version.
8
+ */
9
+
10
+ export interface ReportedCommand {
11
+ name: string;
12
+ description: string;
13
+ argumentHint: string;
14
+ kind?: "command" | "skill";
15
+ }
16
+
17
+ export interface ReportedSkill {
18
+ name: string;
19
+ description: string;
20
+ argumentHint: string;
21
+ }
22
+
23
+ export interface CommandsResult {
24
+ commands: ReportedCommand[];
25
+ error: string | null;
26
+ }
27
+
28
+ interface CommandsCapableHandle {
29
+ commands(): Promise<CommandsResult>;
30
+ }
31
+
32
+ /**
33
+ * The `paseo` object comes from the daemon's bundled client, not from this
34
+ * project's node_modules, so the method may be absent no matter what the types
35
+ * say. Structural detection keeps the plugin working on a daemon that predates
36
+ * `agent.commands()` instead of throwing at runtime.
37
+ */
38
+ export function supportsCommands(handle: unknown): handle is CommandsCapableHandle {
39
+ return typeof (handle as Partial<CommandsCapableHandle> | null)?.commands === "function";
40
+ }
41
+
42
+ export interface ReportedSplit {
43
+ skills: ReportedSkill[];
44
+ commands: ReportedSkill[];
45
+ }
46
+
47
+ /**
48
+ * Reported entries that filesystem discovery did not already find, split into
49
+ * skills and session controls on the `kind` the provider assigned.
50
+ *
51
+ * `kind` is optional and is the provider's own judgement, not ground truth.
52
+ * Claude derives it from a hardcoded denylist of root-only commands and calls
53
+ * everything else a skill, so plenty of session controls land in the skills
54
+ * bucket. There is no better signal available: Claude's built-ins are compiled
55
+ * into its binary, so nothing on disk can confirm the split.
56
+ *
57
+ * An entry with no `kind` counts as a skill. Bucketing unclassified entries as
58
+ * commands would empty the skills section for any provider that omits the field.
59
+ */
60
+ export function selectReported(
61
+ commands: readonly ReportedCommand[],
62
+ discoveredNames: readonly string[],
63
+ ): ReportedSplit {
64
+ const seen = new Set(discoveredNames);
65
+ const split: ReportedSplit = { skills: [], commands: [] };
66
+ for (const command of commands) {
67
+ if (seen.has(command.name)) continue;
68
+ seen.add(command.name);
69
+ const bucket = command.kind === "command" ? split.commands : split.skills;
70
+ bucket.push({
71
+ name: command.name,
72
+ description: command.description,
73
+ argumentHint: command.argumentHint,
74
+ });
75
+ }
76
+ const byName = (a: ReportedSkill, b: ReportedSkill) => a.name.localeCompare(b.name);
77
+ split.skills.sort(byName);
78
+ split.commands.sort(byName);
79
+ return split;
80
+ }
@@ -0,0 +1,93 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { parseFrontmatter } from "./frontmatter";
5
+ import { dedupeByName, makeSkillId, type SkillEntry, type SkillSourceKind } from "./skill-entry";
6
+
7
+ export interface SkillDirectoryCandidate {
8
+ dir: string;
9
+ kind: SkillSourceKind;
10
+ label: string;
11
+ /** Plugin skills are invoked as `plugin:skill`; everything else keeps its name. */
12
+ nameFor?: (frontmatterName: string) => string;
13
+ }
14
+
15
+ /**
16
+ * Reads a provider's whole search path. Candidates arrive in precedence order
17
+ * and the first copy of a name wins, so a project skill shadows a personal one
18
+ * without either being listed twice.
19
+ *
20
+ * The same directory can be named twice — a repository rooted at `$HOME` puts
21
+ * `~/.claude/skills` in both the repo walk and the personal scope — so a
22
+ * directory is read once, under the first scope that reached it.
23
+ */
24
+ export async function readSkillCandidates(
25
+ candidates: SkillDirectoryCandidate[],
26
+ ): Promise<SkillEntry[]> {
27
+ const seen = new Set<string>();
28
+ const unique = candidates.filter((candidate) => {
29
+ const resolved = path.resolve(candidate.dir);
30
+ if (seen.has(resolved)) return false;
31
+ seen.add(resolved);
32
+ return true;
33
+ });
34
+
35
+ const groups = await Promise.all(
36
+ unique.map((candidate) =>
37
+ readSkillsFromDirectory(candidate.dir, candidate.kind, candidate.label, candidate.nameFor),
38
+ ),
39
+ );
40
+ return dedupeByName(groups.flat()).sort((a, b) => a.name.localeCompare(b.name));
41
+ }
42
+
43
+ /**
44
+ * Scans one `skills` directory. Each direct child directory (or symlink to one)
45
+ * holding a SKILL.md is a skill. A missing directory is normal, not an error.
46
+ * An entry whose frontmatter lacks name or description is skipped, matching the
47
+ * providers — listing it would show a skill the agent cannot actually see.
48
+ */
49
+ export async function readSkillsFromDirectory(
50
+ dir: string,
51
+ kind: SkillSourceKind,
52
+ label: string,
53
+ nameFor: (frontmatterName: string) => string = (name) => name,
54
+ ): Promise<SkillEntry[]> {
55
+ let dirEntries;
56
+ try {
57
+ dirEntries = await readdir(dir, { withFileTypes: true });
58
+ } catch {
59
+ return [];
60
+ }
61
+
62
+ const candidates = dirEntries.filter((entry) => entry.isDirectory() || entry.isSymbolicLink());
63
+ const results = await Promise.all(
64
+ candidates.map(async (entry): Promise<SkillEntry | null> => {
65
+ const skillPath = path.join(dir, entry.name, "SKILL.md");
66
+ let raw: string;
67
+ try {
68
+ raw = await readFile(skillPath, "utf8");
69
+ } catch {
70
+ return null;
71
+ }
72
+ const { frontmatter } = parseFrontmatter(raw);
73
+ const rawName = frontmatter.name;
74
+ const description = frontmatter.description;
75
+ if (!rawName || !description) return null;
76
+ const name = nameFor(rawName);
77
+ const userInvocable = frontmatter["user-invocable"] !== "false";
78
+ return {
79
+ id: makeSkillId(kind, dir, name),
80
+ name,
81
+ description,
82
+ source: { kind, label, dir },
83
+ path: skillPath,
84
+ userInvocable,
85
+ status: "discovered",
86
+ };
87
+ }),
88
+ );
89
+
90
+ return results
91
+ .filter((entry): entry is SkillEntry => entry !== null)
92
+ .sort((a, b) => a.name.localeCompare(b.name));
93
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Scopes, not directories. Both providers scan several directories per scope —
3
+ * Codex reads `.agents/skills` and legacy `.codex/skills` in the same walk — so
4
+ * a kind names where a skill applies, and `SkillSource.dir` names where it is.
5
+ */
6
+ export type SkillSourceKind = "project" | "repo" | "personal" | "admin" | "plugin";
7
+
8
+ export interface SkillSource {
9
+ kind: SkillSourceKind;
10
+ label: string;
11
+ dir: string;
12
+ }
13
+
14
+ export interface SkillEntry {
15
+ id: string;
16
+ name: string;
17
+ description: string;
18
+ source: SkillSource;
19
+ path: string;
20
+ userInvocable: boolean;
21
+ status: "discovered";
22
+ }
23
+
24
+ export function makeSkillId(kind: SkillSourceKind, dir: string, name: string): string {
25
+ return `${kind}:${dir}:${name}`;
26
+ }
27
+
28
+ /**
29
+ * First name wins. Callers pass directories in precedence order, so a project
30
+ * skill shadows a personal one of the same name without either being reported
31
+ * twice.
32
+ */
33
+ export function dedupeByName(entries: SkillEntry[]): SkillEntry[] {
34
+ const byName = new Map<string, SkillEntry>();
35
+ for (const entry of entries) {
36
+ if (!byName.has(entry.name)) byName.set(entry.name, entry);
37
+ }
38
+ return [...byName.values()];
39
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * A tripwire for the one hazard `skipLibCheck: true` hides.
3
+ *
4
+ * When `@getpaseo/client` fails to resolve, TypeScript does not complain: every
5
+ * Paseo API type silently degrades to `any` and `tsc` still exits 0, so a clean
6
+ * typecheck stops proving that any host call in this plugin is type-checked at
7
+ * all. `noImplicitAny` catches part of it — but only where our code destructures
8
+ * an SDK value. A handler that merely passes `paseo` through type-checks just as
9
+ * happily against `any`.
10
+ *
11
+ * The directive below is the check, and it fails in both directions:
12
+ *
13
+ * - types resolved → the indexed access errors → the directive is used → pass
14
+ * - types degraded → the access is `any`, no error → TS2578 "Unused
15
+ * '@ts-expect-error' directive" → fail
16
+ *
17
+ * This replaces the throwaway file the root CLAUDE.md used to ask contributors
18
+ * to write by hand. It is type-only, so it contributes nothing to either bundle.
19
+ */
20
+ import type { PaseoAgentHandle } from "@getpaseo/client";
21
+
22
+ // @ts-expect-error - Must stay an error. See above: if this member ever stops
23
+ // erroring, the SDK types are `any` and nothing here is really being checked.
24
+ export type SdkTypesResolved = PaseoAgentHandle["thisMemberDoesNotExist"];