draftgo-cli 3.0.53 → 3.0.54

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.
package/README.md CHANGED
@@ -101,6 +101,8 @@ draftgo mcp test # 验证 initialize、tools/list、项目/正文
101
101
  draftgo mcp serve # 启动 stdio -> 远端 Streamable HTTP bridge
102
102
  ```
103
103
 
104
+ `mcp serve` 会保留远端 `Mcp-Session-Id`。服务重启或 session 过期后,它会自动重新 initialize,并只重试一次被明确拒绝为 session 无效的当前请求;普通工具失败、超时和 5xx 不会自动重试。`mcp test` 使用独立的新连接,只证明当前配置能够建立新 session,不能直接证明宿主此前持有的 session 仍有效。
105
+
104
106
  `setup` 不传 target 时自动检测当前项目的宿主。可用 `--target codex,cursor` 传入多个目标。
105
107
 
106
108
  当前 MCP 宿主支持:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "draftgo-cli",
3
- "version": "3.0.53",
3
+ "version": "3.0.54",
4
4
  "description": "Install and manage the DraftGo skill across AI coding agents (Claude Code, Codex, Cursor, Windsurf, Antigravity, Copilot, Gemini, Kiro).",
5
5
  "bin": {
6
6
  "draftgo": "bin/draftgo.js"
@@ -2,7 +2,7 @@
2
2
  "schema_version": "1.0",
3
3
  "id": "draftgo",
4
4
  "name": "DraftGo 开发助手",
5
- "version": "3.0.53",
5
+ "version": "3.0.54",
6
6
  "entry": "SKILL.md",
7
7
  "description": "以 Skill/reference 任务路由、MCP 实时发现、长正文 checkout/commit、统一验证和完成日志为边界的 DraftGo 工作流。",
8
8
  "license": "MIT",
@@ -84,7 +84,9 @@ draftgo mcp serve
84
84
  - `setup` 写入项目级宿主配置,只替换 `draftgo` MCP 条目并保留其他配置。
85
85
  - `status` 检查配置是否存在、格式是否有效,以及是否误写了凭据或远端 URL。
86
86
  - `test` 从 `.draftgo/config.json` 读取连接,验证 initialize、tools/list、三类正文 resource_list,以及 db_meta 的 api_search、api_describe 和只读 api_call。
87
- - `serve` 启动 stdio bridge,把宿主请求代理到当前项目配置的远端 Streamable HTTP `/mcp`。
87
+ - `serve` 启动 stdio bridge,把宿主请求代理到当前项目配置的远端 Streamable HTTP `/mcp`。远端重启或 session 过期后,bridge 会清除旧 `Mcp-Session-Id`、重新 initialize,并对被明确拒绝为 session 无效的当前请求重试一次;普通超时、服务端错误和工具错误不会自动重试。
88
+
89
+ `draftgo mcp test` 每次使用独立的新连接,因此它证明当前 server、SAT 和协议可以建立新 session,不代表宿主已持有的旧 session 仍然有效。宿主工具报告 session 失效但 `mcp test` 成功时,bridge 应自动恢复;若恢复仍失败,再检查远端日志、代理头传递和服务可用性。
88
90
 
89
91
  `draftgo connect` 保存并验证 server/SAT、探测 `/mcp` 和关键工具,并可提示宿主 setup;`--server` 表示基础地址,完整 MCP endpoint 使用 `--mcp-url` 显式传入。它不下载业务资源,
90
92
  也不创建 pages、navigation、docs、db_meta 等本地镜像。底座仍在开发或暂不可达时,报告诊断结果即可,
package/src/mcp/client.js CHANGED
@@ -65,6 +65,44 @@ class McpRpcError extends Error {
65
65
  }
66
66
  }
67
67
 
68
+ const SESSION_INVALID_CODES = new Set([
69
+ -32002,
70
+ 'SESSION_EXPIRED',
71
+ 'SESSION_NOT_FOUND',
72
+ 'MCP_SESSION_EXPIRED',
73
+ 'MCP_SESSION_NOT_FOUND',
74
+ ]);
75
+
76
+ function isSessionInvalidError(error) {
77
+ if (!error) return false;
78
+ if (Number(error.status) === 404) return true;
79
+ const code = error.code != null
80
+ ? error.code
81
+ : error.rpc && error.rpc.error && error.rpc.error.code;
82
+ if (SESSION_INVALID_CODES.has(code)) return true;
83
+ const message = String(error.message
84
+ || error.rpc && error.rpc.error && error.rpc.error.message
85
+ || '');
86
+ return /\b(?:mcp\s+)?session(?:\s+id)?\s+(?:was\s+)?(?:not\s+found|expired|invalid|unknown|lost)\b/i.test(message)
87
+ || /\b(?:server|mcp)\s+(?:is\s+)?(?:not initialized|uninitialized)\b/i.test(message)
88
+ || /\binitialize first\b/i.test(message);
89
+ }
90
+
91
+ function requestIdKeys(message) {
92
+ const messages = Array.isArray(message) ? message : [message];
93
+ return new Set(messages
94
+ .filter((item) => isObject(item) && typeof item.method === 'string' && item.id != null)
95
+ .map((item) => `${typeof item.id}:${JSON.stringify(item.id)}`));
96
+ }
97
+
98
+ function sessionInvalidResponse(message, expectedIds) {
99
+ const messages = Array.isArray(message) ? message : [message];
100
+ return messages.find((item) => isObject(item)
101
+ && item.error
102
+ && expectedIds.has(`${typeof item.id}:${JSON.stringify(item.id)}`)
103
+ && isSessionInvalidError(item.error)) || null;
104
+ }
105
+
68
106
  function diagnosticNextCursor(result) {
69
107
  const value = diagnosticData(result);
70
108
  if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
@@ -154,6 +192,9 @@ class DraftGoMcpClient {
154
192
  this.sessionId = null;
155
193
  this.protocolVersion = options.protocolVersion || DEFAULT_PROTOCOL_VERSION;
156
194
  this.nextId = 1;
195
+ this.sessionGeneration = 0;
196
+ this.sessionRecovery = null;
197
+ this.sessionRecoveryCount = 0;
157
198
  this.onMessage = typeof options.onMessage === 'function' ? options.onMessage : null;
158
199
  }
159
200
 
@@ -163,28 +204,65 @@ class DraftGoMcpClient {
163
204
  }
164
205
 
165
206
  const initialize = findInitializeRequest(message);
207
+ const internalRecovery = options._sessionRecoveryInternal === true;
208
+ if (!initialize && !internalRecovery && this.sessionRecovery) {
209
+ await this.sessionRecovery;
210
+ }
211
+ if (initialize) this.sessionId = null;
166
212
  const requestedVersion = initialize
167
213
  && initialize.params
168
214
  && initialize.params.protocolVersion;
169
215
  const delivered = [];
216
+ const expectedIds = requestIdKeys(message);
217
+ const attemptedSessionId = initialize ? null : this.sessionId;
218
+ const attemptedGeneration = this.sessionGeneration;
219
+ const canRecover = !initialize
220
+ && !internalRecovery
221
+ && options._sessionRecoveryAttempted !== true
222
+ && attemptedSessionId != null;
223
+ let suppressedSessionError = null;
170
224
 
171
225
  try {
172
226
  await postJsonRpc(this.config, message, {
173
227
  signal: options.signal,
174
228
  timeoutMs: options.timeoutMs,
175
- sessionId: this.sessionId,
229
+ sessionId: attemptedSessionId,
176
230
  protocolVersion: requestedVersion || this.protocolVersion,
177
- onSession: (sessionId) => { this.sessionId = sessionId; },
231
+ onSession: (sessionId) => {
232
+ if (initialize || (this.sessionGeneration === attemptedGeneration
233
+ && this.sessionId === attemptedSessionId)) {
234
+ this.sessionId = sessionId;
235
+ }
236
+ },
178
237
  onMessage: (remoteMessage) => {
179
238
  const safe = redactValue(remoteMessage, this.secrets);
180
239
  delivered.push(safe);
240
+ const invalid = canRecover && sessionInvalidResponse(safe, expectedIds);
241
+ if (invalid) {
242
+ suppressedSessionError = invalid;
243
+ return;
244
+ }
245
+ if (options._suppressCallbacks === true) return;
181
246
  if (this.onMessage) this.onMessage(safe);
182
247
  if (typeof options.onMessage === 'function') options.onMessage(safe);
183
248
  },
184
249
  });
250
+ if (suppressedSessionError) {
251
+ const remoteError = suppressedSessionError.error;
252
+ const error = new McpRpcError(
253
+ remoteError.message || `MCP error ${remoteError.code}`,
254
+ { code: remoteError.code, data: remoteError.data, id: suppressedSessionError.id },
255
+ );
256
+ error.rpc = suppressedSessionError;
257
+ throw error;
258
+ }
185
259
  } catch (error) {
186
260
  if (error && error.rpc) error.rpc = redactValue(error.rpc, this.secrets);
187
261
  if (error && error.message) error.message = redactText(error.message, this.secrets);
262
+ if (canRecover && isSessionInvalidError(error)) {
263
+ await this.recoverSession(attemptedSessionId, attemptedGeneration, options);
264
+ return this.forward(message, { ...options, _sessionRecoveryAttempted: true });
265
+ }
188
266
  throw error;
189
267
  }
190
268
 
@@ -196,6 +274,54 @@ class DraftGoMcpClient {
196
274
  return delivered;
197
275
  }
198
276
 
277
+ async recoverSession(failedSessionId, failedGeneration, options = {}) {
278
+ if (this.sessionGeneration !== failedGeneration) return;
279
+ if (this.sessionId != null && this.sessionId !== failedSessionId) return;
280
+ if (this.sessionRecovery) return this.sessionRecovery;
281
+
282
+ const recoveryNumber = this.sessionRecoveryCount + 1;
283
+ this.sessionId = null;
284
+ const recovery = (async () => {
285
+ if (typeof options.onSessionRecovery === 'function') {
286
+ options.onSessionRecovery('started', { attempt: recoveryNumber });
287
+ }
288
+ try {
289
+ await this.initialize({
290
+ id: `draftgo-session-recovery-${recoveryNumber}`,
291
+ protocolVersion: this.protocolVersion,
292
+ timeoutMs: options.timeoutMs,
293
+ signal: options.signal,
294
+ _sessionRecoveryInternal: true,
295
+ _suppressCallbacks: true,
296
+ });
297
+ this.sessionRecoveryCount = recoveryNumber;
298
+ if (typeof options.onSessionRecovery === 'function') {
299
+ options.onSessionRecovery('succeeded', { attempt: recoveryNumber });
300
+ }
301
+ } catch (error) {
302
+ this.sessionId = null;
303
+ if (typeof options.onSessionRecovery === 'function') {
304
+ options.onSessionRecovery('failed', { attempt: recoveryNumber });
305
+ }
306
+ const message = redactText(error && error.message ? error.message : error, this.secrets);
307
+ const wrapped = new McpRpcError(`DraftGo MCP session recovery failed: ${message}`, {
308
+ code: error && error.code != null ? error.code : -32000,
309
+ data: error && error.data,
310
+ });
311
+ wrapped.status = Number(error && error.status) || 0;
312
+ wrapped.sessionRecovery = true;
313
+ wrapped.cause = error;
314
+ throw wrapped;
315
+ }
316
+ })();
317
+ this.sessionRecovery = recovery;
318
+ try {
319
+ await recovery;
320
+ } finally {
321
+ if (this.sessionRecovery === recovery) this.sessionRecovery = null;
322
+ }
323
+ }
324
+
199
325
  async request(method, params, options = {}) {
200
326
  const id = options.id == null ? this.nextId++ : options.id;
201
327
  const message = { jsonrpc: '2.0', id, method };
@@ -249,6 +375,7 @@ class DraftGoMcpClient {
249
375
  throw error;
250
376
  }
251
377
  if (typeof options.onStage === 'function') options.onStage('initialized', 'succeeded');
378
+ this.sessionGeneration += 1;
252
379
  return result;
253
380
  }
254
381
 
@@ -425,6 +552,7 @@ class DraftGoMcpClient {
425
552
  initialized,
426
553
  protocolVersion: this.protocolVersion,
427
554
  sessionId: this.sessionId,
555
+ sessionRecoveries: this.sessionRecoveryCount,
428
556
  tools,
429
557
  testedTool: tested[0].name,
430
558
  toolResult: tested[0].result,
@@ -457,6 +585,7 @@ module.exports = {
457
585
  diagnosticData,
458
586
  diagnosticNextCursor,
459
587
  findDBMetaListOperation,
588
+ isSessionInvalidError,
460
589
  redactValue,
461
590
  testConnection,
462
591
  callTool,