@flowweave-ai/cli 0.1.3 → 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/README.md +9 -4
- package/bin/flowweave.mjs +181 -12
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -5,20 +5,25 @@
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install -g @flowweave-ai/cli
|
|
7
7
|
flowweave config init --base-url https://host.example/flowweave
|
|
8
|
+
flowweave auth login
|
|
8
9
|
flowweave health --ready
|
|
9
10
|
```
|
|
10
11
|
|
|
11
|
-
配置只保存基础 URL 到 `~/.config/flowweave/config.json`(可由 `FLOWWEAVE_CONFIG_PATH`
|
|
12
|
+
配置只保存基础 URL 到 `~/.config/flowweave/config.json`(可由 `FLOWWEAVE_CONFIG_PATH` 覆盖)。登录会话单独保存到同目录的 `auth.json`(可由 `FLOWWEAVE_AUTH_PATH` 覆盖),文件权限为 `0600`,并且只会发送给登录时的同一平台地址。使用 `flowweave auth status` 检查当前身份,使用 `flowweave auth logout` 撤销服务端会话并删除本地文件。默认登录会隐藏密码输入;非交互环境使用 `--password-stdin`,不要把密码写进命令参数、脚本或日志。
|
|
12
13
|
|
|
13
|
-
页面域命令包括 `node`、`node-directory`、`capability`、`environment`、`credential`、`flow`、`run`、`model` 和 `agent
|
|
14
|
+
页面域命令包括 `node`、`node-directory`、`capability`、`environment`、`credential`、`flow`、`run`、`schedule`、`model` 和 `agent`。它们分别覆盖节点资产、能力仓库、终端环境、网站认证条目、流程编排、FlowRun、周期调度、大模型配置与 Agent 工作台的常用原子操作。`credential` 管理网站凭据,不等于 `auth` 用户登录。每个命令的 JSON 请求体与在线 OpenAPI 一致;运行 `flowweave <域> --help` 查看映射。
|
|
14
15
|
|
|
15
16
|
常用命令示例:
|
|
16
17
|
|
|
17
|
-
`flowweave node-directory delete-many --id <directory-id> --id <directory-id> --dry-run`、`flowweave credential delete-many --id <credential-id> --id <credential-id>`、`flowweave environment publish <setup-session-id> --description '升级 Python 依赖'`、`flowweave agent file-delete <workspace-id> --path
|
|
18
|
+
`flowweave node-directory delete-many --id <directory-id> --id <directory-id> --dry-run`、`flowweave credential delete-many --id <credential-id> --id <credential-id>`、`flowweave environment publish <setup-session-id> --description '升级 Python 依赖'`、`flowweave agent file-delete <workspace-id> --path <workspace-api-returned-path>`、`flowweave run workspace-delete <run-id> --attempt <attempt-id> --path <attempt-workspace-api-returned-path>`。
|
|
18
19
|
|
|
19
20
|
节点目录批量删除、Agent Workspace 文件树删除和 FlowRun 节点工作区删除都使用 JSON 数组请求体。重复传入 `--id` 或 `--path` 即可批量选择;先读取真实资源与路径,并用 `--dry-run` 核对 DELETE URL、范围 query 和请求体。FlowRun 工作目录删除使用 `flowweave run work-directory-delete <run-id> --attempt <attempt-id> --work-directory <directory-id>`。
|
|
20
21
|
|
|
21
|
-
`
|
|
22
|
+
读取 FlowRun 中的记录使用 `flowweave run node <run-id> --node <node-run-id>`;只有用户明确要求时才能执行 `node-copy` 或 `node-delete`。暂停或恢复 Runtime 前,必须先读取 `run runtime`,将返回的 `generation` 与 session `row_version` 写入 `expected_generation`、`expected_session_row_version` 后传给 `run pause` 或 `run resume`。供应商上游余额/用量使用 `flowweave model usage <provider-id>`,它可能依赖该供应商的有效 API 凭据。
|
|
23
|
+
|
|
24
|
+
周期任务使用 `schedule list/create/pause/resume/trigger/delete`。创建请求必须使用在线 `FlowRunScheduleWrite` schema;暂停或恢复前从 `schedule list` 读取当前 `row_version`,再传入 `--expected-row-version`。手动触发会新增一次 occurrence,不会改写既有运行;删除已有执行记录的调度会被平台拒绝。
|
|
25
|
+
|
|
26
|
+
`api`、`upload`、`ws` 是完整契约入口:任意当前或未来的 REST、multipart、WebSocket 原子接口均可直接调用,不需要等待 CLI 发布。三者都会使用当前 `auth login` 会话;不得用 `--header` 手工传 Cookie。写操作支持 `--dry-run`,并可使用 `-H 'Idempotency-Key: …'` 传入一次性幂等键。对于没有快捷命令的新接口,先运行 `flowweave openapi --paths`,再通过通用命令调用。
|
|
22
27
|
|
|
23
28
|
安装页面域 skill:
|
|
24
29
|
|
package/bin/flowweave.mjs
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { basename, dirname, resolve } from 'node:path';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
|
+
import { createInterface } from 'node:readline/promises';
|
|
7
|
+
import WebSocket from 'ws';
|
|
6
8
|
|
|
7
9
|
const API_PREFIX = '/api/v1';
|
|
8
10
|
const CONFIG_PATH = process.env.FLOWWEAVE_CONFIG_PATH
|
|
9
11
|
|| `${process.env.XDG_CONFIG_HOME || `${homedir()}/.config`}/flowweave/config.json`;
|
|
12
|
+
const AUTH_PATH = process.env.FLOWWEAVE_AUTH_PATH || `${dirname(CONFIG_PATH)}/auth.json`;
|
|
13
|
+
const SESSION_COOKIE = 'flowweave_session';
|
|
10
14
|
|
|
11
15
|
class CliError extends Error {}
|
|
12
16
|
|
|
@@ -14,8 +18,9 @@ function usage() {
|
|
|
14
18
|
return `用法:flowweave <命令> [选项]
|
|
15
19
|
|
|
16
20
|
配置与发现:
|
|
17
|
-
config init --base-url <URL> [--force] 设置平台基础 URL
|
|
21
|
+
config init --base-url <URL> [--force] 设置平台基础 URL
|
|
18
22
|
config show 显示当前配置
|
|
23
|
+
auth <login|status|logout> 登录、检查或退出平台用户会话
|
|
19
24
|
health [--ready] 健康检查
|
|
20
25
|
openapi [--paths] 查看在线 OpenAPI 契约
|
|
21
26
|
|
|
@@ -31,8 +36,9 @@ function usage() {
|
|
|
31
36
|
environment <list|get|create|update|delete|setup|publish|stop|version-delete> ...
|
|
32
37
|
credential <list|create|update|delete|delete-many> ...
|
|
33
38
|
flow <list|get|create|update|validate|delete> ...
|
|
34
|
-
run <list|get|start|delete|runtime|replace|cancel|complete|events|workspace-delete|work-directory-delete> ...
|
|
35
|
-
|
|
39
|
+
run <list|get|start|delete|runtime|replace|pause|resume|cancel|complete|events|node|node-copy|node-delete|workspace-delete|work-directory-delete> ...
|
|
40
|
+
schedule <list|create|pause|resume|trigger|delete> ...
|
|
41
|
+
model <list|create|update|delete|discover|usage|test|oauth-start|oauth-poll|oauth-status|oauth-revoke> ...
|
|
36
42
|
agent <default|workspace|runtime|conversations|conversation|create|send|interrupt|resume|work-directories|work-directory-create|work-directory-delete|file-delete> ...
|
|
37
43
|
|
|
38
44
|
所有写入操作都可加 --dry-run 仅查看最终请求。运行 flowweave <命令> --help 查看该命令说明。`;
|
|
@@ -62,7 +68,7 @@ function positional(args) {
|
|
|
62
68
|
const values = [];
|
|
63
69
|
for (let index = 0; index < args.length; index += 1) {
|
|
64
70
|
if (args[index].startsWith('--') || args[index] === '-H' || args[index] === '-q') {
|
|
65
|
-
if (!['--dry-run', '--raw', '--ready', '--paths', '--force'].includes(args[index])) index += 1;
|
|
71
|
+
if (!['--dry-run', '--raw', '--ready', '--paths', '--force', '--password-stdin'].includes(args[index])) index += 1;
|
|
66
72
|
} else {
|
|
67
73
|
values.push(args[index]);
|
|
68
74
|
}
|
|
@@ -97,6 +103,35 @@ async function saveConfig(baseUrl, force) {
|
|
|
97
103
|
return value;
|
|
98
104
|
}
|
|
99
105
|
|
|
106
|
+
async function loadAuth(baseUrl) {
|
|
107
|
+
let data;
|
|
108
|
+
try {
|
|
109
|
+
data = JSON.parse(await readFile(AUTH_PATH, 'utf8'));
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (error?.code === 'ENOENT') {
|
|
112
|
+
throw new CliError(`尚未登录 FlowWeave。请执行:flowweave auth login(${AUTH_PATH})`);
|
|
113
|
+
}
|
|
114
|
+
throw new CliError(`无法读取 FlowWeave 登录会话 ${AUTH_PATH}:${error.message}`);
|
|
115
|
+
}
|
|
116
|
+
if (typeof data?.base_url !== 'string' || typeof data?.session_token !== 'string' || !data.session_token) {
|
|
117
|
+
throw new CliError(`登录会话 ${AUTH_PATH} 格式无效;请重新执行 flowweave auth login`);
|
|
118
|
+
}
|
|
119
|
+
if (normalizeBaseUrl(data.base_url) !== baseUrl) {
|
|
120
|
+
throw new CliError(`登录会话属于其他 FlowWeave 地址;请对 ${baseUrl} 重新执行 flowweave auth login`);
|
|
121
|
+
}
|
|
122
|
+
return data.session_token;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function saveAuth(baseUrl, sessionToken) {
|
|
126
|
+
await mkdir(dirname(AUTH_PATH), { recursive: true, mode: 0o700 });
|
|
127
|
+
await writeFile(AUTH_PATH, `${JSON.stringify({ base_url: baseUrl, session_token: sessionToken }, null, 2)}\n`, { mode: 0o600 });
|
|
128
|
+
await chmod(AUTH_PATH, 0o600);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function clearAuth() {
|
|
132
|
+
await rm(AUTH_PATH, { force: true });
|
|
133
|
+
}
|
|
134
|
+
|
|
100
135
|
function parseJson(value, source) {
|
|
101
136
|
try { return JSON.parse(value); } catch (error) { throw new CliError(`${source} 必须是合法 JSON:${error.message}`); }
|
|
102
137
|
}
|
|
@@ -131,12 +166,30 @@ function pathUrl(baseUrl, path, { raw = false, query = [] } = {}) {
|
|
|
131
166
|
function headers(args) { return Object.fromEntries(pairs(optionValues(args, '-H').concat(optionValues(args, '--header')), ':', '--header').map(([name, value]) => [name.trim(), value.trim()])); }
|
|
132
167
|
function queries(args) { return pairs(optionValues(args, '-q').concat(optionValues(args, '--query')), '=', '--query'); }
|
|
133
168
|
|
|
134
|
-
|
|
169
|
+
function sessionTokenFrom(response) {
|
|
170
|
+
const values = typeof response.headers.getSetCookie === 'function'
|
|
171
|
+
? response.headers.getSetCookie()
|
|
172
|
+
: [response.headers.get('set-cookie')].filter(Boolean);
|
|
173
|
+
for (const value of values) {
|
|
174
|
+
const match = new RegExp(`(?:^|;\\s*)${SESSION_COOKIE}=([^;]+)`).exec(value);
|
|
175
|
+
if (match) return match[1];
|
|
176
|
+
}
|
|
177
|
+
throw new CliError('登录响应没有返回 FlowWeave 会话 Cookie');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function request(method, path, args, {
|
|
181
|
+
raw = false, body, form, query = [], authenticated = true, captureSession = false,
|
|
182
|
+
} = {}) {
|
|
135
183
|
const { baseUrl } = await loadConfig();
|
|
136
184
|
const url = pathUrl(baseUrl, path, { raw, query: [...queries(args), ...query] });
|
|
137
185
|
const requestBody = body === undefined ? await payload(args) : body;
|
|
138
186
|
if (flag(args, '--dry-run')) return { method, url: url.toString(), payload: requestBody ?? form ?? null };
|
|
139
|
-
const
|
|
187
|
+
const suppliedHeaders = headers(args);
|
|
188
|
+
if (Object.keys(suppliedHeaders).some((name) => name.toLowerCase() === 'cookie')) {
|
|
189
|
+
throw new CliError('不要用 --header 传入 Cookie;请执行 flowweave auth login');
|
|
190
|
+
}
|
|
191
|
+
const requestHeaders = { Accept: 'application/json', ...suppliedHeaders };
|
|
192
|
+
if (authenticated) requestHeaders.Cookie = `${SESSION_COOKIE}=${await loadAuth(baseUrl)}`;
|
|
140
193
|
let encoded;
|
|
141
194
|
if (form) { encoded = form; } else if (requestBody !== undefined) { encoded = JSON.stringify(requestBody); requestHeaders['Content-Type'] ||= 'application/json'; }
|
|
142
195
|
let response;
|
|
@@ -145,7 +198,79 @@ async function request(method, path, args, { raw = false, body, form, query = []
|
|
|
145
198
|
let value;
|
|
146
199
|
try { value = text ? JSON.parse(text) : { status: response.status }; } catch { value = { status: response.status, body: text }; }
|
|
147
200
|
if (!response.ok) throw new CliError(`HTTP ${response.status}: ${JSON.stringify(value)}`);
|
|
148
|
-
return value;
|
|
201
|
+
return captureSession ? { value, sessionToken: sessionTokenFrom(response), baseUrl } : value;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function promptText(label) {
|
|
205
|
+
if (!process.stdin.isTTY) throw new CliError(`${label.replace(/[::]\s*$/, '')} 缺少值`);
|
|
206
|
+
const reader = createInterface({ input: process.stdin, output: process.stderr });
|
|
207
|
+
try { return (await reader.question(label)).trim(); } finally { reader.close(); }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function promptSecret(label) {
|
|
211
|
+
if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== 'function') {
|
|
212
|
+
throw new CliError('非交互环境请使用 --password-stdin 从标准输入传入密码');
|
|
213
|
+
}
|
|
214
|
+
process.stderr.write(label);
|
|
215
|
+
const wasRaw = process.stdin.isRaw;
|
|
216
|
+
process.stdin.setRawMode(true);
|
|
217
|
+
process.stdin.resume();
|
|
218
|
+
process.stdin.setEncoding('utf8');
|
|
219
|
+
return new Promise((resolveSecret, reject) => {
|
|
220
|
+
let value = '';
|
|
221
|
+
const finish = (error) => {
|
|
222
|
+
process.stdin.off('data', onData);
|
|
223
|
+
process.stdin.setRawMode(Boolean(wasRaw));
|
|
224
|
+
process.stdin.pause();
|
|
225
|
+
process.stderr.write('\n');
|
|
226
|
+
if (error) reject(error); else resolveSecret(value);
|
|
227
|
+
};
|
|
228
|
+
const onData = (chunk) => {
|
|
229
|
+
for (const character of chunk) {
|
|
230
|
+
if (character === '\u0003') return finish(new CliError('已取消登录'));
|
|
231
|
+
if (character === '\r' || character === '\n') return finish();
|
|
232
|
+
if (character === '\u007f' || character === '\b') {
|
|
233
|
+
if (value) { value = value.slice(0, -1); process.stderr.write('\b \b'); }
|
|
234
|
+
} else {
|
|
235
|
+
value += character;
|
|
236
|
+
process.stderr.write('•');
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
process.stdin.on('data', onData);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function passwordFrom(args) {
|
|
245
|
+
if (!flag(args, '--password-stdin')) return promptSecret('密码:');
|
|
246
|
+
let value = '';
|
|
247
|
+
for await (const chunk of process.stdin) value += String(chunk);
|
|
248
|
+
return value.replace(/\r?\n$/, '');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function auth(args) {
|
|
252
|
+
const [action] = positional(args);
|
|
253
|
+
if (action === 'login') {
|
|
254
|
+
if (flag(args, '--dry-run')) throw new CliError('auth login 不支持 --dry-run,避免密码进入输出');
|
|
255
|
+
const username = (option(args, '--username') || await promptText('用户名:')).trim();
|
|
256
|
+
const password = await passwordFrom(args);
|
|
257
|
+
if (!username || !password) throw new CliError('用户名和密码不能为空');
|
|
258
|
+
const result = await request('POST', '/auth/login', args, {
|
|
259
|
+
body: { username, password }, authenticated: false, captureSession: true,
|
|
260
|
+
});
|
|
261
|
+
await saveAuth(result.baseUrl, result.sessionToken);
|
|
262
|
+
return { authenticated: true, auth_path: AUTH_PATH, user: result.value };
|
|
263
|
+
}
|
|
264
|
+
if (action === 'status') {
|
|
265
|
+
const user = await request('GET', '/auth/me', args);
|
|
266
|
+
return { authenticated: true, auth_path: AUTH_PATH, user };
|
|
267
|
+
}
|
|
268
|
+
if (action === 'logout') {
|
|
269
|
+
await request('POST', '/auth/logout', args, { body: {} });
|
|
270
|
+
await clearAuth();
|
|
271
|
+
return { authenticated: false, auth_path: AUTH_PATH };
|
|
272
|
+
}
|
|
273
|
+
throw new CliError('auth 支持 login|status|logout');
|
|
149
274
|
}
|
|
150
275
|
|
|
151
276
|
async function objectPayload(args, defaults = {}) {
|
|
@@ -283,6 +408,14 @@ async function run(args) {
|
|
|
283
408
|
return request('POST', `/flows/${flowId}/runs`, args, { body });
|
|
284
409
|
}
|
|
285
410
|
if (!id) throw new CliError(`run ${action || ''} 需要 FlowRun ID`);
|
|
411
|
+
if (action === 'node' || action === 'node-copy' || action === 'node-delete') {
|
|
412
|
+
const nodeRunId = option(args, '--node');
|
|
413
|
+
if (!nodeRunId) throw new CliError(`run ${action} 需要 --node <node-run-id>`);
|
|
414
|
+
const path = `/flow-runs/${id}/nodes/${nodeRunId}`;
|
|
415
|
+
if (action === 'node') return request('GET', path, args);
|
|
416
|
+
if (action === 'node-copy') return request('POST', `${path}/copy`, args, { body: await objectPayload(args) });
|
|
417
|
+
return request('DELETE', path, args);
|
|
418
|
+
}
|
|
286
419
|
if (action === 'workspace-delete') {
|
|
287
420
|
const attemptId = option(args, '--attempt');
|
|
288
421
|
const paths = optionValues(args, '--path');
|
|
@@ -311,10 +444,42 @@ async function run(args) {
|
|
|
311
444
|
if (action === 'delete') return request('DELETE', `/flow-runs/${id}`, args);
|
|
312
445
|
if (action === 'runtime') return request('GET', `/flow-runs/${id}/runtime`, args);
|
|
313
446
|
if (action === 'replace') return request('POST', `/flow-runs/${id}/runtime/replacements`, args);
|
|
447
|
+
if (action === 'pause' || action === 'resume') {
|
|
448
|
+
const body = await objectPayload(args);
|
|
449
|
+
if (!Number.isInteger(body.expected_generation) || !Number.isInteger(body.expected_session_row_version)) {
|
|
450
|
+
throw new CliError(`run ${action} 需要 --data 或 --data-file,且必须包含整数 expected_generation 和 expected_session_row_version`);
|
|
451
|
+
}
|
|
452
|
+
return request('POST', `/flow-runs/${id}/runtime/${action}`, args, { body });
|
|
453
|
+
}
|
|
314
454
|
if (action === 'cancel') return request('POST', `/flow-runs/${id}/cancel`, args, { body: {} });
|
|
315
455
|
if (action === 'complete') return request('POST', `/flow-runs/${id}/complete`, args, { body: {} });
|
|
316
456
|
if (action === 'events') return request('GET', `/flow-runs/${id}/events`, args);
|
|
317
|
-
throw new CliError('run 支持 list|get|start|delete|runtime|replace|cancel|complete|events|workspace-delete|work-directory-delete');
|
|
457
|
+
throw new CliError('run 支持 list|get|start|delete|runtime|replace|pause|resume|cancel|complete|events|node|node-copy|node-delete|workspace-delete|work-directory-delete');
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function schedule(args) {
|
|
461
|
+
const [action, id] = positional(args);
|
|
462
|
+
if (action === 'list') {
|
|
463
|
+
if (id) throw new CliError('schedule list 不接受 ID');
|
|
464
|
+
return request('GET', '/flow-run-schedules', args);
|
|
465
|
+
}
|
|
466
|
+
if (action === 'create') {
|
|
467
|
+
if (id) throw new CliError('schedule create 不接受 ID');
|
|
468
|
+
return request('POST', '/flow-run-schedules', args);
|
|
469
|
+
}
|
|
470
|
+
if (!id) throw new CliError(`schedule ${action || ''} 需要调度 ID`);
|
|
471
|
+
if (action === 'pause' || action === 'resume') {
|
|
472
|
+
const expected = Number(option(args, '--expected-row-version'));
|
|
473
|
+
if (!Number.isSafeInteger(expected) || expected < 1) {
|
|
474
|
+
throw new CliError(`schedule ${action} 需要正整数 --expected-row-version`);
|
|
475
|
+
}
|
|
476
|
+
return request('PUT', `/flow-run-schedules/${id}/state`, args, {
|
|
477
|
+
body: { expected_row_version: expected, status: action === 'pause' ? 'PAUSED' : 'ACTIVE' },
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
if (action === 'trigger') return request('POST', `/flow-run-schedules/${id}/trigger`, args, { body: {} });
|
|
481
|
+
if (action === 'delete') return request('DELETE', `/flow-run-schedules/${id}`, args);
|
|
482
|
+
throw new CliError('schedule 支持 list|create|pause|resume|trigger|delete');
|
|
318
483
|
}
|
|
319
484
|
|
|
320
485
|
async function model(args) {
|
|
@@ -327,6 +492,7 @@ async function model(args) {
|
|
|
327
492
|
: request('POST', '/model-providers/discover-models', args);
|
|
328
493
|
}
|
|
329
494
|
if (!id) throw new CliError(`model ${action || ''} 需要模型供应商 ID`);
|
|
495
|
+
if (action === 'usage') return request('GET', `/model-providers/${id}/usage`, args);
|
|
330
496
|
if (action === 'update') return request('PUT', `/model-providers/${id}`, args);
|
|
331
497
|
if (action === 'delete') return request('DELETE', `/model-providers/${id}`, args);
|
|
332
498
|
if (action === 'test') return request('POST', `/model-providers/${id}/test`, args);
|
|
@@ -334,7 +500,7 @@ async function model(args) {
|
|
|
334
500
|
if (action === 'oauth-poll') return request('POST', `/model-providers/${id}/oauth/device/poll`, args, { body: await objectPayload(args) });
|
|
335
501
|
if (action === 'oauth-status') return request('GET', `/model-providers/${id}/oauth/status`, args);
|
|
336
502
|
if (action === 'oauth-revoke') return request('DELETE', `/model-providers/${id}/oauth`, args);
|
|
337
|
-
throw new CliError('model 支持 list|create|update|delete|discover|test|oauth-start|oauth-poll|oauth-status|oauth-revoke');
|
|
503
|
+
throw new CliError('model 支持 list|create|update|delete|discover|usage|test|oauth-start|oauth-poll|oauth-status|oauth-revoke');
|
|
338
504
|
}
|
|
339
505
|
|
|
340
506
|
async function agent(args) {
|
|
@@ -408,6 +574,7 @@ async function websocket(args) {
|
|
|
408
574
|
if (message && messageJson) throw new CliError('--message 与 --message-json 只能使用其中一个');
|
|
409
575
|
if (messageJson) message = JSON.stringify(parseJson(messageJson, '--message-json'));
|
|
410
576
|
if (flag(args, '--dry-run')) return { url: url.toString(), message: message || null, max_messages: maxMessages };
|
|
577
|
+
const sessionToken = await loadAuth(baseUrl);
|
|
411
578
|
await new Promise((resolve, reject) => {
|
|
412
579
|
let received = 0;
|
|
413
580
|
let settled = false;
|
|
@@ -416,7 +583,7 @@ async function websocket(args) {
|
|
|
416
583
|
settled = true;
|
|
417
584
|
if (error) reject(error); else resolve();
|
|
418
585
|
};
|
|
419
|
-
const socket = new WebSocket(url);
|
|
586
|
+
const socket = new WebSocket(url, { headers: { Cookie: `${SESSION_COOKIE}=${sessionToken}` } });
|
|
420
587
|
socket.addEventListener('open', () => { if (message) socket.send(message); });
|
|
421
588
|
socket.addEventListener('message', (event) => {
|
|
422
589
|
const text = String(event.data);
|
|
@@ -440,7 +607,8 @@ async function main(argv) {
|
|
|
440
607
|
if (action === 'init') { const baseUrl = option(args, '--base-url'); if (!baseUrl) throw new CliError('config init 需要 --base-url'); result = { config_path: CONFIG_PATH, base_url: await saveConfig(baseUrl, flag(args, '--force')) }; }
|
|
441
608
|
else if (action === 'show') { const { baseUrl } = await loadConfig(); result = { config_path: CONFIG_PATH, base_url: baseUrl }; }
|
|
442
609
|
else throw new CliError('config 支持 init|show');
|
|
443
|
-
} else if (command === '
|
|
610
|
+
} else if (command === 'auth') result = auth(args);
|
|
611
|
+
else if (command === 'health') result = request('GET', flag(args, '--ready') ? '/health/ready' : '/health', args, { raw: true, authenticated: false });
|
|
444
612
|
else if (command === 'openapi') { result = request('GET', '/openapi.json', args, { raw: true }); if (flag(args, '--paths')) result = openapiPaths(await result); }
|
|
445
613
|
else if (command === 'api') { const [method, path] = positional(args); if (!method || !path) throw new CliError('api 用法:api <method> <PATH>'); result = request(method.toUpperCase(), path, args, { raw: flag(args, '--raw') }); }
|
|
446
614
|
else if (command === 'upload') result = upload(args);
|
|
@@ -452,6 +620,7 @@ async function main(argv) {
|
|
|
452
620
|
else if (command === 'credential') result = credential(args);
|
|
453
621
|
else if (command === 'flow') result = flow(args);
|
|
454
622
|
else if (command === 'run') result = run(args);
|
|
623
|
+
else if (command === 'schedule') result = schedule(args);
|
|
455
624
|
else if (command === 'model') result = model(args);
|
|
456
625
|
else if (command === 'agent') result = agent(args);
|
|
457
626
|
else throw new CliError(`未知命令:${command}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowweave-ai/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "FlowWeave 平台命令行客户端",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=22.0.0"
|
|
15
15
|
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"ws": "^8.18.3"
|
|
18
|
+
},
|
|
16
19
|
"scripts": {
|
|
17
20
|
"test": "node --test test/*.test.mjs",
|
|
18
21
|
"lint": "node --check bin/flowweave.mjs",
|