@winmatrix/agent-sdk 1.0.4 → 1.0.6

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 (75) hide show
  1. package/dist/adapters/claude/ClaudeAdapter.d.ts +71 -1
  2. package/dist/adapters/claude/ClaudeAdapter.d.ts.map +1 -1
  3. package/dist/adapters/claude/ClaudeAdapter.js +490 -86
  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 +15 -1
  14. package/dist/adapters/codex/CodexAdapter.d.ts.map +1 -1
  15. package/dist/adapters/codex/CodexAdapter.js +164 -14
  16. package/dist/adapters/codex/CodexAdapter.js.map +1 -1
  17. package/dist/adapters/codex/CodexSessionStore.d.ts +42 -0
  18. package/dist/adapters/codex/CodexSessionStore.d.ts.map +1 -0
  19. package/dist/adapters/codex/CodexSessionStore.js +279 -0
  20. package/dist/adapters/codex/CodexSessionStore.js.map +1 -0
  21. package/dist/adapters/hermes/HermesAdapter.d.ts +10 -1
  22. package/dist/adapters/hermes/HermesAdapter.d.ts.map +1 -1
  23. package/dist/adapters/hermes/HermesAdapter.js +177 -10
  24. package/dist/adapters/hermes/HermesAdapter.js.map +1 -1
  25. package/dist/adapters/hermes/HermesInvocationUsageReader.d.ts +26 -0
  26. package/dist/adapters/hermes/HermesInvocationUsageReader.d.ts.map +1 -0
  27. package/dist/adapters/hermes/HermesInvocationUsageReader.js +128 -0
  28. package/dist/adapters/hermes/HermesInvocationUsageReader.js.map +1 -0
  29. package/dist/adapters/openclaw/OpenClawAdapter.d.ts +3 -7
  30. package/dist/adapters/openclaw/OpenClawAdapter.d.ts.map +1 -1
  31. package/dist/adapters/openclaw/OpenClawAdapter.js +116 -45
  32. package/dist/adapters/openclaw/OpenClawAdapter.js.map +1 -1
  33. package/dist/core/AgentAdapter.d.ts +51 -2
  34. package/dist/core/AgentAdapter.d.ts.map +1 -1
  35. package/dist/core/AgentAdapter.js.map +1 -1
  36. package/dist/core/WinMatrixAgentClient.d.ts +23 -1
  37. package/dist/core/WinMatrixAgentClient.d.ts.map +1 -1
  38. package/dist/core/WinMatrixAgentClient.js +115 -12
  39. package/dist/core/WinMatrixAgentClient.js.map +1 -1
  40. package/dist/index.d.ts +18 -12
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +10 -13
  43. package/dist/index.js.map +1 -1
  44. package/dist/observability/WorkstationTokenUsageRecorder.d.ts +54 -0
  45. package/dist/observability/WorkstationTokenUsageRecorder.d.ts.map +1 -0
  46. package/dist/observability/WorkstationTokenUsageRecorder.js +264 -0
  47. package/dist/observability/WorkstationTokenUsageRecorder.js.map +1 -0
  48. package/dist/utils/contextBudget.d.ts +27 -0
  49. package/dist/utils/contextBudget.d.ts.map +1 -0
  50. package/dist/utils/contextBudget.js +86 -0
  51. package/dist/utils/contextBudget.js.map +1 -0
  52. package/dist/utils/envSanitizer.d.ts +38 -0
  53. package/dist/utils/envSanitizer.d.ts.map +1 -0
  54. package/dist/utils/envSanitizer.js +94 -0
  55. package/dist/utils/envSanitizer.js.map +1 -0
  56. package/dist/utils/errorClassification.d.ts +26 -0
  57. package/dist/utils/errorClassification.d.ts.map +1 -0
  58. package/dist/utils/errorClassification.js +52 -0
  59. package/dist/utils/errorClassification.js.map +1 -0
  60. package/dist/utils/mcpUrlValidation.d.ts +10 -0
  61. package/dist/utils/mcpUrlValidation.d.ts.map +1 -0
  62. package/dist/utils/mcpUrlValidation.js +18 -0
  63. package/dist/utils/mcpUrlValidation.js.map +1 -0
  64. package/dist/utils/processUtils.d.ts +10 -0
  65. package/dist/utils/processUtils.d.ts.map +1 -1
  66. package/dist/utils/processUtils.js +42 -9
  67. package/dist/utils/processUtils.js.map +1 -1
  68. package/dist/utils/promptGuardrails.d.ts +15 -0
  69. package/dist/utils/promptGuardrails.d.ts.map +1 -0
  70. package/dist/utils/promptGuardrails.js +26 -0
  71. package/dist/utils/promptGuardrails.js.map +1 -0
  72. package/dist/utils/resumeFallback.d.ts.map +1 -1
  73. package/dist/utils/resumeFallback.js +39 -1
  74. package/dist/utils/resumeFallback.js.map +1 -1
  75. package/package.json +3 -4
@@ -7,12 +7,19 @@ 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 { validateMcpBridgeUrl } from '../../utils/mcpUrlValidation.js';
16
+ import { withSecurityPrefix } from '../../utils/promptGuardrails.js';
17
+ import { classifyAdapterError } from '../../utils/errorClassification.js';
18
+ import { applyContextBudget } from '../../utils/contextBudget.js';
13
19
  export { parseSkillMd };
14
20
  import { isLocalTool } from '../../core/AgentAdapter.js';
15
- import { parseClaudeLine, splitLines, } from './ClaudeStreamParser.js';
21
+ import { parseClaudeLine, parseClaudeInvocationUsageFrame, splitLines, } from './ClaudeStreamParser.js';
22
+ import { ClaudeInvocationUsageTracker } from './ClaudeInvocationUsageTracker.js';
16
23
  import { resolveClaudeCommand } from './claudeCommand.js';
17
24
  const CLAUDE_BASE_ARGS = [
18
25
  '--print',
@@ -21,6 +28,20 @@ const CLAUDE_BASE_ARGS = [
21
28
  '--output-format', 'stream-json',
22
29
  '--include-partial-messages',
23
30
  ];
31
+ /**
32
+ * 权限默认档(harden-external-computer-daemon P1/D4)。
33
+ *
34
+ * 优先级:task 显式声明 > daemon 环境变量 > 'acceptEdits'(安全默认)。
35
+ * 交互确认通道(TASK_INTERACTION_RESPOND)当前未接通,print 模式下权限
36
+ * 提示会被自动拒绝——acceptEdits 下 Bash 等未预批工具将被拒;需要旧行为
37
+ * 的部署显式设 WINMATRIX_DAEMON_CLAUDE_PERMISSION_MODE=bypassPermissions,
38
+ * 或在任务配置里声明 permissionMode/allowedTools。
39
+ */
40
+ function resolvePermissionMode(task) {
41
+ return task.claude?.permissionMode
42
+ ?? process.env.WINMATRIX_DAEMON_CLAUDE_PERMISSION_MODE
43
+ ?? 'acceptEdits';
44
+ }
24
45
  export class ClaudeAdapter {
25
46
  meta = {
26
47
  agentType: 'claude-code',
@@ -45,25 +66,63 @@ export class ClaudeAdapter {
45
66
  claudePath;
46
67
  mcpBridgeUrl;
47
68
  mcpApiKey;
69
+ /** steer 递话队列与当前 run 的 stdin 句柄(P2.1/D5) */
70
+ steerQueue = [];
71
+ activeStdin = null;
72
+ workstationId;
73
+ tokenUsageRecorder;
48
74
  mcpConfigWritten = new Map();
49
75
  constructor(config) {
50
- this.claudePath = config?.claudePath ?? 'claude';
51
- this.mcpBridgeUrl = config?.mcpBridgeUrl;
52
- this.mcpApiKey = config?.mcpApiKey;
76
+ const options = config;
77
+ this.claudePath = options?.claudePath ?? 'claude';
78
+ this.mcpBridgeUrl = options?.mcpBridgeUrl;
79
+ this.mcpApiKey = options?.mcpApiKey;
80
+ this.workstationId = options?.workstationId;
81
+ this.tokenUsageRecorder = options?.tokenUsageRecorder;
53
82
  }
54
83
  // ── execute ──
55
84
  async *execute(task, signal) {
56
85
  yield* executeWithResumeFallback(task, signal, (nextTask, nextSignal) => this.executeOnce(nextTask, nextSignal));
57
86
  }
58
87
  async *executeOnce(task, signal) {
88
+ // Pod 场景:通过 @anthropic-ai/claude-agent-sdk.query() 调 claude
89
+ if (task.claude?.useAgentSdk) {
90
+ yield* this.executeViaAgentSdk(task, signal);
91
+ return;
92
+ }
59
93
  const workDir = task.workDir;
60
94
  if (workDir && !existsSync(workDir)) {
61
- yield { type: 'error', message: `Work directory does not exist: ${workDir}` };
95
+ yield {
96
+ type: 'error',
97
+ message: `Work directory does not exist: ${workDir}`,
98
+ code: 'cli_crash',
99
+ retryable: false,
100
+ };
62
101
  return;
63
102
  }
103
+ const budgeted = applyContextBudget({
104
+ ...task,
105
+ systemPrompt: task.systemPrompt ?? task.claude?.systemPrompt,
106
+ });
107
+ if (budgeted.diagnostics) {
108
+ const d = budgeted.diagnostics;
109
+ yield {
110
+ type: 'thinking',
111
+ content: `[ContextBudget] input ${d.originalInputChars}→${d.finalInputChars} chars (${d.compression})`,
112
+ engineEvent: {
113
+ type: 'status',
114
+ text: `[ContextBudget] input ${d.originalInputChars}→${d.finalInputChars} chars (${d.compression})`,
115
+ },
116
+ };
117
+ }
64
118
  const args = [...CLAUDE_BASE_ARGS];
65
- // Permission mode
66
- args.push('--permission-mode', task.claude?.permissionMode ?? 'bypassPermissions');
119
+ // Permission mode(harden-external-computer-daemon P1/D4:默认档降为 acceptEdits)
120
+ const permissionMode = resolvePermissionMode(task);
121
+ if (permissionMode === 'bypassPermissions') {
122
+ console.warn('[ClaudeAdapter] 本次任务以 bypassPermissions 运行(显式声明)。' +
123
+ '外部 CLI 将不做任何权限确认,请确认任务来源可信。');
124
+ }
125
+ args.push('--permission-mode', permissionMode);
67
126
  // workDir
68
127
  if (workDir) {
69
128
  args.push('--add-dir', workDir);
@@ -89,8 +148,11 @@ export class ClaudeAdapter {
89
148
  args.push('--session-id', sessionId);
90
149
  }
91
150
  }
92
- // Local tools (whitelist)
93
- const tools = task.claude?.tools ?? ['Bash', 'Read', 'Write', 'Edit'];
151
+ // Local tools (whitelist)。CLI 默认须与 Agent SDK 路径对齐:含 Skill + mcp__*,
152
+ // 否则 print/acceptEdits WinMatrix MCP 会全部变成 "haven't granted it yet"。
153
+ const tools = task.claude?.tools?.length
154
+ ? task.claude.tools
155
+ : ['Skill', 'Bash', 'Read', 'Write', 'Edit', 'Glob', 'Grep', 'mcp__*'];
94
156
  args.push('--allowedTools', tools.join(','));
95
157
  // Disallowed tools
96
158
  if (task.claude?.disallowedTools?.length) {
@@ -101,14 +163,14 @@ export class ClaudeAdapter {
101
163
  args.push('--max-budget-usd', String(task.claude.maxBudgetUsd));
102
164
  }
103
165
  // System prompt: task.systemPrompt 优先�?task.claude.systemPrompt
104
- const effectiveSystemPrompt = task.systemPrompt ?? task.claude?.systemPrompt;
166
+ const effectiveSystemPrompt = budgeted.systemPrompt ?? task.claude?.systemPrompt;
105
167
  if (effectiveSystemPrompt) {
106
168
  args.push('--append-system-prompt', effectiveSystemPrompt);
107
169
  }
108
- // MCP Bridge 配置:在 workDir 下生成 .claude/mcp.json,让 Claude Code 能调用 WinMatrix 工具
109
- // 仅在 URL 格式合理时生成(避免不可达 URL 导致 CC 启动卡住)
170
+ // MCP Bridge 配置:在 workDir 下生成 .claude/mcp.json,让 Claude Code 能调用 WinMatrix 工具。
171
+ // 任务 token 已由服务端签发,必须优先使用,不能因集群内短服务名启发式而回退。
110
172
  let mcpConfigFile;
111
- if (this.mcpBridgeUrl && this.isMcpBridgeUrlLikelyReachable()) {
173
+ if (this.mcpBridgeUrl && (task.mcpToken || this.isMcpBridgeUrlLikelyReachable())) {
112
174
  const mcpArgs = this.generateMcpConfig(workDir, task.mcpToken);
113
175
  if (mcpArgs) {
114
176
  args.push(...mcpArgs);
@@ -121,8 +183,11 @@ export class ClaudeAdapter {
121
183
  else if (this.mcpBridgeUrl) {
122
184
  console.warn(`[ClaudeAdapter] Skipping MCP config: URL likely unreachable: ${this.mcpBridgeUrl}`);
123
185
  }
124
- // Build env
125
- const env = { ...process.env, ...(task.env ?? {}) };
186
+ // Build env(先剥凭据后显式加,harden-external-computer-daemon P1/D3)
187
+ const { env, strippedKeys } = sanitizeTaskEnv(task.env);
188
+ if (strippedKeys.length > 0) {
189
+ console.warn(`[ClaudeAdapter] 已剥离 ${strippedKeys.length} 个凭据形态环境变量(仅列 key 名): ${strippedKeys.join(', ')}`);
190
+ }
126
191
  // Spawn
127
192
  let proc;
128
193
  try {
@@ -132,10 +197,17 @@ export class ClaudeAdapter {
132
197
  env,
133
198
  cwd: workDir ?? process.cwd(),
134
199
  shell: spawnSpec.shell,
200
+ // POSIX 下独立进程组,killProcess 才能 kill(-pid) 杀整棵树
201
+ detached: process.platform !== 'win32',
135
202
  });
136
203
  }
137
204
  catch (err) {
138
- yield { type: 'error', message: `Failed to spawn claude: ${err instanceof Error ? err.message : String(err)}` };
205
+ yield {
206
+ type: 'error',
207
+ message: `Failed to spawn claude: ${err instanceof Error ? err.message : String(err)}`,
208
+ code: 'cli_crash',
209
+ retryable: false,
210
+ };
139
211
  return;
140
212
  }
141
213
  // Send user message via stdin (stream-json protocol)
@@ -145,10 +217,12 @@ export class ClaudeAdapter {
145
217
  });
146
218
  const userMessage = JSON.stringify({
147
219
  type: 'user',
148
- message: { role: 'user', content: task.input },
220
+ message: { role: 'user', content: withSecurityPrefix(budgeted.input) },
149
221
  }) + '\n';
150
222
  proc.stdin?.write(userMessage);
151
- proc.stdin?.end();
223
+ // steer(P2.1/D5):stdin 保持打开,turn 结束(result 帧)时若有排队递话
224
+ // 则注入下一 turn;无排队才关闭 stdin 让 CLI 退出
225
+ this.activeStdin = proc.stdin ?? null;
152
226
  // Drain stderr to prevent backpressure stall, while keeping a bounded error summary.
153
227
  let stderrText = '';
154
228
  proc.stderr?.on('data', (chunk) => {
@@ -162,6 +236,7 @@ export class ClaudeAdapter {
162
236
  let extractedResultText;
163
237
  let extractedUsage;
164
238
  let completed = false;
239
+ const invocationUsageTracker = new ClaudeInvocationUsageTracker();
165
240
  // Delta text accumulator: fallback for empty/missing result frame
166
241
  let accumulatedDeltaText = '';
167
242
  // Abort handler
@@ -180,9 +255,39 @@ export class ClaudeAdapter {
180
255
  const { lines, remainder } = splitLines(stdoutBuffer, text);
181
256
  stdoutBuffer = remainder;
182
257
  for (const line of lines) {
258
+ const observedAt = new Date().toISOString();
259
+ const usageFrame = parseClaudeInvocationUsageFrame(line);
260
+ if (usageFrame && this.tokenUsageRecorder) {
261
+ const completedUsage = invocationUsageTracker.accept(usageFrame, observedAt);
262
+ if (completedUsage) {
263
+ const workstationId = this.workstationId ?? task.env?.WINMATRIX_WORKSTATION_ID ?? process.env.WINMATRIX_WORKSTATION_ID;
264
+ if (!workstationId?.trim()) {
265
+ throw new Error('WORKSTATION_TOKEN_USAGE_WORKSTATION_ID_MISSING');
266
+ }
267
+ await this.tokenUsageRecorder.recordCompletedInvocation({
268
+ workstationId,
269
+ runtime: 'claude-code',
270
+ sourceInvocationId: completedUsage.sourceInvocationId,
271
+ provider: 'claude',
272
+ startedAt: completedUsage.startedAt,
273
+ occurredAt: completedUsage.occurredAt,
274
+ timeSource: completedUsage.timeSource,
275
+ sessionId: completedUsage.sessionId ?? extractedSessionId ?? sessionId,
276
+ taskId: task.taskId,
277
+ modelUsages: [{
278
+ sourceModelUsageId: `${completedUsage.sourceInvocationId}:model:0`,
279
+ model: completedUsage.model,
280
+ inputTokens: completedUsage.usage.inputTokens,
281
+ outputTokens: completedUsage.usage.outputTokens,
282
+ cacheReadTokens: completedUsage.usage.cacheReadTokens,
283
+ cacheCreationTokens: completedUsage.usage.cacheCreationTokens,
284
+ }],
285
+ });
286
+ }
287
+ }
183
288
  const action = parseClaudeLine(line);
184
289
  if (action.type === 'tool_use' && isLocalTool(action.toolName)) {
185
- yield { type: 'thinking', content: `璋冪敤鏈湴宸ュ叿: ${action.toolName}` };
290
+ yield { type: 'thinking', content: `调用本地工具: ${action.toolName}` };
186
291
  continue;
187
292
  }
188
293
  switch (action.type) {
@@ -206,20 +311,22 @@ export class ClaudeAdapter {
206
311
  yield { type: 'thinking', content: `调用本地工具: ${action.toolName}` };
207
312
  }
208
313
  else {
314
+ const callId = randomUUID();
209
315
  yield {
210
316
  type: 'tool_call',
211
- callId: randomUUID(),
317
+ callId,
212
318
  name: action.toolName,
213
319
  args: action.toolInput ?? {},
214
320
  engineEvent: {
215
321
  type: 'tool_call',
322
+ callId,
216
323
  name: action.toolName,
217
324
  input: action.toolInput,
218
325
  },
219
326
  };
220
327
  }
221
328
  break;
222
- case 'result':
329
+ case 'result': {
223
330
  if (action.sessionId)
224
331
  extractedSessionId = action.sessionId;
225
332
  if (action.result)
@@ -230,11 +337,35 @@ export class ClaudeAdapter {
230
337
  output_tokens: action.usage.output_tokens,
231
338
  };
232
339
  }
340
+ // steer:本 turn 结束。有排队递话则注入下一 turn 继续;
341
+ // 否则关闭 stdin,CLI 处理完退出
342
+ const nextSteer = this.steerQueue.shift();
343
+ if (nextSteer && proc.stdin && !proc.stdin.destroyed && proc.stdin.writable) {
344
+ accumulatedDeltaText = '';
345
+ proc.stdin.write(JSON.stringify({
346
+ type: 'user',
347
+ message: { role: 'user', content: nextSteer },
348
+ }) + '\n');
349
+ yield {
350
+ type: 'thinking',
351
+ content: `[steer] 已注入纠偏消息(队列剩余 ${this.steerQueue.length} 条),继续执行`,
352
+ engineEvent: { type: 'status', text: 'steer_applied' },
353
+ };
354
+ }
355
+ else {
356
+ try {
357
+ proc.stdin?.end();
358
+ }
359
+ catch { /* already closed */ }
360
+ }
233
361
  break;
234
- case 'error':
362
+ }
363
+ case 'error': {
235
364
  completed = true;
236
- yield { type: 'error', message: action.message };
365
+ const classified = classifyAdapterError({ message: action.message, aborted: signal?.aborted });
366
+ yield { type: 'error', message: action.message, code: classified.code, retryable: classified.retryable };
237
367
  return;
368
+ }
238
369
  case 'skip':
239
370
  break;
240
371
  }
@@ -245,7 +376,8 @@ export class ClaudeAdapter {
245
376
  if (!completed) {
246
377
  completed = true;
247
378
  if (startupError) {
248
- yield { type: 'error', message: startupError };
379
+ const classified = classifyAdapterError({ message: startupError, aborted: signal?.aborted });
380
+ yield { type: 'error', message: startupError, code: classified.code, retryable: classified.retryable };
249
381
  return;
250
382
  }
251
383
  if (extractedSessionId || exitResult.code === 0) {
@@ -261,10 +393,13 @@ export class ClaudeAdapter {
261
393
  };
262
394
  }
263
395
  else {
264
- yield {
265
- type: 'error',
266
- message: stderrText.trim() || exitResult.error || `Claude Code exited with code ${exitResult.code}`,
267
- };
396
+ const message = stderrText.trim() || exitResult.error || `Claude Code exited with code ${exitResult.code}`;
397
+ const classified = classifyAdapterError({
398
+ message,
399
+ exitCode: exitResult.code,
400
+ aborted: signal?.aborted,
401
+ });
402
+ yield { type: 'error', message, code: classified.code, retryable: classified.retryable };
268
403
  }
269
404
  }
270
405
  }
@@ -277,7 +412,25 @@ export class ClaudeAdapter {
277
412
  }
278
413
  catch { /* best-effort */ }
279
414
  }
415
+ this.activeStdin = null;
416
+ this.steerQueue = [];
417
+ }
418
+ }
419
+ /**
420
+ * 中途递话(P2.1/D5):将纠偏消息排队,当前 turn 结束(result 帧)时注入
421
+ * stdin 开启下一 turn(dsh steer 语义的 stream-json 落地——stream-json 协议
422
+ * 在 turn 边界处理排队的 user 消息)。
423
+ */
424
+ steer(text) {
425
+ const stdin = this.activeStdin;
426
+ if (!stdin || stdin.destroyed || !stdin.writable) {
427
+ return { accepted: false, reason: 'no_active_run' };
280
428
  }
429
+ const trimmed = text.trim();
430
+ if (!trimmed)
431
+ return { accepted: false, reason: 'empty_text' };
432
+ this.steerQueue.push(trimmed);
433
+ return { accepted: true };
281
434
  }
282
435
  // ── healthCheck ──
283
436
  async healthCheck() {
@@ -307,71 +460,81 @@ export class ClaudeAdapter {
307
460
  async listSessions(options) {
308
461
  try {
309
462
  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
- }
463
+ const entries = this.collectSessionEntries(dir);
363
464
  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
465
  return { sessions: entries.slice(0, limit) };
370
466
  }
371
467
  catch (err) {
372
468
  return { sessions: [], error: err instanceof Error ? err.message : String(err) };
373
469
  }
374
470
  }
471
+ /**
472
+ * 收集给定工作目录范围内的全部会话条目(task 6.10)。
473
+ *
474
+ * 复用 listSessions 的遍历逻辑:索引元数据优先(`~/.claude/sessions/*.json`),
475
+ * 缺失时回退到读取 `.jsonl` 首尾行。结果按 lastActivityAt/createdAt 倒序排列。
476
+ * searchSessions 与 listSessions 共享该集合以保持一致行为。
477
+ */
478
+ collectSessionEntries(dir) {
479
+ const projectDirs = this.resolveProjectDirs(dir);
480
+ if (projectDirs.length === 0) {
481
+ return [];
482
+ }
483
+ const sessionsIndex = this.loadSessionsIndex();
484
+ const entries = [];
485
+ const seen = new Set();
486
+ for (const projectDir of projectDirs) {
487
+ const files = readdirSync(projectDir.path, { withFileTypes: true })
488
+ .filter((d) => d.isFile() && d.name.endsWith('.jsonl'));
489
+ for (const file of files) {
490
+ const sessionId = file.name.replace(/\.jsonl$/, '');
491
+ if (seen.has(sessionId))
492
+ continue;
493
+ seen.add(sessionId);
494
+ const indexPath = join(projectDir.path, file.name);
495
+ const idxMeta = sessionsIndex.get(sessionId);
496
+ if (idxMeta) {
497
+ entries.push({
498
+ id: sessionId,
499
+ cwd: idxMeta.cwd,
500
+ projectPath: idxMeta.cwd ?? projectDir.projectPath,
501
+ summary: idxMeta.summary,
502
+ createdAt: idxMeta.createdAt,
503
+ lastActivityAt: idxMeta.lastActivityAt,
504
+ messageCount: idxMeta.messageCount,
505
+ status: idxMeta.status,
506
+ });
507
+ continue;
508
+ }
509
+ try {
510
+ const stat = readFileSync(indexPath, 'utf-8');
511
+ const lines = stat.trim().length > 0 ? stat.trim().split('\n') : [];
512
+ const firstLine = lines.length > 0 ? JSON.parse(lines[0]) : null;
513
+ const lastLine = lines.length > 0 ? JSON.parse(lines[lines.length - 1]) : null;
514
+ entries.push({
515
+ id: sessionId,
516
+ projectPath: projectDir.projectPath,
517
+ summary: typeof firstLine?.content === 'string'
518
+ ? firstLine.content.slice(0, 120)
519
+ : undefined,
520
+ createdAt: firstLine?.timestamp,
521
+ lastActivityAt: lastLine?.timestamp,
522
+ messageCount: lines.length,
523
+ status: firstLine?.status,
524
+ });
525
+ }
526
+ catch {
527
+ entries.push({ id: sessionId, projectPath: projectDir.projectPath });
528
+ }
529
+ }
530
+ }
531
+ entries.sort((a, b) => {
532
+ const da = a.lastActivityAt ?? a.createdAt ?? '';
533
+ const db = b.lastActivityAt ?? b.createdAt ?? '';
534
+ return db.localeCompare(da);
535
+ });
536
+ return entries;
537
+ }
375
538
  // ── getSession ──
376
539
  async getSession(sessionId, options = {}) {
377
540
  try {
@@ -428,6 +591,124 @@ export class ClaudeAdapter {
428
591
  return this.emptySessionResult(sessionId, err instanceof Error ? err.message : String(err));
429
592
  }
430
593
  }
594
+ // ── searchSessions(task 6.10) ──
595
+ /**
596
+ * 按可见文本字段搜索项目会话。命中 summary / customTitle / firstPrompt /
597
+ * cwd / projectName / projectPath(不区分大小写)。
598
+ *
599
+ * 实现为 collectSessionEntries + 内存过滤:daemon 侧结果集通常很小,
600
+ * 无需引擎原生搜索能力;与 listSessions 共享同一数据源保证一致性。
601
+ * cursor 为简单整数偏移字符串。
602
+ */
603
+ async searchSessions(options) {
604
+ try {
605
+ const needle = options.query?.trim().toLowerCase();
606
+ if (!needle) {
607
+ return { sessions: [] };
608
+ }
609
+ const dir = options.dir?.trim();
610
+ const all = this.collectSessionEntries(dir);
611
+ const matched = all.filter((entry) => this.sessionEntryMatches(entry, needle));
612
+ const limit = options.limit ?? 50;
613
+ const start = this.cursorToOffset(options.cursor);
614
+ const page = matched.slice(start, start + limit);
615
+ const hasNext = start + limit < matched.length;
616
+ return {
617
+ sessions: page,
618
+ ...(hasNext ? { nextCursor: String(start + limit) } : {}),
619
+ };
620
+ }
621
+ catch (err) {
622
+ return { sessions: [], error: err instanceof Error ? err.message : String(err) };
623
+ }
624
+ }
625
+ sessionEntryMatches(entry, needle) {
626
+ const fields = [
627
+ entry.summary,
628
+ entry.customTitle,
629
+ entry.firstPrompt,
630
+ entry.cwd,
631
+ entry.projectName,
632
+ entry.projectPath,
633
+ entry.gitBranch,
634
+ entry.tag,
635
+ ];
636
+ return fields.some((value) => typeof value === 'string' && value.toLowerCase().includes(needle));
637
+ }
638
+ cursorToOffset(cursor) {
639
+ if (!cursor)
640
+ return 0;
641
+ const parsed = Number.parseInt(cursor, 10);
642
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
643
+ }
644
+ // ── deleteSession(task 6.10) ──
645
+ /**
646
+ * 幂等删除会话:删除 `~/.claude/projects/<dir>/<sessionId>.jsonl`,
647
+ * 并清理 `~/.claude/sessions/*.json` 索引中指向该 sessionId 的条目。
648
+ *
649
+ * - 文件不存在(或从未存在)→ `already_deleted`(幂等)。
650
+ * - 文件存在并删除成功 → `deleted`。
651
+ * - 删除失败(权限等)→ `rejected`(含 error)。
652
+ *
653
+ * 仅作用于 Engine 原生 Session 文件,不影响产品 conversation/task 记录(task 9.13)。
654
+ */
655
+ async deleteSession(sessionId, _request) {
656
+ try {
657
+ const resolved = this.resolveSessionFile(sessionId);
658
+ if (!resolved || !existsSync(resolved.path)) {
659
+ // 索引条目仍可能残留:清理之,保持幂等语义
660
+ this.cleanSessionsIndex(sessionId);
661
+ return { status: 'already_deleted' };
662
+ }
663
+ try {
664
+ unlinkSync(resolved.path);
665
+ }
666
+ catch (err) {
667
+ return {
668
+ status: 'rejected',
669
+ error: `Failed to delete session file: ${err instanceof Error ? err.message : String(err)}`,
670
+ };
671
+ }
672
+ this.cleanSessionsIndex(sessionId);
673
+ return { status: 'deleted' };
674
+ }
675
+ catch (err) {
676
+ return {
677
+ status: 'rejected',
678
+ error: err instanceof Error ? err.message : String(err),
679
+ };
680
+ }
681
+ }
682
+ /**
683
+ * 清理 `~/.claude/sessions/*.json` 索引中指向该 sessionId 的条目。
684
+ * 静默处理:索引不存在或解析失败不影响删除回执。
685
+ */
686
+ cleanSessionsIndex(sessionId) {
687
+ try {
688
+ const sessionsDir = join(homedir(), '.claude', 'sessions');
689
+ if (!existsSync(sessionsDir))
690
+ return;
691
+ const files = readdirSync(sessionsDir, { withFileTypes: true });
692
+ for (const file of files) {
693
+ if (!file.isFile() || !file.name.endsWith('.json'))
694
+ continue;
695
+ const filePath = join(sessionsDir, file.name);
696
+ try {
697
+ const meta = JSON.parse(readFileSync(filePath, 'utf-8'));
698
+ if (meta?.sessionId !== sessionId)
699
+ continue;
700
+ // 该索引文件即为该会话的元数据;删除整个文件
701
+ unlinkSync(filePath);
702
+ }
703
+ catch {
704
+ /* skip malformed index entries */
705
+ }
706
+ }
707
+ }
708
+ catch {
709
+ /* index cleanup is best-effort */
710
+ }
711
+ }
431
712
  // ── helpers for session queries ──
432
713
  /** �?~/.claude/sessions/*.json 加载会话元数据索�?*/
433
714
  loadSessionsIndex() {
@@ -482,6 +763,27 @@ export class ClaudeAdapter {
482
763
  : undefined;
483
764
  return this.stringifyClaudeContent(msg.content ?? nested?.content ?? '');
484
765
  }
766
+ /**
767
+ * 从 Claude Agent SDK result 帧读取 is_error / subtype。
768
+ * 字段在帧顶层(query() 约定);个别版本可能挂在 object 形态的 result 上。
769
+ * 溢出仍 yield type:'result'(保留 sessionId / 可见正文),不得收成 type:'error'。
770
+ */
771
+ extractSdkResultSignals(message) {
772
+ if (!message || typeof message !== 'object')
773
+ return { isError: false };
774
+ const frame = message;
775
+ const nested = frame.result && typeof frame.result === 'object' && !Array.isArray(frame.result)
776
+ ? frame.result
777
+ : undefined;
778
+ const isError = frame.is_error === true || frame.isError === true
779
+ || nested?.is_error === true || nested?.isError === true;
780
+ const subtype = typeof frame.subtype === 'string'
781
+ ? frame.subtype
782
+ : typeof nested?.subtype === 'string'
783
+ ? nested.subtype
784
+ : undefined;
785
+ return { isError, ...(subtype ? { subtype } : {}) };
786
+ }
485
787
  stringifyClaudeContent(content) {
486
788
  if (typeof content === 'string')
487
789
  return content;
@@ -582,6 +884,7 @@ export class ClaudeAdapter {
582
884
  generateMcpConfig(workDir, mcpToken) {
583
885
  if (!this.mcpBridgeUrl)
584
886
  return null;
887
+ validateMcpBridgeUrl(this.mcpBridgeUrl);
585
888
  const effectiveToken = mcpToken ?? this.mcpApiKey;
586
889
  const configDir = workDir ? join(workDir, '.claude') : join(homedir(), '.claude');
587
890
  // 仅当使用 Agent �?token 且无 per-task token 时才使用缓存
@@ -702,5 +1005,106 @@ export class ClaudeAdapter {
702
1005
  }
703
1006
  return null;
704
1007
  }
1008
+ // 搬自 run-claude-agent.mjs:38-40,491-511
1009
+ async *executeViaAgentSdk(task, signal) {
1010
+ const sdkPath = process.env.CLAUDE_AGENT_SDK_PATH
1011
+ || '/home/node/.local/lib/node_modules/@anthropic-ai/claude-agent-sdk';
1012
+ const require = createRequire(import.meta.url);
1013
+ const sdk = require(sdkPath);
1014
+ const { query } = sdk;
1015
+ const budgeted = applyContextBudget({
1016
+ ...task,
1017
+ systemPrompt: task.systemPrompt ?? task.claude?.systemPrompt,
1018
+ });
1019
+ if (budgeted.diagnostics) {
1020
+ const d = budgeted.diagnostics;
1021
+ yield {
1022
+ type: 'thinking',
1023
+ content: `[ContextBudget] input ${d.originalInputChars}→${d.finalInputChars} chars (${d.compression})`,
1024
+ engineEvent: {
1025
+ type: 'status',
1026
+ text: `[ContextBudget] input ${d.originalInputChars}→${d.finalInputChars} chars (${d.compression})`,
1027
+ },
1028
+ };
1029
+ }
1030
+ const isRoot = typeof process.getuid === 'function' && process.getuid() === 0;
1031
+ const permissionMode = resolvePermissionMode(task);
1032
+ const maxTurns = Math.min(task.claude?.maxTurns ?? 200, 500);
1033
+ const allowedTools = task.claude?.tools ?? ['Skill', 'Bash', 'Read', 'Write', 'Edit', 'Glob', 'Grep', 'mcp__*'];
1034
+ const queryOptions = {
1035
+ cwd: task.workDir || process.cwd(),
1036
+ settingSources: ['user', 'project'],
1037
+ allowedTools,
1038
+ permissionMode,
1039
+ maxTurns,
1040
+ };
1041
+ if (task.mcpToken && this.mcpBridgeUrl) {
1042
+ validateMcpBridgeUrl(this.mcpBridgeUrl);
1043
+ queryOptions.mcpServers = {
1044
+ winmatrix: {
1045
+ type: 'http',
1046
+ url: this.mcpBridgeUrl,
1047
+ headers: { Authorization: `Bearer ${task.mcpToken}` },
1048
+ },
1049
+ };
1050
+ queryOptions.strictMcpConfig = true;
1051
+ }
1052
+ if (task.resumeSession && task.sessionId)
1053
+ queryOptions.resume = task.sessionId;
1054
+ if (!isRoot)
1055
+ queryOptions.allowDangerouslySkipPermissions = true;
1056
+ const effectiveSystem = budgeted.systemPrompt ?? task.claude?.systemPrompt ?? task.systemPrompt;
1057
+ for await (const message of query({
1058
+ prompt: withSecurityPrefix(effectiveSystem
1059
+ ? `${effectiveSystem}\n\n---\n\n${budgeted.input}`
1060
+ : budgeted.input),
1061
+ options: queryOptions,
1062
+ })) {
1063
+ if (signal?.aborted)
1064
+ return;
1065
+ if (message.type === 'result') {
1066
+ const sessionId = typeof message.session_id === 'string' ? message.session_id : undefined;
1067
+ const { isError, subtype } = this.extractSdkResultSignals(message);
1068
+ yield {
1069
+ type: 'result',
1070
+ content: this.stringifyClaudeContent(message.result ?? ''),
1071
+ ...(sessionId ? { sessionId } : {}),
1072
+ ...(isError ? { isError: true } : {}),
1073
+ ...(subtype ? { sdkSubtype: subtype } : {}),
1074
+ };
1075
+ }
1076
+ else if (message.type === 'assistant' && message.message != null) {
1077
+ // SDK 的 message.message 是 Anthropic API Message 对象(content 为 block 数组),
1078
+ // 直接 String() 会得到 "[object Object]" 并污染下游 deliverable/回调正文。
1079
+ const text = this.extractAssistantMessageText(message.message);
1080
+ if (text) {
1081
+ yield { type: 'delta', content: text, engineEvent: { type: 'content_delta', text } };
1082
+ }
1083
+ }
1084
+ }
1085
+ }
1086
+ /**
1087
+ * assistant 帧 message.message(Anthropic Message 对象 / content 数组 / 字符串)
1088
+ * → content 内 text 块原文。thinking / tool_use / tool_result 块不属于交付文本,
1089
+ * 不混入(deliverable-tracker 只累积 Agent 文本输出)。
1090
+ */
1091
+ extractAssistantMessageText(value) {
1092
+ if (typeof value === 'string')
1093
+ return value;
1094
+ if (!value || typeof value !== 'object')
1095
+ return '';
1096
+ const content = value.content;
1097
+ if (typeof content === 'string')
1098
+ return content;
1099
+ if (!Array.isArray(content))
1100
+ return '';
1101
+ return content
1102
+ .filter((block) => Boolean(block)
1103
+ && typeof block === 'object'
1104
+ && block.type === 'text'
1105
+ && typeof block.text === 'string')
1106
+ .map((block) => block.text)
1107
+ .join('\n');
1108
+ }
705
1109
  }
706
1110
  //# sourceMappingURL=ClaudeAdapter.js.map