@adecore/agents 0.38.0 → 0.40.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 (170) 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.d.ts +1 -0
  5. package/dist/chat/chat-core.js +9 -2
  6. package/dist/chat/chat-process.d.ts +1 -0
  7. package/dist/chat/chat-process.js +1 -1
  8. package/dist/chat/chat-session.d.ts +2 -0
  9. package/dist/chat/chat-session.js +9 -10
  10. package/dist/chat/claude-protocol.js +1 -6
  11. package/dist/chat/claude-transcript.js +1 -3
  12. package/dist/chat/codex-protocol.js +1 -6
  13. package/dist/chat/projector.js +1 -3
  14. package/dist/chat/request-summary.js +1 -3
  15. package/dist/coded-error.js +1 -5
  16. package/dist/context/argv.js +1 -2
  17. package/dist/context/verb.js +1 -3
  18. package/dist/error-text.d.ts +1 -0
  19. package/dist/error-text.js +4 -0
  20. package/dist/fields.d.ts +3 -0
  21. package/dist/fields.js +12 -0
  22. package/dist/fs.d.ts +1 -0
  23. package/dist/fs.js +8 -4
  24. package/dist/lineage.js +11 -5
  25. package/dist/messages/notice-store.js +3 -4
  26. package/dist/providers/accounts/acp-account.js +3 -20
  27. package/dist/providers/accounts/service.d.ts +1 -2
  28. package/dist/providers/accounts/service.js +5 -9
  29. package/dist/providers/accounts/status.js +1 -3
  30. package/dist/providers/acp-agent-state.js +1 -3
  31. package/dist/providers/claude.js +0 -1
  32. package/dist/providers/codex.js +0 -1
  33. package/dist/providers/detect.js +1 -1
  34. package/dist/providers/shell-commands.js +1 -2
  35. package/dist/run-process.js +1 -3
  36. package/dist/skills.d.ts +1 -1
  37. package/dist/skills.js +5 -8
  38. package/dist/tasks/background-limit.js +3 -2
  39. package/dist/tasks/task-coordinator.js +11 -17
  40. package/dist/testing/watch.js +0 -1
  41. package/dist/usage/limits/monitor.js +1 -2
  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 +218 -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 +414 -13
  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-rules.ts +3 -0
  126. package/src/providers/codex.ts +2 -1
  127. package/src/providers/detect.ts +6 -1
  128. package/src/providers/provider.ts +1 -1
  129. package/src/providers/registry.ts +18 -2
  130. package/src/providers/shell-commands.ts +37 -2
  131. package/src/record-directory.ts +19 -0
  132. package/src/run-process.ts +24 -3
  133. package/src/serializer.ts +3 -0
  134. package/src/skills.ts +73 -10
  135. package/src/tasks/background-limit.ts +9 -2
  136. package/src/tasks/end-children.ts +9 -0
  137. package/src/tasks/give-task.ts +8 -0
  138. package/src/tasks/task-coordinator.ts +81 -17
  139. package/src/tasks/task-store.ts +28 -0
  140. package/src/tasks/waiting-child.ts +21 -0
  141. package/src/tasks/wake-parent.ts +26 -0
  142. package/src/tasks/wiring.ts +22 -0
  143. package/src/testing/accounts.ts +2 -0
  144. package/src/testing/fake-acp-capture.ts +16 -0
  145. package/src/testing/fake-acp.ts +66 -0
  146. package/src/testing/fake-claude.ts +177 -0
  147. package/src/testing/fake-cli.ts +29 -0
  148. package/src/testing/fake-codex.ts +122 -0
  149. package/src/testing/manual-clock.ts +2 -0
  150. package/src/testing/watch.ts +7 -1
  151. package/src/title-file.ts +12 -0
  152. package/src/usage/accounts.ts +6 -0
  153. package/src/usage/aggregate.ts +19 -0
  154. package/src/usage/exchange.ts +12 -0
  155. package/src/usage/index-file.ts +27 -0
  156. package/src/usage/limits/monitor.ts +31 -3
  157. package/src/usage/limits/normalize.ts +30 -0
  158. package/src/usage/limits/probe.ts +17 -0
  159. package/src/usage/pricing.ts +35 -0
  160. package/src/usage/projects.ts +8 -0
  161. package/src/usage/readers/claude.ts +12 -9
  162. package/src/usage/readers/codex.ts +40 -8
  163. package/src/usage/readers/grok.ts +17 -9
  164. package/src/usage/readers/host-log.ts +9 -9
  165. package/src/usage/record.ts +22 -0
  166. package/src/usage/roots.ts +6 -0
  167. package/src/usage/scanner.ts +61 -0
  168. package/src/usage/usage-service.ts +14 -0
  169. package/src/watch-seam.ts +8 -0
  170. package/src/windows-command.ts +12 -0
@@ -18,6 +18,7 @@ export interface ThreadItemsParams {
18
18
  export function parseThreadItemsPage(result: unknown): { entries: Record<string, unknown>[]; nextCursor: string | null } {
19
19
  const data = isRecord(result) && Array.isArray(result.data) ? result.data.filter(isRecord) : [];
20
20
  const nextCursor = isRecord(result) && typeof result.nextCursor === 'string' && result.nextCursor !== '' ? result.nextCursor : null;
21
+
21
22
  return { entries: data, nextCursor };
22
23
  }
23
24
 
@@ -40,23 +41,31 @@ function userText(content: unknown): string {
40
41
  export function projectCodexItems(entriesOldestFirst: Record<string, unknown>[], now: number): ChatItem[] {
41
42
  const { thread, projector } = readingThread('Codex', () => now);
42
43
  const protocol = new CodexProtocol(0);
44
+
43
45
  for (const entry of entriesOldestFirst) {
44
46
  const item = isRecord(entry.item) ? entry.item : null;
47
+
45
48
  if (!item || typeof item.id !== 'string') {
46
49
  continue;
47
50
  }
51
+
48
52
  projector.reset();
53
+
49
54
  if (item.type === 'userMessage') {
50
55
  const text = userText(item.content);
56
+
51
57
  if (text !== '') {
52
58
  thread.upsert({ id: `user-${item.id}`, kind: 'user', createdAt: now, turnId: null, text });
53
59
  }
60
+
54
61
  continue;
55
62
  }
63
+
56
64
  for (const event of protocol.handle({ method: 'item/completed', params: { item } })) {
57
65
  projector.project(0, event);
58
66
  }
59
67
  }
68
+
60
69
  return thread.list().map(settledReading);
61
70
  }
62
71
 
@@ -78,12 +87,15 @@ async function withAppServer<T>(spec: CodexProcessSpec, work: (transport: CodexT
78
87
  onFrame: () => undefined,
79
88
  onExit: () => undefined
80
89
  });
90
+
81
91
  try {
82
92
  await transport.request('initialize', {
83
93
  clientInfo: spec.client ?? DEFAULT_CODEX_CLIENT,
84
94
  capabilities: { experimentalApi: true, requestAttestation: false }
85
95
  });
96
+
86
97
  transport.notify('initialized', {});
98
+
87
99
  return await work(transport);
88
100
  } finally {
89
101
  transport.end();
@@ -112,17 +124,21 @@ export interface ThreadForkParams {
112
124
  async function nthTurnId(transport: CodexTransport, threadId: string, turns: number): Promise<string | null> {
113
125
  let seen = 0;
114
126
  let cursor: string | null = null;
127
+
115
128
  do {
116
129
  const page = parseThreadItemsPage(
117
130
  await transport.request('thread/turns/list', { threadId, limit: 100, sortDirection: 'asc', ...(cursor === null ? {} : { cursor }) })
118
131
  );
132
+
119
133
  for (const turn of page.entries) {
120
134
  if (++seen === turns) {
121
135
  return typeof turn.id === 'string' ? turn.id : null;
122
136
  }
123
137
  }
138
+
124
139
  cursor = page.nextCursor;
125
140
  } while (cursor !== null);
141
+
126
142
  return null;
127
143
  }
128
144
 
@@ -134,13 +150,17 @@ async function nthTurnId(transport: CodexTransport, threadId: string, turns: num
134
150
  export function forkThreadOnce(spec: CodexProcessSpec, params: ThreadForkParams): Promise<string> {
135
151
  return withAppServer(spec, async (transport) => {
136
152
  let lastTurnId: string | null = null;
153
+
137
154
  if (params.at !== null) {
138
155
  lastTurnId = 'turnId' in params.at ? params.at.turnId : await nthTurnId(transport, params.threadId, params.at.turns);
156
+
139
157
  if (lastTurnId === null) {
140
158
  throw new ChatError('turn-not-found', 'Codex keeps fewer turns in this thread than the chat shows, so the turn could not be found');
141
159
  }
142
160
  }
161
+
143
162
  let result: unknown;
163
+
144
164
  try {
145
165
  result = await transport.request('thread/fork', {
146
166
  threadId: params.threadId,
@@ -151,10 +171,13 @@ export function forkThreadOnce(spec: CodexProcessSpec, params: ThreadForkParams)
151
171
  } catch (error) {
152
172
  throw new ChatError('fork-failed', errorText(error));
153
173
  }
174
+
154
175
  const thread = isRecord(result) && isRecord(result.thread) ? result.thread : null;
176
+
155
177
  if (thread === null || typeof thread.id !== 'string' || thread.id === '') {
156
178
  throw new ChatError('fork-failed', 'Codex answered the fork without a thread');
157
179
  }
180
+
158
181
  return thread.id;
159
182
  });
160
183
  }
@@ -39,22 +39,27 @@ export class ComposerPreferences {
39
39
  /* The newest pick among the connected clients, narrowed to this provider; empty with none. */
40
40
  for(provider: AgentKind): ComposerPreference {
41
41
  const newest = this.newest();
42
+
42
43
  if (newest === null) {
43
44
  return {};
44
45
  }
46
+
45
47
  const { runtimeMode, selections, accounts } = newest.preference;
46
48
  const selection = selections?.[provider];
47
49
  const account = accounts?.[provider];
50
+
48
51
  return { ...(runtimeMode ? { runtimeMode } : {}), ...(selection ? { selection } : {}), ...(account ? { account } : {}) };
49
52
  }
50
53
 
51
54
  private newest(): Held | null {
52
55
  let newest: Held | null = null;
56
+
53
57
  for (const held of this.byClient.values()) {
54
58
  if (newest === null || isNewer(held, newest)) {
55
59
  newest = held;
56
60
  }
57
61
  }
62
+
58
63
  return newest;
59
64
  }
60
65
  }
@@ -62,5 +67,6 @@ export class ComposerPreferences {
62
67
  function isNewer(held: Held, than: Held): boolean {
63
68
  const changedAt = held.preference.changedAt ?? 0;
64
69
  const otherChangedAt = than.preference.changedAt ?? 0;
70
+
65
71
  return changedAt === otherChangedAt ? held.told > than.told : changedAt > otherChangedAt;
66
72
  }
@@ -21,6 +21,7 @@ function inputTokens(input: unknown): number {
21
21
  if (input === undefined || input === null) {
22
22
  return 0;
23
23
  }
24
+
24
25
  return textTokens(typeof input === 'string' ? input : JSON.stringify(input));
25
26
  }
26
27
 
@@ -35,27 +36,33 @@ export function estimateContextBreakdown(items: readonly ChatItem[], contextToke
35
36
  let filesRead = 0;
36
37
  let conversation = 0;
37
38
  const start = items.findLastIndex((item) => item.kind === 'compaction') + 1;
39
+
38
40
  for (const item of items.slice(start)) {
39
41
  switch (item.kind) {
40
42
  case 'user':
41
43
  conversation += textTokens(item.text);
42
44
  filesRead += (item.attachments ?? []).filter((attachment) => attachmentImageMime(attachment) !== null).length * IMAGE_TOKENS;
45
+
43
46
  break;
44
47
  case 'assistant':
45
48
  if (!item.parentToolUseId) {
46
49
  conversation += textTokens(item.text);
47
50
  }
51
+
48
52
  break;
49
53
  case 'tool': {
50
54
  if (item.parentToolUseId !== null) {
51
55
  break;
52
56
  }
57
+
53
58
  const tokens = inputTokens(item.input) + textTokens(item.output ?? item.progress?.output ?? '');
59
+
54
60
  if (isReadTool(item.name)) {
55
61
  filesRead += tokens;
56
62
  } else {
57
63
  toolOutput += tokens;
58
64
  }
65
+
59
66
  break;
60
67
  }
61
68
  case 'subagent':
@@ -63,18 +70,22 @@ export function estimateContextBreakdown(items: readonly ChatItem[], contextToke
63
70
  if (item.origin !== HOST_TASK_ORIGIN) {
64
71
  toolOutput += textTokens(item.prompt ?? '') + textTokens(item.result ?? '');
65
72
  }
73
+
66
74
  break;
67
75
  default:
68
76
  break;
69
77
  }
70
78
  }
79
+
71
80
  const total = Math.max(0, Math.round(contextTokens));
72
81
  const estimated = toolOutput + filesRead + conversation;
73
82
  const scale = estimated > total ? total / estimated : 1;
83
+
74
84
  const parts = {
75
85
  toolOutput: Math.floor(toolOutput * scale),
76
86
  filesRead: Math.floor(filesRead * scale),
77
87
  conversation: Math.floor(conversation * scale)
78
88
  };
89
+
79
90
  return { ...parts, system: Math.max(0, total - parts.toolOutput - parts.filesRead - parts.conversation) };
80
91
  }
@@ -24,12 +24,16 @@ export class DeltaCoalescer {
24
24
  if (event.type !== 'delta' || event.uiQueries !== undefined) {
25
25
  this.flush();
26
26
  this.send(event);
27
+
27
28
  return;
28
29
  }
30
+
29
31
  if (this.pending !== null && this.pending.itemId === event.itemId) {
30
32
  this.pending.text += event.text;
33
+
31
34
  return;
32
35
  }
36
+
33
37
  this.flush();
34
38
  this.pending = { itemId: event.itemId, text: event.text };
35
39
  this.timer = setTimeout(() => this.flush(), this.tickMs);
@@ -43,10 +47,13 @@ export class DeltaCoalescer {
43
47
  clearTimeout(this.timer);
44
48
  this.timer = null;
45
49
  }
50
+
46
51
  const pending = this.pending;
52
+
47
53
  if (pending === null) {
48
54
  return;
49
55
  }
56
+
50
57
  this.pending = null;
51
58
  this.send({ type: 'delta', itemId: pending.itemId, text: pending.text });
52
59
  }
@@ -57,6 +64,7 @@ export class DeltaCoalescer {
57
64
  clearTimeout(this.timer);
58
65
  this.timer = null;
59
66
  }
67
+
60
68
  this.pending = null;
61
69
  }
62
70
  }
@@ -33,13 +33,16 @@ function titleOf(text: string): string {
33
33
 
34
34
  function records(head: string): Array<Record<string, unknown>> {
35
35
  const lines = head.split('\n');
36
+
36
37
  // The head ends wherever the byte limit fell, so its last line may not parse.
37
38
  return lines.flatMap((line) => {
38
39
  if (line === '') {
39
40
  return [];
40
41
  }
42
+
41
43
  try {
42
44
  const parsed: unknown = JSON.parse(line);
45
+
43
46
  return isRecord(parsed) ? [parsed] : [];
44
47
  } catch {
45
48
  return [];
@@ -54,11 +57,14 @@ function claudePrompt(entry: Record<string, unknown>): string | null {
54
57
  if (CLAUDE_BOOKKEEPING.has(String(entry.type)) || entry.type !== 'user' || entry.isSidechain === true || entry.isMeta === true) {
55
58
  return null;
56
59
  }
60
+
57
61
  const message = typeof entry.message === 'object' && entry.message !== null ? (entry.message as Record<string, unknown>) : {};
58
62
  const said = spokenText(message.content);
63
+
59
64
  if (said === null || said.trim() === '' || said.startsWith('<command-') || said.startsWith('<local-command-')) {
60
65
  return null;
61
66
  }
67
+
62
68
  return said;
63
69
  }
64
70
 
@@ -71,30 +77,38 @@ export async function listClaudeSessions({ projectsDir, folder, fs }: { projects
71
77
  const dir = join(projectsDir, claudeProjectSlug(folder));
72
78
  const names = (await fs.readdir(dir)).filter((name) => name.endsWith('.jsonl'));
73
79
  const stats = await Promise.all(names.map(async (name) => ({ name, stat: await fs.stat(join(dir, name)) })));
80
+
74
81
  const newest = stats
75
82
  .flatMap(({ name, stat }) => (stat === null ? [] : [{ name, mtimeMs: stat.mtimeMs }]))
76
83
  .sort((left, right) => right.mtimeMs - left.mtimeMs)
77
84
  .slice(0, CLAUDE_MAX_FILES);
85
+
78
86
  const sessions: ExternalSession[] = [];
87
+
79
88
  for (const { name, mtimeMs } of newest) {
80
89
  const prompt = records(await fs.readHead(join(dir, name), HEAD_BYTES))
81
90
  .map(claudePrompt)
82
91
  .find((said) => said !== null);
92
+
83
93
  if (prompt !== undefined && prompt !== null) {
84
94
  sessions.push({ provider: 'claude', sessionId: name.slice(0, -'.jsonl'.length), title: titleOf(prompt), updatedAt: mtimeMs, cwd: folder });
85
95
  }
86
96
  }
97
+
87
98
  return sessions;
88
99
  }
89
100
 
90
101
  function codexPrompt(entry: Record<string, unknown>): string | null {
91
102
  const payload = typeof entry.payload === 'object' && entry.payload !== null ? (entry.payload as Record<string, unknown>) : null;
103
+
92
104
  if (payload === null) {
93
105
  return null;
94
106
  }
107
+
95
108
  if (entry.type === 'event_msg' && payload.type === 'user_message' && typeof payload.message === 'string') {
96
109
  return payload.message.trim() === '' ? null : payload.message;
97
110
  }
111
+
98
112
  return null;
99
113
  }
100
114
 
@@ -105,14 +119,17 @@ function newestFirst(names: string[]): string[] {
105
119
 
106
120
  async function rolloutFiles(sessionsDir: string, fs: SessionFs, max: number): Promise<string[]> {
107
121
  const files: string[] = [];
122
+
108
123
  for (const year of newestFirst(await fs.readdir(sessionsDir))) {
109
124
  for (const month of newestFirst(await fs.readdir(join(sessionsDir, year)))) {
110
125
  for (const day of newestFirst(await fs.readdir(join(sessionsDir, year, month)))) {
111
126
  const names = newestFirst(
112
127
  (await fs.readdir(join(sessionsDir, year, month, day))).filter((name) => name.startsWith('rollout-') && name.endsWith('.jsonl'))
113
128
  );
129
+
114
130
  for (const name of names) {
115
131
  files.push(join(sessionsDir, year, month, day, name));
132
+
116
133
  if (files.length >= max) {
117
134
  return files;
118
135
  }
@@ -120,6 +137,7 @@ async function rolloutFiles(sessionsDir: string, fs: SessionFs, max: number): Pr
120
137
  }
121
138
  }
122
139
  }
140
+
123
141
  return files;
124
142
  }
125
143
 
@@ -139,35 +157,45 @@ export async function listCodexSessions({
139
157
  maxFiles?: number;
140
158
  }): Promise<ExternalSession[]> {
141
159
  const sessions: ExternalSession[] = [];
160
+
142
161
  for (const path of await rolloutFiles(sessionsDir, fs, maxFiles)) {
143
162
  const entries = records(await fs.readHead(path, HEAD_BYTES));
144
163
  const meta = entries[0];
164
+
145
165
  const payload =
146
166
  meta?.type === 'session_meta' && typeof meta.payload === 'object' && meta.payload !== null ? (meta.payload as Record<string, unknown>) : null;
167
+
147
168
  if (payload === null || payload.cwd !== folder || typeof payload.id !== 'string' || isForkOrSubagent(payload)) {
148
169
  continue;
149
170
  }
171
+
150
172
  const prompt = entries.map(codexPrompt).find((said) => said !== null);
173
+
151
174
  if (prompt === undefined || prompt === null) {
152
175
  continue;
153
176
  }
177
+
154
178
  const stat = await fs.stat(path);
155
179
  sessions.push({ provider: 'codex', sessionId: payload.id, title: titleOf(prompt), updatedAt: stat?.mtimeMs ?? 0, cwd: folder });
156
180
  }
181
+
157
182
  return sessions;
158
183
  }
159
184
 
160
185
  /* A `node:fs` behind the seam, for a host that lists the real thing. */
161
186
  export async function nodeSessionFs(): Promise<SessionFs> {
162
187
  const { readdir, stat, open } = await import('node:fs/promises');
188
+
163
189
  return {
164
190
  readdir: async (path) => readdir(path).catch(() => []),
165
191
  stat: async (path) => stat(path).catch(() => null),
166
192
  readHead: async (path, maxBytes) => {
167
193
  const handle = await open(path, 'r');
194
+
168
195
  try {
169
196
  const buffer = Buffer.alloc(maxBytes);
170
197
  const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
198
+
171
199
  return buffer.subarray(0, bytesRead).toString('utf8');
172
200
  } finally {
173
201
  await handle.close();
@@ -16,9 +16,11 @@ function imageInfo(bytes: Buffer): ImageInfo | null {
16
16
  ) {
17
17
  return { mime: 'image/png', extension: 'png', width: bytes.readUInt32BE(16), height: bytes.readUInt32BE(20) };
18
18
  }
19
+
19
20
  if (bytes.length >= 13 && /GIF8[79]a/.test(bytes.toString('ascii', 0, 6))) {
20
21
  return { mime: 'image/gif', extension: 'gif', width: bytes.readUInt16LE(6), height: bytes.readUInt16LE(8) };
21
22
  }
23
+
22
24
  if (
23
25
  bytes.length >= 20 &&
24
26
  bytes.toString('ascii', 0, 4) === 'RIFF' &&
@@ -26,56 +28,77 @@ function imageInfo(bytes: Buffer): ImageInfo | null {
26
28
  bytes.readUInt32LE(4) + 8 === bytes.length
27
29
  ) {
28
30
  const kind = bytes.toString('ascii', 12, 16);
31
+
29
32
  if (kind === 'VP8X' && bytes.length >= 30) {
30
33
  return { mime: 'image/webp', extension: 'webp', width: bytes.readUIntLE(24, 3) + 1, height: bytes.readUIntLE(27, 3) + 1 };
31
34
  }
35
+
32
36
  if (kind === 'VP8 ' && bytes.length >= 30 && bytes.subarray(23, 26).equals(Buffer.from([157, 1, 42]))) {
33
37
  return { mime: 'image/webp', extension: 'webp', width: bytes.readUInt16LE(26) & 0x3fff, height: bytes.readUInt16LE(28) & 0x3fff };
34
38
  }
39
+
35
40
  if (kind === 'VP8L' && bytes.length >= 25 && bytes[20] === 0x2f) {
36
41
  const size = bytes.readUInt32LE(21);
42
+
37
43
  return { mime: 'image/webp', extension: 'webp', width: (size & 0x3fff) + 1, height: ((size >> 14) & 0x3fff) + 1 };
38
44
  }
45
+
39
46
  return null;
40
47
  }
48
+
41
49
  if (bytes.length >= 4 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) {
42
50
  let offset = 2;
51
+
43
52
  while (offset + 4 <= bytes.length && bytes[offset] === 255) {
44
53
  const marker = bytes[offset + 1]!;
54
+
45
55
  if (marker === 218 || marker === 217) {
46
56
  break;
47
57
  }
58
+
48
59
  if (marker === 255) {
49
60
  offset++;
61
+
50
62
  continue;
51
63
  }
64
+
52
65
  const size = bytes.readUInt16BE(offset + 2);
66
+
53
67
  if (size < 2 || offset + 2 + size > bytes.length) {
54
68
  return null;
55
69
  }
70
+
56
71
  if ([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207].includes(marker) && size >= 8) {
57
72
  return { mime: 'image/jpeg', extension: 'jpg', width: bytes.readUInt16BE(offset + 7), height: bytes.readUInt16BE(offset + 5) };
58
73
  }
74
+
59
75
  offset += size + 2;
60
76
  }
61
77
  }
78
+
62
79
  return null;
63
80
  }
64
81
 
65
82
  export function generatedImage(data: string, prompt?: string): { upload: ChatAttachmentUpload; width?: number; height?: number } {
66
83
  const parsed = ChatAttachmentUploadSchema.safeParse({ name: 'image', mime: 'application/octet-stream', data });
84
+
67
85
  if (!parsed.success) {
68
86
  throw new Error('The generated image is empty, invalid base64 or exceeds 10 MiB');
69
87
  }
88
+
70
89
  const bytes = Buffer.from(data, 'base64');
90
+
71
91
  // Buffer's decoder accepts malformed padding and trailing bits; the protocol requires canonical base64.
72
92
  if (bytes.toString('base64') !== data) {
73
93
  throw new Error('The generated image contains invalid base64');
74
94
  }
95
+
75
96
  const info = imageInfo(bytes);
97
+
76
98
  if (info === null || (info.width !== undefined && info.width === 0) || (info.height !== undefined && info.height === 0)) {
77
99
  throw new Error('The generated bytes are not a supported image');
78
100
  }
101
+
79
102
  const stem =
80
103
  (prompt ?? '')
81
104
  .normalize('NFKD')
@@ -83,6 +106,8 @@ export function generatedImage(data: string, prompt?: string): { upload: ChatAtt
83
106
  .replace(/^-|-$/g, '')
84
107
  .slice(0, 80)
85
108
  .toLowerCase() || 'generated-image';
109
+
86
110
  const upload = { name: `${stem}.${info.extension}`, mime: info.mime, data };
111
+
87
112
  return { upload, ...(info.width === undefined ? {} : { width: info.width }), ...(info.height === undefined ? {} : { height: info.height }) };
88
113
  }
package/src/chat/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  // Public API: exports.test.ts snapshots this list, so a name added or dropped here is an API change.
2
2
  export { type AcpAgentSpec, type AcpClientInfo, type AcpUsageReport, createAcpBackend } from './acp-backend.ts';
3
+
3
4
  export type {
4
5
  AcpAgentInfo,
5
6
  AcpModel,
@@ -13,12 +14,14 @@ export type {
13
14
  AcpThoughtLevel,
14
15
  AcpTurnUsage
15
16
  } from './acp-protocol.ts';
17
+
16
18
  export { AttachmentStore } from './attachment-store.ts';
17
19
  export type { BackendLaunch, TurnInput, ApprovalDecision, BackendEvent, BackendHost, ChatBackend } from './backend.ts';
18
20
  export { runningInBackground, type BackgroundWork } from './background-work.ts';
19
21
  export { BookmarkStore, type BookmarkListener } from './bookmark-store.ts';
20
22
  export { type InterruptedRun, type ChatCoreHooks, type ChatCoreOptions, ChatCore } from './chat-core.ts';
21
23
  export { COMPACT_ABOVE_BYTES, type ChatLogLine } from './chat-log.ts';
24
+
22
25
  export {
23
26
  type ChatSpawnOptions,
24
27
  type SpawnChatProcess,
@@ -29,6 +32,7 @@ export {
29
32
  type ChatProcess,
30
33
  type ChatChildOptions
31
34
  } from './chat-process.ts';
35
+
32
36
  export {
33
37
  type PromptNotes,
34
38
  type ChatReferences,
@@ -40,6 +44,7 @@ export {
40
44
  type ChatSendExtras,
41
45
  type ResumeDecision
42
46
  } from './chat-session.ts';
47
+
43
48
  export { type ChatRecordExtras, type ChatRecord, ChatStore, type ChatSeq, type ChatStoreOptions } from './chat-store.ts';
44
49
  export { suggestChatTitle, type ChatTitleInput } from './chat-title.ts';
45
50
  export type { ClaudeBackendOptions } from './claude-backend.ts';
package/src/chat/input.ts CHANGED
@@ -22,27 +22,34 @@ function isTokenEnd(char: string | undefined): boolean {
22
22
  function findSkillToken(text: string, name: string, from: number): number {
23
23
  const token = `$${name}`;
24
24
  let index = text.indexOf(token, from);
25
+
25
26
  while (index >= 0) {
26
27
  if (isBoundary(text[index - 1]) && isTokenEnd(text[index + token.length])) {
27
28
  return index;
28
29
  }
30
+
29
31
  index = text.indexOf(token, index + 1);
30
32
  }
33
+
31
34
  return -1;
32
35
  }
33
36
 
34
37
  /* The last `$name` in the text that names one of the skills, longest name first on a tie. */
35
38
  export function lastSkillToken(text: string, skills: string[]): { index: number; name: string } | null {
36
39
  let best: { index: number; name: string } | null = null;
40
+
37
41
  for (const name of [...new Set(skills)].sort((a, b) => b.length - a.length)) {
38
42
  let index = findSkillToken(text, name, 0);
43
+
39
44
  while (index >= 0) {
40
45
  if (best === null || index > best.index) {
41
46
  best = { index, name };
42
47
  }
48
+
43
49
  index = findSkillToken(text, name, index + 1);
44
50
  }
45
51
  }
52
+
46
53
  return best;
47
54
  }
48
55
 
@@ -55,11 +62,14 @@ export function lastSkillToken(text: string, skills: string[]): { index: number;
55
62
  */
56
63
  export function splitSkillPrompt(text: string, skills: string[]): { lead: string; invocation: string | null } {
57
64
  const last = lastSkillToken(text, skills);
65
+
58
66
  if (last === null) {
59
67
  return { lead: text, invocation: null };
60
68
  }
69
+
61
70
  const lead = text.slice(0, last.index).replace(/\$([A-Za-z][A-Za-z0-9_:-]*)/g, (match, name: string) => (skills.includes(name) ? `/${name}` : match));
62
71
  const rest = text.slice(last.index + last.name.length + 1).replace(/^[ \t]+/, '');
72
+
63
73
  return { lead: lead.trimEnd(), invocation: rest === '' ? `/${last.name}` : `/${last.name} ${rest}` };
64
74
  }
65
75
 
@@ -79,11 +89,14 @@ export function buildUserMessage({ text, attachments = [], prefix = '', skills =
79
89
  const note = attachmentNote(attachments);
80
90
  // The prefix must be the first thing the model reads, so it joins the leading block.
81
91
  const prompt = [`${prefix}${lead}`.trimEnd(), note].filter((part) => part.trim() !== '').join('\n\n');
92
+
82
93
  if (prompt !== '') {
83
94
  content.push({ type: 'text', text: prompt });
84
95
  }
96
+
85
97
  if (invocation !== null) {
86
98
  content.push({ type: 'text', text: invocation });
87
99
  }
100
+
88
101
  return { type: 'user', message: { role: 'user', content }, parent_tool_use_id: null, session_id: '', ...(promptId ? { uuid: promptId } : {}) };
89
102
  }