@glyphteck/veyl 0.67.0 → 0.68.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/account.js +2000 -429
- package/dist/accountprofiles.js +98 -5
- package/dist/auth.js +1 -1
- package/dist/cli.js +4154 -2080
- package/dist/index.js +4148 -1017
- package/docs/agents.md +9 -1
- package/docs/api.md +52 -2
- package/docs/validation.md +4 -4
- package/docs/vote-market.md +248 -0
- package/examples/codex-agent/agent-instructions.js +19 -0
- package/examples/codex-agent/agent-state.js +357 -0
- package/examples/codex-agent/chat-whitelist.js +104 -0
- package/examples/codex-agent/codex-app-server.js +737 -0
- package/examples/codex-agent/codex-config.js +13 -0
- package/examples/codex-agent/codex-input.js +89 -0
- package/examples/codex-agent/connector.js +478 -0
- package/examples/codex-agent/index.js +140 -0
- package/examples/codex-agent/instance-lock.js +359 -0
- package/examples/codex-agent/readme.md +88 -0
- package/examples/codex-agent/veyl-channel.js +603 -0
- package/package.json +12 -1
- package/readme.md +1 -0
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { basename, join } from 'node:path';
|
|
5
|
+
import {
|
|
6
|
+
open,
|
|
7
|
+
startSessionRuntime,
|
|
8
|
+
} from '@glyphteck/veyl';
|
|
9
|
+
import { createChatWhitelist } from './chat-whitelist.js';
|
|
10
|
+
|
|
11
|
+
const ATTACHMENT_TYPES = new Set(['img', 'gif', 'm4a', 'mp4', 'file']);
|
|
12
|
+
const MESSAGE_TYPES = new Set([
|
|
13
|
+
'txt',
|
|
14
|
+
'img',
|
|
15
|
+
'gif',
|
|
16
|
+
'm4a',
|
|
17
|
+
'mp4',
|
|
18
|
+
'file',
|
|
19
|
+
'req',
|
|
20
|
+
'rxn',
|
|
21
|
+
'del',
|
|
22
|
+
]);
|
|
23
|
+
const DEFAULT_TYPING_HEARTBEAT_MS = 2_000;
|
|
24
|
+
const DEFAULT_ATTACHMENT_RETRY_MS = 2_000;
|
|
25
|
+
const DEFAULT_ROUTE_RETRY_MS = 2_000;
|
|
26
|
+
const textEncoder = new TextEncoder();
|
|
27
|
+
|
|
28
|
+
function cleanText(value) {
|
|
29
|
+
return String(value ?? '').trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function errorSummary(error) {
|
|
33
|
+
return cleanText(error?.message || error || 'unknown error')
|
|
34
|
+
.split('\n')[0]
|
|
35
|
+
.slice(0, 300);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function wait(ms, signal) {
|
|
39
|
+
if (signal?.aborted) return Promise.resolve();
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
const finish = () => {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
signal?.removeEventListener?.('abort', finish);
|
|
44
|
+
resolve();
|
|
45
|
+
};
|
|
46
|
+
const timer = setTimeout(finish, ms);
|
|
47
|
+
timer.unref?.();
|
|
48
|
+
signal?.addEventListener?.('abort', finish, { once: true });
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function publicOwner(peer) {
|
|
53
|
+
const uid = cleanText(peer?.uid);
|
|
54
|
+
const chatPK = cleanText(peer?.chatPK).toLowerCase();
|
|
55
|
+
if (!uid || !/^[0-9a-f]{64}$/u.test(chatPK)) {
|
|
56
|
+
throw new Error('owner identity is missing a stable uid or chat key');
|
|
57
|
+
}
|
|
58
|
+
return Object.freeze({ uid, chatPK });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function ownerMatchesChat(chat, owner) {
|
|
62
|
+
if (chat?.lineage !== 'direct' || Number(chat?.memberCount) !== 2) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
if (
|
|
66
|
+
cleanText(chat?.peerUid) === owner.uid
|
|
67
|
+
&& cleanText(chat?.peerChatPK).toLowerCase() === owner.chatPK
|
|
68
|
+
) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
const remote = (chat?.members || []).find((member) => (
|
|
72
|
+
cleanText(member?.uid) === owner.uid
|
|
73
|
+
&& cleanText(member?.chatPK).toLowerCase() === owner.chatPK
|
|
74
|
+
));
|
|
75
|
+
return !!remote;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function ownerMessage(event, owner) {
|
|
79
|
+
return event?.type === 'message'
|
|
80
|
+
&& event?.message?.from === 'peer'
|
|
81
|
+
&& MESSAGE_TYPES.has(event?.message?.type)
|
|
82
|
+
&& ownerMatchesChat(event.chat, owner)
|
|
83
|
+
&& cleanText(event?.message?.senderChatPK).toLowerCase() === owner.chatPK;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function messageId(event) {
|
|
87
|
+
return cleanText(event?.message?.id || event?.message?.cid);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function attachmentFilename(event) {
|
|
91
|
+
const raw = basename(cleanText(event?.message?.name))
|
|
92
|
+
.replace(/[^a-zA-Z0-9._-]+/gu, '-');
|
|
93
|
+
return raw.slice(0, 120) || `${event.message.type}-${messageId(event)}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function sameBytes(left, right) {
|
|
97
|
+
return left instanceof Uint8Array
|
|
98
|
+
&& right instanceof Uint8Array
|
|
99
|
+
&& left.byteLength === right.byteLength
|
|
100
|
+
&& left.every((value, index) => value === right[index]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function plainTextFile(message) {
|
|
104
|
+
const mime = cleanText(message?.mime || message?.payload?.m).toLowerCase();
|
|
105
|
+
return message?.type === 'file'
|
|
106
|
+
&& (mime === 'text/plain' || mime.startsWith('text/plain;'));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function bootAccount(client, options) {
|
|
110
|
+
const log = typeof options.log === 'function' ? options.log : () => {};
|
|
111
|
+
log('veyl.account.reading');
|
|
112
|
+
let account = await client.account.me();
|
|
113
|
+
if (!account) {
|
|
114
|
+
const username = cleanText(options.username).replace(/^@+/u, '');
|
|
115
|
+
if (!username) throw new Error('username required to create the Veyl account');
|
|
116
|
+
log('veyl.account.creating', { username });
|
|
117
|
+
await client.account.create({
|
|
118
|
+
username,
|
|
119
|
+
network: options.network,
|
|
120
|
+
});
|
|
121
|
+
account = await client.account.me();
|
|
122
|
+
} else if (!account.signedIn) {
|
|
123
|
+
log('veyl.account.authenticating');
|
|
124
|
+
account = await client.account.login();
|
|
125
|
+
}
|
|
126
|
+
if (!account?.hasVault) {
|
|
127
|
+
log('veyl.vault.creating');
|
|
128
|
+
await client.vault.create();
|
|
129
|
+
} else if (!account.unlocked) {
|
|
130
|
+
log('veyl.vault.unlocking');
|
|
131
|
+
await client.vault.unlock();
|
|
132
|
+
}
|
|
133
|
+
log('veyl.account.ready');
|
|
134
|
+
return client.account.me();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
class WritingLease {
|
|
138
|
+
constructor(channel, chatId, compositionId) {
|
|
139
|
+
this.channel = channel;
|
|
140
|
+
this.chatId = chatId;
|
|
141
|
+
this.compositionId = compositionId;
|
|
142
|
+
this.closed = false;
|
|
143
|
+
this.timer = null;
|
|
144
|
+
this.pending = Promise.resolve();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async start() {
|
|
148
|
+
await this.channel.ensureLive(this.chatId);
|
|
149
|
+
await this.channel.client.chat.markTyping(
|
|
150
|
+
this.chatId,
|
|
151
|
+
true,
|
|
152
|
+
this.compositionId
|
|
153
|
+
);
|
|
154
|
+
this.schedule();
|
|
155
|
+
return this;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
schedule() {
|
|
159
|
+
if (this.closed) return;
|
|
160
|
+
this.timer = setTimeout(() => {
|
|
161
|
+
this.timer = null;
|
|
162
|
+
this.pending = this.pending
|
|
163
|
+
.catch(() => {})
|
|
164
|
+
.then(() => this.channel.client.chat.markTyping(
|
|
165
|
+
this.chatId,
|
|
166
|
+
true,
|
|
167
|
+
this.compositionId
|
|
168
|
+
));
|
|
169
|
+
this.schedule();
|
|
170
|
+
}, this.channel.typingHeartbeatMs);
|
|
171
|
+
this.timer.unref?.();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
pulse() {
|
|
175
|
+
if (!this.closed && !this.timer) this.schedule();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async handoff() {
|
|
179
|
+
if (this.closed) return;
|
|
180
|
+
this.closed = true;
|
|
181
|
+
clearTimeout(this.timer);
|
|
182
|
+
this.timer = null;
|
|
183
|
+
await this.pending.catch(() => {});
|
|
184
|
+
await this.channel.client.chat.markTyping(
|
|
185
|
+
this.chatId,
|
|
186
|
+
false,
|
|
187
|
+
this.compositionId
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async clear() {
|
|
192
|
+
await this.handoff().catch(() => {});
|
|
193
|
+
await this.channel.client.chat.markTyping(this.chatId, false)
|
|
194
|
+
.catch(() => {});
|
|
195
|
+
this.channel.writing.delete(this.chatId);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export class VeylAgentChannel {
|
|
200
|
+
constructor(options) {
|
|
201
|
+
this.client = options.client;
|
|
202
|
+
this.state = options.state;
|
|
203
|
+
this.owner = options.owner;
|
|
204
|
+
this.log = options.log;
|
|
205
|
+
this.typingHeartbeatMs = options.typingHeartbeatMs;
|
|
206
|
+
this.attachmentRetryMs = options.attachmentRetryMs;
|
|
207
|
+
this.routeRetryMs = options.routeRetryMs;
|
|
208
|
+
this.temporaryRoot = options.temporaryRoot || '';
|
|
209
|
+
this.temporaryOwner = options.temporaryOwner || null;
|
|
210
|
+
this.liveChats = new Set();
|
|
211
|
+
this.writing = new Map();
|
|
212
|
+
this.temporaryFiles = new Set();
|
|
213
|
+
this.controller = new AbortController();
|
|
214
|
+
this.sessionController = new AbortController();
|
|
215
|
+
this.sessionTask = options.sessionTask || null;
|
|
216
|
+
this.sessionName = options.sessionName;
|
|
217
|
+
this.listenTask = null;
|
|
218
|
+
this.routeChain = Promise.resolve();
|
|
219
|
+
this.closed = false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async assertTemporaryOwner() {
|
|
223
|
+
if (!this.temporaryRoot) return;
|
|
224
|
+
if (typeof this.temporaryOwner?.assertHeld !== 'function') {
|
|
225
|
+
throw new Error('shared temporary root requires the connector lock');
|
|
226
|
+
}
|
|
227
|
+
await this.temporaryOwner.assertHeld();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async ensureLive(chatIdValue) {
|
|
231
|
+
const chatId = cleanText(chatIdValue).toLowerCase();
|
|
232
|
+
if (!chatId || this.liveChats.has(chatId)) return !!chatId;
|
|
233
|
+
const result = await this.client.chat.enterLive(chatId);
|
|
234
|
+
if (result?.entered !== true) {
|
|
235
|
+
throw new Error(`could not enter Veyl live chat: ${chatId}`);
|
|
236
|
+
}
|
|
237
|
+
if (this.closed) {
|
|
238
|
+
await this.client.chat.leaveLive(chatId);
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
this.liveChats.add(chatId);
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async enterExistingOwnerChats() {
|
|
246
|
+
const chats = await this.client.chat.list({ count: 500 });
|
|
247
|
+
await Promise.all(chats
|
|
248
|
+
.filter((chat) => ownerMatchesChat(chat, this.owner))
|
|
249
|
+
.map((chat) => this.ensureLive(chat.id)));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async prepare(event) {
|
|
253
|
+
const prepared = {
|
|
254
|
+
event,
|
|
255
|
+
chatId: cleanText(event?.chat?.id).toLowerCase(),
|
|
256
|
+
messageId: messageId(event),
|
|
257
|
+
attachment: null,
|
|
258
|
+
};
|
|
259
|
+
if (!ATTACHMENT_TYPES.has(event?.message?.type) || !prepared.messageId) {
|
|
260
|
+
return prepared;
|
|
261
|
+
}
|
|
262
|
+
while (!this.closed) {
|
|
263
|
+
await this.assertTemporaryOwner();
|
|
264
|
+
const directory = await mkdtemp(this.temporaryRoot
|
|
265
|
+
? join(this.temporaryRoot, 'message-')
|
|
266
|
+
: join(tmpdir(), 'veyl-agent-'));
|
|
267
|
+
try {
|
|
268
|
+
const attachment = await this.client.chat.readAttachmentIn(
|
|
269
|
+
prepared.chatId,
|
|
270
|
+
prepared.messageId
|
|
271
|
+
);
|
|
272
|
+
const path = join(directory, attachmentFilename(event));
|
|
273
|
+
await writeFile(path, attachment.bytes, { mode: 0o600 });
|
|
274
|
+
this.temporaryFiles.add(directory);
|
|
275
|
+
prepared.attachment = {
|
|
276
|
+
path,
|
|
277
|
+
image: event.message.type === 'img'
|
|
278
|
+
|| event.message.type === 'gif',
|
|
279
|
+
};
|
|
280
|
+
return prepared;
|
|
281
|
+
} catch (error) {
|
|
282
|
+
if (
|
|
283
|
+
!this.temporaryRoot
|
|
284
|
+
|| await this.temporaryOwner?.owns?.()
|
|
285
|
+
) {
|
|
286
|
+
await rm(directory, { recursive: true, force: true })
|
|
287
|
+
.catch(() => {});
|
|
288
|
+
}
|
|
289
|
+
this.log('veyl.attachment.retry', {
|
|
290
|
+
error: errorSummary(error),
|
|
291
|
+
});
|
|
292
|
+
await wait(this.attachmentRetryMs, this.controller.signal);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
throw new Error('Veyl agent channel closed');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async release(prepared) {
|
|
299
|
+
const directory = prepared?.attachment?.path
|
|
300
|
+
? [...this.temporaryFiles].find((candidate) => (
|
|
301
|
+
prepared.attachment.path.startsWith(`${candidate}/`)
|
|
302
|
+
))
|
|
303
|
+
: null;
|
|
304
|
+
if (!directory) return;
|
|
305
|
+
try {
|
|
306
|
+
await this.assertTemporaryOwner();
|
|
307
|
+
} catch (error) {
|
|
308
|
+
this.log('veyl.temporary.cleanup.skipped', {
|
|
309
|
+
error: errorSummary(error),
|
|
310
|
+
});
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
this.temporaryFiles.delete(directory);
|
|
314
|
+
await rm(directory, { recursive: true, force: true }).catch(() => {});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async beginWriting(prepared, compositionId = '') {
|
|
318
|
+
const existing = this.writing.get(prepared.chatId);
|
|
319
|
+
if (existing) return existing;
|
|
320
|
+
const lease = new WritingLease(
|
|
321
|
+
this,
|
|
322
|
+
prepared.chatId,
|
|
323
|
+
cleanText(compositionId).toLowerCase()
|
|
324
|
+
|| randomBytes(16).toString('hex')
|
|
325
|
+
);
|
|
326
|
+
this.writing.set(prepared.chatId, lease);
|
|
327
|
+
try {
|
|
328
|
+
return await lease.start();
|
|
329
|
+
} catch (error) {
|
|
330
|
+
this.writing.delete(prepared.chatId);
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async send(prepared, text, lease, options = {}) {
|
|
336
|
+
const message = cleanText(text);
|
|
337
|
+
if (!message) throw new Error('agent response required');
|
|
338
|
+
try {
|
|
339
|
+
const cid = cleanText(options.cid);
|
|
340
|
+
let reconciled = false;
|
|
341
|
+
if (cid) {
|
|
342
|
+
try {
|
|
343
|
+
const existing = await this.client.chat.readMessageIn(
|
|
344
|
+
prepared.chatId,
|
|
345
|
+
cid
|
|
346
|
+
);
|
|
347
|
+
const found = existing?.message;
|
|
348
|
+
const textMatches = found?.type === 'txt'
|
|
349
|
+
&& found?.text === message;
|
|
350
|
+
const attachmentMatches = plainTextFile(found)
|
|
351
|
+
&& sameBytes(
|
|
352
|
+
(await this.client.chat.readAttachmentIn(
|
|
353
|
+
prepared.chatId,
|
|
354
|
+
cid,
|
|
355
|
+
{ durableOnly: true }
|
|
356
|
+
)).bytes,
|
|
357
|
+
textEncoder.encode(message)
|
|
358
|
+
);
|
|
359
|
+
if (
|
|
360
|
+
found?.cid !== cid
|
|
361
|
+
|| found?.from !== 'self'
|
|
362
|
+
|| (!textMatches && !attachmentMatches)
|
|
363
|
+
) {
|
|
364
|
+
throw new Error('Veyl response cid belongs to different content');
|
|
365
|
+
}
|
|
366
|
+
reconciled = true;
|
|
367
|
+
} catch (error) {
|
|
368
|
+
if (error?.message !== 'message not found') throw error;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
await lease.handoff();
|
|
372
|
+
if (!reconciled) {
|
|
373
|
+
await this.client.chat.sendTo(prepared.chatId, message, {
|
|
374
|
+
compositionId: lease.compositionId,
|
|
375
|
+
...(cid ? { cid } : {}),
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
this.writing.delete(prepared.chatId);
|
|
379
|
+
} catch (error) {
|
|
380
|
+
await lease.clear();
|
|
381
|
+
throw error;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async react(prepared, emoji, lease) {
|
|
386
|
+
await lease.clear();
|
|
387
|
+
await this.client.chat.reactIn(
|
|
388
|
+
prepared.chatId,
|
|
389
|
+
prepared.messageId,
|
|
390
|
+
emoji
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
markProcessed(events) {
|
|
395
|
+
const records = events.map((item) => item.event || item);
|
|
396
|
+
return typeof this.state.complete === 'function'
|
|
397
|
+
? this.state.complete(records)
|
|
398
|
+
: this.state.mark(records);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async routeEvent(event, onMessage) {
|
|
402
|
+
let prepared = null;
|
|
403
|
+
while (!this.closed && !this.state.has(event)) {
|
|
404
|
+
try {
|
|
405
|
+
prepared ||= await this.prepare(event);
|
|
406
|
+
await this.ensureLive(prepared.chatId);
|
|
407
|
+
await this.client.chat.markReadIn(prepared.chatId, {
|
|
408
|
+
messageId: prepared.messageId,
|
|
409
|
+
});
|
|
410
|
+
await onMessage(prepared);
|
|
411
|
+
return;
|
|
412
|
+
} catch (error) {
|
|
413
|
+
this.log('veyl.route.retry', {
|
|
414
|
+
error: errorSummary(error),
|
|
415
|
+
});
|
|
416
|
+
await wait(this.routeRetryMs, this.controller.signal);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
await this.release(prepared);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async listen(onMessage) {
|
|
423
|
+
if (this.listenTask) return this.listenTask;
|
|
424
|
+
this.listenTask = this.client.listen({
|
|
425
|
+
replay: this.state.initialized,
|
|
426
|
+
chats: 500,
|
|
427
|
+
transactions: false,
|
|
428
|
+
read: false,
|
|
429
|
+
relayReads: false,
|
|
430
|
+
signal: this.controller.signal,
|
|
431
|
+
hasReplayCheckpoint: (chat) => this.state.hasChat(chat),
|
|
432
|
+
isProcessedEvent: (event) => this.state.has(event),
|
|
433
|
+
onEvent: (event) => {
|
|
434
|
+
if (event?.type === 'ready') {
|
|
435
|
+
void this.state.setInitialized().catch((error) => (
|
|
436
|
+
this.log('veyl.state.failed', {
|
|
437
|
+
error: errorSummary(error),
|
|
438
|
+
})
|
|
439
|
+
));
|
|
440
|
+
this.log('veyl.ready', {
|
|
441
|
+
profile: event.account?.profile || null,
|
|
442
|
+
});
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (event?.type === 'error') {
|
|
446
|
+
this.log('veyl.event.error', {
|
|
447
|
+
error: cleanText(event.message),
|
|
448
|
+
});
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (!ownerMessage(event, this.owner) || this.state.has(event)) {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
this.routeChain = this.routeChain
|
|
455
|
+
.then(() => this.routeEvent(event, onMessage))
|
|
456
|
+
.catch((error) => this.log('veyl.route.failed', {
|
|
457
|
+
error: errorSummary(error),
|
|
458
|
+
}));
|
|
459
|
+
},
|
|
460
|
+
}).finally(async () => {
|
|
461
|
+
await this.routeChain;
|
|
462
|
+
});
|
|
463
|
+
return this.listenTask;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async close() {
|
|
467
|
+
if (this.closed) return;
|
|
468
|
+
this.closed = true;
|
|
469
|
+
this.controller.abort();
|
|
470
|
+
await this.listenTask?.catch(() => {});
|
|
471
|
+
await Promise.allSettled([...this.writing.values()].map((lease) => (
|
|
472
|
+
lease.clear()
|
|
473
|
+
)));
|
|
474
|
+
await Promise.allSettled([...this.liveChats].map((chatId) => (
|
|
475
|
+
this.client.chat.leaveLive(chatId)
|
|
476
|
+
)));
|
|
477
|
+
this.liveChats.clear();
|
|
478
|
+
this.sessionController.abort();
|
|
479
|
+
await this.sessionTask?.catch(() => {});
|
|
480
|
+
let canCleanTemporaryRoot = true;
|
|
481
|
+
try {
|
|
482
|
+
await this.assertTemporaryOwner();
|
|
483
|
+
} catch (error) {
|
|
484
|
+
canCleanTemporaryRoot = false;
|
|
485
|
+
this.log('veyl.temporary.cleanup.skipped', {
|
|
486
|
+
error: errorSummary(error),
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
if (canCleanTemporaryRoot) {
|
|
490
|
+
await Promise.allSettled([...this.temporaryFiles].map((directory) => (
|
|
491
|
+
rm(directory, { recursive: true, force: true })
|
|
492
|
+
)));
|
|
493
|
+
this.temporaryFiles.clear();
|
|
494
|
+
if (this.temporaryRoot) {
|
|
495
|
+
await rm(this.temporaryRoot, { recursive: true, force: true })
|
|
496
|
+
.catch(() => {});
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
await this.client.close();
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export async function openVeylAgentChannel(options = {}) {
|
|
504
|
+
if (!options.state) throw new Error('agent state required');
|
|
505
|
+
const ownerTarget = cleanText(options.owner);
|
|
506
|
+
if (!ownerTarget) throw new Error('Veyl owner required');
|
|
507
|
+
const temporaryRoot = cleanText(options.temporaryRoot);
|
|
508
|
+
if (temporaryRoot) {
|
|
509
|
+
if (typeof options.temporaryOwner?.assertHeld !== 'function') {
|
|
510
|
+
throw new Error('shared temporary root requires the connector lock');
|
|
511
|
+
}
|
|
512
|
+
await options.temporaryOwner.assertHeld();
|
|
513
|
+
}
|
|
514
|
+
let admission = createChatWhitelist([
|
|
515
|
+
options.state.owner || ownerTarget,
|
|
516
|
+
]);
|
|
517
|
+
const log = typeof options.log === 'function' ? options.log : () => {};
|
|
518
|
+
const openClient = options.openClient || open;
|
|
519
|
+
log('veyl.channel.opening');
|
|
520
|
+
const client = options.client || await openClient({
|
|
521
|
+
...(options.clientOptions || {}),
|
|
522
|
+
homeDir: options.homeDir,
|
|
523
|
+
profile: options.profile,
|
|
524
|
+
network: options.network,
|
|
525
|
+
chat: {
|
|
526
|
+
...(options.clientOptions?.chat || {}),
|
|
527
|
+
incomingChatDecision: (context) => admission(context),
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
let channel = null;
|
|
531
|
+
try {
|
|
532
|
+
if (temporaryRoot) {
|
|
533
|
+
await options.temporaryOwner.assertHeld();
|
|
534
|
+
await rm(temporaryRoot, { recursive: true, force: true });
|
|
535
|
+
await mkdir(temporaryRoot, { recursive: true, mode: 0o700 });
|
|
536
|
+
}
|
|
537
|
+
await bootAccount(client, options);
|
|
538
|
+
log('veyl.owner.resolving', { owner: ownerTarget });
|
|
539
|
+
const resolved = await client.peers.show(
|
|
540
|
+
options.state.owner?.chatPK || ownerTarget
|
|
541
|
+
);
|
|
542
|
+
const owner = publicOwner(resolved);
|
|
543
|
+
await options.state.pinOwner(owner);
|
|
544
|
+
admission = createChatWhitelist([owner]);
|
|
545
|
+
log('veyl.admission.publishing');
|
|
546
|
+
await client.profile.setChatAdmission({
|
|
547
|
+
direct: 'closed',
|
|
548
|
+
groups: 'closed',
|
|
549
|
+
allow: [owner.chatPK],
|
|
550
|
+
});
|
|
551
|
+
log('veyl.admission.ready');
|
|
552
|
+
channel = new VeylAgentChannel({
|
|
553
|
+
client,
|
|
554
|
+
state: options.state,
|
|
555
|
+
owner,
|
|
556
|
+
log,
|
|
557
|
+
typingHeartbeatMs: Number(options.typingHeartbeatMs)
|
|
558
|
+
|| DEFAULT_TYPING_HEARTBEAT_MS,
|
|
559
|
+
attachmentRetryMs: Number(options.attachmentRetryMs)
|
|
560
|
+
|| DEFAULT_ATTACHMENT_RETRY_MS,
|
|
561
|
+
routeRetryMs: Number(options.routeRetryMs)
|
|
562
|
+
|| DEFAULT_ROUTE_RETRY_MS,
|
|
563
|
+
temporaryRoot,
|
|
564
|
+
temporaryOwner: options.temporaryOwner,
|
|
565
|
+
sessionName: cleanText(options.sessionName) || 'agent',
|
|
566
|
+
});
|
|
567
|
+
if (options.serveCli !== false) {
|
|
568
|
+
let readyResolve;
|
|
569
|
+
const ready = new Promise((resolve) => {
|
|
570
|
+
readyResolve = resolve;
|
|
571
|
+
});
|
|
572
|
+
channel.sessionTask = startSessionRuntime(client, {
|
|
573
|
+
profile: options.profile,
|
|
574
|
+
name: channel.sessionName,
|
|
575
|
+
signal: channel.sessionController.signal,
|
|
576
|
+
onReady: readyResolve,
|
|
577
|
+
});
|
|
578
|
+
log('veyl.cli.starting', { session: channel.sessionName });
|
|
579
|
+
await Promise.race([
|
|
580
|
+
ready,
|
|
581
|
+
channel.sessionTask.then(() => {
|
|
582
|
+
throw new Error('Veyl CLI session stopped before it became ready');
|
|
583
|
+
}),
|
|
584
|
+
]);
|
|
585
|
+
log('veyl.cli.ready', { session: channel.sessionName });
|
|
586
|
+
}
|
|
587
|
+
log('veyl.owner_chats.entering');
|
|
588
|
+
await channel.enterExistingOwnerChats();
|
|
589
|
+
log('veyl.channel.ready');
|
|
590
|
+
return channel;
|
|
591
|
+
} catch (error) {
|
|
592
|
+
if (channel) {
|
|
593
|
+
await channel.close().catch(() => {});
|
|
594
|
+
} else {
|
|
595
|
+
await client.close().catch(() => {});
|
|
596
|
+
if (temporaryRoot && await options.temporaryOwner?.owns?.()) {
|
|
597
|
+
await rm(temporaryRoot, { recursive: true, force: true })
|
|
598
|
+
.catch(() => {});
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
throw error;
|
|
602
|
+
}
|
|
603
|
+
}
|
package/package.json
CHANGED
|
@@ -26,6 +26,17 @@
|
|
|
26
26
|
"examples/bot-fleet/policy.js",
|
|
27
27
|
"examples/bot-fleet/runtime.js",
|
|
28
28
|
"examples/bot-fleet/readme.md",
|
|
29
|
+
"examples/codex-agent/agent-instructions.js",
|
|
30
|
+
"examples/codex-agent/agent-state.js",
|
|
31
|
+
"examples/codex-agent/chat-whitelist.js",
|
|
32
|
+
"examples/codex-agent/codex-app-server.js",
|
|
33
|
+
"examples/codex-agent/codex-config.js",
|
|
34
|
+
"examples/codex-agent/codex-input.js",
|
|
35
|
+
"examples/codex-agent/connector.js",
|
|
36
|
+
"examples/codex-agent/index.js",
|
|
37
|
+
"examples/codex-agent/instance-lock.js",
|
|
38
|
+
"examples/codex-agent/readme.md",
|
|
39
|
+
"examples/codex-agent/veyl-channel.js",
|
|
29
40
|
"LICENSE",
|
|
30
41
|
"readme.md",
|
|
31
42
|
"package.json"
|
|
@@ -42,5 +53,5 @@
|
|
|
42
53
|
"start": "node src/cli.js",
|
|
43
54
|
"lint": "eslint src --quiet"
|
|
44
55
|
},
|
|
45
|
-
"version": "0.
|
|
56
|
+
"version": "0.68.0"
|
|
46
57
|
}
|
package/readme.md
CHANGED
|
@@ -54,6 +54,7 @@ Run `bunx veyl help` for the full command list. Long-running agents should keep
|
|
|
54
54
|
- [JavaScript API](docs/api.md)
|
|
55
55
|
- [CLI reference](docs/cli.md)
|
|
56
56
|
- [agent operation](docs/agents.md)
|
|
57
|
+
- [single-account Codex agent](examples/codex-agent/readme.md)
|
|
57
58
|
- [MCP documentation discovery](docs/discovery.md)
|
|
58
59
|
|
|
59
60
|
## license
|