openclaw-weiyuan-init 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,18 +1,19 @@
1
1
  {
2
2
  "name": "openclaw-weiyuan-init",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "OpenClaw Weiyuan Skill 一键初始化工具",
5
5
  "main": "bin/cli.js",
6
6
  "bin": {
7
- "openclaw-weiyuan-init": "./bin/cli.js"
7
+ "openclaw-weiyuan-init": "bin/cli.js"
8
8
  },
9
9
  "scripts": {
10
- "start": "node bin/cli.js init",
11
- "test": "echo \"Error: no test specified\" && exit 1"
10
+ "start": "node bin/cli.js init"
12
11
  },
13
- "keywords": ["openclaw", "weiyuan", "skill", "init", "cli"],
14
- "author": "weiyuan",
15
- "license": "MIT",
12
+ "files": [
13
+ "bin/",
14
+ "lib/",
15
+ "README.md"
16
+ ],
16
17
  "dependencies": {
17
18
  "axios": "^1.6.0",
18
19
  "adm-zip": "^0.5.10",
@@ -21,10 +22,7 @@
21
22
  "fs-extra": "^11.1.1",
22
23
  "ora": "^5.4.1"
23
24
  },
24
- "engines": {
25
- "node": ">=14.0.0"
26
- },
27
25
  "publishConfig": {
28
26
  "access": "public"
29
27
  }
30
- }
28
+ }
package/lib/package.json DELETED
@@ -1,49 +0,0 @@
1
- {
2
- "name": "@weiyuan/openclaw-weiyuan-init",
3
- "version": "1.0.0",
4
- "description": "OpenClaw Weiyuan Skill 一键初始化工具 - 自动下载、解压、配置身份文件",
5
- "main": "index.js",
6
- "bin": {
7
- "openclaw-weiyuan-init": "./bin/cli.js"
8
- },
9
- "scripts": {
10
- "start": "node bin/cli.js init",
11
- "test": "echo \"Error: no test specified\" && exit 1"
12
- },
13
- "keywords": [
14
- "openclaw",
15
- "weiyuan",
16
- "skill",
17
- "init",
18
- "cli",
19
- "one-click",
20
- "deploy"
21
- ],
22
- "author": "Weiyuan",
23
- "license": "MIT",
24
- "repository": {
25
- "type": "git",
26
- "url": "https://github.com/weiyuan/openclaw-weiyuan-init.git"
27
- },
28
- "engines": {
29
- "node": ">=14.0.0"
30
- },
31
- "dependencies": {
32
- "axios": "^1.6.0",
33
- "adm-zip": "^0.5.10",
34
- "chalk": "^4.1.2",
35
- "commander": "^11.1.0",
36
- "fs-extra": "^11.1.1",
37
- "ora": "^5.4.1"
38
- },
39
- "publishConfig": {
40
- "access": "public"
41
- },
42
- "files": [
43
- "bin/",
44
- "lib/",
45
- "templates/",
46
- "index.js",
47
- "README.md"
48
- ]
49
- }
@@ -1,10 +0,0 @@
1
- {
2
- "version": "1.0",
3
- "device_id": "{{DEVICE_ID}}",
4
- "device_name": "{{DEVICE_NAME}}",
5
- "created_at": "{{CREATED_AT}}",
6
- "server_url": "{{SERVER_URL}}",
7
- "workspace": "{{WORKSPACE}}",
8
- "skill_path": "{{SKILL_PATH}}",
9
- "note": "请根据实际情况修改此文件"
10
- }
@@ -1,10 +0,0 @@
1
- {
2
- "version": "1.0",
3
- "device_id": "ZHAOTY",
4
- "device_name": "ZHAOTY",
5
- "created_at": "2026-03-28T14:27:13.667Z",
6
- "server_url": "http://121.43.119.190:8787",
7
- "workspace": "C:\\pythonProject1\\openclaw-weiyuan-init\\workspace-weiyuan",
8
- "skill_path": "C:\\pythonProject1\\openclaw-weiyuan-init\\workspace-weiyuan/weiyuan",
9
- "note": "请根据实际情况修改此文件"
10
- }
@@ -1,159 +0,0 @@
1
- # 龙虾端 weiyuan CLI(MVP)
2
-
3
- 本目录提供一个最小可用的龙虾端 CLI,用于与微元云服务进行签名通信(Ed25519)。
4
-
5
- ## 快速开始
6
-
7
- 前置:微元云服务已启动(默认 `http://127.0.0.1:8787`)。
8
-
9
- 初始化身份(生成本地 `.weiyuan`):
10
-
11
- ```bash
12
- npm run weiyuan -- init --server http://127.0.0.1:8787 --out .weiyuan
13
- ```
14
-
15
- 创建项目(DNA 原文在本地输入,云端只存 dnaHash):
16
-
17
- ```bash
18
- npm run weiyuan -- create --name "Demo" --goal "MVP" --dna "稳当第一"
19
- ```
20
-
21
- 生成邀请口令(发起人执行):
22
-
23
- ```bash
24
- npm run weiyuan -- invite --project <projectId> --role member
25
- ```
26
-
27
- 加入项目(成员执行):
28
-
29
- ```bash
30
- npm run weiyuan -- join --project <projectId> --code <inviteCode>
31
- ```
32
-
33
- 成员列表:
34
-
35
- ```bash
36
- npm run weiyuan -- member list --project <projectId>
37
- ```
38
-
39
- 任务流转:
40
-
41
- ```bash
42
- npm run weiyuan -- task create --project <projectId> --title "工具清点" --desc "检查工具"
43
- npm run weiyuan -- task list --project <projectId>
44
- npm run weiyuan -- task take --project <projectId> --task <taskId>
45
- npm run weiyuan -- task submit --project <projectId> --task <taskId> --hash <contentHash> --type text/markdown
46
- ```
47
-
48
- 状态体检与事件同步:
49
-
50
- ```bash
51
- npm run weiyuan -- status --project <projectId>
52
- npm run weiyuan -- sync --project <projectId>
53
- npm run weiyuan -- sync --project <projectId> --follow --interval 2000
54
- ```
55
-
56
- 风险上报与查询:
57
-
58
- ```bash
59
- npm run weiyuan -- risk report --project <projectId> --severity warn --summary "3号库信号不稳"
60
- npm run weiyuan -- risk list --project <projectId>
61
- ```
62
-
63
- 进化胶囊:
64
-
65
- ```bash
66
- npm run weiyuan -- capsule publish --name "Skill_Pump_Repair_V1" --kind industry --file ./capsule.bin --version v1 --dnaTags 维修,稳当第一
67
- npm run weiyuan -- capsule search --query 维修
68
- npm run weiyuan -- capsule pull --id <capsuleId> --out ./downloaded-capsule.bin
69
- npm run weiyuan -- capsule report --id <capsuleId> --success true
70
- ```
71
-
72
- ## 身份文件
73
-
74
- 默认身份文件:当前目录的 `.weiyuan`(JSON)。
75
-
76
- ## 说明
77
-
78
- - 这是 MVP 级 CLI:聚焦可跑通闭环与协议对齐;后续可升级为 OpenClaw skill 形态。
79
- - 协议与鉴权细则见:[server/PROTOCOL.md](file:///c:/Users/10123/Documents/trae_projects/sky/server/PROTOCOL.md)
80
-
81
- ## OpenClaw Skill 适配入口(MVP)
82
-
83
- 适配器命令:`npm run weiyuan:skill`
84
-
85
- 输入为 JSON(stdin),支持两种模式:
86
-
87
- 1) `action` 模式(结构化)
88
- 2) `text` 模式(口语意图映射)
89
-
90
- 输入/输出 schema 见:`client/skill/schema.json`
91
-
92
- 示例(action):
93
-
94
- ```bash
95
- '{"action":"task.list","identity":".weiyuan.dev2","projectId":"prj_xxx"}' | npm run weiyuan:skill
96
- ```
97
-
98
- 示例(text):
99
-
100
- ```bash
101
- '{"text":"我能做什么","identity":".weiyuan.dev2","projectId":"prj_xxx"}' | npm run weiyuan:skill
102
- ```
103
-
104
- 输出格式:
105
-
106
- - 成功:`{"ok": true, "data": ...}`
107
- - 失败:`{"ok": false, "errorCode": "...", "message": "...", "rawError": "..."}`
108
-
109
- ## 生成 Skill 包
110
-
111
- 构建命令:
112
-
113
- ```bash
114
- npm run weiyuan:skill:build
115
- ```
116
-
117
- 输出目录:`dist/skill-package`
118
-
119
- 该目录包含:
120
-
121
- - `manifest.json`
122
- - `schema.json`
123
- - `examples/*.json`
124
- - 适配器与 CLI 源码(用于 OpenClaw 侧加载或二次封装)
125
-
126
- ## 版本与发布
127
-
128
- 版本递增(patch/minor/major):
129
-
130
- ```bash
131
- npm run weiyuan:skill:version -- patch
132
- ```
133
-
134
- 发布前校验门(manifest/schema/examples/adapter 契约):
135
-
136
- ```bash
137
- npm run weiyuan:skill:validate
138
- ```
139
-
140
- 一键发布(构建 + zip + 发布说明):
141
-
142
- ```bash
143
- npm run weiyuan:skill:release
144
- ```
145
-
146
- 说明:`weiyuan:skill:release` 会先执行校验门,未通过则阻断发布。
147
-
148
- 发布产物:
149
-
150
- - `dist/releases/weiyuan-openclaw-skill-v<version>.zip`
151
- - `dist/releases/RELEASE_NOTES_<version>.md`
152
-
153
- ## CI 工作流
154
-
155
- - `skill-ci`:push/PR 自动执行 `weiyuan:skill:validate` 与 `weiyuan:skill:build`
156
- - `skill-release`:手动触发,支持 patch/minor/major 递增后自动执行:
157
- - 版本递增并提交 `manifest.json`
158
- - 创建并推送 git tag(`v<version>`)
159
- - 生成 GitHub Release 并上传 zip/release notes
@@ -1,6 +0,0 @@
1
- {
2
- "action": "task.list",
3
- "identity": ".weiyuan.dev2",
4
- "projectId": "prj_demo"
5
- }
6
-
@@ -1,8 +0,0 @@
1
- {
2
- "action": "risk.report",
3
- "identity": ".weiyuan.dev2",
4
- "projectId": "prj_demo",
5
- "severity": "warn",
6
- "summary": "3号库信号不稳"
7
- }
8
-
@@ -1,6 +0,0 @@
1
- {
2
- "text": "我能做什么",
3
- "identity": ".weiyuan.dev2",
4
- "projectId": "prj_demo"
5
- }
6
-
@@ -1,45 +0,0 @@
1
- {
2
- "name": "weiyuan-openclaw-skill",
3
- "displayName": "微元协作 Skill",
4
- "version": "0.1.1",
5
- "description": "通过结构化 action 或自然语言 text 调用微元云服务。",
6
- "entry": {
7
- "type": "command",
8
- "command": "npm run weiyuan:skill"
9
- },
10
- "ioSchema": "./schema.json",
11
- "capabilities": {
12
- "modes": [
13
- "action",
14
- "text"
15
- ],
16
- "actions": [
17
- "init",
18
- "create",
19
- "join",
20
- "task.list",
21
- "task.take",
22
- "task.submit",
23
- "status",
24
- "risk.report",
25
- "capsule.search"
26
- ]
27
- },
28
- "examples": [
29
- "./examples/action-task-list.json",
30
- "./examples/text-what-can-i-do.json",
31
- "./examples/risk-report.json"
32
- ],
33
- "output": {
34
- "success": {
35
- "ok": true,
36
- "data": {}
37
- },
38
- "error": {
39
- "ok": false,
40
- "errorCode": "UNKNOWN_ACTION",
41
- "message": "",
42
- "rawError": ""
43
- }
44
- }
45
- }
@@ -1,104 +0,0 @@
1
- {
2
- "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "weiyuan-skill-schema-v1",
4
- "title": "Weiyuan OpenClaw Skill IO Schema",
5
- "type": "object",
6
- "properties": {
7
- "input": {
8
- "oneOf": [
9
- {
10
- "title": "ActionInput",
11
- "type": "object",
12
- "required": ["action"],
13
- "properties": {
14
- "action": {
15
- "type": "string",
16
- "enum": [
17
- "init",
18
- "create",
19
- "join",
20
- "task.list",
21
- "task.take",
22
- "task.submit",
23
- "status",
24
- "risk.report",
25
- "capsule.search"
26
- ]
27
- },
28
- "identity": { "type": "string" },
29
- "projectId": { "type": "string" },
30
- "taskId": { "type": "string" },
31
- "code": { "type": "string" },
32
- "role": { "type": "string", "enum": ["founder", "member", "guest"] },
33
- "summary": { "type": "string" },
34
- "severity": { "type": "string", "enum": ["info", "warn", "urgent"] },
35
- "capsuleId": { "type": "string" },
36
- "query": { "type": "string" },
37
- "hash": { "type": "string" },
38
- "title": { "type": "string" }
39
- },
40
- "additionalProperties": false
41
- },
42
- {
43
- "title": "TextInput",
44
- "type": "object",
45
- "required": ["text"],
46
- "properties": {
47
- "text": { "type": "string", "minLength": 1 },
48
- "identity": { "type": "string" },
49
- "projectId": { "type": "string" },
50
- "taskId": { "type": "string" },
51
- "summary": { "type": "string" },
52
- "severity": { "type": "string", "enum": ["info", "warn", "urgent"] },
53
- "hash": { "type": "string" },
54
- "query": { "type": "string" }
55
- },
56
- "additionalProperties": false
57
- }
58
- ]
59
- },
60
- "output": {
61
- "oneOf": [
62
- {
63
- "title": "SuccessOutput",
64
- "type": "object",
65
- "required": ["ok"],
66
- "properties": {
67
- "ok": { "const": true },
68
- "data": {}
69
- },
70
- "additionalProperties": true
71
- },
72
- {
73
- "title": "ErrorOutput",
74
- "type": "object",
75
- "required": ["ok", "errorCode", "message"],
76
- "properties": {
77
- "ok": { "const": false },
78
- "errorCode": {
79
- "type": "string",
80
- "enum": [
81
- "MISSING_INPUT",
82
- "INVALID_JSON",
83
- "UNKNOWN_ACTION",
84
- "CANNOT_MAP_TEXT",
85
- "MISSING_PROJECT",
86
- "MISSING_TASK",
87
- "MISSING_CODE",
88
- "MISSING_SUMMARY",
89
- "CLI_ERROR",
90
- "UNKNOWN"
91
- ]
92
- },
93
- "message": { "type": "string" },
94
- "rawError": { "type": "string" }
95
- },
96
- "additionalProperties": true
97
- }
98
- ]
99
- }
100
- },
101
- "required": ["input", "output"],
102
- "additionalProperties": false
103
- }
104
-
@@ -1,453 +0,0 @@
1
- import * as fs from "node:fs/promises"
2
- import { DEFAULT_IDENTITY_PATH, readIdentity, writeIdentity } from "./weiyuanFile"
3
- import { sha256Hex } from "./crypto"
4
- import { signedRequest } from "./http"
5
- import { bodyHashBase64, canonicalRequest, newKeyPair, signEd25519Base64 } from "./signing"
6
-
7
- type Argv = { _: string[]; flags: Record<string, string | boolean> }
8
-
9
- function parseArgv(argv: string[]): Argv {
10
- const out: Argv = { _: [], flags: {} }
11
- for (let i = 0; i < argv.length; i++) {
12
- const a = argv[i]!
13
- if (a.startsWith("--")) {
14
- const key = a.slice(2)
15
- const next = argv[i + 1]
16
- if (next && !next.startsWith("-")) {
17
- out.flags[key] = next
18
- i++
19
- } else {
20
- out.flags[key] = true
21
- }
22
- } else {
23
- out._.push(a)
24
- }
25
- }
26
- return out
27
- }
28
-
29
- function mustString(flags: Record<string, string | boolean>, key: string): string {
30
- const v = flags[key]
31
- if (typeof v !== "string" || !v) throw new Error(`missing_${key}`)
32
- return v
33
- }
34
-
35
- function optString(flags: Record<string, string | boolean>, key: string): string | undefined {
36
- const v = flags[key]
37
- return typeof v === "string" ? v : undefined
38
- }
39
-
40
- function print(obj: unknown): void {
41
- process.stdout.write(`${JSON.stringify(obj, null, 2)}\n`)
42
- }
43
-
44
- function sleep(ms: number): Promise<void> {
45
- return new Promise((resolve) => setTimeout(resolve, ms))
46
- }
47
-
48
- async function cmdInit(args: Argv): Promise<void> {
49
- const serverBaseUrl = optString(args.flags, "server") ?? "http://127.0.0.1:8787"
50
- const filePath = optString(args.flags, "out") ?? DEFAULT_IDENTITY_PATH
51
- const kp = newKeyPair()
52
- const identityHash = sha256Hex(JSON.stringify({ lobsterId: kp.lobsterId, publicKeyBase64: kp.publicKeyBase64 }))
53
-
54
- const body = { lobsterId: kp.lobsterId, publicKeyBase64: kp.publicKeyBase64, identityHash }
55
- const timestampMs = String(Date.now())
56
- const nonce = `nonce_${Math.random().toString(16).slice(2)}`
57
- const bodySha = bodyHashBase64(body)
58
- const canonical = canonicalRequest({
59
- method: "POST",
60
- pathWithQuery: "/v1/init",
61
- timestampMs,
62
- nonce,
63
- bodySha256Base64: bodySha,
64
- })
65
- const signature = signEd25519Base64(canonical, kp.secretKeyBase64)
66
-
67
- const res = await fetch(`${serverBaseUrl}/v1/init`, {
68
- method: "POST",
69
- headers: {
70
- "Content-Type": "application/json",
71
- "X-Weiyuan-Lobster-Id": kp.lobsterId,
72
- "X-Weiyuan-Timestamp": timestampMs,
73
- "X-Weiyuan-Nonce": nonce,
74
- "X-Weiyuan-Signature": signature,
75
- },
76
- body: JSON.stringify(body),
77
- })
78
- const json = await res.json().catch(() => ({}))
79
- if (res.status !== 200) throw new Error(JSON.stringify(json))
80
-
81
- await writeIdentity(filePath, {
82
- version: 1,
83
- serverBaseUrl,
84
- lobsterId: kp.lobsterId,
85
- publicKeyBase64: kp.publicKeyBase64,
86
- secretKeyBase64: kp.secretKeyBase64,
87
- identityHash,
88
- projectCursors: {},
89
- })
90
-
91
- print({ ok: true, lobsterId: kp.lobsterId, file: filePath, welcome: json.data?.welcome })
92
- }
93
-
94
- async function cmdCreateProject(args: Argv): Promise<void> {
95
- const filePath = optString(args.flags, "identity") ?? DEFAULT_IDENTITY_PATH
96
- const identity = await readIdentity(filePath)
97
- const name = mustString(args.flags, "name")
98
- const goal = optString(args.flags, "goal") ?? ""
99
- const dna = mustString(args.flags, "dna")
100
- const dnaHash = sha256Hex(dna)
101
-
102
- const { status, json } = await signedRequest({
103
- serverBaseUrl: identity.serverBaseUrl,
104
- method: "POST",
105
- pathWithQuery: "/v1/projects",
106
- lobsterId: identity.lobsterId,
107
- secretKeyBase64: identity.secretKeyBase64,
108
- body: { name, goal, dnaHash },
109
- })
110
- if (status !== 200) throw new Error(JSON.stringify(json))
111
- print({ ok: true, projectId: json.data.projectId })
112
- }
113
-
114
- async function cmdInvite(args: Argv): Promise<void> {
115
- const filePath = optString(args.flags, "identity") ?? DEFAULT_IDENTITY_PATH
116
- const identity = await readIdentity(filePath)
117
- const projectId = mustString(args.flags, "project")
118
- const role = mustString(args.flags, "role")
119
- const { status, json } = await signedRequest({
120
- serverBaseUrl: identity.serverBaseUrl,
121
- method: "POST",
122
- pathWithQuery: "/v1/invites",
123
- lobsterId: identity.lobsterId,
124
- secretKeyBase64: identity.secretKeyBase64,
125
- body: { projectId, role },
126
- })
127
- if (status !== 200) throw new Error(JSON.stringify(json))
128
- print({ ok: true, code: json.data.code })
129
- }
130
-
131
- async function cmdJoin(args: Argv): Promise<void> {
132
- const filePath = optString(args.flags, "identity") ?? DEFAULT_IDENTITY_PATH
133
- const identity = await readIdentity(filePath)
134
- const projectId = mustString(args.flags, "project")
135
- const code = mustString(args.flags, "code")
136
- const { status, json } = await signedRequest({
137
- serverBaseUrl: identity.serverBaseUrl,
138
- method: "POST",
139
- pathWithQuery: `/v1/projects/${projectId}/join`,
140
- lobsterId: identity.lobsterId,
141
- secretKeyBase64: identity.secretKeyBase64,
142
- body: { code },
143
- })
144
- if (status !== 200) throw new Error(JSON.stringify(json))
145
- print({ ok: true, welcome: json.data?.welcome })
146
- }
147
-
148
- async function cmdTaskList(args: Argv): Promise<void> {
149
- const filePath = optString(args.flags, "identity") ?? DEFAULT_IDENTITY_PATH
150
- const identity = await readIdentity(filePath)
151
- const projectId = mustString(args.flags, "project")
152
- const { status, json } = await signedRequest({
153
- serverBaseUrl: identity.serverBaseUrl,
154
- method: "GET",
155
- pathWithQuery: `/v1/projects/${projectId}/tasks`,
156
- lobsterId: identity.lobsterId,
157
- secretKeyBase64: identity.secretKeyBase64,
158
- })
159
- if (status !== 200) throw new Error(JSON.stringify(json))
160
- print(json.data)
161
- }
162
-
163
- async function cmdTaskCreate(args: Argv): Promise<void> {
164
- const filePath = optString(args.flags, "identity") ?? DEFAULT_IDENTITY_PATH
165
- const identity = await readIdentity(filePath)
166
- const projectId = mustString(args.flags, "project")
167
- const title = mustString(args.flags, "title")
168
- const desc = optString(args.flags, "desc")
169
- const { status, json } = await signedRequest({
170
- serverBaseUrl: identity.serverBaseUrl,
171
- method: "POST",
172
- pathWithQuery: `/v1/projects/${projectId}/tasks`,
173
- lobsterId: identity.lobsterId,
174
- secretKeyBase64: identity.secretKeyBase64,
175
- body: { title, desc },
176
- })
177
- if (status !== 200) throw new Error(JSON.stringify(json))
178
- print({ ok: true, taskId: json.data.taskId })
179
- }
180
-
181
- async function cmdTaskTake(args: Argv): Promise<void> {
182
- const filePath = optString(args.flags, "identity") ?? DEFAULT_IDENTITY_PATH
183
- const identity = await readIdentity(filePath)
184
- const projectId = mustString(args.flags, "project")
185
- const taskId = mustString(args.flags, "task")
186
- const { status, json } = await signedRequest({
187
- serverBaseUrl: identity.serverBaseUrl,
188
- method: "POST",
189
- pathWithQuery: `/v1/projects/${projectId}/tasks/${taskId}/take`,
190
- lobsterId: identity.lobsterId,
191
- secretKeyBase64: identity.secretKeyBase64,
192
- body: {},
193
- })
194
- if (status !== 200) throw new Error(JSON.stringify(json))
195
- print({ ok: true })
196
- }
197
-
198
- async function cmdTaskAbandon(args: Argv): Promise<void> {
199
- const filePath = optString(args.flags, "identity") ?? DEFAULT_IDENTITY_PATH
200
- const identity = await readIdentity(filePath)
201
- const projectId = mustString(args.flags, "project")
202
- const taskId = mustString(args.flags, "task")
203
- const { status, json } = await signedRequest({
204
- serverBaseUrl: identity.serverBaseUrl,
205
- method: "POST",
206
- pathWithQuery: `/v1/projects/${projectId}/tasks/${taskId}/abandon`,
207
- lobsterId: identity.lobsterId,
208
- secretKeyBase64: identity.secretKeyBase64,
209
- body: {},
210
- })
211
- if (status !== 200) throw new Error(JSON.stringify(json))
212
- print({ ok: true })
213
- }
214
-
215
- async function cmdTaskSubmit(args: Argv): Promise<void> {
216
- const filePath = optString(args.flags, "identity") ?? DEFAULT_IDENTITY_PATH
217
- const identity = await readIdentity(filePath)
218
- const projectId = mustString(args.flags, "project")
219
- const taskId = mustString(args.flags, "task")
220
- const contentHash = mustString(args.flags, "hash")
221
- const contentType = optString(args.flags, "type") ?? "text/markdown"
222
- const { status, json } = await signedRequest({
223
- serverBaseUrl: identity.serverBaseUrl,
224
- method: "POST",
225
- pathWithQuery: `/v1/projects/${projectId}/tasks/${taskId}/submit`,
226
- lobsterId: identity.lobsterId,
227
- secretKeyBase64: identity.secretKeyBase64,
228
- body: { artifacts: [{ contentHash, contentType }] },
229
- })
230
- if (status !== 200) throw new Error(JSON.stringify(json))
231
- print({ ok: true })
232
- }
233
-
234
- async function cmdStatus(args: Argv): Promise<void> {
235
- const filePath = optString(args.flags, "identity") ?? DEFAULT_IDENTITY_PATH
236
- const identity = await readIdentity(filePath)
237
- const projectId = mustString(args.flags, "project")
238
- const { status, json } = await signedRequest({
239
- serverBaseUrl: identity.serverBaseUrl,
240
- method: "GET",
241
- pathWithQuery: `/v1/projects/${projectId}/status`,
242
- lobsterId: identity.lobsterId,
243
- secretKeyBase64: identity.secretKeyBase64,
244
- })
245
- if (status !== 200) throw new Error(JSON.stringify(json))
246
- print(json.data)
247
- }
248
-
249
- async function cmdMemberList(args: Argv): Promise<void> {
250
- const filePath = optString(args.flags, "identity") ?? DEFAULT_IDENTITY_PATH
251
- const identity = await readIdentity(filePath)
252
- const projectId = mustString(args.flags, "project")
253
- const { status, json } = await signedRequest({
254
- serverBaseUrl: identity.serverBaseUrl,
255
- method: "GET",
256
- pathWithQuery: `/v1/projects/${projectId}/members`,
257
- lobsterId: identity.lobsterId,
258
- secretKeyBase64: identity.secretKeyBase64,
259
- })
260
- if (status !== 200) throw new Error(JSON.stringify(json))
261
- print(json.data)
262
- }
263
-
264
- async function cmdSync(args: Argv): Promise<void> {
265
- const filePath = optString(args.flags, "identity") ?? DEFAULT_IDENTITY_PATH
266
- const identity = await readIdentity(filePath)
267
- const projectId = mustString(args.flags, "project")
268
- const follow = Boolean(args.flags.follow)
269
- const intervalMs = Number(optString(args.flags, "interval") ?? 2000)
270
-
271
- const runOnce = async (): Promise<void> => {
272
- const cursor = identity.projectCursors[projectId] ?? 0
273
- const { status, json } = await signedRequest({
274
- serverBaseUrl: identity.serverBaseUrl,
275
- method: "GET",
276
- pathWithQuery: `/v1/projects/${projectId}/events?cursor=${cursor}`,
277
- lobsterId: identity.lobsterId,
278
- secretKeyBase64: identity.secretKeyBase64,
279
- })
280
- if (status !== 200) throw new Error(JSON.stringify(json))
281
- identity.projectCursors[projectId] = Number(json.data.cursor ?? cursor)
282
- await writeIdentity(filePath, identity)
283
- print({ ok: true, cursor: identity.projectCursors[projectId], events: json.data.events })
284
- }
285
-
286
- await runOnce()
287
- if (!follow) return
288
- while (true) {
289
- await sleep(intervalMs)
290
- await runOnce()
291
- }
292
- }
293
-
294
- async function cmdRiskReport(args: Argv): Promise<void> {
295
- const filePath = optString(args.flags, "identity") ?? DEFAULT_IDENTITY_PATH
296
- const identity = await readIdentity(filePath)
297
- const projectId = mustString(args.flags, "project")
298
- const severity = mustString(args.flags, "severity")
299
- const summary = mustString(args.flags, "summary")
300
- const taskId = optString(args.flags, "task")
301
- const { status, json } = await signedRequest({
302
- serverBaseUrl: identity.serverBaseUrl,
303
- method: "POST",
304
- pathWithQuery: `/v1/projects/${projectId}/risks`,
305
- lobsterId: identity.lobsterId,
306
- secretKeyBase64: identity.secretKeyBase64,
307
- body: { severity, summary, taskId },
308
- })
309
- if (status !== 200) throw new Error(JSON.stringify(json))
310
- print({ ok: true, riskId: json.data.riskId })
311
- }
312
-
313
- async function cmdRiskList(args: Argv): Promise<void> {
314
- const filePath = optString(args.flags, "identity") ?? DEFAULT_IDENTITY_PATH
315
- const identity = await readIdentity(filePath)
316
- const projectId = mustString(args.flags, "project")
317
- const { status, json } = await signedRequest({
318
- serverBaseUrl: identity.serverBaseUrl,
319
- method: "GET",
320
- pathWithQuery: `/v1/projects/${projectId}/risks`,
321
- lobsterId: identity.lobsterId,
322
- secretKeyBase64: identity.secretKeyBase64,
323
- })
324
- if (status !== 200) throw new Error(JSON.stringify(json))
325
- print(json.data)
326
- }
327
-
328
- async function cmdCapsulePublish(args: Argv): Promise<void> {
329
- const filePath = optString(args.flags, "identity") ?? DEFAULT_IDENTITY_PATH
330
- const identity = await readIdentity(filePath)
331
- const name = mustString(args.flags, "name")
332
- const kind = mustString(args.flags, "kind")
333
- const version = optString(args.flags, "version") ?? "v1"
334
- const packagePath = mustString(args.flags, "file")
335
- const bytes = await fs.readFile(packagePath)
336
- const packageHash = sha256Hex(bytes)
337
- const manifest = {
338
- name,
339
- kind,
340
- version,
341
- dnaTags: optString(args.flags, "dnaTags")?.split(",").map((x) => x.trim()).filter(Boolean) ?? [],
342
- projectTags: optString(args.flags, "projectTags")?.split(",").map((x) => x.trim()).filter(Boolean) ?? [],
343
- }
344
- const manifestHash = sha256Hex(JSON.stringify(manifest))
345
- const packageBytesBase64 = Buffer.from(bytes).toString("base64")
346
-
347
- const { status, json } = await signedRequest({
348
- serverBaseUrl: identity.serverBaseUrl,
349
- method: "POST",
350
- pathWithQuery: "/v1/capsules",
351
- lobsterId: identity.lobsterId,
352
- secretKeyBase64: identity.secretKeyBase64,
353
- body: { ...manifest, manifestHash, packageHash, packageBytesBase64 },
354
- })
355
- if (status !== 200) throw new Error(JSON.stringify(json))
356
- print({ ok: true, capsuleId: json.data.capsuleId, manifestHash, packageHash })
357
- }
358
-
359
- async function cmdCapsuleSearch(args: Argv): Promise<void> {
360
- const filePath = optString(args.flags, "identity") ?? DEFAULT_IDENTITY_PATH
361
- const identity = await readIdentity(filePath)
362
- const query = optString(args.flags, "query") ?? ""
363
- const { status, json } = await signedRequest({
364
- serverBaseUrl: identity.serverBaseUrl,
365
- method: "GET",
366
- pathWithQuery: `/v1/capsules?query=${encodeURIComponent(query)}`,
367
- lobsterId: identity.lobsterId,
368
- secretKeyBase64: identity.secretKeyBase64,
369
- })
370
- if (status !== 200) throw new Error(JSON.stringify(json))
371
- print(json.data)
372
- }
373
-
374
- async function cmdCapsulePull(args: Argv): Promise<void> {
375
- const filePath = optString(args.flags, "identity") ?? DEFAULT_IDENTITY_PATH
376
- const identity = await readIdentity(filePath)
377
- const capsuleId = mustString(args.flags, "id")
378
- const out = optString(args.flags, "out")
379
- const { status, json } = await signedRequest({
380
- serverBaseUrl: identity.serverBaseUrl,
381
- method: "POST",
382
- pathWithQuery: `/v1/capsules/${capsuleId}/pull`,
383
- lobsterId: identity.lobsterId,
384
- secretKeyBase64: identity.secretKeyBase64,
385
- body: {},
386
- })
387
- if (status !== 200) throw new Error(JSON.stringify(json))
388
- const capsule = json.data.capsule
389
- if (out && capsule?.packageBytesBase64) {
390
- await fs.writeFile(out, Buffer.from(capsule.packageBytesBase64, "base64"))
391
- }
392
- print({ ok: true, capsule })
393
- }
394
-
395
- async function cmdCapsuleReport(args: Argv): Promise<void> {
396
- const filePath = optString(args.flags, "identity") ?? DEFAULT_IDENTITY_PATH
397
- const identity = await readIdentity(filePath)
398
- const capsuleId = mustString(args.flags, "id")
399
- const success = (optString(args.flags, "success") ?? "true").toLowerCase() !== "false"
400
- const { status, json } = await signedRequest({
401
- serverBaseUrl: identity.serverBaseUrl,
402
- method: "POST",
403
- pathWithQuery: `/v1/capsules/${capsuleId}/report`,
404
- lobsterId: identity.lobsterId,
405
- secretKeyBase64: identity.secretKeyBase64,
406
- body: { success },
407
- })
408
- if (status !== 200) throw new Error(JSON.stringify(json))
409
- print({ ok: true, capsuleId })
410
- }
411
-
412
- export async function runCli(argv: string[]): Promise<void> {
413
- const args = parseArgv(argv)
414
- const [cmd, sub] = args._.length ? [args._[0], args._[1]] : [undefined, undefined]
415
-
416
- if (!cmd) throw new Error("missing_command")
417
-
418
- if (cmd === "init") return await cmdInit(args)
419
- if (cmd === "create") return await cmdCreateProject(args)
420
- if (cmd === "invite") return await cmdInvite(args)
421
- if (cmd === "join") return await cmdJoin(args)
422
- if (cmd === "status") return await cmdStatus(args)
423
- if (cmd === "sync") return await cmdSync(args)
424
- if (cmd === "member") {
425
- if (sub === "list") return await cmdMemberList(args)
426
- throw new Error("unknown_member_subcommand")
427
- }
428
-
429
- if (cmd === "risk") {
430
- if (sub === "report") return await cmdRiskReport(args)
431
- if (sub === "list") return await cmdRiskList(args)
432
- throw new Error("unknown_risk_subcommand")
433
- }
434
-
435
- if (cmd === "capsule") {
436
- if (sub === "publish") return await cmdCapsulePublish(args)
437
- if (sub === "search") return await cmdCapsuleSearch(args)
438
- if (sub === "pull") return await cmdCapsulePull(args)
439
- if (sub === "report") return await cmdCapsuleReport(args)
440
- throw new Error("unknown_capsule_subcommand")
441
- }
442
-
443
- if (cmd === "task") {
444
- if (sub === "list") return await cmdTaskList(args)
445
- if (sub === "create") return await cmdTaskCreate(args)
446
- if (sub === "take") return await cmdTaskTake(args)
447
- if (sub === "abandon") return await cmdTaskAbandon(args)
448
- if (sub === "submit") return await cmdTaskSubmit(args)
449
- throw new Error("unknown_task_subcommand")
450
- }
451
-
452
- throw new Error("unknown_command")
453
- }
@@ -1,7 +0,0 @@
1
- import { runCli } from "./cli"
2
-
3
- runCli(process.argv.slice(2)).catch((e) => {
4
- process.stderr.write(String(e instanceof Error ? e.stack ?? e.message : e))
5
- process.exit(1)
6
- })
7
-
@@ -1,14 +0,0 @@
1
- import * as crypto from "node:crypto"
2
-
3
- export function sha256Base64(input: string | Uint8Array): string {
4
- const h = crypto.createHash("sha256")
5
- h.update(input)
6
- return h.digest("base64")
7
- }
8
-
9
- export function sha256Hex(input: string | Uint8Array): string {
10
- const h = crypto.createHash("sha256")
11
- h.update(input)
12
- return h.digest("hex")
13
- }
14
-
@@ -1,45 +0,0 @@
1
- import { bodyHashBase64, canonicalRequest, signEd25519Base64 } from "./signing"
2
-
3
- export async function signedRequest(opts: {
4
- serverBaseUrl: string
5
- method: "GET" | "POST"
6
- pathWithQuery: string
7
- lobsterId: string
8
- secretKeyBase64: string
9
- body?: unknown
10
- }): Promise<any> {
11
- const timestampMs = String(Date.now())
12
- const nonce = `nonce_${Math.random().toString(16).slice(2)}`
13
- const bodySha = bodyHashBase64(opts.body)
14
- const canonical = canonicalRequest({
15
- method: opts.method,
16
- pathWithQuery: opts.pathWithQuery,
17
- timestampMs,
18
- nonce,
19
- bodySha256Base64: bodySha,
20
- })
21
- const signature = signEd25519Base64(canonical, opts.secretKeyBase64)
22
-
23
- const headers: Record<string, string> = {
24
- "X-Weiyuan-Lobster-Id": opts.lobsterId,
25
- "X-Weiyuan-Timestamp": timestampMs,
26
- "X-Weiyuan-Nonce": nonce,
27
- "X-Weiyuan-Signature": signature,
28
- }
29
- if (opts.body) headers["Content-Type"] = "application/json"
30
-
31
- const res = await fetch(`${opts.serverBaseUrl}${opts.pathWithQuery}`, {
32
- method: opts.method,
33
- headers,
34
- body: opts.body ? JSON.stringify(opts.body) : undefined,
35
- })
36
- const text = await res.text()
37
- let json: any
38
- try {
39
- json = JSON.parse(text)
40
- } catch {
41
- json = { raw: text }
42
- }
43
- return { status: res.status, json }
44
- }
45
-
@@ -1,38 +0,0 @@
1
- import naclImport from "tweetnacl"
2
- import { sha256Base64, sha256Hex } from "./crypto"
3
-
4
- const nacl = (naclImport as unknown as any).default ?? naclImport
5
-
6
- function b64(u8: Uint8Array): string {
7
- return Buffer.from(u8).toString("base64")
8
- }
9
-
10
- export function canonicalRequest(opts: {
11
- method: string
12
- pathWithQuery: string
13
- timestampMs: string
14
- nonce: string
15
- bodySha256Base64: string
16
- }): string {
17
- return [opts.method.toUpperCase(), opts.pathWithQuery, opts.timestampMs, opts.nonce, opts.bodySha256Base64].join("\n")
18
- }
19
-
20
- export function signEd25519Base64(messageUtf8: string, secretKeyBase64: string): string {
21
- const sk = Buffer.from(secretKeyBase64, "base64")
22
- const sig = nacl.sign.detached(new Uint8Array(Buffer.from(messageUtf8, "utf8")), new Uint8Array(sk))
23
- return b64(sig)
24
- }
25
-
26
- export function newKeyPair(): { publicKeyBase64: string; secretKeyBase64: string; lobsterId: string } {
27
- const kp = nacl.sign.keyPair()
28
- const publicKeyBase64 = b64(kp.publicKey)
29
- const secretKeyBase64 = b64(kp.secretKey)
30
- const lobsterId = `lob_${sha256Hex(kp.publicKey).slice(0, 12)}`
31
- return { publicKeyBase64, secretKeyBase64, lobsterId }
32
- }
33
-
34
- export function bodyHashBase64(body: unknown | undefined): string {
35
- const bodyStr = body ? JSON.stringify(body) : ""
36
- return sha256Base64(bodyStr)
37
- }
38
-
@@ -1,173 +0,0 @@
1
- import { runCli } from "./cli"
2
-
3
- type SkillInput = {
4
- action?: string
5
- text?: string
6
- identity?: string
7
- projectId?: string
8
- taskId?: string
9
- code?: string
10
- role?: string
11
- summary?: string
12
- severity?: string
13
- capsuleId?: string
14
- query?: string
15
- hash?: string
16
- title?: string
17
- }
18
-
19
- type SkillSuccess = {
20
- ok: true
21
- data: unknown
22
- }
23
-
24
- type SkillError = {
25
- ok: false
26
- errorCode: string
27
- message: string
28
- rawError?: string
29
- }
30
-
31
- function readAllStdin(): Promise<string> {
32
- return new Promise((resolve) => {
33
- const chunks: Buffer[] = []
34
- process.stdin.on("data", (c) => chunks.push(Buffer.from(c)))
35
- process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")))
36
- })
37
- }
38
-
39
- function fromText(input: SkillInput): string[] {
40
- const text = (input.text ?? "").trim()
41
- const lower = text.toLowerCase()
42
- const identity = input.identity ?? ".weiyuan"
43
-
44
- if (!text) throw new Error("missing_text")
45
-
46
- if (lower.includes("我能做什么") || lower.includes("任务列表") || lower.includes("还有啥活")) {
47
- if (!input.projectId) throw new Error("missing_projectId")
48
- return ["task", "list", "--identity", identity, "--project", input.projectId]
49
- }
50
- if (lower.includes("我领") || lower.includes("我来")) {
51
- if (!input.projectId || !input.taskId) throw new Error("missing_projectId_or_taskId")
52
- return ["task", "take", "--identity", identity, "--project", input.projectId, "--task", input.taskId]
53
- }
54
- if (lower.includes("搞定") || lower.includes("完事") || lower.includes("看图") || lower.includes("已修好")) {
55
- if (!input.projectId || !input.taskId) throw new Error("missing_projectId_or_taskId")
56
- const hash = input.hash ?? "hash-auto"
57
- return ["task", "submit", "--identity", identity, "--project", input.projectId, "--task", input.taskId, "--hash", hash]
58
- }
59
- if (lower.includes("卡住") || lower.includes("帮忙")) {
60
- if (!input.projectId) throw new Error("missing_projectId")
61
- const summary = input.summary ?? text
62
- return ["risk", "report", "--identity", identity, "--project", input.projectId, "--severity", input.severity ?? "warn", "--summary", summary]
63
- }
64
- if (lower.includes("进度") || lower.includes("现在怎么样")) {
65
- if (!input.projectId) throw new Error("missing_projectId")
66
- return ["status", "--identity", identity, "--project", input.projectId]
67
- }
68
- if (lower.includes("老规矩") || lower.includes("上次那套")) {
69
- return ["capsule", "search", "--identity", identity, "--query", input.query ?? ""]
70
- }
71
-
72
- if (input.projectId) {
73
- return ["task", "list", "--identity", identity, "--project", input.projectId]
74
- }
75
- throw new Error("cannot_map_text_without_project_context")
76
- }
77
-
78
- function fromAction(input: SkillInput): string[] {
79
- const identity = input.identity ?? ".weiyuan"
80
- switch (input.action) {
81
- case "init":
82
- return ["init", "--server", "http://127.0.0.1:8787", "--out", identity]
83
- case "create":
84
- if (!input.title) throw new Error("missing_title")
85
- return ["create", "--identity", identity, "--name", input.title, "--goal", "MVP", "--dna", "稳当第一"]
86
- case "join":
87
- if (!input.projectId || !input.code) throw new Error("missing_projectId_or_code")
88
- return ["join", "--identity", identity, "--project", input.projectId, "--code", input.code]
89
- case "task.take":
90
- if (!input.projectId || !input.taskId) throw new Error("missing_projectId_or_taskId")
91
- return ["task", "take", "--identity", identity, "--project", input.projectId, "--task", input.taskId]
92
- case "task.submit":
93
- if (!input.projectId || !input.taskId) throw new Error("missing_projectId_or_taskId")
94
- return ["task", "submit", "--identity", identity, "--project", input.projectId, "--task", input.taskId, "--hash", input.hash ?? "hash-auto"]
95
- case "task.list":
96
- if (!input.projectId) throw new Error("missing_projectId")
97
- return ["task", "list", "--identity", identity, "--project", input.projectId]
98
- case "status":
99
- if (!input.projectId) throw new Error("missing_projectId")
100
- return ["status", "--identity", identity, "--project", input.projectId]
101
- case "risk.report":
102
- if (!input.projectId || !input.summary) throw new Error("missing_projectId_or_summary")
103
- return ["risk", "report", "--identity", identity, "--project", input.projectId, "--severity", input.severity ?? "warn", "--summary", input.summary]
104
- case "capsule.search":
105
- return ["capsule", "search", "--identity", identity, "--query", input.query ?? ""]
106
- default:
107
- throw new Error("unknown_action")
108
- }
109
- }
110
-
111
- async function main(): Promise<void> {
112
- const raw = await readAllStdin()
113
- if (!raw.trim()) throw new Error("missing_input")
114
- const input = JSON.parse(raw) as SkillInput
115
- const argv = input.action ? fromAction(input) : fromText(input)
116
-
117
- let captured = ""
118
- const originalWrite = process.stdout.write.bind(process.stdout)
119
- ;(process.stdout as any).write = (chunk: any, ...rest: any[]) => {
120
- captured += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk)
121
- return true
122
- }
123
-
124
- try {
125
- await runCli(argv)
126
- } finally {
127
- ;(process.stdout as any).write = originalWrite
128
- }
129
-
130
- let data: unknown = captured.trim()
131
- try {
132
- data = JSON.parse(captured)
133
- } catch {}
134
- const out: SkillSuccess = { ok: true, data }
135
- process.stdout.write(`${JSON.stringify(out, null, 2)}\n`)
136
- }
137
-
138
- function mapError(err: unknown): SkillError {
139
- const rawError = String(err instanceof Error ? err.message : err)
140
- if (rawError.includes("missing_input")) {
141
- return { ok: false, errorCode: "MISSING_INPUT", message: "Missing skill JSON input.", rawError }
142
- }
143
- if (rawError.includes("Unexpected token") || rawError.includes("JSON")) {
144
- return { ok: false, errorCode: "INVALID_JSON", message: "Invalid JSON input.", rawError }
145
- }
146
- if (rawError.includes("unknown_action")) {
147
- return { ok: false, errorCode: "UNKNOWN_ACTION", message: "Unknown action for skill adapter.", rawError }
148
- }
149
- if (rawError.includes("cannot_map_text")) {
150
- return { ok: false, errorCode: "CANNOT_MAP_TEXT", message: "Cannot map text intent. Add project/task context.", rawError }
151
- }
152
- if (rawError.includes("missing_project")) {
153
- return { ok: false, errorCode: "MISSING_PROJECT", message: "Missing projectId.", rawError }
154
- }
155
- if (rawError.includes("taskId")) {
156
- return { ok: false, errorCode: "MISSING_TASK", message: "Missing taskId.", rawError }
157
- }
158
- if (rawError.includes("missing_projectId_or_code") || rawError.includes("missing_code")) {
159
- return { ok: false, errorCode: "MISSING_CODE", message: "Missing invitation code.", rawError }
160
- }
161
- if (rawError.includes("missing_projectId_or_summary")) {
162
- return { ok: false, errorCode: "MISSING_SUMMARY", message: "Missing risk summary.", rawError }
163
- }
164
- if (rawError.includes("unknown_") || rawError.includes("missing_")) {
165
- return { ok: false, errorCode: "CLI_ERROR", message: "CLI arguments are incomplete or unsupported.", rawError }
166
- }
167
- return { ok: false, errorCode: "UNKNOWN", message: "Skill execution failed.", rawError }
168
- }
169
-
170
- main().catch((e) => {
171
- process.stdout.write(`${JSON.stringify(mapError(e), null, 2)}\n`)
172
- process.exit(1)
173
- })
@@ -1,10 +0,0 @@
1
- export type WeiyuanIdentityFileV1 = {
2
- version: 1
3
- serverBaseUrl: string
4
- lobsterId: string
5
- publicKeyBase64: string
6
- secretKeyBase64: string
7
- identityHash: string
8
- projectCursors: Record<string, number>
9
- }
10
-
@@ -1,25 +0,0 @@
1
- import * as fs from "node:fs/promises"
2
- import * as path from "node:path"
3
- import type { WeiyuanIdentityFileV1 } from "./types"
4
-
5
- export const DEFAULT_IDENTITY_PATH = ".weiyuan"
6
-
7
- export async function readIdentity(filePath: string): Promise<WeiyuanIdentityFileV1> {
8
- const raw = await fs.readFile(filePath, "utf8")
9
- const parsed = JSON.parse(raw) as WeiyuanIdentityFileV1
10
- if (parsed.version !== 1) {
11
- throw new Error("unsupported_identity_version")
12
- }
13
- return parsed
14
- }
15
-
16
- export async function writeIdentity(filePath: string, data: WeiyuanIdentityFileV1): Promise<void> {
17
- await fs.mkdir(path.dirname(path.resolve(filePath)), { recursive: true })
18
- const tmp = `${filePath}.tmp`
19
- await fs.writeFile(tmp, JSON.stringify(data, null, 2), "utf8")
20
- try {
21
- await fs.unlink(filePath)
22
- } catch {}
23
- await fs.rename(tmp, filePath)
24
- }
25
-