@winmatrix/agent-sdk 1.0.3 → 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 (102) hide show
  1. package/dist/adapters/claude/ClaudeAdapter.d.ts +60 -1
  2. package/dist/adapters/claude/ClaudeAdapter.d.ts.map +1 -1
  3. package/dist/adapters/claude/ClaudeAdapter.js +404 -84
  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/claude/claudeCommand.js +1 -1
  14. package/dist/adapters/claude/claudeCommand.js.map +1 -1
  15. package/dist/adapters/codex/CodexAdapter.d.ts +20 -0
  16. package/dist/adapters/codex/CodexAdapter.d.ts.map +1 -0
  17. package/dist/adapters/codex/CodexAdapter.js +313 -0
  18. package/dist/adapters/codex/CodexAdapter.js.map +1 -0
  19. package/dist/adapters/hermes/HermesAdapter.d.ts +10 -1
  20. package/dist/adapters/hermes/HermesAdapter.d.ts.map +1 -1
  21. package/dist/adapters/hermes/HermesAdapter.js +225 -21
  22. package/dist/adapters/hermes/HermesAdapter.js.map +1 -1
  23. package/dist/adapters/hermes/HermesInvocationUsageReader.d.ts +26 -0
  24. package/dist/adapters/hermes/HermesInvocationUsageReader.d.ts.map +1 -0
  25. package/dist/adapters/hermes/HermesInvocationUsageReader.js +128 -0
  26. package/dist/adapters/hermes/HermesInvocationUsageReader.js.map +1 -0
  27. package/dist/adapters/openclaw/OpenClawAdapter.d.ts +12 -37
  28. package/dist/adapters/openclaw/OpenClawAdapter.d.ts.map +1 -1
  29. package/dist/adapters/openclaw/OpenClawAdapter.js +191 -635
  30. package/dist/adapters/openclaw/OpenClawAdapter.js.map +1 -1
  31. package/dist/core/AgentAdapter.d.ts +75 -1
  32. package/dist/core/AgentAdapter.d.ts.map +1 -1
  33. package/dist/core/AgentAdapter.js +25 -0
  34. package/dist/core/AgentAdapter.js.map +1 -1
  35. package/dist/core/RuntimeDetector.d.ts.map +1 -1
  36. package/dist/core/RuntimeDetector.js +52 -19
  37. package/dist/core/RuntimeDetector.js.map +1 -1
  38. package/dist/core/WinMatrixAgentClient.d.ts +30 -4
  39. package/dist/core/WinMatrixAgentClient.d.ts.map +1 -1
  40. package/dist/core/WinMatrixAgentClient.js +122 -12
  41. package/dist/core/WinMatrixAgentClient.js.map +1 -1
  42. package/dist/index.d.ts +15 -20
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +8 -19
  45. package/dist/index.js.map +1 -1
  46. package/dist/observability/WorkstationTokenUsageRecorder.d.ts +54 -0
  47. package/dist/observability/WorkstationTokenUsageRecorder.d.ts.map +1 -0
  48. package/dist/observability/WorkstationTokenUsageRecorder.js +264 -0
  49. package/dist/observability/WorkstationTokenUsageRecorder.js.map +1 -0
  50. package/dist/utils/envSanitizer.d.ts +38 -0
  51. package/dist/utils/envSanitizer.d.ts.map +1 -0
  52. package/dist/utils/envSanitizer.js +94 -0
  53. package/dist/utils/envSanitizer.js.map +1 -0
  54. package/dist/utils/errorClassification.d.ts +26 -0
  55. package/dist/utils/errorClassification.d.ts.map +1 -0
  56. package/dist/utils/errorClassification.js +52 -0
  57. package/dist/utils/errorClassification.js.map +1 -0
  58. package/dist/utils/processUtils.d.ts +10 -0
  59. package/dist/utils/processUtils.d.ts.map +1 -1
  60. package/dist/utils/processUtils.js +42 -9
  61. package/dist/utils/processUtils.js.map +1 -1
  62. package/dist/utils/resumeFallback.d.ts +4 -0
  63. package/dist/utils/resumeFallback.d.ts.map +1 -0
  64. package/dist/utils/resumeFallback.js +65 -0
  65. package/dist/utils/resumeFallback.js.map +1 -0
  66. package/package.json +8 -4
  67. package/dist/adapters/openclaw/unwrapOpenClawPayloads.d.ts +0 -6
  68. package/dist/adapters/openclaw/unwrapOpenClawPayloads.d.ts.map +0 -1
  69. package/dist/adapters/openclaw/unwrapOpenClawPayloads.js +0 -24
  70. package/dist/adapters/openclaw/unwrapOpenClawPayloads.js.map +0 -1
  71. package/dist/runtime/DualRuntimeBridge.d.ts +0 -38
  72. package/dist/runtime/DualRuntimeBridge.d.ts.map +0 -1
  73. package/dist/runtime/DualRuntimeBridge.js +0 -321
  74. package/dist/runtime/DualRuntimeBridge.js.map +0 -1
  75. package/dist/runtime/bridgeConfigLoader.d.ts +0 -14
  76. package/dist/runtime/bridgeConfigLoader.d.ts.map +0 -1
  77. package/dist/runtime/bridgeConfigLoader.js +0 -133
  78. package/dist/runtime/bridgeConfigLoader.js.map +0 -1
  79. package/dist/runtime/bridgeConfigTypes.d.ts +0 -26
  80. package/dist/runtime/bridgeConfigTypes.d.ts.map +0 -1
  81. package/dist/runtime/bridgeConfigTypes.js +0 -6
  82. package/dist/runtime/bridgeConfigTypes.js.map +0 -1
  83. package/dist/runtime/bridgeHome.d.ts +0 -19
  84. package/dist/runtime/bridgeHome.d.ts.map +0 -1
  85. package/dist/runtime/bridgeHome.js +0 -61
  86. package/dist/runtime/bridgeHome.js.map +0 -1
  87. package/dist/runtime/bridgeReconnectPolicy.d.ts +0 -8
  88. package/dist/runtime/bridgeReconnectPolicy.d.ts.map +0 -1
  89. package/dist/runtime/bridgeReconnectPolicy.js +0 -16
  90. package/dist/runtime/bridgeReconnectPolicy.js.map +0 -1
  91. package/dist/runtime/bridgeTaskJournal.d.ts +0 -7
  92. package/dist/runtime/bridgeTaskJournal.d.ts.map +0 -1
  93. package/dist/runtime/bridgeTaskJournal.js +0 -66
  94. package/dist/runtime/bridgeTaskJournal.js.map +0 -1
  95. package/dist/runtime/localRuntimeAvailabilityMonitor.d.ts +0 -22
  96. package/dist/runtime/localRuntimeAvailabilityMonitor.d.ts.map +0 -1
  97. package/dist/runtime/localRuntimeAvailabilityMonitor.js +0 -81
  98. package/dist/runtime/localRuntimeAvailabilityMonitor.js.map +0 -1
  99. package/dist/runtime/runDualRuntimeBridge.d.ts +0 -2
  100. package/dist/runtime/runDualRuntimeBridge.d.ts.map +0 -1
  101. package/dist/runtime/runDualRuntimeBridge.js +0 -32
  102. package/dist/runtime/runDualRuntimeBridge.js.map +0 -1
@@ -7,11 +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';
14
+ import { executeWithResumeFallback } from '../../utils/resumeFallback.js';
15
+ import { classifyAdapterError } from '../../utils/errorClassification.js';
12
16
  export { parseSkillMd };
13
17
  import { isLocalTool } from '../../core/AgentAdapter.js';
14
- import { parseClaudeLine, splitLines, } from './ClaudeStreamParser.js';
18
+ import { parseClaudeLine, parseClaudeInvocationUsageFrame, splitLines, } from './ClaudeStreamParser.js';
19
+ import { ClaudeInvocationUsageTracker } from './ClaudeInvocationUsageTracker.js';
15
20
  import { resolveClaudeCommand } from './claudeCommand.js';
16
21
  const CLAUDE_BASE_ARGS = [
17
22
  '--print',
@@ -20,6 +25,20 @@ const CLAUDE_BASE_ARGS = [
20
25
  '--output-format', 'stream-json',
21
26
  '--include-partial-messages',
22
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
+ }
23
42
  export class ClaudeAdapter {
24
43
  meta = {
25
44
  agentType: 'claude-code',
@@ -28,7 +47,9 @@ export class ClaudeAdapter {
28
47
  streaming: true,
29
48
  toolCall: true,
30
49
  session: true,
31
- cancellation: false,
50
+ cancellation: true,
51
+ supportsCancel: true,
52
+ supportsResume: true,
32
53
  },
33
54
  tools: ['Bash', 'Read', 'Write', 'Edit', 'Glob', 'Grep', 'WebSearch', 'WebFetch', 'Task', 'Agent', 'NotebookEdit', 'EnterPlanMode'],
34
55
  skills: [],
@@ -42,22 +63,48 @@ export class ClaudeAdapter {
42
63
  claudePath;
43
64
  mcpBridgeUrl;
44
65
  mcpApiKey;
66
+ /** steer 递话队列与当前 run 的 stdin 句柄(P2.1/D5) */
67
+ steerQueue = [];
68
+ activeStdin = null;
69
+ workstationId;
70
+ tokenUsageRecorder;
45
71
  mcpConfigWritten = new Map();
46
72
  constructor(config) {
47
- this.claudePath = config?.claudePath ?? 'claude';
48
- this.mcpBridgeUrl = config?.mcpBridgeUrl;
49
- 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;
50
79
  }
51
80
  // ── execute ──
52
81
  async *execute(task, signal) {
82
+ yield* executeWithResumeFallback(task, signal, (nextTask, nextSignal) => this.executeOnce(nextTask, nextSignal));
83
+ }
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
+ }
53
90
  const workDir = task.workDir;
54
91
  if (workDir && !existsSync(workDir)) {
55
- 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
+ };
56
98
  return;
57
99
  }
58
100
  const args = [...CLAUDE_BASE_ARGS];
59
- // Permission mode
60
- 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);
61
108
  // workDir
62
109
  if (workDir) {
63
110
  args.push('--add-dir', workDir);
@@ -84,7 +131,9 @@ export class ClaudeAdapter {
84
131
  }
85
132
  }
86
133
  // Local tools (whitelist)
87
- 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'];
88
137
  args.push('--allowedTools', tools.join(','));
89
138
  // Disallowed tools
90
139
  if (task.claude?.disallowedTools?.length) {
@@ -115,8 +164,11 @@ export class ClaudeAdapter {
115
164
  else if (this.mcpBridgeUrl) {
116
165
  console.warn(`[ClaudeAdapter] Skipping MCP config: URL likely unreachable: ${this.mcpBridgeUrl}`);
117
166
  }
118
- // Build env
119
- 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
+ }
120
172
  // Spawn
121
173
  let proc;
122
174
  try {
@@ -126,10 +178,17 @@ export class ClaudeAdapter {
126
178
  env,
127
179
  cwd: workDir ?? process.cwd(),
128
180
  shell: spawnSpec.shell,
181
+ // POSIX 下独立进程组,killProcess 才能 kill(-pid) 杀整棵树
182
+ detached: process.platform !== 'win32',
129
183
  });
130
184
  }
131
185
  catch (err) {
132
- 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
+ };
133
192
  return;
134
193
  }
135
194
  // Send user message via stdin (stream-json protocol)
@@ -142,15 +201,23 @@ export class ClaudeAdapter {
142
201
  message: { role: 'user', content: task.input },
143
202
  }) + '\n';
144
203
  proc.stdin?.write(userMessage);
145
- proc.stdin?.end();
146
- // Drain stderr to prevent backpressure stall (--verbose produces lots of stderr)
147
- proc.stderr?.on('data', () => { });
204
+ // steer(P2.1/D5):stdin 保持打开,turn 结束(result 帧)时若有排队递话
205
+ // 则注入下一 turn;无排队才关闭 stdin CLI 退出
206
+ this.activeStdin = proc.stdin ?? null;
207
+ // Drain stderr to prevent backpressure stall, while keeping a bounded error summary.
208
+ let stderrText = '';
209
+ proc.stderr?.on('data', (chunk) => {
210
+ if (stderrText.length < 4096) {
211
+ stderrText += chunk.toString('utf-8');
212
+ }
213
+ });
148
214
  proc.stderr?.on('error', () => { });
149
215
  // Track result fields
150
216
  let extractedSessionId;
151
217
  let extractedResultText;
152
218
  let extractedUsage;
153
219
  let completed = false;
220
+ const invocationUsageTracker = new ClaudeInvocationUsageTracker();
154
221
  // Delta text accumulator: fallback for empty/missing result frame
155
222
  let accumulatedDeltaText = '';
156
223
  // Abort handler
@@ -169,18 +236,56 @@ export class ClaudeAdapter {
169
236
  const { lines, remainder } = splitLines(stdoutBuffer, text);
170
237
  stdoutBuffer = remainder;
171
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
+ }
172
269
  const action = parseClaudeLine(line);
173
270
  if (action.type === 'tool_use' && isLocalTool(action.toolName)) {
174
- yield { type: 'thinking', content: `璋冪敤鏈湴宸ュ叿: ${action.toolName}` };
271
+ yield { type: 'thinking', content: `调用本地工具: ${action.toolName}` };
175
272
  continue;
176
273
  }
177
274
  switch (action.type) {
178
275
  case 'delta':
179
276
  accumulatedDeltaText += action.text;
180
- yield { type: 'delta', content: action.text };
277
+ yield {
278
+ type: 'delta',
279
+ content: action.text,
280
+ engineEvent: { type: 'content_delta', text: action.text },
281
+ };
181
282
  break;
182
283
  case 'thinking':
183
- yield { type: 'thinking', content: action.text };
284
+ yield {
285
+ type: 'thinking',
286
+ content: action.text,
287
+ engineEvent: { type: 'thinking_delta', text: action.text },
288
+ };
184
289
  break;
185
290
  case 'tool_use':
186
291
  if (isLocalTool(action.toolName)) {
@@ -192,10 +297,15 @@ export class ClaudeAdapter {
192
297
  callId: randomUUID(),
193
298
  name: action.toolName,
194
299
  args: action.toolInput ?? {},
300
+ engineEvent: {
301
+ type: 'tool_call',
302
+ name: action.toolName,
303
+ input: action.toolInput,
304
+ },
195
305
  };
196
306
  }
197
307
  break;
198
- case 'result':
308
+ case 'result': {
199
309
  if (action.sessionId)
200
310
  extractedSessionId = action.sessionId;
201
311
  if (action.result)
@@ -206,11 +316,35 @@ export class ClaudeAdapter {
206
316
  output_tokens: action.usage.output_tokens,
207
317
  };
208
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
+ }
209
340
  break;
210
- case 'error':
341
+ }
342
+ case 'error': {
211
343
  completed = true;
212
- 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 };
213
346
  return;
347
+ }
214
348
  case 'skip':
215
349
  break;
216
350
  }
@@ -221,7 +355,8 @@ export class ClaudeAdapter {
221
355
  if (!completed) {
222
356
  completed = true;
223
357
  if (startupError) {
224
- 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 };
225
360
  return;
226
361
  }
227
362
  if (extractedSessionId || exitResult.code === 0) {
@@ -237,10 +372,13 @@ export class ClaudeAdapter {
237
372
  };
238
373
  }
239
374
  else {
240
- yield {
241
- type: 'error',
242
- message: exitResult.error ?? `Claude Code exited with code ${exitResult.code}`,
243
- };
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 };
244
382
  }
245
383
  }
246
384
  }
@@ -253,8 +391,26 @@ export class ClaudeAdapter {
253
391
  }
254
392
  catch { /* best-effort */ }
255
393
  }
394
+ this.activeStdin = null;
395
+ this.steerQueue = [];
256
396
  }
257
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
+ }
258
414
  // ── healthCheck ──
259
415
  async healthCheck() {
260
416
  return new Promise((resolve) => {
@@ -283,71 +439,81 @@ export class ClaudeAdapter {
283
439
  async listSessions(options) {
284
440
  try {
285
441
  const dir = options.dir?.trim();
286
- const projectDirs = this.resolveProjectDirs(dir);
287
- if (projectDirs.length === 0) {
288
- return { sessions: [] };
289
- }
290
- // load session index from ~/.claude/sessions/
291
- const sessionsIndex = this.loadSessionsIndex();
292
- const entries = [];
293
- const seen = new Set();
294
- for (const projectDir of projectDirs) {
295
- const files = readdirSync(projectDir.path, { withFileTypes: true })
296
- .filter((d) => d.isFile() && d.name.endsWith('.jsonl'));
297
- for (const file of files) {
298
- const sessionId = file.name.replace(/\.jsonl$/, '');
299
- if (seen.has(sessionId))
300
- continue;
301
- seen.add(sessionId);
302
- const indexPath = join(projectDir.path, file.name);
303
- const idxMeta = sessionsIndex.get(sessionId);
304
- if (idxMeta) {
305
- entries.push({
306
- id: sessionId,
307
- cwd: idxMeta.cwd,
308
- projectPath: idxMeta.cwd ?? projectDir.projectPath,
309
- summary: idxMeta.summary,
310
- createdAt: idxMeta.createdAt,
311
- lastActivityAt: idxMeta.lastActivityAt,
312
- messageCount: idxMeta.messageCount,
313
- status: idxMeta.status,
314
- });
315
- continue;
316
- }
317
- try {
318
- const stat = readFileSync(indexPath, 'utf-8');
319
- const lines = stat.trim().length > 0 ? stat.trim().split('\n') : [];
320
- const firstLine = lines.length > 0 ? JSON.parse(lines[0]) : null;
321
- const lastLine = lines.length > 0 ? JSON.parse(lines[lines.length - 1]) : null;
322
- entries.push({
323
- id: sessionId,
324
- projectPath: projectDir.projectPath,
325
- summary: typeof firstLine?.content === 'string'
326
- ? firstLine.content.slice(0, 120)
327
- : undefined,
328
- createdAt: firstLine?.timestamp,
329
- lastActivityAt: lastLine?.timestamp,
330
- messageCount: lines.length,
331
- status: firstLine?.status,
332
- });
333
- }
334
- catch {
335
- entries.push({ id: sessionId, projectPath: projectDir.projectPath });
336
- }
337
- }
338
- }
442
+ const entries = this.collectSessionEntries(dir);
339
443
  const limit = options.limit ?? 50;
340
- entries.sort((a, b) => {
341
- const da = a.lastActivityAt ?? a.createdAt ?? '';
342
- const db = b.lastActivityAt ?? b.createdAt ?? '';
343
- return db.localeCompare(da);
344
- });
345
444
  return { sessions: entries.slice(0, limit) };
346
445
  }
347
446
  catch (err) {
348
447
  return { sessions: [], error: err instanceof Error ? err.message : String(err) };
349
448
  }
350
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
+ }
351
517
  // ── getSession ──
352
518
  async getSession(sessionId, options = {}) {
353
519
  try {
@@ -404,6 +570,124 @@ export class ClaudeAdapter {
404
570
  return this.emptySessionResult(sessionId, err instanceof Error ? err.message : String(err));
405
571
  }
406
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
+ }
407
691
  // ── helpers for session queries ──
408
692
  /** �?~/.claude/sessions/*.json 加载会话元数据索�?*/
409
693
  loadSessionsIndex() {
@@ -678,5 +962,41 @@ export class ClaudeAdapter {
678
962
  }
679
963
  return null;
680
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
+ }
681
1001
  }
682
1002
  //# sourceMappingURL=ClaudeAdapter.js.map