@letta-ai/letta-code 0.31.12 → 0.31.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.
Files changed (44) hide show
  1. package/README.md +1 -1
  2. package/dist/channels-slack.js +20 -1
  3. package/dist/channels-slack.js.map +3 -3
  4. package/dist/gateway-core.js +2 -1
  5. package/dist/gateway-core.js.map +3 -3
  6. package/dist/mcp-client.js +4 -4
  7. package/dist/mcp-client.js.map +2 -2
  8. package/dist/types/agent/subagents/manager.d.ts.map +1 -1
  9. package/dist/types/backend/api/client.d.ts.map +1 -1
  10. package/dist/types/backend/backend.d.ts +1 -0
  11. package/dist/types/backend/backend.d.ts.map +1 -1
  12. package/dist/types/channels/message-channel-tool-definition.d.ts.map +1 -1
  13. package/dist/types/channels/plugin-types.d.ts +1 -1
  14. package/dist/types/channels/plugin-types.d.ts.map +1 -1
  15. package/dist/types/channels/slack/internal-types.d.ts +5 -0
  16. package/dist/types/channels/slack/internal-types.d.ts.map +1 -1
  17. package/dist/types/channels/slack/message-action-contract.d.ts +2 -0
  18. package/dist/types/channels/slack/message-action-contract.d.ts.map +1 -1
  19. package/dist/types/channels/types.d.ts +1 -0
  20. package/dist/types/channels/types.d.ts.map +1 -1
  21. package/dist/types/cli/helpers/git-context.d.ts +1 -1
  22. package/dist/types/cli/helpers/git-context.d.ts.map +1 -1
  23. package/dist/types/tools/impl/bash.d.ts +2 -0
  24. package/dist/types/tools/impl/bash.d.ts.map +1 -1
  25. package/dist/types/tools/impl/exec-command.d.ts +8 -0
  26. package/dist/types/tools/impl/exec-command.d.ts.map +1 -1
  27. package/dist/types/websocket/listener/cwd-change.d.ts.map +1 -1
  28. package/dist/types/websocket/listener/device-git-context.d.ts +17 -0
  29. package/dist/types/websocket/listener/device-git-context.d.ts.map +1 -0
  30. package/dist/types/websocket/listener/listener-constants.d.ts.map +1 -1
  31. package/dist/types/websocket/listener/protocol-outbound.d.ts +1 -0
  32. package/dist/types/websocket/listener/protocol-outbound.d.ts.map +1 -1
  33. package/dist/types/websocket/listener/stream-observers.d.ts.map +1 -1
  34. package/dist/types/websocket/listener/worktree-watcher.d.ts.map +1 -1
  35. package/letta.js +846 -703
  36. package/package.json +3 -2
  37. package/scripts/codex-watch/release-analysis.test.ts +87 -0
  38. package/scripts/codex-watch/release-analysis.ts +57 -7
  39. package/scripts/postinstall-patches.js +37 -14
  40. package/scripts/source-file-size-baseline.json +2 -2
  41. package/skills/dispatching-coding-agents/SKILL.md +6 -6
  42. package/skills/messaging-agents/SKILL.md +18 -5
  43. package/dist/types/tools/impl/foreground-sleep.d.ts +0 -13
  44. package/dist/types/tools/impl/foreground-sleep.d.ts.map +0 -1
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@letta-ai/letta-code",
3
- "version": "0.31.12",
3
+ "version": "0.31.13",
4
4
  "description": "Letta Code is a CLI tool for interacting with stateful Letta agents from the terminal.",
5
5
  "type": "module",
6
- "packageManager": "bun@1.3.10",
6
+ "packageManager": "bun@1.3.14",
7
7
  "bin": {
8
8
  "letta": "letta.js"
9
9
  },
@@ -122,6 +122,7 @@
122
122
  },
123
123
  "license": "Apache-2.0",
124
124
  "engines": {
125
+ "bun": ">=1.3.2",
125
126
  "node": ">=22.19.0"
126
127
  },
127
128
  "publishConfig": {
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  findLatestStableReleaseAfter,
3
+ listStableReleases,
3
4
  type Release,
4
5
  releaseNotesForRange,
5
6
  } from "./release-analysis.ts";
@@ -49,3 +50,89 @@ describe("Codex stable release range", () => {
49
50
  );
50
51
  });
51
52
  });
53
+
54
+ describe("Codex GitHub release fetch", () => {
55
+ test("retries a server error before succeeding", async () => {
56
+ const sleeps: number[] = [];
57
+ let requests = 0;
58
+
59
+ const releases = await listStableReleases({
60
+ fetchImpl: (async () => {
61
+ requests++;
62
+ if (requests === 1) {
63
+ return new Response("Gateway Timeout", { status: 504 });
64
+ }
65
+ return new Response(JSON.stringify(STABLES), { status: 200 });
66
+ }) as typeof fetch,
67
+ sleep: async (delayMs) => {
68
+ sleeps.push(delayMs);
69
+ },
70
+ });
71
+
72
+ expect(requests).toBe(2);
73
+ expect(sleeps).toEqual([1000]);
74
+ expect(releases).toEqual(STABLES);
75
+ });
76
+
77
+ test("retries a network error before succeeding", async () => {
78
+ const sleeps: number[] = [];
79
+ let requests = 0;
80
+
81
+ const releases = await listStableReleases({
82
+ fetchImpl: (async () => {
83
+ requests++;
84
+ if (requests === 1) throw new TypeError("fetch failed: ECONNRESET");
85
+ return new Response(JSON.stringify(STABLES), { status: 200 });
86
+ }) as typeof fetch,
87
+ sleep: async (delayMs) => {
88
+ sleeps.push(delayMs);
89
+ },
90
+ });
91
+
92
+ expect(requests).toBe(2);
93
+ expect(sleeps).toEqual([1000]);
94
+ expect(releases).toEqual(STABLES);
95
+ });
96
+
97
+ test("stops after the bounded retry budget", async () => {
98
+ const sleeps: number[] = [];
99
+ let requests = 0;
100
+
101
+ const result = listStableReleases({
102
+ fetchImpl: (async () => {
103
+ requests++;
104
+ return new Response("Gateway Timeout", { status: 504 });
105
+ }) as typeof fetch,
106
+ sleep: async (delayMs) => {
107
+ sleeps.push(delayMs);
108
+ },
109
+ });
110
+
111
+ await expect(result).rejects.toThrow(
112
+ "GitHub releases API failed (504): Gateway Timeout",
113
+ );
114
+ expect(requests).toBe(3);
115
+ expect(sleeps).toEqual([1000, 2000]);
116
+ });
117
+
118
+ test("fails immediately on a client error", async () => {
119
+ const sleeps: number[] = [];
120
+ let requests = 0;
121
+
122
+ const result = listStableReleases({
123
+ fetchImpl: (async () => {
124
+ requests++;
125
+ return new Response("Not Found", { status: 404 });
126
+ }) as typeof fetch,
127
+ sleep: async (delayMs) => {
128
+ sleeps.push(delayMs);
129
+ },
130
+ });
131
+
132
+ await expect(result).rejects.toThrow(
133
+ "GitHub releases API failed (404): Not Found",
134
+ );
135
+ expect(requests).toBe(1);
136
+ expect(sleeps).toEqual([]);
137
+ });
138
+ });
@@ -21,6 +21,8 @@ export const WATCHED_PATHS = [
21
21
  ];
22
22
 
23
23
  const MAX_COMMITS_PER_PATH = 8;
24
+ const GITHUB_RELEASES_FETCH_ATTEMPTS = 3;
25
+ const GITHUB_RELEASES_RETRY_DELAY_MS = 1_000;
24
26
 
25
27
  export interface Release {
26
28
  tag_name: string;
@@ -167,7 +169,12 @@ export async function analyzeCodexRelease(
167
169
  }
168
170
  }
169
171
 
170
- export async function listStableReleases(): Promise<Release[]> {
172
+ export async function listStableReleases(
173
+ options: {
174
+ fetchImpl?: typeof fetch;
175
+ sleep?: (delayMs: number) => Promise<void>;
176
+ } = {},
177
+ ): Promise<Release[]> {
171
178
  const releases: Release[] = [];
172
179
  const headers: Record<string, string> = {
173
180
  Accept: "application/vnd.github+json",
@@ -175,15 +182,15 @@ export async function listStableReleases(): Promise<Release[]> {
175
182
  };
176
183
  const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
177
184
  if (token) headers.Authorization = `Bearer ${token}`;
185
+ const fetchImpl = options.fetchImpl ?? fetch;
186
+ const sleep =
187
+ options.sleep ??
188
+ ((delayMs: number) =>
189
+ new Promise<void>((resolve) => setTimeout(resolve, delayMs)));
178
190
 
179
191
  for (let page = 1; page <= 10; page++) {
180
192
  const url = `https://api.github.com/repos/${CODEX_REPO}/releases?per_page=100&page=${page}`;
181
- const res = await fetch(url, { headers });
182
- if (!res.ok) {
183
- throw new Error(
184
- `GitHub releases API failed (${res.status}): ${await res.text()}`,
185
- );
186
- }
193
+ const res = await fetchGitHubReleasesPage(url, headers, fetchImpl, sleep);
187
194
  const batch = (await res.json()) as Release[];
188
195
  releases.push(...batch);
189
196
  if (batch.length < 100) break;
@@ -193,6 +200,49 @@ export async function listStableReleases(): Promise<Release[]> {
193
200
  .sort((a, b) => (a.published_at ?? "").localeCompare(b.published_at ?? ""));
194
201
  }
195
202
 
203
+ async function fetchGitHubReleasesPage(
204
+ url: string,
205
+ headers: Record<string, string>,
206
+ fetchImpl: typeof fetch,
207
+ sleep: (delayMs: number) => Promise<void>,
208
+ ): Promise<Response> {
209
+ for (let attempt = 1; ; attempt++) {
210
+ let response: Response;
211
+ try {
212
+ response = await fetchImpl(url, { headers });
213
+ } catch (error) {
214
+ if (attempt === GITHUB_RELEASES_FETCH_ATTEMPTS) {
215
+ throw new Error(
216
+ `GitHub releases API request failed after ${attempt} attempts: ${String(error)}`,
217
+ { cause: error },
218
+ );
219
+ }
220
+ await retryGitHubReleasesFetch(attempt, String(error), sleep);
221
+ continue;
222
+ }
223
+
224
+ if (response.ok) return response;
225
+
226
+ const message = `GitHub releases API failed (${response.status}): ${await response.text()}`;
227
+ if (response.status < 500 || attempt === GITHUB_RELEASES_FETCH_ATTEMPTS) {
228
+ throw new Error(message);
229
+ }
230
+ await retryGitHubReleasesFetch(attempt, message, sleep);
231
+ }
232
+ }
233
+
234
+ async function retryGitHubReleasesFetch(
235
+ attempt: number,
236
+ error: string,
237
+ sleep: (delayMs: number) => Promise<void>,
238
+ ): Promise<void> {
239
+ const delayMs = GITHUB_RELEASES_RETRY_DELAY_MS * 2 ** (attempt - 1);
240
+ console.warn(
241
+ `${error}; retrying GitHub releases request in ${delayMs}ms (${attempt}/${GITHUB_RELEASES_FETCH_ATTEMPTS})`,
242
+ );
243
+ await sleep(delayMs);
244
+ }
245
+
196
246
  function isStableRelease(release: Release): boolean {
197
247
  if (release.draft || release.prerelease) return false;
198
248
  return /^(rust-v|v)?\d+\.\d+\.\d+$/.test(release.tag_name);
@@ -16,6 +16,20 @@ import { fileURLToPath } from "node:url";
16
16
  const __dirname = dirname(fileURLToPath(import.meta.url));
17
17
  const pkgRoot = dirname(__dirname);
18
18
  const require = createRequire(import.meta.url);
19
+ const packageJson = JSON.parse(
20
+ readFileSync(join(pkgRoot, "package.json"), "utf-8"),
21
+ );
22
+ const minimumBunVersion = packageJson.engines.bun.replace(/^>=/, "");
23
+
24
+ function isBunVersionSupported(version) {
25
+ const current = version.split(".").map(Number);
26
+ const minimum = minimumBunVersion.split(".").map(Number);
27
+ for (let index = 0; index < minimum.length; index++) {
28
+ if ((current[index] ?? 0) > (minimum[index] ?? 0)) return true;
29
+ if ((current[index] ?? 0) < (minimum[index] ?? 0)) return false;
30
+ }
31
+ return true;
32
+ }
19
33
 
20
34
  async function copyToResolved(srcRel, targetSpecifier) {
21
35
  const src = join(pkgRoot, srcRel);
@@ -100,7 +114,10 @@ await copyToResolved(
100
114
  "ink/build/hooks/use-input.js",
101
115
  );
102
116
  await copyToResolved("vendor/ink/build/devtools.js", "ink/build/devtools.js");
103
- await copyToResolved("vendor/ink/build/log-update.js", "ink/build/log-update.js");
117
+ await copyToResolved(
118
+ "vendor/ink/build/log-update.js",
119
+ "ink/build/log-update.js",
120
+ );
104
121
  await copyToResolved("vendor/ink/build/wrap-text.js", "ink/build/wrap-text.js");
105
122
 
106
123
  // ink-text-input (optional vendor with externalCursorOffset support)
@@ -111,24 +128,30 @@ await copyToResolved(
111
128
 
112
129
  console.log("[patch] Ink runtime patched");
113
130
 
114
- // On Unix with Bun available, use polyglot shebang to prefer Bun runtime.
131
+ // On Unix with a supported Bun available, use a polyglot shebang to prefer it.
115
132
  // This enables Bun.secrets for secure keychain storage instead of fallback.
116
- // Windows always uses #!/usr/bin/env node (polyglot shebang breaks npm wrappers).
133
+ // Windows and installs with an older Bun keep the Node shebang.
117
134
  if (process.platform !== "win32") {
118
135
  try {
119
- execSync("bun --version", { stdio: "ignore" });
120
- const lettaPath = join(pkgRoot, "letta.js");
121
- if (existsSync(lettaPath)) {
122
- let content = readFileSync(lettaPath, "utf-8");
123
- if (content.startsWith("#!/usr/bin/env node")) {
124
- content = content.replace(
125
- "#!/usr/bin/env node",
126
- `#!/bin/sh
136
+ const bunVersion = execSync("bun --version", { encoding: "utf-8" }).trim();
137
+ if (isBunVersionSupported(bunVersion)) {
138
+ const lettaPath = join(pkgRoot, "letta.js");
139
+ if (existsSync(lettaPath)) {
140
+ let content = readFileSync(lettaPath, "utf-8");
141
+ if (content.startsWith("#!/usr/bin/env node")) {
142
+ content = content.replace(
143
+ "#!/usr/bin/env node",
144
+ `#!/bin/sh
127
145
  ":" //#; exec /usr/bin/env sh -c 'command -v bun >/dev/null && exec bun "$0" "$@" || exec node "$0" "$@"' "$0" "$@"`,
128
- );
129
- writeFileSync(lettaPath, content);
130
- console.log("[patch] Configured letta to prefer Bun runtime");
146
+ );
147
+ writeFileSync(lettaPath, content);
148
+ console.log("[patch] Configured letta to prefer Bun runtime");
149
+ }
131
150
  }
151
+ } else {
152
+ console.log(
153
+ `[patch] Bun ${bunVersion} is below ${minimumBunVersion}; keeping Node runtime`,
154
+ );
132
155
  }
133
156
  } catch {
134
157
  // Bun not available, keep node shebang
@@ -10,7 +10,7 @@
10
10
  "src/cli/app/use-approval-flow.ts": 1163,
11
11
  "src/cli/app/use-configuration-handlers.ts": 1421,
12
12
  "src/cli/app/use-conversation-loop.ts": 2912,
13
- "src/cli/app/use-submit-handler.ts": 4075,
13
+ "src/cli/app/use-submit-handler.ts": 4074,
14
14
  "src/cli/components/AgentSelector.tsx": 1104,
15
15
  "src/cli/components/InputRich.tsx": 2225,
16
16
  "src/cli/components/ModelSelector.tsx": 1259,
@@ -44,6 +44,6 @@
44
44
  "src/websocket/listener/file-commands.ts": 1053,
45
45
  "src/websocket/listener/lifecycle.ts": 1048,
46
46
  "src/websocket/listener/protocol-inbound.ts": 2207,
47
- "src/websocket/listener/protocol-outbound.ts": 1047,
47
+ "src/websocket/listener/protocol-outbound.ts": 1042,
48
48
  "src/websocket/listener/turn.ts": 1058
49
49
  }
@@ -66,7 +66,7 @@ Different agents have different strengths. Track what works in your memory over
66
66
  - Use `--max-budget-usd N` (Claude Code) to cap spend on exploratory tasks
67
67
 
68
68
  ### Known quirks
69
- - **Claude Code can hang on large repos** with unrestricted tools — consider `--allowedTools "Read Grep Glob"` (no Bash) and shorter timeouts for research tasks
69
+ - **Claude Code can hang on large repos** with unrestricted tools — consider `--tools "Read Grep Glob"` (no Bash) and shorter timeouts for research tasks
70
70
  - **Codex compactions can destroy long trajectories** — for very long tasks, prefer multiple shorter sessions over one marathon
71
71
  - **Opus tends to over-generate** — produces more code than necessary. Good for exploration, verify before applying.
72
72
 
@@ -135,7 +135,7 @@ cd /path/to/repo && claude -p "Read /tmp/my-plan.md and critique it. What am I m
135
135
 
136
136
  ## Handling Failures
137
137
 
138
- - **Timeout**: If an agent times out (especially Claude Code on large repos), try: (1) a shorter, more focused prompt, (2) restricting tools with `--allowedTools`, (3) switching to Codex which handles large repos better
138
+ - **Timeout**: If an agent times out (especially Claude Code on large repos), try: (1) a shorter, more focused prompt, (2) restricting tools with `--tools`, (3) switching to Codex which handles large repos better
139
139
  - **Garbage output**: If results are incoherent, the prompt was probably too vague. Rewrite with more specific file paths and clearer instructions.
140
140
  - **Session errors**: Claude Code can hit "stale approval from interrupted session" — `--dangerously-skip-permissions` prevents this. If Codex errors, start a fresh `exec` session.
141
141
  - **Compaction mid-task**: If a Codex session runs long enough to compact, it may lose earlier context. Break long tasks into smaller sequential sessions.
@@ -153,12 +153,13 @@ claude -p "YOUR PROMPT" --model MODEL --dangerously-skip-permissions
153
153
  | `-p` / `--print` | Non-interactive mode, prints response and exits |
154
154
  | `--dangerously-skip-permissions` | Skip approval prompts (prevents stale approval errors on timeout) |
155
155
  | `--model MODEL` | Alias or model name accepted by the installed CLI; omit to use the configured default |
156
- | `--effort LEVEL` | `low`, `medium`, `high` — controls reasoning depth |
156
+ | `--effort LEVEL` | `low`, `medium`, `high`, `xhigh`, `max` — controls reasoning depth |
157
157
  | `--append-system-prompt "..."` | Inject additional system instructions |
158
- | `--allowedTools "Bash Edit Read"` | Restrict available tools |
158
+ | `--allowedTools "Bash Edit Read"` | Tools that execute without prompting for permission |
159
+ | `--tools "Read Grep Glob"` | Restrict which built-in tools are available |
159
160
  | `--max-budget-usd N` | Cap spend for the invocation |
160
161
  | `--add-dir DIR` | Allow access to an additional directory; does not change the working directory |
161
- | `--output-format json` | Structured output with `session_id`, `cost_usd`, `duration_ms` |
162
+ | `--output-format json` | Structured output with `session_id`, `total_cost_usd`, `duration_ms` |
162
163
 
163
164
  Set Claude Code's working directory with `cd /path/to/repo && claude ...` in the Bash command, not with a Claude flag.
164
165
 
@@ -174,7 +175,6 @@ codex exec "YOUR PROMPT" --sandbox workspace-write
174
175
  | `-m MODEL` | Model accepted by the installed CLI; omit to use the configured default |
175
176
  | `--sandbox MODE` | Select a read-only or writable sandbox |
176
177
  | `-C DIR` | Set working directory |
177
- | `--search` | Enable web search tool |
178
178
  | `review` | Native code review — `codex review --uncommitted` or `codex exec review "prompt"` |
179
179
 
180
180
  ## Session Management
@@ -78,12 +78,21 @@ letta -p --from-agent $LETTA_AGENT_ID \
78
78
  "message text"
79
79
  ```
80
80
 
81
+ Use `--computer cloud` to route through the target agent's cloud sandbox:
82
+
83
+ ```bash
84
+ letta -p --from-agent $LETTA_AGENT_ID \
85
+ --agent <id> \
86
+ --computer cloud \
87
+ "message text"
88
+ ```
89
+
81
90
  **Arguments:**
82
91
  | Arg | Required | Description |
83
92
  |-----|----------|-------------|
84
93
  | `--agent <id>` | Yes | Target agent ID to message |
85
94
  | `--from-agent <id>` | Yes | Sender agent ID (injects agent-to-agent system reminder) |
86
- | `--computer <selector>` | No | Route through an online computer by connection name, device ID, or connection ID |
95
+ | `--computer <selector>` | No | Route through `cloud` (target agent's cloud sandbox) or an online computer by connection name, device ID, or connection ID |
87
96
  | `"message text"` | Yes | Message body (positional after flags) |
88
97
 
89
98
  **Example:**
@@ -93,13 +102,17 @@ letta -p --from-agent $LETTA_AGENT_ID \
93
102
  "What do you know about the authentication system?"
94
103
  ```
95
104
 
96
- **Response:**
105
+ **Response (JSON format with `--output json`):**
97
106
  ```json
98
107
  {
99
- "conversation_id": "conversation-xyz789",
100
- "response": "The authentication system uses JWT tokens...",
108
+ "type": "result",
109
+ "subtype": "success",
110
+ "is_error": false,
111
+ "result": "The authentication system uses JWT tokens...",
101
112
  "agent_id": "agent-abc123",
102
- "agent_name": "BackendExpert"
113
+ "conversation_id": "conversation-xyz789",
114
+ "environment": { "source": "same-environment" },
115
+ "usage": { "prompt_tokens": 120, "completion_tokens": 80, "total_tokens": 200, "step_count": 1 }
103
116
  }
104
117
  ```
105
118
 
@@ -1,13 +0,0 @@
1
- export declare const FOREGROUND_SLEEP_BLOCKED_MESSAGE = "Foreground `sleep` is blocked \u2014 it stalls the session while nothing happens. Run the wait in the background and keep working: use Bash with `run_in_background` and a command that exits when the condition is true, e.g. `until grep -q \"Ready in\" dev.log; do sleep 0.5; done`. You get a single completion notification when it exits. For one notification per occurrence (\"tell me every time an ERROR line appears\"), use the Monitor tool instead. `sleep` inside `run_in_background` commands and Monitor scripts is fine.";
2
- /**
3
- * True when the command would execute `sleep` in the foreground: `sleep` in
4
- * command position in any top-level segment, including loop bodies
5
- * (`while true; do sleep 1; done`).
6
- *
7
- * This is a workflow nudge, not a security boundary — commands the splitter
8
- * cannot analyze (unsafe redirects, substitutions in double quotes) are let
9
- * through rather than blocked, and `sleep` in argument position
10
- * (`grep sleep file.txt`) never matches.
11
- */
12
- export declare function commandRunsForegroundSleep(command: string): boolean;
13
- //# sourceMappingURL=foreground-sleep.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"foreground-sleep.d.ts","sourceRoot":"","sources":["../../../../src/tools/impl/foreground-sleep.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,gCAAgC,ghBACyd,CAAC;AA4BvgB;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAwBnE"}