@liumir/lmcode 0.5.13

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.
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ import { fileURLToPath as __cjsShimFileURLToPath } from 'node:url';
3
+ import { dirname as __cjsShimDirname } from 'node:path';
4
+ const __filename = __cjsShimFileURLToPath(import.meta.url);
5
+ const __dirname = __cjsShimDirname(__filename);
6
+ //#region src/utils/suppress-sqlite-warning.ts
7
+ /**
8
+ * Suppress Node's ExperimentalWarning for the built-in `node:sqlite` module.
9
+ *
10
+ * Node emits this warning while `node:sqlite` is loaded as a transitive
11
+ * dependency during ESM module evaluation, before any app code can intercept
12
+ * it via `process.emitWarning` or `process.on('warning')`. We filter the known
13
+ * warning text from stderr to keep startup output clean.
14
+ */
15
+ const originalWrite = process.stderr.write.bind(process.stderr);
16
+ let expectingTraceSuggestion = false;
17
+ function isSQLiteExperimentalWarning(line) {
18
+ return /^\(node:\d+\) ExperimentalWarning: SQLite is an experimental feature/.test(line);
19
+ }
20
+ function isTraceSuggestion(line) {
21
+ return line.startsWith("(Use `node --trace-warnings");
22
+ }
23
+ process.stderr.write = (chunk, ...args) => {
24
+ const lines = (typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8")).split("\n");
25
+ let changed = false;
26
+ const kept = [];
27
+ for (const line of lines) {
28
+ if (isSQLiteExperimentalWarning(line)) {
29
+ expectingTraceSuggestion = true;
30
+ changed = true;
31
+ continue;
32
+ }
33
+ if (expectingTraceSuggestion && isTraceSuggestion(line)) {
34
+ expectingTraceSuggestion = false;
35
+ changed = true;
36
+ continue;
37
+ }
38
+ expectingTraceSuggestion = false;
39
+ kept.push(line);
40
+ }
41
+ if (changed) {
42
+ const filtered = kept.join("\n");
43
+ if (filtered.length === 0) {
44
+ args.find((a) => typeof a === "function")?.();
45
+ return true;
46
+ }
47
+ return originalWrite(filtered, ...args);
48
+ }
49
+ return originalWrite(chunk, ...args);
50
+ };
51
+ //#endregion
52
+ export {};
package/icon.ico ADDED
Binary file
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@liumir/lmcode",
3
+ "version": "0.5.13",
4
+ "description": "A terminal-native AI agent for builders",
5
+ "license": "MIT",
6
+ "author": "liumir",
7
+ "homepage": "https://github.com/Lyin01/LMcode-cli/tree/main/apps/lmcode#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Lyin01/LMcode-cli.git",
11
+ "directory": "apps/lmcode"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/Lyin01/LMcode-cli/issues"
15
+ },
16
+ "keywords": [
17
+ "lmcode",
18
+ "cli",
19
+ "agent",
20
+ "coding-agent",
21
+ "ai",
22
+ "tui"
23
+ ],
24
+ "bin": {
25
+ "lm": "dist/main.mjs"
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "icon.ico",
30
+ "scripts/postinstall.mjs",
31
+ "scripts/postinstall"
32
+ ],
33
+ "type": "module",
34
+ "imports": {
35
+ "#/*": [
36
+ "./src/*.ts",
37
+ "./src/*/index.ts"
38
+ ]
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "scripts": {
44
+ "build": "tsdown",
45
+ "dev": "node scripts/dev.mjs",
46
+ "dev:cli-only": "tsx --import ../../build/register-raw-text-loader.mjs ./src/main.ts",
47
+ "dev:prod": "node dist/main.mjs",
48
+ "clean": "rm -rf dist",
49
+ "typecheck": "tsc -p tsconfig.json --noEmit",
50
+ "test": "pnpm -w run build:packages && vitest run",
51
+ "e2e": "pnpm -w run build:packages && LMCODE_E2E=1 vitest run test/e2e",
52
+ "e2e:real": "pnpm -w run build:packages && LMCODE_E2E_REAL=1 vitest run test/e2e/real-llm-smoke.e2e.test.ts",
53
+ "preinstall": "node -e \"console.log('\\n📦 正在安装 lmcode,请稍候...\\n')\"",
54
+ "postinstall": "node scripts/postinstall.mjs",
55
+ "smoke": "node dist/main.mjs --version"
56
+ },
57
+ "dependencies": {
58
+ "@earendil-works/pi-tui": "^0.78.1",
59
+ "@mariozechner/clipboard": "^0.3.2",
60
+ "chalk": "^5.4.1",
61
+ "cli-highlight": "^2.1.11",
62
+ "commander": "^13.1.0",
63
+ "semver": "^7.7.4",
64
+ "smol-toml": "^1.6.1",
65
+ "zod": "^4.3.6"
66
+ },
67
+ "devDependencies": {
68
+ "@modelcontextprotocol/sdk": "^1.29.0",
69
+ "@lmcode-cli/agent-core": "workspace:^",
70
+ "@lmcode-cli/config": "workspace:^",
71
+ "@lmcode-cli/migration-legacy": "workspace:^",
72
+ "@lmcode-cli/lmcode-sdk": "workspace:^",
73
+ "@lmcode/memory": "workspace:*",
74
+ "@types/semver": "^7.7.0",
75
+ "tsx": "^4.21.0"
76
+ },
77
+ "engines": {
78
+ "node": ">=22.19.0"
79
+ }
80
+ }
@@ -0,0 +1,351 @@
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 `lm` 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 `lm` into the set of filenames that resolve as executables
74
+ * on this platform. POSIX → just `[,]`. 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 `lm` 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 `lm` 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 `lm`) 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
+ }