@a1hvdy/cc-openclaw 0.5.2 → 0.7.0

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 (98) hide show
  1. package/dist/src/command-router/cc-handler.js +72 -0
  2. package/dist/src/command-router/cc-handler.js.map +1 -1
  3. package/dist/src/constants.d.ts +9 -0
  4. package/dist/src/constants.js +10 -0
  5. package/dist/src/constants.js.map +1 -1
  6. package/dist/src/engines/persistent-session.d.ts +2 -0
  7. package/dist/src/engines/persistent-session.js +41 -11
  8. package/dist/src/engines/persistent-session.js.map +1 -1
  9. package/dist/src/lib/config.d.ts +2 -0
  10. package/dist/src/lib/config.js +19 -0
  11. package/dist/src/lib/config.js.map +1 -1
  12. package/dist/src/lib/sysprompt-strip.js +12 -12
  13. package/dist/src/lib/sysprompt-strip.js.map +1 -1
  14. package/dist/src/lib/trajectory.d.ts +1 -1
  15. package/dist/src/lib/trajectory.js.map +1 -1
  16. package/dist/src/lib/vendor-paths.d.ts +6 -4
  17. package/dist/src/lib/vendor-paths.js +21 -14
  18. package/dist/src/lib/vendor-paths.js.map +1 -1
  19. package/dist/src/openai-compat/openai-compat.d.ts +7 -1
  20. package/dist/src/openai-compat/openai-compat.js +8 -1
  21. package/dist/src/openai-compat/openai-compat.js.map +1 -1
  22. package/dist/src/openai-compat/sse-translator.d.ts +23 -3
  23. package/dist/src/openai-compat/sse-translator.js +45 -6
  24. package/dist/src/openai-compat/sse-translator.js.map +1 -1
  25. package/dist/src/session-bootstrap/cwd-patch.js +59 -28
  26. package/dist/src/session-bootstrap/cwd-patch.js.map +1 -1
  27. package/dist/src/types.d.ts +1 -0
  28. package/package.json +2 -3
  29. package/vendor/base-oneshot-session.d.ts +0 -87
  30. package/vendor/base-oneshot-session.js +0 -227
  31. package/vendor/base-oneshot-session.js.map +0 -1
  32. package/vendor/circuit-breaker.d.ts +0 -21
  33. package/vendor/circuit-breaker.js +0 -47
  34. package/vendor/circuit-breaker.js.map +0 -1
  35. package/vendor/consensus.d.ts +0 -20
  36. package/vendor/consensus.js +0 -52
  37. package/vendor/consensus.js.map +0 -1
  38. package/vendor/constants.d.ts +0 -130
  39. package/vendor/constants.js +0 -139
  40. package/vendor/constants.js.map +0 -1
  41. package/vendor/council.d.ts +0 -67
  42. package/vendor/council.js +0 -913
  43. package/vendor/council.js.map +0 -1
  44. package/vendor/embedded-server.d.ts +0 -25
  45. package/vendor/embedded-server.js +0 -373
  46. package/vendor/embedded-server.js.map +0 -1
  47. package/vendor/inbox-manager.d.ts +0 -38
  48. package/vendor/inbox-manager.js +0 -111
  49. package/vendor/inbox-manager.js.map +0 -1
  50. package/vendor/index.d.ts +0 -63
  51. package/vendor/index.js +0 -705
  52. package/vendor/index.js.map +0 -1
  53. package/vendor/logger.d.ts +0 -16
  54. package/vendor/logger.js +0 -44
  55. package/vendor/logger.js.map +0 -1
  56. package/vendor/models.d.ts +0 -69
  57. package/vendor/models.js +0 -289
  58. package/vendor/models.js.map +0 -1
  59. package/vendor/openai-compat.d.ts +0 -197
  60. package/vendor/openai-compat.js +0 -765
  61. package/vendor/openai-compat.js.map +0 -1
  62. package/vendor/persistent-codex-session.d.ts +0 -16
  63. package/vendor/persistent-codex-session.js +0 -105
  64. package/vendor/persistent-codex-session.js.map +0 -1
  65. package/vendor/persistent-cursor-session.d.ts +0 -21
  66. package/vendor/persistent-cursor-session.js +0 -241
  67. package/vendor/persistent-cursor-session.js.map +0 -1
  68. package/vendor/persistent-custom-session.d.ts +0 -78
  69. package/vendor/persistent-custom-session.js +0 -937
  70. package/vendor/persistent-custom-session.js.map +0 -1
  71. package/vendor/persistent-gemini-session.d.ts +0 -21
  72. package/vendor/persistent-gemini-session.js +0 -216
  73. package/vendor/persistent-gemini-session.js.map +0 -1
  74. package/vendor/persistent-session.d.ts +0 -74
  75. package/vendor/persistent-session.js +0 -684
  76. package/vendor/persistent-session.js.map +0 -1
  77. package/vendor/proxy/anthropic-adapter.d.ts +0 -136
  78. package/vendor/proxy/anthropic-adapter.js +0 -392
  79. package/vendor/proxy/anthropic-adapter.js.map +0 -1
  80. package/vendor/proxy/handler.d.ts +0 -39
  81. package/vendor/proxy/handler.js +0 -323
  82. package/vendor/proxy/handler.js.map +0 -1
  83. package/vendor/proxy/schema-cleaner.d.ts +0 -11
  84. package/vendor/proxy/schema-cleaner.js +0 -34
  85. package/vendor/proxy/schema-cleaner.js.map +0 -1
  86. package/vendor/proxy/thought-cache.d.ts +0 -19
  87. package/vendor/proxy/thought-cache.js +0 -53
  88. package/vendor/proxy/thought-cache.js.map +0 -1
  89. package/vendor/session-manager.d.ts +0 -211
  90. package/vendor/session-manager.js +0 -1345
  91. package/vendor/session-manager.js.map +0 -1
  92. package/vendor/skill-resolver.js +0 -107
  93. package/vendor/types.d.ts +0 -466
  94. package/vendor/types.js +0 -8
  95. package/vendor/types.js.map +0 -1
  96. package/vendor/validation.d.ts +0 -31
  97. package/vendor/validation.js +0 -104
  98. package/vendor/validation.js.map +0 -1
@@ -1,684 +0,0 @@
1
- /**
2
- * Persistent Claude Code Session — wraps `claude` CLI via child_process.spawn
3
- *
4
- * Maintains a long-running Claude Code process with streaming JSON I/O.
5
- * Enables multi-turn agent loops, continuous conversation, and real-time streaming.
6
- */
7
- import { spawn } from 'node:child_process';
8
- import { EventEmitter } from 'node:events';
9
- import * as readline from 'node:readline';
10
- import * as fs from 'node:fs';
11
- import * as path from 'node:path';
12
- import { getModelPricing, } from './types.js';
13
- import { resolveAlias, getContextWindow, isClaudeModel } from './models.js';
14
- import { CONTEXT_HIGH_THRESHOLD, MAX_HISTORY_ITEMS, DEFAULT_HISTORY_LIMIT, SESSION_READY_TIMEOUT_MS, SESSION_READY_FALLBACK_MS, TURN_TIMEOUT_MS, COMPACT_TIMEOUT_MS, STOP_SIGKILL_DELAY_MS, SESSION_EVENT, } from './constants.js';
15
- // ─── PersistentClaudeSession ─────────────────────────────────────────────────
16
- export class PersistentClaudeSession extends EventEmitter {
17
- options;
18
- claudeBin;
19
- proc = null;
20
- _rl = null;
21
- _isReady = false;
22
- _isPaused = false;
23
- _isBusy = false;
24
- currentRequestId = 0;
25
- _streamCallbacks = null;
26
- _contextHighFired = false;
27
- _realModel = null;
28
- sessionId;
29
- stats;
30
- constructor(config, claudeBin) {
31
- super();
32
- this.claudeBin = claudeBin || process.env.CLAUDE_BIN || 'claude';
33
- this.options = {
34
- ...config,
35
- permissionMode: config.permissionMode || 'acceptEdits',
36
- hooks: {},
37
- modelOverrides: config.modelOverrides || {},
38
- };
39
- this.stats = {
40
- turns: 0,
41
- toolCalls: 0,
42
- toolErrors: 0,
43
- tokensIn: 0,
44
- tokensOut: 0,
45
- cachedTokens: 0,
46
- costUsd: 0,
47
- startTime: null,
48
- lastActivity: null,
49
- history: [],
50
- };
51
- }
52
- get pid() {
53
- return this.proc?.pid ?? undefined;
54
- }
55
- get isReady() {
56
- return this._isReady;
57
- }
58
- get isPaused() {
59
- return this._isPaused;
60
- }
61
- get isBusy() {
62
- return this._isBusy;
63
- }
64
- // ─── Start ───────────────────────────────────────────────────────────────
65
- async start() {
66
- const resolvedBin = this.claudeBin;
67
- const args = [
68
- '-p',
69
- '--input-format',
70
- 'stream-json',
71
- '--output-format',
72
- 'stream-json',
73
- '--replay-user-messages',
74
- '--verbose',
75
- '--include-partial-messages',
76
- '--permission-mode',
77
- this.options.permissionMode || 'acceptEdits',
78
- ];
79
- // Model alias resolution
80
- if (this.options.model) {
81
- const resolved = this.resolveModel(this.options.model);
82
- if (resolved !== this.options.model)
83
- this.options.model = resolved;
84
- }
85
- // Resume / fork
86
- const resumeId = this.options.claudeResumeId || this.options.resumeSessionId;
87
- if (resumeId) {
88
- args.push('--resume', resumeId);
89
- if (this.options.forkSession)
90
- args.push('--fork-session');
91
- }
92
- if (this.options.customSessionId)
93
- args.push('--session-id', this.options.customSessionId);
94
- // Model — proxy mode mapping
95
- if (this.options.model) {
96
- if (!isClaudeModel(this.options.model) && this.options.baseUrl) {
97
- this._realModel = this.options.model;
98
- args.push('--model', 'opus');
99
- }
100
- else {
101
- const cliModel = this.options.model.includes('/') ? this.options.model.split('/').pop() : this.options.model;
102
- args.push('--model', cliModel);
103
- }
104
- }
105
- // Tool control
106
- if (this.options.allowedTools?.length)
107
- args.push('--allowed-tools', this.options.allowedTools.join(','));
108
- if (this.options.disallowedTools?.length)
109
- args.push('--disallowed-tools', this.options.disallowedTools.join(','));
110
- if (this.options.tools !== undefined && this.options.tools !== null) {
111
- const t = Array.isArray(this.options.tools) ? this.options.tools.join(',') : this.options.tools;
112
- args.push('--tools', t);
113
- }
114
- // System prompts
115
- if (this.options.systemPrompt)
116
- args.push('--system-prompt', this.options.systemPrompt);
117
- if (this.options.appendSystemPrompt)
118
- args.push('--append-system-prompt', this.options.appendSystemPrompt);
119
- // Limits
120
- if (this.options.maxTurns)
121
- args.push('--max-turns', String(this.options.maxTurns));
122
- if (this.options.maxBudgetUsd)
123
- args.push('--max-budget-usd', String(this.options.maxBudgetUsd));
124
- // Permissions
125
- if (this.options.dangerouslySkipPermissions)
126
- args.push('--dangerously-skip-permissions');
127
- // Agents
128
- if (this.options.agents) {
129
- const json = typeof this.options.agents === 'string' ? this.options.agents : JSON.stringify(this.options.agents);
130
- args.push('--agents', json);
131
- }
132
- if (this.options.agent)
133
- args.push('--agent', this.options.agent);
134
- // Directories
135
- if (this.options.addDir?.length) {
136
- for (const dir of this.options.addDir)
137
- args.push('--add-dir', dir);
138
- }
139
- // Effort
140
- if (this.options.effort && this.options.effort !== 'auto')
141
- args.push('--effort', this.options.effort);
142
- // Auto mode
143
- if (this.options.enableAutoMode || this.options.permissionMode === 'auto')
144
- args.push('--enable-auto-mode');
145
- // Session name
146
- if (this.options.sessionName)
147
- args.push('-n', this.options.sessionName);
148
- // New CLI flags
149
- if (this.options.bare)
150
- args.push('--bare');
151
- if (this.options.worktree) {
152
- args.push('--worktree');
153
- if (typeof this.options.worktree === 'string' && this.options.worktree !== 'true')
154
- args.push(this.options.worktree);
155
- }
156
- if (this.options.fallbackModel)
157
- args.push('--fallback-model', this.options.fallbackModel);
158
- if (this.options.jsonSchema)
159
- args.push('--json-schema', this.options.jsonSchema);
160
- if (this.options.mcpConfig) {
161
- const configs = Array.isArray(this.options.mcpConfig) ? this.options.mcpConfig : [this.options.mcpConfig];
162
- for (const c of configs)
163
- args.push('--mcp-config', c);
164
- }
165
- if (this.options.settings)
166
- args.push('--settings', this.options.settings);
167
- if (this.options.noSessionPersistence)
168
- args.push('--no-session-persistence');
169
- if (this.options.betas) {
170
- const bl = Array.isArray(this.options.betas) ? this.options.betas : this.options.betas.split(',');
171
- for (const b of bl)
172
- args.push('--betas', b.trim());
173
- }
174
- // Ensure CWD exists (normalize to prevent path traversal)
175
- if (this.options.cwd) {
176
- this.options.cwd = path.resolve(this.options.cwd);
177
- if (!fs.existsSync(this.options.cwd)) {
178
- fs.mkdirSync(this.options.cwd, { recursive: true });
179
- }
180
- }
181
- // Build spawn environment
182
- // Preserve the parent process PATH so the resolved binary and any PATH-relative
183
- // tools (git, node, npm, etc.) remain accessible on all platforms and distros.
184
- const spawnEnv = {
185
- ...process.env,
186
- PATH: process.env.PATH || '/usr/local/bin:/usr/bin:/bin',
187
- };
188
- if (this.options.baseUrl)
189
- spawnEnv.ANTHROPIC_BASE_URL = this.options.baseUrl;
190
- if (this.options.enableAgentTeams)
191
- spawnEnv.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = 'true';
192
- if (this._realModel && this.options.baseUrl) {
193
- const base = this.options.baseUrl.replace(/\/$/, '');
194
- spawnEnv.ANTHROPIC_BASE_URL = `${base}/real/${this._realModel}`;
195
- }
196
- // Spawn
197
- this.proc = spawn(resolvedBin, args, {
198
- cwd: this.options.cwd,
199
- env: spawnEnv,
200
- stdio: ['pipe', 'pipe', 'pipe'],
201
- detached: true,
202
- });
203
- // Unref so the parent process can exit independently of the child.
204
- this.proc.unref();
205
- // Parse stdout line-by-line
206
- this._rl = readline.createInterface({ input: this.proc.stdout, crlfDelay: Infinity });
207
- this._rl.on('line', (line) => {
208
- if (!line.trim())
209
- return;
210
- try {
211
- const event = JSON.parse(line);
212
- this._handleEvent(event);
213
- }
214
- catch {
215
- this.emit(SESSION_EVENT.LOG, `[stdout] ${line}`);
216
- }
217
- });
218
- this.proc.stderr?.on('data', (data) => {
219
- const sanitized = data
220
- .toString()
221
- .replace(/sk-[a-zA-Z0-9_-]{10,}/g, 'sk-***')
222
- .replace(/ANTHROPIC_API_KEY=[^\s]+/g, 'ANTHROPIC_API_KEY=***')
223
- .replace(/OPENAI_API_KEY=[^\s]+/g, 'OPENAI_API_KEY=***')
224
- .replace(/GEMINI_API_KEY=[^\s]+/g, 'GEMINI_API_KEY=***')
225
- .replace(/Bearer [a-zA-Z0-9_-]+/g, 'Bearer ***');
226
- this.emit(SESSION_EVENT.LOG, `[stderr] ${sanitized}`);
227
- });
228
- this.proc.on('close', (code) => {
229
- this._isReady = false;
230
- this.emit(SESSION_EVENT.CLOSE, code);
231
- });
232
- this.proc.on('error', (err) => {
233
- this.emit(SESSION_EVENT.ERROR, err);
234
- });
235
- // Wait for ready
236
- return new Promise((resolve, reject) => {
237
- const timeout = setTimeout(() => reject(new Error('Timeout waiting for session ready')), SESSION_READY_TIMEOUT_MS);
238
- this.once(SESSION_EVENT.READY, () => {
239
- clearTimeout(timeout);
240
- resolve(this);
241
- });
242
- this.once(SESSION_EVENT.ERROR, (err) => {
243
- clearTimeout(timeout);
244
- reject(err);
245
- });
246
- // Detect premature CLI exit to avoid hanging or marking a dead process as "ready".
247
- const onCloseBeforeReady = (code) => {
248
- if (!this._isReady) {
249
- clearTimeout(timeout);
250
- reject(new Error(`Claude process exited prematurely with code ${code}. Session failed to start.`));
251
- }
252
- };
253
- this.once(SESSION_EVENT.CLOSE, onCloseBeforeReady);
254
- // Emit ready on the first `system` init event from the CLI.
255
- // Fall back to a 2 s timer in case the CLI version doesn't emit one.
256
- const onInit = () => {
257
- if (!this._isReady) {
258
- this._isReady = true;
259
- // Cleanup the early-close listener since initialization succeeded
260
- this.removeListener(SESSION_EVENT.CLOSE, onCloseBeforeReady);
261
- this.emit(SESSION_EVENT.READY);
262
- }
263
- };
264
- this.once(SESSION_EVENT.INIT, onInit);
265
- setTimeout(() => {
266
- this.removeListener(SESSION_EVENT.INIT, onInit);
267
- // If process already exited, reject instead of falsely marking ready
268
- if (this.proc?.killed || this.proc?.exitCode !== null) {
269
- clearTimeout(timeout);
270
- this.removeListener(SESSION_EVENT.CLOSE, onCloseBeforeReady);
271
- reject(new Error('Claude CLI process crashed immediately upon startup. Fallback timer aborted.'));
272
- return;
273
- }
274
- if (!this._isReady) {
275
- this._isReady = true;
276
- this.removeListener(SESSION_EVENT.CLOSE, onCloseBeforeReady);
277
- this.emit(SESSION_EVENT.READY);
278
- }
279
- }, SESSION_READY_FALLBACK_MS);
280
- });
281
- }
282
- // ─── Event Handling ──────────────────────────────────────────────────────
283
- _handleEvent(event) {
284
- const type = event.type;
285
- this.stats.lastActivity = new Date().toISOString();
286
- // Track history (keep last 100)
287
- this.stats.history.push({ time: this.stats.lastActivity, type, event });
288
- if (this.stats.history.length > MAX_HISTORY_ITEMS)
289
- this.stats.history.shift();
290
- switch (type) {
291
- case 'system':
292
- if (event.subtype === 'init') {
293
- this.sessionId = event.session_id;
294
- this.stats.startTime = new Date().toISOString();
295
- this.emit(SESSION_EVENT.INIT, event);
296
- }
297
- this.emit(SESSION_EVENT.SYSTEM, event);
298
- break;
299
- case 'stream_event': {
300
- const inner = event.event;
301
- if (!inner)
302
- break;
303
- const innerType = inner.type;
304
- if (innerType === 'content_block_start') {
305
- const block = inner.content_block;
306
- if (block?.type === 'tool_use') {
307
- this.stats.toolCalls++;
308
- const toolEvent = { tool: { name: block.name, input: {} } };
309
- try {
310
- this._streamCallbacks?.onToolUse?.(toolEvent);
311
- }
312
- catch (err) {
313
- this.emit(SESSION_EVENT.LOG, `[stream callback error] onToolUse: ${err.message}`);
314
- }
315
- this.emit(SESSION_EVENT.TOOL_USE, toolEvent);
316
- }
317
- }
318
- else if (innerType === 'content_block_delta') {
319
- const delta = inner.delta;
320
- if (delta?.type === 'text_delta' && delta.text) {
321
- try {
322
- this._streamCallbacks?.onText?.(delta.text);
323
- }
324
- catch (err) {
325
- this.emit(SESSION_EVENT.LOG, `[stream callback error] onText: ${err.message}`);
326
- }
327
- this.emit(SESSION_EVENT.TEXT, delta.text);
328
- }
329
- }
330
- else if (innerType === 'message_delta') {
331
- const usage = inner.usage;
332
- if (usage) {
333
- this.stats.tokensIn += usage.input_tokens || 0;
334
- this.stats.tokensOut += usage.output_tokens || 0;
335
- this.stats.cachedTokens += usage.cache_read_input_tokens || 0;
336
- this._updateCost();
337
- }
338
- }
339
- this.emit(SESSION_EVENT.STREAM_EVENT, event);
340
- break;
341
- }
342
- case 'user':
343
- this.stats.turns++;
344
- this.emit(SESSION_EVENT.USER_ECHO, event);
345
- break;
346
- case 'assistant':
347
- this.emit(SESSION_EVENT.ASSISTANT, event);
348
- if (event.message?.content && Array.isArray(event.message.content)) {
349
- for (const block of event.message.content) {
350
- if (block.type === 'tool_use') {
351
- this.stats.toolCalls++;
352
- const toolEvent = {
353
- tool: {
354
- name: block.name,
355
- input: block.input || {},
356
- },
357
- };
358
- try {
359
- this._streamCallbacks?.onToolUse?.(toolEvent);
360
- }
361
- catch (err) {
362
- this.emit(SESSION_EVENT.LOG, `[stream callback error] onToolUse: ${err.message}`);
363
- }
364
- this.emit(SESSION_EVENT.TOOL_USE, toolEvent);
365
- }
366
- }
367
- }
368
- break;
369
- case 'tool_use':
370
- this.stats.toolCalls++;
371
- try {
372
- this._streamCallbacks?.onToolUse?.(event);
373
- }
374
- catch (err) {
375
- this.emit(SESSION_EVENT.LOG, `[stream callback error] onToolUse: ${err.message}`);
376
- }
377
- this.emit(SESSION_EVENT.TOOL_USE, event);
378
- break;
379
- case 'tool_result':
380
- try {
381
- this._streamCallbacks?.onToolResult?.(event);
382
- }
383
- catch (err) {
384
- this.emit(SESSION_EVENT.LOG, `[stream callback error] onToolResult: ${err.message}`);
385
- }
386
- if (event.is_error || event.error) {
387
- this.stats.toolErrors++;
388
- this._fireHook('onToolError', {
389
- tool: event.tool_use_id,
390
- error: event.error,
391
- });
392
- }
393
- this.emit(SESSION_EVENT.TOOL_RESULT, event);
394
- break;
395
- case 'error':
396
- this.emit(SESSION_EVENT.ERROR, new Error(String(event.error) || JSON.stringify(event)));
397
- break;
398
- case 'result': {
399
- const usage = event.usage;
400
- if (usage) {
401
- this.stats.tokensIn += usage.input_tokens || 0;
402
- this.stats.tokensOut += usage.output_tokens || 0;
403
- this.stats.cachedTokens += usage.cache_read_input_tokens || 0;
404
- this._updateCost();
405
- }
406
- this.emit(SESSION_EVENT.RESULT, event);
407
- this.emit(SESSION_EVENT.TURN_COMPLETE, event);
408
- this._fireHook('onTurnComplete', {
409
- text: event.result,
410
- usage,
411
- stopReason: event.stop_reason,
412
- });
413
- const totalTokens = this.stats.tokensIn + this.stats.tokensOut;
414
- if (totalTokens > CONTEXT_HIGH_THRESHOLD && !this._contextHighFired) {
415
- this._contextHighFired = true;
416
- this._fireHook('onContextHigh', { tokensUsed: totalTokens, threshold: CONTEXT_HIGH_THRESHOLD });
417
- }
418
- const stopReason = event.stop_reason;
419
- if (stopReason === 'error' || stopReason === 'rate_limit') {
420
- this._fireHook('onStopFailure', { reason: stopReason, error: event.error });
421
- }
422
- break;
423
- }
424
- default:
425
- this.emit(SESSION_EVENT.EVENT, event);
426
- }
427
- }
428
- // ─── Send ────────────────────────────────────────────────────────────────
429
- async send(message, options = {}) {
430
- if (!this._isReady || !this.proc)
431
- throw new Error('Session not ready. Call start() first.');
432
- const requestId = ++this.currentRequestId;
433
- let finalMessage = typeof message === 'string' ? message : message;
434
- if (typeof finalMessage === 'string') {
435
- if (options.effort === 'high' || options.effort === 'max') {
436
- finalMessage = `ultrathink\n\n${finalMessage}`;
437
- }
438
- if (options.plan) {
439
- finalMessage = `/plan ${finalMessage}`;
440
- }
441
- }
442
- const payload = {
443
- type: 'user',
444
- message: {
445
- role: 'user',
446
- content: typeof finalMessage === 'string' ? [{ type: 'text', text: finalMessage }] : finalMessage,
447
- },
448
- };
449
- this.proc.stdin.write(JSON.stringify(payload) + '\n');
450
- if (options.callbacks)
451
- this._streamCallbacks = options.callbacks;
452
- if (options.waitForComplete) {
453
- this._isBusy = true;
454
- try {
455
- return await this._waitForTurnComplete(options.timeout || TURN_TIMEOUT_MS);
456
- }
457
- finally {
458
- this._isBusy = false;
459
- if (options.callbacks)
460
- this._streamCallbacks = null;
461
- }
462
- }
463
- return { requestId, sent: true };
464
- }
465
- // ─── Wait for Turn Complete ──────────────────────────────────────────────
466
- _waitForTurnComplete(timeout) {
467
- return new Promise((resolve, reject) => {
468
- let settled = false;
469
- let streamedText = '';
470
- let allAssistantText = '';
471
- const toolNames = [];
472
- const onText = (chunk) => {
473
- streamedText += chunk;
474
- };
475
- this.on(SESSION_EVENT.TEXT, onText);
476
- const onAssistant = (event) => {
477
- if (event.message?.content && Array.isArray(event.message.content)) {
478
- for (const block of event.message.content) {
479
- if (block.type === 'text' && block.text)
480
- allAssistantText += block.text + '\n';
481
- }
482
- }
483
- };
484
- this.on(SESSION_EVENT.ASSISTANT, onAssistant);
485
- const onToolUse = (event) => {
486
- const tool = event.tool;
487
- toolNames.push(tool?.name || event.name || 'unknown');
488
- };
489
- this.on(SESSION_EVENT.TOOL_USE, onToolUse);
490
- const cleanup = () => {
491
- clearTimeout(timer);
492
- this.removeListener(SESSION_EVENT.TEXT, onText);
493
- this.removeListener(SESSION_EVENT.ASSISTANT, onAssistant);
494
- this.removeListener(SESSION_EVENT.TOOL_USE, onToolUse);
495
- this.removeListener(SESSION_EVENT.TURN_COMPLETE, onTurnComplete);
496
- this.removeListener(SESSION_EVENT.ERROR, onError);
497
- this.removeListener(SESSION_EVENT.CLOSE, onClose);
498
- };
499
- const timer = setTimeout(() => {
500
- if (settled)
501
- return;
502
- settled = true;
503
- cleanup();
504
- reject(new Error('Timeout waiting for response'));
505
- }, timeout);
506
- const onTurnComplete = (event) => {
507
- if (settled)
508
- return;
509
- settled = true;
510
- cleanup();
511
- let text = event.result || streamedText || allAssistantText.trim() || '';
512
- if (!text && toolNames.length > 0) {
513
- const unique = [...new Set(toolNames)];
514
- text = `[Agent completed ${toolNames.length} tool calls: ${unique.join(', ')}]`;
515
- }
516
- resolve({ text, event });
517
- };
518
- const onError = (err) => {
519
- if (settled)
520
- return;
521
- settled = true;
522
- cleanup();
523
- reject(err);
524
- };
525
- const onClose = (code) => {
526
- if (settled)
527
- return;
528
- settled = true;
529
- cleanup();
530
- const text = streamedText || allAssistantText.trim() || '';
531
- resolve({
532
- text,
533
- event: {
534
- type: 'result',
535
- result: text,
536
- stop_reason: 'process_exit',
537
- exit_code: code,
538
- },
539
- });
540
- };
541
- this.once(SESSION_EVENT.TURN_COMPLETE, onTurnComplete);
542
- this.once(SESSION_EVENT.ERROR, onError);
543
- this.once(SESSION_EVENT.CLOSE, onClose);
544
- });
545
- }
546
- // ─── Utilities ───────────────────────────────────────────────────────────
547
- getStats() {
548
- return {
549
- turns: this.stats.turns,
550
- toolCalls: this.stats.toolCalls,
551
- toolErrors: this.stats.toolErrors,
552
- tokensIn: this.stats.tokensIn,
553
- tokensOut: this.stats.tokensOut,
554
- cachedTokens: this.stats.cachedTokens,
555
- costUsd: Math.round(this.stats.costUsd * 10000) / 10000,
556
- isReady: this._isReady,
557
- startTime: this.stats.startTime,
558
- lastActivity: this.stats.lastActivity,
559
- // Approximate context window utilization based on model's known window size.
560
- // Claude Code doesn't expose exact context usage via the JSON protocol,
561
- // so this is a best-effort heuristic. May overcount because cumulative
562
- // token counts include the full conversation history replayed each turn.
563
- contextPercent: Math.min(100, Math.round(((this.stats.tokensIn + this.stats.tokensOut) /
564
- getContextWindow(this.options.resolvedModel || this.options.model || 'claude-sonnet-4-6')) *
565
- 100)),
566
- sessionId: this.sessionId,
567
- uptime: this.stats.startTime ? Math.round((Date.now() - new Date(this.stats.startTime).getTime()) / 1000) : 0,
568
- };
569
- }
570
- getHistory(limit = DEFAULT_HISTORY_LIMIT) {
571
- return this.stats.history.slice(-limit);
572
- }
573
- async compact(summary) {
574
- const msg = summary ? `/compact ${summary}` : '/compact';
575
- return this.send(msg, { waitForComplete: true, timeout: COMPACT_TIMEOUT_MS });
576
- }
577
- getEffort() {
578
- return this.options.effort || 'auto';
579
- }
580
- setEffort(level) {
581
- this.options.effort = level;
582
- }
583
- getCost() {
584
- const pricing = getModelPricing(this.options.model);
585
- const nonCachedIn = Math.max(0, this.stats.tokensIn - this.stats.cachedTokens);
586
- return {
587
- model: this.options.model || 'default',
588
- tokensIn: this.stats.tokensIn,
589
- tokensOut: this.stats.tokensOut,
590
- cachedTokens: this.stats.cachedTokens,
591
- pricing: { inputPer1M: pricing.input, outputPer1M: pricing.output, cachedPer1M: pricing.cached },
592
- breakdown: {
593
- inputCost: (nonCachedIn / 1_000_000) * pricing.input,
594
- cachedCost: (this.stats.cachedTokens / 1_000_000) * (pricing.cached ?? 0),
595
- outputCost: (this.stats.tokensOut / 1_000_000) * pricing.output,
596
- },
597
- totalUsd: this.stats.costUsd,
598
- };
599
- }
600
- resolveModel(alias) {
601
- if (this.options.modelOverrides?.[alias])
602
- return this.options.modelOverrides[alias];
603
- return resolveAlias(alias);
604
- }
605
- pause() {
606
- this._isPaused = true;
607
- this.emit(SESSION_EVENT.PAUSED, { sessionId: this.sessionId });
608
- }
609
- resume() {
610
- this._isPaused = false;
611
- this.emit(SESSION_EVENT.RESUMED, { sessionId: this.sessionId });
612
- }
613
- stop() {
614
- this._fireHook('onStop', { cost: this.getCost(), stats: this.getStats() });
615
- if (this._rl) {
616
- this._rl.close();
617
- this._rl = null;
618
- }
619
- if (this.proc) {
620
- const pid = this.proc.pid;
621
- this.proc.stdin?.end();
622
- this.proc.stdout?.destroy();
623
- this.proc.stderr?.destroy();
624
- try {
625
- process.kill(-pid, 'SIGTERM');
626
- }
627
- catch (err) {
628
- if (err.code !== 'ESRCH') {
629
- this.emit(SESSION_EVENT.LOG, `[stop] kill(-${pid}, SIGTERM) failed: ${err.message}`);
630
- }
631
- try {
632
- this.proc.kill('SIGTERM');
633
- }
634
- catch (innerErr) {
635
- if (innerErr.code !== 'ESRCH') {
636
- this.emit(SESSION_EVENT.LOG, `[stop] proc.kill(SIGTERM) failed: ${innerErr.message}`);
637
- }
638
- }
639
- }
640
- const p = this.proc;
641
- setTimeout(() => {
642
- try {
643
- process.kill(-pid, 'SIGKILL');
644
- }
645
- catch {
646
- /* ESRCH expected — process already gone */
647
- }
648
- try {
649
- p.kill('SIGKILL');
650
- }
651
- catch {
652
- /* ESRCH expected */
653
- }
654
- }, STOP_SIGKILL_DELAY_MS);
655
- this.proc = null;
656
- }
657
- this._isReady = false;
658
- this._isPaused = false;
659
- this.emit(SESSION_EVENT.CLOSE, 143);
660
- }
661
- // ─── Private ─────────────────────────────────────────────────────────────
662
- _updateCost() {
663
- const pricing = getModelPricing(this.options.model);
664
- const nonCachedIn = Math.max(0, this.stats.tokensIn - this.stats.cachedTokens);
665
- this.stats.costUsd =
666
- (nonCachedIn / 1_000_000) * pricing.input +
667
- (this.stats.cachedTokens / 1_000_000) * (pricing.cached ?? 0) +
668
- (this.stats.tokensOut / 1_000_000) * pricing.output;
669
- }
670
- _fireHook(hookName, data) {
671
- const hooks = this.options.hooks;
672
- const hook = hooks?.[hookName];
673
- if (typeof hook === 'function') {
674
- try {
675
- hook(data);
676
- }
677
- catch (err) {
678
- this.emit(SESSION_EVENT.LOG, `[hook error] ${hookName}: ${err.message}`);
679
- }
680
- }
681
- this.emit(`hook:${hookName}`, data);
682
- }
683
- }
684
- //# sourceMappingURL=persistent-session.js.map