@agent-link/agent 0.1.326 → 0.1.329
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/backends/index.js +2 -0
- package/dist/backends/index.js.map +1 -1
- package/dist/backends/types.d.ts +71 -21
- package/dist/backends/types.js +93 -1
- package/dist/backends/types.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/connection.js +324 -43
- package/dist/connection.js.map +1 -1
- package/dist/history-message-transform.d.ts +3 -0
- package/dist/history-message-transform.js.map +1 -1
- package/dist/pi.d.ts +4 -0
- package/dist/pi.js +234 -4
- package/dist/pi.js.map +1 -1
- package/dist/provider-command-discovery.d.ts +25 -0
- package/dist/provider-command-discovery.js +42 -0
- package/dist/provider-command-discovery.js.map +1 -0
- package/dist/provider-command-types.d.ts +23 -0
- package/dist/provider-command-types.js +13 -0
- package/dist/provider-command-types.js.map +1 -0
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +17 -1
- package/dist/providers.js.map +1 -1
- package/dist/runtime.d.ts +2 -0
- package/dist/runtime.js +5 -0
- package/dist/runtime.js.map +1 -1
- package/dist/sdk.d.ts +2 -1
- package/dist/sdk.js +8 -1
- package/dist/sdk.js.map +1 -1
- package/dist/yeaft.d.ts +105 -0
- package/dist/yeaft.js +1334 -0
- package/dist/yeaft.js.map +1 -0
- package/package.json +1 -1
package/dist/yeaft.js
ADDED
|
@@ -0,0 +1,1334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yeaft CLI process driver for AgentLink.
|
|
3
|
+
*
|
|
4
|
+
* MVP integration:
|
|
5
|
+
* - Each conversation owns one `yeaft --input-format stream-json --output-format stream-json` process
|
|
6
|
+
* - User turns are written to stdin as JSONL prompt messages
|
|
7
|
+
* - Yeaft session ids are generated with the required `session_...` shape
|
|
8
|
+
* - JSONL stdout is translated into backend_event messages
|
|
9
|
+
* - Session history is read best-effort from ~/.yeaft/sessions and <workDir>/.yeaft/sessions
|
|
10
|
+
*/
|
|
11
|
+
import { spawn } from 'child_process';
|
|
12
|
+
import { createInterface } from 'readline';
|
|
13
|
+
import { readdirSync, readFileSync, writeFileSync, existsSync, rmSync, mkdirSync, realpathSync, statSync } from 'fs';
|
|
14
|
+
import { dirname, join, resolve } from 'path';
|
|
15
|
+
import { homedir, platform } from 'os';
|
|
16
|
+
import { randomBytes } from 'crypto';
|
|
17
|
+
import { CONFIG_DIR } from './config.js';
|
|
18
|
+
import { getCleanEnv, resetCachedExecutablePath, resolveExecutablePath } from './sdk.js';
|
|
19
|
+
import { getProvider, resolveProviderCommand } from './providers.js';
|
|
20
|
+
import { yeaftCapabilities } from './backends/types.js';
|
|
21
|
+
const conversations = new Map();
|
|
22
|
+
const sendFns = new Set();
|
|
23
|
+
const MODEL_PATTERN = /^[a-zA-Z0-9._:/-]+$/;
|
|
24
|
+
const EFFORT_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
25
|
+
const VP_ID_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
26
|
+
const SESSION_ID_PATTERN = /^session_[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
27
|
+
const TURN_HEARTBEAT_INTERVAL_MS = 15_000;
|
|
28
|
+
const YEAFT_SESSION_LIST_LIMIT = 150;
|
|
29
|
+
const YEAFT_TITLES_FILE = join(CONFIG_DIR, 'yeaft-session-titles.json');
|
|
30
|
+
function generateTurnId() {
|
|
31
|
+
return `turn_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
32
|
+
}
|
|
33
|
+
function generateYeaftSessionId(conversationId) {
|
|
34
|
+
const slug = (conversationId || 'web')
|
|
35
|
+
.toLowerCase()
|
|
36
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
37
|
+
.replace(/^-+|-+$/g, '')
|
|
38
|
+
.slice(0, 24) || 'web';
|
|
39
|
+
return `session_agentlink_${slug}_${randomBytes(5).toString('hex')}`;
|
|
40
|
+
}
|
|
41
|
+
function validYeaftSessionId(value) {
|
|
42
|
+
return typeof value === 'string' && SESSION_ID_PATTERN.test(value) && value.length <= 128;
|
|
43
|
+
}
|
|
44
|
+
function sendFn(msg) {
|
|
45
|
+
for (const fn of sendFns) {
|
|
46
|
+
try {
|
|
47
|
+
fn(msg);
|
|
48
|
+
}
|
|
49
|
+
catch { /* ignore */ }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function addYeaftSendFn(fn) {
|
|
53
|
+
sendFns.add(fn);
|
|
54
|
+
return () => { sendFns.delete(fn); };
|
|
55
|
+
}
|
|
56
|
+
function sessionRef(sessionId) {
|
|
57
|
+
return {
|
|
58
|
+
backendType: 'yeaft',
|
|
59
|
+
backendSessionId: sessionId,
|
|
60
|
+
backendThreadId: sessionId,
|
|
61
|
+
providerSessionId: sessionId || null,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function startYeaftTurn(state) {
|
|
65
|
+
state.turnActive = true;
|
|
66
|
+
state.turnId = generateTurnId();
|
|
67
|
+
state.stateVersion++;
|
|
68
|
+
state.startedToolIds.clear();
|
|
69
|
+
state.pendingToolInputs.clear();
|
|
70
|
+
state.lastUsage = null;
|
|
71
|
+
sendFn({
|
|
72
|
+
type: 'turn_started',
|
|
73
|
+
conversationId: state.conversationId,
|
|
74
|
+
claudeSessionId: state.yeaftSessionId,
|
|
75
|
+
turnId: state.turnId,
|
|
76
|
+
stateVersion: state.stateVersion,
|
|
77
|
+
});
|
|
78
|
+
if (!state.heartbeatTimer) {
|
|
79
|
+
state.heartbeatTimer = setInterval(() => {
|
|
80
|
+
if (!state.turnActive) {
|
|
81
|
+
if (state.heartbeatTimer)
|
|
82
|
+
clearInterval(state.heartbeatTimer);
|
|
83
|
+
state.heartbeatTimer = null;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
sendFn({
|
|
87
|
+
type: 'turn_heartbeat',
|
|
88
|
+
conversationId: state.conversationId,
|
|
89
|
+
claudeSessionId: state.yeaftSessionId,
|
|
90
|
+
turnId: state.turnId,
|
|
91
|
+
stateVersion: state.stateVersion,
|
|
92
|
+
phase: 'running',
|
|
93
|
+
lastActivityAt: Date.now(),
|
|
94
|
+
});
|
|
95
|
+
}, TURN_HEARTBEAT_INTERVAL_MS);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function finishYeaftTurn(state) {
|
|
99
|
+
state.turnActive = false;
|
|
100
|
+
state.stateVersion++;
|
|
101
|
+
state.startedToolIds.clear();
|
|
102
|
+
state.pendingToolInputs.clear();
|
|
103
|
+
if (state.heartbeatTimer) {
|
|
104
|
+
clearInterval(state.heartbeatTimer);
|
|
105
|
+
state.heartbeatTimer = null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function emitYeaftCancelled(conversationId, state) {
|
|
109
|
+
sendFn({
|
|
110
|
+
type: 'backend_event',
|
|
111
|
+
conversationId,
|
|
112
|
+
event: { type: 'turn_cancelled', session: sessionRef(state?.yeaftSessionId || '') },
|
|
113
|
+
});
|
|
114
|
+
sendFn({
|
|
115
|
+
type: 'execution_cancelled',
|
|
116
|
+
conversationId,
|
|
117
|
+
claudeSessionId: state?.yeaftSessionId || null,
|
|
118
|
+
turnId: state?.turnId || null,
|
|
119
|
+
stateVersion: state?.stateVersion || 0,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function buildYeaftUsage(state, rawUsage) {
|
|
123
|
+
return {
|
|
124
|
+
provider: 'yeaft',
|
|
125
|
+
model: state.model || textFromUnknown(rawUsage?.model) || undefined,
|
|
126
|
+
reasoningEffort: state.effort || undefined,
|
|
127
|
+
inputTokens: numberOrUndefined(rawUsage?.input_tokens),
|
|
128
|
+
outputTokens: numberOrUndefined(rawUsage?.output_tokens),
|
|
129
|
+
cacheReadInputTokens: numberOrUndefined(rawUsage?.cache_read_input_tokens),
|
|
130
|
+
cacheCreationInputTokens: numberOrUndefined(rawUsage?.cache_creation_input_tokens),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function numberOrUndefined(value) {
|
|
134
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
135
|
+
}
|
|
136
|
+
function textFromUnknown(value) {
|
|
137
|
+
return typeof value === 'string' ? value : '';
|
|
138
|
+
}
|
|
139
|
+
function stringArrayFromUnknown(value) {
|
|
140
|
+
if (!Array.isArray(value))
|
|
141
|
+
return [];
|
|
142
|
+
return value.filter((item) => typeof item === 'string' && item.length > 0);
|
|
143
|
+
}
|
|
144
|
+
function scopeFromYeaftEvent(event) {
|
|
145
|
+
const vpId = textFromUnknown(event.vpId) || textFromUnknown(event.vp_id);
|
|
146
|
+
const threadId = textFromUnknown(event.threadId) || textFromUnknown(event.thread_id);
|
|
147
|
+
return {
|
|
148
|
+
...(vpId ? { vpId } : {}),
|
|
149
|
+
...(threadId ? { threadId } : {}),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function sendYeaftSetupError(state, message) {
|
|
153
|
+
sendFn({ type: 'backend_event', conversationId: state.conversationId, event: { type: 'error', message } });
|
|
154
|
+
sendFn({ type: 'backend_event', conversationId: state.conversationId, event: { type: 'turn_completed', session: sessionRef(state.yeaftSessionId || '') } });
|
|
155
|
+
sendFn({
|
|
156
|
+
type: 'turn_completed',
|
|
157
|
+
conversationId: state.conversationId,
|
|
158
|
+
workDir: state.workDir,
|
|
159
|
+
claudeSessionId: state.yeaftSessionId,
|
|
160
|
+
turnId: state.turnId,
|
|
161
|
+
stateVersion: state.stateVersion,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
let resolvedYeaftPath = null;
|
|
165
|
+
function resolveCmdWrapperDirectExecution(cmdPath) {
|
|
166
|
+
if (platform() !== 'win32')
|
|
167
|
+
return null;
|
|
168
|
+
if (!cmdPath.toLowerCase().endsWith('.cmd'))
|
|
169
|
+
return null;
|
|
170
|
+
try {
|
|
171
|
+
const content = readFileSync(cmdPath, 'utf-8');
|
|
172
|
+
const jsMatch = content.match(/%dp0%\\(.+?\.js)"/i) ||
|
|
173
|
+
content.match(/%dp0%\\(.+?\.js)/i);
|
|
174
|
+
if (jsMatch) {
|
|
175
|
+
const scriptPath = join(dirname(cmdPath), jsMatch[1].replace(/\\/g, '/'));
|
|
176
|
+
if (existsSync(scriptPath)) {
|
|
177
|
+
return { command: process.execPath, prefixArgs: [scriptPath] };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const exeMatch = content.match(/%dp0%\\(.+?\.exe)"/i) ||
|
|
181
|
+
content.match(/%dp0%\\(.+?\.exe)/i);
|
|
182
|
+
if (exeMatch) {
|
|
183
|
+
const exePath = join(dirname(cmdPath), exeMatch[1].replace(/\\/g, '/'));
|
|
184
|
+
if (existsSync(exePath)) {
|
|
185
|
+
return { command: exePath, prefixArgs: [] };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
function resolveYeaftPath() {
|
|
195
|
+
if (resolvedYeaftPath)
|
|
196
|
+
return resolvedYeaftPath;
|
|
197
|
+
resolvedYeaftPath = resolveExecutablePath('yeaft') || 'yeaft';
|
|
198
|
+
console.log(`[Yeaft] Resolved command: ${resolvedYeaftPath}`);
|
|
199
|
+
return resolvedYeaftPath;
|
|
200
|
+
}
|
|
201
|
+
export function _resetResolvedYeaftPathForTests() {
|
|
202
|
+
resolvedYeaftPath = null;
|
|
203
|
+
resetCachedExecutablePath('yeaft');
|
|
204
|
+
}
|
|
205
|
+
export function getYeaftConversation(conversationId) {
|
|
206
|
+
return conversations.get(conversationId);
|
|
207
|
+
}
|
|
208
|
+
export function getYeaftConversations() {
|
|
209
|
+
return conversations;
|
|
210
|
+
}
|
|
211
|
+
export function rebindYeaftConversation(yeaftSessionId, newConvId) {
|
|
212
|
+
for (const [oldConvId, state] of conversations) {
|
|
213
|
+
if ((state.yeaftSessionId === yeaftSessionId || state.lastYeaftSessionId === yeaftSessionId) && oldConvId !== newConvId) {
|
|
214
|
+
conversations.delete(oldConvId);
|
|
215
|
+
state.conversationId = newConvId;
|
|
216
|
+
conversations.set(newConvId, state);
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
export function createYeaftPlaceholder(conversationId, opts) {
|
|
223
|
+
if (conversations.has(conversationId))
|
|
224
|
+
return;
|
|
225
|
+
conversations.set(conversationId, {
|
|
226
|
+
conversationId,
|
|
227
|
+
child: null,
|
|
228
|
+
abortController: null,
|
|
229
|
+
yeaftSessionId: null,
|
|
230
|
+
lastYeaftSessionId: null,
|
|
231
|
+
workDir: process.cwd(),
|
|
232
|
+
turnActive: false,
|
|
233
|
+
providerId: opts?.providerId || 'yeaft',
|
|
234
|
+
model: null,
|
|
235
|
+
effort: null,
|
|
236
|
+
vps: [],
|
|
237
|
+
defaultVpId: null,
|
|
238
|
+
turnId: null,
|
|
239
|
+
stateVersion: 0,
|
|
240
|
+
heartbeatTimer: null,
|
|
241
|
+
sessionNotified: false,
|
|
242
|
+
startedToolIds: new Set(),
|
|
243
|
+
pendingToolInputs: new Map(),
|
|
244
|
+
lastUsage: null,
|
|
245
|
+
stderrBuf: '',
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
export function restartYeaftConversation(conversationId, opts) {
|
|
249
|
+
const state = conversations.get(conversationId);
|
|
250
|
+
const wasTurnActive = state?.turnActive || false;
|
|
251
|
+
const yeaftSessionId = state?.yeaftSessionId || null;
|
|
252
|
+
if (state)
|
|
253
|
+
cleanupYeaftConversation(conversationId);
|
|
254
|
+
createYeaftPlaceholder(conversationId, { providerId: opts?.providerId || state?.providerId || 'yeaft' });
|
|
255
|
+
const next = conversations.get(conversationId);
|
|
256
|
+
next.lastYeaftSessionId = yeaftSessionId;
|
|
257
|
+
next.workDir = state?.workDir || process.cwd();
|
|
258
|
+
next.model = opts?.model || state?.model || null;
|
|
259
|
+
next.effort = opts?.effort || state?.effort || null;
|
|
260
|
+
next.vps = opts?.vps || state?.vps || [];
|
|
261
|
+
next.defaultVpId = next.vps.includes(state?.defaultVpId || '')
|
|
262
|
+
? state.defaultVpId
|
|
263
|
+
: next.vps[0] || null;
|
|
264
|
+
next.stateVersion = (state?.stateVersion || 0) + 1;
|
|
265
|
+
return { wasTurnActive, yeaftSessionId };
|
|
266
|
+
}
|
|
267
|
+
function normalizeYeaftRunOptions(input) {
|
|
268
|
+
const options = {};
|
|
269
|
+
if (!input)
|
|
270
|
+
return { options };
|
|
271
|
+
if (input.model != null && input.model !== '') {
|
|
272
|
+
const model = input.model.trim();
|
|
273
|
+
if (model.length > 120 || !MODEL_PATTERN.test(model))
|
|
274
|
+
return { options, error: `Invalid Yeaft model name: ${input.model}` };
|
|
275
|
+
options.model = model;
|
|
276
|
+
}
|
|
277
|
+
if (input.effort != null && input.effort !== '' && input.effort !== 'default') {
|
|
278
|
+
const effort = input.effort.trim();
|
|
279
|
+
if (effort.length > 64 || !EFFORT_PATTERN.test(effort))
|
|
280
|
+
return { options, error: `Invalid Yeaft effort value: ${input.effort}` };
|
|
281
|
+
options.effort = effort;
|
|
282
|
+
}
|
|
283
|
+
const rawVps = Array.isArray(input.vps) ? input.vps : [];
|
|
284
|
+
const legacyVp = typeof input.vp?.id === 'string' ? input.vp.id : '';
|
|
285
|
+
const ids = [...rawVps, ...(legacyVp ? [legacyVp] : [])];
|
|
286
|
+
const vps = [];
|
|
287
|
+
const seen = new Set();
|
|
288
|
+
for (const raw of ids) {
|
|
289
|
+
const id = typeof raw === 'string' ? raw.trim() : '';
|
|
290
|
+
if (!id)
|
|
291
|
+
continue;
|
|
292
|
+
if (id.length > 64 || !VP_ID_PATTERN.test(id))
|
|
293
|
+
return { options, error: 'Invalid Yeaft VP id' };
|
|
294
|
+
if (seen.has(id))
|
|
295
|
+
continue;
|
|
296
|
+
seen.add(id);
|
|
297
|
+
vps.push(id);
|
|
298
|
+
}
|
|
299
|
+
if (vps.length)
|
|
300
|
+
options.vps = vps;
|
|
301
|
+
return { options };
|
|
302
|
+
}
|
|
303
|
+
export function handleYeaftChat(conversationId, text, workDir, options, files) {
|
|
304
|
+
let state = conversations.get(conversationId);
|
|
305
|
+
if (!state) {
|
|
306
|
+
createYeaftPlaceholder(conversationId, { providerId: options?.providerId || 'yeaft' });
|
|
307
|
+
state = conversations.get(conversationId);
|
|
308
|
+
}
|
|
309
|
+
state.workDir = workDir;
|
|
310
|
+
if (options?.providerId)
|
|
311
|
+
state.providerId = options.providerId;
|
|
312
|
+
const resumeId = options?.resumeSessionId || state.yeaftSessionId || state.lastYeaftSessionId || generateYeaftSessionId(conversationId);
|
|
313
|
+
if (!validYeaftSessionId(resumeId)) {
|
|
314
|
+
sendYeaftSetupError(state, `Invalid Yeaft session id: ${resumeId}`);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
state.yeaftSessionId = resumeId;
|
|
318
|
+
state.lastYeaftSessionId = resumeId;
|
|
319
|
+
const normalized = normalizeYeaftRunOptions(options?.yeaftOptions);
|
|
320
|
+
if (normalized.error) {
|
|
321
|
+
sendYeaftSetupError(state, normalized.error);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (normalized.options.model)
|
|
325
|
+
state.model = normalized.options.model;
|
|
326
|
+
if (normalized.options.effort)
|
|
327
|
+
state.effort = normalized.options.effort;
|
|
328
|
+
if (normalized.options.vps) {
|
|
329
|
+
state.vps = normalized.options.vps;
|
|
330
|
+
if (!state.defaultVpId || !state.vps.includes(state.defaultVpId)) {
|
|
331
|
+
state.defaultVpId = null;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
persistYeaftSessionMetadata(state);
|
|
335
|
+
if (files?.length) {
|
|
336
|
+
sendYeaftSetupError(state, 'Yeaft attachments are not supported yet.');
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (!state.child) {
|
|
340
|
+
startYeaftProcess(state);
|
|
341
|
+
}
|
|
342
|
+
if (!state.child?.stdin || state.child.stdin.destroyed) {
|
|
343
|
+
sendYeaftSetupError(state, 'Yeaft process is not accepting input.');
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
startYeaftTurn(state);
|
|
347
|
+
sendFn({
|
|
348
|
+
type: 'backend_event',
|
|
349
|
+
conversationId,
|
|
350
|
+
event: { type: 'turn_started', session: sessionRef(state.yeaftSessionId), turnId: state.turnId || undefined },
|
|
351
|
+
});
|
|
352
|
+
const userPayload = {
|
|
353
|
+
type: 'user',
|
|
354
|
+
prompt: text,
|
|
355
|
+
...(state.vps.length ? {
|
|
356
|
+
vps: state.vps,
|
|
357
|
+
vp: { id: state.defaultVpId || state.vps[0] },
|
|
358
|
+
vpId: state.defaultVpId || state.vps[0],
|
|
359
|
+
roster: state.vps,
|
|
360
|
+
defaultVpId: state.defaultVpId || state.vps[0],
|
|
361
|
+
} : {}),
|
|
362
|
+
};
|
|
363
|
+
state.child.stdin.write(JSON.stringify(userPayload) + '\n');
|
|
364
|
+
}
|
|
365
|
+
function persistYeaftSessionMetadata(state) {
|
|
366
|
+
if (!state.yeaftSessionId || !state.workDir)
|
|
367
|
+
return;
|
|
368
|
+
const sessionDir = join(homedir(), '.yeaft', 'sessions', state.yeaftSessionId);
|
|
369
|
+
const sessionFile = join(sessionDir, 'session.json');
|
|
370
|
+
const legacySessionFile = join(sessionDir, 'group.json');
|
|
371
|
+
try {
|
|
372
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
373
|
+
const canonical = readJsonObject(sessionFile) || {};
|
|
374
|
+
const legacy = readJsonObject(legacySessionFile) || {};
|
|
375
|
+
const canonicalIsFormal = typeof canonical.id === 'string' && Array.isArray(canonical.roster);
|
|
376
|
+
const recoveredRoster = canonicalIsFormal
|
|
377
|
+
? canonical.roster
|
|
378
|
+
: [...new Set([
|
|
379
|
+
...stringArrayFromUnknown(legacy.roster),
|
|
380
|
+
...stringArrayFromUnknown(canonical.roster),
|
|
381
|
+
])];
|
|
382
|
+
const existing = {
|
|
383
|
+
...legacy,
|
|
384
|
+
...canonical,
|
|
385
|
+
roster: recoveredRoster,
|
|
386
|
+
...(!canonicalIsFormal
|
|
387
|
+
&& typeof canonical.defaultVpId !== 'string'
|
|
388
|
+
&& typeof legacy.defaultVpId === 'string'
|
|
389
|
+
? { defaultVpId: legacy.defaultVpId }
|
|
390
|
+
: {}),
|
|
391
|
+
};
|
|
392
|
+
const now = new Date().toISOString();
|
|
393
|
+
const existingRoster = stringArrayFromUnknown(existing.roster);
|
|
394
|
+
const roster = state.vps.length ? state.vps : existingRoster;
|
|
395
|
+
if (!state.vps.length && roster.length)
|
|
396
|
+
state.vps = roster;
|
|
397
|
+
const existingDefaultVpId = textFromUnknown(existing.defaultVpId);
|
|
398
|
+
const defaultVpId = state.defaultVpId && roster.includes(state.defaultVpId)
|
|
399
|
+
? state.defaultVpId
|
|
400
|
+
: canonicalIsFormal
|
|
401
|
+
&& Object.prototype.hasOwnProperty.call(canonical, 'defaultVpId')
|
|
402
|
+
&& canonical.defaultVpId === null
|
|
403
|
+
? null
|
|
404
|
+
: roster.includes(existingDefaultVpId)
|
|
405
|
+
? existingDefaultVpId
|
|
406
|
+
: roster[0] || null;
|
|
407
|
+
state.defaultVpId = defaultVpId;
|
|
408
|
+
let workspaceKey = '';
|
|
409
|
+
try {
|
|
410
|
+
workspaceKey = realpathSync(resolve(state.workDir));
|
|
411
|
+
}
|
|
412
|
+
catch { /* keep empty */ }
|
|
413
|
+
const next = {
|
|
414
|
+
...existing,
|
|
415
|
+
id: state.yeaftSessionId,
|
|
416
|
+
name: textFromUnknown(existing.name) || state.yeaftSessionId,
|
|
417
|
+
workDir: state.workDir,
|
|
418
|
+
workspaceKey,
|
|
419
|
+
announcement: textFromUnknown(existing.announcement),
|
|
420
|
+
providerId: state.providerId,
|
|
421
|
+
sessionId: state.yeaftSessionId,
|
|
422
|
+
roster,
|
|
423
|
+
defaultVpId,
|
|
424
|
+
createdAt: textFromUnknown(existing.createdAt) || now,
|
|
425
|
+
metadataUpdatedAt: now,
|
|
426
|
+
};
|
|
427
|
+
writeFileSync(sessionFile, JSON.stringify(next, null, 2) + '\n', 'utf-8');
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
// Best-effort only. Yeaft can still run; this metadata powers AgentLink's
|
|
431
|
+
// directory-scoped session lists when Yeaft stores history under ~/.yeaft.
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
function startYeaftProcess(state) {
|
|
435
|
+
const args = [
|
|
436
|
+
'--input-format', 'stream-json',
|
|
437
|
+
'--output-format', 'stream-json',
|
|
438
|
+
'--session-id', state.yeaftSessionId,
|
|
439
|
+
'--cwd', state.workDir,
|
|
440
|
+
];
|
|
441
|
+
if (state.model)
|
|
442
|
+
args.push('--model', state.model);
|
|
443
|
+
if (state.effort)
|
|
444
|
+
args.push('--effort', state.effort);
|
|
445
|
+
const provider = getProvider(state.providerId);
|
|
446
|
+
const { command, prefixArgs, spawnOpts } = state.providerId === 'yeaft'
|
|
447
|
+
? { command: resolveYeaftPath(), prefixArgs: [], spawnOpts: {} }
|
|
448
|
+
: resolveProviderCommand(provider);
|
|
449
|
+
const direct = resolveCmdWrapperDirectExecution(command);
|
|
450
|
+
let spawnCmd;
|
|
451
|
+
let spawnArgs;
|
|
452
|
+
if (direct) {
|
|
453
|
+
spawnCmd = direct.command;
|
|
454
|
+
spawnArgs = [...direct.prefixArgs, ...prefixArgs, ...args];
|
|
455
|
+
}
|
|
456
|
+
else if (platform() === 'win32' && command.toLowerCase().endsWith('.cmd')) {
|
|
457
|
+
spawnCmd = process.env.COMSPEC || 'cmd.exe';
|
|
458
|
+
spawnArgs = ['/c', command, ...prefixArgs, ...args];
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
spawnCmd = command;
|
|
462
|
+
spawnArgs = [...prefixArgs, ...args];
|
|
463
|
+
}
|
|
464
|
+
const controller = new AbortController();
|
|
465
|
+
state.abortController = controller;
|
|
466
|
+
state.stderrBuf = '';
|
|
467
|
+
const child = spawn(spawnCmd, spawnArgs, {
|
|
468
|
+
cwd: provider.cwd || state.workDir,
|
|
469
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
470
|
+
env: { ...getCleanEnv(), ...(provider.env || {}) },
|
|
471
|
+
signal: controller.signal,
|
|
472
|
+
...spawnOpts,
|
|
473
|
+
windowsHide: true,
|
|
474
|
+
});
|
|
475
|
+
state.child = child;
|
|
476
|
+
const rl = createInterface({ input: child.stdout });
|
|
477
|
+
child.stderr?.on('data', (chunk) => { state.stderrBuf += chunk.toString(); });
|
|
478
|
+
rl.on('line', (line) => {
|
|
479
|
+
if (!line.trim())
|
|
480
|
+
return;
|
|
481
|
+
try {
|
|
482
|
+
const event = JSON.parse(line);
|
|
483
|
+
handleYeaftEvent(state, event);
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
state.stderrBuf += `${line}\n`;
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
child.on('close', (code) => {
|
|
490
|
+
if (state.child !== child)
|
|
491
|
+
return;
|
|
492
|
+
state.child = null;
|
|
493
|
+
state.abortController = null;
|
|
494
|
+
if (state.turnActive) {
|
|
495
|
+
finishYeaftTurn(state);
|
|
496
|
+
const errMsg = state.stderrBuf.trim()
|
|
497
|
+
|| (code !== 0 && code !== null
|
|
498
|
+
? `Yeaft process exited with code ${code}`
|
|
499
|
+
: 'Yeaft process exited before emitting an aggregate turn result');
|
|
500
|
+
sendFn({ type: 'backend_event', conversationId: state.conversationId, event: { type: 'error', message: errMsg } });
|
|
501
|
+
emitYeaftTurnCompleted(state);
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
child.on('error', (err) => {
|
|
505
|
+
if (state.child !== child)
|
|
506
|
+
return;
|
|
507
|
+
state.child = null;
|
|
508
|
+
state.abortController = null;
|
|
509
|
+
finishYeaftTurn(state);
|
|
510
|
+
sendFn({ type: 'backend_event', conversationId: state.conversationId, event: { type: 'error', message: `Failed to start yeaft: ${err.message}` } });
|
|
511
|
+
emitYeaftTurnCompleted(state);
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
function handleYeaftEvent(state, event) {
|
|
515
|
+
const convId = state.conversationId;
|
|
516
|
+
const type = String(event.type || '');
|
|
517
|
+
const subtype = String(event.subtype || '');
|
|
518
|
+
const sessionId = textFromUnknown(event.session_id) || state.yeaftSessionId || '';
|
|
519
|
+
if (sessionId) {
|
|
520
|
+
state.yeaftSessionId = sessionId;
|
|
521
|
+
state.lastYeaftSessionId = sessionId;
|
|
522
|
+
}
|
|
523
|
+
const ref = sessionRef(state.yeaftSessionId || sessionId || 'pending');
|
|
524
|
+
const scope = scopeFromYeaftEvent(event);
|
|
525
|
+
if (type === 'system' && subtype === 'init') {
|
|
526
|
+
if (!state.sessionNotified) {
|
|
527
|
+
state.sessionNotified = true;
|
|
528
|
+
sendFn({ type: 'backend_event', conversationId: convId, event: { type: 'session_started', session: ref } });
|
|
529
|
+
}
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
if (type === 'turn' && subtype === 'start') {
|
|
533
|
+
const turnId = textFromUnknown(event.turn_id);
|
|
534
|
+
if (turnId)
|
|
535
|
+
state.turnId = turnId;
|
|
536
|
+
sendFn({ type: 'backend_event', conversationId: convId, event: { type: 'turn_started', session: ref, turnId: state.turnId || undefined, ...scope } });
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
if (type === 'assistant' && subtype === 'text_delta') {
|
|
540
|
+
const delta = event.delta;
|
|
541
|
+
const text = textFromUnknown(delta?.text) || textFromUnknown(event.text);
|
|
542
|
+
if (text)
|
|
543
|
+
sendFn({ type: 'backend_event', conversationId: convId, event: { type: 'text_delta', session: ref, text, ...scope } });
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (type === 'assistant' && subtype === 'thinking_delta') {
|
|
547
|
+
const delta = event.delta;
|
|
548
|
+
const text = textFromUnknown(delta?.thinking) || textFromUnknown(delta?.text) || textFromUnknown(event.text);
|
|
549
|
+
if (text)
|
|
550
|
+
sendFn({ type: 'backend_event', conversationId: convId, event: { type: 'reasoning_delta', session: ref, text, ...scope } });
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (type === 'assistant' && subtype === 'tool_use') {
|
|
554
|
+
const content = Array.isArray(event.content) ? event.content : [];
|
|
555
|
+
for (const item of content) {
|
|
556
|
+
if (!item || typeof item !== 'object')
|
|
557
|
+
continue;
|
|
558
|
+
const tool = item;
|
|
559
|
+
const id = textFromUnknown(tool.id) || generateToolId('yeaft-tool');
|
|
560
|
+
const name = textFromUnknown(tool.name) || 'tool';
|
|
561
|
+
state.pendingToolInputs.set(id, tool.input);
|
|
562
|
+
emitToolStarted(state, id, name, tool.input, scope);
|
|
563
|
+
}
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (type === 'tool' && subtype === 'start') {
|
|
567
|
+
const id = textFromUnknown(event.tool_use_id) || generateToolId('yeaft-tool');
|
|
568
|
+
const name = textFromUnknown(event.name) || 'tool';
|
|
569
|
+
state.pendingToolInputs.set(id, event.input);
|
|
570
|
+
emitToolStarted(state, id, name, event.input, scope);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (type === 'tool' && subtype === 'result') {
|
|
574
|
+
const id = textFromUnknown(event.tool_use_id) || generateToolId('yeaft-tool');
|
|
575
|
+
const name = textFromUnknown(event.name) || 'tool';
|
|
576
|
+
emitToolCompleted(state, id, name, event.content, !!event.is_error, scope);
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (type === 'task') {
|
|
580
|
+
sendFn({
|
|
581
|
+
type: 'backend_event',
|
|
582
|
+
conversationId: convId,
|
|
583
|
+
event: {
|
|
584
|
+
type: 'task_updated',
|
|
585
|
+
session: ref,
|
|
586
|
+
update: { subtype: subtype || 'updated', task: event.task || null },
|
|
587
|
+
...scope,
|
|
588
|
+
},
|
|
589
|
+
});
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (type === 'sub_agent') {
|
|
593
|
+
sendFn({
|
|
594
|
+
type: 'backend_event',
|
|
595
|
+
conversationId: convId,
|
|
596
|
+
event: {
|
|
597
|
+
type: 'sub_agent_updated',
|
|
598
|
+
session: ref,
|
|
599
|
+
update: {
|
|
600
|
+
subtype: subtype || 'event',
|
|
601
|
+
agentId: textFromUnknown(event.agent_id) || textFromUnknown(event.payload?.agentId),
|
|
602
|
+
payload: event.payload || {},
|
|
603
|
+
},
|
|
604
|
+
...scope,
|
|
605
|
+
},
|
|
606
|
+
});
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (type === 'async_task') {
|
|
610
|
+
sendFn({
|
|
611
|
+
type: 'backend_event',
|
|
612
|
+
conversationId: convId,
|
|
613
|
+
event: {
|
|
614
|
+
type: 'async_task_wait',
|
|
615
|
+
session: ref,
|
|
616
|
+
wait: {
|
|
617
|
+
status: subtype === 'wait_start' ? 'waiting' : 'resumed',
|
|
618
|
+
pendingTaskIds: stringArrayFromUnknown(event.pending_task_ids),
|
|
619
|
+
remainingTaskIds: stringArrayFromUnknown(event.remaining_task_ids),
|
|
620
|
+
aborted: !!event.aborted,
|
|
621
|
+
},
|
|
622
|
+
...scope,
|
|
623
|
+
},
|
|
624
|
+
});
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
if (type === 'usage') {
|
|
628
|
+
const usage = buildYeaftUsage(state, event.usage);
|
|
629
|
+
state.lastUsage = usage;
|
|
630
|
+
sendFn({ type: 'backend_event', conversationId: convId, event: { type: 'usage_update', session: ref, usage, ...scope } });
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (type === 'ask_user' && subtype === 'request') {
|
|
634
|
+
const requestId = textFromUnknown(event.request_id) || generateToolId('ask');
|
|
635
|
+
const question = textFromUnknown(event.question) || 'Input requested';
|
|
636
|
+
const options = Array.isArray(event.options)
|
|
637
|
+
? event.options.map((value) => ({ label: String(value) }))
|
|
638
|
+
: [];
|
|
639
|
+
sendFn({
|
|
640
|
+
type: 'backend_event',
|
|
641
|
+
conversationId: convId,
|
|
642
|
+
event: {
|
|
643
|
+
type: 'user_input_requested',
|
|
644
|
+
session: ref,
|
|
645
|
+
requestId,
|
|
646
|
+
request: { source: 'ask_user_question', questions: [{ question, options }] },
|
|
647
|
+
...scope,
|
|
648
|
+
},
|
|
649
|
+
});
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if (type === 'error') {
|
|
653
|
+
const raw = event.error;
|
|
654
|
+
const message = textFromUnknown(raw?.message) || textFromUnknown(event.message) || 'Yeaft turn failed';
|
|
655
|
+
sendFn({ type: 'backend_event', conversationId: convId, event: { type: 'error', session: ref, message, ...scope } });
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
if (type === 'result') {
|
|
659
|
+
const usage = buildYeaftUsage(state, event.usage);
|
|
660
|
+
state.lastUsage = usage;
|
|
661
|
+
if (event.is_error || subtype === 'error') {
|
|
662
|
+
sendFn({ type: 'backend_event', conversationId: convId, event: { type: 'error', session: ref, message: textFromUnknown(event.error) || 'Yeaft turn failed', ...scope } });
|
|
663
|
+
}
|
|
664
|
+
// Per-VP/ad-hoc results may be scoped or unscoped. Only the stream-json
|
|
665
|
+
// aggregate carries both marker arrays and terminates the global turn.
|
|
666
|
+
const isAggregateResult = Array.isArray(event.vp_results)
|
|
667
|
+
&& Array.isArray(event.dispatched_vp_ids);
|
|
668
|
+
if (scope.vpId || scope.threadId || !isAggregateResult)
|
|
669
|
+
return;
|
|
670
|
+
finishYeaftTurn(state);
|
|
671
|
+
emitYeaftTurnCompleted(state, usage);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
function generateToolId(prefix) {
|
|
675
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
676
|
+
}
|
|
677
|
+
function emitToolStarted(state, id, name, input, scope = {}) {
|
|
678
|
+
if (state.startedToolIds.has(id))
|
|
679
|
+
return;
|
|
680
|
+
state.startedToolIds.add(id);
|
|
681
|
+
const displayName = name === 'route_forward' ? 'RouteForward' : name;
|
|
682
|
+
sendFn({
|
|
683
|
+
type: 'backend_event',
|
|
684
|
+
conversationId: state.conversationId,
|
|
685
|
+
event: {
|
|
686
|
+
type: 'tool_started',
|
|
687
|
+
session: sessionRef(state.yeaftSessionId || 'pending'),
|
|
688
|
+
tool: { id, name: displayName, itemKind: displayName, input, status: 'in_progress' },
|
|
689
|
+
...scope,
|
|
690
|
+
},
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
function emitToolCompleted(state, id, name, output, isError, scope = {}) {
|
|
694
|
+
if (!state.startedToolIds.has(id))
|
|
695
|
+
emitToolStarted(state, id, name, state.pendingToolInputs.get(id), scope);
|
|
696
|
+
const displayName = name === 'route_forward' ? 'RouteForward' : name;
|
|
697
|
+
sendFn({
|
|
698
|
+
type: 'backend_event',
|
|
699
|
+
conversationId: state.conversationId,
|
|
700
|
+
event: {
|
|
701
|
+
type: 'tool_completed',
|
|
702
|
+
session: sessionRef(state.yeaftSessionId || 'pending'),
|
|
703
|
+
tool: { id, name: displayName, output, isError, status: isError ? 'error' : 'completed' },
|
|
704
|
+
...scope,
|
|
705
|
+
},
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
function emitYeaftTurnCompleted(state, usage = state.lastUsage) {
|
|
709
|
+
sendFn({
|
|
710
|
+
type: 'backend_event',
|
|
711
|
+
conversationId: state.conversationId,
|
|
712
|
+
event: { type: 'turn_completed', session: sessionRef(state.yeaftSessionId || ''), ...(usage ? { usage } : {}) },
|
|
713
|
+
});
|
|
714
|
+
sendFn({
|
|
715
|
+
type: 'turn_completed',
|
|
716
|
+
conversationId: state.conversationId,
|
|
717
|
+
workDir: state.workDir,
|
|
718
|
+
claudeSessionId: state.yeaftSessionId,
|
|
719
|
+
turnId: state.turnId,
|
|
720
|
+
stateVersion: state.stateVersion,
|
|
721
|
+
...(usage ? { usage } : {}),
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
export function handleYeaftUserAnswer(requestId, answers, opts = {}) {
|
|
725
|
+
const payload = JSON.stringify({ type: 'ask_user_response', request_id: requestId, answers }) + '\n';
|
|
726
|
+
const candidates = [
|
|
727
|
+
...(opts.conversationId ? [conversations.get(opts.conversationId)] : []),
|
|
728
|
+
...conversations.values(),
|
|
729
|
+
];
|
|
730
|
+
const seen = new Set();
|
|
731
|
+
for (const state of candidates) {
|
|
732
|
+
if (!state || seen.has(state))
|
|
733
|
+
continue;
|
|
734
|
+
seen.add(state);
|
|
735
|
+
if (state.child?.stdin && !state.child.stdin.destroyed) {
|
|
736
|
+
state.child.stdin.write(payload);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
export function cancelYeaftExecution(conversationId) {
|
|
742
|
+
const state = conversations.get(conversationId);
|
|
743
|
+
if (!state) {
|
|
744
|
+
emitYeaftCancelled(conversationId);
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
if (!state.child) {
|
|
748
|
+
if (state.turnActive)
|
|
749
|
+
finishYeaftTurn(state);
|
|
750
|
+
emitYeaftCancelled(conversationId, state);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
finishYeaftTurn(state);
|
|
754
|
+
emitYeaftCancelled(conversationId, state);
|
|
755
|
+
if (platform() === 'win32') {
|
|
756
|
+
try {
|
|
757
|
+
spawn('cmd', ['/c', 'taskkill', '/pid', String(state.child.pid), '/f', '/t'], { stdio: 'ignore', windowsHide: true });
|
|
758
|
+
}
|
|
759
|
+
catch { /* ignore */ }
|
|
760
|
+
}
|
|
761
|
+
else {
|
|
762
|
+
state.child.kill('SIGTERM');
|
|
763
|
+
}
|
|
764
|
+
state.child = null;
|
|
765
|
+
state.abortController = null;
|
|
766
|
+
}
|
|
767
|
+
function cleanupYeaftConversation(conversationId) {
|
|
768
|
+
const state = conversations.get(conversationId);
|
|
769
|
+
if (!state)
|
|
770
|
+
return;
|
|
771
|
+
if (state.child) {
|
|
772
|
+
try {
|
|
773
|
+
state.child.stdin?.end();
|
|
774
|
+
}
|
|
775
|
+
catch { /* ignore */ }
|
|
776
|
+
cancelYeaftExecution(conversationId);
|
|
777
|
+
}
|
|
778
|
+
if (state.heartbeatTimer)
|
|
779
|
+
clearInterval(state.heartbeatTimer);
|
|
780
|
+
if (state.yeaftSessionId)
|
|
781
|
+
state.lastYeaftSessionId = state.yeaftSessionId;
|
|
782
|
+
conversations.delete(conversationId);
|
|
783
|
+
}
|
|
784
|
+
export function cleanupAllYeaftConversations() {
|
|
785
|
+
for (const convId of conversations.keys())
|
|
786
|
+
cleanupYeaftConversation(convId);
|
|
787
|
+
}
|
|
788
|
+
export function evictByYeaftSessionId(yeaftSessionId) {
|
|
789
|
+
for (const [convId, conv] of conversations) {
|
|
790
|
+
if (conv.yeaftSessionId === yeaftSessionId || conv.lastYeaftSessionId === yeaftSessionId) {
|
|
791
|
+
if (conv.turnActive)
|
|
792
|
+
return true;
|
|
793
|
+
cleanupYeaftConversation(convId);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return false;
|
|
797
|
+
}
|
|
798
|
+
function normalizeCursorPath(value) {
|
|
799
|
+
const normalized = value.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
800
|
+
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
|
|
801
|
+
}
|
|
802
|
+
function normalizeSessionLimit(limit) {
|
|
803
|
+
if (limit == null)
|
|
804
|
+
return YEAFT_SESSION_LIST_LIMIT;
|
|
805
|
+
if (limit === Number.POSITIVE_INFINITY)
|
|
806
|
+
return Number.POSITIVE_INFINITY;
|
|
807
|
+
if (!Number.isFinite(limit) || limit <= 0)
|
|
808
|
+
return YEAFT_SESSION_LIST_LIMIT;
|
|
809
|
+
return Math.min(Math.floor(limit), 500);
|
|
810
|
+
}
|
|
811
|
+
function compareCursorSortKeys(a, b) {
|
|
812
|
+
const lastModifiedDiff = b.lastModified - a.lastModified;
|
|
813
|
+
if (lastModifiedDiff !== 0)
|
|
814
|
+
return lastModifiedDiff;
|
|
815
|
+
const sessionDiff = a.sessionId.localeCompare(b.sessionId);
|
|
816
|
+
if (sessionDiff !== 0)
|
|
817
|
+
return sessionDiff;
|
|
818
|
+
return a.tieBreaker.localeCompare(b.tieBreaker);
|
|
819
|
+
}
|
|
820
|
+
function encodeYeaftCursor(scope, sortKey, workDir) {
|
|
821
|
+
return Buffer.from(JSON.stringify({
|
|
822
|
+
v: 1,
|
|
823
|
+
provider: 'yeaft',
|
|
824
|
+
scope,
|
|
825
|
+
...(scope === 'workdir' && workDir ? { workDir: normalizeCursorPath(workDir) } : {}),
|
|
826
|
+
lastModified: sortKey.lastModified,
|
|
827
|
+
sessionId: sortKey.sessionId,
|
|
828
|
+
tieBreaker: sortKey.tieBreaker,
|
|
829
|
+
}), 'utf-8').toString('base64url');
|
|
830
|
+
}
|
|
831
|
+
function decodeYeaftCursor(cursor, scope, workDir) {
|
|
832
|
+
let parsed;
|
|
833
|
+
try {
|
|
834
|
+
parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf-8'));
|
|
835
|
+
}
|
|
836
|
+
catch {
|
|
837
|
+
throw new Error('Invalid yeaft session list cursor');
|
|
838
|
+
}
|
|
839
|
+
if (!parsed || typeof parsed !== 'object')
|
|
840
|
+
throw new Error('Invalid yeaft session list cursor');
|
|
841
|
+
const value = parsed;
|
|
842
|
+
if (value.v !== 1 || value.provider !== 'yeaft' || value.scope !== scope || typeof value.lastModified !== 'number' || !Number.isFinite(value.lastModified) || typeof value.sessionId !== 'string' || typeof value.tieBreaker !== 'string') {
|
|
843
|
+
throw new Error('Invalid yeaft session list cursor');
|
|
844
|
+
}
|
|
845
|
+
if (scope === 'workdir' && value.workDir !== normalizeCursorPath(workDir || ''))
|
|
846
|
+
throw new Error('Yeaft session list cursor does not match requested workDir');
|
|
847
|
+
return { lastModified: value.lastModified, sessionId: value.sessionId, tieBreaker: value.tieBreaker };
|
|
848
|
+
}
|
|
849
|
+
function yeaftRoots(workDir) {
|
|
850
|
+
const roots = [join(homedir(), '.yeaft')];
|
|
851
|
+
if (workDir)
|
|
852
|
+
roots.push(join(workDir, '.yeaft'));
|
|
853
|
+
return Array.from(new Set(roots.map(root => resolve(root))));
|
|
854
|
+
}
|
|
855
|
+
function listYeaftSessionDirs(workDir) {
|
|
856
|
+
const result = [];
|
|
857
|
+
for (const root of yeaftRoots(workDir)) {
|
|
858
|
+
const sessionsDir = join(root, 'sessions');
|
|
859
|
+
if (!existsSync(sessionsDir))
|
|
860
|
+
continue;
|
|
861
|
+
let entries;
|
|
862
|
+
try {
|
|
863
|
+
entries = readdirSync(sessionsDir, { withFileTypes: true });
|
|
864
|
+
}
|
|
865
|
+
catch {
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
for (const entry of entries) {
|
|
869
|
+
if (!entry.isDirectory() || entry.name.startsWith('.'))
|
|
870
|
+
continue;
|
|
871
|
+
result.push(join(sessionsDir, entry.name));
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
return result;
|
|
875
|
+
}
|
|
876
|
+
function loadTitleMap() {
|
|
877
|
+
try {
|
|
878
|
+
const parsed = JSON.parse(readFileSync(YEAFT_TITLES_FILE, 'utf-8'));
|
|
879
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
880
|
+
}
|
|
881
|
+
catch {
|
|
882
|
+
return {};
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
function writeTitleMap(map) {
|
|
886
|
+
if (!existsSync(CONFIG_DIR))
|
|
887
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
888
|
+
writeFileSync(YEAFT_TITLES_FILE, JSON.stringify(map, null, 2) + '\n', 'utf-8');
|
|
889
|
+
}
|
|
890
|
+
function setCustomTitle(sessionId, title) {
|
|
891
|
+
const map = loadTitleMap();
|
|
892
|
+
map[sessionId] = title;
|
|
893
|
+
writeTitleMap(map);
|
|
894
|
+
}
|
|
895
|
+
function removeCustomTitle(sessionId) {
|
|
896
|
+
const map = loadTitleMap();
|
|
897
|
+
if (Object.prototype.hasOwnProperty.call(map, sessionId)) {
|
|
898
|
+
delete map[sessionId];
|
|
899
|
+
writeTitleMap(map);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
function isRawYeaftSessionTitle(title, sessionId) {
|
|
903
|
+
const trimmed = title.trim();
|
|
904
|
+
return !trimmed || trimmed === sessionId || SESSION_ID_PATTERN.test(trimmed);
|
|
905
|
+
}
|
|
906
|
+
function parseSessionDir(sessionDir, stats) {
|
|
907
|
+
const sessionId = sessionDir.replace(/\\/g, '/').split('/').pop() || '';
|
|
908
|
+
if (!sessionId)
|
|
909
|
+
return null;
|
|
910
|
+
const meta = readJsonObject(join(sessionDir, 'session.json')) || readJsonObject(join(sessionDir, 'group.json')) || {};
|
|
911
|
+
let firstPrompt = '';
|
|
912
|
+
let lastAssistant = '';
|
|
913
|
+
let projectPath = textFromUnknown(meta.workDir);
|
|
914
|
+
let lastModified = Date.parse(textFromUnknown(meta.createdAt)) || stats.mtimeMs;
|
|
915
|
+
for (const row of readSessionRows(sessionDir)) {
|
|
916
|
+
const ts = Date.parse(textFromUnknown(row.time) || textFromUnknown(row.ts) || textFromUnknown(row.timestamp));
|
|
917
|
+
if (Number.isFinite(ts) && ts > lastModified)
|
|
918
|
+
lastModified = ts;
|
|
919
|
+
if (!projectPath)
|
|
920
|
+
projectPath = textFromUnknown(row.cwd) || textFromUnknown(row.workDir);
|
|
921
|
+
const role = textFromUnknown(row.role) || (row.from === 'user' ? 'user' : '');
|
|
922
|
+
const content = rowText(row);
|
|
923
|
+
if (role === 'user' && content && !firstPrompt)
|
|
924
|
+
firstPrompt = content.substring(0, 100);
|
|
925
|
+
if (role === 'assistant' && content)
|
|
926
|
+
lastAssistant = content.substring(0, 120);
|
|
927
|
+
}
|
|
928
|
+
const titleMap = loadTitleMap();
|
|
929
|
+
const customTitle = titleMap[sessionId];
|
|
930
|
+
const metaTitle = textFromUnknown(meta.name);
|
|
931
|
+
const usableMetaTitle = isRawYeaftSessionTitle(metaTitle, sessionId) ? '' : metaTitle;
|
|
932
|
+
const title = customTitle || usableMetaTitle || firstPrompt || lastAssistant || sessionId.slice(0, 24);
|
|
933
|
+
return {
|
|
934
|
+
sessionId,
|
|
935
|
+
title,
|
|
936
|
+
...(customTitle ? { customTitle } : {}),
|
|
937
|
+
preview: lastAssistant || firstPrompt || title,
|
|
938
|
+
firstPrompt: firstPrompt || title,
|
|
939
|
+
lastModified,
|
|
940
|
+
projectPath,
|
|
941
|
+
projectFolder: projectPath.replace(/[\\/]+$/, '').split(/[\\/]/).pop() || projectPath,
|
|
942
|
+
sourcePath: sessionDir,
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
function readJsonObject(filePath) {
|
|
946
|
+
try {
|
|
947
|
+
const parsed = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
948
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
949
|
+
}
|
|
950
|
+
catch {
|
|
951
|
+
return null;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
function collectFiles(root, out = []) {
|
|
955
|
+
let entries;
|
|
956
|
+
try {
|
|
957
|
+
entries = readdirSync(root, { withFileTypes: true });
|
|
958
|
+
}
|
|
959
|
+
catch {
|
|
960
|
+
return out;
|
|
961
|
+
}
|
|
962
|
+
for (const entry of entries) {
|
|
963
|
+
const full = join(root, entry.name);
|
|
964
|
+
if (entry.isDirectory())
|
|
965
|
+
collectFiles(full, out);
|
|
966
|
+
else if (entry.isFile() && /\.(jsonl|json|md)$/i.test(entry.name))
|
|
967
|
+
out.push(full);
|
|
968
|
+
}
|
|
969
|
+
return out;
|
|
970
|
+
}
|
|
971
|
+
function collectYeaftHistoryFiles(sessionDir) {
|
|
972
|
+
const normalizedSessionDir = normalizeCursorPath(sessionDir);
|
|
973
|
+
const files = collectFiles(sessionDir);
|
|
974
|
+
const conversationFiles = files.filter((file) => {
|
|
975
|
+
const normalized = normalizeCursorPath(file);
|
|
976
|
+
return normalized.startsWith(`${normalizedSessionDir}/conversation/segments/`) && /\.jsonl$/i.test(file);
|
|
977
|
+
});
|
|
978
|
+
if (conversationFiles.length)
|
|
979
|
+
return conversationFiles.sort();
|
|
980
|
+
// Legacy Yeaft sessions used flatter event/message logs. Only fall back to
|
|
981
|
+
// those when the canonical conversation store is absent; otherwise the same
|
|
982
|
+
// shared user turn can appear both in messages/ and conversation/segments/.
|
|
983
|
+
return files.filter((file) => {
|
|
984
|
+
const normalized = normalizeCursorPath(file);
|
|
985
|
+
if (normalized.includes(`${normalizedSessionDir}/debug/`))
|
|
986
|
+
return false;
|
|
987
|
+
if (normalized.endsWith('/session.json') || normalized.endsWith('/group.json') || normalized.endsWith('/index.json'))
|
|
988
|
+
return false;
|
|
989
|
+
return /\.(jsonl|json)$/i.test(file);
|
|
990
|
+
}).sort();
|
|
991
|
+
}
|
|
992
|
+
function readSessionRows(sessionDir) {
|
|
993
|
+
const rows = [];
|
|
994
|
+
for (const file of collectYeaftHistoryFiles(sessionDir)) {
|
|
995
|
+
const normalized = file.replace(/\\/g, '/');
|
|
996
|
+
if (normalized.endsWith('/session.json') || normalized.endsWith('/group.json') || normalized.endsWith('/index.json'))
|
|
997
|
+
continue;
|
|
998
|
+
let content = '';
|
|
999
|
+
try {
|
|
1000
|
+
content = readFileSync(file, 'utf-8');
|
|
1001
|
+
}
|
|
1002
|
+
catch {
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
for (const line of content.split('\n')) {
|
|
1006
|
+
const trimmed = line.trim();
|
|
1007
|
+
if (!trimmed.startsWith('{'))
|
|
1008
|
+
continue;
|
|
1009
|
+
try {
|
|
1010
|
+
const parsed = JSON.parse(trimmed);
|
|
1011
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
1012
|
+
rows.push(parsed);
|
|
1013
|
+
}
|
|
1014
|
+
catch { /* ignore */ }
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
rows.sort((a, b) => {
|
|
1018
|
+
const at = Date.parse(textFromUnknown(a.time) || textFromUnknown(a.ts) || textFromUnknown(a.timestamp));
|
|
1019
|
+
const bt = Date.parse(textFromUnknown(b.time) || textFromUnknown(b.ts) || textFromUnknown(b.timestamp));
|
|
1020
|
+
if (Number.isFinite(at) && Number.isFinite(bt) && at !== bt)
|
|
1021
|
+
return at - bt;
|
|
1022
|
+
return textFromUnknown(a.id).localeCompare(textFromUnknown(b.id));
|
|
1023
|
+
});
|
|
1024
|
+
return rows;
|
|
1025
|
+
}
|
|
1026
|
+
function rowText(row) {
|
|
1027
|
+
const content = row.content ?? row.text ?? row.message;
|
|
1028
|
+
if (typeof content === 'string')
|
|
1029
|
+
return content.trim();
|
|
1030
|
+
if (Array.isArray(content))
|
|
1031
|
+
return content.map((item) => {
|
|
1032
|
+
if (!item || typeof item !== 'object')
|
|
1033
|
+
return '';
|
|
1034
|
+
const r = item;
|
|
1035
|
+
return textFromUnknown(r.text) || textFromUnknown(r.content);
|
|
1036
|
+
}).filter(Boolean).join('').trim();
|
|
1037
|
+
if (content && typeof content === 'object') {
|
|
1038
|
+
const r = content;
|
|
1039
|
+
return textFromUnknown(r.text) || textFromUnknown(r.content);
|
|
1040
|
+
}
|
|
1041
|
+
return '';
|
|
1042
|
+
}
|
|
1043
|
+
function rowVpId(row) {
|
|
1044
|
+
const meta = row.meta && typeof row.meta === 'object' ? row.meta : null;
|
|
1045
|
+
return textFromUnknown(row.vpId)
|
|
1046
|
+
|| textFromUnknown(row.vp_id)
|
|
1047
|
+
|| textFromUnknown(row.speakerVpId)
|
|
1048
|
+
|| textFromUnknown(row.speaker_vp_id)
|
|
1049
|
+
|| textFromUnknown(row.senderVpId)
|
|
1050
|
+
|| textFromUnknown(meta?.senderVpId);
|
|
1051
|
+
}
|
|
1052
|
+
function rowThreadId(row) {
|
|
1053
|
+
const meta = row.meta && typeof row.meta === 'object' ? row.meta : null;
|
|
1054
|
+
return textFromUnknown(row.threadId)
|
|
1055
|
+
|| textFromUnknown(row.thread_id)
|
|
1056
|
+
|| textFromUnknown(meta?.sourceThreadId);
|
|
1057
|
+
}
|
|
1058
|
+
function rowTimestamp(row) {
|
|
1059
|
+
return textFromUnknown(row.time) || textFromUnknown(row.ts) || textFromUnknown(row.timestamp) || undefined;
|
|
1060
|
+
}
|
|
1061
|
+
function shouldShowYeaftConversationRow(row, role, content) {
|
|
1062
|
+
if (row.internal === true || row.hidden === true)
|
|
1063
|
+
return false;
|
|
1064
|
+
if (role === 'user' && row.userAuthored === false)
|
|
1065
|
+
return false;
|
|
1066
|
+
if (role === 'assistant')
|
|
1067
|
+
return !!content || (Array.isArray(row.toolCalls) && row.toolCalls.length > 0);
|
|
1068
|
+
if (role === 'user')
|
|
1069
|
+
return !!content;
|
|
1070
|
+
if (role === 'tool')
|
|
1071
|
+
return !!(content || row.output || row.toolOutput || row.toolCallId || row.tool_call_id || row.tool_use_id);
|
|
1072
|
+
return false;
|
|
1073
|
+
}
|
|
1074
|
+
function historyMessageKey(msg) {
|
|
1075
|
+
return [
|
|
1076
|
+
msg.role,
|
|
1077
|
+
msg.content || '',
|
|
1078
|
+
msg.toolName || '',
|
|
1079
|
+
msg.toolId || '',
|
|
1080
|
+
msg.toolInput || '',
|
|
1081
|
+
msg.toolOutput || '',
|
|
1082
|
+
msg.providerId || '',
|
|
1083
|
+
msg.vpId || '',
|
|
1084
|
+
msg.threadId || '',
|
|
1085
|
+
msg.timestamp || '',
|
|
1086
|
+
].join('\n');
|
|
1087
|
+
}
|
|
1088
|
+
function userPromptKey(content, row) {
|
|
1089
|
+
const clientMessageId = textFromUnknown(row.clientMessageId) || textFromUnknown(row.client_message_id);
|
|
1090
|
+
if (clientMessageId)
|
|
1091
|
+
return `id:${clientMessageId}`;
|
|
1092
|
+
return `content:${content}`;
|
|
1093
|
+
}
|
|
1094
|
+
function pushUniqueHistoryMessage(messages, seen, msg) {
|
|
1095
|
+
const key = historyMessageKey(msg);
|
|
1096
|
+
if (seen.has(key))
|
|
1097
|
+
return;
|
|
1098
|
+
seen.add(key);
|
|
1099
|
+
messages.push(msg);
|
|
1100
|
+
}
|
|
1101
|
+
function getYeaftSessionEntries(workDir) {
|
|
1102
|
+
const normalizedWorkDir = workDir ? normalizeCursorPath(workDir) : '';
|
|
1103
|
+
const normalizedWorkDirSessionRoot = workDir ? normalizeCursorPath(join(workDir, '.yeaft', 'sessions')) : '';
|
|
1104
|
+
const entries = [];
|
|
1105
|
+
for (const sessionDir of listYeaftSessionDirs(workDir)) {
|
|
1106
|
+
let stats;
|
|
1107
|
+
try {
|
|
1108
|
+
stats = statSync(sessionDir);
|
|
1109
|
+
}
|
|
1110
|
+
catch {
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
const parsed = parseSessionDir(sessionDir, stats);
|
|
1114
|
+
if (!parsed)
|
|
1115
|
+
continue;
|
|
1116
|
+
if (normalizedWorkDir) {
|
|
1117
|
+
const normalizedSessionDir = normalizeCursorPath(sessionDir);
|
|
1118
|
+
const isWorkDirLocalSession = normalizedWorkDirSessionRoot
|
|
1119
|
+
&& (normalizedSessionDir === normalizedWorkDirSessionRoot || normalizedSessionDir.startsWith(`${normalizedWorkDirSessionRoot}/`));
|
|
1120
|
+
const effectiveProjectPath = parsed.projectPath || (isWorkDirLocalSession ? workDir || '' : '');
|
|
1121
|
+
if (!effectiveProjectPath || normalizeCursorPath(effectiveProjectPath) !== normalizedWorkDir)
|
|
1122
|
+
continue;
|
|
1123
|
+
if (!parsed.projectPath) {
|
|
1124
|
+
parsed.projectPath = effectiveProjectPath;
|
|
1125
|
+
parsed.projectFolder = effectiveProjectPath.replace(/[\\/]+$/, '').split(/[\\/]/).pop() || effectiveProjectPath;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
entries.push({
|
|
1129
|
+
session: parsed,
|
|
1130
|
+
sortKey: { lastModified: parsed.lastModified, sessionId: parsed.sessionId, tieBreaker: normalizeCursorPath(sessionDir) },
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
entries.sort((a, b) => compareCursorSortKeys(a.sortKey, b.sortKey));
|
|
1134
|
+
return entries;
|
|
1135
|
+
}
|
|
1136
|
+
export function listYeaftSessions(workDir) {
|
|
1137
|
+
return listYeaftSessionsPage(workDir, { limit: Number.POSITIVE_INFINITY }).sessions;
|
|
1138
|
+
}
|
|
1139
|
+
export function listYeaftSessionsPage(workDir, options = {}) {
|
|
1140
|
+
const entries = getYeaftSessionEntries(workDir);
|
|
1141
|
+
const limit = normalizeSessionLimit(options.limit);
|
|
1142
|
+
let startIndex = 0;
|
|
1143
|
+
if (options.cursor) {
|
|
1144
|
+
const cursor = decodeYeaftCursor(options.cursor, 'workdir', workDir);
|
|
1145
|
+
startIndex = entries.findIndex(entry => compareCursorSortKeys(entry.sortKey, cursor) > 0);
|
|
1146
|
+
if (startIndex < 0)
|
|
1147
|
+
return { sessions: [], hasMore: false };
|
|
1148
|
+
}
|
|
1149
|
+
const pageEntries = limit === Number.POSITIVE_INFINITY ? entries.slice(startIndex) : entries.slice(startIndex, startIndex + limit);
|
|
1150
|
+
const nextIndex = startIndex + pageEntries.length;
|
|
1151
|
+
const hasMore = nextIndex < entries.length;
|
|
1152
|
+
return {
|
|
1153
|
+
sessions: pageEntries.map(({ session }) => ({ sessionId: session.sessionId, title: session.title, customTitle: session.customTitle, preview: session.preview, lastModified: session.lastModified })),
|
|
1154
|
+
hasMore,
|
|
1155
|
+
...(hasMore ? { nextCursor: encodeYeaftCursor('workdir', entries[nextIndex - 1].sortKey, workDir) } : {}),
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
export function listAllYeaftSessionsPage(options = {}) {
|
|
1159
|
+
const entries = getYeaftSessionEntries();
|
|
1160
|
+
const limit = normalizeSessionLimit(options.limit);
|
|
1161
|
+
let startIndex = 0;
|
|
1162
|
+
if (options.cursor) {
|
|
1163
|
+
const cursor = decodeYeaftCursor(options.cursor, 'global');
|
|
1164
|
+
startIndex = entries.findIndex(entry => compareCursorSortKeys(entry.sortKey, cursor) > 0);
|
|
1165
|
+
if (startIndex < 0)
|
|
1166
|
+
return { sessions: [], hasMore: false, indexState: 'ready' };
|
|
1167
|
+
}
|
|
1168
|
+
const pageEntries = limit === Number.POSITIVE_INFINITY ? entries.slice(startIndex) : entries.slice(startIndex, startIndex + limit);
|
|
1169
|
+
const nextIndex = startIndex + pageEntries.length;
|
|
1170
|
+
const hasMore = nextIndex < entries.length;
|
|
1171
|
+
return {
|
|
1172
|
+
sessions: pageEntries.map(({ session }) => ({ sessionId: session.sessionId, projectPath: session.projectPath, projectFolder: session.projectFolder, title: session.title, firstPrompt: session.firstPrompt, lastModified: session.lastModified })),
|
|
1173
|
+
hasMore,
|
|
1174
|
+
...(hasMore ? { nextCursor: encodeYeaftCursor('global', entries[nextIndex - 1].sortKey) } : {}),
|
|
1175
|
+
indexState: 'ready',
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
export function listAllYeaftSessions() {
|
|
1179
|
+
return listAllYeaftSessionsPage({ limit: Number.POSITIVE_INFINITY }).sessions;
|
|
1180
|
+
}
|
|
1181
|
+
function findYeaftSessionDir(workDir, sessionId) {
|
|
1182
|
+
for (const dir of listYeaftSessionDirs(workDir)) {
|
|
1183
|
+
if (dir.replace(/\\/g, '/').split('/').pop() === sessionId)
|
|
1184
|
+
return dir;
|
|
1185
|
+
}
|
|
1186
|
+
return null;
|
|
1187
|
+
}
|
|
1188
|
+
export function readYeaftSessionMessages(workDir, sessionId) {
|
|
1189
|
+
const dir = findYeaftSessionDir(workDir, sessionId);
|
|
1190
|
+
if (!dir)
|
|
1191
|
+
return [];
|
|
1192
|
+
const messages = [];
|
|
1193
|
+
const seenMessages = new Set();
|
|
1194
|
+
const seenUserPrompts = new Set();
|
|
1195
|
+
const toolCallsById = new Map();
|
|
1196
|
+
for (const row of readSessionRows(dir)) {
|
|
1197
|
+
const role = textFromUnknown(row.role) || (row.from === 'user' ? 'user' : '');
|
|
1198
|
+
const content = rowText(row);
|
|
1199
|
+
if (!shouldShowYeaftConversationRow(row, role, content))
|
|
1200
|
+
continue;
|
|
1201
|
+
const timestamp = rowTimestamp(row);
|
|
1202
|
+
const vpId = rowVpId(row);
|
|
1203
|
+
const threadId = rowThreadId(row);
|
|
1204
|
+
if (role === 'assistant' && Array.isArray(row.toolCalls)) {
|
|
1205
|
+
for (const value of row.toolCalls) {
|
|
1206
|
+
if (!value || typeof value !== 'object')
|
|
1207
|
+
continue;
|
|
1208
|
+
const tool = value;
|
|
1209
|
+
const id = textFromUnknown(tool.id) || textFromUnknown(tool.toolCallId);
|
|
1210
|
+
if (!id)
|
|
1211
|
+
continue;
|
|
1212
|
+
toolCallsById.set(id, {
|
|
1213
|
+
name: textFromUnknown(tool.name) || textFromUnknown(tool.toolName) || undefined,
|
|
1214
|
+
input: tool.input,
|
|
1215
|
+
...(vpId ? { vpId } : {}),
|
|
1216
|
+
...(threadId ? { threadId } : {}),
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
if (role === 'user') {
|
|
1221
|
+
const key = userPromptKey(content, row);
|
|
1222
|
+
if (seenUserPrompts.has(key))
|
|
1223
|
+
continue;
|
|
1224
|
+
seenUserPrompts.add(key);
|
|
1225
|
+
pushUniqueHistoryMessage(messages, seenMessages, { role: 'user', content, timestamp });
|
|
1226
|
+
}
|
|
1227
|
+
else if (role === 'assistant' && content) {
|
|
1228
|
+
pushUniqueHistoryMessage(messages, seenMessages, {
|
|
1229
|
+
role: 'assistant',
|
|
1230
|
+
content,
|
|
1231
|
+
timestamp,
|
|
1232
|
+
providerId: 'yeaft',
|
|
1233
|
+
...(vpId ? { vpId } : {}),
|
|
1234
|
+
...(threadId ? { threadId } : {}),
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
else if (role === 'tool') {
|
|
1238
|
+
const toolId = textFromUnknown(row.toolCallId) || textFromUnknown(row.tool_call_id) || textFromUnknown(row.tool_use_id) || textFromUnknown(row.toolId);
|
|
1239
|
+
const call = toolId ? toolCallsById.get(toolId) : undefined;
|
|
1240
|
+
pushUniqueHistoryMessage(messages, seenMessages, {
|
|
1241
|
+
role: 'tool',
|
|
1242
|
+
content: '',
|
|
1243
|
+
toolId,
|
|
1244
|
+
toolName: textFromUnknown(row.name) || textFromUnknown(row.toolName) || call?.name,
|
|
1245
|
+
toolInput: stringifyMaybe(row.input ?? call?.input),
|
|
1246
|
+
toolOutput: textFromUnknown(row.output) || textFromUnknown(row.toolOutput) || textFromUnknown(row.content),
|
|
1247
|
+
timestamp,
|
|
1248
|
+
providerId: 'yeaft',
|
|
1249
|
+
...(vpId || call?.vpId ? { vpId: vpId || call?.vpId } : {}),
|
|
1250
|
+
...(threadId || call?.threadId ? { threadId: threadId || call?.threadId } : {}),
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
return messages;
|
|
1255
|
+
}
|
|
1256
|
+
function stringifyMaybe(value) {
|
|
1257
|
+
if (value == null)
|
|
1258
|
+
return undefined;
|
|
1259
|
+
if (typeof value === 'string')
|
|
1260
|
+
return value;
|
|
1261
|
+
try {
|
|
1262
|
+
return JSON.stringify(value, null, 2);
|
|
1263
|
+
}
|
|
1264
|
+
catch {
|
|
1265
|
+
return String(value);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
export function renameYeaftSession(workDir, sessionId, newTitle) {
|
|
1269
|
+
if (!findYeaftSessionDir(workDir, sessionId))
|
|
1270
|
+
return false;
|
|
1271
|
+
setCustomTitle(sessionId, newTitle);
|
|
1272
|
+
return true;
|
|
1273
|
+
}
|
|
1274
|
+
export function deleteYeaftSession(workDir, sessionId) {
|
|
1275
|
+
const dir = findYeaftSessionDir(workDir, sessionId);
|
|
1276
|
+
if (!dir)
|
|
1277
|
+
return false;
|
|
1278
|
+
try {
|
|
1279
|
+
rmSync(dir, { recursive: true, force: true });
|
|
1280
|
+
removeCustomTitle(sessionId);
|
|
1281
|
+
return true;
|
|
1282
|
+
}
|
|
1283
|
+
catch {
|
|
1284
|
+
return false;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
export function moveYeaftSession(fromWorkDir, toWorkDir, sessionId) {
|
|
1288
|
+
const source = findYeaftSessionDir(fromWorkDir, sessionId);
|
|
1289
|
+
if (!source)
|
|
1290
|
+
return false;
|
|
1291
|
+
const destRoot = join(toWorkDir, '.yeaft', 'sessions');
|
|
1292
|
+
const dest = join(destRoot, sessionId);
|
|
1293
|
+
if (existsSync(dest))
|
|
1294
|
+
return false;
|
|
1295
|
+
try {
|
|
1296
|
+
mkdirSync(destRoot, { recursive: true });
|
|
1297
|
+
rmSync(dest, { recursive: true, force: true });
|
|
1298
|
+
// Use copy+remove to work across volumes.
|
|
1299
|
+
copyDir(source, dest);
|
|
1300
|
+
updateMovedSessionWorkDir(dest, toWorkDir);
|
|
1301
|
+
rmSync(source, { recursive: true, force: true });
|
|
1302
|
+
return true;
|
|
1303
|
+
}
|
|
1304
|
+
catch {
|
|
1305
|
+
return false;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
function copyDir(source, dest) {
|
|
1309
|
+
mkdirSync(dest, { recursive: true });
|
|
1310
|
+
for (const entry of readdirSync(source, { withFileTypes: true })) {
|
|
1311
|
+
const src = join(source, entry.name);
|
|
1312
|
+
const dst = join(dest, entry.name);
|
|
1313
|
+
if (entry.isDirectory())
|
|
1314
|
+
copyDir(src, dst);
|
|
1315
|
+
else
|
|
1316
|
+
writeFileSync(dst, readFileSync(src));
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
function updateMovedSessionWorkDir(sessionDir, workDir) {
|
|
1320
|
+
for (const name of ['session.json', 'group.json']) {
|
|
1321
|
+
const filePath = join(sessionDir, name);
|
|
1322
|
+
const meta = readJsonObject(filePath);
|
|
1323
|
+
if (!meta)
|
|
1324
|
+
continue;
|
|
1325
|
+
meta.workDir = workDir;
|
|
1326
|
+
try {
|
|
1327
|
+
writeFileSync(filePath, JSON.stringify(meta, null, 2) + '\n', 'utf-8');
|
|
1328
|
+
}
|
|
1329
|
+
catch { /* ignore */ }
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
export { yeaftCapabilities };
|
|
1334
|
+
//# sourceMappingURL=yeaft.js.map
|