@letta-ai/letta-code 0.27.30 → 0.28.1
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/agent-presets.js +396 -201
- package/dist/agent-presets.js.map +2 -2
- package/dist/types/agent/model-catalog.d.ts +43 -43
- package/dist/types/types/protocol_v2.d.ts +1 -1
- package/dist/types/types/protocol_v2.d.ts.map +1 -1
- package/letta.js +147978 -132185
- package/package.json +4 -2
- package/scripts/check-module-ownership.js +127 -0
- package/scripts/check-source-file-size.js +81 -0
- package/scripts/check-test-mock-isolation.js +0 -5
- package/scripts/check.js +2 -0
- package/scripts/run-unit-tests.cjs +7 -7
- package/scripts/source-file-size-baseline.json +53 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@letta-ai/letta-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.1",
|
|
4
4
|
"description": "Letta Code is a CLI tool for interacting with stateful Letta agents from the terminal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "bun@1.3.0",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"access": "public"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@earendil-works/pi-ai": "^0.
|
|
53
|
+
"@earendil-works/pi-ai": "^0.80.6",
|
|
54
54
|
"@letta-ai/letta-client": "^1.10.2",
|
|
55
55
|
"@pierre/diffs": "1.2.2",
|
|
56
56
|
"@scarf/scarf": "^1.4.0",
|
|
@@ -95,6 +95,8 @@
|
|
|
95
95
|
"check:boundaries": "node scripts/check-layer-boundaries.js",
|
|
96
96
|
"check:exported-functions": "node scripts/check-exported-functions.js",
|
|
97
97
|
"check:filename-casing": "node scripts/check-filename-casing.js",
|
|
98
|
+
"check:file-size": "node scripts/check-source-file-size.js",
|
|
99
|
+
"check:module-ownership": "node scripts/check-module-ownership.js",
|
|
98
100
|
"check:test-mock-isolation": "bun run scripts/check-test-mock-isolation.js",
|
|
99
101
|
"check:test-coverage": "node scripts/check-test-coverage.cjs",
|
|
100
102
|
"check:skill-frontmatter": "node scripts/check-skill-frontmatter.js",
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import ts from "typescript";
|
|
6
|
+
|
|
7
|
+
const RULES = [
|
|
8
|
+
{
|
|
9
|
+
module: "src/channels/slack/adapter.ts",
|
|
10
|
+
allowedImporters: new Set([
|
|
11
|
+
"src/channels/slack/adapter-test-harness.ts",
|
|
12
|
+
"src/channels/slack/plugin.ts",
|
|
13
|
+
]),
|
|
14
|
+
forbidForwardingExports: true,
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
function findTypeScriptFiles(dir) {
|
|
19
|
+
const files = [];
|
|
20
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
21
|
+
const filePath = path.join(dir, entry.name);
|
|
22
|
+
if (entry.isDirectory()) {
|
|
23
|
+
files.push(...findTypeScriptFiles(filePath));
|
|
24
|
+
} else if (/\.tsx?$/.test(entry.name)) {
|
|
25
|
+
files.push(filePath.replace(/\\/g, "/"));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return files;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolveModule(importer, specifier) {
|
|
32
|
+
let candidate;
|
|
33
|
+
if (specifier.startsWith("@/")) {
|
|
34
|
+
candidate = path.join("src", specifier.slice(2));
|
|
35
|
+
} else if (specifier.startsWith(".")) {
|
|
36
|
+
candidate = path.join(path.dirname(importer), specifier);
|
|
37
|
+
} else {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const normalized = path.normalize(candidate).replace(/\\/g, "/");
|
|
42
|
+
const candidates = [normalized, `${normalized}.ts`, `${normalized}.tsx`];
|
|
43
|
+
return candidates.find((item) => ruleModules.has(item)) ?? null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseTypeScript(filePath, source) {
|
|
47
|
+
const scriptKind = filePath.endsWith(".tsx")
|
|
48
|
+
? ts.ScriptKind.TSX
|
|
49
|
+
: ts.ScriptKind.TS;
|
|
50
|
+
return ts.createSourceFile(
|
|
51
|
+
filePath,
|
|
52
|
+
source,
|
|
53
|
+
ts.ScriptTarget.Latest,
|
|
54
|
+
false,
|
|
55
|
+
scriptKind,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function collectModuleSpecifiers(sourceFile) {
|
|
60
|
+
const specifiers = [];
|
|
61
|
+
|
|
62
|
+
function visit(node) {
|
|
63
|
+
if (
|
|
64
|
+
(ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
|
|
65
|
+
node.moduleSpecifier &&
|
|
66
|
+
ts.isStringLiteralLike(node.moduleSpecifier)
|
|
67
|
+
) {
|
|
68
|
+
specifiers.push(node.moduleSpecifier.text);
|
|
69
|
+
} else if (
|
|
70
|
+
ts.isCallExpression(node) &&
|
|
71
|
+
node.arguments.length === 1 &&
|
|
72
|
+
ts.isStringLiteralLike(node.arguments[0]) &&
|
|
73
|
+
(node.expression.kind === ts.SyntaxKind.ImportKeyword ||
|
|
74
|
+
(ts.isIdentifier(node.expression) &&
|
|
75
|
+
node.expression.text === "require"))
|
|
76
|
+
) {
|
|
77
|
+
specifiers.push(node.arguments[0].text);
|
|
78
|
+
}
|
|
79
|
+
ts.forEachChild(node, visit);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
visit(sourceFile);
|
|
83
|
+
return specifiers;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const rulesByModule = new Map(RULES.map((rule) => [rule.module, rule]));
|
|
87
|
+
const ruleModules = new Set(rulesByModule.keys());
|
|
88
|
+
const files = findTypeScriptFiles("src");
|
|
89
|
+
const failures = [];
|
|
90
|
+
|
|
91
|
+
for (const importer of files) {
|
|
92
|
+
const source = readFileSync(importer, "utf8");
|
|
93
|
+
const sourceFile = parseTypeScript(importer, source);
|
|
94
|
+
for (const specifier of collectModuleSpecifiers(sourceFile)) {
|
|
95
|
+
const module = resolveModule(importer, specifier);
|
|
96
|
+
if (!module) continue;
|
|
97
|
+
const rule = rulesByModule.get(module);
|
|
98
|
+
if (importer !== module && !rule.allowedImporters.has(importer)) {
|
|
99
|
+
failures.push(
|
|
100
|
+
`${importer}: import ${specifier} from its owning module instead of ${module}`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const rule of RULES) {
|
|
107
|
+
if (!rule.forbidForwardingExports) continue;
|
|
108
|
+
const source = readFileSync(rule.module, "utf8");
|
|
109
|
+
const sourceFile = parseTypeScript(rule.module, source);
|
|
110
|
+
const hasForwardingExport = sourceFile.statements.some(
|
|
111
|
+
(statement) =>
|
|
112
|
+
ts.isExportDeclaration(statement) && statement.moduleSpecifier,
|
|
113
|
+
);
|
|
114
|
+
if (hasForwardingExport) {
|
|
115
|
+
failures.push(
|
|
116
|
+
`${rule.module}: forwarding exports hide module ownership; import from the defining module`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (failures.length > 0) {
|
|
122
|
+
console.error("Module ownership check failed:\n");
|
|
123
|
+
for (const failure of failures) console.error(` ${failure}`);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
console.log(`Checked ${files.length} TypeScript files for module ownership`);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
const MAX_LINES = 1000;
|
|
7
|
+
const SOURCE_ROOTS = ["src", "scripts"];
|
|
8
|
+
const SOURCE_EXTENSIONS = new Set([".cjs", ".js", ".jsx", ".ts", ".tsx"]);
|
|
9
|
+
const BASELINE_PATH = "scripts/source-file-size-baseline.json";
|
|
10
|
+
|
|
11
|
+
function findSourceFiles(dir) {
|
|
12
|
+
const files = [];
|
|
13
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
14
|
+
const filePath = path.join(dir, entry.name);
|
|
15
|
+
if (entry.isDirectory()) {
|
|
16
|
+
files.push(...findSourceFiles(filePath));
|
|
17
|
+
} else if (SOURCE_EXTENSIONS.has(path.extname(entry.name))) {
|
|
18
|
+
files.push(filePath.replace(/\\/g, "/"));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return files;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function countLines(filePath) {
|
|
25
|
+
const text = readFileSync(filePath, "utf8");
|
|
26
|
+
if (text.length === 0) return 0;
|
|
27
|
+
const newlineCount = (text.match(/\n/g) ?? []).length;
|
|
28
|
+
return newlineCount + (text.endsWith("\n") ? 0 : 1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const baseline = JSON.parse(readFileSync(BASELINE_PATH, "utf8"));
|
|
32
|
+
const files = SOURCE_ROOTS.flatMap(findSourceFiles).sort();
|
|
33
|
+
const counts = new Map(files.map((file) => [file, countLines(file)]));
|
|
34
|
+
const failures = [];
|
|
35
|
+
|
|
36
|
+
for (const [file, lines] of counts) {
|
|
37
|
+
const allowedLines = baseline[file];
|
|
38
|
+
if (lines <= MAX_LINES) {
|
|
39
|
+
if (allowedLines !== undefined) {
|
|
40
|
+
failures.push(
|
|
41
|
+
`${file}: now ${lines} lines; remove its obsolete baseline entry`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (allowedLines === undefined) {
|
|
48
|
+
failures.push(
|
|
49
|
+
`${file}: ${lines} lines exceeds the ${MAX_LINES}-line limit`,
|
|
50
|
+
);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (lines > allowedLines) {
|
|
54
|
+
failures.push(
|
|
55
|
+
`${file}: grew from the ${allowedLines}-line baseline to ${lines} lines`,
|
|
56
|
+
);
|
|
57
|
+
} else if (lines < allowedLines) {
|
|
58
|
+
failures.push(
|
|
59
|
+
`${file}: shrank from ${allowedLines} to ${lines} lines; ratchet the baseline down`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const file of Object.keys(baseline)) {
|
|
65
|
+
if (!counts.has(file)) {
|
|
66
|
+
failures.push(`${file}: baseline entry points to a missing source file`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (failures.length > 0) {
|
|
71
|
+
console.error("Source file size check failed:\n");
|
|
72
|
+
for (const failure of failures) console.error(` ${failure}`);
|
|
73
|
+
console.error(
|
|
74
|
+
`\nNew source files must stay at or below ${MAX_LINES} lines. Split oversized files by responsibility; only lower or remove baseline entries.`,
|
|
75
|
+
);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.log(
|
|
80
|
+
`Checked ${files.length} source files (maximum ${MAX_LINES} lines)`,
|
|
81
|
+
);
|
|
@@ -42,11 +42,6 @@ const COMPLETE_EXPORT_MOCK_MODULES = new Set([
|
|
|
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
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
45
|
"src/cli/message-search-cache-warm.test.ts::../backend/api/search",
|
|
51
46
|
"src/hooks/prompt-executor.test.ts::../backend/api/generate",
|
|
52
47
|
"src/tools/memory-apply-patch.test.ts::../backend/api/client",
|
package/scripts/check.js
CHANGED
|
@@ -17,6 +17,8 @@ const checks = [
|
|
|
17
17
|
{ name: "layer boundaries", script: ["check:boundaries"] },
|
|
18
18
|
{ name: "exported function style", script: ["check:exported-functions"] },
|
|
19
19
|
{ name: "filename casing", script: ["check:filename-casing"] },
|
|
20
|
+
{ name: "source file size", script: ["check:file-size"] },
|
|
21
|
+
{ name: "module ownership", script: ["check:module-ownership"] },
|
|
20
22
|
{ name: "test mock isolation", script: ["check:test-mock-isolation"] },
|
|
21
23
|
{ name: "test coverage", script: ["check:test-coverage"] },
|
|
22
24
|
{ name: "skill frontmatter", script: ["check:skill-frontmatter"] },
|
|
@@ -34,10 +34,10 @@ const dirs = [
|
|
|
34
34
|
"src/*.test.ts",
|
|
35
35
|
];
|
|
36
36
|
|
|
37
|
-
// slack
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
37
|
+
// slack/media.test.ts imports the real media module. The adapter test harness
|
|
38
|
+
// mocks that module, which in Bun 1.3.x poisons the shared module registry
|
|
39
|
+
// across parallel workers. Run media tests in an isolated process first, then
|
|
40
|
+
// run src/channels with every other test file.
|
|
41
41
|
function findTestFiles(dir, exclude) {
|
|
42
42
|
const results = [];
|
|
43
43
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
@@ -55,15 +55,15 @@ function findTestFiles(dir, exclude) {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
const channelTestFiles = findTestFiles("src/channels", [
|
|
58
|
-
"src/channels/slack
|
|
58
|
+
"src/channels/slack/media.test.ts",
|
|
59
59
|
]);
|
|
60
60
|
|
|
61
61
|
const opts = { stdio: "inherit", shell: process.platform === "win32" };
|
|
62
62
|
let exitCode = 0;
|
|
63
63
|
|
|
64
|
-
// Run
|
|
64
|
+
// Run Slack media tests in isolation first (clean module registry)
|
|
65
65
|
try {
|
|
66
|
-
execSync("bun test src/channels/slack
|
|
66
|
+
execSync("bun test src/channels/slack/media.test.ts --timeout 15000", opts);
|
|
67
67
|
} catch (e) {
|
|
68
68
|
exitCode = e.status ?? 1;
|
|
69
69
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"src/agent/client-skills.test.ts": 1196,
|
|
3
|
+
"src/agent/memory-git.ts": 2324,
|
|
4
|
+
"src/agent/subagents/manager.ts": 1577,
|
|
5
|
+
"src/backend/local-backend.test.ts": 2589,
|
|
6
|
+
"src/backend/local/local-backend.ts": 1058,
|
|
7
|
+
"src/backend/local/local-store.ts": 3629,
|
|
8
|
+
"src/backend/pi-stream-adapter.test.ts": 1442,
|
|
9
|
+
"src/cli/app/AppCoordinator.tsx": 5198,
|
|
10
|
+
"src/cli/app/AppView.tsx": 1750,
|
|
11
|
+
"src/cli/app/use-approval-flow.ts": 1163,
|
|
12
|
+
"src/cli/app/use-configuration-handlers.ts": 1383,
|
|
13
|
+
"src/cli/app/use-conversation-loop.ts": 2935,
|
|
14
|
+
"src/cli/app/use-submit-handler.ts": 4100,
|
|
15
|
+
"src/cli/components/AgentSelector.tsx": 1270,
|
|
16
|
+
"src/cli/components/InputRich.tsx": 2219,
|
|
17
|
+
"src/cli/components/ModelSelector.tsx": 1204,
|
|
18
|
+
"src/cli/components/ProviderSelector.tsx": 1676,
|
|
19
|
+
"src/cli/components/ToolCallMessageRich.tsx": 1109,
|
|
20
|
+
"src/cli/helpers/accumulator.ts": 1596,
|
|
21
|
+
"src/cli/helpers/reflection-transcript.ts": 2080,
|
|
22
|
+
"src/cli/helpers/stream.ts": 1046,
|
|
23
|
+
"src/cli/mods/local-mod-loader.test.ts": 1043,
|
|
24
|
+
"src/cli/reflection-transcript.test.ts": 1118,
|
|
25
|
+
"src/cli/subcommands/skills.ts": 1264,
|
|
26
|
+
"src/headless.ts": 5238,
|
|
27
|
+
"src/hooks/integration.test.ts": 1147,
|
|
28
|
+
"src/index.ts": 2854,
|
|
29
|
+
"src/mods/learning-harness.ts": 2434,
|
|
30
|
+
"src/mods/mod-engine.test.ts": 2178,
|
|
31
|
+
"src/mods/mod-engine.ts": 1919,
|
|
32
|
+
"src/mods/package-installer.test.ts": 1248,
|
|
33
|
+
"src/mods/package-installer.ts": 1201,
|
|
34
|
+
"src/mods/package-registry.ts": 1033,
|
|
35
|
+
"src/permissions/read-only-shell.test.ts": 1303,
|
|
36
|
+
"src/permissions/read-only-shell.ts": 2009,
|
|
37
|
+
"src/providers/chatgpt-usage-service.ts": 1115,
|
|
38
|
+
"src/settings-manager.test.ts": 1669,
|
|
39
|
+
"src/settings-manager.ts": 2113,
|
|
40
|
+
"src/tools/impl/enter-worktree.ts": 1265,
|
|
41
|
+
"src/tools/impl/message-channel.ts": 1208,
|
|
42
|
+
"src/tools/manager.ts": 3293,
|
|
43
|
+
"src/tools/tool-execution-context.test.ts": 1422,
|
|
44
|
+
"src/types/protocol_v2.ts": 2932,
|
|
45
|
+
"src/websocket/listen-client-concurrency.test.ts": 3021,
|
|
46
|
+
"src/websocket/listen-client-protocol.test.ts": 6721,
|
|
47
|
+
"src/websocket/listener/commands/channels.ts": 1390,
|
|
48
|
+
"src/websocket/listener/commands/memory.ts": 1114,
|
|
49
|
+
"src/websocket/listener/file-commands.ts": 1030,
|
|
50
|
+
"src/websocket/listener/lifecycle.ts": 1766,
|
|
51
|
+
"src/websocket/listener/protocol-inbound.ts": 2279,
|
|
52
|
+
"src/websocket/listener/protocol-outbound.ts": 1279
|
|
53
|
+
}
|