protara 0.3.6 → 0.3.7

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 CHANGED
@@ -19,14 +19,74 @@ protara --version
19
19
 
20
20
  CLI 用**独立的**登录态(`credentials.cli.json`),不与桌面端共享 —— 服务端的刷新令牌一次一换,共用一份会互相踢下线。
21
21
 
22
+ ### 两种登录方式:`--login` 会问你一句
23
+
24
+ ```bash
25
+ protara --login
26
+ ```
27
+
28
+ ```
29
+ protara: 登录方式:
30
+ 1) 设备码登录 —— 在浏览器里确认一下(有浏览器时最省事)
31
+ 2) 用户名密码登录 —— 没有浏览器时用(SSH / CI)
32
+ 回车 = 1(设备码)
33
+ protara: 选择 [1/2]:
34
+ ```
35
+
36
+ - **1 / 回车** → 设备码:浏览器里确认一下就完成
37
+ - **2** → 用户名密码:接着问用户名(回显),密码**不回显**
38
+
39
+ **为什么密码登录需要被发现**:SSH 到远程机器、CI、纯终端环境里拿不到浏览器时,设备码走不通
40
+ (`user_code` 可以手输,但那要另一台设备先打开浏览器)。这时密码登录是唯一可用的路径。
41
+
42
+ ### 直接指定,跳过询问
43
+
44
+ ```bash
45
+ protara --login --password # 跳过菜单,直接用账号密码
46
+ protara --login --password --username alice # 用户名也给定
47
+ ```
48
+
49
+ ⚠️ **非交互环境(CI / 管道)不会弹菜单**,自动按设备码走 —— 脚本里 `protara --login`
50
+ 的行为与以前完全一致,不会被卡在等输入。
51
+
52
+ ### 脚本化登录(CI / 自动化)
53
+
22
54
  ```bash
23
- protara --login # 走设备码:浏览器里确认一下就完成
24
- protara status # 查登录态 / 凭据路径 / 终端指纹(排障入口)
55
+ # 方式一:从 stdin 读(推荐,密码不进 shell 历史、不进 ps)
56
+ echo "$MY_PASSWORD" | protara --login --password-stdin --username alice
57
+
58
+ # 方式二:环境变量
59
+ PROTARA_PASSWORD="$MY_PASSWORD" protara --login --password --username alice
60
+ ```
61
+
62
+ ⚠️ **刻意没有 `--password <明文>`**:那会把密码写进 shell 历史、`ps` 的命令行、以及 CI 日志。
63
+ 这不是没做,是不提供。
64
+
65
+ ⚠️ 密码输入**一个字符都不回显**(连 `*` 都没有)—— 终端回滚、tmux `capture-pane`、
66
+ 屏幕共享都可能留下屏幕内容,密码长度也是信息。
67
+
68
+ ### 首登会被要求改密
69
+
70
+ 如果输出里有这句:
71
+
72
+ ```
73
+ protara: ⚠️ 这个账号首次登录需要先改密码。请用浏览器登录平台改密后再回来用 CLI
74
+ ```
75
+
76
+ 说明服务端判了 `must_change_password`。**令牌是有效的**(登录确实成功了),但改密前多数
77
+ 操作会被拒绝。去 Web 端改一次密码再回来。
78
+
79
+ ### 排障
80
+
81
+ ```bash
82
+ protara status # 登录态 / 凭据路径 / 作用域根 / 终端指纹
25
83
  ```
26
84
 
27
85
  ## 用法
28
86
 
29
87
  ```bash
88
+ protara agents # 列出可用 Agent(含 id / 默认模型)
89
+ protara --agent <id> -p "…" # 指定 Agent 提问
30
90
  protara # 交互界面(需要真 TTY)
31
91
  protara -p "帮我看下这个仓库" # 一次性提问;stdout 只有答案正文
32
92
  cat log.txt | protara -p - # 从管道读正文
@@ -46,6 +106,43 @@ protara --help # 全部参数 + 退出码表
46
106
 
47
107
  `-p` 模式下 **stdout 保证纯净**:所有诊断走 stderr 且带 `protara:` 前缀,或写进日志文件。
48
108
 
109
+ ### 切换 Agent
110
+
111
+ 先看有哪些(`--agent` 要的是 `id`,不是名字):
112
+
113
+ ```bash
114
+ protara agents
115
+ ```
116
+
117
+ ```
118
+ ✓ 2 个可用 Agent
119
+
120
+ desktop 优联智能文档(默认)
121
+ 面向内部文档平台的通用助手:能读写工作区文件、跑命令、查技能库
122
+ 默认模型 claude-sonnet-4
123
+ 可选模型 gpt-4o
124
+
125
+ code-reviewer 代码审查助手
126
+ 默认模型 claude-sonnet-4
127
+ ```
128
+
129
+ 然后指定:
130
+
131
+ ```bash
132
+ protara --agent code-reviewer -p "审查一下这个 diff"
133
+ protara --agent code-reviewer --model gpt-4o -p "…" # 同时换模型
134
+ ```
135
+
136
+ **不给 `--agent` 时用内置的 `cli` Agent**(终端专用:工具清单里没有浏览器 / 文档编辑,
137
+ 因为那两套运行时装不进这个 3.3 MB 的包)。它**不出现在上面的列表里** ——
138
+ 它是 CLI 的实现细节,不是给你选的。
139
+
140
+ ⚠️ **Agent 在「建会话」那一刻冻结**,之后不能中途切换 —— 想换就开一个新会话
141
+ (不加 `--session` / `--resume-last` 就是新会话)。这是服务端的口径:
142
+ 会话的 `agent_id` 创建后不可变,因为权限档位、技能集、工具清单都在那时定下了。
143
+
144
+ `protara agents --json` 给脚本用:`{ ok: true, agents: [...] }`(`default: true` 标出默认那个)。
145
+
49
146
  ### 退出码
50
147
 
51
148
  | 码 | 含义 |
@@ -86,7 +183,45 @@ $DESKTOP_HOME/logs/cli.log # 默认 ~/.desktop-agents/logs/cli.log
86
183
  连接器要重新「去认证」一次。
87
184
  3. **凭据目前是明文 `0600` 文件**(refresh 令牌寿命 30 天)。在共享机器上慎用 `--login`。
88
185
 
89
- ## 当前未支持
186
+ ## 能做什么 / 不能做什么
187
+
188
+ CLI 与桌面端**共用同一份工具执行器**,但装配不同 —— 差别只有「有没有那个运行时」。
189
+
190
+ ### ✅ 能用的本机工具(13 条)
191
+
192
+ | 类别 | 工具 |
193
+ |---|---|
194
+ | 文件读写 | `read` / `write` / `edit` / `delete`(废纸篓通道缺省,见下) |
195
+ | 查找 | `grep` / `find` / `glob` / `ls` |
196
+ | 执行 | `bash`(任意命令、可后台) |
197
+ | 技能 | `load_skill` |
198
+ | 传输 | `upload_file` / `download_file`(只要登录态) |
199
+
200
+ 外加:**MCP 连接器工具**(`mcp.json` 与桌面端共享,但授权令牌不共享)、
201
+ `tool_search`(连接器工具的延迟加载检索)。
202
+
203
+ ### ❌ 不能用的两条(会**明确告诉你**,不静默失败)
204
+
205
+ | 工具 | CLI 里的返回 |
206
+ |---|---|
207
+ | `browser`(浏览器操作) | `本客户端没有装配浏览器通道(找不到随包分发的 bsk 运行时)…` |
208
+ | `univer`(文档编辑) | `本客户端没有装配文档编辑通道(找不到随包分发的 univer 运行时)…` |
209
+
210
+ **为什么**:这两个能力依赖**随包分发的大体积运行时**,而 npm 包只有 3.3 MB:
211
+
212
+ | 载荷 | 体积 |
213
+ |---|---|
214
+ | `bsk`(浏览器操作二进制,含 Chromium 驱动) | 23 MB |
215
+ | `univer`(文档编辑:Node 运行时 + 公式/排版/导入导出四份原生模块) | **2.0 GB** |
216
+
217
+ 把 2 GB 塞进 npm 包不现实(npm 单包上限 500 MB,且装一次要几分钟)。
218
+ 所以 CLI 里这两条**如实拒绝**,⛔ 不会偷偷用 `bash` 去跑 —— 那会绕过整条权限口径。
219
+
220
+ > 需要浏览器自动化 / 文档编辑:请用**桌面端**(它随包带了这两套载荷)。
221
+ > 在 CLI 里模型会看到这两条工具(清单与桌面端同一份),调用后收到上面那句拒绝,
222
+ > 然后自己改用 `bash` + 其他方式,或者告诉你「这个我做不了」。
223
+
224
+ ### ❌ 其他没做的
90
225
 
91
226
  连接器「去认证」交互面板、计划条、作业角标面板、diff 分页、图片拖入、多行编辑框
92
227
  (TUI 逐行提交)、Windows 上的沙箱助手。
@@ -51188,6 +51188,9 @@ var DEFAULT = {
51188
51188
  forceHeadless: false,
51189
51189
  verbose: false,
51190
51190
  login: false,
51191
+ loginMethod: "ask",
51192
+ username: null,
51193
+ passwordFromStdin: false,
51191
51194
  logout: false,
51192
51195
  version: false,
51193
51196
  help: false,
@@ -51287,6 +51290,19 @@ function parseArgs(argv) {
51287
51290
  case "--login":
51288
51291
  out.login = true;
51289
51292
  break;
51293
+ case "--password":
51294
+ out.loginMethod = "password";
51295
+ out.login = true;
51296
+ break;
51297
+ case "--username":
51298
+ out.username = next("--username");
51299
+ break;
51300
+ case "--password-stdin":
51301
+ out.passwordFromStdin = true;
51302
+ out.loginMethod = "password";
51303
+ out.login = true;
51304
+ break;
51305
+ break;
51290
51306
  case "--logout":
51291
51307
  out.logout = true;
51292
51308
  break;
@@ -51313,7 +51329,7 @@ function parseArgs(argv) {
51313
51329
  }
51314
51330
  if (positional.length > 0) {
51315
51331
  const [first, ...rest] = positional;
51316
- if (first === "login" || first === "logout" || first === "status") {
51332
+ if (first === "login" || first === "logout" || first === "status" || first === "agents") {
51317
51333
  out.command = first;
51318
51334
  } else if (first !== void 0 && !out.prompt && !out.promptFromStdin) {
51319
51335
  out.prompt = [first, ...rest].join(" ");
@@ -51374,8 +51390,16 @@ var HELP_TEXT = `protara \u2014 \u4F18\u8054\u667A\u80FD\u6587\u6863\u7684\u7EC8
51374
51390
  -v, --verbose \u628A\u8BCA\u65AD\u4E5F\u8F93\u51FA\u5230 stderr\uFF08\u9ED8\u8BA4\u53EA\u5199\u65E5\u5FD7\u6587\u4EF6\uFF09
51375
51391
  --approval <mode> deny\uFF08\u9ED8\u8BA4\uFF0C\u65E0\u4EBA\u5E94\u7B54\u5373\u62D2\u6279\uFF09| remote\uFF08\u8D70\u6D4F\u89C8\u5668\u8FDC\u7A0B\u5BA1\u6279\uFF09
51376
51392
  --origin <url> \u5E73\u53F0\u5730\u5740
51377
- --login / --logout \u53EA\u767B\u5F55 / \u53EA\u9000\u51FA\uFF08\u8BBE\u5907\u7801\u6D41\uFF09
51393
+ --login / --logout \u53EA\u767B\u5F55 / \u53EA\u9000\u51FA
51394
+ \u4E0D\u52A0\u522B\u7684\u53C2\u6570\u65F6\uFF0C\u4F1A\u95EE\u300C1 \u8BBE\u5907\u7801 / 2 \u8D26\u53F7\u5BC6\u7801\u300D\uFF0C
51395
+ \u975E\u4EA4\u4E92\u73AF\u5883\uFF08CI / \u7BA1\u9053\uFF09\u81EA\u52A8\u6309\u8BBE\u5907\u7801\u8D70\uFF0C\u4E0D\u4F1A\u505C\u4E0B\u6765\u7B49\u8F93\u5165
51396
+ --password \u8DF3\u8FC7\u8BE2\u95EE\uFF0C\u76F4\u63A5\u7528**\u8D26\u53F7\u5BC6\u7801**\u767B\u5F55\uFF08SSH / CI \u91CC\u6CA1\u6709\u6D4F\u89C8\u5668\u65F6\u7528\uFF09
51397
+ --username <name> \u8D26\u53F7\u5BC6\u7801\u767B\u5F55\u7684\u7528\u6237\u540D\uFF08\u4E0D\u7ED9\u5C31\u4EA4\u4E92\u8F93\u5165\uFF09
51398
+ --password-stdin \u4ECE stdin \u8BFB\u5BC6\u7801\uFF08\u811A\u672C\u7528\uFF1Aecho "$PW" | protara login --password-stdin\uFF09
51399
+ \u26A0\uFE0F \u523B\u610F**\u4E0D\u63D0\u4F9B** "--password <\u660E\u6587>"\uFF1A\u90A3\u4F1A\u7559\u5728 shell \u5386\u53F2\u91CC\u3002
51400
+ \u975E\u4EA4\u4E92\u4E5F\u53EF\u7528\u73AF\u5883\u53D8\u91CF PROTARA_PASSWORD\u3002
51378
51401
  status \u6253\u5370\u767B\u5F55\u6001 / \u51ED\u636E\u8DEF\u5F84 / \u7EC8\u7AEF\u6307\u7EB9\uFF08\u53EA\u8BFB\uFF0C\u6392\u969C\u7528\uFF09
51402
+ agents \u5217\u51FA\u5F53\u524D\u8EAB\u4EFD\u53EF\u7528\u7684 Agent\uFF08id / \u540D\u79F0 / \u9ED8\u8BA4\u6A21\u578B\uFF09
51379
51403
 
51380
51404
  \u9000\u51FA\u7801\uFF1A
51381
51405
  0 \u56DE\u5408\u6B63\u5E38\u6536\u5C3E 1 \u56DE\u5408\u5931\u8D25\u6216\u88AB\u505C\u6B62 2 \u7528\u6CD5\u9519\u8BEF 3 \u672A\u767B\u5F55 / \u51ED\u636E\u5931\u6548
@@ -88449,6 +88473,47 @@ var plaintextCodec = {
88449
88473
  decrypt: () => null
88450
88474
  };
88451
88475
 
88476
+ // src/cli/agent-selection.ts
88477
+ var BUILTIN_CLI_AGENT_ID = "cli";
88478
+ function isAgentUnavailable(err) {
88479
+ if (typeof err !== "object" || err === null) return false;
88480
+ const candidate = err;
88481
+ if (candidate.status === 404) return true;
88482
+ const text2 = typeof candidate.message === "string" ? candidate.message : String(err);
88483
+ return /agent[_ -]?not[_ -]?found|不存在.*agent|agent.*不存在|没有可用的 agent/i.test(text2);
88484
+ }
88485
+ async function createSessionForCli(deps) {
88486
+ const explicit = deps.explicit?.trim();
88487
+ if (explicit) {
88488
+ const session = await deps.create(explicit);
88489
+ return { sessionId: session.id, choice: { agentId: explicit, source: "explicit" } };
88490
+ }
88491
+ try {
88492
+ const session = await deps.create(BUILTIN_CLI_AGENT_ID);
88493
+ return {
88494
+ sessionId: session.id,
88495
+ choice: { agentId: BUILTIN_CLI_AGENT_ID, source: "builtin" }
88496
+ };
88497
+ } catch (err) {
88498
+ if (!isAgentUnavailable(err)) throw err;
88499
+ const agents = await deps.listAgents();
88500
+ const first = agents[0]?.id;
88501
+ if (!first) {
88502
+ throw err;
88503
+ }
88504
+ const session = await deps.create(first);
88505
+ return {
88506
+ sessionId: session.id,
88507
+ choice: {
88508
+ agentId: first,
88509
+ source: "fallback-first",
88510
+ warning: `\u8FD9\u4E2A\u670D\u52A1\u7AEF\u6CA1\u6709\u53EF\u7528\u7684\u5185\u7F6E CLI Agent\uFF08id="${BUILTIN_CLI_AGENT_ID}"\uFF09\uFF0C\u5DF2\u6539\u7528 "${first}"\u3002
88511
+ \u26A0\uFE0F \u5B83\u7684\u5DE5\u5177\u6E05\u5355\u91CC\u53EF\u80FD\u6709\u6D4F\u89C8\u5668 / \u6587\u6863\u7F16\u8F91\u5DE5\u5177\uFF0C\u800C\u7EC8\u7AEF\u91CC\u8DD1\u4E0D\u4E86\uFF08\u4F1A\u8FD4\u56DE\u300C\u672C\u5BA2\u6237\u7AEF\u6CA1\u6709\u88C5\u914D\u2026\u901A\u9053\u300D\uFF09\u3002`
88512
+ }
88513
+ };
88514
+ }
88515
+ }
88516
+
88452
88517
  // src/core/host.ts
88453
88518
  function isNormalEnd(reason) {
88454
88519
  return reason === "reply" || reason === "done" || reason === "attached";
@@ -88979,21 +89044,21 @@ async function runOnce(options) {
88979
89044
  }
88980
89045
  }
88981
89046
  if (!sessionId) {
88982
- const agentId = args.agentId ?? (await runtime.api.agentsList())[0]?.id ?? "";
88983
- if (!agentId) {
88984
- writers.err("protara: usage: \u5E73\u53F0\u4E0A\u6CA1\u6709\u53EF\u7528\u7684 Agent\n");
88985
- return { exitCode: EXIT.USAGE, sessionId: null, turnId, reason: null, approvalBlocked };
88986
- }
88987
- const session = await runtime.api.sessionsCreate({
88988
- agent_id: agentId,
88989
- model_id: args.modelId ?? null,
88990
- workspace: args.workspace ?? process.cwd(),
88991
- // ⭐ 声明原样转出去(`sessionsCreate` 是唯一那一处;D10 静态守着)。
88992
- // ⚠️ `overrides` 是**必填**(服务端把它整段写进 metadata):CLI 没有逐项微调,
88993
- // 给空对象 = 「只声明档位,不覆盖任何单项」。⛔ 别传 `null`(那等于没声明)。
88994
- permission: args.permission ? { preset: args.permission, overrides: {} } : null
89047
+ const { sessionId: created, choice } = await createSessionForCli({
89048
+ explicit: args.agentId,
89049
+ listAgents: () => runtime.api.agentsList(),
89050
+ create: (agentId) => runtime.api.sessionsCreate({
89051
+ agent_id: agentId,
89052
+ model_id: args.modelId ?? null,
89053
+ workspace: args.workspace ?? process.cwd(),
89054
+ // ⭐ 声明原样转出去(`sessionsCreate` 是唯一那一处;D10 静态守着)。
89055
+ // ⚠️ `overrides` 是**必填**(服务端把它整段写进 metadata):CLI 没有逐项微调,
89056
+ // 给空对象 = 「只声明档位,不覆盖任何单项」。⛔ 别传 `null`(那等于没声明)。
89057
+ permission: args.permission ? { preset: args.permission, overrides: {} } : null
89058
+ })
88995
89059
  });
88996
- sessionId = session.id;
89060
+ sessionId = created;
89061
+ if (choice.warning) log("warn", choice.warning);
88997
89062
  }
88998
89063
  } catch (err) {
88999
89064
  return await fail2(err, null);
@@ -89631,20 +89696,19 @@ async function runTui(options) {
89631
89696
  const runTurn = async (message) => {
89632
89697
  if (!sessionId) {
89633
89698
  try {
89634
- const agents = await runtime.api.agentsList();
89635
- const agentId = options.args.agentId ?? agents[0]?.id ?? "";
89636
- if (!agentId) {
89637
- commitLive([" \u2717 \u5E73\u53F0\u4E0A\u6CA1\u6709\u53EF\u7528\u7684 Agent"]);
89638
- return;
89639
- }
89640
- agentLabel = agentId;
89641
- const session = await runtime.api.sessionsCreate({
89642
- agent_id: agentId,
89643
- model_id: options.args.modelId ?? null,
89644
- workspace: options.args.workspace ?? process.cwd(),
89645
- permission: options.args.permission ? { preset: options.args.permission, overrides: {} } : null
89699
+ const { sessionId: created, choice } = await createSessionForCli({
89700
+ explicit: options.args.agentId,
89701
+ listAgents: () => runtime.api.agentsList(),
89702
+ create: (agentId) => runtime.api.sessionsCreate({
89703
+ agent_id: agentId,
89704
+ model_id: options.args.modelId ?? null,
89705
+ workspace: options.args.workspace ?? process.cwd(),
89706
+ permission: options.args.permission ? { preset: options.args.permission, overrides: {} } : null
89707
+ })
89646
89708
  });
89647
- sessionId = session.id;
89709
+ sessionId = created;
89710
+ agentLabel = choice.agentId;
89711
+ if (choice.warning) commitLive([` \u26A0\uFE0F ${choice.warning.replace(/\n/g, "\n ")}`, ""]);
89648
89712
  await runtime.prefs.rememberWorkspace(options.args.workspace ?? process.cwd());
89649
89713
  } catch (err) {
89650
89714
  commitLive([
@@ -89866,6 +89930,238 @@ async function printAuthStatus(runtime, options) {
89866
89930
  `);
89867
89931
  return EXIT.OK;
89868
89932
  }
89933
+ async function printAgents(runtime, options) {
89934
+ const { writers, json } = options;
89935
+ let agents;
89936
+ try {
89937
+ agents = await runtime.api.agentsList();
89938
+ } catch (err) {
89939
+ const message = err instanceof Error ? err.message : String(err);
89940
+ if (json) {
89941
+ writers.out(`${JSON.stringify({ ok: false, error: message })}
89942
+ `);
89943
+ }
89944
+ writers.err(
89945
+ `protara: \u53D6 Agent \u5217\u8868\u5931\u8D25\uFF1A${message}
89946
+ protara: \u8BF7\u786E\u8BA4\u5DF2\u767B\u5F55\uFF08protara status\uFF09\u4E14\u80FD\u8FDE\u4E0A\u5E73\u53F0\uFF08--origin\uFF09
89947
+ `
89948
+ );
89949
+ return EXIT.TURN_FAILED;
89950
+ }
89951
+ if (json) {
89952
+ writers.out(
89953
+ `${JSON.stringify({
89954
+ ok: true,
89955
+ agents: agents.map((a) => ({ ...a, default: false }))
89956
+ })}
89957
+ `
89958
+ );
89959
+ return EXIT.OK;
89960
+ }
89961
+ if (agents.length === 0) {
89962
+ writers.err("protara: \u8FD9\u4E2A\u8EAB\u4EFD\u6CA1\u6709\u53EF\u7528\u7684 Agent\uFF08\u627E\u7BA1\u7406\u5458\u786E\u8BA4\u6743\u9650\uFF09\n");
89963
+ return EXIT.OK;
89964
+ }
89965
+ writers.out(`\u2713 ${agents.length} \u4E2A\u53EF\u7528 Agent
89966
+
89967
+ `);
89968
+ for (const agent of agents) {
89969
+ writers.out(` ${agent.id.padEnd(18)}${agent.name}
89970
+ `);
89971
+ if (agent.description && agent.description !== agent.name) {
89972
+ writers.out(` ${wrapOneLine(agent.description, 66)}
89973
+ `);
89974
+ }
89975
+ const defaultModel = agent.default_model?.id ?? null;
89976
+ if (defaultModel) writers.out(` \u9ED8\u8BA4\u6A21\u578B ${defaultModel}
89977
+ `);
89978
+ const others = agent.models.map((m) => m.id).filter((id) => id !== defaultModel);
89979
+ if (others.length > 0) {
89980
+ writers.out(` \u53EF\u9009\u6A21\u578B ${wrapOneLine(others.join(", "), 66)}
89981
+ `);
89982
+ }
89983
+ writers.out("\n");
89984
+ }
89985
+ writers.out(
89986
+ ` \u5185\u7F6E ${BUILTIN_CLI_AGENT_ID.padEnd(18)}\u7EC8\u7AEF\u4E13\u7528\uFF08\u9ED8\u8BA4\uFF09 \u2190 \u4E0D\u5728\u4E0A\u9762\u7684\u5217\u8868\u91CC
89987
+ `
89988
+ );
89989
+ writers.out("\n");
89990
+ writers.out('\u7528\u6CD5\uFF1Aprotara --agent <id> -p "\u2026" \u6307\u5B9A Agent\uFF08\u7701\u7565 = \u7528\u5185\u7F6E\u7684 cli\uFF09\n');
89991
+ writers.out(' protara --agent <id> --model <id> -p "\u2026" \u540C\u65F6\u6307\u5B9A\u6A21\u578B\n');
89992
+ return EXIT.OK;
89993
+ }
89994
+ function wrapOneLine(text2, width) {
89995
+ const oneLine2 = text2.replace(/\r?\n/g, " ").trim();
89996
+ if (displayWidth(oneLine2) <= width) return oneLine2;
89997
+ const parts = [];
89998
+ let current2 = "";
89999
+ for (const ch of oneLine2) {
90000
+ if (displayWidth(current2 + ch) > width) {
90001
+ parts.push(current2);
90002
+ current2 = ch;
90003
+ } else {
90004
+ current2 += ch;
90005
+ }
90006
+ }
90007
+ if (current2) parts.push(current2);
90008
+ return parts.join("\n ");
90009
+ }
90010
+ function readSecret(prompt, io) {
90011
+ const stdin = io?.stdin ?? process.stdin;
90012
+ const stderr = io?.stderr ?? process.stderr;
90013
+ if (!stdin.isTTY || typeof stdin.setRawMode !== "function") {
90014
+ return Promise.reject(
90015
+ new Error(
90016
+ "\u9700\u8981\u4EA4\u4E92\u5F0F\u7EC8\u7AEF\u624D\u80FD\u9690\u85CF\u8F93\u5165\u5BC6\u7801\u3002\u811A\u672C\u5316\u767B\u5F55\u8BF7\u7528\u73AF\u5883\u53D8\u91CF PROTARA_PASSWORD\uFF08\u6216 --password-stdin \u4ECE\u7BA1\u9053\u8BFB\uFF09\u3002"
90017
+ )
90018
+ );
90019
+ }
90020
+ stderr.write(prompt);
90021
+ return new Promise((resolve23, reject) => {
90022
+ let buffer = "";
90023
+ const previousRaw = stdin.isRaw;
90024
+ const cleanup = () => {
90025
+ stdin.setRawMode?.(previousRaw ?? false);
90026
+ stdin.removeListener("data", onData);
90027
+ stdin.pause();
90028
+ };
90029
+ const onData = (chunk) => {
90030
+ const text2 = typeof chunk === "string" ? chunk : chunk.toString("utf8");
90031
+ for (const ch of text2) {
90032
+ if (ch === "") {
90033
+ cleanup();
90034
+ stderr.write("\n");
90035
+ reject(new Error("\u5DF2\u53D6\u6D88\uFF08Ctrl-C\uFF09"));
90036
+ return;
90037
+ }
90038
+ if (ch === "\r" || ch === "\n") {
90039
+ cleanup();
90040
+ stderr.write("\n");
90041
+ resolve23(buffer);
90042
+ return;
90043
+ }
90044
+ if (ch === "\x7F" || ch === "\b") {
90045
+ buffer = buffer.slice(0, -1);
90046
+ continue;
90047
+ }
90048
+ if (ch < " ") continue;
90049
+ buffer += ch;
90050
+ }
90051
+ };
90052
+ stdin.setRawMode(true);
90053
+ stdin.resume();
90054
+ stdin.setEncoding("utf8");
90055
+ stdin.on("data", onData);
90056
+ });
90057
+ }
90058
+ async function runPasswordLogin(runtime, options) {
90059
+ const { writers, log } = options;
90060
+ const username = (options.username ?? "").trim() || await askUsername(options);
90061
+ if (!username) {
90062
+ writers.err("protara: usage: \u672A\u63D0\u4F9B\u7528\u6237\u540D\n");
90063
+ writers.err("protara: \u7528 `protara login --password --username <\u540D\u5B57>`\uFF0C\u6216\u4E0D\u5E26 --username \u4EA4\u4E92\u8F93\u5165\n");
90064
+ return EXIT.USAGE;
90065
+ }
90066
+ let password = options.password ?? "";
90067
+ if (!password) {
90068
+ try {
90069
+ const read = options.readPassword ?? ((prompt) => readSecret(prompt));
90070
+ password = await read(`protara: ${username} \u7684\u5BC6\u7801\uFF08\u8F93\u5165\u4E0D\u56DE\u663E\uFF09: `);
90071
+ } catch (err) {
90072
+ writers.err(`protara: auth-failed: ${err instanceof Error ? err.message : String(err)}
90073
+ `);
90074
+ return EXIT.AUTH;
90075
+ }
90076
+ }
90077
+ if (!password) {
90078
+ writers.err("protara: usage: \u5BC6\u7801\u4E3A\u7A7A\n");
90079
+ return EXIT.USAGE;
90080
+ }
90081
+ try {
90082
+ const status = await runtime.auth.loginWithPassword(username, password);
90083
+ log("info", `[auth] \u8D26\u53F7\u5BC6\u7801\u767B\u5F55\u6210\u529F\uFF1A${username}\uFF08channel=password\uFF09`);
90084
+ const who = status.display_name ? `${status.display_name}${status.user_id ? `\uFF08u${status.user_id}\uFF09` : ""}` : `\u7528\u6237 ${username}`;
90085
+ writers.err(`protara: \u5DF2\u767B\u5F55\uFF1A${who}
90086
+ `);
90087
+ writers.err(`protara: \u51ED\u636E\u5199\u5165 ${runtime.store.path()}\uFF08CLI \u72EC\u5360\uFF0C\u4E0E\u684C\u9762\u7AEF\u4E92\u4E0D\u5F71\u54CD\uFF09
90088
+ `);
90089
+ if (status.must_change_password) {
90090
+ writers.err(
90091
+ "protara: \u26A0\uFE0F \u8FD9\u4E2A\u8D26\u53F7\u9996\u6B21\u767B\u5F55\u9700\u8981\u5148\u6539\u5BC6\u7801\u3002\u8BF7\u7528\u6D4F\u89C8\u5668\u767B\u5F55\u5E73\u53F0\u6539\u5BC6\u540E\u518D\u56DE\u6765\u7528 CLI\uFF08\u6539\u5BC6\u524D\u591A\u6570\u64CD\u4F5C\u4F1A\u88AB\u62D2\u7EDD\uFF09\u3002\n"
90092
+ );
90093
+ }
90094
+ return EXIT.OK;
90095
+ } catch (err) {
90096
+ const status = err.status;
90097
+ const detail = err instanceof Error ? err.message : String(err);
90098
+ const hasChineseDetail = /[一-龥]/.test(detail) && detail.length >= 4;
90099
+ const hint = hasChineseDetail ? "" : status === 401 ? "\uFF08\u7528\u6237\u540D\u6216\u5BC6\u7801\u9519\u8BEF\uFF09" : status === 403 ? "\uFF08\u8D26\u53F7\u5DF2\u88AB\u7981\u7528\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\uFF09" : status === 422 ? "\uFF08\u7528\u6237\u540D\u6216\u5BC6\u7801\u683C\u5F0F\u4E0D\u5408\u6CD5\uFF09" : "";
90100
+ writers.err(`protara: auth-failed: ${detail}${hint}
90101
+ `);
90102
+ log("warn", `[auth] \u8D26\u53F7\u5BC6\u7801\u767B\u5F55\u5931\u8D25\uFF1A${username} status=${status ?? "?"}`);
90103
+ return EXIT.AUTH;
90104
+ }
90105
+ }
90106
+ async function askUsername(options) {
90107
+ if (options.readUsername) {
90108
+ return (await options.readUsername("protara: \u7528\u6237\u540D: ") ?? "").trim();
90109
+ }
90110
+ const answer = await readLineOnce({ prompt: "protara: \u7528\u6237\u540D: " });
90111
+ return (answer ?? "").trim();
90112
+ }
90113
+ async function readLineOnce(options) {
90114
+ const input = options.input ?? process.stdin;
90115
+ if (!input.isTTY) return null;
90116
+ const output = options.output ?? process.stderr;
90117
+ const { createInterface: createInterface4 } = await import("node:readline");
90118
+ return await new Promise((resolve23) => {
90119
+ const rl = createInterface4({
90120
+ input,
90121
+ output,
90122
+ terminal: options.terminal ?? true
90123
+ });
90124
+ let answered = false;
90125
+ rl.question(options.prompt, (answer) => {
90126
+ answered = true;
90127
+ resolve23(answer);
90128
+ rl.close();
90129
+ });
90130
+ rl.once("close", () => {
90131
+ if (!answered) resolve23(null);
90132
+ });
90133
+ });
90134
+ }
90135
+ async function askLoginMethod(options) {
90136
+ const { writers } = options;
90137
+ const read = options.readLine ?? defaultReadLine;
90138
+ writers.err("protara: \u767B\u5F55\u65B9\u5F0F\uFF1A\n");
90139
+ writers.err(" 1) \u8BBE\u5907\u7801\u767B\u5F55 \u2014\u2014 \u5728\u6D4F\u89C8\u5668\u91CC\u786E\u8BA4\u4E00\u4E0B\uFF08\u6709\u6D4F\u89C8\u5668\u65F6\u6700\u7701\u4E8B\uFF09\n");
90140
+ writers.err(" 2) \u7528\u6237\u540D\u5BC6\u7801\u767B\u5F55 \u2014\u2014 \u6CA1\u6709\u6D4F\u89C8\u5668\u65F6\u7528\uFF08SSH / CI\uFF09\n");
90141
+ writers.err(" \u56DE\u8F66 = 1\uFF08\u8BBE\u5907\u7801\uFF09\n");
90142
+ for (let attempt = 0; attempt < 3; attempt += 1) {
90143
+ const answer = await read("protara: \u9009\u62E9 [1/2]: ");
90144
+ if (answer === null) {
90145
+ writers.err("\nprotara: \u5DF2\u53D6\u6D88\u767B\u5F55\n");
90146
+ return null;
90147
+ }
90148
+ const text2 = answer.trim();
90149
+ if (text2 === "" || text2 === "1") return "device";
90150
+ if (text2 === "2") return "password";
90151
+ writers.err(`protara: \u53EA\u80FD\u8F93\u5165 1 \u6216 2\uFF08\u6536\u5230 ${JSON.stringify(text2)}\uFF09
90152
+ `);
90153
+ }
90154
+ writers.err("protara: \u8FDE\u7EED 3 \u6B21\u6CA1\u542C\u61C2\uFF0C\u5148\u6309\u8BBE\u5907\u7801\u767B\u5F55\uFF08\u8981\u5BC6\u7801\u767B\u5F55\u8BF7\u52A0 --password\uFF09\n");
90155
+ return "device";
90156
+ }
90157
+ async function defaultReadLine(prompt) {
90158
+ return await readLineOnce({ prompt });
90159
+ }
90160
+ function resolveLoginMethod(method, options) {
90161
+ if (method !== "ask") return method;
90162
+ if (!options.stdinIsTty || !options.stdoutIsTty) return "device";
90163
+ return "ask";
90164
+ }
89869
90165
 
89870
90166
  // src/core/file-logger.ts
89871
90167
  var import_promises37 = require("node:fs/promises");
@@ -89910,25 +90206,47 @@ function createFileLogger(options) {
89910
90206
  for (let i = keep - 1; i >= 1; i -= 1) {
89911
90207
  const from = i === 1 ? file : `${file}.${i - 1}`;
89912
90208
  const to = `${file}.${i}`;
89913
- try {
89914
- if (i === keep - 1) await (0, import_promises37.unlink)(to).catch(() => {
90209
+ if (i === keep - 1) {
90210
+ await (0, import_promises37.unlink)(to).catch(() => {
89915
90211
  });
89916
- await (0, import_promises37.rename)(from, to);
89917
- } catch {
89918
90212
  }
90213
+ await (0, import_promises37.rename)(from, to).catch(() => {
90214
+ });
89919
90215
  }
89920
90216
  bytes = 0;
89921
90217
  };
89922
- const writeOne = async (line) => {
89923
- const text2 = format(line);
89924
- const size = Buffer.byteLength(text2, "utf8");
90218
+ let dirReady = false;
90219
+ const ensureDir = async () => {
90220
+ if (dirReady) return;
90221
+ await (0, import_promises37.mkdir)((0, import_node_path48.dirname)(file), { recursive: true });
90222
+ dirReady = true;
90223
+ };
90224
+ const writeBatch = async (lines) => {
90225
+ if (lines.length === 0) return;
89925
90226
  try {
89926
- await (0, import_promises37.mkdir)((0, import_node_path48.dirname)(file), { recursive: true });
89927
- await rotateIfNeeded(size);
89928
- await (0, import_promises37.appendFile)(file, text2, "utf8");
89929
- bytes += size;
89930
- written += 1;
89931
- if (options.alsoStderr) stderrWrite(text2);
90227
+ await ensureDir();
90228
+ let text2 = "";
90229
+ for (const line of lines) {
90230
+ const formatted = format(line);
90231
+ const lineSize = Buffer.byteLength(formatted, "utf8");
90232
+ if (bytes + Buffer.byteLength(text2, "utf8") + lineSize > maxBytes) {
90233
+ if (text2) {
90234
+ await (0, import_promises37.appendFile)(file, text2, "utf8");
90235
+ bytes += Buffer.byteLength(text2, "utf8");
90236
+ text2 = "";
90237
+ }
90238
+ await rotateIfNeeded(lineSize);
90239
+ }
90240
+ text2 += formatted;
90241
+ }
90242
+ if (text2) {
90243
+ await (0, import_promises37.appendFile)(file, text2, "utf8");
90244
+ bytes += Buffer.byteLength(text2, "utf8");
90245
+ }
90246
+ written += lines.length;
90247
+ if (options.alsoStderr) {
90248
+ for (const line of lines) stderrWrite(format(line));
90249
+ }
89932
90250
  } catch (err) {
89933
90251
  if (!reportedFailure) {
89934
90252
  reportedFailure = true;
@@ -89940,6 +90258,23 @@ function createFileLogger(options) {
89940
90258
  }
89941
90259
  }
89942
90260
  };
90261
+ let buffer = [];
90262
+ const MAX_BATCH = 64;
90263
+ let flushScheduled = false;
90264
+ const flushBuffer = () => {
90265
+ if (buffer.length === 0) return;
90266
+ const batch = buffer;
90267
+ buffer = [];
90268
+ tail = tail.then(() => writeBatch(batch));
90269
+ };
90270
+ const scheduleFlush = () => {
90271
+ if (flushScheduled) return;
90272
+ flushScheduled = true;
90273
+ queueMicrotask(() => {
90274
+ flushScheduled = false;
90275
+ flushBuffer();
90276
+ });
90277
+ };
89943
90278
  const log = (level, message, detail) => {
89944
90279
  if (!enabled) return;
89945
90280
  const suffix = detailTextOf(detail);
@@ -89947,8 +90282,9 @@ function createFileLogger(options) {
89947
90282
  const stack = level === "error" && detail instanceof Error && detail.stack ? `
89948
90283
  ${detail.stack.split("\n").slice(1).join("\n ")}` : "";
89949
90284
  const text2 = suffix ? `${message} ${suffix}${stack}` : `${message}${stack}`;
89950
- const pending = { level, text: text2 };
89951
- tail = tail.then(() => writeOne(pending));
90285
+ buffer.push({ level, text: text2 });
90286
+ if (buffer.length >= MAX_BATCH) flushBuffer();
90287
+ else scheduleFlush();
89952
90288
  };
89953
90289
  return {
89954
90290
  log,
@@ -89957,6 +90293,7 @@ function createFileLogger(options) {
89957
90293
  written: () => written,
89958
90294
  flush: async (timeoutMs = 2e3) => {
89959
90295
  const deadline = Date.now() + timeoutMs;
90296
+ flushBuffer();
89960
90297
  await Promise.race([
89961
90298
  initialSizeDone,
89962
90299
  new Promise((resolve23) => setTimeout(resolve23, Math.max(0, deadline - Date.now())).unref?.())
@@ -90065,7 +90402,7 @@ function installCrashLogging(log) {
90065
90402
 
90066
90403
  // src/cli/index.ts
90067
90404
  function cliVersion() {
90068
- if ("0.3.6") return "0.3.6";
90405
+ if ("0.3.7") return "0.3.7";
90069
90406
  try {
90070
90407
  const here = typeof __dirname === "string" ? `${__dirname}/../../package.json` : "";
90071
90408
  const pkg = JSON.parse((0, import_node_fs11.readFileSync)(here, "utf8"));
@@ -90114,6 +90451,37 @@ async function main(argv, env = process.env) {
90114
90451
  return EXIT.OK;
90115
90452
  }
90116
90453
  if (args.command === "login" || args.login) {
90454
+ let method = resolveLoginMethod(args.loginMethod, {
90455
+ stdinIsTty: Boolean(process.stdin.isTTY),
90456
+ stdoutIsTty: Boolean(process.stdout.isTTY)
90457
+ });
90458
+ if (method === "ask") {
90459
+ const choice = await askLoginMethod({ writers });
90460
+ if (choice === null) return EXIT.AUTH;
90461
+ method = choice;
90462
+ }
90463
+ if (method === "password") {
90464
+ let password = null;
90465
+ if (args.passwordFromStdin) {
90466
+ password = await readPasswordFromStdin();
90467
+ if (password === null) {
90468
+ writers.err("protara: usage: --password-stdin \u4F46\u6CA1\u6709\u4ECE stdin \u8BFB\u5230\u5185\u5BB9\n");
90469
+ return EXIT.USAGE;
90470
+ }
90471
+ } else if (env.PROTARA_PASSWORD) {
90472
+ password = env.PROTARA_PASSWORD;
90473
+ }
90474
+ return await withRuntime(
90475
+ args,
90476
+ (runtime) => runPasswordLogin(runtime, {
90477
+ writers,
90478
+ log,
90479
+ username: args.username,
90480
+ password
90481
+ }),
90482
+ { writers, log, flushLog: () => fileLogger.flush() }
90483
+ );
90484
+ }
90117
90485
  return await withRuntime(args, (runtime) => runDeviceLogin(runtime, { writers, log }), {
90118
90486
  writers,
90119
90487
  log,
@@ -90134,6 +90502,13 @@ async function main(argv, env = process.env) {
90134
90502
  flushLog: () => fileLogger.flush()
90135
90503
  });
90136
90504
  }
90505
+ if (args.command === "agents") {
90506
+ return await withRuntime(args, (runtime) => printAgents(runtime, { writers, json: args.mode === "json" }), {
90507
+ writers,
90508
+ log,
90509
+ flushLog: () => fileLogger.flush()
90510
+ });
90511
+ }
90137
90512
  const resolved = resolveMode(args, {
90138
90513
  stdoutIsTty: Boolean(process.stdout.isTTY),
90139
90514
  stdinIsTty: Boolean(process.stdin.isTTY)
@@ -90287,6 +90662,16 @@ function readStdin(timeoutMs = 0) {
90287
90662
  process.stdin.on("error", finish3);
90288
90663
  });
90289
90664
  }
90665
+ async function readPasswordFromStdin() {
90666
+ const chunks = [];
90667
+ for await (const chunk of process.stdin) {
90668
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
90669
+ if (Buffer.concat(chunks).includes(10)) break;
90670
+ }
90671
+ const text2 = Buffer.concat(chunks).toString("utf8");
90672
+ if (!text2) return null;
90673
+ return text2.replace(/\r?\n[\s\S]*$/, "");
90674
+ }
90290
90675
  function makeAsk() {
90291
90676
  if (!process.stdin.isTTY) return void 0;
90292
90677
  return (question) => new Promise((resolve23) => {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "//name": "⭐ 裸包名(2026-11-12 查过 registry.npmjs.org 上未被占用)。⚠️ npm 包名必须**全小写** —— 上级目录那个 `Protara`(大写 P)在 npm 上是**非法**的,`npm publish` 会直接 400。这也是必须拆独立包的原因之一。",
3
3
  "name": "protara",
4
4
  "//version": "⚠️ **由 `scripts/publish-cli.mjs` 在打包时写入**(取 `desktop-app/package.json` 的版本),⛔ 不要手改:两处版本漂移的症状是「npm 上是 0.3.6、包里 `--version` 打印 0.3.5」,而两个都看起来正常。",
5
- "version": "0.3.6",
5
+ "version": "0.3.7",
6
6
  "description": "Protara 的终端客户端:在终端里与 Agent 对话、跑本机工具、审批权限变更。无 GUI、可管道、退出码规范。",
7
7
  "//type": "CLI 产物是 CJS(`dist/cli/index.cjs`),而 bin 垫片是 ESM(`.mjs`)。`type: module` 让 `.mjs` 名正言顺,同时 `.cjs` 扩展名保证 CJS 解析。",
8
8
  "type": "module",
@@ -40,4 +40,4 @@
40
40
  "terminal",
41
41
  "desktop-agents"
42
42
  ]
43
- }
43
+ }