@ai-workbench/connect 0.7.6 → 0.7.9
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/README.md +52 -5
- package/client/index.cjs +32 -4
- package/helper/auth-status.cjs +6 -6
- package/helper/device-panel.cjs +4 -0
- package/helper/device-panel.css +1 -0
- package/helper/device-panel.html +1 -0
- package/helper/device-preload.cjs +5 -0
- package/helper/device-window.cjs +60 -0
- package/helper/remote-command.cjs +3 -2
- package/helper/remote-login.cjs +1 -1
- package/helper/window.cjs +8 -14
- package/lib/api.d.ts +77 -1
- package/lib/components.d.ts +1 -1
- package/lib/components.js +46 -4
- package/lib/connect.js +43 -17
- package/lib/conversation-detail-settings.d.ts +32 -0
- package/lib/conversation-detail-settings.js +119 -0
- package/lib/conversation-details.d.ts +21 -0
- package/lib/conversation-details.js +381 -0
- package/lib/cordis.d.ts +2 -1
- package/lib/defaults.d.ts +4 -0
- package/lib/defaults.js +1 -0
- package/lib/index.d.ts +5 -1
- package/lib/index.js +26 -17
- package/lib/integration.js +20 -1
- package/lib/login.d.ts +8 -4
- package/lib/login.js +16 -25
- package/lib/needs.js +8 -0
- package/lib/processes.js +28 -2
- package/lib/profile-runtime.js +27 -1
- package/lib/profile-settings.js +3 -3
- package/lib/workspace-bindings.d.ts +4 -1
- package/lib/workspace-bindings.js +51 -1
- package/package.json +1 -1
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { lstat, open, readdir, realpath, rename, rm } from 'node:fs/promises';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import { isAbsolute, join } from 'node:path';
|
|
5
|
+
import { fail, need, NeedError, ok } from "./needs.js";
|
|
6
|
+
import { privateDirectory, readPrivate } from "./private-files.js";
|
|
7
|
+
const queues = new Map();
|
|
8
|
+
const digest = (s) => createHash('sha256').update(s).digest('hex');
|
|
9
|
+
const text = (v) => typeof v === 'string' && v.length > 0 && !/[\x00-\x1f\x7f]/.test(v);
|
|
10
|
+
const integer = (v) => Number.isSafeInteger(v) && Number(v) >= 0;
|
|
11
|
+
const brand = (v) => v === 'claude' || v === 'codex';
|
|
12
|
+
const canonical = (v) => v === null || typeof v !== 'object' ? JSON.stringify(v) : Array.isArray(v) ? '[' + v.map(canonical).join(',') + ']' : '{' + Object.keys(v).sort().map(k => JSON.stringify(k) + ':' + canonical(v[k])).join(',') + '}';
|
|
13
|
+
const sensitiveText = /(?:\bBearer\s+\S+|\bsk-(?:ant-)?[a-zA-Z0-9_-]+|-----BEGIN (?:[A-Z ]*PRIVATE KEY)-----|https?:\/\/[^\s]*[?&](?:code|token|access_token|refresh_token|id_token|client_secret|device_code|user_code|password)=)/i;
|
|
14
|
+
const sensitiveKey = /^(?:headers?|body|cookies?|authorization|proxy-authorization|password|passwd|secret|client_secret|token|access_token|refresh_token|id_token|api[_-]?key|device[_-]?code|user[_-]?code|credentials?|prompt|messages|model[_-]?(?:input|output))$/i;
|
|
15
|
+
function json(v, seen = new Set()) {
|
|
16
|
+
if (v === null || typeof v === 'boolean')
|
|
17
|
+
return true;
|
|
18
|
+
if (typeof v === 'number')
|
|
19
|
+
return Number.isFinite(v);
|
|
20
|
+
if (typeof v === 'string')
|
|
21
|
+
return !sensitiveText.test(v);
|
|
22
|
+
if (typeof v !== 'object' || seen.has(v) || (!Array.isArray(v) && Object.getPrototypeOf(v) !== Object.prototype && Object.getPrototypeOf(v) !== null))
|
|
23
|
+
return false;
|
|
24
|
+
seen.add(v);
|
|
25
|
+
let valid = true;
|
|
26
|
+
for (const key of Reflect.ownKeys(v)) {
|
|
27
|
+
if (Array.isArray(v) && key === 'length')
|
|
28
|
+
continue;
|
|
29
|
+
if (typeof key !== 'string' || !Object.getOwnPropertyDescriptor(v, key)?.enumerable || !('value' in Object.getOwnPropertyDescriptor(v, key))) {
|
|
30
|
+
valid = false;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
if (sensitiveText.test(key) || (sensitiveKey.test(key) && v[key] !== '已脱敏') || !json(v[key], seen)) {
|
|
34
|
+
valid = false;
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (Array.isArray(v) && Object.keys(v).length !== v.length)
|
|
39
|
+
valid = false;
|
|
40
|
+
seen.delete(v);
|
|
41
|
+
return valid;
|
|
42
|
+
}
|
|
43
|
+
export function validRemoteConversationDetail(v) {
|
|
44
|
+
if (!v || typeof v !== 'object' || !json(v))
|
|
45
|
+
return false;
|
|
46
|
+
const required = ['version', 'eventId', 'instanceId', 'epoch', 'sessionId', 'provider', 'turn', 'step', 'sequence', 'kind', 'severity', 'code', 'summary', 'payload'];
|
|
47
|
+
if (required.some(k => !Object.hasOwn(v, k)) || Object.keys(v).some(k => !required.includes(k) && k !== 'method'))
|
|
48
|
+
return false;
|
|
49
|
+
return v.version === 1 && text(v.eventId) && text(v.instanceId) && integer(v.epoch) && text(v.sessionId) && brand(v.provider) && integer(v.sequence) && [v.turn, v.step].every(x => x === null || text(x) || integer(x)) && text(v.kind) && ['info', 'warning', 'action-required'].includes(v.severity) && text(v.code) && typeof v.summary === 'string' && (v.method === undefined || text(v.method));
|
|
50
|
+
}
|
|
51
|
+
const counts = (items) => ({ total: items.length, unknownCount: items.filter(d => !['extensions', 'commands', 'tools', 'usage', 'action-required'].includes(d.kind)).length, actionRequiredCount: items.filter(d => d.severity === 'action-required' || d.kind === 'action-required').length });
|
|
52
|
+
export function createConversationDetails(options) {
|
|
53
|
+
const parent = join(options.profileDir, 'ai-workbench-connect'), root = join(parent, 'conversation-details');
|
|
54
|
+
let disposed = false, uncertain = false, revision = 0, tail = Promise.resolve();
|
|
55
|
+
let snapshot = { ...connectionIds(), revision, phase: 'unavailable', sessions: [] };
|
|
56
|
+
const listeners = new Set();
|
|
57
|
+
function connectionIds() { const c = options.getConnectionState(); return { instanceId: c.instanceId, epoch: c.epoch }; }
|
|
58
|
+
function check(before) { const c = options.getConnectionState(); if (disposed || c.phase === 'disposed')
|
|
59
|
+
throw new NeedError(need('DISPOSED')); if (c.phase !== 'connected' || c.instanceId !== before.instanceId || c.epoch !== before.epoch)
|
|
60
|
+
throw new NeedError(need('CONNECTION_LOST')); }
|
|
61
|
+
function start() { const c = options.getConnectionState(); check(c); return c; }
|
|
62
|
+
async function checked(p, before) { const value = await p; check(before); return value; }
|
|
63
|
+
function publish(phase, sessions = [], code) {
|
|
64
|
+
snapshot = { ...connectionIds(), revision: ++revision, phase, sessions, ...(code ? { need: need(code) } : {}) };
|
|
65
|
+
options.onState?.(structuredClone(snapshot));
|
|
66
|
+
for (const listener of listeners)
|
|
67
|
+
listener(structuredClone(snapshot));
|
|
68
|
+
}
|
|
69
|
+
function error(e) { const code = e instanceof NeedError ? e.need.code : 'REMOTE_DETAIL_STORAGE_FAILED'; if (code === 'REMOTE_DETAIL_STORAGE_FAILED') {
|
|
70
|
+
uncertain = true;
|
|
71
|
+
publish('failed', [], code);
|
|
72
|
+
}
|
|
73
|
+
else if (code === 'DISPOSED')
|
|
74
|
+
publish('disposed', [], code);
|
|
75
|
+
else if (code === 'CONNECTION_LOST' || code === 'REMOTE_DETAIL_SCOPE_INVALID')
|
|
76
|
+
publish('unavailable', [], code); return fail(code); }
|
|
77
|
+
function serial(run) {
|
|
78
|
+
const pending = (queues.get(root) ?? Promise.resolve()).then(async () => { try {
|
|
79
|
+
return await run();
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
return error(e);
|
|
83
|
+
} });
|
|
84
|
+
const done = pending.then(() => { }, () => { });
|
|
85
|
+
queues.set(root, done);
|
|
86
|
+
tail = done;
|
|
87
|
+
void done.then(() => { if (queues.get(root) === done)
|
|
88
|
+
queues.delete(root); });
|
|
89
|
+
return pending;
|
|
90
|
+
}
|
|
91
|
+
async function directory(path, before, create) {
|
|
92
|
+
if (create)
|
|
93
|
+
await checked(privateDirectory(path), before);
|
|
94
|
+
let st;
|
|
95
|
+
try {
|
|
96
|
+
st = await checked(lstat(path), before);
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
if (!create && e.code === 'ENOENT')
|
|
100
|
+
return false;
|
|
101
|
+
throw e;
|
|
102
|
+
}
|
|
103
|
+
if (!st.isDirectory() || st.isSymbolicLink() || st.uid !== process.getuid() || (st.mode & 0o777) !== 0o700 || await checked(realpath(path), before) !== path)
|
|
104
|
+
throw Error('DETAIL_PATH_INVALID');
|
|
105
|
+
if (create)
|
|
106
|
+
await syncDir(join(path, '..'), before);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
async function prepare(identity, before, create = false) {
|
|
110
|
+
if (!/^[a-f0-9]{64}$/.test(identity) || !isAbsolute(options.profileDir) || await checked(realpath(options.profileDir), before) !== options.profileDir)
|
|
111
|
+
throw Error('DETAIL_PROFILE_INVALID');
|
|
112
|
+
if (!await directory(parent, before, create) || !await directory(root, before, create))
|
|
113
|
+
return undefined;
|
|
114
|
+
const domain = join(root, identity);
|
|
115
|
+
return await directory(domain, before, create) ? domain : undefined;
|
|
116
|
+
}
|
|
117
|
+
async function syncDir(path, before) { const h = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW); try {
|
|
118
|
+
check(before);
|
|
119
|
+
const st = await checked(h.stat(), before);
|
|
120
|
+
if (!st.isDirectory())
|
|
121
|
+
throw Error('DETAIL_DIRECTORY_INVALID');
|
|
122
|
+
await checked(h.sync(), before);
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
await h.close();
|
|
126
|
+
} }
|
|
127
|
+
async function durable(file, bytes, before) {
|
|
128
|
+
const temp = file + '.' + randomUUID() + '.tmp';
|
|
129
|
+
let handle;
|
|
130
|
+
try {
|
|
131
|
+
handle = await open(temp, 'wx', 0o600);
|
|
132
|
+
check(before);
|
|
133
|
+
await checked(handle.writeFile(bytes), before);
|
|
134
|
+
await checked(handle.sync(), before);
|
|
135
|
+
await handle.close();
|
|
136
|
+
handle = undefined;
|
|
137
|
+
check(before);
|
|
138
|
+
try {
|
|
139
|
+
const existing = await checked(lstat(file), before);
|
|
140
|
+
if (existing.isSymbolicLink() || !existing.isFile() || existing.nlink !== 1)
|
|
141
|
+
throw Error('DETAIL_PATH_INVALID');
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
if (e.code !== 'ENOENT')
|
|
145
|
+
throw e;
|
|
146
|
+
}
|
|
147
|
+
await checked(rename(temp, file), before);
|
|
148
|
+
await syncDir(join(file, '..'), before);
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
await handle?.close();
|
|
152
|
+
await rm(temp, { force: true });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// A readable record after reopen does not prove the previous rename was durable.
|
|
156
|
+
// O_RDONLY verification may sync existing inodes, but never creates or rewrites one.
|
|
157
|
+
async function verifyReceipt(domain, detail, before) {
|
|
158
|
+
const file = join(domain, digest(detail.eventId) + '.json'), h = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
159
|
+
try {
|
|
160
|
+
check(before);
|
|
161
|
+
const st = await checked(h.stat(), before);
|
|
162
|
+
if (!st.isFile() || st.uid !== process.getuid() || st.nlink !== 1 || (st.mode & 0o777) !== 0o600)
|
|
163
|
+
throw Error('DETAIL_RECORD_INVALID');
|
|
164
|
+
const record = JSON.parse(await checked(h.readFile('utf8'), before));
|
|
165
|
+
if (record.version !== 1 || !validRemoteConversationDetail(record.detail) || record.digest !== digest(canonical(record.detail)) || canonical(record.detail) !== canonical(detail))
|
|
166
|
+
throw Error('DETAIL_RECORD_INVALID');
|
|
167
|
+
await checked(h.sync(), before);
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
await h.close();
|
|
171
|
+
}
|
|
172
|
+
for (const path of [domain, root, parent, options.profileDir])
|
|
173
|
+
await syncDir(path, before);
|
|
174
|
+
}
|
|
175
|
+
async function privateRead(path, before) {
|
|
176
|
+
const st = await checked(lstat(path), before);
|
|
177
|
+
if (!st.isFile() || st.isSymbolicLink() || st.nlink !== 1 || (st.mode & 0o777) !== 0o600)
|
|
178
|
+
throw Error('DETAIL_RECORD_INVALID');
|
|
179
|
+
return await checked(readPrivate(path), before);
|
|
180
|
+
}
|
|
181
|
+
async function key(before, create = false) {
|
|
182
|
+
const path = join(root, 'cursor-key');
|
|
183
|
+
try {
|
|
184
|
+
const value = await privateRead(path, before);
|
|
185
|
+
if (!/^[a-f0-9]{64}$/.test(value))
|
|
186
|
+
throw Error('DETAIL_CURSOR_KEY_INVALID');
|
|
187
|
+
return value;
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
if (!create || e.code !== 'ENOENT')
|
|
191
|
+
throw e;
|
|
192
|
+
}
|
|
193
|
+
const value = randomBytes(32).toString('hex');
|
|
194
|
+
await durable(path, value, before);
|
|
195
|
+
return value;
|
|
196
|
+
}
|
|
197
|
+
async function rows(domain, before) {
|
|
198
|
+
const items = [], ids = new Set(), sequences = new Set();
|
|
199
|
+
for (const name of await checked(readdir(domain), before)) {
|
|
200
|
+
if (!/^[a-f0-9]{64}\.json$/.test(name))
|
|
201
|
+
throw Error('DETAIL_RECORD_INVALID');
|
|
202
|
+
const raw = JSON.parse(await checked(privateRead(join(domain, name), before), before)), d = raw.detail;
|
|
203
|
+
if (raw.version !== 1 || !validRemoteConversationDetail(d) || raw.digest !== digest(canonical(d)) || name !== digest(d.eventId) + '.json' || ids.has(d.eventId) || sequences.has(canonical([d.sessionId, d.provider, d.sequence])))
|
|
204
|
+
throw Error('DETAIL_RECORD_INVALID');
|
|
205
|
+
ids.add(d.eventId);
|
|
206
|
+
sequences.add(canonical([d.sessionId, d.provider, d.sequence]));
|
|
207
|
+
items.push(d);
|
|
208
|
+
}
|
|
209
|
+
return items;
|
|
210
|
+
}
|
|
211
|
+
async function scope(sessionId, provider, before) {
|
|
212
|
+
if (!text(sessionId) || !brand(provider))
|
|
213
|
+
throw new NeedError(need('REMOTE_DETAIL_SCOPE_INVALID'));
|
|
214
|
+
const result = await checked(options.scope(sessionId, provider), before);
|
|
215
|
+
if (!result.ok)
|
|
216
|
+
throw new NeedError(result.error);
|
|
217
|
+
return result.value.identity;
|
|
218
|
+
}
|
|
219
|
+
async function stateRows(items, identity, before) {
|
|
220
|
+
const sessions = [];
|
|
221
|
+
let excluded = false;
|
|
222
|
+
for (const d of items) {
|
|
223
|
+
if (sessions.some(s => s.sessionId === d.sessionId && s.provider === d.provider))
|
|
224
|
+
continue;
|
|
225
|
+
const verified = await checked(options.scope(d.sessionId, d.provider), before);
|
|
226
|
+
if (!verified.ok || verified.value.identity !== identity) {
|
|
227
|
+
excluded = true;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
sessions.push({ sessionId: d.sessionId, provider: d.provider, ...counts(items.filter(r => r.sessionId === d.sessionId && r.provider === d.provider)) });
|
|
231
|
+
}
|
|
232
|
+
check(before);
|
|
233
|
+
publish('ready', sessions, excluded ? 'REMOTE_DETAIL_SCOPE_INVALID' : undefined);
|
|
234
|
+
}
|
|
235
|
+
function cursor(value, secret) { const payload = Buffer.from(canonical(value)).toString('base64url'); return payload + '.' + createHmac('sha256', secret).update(payload).digest('hex'); }
|
|
236
|
+
function readCursor(value, secret) {
|
|
237
|
+
if (!/^[A-Za-z0-9_-]+\.[a-f0-9]{64}$/.test(value))
|
|
238
|
+
throw new NeedError(need('REMOTE_DETAIL_CURSOR_INVALID'));
|
|
239
|
+
const [payload, mac] = value.split('.'), expected = createHmac('sha256', secret).update(payload).digest();
|
|
240
|
+
if (!timingSafeEqual(Buffer.from(mac, 'hex'), expected))
|
|
241
|
+
throw new NeedError(need('REMOTE_DETAIL_CURSOR_INVALID'));
|
|
242
|
+
try {
|
|
243
|
+
return JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
throw new NeedError(need('REMOTE_DETAIL_CURSOR_INVALID'));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const api = {
|
|
250
|
+
appendRemoteConversationDetail(input) {
|
|
251
|
+
let detail;
|
|
252
|
+
try {
|
|
253
|
+
if (!validRemoteConversationDetail(input))
|
|
254
|
+
return Promise.resolve(fail('REMOTE_DETAIL_INVALID'));
|
|
255
|
+
detail = structuredClone(input);
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return Promise.resolve(fail('REMOTE_DETAIL_INVALID'));
|
|
259
|
+
}
|
|
260
|
+
return serial(async () => {
|
|
261
|
+
const before = start();
|
|
262
|
+
if (detail.instanceId !== before.instanceId || detail.epoch !== before.epoch)
|
|
263
|
+
return fail('REMOTE_DETAIL_SCOPE_INVALID');
|
|
264
|
+
if (!Number.isSafeInteger(options.maxEventBytes) || options.maxEventBytes < 1)
|
|
265
|
+
return fail('SETTINGS_REQUIRED');
|
|
266
|
+
const identity = await scope(detail.sessionId, detail.provider, before), domain = (await prepare(identity, before, true)), items = await rows(domain, before), existing = items.find(d => d.eventId === detail.eventId);
|
|
267
|
+
if (existing && canonical(existing) !== canonical(detail))
|
|
268
|
+
return fail('REMOTE_DETAIL_CONFLICT');
|
|
269
|
+
if (!existing && Buffer.byteLength(JSON.stringify(detail), 'utf8') > options.maxEventBytes)
|
|
270
|
+
return fail('REMOTE_DETAIL_TOO_LARGE');
|
|
271
|
+
if (!existing && items.some(d => d.sessionId === detail.sessionId && d.provider === detail.provider && d.sequence >= detail.sequence))
|
|
272
|
+
return fail('REMOTE_DETAIL_SEQUENCE_INVALID');
|
|
273
|
+
// Recheck native ownership after all storage reads and before writing or acknowledging.
|
|
274
|
+
if (await scope(detail.sessionId, detail.provider, before) !== identity)
|
|
275
|
+
return fail('REMOTE_DETAIL_SCOPE_INVALID');
|
|
276
|
+
const file = join(domain, digest(detail.eventId) + '.json');
|
|
277
|
+
await key(before, true);
|
|
278
|
+
if (!existing) {
|
|
279
|
+
await durable(file, JSON.stringify({ version: 1, digest: digest(canonical(detail)), detail }) + '\n', before);
|
|
280
|
+
items.push(detail);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
const h = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
284
|
+
try {
|
|
285
|
+
check(before);
|
|
286
|
+
await checked(h.sync(), before);
|
|
287
|
+
}
|
|
288
|
+
finally {
|
|
289
|
+
await h.close();
|
|
290
|
+
}
|
|
291
|
+
await syncDir(domain, before);
|
|
292
|
+
}
|
|
293
|
+
await stateRows(items, identity, before);
|
|
294
|
+
if (await scope(detail.sessionId, detail.provider, before) !== identity)
|
|
295
|
+
return fail('REMOTE_DETAIL_SCOPE_INVALID');
|
|
296
|
+
check(before);
|
|
297
|
+
uncertain = false;
|
|
298
|
+
return ok({ eventId: detail.eventId, recorded: true });
|
|
299
|
+
});
|
|
300
|
+
},
|
|
301
|
+
getRemoteConversationDetailReceipt(query) {
|
|
302
|
+
let q;
|
|
303
|
+
try {
|
|
304
|
+
q = structuredClone(query);
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
return Promise.resolve(fail('REMOTE_DETAIL_INVALID'));
|
|
308
|
+
}
|
|
309
|
+
return serial(async () => {
|
|
310
|
+
const before = start();
|
|
311
|
+
if (!q || !text(q.eventId) || Object.keys(q).sort().join(',') !== 'eventId,provider,sessionId')
|
|
312
|
+
return fail('REMOTE_DETAIL_INVALID');
|
|
313
|
+
const identity = await scope(q.sessionId, q.provider, before), domain = await prepare(identity, before), items = domain ? await rows(domain, before) : [], d = items.find(r => r.eventId === q.eventId);
|
|
314
|
+
if (d && (d.sessionId !== q.sessionId || d.provider !== q.provider))
|
|
315
|
+
return fail('REMOTE_DETAIL_SCOPE_INVALID');
|
|
316
|
+
if (d)
|
|
317
|
+
await verifyReceipt(domain, d, before);
|
|
318
|
+
else if (uncertain)
|
|
319
|
+
return fail('REMOTE_DETAIL_STORAGE_FAILED');
|
|
320
|
+
if (await scope(q.sessionId, q.provider, before) !== identity)
|
|
321
|
+
return fail('REMOTE_DETAIL_SCOPE_INVALID');
|
|
322
|
+
if (d)
|
|
323
|
+
uncertain = false;
|
|
324
|
+
return ok(d ? { eventId: q.eventId, recorded: true, detail: structuredClone(d) } : { eventId: q.eventId, recorded: false });
|
|
325
|
+
});
|
|
326
|
+
},
|
|
327
|
+
listRemoteConversationDetails(query) {
|
|
328
|
+
let q;
|
|
329
|
+
try {
|
|
330
|
+
q = structuredClone(query);
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
return Promise.resolve(fail('REMOTE_DETAIL_INVALID'));
|
|
334
|
+
}
|
|
335
|
+
return serial(async () => {
|
|
336
|
+
const before = start();
|
|
337
|
+
if (uncertain)
|
|
338
|
+
return fail('REMOTE_DETAIL_STORAGE_FAILED');
|
|
339
|
+
if (!q || Object.keys(q).some(k => !['sessionId', 'provider', 'cursor'].includes(k)))
|
|
340
|
+
return fail('REMOTE_DETAIL_INVALID');
|
|
341
|
+
if (!Number.isSafeInteger(options.pageSize) || options.pageSize < 1)
|
|
342
|
+
return fail('SETTINGS_REQUIRED');
|
|
343
|
+
const identity = await scope(q.sessionId, q.provider, before), domain = await prepare(identity, before), items = domain ? await rows(domain, before) : [], all = items.filter(d => d.sessionId === q.sessionId && d.provider === q.provider).sort((a, b) => a.sequence - b.sequence);
|
|
344
|
+
const secret = all.length ? await key(before) : '';
|
|
345
|
+
if (q.cursor !== undefined && !all.length)
|
|
346
|
+
return fail('REMOTE_DETAIL_CURSOR_INVALID');
|
|
347
|
+
let upper = all.at(-1)?.sequence ?? -1, after = -1;
|
|
348
|
+
if (q.cursor !== undefined) {
|
|
349
|
+
if (typeof q.cursor !== 'string')
|
|
350
|
+
return fail('REMOTE_DETAIL_CURSOR_INVALID');
|
|
351
|
+
const c = readCursor(q.cursor, secret);
|
|
352
|
+
if (!c || typeof c !== 'object' || Array.isArray(c) || Object.keys(c).sort().join(',') !== 'after,identity,provider,sessionId,upper,version' || c.version !== 1 || c.identity !== identity || c.sessionId !== q.sessionId || c.provider !== q.provider || !integer(c.upper) || !integer(c.after) || c.after > c.upper || !all.some(d => d.sequence === c.upper) || !all.some(d => d.sequence === c.after))
|
|
353
|
+
return fail('REMOTE_DETAIL_CURSOR_INVALID');
|
|
354
|
+
upper = c.upper;
|
|
355
|
+
after = c.after;
|
|
356
|
+
}
|
|
357
|
+
const bounded = all.filter(d => d.sequence <= upper), remaining = bounded.filter(d => d.sequence > after), page = remaining.slice(0, options.pageSize);
|
|
358
|
+
if (await scope(q.sessionId, q.provider, before) !== identity)
|
|
359
|
+
return fail('REMOTE_DETAIL_SCOPE_INVALID');
|
|
360
|
+
await stateRows(items, identity, before);
|
|
361
|
+
if (await scope(q.sessionId, q.provider, before) !== identity)
|
|
362
|
+
return fail('REMOTE_DETAIL_SCOPE_INVALID');
|
|
363
|
+
const value = { items: structuredClone(page), ...counts(bounded), ...(page.length < remaining.length ? { nextCursor: cursor({ version: 1, identity, sessionId: q.sessionId, provider: q.provider, upper, after: page.at(-1).sequence }, secret) } : {}) };
|
|
364
|
+
return ok(value);
|
|
365
|
+
});
|
|
366
|
+
},
|
|
367
|
+
getRemoteConversationDetailState() { const c = options.getConnectionState(); if (disposed || c.phase === 'disposed')
|
|
368
|
+
return { ...connectionIds(), revision, phase: 'disposed', sessions: [], need: need('DISPOSED') }; if (c.phase !== 'connected' || snapshot.instanceId !== c.instanceId || snapshot.epoch !== c.epoch)
|
|
369
|
+
return { ...connectionIds(), revision, phase: 'unavailable', sessions: [], need: need('CONNECTION_LOST') }; return structuredClone(snapshot); },
|
|
370
|
+
onRemoteConversationDetailState(listener) { listeners.add(listener); return () => { listeners.delete(listener); }; },
|
|
371
|
+
};
|
|
372
|
+
return { ...api, observeConnection(c) { if (disposed)
|
|
373
|
+
return; if (c.phase !== 'connected') {
|
|
374
|
+
publish(c.phase === 'disposed' ? 'disposed' : 'unavailable', [], c.phase === 'disposed' ? 'DISPOSED' : 'CONNECTION_LOST');
|
|
375
|
+
return;
|
|
376
|
+
} if (uncertain) {
|
|
377
|
+
publish('failed', [], 'REMOTE_DETAIL_STORAGE_FAILED');
|
|
378
|
+
return;
|
|
379
|
+
} publish('loading'); void serial(async () => { const before = start(), context = await checked(options.context(), before); if (!context.ok)
|
|
380
|
+
throw new NeedError(context.error); const identity = context.value.identity, domain = await prepare(identity, before); await stateRows(domain ? await rows(domain, before) : [], identity, before); return ok(undefined); }); }, async dispose() { disposed = true; await tail; publish('disposed', [], 'DISPOSED'); listeners.clear(); } };
|
|
381
|
+
}
|
package/lib/cordis.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import type { ConnectApi, ConnectState, ConnectionState, LoginPromptState, LoginPromptResult, EnrollmentState, WorkspaceBindingState } from './api.ts';
|
|
1
|
+
import type { ConnectApi, ConnectState, ConnectionState, LoginPromptState, LoginPromptResult, EnrollmentState, WorkspaceBindingState, RemoteConversationDetailState } from './api.ts';
|
|
2
2
|
declare module '@deepseek-ai/cordis' {
|
|
3
3
|
interface Context {
|
|
4
4
|
aiWorkbenchConnect: ConnectApi;
|
|
5
5
|
}
|
|
6
6
|
interface Events {
|
|
7
|
+
'ai-workbench/remote-conversation-detail-state': (state: RemoteConversationDetailState) => void;
|
|
7
8
|
'ai-workbench/enrollment-state': (state: EnrollmentState) => void;
|
|
8
9
|
'ai-workbench/workspace-binding-state': (state: WorkspaceBindingState) => void;
|
|
9
10
|
'ai-workbench/workspace-diagnostic': (error: unknown) => void;
|
package/lib/defaults.d.ts
CHANGED
package/lib/defaults.js
CHANGED
|
@@ -2,3 +2,4 @@
|
|
|
2
2
|
export const L_DEFAULTS = Object.freeze({ enrollmentMetadataMaxBytes: 1048576, commandOutputMaxBytes: 1048576, remoteKillGraceMs: 1000, componentDownloadMaxRedirects: 3, componentObservationIntervalMs: 1000, maxDownloadBytes: 200000000, maxExtractedBytes: 600000000, loginOutputMaxBytes: 1048576, ipcMaxBytes: 1048576 });
|
|
3
3
|
export const ELECTRON_PIN = Object.freeze({ source: 'https://github.com/electron/electron/releases/download/v44.0.0/electron-v44.0.0-darwin-arm64.zip', sha256: '076d79742986e1b100b69ebecc691cb07368045e54c9087cef631b8622b76a80', executable: 'Electron.app/Contents/MacOS/Electron' });
|
|
4
4
|
export const NETWORK_DEFAULTS = Object.freeze({ proxyListen: '127.0.0.1:0', logUpload: false, statusRefreshIntervalSeconds: 2, dialTimeoutSeconds: 20, headerTimeoutSeconds: 10, enrollmentObservationSeconds: 30, httpMaxHeaderBytes: 1048576, httpMaxResponseHeaderBytes: 1048576, startupObservationSeconds: 35, shutdownObservationSeconds: 10, controlResponseMaxBytes: 65536, downloadMaxRedirects: 3 });
|
|
5
|
+
export const CONVERSATION_DETAIL_DEFAULTS = Object.freeze({ maxEventBytes: 1048576, pageSize: 100 });
|
package/lib/index.d.ts
CHANGED
|
@@ -12,6 +12,10 @@ export { sshArgs } from './ssh.ts';
|
|
|
12
12
|
export declare const name = "ai-workbench-connect";
|
|
13
13
|
export declare const inject: string[];
|
|
14
14
|
export interface Config {
|
|
15
|
+
conversationDetails?: {
|
|
16
|
+
maxEventBytes?: number;
|
|
17
|
+
pageSize?: number;
|
|
18
|
+
};
|
|
15
19
|
inventory?: ComponentInventory;
|
|
16
20
|
enrollmentMetadataMaxBytes?: number;
|
|
17
21
|
enrollmentPackageDir?: string;
|
|
@@ -35,7 +39,7 @@ export interface Config {
|
|
|
35
39
|
};
|
|
36
40
|
}
|
|
37
41
|
export declare const Config: z<Config>;
|
|
38
|
-
export declare function register(ctx: Context, ports: Ports,
|
|
42
|
+
export declare function register(ctx: Context, ports: Ports, _inventory?: ComponentInventory, extend?: (api: import('./api.ts').ConnectApi) => void): void;
|
|
39
43
|
export declare function apply(ctx: Context, config?: Config): void;
|
|
40
44
|
export { createProfileEnrollment } from './profile-enrollment.ts';
|
|
41
45
|
export { createWorkspaceBindings } from './workspace-bindings.ts';
|
package/lib/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import z from '@deepseek-ai/schemastery';
|
|
2
|
+
import { createConversationDetailSettings } from "./conversation-detail-settings.js";
|
|
2
3
|
import { createConnect } from "./connect.js";
|
|
3
4
|
import { configuredPorts } from "./integration.js";
|
|
4
5
|
import { attachProfileRuntime } from "./profile-runtime.js";
|
|
5
6
|
import { profileSettings } from "./profile-settings.js";
|
|
6
7
|
import { createComponentSources, DEFAULT_SOURCE_REASON } from "./component-source.js";
|
|
7
8
|
import { createComponentProgress } from "./component-download.js";
|
|
8
|
-
import { L_DEFAULTS, NETWORK_DEFAULTS } from "./defaults.js";
|
|
9
|
+
import { L_DEFAULTS, NETWORK_DEFAULTS, CONVERSATION_DETAIL_DEFAULTS } from "./defaults.js";
|
|
9
10
|
import { registerComponents } from "./components.js";
|
|
10
11
|
export { writeNetworkConfig, createNetworkClient } from "./network.js";
|
|
11
12
|
export { createConnect } from "./connect.js";
|
|
@@ -17,25 +18,30 @@ const positive = () => z.number().step(1).min(1);
|
|
|
17
18
|
// aiWorkbenchNetwork itself is a required Cordis injection; missing N holds this module from loading.
|
|
18
19
|
// Own technical defaults use the approved settings record; no global DSH policy is changed.
|
|
19
20
|
export const Config = z.object({
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
conversationDetails: z.object({
|
|
22
|
+
maxEventBytes: positive().max(Number.MAX_SAFE_INTEGER).default(CONVERSATION_DETAIL_DEFAULTS.maxEventBytes).description('conversationDetails.maxEventBytes:完整UTF-8 JSON事件的字节上限;默认1048576,范围1至9007199254740991整数。改大增加单事件存储与内存;改小会拒收超过新值的新事件,不裁剪或删除旧记录。'),
|
|
23
|
+
pageSize: positive().max(Number.MAX_SAFE_INTEGER).default(CONVERSATION_DETAIL_DEFAULTS.pageSize).description('conversationDetails.pageSize:每页条数;默认100,范围1至9007199254740991整数。改大增加一次读取与界面负担,改小增加继续加载次数,不减少完整快照总数。'),
|
|
24
|
+
}).description('管理员诊断记录设置:不自动过期、不按条数淘汰、没有隐藏总条数上限,本版没有清除入口。'),
|
|
25
|
+
enrollmentMetadataMaxBytes: positive().max(2147483647).default(L_DEFAULTS.enrollmentMetadataMaxBytes).description('开通元数据、工作区关联文件、诊断设置文件及设置提交JSON的字节上限;默认1048576,可由管理员修改;改小可能拒绝设置读取/提交,不读取key内容'),
|
|
26
|
+
inventory: z.object({ network: z.string(), connect: z.string(), claude: z.string(), codex: z.string() }).description('由已安装组件的实际版本自动生成,只读信息,不作为功能开关'),
|
|
27
|
+
enrollmentPackageDir: z.string().description('连接资料目录:由管理员安装时配置,员工无需手填'),
|
|
28
|
+
trustDir: z.string().description('主机信任缓存目录:由管理员安装时配置'),
|
|
29
|
+
commandOutputMaxBytes: positive().default(L_DEFAULTS.commandOutputMaxBytes).description('远程命令输出内存上限(字节),默认1048576,管理员可修改'),
|
|
30
|
+
network: z.object({ executable: z.string(), runtimeDir: z.string(), stateDir: z.string(), configFile: z.string(), helperLogFile: z.string(), nodeName: z.string(), startupObservationSeconds: z.number().min(0.001).max(2147483.647).default(NETWORK_DEFAULTS.startupObservationSeconds), shutdownObservationSeconds: z.number().min(0.001).max(2147483.647).default(NETWORK_DEFAULTS.shutdownObservationSeconds), controlResponseMaxBytes: positive().default(NETWORK_DEFAULTS.controlResponseMaxBytes), downloadMaxRedirects: z.number().step(1).min(0).default(NETWORK_DEFAULTS.downloadMaxRedirects), proxyListen: z.string().default(NETWORK_DEFAULTS.proxyListen), logUpload: z.boolean().default(NETWORK_DEFAULTS.logUpload), statusRefreshIntervalSeconds: z.number().min(0.001).max(2147483.647).default(NETWORK_DEFAULTS.statusRefreshIntervalSeconds), dialTimeoutSeconds: z.number().min(1e-9).default(NETWORK_DEFAULTS.dialTimeoutSeconds), headerTimeoutSeconds: z.number().min(0.001).max(2147483.647).default(NETWORK_DEFAULTS.headerTimeoutSeconds), enrollmentObservationSeconds: z.number().min(1e-9).default(NETWORK_DEFAULTS.enrollmentObservationSeconds), httpMaxHeaderBytes: positive().default(NETWORK_DEFAULTS.httpMaxHeaderBytes), httpMaxResponseHeaderBytes: positive().default(NETWORK_DEFAULTS.httpMaxResponseHeaderBytes), controlMaxBytes: positive() }).description('远程网络连接设置:使用默认技术参数,管理员可修改;路径根据当前用户和应用配置目录生成,员工无需填写'),
|
|
26
31
|
helper: z.object({
|
|
27
|
-
componentDownloadSource: z.union([z.const('github'), z.const('npmmirror')]).description('未保存设置节选择时的初始下载源;未设且无旧source时默认npmmirror
|
|
32
|
+
componentDownloadSource: z.union([z.const('github'), z.const('npmmirror')]).description('未保存设置节选择时的初始下载源;未设且无旧source时默认npmmirror(已验证可达的中国镜像),可选github;不自动测速/换源,版本与SHA固定'),
|
|
28
33
|
componentObservationIntervalMs: z.number().step(1).min(1).max(2147483647).default(L_DEFAULTS.componentObservationIntervalMs).description('无进展观察刷新间隔毫秒,默认1000;只刷新秒数,不超时/重试/换源'),
|
|
29
|
-
componentDownloadMaxRedirects: z.number().step(1).min(0).default(L_DEFAULTS.componentDownloadMaxRedirects).description('组件直接HTTPS下载的重定向次数上限;默认3,0
|
|
30
|
-
remoteKillGraceMs: z.number().step(1).min(0).max(2147483647).default(L_DEFAULTS.remoteKillGraceMs).description('
|
|
31
|
-
maxRedirects: z.number().step(1).min(0).description('
|
|
32
|
-
source: z.string().description('兼容初始管理员显式URL;无已保存的设置节选择且未设componentDownloadSource时生效;无隐式换源'), sha256: z.string().description('darwin-arm64
|
|
33
|
-
cacheDir: z.string().description('默认从当前profile自动派生ai-workbench-connect/login-component-cache;管理员可改,员工不填路径'), maxDownloadBytes: positive().default(L_DEFAULTS.maxDownloadBytes).description('
|
|
34
|
-
executable: z.string().description('darwin-arm64默认Electron.app/Contents/MacOS/Electron;其他平台无默认,管理员可改'), loginOutputMaxBytes: positive().default(L_DEFAULTS.loginOutputMaxBytes).description('
|
|
34
|
+
componentDownloadMaxRedirects: z.number().step(1).min(0).default(L_DEFAULTS.componentDownloadMaxRedirects).description('组件直接HTTPS下载的重定向次数上限;默认3,0拒绝重定向;管理员可改,不改变网页登录网络'),
|
|
35
|
+
remoteKillGraceMs: z.number().step(1).min(0).max(2147483647).default(L_DEFAULTS.remoteKillGraceMs).description('远端本轮登录进程组结束宽限毫秒;默认1000,0立即KILL,管理员可改'),
|
|
36
|
+
maxRedirects: z.number().step(1).min(0).description('兼容旧配置;当前由网络连接组件的downloadMaxRedirects设置控制,此项不再生效'),
|
|
37
|
+
source: z.string().description('兼容初始管理员显式URL;无已保存的设置节选择且未设componentDownloadSource时生效;无隐式换源'), sha256: z.string().description('darwin-arm64默认使用Electron 44.0.0对应的下载SHA256;管理员可改,必须校验'),
|
|
38
|
+
cacheDir: z.string().description('默认从当前profile自动派生ai-workbench-connect/login-component-cache;管理员可改,员工不填路径'), maxDownloadBytes: positive().default(L_DEFAULTS.maxDownloadBytes).description('下载字节上限;默认200000000,管理员可改'), maxExtractedBytes: positive().default(L_DEFAULTS.maxExtractedBytes).description('解压字节上限;默认600000000,管理员可改'),
|
|
39
|
+
executable: z.string().description('darwin-arm64默认Electron.app/Contents/MacOS/Electron;其他平台无默认,管理员可改'), loginOutputMaxBytes: positive().default(L_DEFAULTS.loginOutputMaxBytes).description('登录进程输出字节上限;默认1048576,管理员可改'), ipcMaxBytes: positive().default(L_DEFAULTS.ipcMaxBytes).description('私有IPC字节上限;默认1048576,管理员可改'),
|
|
35
40
|
}),
|
|
36
41
|
});
|
|
37
|
-
export function register(ctx, ports,
|
|
38
|
-
|
|
42
|
+
export function register(ctx, ports, _inventory, extend) {
|
|
43
|
+
// 当前独立插件始终检查两品牌;保留旧inventory参数兼容调用,但不以交付清单开关能力。
|
|
44
|
+
const api = createConnect(ports, state => ctx.emit('ai-workbench/connect-state', state), { onConnectionState: state => ctx.emit('ai-workbench/connection-state', state), onPromptState: state => ctx.emit('ai-workbench/login-prompt-state', state), onPromptResult: result => ctx.emit('ai-workbench/login-prompt-result', result) });
|
|
39
45
|
extend?.(api);
|
|
40
46
|
ctx.provide('aiWorkbenchConnect', api);
|
|
41
47
|
ctx.effect(() => { void api.refreshLoginPrompts(); return () => api.dispose(); });
|
|
@@ -43,12 +49,15 @@ export function register(ctx, ports, inventory, extend) {
|
|
|
43
49
|
export function apply(ctx, config = {}) {
|
|
44
50
|
const sourceReason = config.helper?.componentDownloadSource !== undefined ? '管理员通过 helper.componentDownloadSource 指定此下载源。' : config.helper?.source !== undefined ? '管理员通过 helper.source 指定此下载源。' : DEFAULT_SOURCE_REASON;
|
|
45
51
|
config = profileSettings(config, ctx.get('profileContext')?.dir);
|
|
52
|
+
const detailSettings = createConversationDetailSettings({ profileDir: ctx.get('profileContext')?.dir, initial: config.conversationDetails, metadataMaxBytes: config.enrollmentMetadataMaxBytes });
|
|
53
|
+
ctx.provide('aiWorkbenchConversationDetailSettings', detailSettings);
|
|
54
|
+
ctx.effect(() => () => detailSettings.dispose());
|
|
46
55
|
const network = ctx.get('aiWorkbenchNetwork'), component = createComponentProgress();
|
|
47
56
|
const sources = createComponentSources({ profileDir: ctx.get('profileContext')?.dir, metadataMaxBytes: config.enrollmentMetadataMaxBytes ?? L_DEFAULTS.enrollmentMetadataMaxBytes, source: config.helper?.source, sha256: config.helper?.sha256, reason: sourceReason, busy: () => ['downloading', 'verifying', 'unpacking'].includes(component.getState().phase) || ctx.get('aiWorkbenchConnect')?.getLoginPromptState().phase === 'authorizing' });
|
|
48
57
|
ctx.effect(() => () => sources.dispose());
|
|
49
58
|
const ports = configuredPorts(config, network, component, sources);
|
|
50
59
|
register(ctx, ports, config.inventory, api => attachProfileRuntime(ctx, api, ports, config, network));
|
|
51
|
-
registerComponents(ctx, config.inventory, config.helper?.cacheDir, component, sources, config.helper?.componentObservationIntervalMs);
|
|
60
|
+
registerComponents(ctx, config.inventory, config.helper?.cacheDir, component, sources, config.helper?.componentObservationIntervalMs, config.enrollmentMetadataMaxBytes ?? L_DEFAULTS.enrollmentMetadataMaxBytes);
|
|
52
61
|
}
|
|
53
62
|
export { createProfileEnrollment } from "./profile-enrollment.js";
|
|
54
63
|
export { createWorkspaceBindings } from "./workspace-bindings.js";
|
package/lib/integration.js
CHANGED
|
@@ -9,7 +9,8 @@ import { remoteLoginCommand } from "./remote-login.js";
|
|
|
9
9
|
import { runLogin } from "./login.js";
|
|
10
10
|
import { ensureHelper } from "./cache.js";
|
|
11
11
|
import { launchElectron } from "./electron.js";
|
|
12
|
-
import {
|
|
12
|
+
import { parseLoginStatus } from "./auth-status.js";
|
|
13
|
+
import { need, fail, ok } from "./needs.js";
|
|
13
14
|
import { createComponentProgress, downloadComponent } from "./component-download.js";
|
|
14
15
|
export function configuredPorts(config, provider, component = createComponentProgress(), sources) {
|
|
15
16
|
const ports = {};
|
|
@@ -45,6 +46,24 @@ export function configuredPorts(config, provider, component = createComponentPro
|
|
|
45
46
|
return result;
|
|
46
47
|
if (signal.aborted)
|
|
47
48
|
return fail('LOGIN_FAILED');
|
|
49
|
+
// Component preparation may outlive another device's completed login. Recheck immediately before any destructive CLI authorization.
|
|
50
|
+
if (!ports.ssh)
|
|
51
|
+
return fail('AUTH_STATUS_INVALID');
|
|
52
|
+
try {
|
|
53
|
+
const raw = await ports.ssh.status(target, provider, signal);
|
|
54
|
+
if (signal.aborted)
|
|
55
|
+
return fail('CONNECTION_LOST');
|
|
56
|
+
if (!raw.ok)
|
|
57
|
+
return raw;
|
|
58
|
+
const current = parseLoginStatus(provider, raw.value);
|
|
59
|
+
if (!current.ok)
|
|
60
|
+
return current;
|
|
61
|
+
if (current.value.loggedIn)
|
|
62
|
+
return ok(undefined);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return fail('AUTH_STATUS_INVALID');
|
|
66
|
+
}
|
|
48
67
|
return runLogin(target, provider, {
|
|
49
68
|
start: (t, p, s) => createRemoteLogin('ssh', [...sshArgs(t), remoteLoginCommand(p, h.remoteKillGraceMs)], p, h.loginOutputMaxBytes, s, 'remote-control'),
|
|
50
69
|
acquireProxy: (_t, s) => network.acquireProxy(s),
|
package/lib/login.d.ts
CHANGED
|
@@ -6,11 +6,14 @@ export interface ProxyLease {
|
|
|
6
6
|
closed?: Promise<void>;
|
|
7
7
|
release(): Promise<void>;
|
|
8
8
|
}
|
|
9
|
-
export
|
|
10
|
-
provider:
|
|
9
|
+
export type LoginBoot = {
|
|
10
|
+
provider: 'claude';
|
|
11
11
|
url: string;
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
} | {
|
|
13
|
+
provider: 'codex';
|
|
14
|
+
url: string;
|
|
15
|
+
deviceCode: string;
|
|
16
|
+
};
|
|
14
17
|
export interface RemoteLogin {
|
|
15
18
|
ownership?: Promise<{
|
|
16
19
|
pid: number;
|
|
@@ -18,6 +21,7 @@ export interface RemoteLogin {
|
|
|
18
21
|
provider: string;
|
|
19
22
|
}>;
|
|
20
23
|
url: Promise<Result<string>>;
|
|
24
|
+
deviceCode?: Promise<Result<string>>;
|
|
21
25
|
completion: Promise<Result<void>>;
|
|
22
26
|
writeCode(code: string): Promise<void>;
|
|
23
27
|
close(): Promise<void>;
|