@letta-ai/letta-code 0.25.10 → 0.25.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letta-ai/letta-code",
3
- "version": "0.25.10",
3
+ "version": "0.25.11",
4
4
  "description": "Letta Code is a CLI tool for interacting with stateful Letta agents from the terminal.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -61,6 +61,7 @@
61
61
  "ink-spinner": "^5.0.0",
62
62
  "ink-text-input": "^5.0.0",
63
63
  "lint-staged": "16.2.4",
64
+ "madge": "^8.0.0",
64
65
  "minimatch": "^10.0.3",
65
66
  "picomatch": "^2.3.1",
66
67
  "react": "18.2.0",
@@ -71,12 +72,16 @@
71
72
  "lint": "bunx --bun @biomejs/biome@2.2.5 check src",
72
73
  "fix": "bunx --bun @biomejs/biome@2.2.5 check --write src",
73
74
  "typecheck": "tsc --noEmit",
75
+ "check:cycles": "madge --circular --extensions ts,tsx src/",
76
+ "check:boundaries": "node scripts/check-layer-boundaries.js",
77
+ "check:exported-functions": "node scripts/check-exported-functions.js",
78
+ "check:filename-casing": "node scripts/check-filename-casing.js",
74
79
  "check:test-mock-isolation": "bun run scripts/check-test-mock-isolation.js",
75
80
  "check": "bun run scripts/check.js",
76
81
  "dev": "LETTA_DEBUG=${LETTA_DEBUG:-1} LETTA_RESPONSES_WS=${LETTA_RESPONSES_WS:-1} bun --loader=.md:text --loader=.mdx:text --loader=.txt:text run src/index.ts",
77
82
  "build": "node scripts/postinstall-patches.js && bun run build.js",
78
- "test:update-chain:manual": "bun run src/tests/update-chain-smoke.ts --mode manual",
79
- "test:update-chain:startup": "bun run src/tests/update-chain-smoke.ts --mode startup",
83
+ "test:update-chain:manual": "bun run src/test-utils/update-chain-smoke.ts --mode manual",
84
+ "test:update-chain:startup": "bun run src/test-utils/update-chain-smoke.ts --mode startup",
80
85
  "prepublishOnly": "bun run build",
81
86
  "postinstall": "node scripts/postinstall-patches.js || echo letta: vendor patches skipped && node -e \"try{require('fs').chmodSync(require('path').join(require.resolve('node-pty/package.json'),'../prebuilds/darwin-arm64/spawn-helper'),0o755)}catch(e){}\" || true"
82
87
  },
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Enforces that exported functions use `export function` declarations
4
+ * rather than `export const fn = () =>` or `export const fn = async () =>`.
5
+ *
6
+ * Rationale: `export function` declarations are greppable (`grep "export function"`
7
+ * reliably finds all exported functions), hoisted, and easier for agents to locate.
8
+ *
9
+ * Scope: .ts files only — NOT .tsx. React components in .tsx legitimately use
10
+ * `export const Foo = memo(...)` which cannot be a function declaration.
11
+ *
12
+ * Value exports (singletons, data, re-exports) are not flagged.
13
+ */
14
+
15
+ import { readFileSync } from "node:fs";
16
+ import { join } from "node:path";
17
+ import { glob } from "glob";
18
+
19
+ const rootDir = process.cwd();
20
+
21
+ // Matches: export const name = (...) => or export const name = async (...) =>
22
+ // Also catches: export const name = () => and export const name = async () =>
23
+ const ARROW_EXPORT = /^export const \w+ = (async )?\(/;
24
+
25
+ const files = await glob("src/**/*.ts", {
26
+ cwd: rootDir,
27
+ ignore: ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"],
28
+ });
29
+
30
+ let violations = 0;
31
+
32
+ for (const file of files.sort()) {
33
+ const content = readFileSync(join(rootDir, file), "utf-8");
34
+ const lines = content.split("\n");
35
+
36
+ lines.forEach((line, i) => {
37
+ if (ARROW_EXPORT.test(line)) {
38
+ if (violations === 0) {
39
+ console.error("\n❌ Exported arrow functions found:\n");
40
+ }
41
+ console.error(` ${file}:${i + 1}`);
42
+ console.error(` ${line.trim()}`);
43
+ console.error(
44
+ ` ↳ Use 'export function name() {}' instead of 'export const name = () =>'\n`,
45
+ );
46
+ violations++;
47
+ }
48
+ });
49
+ }
50
+
51
+ if (violations > 0) {
52
+ console.error(
53
+ `Found ${violations} exported arrow function${violations === 1 ? "" : "s"}.`,
54
+ );
55
+ process.exit(1);
56
+ } else {
57
+ console.log("✅ No exported arrow functions found.");
58
+ }
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Enforces kebab-case filenames for .ts files in src/.
4
+ * .tsx files are exempt — PascalCase is the React component convention.
5
+ * .d.ts files are exempt.
6
+ *
7
+ * A filename is a violation if its stem (before the first dot) contains
8
+ * any uppercase letter, e.g. bootstrapHandler.ts or LocalStore.ts.
9
+ *
10
+ * Pass --staged to check only git-staged files (used in pre-commit hook).
11
+ */
12
+
13
+ import { execSync } from "node:child_process";
14
+ import { readdirSync, statSync } from "node:fs";
15
+ import { basename, join } from "node:path";
16
+
17
+ const stagedOnly = process.argv.includes("--staged");
18
+
19
+ function isViolation(filename) {
20
+ if (!filename.endsWith(".ts")) return false;
21
+ if (filename.endsWith(".d.ts")) return false;
22
+ const stem = filename.slice(0, filename.indexOf("."));
23
+ return /[A-Z]/.test(stem);
24
+ }
25
+
26
+ function* walkTs(dir) {
27
+ for (const entry of readdirSync(dir)) {
28
+ const full = join(dir, entry);
29
+ if (statSync(full).isDirectory()) yield* walkTs(full);
30
+ else if (isViolation(entry)) yield full;
31
+ }
32
+ }
33
+
34
+ let files;
35
+
36
+ if (stagedOnly) {
37
+ const staged = execSync("git diff --cached --name-only --diff-filter=A", {
38
+ encoding: "utf-8",
39
+ })
40
+ .trim()
41
+ .split("\n")
42
+ .filter((f) => f.startsWith("src/") && isViolation(basename(f)));
43
+ files = staged;
44
+ } else {
45
+ files = [...walkTs("src")];
46
+ }
47
+
48
+ if (files.length === 0) {
49
+ console.log("✅ No filename casing violations found.");
50
+ process.exit(0);
51
+ }
52
+
53
+ console.error("\n❌ Non-kebab-case .ts filenames found:\n");
54
+ for (const f of files) {
55
+ const stem = basename(f).slice(0, basename(f).indexOf("."));
56
+ const kebab = stem
57
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
58
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
59
+ .toLowerCase();
60
+ console.error(` ${f}`);
61
+ console.error(` ↳ rename to: ${basename(f).replace(stem, kebab)}\n`);
62
+ }
63
+
64
+ console.error(
65
+ `Found ${files.length} violation${files.length === 1 ? "" : "s"}.`,
66
+ );
67
+ console.error(
68
+ ".tsx files are exempt (PascalCase is correct for React components).\n",
69
+ );
70
+ process.exit(1);
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Enforces architectural import boundaries between top-level modules.
4
+ *
5
+ * Rules:
6
+ * tools/ must not import from cli/
7
+ * backend/ must not import from cli/ or websocket/
8
+ * providers/ must not import from agent/ or cli/
9
+ *
10
+ * These are currently violation-free. Adding a rule here means you must
11
+ * also ensure no existing code violates it.
12
+ *
13
+ * To add a new rule, append an entry to RULES below.
14
+ */
15
+
16
+ import { readFileSync } from "node:fs";
17
+ import { join } from "node:path";
18
+ import { glob } from "glob";
19
+
20
+ const rootDir = process.cwd();
21
+ const srcDir = join(rootDir, "src");
22
+
23
+ /**
24
+ * Each rule: files under `layer` must not contain an import from any of `forbidden`.
25
+ * `description` is shown on violation.
26
+ */
27
+ const RULES = [
28
+ {
29
+ layer: "tools",
30
+ forbidden: ["cli"],
31
+ description:
32
+ "tools/ run in headless and agent contexts — they must not import from cli/",
33
+ },
34
+ {
35
+ layer: "backend",
36
+ forbidden: ["cli", "websocket"],
37
+ description:
38
+ "backend/ is a low-level abstraction — it must not import from cli/ or websocket/",
39
+ },
40
+ {
41
+ layer: "providers",
42
+ forbidden: ["agent", "cli"],
43
+ description:
44
+ "providers/ are pure LLM adapters — they must not import from agent/ or cli/",
45
+ },
46
+ ];
47
+
48
+ // Matches: import ... from "@/forbidden/..." (static imports only)
49
+ function buildPattern(forbidden) {
50
+ return new RegExp(`from\\s+["'\`]@/(${forbidden.join("|")})/`, "g");
51
+ }
52
+
53
+ let violations = 0;
54
+
55
+ for (const rule of RULES) {
56
+ const files = await glob(`src/${rule.layer}/**/*.{ts,tsx}`, {
57
+ cwd: rootDir,
58
+ ignore: ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts"],
59
+ });
60
+
61
+ const pattern = buildPattern(rule.forbidden);
62
+
63
+ for (const file of files.sort()) {
64
+ const content = readFileSync(join(rootDir, file), "utf-8");
65
+ const lines = content.split("\n");
66
+
67
+ lines.forEach((line, i) => {
68
+ // Reset lastIndex for global regex reuse
69
+ pattern.lastIndex = 0;
70
+ if (pattern.test(line)) {
71
+ if (violations === 0) {
72
+ console.error("\n❌ Layer boundary violations found:\n");
73
+ }
74
+ console.error(` ${file}:${i + 1}`);
75
+ console.error(` ${line.trim()}`);
76
+ console.error(` ↳ ${rule.description}\n`);
77
+ violations++;
78
+ }
79
+ });
80
+ }
81
+ }
82
+
83
+ if (violations > 0) {
84
+ console.error(
85
+ `Found ${violations} boundary violation${violations === 1 ? "" : "s"}.`,
86
+ );
87
+ console.error(
88
+ "Fix by moving the helper to a shared layer (utils/, types/) or inverting the dependency.\n",
89
+ );
90
+ process.exit(1);
91
+ } else {
92
+ console.log("✅ No layer boundary violations found.");
93
+ }
@@ -8,7 +8,7 @@ const TESTS_DIR_ENV = "LETTA_MOCK_ISOLATION_TESTS_DIR";
8
8
  const rootDir = process.cwd();
9
9
  const testsDir = process.env[TESTS_DIR_ENV]
10
10
  ? resolve(process.env[TESTS_DIR_ENV])
11
- : join(rootDir, "src", "tests");
11
+ : join(rootDir, "src");
12
12
 
13
13
  const FORBIDDEN_MOCK_MODULES = new Map([
14
14
  [
@@ -41,24 +41,24 @@ const COMPLETE_EXPORT_MOCK_MODULES = new Set([
41
41
  // explicit test override helper. If a new top-level mock is truly unavoidable,
42
42
  // add a file+module entry here in the same PR with a clear explanation.
43
43
  const ALLOWED_TOP_LEVEL_MOCKS = new Set([
44
- "src/tests/channels/discord-registry.test.ts::../../backend/api/client",
45
- "src/tests/channels/slack-adapter-interop.test.ts::../../channels/slack/media",
46
- "src/tests/channels/slack-adapter-interop.test.ts::../../channels/slack/runtime",
47
- "src/tests/channels/slack-adapter.test.ts::../../channels/slack/media",
48
- "src/tests/channels/slack-adapter.test.ts::../../channels/slack/runtime",
49
- "src/tests/channels/telegram-adapter.test.ts::../../channels/telegram/runtime",
50
- "src/tests/cli/message-search-cache-warm.test.ts::../../backend/api/search",
51
- "src/tests/hooks/prompt-executor.test.ts::../../backend/api/generate",
52
- "src/tests/tools/memory-apply-patch.test.ts::../../backend/api/client",
53
- "src/tests/tools/memory-tool.test.ts::../../backend/api/client",
54
- "src/tests/tools/toolset-client-tool-rule-cleanup.test.ts::../../backend/api/client",
55
- "src/tests/tools/toolset-memfs-detach.test.ts::../../backend/api/client",
56
- "src/tests/websocket/listen-client-concurrency.test.ts::../../agent/approval-execution",
57
- "src/tests/websocket/listen-client-concurrency.test.ts::../../agent/approval-recovery",
58
- "src/tests/websocket/listen-client-concurrency.test.ts::../../agent/message",
59
- "src/tests/websocket/listen-client-concurrency.test.ts::../../backend/api/client",
60
- "src/tests/websocket/listen-client-concurrency.test.ts::../../cli/helpers/approvalClassification",
61
- "src/tests/websocket/listen-client-concurrency.test.ts::../../cli/helpers/stream",
44
+ "src/channels/discord-registry.test.ts::../backend/api/client",
45
+ "src/channels/slack-adapter-interop.test.ts::./slack/media",
46
+ "src/channels/slack-adapter-interop.test.ts::./slack/runtime",
47
+ "src/channels/slack-adapter.test.ts::./slack/media",
48
+ "src/channels/slack-adapter.test.ts::./slack/runtime",
49
+ "src/channels/telegram-adapter.test.ts::./telegram/runtime",
50
+ "src/cli/message-search-cache-warm.test.ts::../backend/api/search",
51
+ "src/hooks/prompt-executor.test.ts::../backend/api/generate",
52
+ "src/tools/memory-apply-patch.test.ts::../backend/api/client",
53
+ "src/tools/memory-tool.test.ts::../backend/api/client",
54
+ "src/tools/toolset-client-tool-rule-cleanup.test.ts::../backend/api/client",
55
+ "src/tools/toolset-memfs-detach.test.ts::../backend/api/client",
56
+ "src/websocket/listen-client-concurrency.test.ts::../agent/approval-execution",
57
+ "src/websocket/listen-client-concurrency.test.ts::../agent/approval-recovery",
58
+ "src/websocket/listen-client-concurrency.test.ts::../agent/message",
59
+ "src/websocket/listen-client-concurrency.test.ts::../backend/api/client",
60
+ "src/websocket/listen-client-concurrency.test.ts::../cli/helpers/approvalClassification",
61
+ "src/websocket/listen-client-concurrency.test.ts::../cli/helpers/stream",
62
62
  ]);
63
63
 
64
64
  const mockModulePattern = /\bmock\.module\s*\(\s*(["'`])([^"'`]+)\1/g;
package/scripts/check.js CHANGED
@@ -1,56 +1,76 @@
1
1
  #!/usr/bin/env bun
2
- // Script to run linting and type checking with helpful error messages
3
2
 
4
- import { $ } from "bun";
3
+ const LABEL_WIDTH = 32;
5
4
 
6
- console.log("🔍 Running lint and type checks...\n");
5
+ function formatLabel(name) {
6
+ const dots = ".".repeat(Math.max(3, LABEL_WIDTH - name.length - 1));
7
+ return `${name} ${dots}`;
8
+ }
9
+
10
+ function parseFileCount(output) {
11
+ const m = output.match(/(?:Processed|Checked) ([\d,]+) files/);
12
+ return m ? Number(m[1].replace(/,/g, "")) : null;
13
+ }
14
+
15
+ const checks = [
16
+ { name: "circular dependencies", script: ["check:cycles", "--no-spinner"] },
17
+ { name: "layer boundaries", script: ["check:boundaries"] },
18
+ { name: "exported function style", script: ["check:exported-functions"] },
19
+ { name: "filename casing", script: ["check:filename-casing"] },
20
+ { name: "test mock isolation", script: ["check:test-mock-isolation"] },
21
+ { name: "biome", script: ["lint"] },
22
+ { name: "typescript", script: ["typecheck"] },
23
+ ];
24
+
25
+ const N = checks.length;
26
+ let failed = 0;
27
+ const wallStart = performance.now();
28
+
29
+ for (let i = 0; i < N; i++) {
30
+ const { name, script } = checks[i];
31
+ const t0 = performance.now();
7
32
 
8
- let failed = false;
33
+ const proc = Bun.spawn(["bun", "run", ...script], {
34
+ cwd: process.cwd(),
35
+ stdout: "pipe",
36
+ stderr: "pipe",
37
+ });
9
38
 
10
- // Run test mock isolation check
11
- console.log("🧪 Checking Bun module mock isolation...");
12
- try {
13
- await $`bun run check:test-mock-isolation`;
14
- console.log("✅ Mock isolation check passed\n");
15
- } catch (error) {
16
- console.error("❌ Mock isolation check failed\n");
17
- console.error(
18
- "Fix the unsafe mock.module() usage above. Prefer explicit test override helpers or scoped mocks with afterEach(mock.restore).\n",
39
+ const [stdout, stderr, exitCode] = await Promise.all([
40
+ new Response(proc.stdout).text(),
41
+ new Response(proc.stderr).text(),
42
+ proc.exited,
43
+ ]);
44
+
45
+ const elapsed = ((performance.now() - t0) / 1000).toFixed(1);
46
+ const ok = exitCode === 0;
47
+ const combined = stdout + stderr;
48
+ const count = parseFileCount(combined);
49
+ const countStr = count ? ` (${count.toLocaleString()} files)` : "";
50
+
51
+ process.stdout.write(
52
+ `[${i + 1}/${N}] ${formatLabel(name)} ${ok ? "PASS" : "FAIL"} ${elapsed}s${countStr}\n`,
19
53
  );
20
- failed = true;
21
- }
22
54
 
23
- // Run lint
24
- console.log("📝 Running Biome linter...");
25
- try {
26
- await $`bun run lint`;
27
- console.log("✅ Linting passed\n");
28
- } catch (error) {
29
- console.error("❌ Linting failed\n");
30
- console.error("To fix automatically, run:");
31
- console.error(" bun run fix\n");
32
- failed = true;
55
+ if (!ok) {
56
+ failed++;
57
+ const lines = combined
58
+ .trim()
59
+ .split("\n")
60
+ .filter((l) => l.trim());
61
+ for (const line of lines) {
62
+ process.stderr.write(` ${line}\n`);
63
+ }
64
+ process.stderr.write("\n");
65
+ }
33
66
  }
34
67
 
35
- // Run typecheck
36
- console.log("🔎 Running TypeScript type checker...");
37
- try {
38
- await $`bun run typecheck`;
39
- console.log("✅ Type checking passed\n");
40
- } catch (error) {
41
- console.error("❌ Type checking failed\n");
42
- console.error("Fix the type errors shown above, then run:");
43
- console.error(" bun run typecheck\n");
44
- failed = true;
45
- }
68
+ const total = ((performance.now() - wallStart) / 1000).toFixed(1);
69
+ process.stdout.write("\n");
46
70
 
47
- if (failed) {
48
- console.error("❌ Checks failed. Please fix the errors above.");
49
- console.error("\nQuick commands:");
50
- console.error(" bun run fix # Auto-fix linting issues");
51
- console.error(" bun run typecheck # Check types only");
52
- console.error(" bun run check # Run both checks");
71
+ if (failed === 0) {
72
+ process.stdout.write(`✓ ${N} checks passed in ${total}s\n`);
73
+ } else {
74
+ process.stderr.write(`✗ ${failed} of ${N} checks failed in ${total}s\n`);
53
75
  process.exit(1);
54
76
  }
55
-
56
- console.log("✅ All checks passed!");