@talesofai/neta-skills 0.15.2 → 0.16.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 +18 -0
- package/README.md +38 -6
- package/README.zh_cn.md +38 -7
- package/bin/apis/index.js +16 -1
- package/bin/cli.js +4 -8
- package/bin/commands/adventure_campaign/create_adventure_campaign.cmd.js +1 -1
- package/bin/commands/adventure_campaign/list_my_adventure_campaigns.cmd.js +1 -1
- package/bin/commands/adventure_campaign/update_adventure_campaign.cmd.js +1 -1
- package/bin/commands/creative/upload.cmd.js +1 -1
- package/bin/commands/load.js +18 -30
- package/bin/commands/premium/create_premium_order.cmd.js +2 -1
- package/bin/commands/premium/get_current_premium_plan.cmd.js +4 -3
- package/bin/commands/premium/get_premium_order.cmd.js +2 -1
- package/bin/commands/premium/list_premium_orders.cmd.js +2 -1
- package/bin/commands/premium/list_premium_plans.cmd.js +2 -1
- package/bin/commands/premium/pay_premium_order.cmd.js +2 -1
- package/bin/commands/user/login.cmd.en_us.yml +8 -0
- package/bin/commands/user/login.cmd.js +41 -0
- package/bin/commands/user/login.cmd.zh_cn.yml +8 -0
- package/bin/commands/user/logout.cmd.en_us.yml +5 -0
- package/bin/commands/user/logout.cmd.js +19 -0
- package/bin/commands/user/logout.cmd.zh_cn.yml +5 -0
- package/bin/utils/auth.js +192 -0
- package/bin/utils/config.js +60 -0
- package/bin/utils/env.js +11 -0
- package/bin/utils/errors.js +3 -0
- package/bin/utils/lang.js +1 -1
- package/bin/utils/logger.js +10 -0
- package/bin/utils/parse_meta.js +1 -1
- package/bin/utils/telemetry.js +4 -2
- package/package.json +2 -1
- package/skills/neta/SKILL.md +19 -0
- package/skills/neta-adventure/SKILL.md +17 -0
- package/skills/neta-character/SKILL.md +17 -0
- package/skills/neta-community/SKILL.md +17 -0
- package/skills/neta-creative/SKILL.md +17 -0
- package/skills/neta-elementum/SKILL.md +17 -0
- package/skills/neta-space/SKILL.md +17 -0
- package/skills/neta-suggest/SKILL.md +17 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @neta/skills-neta
|
|
2
2
|
|
|
3
|
+
## 0.16.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- fix some env problems
|
|
8
|
+
|
|
9
|
+
## 0.16.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- support oauth2 device flow with login command
|
|
14
|
+
|
|
15
|
+
## 0.15.3
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- fix logger
|
|
20
|
+
|
|
3
21
|
## 0.15.2
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# NETA Skills
|
|
2
2
|
|
|
3
3
|
[](https://opensource.org/licenses/MIT)
|
|
4
|
-
[](https://nodejs.org/)
|
|
5
5
|
[](https://www.typescriptlang.org/)
|
|
6
6
|
|
|
7
7
|
[English](./README.md) · [简体中文](./README.zh_cn.md)
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
**NETA Skills** is a collection of powerful AI agent skills and accompanying CLI tools designed for interacting with the [Neta Art](https://www.neta.art/) API. Built for developers and AI agents, it seamlessly extends any agent's capabilities to generate multimedia, manage characters, and process audio/video workflows.
|
|
14
14
|
|
|
15
|
-
You can get your access token
|
|
15
|
+
You can get your access token (`NETA_TOKEN`) from the [Neta Open Portal](https://www.neta.art/open/). On the **global** API host, you may also sign in with the CLI `login` command (OAuth device flow) instead of pasting a token into every environment—see [Authentication](#authentication-neta_token-vs-login) below.
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
@@ -26,6 +26,7 @@ You can get your access token / NETA TOKEN from the [Neta Open Portal](https://w
|
|
|
26
26
|
- 🧭 **Smart Discovery & Suggestions:** Explore interactive feeds and get keyword, tag, and category suggestions for progressive content discovery.
|
|
27
27
|
- 📖 **Interactive Story Adventures:** Craft and play AI-driven narrative campaigns (Adventure) — the agent acts as DM and character roleplayer for immersive interactive fiction.
|
|
28
28
|
- 🤖 **Agent First:** Designed from the ground up to plug into your favorite AI agent frameworks.
|
|
29
|
+
- 🔐 **CLI sign-in (optional):** On the **global** API host (`api.talesofai.com`), you can use OAuth **device authorization** via `neta login` instead of copying `NETA_TOKEN` into every environment.
|
|
29
30
|
|
|
30
31
|
---
|
|
31
32
|
|
|
@@ -68,10 +69,12 @@ npx skills add talesofai/neta-skills/skills/neta-adventure
|
|
|
68
69
|
|
|
69
70
|
### Available Commands
|
|
70
71
|
|
|
71
|
-
The skill includes **
|
|
72
|
+
The skill includes **45 commands** for various tasks:
|
|
72
73
|
|
|
73
74
|
| Category | Command | Description |
|
|
74
75
|
|----------|---------|-------------|
|
|
76
|
+
| **User** | `login` | Start OAuth device login (`request-code`) or complete it (`verify-code`); stores tokens locally on success |
|
|
77
|
+
| | `logout` | Clear stored CLI session (access token and any in-progress device flow) |
|
|
75
78
|
| **Adventure Campaigns** | `create_adventure_campaign` | Create an AI-driven interactive story adventure |
|
|
76
79
|
| | `update_adventure_campaign` | Update an existing adventure campaign |
|
|
77
80
|
| | `list_my_adventure_campaigns` | List your created adventure campaigns |
|
|
@@ -139,6 +142,30 @@ Configure your environment:
|
|
|
139
142
|
export NETA_TOKEN=your_token_here
|
|
140
143
|
```
|
|
141
144
|
|
|
145
|
+
### Authentication (`NETA_TOKEN` vs `login`)
|
|
146
|
+
|
|
147
|
+
You can authenticate in either of these ways:
|
|
148
|
+
|
|
149
|
+
1. **Environment token (works everywhere)**
|
|
150
|
+
Set `NETA_TOKEN` from the [Neta Open Portal](https://www.neta.art/open/). The CLI and API client send it as the `x-token` header when no stored session exists.
|
|
151
|
+
|
|
152
|
+
2. **CLI device login (global API only)**
|
|
153
|
+
When `NETA_API_BASE_URL` points at the **global** host (`…talesofai.com`), you can run:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
npx -y @talesofai/neta-skills@latest login
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The command returns OAuth device fields. Open **`verification_uri_complete`** in a browser, finish sign-in, then run:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
npx -y @talesofai/neta-skills@latest login --action verify-code
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
On success, tokens are stored under the app config directory (see `NETA_CONFIG_DIR` below). Subsequent API calls use `Authorization: Bearer …` until the session expires or you run `logout`.
|
|
166
|
+
|
|
167
|
+
If the CLI reports that login is not supported for your **region** (non-global API), use `NETA_TOKEN` instead.
|
|
168
|
+
|
|
142
169
|
### Running Commands
|
|
143
170
|
|
|
144
171
|
```bash
|
|
@@ -180,7 +207,7 @@ neta-skills/
|
|
|
180
207
|
│ └── neta-adventure/
|
|
181
208
|
├── src/ # TypeScript source for the CLI
|
|
182
209
|
│ ├── apis/ # Typed Neta API client helpers (incl. commerce)
|
|
183
|
-
│ ├── commands/ # CLI command groups: creative, community, adventure, VToken, premium, …
|
|
210
|
+
│ ├── commands/ # CLI command groups: user, creative, community, adventure, VToken, premium, …
|
|
184
211
|
│ ├── utils/ # Shared utilities
|
|
185
212
|
│ └── cli.ts # CLI entrypoint (TypeScript)
|
|
186
213
|
├── bin/ # Built JavaScript output for the CLI
|
|
@@ -221,10 +248,15 @@ Both the AI agent skills and the CLI require the following environment configura
|
|
|
221
248
|
|
|
222
249
|
| Variable | Required | Default | Description |
|
|
223
250
|
|----------|----------|---------|-------------|
|
|
224
|
-
| `NETA_TOKEN` |
|
|
225
|
-
| `NETA_API_BASE_URL` | ❌ |
|
|
251
|
+
| `NETA_TOKEN` | Conditional* | - | API access token from the [Neta Open Portal](https://www.neta.art/open/). Required when not using a CLI session from `login`. |
|
|
252
|
+
| `NETA_API_BASE_URL` | ❌ | `https://api.talesofai.com` | Base URL for the Neta API. Device login is only available when this resolves to the **global** host (`…talesofai.com`). |
|
|
253
|
+
| `NETA_AUTH_API_BASE_URL` | ❌ | derived from `NETA_API_BASE_URL` | OIDC / token endpoints for `login` and refresh (override for custom stacks). |
|
|
254
|
+
| `NETA_CLIENT_ID` | ❌ | built-in public client id | OAuth client id for device login and refresh. |
|
|
255
|
+
| `NETA_CONFIG_DIR` | ❌ | OS-specific config path | Directory where the CLI stores OAuth tokens and device-flow state (see `env-paths` + `neta-cli`). |
|
|
226
256
|
| `DISABLE_TELEMETRY` | ❌ | unset | Set to `1` to disable CLI usage analytics (see below). |
|
|
227
257
|
|
|
258
|
+
\* If you have completed `login` / `verify-code` successfully on the global API, the CLI can run without `NETA_TOKEN` until you `logout` or the refresh session is cleared.
|
|
259
|
+
|
|
228
260
|
### CLI usage analytics (telemetry)
|
|
229
261
|
|
|
230
262
|
The `@talesofai/neta-skills` CLI sends lightweight usage data—such as which command ran, the options you passed, CLI version and locale, a coarse API-region hint, outcomes and timing, and your user UUID when signed in (not your API token)—so we can measure reliability and improve the experience.
|
package/README.zh_cn.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# NETA Skills
|
|
2
2
|
|
|
3
3
|
[](https://opensource.org/licenses/MIT)
|
|
4
|
-
[](https://nodejs.org/)
|
|
5
5
|
[](https://www.typescriptlang.org/)
|
|
6
6
|
|
|
7
7
|
[简体中文](./README.md) · [English](./README.en.md)
|
|
@@ -18,8 +18,7 @@
|
|
|
18
18
|
- 通过推荐引擎和互动 Feed 进行玩法内容发现
|
|
19
19
|
- 创作与游玩 AI 驱动的交互式故事冒险(奇遇剧本),Agent 担任 DM 与角色扮演者
|
|
20
20
|
|
|
21
|
-
你可以在 [Neta 开放平台](https://www.neta.art/open/) 获取访问令牌 `NETA_TOKEN
|
|
22
|
-
也可以在[国内登陆账号后台](https://app.nieta.art/security) 获取访问令牌 `NETA_TOKEN`。
|
|
21
|
+
你可以在 [Neta 开放平台](https://www.neta.art/open/) 或[国内账号后台](https://app.nieta.art/security) 获取访问令牌 `NETA_TOKEN`。在**全球** API 环境下,也可使用 CLI 的 `login`(OAuth 设备流)登录,详见下文「身份验证」小节。
|
|
23
22
|
|
|
24
23
|
---
|
|
25
24
|
|
|
@@ -31,6 +30,7 @@
|
|
|
31
30
|
- 🏷️ **社区与标签集成**:浏览热门标签、空间、玩法合集与角色。
|
|
32
31
|
- 🧭 **智能内容探索**:通过关键词建议、标签推荐、分类导航与智能内容流,渐进式发现玩法与内容。
|
|
33
32
|
- 🤖 **Agent 优先设计**:面向 AI Agent 场景设计,易于在各类 Agent 框架中集成调用。
|
|
33
|
+
- 🔐 **CLI 登录(可选)**:在**全球** API 环境(`api.talesofai.com`)下,可通过 `login` 使用 OAuth **设备授权**登录,无需在每台环境手动配置 `NETA_TOKEN`。
|
|
34
34
|
|
|
35
35
|
---
|
|
36
36
|
|
|
@@ -69,10 +69,12 @@ npx skills add talesofai/neta-skills/skills/zh_cn/neta-adventure
|
|
|
69
69
|
|
|
70
70
|
### 可用指令总览
|
|
71
71
|
|
|
72
|
-
当前技能共包含 **
|
|
72
|
+
当前技能共包含 **36 个命令**,覆盖创作、奇遇、角色与社区探索等场景:
|
|
73
73
|
|
|
74
74
|
| 分类 | 命令 | 说明 |
|
|
75
75
|
|------|------|------|
|
|
76
|
+
| **用户 User** | `login` | OAuth 设备流:默认 `request-code` 发起登录;浏览器完成后用 `verify-code` 换票并写入本地会话 |
|
|
77
|
+
| | `logout` | 清除本地保存的访问令牌及未完成的设备授权状态 |
|
|
76
78
|
| **奇遇剧本 Adventure** | `create_adventure_campaign` | 创建 AI 驱动的交互式故事冒险剧本 |
|
|
77
79
|
| | `update_adventure_campaign` | 更新已有奇遇剧本 |
|
|
78
80
|
| | `list_my_adventure_campaigns` | 列出你创建的奇遇剧本 |
|
|
@@ -136,6 +138,30 @@ pnpm dlx @talesofai/neta-skills --help
|
|
|
136
138
|
export NETA_TOKEN=your_token_here
|
|
137
139
|
```
|
|
138
140
|
|
|
141
|
+
### 身份验证(`NETA_TOKEN` 与 `login`)
|
|
142
|
+
|
|
143
|
+
任选其一即可:
|
|
144
|
+
|
|
145
|
+
1. **环境变量令牌(通用)**
|
|
146
|
+
在 [Neta 开放平台](https://www.neta.art/open/) 或国内后台获取 `NETA_TOKEN`。未建立 CLI 会话时,客户端会将其作为 `x-token` 发送。
|
|
147
|
+
|
|
148
|
+
2. **CLI 设备授权登录(仅全球 API)**
|
|
149
|
+
当 `NETA_API_BASE_URL` 为全球域名(`…talesofai.com`)时,可执行:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
npx -y @talesofai/neta-skills@latest login
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
命令会返回 OAuth 设备授权字段。请让用户在浏览器中打开 **`verification_uri_complete`** 完成登录与授权,完成后执行:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
npx -y @talesofai/neta-skills@latest login --action verify-code
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
成功后令牌保存在本机配置目录(见下文 `NETA_CONFIG_DIR`)。之后在会话有效期内,请求会携带 `Authorization: Bearer …`,直至执行 `logout` 或刷新失效。
|
|
162
|
+
|
|
163
|
+
若 CLI 提示**当前区域不支持**设备登录(非全球 API),请改用 `NETA_TOKEN`。
|
|
164
|
+
|
|
139
165
|
### 运行示例
|
|
140
166
|
|
|
141
167
|
```bash
|
|
@@ -181,7 +207,7 @@ neta-skills/
|
|
|
181
207
|
│ └── neta-adventure/
|
|
182
208
|
├── src/ # CLI 对应的 TypeScript 源码
|
|
183
209
|
│ ├── apis/ # 封装后的 Neta API 调用
|
|
184
|
-
│ ├── commands/ # CLI
|
|
210
|
+
│ ├── commands/ # CLI 命令定义(含 user / creative / community 等)
|
|
185
211
|
│ ├── utils/ # 通用工具方法
|
|
186
212
|
│ └── cli.ts # CLI 入口(TypeScript)
|
|
187
213
|
├── bin/ # 构建后的 JavaScript 产物
|
|
@@ -220,10 +246,15 @@ neta-skills/
|
|
|
220
246
|
|
|
221
247
|
| 变量名 | 必需 | 默认值 | 说明 |
|
|
222
248
|
|--------|------|--------|------|
|
|
223
|
-
| `NETA_TOKEN` |
|
|
224
|
-
| `NETA_API_BASE_URL` | ❌ |
|
|
249
|
+
| `NETA_TOKEN` | 视情况* | - | 从开放平台或国内后台获取的 API 令牌;未使用 `login` 会话时必填 |
|
|
250
|
+
| `NETA_API_BASE_URL` | ❌ | `https://api.talesofai.com` | Neta API 网关;设备登录仅在指向**全球**域名(`…talesofai.com`)时可用 |
|
|
251
|
+
| `NETA_AUTH_API_BASE_URL` | ❌ | 由 `NETA_API_BASE_URL` 推导 | OIDC / 换票端点;自建环境可显式覆盖 |
|
|
252
|
+
| `NETA_CLIENT_ID` | ❌ | 内置公共客户端 ID | 设备流与刷新令牌使用的 OAuth `client_id` |
|
|
253
|
+
| `NETA_CONFIG_DIR` | ❌ | 系统配置目录 | CLI 存放 OAuth 令牌与设备流状态的目录(`env-paths` + `neta-cli`) |
|
|
225
254
|
| `DISABLE_TELEMETRY` | ❌ | 未设置 | 设为 `1` 可关闭 CLI 使用数据统计(见下文) |
|
|
226
255
|
|
|
256
|
+
\* 若你已在**全球** API 上成功执行 `login` / `verify-code`,在会话有效期间可不设 `NETA_TOKEN`,直至 `logout` 或刷新会话失效。
|
|
257
|
+
|
|
227
258
|
### CLI 使用数据(埋点 / 遥测)
|
|
228
259
|
|
|
229
260
|
`@talesofai/neta-skills` CLI 会上报轻量使用数据(例如执行的命令、命令行参数、CLI 版本与语言、大致 API 区域、执行结果与耗时、登录时的用户 UUID 等;**不包含** API Token),用于衡量稳定性并改进产品体验。
|
package/bin/apis/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import axios, { AxiosError } from "axios";
|
|
2
|
+
import { isTokenExpired, readToken, refreshToken } from "../utils/auth.js";
|
|
3
|
+
import { NETA_TOKEN } from "../utils/env.js";
|
|
2
4
|
import { catchErrorResponse, handleAxiosError } from "../utils/errors.js";
|
|
3
5
|
import { createActivityApis } from "./activity.js";
|
|
4
6
|
import { createArtifactApis } from "./artifact.js";
|
|
@@ -29,10 +31,23 @@ export const createApis = (option) => {
|
|
|
29
31
|
},
|
|
30
32
|
timeout: 10 * 1000,
|
|
31
33
|
});
|
|
32
|
-
client.interceptors.request.use((config) => {
|
|
34
|
+
client.interceptors.request.use(async (config) => {
|
|
33
35
|
const now = Date.now();
|
|
34
36
|
config.start_time = now;
|
|
35
37
|
logger.debug("[api] request: %s %s", config.method, config.url);
|
|
38
|
+
let token = await readToken();
|
|
39
|
+
if (token && isTokenExpired(token)) {
|
|
40
|
+
token = await refreshToken().catch((e) => {
|
|
41
|
+
logger.warn("[api] refresh token error: %o", e);
|
|
42
|
+
return null;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (!token) {
|
|
46
|
+
config.headers.set("x-token", NETA_TOKEN);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
config.headers.set("Authorization", `Bearer ${token.access_token}`);
|
|
50
|
+
}
|
|
36
51
|
return config;
|
|
37
52
|
});
|
|
38
53
|
client.interceptors.response.use((response) => {
|
package/bin/cli.js
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { program } from "@commander-js/extra-typings";
|
|
3
|
-
import dotenv from "dotenv-flow";
|
|
4
3
|
import pkg from "../package.json" with { type: "json" };
|
|
5
4
|
import { buildCommands } from "./commands/load.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
silent: true,
|
|
9
|
-
});
|
|
5
|
+
import { setupEnvPaths } from "./utils/config.js";
|
|
6
|
+
setupEnvPaths();
|
|
10
7
|
program
|
|
11
8
|
.name("neta")
|
|
12
9
|
.description("NETA CLI - Neta API Client")
|
|
13
10
|
.version(pkg.version);
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
cli.parse(process.argv);
|
|
11
|
+
await buildCommands(program);
|
|
12
|
+
program.parse(process.argv);
|
|
@@ -41,7 +41,7 @@ export const createAdventureCampaign = createCommand({
|
|
|
41
41
|
inputSchema: createAdventureCampaignParameters,
|
|
42
42
|
}, async ({ name, mission_plot, subtitle, status, header_img, background_img, mission_task, mission_plot_attention, }, { user, apis }) => {
|
|
43
43
|
if (!user) {
|
|
44
|
-
throw new Error("Not authenticated. Please check your NETA_TOKEN.");
|
|
44
|
+
throw new Error("Not authenticated. Please check your NETA_TOKEN or login.");
|
|
45
45
|
}
|
|
46
46
|
const result = await apis.travelCampaign.createCampaign({
|
|
47
47
|
name,
|
|
@@ -30,7 +30,7 @@ export const listMyAdventureCampaigns = createCommand({
|
|
|
30
30
|
inputSchema: listMyAdventureCampaignsParameters,
|
|
31
31
|
}, async ({ page_index, page_size }, { user, apis }) => {
|
|
32
32
|
if (!user) {
|
|
33
|
-
throw new Error("Not authenticated. Please check your NETA_TOKEN.");
|
|
33
|
+
throw new Error("Not authenticated. Please check your NETA_TOKEN or login.");
|
|
34
34
|
}
|
|
35
35
|
const result = await apis.travelCampaign.listMyCampaigns({
|
|
36
36
|
user_uuid: user.uuid,
|
|
@@ -42,7 +42,7 @@ export const updateAdventureCampaign = createCommand({
|
|
|
42
42
|
inputSchema: updateAdventureCampaignParameters,
|
|
43
43
|
}, async ({ campaign_uuid, name, mission_plot, subtitle, status, header_img, background_img, mission_task, mission_plot_attention, }, { user, apis }) => {
|
|
44
44
|
if (!user) {
|
|
45
|
-
throw new Error("Not authenticated. Please check your NETA_TOKEN.");
|
|
45
|
+
throw new Error("Not authenticated. Please check your NETA_TOKEN or login.");
|
|
46
46
|
}
|
|
47
47
|
const patch = Object.fromEntries(Object.entries({
|
|
48
48
|
name,
|
|
@@ -146,7 +146,7 @@ export const upload = createCommand({
|
|
|
146
146
|
}),
|
|
147
147
|
}, async ({ file_path }, { apis, user, log }) => {
|
|
148
148
|
if (!user) {
|
|
149
|
-
throw new Error("Not authenticated. Please check your NETA_TOKEN.");
|
|
149
|
+
throw new Error("Not authenticated. Please check your NETA_TOKEN or login.");
|
|
150
150
|
}
|
|
151
151
|
const regionOptions = apis.baseUrl.endsWith(".cn")
|
|
152
152
|
? OSS_STS_OPTIONS_CN
|
package/bin/commands/load.js
CHANGED
|
@@ -13,8 +13,10 @@ import { Option, } from "@commander-js/extra-typings";
|
|
|
13
13
|
import { Type } from "@sinclair/typebox";
|
|
14
14
|
import { AssertError, Value } from "@sinclair/typebox/value";
|
|
15
15
|
import { createApis } from "../apis/index.js";
|
|
16
|
+
import { API_BASE_URL, IS_DEV } from "../utils/env.js";
|
|
16
17
|
import { ApiResponseError } from "../utils/errors.js";
|
|
17
|
-
import {
|
|
18
|
+
import { getSysLocale } from "../utils/lang.js";
|
|
19
|
+
import { logger } from "../utils/logger.js";
|
|
18
20
|
import { setLocale } from "../utils/parse_meta.js";
|
|
19
21
|
import { formatCommandParams, track, trackConfig, trackConfigUser, } from "../utils/telemetry.js";
|
|
20
22
|
import { isCommand } from "./factory.js";
|
|
@@ -38,15 +40,6 @@ export const loadCommands = async (domains) => {
|
|
|
38
40
|
.map((name) => module[name]);
|
|
39
41
|
})).then((commands) => commands.flat());
|
|
40
42
|
};
|
|
41
|
-
const IS_DEV = process.env["NODE_ENV"] === "development";
|
|
42
|
-
const logger = IS_DEV
|
|
43
|
-
? console
|
|
44
|
-
: {
|
|
45
|
-
error: () => { },
|
|
46
|
-
warn: () => { },
|
|
47
|
-
info: () => { },
|
|
48
|
-
debug: () => { },
|
|
49
|
-
};
|
|
50
43
|
export const buildCommands = async (cli) => {
|
|
51
44
|
setLocale();
|
|
52
45
|
const commands = await loadCommands([
|
|
@@ -55,15 +48,16 @@ export const buildCommands = async (cli) => {
|
|
|
55
48
|
"character_elementum",
|
|
56
49
|
"adventure_campaign",
|
|
57
50
|
"premium",
|
|
51
|
+
"user",
|
|
58
52
|
]);
|
|
59
|
-
|
|
53
|
+
commands.forEach((cmd) => {
|
|
60
54
|
const command = cli.command(cmd.name);
|
|
61
55
|
command.description(cmd.title || cmd.description || "");
|
|
62
56
|
const inputSchema = cmd.inputSchema;
|
|
63
57
|
if (inputSchema && "properties" in inputSchema) {
|
|
64
58
|
const properties = inputSchema.properties;
|
|
65
59
|
if (!properties)
|
|
66
|
-
return
|
|
60
|
+
return;
|
|
67
61
|
Object.entries(properties).forEach(([key, property]) => {
|
|
68
62
|
if (typeof property !== "object")
|
|
69
63
|
return;
|
|
@@ -94,28 +88,25 @@ export const buildCommands = async (cli) => {
|
|
|
94
88
|
});
|
|
95
89
|
}
|
|
96
90
|
command.action(async (args) => {
|
|
97
|
-
const { api_base_url } = cli.opts();
|
|
98
|
-
const baseUrl = typeof api_base_url === "string"
|
|
99
|
-
? api_base_url
|
|
100
|
-
: (process.env["NETA_API_BASE_URL"] ?? "https://api.talesofai.com");
|
|
101
91
|
trackConfig({
|
|
102
|
-
app_region:
|
|
103
|
-
app_language:
|
|
92
|
+
app_region: API_BASE_URL.endsWith("cn") ? "cn" : "global",
|
|
93
|
+
app_language: getSysLocale(),
|
|
104
94
|
});
|
|
105
95
|
const apis = createApis({
|
|
106
96
|
logger,
|
|
107
|
-
baseUrl,
|
|
97
|
+
baseUrl: API_BASE_URL,
|
|
108
98
|
headers: {
|
|
109
|
-
"x-token": process.env["NETA_TOKEN"] ?? "",
|
|
110
99
|
"x-platform": "nieta-app/web",
|
|
111
100
|
},
|
|
112
101
|
});
|
|
113
|
-
const user =
|
|
114
|
-
|
|
102
|
+
const user = cmd.name === "login" || cmd.name === "logout"
|
|
103
|
+
? null
|
|
104
|
+
: await apis.user.me().catch((e) => {
|
|
105
|
+
if (e instanceof ApiResponseError) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
115
108
|
return null;
|
|
116
|
-
}
|
|
117
|
-
return null;
|
|
118
|
-
});
|
|
109
|
+
});
|
|
119
110
|
const startTime = Date.now();
|
|
120
111
|
logger.debug("[telemetry] user: %s", user?.uuid);
|
|
121
112
|
trackConfigUser(user ? { user_unique_id: user.uuid } : null);
|
|
@@ -125,9 +116,7 @@ export const buildCommands = async (cli) => {
|
|
|
125
116
|
});
|
|
126
117
|
const type = cmd.inputSchema ?? Type.Object({});
|
|
127
118
|
const input = Value.Parse(type, args);
|
|
128
|
-
|
|
129
|
-
logger.debug("[command] %s, params: %o", cmd.name, input);
|
|
130
|
-
}
|
|
119
|
+
logger.debug("[command] %s, params: %o", cmd.name, input);
|
|
131
120
|
await cmd
|
|
132
121
|
.execute(input, {
|
|
133
122
|
apis,
|
|
@@ -163,7 +152,7 @@ export const buildCommands = async (cli) => {
|
|
|
163
152
|
type: e.name,
|
|
164
153
|
message: e.message,
|
|
165
154
|
path: e.error?.path,
|
|
166
|
-
schema: e.error?.schema,
|
|
155
|
+
schema: JSON.stringify(e.error?.schema),
|
|
167
156
|
},
|
|
168
157
|
});
|
|
169
158
|
return null;
|
|
@@ -210,6 +199,5 @@ export const buildCommands = async (cli) => {
|
|
|
210
199
|
return null;
|
|
211
200
|
});
|
|
212
201
|
});
|
|
213
|
-
return command;
|
|
214
202
|
});
|
|
215
203
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import { parseDate } from "../../utils/date.js";
|
|
3
|
+
import { IS_GLOBAL } from "../../utils/env.js";
|
|
3
4
|
import { parseMeta } from "../../utils/parse_meta.js";
|
|
4
5
|
import { createCommand } from "../factory.js";
|
|
5
6
|
const meta = parseMeta(Type.Object({
|
|
@@ -18,7 +19,7 @@ export const createPremiumOrder = createCommand({
|
|
|
18
19
|
spu_uuid: Type.String({ description: meta.parameters.spu_uuid }),
|
|
19
20
|
}),
|
|
20
21
|
}, async ({ spu_uuid }, { apis }) => {
|
|
21
|
-
if (!
|
|
22
|
+
if (!IS_GLOBAL) {
|
|
22
23
|
throw new Error("This command is not supported in the current region");
|
|
23
24
|
}
|
|
24
25
|
const order = await apis.commerce.createOrder({
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import { parseDate } from "../../utils/date.js";
|
|
3
|
+
import { IS_GLOBAL } from "../../utils/env.js";
|
|
3
4
|
import { parseMeta } from "../../utils/parse_meta.js";
|
|
4
5
|
import { createCommand } from "../factory.js";
|
|
5
6
|
const PlanningMap = {
|
|
@@ -17,12 +18,12 @@ export const getCurrentPremiumPlan = createCommand({
|
|
|
17
18
|
name: meta.name,
|
|
18
19
|
title: meta.title,
|
|
19
20
|
description: meta.description,
|
|
20
|
-
}, async (_, {
|
|
21
|
-
if (!
|
|
21
|
+
}, async (_, { user }) => {
|
|
22
|
+
if (!IS_GLOBAL) {
|
|
22
23
|
throw new Error("This command is not supported in the current region");
|
|
23
24
|
}
|
|
24
25
|
if (!user) {
|
|
25
|
-
throw new Error("Not authenticated. Please check your NETA_TOKEN.");
|
|
26
|
+
throw new Error("Not authenticated. Please check your NETA_TOKEN or login.");
|
|
26
27
|
}
|
|
27
28
|
const level = user.properties?.vip_level ?? 0;
|
|
28
29
|
return {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import { parseDate } from "../../utils/date.js";
|
|
3
|
+
import { IS_GLOBAL } from "../../utils/env.js";
|
|
3
4
|
import { parseMeta } from "../../utils/parse_meta.js";
|
|
4
5
|
import { createCommand } from "../factory.js";
|
|
5
6
|
const meta = parseMeta(Type.Object({
|
|
@@ -18,7 +19,7 @@ export const getPremiumOrder = createCommand({
|
|
|
18
19
|
order_uuid: Type.String({ description: meta.parameters.order_uuid }),
|
|
19
20
|
}),
|
|
20
21
|
}, async ({ order_uuid }, { apis }) => {
|
|
21
|
-
if (!
|
|
22
|
+
if (!IS_GLOBAL) {
|
|
22
23
|
throw new Error("This command is not supported in the current region");
|
|
23
24
|
}
|
|
24
25
|
const order = await apis.commerce.order({ order_uuid });
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import { parseDate } from "../../utils/date.js";
|
|
3
|
+
import { IS_GLOBAL } from "../../utils/env.js";
|
|
3
4
|
import { parseMeta } from "../../utils/parse_meta.js";
|
|
4
5
|
import { createCommand } from "../factory.js";
|
|
5
6
|
const meta = parseMeta(Type.Object({
|
|
@@ -29,7 +30,7 @@ export const listPremiumOrders = createCommand({
|
|
|
29
30
|
}),
|
|
30
31
|
}),
|
|
31
32
|
}, async ({ page_index, page_size }, { apis }) => {
|
|
32
|
-
if (!
|
|
33
|
+
if (!IS_GLOBAL) {
|
|
33
34
|
throw new Error("This command is not supported in the current region");
|
|
34
35
|
}
|
|
35
36
|
const { list, total } = await apis.commerce.orders({
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import { Value } from "@sinclair/typebox/value";
|
|
3
|
+
import { IS_GLOBAL } from "../../utils/env.js";
|
|
3
4
|
import { safeParseJson } from "../../utils/json.js";
|
|
4
5
|
import { parseMeta } from "../../utils/parse_meta.js";
|
|
5
6
|
import { createCommand } from "../factory.js";
|
|
@@ -44,7 +45,7 @@ export const listPremiumPlans = createCommand({
|
|
|
44
45
|
title: meta.title,
|
|
45
46
|
description: meta.description,
|
|
46
47
|
}, async (_, { apis }) => {
|
|
47
|
-
if (!
|
|
48
|
+
if (!IS_GLOBAL) {
|
|
48
49
|
throw new Error("This command is not supported in the current region");
|
|
49
50
|
}
|
|
50
51
|
const plansConfigValue = await apis.commerce.listPlansConfig();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import { parseDate } from "../../utils/date.js";
|
|
3
|
+
import { IS_GLOBAL } from "../../utils/env.js";
|
|
3
4
|
import { parseMeta } from "../../utils/parse_meta.js";
|
|
4
5
|
import { createCommand } from "../factory.js";
|
|
5
6
|
const meta = parseMeta(Type.Object({
|
|
@@ -22,7 +23,7 @@ export const payPremiumOrder = createCommand({
|
|
|
22
23
|
}),
|
|
23
24
|
}),
|
|
24
25
|
}, async ({ order_uuid, channel }, { apis }) => {
|
|
25
|
-
if (!
|
|
26
|
+
if (!IS_GLOBAL) {
|
|
26
27
|
throw new Error("This command is not supported in the current region");
|
|
27
28
|
}
|
|
28
29
|
const order = await apis.commerce.order({ order_uuid });
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
name: login
|
|
2
|
+
title: Login
|
|
3
|
+
description: Login to your Neta account
|
|
4
|
+
parameters:
|
|
5
|
+
action: Action to perform (request-code or verify-code)
|
|
6
|
+
|
|
7
|
+
errors:
|
|
8
|
+
not_supported_in_current_region: This command is not supported in the current region, please set NETA_TOKEN environment variable for authentication.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { requestDeviceCode, verifyDeviceCode } from "../../utils/auth.js";
|
|
3
|
+
import { IS_GLOBAL } from "../../utils/env.js";
|
|
4
|
+
import { parseMeta } from "../../utils/parse_meta.js";
|
|
5
|
+
import { createCommand } from "../factory.js";
|
|
6
|
+
export const meta = parseMeta(Type.Object({
|
|
7
|
+
name: Type.String(),
|
|
8
|
+
title: Type.String(),
|
|
9
|
+
description: Type.String(),
|
|
10
|
+
parameters: Type.Object({
|
|
11
|
+
action: Type.String(),
|
|
12
|
+
}),
|
|
13
|
+
errors: Type.Object({
|
|
14
|
+
not_supported_in_current_region: Type.String(),
|
|
15
|
+
}),
|
|
16
|
+
}), import.meta);
|
|
17
|
+
export const login = createCommand({
|
|
18
|
+
name: meta.name,
|
|
19
|
+
title: meta.title,
|
|
20
|
+
description: meta.description,
|
|
21
|
+
inputSchema: Type.Object({
|
|
22
|
+
action: Type.Union([Type.Literal("request-code"), Type.Literal("verify-code")], {
|
|
23
|
+
default: "request-code",
|
|
24
|
+
description: meta.parameters.action,
|
|
25
|
+
}),
|
|
26
|
+
}),
|
|
27
|
+
}, async ({ action }, { apis }) => {
|
|
28
|
+
if (!IS_GLOBAL) {
|
|
29
|
+
throw new Error(meta.errors.not_supported_in_current_region);
|
|
30
|
+
}
|
|
31
|
+
if (action === "verify-code") {
|
|
32
|
+
await verifyDeviceCode();
|
|
33
|
+
const user = await apis.user.me();
|
|
34
|
+
return {
|
|
35
|
+
uuid: user.uuid,
|
|
36
|
+
nick_name: user.nick_name,
|
|
37
|
+
avatar_url: user.avatar_url,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return requestDeviceCode();
|
|
41
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { deleteDeviceCode, deleteToken } from "../../utils/auth.js";
|
|
3
|
+
import { parseMeta } from "../../utils/parse_meta.js";
|
|
4
|
+
import { createCommand } from "../factory.js";
|
|
5
|
+
const meta = parseMeta(Type.Object({
|
|
6
|
+
name: Type.String(),
|
|
7
|
+
title: Type.String(),
|
|
8
|
+
description: Type.String(),
|
|
9
|
+
success: Type.String(),
|
|
10
|
+
}), import.meta);
|
|
11
|
+
export const logout = createCommand({
|
|
12
|
+
name: meta.name,
|
|
13
|
+
title: meta.title,
|
|
14
|
+
description: meta.description,
|
|
15
|
+
}, async (_, { log }) => {
|
|
16
|
+
await deleteToken();
|
|
17
|
+
await deleteDeviceCode();
|
|
18
|
+
log.info(meta.success);
|
|
19
|
+
});
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { Value } from "@sinclair/typebox/value";
|
|
3
|
+
import { deleteConfig, readConfig, writeConfig } from "./config.js";
|
|
4
|
+
import { API_BASE_URL, AUTH_API_BASE_URL, CLIENT_ID } from "./env.js";
|
|
5
|
+
import { ApiResponseError } from "./errors.js";
|
|
6
|
+
import { safeParseJson } from "./json.js";
|
|
7
|
+
import { logger } from "./logger.js";
|
|
8
|
+
const DeviceCodeResponse = Type.Object({
|
|
9
|
+
device_code: Type.String(),
|
|
10
|
+
user_code: Type.String(),
|
|
11
|
+
verification_uri: Type.String(),
|
|
12
|
+
verification_uri_complete: Type.String(),
|
|
13
|
+
expires_in: Type.Number(),
|
|
14
|
+
created_at: Type.Number(),
|
|
15
|
+
});
|
|
16
|
+
const TokenResponse = Type.Object({
|
|
17
|
+
access_token: Type.String(),
|
|
18
|
+
id_token: Type.Optional(Type.String()),
|
|
19
|
+
refresh_token: Type.Optional(Type.String()),
|
|
20
|
+
token_type: Type.Literal("Bearer"),
|
|
21
|
+
expires_in: Type.Number(),
|
|
22
|
+
scope: Type.String(),
|
|
23
|
+
created_at: Type.Number(),
|
|
24
|
+
});
|
|
25
|
+
export const storeDeviceCode = (deviceCode) => {
|
|
26
|
+
return writeConfig("device_code", JSON.stringify(deviceCode));
|
|
27
|
+
};
|
|
28
|
+
export const readDeviceCode = async () => {
|
|
29
|
+
const config = await readConfig("device_code");
|
|
30
|
+
if (!config)
|
|
31
|
+
return null;
|
|
32
|
+
const json = safeParseJson(config);
|
|
33
|
+
if (!json)
|
|
34
|
+
return null;
|
|
35
|
+
try {
|
|
36
|
+
return Value.Parse(DeviceCodeResponse, json);
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
logger.warn("[auth] read device code: invalid device code, error: %o", e);
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
export const deleteDeviceCode = async () => {
|
|
44
|
+
return deleteConfig("device_code");
|
|
45
|
+
};
|
|
46
|
+
export const storeToken = (token) => {
|
|
47
|
+
return writeConfig("token", JSON.stringify(token));
|
|
48
|
+
};
|
|
49
|
+
export const readToken = async () => {
|
|
50
|
+
const config = await readConfig("token");
|
|
51
|
+
if (!config)
|
|
52
|
+
return null;
|
|
53
|
+
const json = safeParseJson(config);
|
|
54
|
+
if (!json)
|
|
55
|
+
return null;
|
|
56
|
+
try {
|
|
57
|
+
return Value.Parse(TokenResponse, json);
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
logger.warn("[auth] read token: invalid token, error: %o", e);
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
export const deleteToken = async () => {
|
|
65
|
+
return deleteConfig("token");
|
|
66
|
+
};
|
|
67
|
+
export const isDeviceCodeExpired = (deviceCode) => {
|
|
68
|
+
return deviceCode.created_at + deviceCode.expires_in * 1000 < Date.now();
|
|
69
|
+
};
|
|
70
|
+
export const isTokenExpired = (token) => {
|
|
71
|
+
return token.created_at + token.expires_in * 1000 < Date.now();
|
|
72
|
+
};
|
|
73
|
+
export const verifyDeviceCode = async () => {
|
|
74
|
+
logger.debug("[auth] verify device code");
|
|
75
|
+
const config = await readConfig("device_code");
|
|
76
|
+
if (!config) {
|
|
77
|
+
throw new Error("Device code is not found, please request login again");
|
|
78
|
+
}
|
|
79
|
+
const deviceCodeConfig = Value.Parse(DeviceCodeResponse, safeParseJson(config));
|
|
80
|
+
if (isDeviceCodeExpired(deviceCodeConfig)) {
|
|
81
|
+
throw new Error("Device code is expired, please request login again");
|
|
82
|
+
}
|
|
83
|
+
const res = await fetch(`${AUTH_API_BASE_URL}/oidc/token`, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: {
|
|
86
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
87
|
+
},
|
|
88
|
+
body: new URLSearchParams({
|
|
89
|
+
client_id: CLIENT_ID,
|
|
90
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
91
|
+
device_code: deviceCodeConfig.device_code,
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
if (!res.ok) {
|
|
95
|
+
throw new ApiResponseError(res.status, await res.text());
|
|
96
|
+
}
|
|
97
|
+
const data = await res.json();
|
|
98
|
+
if (typeof data !== "object") {
|
|
99
|
+
throw new Error("Invalid response", { cause: data });
|
|
100
|
+
}
|
|
101
|
+
const token = Value.Parse(TokenResponse, {
|
|
102
|
+
...data,
|
|
103
|
+
created_at: Date.now(),
|
|
104
|
+
});
|
|
105
|
+
if (await storeToken(token)) {
|
|
106
|
+
logger.debug("[auth] verify device code success");
|
|
107
|
+
await deleteDeviceCode();
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
throw new Error("Failed to store token, please try again");
|
|
111
|
+
}
|
|
112
|
+
return token;
|
|
113
|
+
};
|
|
114
|
+
export const requestDeviceCode = async () => {
|
|
115
|
+
logger.debug("[auth] request device code");
|
|
116
|
+
const res = await fetch(`${AUTH_API_BASE_URL}/oidc/device/auth`, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
headers: {
|
|
119
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
120
|
+
},
|
|
121
|
+
body: new URLSearchParams({
|
|
122
|
+
client_id: CLIENT_ID,
|
|
123
|
+
scope: "profile email offline_access",
|
|
124
|
+
resource: API_BASE_URL,
|
|
125
|
+
}),
|
|
126
|
+
});
|
|
127
|
+
if (!res.ok) {
|
|
128
|
+
throw new ApiResponseError(res.status, await res.text());
|
|
129
|
+
}
|
|
130
|
+
const data = await res.json();
|
|
131
|
+
if (typeof data !== "object") {
|
|
132
|
+
throw new Error("Invalid response", { cause: data });
|
|
133
|
+
}
|
|
134
|
+
const deviceCode = Value.Parse(DeviceCodeResponse, {
|
|
135
|
+
...data,
|
|
136
|
+
created_at: Date.now(),
|
|
137
|
+
});
|
|
138
|
+
if (await storeDeviceCode(deviceCode)) {
|
|
139
|
+
logger.debug("[auth] request device code success");
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
throw new Error("Failed to store device code, please try again");
|
|
143
|
+
}
|
|
144
|
+
return deviceCode;
|
|
145
|
+
};
|
|
146
|
+
const REFRESH_TOKEN_EXPIRES_IN = 3600 * 24 * 90; // 90 days in seconds
|
|
147
|
+
export const refreshToken = async () => {
|
|
148
|
+
logger.debug("[auth] refresh token");
|
|
149
|
+
const config = await readConfig("token");
|
|
150
|
+
if (!config) {
|
|
151
|
+
throw new Error("Token is not found, please login again");
|
|
152
|
+
}
|
|
153
|
+
const tokenConfig = Value.Parse(TokenResponse, safeParseJson(config));
|
|
154
|
+
if (!tokenConfig.refresh_token) {
|
|
155
|
+
throw new Error("Refresh token is not found, please login again");
|
|
156
|
+
}
|
|
157
|
+
if (tokenConfig.created_at + REFRESH_TOKEN_EXPIRES_IN * 1000 < Date.now()) {
|
|
158
|
+
await deleteToken();
|
|
159
|
+
throw new Error("Refresh token is expired, please login again");
|
|
160
|
+
}
|
|
161
|
+
const res = await fetch(`${AUTH_API_BASE_URL}/oidc/token`, {
|
|
162
|
+
method: "POST",
|
|
163
|
+
headers: {
|
|
164
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
165
|
+
},
|
|
166
|
+
body: new URLSearchParams({
|
|
167
|
+
client_id: CLIENT_ID,
|
|
168
|
+
grant_type: "refresh_token",
|
|
169
|
+
refresh_token: tokenConfig.refresh_token,
|
|
170
|
+
scope: "profile email offline_access",
|
|
171
|
+
resource: API_BASE_URL,
|
|
172
|
+
}),
|
|
173
|
+
});
|
|
174
|
+
if (!res.ok) {
|
|
175
|
+
throw new ApiResponseError(res.status, await res.text());
|
|
176
|
+
}
|
|
177
|
+
const data = await res.json();
|
|
178
|
+
if (typeof data !== "object") {
|
|
179
|
+
throw new Error("Invalid response", { cause: data });
|
|
180
|
+
}
|
|
181
|
+
const token = Value.Parse(TokenResponse, {
|
|
182
|
+
...data,
|
|
183
|
+
created_at: Date.now(),
|
|
184
|
+
});
|
|
185
|
+
if (await storeToken(token)) {
|
|
186
|
+
logger.debug("[auth] refresh token success");
|
|
187
|
+
return token;
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
throw new Error("Failed to store token, please try again");
|
|
191
|
+
}
|
|
192
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { readFile, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import envPaths from "env-paths";
|
|
5
|
+
import { IS_DEV } from "./env.js";
|
|
6
|
+
import { logger } from "./logger.js";
|
|
7
|
+
const paths = envPaths("neta-cli", {
|
|
8
|
+
suffix: IS_DEV ? "dev" : "",
|
|
9
|
+
});
|
|
10
|
+
const CONFIG_DIR = process.env["NETA_CONFIG_DIR"] ?? paths.config;
|
|
11
|
+
export const setupEnvPaths = () => {
|
|
12
|
+
try {
|
|
13
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
14
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
logger.error("Failed to setup env paths, try to set NETA_CONFIG_DIR manually, error: %o", error);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
export const readConfig = async (name) => {
|
|
22
|
+
try {
|
|
23
|
+
const filePath = resolve(CONFIG_DIR, name);
|
|
24
|
+
if (!existsSync(filePath))
|
|
25
|
+
return null;
|
|
26
|
+
const data = await readFile(filePath, "utf-8");
|
|
27
|
+
if (!data)
|
|
28
|
+
return null;
|
|
29
|
+
return Buffer.from(data, "base64").toString("utf-8");
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
logger.warn("Failed to read config: %s, error: %o", name, e);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
export const writeConfig = async (name, value) => {
|
|
37
|
+
try {
|
|
38
|
+
const filePath = resolve(CONFIG_DIR, name);
|
|
39
|
+
const encoded = Buffer.from(value, "utf-8").toString("base64");
|
|
40
|
+
await writeFile(filePath, encoded, "utf-8");
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
logger.warn("Failed to write config: %s, error: %o", name, e);
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
export const deleteConfig = async (name) => {
|
|
49
|
+
try {
|
|
50
|
+
const filePath = resolve(CONFIG_DIR, name);
|
|
51
|
+
if (!existsSync(filePath))
|
|
52
|
+
return true;
|
|
53
|
+
await unlink(filePath);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
logger.warn("Failed to delete config: %s, error: %o", name, e);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
};
|
package/bin/utils/env.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import dotenv from "dotenv-flow";
|
|
2
|
+
// Load environment variables
|
|
3
|
+
dotenv.config({
|
|
4
|
+
silent: true,
|
|
5
|
+
});
|
|
6
|
+
export const IS_DEV = process.env["NODE_ENV"] === "development";
|
|
7
|
+
export const API_BASE_URL = process.env["NETA_API_BASE_URL"] ?? "https://api.talesofai.com";
|
|
8
|
+
export const AUTH_API_BASE_URL = process.env["NETA_AUTH_API_BASE_URL"];
|
|
9
|
+
export const CLIENT_ID = process.env["NETA_CLIENT_ID"] ?? "ft6zb5zp7fqmq8y807okv";
|
|
10
|
+
export const NETA_TOKEN = process.env["NETA_TOKEN"] ?? "";
|
|
11
|
+
export const IS_GLOBAL = API_BASE_URL.endsWith("talesofai.com");
|
package/bin/utils/errors.js
CHANGED
|
@@ -34,6 +34,9 @@ export const catchErrorResponse = (data) => {
|
|
|
34
34
|
return message;
|
|
35
35
|
};
|
|
36
36
|
export const handleAxiosError = (error) => {
|
|
37
|
+
if (error instanceof ApiResponseError) {
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
37
40
|
if (error instanceof AxiosError) {
|
|
38
41
|
if (error.response?.status) {
|
|
39
42
|
let message = error.message;
|
package/bin/utils/lang.js
CHANGED
package/bin/utils/parse_meta.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { Value } from "@sinclair/typebox/value";
|
|
3
3
|
import yaml from "yaml";
|
|
4
|
-
import {
|
|
4
|
+
import { getSysLocale as getLocaleFromLang } from "./lang.js";
|
|
5
5
|
let _locale = "en_us";
|
|
6
6
|
export const setLocale = (locale) => {
|
|
7
7
|
_locale = locale ?? getLocaleFromLang();
|
package/bin/utils/telemetry.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import axios from "axios";
|
|
2
2
|
import pkg from "../../package.json" with { type: "json" };
|
|
3
|
+
import { IS_DEV } from "./env.js";
|
|
4
|
+
import { logger } from "./logger.js";
|
|
3
5
|
const DISABLE_TELEMETRY = process.env["DISABLE_TELEMETRY"] === "1";
|
|
4
|
-
const IS_DEV = process.env["NODE_ENV"] === "development";
|
|
5
6
|
const api = axios.create({
|
|
6
7
|
baseURL: "https://gator.volces.com/v2/event/json",
|
|
7
8
|
headers: {
|
|
@@ -24,10 +25,11 @@ export const trackConfig = (config) => {
|
|
|
24
25
|
...config,
|
|
25
26
|
};
|
|
26
27
|
};
|
|
27
|
-
const logger = console;
|
|
28
28
|
export const track = (event, params) => {
|
|
29
29
|
if (DISABLE_TELEMETRY)
|
|
30
30
|
return;
|
|
31
|
+
if (!_user)
|
|
32
|
+
return;
|
|
31
33
|
api
|
|
32
34
|
.post("/", {
|
|
33
35
|
user: _user,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@talesofai/neta-skills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"description": "Neta API pi coding agent skills for interacting with Neta API to generate images, videos, songs, and manage characters/elements.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"dayjs": "^1.11.20",
|
|
52
52
|
"dotenv": "^16.4.7",
|
|
53
53
|
"dotenv-flow": "^4.1.0",
|
|
54
|
+
"env-paths": "^4.0.0",
|
|
54
55
|
"magic-bytes.js": "^1.13.0",
|
|
55
56
|
"os-locale": "^8.0.0",
|
|
56
57
|
"p-limit": "^7.3.0",
|
package/skills/neta/SKILL.md
CHANGED
|
@@ -49,6 +49,25 @@ npx skills add talesofai/neta-skills/skills/neta-adventure
|
|
|
49
49
|
- Creating or managing visual style elements (scenes, props, clothing, poses, atmospheres, memes) → use `neta-elementum`
|
|
50
50
|
3. Use this skill only when boundaries are unclear or when you need to explain which sub-skill to pick.
|
|
51
51
|
|
|
52
|
+
## Authorization
|
|
53
|
+
|
|
54
|
+
Use this when the user needs a **logged-in Neta identity** for CLI-backed flows and no valid session exists (or you would otherwise rely on `NETA_TOKEN`).
|
|
55
|
+
|
|
56
|
+
1. **Start the flow**: run **`neta login`** (default action is `request-code`).
|
|
57
|
+
```bash
|
|
58
|
+
npx -y @talesofai/neta-skills@latest login --action request-code
|
|
59
|
+
```
|
|
60
|
+
This begins the OAuth **device authorization** flow stored by the CLI.
|
|
61
|
+
|
|
62
|
+
2. **Browser step**: When the command returns device-authorization fields, show the user **`verification_uri_complete`** (the ready-to-open URL), tell them to open it in a browser and finish sign-in/consent there, then **return to the chat and explicitly say the browser step is done** so you know when to continue.
|
|
63
|
+
|
|
64
|
+
3. **Complete login**: After they confirm in chat, run **`neta login --action verify-code`** to exchange the device code for tokens. On success, show the returned **account basics**: **`uuid`** (long user id), **`nick_name`**, and **`avatar_url`**.
|
|
65
|
+
```bash
|
|
66
|
+
npx -y @talesofai/neta-skills@latest login --action verify-code
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Region / fallback**: Device login is only supported when the CLI is using the **global** API host (see `NETA_API_BASE_URL` / `talesofai.com`). If the command errors with “not supported in the current region”, tell the user to authenticate via the **`NETA_TOKEN`** environment variable instead.
|
|
70
|
+
|
|
52
71
|
## Capability map and sub-skill overview
|
|
53
72
|
|
|
54
73
|
### 1. Spaces and worldbuilding: `neta-space`
|
|
@@ -20,6 +20,23 @@ Determine mode before acting. Do not ask multiple questions.
|
|
|
20
20
|
|
|
21
21
|
Once mode is established, do not re-ask.
|
|
22
22
|
|
|
23
|
+
## Authorization
|
|
24
|
+
|
|
25
|
+
Use this when the user needs a **logged-in Neta identity** for CLI-backed flows and no valid session exists (or you would otherwise rely on `NETA_TOKEN`).
|
|
26
|
+
|
|
27
|
+
1. **Start the flow**: run **`neta login`** (default action is `request-code`).
|
|
28
|
+
```bash
|
|
29
|
+
npx -y @talesofai/neta-skills@latest login --action request-code
|
|
30
|
+
```
|
|
31
|
+
This begins the OAuth **device authorization** flow stored by the CLI.
|
|
32
|
+
|
|
33
|
+
2. **Browser step**: When the command returns device-authorization fields, show the user **`verification_uri_complete`** (the ready-to-open URL), tell them to open it in a browser and finish sign-in/consent there, then **return to the chat and explicitly say the browser step is done** so you know when to continue.
|
|
34
|
+
|
|
35
|
+
3. **Complete login**: After they confirm in chat, run **`neta login --action verify-code`** to exchange the device code for tokens. On success, show the returned **account basics**: **`uuid`** (long user id), **`nick_name`**, and **`avatar_url`**.
|
|
36
|
+
```bash
|
|
37
|
+
npx -y @talesofai/neta-skills@latest login --action verify-code
|
|
38
|
+
```
|
|
39
|
+
|
|
23
40
|
## Commands
|
|
24
41
|
|
|
25
42
|
**Create a campaign**
|
|
@@ -9,6 +9,23 @@ Guide users from inspiration to forging, completing the creation and management
|
|
|
9
9
|
|
|
10
10
|
> This skill requires the **neta-creative** skill to use `make_image` for visual previews.
|
|
11
11
|
|
|
12
|
+
## Authorization
|
|
13
|
+
|
|
14
|
+
Use this when the user needs a **logged-in Neta identity** for CLI-backed flows and no valid session exists (or you would otherwise rely on `NETA_TOKEN`).
|
|
15
|
+
|
|
16
|
+
1. **Start the flow**: run **`neta login`** (default action is `request-code`).
|
|
17
|
+
```bash
|
|
18
|
+
npx -y @talesofai/neta-skills@latest login --action request-code
|
|
19
|
+
```
|
|
20
|
+
This begins the OAuth **device authorization** flow stored by the CLI.
|
|
21
|
+
|
|
22
|
+
2. **Browser step**: When the command returns device-authorization fields, show the user **`verification_uri_complete`** (the ready-to-open URL), tell them to open it in a browser and finish sign-in/consent there, then **return to the chat and explicitly say the browser step is done** so you know when to continue.
|
|
23
|
+
|
|
24
|
+
3. **Complete login**: After they confirm in chat, run **`neta login --action verify-code`** to exchange the device code for tokens. On success, show the returned **account basics**: **`uuid`** (long user id), **`nick_name`**, and **`avatar_url`**.
|
|
25
|
+
```bash
|
|
26
|
+
npx -y @talesofai/neta-skills@latest login --action verify-code
|
|
27
|
+
```
|
|
28
|
+
|
|
12
29
|
## Command Usage
|
|
13
30
|
|
|
14
31
|
### Create Character
|
|
@@ -17,6 +17,23 @@ Used to interact with the Neta API for community feed browsing, interactions, an
|
|
|
17
17
|
3. If the user needs **systematic research or complex filtering by categories/keywords**, switch to `neta-suggest`.
|
|
18
18
|
4. If the user wants to **create new content** (images/videos/songs/MVs), switch to `neta-creative`.
|
|
19
19
|
|
|
20
|
+
## Authorization
|
|
21
|
+
|
|
22
|
+
Use this when the user needs a **logged-in Neta identity** for CLI-backed flows and no valid session exists (or you would otherwise rely on `NETA_TOKEN`).
|
|
23
|
+
|
|
24
|
+
1. **Start the flow**: run **`neta login`** (default action is `request-code`).
|
|
25
|
+
```bash
|
|
26
|
+
npx -y @talesofai/neta-skills@latest login --action request-code
|
|
27
|
+
```
|
|
28
|
+
This begins the OAuth **device authorization** flow stored by the CLI.
|
|
29
|
+
|
|
30
|
+
2. **Browser step**: When the command returns device-authorization fields, show the user **`verification_uri_complete`** (the ready-to-open URL), tell them to open it in a browser and finish sign-in/consent there, then **return to the chat and explicitly say the browser step is done** so you know when to continue.
|
|
31
|
+
|
|
32
|
+
3. **Complete login**: After they confirm in chat, run **`neta login --action verify-code`** to exchange the device code for tokens. On success, show the returned **account basics**: **`uuid`** (long user id), **`nick_name`**, and **`avatar_url`**.
|
|
33
|
+
```bash
|
|
34
|
+
npx -y @talesofai/neta-skills@latest login --action verify-code
|
|
35
|
+
```
|
|
36
|
+
|
|
20
37
|
## Commands
|
|
21
38
|
|
|
22
39
|
### Collection
|
|
@@ -15,6 +15,23 @@ Used to interact with the Neta API for multimedia content creation, creation‑r
|
|
|
15
15
|
2. If, during creation, you discover that the real need is more like “browse recommendations / casually explore / research topics”, combine this skill with `neta-community` or `neta-suggest` instead of overloading this skill.
|
|
16
16
|
3. **Premium** (plans, orders, Stripe): use the commands below. Run **`get_current_premium_plan`** before and after checkout so the user can confirm tier (and end time if returned). Commerce needs the **global** Neta API—see the premium reference. If creation is blocked by **quota / credits (电量)** or **usage frequency (频率)**, say why in one beat; if an upgrade fits their goal, offer the path once (list plans → create order → pay)—**do not** repeat upgrade nudges in the same conversation unless the user asks.
|
|
17
17
|
|
|
18
|
+
## Authorization
|
|
19
|
+
|
|
20
|
+
Use this when the user needs a **logged-in Neta identity** for CLI-backed flows and no valid session exists (or you would otherwise rely on `NETA_TOKEN`).
|
|
21
|
+
|
|
22
|
+
1. **Start the flow**: run **`neta login`** (default action is `request-code`).
|
|
23
|
+
```bash
|
|
24
|
+
npx -y @talesofai/neta-skills@latest login --action request-code
|
|
25
|
+
```
|
|
26
|
+
This begins the OAuth **device authorization** flow stored by the CLI.
|
|
27
|
+
|
|
28
|
+
2. **Browser step**: When the command returns device-authorization fields, show the user **`verification_uri_complete`** (the ready-to-open URL), tell them to open it in a browser and finish sign-in/consent there, then **return to the chat and explicitly say the browser step is done** so you know when to continue.
|
|
29
|
+
|
|
30
|
+
3. **Complete login**: After they confirm in chat, run **`neta login --action verify-code`** to exchange the device code for tokens. On success, show the returned **account basics**: **`uuid`** (long user id), **`nick_name`**, and **`avatar_url`**.
|
|
31
|
+
```bash
|
|
32
|
+
npx -y @talesofai/neta-skills@latest login --action verify-code
|
|
33
|
+
```
|
|
34
|
+
|
|
18
35
|
## Commands
|
|
19
36
|
|
|
20
37
|
### Content creation
|
|
@@ -9,6 +9,23 @@ Through the "Elementum Alchemy" workflow, forge any visual concept into a reusab
|
|
|
9
9
|
|
|
10
10
|
> This skill requires the **neta-creative** skill to use `make_image` for visual previews.
|
|
11
11
|
|
|
12
|
+
## Authorization
|
|
13
|
+
|
|
14
|
+
Use this when the user needs a **logged-in Neta identity** for CLI-backed flows and no valid session exists (or you would otherwise rely on `NETA_TOKEN`).
|
|
15
|
+
|
|
16
|
+
1. **Start the flow**: run **`neta login`** (default action is `request-code`).
|
|
17
|
+
```bash
|
|
18
|
+
npx -y @talesofai/neta-skills@latest login --action request-code
|
|
19
|
+
```
|
|
20
|
+
This begins the OAuth **device authorization** flow stored by the CLI.
|
|
21
|
+
|
|
22
|
+
2. **Browser step**: When the command returns device-authorization fields, show the user **`verification_uri_complete`** (the ready-to-open URL), tell them to open it in a browser and finish sign-in/consent there, then **return to the chat and explicitly say the browser step is done** so you know when to continue.
|
|
23
|
+
|
|
24
|
+
3. **Complete login**: After they confirm in chat, run **`neta login --action verify-code`** to exchange the device code for tokens. On success, show the returned **account basics**: **`uuid`** (long user id), **`nick_name`**, and **`avatar_url`**.
|
|
25
|
+
```bash
|
|
26
|
+
npx -y @talesofai/neta-skills@latest login --action verify-code
|
|
27
|
+
```
|
|
28
|
+
|
|
12
29
|
## Command Usage
|
|
13
30
|
|
|
14
31
|
### Create Elementum
|
|
@@ -17,6 +17,23 @@ Used to interact with the Neta API to browse space‑level content.
|
|
|
17
17
|
- If needed, fetch characters and playable content inside the space.
|
|
18
18
|
3. If the user says “now generate an image/video/song for this space”, first collect the relevant space/collection info here, then switch to `neta-creative` for creation.
|
|
19
19
|
|
|
20
|
+
## Authorization
|
|
21
|
+
|
|
22
|
+
Use this when the user needs a **logged-in Neta identity** for CLI-backed flows and no valid session exists (or you would otherwise rely on `NETA_TOKEN`).
|
|
23
|
+
|
|
24
|
+
1. **Start the flow**: run **`neta login`** (default action is `request-code`).
|
|
25
|
+
```bash
|
|
26
|
+
npx -y @talesofai/neta-skills@latest login --action request-code
|
|
27
|
+
```
|
|
28
|
+
This begins the OAuth **device authorization** flow stored by the CLI.
|
|
29
|
+
|
|
30
|
+
2. **Browser step**: When the command returns device-authorization fields, show the user **`verification_uri_complete`** (the ready-to-open URL), tell them to open it in a browser and finish sign-in/consent there, then **return to the chat and explicitly say the browser step is done** so you know when to continue.
|
|
31
|
+
|
|
32
|
+
3. **Complete login**: After they confirm in chat, run **`neta login --action verify-code`** to exchange the device code for tokens. On success, show the returned **account basics**: **`uuid`** (long user id), **`nick_name`**, and **`avatar_url`**.
|
|
33
|
+
```bash
|
|
34
|
+
npx -y @talesofai/neta-skills@latest login --action verify-code
|
|
35
|
+
```
|
|
36
|
+
|
|
20
37
|
## Space concepts
|
|
21
38
|
|
|
22
39
|
> A space is a themed collection of gameplay experiences — a scene where content is produced and consumed.
|
|
@@ -12,6 +12,23 @@ description: Neta API research and recommendation skill — provide keyword/tag/
|
|
|
12
12
|
3. Before content creation, use this skill to research topics/tags/categories, then hand off to `neta-creative` for concrete creation.
|
|
13
13
|
4. When the user wants to like/comment or otherwise interact with specific works, switch to `neta-community`.
|
|
14
14
|
|
|
15
|
+
## Authorization
|
|
16
|
+
|
|
17
|
+
Use this when the user needs a **logged-in Neta identity** for CLI-backed flows and no valid session exists (or you would otherwise rely on `NETA_TOKEN`).
|
|
18
|
+
|
|
19
|
+
1. **Start the flow**: run **`neta login`** (default action is `request-code`).
|
|
20
|
+
```bash
|
|
21
|
+
npx -y @talesofai/neta-skills@latest login --action request-code
|
|
22
|
+
```
|
|
23
|
+
This begins the OAuth **device authorization** flow stored by the CLI.
|
|
24
|
+
|
|
25
|
+
2. **Browser step**: When the command returns device-authorization fields, show the user **`verification_uri_complete`** (the ready-to-open URL), tell them to open it in a browser and finish sign-in/consent there, then **return to the chat and explicitly say the browser step is done** so you know when to continue.
|
|
26
|
+
|
|
27
|
+
3. **Complete login**: After they confirm in chat, run **`neta login --action verify-code`** to exchange the device code for tokens. On success, show the returned **account basics**: **`uuid`** (long user id), **`nick_name`**, and **`avatar_url`**.
|
|
28
|
+
```bash
|
|
29
|
+
npx -y @talesofai/neta-skills@latest login --action verify-code
|
|
30
|
+
```
|
|
31
|
+
|
|
15
32
|
## Core capabilities
|
|
16
33
|
|
|
17
34
|
### 1. suggest_keywords — keyword suggestions
|