claude-coder 1.9.2 → 1.10.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 (81) hide show
  1. package/README.md +236 -214
  2. package/bin/cli.js +170 -155
  3. package/package.json +55 -55
  4. package/recipes/_shared/roles/developer.md +11 -11
  5. package/recipes/_shared/roles/product.md +12 -12
  6. package/recipes/_shared/roles/tester.md +12 -12
  7. package/recipes/_shared/test/report-format.md +86 -86
  8. package/recipes/backend/base.md +27 -27
  9. package/recipes/backend/components/auth.md +18 -18
  10. package/recipes/backend/components/crud-api.md +18 -18
  11. package/recipes/backend/components/file-service.md +15 -15
  12. package/recipes/backend/manifest.json +20 -20
  13. package/recipes/backend/test/api-test.md +25 -25
  14. package/recipes/console/base.md +37 -37
  15. package/recipes/console/components/modal-form.md +20 -20
  16. package/recipes/console/components/pagination.md +17 -17
  17. package/recipes/console/components/search.md +17 -17
  18. package/recipes/console/components/table-list.md +18 -18
  19. package/recipes/console/components/tabs.md +14 -14
  20. package/recipes/console/components/tree.md +15 -15
  21. package/recipes/console/components/upload.md +15 -15
  22. package/recipes/console/manifest.json +24 -24
  23. package/recipes/console/test/crud-e2e.md +47 -47
  24. package/recipes/h5/base.md +26 -26
  25. package/recipes/h5/components/animation.md +11 -11
  26. package/recipes/h5/components/countdown.md +11 -11
  27. package/recipes/h5/components/share.md +11 -11
  28. package/recipes/h5/components/swiper.md +11 -11
  29. package/recipes/h5/manifest.json +21 -21
  30. package/recipes/h5/test/h5-e2e.md +20 -20
  31. package/src/commands/auth.js +420 -420
  32. package/src/commands/setup-modules/helpers.js +100 -100
  33. package/src/commands/setup-modules/index.js +25 -25
  34. package/src/commands/setup-modules/mcp.js +115 -115
  35. package/src/commands/setup-modules/provider.js +260 -260
  36. package/src/commands/setup-modules/safety.js +47 -47
  37. package/src/commands/setup-modules/simplify.js +52 -52
  38. package/src/commands/setup.js +172 -172
  39. package/src/common/assets.js +259 -245
  40. package/src/common/config.js +147 -125
  41. package/src/common/constants.js +55 -55
  42. package/src/common/indicator.js +260 -260
  43. package/src/common/interaction.js +170 -170
  44. package/src/common/logging.js +77 -77
  45. package/src/common/sdk.js +48 -50
  46. package/src/common/tasks.js +88 -88
  47. package/src/common/utils.js +214 -213
  48. package/src/core/coding.js +35 -33
  49. package/src/core/design.js +268 -0
  50. package/src/core/go.js +264 -264
  51. package/src/core/hooks.js +514 -500
  52. package/src/core/init.js +175 -166
  53. package/src/core/plan.js +194 -188
  54. package/src/core/prompts.js +292 -247
  55. package/src/core/repair.js +36 -36
  56. package/src/core/runner.js +471 -471
  57. package/src/core/scan.js +94 -93
  58. package/src/core/session.js +294 -280
  59. package/src/core/simplify.js +76 -74
  60. package/src/core/state.js +120 -105
  61. package/src/index.js +80 -76
  62. package/templates/{codingSystem.md → coding/system.md} +65 -65
  63. package/templates/{codingUser.md → coding/user.md} +18 -17
  64. package/templates/design/base.md +103 -0
  65. package/templates/design/fixSystem.md +71 -0
  66. package/templates/design/fixUser.md +3 -0
  67. package/templates/design/init.md +304 -0
  68. package/templates/design/system.md +108 -0
  69. package/templates/design/user.md +11 -0
  70. package/templates/{goSystem.md → go/system.md} +130 -130
  71. package/templates/{bash-process.md → other/bash-process.md} +12 -12
  72. package/templates/{coreProtocol.md → other/coreProtocol.md} +30 -29
  73. package/templates/{guidance.json → other/guidance.json} +72 -72
  74. package/templates/{requirements.example.md → other/requirements.example.md} +57 -57
  75. package/templates/{test_rule.md → other/test_rule.md} +192 -194
  76. package/templates/{web-testing.md → other/web-testing.md} +17 -17
  77. package/templates/{planSystem.md → plan/system.md} +78 -78
  78. package/templates/{planUser.md → plan/user.md} +10 -9
  79. package/templates/{scanSystem.md → scan/system.md} +120 -120
  80. package/templates/{scanUser.md → scan/user.md} +10 -10
  81. package/types/index.d.ts +217 -217
@@ -1,280 +1,294 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const { buildEnvVars, log } = require('../common/config');
6
- const { Indicator } = require('../common/indicator');
7
- const { logMessage: baseLogMessage, extractResult, writeSessionSeparator } = require('../common/logging');
8
- const { createHooks } = require('./hooks');
9
- const { assets } = require('../common/assets');
10
-
11
- /**
12
- * @typedef {Object} SessionRunOptions
13
- * @property {string} logFileName - 日志文件名
14
- * @property {import('fs').WriteStream} [logStream] - 外部日志流(与 logFileName 二选一)
15
- * @property {number} [sessionNum=0] - 会话编号
16
- * @property {string} [label=''] - 会话标签
17
- * @property {(session: Session) => Promise<Object>} execute - 执行回调,接收 session 实例
18
- */
19
-
20
- /**
21
- * @typedef {Object} QueryResult
22
- * @property {Array<Object>} messages - 所有 SDK 消息
23
- * @property {boolean} success - 是否成功完成
24
- * @property {string|null} subtype - 结果子类型
25
- * @property {number|null} cost - 美元费用
26
- * @property {Object|null} usage - token 用量 { input_tokens, output_tokens }
27
- * @property {number|null} turns - 对话轮次
28
- */
29
-
30
- /**
31
- * @typedef {Object} RunQueryOpts
32
- * @property {(message: Object, messages: Array<Object>) => void|'break'} [onMessage] - 每条消息的回调,返回 'break' 中断
33
- */
34
-
35
- /**
36
- * SDK 会话管理类。通过 Session.run() 创建和管理一次完整的 AI 会话生命周期。
37
- *
38
- * 使用方式:
39
- * ```js
40
- * const result = await Session.run('coding', config, {
41
- * logFileName: 'coding.log',
42
- * async execute(session) {
43
- * const queryOpts = session.buildQueryOptions();
44
- * const { messages, success } = await session.runQuery(prompt, queryOpts);
45
- * return { success };
46
- * },
47
- * });
48
- * ```
49
- */
50
- class Session {
51
- /** @type {Object|null} SDK 单例 */
52
- static _sdk = null;
53
-
54
- /**
55
- * 确保 SDK 已加载(懒加载单例)
56
- * @param {Object} config - 项目配置
57
- * @returns {Promise<Object>} SDK 实例
58
- */
59
- static async ensureSDK(config) {
60
- if (!Session._sdk) {
61
- Object.assign(process.env, buildEnvVars(config));
62
- const { loadSDK } = require('../common/sdk');
63
- Session._sdk = await loadSDK();
64
- }
65
- return Session._sdk;
66
- }
67
-
68
- /**
69
- * 创建 Session 实例并执行回调,自动管理生命周期(日志、hooks、indicator)
70
- * @param {string} type - 会话类型(coding | plan | scan | go | simplify | repair 等)
71
- * @param {Object} config - 项目配置
72
- * @param {SessionRunOptions} options - 运行选项
73
- * @returns {Promise<Object>} 包含 exitCode、logFile、stalled 以及 execute 返回值
74
- */
75
- static async run(type, config, { logFileName, logStream, sessionNum = 0, label = '', execute }) {
76
- await Session.ensureSDK(config);
77
- const session = new Session(type, config, { logFileName, logStream, sessionNum, label });
78
- try {
79
- const result = await execute(session);
80
- session.finish();
81
- return {
82
- exitCode: session.isStalled() ? 2 : 0,
83
- logFile: session.logFile,
84
- stalled: session.isStalled(),
85
- ...result,
86
- };
87
- } catch (err) {
88
- session.finish();
89
- throw err;
90
- }
91
- }
92
-
93
- /**
94
- * @param {string} type - 会话类型
95
- * @param {Object} config - 项目配置
96
- * @param {Object} options
97
- * @param {string} options.logFileName - 日志文件名
98
- * @param {import('fs').WriteStream} [options.logStream] - 外部日志流
99
- * @param {number} [options.sessionNum=0]
100
- * @param {string} [options.label='']
101
- */
102
- constructor(type, config, { logFileName, logStream, sessionNum = 0, label = '' }) {
103
- this.config = config;
104
- this.type = type;
105
- this.indicator = new Indicator();
106
- /** @type {import('fs').WriteStream|null} */
107
- this.logStream = null;
108
- /** @type {string|null} */
109
- this.logFile = null;
110
- /** @type {Object|null} */
111
- this.hooks = null;
112
- /** @type {Function|null} */
113
- this.cleanup = null;
114
- this._isStalled = () => false;
115
- this.abortController = new AbortController();
116
-
117
- this._initLogging(logFileName, logStream);
118
- writeSessionSeparator(this.logStream, sessionNum, label);
119
- const stallTimeoutMin = this._initHooks(type);
120
- this._startIndicator(sessionNum, stallTimeoutMin);
121
- }
122
-
123
- /**
124
- * 构建 SDK query 选项,自动附加 hooks、abortController、权限模式
125
- * @param {Object} [overrides={}] - 覆盖选项(permissionMode, projectRoot, model 等)
126
- * @returns {Object} SDK query options
127
- */
128
- buildQueryOptions(overrides = {}) {
129
- const mode = overrides.permissionMode || 'bypassPermissions';
130
- const base = {
131
- permissionMode: mode,
132
- cwd: overrides.projectRoot || assets.projectRoot,
133
- env: buildEnvVars(this.config),
134
- settingSources: ['project'],
135
- hooks: this.hooks,
136
- abortController: this.abortController,
137
- };
138
- if (mode === 'bypassPermissions') {
139
- base.allowDangerouslySkipPermissions = true;
140
- }
141
- if (this.config.maxTurns > 0) base.maxTurns = this.config.maxTurns;
142
- if (overrides.model) base.model = overrides.model;
143
- else if (this.config.model) base.model = this.config.model;
144
- return base;
145
- }
146
-
147
- /**
148
- * 执行一次 SDK 查询,遍历消息流并收集结果
149
- * @param {string} prompt - 用户提示
150
- * @param {Object} queryOpts - SDK query 选项(通常来自 buildQueryOptions)
151
- * @param {RunQueryOpts} [opts={}] - 额外选项(onMessage 回调)
152
- * @returns {Promise<QueryResult>}
153
- */
154
- async runQuery(prompt, queryOpts, opts = {}) {
155
- if (this.logStream?.writable) {
156
- const sep = '-'.repeat(40);
157
- if (queryOpts.systemPrompt) {
158
- this.logStream.write(`\n${sep}\n[SYSTEM_PROMPT]\n${sep}\n${queryOpts.systemPrompt}\n`);
159
- }
160
- this.logStream.write(`\n${sep}\n[USER_PROMPT]\n${sep}\n${prompt}\n${sep}\n\n`);
161
- }
162
-
163
- const sdk = Session._sdk;
164
- const messages = [];
165
- const querySession = sdk.query({ prompt, options: queryOpts });
166
-
167
- try {
168
- for await (const message of querySession) {
169
- if (this._isStalled()) {
170
- log('warn', '停顿超时,中断消息循环');
171
- break;
172
- }
173
- messages.push(message);
174
- this._logMessage(message);
175
-
176
- if (opts.onMessage) {
177
- const action = opts.onMessage(message, messages);
178
- if (action === 'break') break;
179
- }
180
- }
181
- } catch (err) {
182
- if (this._isStalled()) {
183
- log('warn', 'SDK 会话因停顿超时中断');
184
- } else {
185
- throw err;
186
- }
187
- }
188
-
189
- const sdkResult = extractResult(messages);
190
- this.logStream.write(`\n[SDK_RESULT] ${JSON.stringify(sdkResult, null, 2)}\n\n`);
191
- const cost = sdkResult?.total_cost_usd || null;
192
- const usage = sdkResult?.usage || null;
193
- const turns = sdkResult?.num_turns || null;
194
-
195
- if (cost != null || turns != null) {
196
- const parts = [];
197
- if (turns != null) parts.push(`turns: ${turns}`);
198
- if (cost != null) parts.push(`cost: $${cost}`);
199
- if (usage) {
200
- const inp = usage.input_tokens || 0;
201
- const out = usage.output_tokens || 0;
202
- parts.push(`tokens: ${inp}+${out}`);
203
- }
204
- const summary = parts.join(', ');
205
- console.log('----- SESSION END -----');
206
- log('info', `session 统计: ${summary}`);
207
- if (this.logStream?.writable) {
208
- this.logStream.write(`[SESSION_INFO] ${summary}\n`);
209
- }
210
- }
211
-
212
- return {
213
- messages,
214
- success: sdkResult?.subtype === 'success',
215
- subtype: sdkResult?.subtype || null,
216
- cost, usage, turns,
217
- };
218
- }
219
-
220
- /** 检查会话是否因停顿超时 */
221
- isStalled() {
222
- return this._isStalled();
223
- }
224
-
225
- /** 结束会话:清理 hooks、关闭日志流、停止 indicator */
226
- finish() {
227
- if (this.cleanup) this.cleanup();
228
- if (this.logStream && !this._externalLogStream) this.logStream.end();
229
- this.indicator.stop();
230
- }
231
-
232
- // ─── Private ────────────────────────────────────────────
233
-
234
- _initLogging(logFileName, externalLogStream) {
235
- if (externalLogStream) {
236
- this.logStream = externalLogStream;
237
- this._externalLogStream = true;
238
- } else {
239
- const logsDir = assets.dir('logs');
240
- this.logFile = path.join(logsDir, logFileName);
241
- this.logStream = fs.createWriteStream(this.logFile, { flags: 'a' });
242
- this._externalLogStream = false;
243
- }
244
- }
245
-
246
- _initHooks(hookType) {
247
- const stallTimeoutMs = this.config.stallTimeout * 1000;
248
- const result = createHooks(hookType, this.indicator, this.logStream, {
249
- stallTimeoutMs,
250
- abortController: this.abortController,
251
- editThreshold: this.config.editThreshold,
252
- });
253
- this.hooks = result.hooks;
254
- this.cleanup = result.cleanup;
255
- this._isStalled = result.isStalled;
256
- return Math.floor(stallTimeoutMs / 60000);
257
- }
258
-
259
- _startIndicator(sessionNum, stallTimeoutMin) {
260
- this.indicator.start(sessionNum, stallTimeoutMin, assets.projectRoot);
261
- }
262
-
263
- _logMessage(message) {
264
- const hasText = message.type === 'assistant'
265
- && message.message?.content?.some(b => b.type === 'text' && b.text);
266
-
267
- if (hasText && this.indicator) {
268
- this.indicator.pauseRendering();
269
- process.stderr.write('\r\x1b[K');
270
- }
271
-
272
- baseLogMessage(message, this.logStream, this.indicator);
273
-
274
- if (hasText && this.indicator) {
275
- this.indicator.resumeRendering();
276
- }
277
- }
278
- }
279
-
280
- module.exports = { Session };
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { buildEnvVars, log, COLOR } = require('../common/config');
6
+ const { Indicator } = require('../common/indicator');
7
+ const { logMessage: baseLogMessage, extractResult, writeSessionSeparator } = require('../common/logging');
8
+ const { createHooks } = require('./hooks');
9
+ const { assets } = require('../common/assets');
10
+
11
+ /**
12
+ * @typedef {Object} SessionRunOptions
13
+ * @property {string} logFileName - 日志文件名
14
+ * @property {import('fs').WriteStream} [logStream] - 外部日志流(与 logFileName 二选一)
15
+ * @property {number} [sessionNum=1] - 会话编号
16
+ * @property {string} [label=''] - 会话标签
17
+ * @property {(session: Session) => Promise<Object>} execute - 执行回调,接收 session 实例
18
+ */
19
+
20
+ /**
21
+ * @typedef {Object} QueryResult
22
+ * @property {Array<Object>} messages - 所有 SDK 消息
23
+ * @property {boolean} success - 是否成功完成
24
+ * @property {string|null} subtype - 结果子类型
25
+ * @property {number|null} cost - 美元费用
26
+ * @property {Object|null} usage - token 用量 { input_tokens, output_tokens }
27
+ * @property {number|null} turns - 对话轮次
28
+ */
29
+
30
+ /**
31
+ * @typedef {Object} RunQueryOpts
32
+ * @property {(message: Object, messages: Array<Object>) => void|'break'} [onMessage] - 每条消息的回调,返回 'break' 中断
33
+ */
34
+
35
+ /**
36
+ * SDK 会话管理类。通过 Session.run() 创建和管理一次完整的 AI 会话生命周期。
37
+ *
38
+ * 使用方式:
39
+ * ```js
40
+ * const result = await Session.run('coding', config, {
41
+ * logFileName: 'coding.log',
42
+ * async execute(session) {
43
+ * const queryOpts = session.buildQueryOptions();
44
+ * const { messages, success } = await session.runQuery(prompt, queryOpts);
45
+ * return { success };
46
+ * },
47
+ * });
48
+ * ```
49
+ */
50
+ class Session {
51
+ /** @type {Object|null} SDK 单例 */
52
+ static _sdk = null;
53
+
54
+ /**
55
+ * 确保 SDK 已加载(懒加载单例)
56
+ * @param {Object} config - 项目配置
57
+ * @returns {Promise<Object>} SDK 实例
58
+ */
59
+ static async ensureSDK(config) {
60
+ if (!Session._sdk) {
61
+ Object.assign(process.env, buildEnvVars(config));
62
+ const { loadSDK } = require('../common/sdk');
63
+ Session._sdk = await loadSDK();
64
+ }
65
+ return Session._sdk;
66
+ }
67
+
68
+ /**
69
+ * 创建 Session 实例并执行回调,自动管理生命周期(日志、hooks、indicator)
70
+ * @param {string} type - 会话类型(coding | plan | scan | go | simplify | repair 等)
71
+ * @param {Object} config - 项目配置
72
+ * @param {SessionRunOptions} options - 运行选项
73
+ * @returns {Promise<Object>} 包含 exitCode、logFile、stalled 以及 execute 返回值
74
+ */
75
+ static async run(type, config, { logFileName, logStream, sessionNum = 1, label = '', execute }) {
76
+ await Session.ensureSDK(config);
77
+ const session = new Session(type, config, { logFileName, logStream, sessionNum, label });
78
+ try {
79
+ const result = await execute(session);
80
+ session.finish();
81
+ return {
82
+ exitCode: session.isStalled() ? 2 : 0,
83
+ logFile: session.logFile,
84
+ stalled: session.isStalled(),
85
+ ...result,
86
+ };
87
+ } catch (err) {
88
+ session.finish();
89
+ throw err;
90
+ }
91
+ }
92
+
93
+ /**
94
+ * @param {string} type - 会话类型
95
+ * @param {Object} config - 项目配置
96
+ * @param {Object} options
97
+ * @param {string} options.logFileName - 日志文件名
98
+ * @param {import('fs').WriteStream} [options.logStream] - 外部日志流
99
+ * @param {number} [options.sessionNum=0]
100
+ * @param {string} [options.label='']
101
+ */
102
+ constructor(type, config, { logFileName, logStream, sessionNum = 1, label = '' }) {
103
+ this.config = config;
104
+ this.type = type;
105
+ this.indicator = new Indicator();
106
+ /** @type {import('fs').WriteStream|null} */
107
+ this.logStream = null;
108
+ /** @type {string|null} */
109
+ this.logFile = null;
110
+ /** @type {Object|null} */
111
+ this.hooks = null;
112
+ /** @type {Function|null} */
113
+ this.cleanup = null;
114
+ this._isStalled = () => false;
115
+ this.abortController = new AbortController();
116
+
117
+ this._initLogging(logFileName, logStream);
118
+ writeSessionSeparator(this.logStream, sessionNum, label);
119
+ const stallTimeoutMin = this._initHooks(type);
120
+ this._startIndicator(sessionNum, stallTimeoutMin);
121
+ }
122
+
123
+ /**
124
+ * 构建 SDK query 选项,自动附加 hooks、abortController、权限模式
125
+ * @param {Object} [overrides={}] - 覆盖选项(permissionMode, projectRoot, model 等)
126
+ * @returns {Object} SDK query options
127
+ */
128
+ buildQueryOptions(overrides = {}) {
129
+ const mode = overrides.permissionMode || 'bypassPermissions';
130
+ const base = {
131
+ permissionMode: mode,
132
+ cwd: overrides.projectRoot || assets.projectRoot,
133
+ env: buildEnvVars(this.config),
134
+ settingSources: ['project'],
135
+ hooks: this.hooks,
136
+ abortController: this.abortController,
137
+ };
138
+ if (mode === 'bypassPermissions') {
139
+ base.allowDangerouslySkipPermissions = true;
140
+ }
141
+ if (this.config.maxTurns > 0) base.maxTurns = this.config.maxTurns;
142
+ if (overrides.model) base.model = overrides.model;
143
+ else if (this.config.model) base.model = this.config.model;
144
+ return base;
145
+ }
146
+
147
+ /**
148
+ * 执行一次 SDK 查询,遍历消息流并收集结果
149
+ * @param {string} prompt - 用户提示
150
+ * @param {Object} queryOpts - SDK query 选项(通常来自 buildQueryOptions)
151
+ * @param {RunQueryOpts} [opts={}] - 额外选项(onMessage 回调, continue, resume)
152
+ * @returns {Promise<QueryResult>}
153
+ */
154
+ async runQuery(prompt, queryOpts, opts = {}) {
155
+ if (this.logStream?.writable) {
156
+ const sep = '-'.repeat(40);
157
+ if (queryOpts.systemPrompt) {
158
+ this.logStream.write(`\n${sep}\n[SYSTEM_PROMPT]\n${sep}\n${queryOpts.systemPrompt}\n`);
159
+ }
160
+ this.logStream.write(`\n${sep}\n[USER_PROMPT]\n${sep}\n${prompt}\n${sep}\n\n`);
161
+ }
162
+
163
+ const sdk = Session._sdk;
164
+ const messages = [];
165
+ const queryPayload = { prompt, options: queryOpts };
166
+ if (opts.continue) queryPayload.options.continue = true;
167
+ if (opts.resume) queryPayload.options.resume = opts.resume;
168
+ const querySession = sdk.query(queryPayload);
169
+
170
+ try {
171
+ for await (const message of querySession) {
172
+ if (this._isStalled()) {
173
+ log('warn', '停顿超时,中断消息循环');
174
+ break;
175
+ }
176
+ messages.push(message);
177
+ this._logMessage(message);
178
+
179
+ if (opts.onMessage) {
180
+ const action = opts.onMessage(message, messages);
181
+ if (action === 'break') break;
182
+ }
183
+ }
184
+ } catch (err) {
185
+ if (this._isStalled()) {
186
+ log('warn', 'SDK 会话因停顿超时中断');
187
+ } else {
188
+ throw err;
189
+ }
190
+ }
191
+
192
+ const sdkResult = extractResult(messages);
193
+ this.logStream.write(`\n[SDK_RESULT] ${JSON.stringify(sdkResult, null, 2)}\n\n`);
194
+ const cost = sdkResult?.total_cost_usd || null;
195
+ const usage = sdkResult?.usage || null;
196
+ const turns = sdkResult?.num_turns || null;
197
+ const sessionId = sdkResult?.session_id || null;
198
+
199
+ if (cost != null || turns != null || sessionId) {
200
+ const fmtTokens = (n) => n >= 10000 ? `${(n / 1000).toFixed(1)}k` : String(n);
201
+ const fmtCost = (c) => c >= 1 ? `$${c.toFixed(2)}` : `$${c.toFixed(4)}`;
202
+
203
+ const cliParts = [];
204
+ if (sessionId) cliParts.push(`${COLOR.dim}sid:${COLOR.reset} ${sessionId.slice(0, 8)}`);
205
+ if (turns != null) cliParts.push(`${COLOR.dim}turns:${COLOR.reset} ${turns}`);
206
+ if (cost != null) cliParts.push(`${COLOR.dim}cost:${COLOR.reset} ${COLOR.yellow}${fmtCost(cost)}${COLOR.reset}`);
207
+ if (usage) {
208
+ const inp = usage.input_tokens || 0;
209
+ const out = usage.output_tokens || 0;
210
+ cliParts.push(`${COLOR.dim}tokens:${COLOR.reset} ${fmtTokens(inp)}+${fmtTokens(out)}`);
211
+ }
212
+ console.error(`${COLOR.dim}─── session end ───${COLOR.reset}`);
213
+ log('info', `session 统计: ${cliParts.join(' ')}`);
214
+
215
+ // 日志文件保留完整精度
216
+ const logParts = [];
217
+ if (sessionId) logParts.push(`sid: ${sessionId}`);
218
+ if (turns != null) logParts.push(`turns: ${turns}`);
219
+ if (cost != null) logParts.push(`cost: $${cost}`);
220
+ if (usage) logParts.push(`tokens: ${usage.input_tokens || 0}+${usage.output_tokens || 0}`);
221
+ if (this.logStream?.writable) {
222
+ this.logStream.write(`[SESSION_INFO] ${logParts.join(', ')}\n`);
223
+ }
224
+ }
225
+
226
+ return {
227
+ messages,
228
+ success: sdkResult?.subtype === 'success',
229
+ subtype: sdkResult?.subtype || null,
230
+ cost, usage, turns, sessionId,
231
+ };
232
+ }
233
+
234
+ /** 检查会话是否因停顿超时 */
235
+ isStalled() {
236
+ return this._isStalled();
237
+ }
238
+
239
+ /** 结束会话:清理 hooks、关闭日志流、停止 indicator */
240
+ finish() {
241
+ if (this.cleanup) this.cleanup();
242
+ if (this.logStream && !this._externalLogStream) this.logStream.end();
243
+ this.indicator.stop();
244
+ }
245
+
246
+ // ─── Private ────────────────────────────────────────────
247
+
248
+ _initLogging(logFileName, externalLogStream) {
249
+ if (externalLogStream) {
250
+ this.logStream = externalLogStream;
251
+ this._externalLogStream = true;
252
+ } else {
253
+ const logsDir = assets.dir('logs');
254
+ this.logFile = path.join(logsDir, logFileName);
255
+ this.logStream = fs.createWriteStream(this.logFile, { flags: 'a' });
256
+ this._externalLogStream = false;
257
+ }
258
+ }
259
+
260
+ _initHooks(hookType) {
261
+ const stallTimeoutMs = this.config.stallTimeout * 1000;
262
+ const result = createHooks(hookType, this.indicator, this.logStream, {
263
+ stallTimeoutMs,
264
+ abortController: this.abortController,
265
+ editThreshold: this.config.editThreshold,
266
+ });
267
+ this.hooks = result.hooks;
268
+ this.cleanup = result.cleanup;
269
+ this._isStalled = result.isStalled;
270
+ return Math.floor(stallTimeoutMs / 60000);
271
+ }
272
+
273
+ _startIndicator(sessionNum, stallTimeoutMin) {
274
+ this.indicator.start(sessionNum, stallTimeoutMin, assets.projectRoot);
275
+ }
276
+
277
+ _logMessage(message) {
278
+ const hasText = message.type === 'assistant'
279
+ && message.message?.content?.some(b => b.type === 'text' && b.text);
280
+
281
+ if (hasText && this.indicator) {
282
+ this.indicator.pauseRendering();
283
+ process.stderr.write('\r\x1b[K');
284
+ }
285
+
286
+ baseLogMessage(message, this.logStream, this.indicator);
287
+
288
+ if (hasText && this.indicator) {
289
+ this.indicator.resumeRendering();
290
+ }
291
+ }
292
+ }
293
+
294
+ module.exports = { Session };