@lingerai/cli 0.1.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lingerai/cli",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Linger 平台命令行工具——让通用 Agent(Hermes / OpenClaw / Claude Code / Cursor)用一条命令接入 Linger:登录、查钱包、发单接单、管理能力。",
5
5
  "type": "module",
6
6
  "bin": {
package/src/api.js CHANGED
@@ -160,6 +160,23 @@ export async function runtimeAct(baseUrl, token, action, payload, idempotencyKey
160
160
  // 能力上架(独立端点 · 卖方)
161
161
  // ============================================================
162
162
 
163
+ /**
164
+ * 上报技能清单(卖方·A 流程第一步)。
165
+ * 把 agent 自己会的技能写进平台 agents.skill_catalog,供网页「AI 识别」挑能力卡。
166
+ * 用 agent token 调用时后端从 token 推导 agent_id(agent 不必知道自己 UUID)。
167
+ * @param {Array<{name:string,description:string}>} skills 技能清单
168
+ */
169
+ export async function reportSkills(baseUrl, token, skills, idempotencyKey, opts = {}) {
170
+ return authedPost(
171
+ baseUrl,
172
+ '/api/v1/capabilities/skill_catalog',
173
+ token,
174
+ { skills },
175
+ idempotencyKey,
176
+ opts
177
+ );
178
+ }
179
+
163
180
  /**
164
181
  * 创建能力卡草稿(卖方)。
165
182
  * @param {object} capData CreateCapabilityRequest 字段(agent_id + title 必填,其余草稿可空)
@@ -0,0 +1,174 @@
1
+ // -*- linger auth login 本地 loopback 回调页(三状态 HTML)-*-
2
+ //
3
+ // 设计:对齐 A2A 平台网页设计规范(a2a-frontend/src/shared/tokens.css)——
4
+ // 白底(#F5F6F8 / #FFFFFF)+ 近黑文字(#0F1419 / #4B5563)+ 黑白为主、克制。
5
+ // 不用桌面端 Aria 琥珀金(那是另一条产品线的品牌色 · 2026-06-20 产品负责人纠正)。
6
+ // 参考飞书/主流 OAuth 成功页:居中、一屏、纯内联零外部资源(localhost 离线可渲染)、
7
+ // 图标用内联 SVG、动画包 prefers-reduced-motion 降级、文案极简(每页 1 句 caption)。
8
+ //
9
+ // 三个常量对应 oauth.js loopback handler 的三个 res.end:
10
+ // SUCCESS — 拿到 code(绑定成功 · 带「打开 Linger 主页」按钮)
11
+ // ERROR — error(授权未完成 · 回命令行重试)
12
+ // MISSING — 缺授权码(回调异常)
13
+
14
+ const CALLBACK_HTML_SUCCESS = `<!DOCTYPE html>
15
+ <html lang="zh-CN">
16
+ <head>
17
+ <meta charset="utf-8">
18
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
19
+ <title>绑定成功 · Linger</title>
20
+ <style>
21
+ *{margin:0;padding:0;box-sizing:border-box}
22
+ html,body{height:100%}
23
+ body{
24
+ font-family:-apple-system,BlinkMacSystemFont,"SF Pro Display","SF Pro Text","Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei",system-ui,sans-serif;
25
+ background:#F5F6F8;color:#0F1419;
26
+ display:flex;align-items:center;justify-content:center;
27
+ min-height:100vh;min-height:100dvh;
28
+ padding:24px;line-height:1.6;-webkit-font-smoothing:antialiased;
29
+ }
30
+ .card{
31
+ width:100%;max-width:380px;text-align:center;
32
+ padding:8px;opacity:0;transform:translateY(10px);
33
+ animation:rise .6s cubic-bezier(.16,1,.3,1) .05s forwards;
34
+ }
35
+ .badge{
36
+ width:72px;height:72px;border-radius:999px;margin:0 auto 32px;
37
+ display:flex;align-items:center;justify-content:center;
38
+ background:rgba(15,20,25,.06);
39
+ }
40
+ .badge svg{width:34px;height:34px}
41
+ .check{
42
+ fill:none;stroke:#0F1419;stroke-width:2.5;
43
+ stroke-linecap:round;stroke-linejoin:round;
44
+ stroke-dasharray:32;stroke-dashoffset:32;
45
+ animation:draw .5s cubic-bezier(.65,0,.45,1) .35s forwards;
46
+ }
47
+ h1{font-size:21px;font-weight:500;letter-spacing:.02em;margin-bottom:12px}
48
+ p{font-size:14px;color:#4B5563;letter-spacing:.02em;margin-bottom:36px}
49
+ .btn{
50
+ display:inline-block;padding:13px 32px;border-radius:8px;
51
+ background:#0F1419;color:#fff;
52
+ font-size:14px;font-weight:500;letter-spacing:.02em;text-decoration:none;
53
+ transition:transform .2s ease-out,background .2s ease-out;
54
+ }
55
+ .btn:hover{background:#2C3340}
56
+ .btn:active{transform:scale(.97)}
57
+ @keyframes rise{to{opacity:1;transform:translateY(0)}}
58
+ @keyframes draw{to{stroke-dashoffset:0}}
59
+ @media (prefers-reduced-motion:reduce){
60
+ .card{animation:none;opacity:1;transform:none}
61
+ .check{animation:none;stroke-dashoffset:0}
62
+ .btn{transition:none}
63
+ }
64
+ </style>
65
+ </head>
66
+ <body>
67
+ <main class="card">
68
+ <div class="badge" aria-hidden="true">
69
+ <svg viewBox="0 0 24 24"><path class="check" d="M5 13l4 4L19 7"/></svg>
70
+ </div>
71
+ <h1>绑定成功 · 已连接 Linger</h1>
72
+ <p>可以关闭此页面,回到命令行</p>
73
+ <a class="btn" href="https://a2a.linger.chimap.cn">打开 Linger 主页</a>
74
+ </main>
75
+ </body>
76
+ </html>`;
77
+
78
+ const CALLBACK_HTML_ERROR = `<!DOCTYPE html>
79
+ <html lang="zh-CN">
80
+ <head>
81
+ <meta charset="utf-8">
82
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
83
+ <title>授权未完成 · Linger</title>
84
+ <style>
85
+ *{margin:0;padding:0;box-sizing:border-box}
86
+ html,body{height:100%}
87
+ body{
88
+ font-family:-apple-system,BlinkMacSystemFont,"SF Pro Display","SF Pro Text","Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei",system-ui,sans-serif;
89
+ background:#F5F6F8;color:#0F1419;
90
+ display:flex;align-items:center;justify-content:center;
91
+ min-height:100vh;min-height:100dvh;
92
+ padding:24px;line-height:1.6;-webkit-font-smoothing:antialiased;
93
+ }
94
+ .card{
95
+ width:100%;max-width:380px;text-align:center;padding:8px;
96
+ opacity:0;transform:translateY(10px);
97
+ animation:rise .6s cubic-bezier(.16,1,.3,1) .05s forwards;
98
+ }
99
+ .badge{
100
+ width:72px;height:72px;border-radius:999px;margin:0 auto 32px;
101
+ display:flex;align-items:center;justify-content:center;
102
+ background:rgba(15,20,25,.05);
103
+ }
104
+ .badge svg{width:32px;height:32px}
105
+ .mark{fill:none;stroke:#0F1419;stroke-width:2.2;stroke-linecap:round;opacity:.55}
106
+ h1{font-size:21px;font-weight:500;letter-spacing:.02em;margin-bottom:12px}
107
+ p{font-size:14px;color:#4B5563;letter-spacing:.02em}
108
+ @keyframes rise{to{opacity:1;transform:translateY(0)}}
109
+ @media (prefers-reduced-motion:reduce){.card{animation:none;opacity:1;transform:none}}
110
+ </style>
111
+ </head>
112
+ <body>
113
+ <main class="card">
114
+ <div class="badge" aria-hidden="true">
115
+ <svg viewBox="0 0 24 24">
116
+ <line class="mark" x1="12" y1="7" x2="12" y2="13"/>
117
+ <line class="mark" x1="12" y1="17" x2="12" y2="17"/>
118
+ </svg>
119
+ </div>
120
+ <h1>授权未完成</h1>
121
+ <p>可以关闭此页面,回到命令行重试</p>
122
+ </main>
123
+ </body>
124
+ </html>`;
125
+
126
+ const CALLBACK_HTML_MISSING = `<!DOCTYPE html>
127
+ <html lang="zh-CN">
128
+ <head>
129
+ <meta charset="utf-8">
130
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
131
+ <title>回调异常 · Linger</title>
132
+ <style>
133
+ *{margin:0;padding:0;box-sizing:border-box}
134
+ html,body{height:100%}
135
+ body{
136
+ font-family:-apple-system,BlinkMacSystemFont,"SF Pro Display","SF Pro Text","Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei",system-ui,sans-serif;
137
+ background:#F5F6F8;color:#0F1419;
138
+ display:flex;align-items:center;justify-content:center;
139
+ min-height:100vh;min-height:100dvh;
140
+ padding:24px;line-height:1.6;-webkit-font-smoothing:antialiased;
141
+ }
142
+ .card{
143
+ width:100%;max-width:380px;text-align:center;padding:8px;
144
+ opacity:0;transform:translateY(10px);
145
+ animation:rise .6s cubic-bezier(.16,1,.3,1) .05s forwards;
146
+ }
147
+ .badge{
148
+ width:72px;height:72px;border-radius:999px;margin:0 auto 32px;
149
+ display:flex;align-items:center;justify-content:center;
150
+ background:rgba(15,20,25,.05);
151
+ }
152
+ .badge svg{width:30px;height:30px}
153
+ .mark{fill:none;stroke:#0F1419;stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round;opacity:.55}
154
+ h1{font-size:21px;font-weight:500;letter-spacing:.02em;margin-bottom:12px}
155
+ p{font-size:14px;color:#4B5563;letter-spacing:.02em}
156
+ @keyframes rise{to{opacity:1;transform:translateY(0)}}
157
+ @media (prefers-reduced-motion:reduce){.card{animation:none;opacity:1;transform:none}}
158
+ </style>
159
+ </head>
160
+ <body>
161
+ <main class="card">
162
+ <div class="badge" aria-hidden="true">
163
+ <svg viewBox="0 0 24 24">
164
+ <path class="mark" d="M9.2 9.2a2.8 2.8 0 1 1 4.3 2.6c-.9.6-1.5 1.1-1.5 2.2"/>
165
+ <line class="mark" x1="12" y1="17.5" x2="12" y2="17.5"/>
166
+ </svg>
167
+ </div>
168
+ <h1>回调异常,缺少授权码</h1>
169
+ <p>可以关闭此页面</p>
170
+ </main>
171
+ </body>
172
+ </html>`;
173
+
174
+ export { CALLBACK_HTML_SUCCESS, CALLBACK_HTML_ERROR, CALLBACK_HTML_MISSING };
package/src/cli-parse.js CHANGED
@@ -34,6 +34,7 @@
34
34
  // task cancel <task_id> → 'task:cancel'
35
35
  //
36
36
  // 能力上架
37
+ // capability report-skills ... → 'capability:report-skills'(agent 上报技能清单)
37
38
  // capability create ... → 'capability:create'
38
39
  // capability publish <cap_id> → 'capability:publish'
39
40
  //
@@ -93,7 +94,10 @@ export function parseArgs(argv) {
93
94
  type: null, // capability create --type
94
95
  category: null, // capability create --category
95
96
  tags: null, // capability create --tags (逗号分隔)
96
- priceCents: null, // capability create --price-cents
97
+ priceCents: null, // capability create --price-cents(兼容保留·已弃用)
98
+ price: null, // capability create --price(元·面向 agent 的友好单位)
99
+ skills: null, // capability report-skills --skills(JSON 数组字符串)
100
+ skillsFile: null, // capability report-skills --skills-file(JSON 文件路径)
97
101
  title: null, // task create --title / capability create --title
98
102
  taskTitle: null, // task create --title(同 title)
99
103
  bountyCents: null, // task create --bounty-cents
@@ -164,6 +168,15 @@ export function parseArgs(argv) {
164
168
  } else if (a === '--price-cents') {
165
169
  const v = args.shift();
166
170
  flags.priceCents = v != null ? parseInt(v, 10) : null;
171
+ } else if (a === '--price') {
172
+ // 面向 agent 的友好单位:元(浮点)。index.js 里换算成分。
173
+ const v = args.shift();
174
+ flags.price = v != null ? parseFloat(v) : null;
175
+ } else if (a === '--skills') {
176
+ // capability report-skills:JSON 数组字符串 [{"name","description"}]
177
+ flags.skills = args.shift() ?? null;
178
+ } else if (a === '--skills-file') {
179
+ flags.skillsFile = args.shift() ?? null;
167
180
  } else if (a === '--title') {
168
181
  flags.title = args.shift() ?? null;
169
182
  } else if (a === '--bounty-cents') {
@@ -217,9 +230,10 @@ export function parseArgs(argv) {
217
230
  }
218
231
  } else if (c0 === 'capability') {
219
232
  switch (c1) {
220
- case 'create': command = 'capability:create'; break;
221
- case 'publish': command = 'capability:publish'; break;
222
- default: command = 'unknown';
233
+ case 'create': command = 'capability:create'; break;
234
+ case 'publish': command = 'capability:publish'; break;
235
+ case 'report-skills': command = 'capability:report-skills'; break;
236
+ default: command = 'unknown';
223
237
  }
224
238
  } else if (c0 === 'qa') {
225
239
  // qa <task_id> <message> 或 qa <task_id> --list
package/src/format.js CHANGED
@@ -131,15 +131,29 @@ export function formatRuntimeAct(data, label, { json = false } = {}) {
131
131
  // 能力上架
132
132
  // ============================================================
133
133
 
134
+ /**
135
+ * 格式化「上报技能清单」结果(agent 上报 → skill_catalog)。
136
+ * @param {object} data { ok, agent_id, skills_count }
137
+ */
138
+ export function formatReportSkills(data, { json = false } = {}) {
139
+ if (json) return asJson(data);
140
+ const n = data.skills_count != null ? data.skills_count : '?';
141
+ return [
142
+ `已上报 ${n} 项技能到 Linger 平台。`,
143
+ `下一步:让用户打开 Linger 网页能力页点「AI 识别」,挑出要上架的能力卡并确认上架。`,
144
+ ].join('\n');
145
+ }
146
+
134
147
  /**
135
148
  * 格式化能力卡创建结果。
136
- * @param {object} data { id, title, status, ... }
149
+ * @param {object} data { id, title, status, price_cents, ... }
137
150
  */
138
151
  export function formatCapabilityCreate(data, { json = false } = {}) {
139
152
  if (json) return asJson(data);
140
153
  const lines = [`能力卡草稿已创建!`];
141
154
  lines.push(` 能力 ID:${data.id}`);
142
155
  if (data.title) lines.push(` 标题:${data.title}`);
156
+ if (data.price_cents != null) lines.push(` 报价:${yuan(data.price_cents)}`);
143
157
  if (data.status) lines.push(` 状态:${data.status}`);
144
158
  return lines.join('\n');
145
159
  }
package/src/index.js CHANGED
@@ -5,6 +5,7 @@
5
5
  // 薄包装铁律:index.js 只做「解析参数 → 调 api → 格式化输出」,不含业务逻辑。
6
6
 
7
7
  import { createHash } from 'node:crypto';
8
+ import { readFileSync } from 'node:fs';
8
9
  import { parseArgs } from './cli-parse.js';
9
10
  import { resolveBaseUrl } from './config.js';
10
11
  import { loadCredentials } from './credentials.js';
@@ -16,6 +17,7 @@ import {
16
17
  applyTask,
17
18
  selectApplication,
18
19
  runtimeAct,
20
+ reportSkills,
19
21
  createCapability,
20
22
  publishCapability,
21
23
  postComment,
@@ -34,6 +36,7 @@ import {
34
36
  formatTaskApply,
35
37
  formatTaskSelect,
36
38
  formatRuntimeAct,
39
+ formatReportSkills,
37
40
  formatCapabilityCreate,
38
41
  formatCapabilityPublish,
39
42
  formatPostComment,
@@ -154,16 +157,22 @@ const HELP = `Linger 命令行工具(v0.4.2 · M7 全量命令)
154
157
  [--reason <原因>]
155
158
  [--idempotency-key <key>]
156
159
 
157
- ── 能力上架 ─────────────────────────────────────────────────
158
- capability create 创建能力卡草稿(卖方)
160
+ ── 能力上架(默认:agent 只上报技能,识别/选品/上架在 Linger 网页做)──
161
+ capability report-skills 上报技能清单给平台(推荐·A 流程第一步)
162
+ --skills '<JSON 数组>' [{"name":"能力名","description":"能做什么"}]
163
+ --skills-file <path> (或)从 JSON 文件读技能清单
164
+ 上报后由用户在 Linger 网页点「AI 识别」挑能力卡、确认上架
165
+
166
+ ── 以下两条是「用户主动直传某个能力」的快捷口子·常规不需要 agent 自己跑 ──
167
+ capability create 手动创建一张能力卡草稿
159
168
  --agent-id <agent_id>
160
169
  --title <标题>
161
170
  [--description <描述>]
162
- [--price-cents <标准报价分>]
171
+ [--price <元>] 标准报价(元·如 --price 50 表示 ¥50.00)
163
172
  [--tags <标签1,标签2>]
164
173
  [--idempotency-key <key>]
165
174
 
166
- capability publish <cap_id> 上架能力(卖方)
175
+ capability publish <cap_id> 上架能力(默认建议由用户在网页确认)
167
176
  [--idempotency-key <key>]
168
177
 
169
178
  ── 问答 / 自治 ──────────────────────────────────────────────
@@ -442,6 +451,35 @@ export async function run(argv, deps = {}) {
442
451
 
443
452
  // ── 能力上架 ─────────────────────────────────────────────
444
453
 
454
+ if (command === 'capability:report-skills') {
455
+ // A 流程第一步:agent 把自己会的技能上报到平台 skill_catalog,
456
+ // 之后用户在网页点「AI 识别」挑能力卡。skills 来自 --skills 或 --skills-file。
457
+ let skillsRaw = flags.skills;
458
+ if (!skillsRaw && flags.skillsFile) {
459
+ try {
460
+ skillsRaw = readFileSync(flags.skillsFile, 'utf8');
461
+ } catch (e) {
462
+ err(`读取 --skills-file 失败:${e.message}`); return 2;
463
+ }
464
+ }
465
+ if (!skillsRaw) {
466
+ err('缺少技能清单。用法:linger capability report-skills --skills \'[{"name":"译稿","description":"中英互译"}]\'(或 --skills-file <path>)');
467
+ return 2;
468
+ }
469
+ let skills;
470
+ try {
471
+ skills = JSON.parse(skillsRaw);
472
+ } catch (e) {
473
+ err(`--skills 不是合法 JSON:${e.message}`); return 2;
474
+ }
475
+ if (!Array.isArray(skills) || skills.length === 0) {
476
+ err('技能清单必须是非空 JSON 数组:[{"name":"...","description":"..."}]'); return 2;
477
+ }
478
+ const data = await reportSkills(baseUrl, token, skills, idemKey, { fetchImpl });
479
+ log(formatReportSkills(data, { json: flags.json }));
480
+ return 0;
481
+ }
482
+
445
483
  if (command === 'capability:create') {
446
484
  if (!flags.agentId) { err('缺少 --agent-id 参数'); return 2; }
447
485
  if (!flags.title) { err('缺少 --title 参数'); return 2; }
@@ -450,7 +488,14 @@ export async function run(argv, deps = {}) {
450
488
  if (flags.type) capData.type = flags.type;
451
489
  if (flags.category) capData.category = flags.category;
452
490
  if (flags.tags && flags.tags.length > 0) capData.tags = flags.tags;
453
- if (flags.priceCents != null) capData.price_cents = flags.priceCents;
491
+ // 价格:优先 --price(元·友好单位),兼容旧 --price-cents(分)。后端一律存分。
492
+ let priceCents = null;
493
+ if (flags.price != null && Number.isFinite(flags.price)) {
494
+ priceCents = Math.round(flags.price * 100);
495
+ } else if (flags.priceCents != null) {
496
+ priceCents = flags.priceCents;
497
+ }
498
+ if (priceCents != null) capData.price_cents = priceCents;
454
499
  // M5:交付物边界说明(草稿可空,上架前平台要求必填)
455
500
  if (flags.deliverableBoundary) capData.deliverable_boundary = flags.deliverableBoundary;
456
501
  const data = await createCapability(baseUrl, token, capData, idemKey, { fetchImpl });
package/src/oauth.js CHANGED
@@ -17,6 +17,7 @@ import { spawn } from 'node:child_process';
17
17
 
18
18
  import { generateVerifier, challengeFromVerifier, generateState } from './pkce.js';
19
19
  import { saveCredentials } from './credentials.js';
20
+ import { CALLBACK_HTML_SUCCESS, CALLBACK_HTML_ERROR, CALLBACK_HTML_MISSING } from './callback-pages.js';
20
21
 
21
22
  // M5 预注册 client_id(已在平台 oauth_clients 表预注册,无需动态注册)。
22
23
  export const POC_CLIENT_ID = 'linger-cli';
@@ -116,13 +117,13 @@ export function startLoopbackServer({ host = '127.0.0.1', port = 0 } = {}) {
116
117
  // 给浏览器一个「可以关掉这个标签页了」的极简页面
117
118
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
118
119
  if (error) {
119
- res.end('<p>授权未完成,可以关闭此页面,回到命令行查看。</p>');
120
+ res.end(CALLBACK_HTML_ERROR);
120
121
  rejectCode(new Error(`用户未授权或授权被拒绝:${error}`));
121
122
  } else if (code) {
122
- res.end('<p>登录成功,可以关闭此页面,回到命令行。</p>');
123
+ res.end(CALLBACK_HTML_SUCCESS);
123
124
  resolveCode({ code, state });
124
125
  } else {
125
- res.end('<p>回调缺少授权码,可以关闭此页面。</p>');
126
+ res.end(CALLBACK_HTML_MISSING);
126
127
  rejectCode(new Error('回调地址未带授权码'));
127
128
  }
128
129
  });
@@ -275,6 +276,8 @@ export async function runAuthLogin(opts = {}) {
275
276
 
276
277
  const creds = { ...tokenObj, base_url: baseUrl };
277
278
  const file = saveCredentials(creds, credDir ? { dir: credDir } : {});
278
- log(`登录完成,凭证已保存到 ${file}`);
279
+ log('✓ 绑定成功,你的 Agent 已接入 Linger。');
280
+ log(` 在主页查看你的 Agent:${baseUrl}`);
281
+ log(` 凭证已保存到 ${file}`);
279
282
  return creds;
280
283
  }