skillett 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.
@@ -0,0 +1,253 @@
1
+ import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
2
+ import { resolve, join, basename } from "node:path";
3
+ import chalk from "chalk";
4
+ import ora from "ora";
5
+ import pg from "pg";
6
+ const { Client } = pg;
7
+ export async function seed(skillDir, opts) {
8
+ const resolvedDir = resolve(skillDir);
9
+ // Validate skill folder
10
+ const skillMdPath = join(resolvedDir, "SKILL.md");
11
+ if (!existsSync(skillMdPath)) {
12
+ console.log(chalk.red(`Not a skill folder: ${resolvedDir}`));
13
+ console.log(chalk.gray("Expected SKILL.md at the root of the folder"));
14
+ process.exit(1);
15
+ }
16
+ // Extract integration slug and version from SKILL.md frontmatter
17
+ const folderName = basename(resolvedDir);
18
+ const slug = folderName.replace(/^skillett-/, "");
19
+ const skillMdContent = readFileSync(skillMdPath, "utf8");
20
+ const skillFm = parseFrontmatter(skillMdContent.match(/^---\n([\s\S]*?)\n---/)?.[1] ?? "");
21
+ const version = skillFm.version || "1.0.0";
22
+ // Find all endpoint markdown files
23
+ const endpointsDir = join(resolvedDir, "endpoints");
24
+ if (!existsSync(endpointsDir)) {
25
+ console.log(chalk.red(`No endpoints/ directory found in ${resolvedDir}`));
26
+ process.exit(1);
27
+ }
28
+ const endpoints = scanEndpoints(endpointsDir);
29
+ if (endpoints.length === 0) {
30
+ console.log(chalk.red("No endpoint files found with valid frontmatter"));
31
+ process.exit(1);
32
+ }
33
+ // Resolve env vars
34
+ const serverEnv = loadServerEnv();
35
+ const databaseUrl = opts.databaseUrl ?? process.env.DATABASE_URL ?? serverEnv.DATABASE_URL;
36
+ if (!databaseUrl) {
37
+ console.log(chalk.red("No DATABASE_URL found in server/.env"));
38
+ process.exit(1);
39
+ }
40
+ // Scan all markdown files for skill_files table
41
+ const skillFiles = scanAllFiles(resolvedDir);
42
+ // Step 1: Upsert skill, routes, and skill_files
43
+ const sql = buildSql(slug, version, endpoints, skillFiles);
44
+ const spinner = ora(`Seeding ${chalk.bold(String(endpoints.length))} routes + ${chalk.bold(String(skillFiles.length))} files for ${chalk.bold(slug)}…`).start();
45
+ const client = new Client({ connectionString: databaseUrl });
46
+ try {
47
+ await client.connect();
48
+ await client.query(sql);
49
+ spinner.succeed(`Seeded ${chalk.bold(String(endpoints.length))} routes + ${chalk.bold(String(skillFiles.length))} files for ${chalk.bold(slug)}`);
50
+ }
51
+ catch (err) {
52
+ spinner.fail("Seed failed");
53
+ const pgErr = err;
54
+ console.error(chalk.red(pgErr.message));
55
+ if (pgErr.detail)
56
+ console.error(chalk.gray(`Detail: ${pgErr.detail}`));
57
+ if (pgErr.hint)
58
+ console.error(chalk.gray(`Hint: ${pgErr.hint}`));
59
+ process.exit(1);
60
+ }
61
+ finally {
62
+ await client.end();
63
+ }
64
+ }
65
+ /**
66
+ * Recursively scan endpoints/ for .md files with frontmatter.
67
+ */
68
+ function scanEndpoints(endpointsDir) {
69
+ const endpoints = [];
70
+ for (const category of readdirSync(endpointsDir)) {
71
+ const categoryPath = join(endpointsDir, category);
72
+ if (!statSync(categoryPath).isDirectory())
73
+ continue;
74
+ for (const file of readdirSync(categoryPath)) {
75
+ if (!file.endsWith(".md"))
76
+ continue;
77
+ const filePath = join(categoryPath, file);
78
+ const content = readFileSync(filePath, "utf8");
79
+ const parsed = parseEndpointFile(content, file, category);
80
+ if (parsed) {
81
+ endpoints.push(parsed);
82
+ }
83
+ else {
84
+ console.log(chalk.yellow(` ⚠ Skipping ${category}/${file} — no valid frontmatter`));
85
+ }
86
+ }
87
+ }
88
+ return endpoints;
89
+ }
90
+ /**
91
+ * Parse an endpoint markdown file: extract frontmatter.
92
+ */
93
+ function parseEndpointFile(content, filename, category) {
94
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
95
+ if (!fmMatch)
96
+ return null;
97
+ const fm = parseFrontmatter(fmMatch[1]);
98
+ if (!fm.method || !fm.path)
99
+ return null;
100
+ const name = basename(filename, ".md");
101
+ // Extract description from first line after the heading
102
+ const afterFrontmatter = content.slice(fmMatch[0].length).trim();
103
+ const lines = afterFrontmatter.split("\n");
104
+ let description = "";
105
+ for (const line of lines) {
106
+ const trimmed = line.trim();
107
+ if (trimmed.startsWith("#"))
108
+ continue;
109
+ if (trimmed === "")
110
+ continue;
111
+ if (trimmed.startsWith("**Execute:**"))
112
+ continue;
113
+ if (trimmed.startsWith("|") || trimmed.startsWith("##"))
114
+ break;
115
+ description = trimmed;
116
+ break;
117
+ }
118
+ return {
119
+ name,
120
+ category,
121
+ description,
122
+ endpoint_config: {
123
+ method: fm.method,
124
+ base_url: fm.base_url || "https://api.github.com",
125
+ path: fm.path,
126
+ param_mapping: fm.param_mapping || {},
127
+ },
128
+ };
129
+ }
130
+ /**
131
+ * Simple YAML frontmatter parser (handles our known structure).
132
+ */
133
+ function parseFrontmatter(raw) {
134
+ const result = {};
135
+ const paramMapping = {};
136
+ let inParamMapping = false;
137
+ for (const line of raw.split("\n")) {
138
+ if (line.match(/^param_mapping:\s*\{\}\s*$/)) {
139
+ result.param_mapping = {};
140
+ continue;
141
+ }
142
+ if (line.match(/^param_mapping:\s*$/)) {
143
+ inParamMapping = true;
144
+ continue;
145
+ }
146
+ if (inParamMapping) {
147
+ const mapMatch = line.match(/^\s+(\w+):\s*(.+)$/);
148
+ if (mapMatch) {
149
+ paramMapping[mapMatch[1]] = mapMatch[2].trim();
150
+ continue;
151
+ }
152
+ else {
153
+ inParamMapping = false;
154
+ result.param_mapping = paramMapping;
155
+ }
156
+ }
157
+ const kvMatch = line.match(/^(\w+):\s*(.+)$/);
158
+ if (kvMatch) {
159
+ result[kvMatch[1]] = kvMatch[2].trim();
160
+ }
161
+ }
162
+ if (inParamMapping) {
163
+ result.param_mapping = paramMapping;
164
+ }
165
+ return result;
166
+ }
167
+ /**
168
+ * Recursively scan all files in the skill directory for skill_files table.
169
+ */
170
+ function scanAllFiles(skillDir, basePath = "") {
171
+ const files = [];
172
+ for (const entry of readdirSync(skillDir)) {
173
+ const fullPath = join(skillDir, entry);
174
+ const relativePath = basePath ? `${basePath}/${entry}` : entry;
175
+ if (statSync(fullPath).isDirectory()) {
176
+ files.push(...scanAllFiles(fullPath, relativePath));
177
+ }
178
+ else if (entry.endsWith(".md")) {
179
+ files.push({
180
+ file_path: relativePath,
181
+ content: readFileSync(fullPath, "utf8"),
182
+ });
183
+ }
184
+ }
185
+ return files;
186
+ }
187
+ /**
188
+ * Build the full SQL for upserting skill, routes, and skill_files.
189
+ */
190
+ function buildSql(slug, version, endpoints, skillFiles) {
191
+ const statements = [];
192
+ // Upsert into skills table
193
+ statements.push(`INSERT INTO skills (name, slug, latest_version)
194
+ VALUES ('${capitalize(slug)}', '${slug}', '${version}')
195
+ ON CONFLICT (slug) DO UPDATE SET latest_version = EXCLUDED.latest_version;`);
196
+ // Ensure integration exists and link to skill
197
+ statements.push(`INSERT INTO integrations (name, slug, auth_type, status, skill_id)
198
+ VALUES ('${capitalize(slug)}', '${slug}', 'oauth2', 'active',
199
+ (SELECT id FROM skills WHERE slug = '${slug}'))
200
+ ON CONFLICT (slug) DO UPDATE SET
201
+ skill_id = EXCLUDED.skill_id;`);
202
+ // Upsert routes
203
+ for (const ep of endpoints) {
204
+ const paramMapping = JSON.stringify(ep.endpoint_config.param_mapping).replace(/'/g, "''");
205
+ statements.push(`INSERT INTO routes (integration_id, name, method, base_url, path, param_mapping, category)
206
+ VALUES (
207
+ (SELECT id FROM integrations WHERE slug = '${slug}'),
208
+ '${ep.name}',
209
+ '${ep.endpoint_config.method}',
210
+ '${ep.endpoint_config.base_url}',
211
+ '${ep.endpoint_config.path}',
212
+ '${paramMapping}'::jsonb,
213
+ '${ep.category}'
214
+ )
215
+ ON CONFLICT (integration_id, name) DO UPDATE SET
216
+ method = EXCLUDED.method,
217
+ base_url = EXCLUDED.base_url,
218
+ path = EXCLUDED.path,
219
+ param_mapping = EXCLUDED.param_mapping,
220
+ category = EXCLUDED.category;`);
221
+ }
222
+ // Upsert skill_files
223
+ for (const sf of skillFiles) {
224
+ const escapedContent = sf.content.replace(/'/g, "''");
225
+ const escapedPath = sf.file_path.replace(/'/g, "''");
226
+ statements.push(`INSERT INTO skill_files (skill_id, file_path, content)
227
+ VALUES (
228
+ (SELECT id FROM skills WHERE slug = '${slug}'),
229
+ '${escapedPath}',
230
+ '${escapedContent}'
231
+ )
232
+ ON CONFLICT (skill_id, file_path) DO UPDATE SET
233
+ content = EXCLUDED.content,
234
+ updated_at = now();`);
235
+ }
236
+ return statements.join("\n\n");
237
+ }
238
+ function capitalize(s) {
239
+ return s.charAt(0).toUpperCase() + s.slice(1);
240
+ }
241
+ function loadServerEnv() {
242
+ const serverEnv = resolve(process.cwd(), "server", ".env");
243
+ if (!existsSync(serverEnv))
244
+ return {};
245
+ const content = readFileSync(serverEnv, "utf8");
246
+ const result = {};
247
+ for (const line of content.split("\n")) {
248
+ const match = line.match(/^([A-Z_]+)=(.+)$/);
249
+ if (match)
250
+ result[match[1]] = match[2].trim();
251
+ }
252
+ return result;
253
+ }
@@ -0,0 +1,3 @@
1
+ export declare function setup(options?: {
2
+ key?: string;
3
+ }): Promise<void>;
@@ -0,0 +1,149 @@
1
+ import { createInterface } from "node:readline";
2
+ import { writeFileSync, mkdirSync, existsSync, readFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import chalk from "chalk";
5
+ import open from "open";
6
+ import ora from "ora";
7
+ import { saveApiKey, checkGitignore } from "../lib/config.js";
8
+ import { validateKey, getDashboardUrl } from "../lib/api.js";
9
+ import { ensureSkillsDir } from "../lib/paths.js";
10
+ function prompt(question) {
11
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
12
+ return new Promise((resolve) => {
13
+ rl.question(question, (answer) => {
14
+ rl.close();
15
+ resolve(answer.trim());
16
+ });
17
+ });
18
+ }
19
+ export async function setup(options = {}) {
20
+ console.log();
21
+ console.log(chalk.bold(" Skillett Setup"));
22
+ console.log();
23
+ // Check if already set up (skip in non-interactive mode)
24
+ const skillDir = resolve(process.cwd(), ".claude", "skills", "skillett");
25
+ if (!options.key && existsSync(resolve(skillDir, "SKILL.md"))) {
26
+ try {
27
+ const envContent = readFileSync(resolve(process.cwd(), ".env"), "utf8");
28
+ const existing = envContent.match(/^SKILLETT_API_KEY=(.+)$/m);
29
+ if (existing) {
30
+ console.log(chalk.green(" Already set up."));
31
+ console.log(chalk.gray(` Key: ${existing[1].slice(0, 11)}…`));
32
+ console.log(chalk.gray(` Skill: .claude/skills/skillett/`));
33
+ console.log();
34
+ const answer = await prompt("Reconfigure? (y/N) ");
35
+ if (answer.toLowerCase() !== "y") {
36
+ return;
37
+ }
38
+ console.log();
39
+ }
40
+ }
41
+ catch {
42
+ // .env doesn't exist, continue
43
+ }
44
+ }
45
+ let key = options.key;
46
+ // If key provided via --key flag or SKILLETT_API_KEY env var, use it directly
47
+ if (!key) {
48
+ key = process.env.SKILLETT_API_KEY;
49
+ }
50
+ if (!key) {
51
+ // Interactive mode — prompt for key
52
+ const dashboardUrl = getDashboardUrl();
53
+ console.log(chalk.bold(" Get your API key from the Skillett dashboard:"));
54
+ console.log(chalk.cyan(` ${dashboardUrl}/api-keys`));
55
+ console.log();
56
+ try {
57
+ await open(`${dashboardUrl}/api-keys`);
58
+ }
59
+ catch {
60
+ // Non-fatal — user can open manually
61
+ }
62
+ key = await prompt(" Paste your API key: ");
63
+ }
64
+ if (!key.startsWith("sk_")) {
65
+ console.log(chalk.red(" Invalid key format. Keys start with sk_"));
66
+ process.exit(1);
67
+ }
68
+ // Validate
69
+ const spinner = ora(" Validating key…").start();
70
+ const valid = await validateKey(key);
71
+ if (!valid) {
72
+ spinner.fail(" Invalid API key");
73
+ process.exit(1);
74
+ }
75
+ spinner.succeed(" Key validated");
76
+ // Save to .env
77
+ saveApiKey(key);
78
+ console.log(chalk.green(" ✓ Saved to .env"));
79
+ // Check .gitignore
80
+ if (!checkGitignore()) {
81
+ console.log(chalk.yellow(" ⚠ .env is not in your .gitignore — add it to avoid leaking your key"));
82
+ }
83
+ // Install core skill
84
+ ensureSkillsDir();
85
+ if (!existsSync(skillDir)) {
86
+ mkdirSync(skillDir, { recursive: true });
87
+ }
88
+ // Read from the real SKILL.md in the skills/skillett/ directory if available,
89
+ // otherwise use the bundled version
90
+ const repoSkillPath = resolve(process.cwd(), "skills", "skillett", "SKILL.md");
91
+ const coreSkillContent = existsSync(repoSkillPath)
92
+ ? readFileSync(repoSkillPath, "utf8")
93
+ : BUNDLED_SKILL_MD;
94
+ writeFileSync(resolve(skillDir, "SKILL.md"), coreSkillContent);
95
+ console.log(chalk.green(" ✓ Installed core skill to .claude/skills/skillett/"));
96
+ console.log();
97
+ console.log(chalk.bold(" You're all set! Your agent can now:"));
98
+ console.log(" • Discover skills: GET /v1");
99
+ console.log(" • Install a skill: npx skillett add github");
100
+ console.log(" • Execute: POST /v1/{integration}/{endpoint}");
101
+ console.log();
102
+ }
103
+ const BUNDLED_SKILL_MD = `---
104
+ name: skillett
105
+ version: 1.0.0
106
+ description: >
107
+ Skillett gateway — discover and execute agent skills for connected
108
+ integrations (GitHub, Slack, Gmail, etc.).
109
+ user-invocable: true
110
+ disable-model-invocation: false
111
+ ---
112
+
113
+ # Skillett
114
+
115
+ You are connected to Skillett, an agent skills platform. Users connect
116
+ their integrations (GitHub, Slack, Gmail, etc.) through the Skillett
117
+ dashboard, and you interact with those services through the Skillett API.
118
+
119
+ ## Authentication
120
+
121
+ Use the \`SKILLETT_API_KEY\` environment variable from the project's \`.env\`
122
+ file. Include it as a Bearer token in all requests:
123
+
124
+ Authorization: Bearer $SKILLETT_API_KEY
125
+
126
+ Base URL: \`https://api.skillett.dev\`
127
+
128
+ ## Discover Available Skills
129
+
130
+ GET /v1
131
+
132
+ Returns your connected skills with status (ready, incomplete, error),
133
+ install command, and endpoint count.
134
+
135
+ ## Execute an Endpoint
136
+
137
+ POST /v1/{integration}/{endpoint_name}
138
+ Content-Type: application/json
139
+
140
+ { "params": { ... } }
141
+
142
+ ## Install a Skill
143
+
144
+ When you discover a ready skill, install its full docs:
145
+
146
+ npx skillett add {slug}
147
+
148
+ This downloads the skill folder with detailed parameter docs for every endpoint.
149
+ `;
@@ -0,0 +1 @@
1
+ export declare function skills(integration?: string, endpoint?: string): Promise<void>;
@@ -0,0 +1,50 @@
1
+ import ora from "ora";
2
+ import { loadApiKey } from "../lib/config.js";
3
+ import { skillettFetch } from "../lib/api.js";
4
+ export async function skills(integration, endpoint) {
5
+ const apiKey = loadApiKey();
6
+ if (!apiKey) {
7
+ console.error(JSON.stringify({ error: "not_authenticated", message: "Run `skillett login` first." }));
8
+ process.exit(1);
9
+ }
10
+ const isTTY = process.stdout.isTTY;
11
+ // Level 3: Full endpoint docs
12
+ if (integration && endpoint) {
13
+ const spinner = isTTY ? ora(" Loading endpoint docs…").start() : null;
14
+ const res = await skillettFetch(`/v1/skills/${integration}/${endpoint}`, apiKey);
15
+ spinner?.stop();
16
+ if (!res.ok) {
17
+ const body = await res.json().catch(() => ({ error: "request_failed" }));
18
+ console.log(JSON.stringify(body, null, 2));
19
+ process.exit(1);
20
+ }
21
+ const data = await res.json();
22
+ console.log(JSON.stringify(data, null, 2));
23
+ return;
24
+ }
25
+ // Level 2: List endpoints for integration
26
+ if (integration) {
27
+ const spinner = isTTY ? ora(" Loading endpoints…").start() : null;
28
+ const res = await skillettFetch(`/v1/skills/${integration}`, apiKey);
29
+ spinner?.stop();
30
+ if (!res.ok) {
31
+ const body = await res.json().catch(() => ({ error: "request_failed" }));
32
+ console.log(JSON.stringify(body, null, 2));
33
+ process.exit(1);
34
+ }
35
+ const data = await res.json();
36
+ console.log(JSON.stringify(data, null, 2));
37
+ return;
38
+ }
39
+ // Level 1: List integrations
40
+ const spinner = isTTY ? ora(" Loading integrations…").start() : null;
41
+ const res = await skillettFetch("/v1/skills", apiKey);
42
+ spinner?.stop();
43
+ if (!res.ok) {
44
+ const body = await res.json().catch(() => ({ error: "request_failed" }));
45
+ console.log(JSON.stringify(body, null, 2));
46
+ process.exit(1);
47
+ }
48
+ const data = await res.json();
49
+ console.log(JSON.stringify(data, null, 2));
50
+ }
@@ -0,0 +1 @@
1
+ export declare function status(): Promise<void>;
@@ -0,0 +1,67 @@
1
+ import chalk from "chalk";
2
+ import ora from "ora";
3
+ import { loadApiKey } from "../lib/config.js";
4
+ import { skillettFetch } from "../lib/api.js";
5
+ export async function status() {
6
+ const apiKey = loadApiKey();
7
+ if (!apiKey) {
8
+ const output = {
9
+ authenticated: false,
10
+ message: "Not logged in. Run `skillett login` to get started.",
11
+ };
12
+ if (process.stdout.isTTY) {
13
+ console.log();
14
+ console.log(chalk.yellow(" Not logged in."));
15
+ console.log(chalk.gray(" Run `skillett login` to get started."));
16
+ console.log();
17
+ }
18
+ else {
19
+ console.log(JSON.stringify(output, null, 2));
20
+ }
21
+ process.exit(1);
22
+ }
23
+ const isTTY = process.stdout.isTTY;
24
+ const spinner = isTTY ? ora(" Checking status…").start() : null;
25
+ // Fetch user info and skills in parallel
26
+ const [userRes, skillsRes] = await Promise.all([
27
+ skillettFetch("/auth/me", apiKey).catch(() => null),
28
+ skillettFetch("/v1/skills", apiKey).catch(() => null),
29
+ ]);
30
+ spinner?.stop();
31
+ const user = userRes?.ok ? await userRes.json() : null;
32
+ const skillsData = skillsRes?.ok ? await skillsRes.json() : null;
33
+ if (isTTY) {
34
+ console.log();
35
+ if (user) {
36
+ console.log(chalk.bold(" Account"));
37
+ console.log(` Email: ${user.email || "—"}`);
38
+ console.log(` Name: ${user.display_name || "—"}`);
39
+ console.log(` Key: ${apiKey.slice(0, 11)}…`);
40
+ }
41
+ else {
42
+ console.log(chalk.red(" API key is invalid or expired."));
43
+ console.log(chalk.gray(" Run `skillett login` to re-authenticate."));
44
+ }
45
+ if (skillsData?.integrations?.length) {
46
+ console.log();
47
+ console.log(chalk.bold(" Integrations"));
48
+ for (const i of skillsData.integrations) {
49
+ const icon = i.status === "connected" ? chalk.green("●") : chalk.gray("○");
50
+ console.log(` ${icon} ${i.name} (${i.slug}) — ${i.endpoints} endpoints`);
51
+ }
52
+ }
53
+ else if (skillsData) {
54
+ console.log();
55
+ console.log(chalk.gray(" No integrations connected."));
56
+ console.log(chalk.gray(" Run `skillett connect <integration>` to get started."));
57
+ }
58
+ console.log();
59
+ }
60
+ else {
61
+ console.log(JSON.stringify({
62
+ authenticated: !!user,
63
+ user,
64
+ integrations: skillsData?.integrations || [],
65
+ }, null, 2));
66
+ }
67
+ }
@@ -0,0 +1 @@
1
+ export declare function whoami(): Promise<void>;
@@ -0,0 +1,27 @@
1
+ import chalk from "chalk";
2
+ import ora from "ora";
3
+ import { loadApiKey } from "../lib/config.js";
4
+ import { whoami as whoamiApi } from "../lib/api.js";
5
+ export async function whoami() {
6
+ const apiKey = loadApiKey();
7
+ if (!apiKey) {
8
+ console.log(chalk.red("Not logged in. Run: npx skillett login"));
9
+ process.exit(1);
10
+ }
11
+ const spinner = ora("Fetching user info…").start();
12
+ try {
13
+ const user = await whoamiApi(apiKey);
14
+ spinner.stop();
15
+ console.log();
16
+ console.log(chalk.bold("Logged in as:"));
17
+ console.log(` ${chalk.cyan("Email:")} ${user.email || "—"}`);
18
+ console.log(` ${chalk.cyan("Name:")} ${user.display_name || "—"}`);
19
+ console.log(` ${chalk.cyan("ID:")} ${user.id}`);
20
+ console.log();
21
+ }
22
+ catch (err) {
23
+ spinner.fail("Failed to fetch user info");
24
+ console.error(chalk.red(err.message));
25
+ process.exit(1);
26
+ }
27
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env node
2
+ import { loadEnv } from "./lib/config.js";
3
+ loadEnv();
4
+ import { Command } from "commander";
5
+ import { login } from "./commands/login.js";
6
+ import { skills } from "./commands/skills.js";
7
+ import { run } from "./commands/run.js";
8
+ import { status } from "./commands/status.js";
9
+ import { connect } from "./commands/connect.js";
10
+ import { disconnect } from "./commands/disconnect.js";
11
+ import { pull } from "./commands/pull.js";
12
+ import { seed } from "./commands/seed.js";
13
+ const HELP_TEXT = `
14
+ Skillett — Agent Skills Platform
15
+
16
+ Connect and use external services (GitHub, Slack, Gmail, etc.) through CLI commands.
17
+
18
+ COMMANDS
19
+
20
+ skillett login Authenticate with Skillett (opens browser)
21
+ skillett login --key <key> Authenticate with an API key directly
22
+ skillett status Show current user, connections, and API key info
23
+ skillett skills List available integrations and connection status
24
+ skillett skills <integration> List all endpoints for an integration
25
+ skillett skills <integ> <name> Show full docs for a specific endpoint
26
+ skillett run <integ> <name> Execute an endpoint
27
+ skillett connect <integration> Connect an integration (opens browser for OAuth)
28
+ skillett disconnect <integ> Disconnect an integration
29
+ skillett pull <integration> Download skill docs locally for faster agent context
30
+ skillett help Show this help message
31
+
32
+ QUICK START
33
+
34
+ skillett login # authenticate
35
+ skillett skills # see what's available
36
+ skillett run github create_issue \\
37
+ --repo acme/webapp --title "Fix login bug" # execute
38
+
39
+ PASSING PARAMETERS
40
+
41
+ As flags: skillett run github create_issue --repo acme/webapp --title "Bug"
42
+ As JSON: skillett run github create_issue '{"repo":"acme/webapp","title":"Bug"}'
43
+
44
+ OUTPUT
45
+
46
+ All commands output JSON to stdout. Errors include an "error" field.
47
+ Exit code 0 = success, non-zero = failure.
48
+ `;
49
+ const program = new Command();
50
+ program
51
+ .name("skillett")
52
+ .description("Skillett — Agent Skills Platform")
53
+ .version("0.2.0")
54
+ .addHelpText("after", HELP_TEXT);
55
+ // Default action (no subcommand) — show help
56
+ program.action(() => {
57
+ console.log(HELP_TEXT);
58
+ });
59
+ program
60
+ .command("login")
61
+ .description("Authenticate with Skillett (opens browser)")
62
+ .option("--key <api-key>", "API key (skips browser flow)")
63
+ .action(login);
64
+ // Keep setup as hidden alias
65
+ program
66
+ .command("setup", { hidden: true })
67
+ .option("--key <api-key>", "API key")
68
+ .action(login);
69
+ program
70
+ .command("status")
71
+ .description("Show current user, connections, and API key info")
72
+ .action(status);
73
+ program
74
+ .command("skills")
75
+ .description("List integrations, endpoints, or endpoint docs")
76
+ .argument("[integration]", "Integration slug (e.g. github)")
77
+ .argument("[endpoint]", "Endpoint name (e.g. create_issue)")
78
+ .action(skills);
79
+ program
80
+ .command("run")
81
+ .description("Execute an endpoint")
82
+ .argument("<integration>", "Integration slug (e.g. github)")
83
+ .argument("<endpoint>", "Endpoint name (e.g. create_issue)")
84
+ .allowUnknownOption(true)
85
+ .action((integration, endpoint, _opts, cmd) => {
86
+ // Pass remaining args (--flags and positional JSON) to the run handler
87
+ const extraArgs = cmd.args.slice(2);
88
+ return run(integration, endpoint, extraArgs);
89
+ });
90
+ program
91
+ .command("connect")
92
+ .description("Connect an integration (opens browser for OAuth)")
93
+ .argument("<integration>", "Integration slug (e.g. github)")
94
+ .action(connect);
95
+ program
96
+ .command("disconnect")
97
+ .description("Disconnect an integration")
98
+ .argument("<integration>", "Integration slug (e.g. github)")
99
+ .action(disconnect);
100
+ program
101
+ .command("pull")
102
+ .description("Download skill docs locally for faster agent context")
103
+ .argument("<integration>", "Integration slug (e.g. github)")
104
+ .option("--path <dir>", "Custom output directory")
105
+ .action(pull);
106
+ program
107
+ .command("seed", { hidden: true })
108
+ .argument("<skill-folder>", "Path to skill folder")
109
+ .option("--database-url <url>", "Postgres connection string")
110
+ .description("(Internal) Seed endpoints into the database from skill folder")
111
+ .action(seed);
112
+ program.parse();