scream-code 0.19.3 → 0.19.6

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/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-NpW56jQ9.mjs")).main();
9
+ (await import("./app-CE9aji1S.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);
@@ -8,8 +8,8 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
8
8
  import { join } from "node:path";
9
9
  import { arch, hostname, release, type } from "node:os";
10
10
  import { execFileSync } from "node:child_process";
11
- import chalk from "chalk";
12
11
  import { Container, Input, Key, matchesKey, truncateToWidth, visibleWidth } from "@liutod-scream/pi-tui";
12
+ import chalk from "chalk";
13
13
  //#region ../../packages/config/src/identity.ts
14
14
  /**
15
15
  * Scream host and device identity header factories.
@@ -140,6 +140,7 @@ const dictionaries = {
140
140
  "status.cron_fired_coalesced": "⏰ 定时任务已触发(合并 {count} 次):{preview}",
141
141
  "status.cron_once": "⏰ 一次性提醒已触发:{preview}",
142
142
  "status.cron_stale": "⚠️ 定时任务已过期,仍触发:{preview}",
143
+ "status.cron_empty": "⚠️ 定时任务执行结束但没有输出:{preview}",
143
144
  "status.reconnecting_detail": "⚠️ 重连中 {attempt}/{max}:{reason} · {delay}s",
144
145
  "status.reason_rate_limit": "限流",
145
146
  "status.reason_server": "服务器错误",
@@ -756,10 +757,11 @@ const dictionaries = {
756
757
  "memory.detail_nav_hint": " Enter/Esc 返回 | i 注入 | d 删除 | e 编辑",
757
758
  "plugin.no_session": "请先创建或恢复一个会话,再使用 Plugin 中心。",
758
759
  "extension.no_session": "请先创建或恢复一个会话,再使用扩展命令。",
759
- "extension.usage": "用法:/extension activate|deactivate|status [pluginId]",
760
+ "extension.usage": "用法:/extension activate|deactivate|reload|status [pluginId]",
760
761
  "extension.missing_id": "缺少扩展 id:/extension {action} <pluginId>",
761
762
  "extension.activated": "扩展 {pluginId} 已激活。",
762
763
  "extension.deactivated": "扩展 {pluginId} 已停用。",
764
+ "extension.reloaded": "扩展 {pluginId} 已重新加载。",
763
765
  "extension.no_extensions": "没有安装代码插件(entryPoint)。",
764
766
  "plugin.loading": "正在加载 Plugin 中心…",
765
767
  "plugin.center_title": "Plugin 中心",
@@ -1361,6 +1363,9 @@ const dictionaries = {
1361
1363
  "cron.kind_recurring": "重复",
1362
1364
  "cron.kind_once": "一次性",
1363
1365
  "cron.stale": "已过期",
1366
+ "cron.silent": "静默",
1367
+ "cron.mute": "静音",
1368
+ "cron.last_error": "上次失败:{error}",
1364
1369
  "cron.sched_5m": "每 5 分钟",
1365
1370
  "cron.sched_hourly": "每小时",
1366
1371
  "cron.sched_daily9": "每天 09:00",
@@ -1517,6 +1522,7 @@ const dictionaries = {
1517
1522
  "status.cron_fired_coalesced": "⏰ Scheduled task fired ({count} runs merged): {preview}",
1518
1523
  "status.cron_once": "⏰ One-time reminder fired: {preview}",
1519
1524
  "status.cron_stale": "⚠️ Scheduled task expired, fired anyway: {preview}",
1525
+ "status.cron_empty": "⚠️ Scheduled task finished with no output: {preview}",
1520
1526
  "status.reconnecting_detail": "⚠️ Reconnecting {attempt}/{max}: {reason} · {delay}s",
1521
1527
  "status.reason_rate_limit": "rate limited",
1522
1528
  "status.reason_server": "server error",
@@ -2133,10 +2139,11 @@ const dictionaries = {
2133
2139
  "memory.detail_nav_hint": " Enter/Esc Back | i Inject | d Delete | e Edit",
2134
2140
  "plugin.no_session": "Please create or resume a session before using the Plugin center.",
2135
2141
  "extension.no_session": "Please create or resume a session before using extension commands.",
2136
- "extension.usage": "Usage: /extension activate|deactivate|status [pluginId]",
2142
+ "extension.usage": "Usage: /extension activate|deactivate|reload|status [pluginId]",
2137
2143
  "extension.missing_id": "Missing plugin id: /extension {action} <pluginId>",
2138
2144
  "extension.activated": "Extension {pluginId} activated.",
2139
2145
  "extension.deactivated": "Extension {pluginId} deactivated.",
2146
+ "extension.reloaded": "Extension {pluginId} reloaded.",
2140
2147
  "extension.no_extensions": "No code extensions (entryPoint) installed.",
2141
2148
  "plugin.loading": "Loading Plugin center…",
2142
2149
  "plugin.center_title": "Plugin Center",
@@ -2738,6 +2745,9 @@ const dictionaries = {
2738
2745
  "cron.kind_recurring": "repeats",
2739
2746
  "cron.kind_once": "one-shot",
2740
2747
  "cron.stale": "expired",
2748
+ "cron.silent": "silent",
2749
+ "cron.mute": "muted",
2750
+ "cron.last_error": "last failed: {error}",
2741
2751
  "cron.sched_5m": "Every 5 minutes",
2742
2752
  "cron.sched_hourly": "Hourly",
2743
2753
  "cron.sched_daily9": "Daily at 09:00",
@@ -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-yvsx17gg.mjs";
6
+ import { t as TextInputDialogComponent } from "./text-input-dialog-BqMy3ifH.mjs";
7
7
  export { TextInputDialogComponent };
package/docs/line.md CHANGED
@@ -104,11 +104,20 @@
104
104
  | `/status` | 当前模型、权限/计划/群狼/RLM 模式、上下文占用与思考级别 |
105
105
  | `/model <名称>` | 切换模型;不带参数显示当前模型 |
106
106
  | `/compact` | 压缩上下文 |
107
+ | `/stop` | 停止**正在运行**的那一轮,并丢弃该聊天还在排队的消息(IM 版的 ESC;忙时也可用),回执「已停止当前任务。」;若 10 秒内停不下来(工具挂死),会关闭在线会话,下一条消息自动恢复同一会话 |
108
+ | `/cancel` | 停止并开启新会话(等价 `/stop` + `/new`,忙时也可用) |
109
+ | `/cron` | 查看本聊天的定时任务(含「静默/静音」标记与「上次失败」);`/cron add [--silent\|--mute] <分 时 日 月 周> <要做的事>` 新建;`/cron rm <id>` 删除 |
110
+
111
+ - `--silent`:触发时不发「⏰ 已触发」,只发结果;`--mute`:什么都不发(纯后台维护任务)。模型侧 `CronCreate` 对应参数 `silent` / `mute`。
112
+ - 任务失败会在 `/cron` 列表里留一行「上次失败:…」,下次成功自动清除。
107
113
  | `/new` | 丢弃当前会话,下条消息开新会话 |
108
114
  | `/help` | 命令列表(含自定义命令) |
109
115
  | `/自定义` | 你自己在配置里定义的命令(`prompt` 型或 `exec` 型) |
110
116
 
111
- - 有任务正在处理时,`/model`、`/compact`、`/new` 会回复「请等这一轮结束」,避免与运行中的回合争用。
117
+ - 有任务正在处理时,`/model`、`/compact`、`/new` 会回复「请等这一轮结束(回复 /stop 可中止)」,避免与运行中的回合争用;`/stop` 本身**不受忙限制**。
118
+ - 消息排在别人后面时,会立刻回一条 `收到,前面还有 N 条在处理中。回复 /stop 可中止当前任务。`——不再静默排队。
119
+ - **定时任务现在能回到聊天**:`cron.fired` 在会话里触发后,守护进程会先发一行 `⏰ 定时任务已触发:…`,等这一轮跑完再把最终内容发到该聊天。投递靠入站时缓存的聊天目标(飞书 chat id / 微信 peer + context_token),因此**微信在守护进程重启后、你再次发消息之前无法主动推送**(与平台限制一致,日志会提示)。触发时若正有你的回合在跑,结果并入那一轮回复,不会重复推送。
120
+ - `/status` 末尾会带上启动时的语音能力探测结果(ffmpeg / AMR-NB 编码器是否可用),缺编码器意味着语音会降级成文件。
112
121
  - **自定义命令**:配置项的 `commands` 数组里定义,`prompt` 型会把模板(`{args}` 会被替换,无占位符则把参数附在末尾)作为一轮输入发给模型;`exec` 型在守护进程里执行 shell(macOS/Linux 为 `/bin/sh -c`,Windows 为 `%ComSpec% /d /s /c`;30 s 超时、输出截断 8 KiB、参数通过 `LINE_ARGS` 环境变量传递、`work_dir` 必须位于工作目录内)。Windows 上出于命令注入安全考虑**不支持 `{args}` 占位符**,请在脚本里读 `%LINE_ARGS%`。命令名不能与内置命令重名、不能重复。
113
122
  - **未识别的命令照常发给模型**;需要界面交互的命令(`/theme`、`/line` 向导、无参 `/model` 选择器、`/sessions`)会提示「需要桌面端」。
114
123
 
@@ -0,0 +1,183 @@
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
+ - 同一个进程内同一插件不能重复激活;重新激活前先 `deactivate`。
78
+
79
+ ## context API
80
+
81
+ | 成员 | 说明 |
82
+ |---|---|
83
+ | `pluginId` | 当前插件 id |
84
+ | `config` | 清单 `config` 块;未声明时为 `undefined` |
85
+ | `services.tools.registerUserTool(input)` | 注册进程内工具,立即对当前 agent 可见 |
86
+ | `services.tools.unregisterUserTool(name)` | 注销自己注册的工具 |
87
+ | `services.tools.unregisterToolsByOwner(id)` | 按 owner 批量注销;宿主在停用时也会自动执行 |
88
+ | `events.subscribe(type \| '*', handler)` | 订阅 agent 事件,返回 `unsubscribe`;`deactivate` 时宿主也会统一释放 |
89
+ | `services` | agent 的子系统清单;只应调用这里列出的工具相关成员 |
90
+
91
+ `registerUserTool` 的入参:
92
+
93
+ | 字段 | 说明 |
94
+ |---|---|
95
+ | `name` | 工具名,全局唯一 |
96
+ | `description` | 给模型看的一句话说明 |
97
+ | `parameters` | JSON Schema 对象(`type: "object"` 及 `properties` / `required`) |
98
+ | `execute(args, ctx)` | 返回 `{ output, message?, isError? }` 或 Promise;`output` 是模型看到的内容,`message` 只给 UI |
99
+
100
+ 运行时会给注册自动打上 `ownerPluginId`,因此停用、移除、熔断都能精确回收该插件的工具;插件自己声明的 owner 优先。`execute` 抛错会被转成工具错误,不会打断 agent loop,但会计入熔断计数。
101
+
102
+ ## 迭代
103
+
104
+ 改完入口文件后执行 `/extension reload <id>`(等价于 `deactivate` + `activate`),或者在会话里用 `ManagePlugin` 先 `deactivate` 再 `activate`。
105
+
106
+ 注意区分两个动作:`ManagePlugin {action:"reload"}` 只是**重读插件表**,不会重新 import;要重新 import 改过的入口文件,用 `/extension reload <id>` 或 `ManagePlugin {action:"reload_code", id:"<id>"}`。
107
+
108
+ 只调用 `activate` 不会加载新代码:模块按入口路径缓存,`deactivate` 会丢掉该缓存并让下一次 import 走新的 specifier,重新激活才读到新文件。激活失败、import 失败同样会让缓存失效,所以「改错 → 报错 → 改对 → reload」是可行的。
109
+
110
+ ## 已知限制
111
+
112
+ - **重启进程后需要重新激活**:插件记录仍在插件中心(`/plugin`、`ManagePlugin list` 可见),但激活态只存在于进程内。
113
+ - **激活绑定当前会话的 main agent**:工具注册在那一个 agent 上。同一进程里的另一个会话可能看到 `●` 状态,但拿不到这个工具。
114
+ - **熔断分两种,恢复方式不同**:
115
+ - **工具连续失败 3 次**:持久禁用(写进插件记录),`check` 的 `circuit` 能看到计数,`ManagePlugin { "action": "reset", "id": "<id>" }` 清除并重新启用。
116
+ - **事件 handler 连续抛错 3 次**:只在**当前进程内**停用,不改插件记录,`check.circuit` 也不会显示;修好 handler 后重新 `activate` 即可。
117
+ - **`reload` 只重新 import 入口模块**:入口用 `import './lib.mjs'` 引的其它文件仍走进程缓存,多文件插件改完辅助文件不会生效(改放进入口,或重启进程)。
118
+ - **每次 reload 都会在进程里留下一份旧模块实例**:这是「改了立刻生效」的代价,量级等于编辑次数,不影响正确性。
119
+ - 没有目录扫描或自动发现:新插件必须显式 `register_generated` / `install`。
120
+
121
+ ## 完整示例:text-stats
122
+
123
+ `scream.plugin.json`:
124
+
125
+ ```json
126
+ {
127
+ "name": "text-stats",
128
+ "version": "0.1.0",
129
+ "description": "Count lines and characters in a file",
130
+ "entryPoint": "./index.mjs"
131
+ }
132
+ ```
133
+
134
+ `index.mjs`:
135
+
136
+ ```js
137
+ import { readFile } from 'node:fs/promises';
138
+
139
+ // Kept in module scope so deactivate() can unregister without a context arg.
140
+ let tools;
141
+
142
+ export async function activate(context) {
143
+ tools = context.services.tools;
144
+ tools.registerUserTool({
145
+ name: 'text_stats',
146
+ description: '统计文件的行数与字符数',
147
+ parameters: {
148
+ type: 'object',
149
+ properties: {
150
+ path: { type: 'string', description: '要统计的文件路径' },
151
+ },
152
+ required: ['path'],
153
+ additionalProperties: false,
154
+ },
155
+ execute: async (args) => {
156
+ const text = await readFile(String(args.path), 'utf8');
157
+ return {
158
+ output: `lines=${text.split('\n').length} chars=${text.length}`,
159
+ message: `已统计 ${String(args.path)}`,
160
+ };
161
+ },
162
+ });
163
+ }
164
+
165
+ export async function deactivate() {
166
+ // The host reclaims owned tools too; this keeps the plugin self-contained.
167
+ tools?.unregisterUserTool('text_stats');
168
+ tools = undefined;
169
+ }
170
+ ```
171
+
172
+ 启用与迭代:
173
+
174
+ 1. `ManagePlugin { "action": "register_generated", "source": "/Users/me/.scream-code/plugins/generated/text-stats" }`
175
+ 2. `ManagePlugin { "action": "activate", "id": "text-stats" }`
176
+ 3. 改完代码后 `/extension reload text-stats`
177
+
178
+ ## 安全边界
179
+
180
+ - 入口只能在插件目录内:清单层强制 `./` 相对路径,并对 realpath 结果做越界检查(见 `resolvePluginPathField`)。越界的 `entryPoint` 会被清单层拒绝,`register_generated` 报 `entry_point_rejected`,不会降级成"看起来注册成功"的纯数据插件。
181
+ - 注册不执行代码:`register_generated` 只解析清单、登记记录;`activate` 是唯一的执行点,且单独要求审批。
182
+ - 审批身份包含目录路径与插件 id:`register_generated` 的审批串带完整 source,`activate` / `deactivate` / `remove` / `enable` / `disable` / `reset` / `rollback` / `set_mcp_enabled` / `reload_code` 带插件 id(如 `ManagePlugin(activate text-stats)`)。会话级授权只能覆盖同名对象,不能挪用到另一个目录或另一个插件。
183
+ - 激活后代码运行在 agent 进程内,可访问 `context.services`。只激活自己写的或完全信任的插件。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scream-code",
3
- "version": "0.19.3",
3
+ "version": "0.19.6",
4
4
  "description": "A terminal-native AI agent for builders",
5
5
  "license": "MIT",
6
6
  "author": "ScreamCli",