@adecore/agents 0.39.0 → 0.40.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/dist/async.d.ts +5 -0
  2. package/dist/async.js +13 -5
  3. package/dist/chat/acp-protocol.js +3 -5
  4. package/dist/chat/chat-core.js +0 -2
  5. package/dist/chat/chat-process.d.ts +1 -0
  6. package/dist/chat/chat-process.js +1 -1
  7. package/dist/chat/chat-session.js +6 -8
  8. package/dist/chat/claude-protocol.js +1 -6
  9. package/dist/chat/claude-transcript.js +1 -3
  10. package/dist/chat/codex-protocol.js +1 -6
  11. package/dist/chat/projector.js +1 -3
  12. package/dist/chat/request-summary.js +1 -3
  13. package/dist/coded-error.js +1 -5
  14. package/dist/context/argv.js +1 -2
  15. package/dist/context/verb.js +1 -3
  16. package/dist/error-text.d.ts +1 -0
  17. package/dist/error-text.js +4 -0
  18. package/dist/fields.d.ts +3 -0
  19. package/dist/fields.js +12 -0
  20. package/dist/fs.d.ts +1 -0
  21. package/dist/fs.js +8 -4
  22. package/dist/lineage.js +11 -5
  23. package/dist/messages/notice-store.js +3 -4
  24. package/dist/providers/accounts/acp-account.js +3 -20
  25. package/dist/providers/accounts/service.d.ts +1 -2
  26. package/dist/providers/accounts/service.js +5 -9
  27. package/dist/providers/accounts/status.js +1 -3
  28. package/dist/providers/acp-agent-state.js +1 -3
  29. package/dist/providers/claude.js +0 -1
  30. package/dist/providers/codex-models.json +3 -2
  31. package/dist/providers/codex.js +0 -1
  32. package/dist/providers/detect.js +1 -1
  33. package/dist/providers/shell-commands.js +1 -2
  34. package/dist/run-process.js +1 -3
  35. package/dist/skills.d.ts +1 -1
  36. package/dist/skills.js +5 -8
  37. package/dist/tasks/background-limit.js +3 -2
  38. package/dist/tasks/task-coordinator.js +11 -17
  39. package/dist/testing/watch.js +0 -1
  40. package/dist/usage/limits/monitor.js +1 -2
  41. package/dist/usage/prices-snapshot.json +8 -0
  42. package/dist/usage/readers/claude.js +3 -10
  43. package/dist/usage/readers/codex.js +2 -9
  44. package/dist/usage/readers/grok.js +3 -10
  45. package/dist/usage/readers/host-log.js +3 -10
  46. package/dist/usage/record.d.ts +2 -0
  47. package/dist/usage/record.js +13 -0
  48. package/package.json +6 -6
  49. package/src/async.ts +23 -5
  50. package/src/chat/acp-backend.ts +121 -0
  51. package/src/chat/acp-protocol.ts +172 -6
  52. package/src/chat/attachment-store.ts +21 -0
  53. package/src/chat/backend.ts +0 -2
  54. package/src/chat/bookmark-store.ts +23 -0
  55. package/src/chat/chat-core.ts +208 -7
  56. package/src/chat/chat-log.ts +19 -0
  57. package/src/chat/chat-process.ts +52 -1
  58. package/src/chat/chat-session.ts +401 -10
  59. package/src/chat/chat-store.ts +50 -0
  60. package/src/chat/chat-title.ts +9 -0
  61. package/src/chat/claude-backend.ts +50 -0
  62. package/src/chat/claude-protocol.ts +155 -8
  63. package/src/chat/claude-title.ts +10 -0
  64. package/src/chat/claude-transcript.ts +47 -4
  65. package/src/chat/codex-backend.ts +131 -0
  66. package/src/chat/codex-protocol.ts +180 -8
  67. package/src/chat/codex-thread.ts +23 -0
  68. package/src/chat/composer-preferences.ts +6 -0
  69. package/src/chat/context-breakdown.ts +11 -0
  70. package/src/chat/delta-coalescer.ts +8 -0
  71. package/src/chat/external-sessions.ts +28 -0
  72. package/src/chat/generated-image.ts +25 -0
  73. package/src/chat/index.ts +5 -0
  74. package/src/chat/input.ts +13 -0
  75. package/src/chat/json-rpc-transport.ts +28 -0
  76. package/src/chat/limit-resume.ts +13 -0
  77. package/src/chat/projector.ts +160 -4
  78. package/src/chat/readable-text.ts +3 -0
  79. package/src/chat/request-summary.ts +18 -4
  80. package/src/chat/subagent-projection.ts +3 -0
  81. package/src/chat/subagent-reader.ts +76 -0
  82. package/src/chat/subagent-settlement.ts +24 -0
  83. package/src/chat/thread-search.ts +13 -0
  84. package/src/chat/thread.ts +31 -0
  85. package/src/chat/ui-queries.ts +122 -0
  86. package/src/chat/ui-replies.ts +34 -0
  87. package/src/chat/visual-store.ts +49 -0
  88. package/src/chat/wake-chat.ts +3 -0
  89. package/src/client-sinks.ts +2 -0
  90. package/src/coded-error.ts +1 -5
  91. package/src/context/argv.ts +18 -2
  92. package/src/context/index.ts +1 -0
  93. package/src/context/refusal.ts +3 -0
  94. package/src/context/verb.ts +32 -4
  95. package/src/coordination/index.ts +10 -0
  96. package/src/error-text.ts +7 -0
  97. package/src/fields.ts +14 -0
  98. package/src/fs.ts +20 -4
  99. package/src/host/agent-host.ts +20 -0
  100. package/src/host/environment.ts +6 -0
  101. package/src/host/handlers.ts +18 -0
  102. package/src/host/memory-port.ts +4 -0
  103. package/src/host/wiring.ts +10 -0
  104. package/src/index.ts +1 -0
  105. package/src/lineage.ts +24 -5
  106. package/src/messages/deliver-message.ts +6 -0
  107. package/src/messages/deliver-notice.ts +13 -0
  108. package/src/messages/notice-store.ts +16 -4
  109. package/src/modes.ts +1 -0
  110. package/src/outbox/outbox-worker.ts +31 -0
  111. package/src/outbox/outbox.ts +5 -0
  112. package/src/providers/accounts/accounts.ts +36 -0
  113. package/src/providers/accounts/acp-account.ts +22 -27
  114. package/src/providers/accounts/launch.ts +4 -0
  115. package/src/providers/accounts/service.ts +110 -12
  116. package/src/providers/accounts/shadow-home.ts +10 -0
  117. package/src/providers/accounts/status.ts +13 -4
  118. package/src/providers/accounts/store.ts +25 -0
  119. package/src/providers/accounts/variables.ts +11 -1
  120. package/src/providers/acp-agent-state.ts +24 -3
  121. package/src/providers/acp-provider.ts +5 -0
  122. package/src/providers/acp-usage-log.ts +2 -0
  123. package/src/providers/catalog.ts +17 -0
  124. package/src/providers/claude.ts +10 -1
  125. package/src/providers/codex-models.json +3 -2
  126. package/src/providers/codex-rules.ts +3 -0
  127. package/src/providers/codex.ts +2 -1
  128. package/src/providers/detect.ts +6 -1
  129. package/src/providers/provider.ts +1 -1
  130. package/src/providers/registry.ts +18 -2
  131. package/src/providers/shell-commands.ts +37 -2
  132. package/src/record-directory.ts +19 -0
  133. package/src/run-process.ts +24 -3
  134. package/src/serializer.ts +3 -0
  135. package/src/skills.ts +73 -10
  136. package/src/tasks/background-limit.ts +9 -2
  137. package/src/tasks/end-children.ts +9 -0
  138. package/src/tasks/give-task.ts +8 -0
  139. package/src/tasks/task-coordinator.ts +81 -17
  140. package/src/tasks/task-store.ts +28 -0
  141. package/src/tasks/waiting-child.ts +21 -0
  142. package/src/tasks/wake-parent.ts +26 -0
  143. package/src/tasks/wiring.ts +22 -0
  144. package/src/testing/accounts.ts +2 -0
  145. package/src/testing/fake-acp-capture.ts +16 -0
  146. package/src/testing/fake-acp.ts +66 -0
  147. package/src/testing/fake-claude.ts +177 -0
  148. package/src/testing/fake-cli.ts +29 -0
  149. package/src/testing/fake-codex.ts +122 -0
  150. package/src/testing/manual-clock.ts +2 -0
  151. package/src/testing/watch.ts +7 -1
  152. package/src/title-file.ts +12 -0
  153. package/src/usage/accounts.ts +6 -0
  154. package/src/usage/aggregate.ts +19 -0
  155. package/src/usage/exchange.ts +12 -0
  156. package/src/usage/index-file.ts +27 -0
  157. package/src/usage/limits/monitor.ts +31 -3
  158. package/src/usage/limits/normalize.ts +30 -0
  159. package/src/usage/limits/probe.ts +17 -0
  160. package/src/usage/prices-snapshot.json +8 -0
  161. package/src/usage/pricing.ts +35 -0
  162. package/src/usage/projects.ts +8 -0
  163. package/src/usage/readers/claude.ts +12 -9
  164. package/src/usage/readers/codex.ts +40 -8
  165. package/src/usage/readers/grok.ts +17 -9
  166. package/src/usage/readers/host-log.ts +9 -9
  167. package/src/usage/record.ts +22 -0
  168. package/src/usage/roots.ts +6 -0
  169. package/src/usage/scanner.ts +61 -0
  170. package/src/usage/usage-service.ts +14 -0
  171. package/src/watch-seam.ts +8 -0
  172. package/src/windows-command.ts +12 -0
@@ -34,28 +34,37 @@ export type StoredAccounts = Record<string, unknown>;
34
34
  /* Every entry that does not parse is dropped on its own; a file that is not JSON at all reads as none. */
35
35
  export async function readAccounts(path: string, providers: AccountProviders, env: Env, host: AccountsHost = DEFAULT_ACCOUNTS_HOST): Promise<StoredAccounts> {
36
36
  let parsed: unknown;
37
+
37
38
  try {
38
39
  parsed = JSON.parse(await readFile(path, 'utf8'));
39
40
  } catch (e) {
40
41
  if (isNotFound(e) || e instanceof SyntaxError) {
41
42
  return {};
42
43
  }
44
+
43
45
  throw e;
44
46
  }
47
+
45
48
  const entries = typeof parsed === 'object' && parsed !== null ? (parsed as { accounts?: unknown }).accounts : null;
49
+
46
50
  if (typeof entries !== 'object' || entries === null || Array.isArray(entries)) {
47
51
  return {};
48
52
  }
53
+
49
54
  const stored: StoredAccounts = {};
55
+
50
56
  for (const [id, raw] of Object.entries(entries)) {
51
57
  if (!ProviderAccountIdSchema.safeParse(id).success) {
52
58
  continue;
53
59
  }
60
+
54
61
  const account = readAccount(id, raw, providers, env, host);
62
+
55
63
  if (account !== null) {
56
64
  stored[id] = account;
57
65
  }
58
66
  }
67
+
59
68
  return stored;
60
69
  }
61
70
 
@@ -66,9 +75,11 @@ export async function writeAccounts(path: string, stored: StoredAccounts): Promi
66
75
  /* What a client reads of an entry of a kind it may not know either: the fields this version knows. */
67
76
  function wireAccount(entry: unknown): ProviderAccount {
68
77
  const parsed = ProviderAccountSchema.safeParse(entry);
78
+
69
79
  if (!parsed.success) {
70
80
  return { kind: String((entry as { kind?: unknown }).kind) };
71
81
  }
82
+
72
83
  return parsed.data.env === undefined ? parsed.data : { ...parsed.data, env: redactVariables(parsed.data.env) };
73
84
  }
74
85
 
@@ -84,6 +95,7 @@ export class InvalidAccountError extends CodedError<'invalid-account'> {
84
95
 
85
96
  function storedFolders(entry: unknown): Pick<ProviderAccount, 'home' | 'shadowHome'> {
86
97
  const parsed = ProviderAccountSchema.safeParse(entry);
98
+
87
99
  return parsed.success ? { home: parsed.data.home, shadowHome: parsed.data.shadowHome } : {};
88
100
  }
89
101
 
@@ -102,18 +114,23 @@ function linkProblem(
102
114
  if (isDefaultAccount(id, account) || provider.home === undefined || account.home === undefined) {
103
115
  return null;
104
116
  }
117
+
105
118
  const was = storedFolders(before);
119
+
106
120
  if (was.home === account.home && was.shadowHome === account.shadowHome) {
107
121
  return null;
108
122
  }
123
+
109
124
  for (const folder of [account.home, account.shadowHome]) {
110
125
  if (folder !== undefined && !isDirectory(expandHome(folder, env))) {
111
126
  return `${folder} is not a folder on this machine`;
112
127
  }
113
128
  }
129
+
114
130
  if (accountFolder(id, account, provider, env) === defaultFolder(provider, env)) {
115
131
  return `${defaultFolder(provider, env)} is the folder of the default ${provider.name} account`;
116
132
  }
133
+
117
134
  return null;
118
135
  }
119
136
 
@@ -133,23 +150,31 @@ export function acceptAccounts(
133
150
  ): StoredAccounts {
134
151
  const stored: StoredAccounts = {};
135
152
  const reserved = { host, folderVariables: folderVariablesOf(providers) };
153
+
136
154
  for (const [id, account] of Object.entries(withDefaults(saved, providers.kinds))) {
137
155
  if (!isOfferedKind(account.kind, providers.kinds)) {
138
156
  if (isOfferedKind(id, providers.kinds)) {
139
157
  throw new InvalidAccountError(`"${id}" is the id of the default ${id} account`);
140
158
  }
159
+
141
160
  const kept = before[id];
142
161
  const keptKind = typeof kept === 'object' && kept !== null ? (kept as { kind?: unknown }).kind : undefined;
143
162
  stored[id] = keptKind === account.kind ? kept : account;
163
+
144
164
  continue;
145
165
  }
166
+
146
167
  const provider = providers.get(account.kind);
168
+
147
169
  const problem =
148
170
  accountProblem(id, account, provider, env, reserved, providers.kinds) ?? linkProblem(id, account, before[id], provider, env, isDirectory);
171
+
149
172
  if (problem !== null) {
150
173
  throw new InvalidAccountError(`${id}: ${problem}`);
151
174
  }
175
+
152
176
  stored[id] = account;
153
177
  }
178
+
154
179
  return stored;
155
180
  }
@@ -15,7 +15,6 @@ export class SecretsUnavailableError extends CodedError<'secrets-unavailable'> {
15
15
  }
16
16
  }
17
17
 
18
- /* What keeps the sensitive values; a test keeps them in memory. */
19
18
  export interface SecretStore {
20
19
  /* Null for a key it has nothing under. */
21
20
  read(key: string): Promise<string | null>;
@@ -60,18 +59,23 @@ function isPrintableAscii(value: string): boolean {
60
59
  /* What is wrong with the variables of an account, or null when nothing is. A secret that is only redacted is judged by the service. */
61
60
  export function variablesProblem(variables: readonly ProviderAccountVariable[], reserved: ReservedVariables): string | null {
62
61
  const seen = new Set<string>();
62
+
63
63
  for (const variable of variables) {
64
64
  if (seen.has(variable.name)) {
65
65
  return `${variable.name} is set twice`;
66
66
  }
67
+
67
68
  seen.add(variable.name);
69
+
68
70
  if (isReservedName(variable.name, reserved)) {
69
71
  return `${variable.name} is set by ${reserved.host.name} itself`;
70
72
  }
73
+
71
74
  if (variable.sensitive && !isPrintableAscii(variable.value)) {
72
75
  return `The value of ${variable.name} can only hold printable ASCII characters`;
73
76
  }
74
77
  }
78
+
75
79
  return null;
76
80
  }
77
81
 
@@ -90,6 +94,7 @@ const NOT_FOUND = 44;
90
94
 
91
95
  async function run(args: string[], stdin?: string): Promise<{ code: number; stdout: string; stderr: string }> {
92
96
  const { exitCode, stdout, stderr } = await runProcess(['security', ...args], { ...(stdin === undefined ? {} : { stdin }) });
97
+
93
98
  return { code: exitCode ?? 1, stdout, stderr };
94
99
  }
95
100
 
@@ -102,24 +107,29 @@ export function keychainSecrets(service: string): SecretStore {
102
107
  return {
103
108
  read: async (key) => {
104
109
  const { code, stdout, stderr } = await run(['find-generic-password', '-s', service, '-a', key, '-w']);
110
+
105
111
  if (code === NOT_FOUND) {
106
112
  return null;
107
113
  }
114
+
108
115
  if (code !== 0) {
109
116
  throw new Error(`The keychain did not give ${key}: ${stderr.trim()}`);
110
117
  }
118
+
111
119
  return stdout.endsWith('\n') ? stdout.slice(0, -1) : stdout;
112
120
  },
113
121
  write: async (key, value) => {
114
122
  const hex = Buffer.from(value, 'utf8').toString('hex');
115
123
  const quoted = `"${key.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
116
124
  const { code, stderr } = await run(['-i'], `add-generic-password -U -s ${service} -a ${quoted} -X ${hex}\n`);
125
+
117
126
  if (code !== 0) {
118
127
  throw new Error(`The keychain did not take ${key}: ${stderr.trim()}`);
119
128
  }
120
129
  },
121
130
  remove: async (key) => {
122
131
  const { code, stderr } = await run(['delete-generic-password', '-s', service, '-a', key]);
132
+
123
133
  if (code !== 0 && code !== NOT_FOUND) {
124
134
  throw new Error(`The keychain did not remove ${key}: ${stderr.trim()}`);
125
135
  }
@@ -32,6 +32,7 @@ function capitalized(option: ModelOptionDescriptor): ModelOptionDescriptor {
32
32
  if (option.type !== 'select') {
33
33
  return option;
34
34
  }
35
+
35
36
  return {
36
37
  ...option,
37
38
  choices: option.choices.map((choice) => ({ ...choice, label: choice.label.charAt(0).toLocaleUpperCase('en') + choice.label.slice(1) }))
@@ -41,9 +42,11 @@ function capitalized(option: ModelOptionDescriptor): ModelOptionDescriptor {
41
42
  /* The reasoning effort a session offers, as the `effort` option the composer draws for every provider. */
42
43
  function effortOption(info: AcpSessionInfo): ModelOptionDescriptor[] {
43
44
  const level = info.thoughtLevel;
45
+
44
46
  if (level === null || !level.choices.some((choice) => choice.id === level.current)) {
45
47
  return [];
46
48
  }
49
+
47
50
  return [
48
51
  {
49
52
  id: 'effort',
@@ -62,6 +65,7 @@ function effortOption(info: AcpSessionInfo): ModelOptionDescriptor[] {
62
65
  /* Parses one stored part on its own, so a part a later version wrote differently costs only that part. */
63
66
  function storedPart<T>(schema: z.ZodType<T>, value: unknown): T | null {
64
67
  const parsed = schema.safeParse(value);
68
+
65
69
  return parsed.success ? parsed.data : null;
66
70
  }
67
71
 
@@ -99,6 +103,7 @@ export class AcpAgentState {
99
103
 
100
104
  onChange(listener: () => void): () => void {
101
105
  this.listeners.add(listener);
106
+
102
107
  return () => {
103
108
  this.listeners.delete(listener);
104
109
  };
@@ -108,20 +113,22 @@ export class AcpAgentState {
108
113
  if (!this.options.adoptModels || info.models === null) {
109
114
  return;
110
115
  }
111
- const adopted: Adopted = { defaultModel: info.models.current, models: adoptableModels(info), options: effortOption(info) };
112
- const changed = this.adopt(adopted);
113
- this.settle(changed);
116
+
117
+ this.settle(this.adopt({ defaultModel: info.models.current, models: adoptableModels(info), options: effortOption(info) }));
114
118
  }
115
119
 
116
120
  commandsListed(commands: readonly string[]): void {
117
121
  const names = [...new Set(commands)].sort();
122
+
118
123
  if (this.commands !== null && JSON.stringify(names) === JSON.stringify(this.commands)) {
119
124
  return;
120
125
  }
126
+
121
127
  this.commands = names;
122
128
  const changed = this.refresh();
123
129
  // The commands are saved even when the capabilities stay, so a restart knows them too.
124
130
  this.save();
131
+
125
132
  if (changed) {
126
133
  this.notify();
127
134
  }
@@ -131,25 +138,31 @@ export class AcpAgentState {
131
138
  if (this.reportsContext) {
132
139
  return;
133
140
  }
141
+
134
142
  this.reportsContext = true;
135
143
  this.settle(this.refresh());
136
144
  }
137
145
 
138
146
  private restore(): void {
139
147
  let stored: unknown;
148
+
140
149
  try {
141
150
  stored = this.options.store?.load();
142
151
  } catch {
143
152
  return;
144
153
  }
154
+
145
155
  if (typeof stored !== 'object' || stored === null) {
146
156
  return;
147
157
  }
158
+
148
159
  const record = stored as Record<string, unknown>;
149
160
  const adopted = storedPart(AdoptedSchema, record.models);
161
+
150
162
  if (adopted !== null && this.options.adoptModels) {
151
163
  this.adopt(adopted);
152
164
  }
165
+
153
166
  this.reportsContext = record.reportsContext === true;
154
167
  this.commands = storedPart(z.array(z.string()), record.commands);
155
168
  this.refresh();
@@ -158,6 +171,7 @@ export class AcpAgentState {
158
171
  /* What the agent showed it can do, under what the host set itself. */
159
172
  private learned(): Partial<ProviderCapabilities> {
160
173
  const commands = this.commands ?? [];
174
+
161
175
  return {
162
176
  ...(this.reportsContext ? { reportsContextWindow: true } : {}),
163
177
  ...(commands.includes('compact') ? { compaction: 'prompt' as const } : {}),
@@ -168,14 +182,19 @@ export class AcpAgentState {
168
182
  /* Lays what the agent showed over the capabilities, in place; answers whether they changed. */
169
183
  private refresh(): boolean {
170
184
  const next: ProviderCapabilities = { ...this.options.base, ...this.learned(), ...this.options.overrides };
185
+
171
186
  if (JSON.stringify(next) === JSON.stringify(this.capabilities)) {
172
187
  return false;
173
188
  }
189
+
174
190
  const capabilities = this.capabilities as Partial<ProviderCapabilities>;
191
+
175
192
  for (const key of Object.keys(capabilities) as Array<keyof ProviderCapabilities>) {
176
193
  delete capabilities[key];
177
194
  }
195
+
178
196
  Object.assign(capabilities, next);
197
+
179
198
  return true;
180
199
  }
181
200
 
@@ -184,7 +203,9 @@ export class AcpAgentState {
184
203
  if (!adopted.models.some((model) => model.slug === adopted.defaultModel)) {
185
204
  return false;
186
205
  }
206
+
187
207
  this.adopted = adopted;
208
+
188
209
  return this.options.catalog.adopt(adopted.models, adopted.defaultModel, adopted.options.map(capitalized));
189
210
  }
190
211
 
@@ -72,15 +72,19 @@ export function createAcpProvider(spec: AcpProviderSpec): ChatProvider {
72
72
  base: ACP_CAPABILITIES,
73
73
  overrides: spec.capabilities ?? {}
74
74
  });
75
+
75
76
  const onSession = (info: AcpSessionInfo): void => {
76
77
  state.session(info);
77
78
  spec.onSession?.(info);
78
79
  };
80
+
79
81
  const onCommands = (commands: string[]): void => {
80
82
  state.commandsListed(commands);
81
83
  spec.onCommands?.(commands);
82
84
  };
85
+
83
86
  const usageLog = spec.usageLog === undefined || OWN_USAGE_FILES.has(spec.kind) ? null : spec.usageLog;
87
+
84
88
  const onUsage =
85
89
  usageLog === null
86
90
  ? spec.onUsage
@@ -88,6 +92,7 @@ export function createAcpProvider(spec: AcpProviderSpec): ChatProvider {
88
92
  writeAcpUsage(usageLog, report, Date.now());
89
93
  spec.onUsage?.(report);
90
94
  };
95
+
91
96
  return {
92
97
  kind: spec.kind,
93
98
  name: spec.name,
@@ -10,8 +10,10 @@ import { hostUsageLine } from '../usage/readers/host-log.ts';
10
10
  */
11
11
  export function writeAcpUsage(folder: string, report: AcpUsageReport, at: number): void {
12
12
  const { sessionId, cwd, model, ...tokens } = report;
13
+
13
14
  try {
14
15
  mkdirSync(folder, { recursive: true, mode: 0o700 });
16
+
15
17
  appendFileSync(
16
18
  join(folder, `${encodeURIComponent(sessionId === '' ? 'session' : sessionId)}.jsonl`),
17
19
  hostUsageLine({ at, model, session: sessionId, cwd, ...tokens }),
@@ -35,9 +35,11 @@ export class ModelCatalog {
35
35
  if (timeOf(data) < this.shippedAt) {
36
36
  return false;
37
37
  }
38
+
38
39
  const changed = JSON.stringify(data) !== JSON.stringify(this.data);
39
40
  this.data = data;
40
41
  this.hostWindows = undefined;
42
+
41
43
  return changed;
42
44
  }
43
45
 
@@ -55,24 +57,31 @@ export class ModelCatalog {
55
57
  if (models.length === 0 || !models.some((model) => model.slug === defaultModel)) {
56
58
  return false;
57
59
  }
60
+
58
61
  const base = this.profileOf(this.data.defaultModel) ?? { options: [], contextWindowTokens: {} };
59
62
  const replaced = new Set(options.map((option) => option.id));
60
63
  this.hostWindows ??= base.contextWindowTokens;
61
64
  const profile = { ...base, contextWindowTokens: this.hostWindows, options: [...base.options.filter((option) => !replaced.has(option.id)), ...options] };
65
+
62
66
  const profileName = (window: number | undefined): string => (window === undefined ? ADOPTED_PROFILE : `${ADOPTED_PROFILE}-${window}`);
67
+
63
68
  const profiles: Record<string, ModelCatalogProfile> = {};
69
+
64
70
  for (const model of models) {
65
71
  profiles[profileName(model.contextWindow)] =
66
72
  model.contextWindow === undefined ? profile : { ...profile, contextWindowTokens: { '*': model.contextWindow } };
67
73
  }
74
+
68
75
  const data: ModelCatalogInput = {
69
76
  ...(this.data.updatedAt === undefined ? {} : { updatedAt: this.data.updatedAt }),
70
77
  defaultModel,
71
78
  profiles,
72
79
  models: models.map((model) => ({ slug: model.slug, name: model.name, profile: profileName(model.contextWindow) }))
73
80
  };
81
+
74
82
  const changed = JSON.stringify(data) !== JSON.stringify(this.data);
75
83
  this.data = data;
84
+
76
85
  return changed;
77
86
  }
78
87
 
@@ -98,6 +107,7 @@ export class ModelCatalog {
98
107
  /* Accepts a slug or an alias; unknown models answer null so the caller can decide what to do. */
99
108
  resolveModel(name: string): string | null {
100
109
  const model = this.data.models.find((entry) => entry.slug === name || entry.aliases?.includes(name));
110
+
101
111
  return model?.slug ?? null;
102
112
  }
103
113
 
@@ -105,8 +115,10 @@ export class ModelCatalog {
105
115
  normalize(selection: Partial<ModelSelection> | undefined): ModelSelection {
106
116
  const model = (selection?.model && this.resolveModel(selection.model)) || this.defaultModel;
107
117
  const options: ModelSelection['options'] = {};
118
+
108
119
  for (const descriptor of this.profileOf(model)?.options ?? []) {
109
120
  const given = selection?.options?.[descriptor.id];
121
+
110
122
  if (descriptor.type === 'select') {
111
123
  options[descriptor.id] =
112
124
  typeof given === 'string' && descriptor.choices.some((choice) => choice.id === given) ? given : descriptor.defaultChoice;
@@ -114,20 +126,25 @@ export class ModelCatalog {
114
126
  options[descriptor.id] = typeof given === 'boolean' ? given : descriptor.defaultValue;
115
127
  }
116
128
  }
129
+
117
130
  return { model, options };
118
131
  }
119
132
 
120
133
  contextWindowFor(selection: ModelSelection): number | null {
121
134
  const profile = this.profileOf(selection.model);
135
+
122
136
  if (!profile) {
123
137
  return null;
124
138
  }
139
+
125
140
  const chosen = selection.options.contextWindow;
141
+
126
142
  return (typeof chosen === 'string' ? profile.contextWindowTokens[chosen] : undefined) ?? profile.contextWindowTokens['*'] ?? null;
127
143
  }
128
144
 
129
145
  private profileOf(slug: string): ModelCatalogProfile | undefined {
130
146
  const model = this.data.models.find((entry) => entry.slug === slug);
147
+
131
148
  return model ? this.data.profiles[model.profile] : undefined;
132
149
  }
133
150
  }
@@ -35,6 +35,7 @@ export function claudeRuntimeMode(mode: string): RuntimeMode | undefined {
35
35
  if (mode === 'default') {
36
36
  return 'supervised';
37
37
  }
38
+
38
39
  return (Object.keys(PERMISSION_MODE) as RuntimeMode[]).find((key) => PERMISSION_MODE[key] === mode);
39
40
  }
40
41
 
@@ -55,12 +56,15 @@ export function claudeArgs(launch: ClaudeLaunch): string[] {
55
56
  ...(launch.folders ?? []).map((folder) => `--add-dir=${folder}`),
56
57
  ...(launch.projectConfig === false ? CLAUDE_WITHOUT_PROJECT_CONFIG_ARGS : [])
57
58
  ];
59
+
58
60
  const contextWindow = launch.selection.options.contextWindow;
59
61
  args.push('--model', `${launch.selection.model}${contextWindow === '1m' ? '[1m]' : ''}`);
60
62
  const effort = launch.selection.options.effort;
63
+
61
64
  if (typeof effort === 'string' && FLAG_EFFORTS.has(effort)) {
62
65
  args.push('--effort', effort);
63
66
  }
67
+
64
68
  /*
65
69
  * Fast mode has no flag of its own; the CLI reads it from the settings blob, which is also the
66
70
  * opt-in the init frame asks for (without it the frame answers `sdk_opt_in_required`). It only
@@ -72,19 +76,25 @@ export function claudeArgs(launch: ClaudeLaunch): string[] {
72
76
  ...(launch.selection.options.fastMode === true ? { fastMode: true } : {}),
73
77
  ...(launch.selection.options.contextWindow === '200k' ? { autoCompactEnabled: true } : {})
74
78
  };
79
+
75
80
  if (Object.keys(settings).length > 0) {
76
81
  args.push('--settings', JSON.stringify(settings));
77
82
  }
83
+
78
84
  const permissionMode = PERMISSION_MODE[launch.runtimeMode];
85
+
79
86
  if (permissionMode) {
80
87
  args.push('--permission-mode', permissionMode);
81
88
  }
89
+
82
90
  if (permissionMode === 'bypassPermissions') {
83
91
  args.push('--allow-dangerously-skip-permissions');
84
92
  }
93
+
85
94
  if (launch.resume) {
86
95
  args.push('--resume', launch.resume);
87
96
  }
97
+
88
98
  return args;
89
99
  }
90
100
 
@@ -98,7 +108,6 @@ export function claudeEnv(selection: ModelSelection): Record<string, string> {
98
108
  return selection.options.contextWindow === '200k' ? { CLAUDE_CODE_AUTO_COMPACT_WINDOW: '200000' } : {};
99
109
  }
100
110
 
101
- // What the CLI can do, as far as a client has to know.
102
111
  export const CLAUDE_CAPABILITIES: ProviderCapabilities = {
103
112
  chat: true,
104
113
  terminal: true,
@@ -1,5 +1,5 @@
1
1
  {
2
- "updatedAt": "2026-09-29T17:26:43Z",
2
+ "updatedAt": "2026-10-26T22:10:00Z",
3
3
  "defaultModel": "gpt-6-astra",
4
4
  "profiles": {
5
5
  "ultra": {
@@ -124,10 +124,11 @@
124
124
  "models": [
125
125
  { "slug": "gpt-6-astra", "name": "GPT-6 Astra", "profile": "ultra", "aliases": ["astra", "gpt-6"] },
126
126
  { "slug": "gpt-6.1-sol", "name": "GPT-6.1 Sol", "badge": "new", "profile": "max", "aliases": ["sol"] },
127
+ { "slug": "gpt-6.1-terra", "name": "GPT-6.1 Terra", "badge": "new", "profile": "ultra-low", "aliases": ["terra"] },
127
128
  { "slug": "gpt-6-sol", "name": "GPT-6 Sol", "profile": "max" },
128
129
  { "slug": "gpt-6-luna", "name": "GPT-6 Luna", "badge": "new", "profile": "max", "aliases": ["luna"] },
129
130
  { "slug": "gpt-5.6-sol", "name": "GPT-5.6 Sol", "profile": "ultra-low", "legacy": true },
130
- { "slug": "gpt-5.6-terra", "name": "GPT-5.6 Terra", "profile": "ultra", "aliases": ["terra"], "badge": "retiring", "legacy": true },
131
+ { "slug": "gpt-5.6-terra", "name": "GPT-5.6 Terra", "profile": "ultra", "badge": "retiring", "legacy": true },
131
132
  { "slug": "gpt-5.6-luna", "name": "GPT-5.6 Luna", "profile": "max", "legacy": true },
132
133
  { "slug": "gpt-5.5", "name": "GPT-5.5", "profile": "xhigh", "legacy": true },
133
134
  { "slug": "gpt-5.3-codex-spark", "name": "GPT-5.3 Codex Spark", "profile": "spark", "aliases": ["spark"], "legacy": true }
@@ -39,6 +39,7 @@ export function defaultCodexHome(env: Record<string, string | undefined> = proce
39
39
  /* Idempotent: a file that already says the same is left alone. */
40
40
  export async function installCodexRules(path: string, rules: CodexRules): Promise<CodexRulesResult> {
41
41
  const text = codexRulesText(rules);
42
+
42
43
  try {
43
44
  if ((await readFile(path, 'utf8')) === text) {
44
45
  return 'unchanged';
@@ -48,7 +49,9 @@ export async function installCodexRules(path: string, rules: CodexRules): Promis
48
49
  throw new Error(`Cannot read ${path}: ${errorText(e)}`);
49
50
  }
50
51
  }
52
+
51
53
  await mkdir(dirname(path), { recursive: true });
52
54
  await writeAtomic(path, text, 0o644);
55
+
53
56
  return 'written';
54
57
  }
@@ -30,8 +30,10 @@ export function codexThreadOptions(runtimeMode: RuntimeMode): CodexThreadOptions
30
30
 
31
31
  export function codexRuntimeMode(approvalPolicy: string, sandbox: string): RuntimeMode | undefined {
32
32
  const names: Record<string, string> = { readOnly: 'read-only', workspaceWrite: 'workspace-write', dangerFullAccess: 'danger-full-access' };
33
+
33
34
  return (Object.keys(THREAD_OPTIONS) as RuntimeMode[]).find((mode) => {
34
35
  const options = THREAD_OPTIONS[mode];
36
+
35
37
  return options.approvalPolicy === approvalPolicy && options.sandbox === (names[sandbox] ?? sandbox);
36
38
  });
37
39
  }
@@ -44,7 +46,6 @@ export function codexServiceTier(selection: ModelSelection): string | null {
44
46
  return selection.options.serviceTier === true ? CODEX_PRIORITY_TIER : null;
45
47
  }
46
48
 
47
- // What the app-server can do, as far as a client has to know.
48
49
  export const CODEX_CAPABILITIES: ProviderCapabilities = {
49
50
  chat: true,
50
51
  terminal: true,
@@ -18,16 +18,21 @@ export async function detectCli(
18
18
  ): Promise<CliDetection> {
19
19
  try {
20
20
  let exited: (code: number | null) => void = () => undefined;
21
+
21
22
  const code = new Promise<number | null>((resolve) => {
22
23
  exited = resolve;
23
24
  });
25
+
24
26
  const proc = spawn({ command: [command, ...flags, '--version'], cwd: process.cwd(), env: definedEnv(env), onExit: (exitCode) => exited(exitCode) });
25
27
  proc.stdin.end();
26
28
  const output = await new Response(proc.stdout).text();
29
+
27
30
  if ((await code) !== 0) {
28
31
  return { installed: false, version: null };
29
32
  }
30
- const version = /\d+\.\d+\.\d+/.exec(output)?.[0] ?? output.trim() ?? null;
33
+
34
+ const version = /\d+\.\d+\.\d+/.exec(output)?.[0] ?? output.trim();
35
+
31
36
  return { installed: true, version };
32
37
  } catch {
33
38
  return { installed: false, version: null };
@@ -26,7 +26,7 @@ export interface ChatProvider {
26
26
  readonly name: string;
27
27
  readonly catalog: ModelCatalog;
28
28
  readonly capabilities: ProviderCapabilities;
29
- // The executable and its leading arguments; a test points this at a fake CLI.
29
+ // The executable and its leading arguments.
30
30
  readonly command: string[];
31
31
  // What a terminal runs to continue one of this CLI's sessions; `{id}` stands for the session id.
32
32
  readonly resumeCommand: string;