@letta-ai/letta-code 0.34.25 → 0.34.26

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 (76) hide show
  1. package/cursor-worker.js +167 -0
  2. package/dist/agent-presets.js +1 -64
  3. package/dist/agent-presets.js.map +2 -2
  4. package/dist/channels-slack.js +30 -24
  5. package/dist/channels-slack.js.map +3 -3
  6. package/dist/mcp-client.js +233 -75
  7. package/dist/mcp-client.js.map +9 -9
  8. package/dist/mcp-oauth.js +233 -75
  9. package/dist/mcp-oauth.js.map +9 -9
  10. package/dist/types/agent/subagents/index.d.ts +4 -0
  11. package/dist/types/agent/subagents/index.d.ts.map +1 -1
  12. package/dist/types/agent/subagents/manager.d.ts.map +1 -1
  13. package/dist/types/agent/subagents/memory-worker.d.ts +2 -8
  14. package/dist/types/agent/subagents/memory-worker.d.ts.map +1 -1
  15. package/dist/types/backend/api/providers.d.ts +8 -2
  16. package/dist/types/backend/api/providers.d.ts.map +1 -1
  17. package/dist/types/backend/backend.d.ts +5 -2
  18. package/dist/types/backend/backend.d.ts.map +1 -1
  19. package/dist/types/backend/dev/fake-headless-backend.d.ts +2 -1
  20. package/dist/types/backend/dev/fake-headless-backend.d.ts.map +1 -1
  21. package/dist/types/backend/dev/pi-model-factory.d.ts.map +1 -1
  22. package/dist/types/backend/local/local-provider-auth-store.d.ts +2 -0
  23. package/dist/types/backend/local/local-provider-auth-store.d.ts.map +1 -1
  24. package/dist/types/backend/local/local-transcript-history.d.ts +10 -0
  25. package/dist/types/backend/local/local-transcript-history.d.ts.map +1 -0
  26. package/dist/types/backend/local/local-transcript.d.ts +6 -0
  27. package/dist/types/backend/local/local-transcript.d.ts.map +1 -1
  28. package/dist/types/channels/slack/attachment-primitives.d.ts +2 -1
  29. package/dist/types/channels/slack/attachment-primitives.d.ts.map +1 -1
  30. package/dist/types/cli/subcommands/mcp.d.ts +2 -2
  31. package/dist/types/cli/subcommands/mcp.d.ts.map +1 -1
  32. package/dist/types/cron/process-identity.d.ts.map +1 -1
  33. package/dist/types/cron/run-log.d.ts +1 -0
  34. package/dist/types/cron/run-log.d.ts.map +1 -1
  35. package/dist/types/headless-environment-response.d.ts +27 -0
  36. package/dist/types/headless-environment-response.d.ts.map +1 -1
  37. package/dist/types/providers/byok-providers.d.ts +14 -0
  38. package/dist/types/providers/byok-providers.d.ts.map +1 -1
  39. package/dist/types/tools/impl/claude-stream-session.d.ts.map +1 -1
  40. package/dist/types/tools/impl/codex-app-server.d.ts.map +1 -1
  41. package/dist/types/tools/impl/cursor-session.d.ts +26 -0
  42. package/dist/types/tools/impl/cursor-session.d.ts.map +1 -0
  43. package/dist/types/tools/impl/cursor-worker.d.ts +32 -0
  44. package/dist/types/tools/impl/cursor-worker.d.ts.map +1 -0
  45. package/dist/types/tools/impl/external-coding-agent-task.d.ts.map +1 -1
  46. package/dist/types/tools/impl/external-coding-agent.d.ts +1 -1
  47. package/dist/types/tools/impl/external-coding-agent.d.ts.map +1 -1
  48. package/dist/types/tools/impl/memory-task-lifecycle.d.ts +2 -2
  49. package/dist/types/tools/impl/memory-task-lifecycle.d.ts.map +1 -1
  50. package/dist/types/tools/impl/remote-external-coding-agent.d.ts.map +1 -1
  51. package/dist/types/tools/impl/send-agent-message.d.ts.map +1 -1
  52. package/dist/types/tools/impl/task.d.ts.map +1 -1
  53. package/dist/types/tools/workflow/sdk-spawner.d.ts.map +1 -1
  54. package/dist/types/utils/native-cli-env.d.ts +3 -0
  55. package/dist/types/utils/native-cli-env.d.ts.map +1 -0
  56. package/dist/types/utils/process-liveness.d.ts +9 -0
  57. package/dist/types/utils/process-liveness.d.ts.map +1 -1
  58. package/letta.js +3512 -2894
  59. package/npm-shrinkwrap.json +275 -101
  60. package/package.json +5 -3
  61. package/scripts/agent-watch/verify-pr-identity.test.ts +148 -0
  62. package/scripts/agent-watch/verify-pr-identity.ts +72 -14
  63. package/scripts/claude-watch/agent-watch.ts +8 -2
  64. package/scripts/claude-watch/release-source.test.ts +120 -0
  65. package/scripts/claude-watch/release-source.ts +79 -9
  66. package/scripts/codex-watch/agent-watch.ts +2 -2
  67. package/scripts/codex-watch/release-analysis.test.ts +112 -6
  68. package/scripts/codex-watch/release-analysis.ts +63 -6
  69. package/scripts/source-file-size-baseline.json +5 -4
  70. package/skills/initializing-memory/SKILL.md +1 -1
  71. package/skills/managing-agent-webhooks/SKILL.md +23 -3
  72. package/skills/managing-agent-webhooks/scripts/manage-agent-webhooks.mjs +40 -11
  73. package/skills/self-configuration/SKILL.md +1 -1
  74. package/skills/syncing-memory-filesystem/SKILL.md +2 -10
  75. package/dist/types/agent/memory-worktree.d.ts +0 -83
  76. package/dist/types/agent/memory-worktree.d.ts.map +0 -1
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import {
3
3
  enforceWatcherPrIdentity,
4
+ findWatcherPullRequests,
4
5
  parseArgs,
5
6
  type WatcherPullRequest,
6
7
  } from "./verify-pr-identity.ts";
@@ -20,6 +21,25 @@ function pullRequest(
20
21
  };
21
22
  }
22
23
 
24
+ function apiPullRequest(
25
+ overrides: Partial<{
26
+ body: string | null;
27
+ draft: boolean;
28
+ html_url: string;
29
+ number: number;
30
+ user: { login: string } | null;
31
+ }> = {},
32
+ ) {
33
+ return {
34
+ body: marker,
35
+ draft: true,
36
+ html_url: "https://github.com/letta-ai/letta-code/pull/123",
37
+ number: 123,
38
+ user: { login: "amelia-letta" },
39
+ ...overrides,
40
+ };
41
+ }
42
+
23
43
  function enforce(
24
44
  pullRequests: WatcherPullRequest[],
25
45
  closed: string[] = [],
@@ -35,6 +55,134 @@ function enforce(
35
55
  });
36
56
  }
37
57
 
58
+ describe("findWatcherPullRequests", () => {
59
+ test("finds a marker PR omitted from every bounded search attempt", () => {
60
+ const calls: string[][] = [];
61
+ let waits = 0;
62
+ const result = findWatcherPullRequests(
63
+ { repo: "letta-ai/letta-code", marker },
64
+ (args) => {
65
+ calls.push(args);
66
+ return args[0] === "pr" ? "[]" : JSON.stringify([[apiPullRequest()]]);
67
+ },
68
+ () => {
69
+ waits += 1;
70
+ },
71
+ );
72
+
73
+ expect(result.map((candidate) => candidate.number)).toEqual([123]);
74
+ expect(calls.filter(([command]) => command === "pr")).toHaveLength(3);
75
+ expect(calls.at(-1)?.slice(0, 6)).toEqual([
76
+ "api",
77
+ "--method",
78
+ "GET",
79
+ "--paginate",
80
+ "--slurp",
81
+ "repos/letta-ai/letta-code/pulls",
82
+ ]);
83
+ expect(waits).toBe(2);
84
+ });
85
+
86
+ test("does not miss a duplicate omitted from server-side search", () => {
87
+ const duplicate = apiPullRequest({
88
+ html_url: "https://github.com/letta-ai/letta-code/pull/124",
89
+ number: 124,
90
+ });
91
+ const result = findWatcherPullRequests(
92
+ { repo: "letta-ai/letta-code", marker },
93
+ (args) =>
94
+ args[0] === "pr"
95
+ ? JSON.stringify([pullRequest()])
96
+ : JSON.stringify([[apiPullRequest(), duplicate]]),
97
+ () => {},
98
+ );
99
+
100
+ expect(result.map((candidate) => candidate.number)).toEqual([123, 124]);
101
+ expect(() => enforce(result)).toThrow("Multiple open watcher PRs");
102
+ });
103
+
104
+ test("proves persistent absence authoritatively and fails closed on API failure", () => {
105
+ const absent = findWatcherPullRequests(
106
+ { repo: "letta-ai/letta-code", marker },
107
+ (args) => (args[0] === "pr" ? "[]" : "[[]]"),
108
+ () => {},
109
+ );
110
+ expect(absent).toEqual([]);
111
+
112
+ expect(() =>
113
+ findWatcherPullRequests(
114
+ { repo: "letta-ai/letta-code", marker },
115
+ (args) => {
116
+ if (args[0] === "pr") throw new Error("search unavailable");
117
+ throw new Error("authoritative listing unavailable");
118
+ },
119
+ () => {},
120
+ ),
121
+ ).toThrow("authoritative listing unavailable");
122
+ });
123
+
124
+ test("checks every authoritative page beyond 1,000 open pull requests", () => {
125
+ const pages = Array.from({ length: 10 }, (_, page) =>
126
+ Array.from({ length: 100 }, (_, index) =>
127
+ apiPullRequest({
128
+ body: "unrelated",
129
+ number: page * 100 + index + 1,
130
+ }),
131
+ ),
132
+ );
133
+ pages.push([apiPullRequest({ number: 1_001 })]);
134
+ const result = findWatcherPullRequests(
135
+ { repo: "letta-ai/letta-code", marker },
136
+ (args) => (args[0] === "pr" ? "[]" : JSON.stringify(pages)),
137
+ () => {},
138
+ );
139
+
140
+ expect(result.map((candidate) => candidate.number)).toEqual([1_001]);
141
+ });
142
+
143
+ test("rejects fuzzy authoritative candidates and keeps every exact match", () => {
144
+ const result = findWatcherPullRequests(
145
+ { repo: "letta-ai/letta-code", marker },
146
+ (args) =>
147
+ args[0] === "pr"
148
+ ? JSON.stringify([pullRequest()])
149
+ : JSON.stringify([
150
+ [
151
+ apiPullRequest({
152
+ body: "Pi ai watch 1.0.0 1.0.1",
153
+ number: 999,
154
+ }),
155
+ apiPullRequest(),
156
+ ],
157
+ ]),
158
+ () => {},
159
+ );
160
+
161
+ expect(result.map((candidate) => candidate.number)).toEqual([123]);
162
+ });
163
+
164
+ test("passes shell metacharacters as one search argument", () => {
165
+ const unsafeMarker = 'Watcher: $(touch /tmp/pwned); "quoted"';
166
+ const calls: string[][] = [];
167
+ findWatcherPullRequests(
168
+ { repo: "letta-ai/letta-code", marker: unsafeMarker },
169
+ (args) => {
170
+ calls.push(args);
171
+ return args[0] === "pr" ? "[]" : "[[]]";
172
+ },
173
+ () => {},
174
+ );
175
+
176
+ for (const command of calls.filter(([name]) => name === "pr")) {
177
+ expect(command[command.indexOf("--search") + 1]).toBe(
178
+ `${unsafeMarker} in:body`,
179
+ );
180
+ }
181
+ expect(calls.at(-1)).toContain("state=open");
182
+ expect(calls.at(-1)).toContain("per_page=100");
183
+ });
184
+ });
185
+
38
186
  describe("enforceWatcherPrIdentity", () => {
39
187
  test("returns the matching Amelia draft PR", () => {
40
188
  expect(enforce([pullRequest()])?.number).toBe(123);
@@ -95,22 +95,80 @@ function runGh(args: string[]): string {
95
95
  return result.stdout;
96
96
  }
97
97
 
98
+ interface GitHubPullRequest {
99
+ body: string | null;
100
+ draft: boolean;
101
+ html_url: string;
102
+ number: number;
103
+ user: { login: string } | null;
104
+ }
105
+
106
+ const SEARCH_ATTEMPTS = 3;
107
+
108
+ export function findWatcherPullRequests(
109
+ args: Pick<Args, "repo" | "marker">,
110
+ executeGh: (command: string[]) => string = runGh,
111
+ waitForSearchIndex: () => void = () => Bun.sleepSync(1_000),
112
+ ): WatcherPullRequest[] {
113
+ for (let attempt = 0; attempt < SEARCH_ATTEMPTS; attempt += 1) {
114
+ let foundExactHint = false;
115
+ try {
116
+ const candidates = JSON.parse(
117
+ executeGh([
118
+ "pr",
119
+ "list",
120
+ "--repo",
121
+ args.repo,
122
+ "--state",
123
+ "open",
124
+ "--search",
125
+ `${args.marker} in:body`,
126
+ "--limit",
127
+ "1000",
128
+ "--json",
129
+ "author,body,isDraft,number,url",
130
+ ]),
131
+ ) as WatcherPullRequest[];
132
+ foundExactHint = candidates.some((pullRequest) =>
133
+ pullRequest.body?.includes(args.marker),
134
+ );
135
+ } catch {
136
+ // Search is only an acceleration hint. The authoritative list below must
137
+ // still prove presence, absence, and uniqueness.
138
+ }
139
+ if (foundExactHint) break;
140
+ if (attempt + 1 < SEARCH_ATTEMPTS) waitForSearchIndex();
141
+ }
142
+
143
+ const pages = JSON.parse(
144
+ executeGh([
145
+ "api",
146
+ "--method",
147
+ "GET",
148
+ "--paginate",
149
+ "--slurp",
150
+ `repos/${args.repo}/pulls`,
151
+ "-f",
152
+ "state=open",
153
+ "-f",
154
+ "per_page=100",
155
+ ]),
156
+ ) as GitHubPullRequest[][];
157
+ return pages
158
+ .flatMap((page) => page)
159
+ .filter((pullRequest) => pullRequest.body?.includes(args.marker))
160
+ .map((pullRequest) => ({
161
+ author: pullRequest.user,
162
+ body: pullRequest.body,
163
+ isDraft: pullRequest.draft,
164
+ number: pullRequest.number,
165
+ url: pullRequest.html_url,
166
+ }));
167
+ }
168
+
98
169
  function main(): void {
99
170
  const args = parseArgs(process.argv.slice(2));
100
- const pullRequests = JSON.parse(
101
- runGh([
102
- "pr",
103
- "list",
104
- "--repo",
105
- args.repo,
106
- "--state",
107
- "open",
108
- "--limit",
109
- "1000",
110
- "--json",
111
- "author,body,isDraft,number,url",
112
- ]),
113
- ) as WatcherPullRequest[];
171
+ const pullRequests = findWatcherPullRequests(args);
114
172
  const verified = enforceWatcherPrIdentity({
115
173
  ...args,
116
174
  pullRequests,
@@ -212,14 +212,20 @@ async function selectCandidate(
212
212
  state: ClaudeWatchStateSnapshot | null,
213
213
  args: Args,
214
214
  ): Promise<ClaudeReleaseCandidate> {
215
+ const previousVersion =
216
+ args.previousVersion ?? state?.package_version ?? null;
215
217
  const [githubReleases, npmMetadata] = await Promise.all([
216
- fetchClaudeGitHubReleases(),
218
+ fetchClaudeGitHubReleases({
219
+ currentVersion: args.currentVersion,
220
+ previousVersion,
221
+ allowNpmOnlyExactVersions: args.validationOnly,
222
+ }),
217
223
  fetchClaudeNpmMetadata(),
218
224
  ]);
219
225
  const selected = selectClaudeReleaseCandidate({
220
226
  githubReleases,
221
227
  npmMetadata,
222
- previousVersion: args.previousVersion ?? state?.package_version ?? null,
228
+ previousVersion,
223
229
  currentVersion: args.currentVersion,
224
230
  allowNpmOnlyExactVersions: args.validationOnly,
225
231
  });
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  ClaudeReleaseSourceDisagreementError,
3
+ fetchClaudeGitHubReleases,
3
4
  parseClaudeGitHubReleases,
4
5
  parseClaudeNpmMetadata,
5
6
  releaseNotesForRange,
@@ -45,6 +46,125 @@ describe("parseClaudeGitHubReleases", () => {
45
46
  });
46
47
  });
47
48
 
49
+ describe("fetchClaudeGitHubReleases", () => {
50
+ test("resolves the latest release without fetching a list page", async () => {
51
+ const urls: string[] = [];
52
+ const fetched = await fetchClaudeGitHubReleases({
53
+ previousVersion: "1.2.0",
54
+ retries: 0,
55
+ fetch: (async (input) => {
56
+ urls.push(String(input));
57
+ return Response.json(github("1.2.0", 2));
58
+ }) as typeof fetch,
59
+ });
60
+
61
+ expect(fetched).toEqual([github("1.2.0", 2)]);
62
+ expect(urls).toEqual([
63
+ "https://api.github.com/repos/anthropics/claude-code/releases/latest",
64
+ ]);
65
+ });
66
+
67
+ test("resolves an explicit release without fetching a list page", async () => {
68
+ const urls: string[] = [];
69
+ const fetched = await fetchClaudeGitHubReleases({
70
+ currentVersion: "1.1.0",
71
+ previousVersion: "1.1.0",
72
+ retries: 0,
73
+ fetch: (async (input) => {
74
+ const url = String(input);
75
+ urls.push(url);
76
+ return Response.json(
77
+ url.endsWith("/latest") ? github("1.2.0", 2) : github("1.1.0", 1),
78
+ );
79
+ }) as typeof fetch,
80
+ });
81
+
82
+ expect(fetched).toEqual([github("1.1.0", 1), github("1.2.0", 2)]);
83
+ expect(urls).toEqual([
84
+ "https://api.github.com/repos/anthropics/claude-code/releases/latest",
85
+ "https://api.github.com/repos/anthropics/claude-code/releases/tags/v1.1.0",
86
+ ]);
87
+ });
88
+
89
+ test("pages only until the previous release needed for a range", async () => {
90
+ const urls: string[] = [];
91
+ const filler = Array.from({ length: 100 }, (_, index) => ({
92
+ ...github(`2.0.${index}`, 3),
93
+ published_at: `2026-08-02T${String(Math.floor(index / 60)).padStart(2, "0")}:${String(index % 60).padStart(2, "0")}:00Z`,
94
+ }));
95
+ const fetched = await fetchClaudeGitHubReleases({
96
+ currentVersion: "1.2.0",
97
+ previousVersion: "1.0.0",
98
+ retries: 0,
99
+ fetch: (async (input) => {
100
+ const url = String(input);
101
+ urls.push(url);
102
+ if (url.endsWith("/latest")) return Response.json(github("1.2.0", 2));
103
+ if (url.endsWith("/tags/v1.2.0"))
104
+ return Response.json(github("1.2.0", 2));
105
+ if (url.endsWith("/tags/v1.0.0"))
106
+ return Response.json(github("1.0.0", 0));
107
+ if (url.endsWith("page=1")) return Response.json(filler);
108
+ if (url.endsWith("page=2"))
109
+ return Response.json([github("1.1.0", 1), github("1.0.0", 0)]);
110
+ throw new Error(`Unexpected request ${url}`);
111
+ }) as typeof fetch,
112
+ });
113
+
114
+ expect(fetched.map(({ tag_name }) => tag_name)).toContain("1.1.0");
115
+ expect(urls.filter((url) => url.includes("?per_page=100"))).toEqual([
116
+ "https://api.github.com/repos/anthropics/claude-code/releases?per_page=100&page=1",
117
+ "https://api.github.com/repos/anthropics/claude-code/releases?per_page=100&page=2",
118
+ ]);
119
+ });
120
+
121
+ test("keeps validation-only npm replays off list pages", async () => {
122
+ const urls: string[] = [];
123
+ const fetched = await fetchClaudeGitHubReleases({
124
+ currentVersion: "1.1.0",
125
+ previousVersion: "1.0.0",
126
+ allowNpmOnlyExactVersions: true,
127
+ retries: 0,
128
+ fetch: (async (input) => {
129
+ const url = String(input);
130
+ urls.push(url);
131
+ if (url.endsWith("/latest")) return Response.json(github("1.2.0", 2));
132
+ return new Response("Not Found", {
133
+ status: 404,
134
+ statusText: "Not Found",
135
+ });
136
+ }) as typeof fetch,
137
+ });
138
+
139
+ expect(fetched).toEqual([github("1.2.0", 2)]);
140
+ expect(urls.some((url) => url.includes("?per_page=100"))).toBe(false);
141
+ });
142
+
143
+ test("preserves point-endpoint retry errors for a missing explicit tag", async () => {
144
+ let requests = 0;
145
+ const result = fetchClaudeGitHubReleases({
146
+ currentVersion: "9.9.9",
147
+ retries: 1,
148
+ backoffMs: 0,
149
+ sleep: async () => {},
150
+ fetch: (async (input) => {
151
+ requests++;
152
+ if (String(input).endsWith("/latest"))
153
+ return Response.json(github("1.2.0", 2));
154
+ return new Response("Not Found", {
155
+ status: 404,
156
+ statusText: "Not Found",
157
+ });
158
+ }) as typeof fetch,
159
+ });
160
+
161
+ await expect(result).rejects.toThrow(
162
+ "Failed to fetch https://api.github.com/repos/anthropics/claude-code/releases/tags/v9.9.9 after 2 attempts",
163
+ );
164
+ expect(requests).toBe(3);
165
+ });
166
+ });
167
+
48
168
  describe("parseClaudeNpmMetadata", () => {
49
169
  test("includes channels, all publish times, integrity, and tarballs", () => {
50
170
  const parsed = parseClaudeNpmMetadata({
@@ -18,6 +18,9 @@ export interface ReleaseSourceFetchOptions {
18
18
  retries?: number;
19
19
  backoffMs?: number;
20
20
  sleep?: (milliseconds: number) => Promise<void>;
21
+ currentVersion?: string | null;
22
+ previousVersion?: string | null;
23
+ allowNpmOnlyExactVersions?: boolean;
21
24
  }
22
25
 
23
26
  export interface ReleaseSelectionOptions {
@@ -373,27 +376,94 @@ async function fetchJson(
373
376
  });
374
377
  }
375
378
 
379
+ function isNotFoundFetchError(error: unknown): boolean {
380
+ return (
381
+ error instanceof Error &&
382
+ error.cause instanceof Error &&
383
+ error.cause.message.startsWith("404 ")
384
+ );
385
+ }
386
+
376
387
  export async function fetchClaudeGitHubReleases(
377
388
  options: ReleaseSourceFetchOptions = {},
378
389
  ): Promise<ClaudeGitHubRelease[]> {
379
- const rawReleases: unknown[] = [];
390
+ const headers = {
391
+ Accept: "application/vnd.github+json",
392
+ "User-Agent": "letta-claude-watch",
393
+ ...(process.env.GH_TOKEN
394
+ ? { Authorization: `Bearer ${process.env.GH_TOKEN}` }
395
+ : {}),
396
+ };
397
+ const currentVersion = options.currentVersion
398
+ ? normalizeGitHubReleaseTag(options.currentVersion)
399
+ : null;
400
+ const previousVersion = options.previousVersion
401
+ ? normalizeGitHubReleaseTag(options.previousVersion)
402
+ : null;
403
+ const rawReleases: unknown[] = [
404
+ await fetchJson(`${GITHUB_RELEASES_URL}/latest`, options, headers),
405
+ ];
406
+ let foundCurrent = true;
407
+ if (currentVersion) {
408
+ try {
409
+ rawReleases.push(
410
+ await fetchJson(
411
+ `${GITHUB_RELEASES_URL}/tags/${encodeURIComponent(`v${currentVersion}`)}`,
412
+ options,
413
+ headers,
414
+ ),
415
+ );
416
+ } catch (error) {
417
+ if (!options.allowNpmOnlyExactVersions || !isNotFoundFetchError(error)) {
418
+ throw error;
419
+ }
420
+ foundCurrent = false;
421
+ }
422
+ }
423
+
424
+ const pointReleases = parseClaudeGitHubReleases(rawReleases);
425
+ const resolvedCurrentVersion =
426
+ currentVersion ?? pointReleases.at(-1)?.tag_name;
427
+ if (
428
+ !previousVersion ||
429
+ previousVersion === resolvedCurrentVersion ||
430
+ !foundCurrent
431
+ ) {
432
+ return pointReleases;
433
+ }
434
+
435
+ try {
436
+ rawReleases.push(
437
+ await fetchJson(
438
+ `${GITHUB_RELEASES_URL}/tags/${encodeURIComponent(`v${previousVersion}`)}`,
439
+ options,
440
+ headers,
441
+ ),
442
+ );
443
+ } catch (error) {
444
+ if (!options.allowNpmOnlyExactVersions || !isNotFoundFetchError(error)) {
445
+ throw error;
446
+ }
447
+ return parseClaudeGitHubReleases(rawReleases);
448
+ }
449
+
380
450
  for (let page = 1; ; page += 1) {
381
451
  const payload = await fetchJson(
382
452
  `${GITHUB_RELEASES_URL}?per_page=100&page=${page}`,
383
453
  options,
384
- {
385
- Accept: "application/vnd.github+json",
386
- "User-Agent": "letta-claude-watch",
387
- ...(process.env.GH_TOKEN
388
- ? { Authorization: `Bearer ${process.env.GH_TOKEN}` }
389
- : {}),
390
- },
454
+ headers,
391
455
  );
392
456
  if (!Array.isArray(payload)) {
393
457
  throw new TypeError("GitHub releases response must be an array");
394
458
  }
395
459
  rawReleases.push(...payload);
396
- if (payload.length < 100) break;
460
+ const parsedPage = parseClaudeGitHubReleases(payload);
461
+ if (
462
+ parsedPage.some(({ tag_name }) => tag_name === previousVersion) ||
463
+ payload.length < 100
464
+ ) {
465
+ break;
466
+ }
397
467
  }
398
468
  return parseClaudeGitHubReleases(rawReleases);
399
469
  }
@@ -90,7 +90,7 @@ async function main() {
90
90
  let stableReleases: Release[] | undefined;
91
91
 
92
92
  if (!state.audit_cursor_validated) {
93
- stableReleases = await listStableReleases();
93
+ stableReleases = await listStableReleases({ fullHistory: true });
94
94
  state = validateLegacyCodexAuditCursor(
95
95
  state,
96
96
  stableReleases.map((release) => release.tag_name),
@@ -103,7 +103,7 @@ async function main() {
103
103
  if (!sinceTag && !currentTag) {
104
104
  const cursorTag = getCodexAuditCursorTag(state);
105
105
  if (cursorTag) {
106
- stableReleases ??= await listStableReleases();
106
+ stableReleases ??= await listStableReleases({ sinceTag: cursorTag });
107
107
  const latestRelease = findLatestStableReleaseAfter(
108
108
  stableReleases,
109
109
  cursorTag,