@aisa-one/cli 0.2.2 → 0.2.4
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/CHANGELOG.md +154 -1
- package/README.md +250 -103
- package/dist/api.d.ts +46 -1
- package/dist/api.js +77 -8
- package/dist/cache.d.ts +18 -0
- package/dist/cache.js +65 -0
- package/dist/catalog.d.ts +90 -0
- package/dist/catalog.js +101 -0
- package/dist/commands/account.js +4 -2
- package/dist/commands/api.d.ts +18 -4
- package/dist/commands/api.js +357 -24
- package/dist/commands/auth.js +1 -1
- package/dist/commands/cacheCmd.d.ts +2 -0
- package/dist/commands/cacheCmd.js +10 -0
- package/dist/commands/completionCmd.d.ts +7 -0
- package/dist/commands/completionCmd.js +37 -0
- package/dist/commands/configCmd.js +19 -1
- package/dist/commands/finance.d.ts +4 -0
- package/dist/commands/finance.js +212 -29
- package/dist/commands/models.js +6 -0
- package/dist/commands/run.d.ts +21 -0
- package/dist/commands/run.js +113 -6
- package/dist/commands/search.js +8 -3
- package/dist/commands/skills.d.ts +6 -2
- package/dist/commands/skills.js +199 -144
- package/dist/commands/twitter.js +2 -3
- package/dist/commands/video.d.ts +40 -0
- package/dist/commands/video.js +272 -55
- package/dist/completion.d.ts +26 -0
- package/dist/completion.js +293 -0
- package/dist/constants.d.ts +55 -5
- package/dist/constants.js +136 -11
- package/dist/index.js +88 -26
- package/dist/skills-registry.d.ts +64 -0
- package/dist/skills-registry.js +219 -0
- package/dist/types.d.ts +24 -0
- package/dist/utils/file.d.ts +5 -1
- package/dist/utils/file.js +5 -1
- package/dist/utils/pool.d.ts +8 -0
- package/dist/utils/pool.js +27 -0
- package/package.json +10 -4
- package/tests/account.test.ts +0 -89
- package/tests/streaming.test.ts +0 -73
package/dist/commands/skills.js
CHANGED
|
@@ -1,80 +1,11 @@
|
|
|
1
1
|
import ora from "ora";
|
|
2
2
|
import chalk from "chalk";
|
|
3
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { error, success, hint, truncate } from "../utils/display.js";
|
|
6
6
|
import { expandHome, ensureDir, writeSkillFiles, removeDir, detectAgents } from "../utils/file.js";
|
|
7
7
|
import { AGENT_DIRS } from "../constants.js";
|
|
8
|
-
|
|
9
|
-
const SKILLS_REPO = "AIsa-team/agent-skills";
|
|
10
|
-
const GH_API = `https://api.github.com/repos/${SKILLS_REPO}`;
|
|
11
|
-
const GH_RAW = `https://raw.githubusercontent.com/${SKILLS_REPO}/main`;
|
|
12
|
-
/** Fetch the repo tree and extract top-level skill folder names. */
|
|
13
|
-
async function fetchSkillSlugs() {
|
|
14
|
-
const res = await fetch(`${GH_API}/git/trees/main`);
|
|
15
|
-
if (!res.ok)
|
|
16
|
-
throw new Error(`GitHub API error: ${res.status}`);
|
|
17
|
-
const data = (await res.json());
|
|
18
|
-
// Top-level directories (excluding dotfiles, LICENSE, README) are skills
|
|
19
|
-
return data.tree
|
|
20
|
-
.filter((e) => e.type === "tree" &&
|
|
21
|
-
!e.path.startsWith(".") &&
|
|
22
|
-
e.path !== "node_modules")
|
|
23
|
-
.map((e) => e.path);
|
|
24
|
-
}
|
|
25
|
-
/** Fetch and parse SKILL.md frontmatter for a single skill. */
|
|
26
|
-
async function fetchSkillMeta(slug) {
|
|
27
|
-
const res = await fetch(`${GH_RAW}/${slug}/SKILL.md`);
|
|
28
|
-
if (!res.ok)
|
|
29
|
-
return null;
|
|
30
|
-
const text = await res.text();
|
|
31
|
-
return parseSkillFrontmatter(slug, text);
|
|
32
|
-
}
|
|
33
|
-
/** Parse YAML frontmatter from SKILL.md content. */
|
|
34
|
-
function parseSkillFrontmatter(slug, content) {
|
|
35
|
-
const info = { slug, name: slug, description: "", emoji: "" };
|
|
36
|
-
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
37
|
-
if (!match)
|
|
38
|
-
return info;
|
|
39
|
-
const frontmatter = match[1];
|
|
40
|
-
const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
|
|
41
|
-
if (nameMatch)
|
|
42
|
-
info.name = nameMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
43
|
-
const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
|
|
44
|
-
if (descMatch)
|
|
45
|
-
info.description = descMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
46
|
-
const metaMatch = frontmatter.match(/^metadata:\s*(.+)$/m);
|
|
47
|
-
if (metaMatch) {
|
|
48
|
-
try {
|
|
49
|
-
const meta = JSON.parse(metaMatch[1]);
|
|
50
|
-
// agent-skills repo uses metadata.aisa; legacy skills used metadata.openclaw.
|
|
51
|
-
info.emoji = meta?.aisa?.emoji || meta?.openclaw?.emoji || "";
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
// ignore malformed metadata
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return info;
|
|
58
|
-
}
|
|
59
|
-
/** Fetch all files in a skill folder from GitHub. */
|
|
60
|
-
async function fetchSkillFiles(slug) {
|
|
61
|
-
const treeRes = await fetch(`${GH_API}/git/trees/main?recursive=1`);
|
|
62
|
-
if (!treeRes.ok)
|
|
63
|
-
throw new Error(`GitHub API error: ${treeRes.status}`);
|
|
64
|
-
const treeData = (await treeRes.json());
|
|
65
|
-
const prefix = `${slug}/`;
|
|
66
|
-
const blobs = treeData.tree.filter((e) => e.type === "blob" && e.path.startsWith(prefix));
|
|
67
|
-
const files = [];
|
|
68
|
-
await Promise.all(blobs.map(async (entry) => {
|
|
69
|
-
const res = await fetch(`${GH_RAW}/${entry.path}`);
|
|
70
|
-
if (!res.ok)
|
|
71
|
-
return;
|
|
72
|
-
const content = await res.text();
|
|
73
|
-
const relativePath = entry.path.slice(prefix.length);
|
|
74
|
-
files.push({ path: relativePath, content });
|
|
75
|
-
}));
|
|
76
|
-
return files;
|
|
77
|
-
}
|
|
8
|
+
import { SKILLS_REPO, getSkillIndex, listSkills, resolveSlug, leafName, fetchSkillFiles, fetchSkillMarkdown, parseSkillFrontmatter, } from "../skills-registry.js";
|
|
78
9
|
// --- Skill Templates (for init) ---
|
|
79
10
|
const TEMPLATES = {
|
|
80
11
|
default: `---
|
|
@@ -102,14 +33,14 @@ aisa run <slug> <path> -q "param=value"
|
|
|
102
33
|
`,
|
|
103
34
|
llm: `---
|
|
104
35
|
name: llm-assistant
|
|
105
|
-
description: "Use
|
|
36
|
+
description: "Use AIsa's unified LLM gateway to chat with 70+ AI models."
|
|
106
37
|
homepage: https://aisa.one
|
|
107
38
|
metadata: {"aisa":{"emoji":"🤖","requires":{"bins":["curl"],"env":["AISA_API_KEY"]},"primaryEnv":"AISA_API_KEY","compatibility":["openclaw","claude-code","hermes"]}}
|
|
108
39
|
---
|
|
109
40
|
|
|
110
41
|
# LLM Assistant Skill
|
|
111
42
|
|
|
112
|
-
Use the
|
|
43
|
+
Use the AIsa unified gateway to access 70+ language models through a single API.
|
|
113
44
|
|
|
114
45
|
## Authentication
|
|
115
46
|
|
|
@@ -138,14 +69,14 @@ aisa models --provider anthropic
|
|
|
138
69
|
`,
|
|
139
70
|
search: `---
|
|
140
71
|
name: web-search
|
|
141
|
-
description: "Search the web, YouTube, and academic papers via
|
|
72
|
+
description: "Search the web, YouTube, and academic papers via AIsa APIs."
|
|
142
73
|
homepage: https://aisa.one
|
|
143
74
|
metadata: {"aisa":{"emoji":"🔍","requires":{"bins":["curl"],"env":["AISA_API_KEY"]},"primaryEnv":"AISA_API_KEY","compatibility":["openclaw","claude-code","hermes"]}}
|
|
144
75
|
---
|
|
145
76
|
|
|
146
77
|
# Web Search Skill
|
|
147
78
|
|
|
148
|
-
Search the web, YouTube, and academic papers through
|
|
79
|
+
Search the web, YouTube, and academic papers through AIsa's unified search APIs.
|
|
149
80
|
|
|
150
81
|
## Authentication
|
|
151
82
|
|
|
@@ -156,7 +87,7 @@ export AISA_API_KEY=sk-your-key
|
|
|
156
87
|
## Smart Search
|
|
157
88
|
|
|
158
89
|
\`\`\`bash
|
|
159
|
-
aisa web-search "latest AI research" --type
|
|
90
|
+
aisa web-search "latest AI research" --type tavily
|
|
160
91
|
\`\`\`
|
|
161
92
|
|
|
162
93
|
## YouTube Search
|
|
@@ -173,14 +104,14 @@ aisa scholar "transformer architecture"
|
|
|
173
104
|
`,
|
|
174
105
|
finance: `---
|
|
175
106
|
name: finance-analyst
|
|
176
|
-
description: "Access stock prices, earnings, SEC filings, and financial data via
|
|
107
|
+
description: "Access stock prices, earnings, SEC filings, and financial data via AIsa."
|
|
177
108
|
homepage: https://aisa.one
|
|
178
109
|
metadata: {"aisa":{"emoji":"📊","requires":{"bins":["curl"],"env":["AISA_API_KEY"]},"primaryEnv":"AISA_API_KEY","compatibility":["openclaw","claude-code","hermes"]}}
|
|
179
110
|
---
|
|
180
111
|
|
|
181
112
|
# Finance Analyst Skill
|
|
182
113
|
|
|
183
|
-
Access real-time and historical financial data through
|
|
114
|
+
Access real-time and historical financial data through AIsa's finance APIs.
|
|
184
115
|
|
|
185
116
|
## Authentication
|
|
186
117
|
|
|
@@ -205,14 +136,14 @@ aisa crypto ETH --period 30d
|
|
|
205
136
|
`,
|
|
206
137
|
twitter: `---
|
|
207
138
|
name: twitter-manager
|
|
208
|
-
description: "Search Twitter, get user profiles and trends via
|
|
139
|
+
description: "Search Twitter, get user profiles and trends via AIsa."
|
|
209
140
|
homepage: https://aisa.one
|
|
210
141
|
metadata: {"aisa":{"emoji":"🐦","requires":{"bins":["curl"],"env":["AISA_API_KEY"]},"primaryEnv":"AISA_API_KEY","compatibility":["openclaw","claude-code","hermes"]}}
|
|
211
142
|
---
|
|
212
143
|
|
|
213
144
|
# Twitter Manager Skill
|
|
214
145
|
|
|
215
|
-
Interact with Twitter/X through
|
|
146
|
+
Interact with Twitter/X through AIsa's Twitter APIs.
|
|
216
147
|
|
|
217
148
|
## Authentication
|
|
218
149
|
|
|
@@ -240,14 +171,14 @@ aisa twitter trends
|
|
|
240
171
|
`,
|
|
241
172
|
video: `---
|
|
242
173
|
name: video-generator
|
|
243
|
-
description: "Generate videos from text prompts using
|
|
174
|
+
description: "Generate videos from text prompts using AIsa's video synthesis API."
|
|
244
175
|
homepage: https://aisa.one
|
|
245
176
|
metadata: {"aisa":{"emoji":"🎬","requires":{"bins":["curl"],"env":["AISA_API_KEY"]},"primaryEnv":"AISA_API_KEY","compatibility":["openclaw","claude-code","hermes"]}}
|
|
246
177
|
---
|
|
247
178
|
|
|
248
179
|
# Video Generator Skill
|
|
249
180
|
|
|
250
|
-
Generate videos from text prompts using
|
|
181
|
+
Generate videos from text prompts using AIsa's AI video generation APIs.
|
|
251
182
|
|
|
252
183
|
## Authentication
|
|
253
184
|
|
|
@@ -275,27 +206,40 @@ aisa video status <task-id>
|
|
|
275
206
|
`,
|
|
276
207
|
};
|
|
277
208
|
// --- Commands ---
|
|
278
|
-
|
|
209
|
+
function printSkill(s) {
|
|
210
|
+
const emoji = s.emoji ? `${s.emoji} ` : "";
|
|
211
|
+
console.log(` ${emoji}${chalk.cyan.bold(s.name)} ${chalk.gray(s.slug)}`);
|
|
212
|
+
if (s.description) {
|
|
213
|
+
console.log(` ${chalk.gray(truncate(s.description, 80))}`);
|
|
214
|
+
}
|
|
215
|
+
console.log();
|
|
216
|
+
}
|
|
217
|
+
export async function skillsListAction(options) {
|
|
279
218
|
const spinner = ora("Fetching skills from GitHub...").start();
|
|
280
219
|
try {
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
220
|
+
const index = await getSkillIndex({ refresh: options.refresh });
|
|
221
|
+
let skills = await listSkills(index);
|
|
222
|
+
if (options.category) {
|
|
223
|
+
const cat = options.category.toLowerCase();
|
|
224
|
+
skills = skills.filter((s) => s.slug.split("/")[0].toLowerCase() === cat);
|
|
225
|
+
}
|
|
226
|
+
const total = skills.length;
|
|
227
|
+
if (options.limit)
|
|
228
|
+
skills = skills.slice(0, parseInt(options.limit));
|
|
285
229
|
spinner.stop();
|
|
286
230
|
if (skills.length === 0) {
|
|
287
|
-
console.log(
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
console.log(` ${emoji}${chalk.cyan.bold(s.name)} ${chalk.gray(s.slug)}`);
|
|
294
|
-
if (s.description) {
|
|
295
|
-
console.log(` ${chalk.gray(truncate(s.description, 80))}`);
|
|
231
|
+
console.log(options.category
|
|
232
|
+
? ` No skills in category "${options.category}".`
|
|
233
|
+
: " No skills found.");
|
|
234
|
+
if (options.category) {
|
|
235
|
+
const categories = [...new Set(index.slugs.map((s) => s.split("/")[0]))];
|
|
236
|
+
hint(`Categories: ${categories.join(", ")}`);
|
|
296
237
|
}
|
|
297
|
-
|
|
238
|
+
return;
|
|
298
239
|
}
|
|
240
|
+
console.log(chalk.bold(`\n ${skills.length}${skills.length < total ? ` of ${total}` : ""} skills available\n`));
|
|
241
|
+
for (const s of skills)
|
|
242
|
+
printSkill(s);
|
|
299
243
|
hint("Install: aisa skills install <slug>");
|
|
300
244
|
hint("Details: aisa skills show <slug>");
|
|
301
245
|
}
|
|
@@ -304,30 +248,26 @@ export async function skillsListAction(_options) {
|
|
|
304
248
|
error(err.message);
|
|
305
249
|
}
|
|
306
250
|
}
|
|
307
|
-
export async function skillsSearchAction(query,
|
|
251
|
+
export async function skillsSearchAction(query, options) {
|
|
308
252
|
const spinner = ora(`Searching skills: "${query}"...`).start();
|
|
309
253
|
try {
|
|
310
|
-
const
|
|
311
|
-
const
|
|
312
|
-
const skills = metas.filter((m) => m !== null);
|
|
254
|
+
const index = await getSkillIndex({ refresh: options.refresh });
|
|
255
|
+
const skills = await listSkills(index);
|
|
313
256
|
const q = query.toLowerCase();
|
|
314
|
-
|
|
257
|
+
let matches = skills.filter((s) => s.name.toLowerCase().includes(q) ||
|
|
315
258
|
s.slug.toLowerCase().includes(q) ||
|
|
316
259
|
s.description.toLowerCase().includes(q));
|
|
260
|
+
const total = matches.length;
|
|
261
|
+
if (options.limit)
|
|
262
|
+
matches = matches.slice(0, parseInt(options.limit));
|
|
317
263
|
spinner.stop();
|
|
318
264
|
if (matches.length === 0) {
|
|
319
265
|
console.log(` No skills found for "${query}".`);
|
|
320
266
|
return;
|
|
321
267
|
}
|
|
322
|
-
console.log(chalk.bold(`\n ${matches.length} result(s)\n`));
|
|
323
|
-
for (const s of matches)
|
|
324
|
-
|
|
325
|
-
console.log(` ${emoji}${chalk.cyan.bold(s.name)} ${chalk.gray(s.slug)}`);
|
|
326
|
-
if (s.description) {
|
|
327
|
-
console.log(` ${chalk.gray(truncate(s.description, 80))}`);
|
|
328
|
-
}
|
|
329
|
-
console.log();
|
|
330
|
-
}
|
|
268
|
+
console.log(chalk.bold(`\n ${matches.length}${matches.length < total ? ` of ${total}` : ""} result(s)\n`));
|
|
269
|
+
for (const s of matches)
|
|
270
|
+
printSkill(s);
|
|
331
271
|
hint("Install: aisa skills install <slug>");
|
|
332
272
|
}
|
|
333
273
|
catch (err) {
|
|
@@ -338,29 +278,19 @@ export async function skillsSearchAction(query, _options) {
|
|
|
338
278
|
export async function skillsShowAction(slug) {
|
|
339
279
|
const spinner = ora(`Loading ${slug}...`).start();
|
|
340
280
|
try {
|
|
341
|
-
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}
|
|
348
|
-
const content = await res.text();
|
|
349
|
-
const meta = parseSkillFrontmatter(slug, content);
|
|
350
|
-
// Fetch file list
|
|
351
|
-
const treeRes = await fetch(`${GH_API}/git/trees/main?recursive=1`);
|
|
352
|
-
const treeData = (await treeRes.json());
|
|
353
|
-
const prefix = `${slug}/`;
|
|
354
|
-
const files = treeData.tree
|
|
355
|
-
.filter((e) => e.type === "blob" && e.path.startsWith(prefix))
|
|
356
|
-
.map((e) => e.path.slice(prefix.length));
|
|
281
|
+
const index = await getSkillIndex();
|
|
282
|
+
const canonical = resolveSlug(slug, index);
|
|
283
|
+
const content = await fetchSkillMarkdown(canonical);
|
|
284
|
+
const meta = parseSkillFrontmatter(canonical, content);
|
|
285
|
+
const prefix = `${canonical}/`;
|
|
286
|
+
const files = (index.blobs[canonical] || []).map((b) => b.path.slice(prefix.length));
|
|
357
287
|
spinner.stop();
|
|
358
288
|
const emoji = meta.emoji ? `${meta.emoji} ` : "";
|
|
359
289
|
console.log(`\n ${emoji}${chalk.cyan.bold(meta.name)}`);
|
|
360
290
|
console.log(` ${meta.description}`);
|
|
361
|
-
console.log(` Slug: ${chalk.gray(
|
|
291
|
+
console.log(` Slug: ${chalk.gray(canonical)}`);
|
|
362
292
|
console.log(` Files: ${files.join(", ")}`);
|
|
363
|
-
console.log(` Source: ${chalk.gray(`https://github.com/${SKILLS_REPO}/tree/main/${
|
|
293
|
+
console.log(` Source: ${chalk.gray(`https://github.com/${SKILLS_REPO}/tree/main/${canonical}`)}`);
|
|
364
294
|
// Print SKILL.md body (after frontmatter)
|
|
365
295
|
const body = content.replace(/^---\n[\s\S]*?\n---\n*/, "").trim();
|
|
366
296
|
if (body) {
|
|
@@ -368,20 +298,83 @@ export async function skillsShowAction(slug) {
|
|
|
368
298
|
console.log(body);
|
|
369
299
|
}
|
|
370
300
|
console.log();
|
|
371
|
-
hint(`Install: aisa skills install ${
|
|
301
|
+
hint(`Install: aisa skills install ${leafName(canonical)}`);
|
|
372
302
|
}
|
|
373
303
|
catch (err) {
|
|
374
304
|
spinner.fail("Failed to load skill");
|
|
375
305
|
error(err.message);
|
|
376
306
|
}
|
|
377
307
|
}
|
|
308
|
+
/** Agent skill loaders expect one directory level, named to match SKILL.md's `name:`. */
|
|
309
|
+
function installedName(canonical) {
|
|
310
|
+
return leafName(canonical);
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Marker recording which skill owns an installed directory.
|
|
314
|
+
*
|
|
315
|
+
* Frontmatter `name:` is not an identity: two skills in different categories can
|
|
316
|
+
* share both a leaf name and a `name:`, and comparing those would let the second
|
|
317
|
+
* install silently overwrite the first. The canonical slug is the only unique
|
|
318
|
+
* key, and nothing in SKILL.md carries it.
|
|
319
|
+
*/
|
|
320
|
+
const MARKER_FILE = ".aisa-skill.json";
|
|
321
|
+
function readMarker(dir) {
|
|
322
|
+
try {
|
|
323
|
+
const parsed = JSON.parse(readFileSync(join(dir, MARKER_FILE), "utf-8"));
|
|
324
|
+
return typeof parsed?.slug === "string" ? parsed : undefined;
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Identify the skill currently installed at `dir`. Falls back to frontmatter for
|
|
332
|
+
* directories written before markers existed, where the name is all we have.
|
|
333
|
+
*/
|
|
334
|
+
function occupant(dir) {
|
|
335
|
+
const marker = readMarker(dir);
|
|
336
|
+
if (marker)
|
|
337
|
+
return { slug: marker.slug };
|
|
338
|
+
const skillMd = join(dir, "SKILL.md");
|
|
339
|
+
if (!existsSync(skillMd))
|
|
340
|
+
return undefined;
|
|
341
|
+
try {
|
|
342
|
+
return { name: parseSkillFrontmatter("", readFileSync(skillMd, "utf-8")).name };
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Why replacing what is already installed would be unsafe, or undefined when it
|
|
350
|
+
* is the same skill.
|
|
351
|
+
*
|
|
352
|
+
* Frontmatter `name:` cannot settle ownership: two skills in different
|
|
353
|
+
* categories may share both a leaf name and a `name:`. That ambiguity is only
|
|
354
|
+
* reachable when the caller named a category — a bare name would have been
|
|
355
|
+
* rejected as ambiguous before we got here — so an unmarked directory is
|
|
356
|
+
* treated as unverifiable exactly in that case.
|
|
357
|
+
*/
|
|
358
|
+
function describeConflict(present, canonical, expectedName, requestedCanonical) {
|
|
359
|
+
if (!present)
|
|
360
|
+
return undefined;
|
|
361
|
+
if (present.slug) {
|
|
362
|
+
return present.slug === canonical ? undefined : `holds a different skill (${present.slug})`;
|
|
363
|
+
}
|
|
364
|
+
if (requestedCanonical) {
|
|
365
|
+
return `holds an unmarked install that cannot be verified as ${canonical}`;
|
|
366
|
+
}
|
|
367
|
+
return present.name === expectedName ? undefined : `holds a different skill ("${present.name}")`;
|
|
368
|
+
}
|
|
378
369
|
export async function skillsInstallAction(slug, options) {
|
|
379
370
|
const spinner = ora(`Fetching skill '${slug}' from GitHub...`).start();
|
|
380
371
|
try {
|
|
381
|
-
const
|
|
372
|
+
const index = await getSkillIndex();
|
|
373
|
+
const canonical = resolveSlug(slug, index);
|
|
374
|
+
const files = await fetchSkillFiles(canonical, index);
|
|
382
375
|
if (files.length === 0) {
|
|
383
376
|
spinner.fail("Skill not found");
|
|
384
|
-
error(`No
|
|
377
|
+
error(`No files found for "${canonical}" in ${SKILLS_REPO}`);
|
|
385
378
|
return;
|
|
386
379
|
}
|
|
387
380
|
spinner.stop();
|
|
@@ -405,15 +398,49 @@ export async function skillsInstallAction(slug, options) {
|
|
|
405
398
|
targets = Object.keys(AGENT_DIRS);
|
|
406
399
|
}
|
|
407
400
|
}
|
|
401
|
+
const dirName = installedName(canonical);
|
|
402
|
+
const expectedName = parseSkillFrontmatter(canonical, files.find((f) => f.path === "SKILL.md")?.content.toString("utf-8") || "").name;
|
|
403
|
+
const marker = { slug: canonical, installedAt: new Date().toISOString() };
|
|
404
|
+
// A bare name is only resolvable when the leaf is unique across the repo —
|
|
405
|
+
// resolveSlug rejects it otherwise — so whatever occupies the directory must
|
|
406
|
+
// be this skill. A canonical slug bypasses that check, which is exactly when
|
|
407
|
+
// an unmarked directory becomes unverifiable.
|
|
408
|
+
const requestedCanonical = slug.includes("/");
|
|
408
409
|
let installed = 0;
|
|
409
410
|
for (const agent of targets) {
|
|
410
|
-
const dir = expandHome(join(AGENT_DIRS[agent],
|
|
411
|
+
const dir = expandHome(join(AGENT_DIRS[agent], dirName));
|
|
412
|
+
// A different skill already occupying this directory name means the
|
|
413
|
+
// install would replace it. Compare canonical slugs where we have them —
|
|
414
|
+
// names collide across categories.
|
|
415
|
+
const exists = existsSync(dir);
|
|
416
|
+
const present = exists ? occupant(dir) : undefined;
|
|
417
|
+
const conflict = exists && !present
|
|
418
|
+
? // No marker and no readable SKILL.md: an interrupted install, or a
|
|
419
|
+
// directory the user put there. Neither is safe to delete silently.
|
|
420
|
+
"exists but is not a recognizable skill install"
|
|
421
|
+
: describeConflict(present, canonical, expectedName, requestedCanonical);
|
|
422
|
+
if (conflict && !options.force) {
|
|
423
|
+
error(`${AGENT_DIRS[agent]}${dirName}/ ${conflict} — pass --force to replace`);
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
// Replace rather than merge: writing over a directory only overwrites
|
|
427
|
+
// files that happen to share a name, leaving the previous skill's scripts
|
|
428
|
+
// and assets behind for an agent to keep loading. Keyed on the directory
|
|
429
|
+
// existing, not on identifying its occupant — an unidentifiable directory
|
|
430
|
+
// still must not be merged into.
|
|
431
|
+
if (exists)
|
|
432
|
+
removeDir(dir);
|
|
411
433
|
ensureDir(dir);
|
|
412
434
|
writeSkillFiles(dir, files);
|
|
413
|
-
|
|
435
|
+
writeFileSync(join(dir, MARKER_FILE), JSON.stringify(marker, null, 2) + "\n", "utf-8");
|
|
436
|
+
console.log(` ${chalk.green("✓")} ${AGENT_DIRS[agent]}${dirName} (${agent})`);
|
|
414
437
|
installed++;
|
|
415
438
|
}
|
|
416
|
-
|
|
439
|
+
if (installed === 0) {
|
|
440
|
+
error("Nothing installed.");
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
success(`Skill '${canonical}' installed to ${installed} agent(s)`);
|
|
417
444
|
}
|
|
418
445
|
catch (err) {
|
|
419
446
|
spinner.fail("Failed to install skill");
|
|
@@ -421,6 +448,12 @@ export async function skillsInstallAction(slug, options) {
|
|
|
421
448
|
}
|
|
422
449
|
}
|
|
423
450
|
export function skillsRemoveAction(slug, options) {
|
|
451
|
+
// Installed directories use the leaf name, so a bare name is what's on disk.
|
|
452
|
+
const requested = slug.replace(/^\/+|\/+$/g, "");
|
|
453
|
+
const dirName = leafName(requested);
|
|
454
|
+
// A canonical slug names one specific skill; a bare name names whatever holds
|
|
455
|
+
// that directory.
|
|
456
|
+
const wantsCanonical = requested.includes("/");
|
|
424
457
|
let targets;
|
|
425
458
|
if (options.agent && options.agent !== "all") {
|
|
426
459
|
targets = [options.agent];
|
|
@@ -429,20 +462,42 @@ export function skillsRemoveAction(slug, options) {
|
|
|
429
462
|
targets = Object.keys(AGENT_DIRS);
|
|
430
463
|
}
|
|
431
464
|
let removed = 0;
|
|
465
|
+
let refused = 0;
|
|
432
466
|
for (const agent of targets) {
|
|
433
|
-
const dir = expandHome(join(AGENT_DIRS[agent],
|
|
434
|
-
if (existsSync(dir))
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
467
|
+
const dir = expandHome(join(AGENT_DIRS[agent], dirName));
|
|
468
|
+
if (!existsSync(dir))
|
|
469
|
+
continue;
|
|
470
|
+
const marker = readMarker(dir);
|
|
471
|
+
// Leaf names collide across categories, so deleting by name alone can take
|
|
472
|
+
// out a different skill than the one asked for.
|
|
473
|
+
if (wantsCanonical && !options.force) {
|
|
474
|
+
if (marker && marker.slug !== requested) {
|
|
475
|
+
error(`${AGENT_DIRS[agent]}${dirName}/ holds ${marker.slug}, not ${requested} — skipped`);
|
|
476
|
+
refused++;
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
// Pre-marker installs carry no slug, and a same-named skill from another
|
|
480
|
+
// category is indistinguishable from the requested one.
|
|
481
|
+
if (!marker) {
|
|
482
|
+
error(`${AGENT_DIRS[agent]}${dirName}/ has no install marker — cannot verify it is ${requested} — skipped`);
|
|
483
|
+
refused++;
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (!marker) {
|
|
488
|
+
hint(`${AGENT_DIRS[agent]}${dirName}/ has no install marker — removing by directory name`);
|
|
438
489
|
}
|
|
490
|
+
removeDir(dir);
|
|
491
|
+
console.log(` ${chalk.green("✓")} Removed from ${AGENT_DIRS[agent]}`);
|
|
492
|
+
removed++;
|
|
439
493
|
}
|
|
440
494
|
if (removed === 0) {
|
|
441
|
-
console.log(
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
495
|
+
console.log(refused > 0
|
|
496
|
+
? " Nothing removed — pass --force to remove regardless of what is installed."
|
|
497
|
+
: " Skill not found in any agent directory.");
|
|
498
|
+
return;
|
|
445
499
|
}
|
|
500
|
+
success(`Removed '${dirName}' from ${removed} agent(s)`);
|
|
446
501
|
}
|
|
447
502
|
export function skillsInitAction(name, options) {
|
|
448
503
|
const dir = resolve(name);
|
package/dist/commands/twitter.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import ora from "ora";
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { requireApiKey, getConfig, setConfig } from "../config.js";
|
|
4
|
-
import { apiRequest } from "../api.js";
|
|
5
|
-
import { APIS_BASE_URL } from "../constants.js";
|
|
4
|
+
import { apiRequest, resolveBases } from "../api.js";
|
|
6
5
|
import { error, success, formatJson } from "../utils/display.js";
|
|
7
6
|
function requireCookies() {
|
|
8
7
|
const cookies = getConfig("twitterCookies");
|
|
@@ -344,7 +343,7 @@ export async function twitterUploadMediaAction(filePath, options) {
|
|
|
344
343
|
}
|
|
345
344
|
formData.append("login_cookies", auth.login_cookies);
|
|
346
345
|
formData.append("proxy", auth.proxy);
|
|
347
|
-
const url = `${
|
|
346
|
+
const url = `${resolveBases().domain}/twitter/upload_media_v2`;
|
|
348
347
|
const res = await fetch(url, {
|
|
349
348
|
method: "POST",
|
|
350
349
|
headers: {
|
package/dist/commands/video.d.ts
CHANGED
|
@@ -1,9 +1,49 @@
|
|
|
1
|
+
import { type MediaType } from "../constants.js";
|
|
2
|
+
/**
|
|
3
|
+
* Each upstream nests the finished video somewhere different (DashScope puts it
|
|
4
|
+
* at result.output.video_url, Seedance under result.data[].content[].url), so
|
|
5
|
+
* search rather than hardcode a path.
|
|
6
|
+
*
|
|
7
|
+
* Every pass covers the whole tree before the next one starts, and each is
|
|
8
|
+
* narrower than a plain "ends in url" match would be. Responses ship thumbnails
|
|
9
|
+
* and cover images beside the clip, and `thumbnail_url` both ends in "url" and
|
|
10
|
+
* tends to sit closer to the root than the video does — matching it would make
|
|
11
|
+
* `--output` download a still image.
|
|
12
|
+
*/
|
|
13
|
+
export declare function findVideoUrl(value: unknown): string | undefined;
|
|
14
|
+
export interface MediaInput {
|
|
15
|
+
type: MediaType;
|
|
16
|
+
url: string;
|
|
17
|
+
}
|
|
18
|
+
/** Parse `--media first_frame=https://…`, and bare URLs from `--image`. */
|
|
19
|
+
export declare function parseMedia(specs?: string[], images?: string[]): MediaInput[];
|
|
20
|
+
/**
|
|
21
|
+
* Build the request body for a model's vendor. The gateway rewrites only the
|
|
22
|
+
* top-level `model` and forwards the rest verbatim, so a Bailian body sent to a
|
|
23
|
+
* Seedance model is rejected with a 400.
|
|
24
|
+
*/
|
|
25
|
+
export declare function buildVideoBody(model: string, prompt: string, media: MediaInput[], options: {
|
|
26
|
+
resolution?: string;
|
|
27
|
+
duration?: string;
|
|
28
|
+
}): Record<string, unknown>;
|
|
29
|
+
/**
|
|
30
|
+
* Parse a `--body` payload. The option means "send this verbatim", so the model
|
|
31
|
+
* it names wins over the CLI's default — only an explicit `--model` overrides
|
|
32
|
+
* it. Anything else would silently redirect a valid request to another model.
|
|
33
|
+
*/
|
|
34
|
+
export declare function parseBodyOverride(raw: string, explicitModel?: string): Record<string, unknown>;
|
|
1
35
|
export declare function videoCreateAction(prompt: string, options: {
|
|
2
36
|
model?: string;
|
|
3
37
|
wait?: boolean;
|
|
4
38
|
output?: string;
|
|
5
39
|
raw?: boolean;
|
|
40
|
+
resolution?: string;
|
|
41
|
+
duration?: string;
|
|
42
|
+
image?: string[];
|
|
43
|
+
media?: string[];
|
|
44
|
+
body?: string;
|
|
6
45
|
}): Promise<void>;
|
|
7
46
|
export declare function videoStatusAction(taskId: string, options: {
|
|
8
47
|
raw?: boolean;
|
|
48
|
+
output?: string;
|
|
9
49
|
}): Promise<void>;
|