@winmatrix/agent-sdk 1.0.4 → 1.0.5
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/adapters/claude/ClaudeAdapter.d.ts +59 -1
- package/dist/adapters/claude/ClaudeAdapter.d.ts.map +1 -1
- package/dist/adapters/claude/ClaudeAdapter.js +375 -79
- package/dist/adapters/claude/ClaudeAdapter.js.map +1 -1
- package/dist/adapters/claude/ClaudeInvocationUsageTracker.d.ts +17 -0
- package/dist/adapters/claude/ClaudeInvocationUsageTracker.d.ts.map +1 -0
- package/dist/adapters/claude/ClaudeInvocationUsageTracker.js +79 -0
- package/dist/adapters/claude/ClaudeInvocationUsageTracker.js.map +1 -0
- package/dist/adapters/claude/ClaudeStreamParser.d.ts +19 -0
- package/dist/adapters/claude/ClaudeStreamParser.d.ts.map +1 -1
- package/dist/adapters/claude/ClaudeStreamParser.js +92 -0
- package/dist/adapters/claude/ClaudeStreamParser.js.map +1 -1
- package/dist/adapters/codex/CodexAdapter.d.ts.map +1 -1
- package/dist/adapters/codex/CodexAdapter.js +36 -9
- package/dist/adapters/codex/CodexAdapter.js.map +1 -1
- package/dist/adapters/hermes/HermesAdapter.d.ts +9 -1
- package/dist/adapters/hermes/HermesAdapter.d.ts.map +1 -1
- package/dist/adapters/hermes/HermesAdapter.js +146 -7
- package/dist/adapters/hermes/HermesAdapter.js.map +1 -1
- package/dist/adapters/hermes/HermesInvocationUsageReader.d.ts +26 -0
- package/dist/adapters/hermes/HermesInvocationUsageReader.d.ts.map +1 -0
- package/dist/adapters/hermes/HermesInvocationUsageReader.js +128 -0
- package/dist/adapters/hermes/HermesInvocationUsageReader.js.map +1 -0
- package/dist/adapters/openclaw/OpenClawAdapter.d.ts.map +1 -1
- package/dist/adapters/openclaw/OpenClawAdapter.js +29 -7
- package/dist/adapters/openclaw/OpenClawAdapter.js.map +1 -1
- package/dist/core/AgentAdapter.d.ts +44 -1
- package/dist/core/AgentAdapter.d.ts.map +1 -1
- package/dist/core/AgentAdapter.js.map +1 -1
- package/dist/core/WinMatrixAgentClient.d.ts +23 -1
- package/dist/core/WinMatrixAgentClient.d.ts.map +1 -1
- package/dist/core/WinMatrixAgentClient.js +115 -12
- package/dist/core/WinMatrixAgentClient.js.map +1 -1
- package/dist/index.d.ts +13 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -13
- package/dist/index.js.map +1 -1
- package/dist/observability/WorkstationTokenUsageRecorder.d.ts +54 -0
- package/dist/observability/WorkstationTokenUsageRecorder.d.ts.map +1 -0
- package/dist/observability/WorkstationTokenUsageRecorder.js +264 -0
- package/dist/observability/WorkstationTokenUsageRecorder.js.map +1 -0
- package/dist/utils/envSanitizer.d.ts +38 -0
- package/dist/utils/envSanitizer.d.ts.map +1 -0
- package/dist/utils/envSanitizer.js +94 -0
- package/dist/utils/envSanitizer.js.map +1 -0
- package/dist/utils/errorClassification.d.ts +26 -0
- package/dist/utils/errorClassification.d.ts.map +1 -0
- package/dist/utils/errorClassification.js +52 -0
- package/dist/utils/errorClassification.js.map +1 -0
- package/dist/utils/processUtils.d.ts +10 -0
- package/dist/utils/processUtils.d.ts.map +1 -1
- package/dist/utils/processUtils.js +42 -9
- package/dist/utils/processUtils.js.map +1 -1
- package/dist/utils/resumeFallback.d.ts.map +1 -1
- package/dist/utils/resumeFallback.js +11 -1
- package/dist/utils/resumeFallback.js.map +1 -1
- package/package.json +2 -3
|
@@ -7,12 +7,16 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFile
|
|
|
7
7
|
import { join } from 'node:path';
|
|
8
8
|
import { randomUUID } from 'node:crypto';
|
|
9
9
|
import { homedir } from 'node:os';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
10
11
|
import { parseSkillMd } from '../../utils/parseSkillMd.js';
|
|
11
12
|
import { waitForExit, killProcess } from '../../utils/processUtils.js';
|
|
13
|
+
import { sanitizeTaskEnv } from '../../utils/envSanitizer.js';
|
|
12
14
|
import { executeWithResumeFallback } from '../../utils/resumeFallback.js';
|
|
15
|
+
import { classifyAdapterError } from '../../utils/errorClassification.js';
|
|
13
16
|
export { parseSkillMd };
|
|
14
17
|
import { isLocalTool } from '../../core/AgentAdapter.js';
|
|
15
|
-
import { parseClaudeLine, splitLines, } from './ClaudeStreamParser.js';
|
|
18
|
+
import { parseClaudeLine, parseClaudeInvocationUsageFrame, splitLines, } from './ClaudeStreamParser.js';
|
|
19
|
+
import { ClaudeInvocationUsageTracker } from './ClaudeInvocationUsageTracker.js';
|
|
16
20
|
import { resolveClaudeCommand } from './claudeCommand.js';
|
|
17
21
|
const CLAUDE_BASE_ARGS = [
|
|
18
22
|
'--print',
|
|
@@ -21,6 +25,20 @@ const CLAUDE_BASE_ARGS = [
|
|
|
21
25
|
'--output-format', 'stream-json',
|
|
22
26
|
'--include-partial-messages',
|
|
23
27
|
];
|
|
28
|
+
/**
|
|
29
|
+
* 权限默认档(harden-external-computer-daemon P1/D4)。
|
|
30
|
+
*
|
|
31
|
+
* 优先级:task 显式声明 > daemon 环境变量 > 'acceptEdits'(安全默认)。
|
|
32
|
+
* 交互确认通道(TASK_INTERACTION_RESPOND)当前未接通,print 模式下权限
|
|
33
|
+
* 提示会被自动拒绝——acceptEdits 下 Bash 等未预批工具将被拒;需要旧行为
|
|
34
|
+
* 的部署显式设 WINMATRIX_DAEMON_CLAUDE_PERMISSION_MODE=bypassPermissions,
|
|
35
|
+
* 或在任务配置里声明 permissionMode/allowedTools。
|
|
36
|
+
*/
|
|
37
|
+
function resolvePermissionMode(task) {
|
|
38
|
+
return task.claude?.permissionMode
|
|
39
|
+
?? process.env.WINMATRIX_DAEMON_CLAUDE_PERMISSION_MODE
|
|
40
|
+
?? 'acceptEdits';
|
|
41
|
+
}
|
|
24
42
|
export class ClaudeAdapter {
|
|
25
43
|
meta = {
|
|
26
44
|
agentType: 'claude-code',
|
|
@@ -45,25 +63,48 @@ export class ClaudeAdapter {
|
|
|
45
63
|
claudePath;
|
|
46
64
|
mcpBridgeUrl;
|
|
47
65
|
mcpApiKey;
|
|
66
|
+
/** steer 递话队列与当前 run 的 stdin 句柄(P2.1/D5) */
|
|
67
|
+
steerQueue = [];
|
|
68
|
+
activeStdin = null;
|
|
69
|
+
workstationId;
|
|
70
|
+
tokenUsageRecorder;
|
|
48
71
|
mcpConfigWritten = new Map();
|
|
49
72
|
constructor(config) {
|
|
50
|
-
|
|
51
|
-
this.
|
|
52
|
-
this.
|
|
73
|
+
const options = config;
|
|
74
|
+
this.claudePath = options?.claudePath ?? 'claude';
|
|
75
|
+
this.mcpBridgeUrl = options?.mcpBridgeUrl;
|
|
76
|
+
this.mcpApiKey = options?.mcpApiKey;
|
|
77
|
+
this.workstationId = options?.workstationId;
|
|
78
|
+
this.tokenUsageRecorder = options?.tokenUsageRecorder;
|
|
53
79
|
}
|
|
54
80
|
// ── execute ──
|
|
55
81
|
async *execute(task, signal) {
|
|
56
82
|
yield* executeWithResumeFallback(task, signal, (nextTask, nextSignal) => this.executeOnce(nextTask, nextSignal));
|
|
57
83
|
}
|
|
58
84
|
async *executeOnce(task, signal) {
|
|
85
|
+
// Pod 场景:通过 @anthropic-ai/claude-agent-sdk.query() 调 claude
|
|
86
|
+
if (task.claude?.useAgentSdk) {
|
|
87
|
+
yield* this.executeViaAgentSdk(task, signal);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
59
90
|
const workDir = task.workDir;
|
|
60
91
|
if (workDir && !existsSync(workDir)) {
|
|
61
|
-
yield {
|
|
92
|
+
yield {
|
|
93
|
+
type: 'error',
|
|
94
|
+
message: `Work directory does not exist: ${workDir}`,
|
|
95
|
+
code: 'cli_crash',
|
|
96
|
+
retryable: false,
|
|
97
|
+
};
|
|
62
98
|
return;
|
|
63
99
|
}
|
|
64
100
|
const args = [...CLAUDE_BASE_ARGS];
|
|
65
|
-
// Permission mode
|
|
66
|
-
|
|
101
|
+
// Permission mode(harden-external-computer-daemon P1/D4:默认档降为 acceptEdits)
|
|
102
|
+
const permissionMode = resolvePermissionMode(task);
|
|
103
|
+
if (permissionMode === 'bypassPermissions') {
|
|
104
|
+
console.warn('[ClaudeAdapter] 本次任务以 bypassPermissions 运行(显式声明)。' +
|
|
105
|
+
'外部 CLI 将不做任何权限确认,请确认任务来源可信。');
|
|
106
|
+
}
|
|
107
|
+
args.push('--permission-mode', permissionMode);
|
|
67
108
|
// workDir
|
|
68
109
|
if (workDir) {
|
|
69
110
|
args.push('--add-dir', workDir);
|
|
@@ -90,7 +131,9 @@ export class ClaudeAdapter {
|
|
|
90
131
|
}
|
|
91
132
|
}
|
|
92
133
|
// Local tools (whitelist)
|
|
93
|
-
const tools = task.claude?.tools
|
|
134
|
+
const tools = task.claude?.tools?.length
|
|
135
|
+
? task.claude.tools
|
|
136
|
+
: ['Bash', 'Read', 'Write', 'Edit'];
|
|
94
137
|
args.push('--allowedTools', tools.join(','));
|
|
95
138
|
// Disallowed tools
|
|
96
139
|
if (task.claude?.disallowedTools?.length) {
|
|
@@ -121,8 +164,11 @@ export class ClaudeAdapter {
|
|
|
121
164
|
else if (this.mcpBridgeUrl) {
|
|
122
165
|
console.warn(`[ClaudeAdapter] Skipping MCP config: URL likely unreachable: ${this.mcpBridgeUrl}`);
|
|
123
166
|
}
|
|
124
|
-
// Build env
|
|
125
|
-
const
|
|
167
|
+
// Build env(先剥凭据后显式加,harden-external-computer-daemon P1/D3)
|
|
168
|
+
const { env, strippedKeys } = sanitizeTaskEnv(task.env);
|
|
169
|
+
if (strippedKeys.length > 0) {
|
|
170
|
+
console.warn(`[ClaudeAdapter] 已剥离 ${strippedKeys.length} 个凭据形态环境变量(仅列 key 名): ${strippedKeys.join(', ')}`);
|
|
171
|
+
}
|
|
126
172
|
// Spawn
|
|
127
173
|
let proc;
|
|
128
174
|
try {
|
|
@@ -132,10 +178,17 @@ export class ClaudeAdapter {
|
|
|
132
178
|
env,
|
|
133
179
|
cwd: workDir ?? process.cwd(),
|
|
134
180
|
shell: spawnSpec.shell,
|
|
181
|
+
// POSIX 下独立进程组,killProcess 才能 kill(-pid) 杀整棵树
|
|
182
|
+
detached: process.platform !== 'win32',
|
|
135
183
|
});
|
|
136
184
|
}
|
|
137
185
|
catch (err) {
|
|
138
|
-
yield {
|
|
186
|
+
yield {
|
|
187
|
+
type: 'error',
|
|
188
|
+
message: `Failed to spawn claude: ${err instanceof Error ? err.message : String(err)}`,
|
|
189
|
+
code: 'cli_crash',
|
|
190
|
+
retryable: false,
|
|
191
|
+
};
|
|
139
192
|
return;
|
|
140
193
|
}
|
|
141
194
|
// Send user message via stdin (stream-json protocol)
|
|
@@ -148,7 +201,9 @@ export class ClaudeAdapter {
|
|
|
148
201
|
message: { role: 'user', content: task.input },
|
|
149
202
|
}) + '\n';
|
|
150
203
|
proc.stdin?.write(userMessage);
|
|
151
|
-
|
|
204
|
+
// steer(P2.1/D5):stdin 保持打开,turn 结束(result 帧)时若有排队递话
|
|
205
|
+
// 则注入下一 turn;无排队才关闭 stdin 让 CLI 退出
|
|
206
|
+
this.activeStdin = proc.stdin ?? null;
|
|
152
207
|
// Drain stderr to prevent backpressure stall, while keeping a bounded error summary.
|
|
153
208
|
let stderrText = '';
|
|
154
209
|
proc.stderr?.on('data', (chunk) => {
|
|
@@ -162,6 +217,7 @@ export class ClaudeAdapter {
|
|
|
162
217
|
let extractedResultText;
|
|
163
218
|
let extractedUsage;
|
|
164
219
|
let completed = false;
|
|
220
|
+
const invocationUsageTracker = new ClaudeInvocationUsageTracker();
|
|
165
221
|
// Delta text accumulator: fallback for empty/missing result frame
|
|
166
222
|
let accumulatedDeltaText = '';
|
|
167
223
|
// Abort handler
|
|
@@ -180,9 +236,39 @@ export class ClaudeAdapter {
|
|
|
180
236
|
const { lines, remainder } = splitLines(stdoutBuffer, text);
|
|
181
237
|
stdoutBuffer = remainder;
|
|
182
238
|
for (const line of lines) {
|
|
239
|
+
const observedAt = new Date().toISOString();
|
|
240
|
+
const usageFrame = parseClaudeInvocationUsageFrame(line);
|
|
241
|
+
if (usageFrame && this.tokenUsageRecorder) {
|
|
242
|
+
const completedUsage = invocationUsageTracker.accept(usageFrame, observedAt);
|
|
243
|
+
if (completedUsage) {
|
|
244
|
+
const workstationId = this.workstationId ?? task.env?.WINMATRIX_WORKSTATION_ID ?? process.env.WINMATRIX_WORKSTATION_ID;
|
|
245
|
+
if (!workstationId?.trim()) {
|
|
246
|
+
throw new Error('WORKSTATION_TOKEN_USAGE_WORKSTATION_ID_MISSING');
|
|
247
|
+
}
|
|
248
|
+
await this.tokenUsageRecorder.recordCompletedInvocation({
|
|
249
|
+
workstationId,
|
|
250
|
+
runtime: 'claude-code',
|
|
251
|
+
sourceInvocationId: completedUsage.sourceInvocationId,
|
|
252
|
+
provider: 'claude',
|
|
253
|
+
startedAt: completedUsage.startedAt,
|
|
254
|
+
occurredAt: completedUsage.occurredAt,
|
|
255
|
+
timeSource: completedUsage.timeSource,
|
|
256
|
+
sessionId: completedUsage.sessionId ?? extractedSessionId ?? sessionId,
|
|
257
|
+
taskId: task.taskId,
|
|
258
|
+
modelUsages: [{
|
|
259
|
+
sourceModelUsageId: `${completedUsage.sourceInvocationId}:model:0`,
|
|
260
|
+
model: completedUsage.model,
|
|
261
|
+
inputTokens: completedUsage.usage.inputTokens,
|
|
262
|
+
outputTokens: completedUsage.usage.outputTokens,
|
|
263
|
+
cacheReadTokens: completedUsage.usage.cacheReadTokens,
|
|
264
|
+
cacheCreationTokens: completedUsage.usage.cacheCreationTokens,
|
|
265
|
+
}],
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
183
269
|
const action = parseClaudeLine(line);
|
|
184
270
|
if (action.type === 'tool_use' && isLocalTool(action.toolName)) {
|
|
185
|
-
yield { type: 'thinking', content:
|
|
271
|
+
yield { type: 'thinking', content: `调用本地工具: ${action.toolName}` };
|
|
186
272
|
continue;
|
|
187
273
|
}
|
|
188
274
|
switch (action.type) {
|
|
@@ -219,7 +305,7 @@ export class ClaudeAdapter {
|
|
|
219
305
|
};
|
|
220
306
|
}
|
|
221
307
|
break;
|
|
222
|
-
case 'result':
|
|
308
|
+
case 'result': {
|
|
223
309
|
if (action.sessionId)
|
|
224
310
|
extractedSessionId = action.sessionId;
|
|
225
311
|
if (action.result)
|
|
@@ -230,11 +316,35 @@ export class ClaudeAdapter {
|
|
|
230
316
|
output_tokens: action.usage.output_tokens,
|
|
231
317
|
};
|
|
232
318
|
}
|
|
319
|
+
// steer:本 turn 结束。有排队递话则注入下一 turn 继续;
|
|
320
|
+
// 否则关闭 stdin,CLI 处理完退出
|
|
321
|
+
const nextSteer = this.steerQueue.shift();
|
|
322
|
+
if (nextSteer && proc.stdin && !proc.stdin.destroyed && proc.stdin.writable) {
|
|
323
|
+
accumulatedDeltaText = '';
|
|
324
|
+
proc.stdin.write(JSON.stringify({
|
|
325
|
+
type: 'user',
|
|
326
|
+
message: { role: 'user', content: nextSteer },
|
|
327
|
+
}) + '\n');
|
|
328
|
+
yield {
|
|
329
|
+
type: 'thinking',
|
|
330
|
+
content: `[steer] 已注入纠偏消息(队列剩余 ${this.steerQueue.length} 条),继续执行`,
|
|
331
|
+
engineEvent: { type: 'status', text: 'steer_applied' },
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
try {
|
|
336
|
+
proc.stdin?.end();
|
|
337
|
+
}
|
|
338
|
+
catch { /* already closed */ }
|
|
339
|
+
}
|
|
233
340
|
break;
|
|
234
|
-
|
|
341
|
+
}
|
|
342
|
+
case 'error': {
|
|
235
343
|
completed = true;
|
|
236
|
-
|
|
344
|
+
const classified = classifyAdapterError({ message: action.message, aborted: signal?.aborted });
|
|
345
|
+
yield { type: 'error', message: action.message, code: classified.code, retryable: classified.retryable };
|
|
237
346
|
return;
|
|
347
|
+
}
|
|
238
348
|
case 'skip':
|
|
239
349
|
break;
|
|
240
350
|
}
|
|
@@ -245,7 +355,8 @@ export class ClaudeAdapter {
|
|
|
245
355
|
if (!completed) {
|
|
246
356
|
completed = true;
|
|
247
357
|
if (startupError) {
|
|
248
|
-
|
|
358
|
+
const classified = classifyAdapterError({ message: startupError, aborted: signal?.aborted });
|
|
359
|
+
yield { type: 'error', message: startupError, code: classified.code, retryable: classified.retryable };
|
|
249
360
|
return;
|
|
250
361
|
}
|
|
251
362
|
if (extractedSessionId || exitResult.code === 0) {
|
|
@@ -261,10 +372,13 @@ export class ClaudeAdapter {
|
|
|
261
372
|
};
|
|
262
373
|
}
|
|
263
374
|
else {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
message
|
|
267
|
-
|
|
375
|
+
const message = stderrText.trim() || exitResult.error || `Claude Code exited with code ${exitResult.code}`;
|
|
376
|
+
const classified = classifyAdapterError({
|
|
377
|
+
message,
|
|
378
|
+
exitCode: exitResult.code,
|
|
379
|
+
aborted: signal?.aborted,
|
|
380
|
+
});
|
|
381
|
+
yield { type: 'error', message, code: classified.code, retryable: classified.retryable };
|
|
268
382
|
}
|
|
269
383
|
}
|
|
270
384
|
}
|
|
@@ -277,8 +391,26 @@ export class ClaudeAdapter {
|
|
|
277
391
|
}
|
|
278
392
|
catch { /* best-effort */ }
|
|
279
393
|
}
|
|
394
|
+
this.activeStdin = null;
|
|
395
|
+
this.steerQueue = [];
|
|
280
396
|
}
|
|
281
397
|
}
|
|
398
|
+
/**
|
|
399
|
+
* 中途递话(P2.1/D5):将纠偏消息排队,当前 turn 结束(result 帧)时注入
|
|
400
|
+
* stdin 开启下一 turn(dsh steer 语义的 stream-json 落地——stream-json 协议
|
|
401
|
+
* 在 turn 边界处理排队的 user 消息)。
|
|
402
|
+
*/
|
|
403
|
+
steer(text) {
|
|
404
|
+
const stdin = this.activeStdin;
|
|
405
|
+
if (!stdin || stdin.destroyed || !stdin.writable) {
|
|
406
|
+
return { accepted: false, reason: 'no_active_run' };
|
|
407
|
+
}
|
|
408
|
+
const trimmed = text.trim();
|
|
409
|
+
if (!trimmed)
|
|
410
|
+
return { accepted: false, reason: 'empty_text' };
|
|
411
|
+
this.steerQueue.push(trimmed);
|
|
412
|
+
return { accepted: true };
|
|
413
|
+
}
|
|
282
414
|
// ── healthCheck ──
|
|
283
415
|
async healthCheck() {
|
|
284
416
|
return new Promise((resolve) => {
|
|
@@ -307,71 +439,81 @@ export class ClaudeAdapter {
|
|
|
307
439
|
async listSessions(options) {
|
|
308
440
|
try {
|
|
309
441
|
const dir = options.dir?.trim();
|
|
310
|
-
const
|
|
311
|
-
if (projectDirs.length === 0) {
|
|
312
|
-
return { sessions: [] };
|
|
313
|
-
}
|
|
314
|
-
// load session index from ~/.claude/sessions/
|
|
315
|
-
const sessionsIndex = this.loadSessionsIndex();
|
|
316
|
-
const entries = [];
|
|
317
|
-
const seen = new Set();
|
|
318
|
-
for (const projectDir of projectDirs) {
|
|
319
|
-
const files = readdirSync(projectDir.path, { withFileTypes: true })
|
|
320
|
-
.filter((d) => d.isFile() && d.name.endsWith('.jsonl'));
|
|
321
|
-
for (const file of files) {
|
|
322
|
-
const sessionId = file.name.replace(/\.jsonl$/, '');
|
|
323
|
-
if (seen.has(sessionId))
|
|
324
|
-
continue;
|
|
325
|
-
seen.add(sessionId);
|
|
326
|
-
const indexPath = join(projectDir.path, file.name);
|
|
327
|
-
const idxMeta = sessionsIndex.get(sessionId);
|
|
328
|
-
if (idxMeta) {
|
|
329
|
-
entries.push({
|
|
330
|
-
id: sessionId,
|
|
331
|
-
cwd: idxMeta.cwd,
|
|
332
|
-
projectPath: idxMeta.cwd ?? projectDir.projectPath,
|
|
333
|
-
summary: idxMeta.summary,
|
|
334
|
-
createdAt: idxMeta.createdAt,
|
|
335
|
-
lastActivityAt: idxMeta.lastActivityAt,
|
|
336
|
-
messageCount: idxMeta.messageCount,
|
|
337
|
-
status: idxMeta.status,
|
|
338
|
-
});
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
341
|
-
try {
|
|
342
|
-
const stat = readFileSync(indexPath, 'utf-8');
|
|
343
|
-
const lines = stat.trim().length > 0 ? stat.trim().split('\n') : [];
|
|
344
|
-
const firstLine = lines.length > 0 ? JSON.parse(lines[0]) : null;
|
|
345
|
-
const lastLine = lines.length > 0 ? JSON.parse(lines[lines.length - 1]) : null;
|
|
346
|
-
entries.push({
|
|
347
|
-
id: sessionId,
|
|
348
|
-
projectPath: projectDir.projectPath,
|
|
349
|
-
summary: typeof firstLine?.content === 'string'
|
|
350
|
-
? firstLine.content.slice(0, 120)
|
|
351
|
-
: undefined,
|
|
352
|
-
createdAt: firstLine?.timestamp,
|
|
353
|
-
lastActivityAt: lastLine?.timestamp,
|
|
354
|
-
messageCount: lines.length,
|
|
355
|
-
status: firstLine?.status,
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
catch {
|
|
359
|
-
entries.push({ id: sessionId, projectPath: projectDir.projectPath });
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
442
|
+
const entries = this.collectSessionEntries(dir);
|
|
363
443
|
const limit = options.limit ?? 50;
|
|
364
|
-
entries.sort((a, b) => {
|
|
365
|
-
const da = a.lastActivityAt ?? a.createdAt ?? '';
|
|
366
|
-
const db = b.lastActivityAt ?? b.createdAt ?? '';
|
|
367
|
-
return db.localeCompare(da);
|
|
368
|
-
});
|
|
369
444
|
return { sessions: entries.slice(0, limit) };
|
|
370
445
|
}
|
|
371
446
|
catch (err) {
|
|
372
447
|
return { sessions: [], error: err instanceof Error ? err.message : String(err) };
|
|
373
448
|
}
|
|
374
449
|
}
|
|
450
|
+
/**
|
|
451
|
+
* 收集给定工作目录范围内的全部会话条目(task 6.10)。
|
|
452
|
+
*
|
|
453
|
+
* 复用 listSessions 的遍历逻辑:索引元数据优先(`~/.claude/sessions/*.json`),
|
|
454
|
+
* 缺失时回退到读取 `.jsonl` 首尾行。结果按 lastActivityAt/createdAt 倒序排列。
|
|
455
|
+
* searchSessions 与 listSessions 共享该集合以保持一致行为。
|
|
456
|
+
*/
|
|
457
|
+
collectSessionEntries(dir) {
|
|
458
|
+
const projectDirs = this.resolveProjectDirs(dir);
|
|
459
|
+
if (projectDirs.length === 0) {
|
|
460
|
+
return [];
|
|
461
|
+
}
|
|
462
|
+
const sessionsIndex = this.loadSessionsIndex();
|
|
463
|
+
const entries = [];
|
|
464
|
+
const seen = new Set();
|
|
465
|
+
for (const projectDir of projectDirs) {
|
|
466
|
+
const files = readdirSync(projectDir.path, { withFileTypes: true })
|
|
467
|
+
.filter((d) => d.isFile() && d.name.endsWith('.jsonl'));
|
|
468
|
+
for (const file of files) {
|
|
469
|
+
const sessionId = file.name.replace(/\.jsonl$/, '');
|
|
470
|
+
if (seen.has(sessionId))
|
|
471
|
+
continue;
|
|
472
|
+
seen.add(sessionId);
|
|
473
|
+
const indexPath = join(projectDir.path, file.name);
|
|
474
|
+
const idxMeta = sessionsIndex.get(sessionId);
|
|
475
|
+
if (idxMeta) {
|
|
476
|
+
entries.push({
|
|
477
|
+
id: sessionId,
|
|
478
|
+
cwd: idxMeta.cwd,
|
|
479
|
+
projectPath: idxMeta.cwd ?? projectDir.projectPath,
|
|
480
|
+
summary: idxMeta.summary,
|
|
481
|
+
createdAt: idxMeta.createdAt,
|
|
482
|
+
lastActivityAt: idxMeta.lastActivityAt,
|
|
483
|
+
messageCount: idxMeta.messageCount,
|
|
484
|
+
status: idxMeta.status,
|
|
485
|
+
});
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
try {
|
|
489
|
+
const stat = readFileSync(indexPath, 'utf-8');
|
|
490
|
+
const lines = stat.trim().length > 0 ? stat.trim().split('\n') : [];
|
|
491
|
+
const firstLine = lines.length > 0 ? JSON.parse(lines[0]) : null;
|
|
492
|
+
const lastLine = lines.length > 0 ? JSON.parse(lines[lines.length - 1]) : null;
|
|
493
|
+
entries.push({
|
|
494
|
+
id: sessionId,
|
|
495
|
+
projectPath: projectDir.projectPath,
|
|
496
|
+
summary: typeof firstLine?.content === 'string'
|
|
497
|
+
? firstLine.content.slice(0, 120)
|
|
498
|
+
: undefined,
|
|
499
|
+
createdAt: firstLine?.timestamp,
|
|
500
|
+
lastActivityAt: lastLine?.timestamp,
|
|
501
|
+
messageCount: lines.length,
|
|
502
|
+
status: firstLine?.status,
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
catch {
|
|
506
|
+
entries.push({ id: sessionId, projectPath: projectDir.projectPath });
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
entries.sort((a, b) => {
|
|
511
|
+
const da = a.lastActivityAt ?? a.createdAt ?? '';
|
|
512
|
+
const db = b.lastActivityAt ?? b.createdAt ?? '';
|
|
513
|
+
return db.localeCompare(da);
|
|
514
|
+
});
|
|
515
|
+
return entries;
|
|
516
|
+
}
|
|
375
517
|
// ── getSession ──
|
|
376
518
|
async getSession(sessionId, options = {}) {
|
|
377
519
|
try {
|
|
@@ -428,6 +570,124 @@ export class ClaudeAdapter {
|
|
|
428
570
|
return this.emptySessionResult(sessionId, err instanceof Error ? err.message : String(err));
|
|
429
571
|
}
|
|
430
572
|
}
|
|
573
|
+
// ── searchSessions(task 6.10) ──
|
|
574
|
+
/**
|
|
575
|
+
* 按可见文本字段搜索项目会话。命中 summary / customTitle / firstPrompt /
|
|
576
|
+
* cwd / projectName / projectPath(不区分大小写)。
|
|
577
|
+
*
|
|
578
|
+
* 实现为 collectSessionEntries + 内存过滤:daemon 侧结果集通常很小,
|
|
579
|
+
* 无需引擎原生搜索能力;与 listSessions 共享同一数据源保证一致性。
|
|
580
|
+
* cursor 为简单整数偏移字符串。
|
|
581
|
+
*/
|
|
582
|
+
async searchSessions(options) {
|
|
583
|
+
try {
|
|
584
|
+
const needle = options.query?.trim().toLowerCase();
|
|
585
|
+
if (!needle) {
|
|
586
|
+
return { sessions: [] };
|
|
587
|
+
}
|
|
588
|
+
const dir = options.dir?.trim();
|
|
589
|
+
const all = this.collectSessionEntries(dir);
|
|
590
|
+
const matched = all.filter((entry) => this.sessionEntryMatches(entry, needle));
|
|
591
|
+
const limit = options.limit ?? 50;
|
|
592
|
+
const start = this.cursorToOffset(options.cursor);
|
|
593
|
+
const page = matched.slice(start, start + limit);
|
|
594
|
+
const hasNext = start + limit < matched.length;
|
|
595
|
+
return {
|
|
596
|
+
sessions: page,
|
|
597
|
+
...(hasNext ? { nextCursor: String(start + limit) } : {}),
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
catch (err) {
|
|
601
|
+
return { sessions: [], error: err instanceof Error ? err.message : String(err) };
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
sessionEntryMatches(entry, needle) {
|
|
605
|
+
const fields = [
|
|
606
|
+
entry.summary,
|
|
607
|
+
entry.customTitle,
|
|
608
|
+
entry.firstPrompt,
|
|
609
|
+
entry.cwd,
|
|
610
|
+
entry.projectName,
|
|
611
|
+
entry.projectPath,
|
|
612
|
+
entry.gitBranch,
|
|
613
|
+
entry.tag,
|
|
614
|
+
];
|
|
615
|
+
return fields.some((value) => typeof value === 'string' && value.toLowerCase().includes(needle));
|
|
616
|
+
}
|
|
617
|
+
cursorToOffset(cursor) {
|
|
618
|
+
if (!cursor)
|
|
619
|
+
return 0;
|
|
620
|
+
const parsed = Number.parseInt(cursor, 10);
|
|
621
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
622
|
+
}
|
|
623
|
+
// ── deleteSession(task 6.10) ──
|
|
624
|
+
/**
|
|
625
|
+
* 幂等删除会话:删除 `~/.claude/projects/<dir>/<sessionId>.jsonl`,
|
|
626
|
+
* 并清理 `~/.claude/sessions/*.json` 索引中指向该 sessionId 的条目。
|
|
627
|
+
*
|
|
628
|
+
* - 文件不存在(或从未存在)→ `already_deleted`(幂等)。
|
|
629
|
+
* - 文件存在并删除成功 → `deleted`。
|
|
630
|
+
* - 删除失败(权限等)→ `rejected`(含 error)。
|
|
631
|
+
*
|
|
632
|
+
* 仅作用于 Engine 原生 Session 文件,不影响产品 conversation/task 记录(task 9.13)。
|
|
633
|
+
*/
|
|
634
|
+
async deleteSession(sessionId, _request) {
|
|
635
|
+
try {
|
|
636
|
+
const resolved = this.resolveSessionFile(sessionId);
|
|
637
|
+
if (!resolved || !existsSync(resolved.path)) {
|
|
638
|
+
// 索引条目仍可能残留:清理之,保持幂等语义
|
|
639
|
+
this.cleanSessionsIndex(sessionId);
|
|
640
|
+
return { status: 'already_deleted' };
|
|
641
|
+
}
|
|
642
|
+
try {
|
|
643
|
+
unlinkSync(resolved.path);
|
|
644
|
+
}
|
|
645
|
+
catch (err) {
|
|
646
|
+
return {
|
|
647
|
+
status: 'rejected',
|
|
648
|
+
error: `Failed to delete session file: ${err instanceof Error ? err.message : String(err)}`,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
this.cleanSessionsIndex(sessionId);
|
|
652
|
+
return { status: 'deleted' };
|
|
653
|
+
}
|
|
654
|
+
catch (err) {
|
|
655
|
+
return {
|
|
656
|
+
status: 'rejected',
|
|
657
|
+
error: err instanceof Error ? err.message : String(err),
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* 清理 `~/.claude/sessions/*.json` 索引中指向该 sessionId 的条目。
|
|
663
|
+
* 静默处理:索引不存在或解析失败不影响删除回执。
|
|
664
|
+
*/
|
|
665
|
+
cleanSessionsIndex(sessionId) {
|
|
666
|
+
try {
|
|
667
|
+
const sessionsDir = join(homedir(), '.claude', 'sessions');
|
|
668
|
+
if (!existsSync(sessionsDir))
|
|
669
|
+
return;
|
|
670
|
+
const files = readdirSync(sessionsDir, { withFileTypes: true });
|
|
671
|
+
for (const file of files) {
|
|
672
|
+
if (!file.isFile() || !file.name.endsWith('.json'))
|
|
673
|
+
continue;
|
|
674
|
+
const filePath = join(sessionsDir, file.name);
|
|
675
|
+
try {
|
|
676
|
+
const meta = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
677
|
+
if (meta?.sessionId !== sessionId)
|
|
678
|
+
continue;
|
|
679
|
+
// 该索引文件即为该会话的元数据;删除整个文件
|
|
680
|
+
unlinkSync(filePath);
|
|
681
|
+
}
|
|
682
|
+
catch {
|
|
683
|
+
/* skip malformed index entries */
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
catch {
|
|
688
|
+
/* index cleanup is best-effort */
|
|
689
|
+
}
|
|
690
|
+
}
|
|
431
691
|
// ── helpers for session queries ──
|
|
432
692
|
/** �?~/.claude/sessions/*.json 加载会话元数据索�?*/
|
|
433
693
|
loadSessionsIndex() {
|
|
@@ -702,5 +962,41 @@ export class ClaudeAdapter {
|
|
|
702
962
|
}
|
|
703
963
|
return null;
|
|
704
964
|
}
|
|
965
|
+
// 搬自 run-claude-agent.mjs:38-40,491-511
|
|
966
|
+
async *executeViaAgentSdk(task, signal) {
|
|
967
|
+
const sdkPath = process.env.CLAUDE_AGENT_SDK_PATH
|
|
968
|
+
|| '/home/node/.local/lib/node_modules/@anthropic-ai/claude-agent-sdk';
|
|
969
|
+
const require = createRequire(import.meta.url);
|
|
970
|
+
const sdk = require(sdkPath);
|
|
971
|
+
const { query } = sdk;
|
|
972
|
+
const isRoot = typeof process.getuid === 'function' && process.getuid() === 0;
|
|
973
|
+
const permissionMode = resolvePermissionMode(task);
|
|
974
|
+
const maxTurns = Math.min(task.claude?.maxTurns ?? 200, 500);
|
|
975
|
+
const allowedTools = task.claude?.tools ?? ['Skill', 'Read', 'Write', 'Bash', 'mcp__*'];
|
|
976
|
+
const queryOptions = {
|
|
977
|
+
cwd: task.workDir || process.cwd(),
|
|
978
|
+
settingSources: ['user', 'project'],
|
|
979
|
+
allowedTools,
|
|
980
|
+
permissionMode,
|
|
981
|
+
maxTurns,
|
|
982
|
+
};
|
|
983
|
+
if (task.resumeSession && task.sessionId)
|
|
984
|
+
queryOptions.resume = task.sessionId;
|
|
985
|
+
if (!isRoot)
|
|
986
|
+
queryOptions.allowDangerouslySkipPermissions = true;
|
|
987
|
+
for await (const message of query({
|
|
988
|
+
prompt: task.prompt || task.input || '',
|
|
989
|
+
options: queryOptions,
|
|
990
|
+
})) {
|
|
991
|
+
if (signal?.aborted)
|
|
992
|
+
return;
|
|
993
|
+
if (message.type === 'result' && message.result != null) {
|
|
994
|
+
yield { type: 'result', content: String(message.result) };
|
|
995
|
+
}
|
|
996
|
+
else if (message.type === 'assistant' && message.message != null) {
|
|
997
|
+
yield { type: 'delta', content: String(message.message), engineEvent: { type: 'content_delta', text: String(message.message) } };
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
705
1001
|
}
|
|
706
1002
|
//# sourceMappingURL=ClaudeAdapter.js.map
|