@letta-ai/letta-code 0.30.26 → 0.30.28

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 (55) hide show
  1. package/dist/agent-presets.js +17 -17
  2. package/dist/agent-presets.js.map +1 -1
  3. package/dist/mcp-client.js +2 -2
  4. package/dist/mcp-client.js.map +1 -1
  5. package/dist/types/agent/turn-recovery-policy.d.ts +33 -0
  6. package/dist/types/agent/turn-recovery-policy.d.ts.map +1 -1
  7. package/dist/types/tools/impl/apply-patch.d.ts.map +1 -1
  8. package/dist/types/tools/secret-substitution.d.ts.map +1 -1
  9. package/dist/types/types/loop-status-protocol.d.ts +17 -0
  10. package/dist/types/types/loop-status-protocol.d.ts.map +1 -0
  11. package/dist/types/types/protocol_v2.d.ts +2 -19
  12. package/dist/types/types/protocol_v2.d.ts.map +1 -1
  13. package/dist/types/websocket/listener/inbound-queue.d.ts +5 -0
  14. package/dist/types/websocket/listener/inbound-queue.d.ts.map +1 -0
  15. package/dist/types/websocket/listener/protocol-outbound-routing.d.ts +9 -0
  16. package/dist/types/websocket/listener/protocol-outbound-routing.d.ts.map +1 -0
  17. package/dist/types/websocket/listener/protocol-outbound.d.ts.map +1 -1
  18. package/dist/types/websocket/listener/runtime.d.ts.map +1 -1
  19. package/dist/types/websocket/listener/turn-correlation.d.ts +10 -0
  20. package/dist/types/websocket/listener/turn-correlation.d.ts.map +1 -0
  21. package/dist/types/websocket/listener/types.d.ts +4 -0
  22. package/dist/types/websocket/listener/types.d.ts.map +1 -1
  23. package/letta.js +545 -120
  24. package/package.json +1 -1
  25. package/scripts/claude-watch/agent-watch.ts +622 -0
  26. package/scripts/claude-watch/docs-snapshot.test.ts +259 -0
  27. package/scripts/claude-watch/docs-snapshot.ts +672 -0
  28. package/scripts/claude-watch/fixtures/historical-replays.json +52 -0
  29. package/scripts/claude-watch/github.ts +137 -0
  30. package/scripts/claude-watch/release-analysis.test.ts +235 -0
  31. package/scripts/claude-watch/release-analysis.ts +297 -0
  32. package/scripts/claude-watch/release-source.test.ts +179 -0
  33. package/scripts/claude-watch/release-source.ts +369 -0
  34. package/scripts/claude-watch/runtime-observations.ts +98 -0
  35. package/scripts/claude-watch/runtime-probe.test.ts +576 -0
  36. package/scripts/claude-watch/runtime-probe.ts +911 -0
  37. package/scripts/claude-watch/runtime-sandbox.ts +170 -0
  38. package/scripts/claude-watch/state-branch.test.ts +211 -0
  39. package/scripts/claude-watch/state-branch.ts +316 -0
  40. package/scripts/claude-watch/tracker.test.ts +148 -0
  41. package/scripts/claude-watch/tracker.ts +325 -0
  42. package/scripts/claude-watch/types.ts +186 -0
  43. package/scripts/claude-watch/update-tracker.ts +201 -0
  44. package/scripts/codex-watch/agent-watch.ts +2 -2
  45. package/scripts/codex-watch/release-analysis.ts +14 -2
  46. package/scripts/codex-watch/tracker.ts +1 -3
  47. package/scripts/run-unit-tests.cjs +2 -0
  48. package/scripts/source-file-size-baseline.json +6 -5
  49. package/skills/creating-mods/references/commands.md +1 -1
  50. package/skills/creating-mods/references/ui.md +1 -1
  51. package/skills/customizing-commands/SKILL.md +1 -1
  52. package/skills/initializing-memory/SKILL.md +7 -7
  53. package/skills/self-configuration/SKILL.md +4 -4
  54. package/scripts/codex-watch/check-release.ts +0 -128
  55. package/scripts/codex-watch/render-issue.ts +0 -273
@@ -0,0 +1,369 @@
1
+ import type {
2
+ ClaudeGitHubRelease,
3
+ ClaudeNpmMetadata,
4
+ ClaudeNpmVersion,
5
+ ClaudeReleaseCandidate,
6
+ } from "./types.ts";
7
+
8
+ const GITHUB_RELEASES_URL =
9
+ "https://api.github.com/repos/anthropics/claude-code/releases";
10
+ const NPM_METADATA_URL =
11
+ "https://registry.npmjs.org/%40anthropic-ai%2Fclaude-code";
12
+ const EXACT_SEMVER =
13
+ /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u;
14
+
15
+ export interface ReleaseSourceFetchOptions {
16
+ fetch?: typeof globalThis.fetch;
17
+ timeoutMs?: number;
18
+ retries?: number;
19
+ backoffMs?: number;
20
+ sleep?: (milliseconds: number) => Promise<void>;
21
+ }
22
+
23
+ export interface ReleaseSelectionOptions {
24
+ githubReleases: ClaudeGitHubRelease[];
25
+ npmMetadata: ClaudeNpmMetadata;
26
+ processedPackageVersions?: string[];
27
+ previousVersion?: string | null;
28
+ currentVersion?: string | null;
29
+ allowNpmOnlyExactVersions?: boolean;
30
+ }
31
+
32
+ export class ClaudeReleaseSourceDisagreementError extends Error {
33
+ readonly code = "CLAUDE_RELEASE_SOURCE_DISAGREEMENT";
34
+ readonly githubVersion: string | null;
35
+ readonly npmVersion: string;
36
+
37
+ constructor(
38
+ githubVersion: string | null,
39
+ npmVersion: string,
40
+ detail?: string,
41
+ ) {
42
+ const action =
43
+ "Wait for the GitHub release and npm latest channel to agree, then rerun the watcher.";
44
+ super(
45
+ `Claude Code release sources disagree: GitHub latest is ${githubVersion ?? "missing"}, ` +
46
+ `npm latest is ${npmVersion}.${detail ? ` ${detail}` : ""} ${action}`,
47
+ );
48
+ this.name = "ClaudeReleaseSourceDisagreementError";
49
+ this.githubVersion = githubVersion;
50
+ this.npmVersion = npmVersion;
51
+ }
52
+ }
53
+
54
+ export function normalizeGitHubReleaseTag(tag: string): string {
55
+ return tag.startsWith("v") ? tag.slice(1) : tag;
56
+ }
57
+
58
+ function object(value: unknown, context: string): Record<string, unknown> {
59
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
60
+ throw new TypeError(`${context} must be an object`);
61
+ }
62
+ return value as Record<string, unknown>;
63
+ }
64
+
65
+ function string(value: unknown, context: string): string {
66
+ if (typeof value !== "string" || value.length === 0) {
67
+ throw new TypeError(`${context} must be a non-empty string`);
68
+ }
69
+ return value;
70
+ }
71
+
72
+ function nullableString(value: unknown, context: string): string | null {
73
+ if (value === null || value === undefined) return null;
74
+ return string(value, context);
75
+ }
76
+
77
+ function compareTimestampsThenVersions(
78
+ left: { published_at: string; version: string },
79
+ right: { published_at: string; version: string },
80
+ ): number {
81
+ return (
82
+ Date.parse(left.published_at) - Date.parse(right.published_at) ||
83
+ left.version.localeCompare(right.version, undefined, { numeric: true })
84
+ );
85
+ }
86
+
87
+ /** Parse, normalize, filter, de-duplicate, and chronologically order GitHub releases. */
88
+ export function parseClaudeGitHubReleases(
89
+ input: unknown,
90
+ ): ClaudeGitHubRelease[] {
91
+ if (!Array.isArray(input))
92
+ throw new TypeError("GitHub releases response must be an array");
93
+
94
+ const byVersion = new Map<string, ClaudeGitHubRelease>();
95
+ for (const [index, item] of input.entries()) {
96
+ const raw = object(item, `GitHub release ${index}`);
97
+ if (raw.draft === true || raw.prerelease === true) continue;
98
+ if (typeof raw.draft !== "boolean" || typeof raw.prerelease !== "boolean") {
99
+ throw new TypeError(`GitHub release ${index} has invalid release flags`);
100
+ }
101
+ const version = normalizeGitHubReleaseTag(
102
+ string(raw.tag_name, `GitHub release ${index}.tag_name`),
103
+ );
104
+ if (!EXACT_SEMVER.test(version)) continue;
105
+ const publishedAt = nullableString(
106
+ raw.published_at,
107
+ `GitHub release ${index}.published_at`,
108
+ );
109
+ if (!publishedAt || Number.isNaN(Date.parse(publishedAt))) continue;
110
+ const release: ClaudeGitHubRelease = {
111
+ tag_name: version,
112
+ draft: false,
113
+ prerelease: false,
114
+ html_url: string(raw.html_url, `GitHub release ${index}.html_url`),
115
+ body: nullableString(raw.body, `GitHub release ${index}.body`),
116
+ published_at: publishedAt,
117
+ };
118
+ const existing = byVersion.get(version);
119
+ if (
120
+ !existing ||
121
+ Date.parse(publishedAt) < Date.parse(existing.published_at ?? publishedAt)
122
+ ) {
123
+ byVersion.set(version, release);
124
+ }
125
+ }
126
+
127
+ return [...byVersion.values()].sort((left, right) =>
128
+ compareTimestampsThenVersions(
129
+ { published_at: left.published_at ?? "", version: left.tag_name },
130
+ { published_at: right.published_at ?? "", version: right.tag_name },
131
+ ),
132
+ );
133
+ }
134
+
135
+ /** Parse the npm registry document into the small, stable shape used by the watcher. */
136
+ export function parseClaudeNpmMetadata(input: unknown): ClaudeNpmMetadata {
137
+ const raw = object(input, "npm metadata response");
138
+ const rawTags = object(raw["dist-tags"], "npm dist-tags");
139
+ const latest = string(rawTags.latest, "npm dist-tags.latest");
140
+ if (!EXACT_SEMVER.test(latest)) {
141
+ throw new TypeError(
142
+ `npm dist-tags.latest is not an exact semver: ${latest}`,
143
+ );
144
+ }
145
+ const rawVersions = object(raw.versions, "npm versions");
146
+ const rawTimes = object(raw.time, "npm publish times");
147
+ const versions: ClaudeNpmVersion[] = [];
148
+
149
+ for (const [version, versionValue] of Object.entries(rawVersions)) {
150
+ const versionDocument = object(versionValue, `npm version ${version}`);
151
+ const dist = object(versionDocument.dist, `npm version ${version}.dist`);
152
+ const publishedAt = string(
153
+ rawTimes[version],
154
+ `npm publish time ${version}`,
155
+ );
156
+ if (Number.isNaN(Date.parse(publishedAt))) {
157
+ throw new TypeError(
158
+ `npm publish time ${version} is not a valid timestamp`,
159
+ );
160
+ }
161
+ versions.push({
162
+ version,
163
+ published_at: publishedAt,
164
+ integrity: string(
165
+ dist.integrity,
166
+ `npm version ${version}.dist.integrity`,
167
+ ),
168
+ tarball_url: string(dist.tarball, `npm version ${version}.dist.tarball`),
169
+ });
170
+ }
171
+ versions.sort(compareTimestampsThenVersions);
172
+
173
+ if (!versions.some((version) => version.version === latest)) {
174
+ throw new TypeError(
175
+ `npm latest version ${latest} is absent from npm versions`,
176
+ );
177
+ }
178
+
179
+ return {
180
+ dist_tags: {
181
+ latest,
182
+ stable: nullableString(rawTags.stable, "npm dist-tags.stable"),
183
+ next: nullableString(rawTags.next, "npm dist-tags.next"),
184
+ },
185
+ versions,
186
+ };
187
+ }
188
+
189
+ function assertSourcesAgree(
190
+ githubReleases: ClaudeGitHubRelease[],
191
+ npmMetadata: ClaudeNpmMetadata,
192
+ ): void {
193
+ const npmLatest = npmMetadata.dist_tags.latest;
194
+ const githubLatest = githubReleases.at(-1)?.tag_name ?? null;
195
+ if (githubLatest !== npmLatest) {
196
+ throw new ClaudeReleaseSourceDisagreementError(githubLatest, npmLatest);
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Select one release without side effects. Sources are checked before any selection,
202
+ * so a disagreement cannot accidentally advance persisted watcher state.
203
+ */
204
+ export function selectClaudeReleaseCandidate(
205
+ options: ReleaseSelectionOptions,
206
+ ): ClaudeReleaseCandidate | null {
207
+ const githubReleases = [...options.githubReleases].sort((left, right) =>
208
+ compareTimestampsThenVersions(
209
+ { published_at: left.published_at ?? "", version: left.tag_name },
210
+ { published_at: right.published_at ?? "", version: right.tag_name },
211
+ ),
212
+ );
213
+ assertSourcesAgree(githubReleases, options.npmMetadata);
214
+
215
+ const npmByVersion = new Map(
216
+ options.npmMetadata.versions.map((version) => [version.version, version]),
217
+ );
218
+ const currentVersion = options.currentVersion ?? null;
219
+ const terminalVersion =
220
+ options.previousVersion ?? options.processedPackageVersions?.at(-1) ?? null;
221
+ const allowNpmOnlyExactVersions =
222
+ options.allowNpmOnlyExactVersions === true && currentVersion !== null;
223
+
224
+ if (
225
+ options.previousVersion &&
226
+ (!npmByVersion.has(options.previousVersion) ||
227
+ (!allowNpmOnlyExactVersions &&
228
+ !githubReleases.some(
229
+ ({ tag_name }) => tag_name === options.previousVersion,
230
+ )))
231
+ ) {
232
+ throw new ClaudeReleaseSourceDisagreementError(
233
+ githubReleases.at(-1)?.tag_name ?? null,
234
+ options.npmMetadata.dist_tags.latest,
235
+ `Explicit previous version ${options.previousVersion} is not present in both sources.`,
236
+ );
237
+ }
238
+
239
+ let release: ClaudeGitHubRelease | undefined;
240
+ if (currentVersion) {
241
+ release = githubReleases.find(
242
+ ({ tag_name }) => tag_name === currentVersion,
243
+ );
244
+ const npmVersion = npmByVersion.get(currentVersion);
245
+ if (!npmVersion || (!release && !allowNpmOnlyExactVersions)) {
246
+ throw new ClaudeReleaseSourceDisagreementError(
247
+ githubReleases.at(-1)?.tag_name ?? null,
248
+ options.npmMetadata.dist_tags.latest,
249
+ `Explicit current version ${currentVersion} is not present in both sources.`,
250
+ );
251
+ }
252
+ if (!release) {
253
+ return {
254
+ ...npmVersion,
255
+ release_url: "https://github.com/anthropics/claude-code/releases",
256
+ release_notes_md:
257
+ `Historical validation-only npm replay for ${currentVersion}; ` +
258
+ "this exact version is not retained in the GitHub release feed.",
259
+ release_published_at: npmVersion.published_at,
260
+ dist_tags: options.npmMetadata.dist_tags,
261
+ };
262
+ }
263
+ } else if (!terminalVersion) {
264
+ release = githubReleases.find(
265
+ ({ tag_name }) => tag_name === options.npmMetadata.dist_tags.latest,
266
+ );
267
+ } else {
268
+ const terminalIndex = githubReleases.findIndex(
269
+ ({ tag_name }) => tag_name === terminalVersion,
270
+ );
271
+ if (terminalIndex < 0) {
272
+ throw new ClaudeReleaseSourceDisagreementError(
273
+ githubReleases.at(-1)?.tag_name ?? null,
274
+ options.npmMetadata.dist_tags.latest,
275
+ `Terminal version ${terminalVersion} is not present in GitHub releases.`,
276
+ );
277
+ }
278
+ release = githubReleases[terminalIndex + 1];
279
+ }
280
+
281
+ if (!release) return null;
282
+ const npmVersion = npmByVersion.get(release.tag_name);
283
+ if (!npmVersion || !release.published_at) {
284
+ throw new ClaudeReleaseSourceDisagreementError(
285
+ githubReleases.at(-1)?.tag_name ?? null,
286
+ options.npmMetadata.dist_tags.latest,
287
+ `Selected version ${release.tag_name} is incomplete in a release source.`,
288
+ );
289
+ }
290
+ return {
291
+ ...npmVersion,
292
+ release_url: release.html_url,
293
+ release_notes_md: release.body ?? "",
294
+ release_published_at: release.published_at,
295
+ dist_tags: options.npmMetadata.dist_tags,
296
+ };
297
+ }
298
+
299
+ async function fetchJson(
300
+ url: string,
301
+ options: ReleaseSourceFetchOptions,
302
+ headers: HeadersInit,
303
+ ): Promise<unknown> {
304
+ const fetchImplementation = options.fetch ?? globalThis.fetch;
305
+ const retries = options.retries ?? 2;
306
+ const timeoutMs = options.timeoutMs ?? 10_000;
307
+ const backoffMs = options.backoffMs ?? 250;
308
+ const sleep =
309
+ options.sleep ??
310
+ ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
311
+ let lastError: unknown;
312
+
313
+ for (let attempt = 0; attempt <= retries; attempt += 1) {
314
+ const controller = new AbortController();
315
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
316
+ try {
317
+ const response = await fetchImplementation(url, {
318
+ headers,
319
+ signal: controller.signal,
320
+ });
321
+ if (!response.ok)
322
+ throw new Error(`${response.status} ${response.statusText}`);
323
+ return await response.json();
324
+ } catch (error) {
325
+ lastError = error;
326
+ if (attempt === retries) break;
327
+ await sleep(backoffMs * 2 ** attempt);
328
+ } finally {
329
+ clearTimeout(timer);
330
+ }
331
+ }
332
+ throw new Error(`Failed to fetch ${url} after ${retries + 1} attempts`, {
333
+ cause: lastError,
334
+ });
335
+ }
336
+
337
+ export async function fetchClaudeGitHubReleases(
338
+ options: ReleaseSourceFetchOptions = {},
339
+ ): Promise<ClaudeGitHubRelease[]> {
340
+ const rawReleases: unknown[] = [];
341
+ for (let page = 1; ; page += 1) {
342
+ const payload = await fetchJson(
343
+ `${GITHUB_RELEASES_URL}?per_page=100&page=${page}`,
344
+ options,
345
+ {
346
+ Accept: "application/vnd.github+json",
347
+ "User-Agent": "letta-claude-watch",
348
+ ...(process.env.GH_TOKEN
349
+ ? { Authorization: `Bearer ${process.env.GH_TOKEN}` }
350
+ : {}),
351
+ },
352
+ );
353
+ if (!Array.isArray(payload)) {
354
+ throw new TypeError("GitHub releases response must be an array");
355
+ }
356
+ rawReleases.push(...payload);
357
+ if (payload.length < 100) break;
358
+ }
359
+ return parseClaudeGitHubReleases(rawReleases);
360
+ }
361
+
362
+ export async function fetchClaudeNpmMetadata(
363
+ options: ReleaseSourceFetchOptions = {},
364
+ ): Promise<ClaudeNpmMetadata> {
365
+ const payload = await fetchJson(NPM_METADATA_URL, options, {
366
+ Accept: "application/json",
367
+ });
368
+ return parseClaudeNpmMetadata(payload);
369
+ }
@@ -0,0 +1,98 @@
1
+ export interface ProbeTranscript {
2
+ toolCalls: Array<{ id: string | null; name: string; input: unknown }>;
3
+ toolResults: Array<{
4
+ toolUseId: string | null;
5
+ content: string;
6
+ isError: boolean;
7
+ }>;
8
+ }
9
+
10
+ export function evaluateProbe(
11
+ name: string,
12
+ parsed: ProbeTranscript,
13
+ ): { complete: boolean; assertions: Record<string, boolean> } {
14
+ if (name === "read-lines-9-10-tab-prefix") {
15
+ const read = parsed.toolCalls.find((call) => call.name === "Read");
16
+ const input = record(read?.input);
17
+ const result = parsed.toolResults.find(
18
+ (candidate) => candidate.toolUseId === read?.id,
19
+ );
20
+ const content = result?.content ?? "";
21
+ return {
22
+ complete:
23
+ input?.offset === 9 && input.limit === 2 && result !== undefined,
24
+ assertions: {
25
+ exact_line_9: /(?:^|\n)9\tline9(?:\n|$)/u.test(content),
26
+ exact_line_10: /(?:^|\n)10\tline10(?:\n|$)/u.test(content),
27
+ no_line_9_padding: !/(?:^|\n)[ \t]+9\tline9(?:\n|$)/u.test(content),
28
+ no_arrow_separator: !content.includes("→"),
29
+ result_not_error: result?.isError === false,
30
+ },
31
+ };
32
+ }
33
+ if (name === "task-metadata-delete-contract") {
34
+ const calls = parsed.toolCalls;
35
+ const deletedIndex = calls.findIndex(
36
+ (call) =>
37
+ call.name === "TaskUpdate" && record(call.input)?.status === "deleted",
38
+ );
39
+ const beforeGet = calls.find(
40
+ (call, index) => call.name === "TaskGet" && index < deletedIndex,
41
+ );
42
+ const afterGet = calls.find(
43
+ (call, index) => call.name === "TaskGet" && index > deletedIndex,
44
+ );
45
+ const list = calls.find(
46
+ (call, index) => call.name === "TaskList" && index > deletedIndex,
47
+ );
48
+ const metadataUpdate = calls.find(
49
+ (call) =>
50
+ call.name === "TaskUpdate" &&
51
+ record(call.input)?.metadata !== undefined,
52
+ );
53
+ const resultFor = (id: string | null | undefined) =>
54
+ parsed.toolResults.find((candidate) => candidate.toolUseId === id);
55
+ const beforeResult = resultFor(beforeGet?.id);
56
+ const afterResult = resultFor(afterGet?.id);
57
+ const listResult = resultFor(list?.id);
58
+ const metadataResult = resultFor(metadataUpdate?.id);
59
+ const metadata = record(record(metadataUpdate?.input)?.metadata);
60
+ return {
61
+ complete:
62
+ calls.some((call) => call.name === "TaskCreate") &&
63
+ calls.some((call) => call.name === "TaskUpdate") &&
64
+ deletedIndex >= 0 &&
65
+ metadataResult !== undefined &&
66
+ beforeResult !== undefined &&
67
+ afterResult !== undefined &&
68
+ listResult !== undefined,
69
+ assertions: {
70
+ metadata_arbitrary_values_accepted:
71
+ metadata?.count === 3 &&
72
+ Array.isArray(metadata.flags) &&
73
+ metadata.flags[0] === "ready" &&
74
+ record(metadata.details)?.source === "claude-watch" &&
75
+ metadataResult?.isError === false,
76
+ metadata_null_update_accepted:
77
+ metadata?.probe === null && metadataResult?.isError === false,
78
+ deleted_task_get_errors:
79
+ afterResult?.isError === true ||
80
+ /not[ -]?found|does not exist|deleted/iu.test(
81
+ afterResult?.content ?? "",
82
+ ),
83
+ deleted_task_absent:
84
+ listResult !== undefined && !/probe-task/iu.test(listResult.content),
85
+ },
86
+ };
87
+ }
88
+ return {
89
+ complete: parsed.toolCalls.length > 0 && parsed.toolResults.length > 0,
90
+ assertions: {},
91
+ };
92
+ }
93
+
94
+ function record(value: unknown): Record<string, unknown> | null {
95
+ return value !== null && typeof value === "object" && !Array.isArray(value)
96
+ ? (value as Record<string, unknown>)
97
+ : null;
98
+ }