@aisa-one/cli 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +72 -0
- package/README.md +10 -9
- package/dist/api.d.ts +0 -1
- package/dist/api.js +0 -1
- package/dist/commands/account.d.ts +4 -1
- package/dist/commands/account.js +33 -5
- package/dist/commands/chat.js +1 -1
- package/dist/commands/run.js +1 -1
- package/dist/commands/skills.js +16 -16
- package/dist/commands/twitter.js +4 -4
- package/dist/config.js +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/index.js +3 -2
- package/dist/types.d.ts +8 -1
- package/dist/utils/streaming.d.ts +0 -1
- package/dist/utils/streaming.js +2 -1
- package/package.json +1 -2
- package/tests/account.test.ts +89 -0
- package/tests/streaming.test.ts +73 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
## [0.2.2] — 2026-08-03
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`aisa balance` now queries the live account wallet through the API-key
|
|
15
|
+
balance endpoint.** It reports both the account balance and the amount
|
|
16
|
+
available to the current API key, with `--json` support for automation.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **Skills registry now points at the canonical `agent-skills` repo.** The
|
|
21
|
+
`SKILLS_REPO` constant in `src/commands/skills.ts` was pinned to
|
|
22
|
+
`AIsa-team/OpenClaw-Skills`, which has been renamed to
|
|
23
|
+
`AIsa-team/agent-skills`. Every `aisa skills` subcommand kept working
|
|
24
|
+
only because GitHub serves a 301 redirect on the old slug; the constant
|
|
25
|
+
now references the canonical name so the registry no longer depends on
|
|
26
|
+
that redirect. ([#3](https://github.com/AIsa-team/cli/pull/3))
|
|
27
|
+
- **`aisa skills list` and `aisa skills show` now render emojis for live
|
|
28
|
+
skills.** `parseSkillFrontmatter` only read `metadata.openclaw.emoji`,
|
|
29
|
+
but skills in `agent-skills` publish under `metadata.aisa.emoji`. The
|
|
30
|
+
parser now reads the new key first and falls back to the legacy
|
|
31
|
+
`openclaw` key for any older skills still in the wild.
|
|
32
|
+
([#4](https://github.com/AIsa-team/cli/pull/4))
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- **`aisa skills init` templates aligned with `agent-skills` conventions.**
|
|
37
|
+
All six built-in scaffolds (`default`, `llm`, `search`, `finance`,
|
|
38
|
+
`twitter`, `video`) now emit:
|
|
39
|
+
- `homepage: https://aisa.one` (was `https://openclaw.ai`)
|
|
40
|
+
- `metadata.aisa.*` instead of `metadata.openclaw.*`
|
|
41
|
+
- `compatibility: ["openclaw", "claude-code", "hermes"]` to match the
|
|
42
|
+
shape used by `marketpulse`, `multi-source-search`,
|
|
43
|
+
`prediction-market-arbitrage`, and the rest of the live skills.
|
|
44
|
+
|
|
45
|
+
Skills scaffolded with `aisa skills init` will now render correctly in
|
|
46
|
+
the registry and pass review when contributed back upstream.
|
|
47
|
+
([#5](https://github.com/AIsa-team/cli/pull/5))
|
|
48
|
+
|
|
49
|
+
### Documentation
|
|
50
|
+
|
|
51
|
+
- README Skills section now references
|
|
52
|
+
[`AIsa-team/agent-skills`](https://github.com/AIsa-team/agent-skills)
|
|
53
|
+
directly instead of the renamed `OpenClaw-Skills`.
|
|
54
|
+
([#2](https://github.com/AIsa-team/cli/pull/2))
|
|
55
|
+
|
|
56
|
+
## [0.2.0] — Initial public release
|
|
57
|
+
|
|
58
|
+
- LLM gateway commands (`aisa chat`, `aisa models`).
|
|
59
|
+
- API discovery and execution (`aisa run`, `aisa api …` WIP).
|
|
60
|
+
- Web search (`aisa web-search`, `aisa scholar`).
|
|
61
|
+
- Finance (`aisa stock`, `aisa crypto`, `aisa screener`).
|
|
62
|
+
- Twitter/X (`aisa twitter …`, `aisa tweet`).
|
|
63
|
+
- Video generation (`aisa video create|status`).
|
|
64
|
+
- Skills registry (`aisa skills list|search|show|install|remove|init`).
|
|
65
|
+
- MCP server auto-config (`aisa mcp setup|status`).
|
|
66
|
+
- Config commands (`aisa config get|set|list|reset`) and auth
|
|
67
|
+
(`aisa login|logout|whoami`).
|
|
68
|
+
|
|
69
|
+
[Unreleased]: https://github.com/AIsa-team/cli/compare/v0.2.2...HEAD
|
|
70
|
+
[0.2.2]: https://github.com/AIsa-team/cli/compare/v0.2.1...v0.2.2
|
|
71
|
+
[0.2.1]: https://github.com/AIsa-team/cli/releases/tag/v0.2.1
|
|
72
|
+
[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
|
|
57
|
+
aisa models show gpt-4.1-mini # model details
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
### API Discovery & Execution
|
|
@@ -115,22 +115,23 @@ aisa video status <task-id> # check status
|
|
|
115
115
|
### Account
|
|
116
116
|
|
|
117
117
|
```bash
|
|
118
|
-
aisa balance #
|
|
118
|
+
aisa balance # Show wallet and API key credit balance
|
|
119
|
+
aisa balance --json # Output the balance response as JSON
|
|
119
120
|
aisa usage --limit 20 # 🚧 WIP
|
|
120
121
|
```
|
|
121
122
|
|
|
122
123
|
## Skills
|
|
123
124
|
|
|
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 [
|
|
125
|
+
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
126
|
|
|
126
127
|
### Browse & Install
|
|
127
128
|
|
|
128
129
|
```bash
|
|
129
130
|
aisa skills list # list all available skills
|
|
130
131
|
aisa skills search "financial analysis" # search by keyword
|
|
131
|
-
aisa skills show
|
|
132
|
-
aisa skills install
|
|
133
|
-
aisa skills remove
|
|
132
|
+
aisa skills show marketpulse # show skill details
|
|
133
|
+
aisa skills install marketpulse # install to agent directories
|
|
134
|
+
aisa skills remove marketpulse # uninstall
|
|
134
135
|
```
|
|
135
136
|
|
|
136
137
|
Skills install to agent directories automatically:
|
|
@@ -155,7 +156,7 @@ aisa skills init my-skill --template llm # LLM template
|
|
|
155
156
|
|
|
156
157
|
Available templates: `default`, `llm`, `search`, `finance`, `twitter`, `video`
|
|
157
158
|
|
|
158
|
-
To publish a skill, submit a pull request to [AIsa-team/
|
|
159
|
+
To publish a skill, submit a pull request to [AIsa-team/agent-skills](https://github.com/AIsa-team/agent-skills).
|
|
159
160
|
|
|
160
161
|
## MCP Server
|
|
161
162
|
|
|
@@ -178,7 +179,7 @@ aisa config reset # reset to defaults
|
|
|
178
179
|
```
|
|
179
180
|
|
|
180
181
|
Settings:
|
|
181
|
-
- `defaultModel` — default model for `aisa chat` (default: `gpt-4.1`)
|
|
182
|
+
- `defaultModel` — default model for `aisa chat` (default: `gpt-4.1-mini`)
|
|
182
183
|
- `baseUrl` — API base URL (default: `https://api.aisa.one`)
|
|
183
184
|
- `outputFormat` — output format: `text` or `json`
|
|
184
185
|
|
package/dist/api.d.ts
CHANGED
package/dist/api.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
export declare function
|
|
1
|
+
export declare function formatMicrosUSD(micros: number | string | bigint): string;
|
|
2
|
+
export declare function balanceAction(options?: {
|
|
3
|
+
json?: boolean;
|
|
4
|
+
}): Promise<void>;
|
|
2
5
|
export declare function usageAction(_options: {
|
|
3
6
|
limit?: string;
|
|
4
7
|
days?: string;
|
package/dist/commands/account.js
CHANGED
|
@@ -1,10 +1,38 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { requireApiKey } from "../config.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
import { apiRequest } from "../api.js";
|
|
4
|
+
import { formatJson, hint } from "../utils/display.js";
|
|
5
|
+
export function formatMicrosUSD(micros) {
|
|
6
|
+
const value = typeof micros === "bigint" ? micros : BigInt(micros);
|
|
7
|
+
const negative = value < 0n;
|
|
8
|
+
let absolute = negative ? -value : value;
|
|
9
|
+
let cents = (absolute % 1000000n + 5000n) / 10000n;
|
|
10
|
+
let dollars = absolute / 1000000n;
|
|
11
|
+
if (cents === 100n) {
|
|
12
|
+
dollars += 1n;
|
|
13
|
+
cents = 0n;
|
|
14
|
+
}
|
|
15
|
+
return `${negative ? "-" : ""}$${dollars.toString()}.${cents.toString().padStart(2, "0")}`;
|
|
16
|
+
}
|
|
17
|
+
export async function balanceAction(options = {}) {
|
|
18
|
+
const key = requireApiKey();
|
|
19
|
+
const res = await apiRequest(key, "credits/balance");
|
|
20
|
+
if (!res.success || !res.data) {
|
|
21
|
+
throw new Error(res.error || "Failed to fetch balance");
|
|
22
|
+
}
|
|
23
|
+
if (options.json) {
|
|
24
|
+
console.log(formatJson(res.data));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const balance = res.data;
|
|
28
|
+
console.log(` Balance: ${formatMicrosUSD(balance.account_balance_micros_usd)} ${balance.currency}`);
|
|
29
|
+
console.log(` Available with key: ${formatMicrosUSD(balance.available_balance_micros_usd)} ${balance.currency}`);
|
|
30
|
+
if (balance.api_key.unlimited) {
|
|
31
|
+
console.log(" API key limit: Unlimited");
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
console.log(` API key limit: ${formatMicrosUSD(balance.api_key.remaining_micros_usd)} remaining`);
|
|
35
|
+
}
|
|
8
36
|
}
|
|
9
37
|
export async function usageAction(_options) {
|
|
10
38
|
requireApiKey();
|
package/dist/commands/chat.js
CHANGED
|
@@ -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) {
|
package/dist/commands/run.js
CHANGED
|
@@ -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));
|
package/dist/commands/skills.js
CHANGED
|
@@ -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/
|
|
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
|
-
|
|
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://
|
|
84
|
-
metadata: {"
|
|
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://
|
|
107
|
-
metadata: {"
|
|
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://
|
|
143
|
-
metadata: {"
|
|
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://
|
|
178
|
-
metadata: {"
|
|
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://
|
|
210
|
-
metadata: {"
|
|
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://
|
|
245
|
-
metadata: {"
|
|
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
|
package/dist/commands/twitter.js
CHANGED
|
@@ -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 {
|
|
337
|
-
|
|
338
|
-
formData
|
|
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/constants.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const VERSION = "0.2.
|
|
1
|
+
export declare const VERSION = "0.2.2";
|
|
2
2
|
export declare const BASE_URL = "https://api.aisa.one";
|
|
3
3
|
export declare const CLI_BASE_URL = "https://api.aisa.one/v1";
|
|
4
4
|
export declare const APIS_BASE_URL = "https://api.aisa.one/apis/v1";
|
package/dist/constants.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -58,7 +58,8 @@ program
|
|
|
58
58
|
// ── Account ──
|
|
59
59
|
program
|
|
60
60
|
.command("balance")
|
|
61
|
-
.description("Show credit balance
|
|
61
|
+
.description("Show credit balance")
|
|
62
|
+
.option("--json", "Output raw JSON")
|
|
62
63
|
.action(wrap(balanceAction));
|
|
63
64
|
program
|
|
64
65
|
.command("usage")
|
|
@@ -107,7 +108,7 @@ program
|
|
|
107
108
|
program
|
|
108
109
|
.command("chat [message]")
|
|
109
110
|
.description("Chat with AI models via the AISA gateway")
|
|
110
|
-
.option("--model <model>", "Model ID (default: gpt-4.1)")
|
|
111
|
+
.option("--model <model>", "Model ID (default: gpt-4.1-mini)")
|
|
111
112
|
.option("--system <prompt>", "System prompt")
|
|
112
113
|
.option("--no-stream", "Disable streaming")
|
|
113
114
|
.option("--json", "Output raw JSON response")
|
package/dist/types.d.ts
CHANGED
|
@@ -26,8 +26,15 @@ export interface Model {
|
|
|
26
26
|
supported_endpoint_types?: string[] | null;
|
|
27
27
|
}
|
|
28
28
|
export interface BalanceResponse {
|
|
29
|
-
balance: number;
|
|
30
29
|
currency: string;
|
|
30
|
+
account_balance_micros_usd: number;
|
|
31
|
+
available_balance_micros_usd: number;
|
|
32
|
+
api_key: {
|
|
33
|
+
unlimited: boolean;
|
|
34
|
+
remaining_micros_usd: number;
|
|
35
|
+
used_micros_usd: number;
|
|
36
|
+
};
|
|
37
|
+
as_of: string;
|
|
31
38
|
}
|
|
32
39
|
export interface UsageRecord {
|
|
33
40
|
timestamp: string;
|
package/dist/utils/streaming.js
CHANGED
|
@@ -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 +=
|
|
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.
|
|
3
|
+
"version": "0.2.2",
|
|
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,89 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { balanceAction, formatMicrosUSD } from "../src/commands/account.js";
|
|
3
|
+
|
|
4
|
+
const balance = {
|
|
5
|
+
currency: "USD",
|
|
6
|
+
account_balance_micros_usd: 2_500_000,
|
|
7
|
+
available_balance_micros_usd: 1_000_000,
|
|
8
|
+
api_key: {
|
|
9
|
+
unlimited: false,
|
|
10
|
+
remaining_micros_usd: 1_000_000,
|
|
11
|
+
used_micros_usd: 300_000,
|
|
12
|
+
},
|
|
13
|
+
as_of: "2026-08-03T12:00:00Z",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
describe("balanceAction", () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
process.env.AISA_API_KEY = "test-balance-key";
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
delete process.env.AISA_API_KEY;
|
|
23
|
+
vi.unstubAllGlobals();
|
|
24
|
+
vi.restoreAllMocks();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("formats microdollars without floating-point drift", () => {
|
|
28
|
+
expect(formatMicrosUSD(2_500_000)).toBe("$2.50");
|
|
29
|
+
expect(formatMicrosUSD(2_505_001)).toBe("$2.51");
|
|
30
|
+
expect(formatMicrosUSD(1_999_999)).toBe("$2.00");
|
|
31
|
+
expect(formatMicrosUSD(-1_500_000)).toBe("-$1.50");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("requests the balance endpoint with the API key and renders the summary", async () => {
|
|
35
|
+
const fetchMock = vi.fn(async () => new Response(JSON.stringify(balance), { status: 200 }));
|
|
36
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
37
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
38
|
+
|
|
39
|
+
await balanceAction();
|
|
40
|
+
|
|
41
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
42
|
+
const [url, init] = fetchMock.mock.calls[0];
|
|
43
|
+
expect(url).toBe("https://api.aisa.one/v1/credits/balance");
|
|
44
|
+
expect((init?.headers as Record<string, string>).Authorization).toBe("Bearer test-balance-key");
|
|
45
|
+
expect(log.mock.calls.map(([line]) => line).join("\n")).toContain("Balance:");
|
|
46
|
+
expect(log.mock.calls.map(([line]) => line).join("\n")).toContain("$2.50 USD");
|
|
47
|
+
expect(log.mock.calls.map(([line]) => line).join("\n")).toContain("$1.00 USD");
|
|
48
|
+
expect(log.mock.calls.map(([line]) => line).join("\n")).not.toContain("test-balance-key");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("renders unlimited keys", async () => {
|
|
52
|
+
const fetchMock = vi.fn(async () => new Response(JSON.stringify({
|
|
53
|
+
...balance,
|
|
54
|
+
api_key: { ...balance.api_key, unlimited: true },
|
|
55
|
+
}), { status: 200 }));
|
|
56
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
57
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
58
|
+
|
|
59
|
+
await balanceAction();
|
|
60
|
+
|
|
61
|
+
expect(log.mock.calls.map(([line]) => line).join("\n")).toContain("API key limit: Unlimited");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("supports raw JSON output", async () => {
|
|
65
|
+
const fetchMock = vi.fn(async () => new Response(JSON.stringify(balance), { status: 200 }));
|
|
66
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
67
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
68
|
+
|
|
69
|
+
await balanceAction({ json: true });
|
|
70
|
+
|
|
71
|
+
expect(JSON.parse(log.mock.calls[0][0] as string)).toEqual(balance);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it.each([401, 403, 404, 500])("raises API errors for HTTP %s", async (status) => {
|
|
75
|
+
const fetchMock = vi.fn(async () => new Response(JSON.stringify({ error: "balance unavailable" }), { status }));
|
|
76
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
77
|
+
|
|
78
|
+
await expect(balanceAction()).rejects.toThrow(`${status}: balance unavailable`);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("raises network errors", async () => {
|
|
82
|
+
const fetchMock = vi.fn(async () => {
|
|
83
|
+
throw new Error("connection refused");
|
|
84
|
+
});
|
|
85
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
86
|
+
|
|
87
|
+
await expect(balanceAction()).rejects.toThrow("connection refused");
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -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
|
+
});
|