scream-code 0.19.5 → 0.19.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.
@@ -3,5 +3,5 @@ import { fileURLToPath as __cjsShimFileURLToPath } from 'node:url';
3
3
  import { dirname as __cjsShimDirname } from 'node:path';
4
4
  const __filename = __cjsShimFileURLToPath(import.meta.url);
5
5
  const __dirname = __cjsShimDirname(__filename);
6
- import { Qt as handleConnectCommand } from "./dispatch-Brb_yK-J.mjs";
6
+ import { Qt as handleConnectCommand } from "./dispatch-BiOeRj_C.mjs";
7
7
  export { handleConnectCommand };
package/dist/main.mjs CHANGED
@@ -6,7 +6,7 @@ const __dirname = __cjsShimDirname(__filename);
6
6
  import "./suppress-sqlite-warning-C2VB0doZ.mjs";
7
7
  //#region src/main.ts
8
8
  try {
9
- (await import("./app-JxeaGaIc.mjs")).main();
9
+ (await import("./app-Bpl0l99M.mjs")).main();
10
10
  } catch (error) {
11
11
  process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
12
12
  process.exit(1);
@@ -3,5 +3,5 @@ import { fileURLToPath as __cjsShimFileURLToPath } from 'node:url';
3
3
  import { dirname as __cjsShimDirname } from 'node:path';
4
4
  const __filename = __cjsShimFileURLToPath(import.meta.url);
5
5
  const __dirname = __cjsShimDirname(__filename);
6
- import { t as TextInputDialogComponent } from "./text-input-dialog-D6PfbOws.mjs";
6
+ import { t as TextInputDialogComponent } from "./text-input-dialog-Ce2AEFU9.mjs";
7
7
  export { TextInputDialogComponent };
@@ -757,10 +757,11 @@ const dictionaries = {
757
757
  "memory.detail_nav_hint": " Enter/Esc 返回 | i 注入 | d 删除 | e 编辑",
758
758
  "plugin.no_session": "请先创建或恢复一个会话,再使用 Plugin 中心。",
759
759
  "extension.no_session": "请先创建或恢复一个会话,再使用扩展命令。",
760
- "extension.usage": "用法:/extension activate|deactivate|status [pluginId]",
760
+ "extension.usage": "用法:/extension activate|deactivate|reload|status [pluginId]",
761
761
  "extension.missing_id": "缺少扩展 id:/extension {action} <pluginId>",
762
762
  "extension.activated": "扩展 {pluginId} 已激活。",
763
763
  "extension.deactivated": "扩展 {pluginId} 已停用。",
764
+ "extension.reloaded": "扩展 {pluginId} 已重新加载。",
764
765
  "extension.no_extensions": "没有安装代码插件(entryPoint)。",
765
766
  "plugin.loading": "正在加载 Plugin 中心…",
766
767
  "plugin.center_title": "Plugin 中心",
@@ -972,6 +973,7 @@ const dictionaries = {
972
973
  "usage.subagent_title": "子 Agent 用量",
973
974
  "usage.panel_title": " 用量 ",
974
975
  "usage.session_total": "会话总计",
976
+ "usage.cost": "花费",
975
977
  "usage.cache_rw": "缓存",
976
978
  "usage.main_agent": "主 Agent",
977
979
  "usage.sub_agent": "子 Agent",
@@ -2138,10 +2140,11 @@ const dictionaries = {
2138
2140
  "memory.detail_nav_hint": " Enter/Esc Back | i Inject | d Delete | e Edit",
2139
2141
  "plugin.no_session": "Please create or resume a session before using the Plugin center.",
2140
2142
  "extension.no_session": "Please create or resume a session before using extension commands.",
2141
- "extension.usage": "Usage: /extension activate|deactivate|status [pluginId]",
2143
+ "extension.usage": "Usage: /extension activate|deactivate|reload|status [pluginId]",
2142
2144
  "extension.missing_id": "Missing plugin id: /extension {action} <pluginId>",
2143
2145
  "extension.activated": "Extension {pluginId} activated.",
2144
2146
  "extension.deactivated": "Extension {pluginId} deactivated.",
2147
+ "extension.reloaded": "Extension {pluginId} reloaded.",
2145
2148
  "extension.no_extensions": "No code extensions (entryPoint) installed.",
2146
2149
  "plugin.loading": "Loading Plugin center…",
2147
2150
  "plugin.center_title": "Plugin Center",
@@ -2353,6 +2356,7 @@ const dictionaries = {
2353
2356
  "usage.subagent_title": "Sub-Agent Usage",
2354
2357
  "usage.panel_title": " Usage ",
2355
2358
  "usage.session_total": "Session Total",
2359
+ "usage.cost": "Cost",
2356
2360
  "usage.cache_rw": "cache",
2357
2361
  "usage.main_agent": "Main Agent",
2358
2362
  "usage.sub_agent": "Sub-Agent",
@@ -0,0 +1,198 @@
1
+ # Plugins — 给自己写插件
2
+
3
+ Scream 可以给自己写插件:遇到技能和 MCP 都覆盖不了的需求时,直接产出一个代码插件。
4
+
5
+ 插件能贡献四类能力:技能(提示词)、MCP server、shell hooks、以及在进程内运行的工具代码。注册后技能立即生效;MCP server 与 hooks 随激活启动,**代码只在 `activate` 时执行**。
6
+
7
+ ## 快速开始
8
+
9
+ **先征得同意**:动手写之前用 `AskUserQuestion` 问一句("要不要我做一个插件来完成这个?")。审批弹窗在 auto/yolo 下不会出现,所以 yolo 里问句是唯一还会出现的同意步骤(与 `/make-skill` 的确认方式一致)。auto 模式连问句也被禁(`AutoModeAskUserQuestionDenyPermissionPolicy`),因此自产插件属于交互模式的用法,无人值守场景请显式 `install`。manual 模式下 `register_generated` 仍会单独弹一次审批。
10
+
11
+ 目录约定 `<scream home>/plugins/generated/<id>/`(`<scream home>` 默认 `~/.scream-code`)。放在别处的目录也能注册,但任何目录都不会被自动扫描——注册必须显式发生。
12
+
13
+ `scream.plugin.json` 最小内容:
14
+
15
+ ```json
16
+ {
17
+ "name": "text-stats",
18
+ "version": "0.1.0",
19
+ "description": "Count lines and characters in a file",
20
+ "entryPoint": "./index.mjs"
21
+ }
22
+ ```
23
+
24
+ 入口 `index.mjs` 最小内容:
25
+
26
+ ```js
27
+ import { readFile } from 'node:fs/promises';
28
+
29
+ export async function activate(context) {
30
+ context.services.tools.registerUserTool({
31
+ name: 'text_stats',
32
+ description: 'Count lines and characters in a file',
33
+ parameters: {
34
+ type: 'object',
35
+ properties: { path: { type: 'string' } },
36
+ required: ['path'],
37
+ },
38
+ execute: async (args) => {
39
+ const text = await readFile(String(args.path), 'utf8');
40
+ return { output: `lines=${text.split('\n').length} chars=${text.length}` };
41
+ },
42
+ });
43
+ }
44
+ ```
45
+
46
+ 两步启用:
47
+
48
+ 1. `ManagePlugin { "action": "register_generated", "source": "<插件目录的绝对路径>" }` —— 登记目录、热应用技能,**不 import 入口**。
49
+ 2. `ManagePlugin { "action": "activate", "id": "text-stats" }` —— 单独审批后 import 入口并执行 `activate`,工具此刻才对模型可见。
50
+
51
+ ## 清单字段
52
+
53
+ | 字段 | 必填 | 说明 |
54
+ |---|---|---|
55
+ | `name` | 是 | 插件 id,须匹配 `^[a-z0-9][a-z0-9_-]{0,63}$` |
56
+ | `version` | 否 | 版本字符串,仅用于展示与检查 |
57
+ | `description` | 否 | 一句话说明 |
58
+ | `skills` | 否 | 技能目录,字符串或字符串数组;每项必须以 `./` 开头且位于插件目录内 |
59
+ | `mcpServers` | 否 | MCP server 配置对象,key 为 server 名;注册不会拉起进程,随 `activate` / `enable` 启动 |
60
+ | `hooks` | 否 | HookDef 数组(`event` / `command` / `matcher` / `timeout`),激活时注入、停用时移除 |
61
+ | `config` | 否 | 任意 JSON 对象,原样作为 `context.config` 交给入口模块 |
62
+ | `entryPoint` | 否 | 代码入口;必须 `./` 开头,且 realpath 后仍位于插件目录内。不满足时清单层丢弃该字段并留下诊断,`register_generated` 会据此报 `entry_point_rejected`,不会静默降级 |
63
+
64
+ `entryPoint` 指向的文件必须存在:`register_generated` 在登记前会 stat 该路径,缺失时返回 `entry_file_missing`,不会留下半登记记录。
65
+
66
+ ## 入口模块契约
67
+
68
+ ```js
69
+ export async function activate(context) {}
70
+
71
+ export async function deactivate() {}
72
+ ```
73
+
74
+ - 必须是 ESM。也接受 `export default { activate, deactivate }` 的写法(运行时统一经 `normalizeExtensionModule` 归一化)。
75
+ - `activate(context)` 必须导出;`deactivate()` 可选,且不接收参数——需要清理的东西在 `activate` 里捕获到模块作用域。
76
+ - `activate` 抛错时,本次激活完成一半的 hooks 与事件订阅会被回滚,错误写入插件记录并返回 `activation_failed`。
77
+ - 激活是**按会话**的:同一个插件可以在 A、B 两个会话里各跑一份(各自的 hooks、事件订阅、工具互不相干),但**同一会话内**不能重复激活;停用一个会话不影响另一个。
78
+ - 会话关闭时,它激活过的插件会被自动 `deactivate`,不会残留。
79
+
80
+ ## context API
81
+
82
+ | 成员 | 说明 |
83
+ |---|---|
84
+ | `pluginId` | 当前插件 id |
85
+ | `config` | 清单 `config` 块;未声明时为 `undefined` |
86
+ | `services.tools.registerUserTool(input)` | 注册进程内工具,立即对当前 agent 可见 |
87
+ | `services.tools.unregisterUserTool(name)` | 注销自己注册的工具 |
88
+ | `services.tools.unregisterToolsByOwner(id)` | 按 owner 批量注销;宿主在停用时也会自动执行 |
89
+ | `events.subscribe(type \| '*', handler)` | 订阅 agent 事件,返回 `unsubscribe`;`deactivate` 时宿主也会统一释放 |
90
+ | `services` | agent 的子系统清单;只应调用这里列出的工具相关成员 |
91
+ | `ui.notify(level, message)` | 向用户报一句:`info` / `warn` / `error`。作为状态区消息显示,不打断 |
92
+ | `ui.confirm({ title, body?, confirmLabel?, cancelLabel? })` | 问用户是/否,返回 `Promise<boolean>`;无界面宿主一律返回 `false` 并说明原因 |
93
+ | `ui.progress(title)` | 返回 `{ update(label), done(message?), fail(message?) }`;文本进度,`update` 限流约 5 次/秒 |
94
+
95
+ `registerUserTool` 的入参:
96
+
97
+ | 字段 | 说明 |
98
+ |---|---|
99
+ | `name` | 工具名,全局唯一 |
100
+ | `description` | 给模型看的一句话说明 |
101
+ | `parameters` | JSON Schema 对象(`type: "object"` 及 `properties` / `required`) |
102
+ | `execute(args, ctx)` | 返回 `{ output, message?, isError? }` 或 Promise;`output` 是模型看到的内容,`message` 只给 UI |
103
+
104
+ 运行时会给注册自动打上 `ownerPluginId`,因此停用、移除、熔断都能精确回收该插件的工具;插件自己声明的 owner 优先。`execute` 抛错会被转成工具错误,不会打断 agent loop,但会计入熔断计数。
105
+
106
+ ## 界面交互(ui)
107
+
108
+ 三个原语,全部**降级而不抛错**:
109
+
110
+ | 宿主 | `notify` | `confirm` | `progress` |
111
+ |---|---|---|---|
112
+ | TUI | 状态区一行 | 弹出选择框,选「确认」为 `true` | 状态区文本逐步更新 |
113
+ | `/line`、SDK、print | 事件日志 | 直接 `false` + 一条 warn | 状态区文本(若宿主有) |
114
+
115
+ `confirm` 走的是和 `AskUserQuestion` 同一条提问通道,所以不需要插件自己画界面;`notify` 复用宿主的警示事件(`code: "plugin:<level>"`),宿主可据此区分级别。**插件不能阻塞会话**:任何等待都有结果,宿主不在就按「否」处理。
116
+
117
+ ## 迭代
118
+
119
+ 改完入口文件后执行 `/extension reload <id>`(等价于 `deactivate` + `activate`),或者在会话里用 `ManagePlugin` 先 `deactivate` 再 `activate`。
120
+
121
+ 注意区分两个动作:`ManagePlugin {action:"reload"}` 只是**重读插件表**,不会重新 import;要重新 import 改过的入口文件,用 `/extension reload <id>` 或 `ManagePlugin {action:"reload_code", id:"<id>"}`。
122
+
123
+ 只调用 `activate` 不会加载新代码:模块按入口路径缓存,`deactivate` 会丢掉该缓存并让下一次 import 走新的 specifier,重新激活才读到新文件。激活失败、import 失败同样会让缓存失效,所以「改错 → 报错 → 改对 → reload」是可行的。
124
+
125
+ ## 已知限制
126
+
127
+ - **重启进程后需要重新激活**:插件记录仍在插件中心(`/plugin`、`ManagePlugin list` 可见),但激活态只存在于进程内。
128
+ - **激活绑定当前会话的 main agent**:工具注册在那一个 agent 上。同一进程里的另一个会话可能看到 `●` 状态,但拿不到这个工具。
129
+ - **熔断分两种,恢复方式不同**:
130
+ - **工具连续失败 3 次**:持久禁用(写进插件记录),`check` 的 `circuit` 能看到计数,`ManagePlugin { "action": "reset", "id": "<id>" }` 清除并重新启用。
131
+ - **事件 handler 连续抛错 3 次**:只在**当前进程内**停用,不改插件记录,`check.circuit` 也不会显示;修好 handler 后重新 `activate` 即可。
132
+ - **`reload` 只重新 import 入口模块**:入口用 `import './lib.mjs'` 引的其它文件仍走进程缓存,多文件插件改完辅助文件不会生效(改放进入口,或重启进程)。
133
+ - **每次 reload 都会在进程里留下一份旧模块实例**:这是「改了立刻生效」的代价,量级等于编辑次数,不影响正确性。
134
+ - 没有目录扫描或自动发现:新插件必须显式 `register_generated` / `install`。
135
+
136
+ ## 完整示例:text-stats
137
+
138
+ `scream.plugin.json`:
139
+
140
+ ```json
141
+ {
142
+ "name": "text-stats",
143
+ "version": "0.1.0",
144
+ "description": "Count lines and characters in a file",
145
+ "entryPoint": "./index.mjs"
146
+ }
147
+ ```
148
+
149
+ `index.mjs`:
150
+
151
+ ```js
152
+ import { readFile } from 'node:fs/promises';
153
+
154
+ // Kept in module scope so deactivate() can unregister without a context arg.
155
+ let tools;
156
+
157
+ export async function activate(context) {
158
+ tools = context.services.tools;
159
+ tools.registerUserTool({
160
+ name: 'text_stats',
161
+ description: '统计文件的行数与字符数',
162
+ parameters: {
163
+ type: 'object',
164
+ properties: {
165
+ path: { type: 'string', description: '要统计的文件路径' },
166
+ },
167
+ required: ['path'],
168
+ additionalProperties: false,
169
+ },
170
+ execute: async (args) => {
171
+ const text = await readFile(String(args.path), 'utf8');
172
+ return {
173
+ output: `lines=${text.split('\n').length} chars=${text.length}`,
174
+ message: `已统计 ${String(args.path)}`,
175
+ };
176
+ },
177
+ });
178
+ }
179
+
180
+ export async function deactivate() {
181
+ // The host reclaims owned tools too; this keeps the plugin self-contained.
182
+ tools?.unregisterUserTool('text_stats');
183
+ tools = undefined;
184
+ }
185
+ ```
186
+
187
+ 启用与迭代:
188
+
189
+ 1. `ManagePlugin { "action": "register_generated", "source": "/Users/me/.scream-code/plugins/generated/text-stats" }`
190
+ 2. `ManagePlugin { "action": "activate", "id": "text-stats" }`
191
+ 3. 改完代码后 `/extension reload text-stats`
192
+
193
+ ## 安全边界
194
+
195
+ - 入口只能在插件目录内:清单层强制 `./` 相对路径,并对 realpath 结果做越界检查(见 `resolvePluginPathField`)。越界的 `entryPoint` 会被清单层拒绝,`register_generated` 报 `entry_point_rejected`,不会降级成"看起来注册成功"的纯数据插件。
196
+ - 注册不执行代码:`register_generated` 只解析清单、登记记录;`activate` 是唯一的执行点,且单独要求审批。
197
+ - 审批身份包含目录路径与插件 id:`register_generated` 的审批串带完整 source,`activate` / `deactivate` / `remove` / `enable` / `disable` / `reset` / `rollback` / `set_mcp_enabled` / `reload_code` 带插件 id(如 `ManagePlugin(activate text-stats)`)。会话级授权只能覆盖同名对象,不能挪用到另一个目录或另一个插件。
198
+ - 激活后代码运行在 agent 进程内,可访问 `context.services`。只激活自己写的或完全信任的插件。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scream-code",
3
- "version": "0.19.5",
3
+ "version": "0.19.7",
4
4
  "description": "A terminal-native AI agent for builders",
5
5
  "license": "MIT",
6
6
  "author": "ScreamCli",
@@ -58,7 +58,7 @@
58
58
  "@earendil-works/pi-codemode": "^1.1.0",
59
59
  "@huggingface/tokenizers": "^0.2.0",
60
60
  "@larksuiteoapi/node-sdk": "^1.76.0",
61
- "@liutod-scream/pi-tui": "^1.1.0",
61
+ "@liutod-scream/pi-tui": "^1.5.0",
62
62
  "@mariozechner/clipboard": "^0.3.2",
63
63
  "chalk": "^5.4.1",
64
64
  "cli-highlight": "^2.1.11",