@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.
- package/dist/async.d.ts +5 -0
- package/dist/async.js +13 -5
- package/dist/chat/acp-protocol.js +3 -5
- package/dist/chat/chat-core.d.ts +1 -0
- package/dist/chat/chat-core.js +9 -2
- package/dist/chat/chat-process.d.ts +1 -0
- package/dist/chat/chat-process.js +1 -1
- package/dist/chat/chat-session.d.ts +2 -0
- package/dist/chat/chat-session.js +9 -10
- package/dist/chat/claude-protocol.js +1 -6
- package/dist/chat/claude-transcript.js +1 -3
- package/dist/chat/codex-protocol.js +1 -6
- package/dist/chat/projector.js +1 -3
- package/dist/chat/request-summary.js +1 -3
- package/dist/coded-error.js +1 -5
- package/dist/context/argv.js +1 -2
- package/dist/context/verb.js +1 -3
- package/dist/error-text.d.ts +1 -0
- package/dist/error-text.js +4 -0
- package/dist/fields.d.ts +3 -0
- package/dist/fields.js +12 -0
- package/dist/fs.d.ts +1 -0
- package/dist/fs.js +8 -4
- package/dist/lineage.js +11 -5
- package/dist/messages/notice-store.js +3 -4
- package/dist/providers/accounts/acp-account.js +3 -20
- package/dist/providers/accounts/service.d.ts +1 -2
- package/dist/providers/accounts/service.js +5 -9
- package/dist/providers/accounts/status.js +1 -3
- package/dist/providers/acp-agent-state.js +1 -3
- package/dist/providers/claude.js +0 -1
- package/dist/providers/codex.js +0 -1
- package/dist/providers/detect.js +1 -1
- package/dist/providers/shell-commands.js +1 -2
- package/dist/run-process.js +1 -3
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +5 -8
- package/dist/tasks/background-limit.js +3 -2
- package/dist/tasks/task-coordinator.js +11 -17
- package/dist/testing/watch.js +0 -1
- package/dist/usage/limits/monitor.js +1 -2
- package/dist/usage/readers/claude.js +3 -10
- package/dist/usage/readers/codex.js +2 -9
- package/dist/usage/readers/grok.js +3 -10
- package/dist/usage/readers/host-log.js +3 -10
- package/dist/usage/record.d.ts +2 -0
- package/dist/usage/record.js +13 -0
- package/package.json +6 -6
- package/src/async.ts +23 -5
- package/src/chat/acp-backend.ts +121 -0
- package/src/chat/acp-protocol.ts +172 -6
- package/src/chat/attachment-store.ts +21 -0
- package/src/chat/backend.ts +0 -2
- package/src/chat/bookmark-store.ts +23 -0
- package/src/chat/chat-core.ts +218 -7
- package/src/chat/chat-log.ts +19 -0
- package/src/chat/chat-process.ts +52 -1
- package/src/chat/chat-session.ts +414 -13
- package/src/chat/chat-store.ts +50 -0
- package/src/chat/chat-title.ts +9 -0
- package/src/chat/claude-backend.ts +50 -0
- package/src/chat/claude-protocol.ts +155 -8
- package/src/chat/claude-title.ts +10 -0
- package/src/chat/claude-transcript.ts +47 -4
- package/src/chat/codex-backend.ts +131 -0
- package/src/chat/codex-protocol.ts +180 -8
- package/src/chat/codex-thread.ts +23 -0
- package/src/chat/composer-preferences.ts +6 -0
- package/src/chat/context-breakdown.ts +11 -0
- package/src/chat/delta-coalescer.ts +8 -0
- package/src/chat/external-sessions.ts +28 -0
- package/src/chat/generated-image.ts +25 -0
- package/src/chat/index.ts +5 -0
- package/src/chat/input.ts +13 -0
- package/src/chat/json-rpc-transport.ts +28 -0
- package/src/chat/limit-resume.ts +13 -0
- package/src/chat/projector.ts +160 -4
- package/src/chat/readable-text.ts +3 -0
- package/src/chat/request-summary.ts +18 -4
- package/src/chat/subagent-projection.ts +3 -0
- package/src/chat/subagent-reader.ts +76 -0
- package/src/chat/subagent-settlement.ts +24 -0
- package/src/chat/thread-search.ts +13 -0
- package/src/chat/thread.ts +31 -0
- package/src/chat/ui-queries.ts +122 -0
- package/src/chat/ui-replies.ts +34 -0
- package/src/chat/visual-store.ts +49 -0
- package/src/chat/wake-chat.ts +3 -0
- package/src/client-sinks.ts +2 -0
- package/src/coded-error.ts +1 -5
- package/src/context/argv.ts +18 -2
- package/src/context/index.ts +1 -0
- package/src/context/refusal.ts +3 -0
- package/src/context/verb.ts +32 -4
- package/src/coordination/index.ts +10 -0
- package/src/error-text.ts +7 -0
- package/src/fields.ts +14 -0
- package/src/fs.ts +20 -4
- package/src/host/agent-host.ts +20 -0
- package/src/host/environment.ts +6 -0
- package/src/host/handlers.ts +18 -0
- package/src/host/memory-port.ts +4 -0
- package/src/host/wiring.ts +10 -0
- package/src/index.ts +1 -0
- package/src/lineage.ts +24 -5
- package/src/messages/deliver-message.ts +6 -0
- package/src/messages/deliver-notice.ts +13 -0
- package/src/messages/notice-store.ts +16 -4
- package/src/modes.ts +1 -0
- package/src/outbox/outbox-worker.ts +31 -0
- package/src/outbox/outbox.ts +5 -0
- package/src/providers/accounts/accounts.ts +36 -0
- package/src/providers/accounts/acp-account.ts +22 -27
- package/src/providers/accounts/launch.ts +4 -0
- package/src/providers/accounts/service.ts +110 -12
- package/src/providers/accounts/shadow-home.ts +10 -0
- package/src/providers/accounts/status.ts +13 -4
- package/src/providers/accounts/store.ts +25 -0
- package/src/providers/accounts/variables.ts +11 -1
- package/src/providers/acp-agent-state.ts +24 -3
- package/src/providers/acp-provider.ts +5 -0
- package/src/providers/acp-usage-log.ts +2 -0
- package/src/providers/catalog.ts +17 -0
- package/src/providers/claude.ts +10 -1
- package/src/providers/codex-rules.ts +3 -0
- package/src/providers/codex.ts +2 -1
- package/src/providers/detect.ts +6 -1
- package/src/providers/provider.ts +1 -1
- package/src/providers/registry.ts +18 -2
- package/src/providers/shell-commands.ts +37 -2
- package/src/record-directory.ts +19 -0
- package/src/run-process.ts +24 -3
- package/src/serializer.ts +3 -0
- package/src/skills.ts +73 -10
- package/src/tasks/background-limit.ts +9 -2
- package/src/tasks/end-children.ts +9 -0
- package/src/tasks/give-task.ts +8 -0
- package/src/tasks/task-coordinator.ts +81 -17
- package/src/tasks/task-store.ts +28 -0
- package/src/tasks/waiting-child.ts +21 -0
- package/src/tasks/wake-parent.ts +26 -0
- package/src/tasks/wiring.ts +22 -0
- package/src/testing/accounts.ts +2 -0
- package/src/testing/fake-acp-capture.ts +16 -0
- package/src/testing/fake-acp.ts +66 -0
- package/src/testing/fake-claude.ts +177 -0
- package/src/testing/fake-cli.ts +29 -0
- package/src/testing/fake-codex.ts +122 -0
- package/src/testing/manual-clock.ts +2 -0
- package/src/testing/watch.ts +7 -1
- package/src/title-file.ts +12 -0
- package/src/usage/accounts.ts +6 -0
- package/src/usage/aggregate.ts +19 -0
- package/src/usage/exchange.ts +12 -0
- package/src/usage/index-file.ts +27 -0
- package/src/usage/limits/monitor.ts +31 -3
- package/src/usage/limits/normalize.ts +30 -0
- package/src/usage/limits/probe.ts +17 -0
- package/src/usage/pricing.ts +35 -0
- package/src/usage/projects.ts +8 -0
- package/src/usage/readers/claude.ts +12 -9
- package/src/usage/readers/codex.ts +40 -8
- package/src/usage/readers/grok.ts +17 -9
- package/src/usage/readers/host-log.ts +9 -9
- package/src/usage/record.ts +22 -0
- package/src/usage/roots.ts +6 -0
- package/src/usage/scanner.ts +61 -0
- package/src/usage/usage-service.ts +14 -0
- package/src/watch-seam.ts +8 -0
- package/src/windows-command.ts +12 -0
package/src/chat/ui-replies.ts
CHANGED
|
@@ -50,18 +50,22 @@ export class UiReplies {
|
|
|
50
50
|
/* The preview of every reply still streaming, as deltas, for a client that attached in the middle of one. */
|
|
51
51
|
previews(): ChatEvent[] {
|
|
52
52
|
const events: ChatEvent[] = [];
|
|
53
|
+
|
|
53
54
|
for (const [itemId, preview] of this.compiled) {
|
|
54
55
|
const item = this.options.thread.get(itemId);
|
|
56
|
+
|
|
55
57
|
if (item?.kind === 'assistant' && item.streaming && item.text.length === preview.textLength) {
|
|
56
58
|
events.push({ type: 'delta', itemId, text: '', ui: preview.blocks, textLength: preview.textLength });
|
|
57
59
|
}
|
|
58
60
|
}
|
|
61
|
+
|
|
59
62
|
return events;
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
previewItems(items: ChatItem[]): ChatItem[] {
|
|
63
66
|
return items.map((item) => {
|
|
64
67
|
const preview = this.compiled.get(item.id);
|
|
68
|
+
|
|
65
69
|
return item.kind === 'assistant' && preview?.textLength === item.text.length ? { ...item, ui: preview.blocks } : item;
|
|
66
70
|
});
|
|
67
71
|
}
|
|
@@ -69,27 +73,38 @@ export class UiReplies {
|
|
|
69
73
|
/* Hands one event of the chat on, with the blocks of a reply it finished in place of its fences. */
|
|
70
74
|
pass(event: ChatEvent): void {
|
|
71
75
|
const { thread, emit } = this.options;
|
|
76
|
+
|
|
72
77
|
if (event.type === 'reset') {
|
|
73
78
|
this.clear();
|
|
74
79
|
}
|
|
80
|
+
|
|
75
81
|
if (event.type === 'item' && event.item.kind === 'tool') {
|
|
76
82
|
this.attachments.delete(event.item.turnId);
|
|
77
83
|
}
|
|
84
|
+
|
|
78
85
|
const item = event.type === 'item' ? event.item : event.type === 'delta' ? thread.get(event.itemId) : null;
|
|
86
|
+
|
|
79
87
|
if (item?.kind !== 'assistant' || item.parentToolUseId) {
|
|
80
88
|
emit(event);
|
|
89
|
+
|
|
81
90
|
return;
|
|
82
91
|
}
|
|
92
|
+
|
|
83
93
|
const tail = this.unscannedText(item, event.type === 'item');
|
|
94
|
+
|
|
84
95
|
if (uiMayReferenceHost(tail)) {
|
|
85
96
|
this.referencing.add(item.id);
|
|
86
97
|
}
|
|
98
|
+
|
|
87
99
|
let stream = this.streams.get(item.id);
|
|
100
|
+
|
|
88
101
|
if (!stream && !uiHasFence(tail, this.options.fenceLanguage)) {
|
|
89
102
|
this.forgetScan(item);
|
|
90
103
|
emit(event);
|
|
104
|
+
|
|
91
105
|
return;
|
|
92
106
|
}
|
|
107
|
+
|
|
93
108
|
if (!stream) {
|
|
94
109
|
stream = new UiStream({
|
|
95
110
|
querySchemas: this.options.host?.schemas,
|
|
@@ -101,21 +116,28 @@ export class UiReplies {
|
|
|
101
116
|
emit({ type: 'delta', itemId: item.id, text: '', ui: preview.blocks, textLength: preview.textLength });
|
|
102
117
|
}
|
|
103
118
|
});
|
|
119
|
+
|
|
104
120
|
this.streams.set(item.id, stream);
|
|
105
121
|
}
|
|
122
|
+
|
|
106
123
|
if (this.referencing.has(item.id)) {
|
|
107
124
|
this.options.host?.observe(item, false);
|
|
108
125
|
}
|
|
126
|
+
|
|
109
127
|
this.forgetScan(item);
|
|
110
128
|
const latestAttachment = this.latestAttachment(item.turnId);
|
|
129
|
+
|
|
111
130
|
if (event.type !== 'item' || item.streaming) {
|
|
112
131
|
emit(event);
|
|
113
132
|
stream.update(item.text, latestAttachment);
|
|
133
|
+
|
|
114
134
|
return;
|
|
115
135
|
}
|
|
136
|
+
|
|
116
137
|
const ui = stream
|
|
117
138
|
.finish(item.text, latestAttachment)
|
|
118
139
|
.map((block) => ({ ...block, revision: createHash('sha256').update(JSON.stringify(block)).digest('hex') }));
|
|
140
|
+
|
|
119
141
|
this.streams.delete(item.id);
|
|
120
142
|
this.compiled.delete(item.id);
|
|
121
143
|
this.repair(ui.flatMap((block) => block.diagnostics));
|
|
@@ -128,6 +150,7 @@ export class UiReplies {
|
|
|
128
150
|
private repair(diagnostics: UiDiagnostic[]): void {
|
|
129
151
|
const say = (found: UiDiagnostic[], audience: 'person' | 'agent') => {
|
|
130
152
|
const messages = [...new Set(found.map((diagnostic) => diagnostic.message))].slice(0, 6);
|
|
153
|
+
|
|
131
154
|
if (messages.length > 0) {
|
|
132
155
|
this.options.repair(
|
|
133
156
|
`Your last ${this.options.fenceLanguage} block had these diagnostics: ${messages.join(' ')} Use the catalog's supported props and components.`.slice(
|
|
@@ -138,10 +161,12 @@ export class UiReplies {
|
|
|
138
161
|
);
|
|
139
162
|
}
|
|
140
163
|
};
|
|
164
|
+
|
|
141
165
|
say(
|
|
142
166
|
diagnostics.filter((diagnostic) => diagnostic.severity !== 'warning'),
|
|
143
167
|
'person'
|
|
144
168
|
);
|
|
169
|
+
|
|
145
170
|
say(
|
|
146
171
|
diagnostics.filter((diagnostic) => diagnostic.severity === 'warning'),
|
|
147
172
|
'agent'
|
|
@@ -152,6 +177,7 @@ export class UiReplies {
|
|
|
152
177
|
for (const stream of this.streams.values()) {
|
|
153
178
|
stream.dispose();
|
|
154
179
|
}
|
|
180
|
+
|
|
155
181
|
this.streams.clear();
|
|
156
182
|
this.compiled.clear();
|
|
157
183
|
this.scanned.clear();
|
|
@@ -166,11 +192,14 @@ export class UiReplies {
|
|
|
166
192
|
private unscannedText(item: ChatAssistantItem, whole: boolean): string {
|
|
167
193
|
const scanned = whole ? 0 : (this.scanned.get(item.id) ?? 0);
|
|
168
194
|
this.scanned.set(item.id, item.text.length);
|
|
195
|
+
|
|
169
196
|
if (scanned === 0 || scanned > item.text.length) {
|
|
170
197
|
return item.text;
|
|
171
198
|
}
|
|
199
|
+
|
|
172
200
|
const line = item.text.lastIndexOf('\n', scanned - 1) + 1;
|
|
173
201
|
const from = Math.max(line, scanned - UI_SCAN_OVERLAP);
|
|
202
|
+
|
|
174
203
|
return from === line ? item.text.slice(from) : `\0${item.text.slice(from)}`;
|
|
175
204
|
}
|
|
176
205
|
|
|
@@ -186,8 +215,10 @@ export class UiReplies {
|
|
|
186
215
|
if (this.attachments.has(turnId)) {
|
|
187
216
|
return this.attachments.get(turnId);
|
|
188
217
|
}
|
|
218
|
+
|
|
189
219
|
const attachment = this.findAttachment(turnId);
|
|
190
220
|
this.attachments.set(turnId, attachment);
|
|
221
|
+
|
|
191
222
|
return attachment;
|
|
192
223
|
}
|
|
193
224
|
|
|
@@ -196,11 +227,14 @@ export class UiReplies {
|
|
|
196
227
|
if (item.kind !== 'tool' || item.turnId !== turnId || item.name !== 'ImageGeneration' || item.state !== 'done') {
|
|
197
228
|
continue;
|
|
198
229
|
}
|
|
230
|
+
|
|
199
231
|
const input = GeneratedImageInputSchema.safeParse(item.input);
|
|
232
|
+
|
|
200
233
|
if (input.success && input.data.attachment?.mime.startsWith('image/')) {
|
|
201
234
|
return input.data.attachment.id;
|
|
202
235
|
}
|
|
203
236
|
}
|
|
237
|
+
|
|
204
238
|
return undefined;
|
|
205
239
|
}
|
|
206
240
|
}
|
package/src/chat/visual-store.ts
CHANGED
|
@@ -72,26 +72,34 @@ function checked(input: VisualInput): {
|
|
|
72
72
|
heights: VisualHeight[] | undefined;
|
|
73
73
|
} {
|
|
74
74
|
const title = typeof input.title === 'string' ? input.title.trim() : '';
|
|
75
|
+
|
|
75
76
|
if (title === '') {
|
|
76
77
|
refuse('visual-invalid', 'A visual needs a title; give it a short one that says what the page shows');
|
|
77
78
|
}
|
|
79
|
+
|
|
78
80
|
if (title.length > VISUAL_LIMITS.title) {
|
|
79
81
|
refuse('visual-invalid', `The title has ${title.length} characters; keep it to ${VISUAL_LIMITS.title} or fewer`);
|
|
80
82
|
}
|
|
83
|
+
|
|
81
84
|
if (typeof input.html !== 'string' || input.html.trim() === '') {
|
|
82
85
|
refuse('visual-invalid', 'The page is empty; pass one self-contained HTML document');
|
|
83
86
|
}
|
|
87
|
+
|
|
84
88
|
const maxHeight = input.maxHeight ?? VISUAL_LIMITS.maxHeight;
|
|
89
|
+
|
|
85
90
|
if (!isSize(maxHeight) || maxHeight < VISUAL_LIMITS.minHeight || maxHeight > VISUAL_LIMITS.maxHeight) {
|
|
86
91
|
refuse(
|
|
87
92
|
'visual-invalid',
|
|
88
93
|
`The maximum height must be from ${VISUAL_LIMITS.minHeight} to ${VISUAL_LIMITS.maxHeight} CSS pixels; leave it out for ${VISUAL_LIMITS.maxHeight}`
|
|
89
94
|
);
|
|
90
95
|
}
|
|
96
|
+
|
|
91
97
|
if (input.layout !== undefined && !VisualLayoutSchema.safeParse(input.layout).success) {
|
|
92
98
|
refuse('visual-invalid', 'The layout must be inline or wide; leave it out for the reply column');
|
|
93
99
|
}
|
|
100
|
+
|
|
94
101
|
const heights = input.heights ?? [];
|
|
102
|
+
|
|
95
103
|
if (
|
|
96
104
|
!Array.isArray(heights) ||
|
|
97
105
|
heights.length > VISUAL_LIMITS.heights ||
|
|
@@ -99,9 +107,11 @@ function checked(input: VisualInput): {
|
|
|
99
107
|
) {
|
|
100
108
|
refuse('visual-invalid', `Measured heights are at most ${VISUAL_LIMITS.heights} pairs of [width, height] in CSS pixels, with a positive width`);
|
|
101
109
|
}
|
|
110
|
+
|
|
102
111
|
// One measurement per width, the last one given, ascending as `visualFrameHeight` reads them.
|
|
103
112
|
const byWidth = new Map(heights.map((pair): [number, number] => [Math.max(1, Math.round(pair[0])), Math.ceil(pair[1])]));
|
|
104
113
|
const sorted = [...byWidth].sort((a, b) => a[0] - b[0]);
|
|
114
|
+
|
|
105
115
|
return { title, html: input.html, maxHeight: Math.round(maxHeight), layout: input.layout, heights: sorted.length === 0 ? undefined : sorted };
|
|
106
116
|
}
|
|
107
117
|
|
|
@@ -115,6 +125,7 @@ function tooLarge(bytes: number): never {
|
|
|
115
125
|
|
|
116
126
|
function refuseOversized(html: string): void {
|
|
117
127
|
const bytes = Buffer.byteLength(html, 'utf8');
|
|
128
|
+
|
|
118
129
|
if (bytes > VISUAL_LIMITS.bytes) {
|
|
119
130
|
tooLarge(bytes);
|
|
120
131
|
}
|
|
@@ -143,6 +154,7 @@ export class VisualStore {
|
|
|
143
154
|
|
|
144
155
|
listen(listener: VisualListener): () => void {
|
|
145
156
|
this.listeners.add(listener);
|
|
157
|
+
|
|
146
158
|
return () => {
|
|
147
159
|
this.listeners.delete(listener);
|
|
148
160
|
};
|
|
@@ -160,6 +172,7 @@ export class VisualStore {
|
|
|
160
172
|
async prepareWorkspace(chatId: string): Promise<string> {
|
|
161
173
|
const path = this.workspacePath(chatId);
|
|
162
174
|
await mkdir(path, { recursive: true, mode: 0o700 });
|
|
175
|
+
|
|
163
176
|
return path;
|
|
164
177
|
}
|
|
165
178
|
|
|
@@ -168,10 +181,13 @@ export class VisualStore {
|
|
|
168
181
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,119}\.html$/.test(name)) {
|
|
169
182
|
refuse('visual-invalid', 'Use an HTML filename such as chart.html, with letters, numbers, dots, hyphens or underscores and no directory');
|
|
170
183
|
}
|
|
184
|
+
|
|
171
185
|
if (html.trim() === '') {
|
|
172
186
|
refuse('visual-invalid', 'The page is empty; pass one self-contained HTML document');
|
|
173
187
|
}
|
|
188
|
+
|
|
174
189
|
refuseOversized(html);
|
|
190
|
+
|
|
175
191
|
return this.writeWorkspaceFile(chatId, name, html);
|
|
176
192
|
}
|
|
177
193
|
|
|
@@ -185,12 +201,16 @@ export class VisualStore {
|
|
|
185
201
|
// Before the bootstrap is added, so a page far too large is never copied first.
|
|
186
202
|
refuseOversized(html);
|
|
187
203
|
const page = Buffer.from(injectVisualBootstrap(html), 'utf8');
|
|
204
|
+
|
|
188
205
|
if (page.byteLength > VISUAL_LIMITS.bytes) {
|
|
189
206
|
tooLarge(page.byteLength);
|
|
190
207
|
}
|
|
208
|
+
|
|
191
209
|
const turnId = typeof input.turnId === 'string' && input.turnId !== '' ? input.turnId : undefined;
|
|
210
|
+
|
|
192
211
|
return this.writes.run(chatId, async () => {
|
|
193
212
|
const visuals = await this.readFile(chatId);
|
|
213
|
+
|
|
194
214
|
const visual: ChatVisual = {
|
|
195
215
|
id: randomBytes(8).toString('hex'),
|
|
196
216
|
title,
|
|
@@ -201,16 +221,21 @@ export class VisualStore {
|
|
|
201
221
|
size: page.byteLength,
|
|
202
222
|
...(turnId === undefined ? {} : { turnId })
|
|
203
223
|
};
|
|
224
|
+
|
|
204
225
|
await mkdir(this.attachments.folderOf(chatId), { recursive: true, mode: 0o700 });
|
|
205
226
|
await writeAtomic(this.pagePath(chatId, visual.id), page);
|
|
206
227
|
const next = [...visuals, visual];
|
|
228
|
+
|
|
207
229
|
try {
|
|
208
230
|
await this.writeFile(chatId, next);
|
|
209
231
|
} catch (e) {
|
|
210
232
|
await rm(this.pagePath(chatId, visual.id), { force: true });
|
|
233
|
+
|
|
211
234
|
throw e;
|
|
212
235
|
}
|
|
236
|
+
|
|
213
237
|
this.tell(chatId, next);
|
|
238
|
+
|
|
214
239
|
return visual;
|
|
215
240
|
});
|
|
216
241
|
}
|
|
@@ -219,13 +244,16 @@ export class VisualStore {
|
|
|
219
244
|
remove(chatId: string, visualId: string): Promise<ChatVisual[]> {
|
|
220
245
|
return this.writes.run(chatId, async () => {
|
|
221
246
|
const visuals = await this.readFile(chatId);
|
|
247
|
+
|
|
222
248
|
if (!visuals.some((visual) => visual.id === visualId)) {
|
|
223
249
|
return visuals;
|
|
224
250
|
}
|
|
251
|
+
|
|
225
252
|
const next = visuals.filter((visual) => visual.id !== visualId);
|
|
226
253
|
await this.writeFile(chatId, next);
|
|
227
254
|
await rm(this.pagePath(chatId, visualId), { force: true });
|
|
228
255
|
this.tell(chatId, next);
|
|
256
|
+
|
|
229
257
|
return next;
|
|
230
258
|
});
|
|
231
259
|
}
|
|
@@ -238,6 +266,7 @@ export class VisualStore {
|
|
|
238
266
|
await rm(this.workspacePath(chatId), { recursive: true, force: true });
|
|
239
267
|
await Promise.all((visuals ?? []).map((visual) => rm(this.pagePath(chatId, visual.id), { force: true })));
|
|
240
268
|
this.known.delete(chatId);
|
|
269
|
+
|
|
241
270
|
if (visuals === null || visuals.length > 0) {
|
|
242
271
|
this.tell(chatId, []);
|
|
243
272
|
}
|
|
@@ -250,29 +279,36 @@ export class VisualStore {
|
|
|
250
279
|
*/
|
|
251
280
|
async copyChat(fromChatId: string, toChatId: string, keep: (visual: ChatVisual) => boolean = () => true): Promise<ChatVisual[]> {
|
|
252
281
|
const kept = (await this.list(fromChatId)).filter(keep);
|
|
282
|
+
|
|
253
283
|
if (kept.length === 0) {
|
|
254
284
|
return this.list(toChatId);
|
|
255
285
|
}
|
|
286
|
+
|
|
256
287
|
return this.writes.run(toChatId, async () => {
|
|
257
288
|
const copied: ChatVisual[] = [];
|
|
258
289
|
await mkdir(this.attachments.folderOf(toChatId), { recursive: true, mode: 0o700 });
|
|
290
|
+
|
|
259
291
|
for (const visual of kept) {
|
|
260
292
|
const page = await readFile(this.pagePath(fromChatId, visual.id)).catch((e: unknown) => {
|
|
261
293
|
if (isNotFound(e)) {
|
|
262
294
|
return null;
|
|
263
295
|
}
|
|
296
|
+
|
|
264
297
|
throw e;
|
|
265
298
|
});
|
|
299
|
+
|
|
266
300
|
if (page !== null) {
|
|
267
301
|
await writeAtomic(this.pagePath(toChatId, visual.id), page);
|
|
268
302
|
copied.push(visual);
|
|
269
303
|
}
|
|
270
304
|
}
|
|
305
|
+
|
|
271
306
|
const existing = await this.readFile(toChatId);
|
|
272
307
|
const ids = new Set(existing.map((visual) => visual.id));
|
|
273
308
|
const next = [...existing, ...copied.filter((visual) => !ids.has(visual.id))];
|
|
274
309
|
await this.writeFile(toChatId, next);
|
|
275
310
|
this.tell(toChatId, next);
|
|
311
|
+
|
|
276
312
|
return next;
|
|
277
313
|
});
|
|
278
314
|
}
|
|
@@ -280,9 +316,11 @@ export class VisualStore {
|
|
|
280
316
|
/* A page as an attachment of its chat, from the lists this store has seen; null for one it does not know. */
|
|
281
317
|
attachment(chatId: string, visualId: string): ChatAttachment | null {
|
|
282
318
|
const visual = this.known.get(chatId)?.find((candidate) => candidate.id === visualId);
|
|
319
|
+
|
|
283
320
|
if (!visual) {
|
|
284
321
|
return null;
|
|
285
322
|
}
|
|
323
|
+
|
|
286
324
|
return { id: visual.id, name: `${visual.title}.html`, mime: 'text/html', size: visual.size, path: this.pagePath(chatId, visual.id) };
|
|
287
325
|
}
|
|
288
326
|
|
|
@@ -294,6 +332,7 @@ export class VisualStore {
|
|
|
294
332
|
return this.writes.run(chatId, async () => {
|
|
295
333
|
const path = join(await this.prepareWorkspace(chatId), name);
|
|
296
334
|
await writeAtomic(path, content);
|
|
335
|
+
|
|
297
336
|
return path;
|
|
298
337
|
});
|
|
299
338
|
}
|
|
@@ -306,31 +345,41 @@ export class VisualStore {
|
|
|
306
345
|
|
|
307
346
|
private async readFile(chatId: string): Promise<ChatVisual[]> {
|
|
308
347
|
let raw: string;
|
|
348
|
+
|
|
309
349
|
try {
|
|
310
350
|
raw = await readFile(join(this.dir, visualFileName(chatId)), 'utf8');
|
|
311
351
|
} catch (e) {
|
|
312
352
|
if (isNotFound(e)) {
|
|
313
353
|
this.known.delete(chatId);
|
|
354
|
+
|
|
314
355
|
return [];
|
|
315
356
|
}
|
|
357
|
+
|
|
316
358
|
throw e;
|
|
317
359
|
}
|
|
360
|
+
|
|
318
361
|
// A file that does not parse is refused rather than read as empty, which the next write would make true.
|
|
319
362
|
const parsed = VisualFileSchema.safeParse(JSON.parse(raw));
|
|
363
|
+
|
|
320
364
|
if (!parsed.success) {
|
|
321
365
|
throw new Error(`The visuals file of chat ${chatId} is not valid: ${parsed.error.issues[0]?.message ?? 'unknown problem'}`);
|
|
322
366
|
}
|
|
367
|
+
|
|
323
368
|
this.known.set(chatId, parsed.data.visuals);
|
|
369
|
+
|
|
324
370
|
return parsed.data.visuals;
|
|
325
371
|
}
|
|
326
372
|
|
|
327
373
|
private async writeFile(chatId: string, visuals: readonly ChatVisual[]): Promise<void> {
|
|
328
374
|
const path = join(this.dir, visualFileName(chatId));
|
|
375
|
+
|
|
329
376
|
if (visuals.length === 0) {
|
|
330
377
|
await rm(path, { force: true });
|
|
331
378
|
this.known.delete(chatId);
|
|
379
|
+
|
|
332
380
|
return;
|
|
333
381
|
}
|
|
382
|
+
|
|
334
383
|
await mkdir(this.dir, { recursive: true, mode: 0o700 });
|
|
335
384
|
await writeAtomic(path, JSON.stringify({ version: 1, visuals }));
|
|
336
385
|
this.known.set(chatId, [...visuals]);
|
package/src/chat/wake-chat.ts
CHANGED
|
@@ -29,8 +29,10 @@ export async function loadChat(deps: ChatOpenerDeps, chatId: string): Promise<Ch
|
|
|
29
29
|
if (!deps.placed(chatId) || !(await deps.chats.hasStored(chatId))) {
|
|
30
30
|
return null;
|
|
31
31
|
}
|
|
32
|
+
|
|
32
33
|
await deps.chats.create({ chatId });
|
|
33
34
|
}
|
|
35
|
+
|
|
34
36
|
return deps.chats.get(chatId) ?? null;
|
|
35
37
|
}
|
|
36
38
|
|
|
@@ -38,6 +40,7 @@ export async function loadChat(deps: ChatOpenerDeps, chatId: string): Promise<Ch
|
|
|
38
40
|
export function chatOpener(deps: ChatOpenerDeps) {
|
|
39
41
|
return async (chatId: string): Promise<WakeChat | null> => {
|
|
40
42
|
const session = await loadChat(deps, chatId);
|
|
43
|
+
|
|
41
44
|
return session
|
|
42
45
|
? {
|
|
43
46
|
items: () => session.thread.list(),
|
package/src/client-sinks.ts
CHANGED
|
@@ -13,11 +13,13 @@ export class ClientSinks<TEvent> {
|
|
|
13
13
|
|
|
14
14
|
subscribe(clientId: string, sink: (event: TEvent) => void): () => void {
|
|
15
15
|
this.sinks.set(clientId, sink);
|
|
16
|
+
|
|
16
17
|
return () => {
|
|
17
18
|
// A client that subscribes again before it unsubscribes keeps its newer sink.
|
|
18
19
|
if (this.sinks.get(clientId) !== sink) {
|
|
19
20
|
return;
|
|
20
21
|
}
|
|
22
|
+
|
|
21
23
|
this.sinks.delete(clientId);
|
|
22
24
|
this.onRelease?.(clientId);
|
|
23
25
|
};
|
package/src/coded-error.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* The shape every failure on the wire has: a code a client can branch on and a message a person reads.
|
|
3
|
-
* A domain gives it a union of its own codes, so a typo in a code is a compile error instead of a
|
|
4
|
-
* string the other end never matches.
|
|
5
|
-
*/
|
|
1
|
+
/* The shape every failure on the wire has: a code a client can branch on and a message a person reads. */
|
|
6
2
|
export class CodedError<TCode extends string = string> extends Error {
|
|
7
3
|
readonly code: TCode;
|
|
8
4
|
/* What the actor can do instead. Only a refusal fills this; the rest carry an empty list. */
|
package/src/context/argv.ts
CHANGED
|
@@ -6,22 +6,26 @@ export type ParsedArgv =
|
|
|
6
6
|
* Splits the words after a verb into positionals, `--flag value` pairs (`--flag=value` too, for a
|
|
7
7
|
* value that itself starts with `--`) and the switches the verb declared. Anything not declared a
|
|
8
8
|
* switch takes a value, so a flag that lost its argument is refused instead of swallowing the next
|
|
9
|
-
* word
|
|
10
|
-
* both a switch and a flag is on when written bare and takes a value only after `=` (`--state=full`).
|
|
9
|
+
* word. A name that is both a switch and a flag takes a value only after `=` (`--state=full`).
|
|
11
10
|
*/
|
|
12
11
|
export function parseArgv(argv: readonly string[], known: readonly string[], switches: readonly string[] = []): ParsedArgv {
|
|
13
12
|
const positionals: string[] = [];
|
|
14
13
|
const flags: Record<string, string> = {};
|
|
15
14
|
const given = new Set<string>();
|
|
16
15
|
const all = [...new Set([...known, ...switches])];
|
|
16
|
+
|
|
17
17
|
for (let i = 0; i < argv.length; i++) {
|
|
18
18
|
const word = argv[i]!;
|
|
19
|
+
|
|
19
20
|
if (!word.startsWith('--')) {
|
|
20
21
|
positionals.push(word);
|
|
22
|
+
|
|
21
23
|
continue;
|
|
22
24
|
}
|
|
25
|
+
|
|
23
26
|
const equals = word.indexOf('=');
|
|
24
27
|
const name = word.slice(2, equals === -1 ? undefined : equals);
|
|
28
|
+
|
|
25
29
|
if (!all.includes(name)) {
|
|
26
30
|
return {
|
|
27
31
|
ok: false,
|
|
@@ -29,33 +33,45 @@ export function parseArgv(argv: readonly string[], known: readonly string[], swi
|
|
|
29
33
|
message: all.length === 0 ? `--${name} is not a flag here; this verb takes none` : `--${name} is not one of --${all.join(', --')}`
|
|
30
34
|
};
|
|
31
35
|
}
|
|
36
|
+
|
|
32
37
|
if (Object.hasOwn(flags, name) || given.has(name)) {
|
|
33
38
|
return { ok: false, code: 'duplicate-flag', message: `--${name} is given twice` };
|
|
34
39
|
}
|
|
40
|
+
|
|
35
41
|
if (switches.includes(name)) {
|
|
36
42
|
if (equals !== -1 && known.includes(name)) {
|
|
37
43
|
flags[name] = word.slice(equals + 1);
|
|
38
44
|
given.add(name);
|
|
45
|
+
|
|
39
46
|
continue;
|
|
40
47
|
}
|
|
48
|
+
|
|
41
49
|
if (equals !== -1) {
|
|
42
50
|
return { ok: false, code: 'unexpected-value', message: `--${name} takes no value; it is on when you write it and off when you do not` };
|
|
43
51
|
}
|
|
52
|
+
|
|
44
53
|
given.add(name);
|
|
54
|
+
|
|
45
55
|
continue;
|
|
46
56
|
}
|
|
57
|
+
|
|
47
58
|
let value: string;
|
|
59
|
+
|
|
48
60
|
if (equals !== -1) {
|
|
49
61
|
value = word.slice(equals + 1);
|
|
50
62
|
} else {
|
|
51
63
|
const next = argv[i + 1];
|
|
64
|
+
|
|
52
65
|
if (next === undefined || next.startsWith('--')) {
|
|
53
66
|
return { ok: false, code: 'missing-value', message: `--${name} needs a value (write --${name}=<value> for one that starts with --)` };
|
|
54
67
|
}
|
|
68
|
+
|
|
55
69
|
value = next;
|
|
56
70
|
i++;
|
|
57
71
|
}
|
|
72
|
+
|
|
58
73
|
flags[name] = value;
|
|
59
74
|
}
|
|
75
|
+
|
|
60
76
|
return { ok: true, positionals, flags, switches: given };
|
|
61
77
|
}
|
package/src/context/index.ts
CHANGED
package/src/context/refusal.ts
CHANGED
|
@@ -20,6 +20,7 @@ function rows(code: string, message: string, lines: readonly string[]): string[]
|
|
|
20
20
|
/* What a verb is refused with, whole: the `refused` row and what to do instead under it. */
|
|
21
21
|
export function refusalBody(code: string, message: string, lines: readonly string[] = []): string {
|
|
22
22
|
const [first, ...rest] = rows(code, message, lines);
|
|
23
|
+
|
|
23
24
|
return [`refused\t${first}`, ...rest].join('\n');
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -39,8 +40,10 @@ export function parseRefusalBody(text: string): ParsedRefusal | null {
|
|
|
39
40
|
const [first = '', ...lines] = text.split('\n').filter((line) => line.trim() !== '');
|
|
40
41
|
const parts = first.split('\t');
|
|
41
42
|
const [code, message] = parts[0] === 'refused' ? parts.slice(1) : parts;
|
|
43
|
+
|
|
42
44
|
if (!code || !message) {
|
|
43
45
|
return null;
|
|
44
46
|
}
|
|
47
|
+
|
|
45
48
|
return { code, message, lines };
|
|
46
49
|
}
|