@a1hvdy/cc-openclaw 0.6.0 → 0.7.1

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