@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.
Files changed (57) hide show
  1. package/dist/adapters/claude/ClaudeAdapter.d.ts +59 -1
  2. package/dist/adapters/claude/ClaudeAdapter.d.ts.map +1 -1
  3. package/dist/adapters/claude/ClaudeAdapter.js +375 -79
  4. package/dist/adapters/claude/ClaudeAdapter.js.map +1 -1
  5. package/dist/adapters/claude/ClaudeInvocationUsageTracker.d.ts +17 -0
  6. package/dist/adapters/claude/ClaudeInvocationUsageTracker.d.ts.map +1 -0
  7. package/dist/adapters/claude/ClaudeInvocationUsageTracker.js +79 -0
  8. package/dist/adapters/claude/ClaudeInvocationUsageTracker.js.map +1 -0
  9. package/dist/adapters/claude/ClaudeStreamParser.d.ts +19 -0
  10. package/dist/adapters/claude/ClaudeStreamParser.d.ts.map +1 -1
  11. package/dist/adapters/claude/ClaudeStreamParser.js +92 -0
  12. package/dist/adapters/claude/ClaudeStreamParser.js.map +1 -1
  13. package/dist/adapters/codex/CodexAdapter.d.ts.map +1 -1
  14. package/dist/adapters/codex/CodexAdapter.js +36 -9
  15. package/dist/adapters/codex/CodexAdapter.js.map +1 -1
  16. package/dist/adapters/hermes/HermesAdapter.d.ts +9 -1
  17. package/dist/adapters/hermes/HermesAdapter.d.ts.map +1 -1
  18. package/dist/adapters/hermes/HermesAdapter.js +146 -7
  19. package/dist/adapters/hermes/HermesAdapter.js.map +1 -1
  20. package/dist/adapters/hermes/HermesInvocationUsageReader.d.ts +26 -0
  21. package/dist/adapters/hermes/HermesInvocationUsageReader.d.ts.map +1 -0
  22. package/dist/adapters/hermes/HermesInvocationUsageReader.js +128 -0
  23. package/dist/adapters/hermes/HermesInvocationUsageReader.js.map +1 -0
  24. package/dist/adapters/openclaw/OpenClawAdapter.d.ts.map +1 -1
  25. package/dist/adapters/openclaw/OpenClawAdapter.js +29 -7
  26. package/dist/adapters/openclaw/OpenClawAdapter.js.map +1 -1
  27. package/dist/core/AgentAdapter.d.ts +44 -1
  28. package/dist/core/AgentAdapter.d.ts.map +1 -1
  29. package/dist/core/AgentAdapter.js.map +1 -1
  30. package/dist/core/WinMatrixAgentClient.d.ts +23 -1
  31. package/dist/core/WinMatrixAgentClient.d.ts.map +1 -1
  32. package/dist/core/WinMatrixAgentClient.js +115 -12
  33. package/dist/core/WinMatrixAgentClient.js.map +1 -1
  34. package/dist/index.d.ts +13 -12
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +7 -13
  37. package/dist/index.js.map +1 -1
  38. package/dist/observability/WorkstationTokenUsageRecorder.d.ts +54 -0
  39. package/dist/observability/WorkstationTokenUsageRecorder.d.ts.map +1 -0
  40. package/dist/observability/WorkstationTokenUsageRecorder.js +264 -0
  41. package/dist/observability/WorkstationTokenUsageRecorder.js.map +1 -0
  42. package/dist/utils/envSanitizer.d.ts +38 -0
  43. package/dist/utils/envSanitizer.d.ts.map +1 -0
  44. package/dist/utils/envSanitizer.js +94 -0
  45. package/dist/utils/envSanitizer.js.map +1 -0
  46. package/dist/utils/errorClassification.d.ts +26 -0
  47. package/dist/utils/errorClassification.d.ts.map +1 -0
  48. package/dist/utils/errorClassification.js +52 -0
  49. package/dist/utils/errorClassification.js.map +1 -0
  50. package/dist/utils/processUtils.d.ts +10 -0
  51. package/dist/utils/processUtils.d.ts.map +1 -1
  52. package/dist/utils/processUtils.js +42 -9
  53. package/dist/utils/processUtils.js.map +1 -1
  54. package/dist/utils/resumeFallback.d.ts.map +1 -1
  55. package/dist/utils/resumeFallback.js +11 -1
  56. package/dist/utils/resumeFallback.js.map +1 -1
  57. 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
- this.claudePath = config?.claudePath ?? 'claude';
51
- this.mcpBridgeUrl = config?.mcpBridgeUrl;
52
- this.mcpApiKey = config?.mcpApiKey;
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 { type: 'error', message: `Work directory does not exist: ${workDir}` };
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
- args.push('--permission-mode', task.claude?.permissionMode ?? 'bypassPermissions');
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 ?? ['Bash', 'Read', 'Write', 'Edit'];
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 env = { ...process.env, ...(task.env ?? {}) };
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 { type: 'error', message: `Failed to spawn claude: ${err instanceof Error ? err.message : String(err)}` };
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
- proc.stdin?.end();
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: `璋冪敤鏈湴宸ュ叿: ${action.toolName}` };
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
- case 'error':
341
+ }
342
+ case 'error': {
235
343
  completed = true;
236
- yield { type: 'error', message: action.message };
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
- yield { type: 'error', message: startupError };
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
- yield {
265
- type: 'error',
266
- message: stderrText.trim() || exitResult.error || `Claude Code exited with code ${exitResult.code}`,
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 projectDirs = this.resolveProjectDirs(dir);
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