@talesofai/neta-skills 0.15.3 → 0.16.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +38 -6
  3. package/README.zh_cn.md +38 -7
  4. package/bin/apis/index.js +16 -1
  5. package/bin/cli.js +4 -8
  6. package/bin/commands/adventure_campaign/create_adventure_campaign.cmd.js +1 -1
  7. package/bin/commands/adventure_campaign/list_my_adventure_campaigns.cmd.js +1 -1
  8. package/bin/commands/adventure_campaign/update_adventure_campaign.cmd.js +1 -1
  9. package/bin/commands/creative/upload.cmd.js +1 -1
  10. package/bin/commands/load.js +18 -33
  11. package/bin/commands/premium/create_premium_order.cmd.js +2 -1
  12. package/bin/commands/premium/get_current_premium_plan.cmd.js +4 -3
  13. package/bin/commands/premium/get_premium_order.cmd.js +2 -1
  14. package/bin/commands/premium/list_premium_orders.cmd.js +2 -1
  15. package/bin/commands/premium/list_premium_plans.cmd.js +2 -1
  16. package/bin/commands/premium/pay_premium_order.cmd.js +2 -1
  17. package/bin/commands/user/login.cmd.en_us.yml +8 -0
  18. package/bin/commands/user/login.cmd.js +41 -0
  19. package/bin/commands/user/login.cmd.zh_cn.yml +8 -0
  20. package/bin/commands/user/logout.cmd.en_us.yml +5 -0
  21. package/bin/commands/user/logout.cmd.js +19 -0
  22. package/bin/commands/user/logout.cmd.zh_cn.yml +5 -0
  23. package/bin/utils/auth.js +192 -0
  24. package/bin/utils/config.js +60 -0
  25. package/bin/utils/env.js +11 -0
  26. package/bin/utils/errors.js +3 -0
  27. package/bin/utils/lang.js +1 -1
  28. package/bin/utils/logger.js +10 -0
  29. package/bin/utils/parse_meta.js +1 -1
  30. package/bin/utils/telemetry.js +4 -2
  31. package/package.json +2 -1
  32. package/skills/neta/SKILL.md +19 -0
  33. package/skills/neta-adventure/SKILL.md +17 -0
  34. package/skills/neta-character/SKILL.md +17 -0
  35. package/skills/neta-community/SKILL.md +17 -0
  36. package/skills/neta-creative/SKILL.md +17 -0
  37. package/skills/neta-elementum/SKILL.md +17 -0
  38. package/skills/neta-space/SKILL.md +17 -0
  39. 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.2
4
+
5
+ ### Patch Changes
6
+
7
+ - fix auth api endpoint
8
+
9
+ ## 0.16.1
10
+
11
+ ### Patch Changes
12
+
13
+ - fix some env problems
14
+
15
+ ## 0.16.0
16
+
17
+ ### Minor Changes
18
+
19
+ - support oauth2 device flow with login command
20
+
3
21
  ## 0.15.3
4
22
 
5
23
  ### Patch Changes
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # NETA Skills
2
2
 
3
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
- [![Node](https://img.shields.io/badge/Node-%3E%3D20-green)](https://nodejs.org/)
4
+ [![Node](https://img.shields.io/badge/Node-%3E%3D22-green)](https://nodejs.org/)
5
5
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.7-blue)](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 / NETA TOKEN from the [Neta Open Portal](https://www.neta.art/open/).
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 **42 commands** for various tasks:
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` | | - | Your Neta Art API access token. |
225
- | `NETA_API_BASE_URL` | ❌ | default: `https://api.talesofai.com` | Base URL for the Neta API. |
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
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
- [![Node](https://img.shields.io/badge/Node-%3E%3D20-green)](https://nodejs.org/)
4
+ [![Node](https://img.shields.io/badge/Node-%3E%3D22-green)](https://nodejs.org/)
5
5
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.7-blue)](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
- 当前技能共包含 **34 个命令**,覆盖创作、奇遇、角色与社区探索等场景:
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 命令定义(TS + YAML 描述)
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` | | - | Neta Art API 访问令牌 |
224
- | `NETA_API_BASE_URL` | ❌ | default: `https://api.talesofai.com` | Neta API 网关地址 |
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
- // Load environment variables
7
- dotenv.config({
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
- const cli = program.option("--api_base_url <string>", "api base url (default: NETA_API_BASE_URL or locale-based default)");
15
- await buildCommands(cli);
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
@@ -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 { getLocale } from "../utils/lang.js";
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,18 +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
- // biome-ignore lint/suspicious/noConsole: use console
46
- error: console.error,
47
- // biome-ignore lint/suspicious/noConsole: use console
48
- warn: console.warn,
49
- // biome-ignore lint/suspicious/noConsole: use console
50
- info: console.info,
51
- debug: () => { },
52
- };
53
43
  export const buildCommands = async (cli) => {
54
44
  setLocale();
55
45
  const commands = await loadCommands([
@@ -58,15 +48,16 @@ export const buildCommands = async (cli) => {
58
48
  "character_elementum",
59
49
  "adventure_campaign",
60
50
  "premium",
51
+ "user",
61
52
  ]);
62
- return commands.map((cmd) => {
53
+ commands.forEach((cmd) => {
63
54
  const command = cli.command(cmd.name);
64
55
  command.description(cmd.title || cmd.description || "");
65
56
  const inputSchema = cmd.inputSchema;
66
57
  if (inputSchema && "properties" in inputSchema) {
67
58
  const properties = inputSchema.properties;
68
59
  if (!properties)
69
- return command;
60
+ return;
70
61
  Object.entries(properties).forEach(([key, property]) => {
71
62
  if (typeof property !== "object")
72
63
  return;
@@ -97,28 +88,25 @@ export const buildCommands = async (cli) => {
97
88
  });
98
89
  }
99
90
  command.action(async (args) => {
100
- const { api_base_url } = cli.opts();
101
- const baseUrl = typeof api_base_url === "string"
102
- ? api_base_url
103
- : (process.env["NETA_API_BASE_URL"] ?? "https://api.talesofai.com");
104
91
  trackConfig({
105
- app_region: baseUrl.endsWith("cn") ? "cn" : "global",
106
- app_language: getLocale(),
92
+ app_region: API_BASE_URL.endsWith("cn") ? "cn" : "global",
93
+ app_language: getSysLocale(),
107
94
  });
108
95
  const apis = createApis({
109
96
  logger,
110
- baseUrl,
97
+ baseUrl: API_BASE_URL,
111
98
  headers: {
112
- "x-token": process.env["NETA_TOKEN"] ?? "",
113
99
  "x-platform": "nieta-app/web",
114
100
  },
115
101
  });
116
- const user = await apis.user.me().catch((e) => {
117
- if (e instanceof ApiResponseError) {
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
+ }
118
108
  return null;
119
- }
120
- return null;
121
- });
109
+ });
122
110
  const startTime = Date.now();
123
111
  logger.debug("[telemetry] user: %s", user?.uuid);
124
112
  trackConfigUser(user ? { user_unique_id: user.uuid } : null);
@@ -128,9 +116,7 @@ export const buildCommands = async (cli) => {
128
116
  });
129
117
  const type = cmd.inputSchema ?? Type.Object({});
130
118
  const input = Value.Parse(type, args);
131
- if (IS_DEV) {
132
- logger.debug("[command] %s, params: %o", cmd.name, input);
133
- }
119
+ logger.debug("[command] %s, params: %o", cmd.name, input);
134
120
  await cmd
135
121
  .execute(input, {
136
122
  apis,
@@ -166,7 +152,7 @@ export const buildCommands = async (cli) => {
166
152
  type: e.name,
167
153
  message: e.message,
168
154
  path: e.error?.path,
169
- schema: e.error?.schema,
155
+ schema: JSON.stringify(e.error?.schema),
170
156
  },
171
157
  });
172
158
  return null;
@@ -213,6 +199,5 @@ export const buildCommands = async (cli) => {
213
199
  return null;
214
200
  });
215
201
  });
216
- return command;
217
202
  });
218
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 (!apis.baseUrl.endsWith("talesofai.com")) {
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 (_, { apis, user }) => {
21
- if (!apis.baseUrl.endsWith("talesofai.com")) {
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 (!apis.baseUrl.endsWith("talesofai.com")) {
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 (!apis.baseUrl.endsWith("talesofai.com")) {
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 (!apis.baseUrl.endsWith("talesofai.com")) {
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 (!apis.baseUrl.endsWith("talesofai.com")) {
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,8 @@
1
+ name: login
2
+ title: 登录
3
+ description: 登录到你的 Neta 账户
4
+ parameters:
5
+ action: 要执行的操作 (request-code 请求验证码, verify-code 验证验证码)
6
+
7
+ errors:
8
+ not_supported_in_current_region: 当前区域不支持此命令, 请设置 NETA_TOKEN 环境变量进行身份验证
@@ -0,0 +1,5 @@
1
+ name: logout
2
+ title: Logout
3
+ description: Logout from your Neta account
4
+
5
+ success: Logout success
@@ -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,5 @@
1
+ name: logout
2
+ title: 退出登录
3
+ description: 退出登录你的 Neta 账户
4
+
5
+ success: 退出登录成功
@@ -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
+ };
@@ -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"] ?? "https://auth.talesofai.com";
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");
@@ -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
@@ -1,5 +1,5 @@
1
1
  import osLocale from "os-locale";
2
- export const getLocale = () => {
2
+ export const getSysLocale = () => {
3
3
  const locale = osLocale();
4
4
  if (locale.startsWith("zh")) {
5
5
  return "zh_cn";
@@ -0,0 +1,10 @@
1
+ /** biome-ignore-all lint/suspicious/noConsole: use console */
2
+ import { IS_DEV } from "./env.js";
3
+ export const logger = IS_DEV
4
+ ? console
5
+ : {
6
+ error: console.error,
7
+ warn: console.warn,
8
+ info: console.info,
9
+ debug: () => { },
10
+ };
@@ -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 { getLocale as getLocaleFromLang } from "./lang.js";
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();
@@ -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.15.3",
3
+ "version": "0.16.2",
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",
@@ -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