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.
- package/dist/commands/add.d.ts +1 -0
- package/dist/commands/add.js +39 -0
- package/dist/commands/connect.d.ts +1 -0
- package/dist/commands/connect.js +81 -0
- package/dist/commands/disconnect.d.ts +1 -0
- package/dist/commands/disconnect.js +43 -0
- package/dist/commands/list.d.ts +1 -0
- package/dist/commands/list.js +43 -0
- package/dist/commands/login.d.ts +3 -0
- package/dist/commands/login.js +172 -0
- package/dist/commands/pull.d.ts +3 -0
- package/dist/commands/pull.js +46 -0
- package/dist/commands/remove.d.ts +1 -0
- package/dist/commands/remove.js +12 -0
- package/dist/commands/run.d.ts +1 -0
- package/dist/commands/run.js +85 -0
- package/dist/commands/seed.d.ts +3 -0
- package/dist/commands/seed.js +253 -0
- package/dist/commands/setup.d.ts +3 -0
- package/dist/commands/setup.js +149 -0
- package/dist/commands/skills.d.ts +1 -0
- package/dist/commands/skills.js +50 -0
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +67 -0
- package/dist/commands/whoami.d.ts +1 -0
- package/dist/commands/whoami.js +27 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +112 -0
- package/dist/lib/api.d.ts +25 -0
- package/dist/lib/api.js +50 -0
- package/dist/lib/config.d.ts +20 -0
- package/dist/lib/config.js +115 -0
- package/dist/lib/device-auth.d.ts +21 -0
- package/dist/lib/device-auth.js +38 -0
- package/dist/lib/paths.d.ts +4 -0
- package/dist/lib/paths.js +17 -0
- package/package.json +33 -0
|
@@ -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,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
|
+
}
|
package/dist/index.d.ts
ADDED
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();
|