@geteigen/cli 0.0.0-stage → 3.0.0

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 (168) hide show
  1. package/README.md +154 -2
  2. package/dist/agent.d.ts +32 -0
  3. package/dist/agent.js +246 -0
  4. package/dist/auth.d.ts +5 -0
  5. package/dist/auth.js +33 -0
  6. package/dist/cli/src/agent.d.ts +24 -0
  7. package/dist/cli/src/agent.js +260 -0
  8. package/dist/cli/src/agentTools.d.ts +10 -0
  9. package/dist/cli/src/agentTools.js +70 -0
  10. package/dist/cli/src/auth.d.ts +5 -0
  11. package/dist/cli/src/auth.js +33 -0
  12. package/dist/cli/src/config.d.ts +11 -0
  13. package/dist/cli/src/config.js +51 -0
  14. package/dist/cli/src/cursorAuth.d.ts +10 -0
  15. package/dist/cli/src/cursorAuth.js +92 -0
  16. package/dist/cli/src/index.d.ts +2 -0
  17. package/dist/cli/src/index.js +244 -0
  18. package/dist/cli/src/listModels.d.ts +14 -0
  19. package/dist/cli/src/listModels.js +196 -0
  20. package/dist/cli/src/listSessions.d.ts +9 -0
  21. package/dist/cli/src/listSessions.js +37 -0
  22. package/dist/cli/src/machineConfig.d.ts +20 -0
  23. package/dist/cli/src/machineConfig.js +66 -0
  24. package/dist/cli/src/pickFolder.d.ts +5 -0
  25. package/dist/cli/src/pickFolder.js +65 -0
  26. package/dist/cli/src/proc.d.ts +23 -0
  27. package/dist/cli/src/proc.js +77 -0
  28. package/dist/cli/src/service.d.ts +17 -0
  29. package/dist/cli/src/service.js +246 -0
  30. package/dist/cli/src/workRoots.d.ts +30 -0
  31. package/dist/cli/src/workRoots.js +221 -0
  32. package/dist/cli/src/worker.d.ts +8 -0
  33. package/dist/cli/src/worker.js +51 -0
  34. package/dist/cli/src/workerCore.d.ts +47 -0
  35. package/dist/cli/src/workerCore.js +702 -0
  36. package/dist/cli/src/workspace.d.ts +8 -0
  37. package/dist/cli/src/workspace.js +269 -0
  38. package/dist/config.d.ts +11 -0
  39. package/dist/config.js +51 -0
  40. package/dist/cursorAuth.d.ts +10 -0
  41. package/dist/cursorAuth.js +95 -0
  42. package/dist/index.d.ts +2 -0
  43. package/dist/index.js +108 -0
  44. package/dist/listModels.d.ts +6 -0
  45. package/dist/listModels.js +62 -0
  46. package/dist/listSessions.d.ts +9 -0
  47. package/dist/listSessions.js +37 -0
  48. package/dist/pickFolder.d.ts +5 -0
  49. package/dist/pickFolder.js +65 -0
  50. package/dist/shared/actionDrafts.d.ts +83 -0
  51. package/dist/shared/actionDrafts.js +169 -0
  52. package/dist/shared/agentArtifacts.d.ts +49 -0
  53. package/dist/shared/agentArtifacts.js +169 -0
  54. package/dist/shared/agentLabel.d.ts +18 -0
  55. package/dist/shared/agentLabel.js +22 -0
  56. package/dist/shared/agentMentions.d.ts +38 -0
  57. package/dist/shared/agentMentions.js +110 -0
  58. package/dist/shared/agentRepos.d.ts +28 -0
  59. package/dist/shared/agentRepos.js +60 -0
  60. package/dist/shared/agentRunStatus.d.ts +12 -0
  61. package/dist/shared/agentRunStatus.js +17 -0
  62. package/dist/shared/agentUsage.d.ts +24 -0
  63. package/dist/shared/agentUsage.js +139 -0
  64. package/dist/shared/approvals.d.ts +44 -0
  65. package/dist/shared/approvals.js +97 -0
  66. package/dist/shared/askEigen.d.ts +190 -0
  67. package/dist/shared/askEigen.js +76 -0
  68. package/dist/shared/attribution.d.ts +25 -0
  69. package/dist/shared/attribution.js +63 -0
  70. package/dist/shared/automations.d.ts +136 -0
  71. package/dist/shared/automations.js +546 -0
  72. package/dist/shared/backends/claudeCode.d.ts +42 -0
  73. package/dist/shared/backends/claudeCode.js +540 -0
  74. package/dist/shared/backends/codex.d.ts +15 -0
  75. package/dist/shared/backends/codex.js +287 -0
  76. package/dist/shared/backends/cursor.d.ts +102 -0
  77. package/dist/shared/backends/cursor.js +1197 -0
  78. package/dist/shared/backends/index.d.ts +18 -0
  79. package/dist/shared/backends/index.js +58 -0
  80. package/dist/shared/backends/opencode.d.ts +40 -0
  81. package/dist/shared/backends/opencode.js +397 -0
  82. package/dist/shared/backends/types.d.ts +171 -0
  83. package/dist/shared/backends/types.js +19 -0
  84. package/dist/shared/backends/usage.d.ts +8 -0
  85. package/dist/shared/backends/usage.js +71 -0
  86. package/dist/shared/billingPlans.d.ts +60 -0
  87. package/dist/shared/billingPlans.js +262 -0
  88. package/dist/shared/brain.d.ts +353 -0
  89. package/dist/shared/brain.js +487 -0
  90. package/dist/shared/clarifyingQuestions.d.ts +13 -0
  91. package/dist/shared/clarifyingQuestions.js +207 -0
  92. package/dist/shared/claudeModels.d.ts +10 -0
  93. package/dist/shared/claudeModels.js +52 -0
  94. package/dist/shared/cloudBackends.d.ts +21 -0
  95. package/dist/shared/cloudBackends.js +29 -0
  96. package/dist/shared/codexModels.d.ts +10 -0
  97. package/dist/shared/codexModels.js +28 -0
  98. package/dist/shared/contextWindows.d.ts +2 -0
  99. package/dist/shared/contextWindows.js +30 -0
  100. package/dist/shared/cursorMcpConfig.d.ts +14 -0
  101. package/dist/shared/cursorMcpConfig.js +43 -0
  102. package/dist/shared/cursorProjectMcp.d.ts +10 -0
  103. package/dist/shared/cursorProjectMcp.js +42 -0
  104. package/dist/shared/docs.d.ts +59 -0
  105. package/dist/shared/docs.js +87 -0
  106. package/dist/shared/events.d.ts +929 -0
  107. package/dist/shared/events.js +59 -0
  108. package/dist/shared/integrations.d.ts +266 -0
  109. package/dist/shared/integrations.js +449 -0
  110. package/dist/shared/issueTranscript.d.ts +64 -0
  111. package/dist/shared/issueTranscript.js +387 -0
  112. package/dist/shared/issues.d.ts +139 -0
  113. package/dist/shared/issues.js +217 -0
  114. package/dist/shared/machines.d.ts +115 -0
  115. package/dist/shared/machines.js +58 -0
  116. package/dist/shared/markdownMedia.d.ts +13 -0
  117. package/dist/shared/markdownMedia.js +41 -0
  118. package/dist/shared/notificationText.d.ts +6 -0
  119. package/dist/shared/notificationText.js +38 -0
  120. package/dist/shared/opencodeModels.d.ts +19 -0
  121. package/dist/shared/opencodeModels.js +62 -0
  122. package/dist/shared/opencodeSession.d.ts +7 -0
  123. package/dist/shared/opencodeSession.js +30 -0
  124. package/dist/shared/orchestration.d.ts +58 -0
  125. package/dist/shared/orchestration.js +12 -0
  126. package/dist/shared/orgs.d.ts +47 -0
  127. package/dist/shared/orgs.js +49 -0
  128. package/dist/shared/personalOrg.d.ts +5 -0
  129. package/dist/shared/personalOrg.js +12 -0
  130. package/dist/shared/plans.d.ts +3 -0
  131. package/dist/shared/plans.js +19 -0
  132. package/dist/shared/providerModels.d.ts +40 -0
  133. package/dist/shared/providerModels.js +503 -0
  134. package/dist/shared/roomAttention.d.ts +17 -0
  135. package/dist/shared/roomAttention.js +36 -0
  136. package/dist/shared/roomContext.d.ts +120 -0
  137. package/dist/shared/roomContext.js +133 -0
  138. package/dist/shared/roomPermissions.d.ts +56 -0
  139. package/dist/shared/roomPermissions.js +133 -0
  140. package/dist/shared/sessionSeats.d.ts +29 -0
  141. package/dist/shared/sessionSeats.js +57 -0
  142. package/dist/shared/slashCommands.d.ts +39 -0
  143. package/dist/shared/slashCommands.js +166 -0
  144. package/dist/shared/space.d.ts +33 -0
  145. package/dist/shared/space.js +54 -0
  146. package/dist/shared/spaceFeed.d.ts +78 -0
  147. package/dist/shared/spaceFeed.js +194 -0
  148. package/dist/shared/stringifyUnknown.d.ts +1 -0
  149. package/dist/shared/stringifyUnknown.js +1 -0
  150. package/dist/shared/swarm.d.ts +320 -0
  151. package/dist/shared/swarm.js +328 -0
  152. package/dist/shared/swarmVisual.d.ts +79 -0
  153. package/dist/shared/swarmVisual.js +212 -0
  154. package/dist/shared/terminalFilter.d.ts +13 -0
  155. package/dist/shared/terminalFilter.js +46 -0
  156. package/dist/shared/toolIntegration.d.ts +56 -0
  157. package/dist/shared/toolIntegration.js +180 -0
  158. package/dist/shared/typing.d.ts +9 -0
  159. package/dist/shared/typing.js +37 -0
  160. package/dist/shared/uploads.d.ts +31 -0
  161. package/dist/shared/uploads.js +65 -0
  162. package/dist/shared/voiceApprovals.d.ts +36 -0
  163. package/dist/shared/voiceApprovals.js +229 -0
  164. package/dist/shared/workspace.d.ts +47 -0
  165. package/dist/shared/workspace.js +40 -0
  166. package/dist/worker.d.ts +1 -0
  167. package/dist/worker.js +266 -0
  168. package/package.json +51 -4
@@ -0,0 +1,503 @@
1
+ import { isCodexModelId } from "./codexModels.js";
2
+ const ANTHROPIC_MODELS_URL = "https://api.anthropic.com/v1/models?limit=200";
3
+ const OPENAI_MODELS_URL = "https://api.openai.com/v1/models";
4
+ const ANTHROPIC_VERSION = "2023-06-01";
5
+ export function displayNameFromId(id) {
6
+ if (id === "auto")
7
+ return "Auto";
8
+ return id
9
+ .replace(/^claude-/, "")
10
+ .replace(/[_-]+/g, " ")
11
+ .replace(/\b\w/g, (c) => c.toUpperCase());
12
+ }
13
+ /** First list wins id + display name; later lists only append new ids. */
14
+ export function mergeModelLists(...lists) {
15
+ const seen = new Set();
16
+ const out = [];
17
+ for (const list of lists) {
18
+ if (!list)
19
+ continue;
20
+ for (const model of list) {
21
+ const id = model?.id?.trim();
22
+ if (!id || seen.has(id))
23
+ continue;
24
+ seen.add(id);
25
+ out.push({
26
+ id,
27
+ displayName: model.displayName?.trim() || displayNameFromId(id),
28
+ ...(model.description ? { description: model.description } : {}),
29
+ ...(model.free ? { free: true } : {}),
30
+ ...(model.group ? { group: model.group } : {}),
31
+ });
32
+ }
33
+ }
34
+ return out;
35
+ }
36
+ /** Keep a currently-selected id visible in the picker even if the catalog moved on. */
37
+ export function ensureModelPresent(models, modelId) {
38
+ const id = modelId?.trim();
39
+ if (!id || models.some((m) => m.id === id))
40
+ return models;
41
+ return [...models, { id, displayName: displayNameFromId(id) }];
42
+ }
43
+ function asRecordArray(json) {
44
+ if (Array.isArray(json)) {
45
+ return json.filter((item) => Boolean(item && typeof item === "object"));
46
+ }
47
+ if (json && typeof json === "object") {
48
+ const rec = json;
49
+ const nested = rec.data ?? rec.models ?? rec.items;
50
+ if (Array.isArray(nested)) {
51
+ return nested.filter((item) => Boolean(item && typeof item === "object"));
52
+ }
53
+ }
54
+ return [];
55
+ }
56
+ function createdMs(value) {
57
+ if (typeof value === "number" && Number.isFinite(value)) {
58
+ return value < 1e12 ? value * 1000 : value;
59
+ }
60
+ if (typeof value === "string" && value.trim()) {
61
+ const asNum = Number(value);
62
+ if (Number.isFinite(asNum) && asNum > 0) {
63
+ return asNum < 1e12 ? asNum * 1000 : asNum;
64
+ }
65
+ const parsed = Date.parse(value);
66
+ if (!Number.isNaN(parsed))
67
+ return parsed;
68
+ }
69
+ return 0;
70
+ }
71
+ export function parseAnthropicModelsResponse(json) {
72
+ const rows = asRecordArray(json)
73
+ .map((rec) => {
74
+ const id = String(rec.id || "").trim();
75
+ if (!id)
76
+ return null;
77
+ const displayName = String(rec.display_name || rec.displayName || "").trim();
78
+ return {
79
+ id,
80
+ displayName: displayName || displayNameFromId(id),
81
+ created: createdMs(rec.created_at ?? rec.createdAt ?? rec.created),
82
+ };
83
+ })
84
+ .filter((row) => Boolean(row));
85
+ rows.sort((a, b) => b.created - a.created);
86
+ return rows.map(({ id, displayName }) => ({ id, displayName }));
87
+ }
88
+ function isExcludedOpenaiModel(id) {
89
+ const lower = id.toLowerCase();
90
+ return (lower.includes("embed") ||
91
+ lower.includes("whisper") ||
92
+ lower.includes("tts") ||
93
+ lower.includes("dall-e") ||
94
+ lower.includes("davinci") ||
95
+ lower.includes("babbage") ||
96
+ lower.includes("ada-") ||
97
+ lower.includes("realtime") ||
98
+ lower.includes("transcribe") ||
99
+ lower.includes("moderation") ||
100
+ lower.includes("image") ||
101
+ lower.includes("audio") ||
102
+ lower.includes("sora") ||
103
+ lower.startsWith("ft:"));
104
+ }
105
+ function openaiRank(id) {
106
+ const lower = id.toLowerCase();
107
+ if (lower.includes("codex"))
108
+ return 0;
109
+ if (lower.startsWith("gpt-6"))
110
+ return 1;
111
+ if (lower.startsWith("gpt-5"))
112
+ return 2;
113
+ if (lower.startsWith("o4") || lower.startsWith("o3"))
114
+ return 3;
115
+ if (lower.startsWith("gpt-4"))
116
+ return 4;
117
+ return 5;
118
+ }
119
+ export function parseOpenaiModelsResponse(json) {
120
+ const rows = asRecordArray(json)
121
+ .map((rec) => {
122
+ const id = String(rec.id || rec.slug || rec.model || "").trim();
123
+ if (!id || isExcludedOpenaiModel(id) || !isCodexModelId(id))
124
+ return null;
125
+ return { id, created: createdMs(rec.created) };
126
+ })
127
+ .filter((row) => Boolean(row));
128
+ rows.sort((a, b) => {
129
+ const rank = openaiRank(a.id) - openaiRank(b.id);
130
+ if (rank !== 0)
131
+ return rank;
132
+ return b.created - a.created;
133
+ });
134
+ return rows.map(({ id }) => ({ id, displayName: displayNameFromId(id) }));
135
+ }
136
+ const ANSI_RE = /\x1b\[[0-9;]*[a-zA-Z]/g;
137
+ const MODEL_LINE_RE = /^([a-zA-Z0-9][a-zA-Z0-9._-]*)\s+[-–—|:]\s+(.+)$/;
138
+ const MODEL_TOKEN_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
139
+ function stripAnsi(text) {
140
+ return text.replace(ANSI_RE, "").replace(/\r/g, "");
141
+ }
142
+ function modelFromUnknown(value) {
143
+ if (typeof value === "string") {
144
+ const id = value.trim();
145
+ if (!id || !MODEL_TOKEN_RE.test(id) || !isCodexModelId(id))
146
+ return null;
147
+ return { id, displayName: displayNameFromId(id) };
148
+ }
149
+ if (!value || typeof value !== "object")
150
+ return null;
151
+ const rec = value;
152
+ const id = String(rec.id || rec.slug || rec.model || rec.name || "").trim();
153
+ if (!id || !isCodexModelId(id))
154
+ return null;
155
+ const displayName = String(rec.display_name || rec.displayName || rec.label || rec.name || "").trim();
156
+ return {
157
+ id,
158
+ displayName: displayName && displayName !== id ? displayName : displayNameFromId(id),
159
+ };
160
+ }
161
+ function parseCodexDebugJson(json) {
162
+ if (Array.isArray(json)) {
163
+ return mergeModelLists(json.map(modelFromUnknown).filter((m) => Boolean(m)));
164
+ }
165
+ if (json && typeof json === "object") {
166
+ const rec = json;
167
+ const nested = rec.models ?? rec.data ?? rec.items ?? rec.available;
168
+ if (nested)
169
+ return parseCodexDebugJson(nested);
170
+ }
171
+ return [];
172
+ }
173
+ /** Parse `codex debug models` — JSON, "id — label" lines, or bare ids. */
174
+ export function parseCodexDebugModels(stdout) {
175
+ const text = stripAnsi(stdout || "").trim();
176
+ if (!text)
177
+ return [];
178
+ const jsonStart = text.indexOf("[") >= 0 && (text.indexOf("{") < 0 || text.indexOf("[") < text.indexOf("{"))
179
+ ? text.indexOf("[")
180
+ : text.indexOf("{");
181
+ if (jsonStart >= 0) {
182
+ try {
183
+ const parsed = parseCodexDebugJson(JSON.parse(text.slice(jsonStart)));
184
+ if (parsed.length)
185
+ return parsed;
186
+ }
187
+ catch {
188
+ // fall through to line parser
189
+ }
190
+ }
191
+ const models = [];
192
+ const seen = new Set();
193
+ for (const raw of text.split("\n")) {
194
+ const line = raw.trim();
195
+ if (!line || /^(available models|models):?$/i.test(line))
196
+ continue;
197
+ const labeled = line.match(MODEL_LINE_RE);
198
+ const id = (labeled?.[1] || line.split(/\s+/)[0] || "").trim();
199
+ if (!id || !MODEL_TOKEN_RE.test(id) || !isCodexModelId(id) || seen.has(id)) {
200
+ continue;
201
+ }
202
+ seen.add(id);
203
+ const label = labeled?.[2]?.trim();
204
+ models.push({
205
+ id,
206
+ displayName: label && label !== id ? label : displayNameFromId(id),
207
+ });
208
+ }
209
+ return models;
210
+ }
211
+ export async function fetchAnthropicModels(apiKey) {
212
+ const key = apiKey.trim();
213
+ if (!key)
214
+ throw new Error("Anthropic API key is required to list models");
215
+ const res = await fetch(ANTHROPIC_MODELS_URL, {
216
+ headers: {
217
+ "x-api-key": key,
218
+ "anthropic-version": ANTHROPIC_VERSION,
219
+ },
220
+ });
221
+ if (!res.ok) {
222
+ throw new Error(`Anthropic models request failed (${res.status})`);
223
+ }
224
+ return parseAnthropicModelsResponse(await res.json());
225
+ }
226
+ export async function fetchOpenaiCodingModels(apiKey) {
227
+ const key = apiKey.trim();
228
+ if (!key)
229
+ throw new Error("OpenAI API key is required to list models");
230
+ const res = await fetch(OPENAI_MODELS_URL, {
231
+ headers: { Authorization: `Bearer ${key}` },
232
+ });
233
+ if (!res.ok) {
234
+ throw new Error(`OpenAI models request failed (${res.status})`);
235
+ }
236
+ return parseOpenaiModelsResponse(await res.json());
237
+ }
238
+ // ── OpenCode (models.dev) catalog ────────────────────────────────────────────
239
+ /**
240
+ * The model database the OpenCode CLI itself is built on. Fetching it makes
241
+ * the picker show every provider OpenCode supports — including free models
242
+ * that need no sign-in — without any worker or API key.
243
+ */
244
+ const OPENCODE_CATALOG_URLS = [
245
+ "https://models.opencode.ai/api.json",
246
+ "https://models.dev/api.json",
247
+ ];
248
+ const OPENCODE_CATALOG_TTL_MS = 6 * 60 * 60_000;
249
+ const OPENCODE_CATALOG_FETCH_TIMEOUT_MS = 30_000;
250
+ let opencodeCatalogCache = null;
251
+ let opencodeCatalogInFlight = null;
252
+ /** Sync peek for keyless checks in hot paths (null until the first fetch). */
253
+ export function cachedOpencodeCatalog() {
254
+ return opencodeCatalogCache?.catalog ?? null;
255
+ }
256
+ function modelsDevText(parsed, field) {
257
+ const value = parsed?.[field];
258
+ return typeof value === "string" ? value.trim() : "";
259
+ }
260
+ /** Can this model drive an agent: tool calling + text output. */
261
+ function modelsDevAgentCapable(model) {
262
+ if (model.tool_call === false)
263
+ return false;
264
+ const modalities = model.modalities;
265
+ const outputs = modalities?.output;
266
+ if (Array.isArray(outputs) && outputs.length && !outputs.includes("text")) {
267
+ return false;
268
+ }
269
+ return true;
270
+ }
271
+ function modelsDevFree(model) {
272
+ const cost = model.cost;
273
+ if (!cost)
274
+ return false;
275
+ return Number(cost.input) === 0 && Number(cost.output) === 0;
276
+ }
277
+ export function parseModelsDevCatalog(json) {
278
+ const providers = json && typeof json === "object" && !Array.isArray(json)
279
+ ? json
280
+ : {};
281
+ const models = [];
282
+ const seen = new Set();
283
+ const keylessModelIds = new Set();
284
+ for (const [providerId, providerRaw] of Object.entries(providers)) {
285
+ if (!providerRaw || typeof providerRaw !== "object")
286
+ continue;
287
+ const provider = providerRaw;
288
+ const providerName = modelsDevText(provider, "name") || providerId;
289
+ const modelMap = provider.models;
290
+ if (!modelMap || typeof modelMap !== "object")
291
+ continue;
292
+ for (const [rawModelId, modelRaw] of Object.entries(modelMap)) {
293
+ if (!modelRaw || typeof modelRaw !== "object")
294
+ continue;
295
+ const model = modelRaw;
296
+ const id = modelsDevText(model, "id") || rawModelId.trim();
297
+ if (!id || /\s/.test(id))
298
+ continue;
299
+ const composite = `${providerId}/${id}`;
300
+ if (seen.has(composite))
301
+ continue;
302
+ seen.add(composite);
303
+ if (!modelsDevAgentCapable(model))
304
+ continue;
305
+ const free = modelsDevFree(model);
306
+ const keyless = free && providerId === "opencode";
307
+ if (keyless)
308
+ keylessModelIds.add(composite);
309
+ models.push({
310
+ id: composite,
311
+ displayName: modelsDevText(model, "name") || id,
312
+ ...(free ? { free: true } : {}),
313
+ description: free
314
+ ? keyless
315
+ ? `${providerName} — free, no API key`
316
+ : `${providerName} — free (needs the provider's own API key)`
317
+ : providerName,
318
+ });
319
+ }
320
+ }
321
+ models.sort((a, b) => a.id.localeCompare(b.id));
322
+ return { models, keylessModelIds };
323
+ }
324
+ const OPENCODE_ZEN_MODELS_URL = "https://opencode.ai/zen/v1/models";
325
+ /** Model ids OpenCode's gateway serves right now (`opencode models` uses the same list). */
326
+ async function fetchOpencodeZenModelIds() {
327
+ try {
328
+ const res = await fetch(OPENCODE_ZEN_MODELS_URL, {
329
+ signal: AbortSignal.timeout(OPENCODE_CATALOG_FETCH_TIMEOUT_MS),
330
+ });
331
+ if (!res.ok)
332
+ return null;
333
+ const json = (await res.json());
334
+ const ids = (json.data || [])
335
+ .map((m) => (typeof m.id === "string" ? m.id.trim() : ""))
336
+ .filter((id) => id && !/\s/.test(id));
337
+ return ids.length ? ids : null;
338
+ }
339
+ catch {
340
+ return null;
341
+ }
342
+ }
343
+ /**
344
+ * models.dev keeps free gateway models long after OpenCode retires them, and
345
+ * the CLI answers those with "Unexpected server error". Keep only the free
346
+ * ones the gateway still lists.
347
+ */
348
+ export function applyOpencodeZenModels(catalog, zenIds) {
349
+ if (!zenIds)
350
+ return catalog;
351
+ const live = new Set(zenIds.map((id) => `opencode/${id}`));
352
+ const known = new Set(catalog.models.map((m) => m.id));
353
+ const keylessModelIds = new Set();
354
+ const models = catalog.models.filter((m) => !m.id.startsWith("opencode/") || live.has(m.id));
355
+ for (const m of models) {
356
+ if (catalog.keylessModelIds.has(m.id))
357
+ keylessModelIds.add(m.id);
358
+ }
359
+ for (const id of zenIds) {
360
+ const composite = `opencode/${id}`;
361
+ if (!/-free$/i.test(id))
362
+ continue;
363
+ keylessModelIds.add(composite);
364
+ if (known.has(composite))
365
+ continue;
366
+ models.push({
367
+ id: composite,
368
+ displayName: id,
369
+ free: true,
370
+ description: "OpenCode — free, no API key",
371
+ });
372
+ }
373
+ models.sort((a, b) => a.id.localeCompare(b.id));
374
+ return { models, keylessModelIds };
375
+ }
376
+ async function fetchOpencodeCatalogOnce() {
377
+ const zen = fetchOpencodeZenModelIds();
378
+ let lastError = null;
379
+ for (const url of OPENCODE_CATALOG_URLS) {
380
+ try {
381
+ const res = await fetch(url, {
382
+ signal: AbortSignal.timeout(OPENCODE_CATALOG_FETCH_TIMEOUT_MS),
383
+ });
384
+ if (!res.ok)
385
+ throw new Error(`models.dev request failed (${res.status})`);
386
+ const json = (await res.json());
387
+ return applyOpencodeZenModels(parseModelsDevCatalog(json), await zen);
388
+ }
389
+ catch (err) {
390
+ lastError = err;
391
+ }
392
+ }
393
+ throw lastError instanceof Error ? lastError : new Error("models.dev unreachable");
394
+ }
395
+ /** Full OpenCode model database (models.dev), cached 6 h and stale-safe. */
396
+ export async function fetchOpencodeCatalog() {
397
+ const cached = opencodeCatalogCache;
398
+ if (cached && Date.now() - cached.at < OPENCODE_CATALOG_TTL_MS) {
399
+ return cached.catalog;
400
+ }
401
+ opencodeCatalogInFlight ??= fetchOpencodeCatalogOnce()
402
+ .then((catalog) => {
403
+ opencodeCatalogCache = { at: Date.now(), catalog };
404
+ return catalog;
405
+ })
406
+ .finally(() => {
407
+ opencodeCatalogInFlight = null;
408
+ });
409
+ const inflight = opencodeCatalogInFlight;
410
+ try {
411
+ return await inflight;
412
+ }
413
+ catch {
414
+ // Serve the previous copy while the next attempt runs; otherwise empty.
415
+ if (cached)
416
+ return cached.catalog;
417
+ opencodeCatalogInFlight = null;
418
+ throw new Error("Could not load the OpenCode model catalog");
419
+ }
420
+ }
421
+ const OPENCODE_FREE_GROUP_ORDER = [
422
+ "xAI",
423
+ "Zhipu",
424
+ "MiniMax",
425
+ "Moonshot",
426
+ "Alibaba",
427
+ "DeepSeek",
428
+ "NVIDIA",
429
+ "Xiaomi",
430
+ "OpenCode",
431
+ ];
432
+ /** Underlying lab for an `opencode/<slug>` free model. */
433
+ export function opencodeFreeModelGroup(id) {
434
+ const slug = id.includes("/") ? id.slice(id.indexOf("/") + 1) : id;
435
+ if (/^grok/i.test(slug))
436
+ return "xAI";
437
+ if (/^glm/i.test(slug))
438
+ return "Zhipu";
439
+ if (/^minimax/i.test(slug))
440
+ return "MiniMax";
441
+ if (/^kimi/i.test(slug))
442
+ return "Moonshot";
443
+ if (/^qwen/i.test(slug))
444
+ return "Alibaba";
445
+ if (/^deepseek/i.test(slug))
446
+ return "DeepSeek";
447
+ if (/^nemotron/i.test(slug))
448
+ return "NVIDIA";
449
+ if (/^mimo/i.test(slug))
450
+ return "Xiaomi";
451
+ return "OpenCode";
452
+ }
453
+ /**
454
+ * Preferred free defaults on OpenCode's own gateway (no API key).
455
+ */
456
+ export const OPENCODE_FREE_PICKER_PREFERENCES = [
457
+ "opencode/glm-5.3-flash-free",
458
+ "opencode/deepseek-v4.1-flash-free",
459
+ "opencode/big-pickle",
460
+ "opencode/grok-code",
461
+ "opencode/minimax-m3-free",
462
+ "opencode/kimi-k2.5-free",
463
+ "opencode/qwen3.6-plus-free",
464
+ ];
465
+ export function firstKeylessFreeOpencodeModel() {
466
+ const keylessModelIds = cachedOpencodeCatalog()?.keylessModelIds;
467
+ if (!keylessModelIds?.size)
468
+ return null;
469
+ for (const id of OPENCODE_FREE_PICKER_PREFERENCES) {
470
+ if (keylessModelIds.has(id))
471
+ return id;
472
+ }
473
+ return keylessModelIds.values().next().value ?? null;
474
+ }
475
+ /** Only OpenCode-gateway free models, grouped by provider. */
476
+ export function opencodePickerModels(catalog) {
477
+ const pref = new Map(OPENCODE_FREE_PICKER_PREFERENCES.map((id, i) => [id, i]));
478
+ const free = [];
479
+ for (const model of catalog.models) {
480
+ if (!catalog.keylessModelIds.has(model.id))
481
+ continue;
482
+ const group = opencodeFreeModelGroup(model.id);
483
+ free.push({
484
+ ...model,
485
+ free: true,
486
+ group,
487
+ description: model.description || `${group} — free, no API key`,
488
+ });
489
+ }
490
+ free.sort((a, b) => {
491
+ const ga = OPENCODE_FREE_GROUP_ORDER.indexOf(a.group || "OpenCode");
492
+ const gb = OPENCODE_FREE_GROUP_ORDER.indexOf(b.group || "OpenCode");
493
+ const g = (ga < 0 ? 99 : ga) - (gb < 0 ? 99 : gb);
494
+ if (g)
495
+ return g;
496
+ const pa = pref.get(a.id) ?? 100;
497
+ const pb = pref.get(b.id) ?? 100;
498
+ if (pa !== pb)
499
+ return pa - pb;
500
+ return a.displayName.localeCompare(b.displayName);
501
+ });
502
+ return free;
503
+ }
@@ -0,0 +1,17 @@
1
+ import type { AgentRunStatus, ApprovalRequestInfo, PingInfo } from "./events.js";
2
+ export type AttentionKind = "approval" | "ping" | "error" | "drive" | "running" | "unread";
3
+ export type RoomAttention = {
4
+ kind: AttentionKind;
5
+ label: string;
6
+ count?: number;
7
+ };
8
+ export declare function computeRoomAttention({ pendingApprovals, openPings, agentError, errorByAgent, agentStatus, statusByAgent, pendingDrive, unreadCount, }: {
9
+ pendingApprovals: ApprovalRequestInfo[];
10
+ openPings: PingInfo[];
11
+ agentError: string;
12
+ errorByAgent: Record<string, string>;
13
+ agentStatus: AgentRunStatus;
14
+ statusByAgent: Record<string, AgentRunStatus>;
15
+ pendingDrive: boolean;
16
+ unreadCount: number;
17
+ }): RoomAttention | null;
@@ -0,0 +1,36 @@
1
+ export function computeRoomAttention({ pendingApprovals, openPings, agentError, errorByAgent, agentStatus, statusByAgent, pendingDrive, unreadCount, }) {
2
+ if (pendingApprovals.length > 0) {
3
+ return {
4
+ kind: "approval",
5
+ label: pendingApprovals.length === 1 ? "Approval" : "Approvals",
6
+ count: pendingApprovals.length,
7
+ };
8
+ }
9
+ if (openPings.length > 0) {
10
+ return {
11
+ kind: "ping",
12
+ label: openPings.length === 1 ? "Review ping" : "Review pings",
13
+ count: openPings.length,
14
+ };
15
+ }
16
+ const errors = Object.values(errorByAgent).filter(Boolean);
17
+ if (agentError || errors.length) {
18
+ return { kind: "error", label: "Error" };
19
+ }
20
+ if (pendingDrive) {
21
+ return { kind: "drive", label: "Drive request" };
22
+ }
23
+ const running = agentStatus === "running" ||
24
+ Object.values(statusByAgent).some((status) => status === "running");
25
+ if (running) {
26
+ return { kind: "running", label: "Running" };
27
+ }
28
+ if (unreadCount > 0) {
29
+ return {
30
+ kind: "unread",
31
+ label: unreadCount === 1 ? "New message" : "New messages",
32
+ count: unreadCount,
33
+ };
34
+ }
35
+ return null;
36
+ }
@@ -0,0 +1,120 @@
1
+ /** Room-scoped repo map + shared memory contracts. */
2
+ export declare const MEMORY_KINDS: readonly ["goal", "decision", "constraint", "discovery", "handoff", "feedback"];
3
+ export type MemoryKind = (typeof MEMORY_KINDS)[number];
4
+ export declare const MEMORY_STATUSES: readonly ["active", "proposed", "archived", "superseded"];
5
+ export type MemoryStatus = (typeof MEMORY_STATUSES)[number];
6
+ export declare const MEMORY_SOURCES: readonly ["human", "auto", "agent_proposed"];
7
+ export type MemorySource = (typeof MEMORY_SOURCES)[number];
8
+ export declare const AUTO_MEMORY_MODES: readonly ["off", "extract"];
9
+ export type AutoMemoryMode = (typeof AUTO_MEMORY_MODES)[number];
10
+ export declare function isMemorySource(v: unknown): v is MemorySource;
11
+ export declare function parseAutoMemoryMode(raw: unknown, fallback?: AutoMemoryMode): AutoMemoryMode;
12
+ export type RepoMapStatus = "idle" | "scanning" | "ready" | "error";
13
+ export type RepoMapNodeKind = "file" | "symbol";
14
+ export type RepoMapEdgeRel = "contains" | "imports" | "requires" | "references";
15
+ export interface RepoMapNode {
16
+ id: string;
17
+ kind: RepoMapNodeKind;
18
+ path: string;
19
+ ext?: string;
20
+ keywords: string[];
21
+ name?: string;
22
+ symbolType?: string;
23
+ lineStart?: number;
24
+ lineEnd?: number;
25
+ exported?: boolean;
26
+ }
27
+ export interface RepoMapEdge {
28
+ from: string;
29
+ to: string;
30
+ rel: RepoMapEdgeRel;
31
+ }
32
+ export interface RepoMapGraph {
33
+ nodes: RepoMapNode[];
34
+ edges: RepoMapEdge[];
35
+ }
36
+ export interface RepoMapInfo {
37
+ id: string;
38
+ roomId: string;
39
+ repoKey: string;
40
+ gitSha: string | null;
41
+ status: RepoMapStatus;
42
+ error?: string | null;
43
+ fileCount: number;
44
+ symbolCount: number;
45
+ edgeCount: number;
46
+ generatedAt: number;
47
+ }
48
+ export interface MemoryEntryInfo {
49
+ id: string;
50
+ roomId: string;
51
+ kind: MemoryKind;
52
+ title: string;
53
+ content: string;
54
+ status: MemoryStatus;
55
+ pinned: boolean;
56
+ revision: number;
57
+ createdByUserId?: string | null;
58
+ createdByAgentId?: string | null;
59
+ createdAt: number;
60
+ updatedAt: number;
61
+ sourceMessageId?: string | null;
62
+ sourcePath?: string | null;
63
+ supersedesId?: string | null;
64
+ /** Who wrote this row. Auto entries are extractor-created, no click. */
65
+ source?: MemorySource;
66
+ }
67
+ export interface AgentContextReceiptInfo {
68
+ id: string;
69
+ roomId: string;
70
+ agentId: string;
71
+ runId: string;
72
+ mapId?: string | null;
73
+ gitSha?: string | null;
74
+ memoryVersion: number;
75
+ entryIds: string[];
76
+ fileIds: string[];
77
+ isBaseline: boolean;
78
+ createdAt: number;
79
+ }
80
+ export interface HandoffDraft {
81
+ title: string;
82
+ content: string;
83
+ sourcePath?: string;
84
+ branch?: string | null;
85
+ prUrl?: string | null;
86
+ touchedPaths: string[];
87
+ }
88
+ export interface RoomContextSnapshot {
89
+ memoryVersion: number;
90
+ map: RepoMapInfo | null;
91
+ entries: MemoryEntryInfo[];
92
+ lastReceiptByAgent: Record<string, AgentContextReceiptInfo>;
93
+ }
94
+ export interface PackedContext {
95
+ text: string;
96
+ memoryVersion: number;
97
+ mapId: string | null;
98
+ gitSha: string | null;
99
+ entryIds: string[];
100
+ fileIds: string[];
101
+ isBaseline: boolean;
102
+ estimatedChars: number;
103
+ }
104
+ export declare const MEMORY_TITLE_MAX = 160;
105
+ export declare const MEMORY_CONTENT_MAX = 4000;
106
+ export declare const CONTEXT_BUDGET_CHARS = 12000;
107
+ export declare const BASELINE_BUDGET_CHARS = 8000;
108
+ export declare const TASK_PACK_FILES = 12;
109
+ export declare const TASK_PACK_EDGES = 20;
110
+ export declare function isMemoryKind(v: unknown): v is MemoryKind;
111
+ /** Standing instructions / jailbreak-shaped text must never auto-accept. */
112
+ export declare function looksLikeMemoryInjection(input: string): boolean;
113
+ export declare function isMemoryStatus(v: unknown): v is MemoryStatus;
114
+ export declare function stripControlChars(input: string): string;
115
+ export declare function redactSecrets(input: string): string;
116
+ export declare function sanitizeMemoryText(input: unknown, max: number): string;
117
+ export declare function tokenizeQuery(text: string): string[];
118
+ export type ContextIntent = "debug" | "explain" | "test" | "refactor" | "feature" | "edit" | "general";
119
+ export declare function classifyIntent(query: string): ContextIntent;
120
+ export declare function wrapUntrustedBlock(tag: string, attrs: string, body: string): string;