@xiaoailazy/coexistree-cli 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 +73 -0
- package/bin/coexistree.js +79 -0
- package/lib/argv.js +82 -0
- package/lib/commands/ask.js +53 -0
- package/lib/commands/config.js +130 -0
- package/lib/commands/task.js +298 -0
- package/lib/config.js +108 -0
- package/lib/env.js +56 -0
- package/lib/http.js +86 -0
- package/lib/md-file.js +21 -0
- package/lib/task-paths.js +34 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# @xiaoailazy/coexistree-cli
|
|
2
|
+
|
|
3
|
+
CoExistree **Agent CLI**:产研任务、系统提问。配合 [`@xiaoailazy/coexistree-skills`](../skills) 供各类 Agent 使用。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @xiaoailazy/coexistree-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
一键(CLI + Skills):`npm install -g @xiaoailazy/coexistree`
|
|
12
|
+
|
|
13
|
+
本地开发:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cd packages/cli && npm install && npm link
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 配置(推荐)
|
|
20
|
+
|
|
21
|
+
在 Web **密钥管理** 开启并复制 Agent Key,并确认目标系统的 **system code**。
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
coexistree config initial \
|
|
25
|
+
--api-key "ce_从Web复制" \
|
|
26
|
+
--system-code "order-service" \
|
|
27
|
+
--base-url http://localhost:8080
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
写入 **`~/.coexistree/config.json`**(权限 `600`)。之后所有子命令自动读取,**无需** `--env-file`。
|
|
31
|
+
|
|
32
|
+
| 子命令 | 说明 |
|
|
33
|
+
|--------|------|
|
|
34
|
+
| `config initial` | 创建/覆盖配置(已有文件需 `--force`) |
|
|
35
|
+
| `config show` | 查看配置(api-key 脱敏) |
|
|
36
|
+
| `config path` | 打印配置文件路径 |
|
|
37
|
+
| `task create` | 创建任务并上传需求 `.md` |
|
|
38
|
+
| `task list` | 任务列表(`scope`: 待开发 / 待补用例) |
|
|
39
|
+
| `task pull` | 拉取 REQUIREMENT / DESIGN / TEST_CASE 到本地 |
|
|
40
|
+
| `task upload` | 上传 DESIGN / TEST_CASE(必填 `.md` 路径) |
|
|
41
|
+
| `ask` | 向系统提问 |
|
|
42
|
+
|
|
43
|
+
自定义路径:`coexistree --config /path/to/config.json ...`
|
|
44
|
+
|
|
45
|
+
### 覆盖优先级(高→低)
|
|
46
|
+
|
|
47
|
+
1. 已存在的环境变量
|
|
48
|
+
2. `--env-file`(仅填充尚未设置的变量)
|
|
49
|
+
3. 配置文件
|
|
50
|
+
4. 全局 `--base-url`
|
|
51
|
+
|
|
52
|
+
## 使用
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
coexistree task create --title "迭代 1" docs/req.md
|
|
56
|
+
coexistree task list
|
|
57
|
+
coexistree task pull --task-id 42 --doc-type REQUIREMENT
|
|
58
|
+
coexistree task upload --task-id 42 --doc-type DESIGN docs/superpowers/specs/...-design.md
|
|
59
|
+
|
|
60
|
+
coexistree ask --question "变更批次失败时有哪些 stage?"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
上传需求/设计**不会**自动合并进知识树;合并在 Web 对任务执行「应用变更」。
|
|
64
|
+
|
|
65
|
+
## 发布
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
cd packages/cli
|
|
69
|
+
npm test
|
|
70
|
+
npm publish --access public
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
需已登录 npm 且对 `@xiaoailazy` 作用域有发布权限。
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "module";
|
|
3
|
+
import { parseGlobalArgv } from "../lib/argv.js";
|
|
4
|
+
import { bootstrapConfig, defaultConfigPath } from "../lib/config.js";
|
|
5
|
+
import { runAsk } from "../lib/commands/ask.js";
|
|
6
|
+
import { runConfig } from "../lib/commands/config.js";
|
|
7
|
+
import { runTask } from "../lib/commands/task.js";
|
|
8
|
+
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const { version: CLI_VERSION } = require("../package.json");
|
|
11
|
+
|
|
12
|
+
function printHelp() {
|
|
13
|
+
console.log(`coexistree — CoExistree Agent CLI
|
|
14
|
+
|
|
15
|
+
用法:
|
|
16
|
+
coexistree config initial --api-key <k> --system-code <code> [--base-url <url>]
|
|
17
|
+
coexistree config show | path
|
|
18
|
+
coexistree task create --title <title> [--summary <text>] <requirement.md>
|
|
19
|
+
coexistree task list [--scope pending-development|needs-test-case] [--page 0] [--size 5]
|
|
20
|
+
coexistree task upload --task-id <id> --doc-type DESIGN|TEST_CASE <file.md>
|
|
21
|
+
coexistree task pull --task-id <id> --doc-type REQUIREMENT|DESIGN|TEST_CASE [--document-id <id>]
|
|
22
|
+
coexistree ask --question <text>
|
|
23
|
+
|
|
24
|
+
先 config initial。配置: ${defaultConfigPath()}
|
|
25
|
+
|
|
26
|
+
全局选项(写在子命令前):
|
|
27
|
+
--config <path> 配置文件路径(默认 ~/.coexistree/config.json)
|
|
28
|
+
--env-file <path> 可选,临时覆盖配置
|
|
29
|
+
--base-url <url> 单次覆盖 API 地址
|
|
30
|
+
-v, --version 打印 CLI 版本并退出
|
|
31
|
+
--verbose 打印请求/响应调试信息到 stderr
|
|
32
|
+
-h, --help
|
|
33
|
+
|
|
34
|
+
示例: coexistree task list
|
|
35
|
+
coexistree task pull --task-id 42 --doc-type REQUIREMENT
|
|
36
|
+
coexistree task upload --task-id 42 --doc-type DESIGN path/design.md
|
|
37
|
+
`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { values, positionals } = parseGlobalArgv(process.argv.slice(2));
|
|
41
|
+
|
|
42
|
+
if (values.version) {
|
|
43
|
+
console.log(`coexistree ${CLI_VERSION}`);
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (values.help && positionals.length === 0) {
|
|
48
|
+
printHelp();
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const sub = positionals[0];
|
|
53
|
+
const rest = positionals.slice(1);
|
|
54
|
+
|
|
55
|
+
if (!sub || sub === "help" || values.help) {
|
|
56
|
+
printHelp();
|
|
57
|
+
process.exit(sub ? 0 : 1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
if (sub === "config") {
|
|
62
|
+
await runConfig(rest, values);
|
|
63
|
+
} else if (sub === "task") {
|
|
64
|
+
bootstrapConfig(values);
|
|
65
|
+
await runTask(rest, values);
|
|
66
|
+
} else {
|
|
67
|
+
bootstrapConfig(values);
|
|
68
|
+
if (sub === "ask") {
|
|
69
|
+
await runAsk(rest, values);
|
|
70
|
+
} else {
|
|
71
|
+
console.error(`未知子命令: ${sub}`);
|
|
72
|
+
printHelp();
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error(err.message ?? String(err));
|
|
78
|
+
process.exit(err.exitCode ?? 1);
|
|
79
|
+
}
|
package/lib/argv.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const GLOBAL_FLAGS = new Set([
|
|
2
|
+
"--config",
|
|
3
|
+
"--env-file",
|
|
4
|
+
"--base-url",
|
|
5
|
+
"-h",
|
|
6
|
+
"--help",
|
|
7
|
+
"-v",
|
|
8
|
+
"--version",
|
|
9
|
+
"--verbose",
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 仅解析子命令前的全局参数,其余原样交给子命令(避免顶层 parseArgs 拒绝 --api-key 等)。
|
|
14
|
+
*/
|
|
15
|
+
export function parseGlobalArgv(argv) {
|
|
16
|
+
const values = { help: false, version: false, verbose: false };
|
|
17
|
+
const positionals = [];
|
|
18
|
+
let i = 0;
|
|
19
|
+
|
|
20
|
+
while (i < argv.length) {
|
|
21
|
+
const arg = argv[i];
|
|
22
|
+
if (arg === "-h" || arg === "--help") {
|
|
23
|
+
values.help = true;
|
|
24
|
+
i++;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (arg === "-v" || arg === "--version") {
|
|
28
|
+
values.version = true;
|
|
29
|
+
i++;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (arg === "--verbose") {
|
|
33
|
+
values.verbose = true;
|
|
34
|
+
i++;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (!arg.startsWith("-")) {
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
if (!GLOBAL_FLAGS.has(arg) && !arg.startsWith("--config=") && !arg.startsWith("--env-file=") && !arg.startsWith("--base-url=")) {
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (arg === "--config") {
|
|
45
|
+
values.config = argv[++i];
|
|
46
|
+
i++;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (arg.startsWith("--config=")) {
|
|
50
|
+
values.config = arg.slice("--config=".length);
|
|
51
|
+
i++;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (arg === "--env-file") {
|
|
55
|
+
values["env-file"] = argv[++i];
|
|
56
|
+
i++;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (arg.startsWith("--env-file=")) {
|
|
60
|
+
values["env-file"] = arg.slice("--env-file=".length);
|
|
61
|
+
i++;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (arg === "--base-url") {
|
|
65
|
+
values["base-url"] = argv[++i];
|
|
66
|
+
i++;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (arg.startsWith("--base-url=")) {
|
|
70
|
+
values["base-url"] = arg.slice("--base-url=".length);
|
|
71
|
+
i++;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
while (i < argv.length) {
|
|
78
|
+
positionals.push(argv[i++]);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { values, positionals };
|
|
82
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { resolveConfigPath } from "../config.js";
|
|
3
|
+
import { agentConfig, agentHeaders, die } from "../env.js";
|
|
4
|
+
import { agentFetch, logVerbose, printApiResult } from "../http.js";
|
|
5
|
+
|
|
6
|
+
export async function runAsk(positionals, globalValues) {
|
|
7
|
+
const { values } = parseArgs({
|
|
8
|
+
args: positionals,
|
|
9
|
+
options: {
|
|
10
|
+
question: { type: "string" },
|
|
11
|
+
help: { type: "boolean", short: "h", default: false },
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
if (values.help) {
|
|
16
|
+
console.log(`ask — 向系统提问(同步 JSON 回答)
|
|
17
|
+
|
|
18
|
+
选项:
|
|
19
|
+
--question <text> 必填
|
|
20
|
+
`);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const question = values.question?.trim();
|
|
25
|
+
if (!question) {
|
|
26
|
+
die("ask 需要 --question");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const verbose = Boolean(globalValues.verbose);
|
|
30
|
+
const config = agentConfig(globalValues);
|
|
31
|
+
const url = `${config.baseUrl}/api/v1/agent/ask`;
|
|
32
|
+
|
|
33
|
+
logVerbose(verbose, `config file: ${resolveConfigPath(globalValues)}`);
|
|
34
|
+
logVerbose(
|
|
35
|
+
verbose,
|
|
36
|
+
`resolved: baseUrl=${config.baseUrl} systemCode=${config.systemCode} apiKey=${config.apiKey.slice(0, 4)}... (len=${config.apiKey.length})`,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const { body } = await agentFetch(
|
|
40
|
+
url,
|
|
41
|
+
{
|
|
42
|
+
method: "POST",
|
|
43
|
+
headers: {
|
|
44
|
+
...agentHeaders(config),
|
|
45
|
+
"Content-Type": "application/json",
|
|
46
|
+
},
|
|
47
|
+
body: JSON.stringify({ question }),
|
|
48
|
+
},
|
|
49
|
+
{ verbose, label: "ask" },
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
printApiResult(body);
|
|
53
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
3
|
+
import {
|
|
4
|
+
defaultConfigPath,
|
|
5
|
+
readConfigFile,
|
|
6
|
+
resolveConfigPath,
|
|
7
|
+
writeConfigFile,
|
|
8
|
+
} from "../config.js";
|
|
9
|
+
import { die } from "../env.js";
|
|
10
|
+
|
|
11
|
+
function maskSecret(value) {
|
|
12
|
+
if (!value || value.length < 8) return "****";
|
|
13
|
+
return `${value.slice(0, 4)}...${value.slice(-4)}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function runConfig(positionals, globalValues) {
|
|
17
|
+
const action = positionals[0];
|
|
18
|
+
if (!action || action === "help") {
|
|
19
|
+
printConfigHelp();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (action === "initial") {
|
|
24
|
+
await runConfigInitial(positionals.slice(1), globalValues);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (action === "show") {
|
|
29
|
+
runConfigShow(globalValues);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (action === "path") {
|
|
34
|
+
console.log(resolveConfigPath(globalValues));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
die(`未知 config 子命令: ${action}(可用: initial, show, path)`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function runConfigInitial(args, globalValues) {
|
|
42
|
+
const { values } = parseArgs({
|
|
43
|
+
args,
|
|
44
|
+
options: {
|
|
45
|
+
"api-key": { type: "string" },
|
|
46
|
+
"system-code": { type: "string" },
|
|
47
|
+
"base-url": { type: "string" },
|
|
48
|
+
force: { type: "boolean", default: false },
|
|
49
|
+
help: { type: "boolean", short: "h", default: false },
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (values.help) {
|
|
54
|
+
printConfigInitialHelp();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const configPath = resolveConfigPath(globalValues);
|
|
59
|
+
const saved = writeConfigFile(
|
|
60
|
+
configPath,
|
|
61
|
+
{
|
|
62
|
+
apiKey: values["api-key"],
|
|
63
|
+
systemCode: values["system-code"],
|
|
64
|
+
baseUrl: values["base-url"],
|
|
65
|
+
},
|
|
66
|
+
{ force: values.force },
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
console.error(`已写入配置: ${configPath}`);
|
|
70
|
+
console.log(
|
|
71
|
+
JSON.stringify(
|
|
72
|
+
{
|
|
73
|
+
apiKey: maskSecret(saved.apiKey),
|
|
74
|
+
systemCode: saved.systemCode,
|
|
75
|
+
baseUrl: saved.baseUrl,
|
|
76
|
+
},
|
|
77
|
+
null,
|
|
78
|
+
2,
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function runConfigShow(globalValues) {
|
|
84
|
+
const configPath = resolveConfigPath(globalValues);
|
|
85
|
+
if (!existsSync(configPath)) {
|
|
86
|
+
die(`尚未初始化,请运行: coexistree config initial --api-key ... --system-code ...`);
|
|
87
|
+
}
|
|
88
|
+
const config = readConfigFile(configPath);
|
|
89
|
+
console.log(
|
|
90
|
+
JSON.stringify(
|
|
91
|
+
{
|
|
92
|
+
path: configPath,
|
|
93
|
+
apiKey: maskSecret(config.apiKey),
|
|
94
|
+
systemCode: config.systemCode,
|
|
95
|
+
baseUrl: config.baseUrl ?? "http://localhost:8080",
|
|
96
|
+
},
|
|
97
|
+
null,
|
|
98
|
+
2,
|
|
99
|
+
),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function printConfigHelp() {
|
|
104
|
+
console.log(`config — 本地 CLI 配置(默认文件 ~/.coexistree/config.json)
|
|
105
|
+
|
|
106
|
+
子命令:
|
|
107
|
+
initial 首次写入配置(必填 --api-key --system-code)
|
|
108
|
+
show 查看当前配置(api-key 脱敏)
|
|
109
|
+
path 打印配置文件路径
|
|
110
|
+
|
|
111
|
+
全局可配合: --config <path> 指定配置文件路径
|
|
112
|
+
`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function printConfigInitialHelp() {
|
|
116
|
+
console.log(`config initial — 生成本地配置文件
|
|
117
|
+
|
|
118
|
+
选项:
|
|
119
|
+
--api-key <string> 必填(Web「密钥管理」中复制)
|
|
120
|
+
--system-code <string> 必填(系统 code,如 order-service)
|
|
121
|
+
--base-url <url> 可选,默认 http://localhost:8080
|
|
122
|
+
--force 覆盖已有配置文件
|
|
123
|
+
|
|
124
|
+
示例:
|
|
125
|
+
coexistree config initial \\
|
|
126
|
+
--api-key "ce_xxxx" \\
|
|
127
|
+
--system-code order-service \\
|
|
128
|
+
--base-url http://localhost:8080
|
|
129
|
+
`);
|
|
130
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { Blob } from "node:buffer";
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import { resolveConfigPath } from "../config.js";
|
|
6
|
+
import { agentConfig, agentHeaders, die } from "../env.js";
|
|
7
|
+
import { agentFetch, logVerbose, printApiResult } from "../http.js";
|
|
8
|
+
import { readMdPath } from "../md-file.js";
|
|
9
|
+
import { buildPullDefaultPath } from "../task-paths.js";
|
|
10
|
+
function taskFetch(globalValues, method, path, options = {}) {
|
|
11
|
+
const verbose = Boolean(globalValues.verbose);
|
|
12
|
+
const config = agentConfig(globalValues);
|
|
13
|
+
const url = `${config.baseUrl}${path}`;
|
|
14
|
+
logVerbose(verbose, `config file: ${resolveConfigPath(globalValues)}`);
|
|
15
|
+
return agentFetch(
|
|
16
|
+
url,
|
|
17
|
+
{
|
|
18
|
+
method,
|
|
19
|
+
headers: { ...agentHeaders(config), ...options.headers },
|
|
20
|
+
body: options.body,
|
|
21
|
+
},
|
|
22
|
+
{ verbose, label: options.label ?? "task" },
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function runTaskCreate(positionals, globalValues) {
|
|
27
|
+
const { values, positionals: files } = parseArgs({
|
|
28
|
+
args: positionals,
|
|
29
|
+
options: {
|
|
30
|
+
title: { type: "string" },
|
|
31
|
+
summary: { type: "string" },
|
|
32
|
+
help: { type: "boolean", short: "h", default: false },
|
|
33
|
+
},
|
|
34
|
+
allowPositionals: true,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (values.help) {
|
|
38
|
+
console.log(`task create — 创建任务并上传需求 .md
|
|
39
|
+
|
|
40
|
+
选项:
|
|
41
|
+
--title <string> 必填
|
|
42
|
+
--summary <string> 可选
|
|
43
|
+
<requirement.md> 必填,恰好 1 个本地文件
|
|
44
|
+
`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!values.title?.trim()) {
|
|
49
|
+
die("task create 需要 --title");
|
|
50
|
+
}
|
|
51
|
+
if (files.length !== 1) {
|
|
52
|
+
die("task create 需要恰好 1 个 .md 文件路径");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const prepared = readMdPath(files[0]);
|
|
56
|
+
console.error(`上传需求: ${prepared.abs}`);
|
|
57
|
+
|
|
58
|
+
const form = new FormData();
|
|
59
|
+
form.append("title", values.title.trim());
|
|
60
|
+
if (values.summary?.trim()) {
|
|
61
|
+
form.append("summary", values.summary.trim());
|
|
62
|
+
}
|
|
63
|
+
form.append("file", new Blob([prepared.data], { type: "text/markdown" }), prepared.name);
|
|
64
|
+
|
|
65
|
+
const { body } = await taskFetch(globalValues, "POST", "/api/v1/agent/tasks", {
|
|
66
|
+
body: form,
|
|
67
|
+
label: "task create",
|
|
68
|
+
});
|
|
69
|
+
printApiResult(body);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function runTaskList(positionals, globalValues) {
|
|
73
|
+
const { values } = parseArgs({
|
|
74
|
+
args: positionals,
|
|
75
|
+
options: {
|
|
76
|
+
scope: { type: "string", default: "pending-development" },
|
|
77
|
+
page: { type: "string", default: "0" },
|
|
78
|
+
size: { type: "string", default: "5" },
|
|
79
|
+
help: { type: "boolean", short: "h", default: false },
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (values.help) {
|
|
84
|
+
console.log(`task list — 分页列出任务
|
|
85
|
+
|
|
86
|
+
选项:
|
|
87
|
+
--scope <name> pending-development(默认)| needs-test-case
|
|
88
|
+
--page <n> 页码,从 0 起,默认 0
|
|
89
|
+
--size <n> 每页条数,默认 5
|
|
90
|
+
`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const scope = values.scope?.trim() || "pending-development";
|
|
95
|
+
if (!["pending-development", "needs-test-case"].includes(scope)) {
|
|
96
|
+
die("task list --scope 须为 pending-development 或 needs-test-case");
|
|
97
|
+
}
|
|
98
|
+
const qs = new URLSearchParams({
|
|
99
|
+
scope,
|
|
100
|
+
page: values.page ?? "0",
|
|
101
|
+
size: values.size ?? "5",
|
|
102
|
+
});
|
|
103
|
+
const { body } = await taskFetch(globalValues, "GET", `/api/v1/agent/tasks?${qs}`, {
|
|
104
|
+
label: "task list",
|
|
105
|
+
});
|
|
106
|
+
printApiResult(body);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const UPLOAD_DOC_TYPES = {
|
|
110
|
+
DESIGN: { label: "设计" },
|
|
111
|
+
TEST_CASE: { label: "测试用例" },
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
async function runTaskUpload(positionals, globalValues) {
|
|
115
|
+
const { values, positionals: files } = parseArgs({
|
|
116
|
+
args: positionals,
|
|
117
|
+
options: {
|
|
118
|
+
"task-id": { type: "string" },
|
|
119
|
+
"doc-type": { type: "string" },
|
|
120
|
+
help: { type: "boolean", short: "h", default: false },
|
|
121
|
+
},
|
|
122
|
+
allowPositionals: true,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (values.help) {
|
|
126
|
+
console.log(`task upload — 为任务上传 Markdown(设计或测试用例)
|
|
127
|
+
|
|
128
|
+
选项:
|
|
129
|
+
--task-id <id> 必填(变更批次 ID / taskId)
|
|
130
|
+
--doc-type <type> 必填:DESIGN | TEST_CASE
|
|
131
|
+
<file.md> 必填
|
|
132
|
+
`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const taskId = values["task-id"]?.trim();
|
|
137
|
+
if (!taskId || !/^\d+$/.test(taskId)) {
|
|
138
|
+
die("task upload 需要 --task-id <数字>");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const docType = values["doc-type"]?.trim()?.toUpperCase();
|
|
142
|
+
if (!docType || !UPLOAD_DOC_TYPES[docType]) {
|
|
143
|
+
die("task upload 需要 --doc-type DESIGN 或 TEST_CASE");
|
|
144
|
+
}
|
|
145
|
+
if (files.length !== 1) {
|
|
146
|
+
die(`task upload --doc-type ${docType} 需要恰好 1 个 .md 文件路径`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const spec = UPLOAD_DOC_TYPES[docType];
|
|
150
|
+
const sourcePath = files[0];
|
|
151
|
+
const prepared = readMdPath(sourcePath);
|
|
152
|
+
console.error(`上传${spec.label}到任务 ${taskId}: ${prepared.abs}`);
|
|
153
|
+
|
|
154
|
+
const form = new FormData();
|
|
155
|
+
form.append("file", new Blob([prepared.data], { type: "text/markdown" }), prepared.name);
|
|
156
|
+
form.append("docContentType", docType);
|
|
157
|
+
|
|
158
|
+
const { body } = await taskFetch(
|
|
159
|
+
globalValues,
|
|
160
|
+
"POST",
|
|
161
|
+
`/api/v1/agent/tasks/${taskId}/documents`,
|
|
162
|
+
{ body: form, label: `task upload ${docType}` },
|
|
163
|
+
);
|
|
164
|
+
printApiResult(body);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const PULL_DOC_TYPES = {
|
|
168
|
+
REQUIREMENT: { kind: "prd", dir: "prd" },
|
|
169
|
+
DESIGN: { kind: "design", dir: "specs" },
|
|
170
|
+
TEST_CASE: { kind: "testcase", dir: "testcases" },
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
async function runTaskPull(positionals, globalValues) {
|
|
174
|
+
const { values } = parseArgs({
|
|
175
|
+
args: positionals,
|
|
176
|
+
options: {
|
|
177
|
+
"task-id": { type: "string" },
|
|
178
|
+
"doc-type": { type: "string" },
|
|
179
|
+
"document-id": { type: "string" },
|
|
180
|
+
output: { type: "string" },
|
|
181
|
+
force: { type: "boolean", default: false },
|
|
182
|
+
help: { type: "boolean", short: "h", default: false },
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (values.help) {
|
|
187
|
+
console.log(`task pull — 拉取 Markdown 到本地
|
|
188
|
+
|
|
189
|
+
选项:
|
|
190
|
+
--task-id <id> 必填
|
|
191
|
+
--doc-type <type> REQUIREMENT | DESIGN | TEST_CASE
|
|
192
|
+
--document-id <id> 可选,省略则拉该类型最新
|
|
193
|
+
--output <path> 可选
|
|
194
|
+
--force 覆盖已存在文件
|
|
195
|
+
|
|
196
|
+
默认: docs/superpowers/{prd|specs|testcases}/{suggestedBasename}-{kind}-{documentId}.md
|
|
197
|
+
`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const taskId = values["task-id"]?.trim();
|
|
202
|
+
if (!taskId || !/^\d+$/.test(taskId)) {
|
|
203
|
+
die("task pull 需要 --task-id <数字>");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const docType = values["doc-type"]?.trim()?.toUpperCase();
|
|
207
|
+
if (!docType || !PULL_DOC_TYPES[docType]) {
|
|
208
|
+
die("task pull 需要 --doc-type REQUIREMENT、DESIGN 或 TEST_CASE");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const documentId = values["document-id"]?.trim();
|
|
212
|
+
const qs = new URLSearchParams({ docContentType: docType });
|
|
213
|
+
const path = documentId
|
|
214
|
+
? `/api/v1/agent/tasks/${taskId}/documents/${documentId}?${qs}`
|
|
215
|
+
: `/api/v1/agent/tasks/${taskId}/documents/latest?${qs}`;
|
|
216
|
+
|
|
217
|
+
const { body } = await taskFetch(globalValues, "GET", path, {
|
|
218
|
+
label: `task pull ${docType}`,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
if (body.success === false) {
|
|
222
|
+
printApiResult(body);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const data = body.data;
|
|
227
|
+
if (!data || typeof data.content !== "string") {
|
|
228
|
+
die("pull 响应缺少 data.content");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const customOutput = values.output?.trim();
|
|
232
|
+
const spec = PULL_DOC_TYPES[docType];
|
|
233
|
+
if (data.documentId == null || data.documentId === "") {
|
|
234
|
+
die("pull 响应缺少 data.documentId");
|
|
235
|
+
}
|
|
236
|
+
const outPath =
|
|
237
|
+
customOutput ||
|
|
238
|
+
buildPullDefaultPath({
|
|
239
|
+
suggestedBasename: data.suggestedBasename,
|
|
240
|
+
kind: spec.kind,
|
|
241
|
+
dir: spec.dir,
|
|
242
|
+
documentId: data.documentId,
|
|
243
|
+
taskId: data.taskId ?? taskId,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
if (!values.force && existsSync(outPath)) {
|
|
247
|
+
die(`文件已存在: ${outPath},请使用 --force 或另选 --output`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
251
|
+
writeFileSync(outPath, data.content, "utf8");
|
|
252
|
+
console.error(`已写入: ${outPath}`);
|
|
253
|
+
if (body?.data && typeof body.data === "object") {
|
|
254
|
+
body.data.outputPath = outPath;
|
|
255
|
+
}
|
|
256
|
+
printApiResult(body);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function printTaskHelp() {
|
|
260
|
+
console.log(`task — 产研任务(合并进知识树须在 Web「应用变更」)
|
|
261
|
+
|
|
262
|
+
create --title <t> [--summary <s>] <requirement.md>
|
|
263
|
+
list [--scope pending-development|needs-test-case] [--page 0] [--size 5]
|
|
264
|
+
pull --task-id <id> --doc-type REQUIREMENT|DESIGN|TEST_CASE [--document-id <id>]
|
|
265
|
+
upload --task-id <id> --doc-type DESIGN|TEST_CASE <file.md>
|
|
266
|
+
`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export async function runTask(positionals, globalValues) {
|
|
270
|
+
const action = positionals[0];
|
|
271
|
+
const rest = positionals.slice(1);
|
|
272
|
+
|
|
273
|
+
if (!action || action === "help") {
|
|
274
|
+
printTaskHelp();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (action === "create") {
|
|
279
|
+
await runTaskCreate(rest, globalValues);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (action === "list") {
|
|
283
|
+
await runTaskList(rest, globalValues);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (action === "upload") {
|
|
287
|
+
await runTaskUpload(rest, globalValues);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (action === "pull") {
|
|
291
|
+
await runTaskPull(rest, globalValues);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
die(
|
|
296
|
+
`未知 task 子命令: ${action}(可用: create, list, upload, pull)`,
|
|
297
|
+
);
|
|
298
|
+
}
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { die, loadEnvFile } from "./env.js";
|
|
5
|
+
|
|
6
|
+
const CONFIG_DIR = join(homedir(), ".coexistree");
|
|
7
|
+
const DEFAULT_CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
8
|
+
|
|
9
|
+
export function defaultConfigPath() {
|
|
10
|
+
return DEFAULT_CONFIG_FILE;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function resolveConfigPath(cliValues) {
|
|
14
|
+
const custom = cliValues.config?.trim();
|
|
15
|
+
return custom ? resolve(custom) : DEFAULT_CONFIG_FILE;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function readConfigFile(configPath) {
|
|
19
|
+
if (!existsSync(configPath)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
let raw;
|
|
23
|
+
try {
|
|
24
|
+
raw = readFileSync(configPath, "utf8");
|
|
25
|
+
} catch (err) {
|
|
26
|
+
die(`无法读取配置文件 ${configPath}: ${err.message}`);
|
|
27
|
+
}
|
|
28
|
+
let parsed;
|
|
29
|
+
try {
|
|
30
|
+
parsed = JSON.parse(raw);
|
|
31
|
+
} catch {
|
|
32
|
+
die(`配置文件不是合法 JSON: ${configPath}`);
|
|
33
|
+
}
|
|
34
|
+
return normalizeConfig(parsed);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function normalizeConfig(obj) {
|
|
38
|
+
if (!obj || typeof obj !== "object") {
|
|
39
|
+
die("配置文件格式无效");
|
|
40
|
+
}
|
|
41
|
+
const apiKey = pickString(obj, ["apiKey", "api_key", "COEXISTREE_API_KEY"]);
|
|
42
|
+
const systemCode = pickString(obj, ["systemCode", "system_code", "COEXISTREE_SYSTEM_CODE"]);
|
|
43
|
+
const baseUrl = pickString(obj, ["baseUrl", "base_url", "COEXISTREE_BASE_URL"]);
|
|
44
|
+
return { apiKey, systemCode, baseUrl };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function pickString(obj, keys) {
|
|
48
|
+
for (const k of keys) {
|
|
49
|
+
const v = obj[k];
|
|
50
|
+
if (v !== undefined && v !== null && String(v).trim() !== "") {
|
|
51
|
+
return String(v).trim();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Apply config file values only where process.env is not already set. */
|
|
58
|
+
export function applyConfigToEnv(config) {
|
|
59
|
+
if (!config) return;
|
|
60
|
+
if (config.apiKey && process.env.COEXISTREE_API_KEY === undefined) {
|
|
61
|
+
process.env.COEXISTREE_API_KEY = config.apiKey;
|
|
62
|
+
}
|
|
63
|
+
if (config.systemCode && process.env.COEXISTREE_SYSTEM_CODE === undefined) {
|
|
64
|
+
process.env.COEXISTREE_SYSTEM_CODE = config.systemCode;
|
|
65
|
+
}
|
|
66
|
+
if (config.baseUrl && process.env.COEXISTREE_BASE_URL === undefined) {
|
|
67
|
+
process.env.COEXISTREE_BASE_URL = config.baseUrl;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 优先级(低→高):配置文件 → --env-file → 已有环境变量 → 命令行 --base-url */
|
|
72
|
+
export function bootstrapConfig(cliValues) {
|
|
73
|
+
const configPath = resolveConfigPath(cliValues);
|
|
74
|
+
applyConfigToEnv(readConfigFile(configPath));
|
|
75
|
+
if (cliValues["env-file"]) {
|
|
76
|
+
loadEnvFile(cliValues["env-file"]);
|
|
77
|
+
}
|
|
78
|
+
return configPath;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function writeConfigFile(configPath, config, { force = false } = {}) {
|
|
82
|
+
const missing = [];
|
|
83
|
+
if (!config.apiKey) missing.push("--api-key");
|
|
84
|
+
if (!config.systemCode) missing.push("--system-code");
|
|
85
|
+
if (missing.length > 0) {
|
|
86
|
+
die(`config initial 缺少必填项: ${missing.join(", ")}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const normalized = {
|
|
90
|
+
apiKey: config.apiKey.trim(),
|
|
91
|
+
systemCode: config.systemCode.trim(),
|
|
92
|
+
baseUrl: (config.baseUrl?.trim() || "http://localhost:8080").replace(/\/$/, ""),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (existsSync(configPath) && !force) {
|
|
96
|
+
die(`配置文件已存在: ${configPath}\n若要覆盖请加 --force`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
100
|
+
const body = JSON.stringify(normalized, null, 2) + "\n";
|
|
101
|
+
writeFileSync(configPath, body, { encoding: "utf8", mode: 0o600 });
|
|
102
|
+
try {
|
|
103
|
+
chmodSync(configPath, 0o600);
|
|
104
|
+
} catch {
|
|
105
|
+
/* Windows 等环境可能不支持,忽略 */
|
|
106
|
+
}
|
|
107
|
+
return normalized;
|
|
108
|
+
}
|
package/lib/env.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
export function die(message, code = 1) {
|
|
4
|
+
const err = new Error(message);
|
|
5
|
+
err.exitCode = code;
|
|
6
|
+
throw err;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function loadEnvFile(envPath) {
|
|
10
|
+
if (!existsSync(envPath)) {
|
|
11
|
+
die(`找不到 env 文件: ${envPath}`);
|
|
12
|
+
}
|
|
13
|
+
const text = readFileSync(envPath, "utf8");
|
|
14
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
15
|
+
const line = rawLine.trim();
|
|
16
|
+
if (!line || line.startsWith("#")) continue;
|
|
17
|
+
const eq = line.indexOf("=");
|
|
18
|
+
if (eq <= 0) continue;
|
|
19
|
+
const key = line.slice(0, eq).trim();
|
|
20
|
+
let val = line.slice(eq + 1).trim();
|
|
21
|
+
if (
|
|
22
|
+
(val.startsWith('"') && val.endsWith('"')) ||
|
|
23
|
+
(val.startsWith("'") && val.endsWith("'"))
|
|
24
|
+
) {
|
|
25
|
+
val = val.slice(1, -1);
|
|
26
|
+
}
|
|
27
|
+
if (process.env[key] === undefined) {
|
|
28
|
+
process.env[key] = val;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function requireEnv(name) {
|
|
34
|
+
const v = process.env[name];
|
|
35
|
+
if (v === undefined || v.trim() === "") {
|
|
36
|
+
die(`缺少 ${name}:请先运行 coexistree config initial,或设置环境变量 / --env-file`);
|
|
37
|
+
}
|
|
38
|
+
return v.trim();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function agentConfig(cliValues) {
|
|
42
|
+
const baseUrl = (cliValues["base-url"] ?? process.env.COEXISTREE_BASE_URL ?? "http://localhost:8080").replace(
|
|
43
|
+
/\/$/,
|
|
44
|
+
"",
|
|
45
|
+
);
|
|
46
|
+
const apiKey = requireEnv("COEXISTREE_API_KEY");
|
|
47
|
+
const systemCode = requireEnv("COEXISTREE_SYSTEM_CODE");
|
|
48
|
+
return { baseUrl, apiKey, systemCode };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function agentHeaders(config) {
|
|
52
|
+
return {
|
|
53
|
+
"X-Api-Key": config.apiKey,
|
|
54
|
+
"X-System-Code": config.systemCode,
|
|
55
|
+
};
|
|
56
|
+
}
|
package/lib/http.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { die } from "./env.js";
|
|
2
|
+
|
|
3
|
+
function maskKey(key) {
|
|
4
|
+
if (!key || key.length < 8) return "(empty or too short)";
|
|
5
|
+
return `${key.slice(0, 4)}...${key.slice(-4)} (len=${key.length})`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function logVerbose(verbose, ...lines) {
|
|
9
|
+
if (!verbose) return;
|
|
10
|
+
for (const line of lines) {
|
|
11
|
+
console.error(`[coexistree] ${line}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function agentFetch(url, options, { verbose = false, label = "request" } = {}) {
|
|
16
|
+
logVerbose(verbose, `${label}: ${options.method ?? "GET"} ${url}`);
|
|
17
|
+
if (verbose && options.headers) {
|
|
18
|
+
const h = { ...options.headers };
|
|
19
|
+
if (h["X-Api-Key"]) h["X-Api-Key"] = maskKey(h["X-Api-Key"]);
|
|
20
|
+
logVerbose(verbose, `${label} headers: ${JSON.stringify(h)}`);
|
|
21
|
+
}
|
|
22
|
+
if (verbose && options.body && typeof options.body === "string") {
|
|
23
|
+
const preview = options.body.length > 200 ? `${options.body.slice(0, 200)}...` : options.body;
|
|
24
|
+
logVerbose(verbose, `${label} body: ${preview}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const res = await fetch(url, options);
|
|
28
|
+
|
|
29
|
+
logVerbose(
|
|
30
|
+
verbose,
|
|
31
|
+
`${label} response: HTTP ${res.status} ${res.statusText}`,
|
|
32
|
+
`${label} response headers: content-type=${res.headers.get("content-type") ?? "(none)"}`,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const { body, rawText } = await readJsonResponse(res, { verbose, label });
|
|
36
|
+
interpretAuthHint(verbose, res.status, body);
|
|
37
|
+
return { res, body, rawText };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function interpretAuthHint(verbose, status, body) {
|
|
41
|
+
if (!verbose || status !== 401) return;
|
|
42
|
+
const msg = body?.message ?? "";
|
|
43
|
+
if (msg === "Unauthorized") {
|
|
44
|
+
logVerbose(
|
|
45
|
+
verbose,
|
|
46
|
+
"hint: 若后端日志已有 Agent auth OK 仍返回 Unauthorized,多为异步 dispatch 未带上 SecurityContext,请重建含最新 AgentApiKeyAuthFilter 的后端。",
|
|
47
|
+
"hint: 若日志无 Agent auth request,则检查 AGENT_API_ENABLED、URL、Nginx 是否转发 X-Api-Key 等头。",
|
|
48
|
+
);
|
|
49
|
+
} else if (msg === "Invalid API key") {
|
|
50
|
+
logVerbose(verbose, "hint: X-Api-Key 无效或已关闭,请在 Web「密钥管理」核对 key 与开关状态。");
|
|
51
|
+
} else if (msg === "Missing X-Api-Key") {
|
|
52
|
+
logVerbose(verbose, "hint: 服务端未收到 X-Api-Key(可能被反向代理剥离)。");
|
|
53
|
+
}
|
|
54
|
+
if (body?.code === "PERMISSION_DENIED") {
|
|
55
|
+
logVerbose(
|
|
56
|
+
verbose,
|
|
57
|
+
"hint: 该 userId 不是 systemId 的成员。请在 Web 端把用户加入系统,或 config initial 改用 SUPER_ADMIN 用户 ID(通常是 admin)。",
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function readJsonResponse(res, { verbose = false, label = "response" } = {}) {
|
|
63
|
+
const text = await res.text();
|
|
64
|
+
if (verbose) {
|
|
65
|
+
const preview = text.length > 800 ? `${text.slice(0, 800)}...` : text;
|
|
66
|
+
logVerbose(verbose, `${label} body raw: ${preview || "(empty)"}`);
|
|
67
|
+
}
|
|
68
|
+
let body;
|
|
69
|
+
try {
|
|
70
|
+
body = text ? JSON.parse(text) : {};
|
|
71
|
+
} catch {
|
|
72
|
+
die(`服务器返回非 JSON (${res.status}): ${text.slice(0, 500)}`, 2);
|
|
73
|
+
}
|
|
74
|
+
return { res, body, rawText: text };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function printApiResult(body) {
|
|
78
|
+
console.log(JSON.stringify(body, null, 2));
|
|
79
|
+
if (body.success === false) {
|
|
80
|
+
die(body.message ?? body.code ?? "request failed", 2);
|
|
81
|
+
}
|
|
82
|
+
const data = body.data;
|
|
83
|
+
if (data && typeof data === "object" && data.success === false) {
|
|
84
|
+
die(data.error ?? data.stage ?? "operation failed", 2);
|
|
85
|
+
}
|
|
86
|
+
}
|
package/lib/md-file.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { resolve, basename } from "node:path";
|
|
3
|
+
import { die } from "./env.js";
|
|
4
|
+
|
|
5
|
+
export const MAX_MD_BYTES = 10 * 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
export function readMdPath(filePath) {
|
|
8
|
+
const abs = resolve(filePath);
|
|
9
|
+
if (!existsSync(abs)) {
|
|
10
|
+
die(`文件不存在: ${abs}`);
|
|
11
|
+
}
|
|
12
|
+
const data = readFileSync(abs);
|
|
13
|
+
if (data.length > MAX_MD_BYTES) {
|
|
14
|
+
die(`单文件超过 10MB: ${abs}`);
|
|
15
|
+
}
|
|
16
|
+
const name = basename(abs);
|
|
17
|
+
if (!name.endsWith(".md") || name.includes("..")) {
|
|
18
|
+
die(`文件名须为安全的 .md basename: ${abs}`);
|
|
19
|
+
}
|
|
20
|
+
return { abs, name, data };
|
|
21
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
const SLUG_MAX = 40;
|
|
4
|
+
|
|
5
|
+
export function slugFromTitle(title) {
|
|
6
|
+
if (!title || !String(title).trim()) return "untitled";
|
|
7
|
+
let s = String(title).trim().toLowerCase();
|
|
8
|
+
s = s.replace(/[\s_.,;:!?/\\]+/g, "-");
|
|
9
|
+
s = s.replace(/[^\p{L}\p{N}-]/gu, "");
|
|
10
|
+
s = s.replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
11
|
+
if (!s) return "untitled";
|
|
12
|
+
const chars = [...s];
|
|
13
|
+
if (chars.length > SLUG_MAX) {
|
|
14
|
+
s = chars.slice(0, SLUG_MAX).join("").replace(/-+$/, "");
|
|
15
|
+
}
|
|
16
|
+
return s || "untitled";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function formatDateLocal(d = new Date()) {
|
|
20
|
+
const y = d.getFullYear();
|
|
21
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
22
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
23
|
+
return `${y}-${m}-${day}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function buildBasename({ taskId, title, date = new Date() }) {
|
|
27
|
+
return `${formatDateLocal(date)}-${slugFromTitle(title)}-task${taskId}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildPullDefaultPath({ suggestedBasename, kind, dir, documentId, taskId }) {
|
|
31
|
+
const base = suggestedBasename?.trim() || `untitled-task${taskId}`;
|
|
32
|
+
return join("docs/superpowers", dir, `${base}-${kind}-${documentId}.md`);
|
|
33
|
+
}
|
|
34
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xiaoailazy/coexistree-cli",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "CoExistree Agent CLI — tasks, documents, and knowledge-tree ask via HTTP",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/xiaoailazy/coexistree.git",
|
|
10
|
+
"directory": "packages/cli"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"coexistree",
|
|
17
|
+
"cli",
|
|
18
|
+
"agent",
|
|
19
|
+
"knowledge-tree"
|
|
20
|
+
],
|
|
21
|
+
"bin": {
|
|
22
|
+
"coexistree": "./bin/coexistree.js"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"bin",
|
|
26
|
+
"lib",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"test": "node --test test/**/*.test.js"
|
|
34
|
+
}
|
|
35
|
+
}
|