scream-code 0.12.3 → 0.12.5

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-D0r0X4TD.mjs")).main();
9
+ (await import("./app-35ccG0J0.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);
@@ -563,6 +563,7 @@ const dictionaries = {
563
563
  "memory.session_label": "会话: ",
564
564
  "memory.pitfall_label": "踩坑: ",
565
565
  "memory.experience_label": "经验: ",
566
+ "memory.note_label": "笔记: ",
566
567
  "memory.detail_nav_hint": " Enter/Esc 返回 | i 注入 | d 删除",
567
568
  "skill.no_session": "请先创建或恢复一个会话,再使用 Skill 中心。",
568
569
  "skill.loading": "正在加载 Skill 中心…",
@@ -1034,6 +1035,9 @@ const dictionaries = {
1034
1035
  "config.yolo_already_off": "YES 模式已关闭",
1035
1036
  "config.ask_already_on": "ASK 模式已开启",
1036
1037
  "config.ask_already_off": "ASK 模式已关闭",
1038
+ "config.rlm_on": "RLM 模式已开启 — 持久化 Python 内核可用",
1039
+ "config.rlm_off": "RLM 模式已关闭",
1040
+ "config.rlm_usage": "用法: /rlm (切换) | /rlm on | /rlm off",
1037
1041
  "config.auto_already_on": "自动模式已开启",
1038
1042
  "config.auto_already_off": "自动模式已关闭",
1039
1043
  "settings.title": "设置",
@@ -1069,6 +1073,8 @@ const dictionaries = {
1069
1073
  "registry.auto_desc": "切换自动权限模式",
1070
1074
  "registry.yolo_desc": "切换至自动批准模式(yolo)",
1071
1075
  "registry.ask_desc": "切换至只读问答模式(ask)",
1076
+ "registry.rlm_desc": "RLM 模式:面向长任务的持久 Python 工作环境,支持无限递归子代理(/rlm 切换开关)",
1077
+ "registry.rlm_max_depth_desc": "设置 RLM 递归深度上限(/rlm-max-depth N)",
1072
1078
  "registry.wolfpack_desc": "切换群狼协作模式,自动批准+批量并发",
1073
1079
  "registry.sessions_desc": "浏览并恢复会话",
1074
1080
  "registry.goal_desc": "查看/管理自动目标",
@@ -1143,6 +1149,7 @@ const dictionaries = {
1143
1149
  "badge.plan": "计划",
1144
1150
  "badge.fusion": "融合计划",
1145
1151
  "badge.wolfpack": "群狼",
1152
+ "badge.rlm": "RLM",
1146
1153
  "badge.goal": "目标",
1147
1154
  "badge.auto": "自动",
1148
1155
  "badge.yes": "YES",
@@ -1618,6 +1625,7 @@ const dictionaries = {
1618
1625
  "memory.session_label": "Session: ",
1619
1626
  "memory.pitfall_label": "Pitfalls: ",
1620
1627
  "memory.experience_label": "Experience: ",
1628
+ "memory.note_label": "Note: ",
1621
1629
  "memory.detail_nav_hint": " Enter/Esc Back | i Inject | d Delete",
1622
1630
  "skill.no_session": "Please create or resume a session before using the Skill center.",
1623
1631
  "skill.loading": "Loading Skill center…",
@@ -2089,6 +2097,9 @@ const dictionaries = {
2089
2097
  "config.yolo_already_off": "YES mode is already off",
2090
2098
  "config.ask_already_on": "ASK mode is already on",
2091
2099
  "config.ask_already_off": "ASK mode is already off",
2100
+ "config.rlm_on": "RLM mode enabled — persistent python kernel available",
2101
+ "config.rlm_off": "RLM mode disabled",
2102
+ "config.rlm_usage": "Usage: /rlm (toggle) | /rlm on | /rlm off",
2092
2103
  "config.auto_already_on": "Auto mode is already on",
2093
2104
  "config.auto_already_off": "Auto mode is already off",
2094
2105
  "settings.title": "Settings",
@@ -2124,6 +2135,8 @@ const dictionaries = {
2124
2135
  "registry.auto_desc": "Toggle auto permission mode",
2125
2136
  "registry.yolo_desc": "Toggle auto-approve mode (yolo)",
2126
2137
  "registry.ask_desc": "Toggle read-only Q&A mode (ask)",
2138
+ "registry.rlm_desc": "RLM mode: a persistent Python workspace for long-running tasks with unlimited recursive subagents (/rlm toggles)",
2139
+ "registry.rlm_max_depth_desc": "Set RLM recursion depth limit (/rlm-max-depth N)",
2127
2140
  "registry.wolfpack_desc": "Toggle wolfpack mode, auto-approve + batch concurrency",
2128
2141
  "registry.sessions_desc": "Browse and restore sessions",
2129
2142
  "registry.goal_desc": "View/manage auto goals",
@@ -2198,6 +2211,7 @@ const dictionaries = {
2198
2211
  "badge.plan": "Plan",
2199
2212
  "badge.fusion": "Fusion",
2200
2213
  "badge.wolfpack": "Wolfpack",
2214
+ "badge.rlm": "RLM",
2201
2215
  "badge.goal": "Goal",
2202
2216
  "badge.auto": "Auto",
2203
2217
  "badge.yes": "YES",
@@ -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-DHhRrX3G.mjs";
6
+ import { t as TextInputDialogComponent } from "./text-input-dialog-BQeV1dTM.mjs";
7
7
  export { TextInputDialogComponent };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scream-code",
3
- "version": "0.12.3",
3
+ "version": "0.12.5",
4
4
  "description": "A terminal-native AI agent for builders",
5
5
  "license": "MIT",
6
6
  "author": "ScreamCli",
@@ -27,9 +27,7 @@
27
27
  },
28
28
  "files": [
29
29
  "dist",
30
- "icon.ico",
31
- "scripts/postinstall.mjs",
32
- "scripts/postinstall"
30
+ "icon.ico"
33
31
  ],
34
32
  "type": "module",
35
33
  "imports": {
@@ -53,8 +51,6 @@
53
51
  "test": "pnpm -w run build:packages && vitest run",
54
52
  "e2e": "pnpm -w run build:packages && SCREAM_E2E=1 vitest run test/e2e",
55
53
  "e2e:real": "pnpm -w run build:packages && SCREAM_E2E_REAL=1 vitest run test/e2e/real-llm-smoke.e2e.test.ts",
56
- "preinstall": "node -e \"console.log('\\n📦 正在安装 scream-code,请稍候...\\n')\"",
57
- "postinstall": "node scripts/postinstall.mjs",
58
54
  "prepublishOnly": "node scripts/verify-publish.mjs",
59
55
  "smoke": "node dist/main.mjs --version"
60
56
  },
@@ -1,351 +0,0 @@
1
- /**
2
- * Detection of the previous Python `scream-cli` shim and the actual
3
- * filesystem operations that perform (or refuse) the rename.
4
- *
5
- * Detection:
6
- * - {@link detectLegacyShims}: walks the caller-supplied
7
- * `pathString` and returns every legacy `scream` shim along the
8
- * way (in PATH order). A shim qualifies when it realpath-resolves
9
- * outside our own installed package root and its head 4 KiB
10
- * contains the `scream_cli` module marker — the setuptools
11
- * entry-point format produced by `uv tool install`,
12
- * `pipx install`, `pip install`, etc. Returning all hits (not
13
- * just the first) matters because a user with both uv- and
14
- * pipx-installed `scream-cli` has two legacy shims in different
15
- * dirs, and renaming only the earlier one leaves the later one
16
- * shadowing our new CLI. Callers should pass the `detection`
17
- * field from `postinstallPaths()` so detection sees the union of
18
- * the shell PATH and the installer's PATH.
19
- * - {@link isLegacyShim}: same criterion in standalone form, used to
20
- * decide whether an existing `scream-legacy` is itself a legacy CLI
21
- * (safe to consolidate over) or a user-managed file (preserve).
22
- *
23
- * Classify + execute (two-phase):
24
- * - {@link classifyShim}: pre-flight inspection that says what we
25
- * COULD do to a given shim. Returns one of `renameable`,
26
- * `consolidate`, `delete-only`, `blocked`. No filesystem writes.
27
- * - {@link renameInPlace} / {@link deleteShim}: the primitive
28
- * operations that actually mutate the filesystem. Run after the
29
- * orchestrator has looked at the full set of classifications and
30
- * decided to proceed.
31
- *
32
- * Splitting classification from execution lets the orchestrator make
33
- * the abort-or-proceed decision once, against the whole detected set,
34
- * rather than discovering mid-loop that something failed and ending up
35
- * with a misleading "scream now launches the new CLI" notice in front of
36
- * a "permission denied" notice. Uses `fs.lstat` (not `fs.access`) to
37
- * detect dangling symlinks at the target so we don't clobber them.
38
- */
39
-
40
- import { constants as fsConstants, promises as fs } from 'node:fs';
41
- import { delimiter, dirname, extname, join, sep } from 'node:path';
42
-
43
- const LEGACY_BIN = 'scream';
44
- const LEGACY_RENAME = 'scream-legacy';
45
- const PYTHON_MARKER = 'scream_cli';
46
- const IS_WINDOWS = process.platform === 'win32';
47
-
48
- // Read window for the marker sniff.
49
- // POSIX: setuptools entry-point scripts are a few hundred bytes —
50
- // 4 KiB is generous.
51
- // Windows: `uv tool install` produces a Rust-built launcher .exe
52
- // (~45 KiB) and the `scream_cli` module name sits embedded
53
- // near the END of the file (verified offset ≈ 44103 on
54
- // uv 0.11). Cap reads at 256 KiB so a hostile or
55
- // unexpectedly large file can't make us hold a lot of
56
- // memory.
57
- const SHIM_SNIFF_BYTES_POSIX = 4096;
58
- const SHIM_SNIFF_BYTES_WINDOWS_MAX = 256 * 1024;
59
-
60
- function pathEntries(pathString) {
61
- if (!pathString) return [];
62
- const seen = new Set();
63
- const out = [];
64
- for (const entry of pathString.split(delimiter)) {
65
- if (!entry || seen.has(entry)) continue;
66
- seen.add(entry);
67
- out.push(entry);
68
- }
69
- return out;
70
- }
71
-
72
- /**
73
- * Expand `scream` into the set of filenames that resolve as executables
74
- * on this platform. POSIX → just `['scream']`. Windows → adds every
75
- * `PATHEXT` extension (so we find `scream.exe`, `scream.cmd`, etc).
76
- */
77
- function executableCandidates(basename) {
78
- if (!IS_WINDOWS) return [basename];
79
- const pathext = (process.env['PATHEXT'] ?? '.EXE;.CMD;.BAT;.COM')
80
- .toLowerCase()
81
- .split(';')
82
- .map((e) => e.trim())
83
- .filter(Boolean);
84
- return [basename, ...pathext.map((ext) => basename + ext)];
85
- }
86
-
87
- async function isExecutableFile(filePath) {
88
- try {
89
- const info = await fs.stat(filePath);
90
- if (!info.isFile()) return false;
91
- // Windows: stat().mode doesn't reflect ACLs in any useful way.
92
- // Callers already restrict to PATHEXT candidates, so existence
93
- // suffices.
94
- if (IS_WINDOWS) return true;
95
- return (info.mode & 0o111) !== 0;
96
- } catch {
97
- return false;
98
- }
99
- }
100
-
101
- async function readShimHead(filePath) {
102
- let handle;
103
- try {
104
- handle = await fs.open(filePath, 'r');
105
- const stat = await handle.stat();
106
- const limit = IS_WINDOWS ? SHIM_SNIFF_BYTES_WINDOWS_MAX : SHIM_SNIFF_BYTES_POSIX;
107
- const target = Math.min(stat.size, limit);
108
- const buffer = Buffer.alloc(target);
109
- const { bytesRead } = await handle.read(buffer, 0, target, 0);
110
- // `latin1` is a 1-to-1 byte→char mapping; we're searching for an
111
- // ASCII substring, so we don't want UTF-8 decoding to mangle the
112
- // bytes around the marker.
113
- return buffer.subarray(0, bytesRead).toString('latin1');
114
- } catch {
115
- return null;
116
- } finally {
117
- if (handle) await handle.close().catch(() => {});
118
- }
119
- }
120
-
121
- /**
122
- * Walk `pathString` and return every legacy `scream` shim along the
123
- * way, in PATH order. The orchestrator renames each in turn — a
124
- * single rename is insufficient when the user has multiple legacy
125
- * installs (e.g. one from `uv tool install` and another from `pipx
126
- * install`) in different directories, because the survivor still
127
- * shadows the new CLI.
128
- *
129
- * Each entry has the same shape as the previous single-return
130
- * value: `{ shimPath, realPath }`. The empty array means
131
- * "fresh-install / no-op".
132
- */
133
- export async function detectLegacyShims(ownRoot, pathString) {
134
- const ownRootPrefix = ownRoot ? ownRoot + sep : null;
135
- const candidates = executableCandidates(LEGACY_BIN);
136
- const results = [];
137
- const seenShims = new Set();
138
-
139
- for (const dir of pathEntries(pathString)) {
140
- for (const name of candidates) {
141
- const shimPath = join(dir, name);
142
- if (seenShims.has(shimPath)) continue;
143
- if (!(await isExecutableFile(shimPath))) continue;
144
-
145
- let realPath;
146
- try {
147
- realPath = await fs.realpath(shimPath);
148
- } catch {
149
- continue;
150
- }
151
-
152
- // Defence-in-depth: never touch a `scream` that resolves into our
153
- // own installed package. The `scream_cli` marker check below
154
- // already excludes the manager's generated wrapper today, but
155
- // this layer keeps us safe if anything in our bundle ever
156
- // happens to contain the marker substring.
157
- if (
158
- ownRootPrefix !== null &&
159
- (realPath === ownRoot || realPath.startsWith(ownRootPrefix))
160
- ) {
161
- continue;
162
- }
163
-
164
- const head = await readShimHead(realPath);
165
- if (!head || !head.includes(PYTHON_MARKER)) continue;
166
-
167
- seenShims.add(shimPath);
168
- results.push({ shimPath, realPath });
169
- }
170
- }
171
- return results;
172
- }
173
-
174
- /**
175
- * Does the file at `p` look like the legacy Python `scream-cli`?
176
- *
177
- * Same criterion as {@link detectLegacyShim} uses to recognize the
178
- * original shim: realpath-resolvable and the first 4 KiB of the
179
- * resolved file contains the `scream_cli` module name. Used to decide
180
- * whether an existing `scream-legacy` is itself a legacy shim (safe to
181
- * drop the duplicate `scream`) or a user-managed file we must not
182
- * clobber.
183
- */
184
- export async function isLegacyShim(p) {
185
- let real;
186
- try {
187
- real = await fs.realpath(p);
188
- } catch {
189
- return false;
190
- }
191
- const head = await readShimHead(real);
192
- return Boolean(head && head.includes(PYTHON_MARKER));
193
- }
194
-
195
- async function pathExists(p) {
196
- try {
197
- // lstat (not access/stat) so a dangling symlink at `p` still
198
- // reports as existing — `fs.access` follows symlinks and would
199
- // return ENOENT for a broken link, after which `fs.rename` would
200
- // silently replace it.
201
- await fs.lstat(p);
202
- return true;
203
- } catch {
204
- return false;
205
- }
206
- }
207
-
208
- /**
209
- * Compute where `shimPath` should be renamed to. Preserves the file
210
- * extension so a Windows `C:\…\scream.exe` ends up at `scream-legacy.exe`
211
- * rather than an extension-less `scream-legacy` that `scream.exe -- legacy`
212
- * shells won't run.
213
- */
214
- function renameTargetFor(shimPath) {
215
- const ext = extname(shimPath); // "" on POSIX, ".exe" on Windows
216
- return join(dirname(shimPath), LEGACY_RENAME + ext);
217
- }
218
-
219
- /**
220
- * Is the directory containing `shimPath` a system-managed location
221
- * the current user can't write to?
222
- *
223
- * POSIX: dir owned by uid 0 (root) — captures
224
- * `sudo pip install scream-cli` → `/usr/local/bin/`.
225
- *
226
- * Windows: dir under one of the well-known system roots
227
- * (`C:\Program Files`, `C:\ProgramData`, `C:\Windows`). uv tool
228
- * installs land in user space (`%USERPROFILE%\.local\bin`) so this
229
- * path almost never fires on Windows in practice, but the heuristic
230
- * correctly classifies the rare admin-prefix install case.
231
- *
232
- * The dedicated permission-denied notice (`logPermissionDenied`)
233
- * uses this to switch from a bare "rename it manually" message to a
234
- * sudo-aware / admin-aware explanation.
235
- */
236
- async function isSystemOwnedDir(shimPath) {
237
- if (IS_WINDOWS) {
238
- const dir = dirname(shimPath).toLowerCase();
239
- const systemRoots = [
240
- 'c:\\program files',
241
- 'c:\\program files (x86)',
242
- 'c:\\programdata',
243
- 'c:\\windows',
244
- ];
245
- return systemRoots.some(
246
- (root) => dir === root || dir.startsWith(root + '\\'),
247
- );
248
- }
249
- try {
250
- const info = await fs.stat(dirname(shimPath));
251
- return info.uid === 0;
252
- } catch {
253
- return false;
254
- }
255
- }
256
-
257
- async function canWriteDir(dir) {
258
- try {
259
- // For `fs.rename` and `fs.unlink` the parent dir needs to be both
260
- // writable and executable (POSIX `wx`). `fs.access` follows the
261
- // same semantics. On Windows ACL details are coarse but
262
- // `fs.access(W_OK)` is a reasonable best-effort.
263
- await fs.access(dir, fsConstants.W_OK | fsConstants.X_OK);
264
- return true;
265
- } catch {
266
- return false;
267
- }
268
- }
269
-
270
- /**
271
- * Pre-flight inspection of a single legacy shim. Returns what action
272
- * we could take WITHOUT executing it. The orchestrator uses these
273
- * classifications to decide the whole-set strategy (abort vs proceed,
274
- * which shim becomes scream-legacy) before any filesystem writes
275
- * happen.
276
- *
277
- * Result shapes (all carry `shimPath` and `target` so the renderer
278
- * has the paths):
279
- * - `renameable` : can `fs.rename(shim → target)` cleanly;
280
- * target slot is free.
281
- * - `consolidate` : target already exists and is itself a legacy
282
- * shim; we'd `fs.unlink(shim)` and leave the
283
- * existing `scream-legacy` as the canonical
284
- * fallback (functionally equivalent — same
285
- * upstream package).
286
- * - `delete-only` : target exists but is a user-managed file we
287
- * won't clobber. We can still `fs.unlink(shim)`
288
- * to stop the shadowing; the
289
- * "preserve original scream" invariant fails for
290
- * THIS dir (we tell the user).
291
- * - `blocked` : we can't write to the parent dir. Carries
292
- * `isSystemPath` so the renderer can suggest
293
- * sudo (POSIX) or admin PowerShell (Windows).
294
- */
295
- export async function classifyShim(shimPath) {
296
- const target = renameTargetFor(shimPath);
297
- const dir = dirname(shimPath);
298
-
299
- if (!(await canWriteDir(dir))) {
300
- return {
301
- kind: 'blocked',
302
- shimPath,
303
- target,
304
- isSystemPath: await isSystemOwnedDir(shimPath),
305
- };
306
- }
307
-
308
- if (await pathExists(target)) {
309
- if (await isLegacyShim(target)) {
310
- return { kind: 'consolidate', shimPath, target };
311
- }
312
- return { kind: 'delete-only', shimPath, target };
313
- }
314
-
315
- return { kind: 'renameable', shimPath, target };
316
- }
317
-
318
- /**
319
- * Execute an `fs.rename`. Pre-flight classification establishes
320
- * whether this is expected to succeed; this primitive just runs the
321
- * call and wraps the error.
322
- */
323
- export async function renameInPlace(shimPath, target) {
324
- try {
325
- await fs.rename(shimPath, target);
326
- return { success: true };
327
- } catch (err) {
328
- const code = err && typeof err === 'object' ? err.code : undefined;
329
- const message = err instanceof Error ? err.message : String(err);
330
- return { success: false, code, message };
331
- }
332
- }
333
-
334
- /**
335
- * Execute an `fs.unlink`. Used when:
336
- * - we're consolidating onto an existing legacy `scream-legacy`, or
337
- * - we couldn't rename (foreign target) but can still remove the
338
- * shadow, or
339
- * - this is a non-first shim and we just want to clear it out so
340
- * PATH order resolves to our new CLI.
341
- */
342
- export async function deleteShim(shimPath) {
343
- try {
344
- await fs.unlink(shimPath);
345
- return { success: true };
346
- } catch (err) {
347
- const code = err && typeof err === 'object' ? err.code : undefined;
348
- const message = err instanceof Error ? err.message : String(err);
349
- return { success: false, code, message };
350
- }
351
- }