@aisa-one/cli 0.2.0 → 0.2.1

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 ADDED
@@ -0,0 +1,62 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@aisa-one/cli` are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Fixed
11
+
12
+ - **Skills registry now points at the canonical `agent-skills` repo.** The
13
+ `SKILLS_REPO` constant in `src/commands/skills.ts` was pinned to
14
+ `AIsa-team/OpenClaw-Skills`, which has been renamed to
15
+ `AIsa-team/agent-skills`. Every `aisa skills` subcommand kept working
16
+ only because GitHub serves a 301 redirect on the old slug; the constant
17
+ now references the canonical name so the registry no longer depends on
18
+ that redirect. ([#3](https://github.com/AIsa-team/cli/pull/3))
19
+ - **`aisa skills list` and `aisa skills show` now render emojis for live
20
+ skills.** `parseSkillFrontmatter` only read `metadata.openclaw.emoji`,
21
+ but skills in `agent-skills` publish under `metadata.aisa.emoji`. The
22
+ parser now reads the new key first and falls back to the legacy
23
+ `openclaw` key for any older skills still in the wild.
24
+ ([#4](https://github.com/AIsa-team/cli/pull/4))
25
+
26
+ ### Changed
27
+
28
+ - **`aisa skills init` templates aligned with `agent-skills` conventions.**
29
+ All six built-in scaffolds (`default`, `llm`, `search`, `finance`,
30
+ `twitter`, `video`) now emit:
31
+ - `homepage: https://aisa.one` (was `https://openclaw.ai`)
32
+ - `metadata.aisa.*` instead of `metadata.openclaw.*`
33
+ - `compatibility: ["openclaw", "claude-code", "hermes"]` to match the
34
+ shape used by `marketpulse`, `multi-source-search`,
35
+ `prediction-market-arbitrage`, and the rest of the live skills.
36
+
37
+ Skills scaffolded with `aisa skills init` will now render correctly in
38
+ the registry and pass review when contributed back upstream.
39
+ ([#5](https://github.com/AIsa-team/cli/pull/5))
40
+
41
+ ### Documentation
42
+
43
+ - README Skills section now references
44
+ [`AIsa-team/agent-skills`](https://github.com/AIsa-team/agent-skills)
45
+ directly instead of the renamed `OpenClaw-Skills`.
46
+ ([#2](https://github.com/AIsa-team/cli/pull/2))
47
+
48
+ ## [0.2.0] — Initial public release
49
+
50
+ - LLM gateway commands (`aisa chat`, `aisa models`).
51
+ - API discovery and execution (`aisa run`, `aisa api …` WIP).
52
+ - Web search (`aisa web-search`, `aisa scholar`).
53
+ - Finance (`aisa stock`, `aisa crypto`, `aisa screener`).
54
+ - Twitter/X (`aisa twitter …`, `aisa tweet`).
55
+ - Video generation (`aisa video create|status`).
56
+ - Skills registry (`aisa skills list|search|show|install|remove|init`).
57
+ - MCP server auto-config (`aisa mcp setup|status`).
58
+ - Config commands (`aisa config get|set|list|reset`) and auth
59
+ (`aisa login|logout|whoami`).
60
+
61
+ [Unreleased]: https://github.com/AIsa-team/cli/compare/v0.2.0...HEAD
62
+ [0.2.0]: https://github.com/AIsa-team/cli/releases/tag/v0.2.0
package/README.md CHANGED
@@ -47,14 +47,14 @@ aisa whoami # Show auth status
47
47
  Chat with 70+ models (GPT, Claude, Gemini, Qwen, Deepseek, Grok) through a single OpenAI-compatible endpoint.
48
48
 
49
49
  ```bash
50
- aisa chat "your message" --model gpt-4.1
50
+ aisa chat "your message" --model gpt-4.1-mini
51
51
  aisa chat "explain this" --model claude-opus-4-6 --stream
52
52
  aisa chat "respond in JSON" --model gemini-2.5-pro --json
53
53
  echo "summarize this" | aisa chat # pipe support
54
54
 
55
55
  aisa models # list all models
56
56
  aisa models --provider anthropic # filter by provider
57
- aisa models show gpt-4.1 # model details
57
+ aisa models show gpt-4.1-mini # model details
58
58
  ```
59
59
 
60
60
  ### API Discovery & Execution
@@ -121,16 +121,16 @@ aisa usage --limit 20 # 🚧 WIP
121
121
 
122
122
  ## Skills
123
123
 
124
- Skills are markdown files that teach AI coding agents (Claude Code, Cursor, Copilot, etc.) how to use AISA APIs. Skills are sourced from the [OpenClaw-Skills](https://github.com/AIsa-team/OpenClaw-Skills) repository.
124
+ Skills are markdown files that teach AI coding agents (Claude Code, Cursor, Copilot, etc.) how to use AISA APIs. Skills are sourced from the [agent-skills](https://github.com/AIsa-team/agent-skills) repository.
125
125
 
126
126
  ### Browse & Install
127
127
 
128
128
  ```bash
129
129
  aisa skills list # list all available skills
130
130
  aisa skills search "financial analysis" # search by keyword
131
- aisa skills show market # show skill details
132
- aisa skills install market # install to agent directories
133
- aisa skills remove market # uninstall
131
+ aisa skills show marketpulse # show skill details
132
+ aisa skills install marketpulse # install to agent directories
133
+ aisa skills remove marketpulse # uninstall
134
134
  ```
135
135
 
136
136
  Skills install to agent directories automatically:
@@ -155,7 +155,7 @@ aisa skills init my-skill --template llm # LLM template
155
155
 
156
156
  Available templates: `default`, `llm`, `search`, `finance`, `twitter`, `video`
157
157
 
158
- To publish a skill, submit a pull request to [AIsa-team/OpenClaw-Skills](https://github.com/AIsa-team/OpenClaw-Skills).
158
+ To publish a skill, submit a pull request to [AIsa-team/agent-skills](https://github.com/AIsa-team/agent-skills).
159
159
 
160
160
  ## MCP Server
161
161
 
@@ -178,7 +178,7 @@ aisa config reset # reset to defaults
178
178
  ```
179
179
 
180
180
  Settings:
181
- - `defaultModel` — default model for `aisa chat` (default: `gpt-4.1`)
181
+ - `defaultModel` — default model for `aisa chat` (default: `gpt-4.1-mini`)
182
182
  - `baseUrl` — API base URL (default: `https://api.aisa.one`)
183
183
  - `outputFormat` — output format: `text` or `json`
184
184
 
@@ -0,0 +1,5 @@
1
+ import type { AgentManifest } from "@aisa-one/agent-spec";
2
+ export type Prompt = (question: string) => Promise<string>;
3
+ export declare function collectEnv(manifest: AgentManifest, existingEnvText: string | null, deps?: {
4
+ prompt?: Prompt;
5
+ }): Promise<string>;
@@ -0,0 +1,49 @@
1
+ import { getApiKey } from "../config.js";
2
+ function parseEnvText(text) {
3
+ const map = new Map();
4
+ for (const line of (text ?? "").split("\n")) {
5
+ const m = line.match(/^([A-Z][A-Z0-9_]*)=(.*)$/);
6
+ if (m && m[2] !== "")
7
+ map.set(m[1], m[2]);
8
+ }
9
+ return map;
10
+ }
11
+ export async function collectEnv(manifest, existingEnvText, deps = {}) {
12
+ const prompt = deps.prompt ?? (async () => "");
13
+ const existing = parseEnvText(existingEnvText);
14
+ const values = new Map(existing);
15
+ // .env is user keys only. System defaults travel in the artifact's
16
+ // agent.json (render reads them; the installer passes them as process
17
+ // env for old artifacts). Legacy installs seeded system vars into .env
18
+ // — strip them, or updates would freeze them forever (HERMES_HOME even
19
+ // overrides hermes' --profile switch).
20
+ for (const legacy of ["HERMES_HOME", "PROFILE_ID", "MODEL_DEFAULT", "MODEL_PROVIDER"])
21
+ values.delete(legacy);
22
+ for (const v of manifest.env.required) {
23
+ if (values.has(v.name))
24
+ continue;
25
+ let val = process.env[v.name] || "";
26
+ if (!val && v.name === "AISA_API_KEY")
27
+ val = getApiKey() ?? "";
28
+ if (!val)
29
+ val = await prompt(`${v.name} (${v.description}): `);
30
+ if (!val)
31
+ throw new Error(`missing required ${v.name} — set it in your environment` +
32
+ (v.name === "AISA_API_KEY" ? ` or run "aisa login --key <key>"` : ""));
33
+ values.set(v.name, val);
34
+ }
35
+ const lines = [
36
+ `# ${manifest.name} — profile environment (managed by aisa agent install)`,
37
+ ];
38
+ for (const [k, v] of values)
39
+ lines.push(`${k}=${v}`);
40
+ for (const v of manifest.env.optional) {
41
+ if (values.has(v.name))
42
+ continue;
43
+ if (process.env[v.name])
44
+ lines.push(`${v.name}=${process.env[v.name]}`);
45
+ else
46
+ lines.push(`# ${v.name}= (${v.description}${v.degrade ? `; degrade: ${v.degrade}` : ""})`);
47
+ }
48
+ return lines.join("\n") + "\n";
49
+ }
@@ -0,0 +1,7 @@
1
+ import { type AgentIndex } from "@aisa-one/agent-spec";
2
+ export declare const DEFAULT_INDEX_URL = "https://raw.githubusercontent.com/AIsa-team/agent-index/main/index.json";
3
+ export type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;
4
+ export declare function indexUrl(): string;
5
+ export declare function fetchIndex(fetchImpl?: FetchLike): Promise<AgentIndex>;
6
+ export declare function requireAgent(index: AgentIndex, id: string): AgentIndex["agents"][string];
7
+ export declare function siblingAssetUrl(artifactUrl: string, filename: string): string;
@@ -0,0 +1,22 @@
1
+ import { parseIndex } from "@aisa-one/agent-spec";
2
+ export const DEFAULT_INDEX_URL = "https://raw.githubusercontent.com/AIsa-team/agent-index/main/index.json";
3
+ export function indexUrl() {
4
+ return process.env.AISA_AGENT_INDEX_URL || DEFAULT_INDEX_URL;
5
+ }
6
+ export async function fetchIndex(fetchImpl = fetch) {
7
+ const res = await fetchImpl(indexUrl());
8
+ if (!res.ok)
9
+ throw new Error(`failed to fetch agent index (${res.status}): ${indexUrl()}`);
10
+ return parseIndex(await res.text());
11
+ }
12
+ export function requireAgent(index, id) {
13
+ const entry = index.agents[id];
14
+ if (!entry) {
15
+ const available = Object.keys(index.agents).sort().join(", ") || "(none)";
16
+ throw new Error(`unknown agent "${id}"\navailable agents: ${available}`);
17
+ }
18
+ return entry;
19
+ }
20
+ export function siblingAssetUrl(artifactUrl, filename) {
21
+ return artifactUrl.replace(/[^/]+$/, filename);
22
+ }
@@ -0,0 +1,19 @@
1
+ import { type AgentManifest } from "@aisa-one/agent-spec";
2
+ import type { FetchLike } from "./index-client.js";
3
+ export interface InstalledMarker {
4
+ id: string;
5
+ version: string;
6
+ target: "hermes";
7
+ pinned: boolean;
8
+ }
9
+ export declare function hermesRoot(): string;
10
+ export declare function profileDir(id: string): string;
11
+ export declare function downloadArtifact(url: string, sha256: string, fetchImpl?: FetchLike): Promise<Buffer>;
12
+ export declare function extractBundle(buf: Buffer, destDir: string): Promise<void>;
13
+ export declare function readBundleManifest(destDir: string): Promise<AgentManifest>;
14
+ export declare function writeMarker(destDir: string, m: InstalledMarker): Promise<void>;
15
+ export declare function readMarker(dir: string): Promise<InstalledMarker | null>;
16
+ export declare function listInstalled(): Promise<{
17
+ dir: string;
18
+ marker: InstalledMarker;
19
+ }[]>;
@@ -0,0 +1,70 @@
1
+ import { mkdir, readFile, writeFile, readdir, rename, rm, mkdtemp } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import { createHash } from "node:crypto";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import * as tar from "tar";
7
+ import { parseManifest } from "@aisa-one/agent-spec";
8
+ import { stringify as yamlStringify } from "yaml";
9
+ import { expandHome } from "../utils/file.js";
10
+ const MARKER = ".agentspec.json";
11
+ export function hermesRoot() {
12
+ return expandHome(process.env.HERMES_HOME || "~/.hermes");
13
+ }
14
+ export function profileDir(id) {
15
+ return join(hermesRoot(), "profiles", id);
16
+ }
17
+ export async function downloadArtifact(url, sha256, fetchImpl = fetch) {
18
+ const res = await fetchImpl(url);
19
+ if (!res.ok)
20
+ throw new Error(`download failed (${res.status}): ${url}`);
21
+ const buf = Buffer.from(await res.arrayBuffer());
22
+ const actual = createHash("sha256").update(buf).digest("hex");
23
+ if (actual !== sha256)
24
+ throw new Error(`sha256 mismatch for ${url}\n expected ${sha256}\n actual ${actual}`);
25
+ return buf;
26
+ }
27
+ export async function extractBundle(buf, destDir) {
28
+ const tmp = await mkdtemp(join(tmpdir(), "aisa-agent-"));
29
+ const tarPath = join(tmp, "bundle.tar.gz");
30
+ await writeFile(tarPath, buf);
31
+ await mkdir(destDir, { recursive: true });
32
+ await tar.x({ file: tarPath, cwd: destDir });
33
+ // promote profile/ contents to the root (hermes profile layout)
34
+ const profileSub = join(destDir, "profile");
35
+ if (existsSync(profileSub)) {
36
+ for (const name of await readdir(profileSub)) {
37
+ await rm(join(destDir, name), { recursive: true, force: true });
38
+ await rename(join(profileSub, name), join(destDir, name));
39
+ }
40
+ await rm(profileSub, { recursive: true, force: true });
41
+ }
42
+ }
43
+ export async function readBundleManifest(destDir) {
44
+ const raw = JSON.parse(await readFile(join(destDir, "agent.json"), "utf8"));
45
+ return parseManifest(yamlStringify(raw)); // 复用 schema 校验(YAML 是 JSON 超集)
46
+ }
47
+ export async function writeMarker(destDir, m) {
48
+ await writeFile(join(destDir, MARKER), JSON.stringify(m, null, 2) + "\n");
49
+ }
50
+ export async function readMarker(dir) {
51
+ try {
52
+ return JSON.parse(await readFile(join(dir, MARKER), "utf8"));
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ }
58
+ export async function listInstalled() {
59
+ const root = join(hermesRoot(), "profiles");
60
+ if (!existsSync(root))
61
+ return [];
62
+ const out = [];
63
+ for (const name of await readdir(root)) {
64
+ const dir = join(root, name);
65
+ const marker = await readMarker(dir);
66
+ if (marker)
67
+ out.push({ dir, marker });
68
+ }
69
+ return out.sort((a, b) => a.marker.id.localeCompare(b.marker.id));
70
+ }
@@ -0,0 +1,15 @@
1
+ import type { AgentManifest } from "@aisa-one/agent-spec";
2
+ export type Exec = (cmd: string, args: string[]) => Promise<{
3
+ code: number;
4
+ stdout: string;
5
+ stderr: string;
6
+ }>;
7
+ export declare const SKIP_OPTIONAL_ENV = "AGENT_SKIP_OPTIONAL_SETUP";
8
+ export declare const SETUP_PYTHON_ENV = "AGENT_SETUP_PYTHON";
9
+ export interface SetupResult {
10
+ name: string;
11
+ status: "ok" | "skipped" | "failed";
12
+ }
13
+ /** Build the venvs declared in manifest.setup.python and point their env vars
14
+ * at the venv interpreters (written into <profileDir>/.env). */
15
+ export declare function runPythonSetup(manifest: AgentManifest, profileDir: string, exec: Exec): Promise<SetupResult[]>;
@@ -0,0 +1,51 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ export const SKIP_OPTIONAL_ENV = "AGENT_SKIP_OPTIONAL_SETUP";
5
+ // venv 基础解释器覆盖(如系统 python3 过旧时指向 python3.12)
6
+ export const SETUP_PYTHON_ENV = "AGENT_SETUP_PYTHON";
7
+ async function upsertEnvLine(dir, key, value) {
8
+ const envPath = join(dir, ".env");
9
+ const text = existsSync(envPath) ? await readFile(envPath, "utf8") : "";
10
+ const lines = text.split("\n").filter((l) => !l.startsWith(`${key}=`));
11
+ while (lines.length && lines[lines.length - 1] === "")
12
+ lines.pop();
13
+ lines.push(`${key}=${value}`);
14
+ await writeFile(envPath, lines.join("\n") + "\n");
15
+ }
16
+ /** Build the venvs declared in manifest.setup.python and point their env vars
17
+ * at the venv interpreters (written into <profileDir>/.env). */
18
+ export async function runPythonSetup(manifest, profileDir, exec) {
19
+ const skipOptional = process.env[SKIP_OPTIONAL_ENV] === "1";
20
+ const results = [];
21
+ for (const s of manifest.setup.python) {
22
+ if (s.optional && skipOptional) {
23
+ console.log(`setup ${s.name}: optional, skipped (${SKIP_OPTIONAL_ENV}=1)`);
24
+ results.push({ name: s.name, status: "skipped" });
25
+ continue;
26
+ }
27
+ const venvDir = join(profileDir, ".venvs", s.name);
28
+ const venvPython = join(venvDir, "bin", "python");
29
+ const reqPath = join(profileDir, s.requirements);
30
+ const basePython = process.env[SETUP_PYTHON_ENV] || "python3";
31
+ console.log(`setup ${s.name}: building venv (${s.requirements})…`);
32
+ const created = await exec(basePython, ["-m", "venv", venvDir]);
33
+ const installed = created.code === 0
34
+ ? await exec(venvPython, ["-m", "pip", "install", "-r", reqPath])
35
+ : created;
36
+ if (installed.code !== 0) {
37
+ const detail = installed.stderr.slice(-500);
38
+ if (!s.optional)
39
+ throw new Error(`setup ${s.name} failed (required):\n${detail}`);
40
+ console.log(`setup ${s.name}: failed, degrading (optional). ${s.description ?? ""}`);
41
+ if (detail.trim())
42
+ console.log(` reason: ${detail.trim().split("\n").slice(-3).join(" | ")}`);
43
+ results.push({ name: s.name, status: "failed" });
44
+ continue;
45
+ }
46
+ await upsertEnvLine(profileDir, s.env, venvPython);
47
+ console.log(`setup ${s.name}: ok -> ${s.env}=${venvPython}`);
48
+ results.push({ name: s.name, status: "ok" });
49
+ }
50
+ return results;
51
+ }
package/dist/api.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { type Response } from "node-fetch";
2
1
  import type { ApiResponse } from "./types.js";
3
2
  export interface RequestOptions {
4
3
  method?: "GET" | "POST" | "PUT" | "DELETE";
package/dist/api.js CHANGED
@@ -1,4 +1,3 @@
1
- import fetch from "node-fetch";
2
1
  import { CLI_BASE_URL, APIS_BASE_URL } from "./constants.js";
3
2
  import { getConfig } from "./config.js";
4
3
  function getBaseUrl() {
@@ -0,0 +1,37 @@
1
+ import { type FetchLike } from "../agent/index-client.js";
2
+ import { type Prompt } from "../agent/env-setup.js";
3
+ import type { AgentIndex } from "@aisa-one/agent-spec";
4
+ export type Exec = (cmd: string, args: string[]) => Promise<{
5
+ code: number;
6
+ stdout: string;
7
+ stderr: string;
8
+ }>;
9
+ export declare function agentListAction(_opts: Record<string, never>, deps?: {
10
+ fetchImpl?: FetchLike;
11
+ }): Promise<AgentIndex>;
12
+ export declare function agentInfoAction(id: string, deps?: {
13
+ fetchImpl?: FetchLike;
14
+ }): Promise<AgentIndex["agents"][string]>;
15
+ export declare function agentInstallAction(id: string, opts: {
16
+ version?: string;
17
+ runtime?: string;
18
+ }, deps?: {
19
+ fetchImpl?: FetchLike;
20
+ prompt?: Prompt;
21
+ exec?: Exec;
22
+ }): Promise<void>;
23
+ export declare function agentUpdateAction(id: string | undefined, _opts: Record<string, never>, deps?: {
24
+ fetchImpl?: FetchLike;
25
+ prompt?: Prompt;
26
+ exec?: Exec;
27
+ }): Promise<{
28
+ id: string;
29
+ from: string;
30
+ to: string;
31
+ status: "updated" | "up-to-date" | "pinned";
32
+ }[]>;
33
+ export declare function agentGuideAction(id: string, opts: {
34
+ md?: boolean;
35
+ }, deps?: {
36
+ fetchImpl?: FetchLike;
37
+ }): Promise<string>;
@@ -0,0 +1,129 @@
1
+ import chalk from "chalk";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+ import { existsSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { execFile } from "node:child_process";
6
+ import { table, success, hint, truncate } from "../utils/display.js";
7
+ import { fetchIndex, requireAgent, siblingAssetUrl } from "../agent/index-client.js";
8
+ import { hermesRoot, profileDir, downloadArtifact, extractBundle, readBundleManifest, writeMarker, listInstalled, } from "../agent/installer.js";
9
+ import { collectEnv } from "../agent/env-setup.js";
10
+ import { runPythonSetup } from "../agent/setup-python.js";
11
+ const realExec = (cmd, args) => new Promise((resolve) => {
12
+ execFile(cmd, args, (err, stdout, stderr) => resolve({ code: err ? 1 : 0, stdout: String(stdout), stderr: String(stderr) }));
13
+ });
14
+ export async function agentListAction(_opts, deps = {}) {
15
+ const index = await fetchIndex(deps.fetchImpl);
16
+ const rows = Object.entries(index.agents).sort(([a], [b]) => a.localeCompare(b))
17
+ .map(([id, e]) => [id, e.name, e.latest, truncate(e.description, 60)]);
18
+ console.log(table(["ID", "NAME", "LATEST", "DESCRIPTION"], rows));
19
+ hint(`install one with: aisa agent install <id>`);
20
+ return index;
21
+ }
22
+ export async function agentInfoAction(id, deps = {}) {
23
+ const index = await fetchIndex(deps.fetchImpl);
24
+ const e = requireAgent(index, id);
25
+ console.log(`${chalk.bold(e.name)} (${id})`);
26
+ console.log(` ${e.description}`);
27
+ console.log(` repo: https://github.com/${e.repo}`);
28
+ console.log(` latest: ${e.latest}`);
29
+ for (const [v, spec] of Object.entries(e.versions))
30
+ console.log(` ${v}: targets ${Object.keys(spec.targets).join(", ")}`);
31
+ hint(`install with: aisa agent install ${id}`);
32
+ return e;
33
+ }
34
+ export async function agentInstallAction(id, opts, deps = {}) {
35
+ const runtime = opts.runtime ?? "hermes";
36
+ if (runtime !== "hermes")
37
+ throw new Error(`runtime "${runtime}" not supported yet — v1 supports: hermes`);
38
+ if (!existsSync(hermesRoot()))
39
+ throw new Error(`hermes not found at ${hermesRoot()} — install it first:\n` +
40
+ ` curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash`);
41
+ const index = await fetchIndex(deps.fetchImpl);
42
+ const entry = requireAgent(index, id);
43
+ const version = opts.version ?? entry.latest;
44
+ const versionSpec = entry.versions[version];
45
+ if (!versionSpec)
46
+ throw new Error(`version ${version} not found for ${id} — available: ${Object.keys(entry.versions).join(", ")}`);
47
+ const artifact = versionSpec.targets[runtime];
48
+ if (!artifact)
49
+ throw new Error(`no ${runtime} artifact for ${id}@${version}`);
50
+ const dir = profileDir(id);
51
+ const existingEnv = existsSync(join(dir, ".env"))
52
+ ? await readFile(join(dir, ".env"), "utf8") : null;
53
+ const buf = await downloadArtifact(artifact.url, artifact.sha256, deps.fetchImpl);
54
+ await extractBundle(buf, dir);
55
+ const manifest = await readBundleManifest(dir);
56
+ const envText = await collectEnv(manifest, existingEnv, { prompt: deps.prompt });
57
+ await writeFile(join(dir, ".env"), envText);
58
+ const exec = deps.exec ?? realExec;
59
+ // system vars go to render as process env, never into .env — new render.sh
60
+ // reads them from the bundled agent.json itself, old artifacts (≤1.0.4)
61
+ // still require them in the environment
62
+ const r = await exec("env", [
63
+ `PROFILE_ID=${manifest.id}`,
64
+ `MODEL_DEFAULT=${manifest.models.default}`,
65
+ `MODEL_PROVIDER=${manifest.models.provider}`,
66
+ "bash", join(dir, "scripts", "render.sh"), dir,
67
+ ]);
68
+ if (r.code !== 0)
69
+ throw new Error(`render failed: ${r.stderr}`);
70
+ await runPythonSetup(manifest, dir, exec);
71
+ await writeMarker(dir, {
72
+ id, version, target: "hermes",
73
+ pinned: !!opts.version || manifest.update.channel === "pinned",
74
+ });
75
+ success(`installed ${entry.name} ${version} -> ${dir}`);
76
+ hint(`start it with: hermes --profile ${id}`);
77
+ }
78
+ export async function agentUpdateAction(id, _opts, deps = {}) {
79
+ const installed = await listInstalled();
80
+ const targets = id ? installed.filter((i) => i.marker.id === id) : installed;
81
+ if (id && targets.length === 0)
82
+ throw new Error(`agent "${id}" is not installed (no .agentspec.json marker found)`);
83
+ if (targets.length === 0) {
84
+ hint("no installed agents found");
85
+ return [];
86
+ }
87
+ const index = await fetchIndex(deps.fetchImpl);
88
+ const results = [];
89
+ for (const { marker } of targets) {
90
+ if (marker.pinned) {
91
+ console.log(`${marker.id}: pinned at ${marker.version} — skipping`);
92
+ results.push({ id: marker.id, from: marker.version, to: marker.version, status: "pinned" });
93
+ continue;
94
+ }
95
+ const entry = index.agents[marker.id];
96
+ if (!entry || entry.latest === marker.version) {
97
+ results.push({ id: marker.id, from: marker.version, to: marker.version, status: "up-to-date" });
98
+ continue;
99
+ }
100
+ await agentInstallAction(marker.id, {}, deps); // install 已保留 .env、更新 marker
101
+ results.push({ id: marker.id, from: marker.version, to: entry.latest, status: "updated" });
102
+ }
103
+ for (const r of results)
104
+ if (r.status === "updated")
105
+ success(`${r.id}: ${r.from} -> ${r.to}`);
106
+ return results;
107
+ }
108
+ export async function agentGuideAction(id, opts, deps = {}) {
109
+ const fetchImpl = deps.fetchImpl ?? fetch;
110
+ const index = await fetchIndex(deps.fetchImpl);
111
+ const entry = requireAgent(index, id);
112
+ const artifact = entry.versions[entry.latest]?.targets["hermes"];
113
+ if (!artifact)
114
+ throw new Error(`no hermes artifact for ${id}@${entry.latest}`);
115
+ // 优先读索引携带的资产 URL(2026-07-15 起 publish 写入);老条目回落到
116
+ // legacy 裸文件名拼接,保证旧版本 Release 仍可用
117
+ const url = opts.md
118
+ ? (artifact.installMd ?? siblingAssetUrl(artifact.url, "INSTALL.md"))
119
+ : (artifact.guidePrompt ?? siblingAssetUrl(artifact.url, "guide-prompt.txt"));
120
+ const filename = url.split("/").pop();
121
+ const res = await fetchImpl(url);
122
+ if (!res.ok)
123
+ throw new Error(`failed to fetch ${filename} (${res.status}): ${url}`);
124
+ const text = await res.text();
125
+ console.log(text);
126
+ if (!opts.md)
127
+ hint("paste the text above into your local AI agent (Claude Code, hermes, …)");
128
+ return text;
129
+ }
@@ -23,7 +23,7 @@ export async function chatAction(message, options) {
23
23
  error("Empty message");
24
24
  process.exit(1);
25
25
  }
26
- const model = options.model || getConfig("defaultModel") || "gpt-4.1";
26
+ const model = options.model || getConfig("defaultModel") || "gpt-4.1-mini";
27
27
  const stream = options.stream !== false; // default true
28
28
  const messages = [];
29
29
  if (options.system) {
@@ -31,7 +31,7 @@ export async function runAction(slug, path, options) {
31
31
  }
32
32
  // Detect method
33
33
  const method = (options.method || (body ? "POST" : "GET")).toUpperCase();
34
- const endpoint = `${slug}${path}`;
34
+ const endpoint = path.startsWith("/") ? `${slug}${path}` : `${slug}/${path}`;
35
35
  // Auto-detect domain API vs LLM API based on slug
36
36
  const domainSlugs = ["financial", "search", "youtube", "scholar", "tavily", "querit", "twitter", "services", "crypto"];
37
37
  const isDomain = domainSlugs.some((s) => slug.startsWith(s));
@@ -2,12 +2,11 @@ import ora from "ora";
2
2
  import chalk from "chalk";
3
3
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
4
4
  import { join, resolve } from "node:path";
5
- import fetch from "node-fetch";
6
5
  import { error, success, hint, truncate } from "../utils/display.js";
7
6
  import { expandHome, ensureDir, writeSkillFiles, removeDir, detectAgents } from "../utils/file.js";
8
7
  import { AGENT_DIRS } from "../constants.js";
9
8
  // --- GitHub-backed skill registry ---
10
- const SKILLS_REPO = "AIsa-team/OpenClaw-Skills";
9
+ const SKILLS_REPO = "AIsa-team/agent-skills";
11
10
  const GH_API = `https://api.github.com/repos/${SKILLS_REPO}`;
12
11
  const GH_RAW = `https://raw.githubusercontent.com/${SKILLS_REPO}/main`;
13
12
  /** Fetch the repo tree and extract top-level skill folder names. */
@@ -48,7 +47,8 @@ function parseSkillFrontmatter(slug, content) {
48
47
  if (metaMatch) {
49
48
  try {
50
49
  const meta = JSON.parse(metaMatch[1]);
51
- info.emoji = meta?.openclaw?.emoji || "";
50
+ // agent-skills repo uses metadata.aisa; legacy skills used metadata.openclaw.
51
+ info.emoji = meta?.aisa?.emoji || meta?.openclaw?.emoji || "";
52
52
  }
53
53
  catch {
54
54
  // ignore malformed metadata
@@ -80,8 +80,8 @@ const TEMPLATES = {
80
80
  default: `---
81
81
  name: my-skill
82
82
  description: "Describe what this skill does."
83
- homepage: https://openclaw.ai
84
- metadata: {"openclaw":{"emoji":"","requires":{"bins":["curl"],"env":["AISA_API_KEY"]},"primaryEnv":"AISA_API_KEY"}}
83
+ homepage: https://aisa.one
84
+ metadata: {"aisa":{"emoji":"","requires":{"bins":["curl"],"env":["AISA_API_KEY"]},"primaryEnv":"AISA_API_KEY","compatibility":["openclaw","claude-code","hermes"]}}
85
85
  ---
86
86
 
87
87
  # My Skill
@@ -103,8 +103,8 @@ aisa run <slug> <path> -q "param=value"
103
103
  llm: `---
104
104
  name: llm-assistant
105
105
  description: "Use AISA's unified LLM gateway to chat with 70+ AI models."
106
- homepage: https://openclaw.ai
107
- metadata: {"openclaw":{"emoji":"🤖","requires":{"bins":["curl"],"env":["AISA_API_KEY"]},"primaryEnv":"AISA_API_KEY"}}
106
+ homepage: https://aisa.one
107
+ metadata: {"aisa":{"emoji":"🤖","requires":{"bins":["curl"],"env":["AISA_API_KEY"]},"primaryEnv":"AISA_API_KEY","compatibility":["openclaw","claude-code","hermes"]}}
108
108
  ---
109
109
 
110
110
  # LLM Assistant Skill
@@ -120,7 +120,7 @@ export AISA_API_KEY=sk-your-key
120
120
  ## Chat Completion
121
121
 
122
122
  \`\`\`bash
123
- aisa chat "Your question here" --model gpt-4.1
123
+ aisa chat "Your question here" --model gpt-4.1-mini
124
124
  \`\`\`
125
125
 
126
126
  ## Streaming
@@ -139,8 +139,8 @@ aisa models --provider anthropic
139
139
  search: `---
140
140
  name: web-search
141
141
  description: "Search the web, YouTube, and academic papers via AISA APIs."
142
- homepage: https://openclaw.ai
143
- metadata: {"openclaw":{"emoji":"🔍","requires":{"bins":["curl"],"env":["AISA_API_KEY"]},"primaryEnv":"AISA_API_KEY"}}
142
+ homepage: https://aisa.one
143
+ metadata: {"aisa":{"emoji":"🔍","requires":{"bins":["curl"],"env":["AISA_API_KEY"]},"primaryEnv":"AISA_API_KEY","compatibility":["openclaw","claude-code","hermes"]}}
144
144
  ---
145
145
 
146
146
  # Web Search Skill
@@ -174,8 +174,8 @@ aisa scholar "transformer architecture"
174
174
  finance: `---
175
175
  name: finance-analyst
176
176
  description: "Access stock prices, earnings, SEC filings, and financial data via AISA."
177
- homepage: https://openclaw.ai
178
- metadata: {"openclaw":{"emoji":"📊","requires":{"bins":["curl"],"env":["AISA_API_KEY"]},"primaryEnv":"AISA_API_KEY"}}
177
+ homepage: https://aisa.one
178
+ metadata: {"aisa":{"emoji":"📊","requires":{"bins":["curl"],"env":["AISA_API_KEY"]},"primaryEnv":"AISA_API_KEY","compatibility":["openclaw","claude-code","hermes"]}}
179
179
  ---
180
180
 
181
181
  # Finance Analyst Skill
@@ -206,8 +206,8 @@ aisa crypto ETH --period 30d
206
206
  twitter: `---
207
207
  name: twitter-manager
208
208
  description: "Search Twitter, get user profiles and trends via AISA."
209
- homepage: https://openclaw.ai
210
- metadata: {"openclaw":{"emoji":"🐦","requires":{"bins":["curl"],"env":["AISA_API_KEY"]},"primaryEnv":"AISA_API_KEY"}}
209
+ homepage: https://aisa.one
210
+ metadata: {"aisa":{"emoji":"🐦","requires":{"bins":["curl"],"env":["AISA_API_KEY"]},"primaryEnv":"AISA_API_KEY","compatibility":["openclaw","claude-code","hermes"]}}
211
211
  ---
212
212
 
213
213
  # Twitter Manager Skill
@@ -241,8 +241,8 @@ aisa twitter trends
241
241
  video: `---
242
242
  name: video-generator
243
243
  description: "Generate videos from text prompts using AISA's video synthesis API."
244
- homepage: https://openclaw.ai
245
- metadata: {"openclaw":{"emoji":"🎬","requires":{"bins":["curl"],"env":["AISA_API_KEY"]},"primaryEnv":"AISA_API_KEY"}}
244
+ homepage: https://aisa.one
245
+ metadata: {"aisa":{"emoji":"🎬","requires":{"bins":["curl"],"env":["AISA_API_KEY"]},"primaryEnv":"AISA_API_KEY","compatibility":["openclaw","claude-code","hermes"]}}
246
246
  ---
247
247
 
248
248
  # Video Generator Skill
@@ -1,6 +1,5 @@
1
1
  import ora from "ora";
2
2
  import chalk from "chalk";
3
- import fetch from "node-fetch";
4
3
  import { requireApiKey, getConfig, setConfig } from "../config.js";
5
4
  import { apiRequest } from "../api.js";
6
5
  import { APIS_BASE_URL } from "../constants.js";
@@ -333,9 +332,10 @@ export async function twitterUploadMediaAction(filePath, options) {
333
332
  const spinner = ora("Uploading media...").start();
334
333
  let formData;
335
334
  try {
336
- const { FormData: NFFormData, fileFromSync } = await import("node-fetch");
337
- formData = new NFFormData();
338
- formData.append("file", fileFromSync(filePath));
335
+ const { readFileSync } = await import("node:fs");
336
+ const { basename } = await import("node:path");
337
+ formData = new FormData();
338
+ formData.append("file", new Blob([readFileSync(filePath)]), basename(filePath));
339
339
  }
340
340
  catch {
341
341
  spinner.fail("Failed to read file");
package/dist/config.js CHANGED
@@ -4,7 +4,7 @@ const config = new Conf({
4
4
  projectName: "aisa-cli",
5
5
  schema: {
6
6
  apiKey: { type: "string", default: "" },
7
- defaultModel: { type: "string", default: "gpt-4.1" },
7
+ defaultModel: { type: "string", default: "gpt-4.1-mini" },
8
8
  baseUrl: { type: "string", default: "https://api.aisa.one/v1" },
9
9
  outputFormat: { type: "string", default: "text" },
10
10
  twitterCookies: { type: "string", default: "" },
package/dist/index.js CHANGED
@@ -107,7 +107,7 @@ program
107
107
  program
108
108
  .command("chat [message]")
109
109
  .description("Chat with AI models via the AISA gateway")
110
- .option("--model <model>", "Model ID (default: gpt-4.1)")
110
+ .option("--model <model>", "Model ID (default: gpt-4.1-mini)")
111
111
  .option("--system <prompt>", "System prompt")
112
112
  .option("--no-stream", "Disable streaming")
113
113
  .option("--json", "Output raw JSON response")
@@ -1,4 +1,3 @@
1
- import type { Response } from "node-fetch";
2
1
  export declare function handleSSEStream(res: Response, onToken: (token: string) => void, onDone?: (usage?: {
3
2
  prompt_tokens: number;
4
3
  completion_tokens: number;
@@ -2,10 +2,11 @@ export async function handleSSEStream(res, onToken, onDone) {
2
2
  const body = res.body;
3
3
  if (!body)
4
4
  throw new Error("No response body");
5
+ const decoder = new TextDecoder();
5
6
  let buffer = "";
6
7
  let usage;
7
8
  for await (const chunk of body) {
8
- buffer += chunk.toString();
9
+ buffer += decoder.decode(chunk, { stream: true });
9
10
  const lines = buffer.split("\n");
10
11
  buffer = lines.pop() || "";
11
12
  for (const line of lines) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aisa-one/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "CLI for the AISA unified AI infrastructure platform - access 70+ LLMs, web search, finance, Twitter, and video generation APIs",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -26,7 +26,6 @@
26
26
  "chalk": "^5.3.0",
27
27
  "commander": "^12.0.0",
28
28
  "conf": "^12.0.0",
29
- "node-fetch": "^3.3.2",
30
29
  "ora": "^8.0.1"
31
30
  },
32
31
  "devDependencies": {
@@ -0,0 +1,73 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { handleSSEStream } from "../src/utils/streaming.js";
3
+
4
+ function sseResponse(chunks: string[]): Response {
5
+ const encoder = new TextEncoder();
6
+ const stream = new ReadableStream<Uint8Array>({
7
+ start(controller) {
8
+ for (const chunk of chunks) {
9
+ controller.enqueue(encoder.encode(chunk));
10
+ }
11
+ controller.close();
12
+ },
13
+ });
14
+ return new Response(stream);
15
+ }
16
+
17
+ describe("handleSSEStream", () => {
18
+ it("emits tokens from a native fetch Response body", async () => {
19
+ const res = sseResponse([
20
+ 'data: {"choices":[{"delta":{"content":"Hello"}}]}\n\n',
21
+ 'data: {"choices":[{"delta":{"content":" world"}}]}\n\n',
22
+ "data: [DONE]\n\n",
23
+ ]);
24
+
25
+ const tokens: string[] = [];
26
+ let done = false;
27
+ await handleSSEStream(
28
+ res,
29
+ (t) => tokens.push(t),
30
+ () => {
31
+ done = true;
32
+ }
33
+ );
34
+
35
+ expect(tokens.join("")).toBe("Hello world");
36
+ expect(done).toBe(true);
37
+ });
38
+
39
+ it("handles multi-byte characters split across chunks", async () => {
40
+ const encoder = new TextEncoder();
41
+ const line = encoder.encode(
42
+ 'data: {"choices":[{"delta":{"content":"你好"}}]}\n\ndata: [DONE]\n\n'
43
+ );
44
+ // split in the middle of a multi-byte character
45
+ const stream = new ReadableStream<Uint8Array>({
46
+ start(controller) {
47
+ controller.enqueue(line.slice(0, 40));
48
+ controller.enqueue(line.slice(40));
49
+ controller.close();
50
+ },
51
+ });
52
+ const res = new Response(stream);
53
+
54
+ const tokens: string[] = [];
55
+ await handleSSEStream(res, (t) => tokens.push(t));
56
+
57
+ expect(tokens.join("")).toBe("你好");
58
+ });
59
+
60
+ it("reports usage to onDone", async () => {
61
+ const res = sseResponse([
62
+ 'data: {"choices":[{"delta":{"content":"hi"}}],"usage":{"prompt_tokens":3,"completion_tokens":1}}\n\n',
63
+ "data: [DONE]\n\n",
64
+ ]);
65
+
66
+ let usage: { prompt_tokens: number; completion_tokens: number } | undefined;
67
+ await handleSSEStream(res, () => {}, (u) => {
68
+ usage = u;
69
+ });
70
+
71
+ expect(usage).toEqual({ prompt_tokens: 3, completion_tokens: 1 });
72
+ });
73
+ });