@fastthree-ai/cli 0.0.0-stage → 0.2.0

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/dist/api.js ADDED
@@ -0,0 +1,133 @@
1
+ /**
2
+ * HTTP 客户端 + SSE 流式解析。
3
+ *
4
+ * 后端响应包络(OpenApiController 系)是 CommonResult { code, message, data },
5
+ * HTTP 永远 200,业务错误用 code 字段表达 —— 所以这里把 code != 0/200 视为失败。
6
+ *
7
+ * LLM 的 /chat/completions 走 OpenAI 兼容格式,不走 CommonResult 包络,
8
+ * 故 streamChat 直接读 SSE data 行不做包络剥离。
9
+ */
10
+ import { resolveBaseUrl, requireToken } from "./config.js";
11
+ export class ApiError extends Error {
12
+ code;
13
+ constructor(code, message) {
14
+ super(message);
15
+ this.code = code;
16
+ this.name = "ApiError";
17
+ }
18
+ }
19
+ function buildUrl(path, baseUrlOverride) {
20
+ return `${resolveBaseUrl(baseUrlOverride)}${path}`;
21
+ }
22
+ function authHeaders(token) {
23
+ const t = token || requireToken();
24
+ return {
25
+ Authorization: `Bearer ${t}`,
26
+ "Content-Type": "application/json",
27
+ };
28
+ }
29
+ /** 调用 GET/POST/PUT/DELETE,自动解 CommonResult 包络 */
30
+ export async function request(method, path, options = {}) {
31
+ const url = buildUrl(path, options.baseUrlOverride);
32
+ const headers = { ...authHeaders(options.token), ...(options.headers || {}) };
33
+ const init = { method, headers };
34
+ if (options.body !== undefined && method !== "GET" && method !== "HEAD") {
35
+ init.body = typeof options.body === "string" ? options.body : JSON.stringify(options.body);
36
+ }
37
+ const resp = await fetch(url, init);
38
+ const text = await resp.text();
39
+ let payload = null;
40
+ try {
41
+ payload = text ? JSON.parse(text) : null;
42
+ }
43
+ catch {
44
+ throw new ApiError(resp.status, `响应解析失败: ${text.slice(0, 200)}`);
45
+ }
46
+ // 后端惯例:HTTP 永远 200,业务错误用 code 字段表示
47
+ if (payload && typeof payload.code === "number" && payload.code !== 0 && payload.code !== 200) {
48
+ throw new ApiError(payload.code, payload.message || `请求失败 (code=${payload.code})`);
49
+ }
50
+ if (!payload || payload.data === undefined) {
51
+ throw new ApiError(resp.status, `响应格式异常: ${text.slice(0, 200)}`);
52
+ }
53
+ return payload.data;
54
+ }
55
+ /** 开放 API 上传:multipart/form-data,file 字段;返回可直接填入出图参数的 URL */
56
+ export async function uploadFile(filePath, token, baseUrlOverride) {
57
+ const { readFile } = await import("node:fs/promises");
58
+ const { basename } = await import("node:path");
59
+ const buf = await readFile(filePath);
60
+ const filename = basename(filePath);
61
+ const form = new FormData();
62
+ form.append("file", new Blob([buf]), filename);
63
+ const url = buildUrl("/api/open/v1/upload", baseUrlOverride);
64
+ const resp = await fetch(url, {
65
+ method: "POST",
66
+ headers: { Authorization: `Bearer ${token || requireToken()}` },
67
+ body: form,
68
+ });
69
+ const text = await resp.text();
70
+ let payload = null;
71
+ try {
72
+ payload = text ? JSON.parse(text) : null;
73
+ }
74
+ catch {
75
+ throw new ApiError(resp.status, `响应解析失败: ${text.slice(0, 200)}`);
76
+ }
77
+ if (!payload || (payload.code !== 0 && payload.code !== 200)) {
78
+ throw new ApiError(payload?.code ?? resp.status, payload?.message || "上传失败");
79
+ }
80
+ return payload.data;
81
+ }
82
+ /**
83
+ * 流式调用 /chat/completions,逐 chunk 回调。
84
+ * 解析规则:SSE 事件以空行分隔,data: 行 JSON parse;[DONE] 结束。
85
+ */
86
+ export async function streamChat(body, onChunk, signal) {
87
+ const url = buildUrl("/api/open/v1/chat/completions");
88
+ const resp = await fetch(url, {
89
+ method: "POST",
90
+ headers: { ...authHeaders(), Accept: "text/event-stream" },
91
+ body: JSON.stringify({ ...body, stream: true }),
92
+ signal,
93
+ });
94
+ const contentType = resp.headers.get("content-type") || "";
95
+ if (!contentType.includes("event-stream")) {
96
+ const text = await resp.text();
97
+ throw new ApiError(resp.status, `流式响应失败 (content-type=${contentType}): ${text.slice(0, 200)}`);
98
+ }
99
+ if (!resp.body)
100
+ throw new Error("响应没有 body 流");
101
+ const reader = resp.body.getReader();
102
+ const decoder = new TextDecoder("utf-8");
103
+ let buffer = "";
104
+ while (true) {
105
+ const { done, value } = await reader.read();
106
+ if (done)
107
+ break;
108
+ buffer += decoder.decode(value, { stream: true });
109
+ let sep;
110
+ while ((sep = buffer.indexOf("\n\n")) >= 0) {
111
+ const rawEvent = buffer.slice(0, sep);
112
+ buffer = buffer.slice(sep + 2);
113
+ const lines = rawEvent.split("\n");
114
+ let dataLine = "";
115
+ for (const line of lines) {
116
+ if (line.startsWith("data:")) {
117
+ dataLine += line.slice(5).trim();
118
+ }
119
+ }
120
+ if (!dataLine)
121
+ continue;
122
+ if (dataLine === "[DONE]")
123
+ return;
124
+ try {
125
+ onChunk(JSON.parse(dataLine));
126
+ }
127
+ catch {
128
+ // 跳过无法解析的行
129
+ }
130
+ }
131
+ }
132
+ }
133
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import { request } from "../api.js";
2
+ export function registerAccountCommands(program) {
3
+ program
4
+ .command("account")
5
+ .description("查询账户算力/积分/会员档位")
6
+ .action(async () => {
7
+ const data = await request("GET", "/api/open/v1/account");
8
+ console.log(`用户ID: ${data.userId}`);
9
+ console.log(`用户名: ${data.username}`);
10
+ console.log(`积分: ${data.points}`);
11
+ console.log(`会员档位: ${data.membershipTier ?? "-"}`);
12
+ console.log(`等级: ${data.level ?? "-"}`);
13
+ });
14
+ }
15
+ //# sourceMappingURL=account.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"account.js","sourceRoot":"","sources":["../../src/commands/account.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAUpC,MAAM,UAAU,uBAAuB,CAAC,OAAgB;IACtD,OAAO;SACJ,OAAO,CAAC,SAAS,CAAC;SAClB,WAAW,CAAC,gBAAgB,CAAC;SAC7B,MAAM,CAAC,KAAK,IAAI,EAAE;QACjB,MAAM,IAAI,GAAG,MAAM,OAAO,CAAc,KAAK,EAAE,sBAAsB,CAAC,CAAC;QACvE,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;QACtC,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;QACvC,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;QACtC,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,CAAC,cAAc,IAAI,GAAG,EAAE,CAAC,CAAC;QACnD,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,CAAC,KAAK,IAAI,GAAG,EAAE,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;AACP,CAAC"}
@@ -0,0 +1,53 @@
1
+ import { streamChat } from "../api.js";
2
+ export function registerChatCommands(program) {
3
+ program
4
+ .command("chat <model>")
5
+ .description("与 LLM 模型聊天(流式输出到 stdout)")
6
+ .option("-m, --message <text>", "单条消息(不进交互模式)")
7
+ .option("--max-tokens <n>", "max_tokens", "1024")
8
+ .action(async (model, opts) => {
9
+ const maxTokens = Number(opts.maxTokens || "1024");
10
+ if (opts.message) {
11
+ const messages = [{ role: "user", content: opts.message }];
12
+ await streamChat({ model, messages, max_tokens: maxTokens }, (chunk) => {
13
+ const delta = chunk.choices?.[0]?.delta?.content;
14
+ if (delta)
15
+ process.stdout.write(delta);
16
+ });
17
+ process.stdout.write("\n");
18
+ return;
19
+ }
20
+ // 交互模式
21
+ const { createInterface } = await import("node:readline");
22
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
23
+ const messages = [];
24
+ console.error(`已进入交互模式(model=${model})。输入 :q 退出。`);
25
+ try {
26
+ while (true) {
27
+ const line = await new Promise((resolve) => rl.question("> ", resolve));
28
+ const trimmed = line.trim();
29
+ if (!trimmed)
30
+ continue;
31
+ if (trimmed === ":q" || trimmed === ":quit")
32
+ break;
33
+ messages.push({ role: "user", content: trimmed });
34
+ // 拼接 delta 写回 messages 作为 assistant 回包,保证下一轮上下文完整
35
+ let assistant = "";
36
+ await streamChat({ model, messages, max_tokens: maxTokens }, (chunk) => {
37
+ const delta = chunk.choices?.[0]?.delta?.content;
38
+ if (delta) {
39
+ assistant += delta;
40
+ process.stdout.write(delta);
41
+ }
42
+ });
43
+ process.stdout.write("\n");
44
+ if (assistant)
45
+ messages.push({ role: "assistant", content: assistant });
46
+ }
47
+ }
48
+ finally {
49
+ rl.close();
50
+ }
51
+ });
52
+ }
53
+ //# sourceMappingURL=chat.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat.js","sourceRoot":"","sources":["../../src/commands/chat.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,104 @@
1
+ import * as readline from "node:readline/promises";
2
+ import { stdin as input, stdout as output } from "node:process";
3
+ import { loadConfig, saveConfig, resolveBaseUrl, requireToken } from "../config.js";
4
+ import { request, ApiError } from "../api.js";
5
+ export function registerConfigCommands(program) {
6
+ const cfg = program.command("config").description("管理本地配置(base_url / token)");
7
+ cfg.command("set-token <token>")
8
+ .description("保存 API Token(Authorization: Bearer 的凭据)")
9
+ .action((token) => {
10
+ const cur = loadConfig();
11
+ saveConfig({ ...cur, token });
12
+ console.log("✓ Token 已保存到 ~/.ft/config.json");
13
+ });
14
+ cfg.command("set-base-url <url>")
15
+ .description("设置后端 base URL(默认 https://ai-api.fastthree.com)")
16
+ .action((url) => {
17
+ const cur = loadConfig();
18
+ saveConfig({ ...cur, baseUrl: url });
19
+ console.log(`✓ baseUrl 已更新为 ${url}`);
20
+ });
21
+ cfg.command("show")
22
+ .description("查看当前配置(token 脱敏显示)")
23
+ .action(() => {
24
+ const cur = loadConfig();
25
+ const masked = cur.token
26
+ ? `${cur.token.slice(0, 6)}***${cur.token.slice(-4)}`
27
+ : "(未设置)";
28
+ console.log(`baseUrl: ${cur.baseUrl}`);
29
+ console.log(`token: ${masked}`);
30
+ });
31
+ cfg.command("verify")
32
+ .description("调 /account 健康检查,验证 token 与连通性")
33
+ .action(async () => {
34
+ await verifyAndPrint();
35
+ });
36
+ }
37
+ /**
38
+ * ft configure — 交互式配置入口。
39
+ *
40
+ * 不带参数:提示输入 token + base-url,保存。
41
+ * --verify:保存后调 /account 验证(也可单独对已存配置运行 ft configure --verify)。
42
+ */
43
+ export function registerConfigureCommand(program) {
44
+ program
45
+ .command("configure")
46
+ .description("交互式配置 token 与 base-url(--verify 同时做健康检查)")
47
+ .option("--verify", "保存后(或对已存配置)调 /account 验证")
48
+ .action(async (opts) => {
49
+ const cur = loadConfig();
50
+ let next = cur;
51
+ // 仅当 stdin 是 TTY 时走交互;非 TTY(如管道/CI)跳过提示,直接对已存配置做 --verify
52
+ if (process.stdin.isTTY) {
53
+ const rl = readline.createInterface({ input, output });
54
+ try {
55
+ const tokenInput = await rl.question(`API Token [${maskToken(cur.token)}]: `);
56
+ const baseUrlInput = await rl.question(`base URL [${cur.baseUrl}]: `);
57
+ next = {
58
+ token: tokenInput.trim() || cur.token,
59
+ baseUrl: baseUrlInput.trim() || cur.baseUrl,
60
+ };
61
+ if (next.token !== cur.token || next.baseUrl !== cur.baseUrl) {
62
+ saveConfig(next);
63
+ console.log("✓ 配置已保存到 ~/.ft/config.json");
64
+ }
65
+ else {
66
+ console.log("(配置未变更)");
67
+ }
68
+ }
69
+ finally {
70
+ rl.close();
71
+ }
72
+ }
73
+ if (opts.verify) {
74
+ await verifyAndPrint();
75
+ }
76
+ });
77
+ }
78
+ /** 调 /account 健康检查,成功打印账户信息,失败打印错误并 exit 1。 */
79
+ async function verifyAndPrint() {
80
+ try {
81
+ requireToken();
82
+ const data = await request("GET", "/api/open/v1/account");
83
+ console.log("✓ 验证通过");
84
+ console.log(JSON.stringify(data, null, 2));
85
+ }
86
+ catch (e) {
87
+ const msg = e instanceof Error ? e.message : String(e);
88
+ if (e instanceof ApiError) {
89
+ console.error(`✗ 验证失败 (code=${e.code}): ${msg}`);
90
+ }
91
+ else {
92
+ console.error(`✗ 验证失败: ${msg}`);
93
+ }
94
+ process.exit(1);
95
+ }
96
+ }
97
+ function maskToken(token) {
98
+ if (!token)
99
+ return "(未设置)";
100
+ return `${token.slice(0, 6)}***${token.slice(-4)}`;
101
+ }
102
+ // 兼容旧调用方:resolveBaseUrl 仍可从这里 re-export
103
+ export { resolveBaseUrl };
104
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/commands/config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,QAAQ,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,KAAK,IAAI,KAAK,EAAE,MAAM,IAAI,MAAM,EAAE,MAAM,cAAc,CAAC;AAChE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACpF,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAE9C,MAAM,UAAU,sBAAsB,CAAC,OAAgB;IACrD,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,WAAW,CAAC,0BAA0B,CAAC,CAAC;IAE9E,GAAG,CAAC,OAAO,CAAC,mBAAmB,CAAC;SAC7B,WAAW,CAAC,yCAAyC,CAAC;SACtD,MAAM,CAAC,CAAC,KAAa,EAAE,EAAE;QACxB,MAAM,GAAG,GAAG,UAAU,EAAE,CAAC;QACzB,UAAU,CAAC,EAAE,GAAG,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;QAC9B,OAAO,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAC;IAChD,CAAC,CAAC,CAAC;IAEL,GAAG,CAAC,OAAO,CAAC,oBAAoB,CAAC;SAC9B,WAAW,CAAC,gDAAgD,CAAC;SAC7D,MAAM,CAAC,CAAC,GAAW,EAAE,EAAE;QACtB,MAAM,GAAG,GAAG,UAAU,EAAE,CAAC;QACzB,UAAU,CAAC,EAAE,GAAG,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC;QACrC,OAAO,CAAC,GAAG,CAAC,kBAAkB,GAAG,EAAE,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;IAEL,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC;SAChB,WAAW,CAAC,oBAAoB,CAAC;SACjC,MAAM,CAAC,GAAG,EAAE;QACX,MAAM,GAAG,GAAG,UAAU,EAAE,CAAC;QACzB,MAAM,MAAM,GAAG,GAAG,CAAC,KAAK;YACtB,CAAC,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE;YACrD,CAAC,CAAC,OAAO,CAAC;QACZ,OAAO,CAAC,GAAG,CAAC,YAAY,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;QACvC,OAAO,CAAC,GAAG,CAAC,YAAY,MAAM,EAAE,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;IAEL,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC;SAClB,WAAW,CAAC,+BAA+B,CAAC;SAC5C,MAAM,CAAC,KAAK,IAAI,EAAE;QACjB,MAAM,cAAc,EAAE,CAAC;IACzB,CAAC,CAAC,CAAC;AACP,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB,CAAC,OAAgB;IACvD,OAAO;SACJ,OAAO,CAAC,WAAW,CAAC;SACpB,WAAW,CAAC,0CAA0C,CAAC;SACvD,MAAM,CAAC,UAAU,EAAE,0BAA0B,CAAC;SAC9C,MAAM,CAAC,KAAK,EAAE,IAA0B,EAAE,EAAE;QAC3C,MAAM,GAAG,GAAG,UAAU,EAAE,CAAC;QACzB,IAAI,IAAI,GAAG,GAAG,CAAC;QACf,0DAA0D;QAC1D,IAAI,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;YACxB,MAAM,EAAE,GAAG,QAAQ,CAAC,eAAe,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;YACvD,IAAI,CAAC;gBACH,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,cAAc,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;gBAC9E,MAAM,YAAY,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,aAAa,GAAG,CAAC,OAAO,KAAK,CAAC,CAAC;gBACtE,IAAI,GAAG;oBACL,KAAK,EAAE,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,CAAC,KAAK;oBACrC,OAAO,EAAE,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,CAAC,OAAO;iBAC5C,CAAC;gBACF,IAAI,IAAI,CAAC,KAAK,KAAK,GAAG,CAAC,KAAK,IAAI,IAAI,CAAC,OAAO,KAAK,GAAG,CAAC,OAAO,EAAE,CAAC;oBAC7D,UAAU,CAAC,IAAI,CAAC,CAAC;oBACjB,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC;gBAC5C,CAAC;qBAAM,CAAC;oBACN,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;gBACzB,CAAC;YACH,CAAC;oBAAS,CAAC;gBACT,EAAE,CAAC,KAAK,EAAE,CAAC;YACb,CAAC;QACH,CAAC;QACD,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,MAAM,cAAc,EAAE,CAAC;QACzB,CAAC;IACH,CAAC,CAAC,CAAC;AACP,CAAC;AAED,+CAA+C;AAC/C,KAAK,UAAU,cAAc;IAC3B,IAAI,CAAC;QACH,YAAY,EAAE,CAAC;QACf,MAAM,IAAI,GAAG,MAAM,OAAO,CAA0B,KAAK,EAAE,sBAAsB,CAAC,CAAC;QACnF,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACtB,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAC7C,CAAC;IAAC,OAAO,CAAU,EAAE,CAAC;QACpB,MAAM,GAAG,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACvD,IAAI,CAAC,YAAY,QAAQ,EAAE,CAAC;YAC1B,OAAO,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,IAAI,MAAM,GAAG,EAAE,CAAC,CAAC;QACnD,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,KAAK,CAAC,WAAW,GAAG,EAAE,CAAC,CAAC;QAClC,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAED,SAAS,SAAS,CAAC,KAAa;IAC9B,IAAI,CAAC,KAAK;QAAE,OAAO,OAAO,CAAC;IAC3B,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AACrD,CAAC;AAED,wCAAwC;AACxC,OAAO,EAAE,cAAc,EAAE,CAAC"}
@@ -0,0 +1,48 @@
1
+ import { request } from "../api.js";
2
+ function parseKV(list) {
3
+ const out = {};
4
+ for (const kv of list) {
5
+ const i = kv.indexOf("=");
6
+ if (i < 0)
7
+ continue;
8
+ const k = kv.slice(0, i).trim();
9
+ let v = kv.slice(i + 1).trim();
10
+ if (v === "true")
11
+ v = true;
12
+ else if (v === "false")
13
+ v = false;
14
+ else if (/^-?\d+(\.\d+)?$/.test(v))
15
+ v = Number(v);
16
+ else {
17
+ try {
18
+ v = JSON.parse(v);
19
+ }
20
+ catch {
21
+ // 保持字符串
22
+ }
23
+ }
24
+ out[k] = v;
25
+ }
26
+ return out;
27
+ }
28
+ export function registerDrawCommands(program) {
29
+ program
30
+ .command("draw <apiKey>")
31
+ .description("提交出图任务。apiKey 见 ft models draw;模型参数用 key=value 形式传入")
32
+ .option("-p, --param <kv...>", "模型参数,可重复(如 -p prompt=cat -p image=http://...)")
33
+ .option("--output-tier <tier>", "出图档位(fast/plus/ultra/max,可选)")
34
+ .option("--callback-url <url>", "回调 URL(可选,需同时提供 --callback-secret)")
35
+ .option("--callback-secret <secret>", "回调密钥(16~256 字符)")
36
+ .action(async (apiKey, opts) => {
37
+ const body = { ...parseKV(opts.param || []) };
38
+ if (opts.outputTier)
39
+ body.output_tier = opts.outputTier;
40
+ if (opts.callbackUrl)
41
+ body.callback_url = opts.callbackUrl;
42
+ if (opts.callbackSecret)
43
+ body.callback_secret = opts.callbackSecret;
44
+ const data = await request("POST", `/api/open/v1/tasks/${encodeURIComponent(apiKey)}`, { body });
45
+ console.log(JSON.stringify(data, null, 2));
46
+ });
47
+ }
48
+ //# sourceMappingURL=draw.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"draw.js","sourceRoot":"","sources":["../../src/commands/draw.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAOpC,SAAS,OAAO,CAAC,IAAc;IAC7B,MAAM,GAAG,GAA4B,EAAE,CAAC;IACxC,KAAK,MAAM,EAAE,IAAI,IAAI,EAAE,CAAC;QACtB,MAAM,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC1B,IAAI,CAAC,GAAG,CAAC;YAAE,SAAS;QACpB,MAAM,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAChC,IAAI,CAAC,GAAY,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACxC,IAAI,CAAC,KAAK,MAAM;YAAE,CAAC,GAAG,IAAI,CAAC;aACtB,IAAI,CAAC,KAAK,OAAO;YAAE,CAAC,GAAG,KAAK,CAAC;aAC7B,IAAI,iBAAiB,CAAC,IAAI,CAAC,CAAW,CAAC;YAAE,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;aACvD,CAAC;YACJ,IAAI,CAAC;gBACH,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAW,CAAC,CAAC;YAC9B,CAAC;YAAC,MAAM,CAAC;gBACP,QAAQ;YACV,CAAC;QACH,CAAC;QACD,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACb,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,OAAgB;IACnD,OAAO;SACJ,OAAO,CAAC,eAAe,CAAC;SACxB,WAAW,CAAC,qDAAqD,CAAC;SAClE,MAAM,CAAC,qBAAqB,EAAE,+CAA+C,CAAC;SAC9E,MAAM,CAAC,sBAAsB,EAAE,8BAA8B,CAAC;SAC9D,MAAM,CAAC,sBAAsB,EAAE,oCAAoC,CAAC;SACpE,MAAM,CAAC,4BAA4B,EAAE,iBAAiB,CAAC;SACvD,MAAM,CACL,KAAK,EACH,MAAc,EACd,IAKC,EACD,EAAE;QACF,MAAM,IAAI,GAA4B,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,EAAE,CAAC;QACvE,IAAI,IAAI,CAAC,UAAU;YAAE,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,UAAU,CAAC;QACxD,IAAI,IAAI,CAAC,WAAW;YAAE,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,WAAW,CAAC;QAC3D,IAAI,IAAI,CAAC,cAAc;YAAE,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,cAAc,CAAC;QACpE,MAAM,IAAI,GAAG,MAAM,OAAO,CACxB,MAAM,EACN,sBAAsB,kBAAkB,CAAC,MAAM,CAAC,EAAE,EAClD,EAAE,IAAI,EAAE,CACT,CAAC;QACF,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAC7C,CAAC,CACF,CAAC;AACN,CAAC"}
@@ -0,0 +1,62 @@
1
+ import { request } from "../api.js";
2
+ /**
3
+ * 与 OpenApiController.ALLOWED_OPEN_API_KEYS 保持一致。
4
+ * 后端白名单变化时这里也要同步——它是「入口白名单」,前端手册 docsModel.ts
5
+ * 的 PUBLIC_API_KEYS 是「手册白名单」,二者口径不同,本表只覆盖允许直接点名的 key。
6
+ */
7
+ const DRAW_MODELS = [
8
+ { key: "matting", desc: "抠图" },
9
+ { key: "extract", desc: "图案提取(主档)" },
10
+ { key: "extract-fast", desc: "图案提取(快速档)" },
11
+ { key: "extract-ultra", desc: "图案提取(超清档)" },
12
+ { key: "image-edit", desc: "图像编辑(主档)" },
13
+ { key: "image-edit-fast", desc: "图像编辑(快速档)" },
14
+ { key: "image-edit-ultra", desc: "图像编辑(超清档)" },
15
+ { key: "image-edit-2.5", desc: "图像编辑 2.5(主档)" },
16
+ { key: "image-edit-2.5-fast", desc: "图像编辑 2.5(快速档)" },
17
+ { key: "image-edit-2.5-plus", desc: "图像编辑 2.5(增强档)" },
18
+ { key: "image-edit-2.5-ultra", desc: "图像编辑 2.5(超清档)" },
19
+ { key: "image-edit-2.5-max", desc: "图像编辑 2.5(顶配档)" },
20
+ { key: "seamless-tile", desc: "无缝拼贴" },
21
+ { key: "seamless-tile-fast", desc: "无缝拼贴(快速档)" },
22
+ { key: "pattern-extract-rh", desc: "图案提取 RH" },
23
+ { key: "upscale", desc: "超分辨率放大" },
24
+ { key: "image-expand", desc: "扩图" },
25
+ { key: "nano banana 2", desc: "Nano Banana 2" },
26
+ { key: "nano banana pro", desc: "Nano Banana Pro" },
27
+ { key: "nano-banana-lite", desc: "Nano Banana Lite" },
28
+ { key: "turbo", desc: "Turbo" },
29
+ { key: "cut-pieces", desc: "裁片提取(按片计费)" },
30
+ ];
31
+ export function registerModelsCommands(program) {
32
+ const m = program.command("models").description("查询模型列表");
33
+ // 不带子命令时直接显示 help:commander 默认对"有子命令但未命中"的调用静默退出,
34
+ // 用户会以为命令坏了。显式给出子命令引导。
35
+ m.action(() => {
36
+ console.log("用法: ft models draw 列出可调用的出图模型 api_key");
37
+ console.log(" ft models llm 列出可用的 LLM 模型\n");
38
+ m.help({ error: false });
39
+ });
40
+ m.command("draw")
41
+ .description("列出可调用的出图模型 api_key(作为 ft draw <apiKey> 的参数)")
42
+ .action(() => {
43
+ console.log("出图模型 api_key(用于 ft draw <apiKey>):\n");
44
+ for (const it of DRAW_MODELS) {
45
+ console.log(` ${it.key.padEnd(22)} ${it.desc}`);
46
+ }
47
+ });
48
+ m.command("llm")
49
+ .description("查询 LLM 模型(GET /api/open/v1/models)")
50
+ .action(async () => {
51
+ const data = await request("GET", "/api/open/v1/models");
52
+ if (!data?.data?.length) {
53
+ console.log("(无可用 LLM 模型)");
54
+ return;
55
+ }
56
+ console.log("LLM 模型:\n");
57
+ for (const it of data.data) {
58
+ console.log(` ${it.id}`);
59
+ }
60
+ });
61
+ }
62
+ //# sourceMappingURL=models.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"models.js","sourceRoot":"","sources":["../../src/commands/models.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import { request } from "../api.js";
2
+ export function registerTaskCommands(program) {
3
+ program
4
+ .command("task <id>")
5
+ .description("按任务 ID 查询状态与结果(GET /api/open/v1/tasks/{id})")
6
+ .action(async (id) => {
7
+ const data = await request("GET", `/api/open/v1/tasks/${encodeURIComponent(id)}`);
8
+ console.log(JSON.stringify(data, null, 2));
9
+ });
10
+ }
11
+ //# sourceMappingURL=task.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task.js","sourceRoot":"","sources":["../../src/commands/task.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAUpC,MAAM,UAAU,oBAAoB,CAAC,OAAgB;IACnD,OAAO;SACJ,OAAO,CAAC,WAAW,CAAC;SACpB,WAAW,CAAC,6CAA6C,CAAC;SAC1D,MAAM,CAAC,KAAK,EAAE,EAAU,EAAE,EAAE;QAC3B,MAAM,IAAI,GAAG,MAAM,OAAO,CACxB,KAAK,EACL,sBAAsB,kBAAkB,CAAC,EAAE,CAAC,EAAE,CAC/C,CAAC;QACF,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;AACP,CAAC"}
@@ -0,0 +1,11 @@
1
+ import { uploadFile } from "../api.js";
2
+ export function registerUploadCommands(program) {
3
+ program
4
+ .command("upload <file>")
5
+ .description("上传本地图片到开放 API,返回可直接填入出图参数的 URL")
6
+ .action(async (file) => {
7
+ const url = await uploadFile(file);
8
+ console.log(url);
9
+ });
10
+ }
11
+ //# sourceMappingURL=upload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upload.js","sourceRoot":"","sources":["../../src/commands/upload.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC,MAAM,UAAU,sBAAsB,CAAC,OAAgB;IACrD,OAAO;SACJ,OAAO,CAAC,eAAe,CAAC;SACxB,WAAW,CAAC,gCAAgC,CAAC;SAC7C,MAAM,CAAC,KAAK,EAAE,IAAY,EAAE,EAAE;QAC7B,MAAM,GAAG,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC,CAAC;QACnC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACnB,CAAC,CAAC,CAAC;AACP,CAAC"}
package/dist/config.js ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * 本地配置读写:base_url + token,落在 ~/.ft/config.json。
3
+ *
4
+ * 文件权限 0o600 — token 是凭据,不该让本机其他用户读到。
5
+ *
6
+ * 环境变量优先级高于配置文件,方便 CI/容器场景一次性注入:
7
+ * FT_API_TOKEN — 覆盖 config.token
8
+ * FT_API_BASE_URL — 覆盖 config.baseUrl(与 ft-mcp 同名环境变量对齐)
9
+ */
10
+ import { homedir } from "node:os";
11
+ import { join } from "node:path";
12
+ import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
13
+ const CONFIG_DIR = join(homedir(), ".ft");
14
+ const CONFIG_FILE = join(CONFIG_DIR, "config.json");
15
+ /** 与 Nginx conf.d/fastthree.conf 里的对外 API 域名一致 */
16
+ const DEFAULT_BASE_URL = "https://ai-api.fastthree.com";
17
+ export function loadConfig() {
18
+ // 环境变量优先于配置文件
19
+ const envToken = process.env.FT_API_TOKEN;
20
+ const envBaseUrl = process.env.FT_API_BASE_URL;
21
+ if (!existsSync(CONFIG_FILE)) {
22
+ return {
23
+ baseUrl: envBaseUrl || DEFAULT_BASE_URL,
24
+ token: envToken || "",
25
+ };
26
+ }
27
+ try {
28
+ const raw = readFileSync(CONFIG_FILE, "utf8");
29
+ const parsed = JSON.parse(raw);
30
+ return {
31
+ baseUrl: envBaseUrl || parsed.baseUrl || DEFAULT_BASE_URL,
32
+ token: envToken || parsed.token || "",
33
+ };
34
+ }
35
+ catch {
36
+ return {
37
+ baseUrl: envBaseUrl || DEFAULT_BASE_URL,
38
+ token: envToken || "",
39
+ };
40
+ }
41
+ }
42
+ export function saveConfig(cfg) {
43
+ mkdirSync(CONFIG_DIR, { recursive: true });
44
+ writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), { mode: 0o600 });
45
+ }
46
+ /** 调用任何需要鉴权的端点前都先取 token;没配就报错并指引用户去 set-token */
47
+ export function requireToken() {
48
+ const { token } = loadConfig();
49
+ if (!token) {
50
+ throw new Error("未配置 API Token。可通过以下方式之一配置:\n" +
51
+ " 1. 环境变量: export FT_API_TOKEN=<token>\n" +
52
+ " 2. 配置文件: ft config set-token <token>");
53
+ }
54
+ return token;
55
+ }
56
+ export function resolveBaseUrl(override) {
57
+ const envBaseUrl = process.env.FT_API_BASE_URL;
58
+ const { baseUrl } = loadConfig();
59
+ return (override || envBaseUrl || baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
60
+ }
61
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAO7E,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,KAAK,CAAC,CAAC;AAC1C,MAAM,WAAW,GAAG,IAAI,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;AAEpD,kDAAkD;AAClD,MAAM,gBAAgB,GAAG,8BAA8B,CAAC;AAExD,MAAM,UAAU,UAAU;IACxB,cAAc;IACd,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC;IAC1C,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC;IAC/C,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QAC7B,OAAO;YACL,OAAO,EAAE,UAAU,IAAI,gBAAgB;YACvC,KAAK,EAAE,QAAQ,IAAI,EAAE;SACtB,CAAC;IACJ,CAAC;IACD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;QAC9C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAsB,CAAC;QACpD,OAAO;YACL,OAAO,EAAE,UAAU,IAAI,MAAM,CAAC,OAAO,IAAI,gBAAgB;YACzD,KAAK,EAAE,QAAQ,IAAI,MAAM,CAAC,KAAK,IAAI,EAAE;SACtC,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO;YACL,OAAO,EAAE,UAAU,IAAI,gBAAgB;YACvC,KAAK,EAAE,QAAQ,IAAI,EAAE;SACtB,CAAC;IACJ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,GAAa;IACtC,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,aAAa,CAAC,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;AAC5E,CAAC;AAED,kDAAkD;AAClD,MAAM,UAAU,YAAY;IAC1B,MAAM,EAAE,KAAK,EAAE,GAAG,UAAU,EAAE,CAAC;IAC/B,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,8BAA8B;YAC5B,0CAA0C;YAC1C,wCAAwC,CAC3C,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,QAAiB;IAC9C,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC;IAC/C,MAAM,EAAE,OAAO,EAAE,GAAG,UAAU,EAAE,CAAC;IACjC,OAAO,CAAC,QAAQ,IAAI,UAAU,IAAI,OAAO,IAAI,gBAAgB,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AACpF,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ft — FastThree 命令行工具入口
4
+ *
5
+ * 封装 /api/open/v1 开放 REST API:配置 token、查询账户、列模型、
6
+ * 上传图片、提交出图任务、查询任务、与 LLM 聊天(流式)。
7
+ */
8
+ import { program } from "commander";
9
+ import { registerConfigCommands, registerConfigureCommand } from "./commands/config.js";
10
+ import { registerAccountCommands } from "./commands/account.js";
11
+ import { registerModelsCommands } from "./commands/models.js";
12
+ import { registerUploadCommands } from "./commands/upload.js";
13
+ import { registerDrawCommands } from "./commands/draw.js";
14
+ import { registerTaskCommands } from "./commands/task.js";
15
+ import { registerChatCommands } from "./commands/chat.js";
16
+ program
17
+ .name("ft")
18
+ .description("FastThree 命令行工具 — 封装 /api/open/v1 开放 REST API")
19
+ .version("0.2.0");
20
+ registerConfigureCommand(program);
21
+ registerConfigCommands(program);
22
+ registerAccountCommands(program);
23
+ registerModelsCommands(program);
24
+ registerUploadCommands(program);
25
+ registerDrawCommands(program);
26
+ registerTaskCommands(program);
27
+ registerChatCommands(program);
28
+ program.parseAsync(process.argv).catch((err) => {
29
+ const msg = err instanceof Error ? err.message : String(err);
30
+ console.error(msg);
31
+ process.exit(1);
32
+ });
33
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;GAKG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,sBAAsB,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AACxF,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE1D,OAAO;KACJ,IAAI,CAAC,IAAI,CAAC;KACV,WAAW,CAAC,+CAA+C,CAAC;KAC5D,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpB,wBAAwB,CAAC,OAAO,CAAC,CAAC;AAClC,sBAAsB,CAAC,OAAO,CAAC,CAAC;AAChC,uBAAuB,CAAC,OAAO,CAAC,CAAC;AACjC,sBAAsB,CAAC,OAAO,CAAC,CAAC;AAChC,sBAAsB,CAAC,OAAO,CAAC,CAAC;AAChC,oBAAoB,CAAC,OAAO,CAAC,CAAC;AAC9B,oBAAoB,CAAC,OAAO,CAAC,CAAC;AAC9B,oBAAoB,CAAC,OAAO,CAAC,CAAC;AAE9B,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IACtD,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC7D,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACnB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,36 @@
1
- {
2
- "name": "@fastthree-ai/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "@fastthree-ai/cli",
3
+ "version": "0.2.0",
4
+ "description": "FastThree 命令行工具,封装 /api/open/v1 开放 REST API",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "ft": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsx watch src/index.ts",
13
+ "start": "node dist/index.js",
14
+ "clean": "rimraf dist",
15
+ "prepublishOnly": "npm run clean && npm run build"
16
+ },
17
+ "engines": {
18
+ "node": ">=18"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "README.md"
23
+ ],
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "dependencies": {
28
+ "commander": "^12.1.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^20.11.0",
32
+ "rimraf": "^5.0.5",
33
+ "tsx": "^4.7.0",
34
+ "typescript": "^5.3.0"
35
+ }
36
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.