draftgo-cli 4.0.25 → 4.0.26

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 (87) hide show
  1. package/README.md +21 -37
  2. package/package.json +3 -5
  3. package/resources/skill/SKILL.md +9 -5
  4. package/resources/skill/manifest.json +2 -5
  5. package/resources/skill/references/ai.md +41 -0
  6. package/resources/skill/references/app-api.md +2 -50
  7. package/resources/skill/references/architecture.md +1 -1
  8. package/resources/skill/references/chat-sdk.md +29 -37
  9. package/resources/skill/references/checkout.md +4 -4
  10. package/resources/skill/references/data.md +0 -46
  11. package/resources/skill/references/delivery.md +3 -3
  12. package/resources/skill/references/diagnostics.md +10 -11
  13. package/resources/skill/references/frontend.md +23 -20
  14. package/resources/skill/references/mcp.md +4 -14
  15. package/resources/skill/references/methods.md +15 -68
  16. package/resources/skill/references/modules.md +23 -44
  17. package/resources/skill/references/runtime.md +3 -20
  18. package/resources/skill/story/SKILL.md +2 -2
  19. package/src/apiContractCache.js +14 -6
  20. package/src/cli.js +0 -7
  21. package/src/commandRegistry.js +0 -6
  22. package/src/commands/api.js +87 -17
  23. package/src/commands/apiKey.js +2 -6
  24. package/src/commands/autoPush.js +15 -51
  25. package/src/commands/capabilities.js +22 -15
  26. package/src/commands/check.js +19 -53
  27. package/src/commands/checkout.js +1 -4
  28. package/src/commands/clean.js +1 -1
  29. package/src/commands/commit.js +1 -4
  30. package/src/commands/components.js +12 -8
  31. package/src/commands/conflict.js +4 -6
  32. package/src/commands/conflicts.js +1 -2
  33. package/src/commands/connect.js +0 -8
  34. package/src/commands/delete.js +15 -11
  35. package/src/commands/deploy.js +64 -26
  36. package/src/commands/diff.js +1 -4
  37. package/src/commands/group.js +2 -3
  38. package/src/commands/help.js +19 -41
  39. package/src/commands/init.js +13 -6
  40. package/src/commands/local.js +4 -1
  41. package/src/commands/map.js +138 -23
  42. package/src/commands/reconcile.js +1 -15
  43. package/src/commands/role.js +1 -2
  44. package/src/commands/status.js +12 -40
  45. package/src/commands/verify.js +8 -7
  46. package/src/commands/worklog.js +11 -5
  47. package/src/contractCompatibility.js +10 -2
  48. package/src/localRuntime/compose.js +41 -27
  49. package/src/localRuntime/detect.js +6 -6
  50. package/src/localRuntime/index.js +47 -47
  51. package/src/localRuntime/services.js +2 -39
  52. package/src/mcp/client.js +99 -134
  53. package/src/mcp/parallel.js +25 -2
  54. package/src/mcp/protocol.js +38 -9
  55. package/src/mcp/tools.js +10 -19
  56. package/src/projectConfig.js +1 -4
  57. package/src/{workspaceHealth.js → projectHealth.js} +5 -5
  58. package/src/projectMap.js +1 -1
  59. package/src/runtimeFiles.js +2 -1
  60. package/src/worklog.js +3 -2
  61. package/src/worktree/backend.js +127 -15
  62. package/src/worktree/index.js +64 -22
  63. package/src/worktree/locks.js +52 -0
  64. package/src/worktree/manifest.js +18 -4
  65. package/src/worktree/status.js +4 -2
  66. package/resources/custom-service-sdk/ai.go +0 -520
  67. package/resources/custom-service-sdk/ai_test.go +0 -156
  68. package/resources/custom-service-sdk/auth_test.go +0 -56
  69. package/resources/custom-service-sdk/billing.go +0 -596
  70. package/resources/custom-service-sdk/billing_test.go +0 -150
  71. package/resources/custom-service-sdk/go.mod +0 -3
  72. package/resources/custom-service-sdk/manifest.json +0 -77
  73. package/resources/custom-service-sdk/platform.go +0 -352
  74. package/resources/custom-service-sdk/platform_logger_test.go +0 -24
  75. package/resources/custom-service-sdk/registration_test.go +0 -39
  76. package/resources/custom-service-sdk/resources.go +0 -247
  77. package/resources/custom-service-sdk/resources_billing_test.go +0 -115
  78. package/resources/custom-service-sdk/resources_files_test.go +0 -57
  79. package/resources/custom-service-sdk/resources_scope_test.go +0 -92
  80. package/resources/custom-service-sdk/sdk.go +0 -209
  81. package/resources/skill/references/aihub.md +0 -116
  82. package/resources/skill/references/custom-services.md +0 -201
  83. package/src/commands/customService.js +0 -95
  84. package/src/commands/dataRange.js +0 -33
  85. package/src/commands/grant.js +0 -29
  86. package/src/commands/space.js +0 -41
  87. package/src/customServices.js +0 -484
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const DEFAULT_PROTOCOL_VERSION = '2026-07-28';
4
+ const DEFAULT_MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
4
5
 
5
6
  class McpHttpError extends Error {
6
7
  constructor(message, { status = 0, rpc = null, code = null } = {}) {
@@ -62,13 +63,14 @@ function routingHeaders(message) {
62
63
  return headers;
63
64
  }
64
65
 
65
- async function parseEventStream(body, onMessage) {
66
+ async function parseEventStream(body, onMessage, maxBytes = DEFAULT_MAX_RESPONSE_BYTES) {
66
67
  if (!body) return [];
67
68
  const reader = body.getReader();
68
69
  const decoder = new TextDecoder();
69
70
  let pending = '';
70
71
  let dataLines = [];
71
- const messages = [];
72
+ const messages = [];
73
+ let size = 0;
72
74
  const consume = () => {
73
75
  if (!dataLines.length) return;
74
76
  const data = dataLines.join('\n');
@@ -84,7 +86,12 @@ async function parseEventStream(body, onMessage) {
84
86
  }
85
87
  };
86
88
  while (true) {
87
- const { done, value } = await reader.read();
89
+ const { done, value } = await reader.read();
90
+ size += value ? value.byteLength : 0;
91
+ if (size > maxBytes) {
92
+ await reader.cancel().catch(() => {});
93
+ throw new McpHttpError(`DraftGo MCP response exceeds ${maxBytes} bytes.`);
94
+ }
88
95
  pending += decoder.decode(value || new Uint8Array(), { stream: !done });
89
96
  const lines = pending.split(/\r?\n/);
90
97
  pending = lines.pop() || '';
@@ -99,7 +106,7 @@ async function parseEventStream(body, onMessage) {
99
106
  return messages;
100
107
  }
101
108
 
102
- async function readLimitedText(response, maxBytes = 65536) {
109
+ async function readLimitedText(response, maxBytes = 65536) {
103
110
  if (!response.body) return '';
104
111
  const reader = response.body.getReader();
105
112
  const chunks = [];
@@ -112,7 +119,26 @@ async function readLimitedText(response, maxBytes = 65536) {
112
119
  size += chunk.length;
113
120
  }
114
121
  return Buffer.concat(chunks).toString('utf8');
115
- }
122
+ }
123
+
124
+ async function readBoundedText(response, maxBytes) {
125
+ if (!response.body) return '';
126
+ const reader = response.body.getReader();
127
+ const chunks = [];
128
+ let size = 0;
129
+ while (true) {
130
+ const { done, value } = await reader.read();
131
+ if (done) break;
132
+ const chunk = Buffer.from(value);
133
+ size += chunk.length;
134
+ if (size > maxBytes) {
135
+ await reader.cancel().catch(() => {});
136
+ throw new McpHttpError(`DraftGo MCP response exceeds ${maxBytes} bytes.`);
137
+ }
138
+ chunks.push(chunk);
139
+ }
140
+ return Buffer.concat(chunks).toString('utf8');
141
+ }
116
142
 
117
143
  async function postJsonRpc(config, message, options = {}) {
118
144
  const token = String(config.token || config.sat || '');
@@ -163,9 +189,11 @@ async function postJsonRpc(config, message, options = {}) {
163
189
  }
164
190
 
165
191
  if (contentType.includes('text/event-stream')) {
166
- return await parseEventStream(response.body, options.onMessage);
167
- }
168
- const text = await response.text();
192
+ return await parseEventStream(response.body, options.onMessage,
193
+ Number(options.maxResponseBytes || config.mcp_max_response_bytes || DEFAULT_MAX_RESPONSE_BYTES));
194
+ }
195
+ const text = await readBoundedText(response,
196
+ Number(options.maxResponseBytes || config.mcp_max_response_bytes || DEFAULT_MAX_RESPONSE_BYTES));
169
197
  if (!text.trim()) return [];
170
198
  let parsed;
171
199
  try { parsed = JSON.parse(text); } catch {
@@ -183,7 +211,8 @@ async function postJsonRpc(config, message, options = {}) {
183
211
  }
184
212
  }
185
213
 
186
- module.exports = {
214
+ module.exports = {
215
+ DEFAULT_MAX_RESPONSE_BYTES,
187
216
  DEFAULT_PROTOCOL_VERSION,
188
217
  McpHttpError,
189
218
  redactText,
package/src/mcp/tools.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { DraftGoMcpClient } = require('./client');
3
+ const { DraftGoMcpClient, TOOL_ALIASES } = require('./client');
4
4
  const { WorktreeError } = require('../worktree/errors');
5
5
  const { unwrapToolResult, unwrapProtectedResult, toolNameMatches } = require('../worktree/backend');
6
6
 
@@ -15,6 +15,8 @@ const TOOL_NAMES = Object.freeze({
15
15
  apiCall: 'draftgo_api_call',
16
16
  });
17
17
 
18
+ // Keep server-generation naming compatibility at the protocol boundary so
19
+ // commands only deal with one canonical Registry contract.
18
20
  const PROTECTED_RESOURCE_TOOLS = new Set([
19
21
  TOOL_NAMES.resourceList,
20
22
  TOOL_NAMES.resourceSearch,
@@ -24,28 +26,17 @@ const PROTECTED_RESOURCE_TOOLS = new Set([
24
26
 
25
27
  async function openToolSession(config, expected = [], options = {}) {
26
28
  const client = options.client || new DraftGoMcpClient(config);
27
- await client.initialize(options);
28
- const tools = await client.listAllTools(options);
29
+ const initialized = options.clientInitialized ? null : await client.initialize(options);
30
+ const tools = options.tools || await client.listAllTools(options);
29
31
  const names = {};
30
32
  for (const expectedName of expected) {
31
- const found = tools.find((tool) => tool && toolNameMatches(String(tool.name || ''), expectedName));
33
+ const aliases = TOOL_ALIASES[expectedName] || [expectedName];
34
+ const found = aliases.map((alias) => tools.find((tool) => tool
35
+ && toolNameMatches(String(tool.name || ''), alias))).find(Boolean);
32
36
  if (!found) throw new WorktreeError('MCP_TOOL_UNAVAILABLE', `${expectedName} is not available.`);
33
37
  names[expectedName] = found.name;
34
38
  }
35
- return { client, tools, names };
36
- }
37
-
38
- function configuredScope(config, description = null) {
39
- const operation = description && (description.operation || description) || {};
40
- if (String(operation.ownership_mode || '').toLowerCase() === 'platform_only') return {};
41
- const supported = Array.isArray(operation.supported_scopes)
42
- ? operation.supported_scopes.map(value => String(value).toLowerCase()) : [];
43
- const needsSpace = String(operation.ownership_mode || '').toLowerCase() === 'workspace_required'
44
- && supported.length > 0 && !supported.includes('platform');
45
- if (config && config.scope_type === 'space' && Number(config.space_id) > 0 && (needsSpace || supported.includes('space'))) {
46
- return { scope_type: 'space', space_id: Number(config.space_id) };
47
- }
48
- return {};
39
+ return { client, initialized, tools, names };
49
40
  }
50
41
 
51
42
  async function callStructured(session, expectedName, args = {}, options = {}) {
@@ -57,4 +48,4 @@ async function callStructured(session, expectedName, args = {}, options = {}) {
57
48
  : data;
58
49
  }
59
50
 
60
- module.exports = { TOOL_NAMES, openToolSession, callStructured, configuredScope };
51
+ module.exports = { TOOL_NAMES, TOOL_ALIASES, openToolSession, callStructured };
@@ -100,13 +100,10 @@ function writeProjectConfig(projectDir, server, token, options = {}) {
100
100
  if (!apiKey) throw new Error('DraftGo API Key is required.');
101
101
  const existing = readExisting(projectDir);
102
102
  const config = {
103
- ...existing,
104
103
  server: normalizedServer,
105
104
  token: apiKey,
106
105
  auto_push: existing.auto_push === true,
107
106
  };
108
- if (options.scope_type) config.scope_type = String(options.scope_type).trim().toLowerCase();
109
- if (options.space_id) config.space_id = Number(options.space_id);
110
107
  if (options.mcp_url) config.mcp_url = normalizeMcpEndpoint(options.mcp_url);
111
108
  else delete config.mcp_url;
112
109
  const file = configPath(projectDir);
@@ -124,7 +121,7 @@ function loadProjectConfig(projectDir, { requireToken = true } = {}) {
124
121
  const token = String(config.token || config.sat || '').trim();
125
122
  if (!server) throw new Error('DraftGo project config is missing server.');
126
123
  if (requireToken && !token) throw new Error('DraftGo project config is missing API Key.');
127
- const result = { ...config, server, token, path: file };
124
+ const result = { server, token, auto_push: config.auto_push === true, path: file };
128
125
  if (mcpUrl) result.mcp_url = mcpUrl;
129
126
  else delete result.mcp_url;
130
127
  return result;
@@ -5,7 +5,7 @@ const path = require('path');
5
5
  const runtime = require('./runtimeFiles');
6
6
 
7
7
  const PROTECTED_PREFIXES = ['worktree/', 'conflicts/', 'lessons/', 'tmp/', 'config.json', 'api-contract-cache.json', 'api-contract-cache.json.lock', 'worklog.md', 'story.yaml', '.version', 'runtime-manifest.json'];
8
- function workspaceHealth(projectDir) {
8
+ function projectHealth(projectDir) {
9
9
  const root = runtime.draftgoRoot(projectDir); const registered = new Set(runtime.load(projectDir).entries.map((entry) => String(entry.path).replace(/\\/g, '/')));
10
10
  const summary = { managed_files: 0, unknown_files: 0, temporary_files: 0, artifact_files: 0, total_bytes: 0, reclaimable_bytes: 0, warnings: [] };
11
11
  if (!fs.existsSync(root)) return summary;
@@ -13,7 +13,7 @@ function workspaceHealth(projectDir) {
13
13
  for (const item of fs.readdirSync(directory, { withFileTypes: true })) {
14
14
  const absolute = path.join(directory, item.name); const relative = path.relative(root, absolute).replace(/\\/g, '/');
15
15
  if (item.isDirectory()) {
16
- if ((/^(.+\/)?(build|dist|node_modules|tmp-build|build-)/i.test(relative)) && directorySize(absolute) > 50 * 1024 * 1024) summary.warnings.push({ code: 'DG-WORKSPACE-LARGE-BUILD', path: relative });
16
+ if ((/^(.+\/)?(build|dist|node_modules|tmp-build|build-)/i.test(relative)) && directorySize(absolute) > 50 * 1024 * 1024) summary.warnings.push({ code: 'DG-PROJECT-LARGE-BUILD', path: relative });
17
17
  visit(absolute); continue;
18
18
  }
19
19
  if (!item.isFile()) continue;
@@ -23,11 +23,11 @@ function workspaceHealth(projectDir) {
23
23
  if (relative.startsWith('tmp/')) summary.temporary_files += 1;
24
24
  if (relative.startsWith('artifacts/')) summary.artifact_files += 1;
25
25
  if (registered.has(relative) || relative.startsWith('tmp/')) summary.reclaimable_bytes += size;
26
- if (!managed && (/\.exe$/i.test(relative) || (/^[^/]+\.(go|json)$/i.test(relative) && !['config.json', 'runtime-manifest.json'].includes(relative)))) summary.warnings.push({ code: 'DG-WORKSPACE-UNKNOWN-FILE', path: relative, size });
27
- if (relative.startsWith('artifacts/') && !registered.has(relative)) summary.warnings.push({ code: 'DG-WORKSPACE-UNREGISTERED-ARTIFACT', path: relative, size });
26
+ if (!managed && (/\.exe$/i.test(relative) || (/^[^/]+\.(go|json)$/i.test(relative) && !['config.json', 'runtime-manifest.json'].includes(relative)))) summary.warnings.push({ code: 'DG-PROJECT-UNKNOWN-FILE', path: relative, size });
27
+ if (relative.startsWith('artifacts/') && !registered.has(relative)) summary.warnings.push({ code: 'DG-PROJECT-UNREGISTERED-ARTIFACT', path: relative, size });
28
28
  }
29
29
  };
30
30
  visit(root); return summary;
31
31
  }
32
32
  function directorySize(directory) { let total = 0; const visit = (current) => { for (const item of fs.readdirSync(current, { withFileTypes: true })) { const absolute = path.join(current, item.name); if (item.isDirectory()) visit(absolute); else if (item.isFile()) total += fs.statSync(absolute).size; if (total > 50 * 1024 * 1024) return; } }; visit(directory); return total; }
33
- module.exports = { workspaceHealth };
33
+ module.exports = { projectHealth };
package/src/projectMap.js CHANGED
@@ -110,7 +110,7 @@ function buildProjectMap(projectDir) {
110
110
  docs,
111
111
  pageGroups: collectGroups(pages),
112
112
  routeRefs: Object.fromEntries([...routeRefs].map(([route, sources]) => [route, [...sources]])),
113
- remote_only: ['db_meta', 'aihub', 'system_config', 'roles', 'users', 'doc_categories', 'custom_services'],
113
+ remote_only: ['db_meta', 'models', 'prompts', 'plugins', 'knowledge', 'memory', 'agents', 'custom_services', 'system_config', 'roles', 'users', 'doc_categories'],
114
114
  };
115
115
  }
116
116
 
@@ -8,7 +8,7 @@ const RETRYABLE_RENAME_CODES = new Set(['EPERM', 'EBUSY', 'EACCES']);
8
8
  const sleepBuffer = new Int32Array(new SharedArrayBuffer(4));
9
9
  function draftgoRoot(projectDir) { return path.resolve(projectDir, '.draftgo'); }
10
10
  function manifestPath(projectDir) { return path.join(draftgoRoot(projectDir), 'runtime-manifest.json'); }
11
- function inside(root, target) { const relation = path.relative(root, target); return relation && !relation.startsWith('..') && !path.isAbsolute(relation); }
11
+ function inside(root, target) { const relation = path.relative(path.resolve(root), path.resolve(target)); return relation && !relation.startsWith('..') && !path.isAbsolute(relation); }
12
12
  function load(projectDir) {
13
13
  const file = manifestPath(projectDir);
14
14
  if (!fs.existsSync(file)) return { schema_version: SCHEMA_VERSION, entries: [] };
@@ -36,6 +36,7 @@ function save(projectDir, value) {
36
36
  function register(projectDir, file, type, command, options = {}) {
37
37
  const root = draftgoRoot(projectDir); const absolute = path.resolve(file);
38
38
  if (!inside(root, absolute)) throw new Error('Managed runtime files must stay inside .draftgo.');
39
+ if (fs.existsSync(absolute) && !inside(fs.realpathSync(root), fs.realpathSync(absolute))) throw new Error('Managed runtime file escapes .draftgo.');
39
40
  const relative = path.relative(root, absolute).replace(/\\/g, '/'); const value = load(projectDir);
40
41
  const entry = { path: relative, type: String(type), command: String(command), created_at: new Date().toISOString(), cleanable: options.cleanable !== false };
41
42
  value.entries = value.entries.filter((item) => item.path !== relative); value.entries.push(entry); save(projectDir, value); return entry;
package/src/worklog.js CHANGED
@@ -5,7 +5,7 @@ const path = require('path');
5
5
  const crypto = require('crypto');
6
6
  const { dgDir } = require('./paths');
7
7
 
8
- const STATUS = Object.freeze({ pending: '', active: '●', completed: '√' });
8
+ const STATUS = Object.freeze({ pending: '', active: '●', waiting: '?', completed: '√' });
9
9
  const LOCK_WAIT_ARRAY = new Int32Array(new SharedArrayBuffer(4));
10
10
 
11
11
  function formatLocalDate(value = new Date()) {
@@ -47,6 +47,7 @@ function normalizeDate(value, now = new Date()) {
47
47
  function parseStatus(marker) {
48
48
  if (marker === '●') return 'active';
49
49
  if (marker === '√') return 'completed';
50
+ if (marker === '?') return 'waiting';
50
51
  if (marker === '') return 'pending';
51
52
  throw invalid(`unsupported status marker ${JSON.stringify(marker)}.`);
52
53
  }
@@ -72,7 +73,7 @@ function parseWorklog(source) {
72
73
  cursor += 1;
73
74
  const entries = [];
74
75
  while (cursor < lines.length && lines[cursor] !== '') {
75
- const itemMatch = /^([1-9]\d*)\.\s+\[\s*(●|√)?\s*\]\s+(.+)$/.exec(lines[cursor]);
76
+ const itemMatch = /^([1-9]\d*)\.\s+\[\s*(●|√|\?)?\s*\]\s+(.+)$/.exec(lines[cursor]);
76
77
  if (!itemMatch) throw invalid(`invalid item at line ${cursor + 1}.`);
77
78
  const number = Number(itemMatch[1]);
78
79
  if (!Number.isSafeInteger(number) || number !== entries.length + 1) {
@@ -10,6 +10,12 @@ const { normalizeSha256 } = require('./streams');
10
10
  const METADATA_TOOL = 'draftgo_resource_get_metadata';
11
11
  const MAX_ERROR_BYTES = 64 * 1024;
12
12
  const MAX_JSON_BYTES = 1024 * 1024;
13
+ const MAX_CHECKOUT_JSON_BYTES = 64 * 1024 * 1024;
14
+ const FINAL_OPERATIONS = Object.freeze({
15
+ pages: Object.freeze({ checkout: 'listPageCheckout', commit: 'updatePageCommit', contentType: 'text/html; charset=utf-8' }),
16
+ navigations: Object.freeze({ checkout: 'listNavigationCheckout', commit: 'updateNavigationCommit', contentType: 'text/html; charset=utf-8' }),
17
+ docs: Object.freeze({ checkout: 'listContentArticleCheckout', commit: 'updateContentArticleCommit', contentType: 'text/html; charset=utf-8', jsonCommit: true }),
18
+ });
13
19
 
14
20
  function isObject(value) {
15
21
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
@@ -139,6 +145,68 @@ function firstValue(...values) {
139
145
  return values.find((value) => value !== undefined && value !== null && value !== '');
140
146
  }
141
147
 
148
+ function operationPath(config, template, resourceId) {
149
+ const encoded = encodeURIComponent(String(resourceId));
150
+ const relative = String(template || '').replace(/\{[^}]+\}/, encoded);
151
+ if (!relative.startsWith('/api/')) {
152
+ throw new WorktreeError('INVALID_WORKFLOW_PATH', 'DraftGo checkout workflow returned an invalid API path.');
153
+ }
154
+ return new URL(relative, `${config.server.replace(/\/+$/, '')}/`).toString();
155
+ }
156
+
157
+ async function finalMetadata(config, resourceType, resourceId, session, options = {}) {
158
+ // Loaded lazily to avoid the API command's dependency on this backend module.
159
+ const { registryRevision, describeOperation } = require('../commands/api');
160
+ const { TOOL_NAMES, callStructured } = require('../mcp/tools');
161
+ const type = canonicalResourceType(resourceType);
162
+ const spec = FINAL_OPERATIONS[type];
163
+ const revision = await registryRevision(session, spec.checkout);
164
+ const checkout = await describeOperation(options.projectDir || process.cwd(), config, session, spec.checkout, revision);
165
+ const commit = await describeOperation(options.projectDir || process.cwd(), config, session, spec.commit, revision);
166
+ const checkoutOperation = checkout && checkout.operation || {};
167
+ const commitOperation = commit && commit.operation || {};
168
+ const checkoutResult = await callStructured(session, TOOL_NAMES.apiCall, {
169
+ operation_id: spec.checkout,
170
+ registry_revision: revision,
171
+ path: { id: Number(resourceId) },
172
+ }, options);
173
+ const workflow = checkoutResult && checkoutResult.workflow || {};
174
+ if (workflow.required !== true || workflow.workflow !== 'checkout_commit') {
175
+ throw new WorktreeError('INVALID_WORKFLOW_DESCRIPTOR', 'DraftGo did not return a checkout/commit workflow descriptor.');
176
+ }
177
+ const checkoutURL = operationPath(config, checkoutOperation.path || workflow.path, resourceId);
178
+ const response = await (options.fetch || fetch)(checkoutURL, {
179
+ method: String(checkoutOperation.method || 'GET').toUpperCase(),
180
+ headers: authHeaders(config, { Accept: 'application/json' }),
181
+ signal: options.signal,
182
+ redirect: 'error',
183
+ });
184
+ if (!response.ok) throw await errorForResponse(response, config.token || config.sat);
185
+ const raw = await readBounded(response, MAX_CHECKOUT_JSON_BYTES, true);
186
+ const value = parseJsonText(raw, 'DraftGo checkout endpoint');
187
+ const version = firstValue(value.content_version, value.revision);
188
+ const content = typeof value.content === 'string' ? value.content : null;
189
+ const contentURL = firstValue(value.content_url, checkoutURL);
190
+ const contentType = type === 'docs' ? spec.contentType : spec.contentType;
191
+ const size = content == null ? null : Buffer.byteLength(content, 'utf8');
192
+ return normalizeMetadata(config, type, resourceId, {
193
+ resource_type: type,
194
+ resource_id: String(resourceId),
195
+ title: value.title || '', route: value.route || null, code: value.code || null, slug: value.slug || null,
196
+ content_type: contentType,
197
+ file_extension: '.html',
198
+ content_size: size,
199
+ content_hash: value.content_hash,
200
+ ...(type === 'docs' ? { base_revision: version } : { base_version: version }),
201
+ etag: value.content_hash,
202
+ download_url: contentURL,
203
+ commit_url: operationPath(config, commitOperation.path, resourceId),
204
+ commit_method: String(commitOperation.method || 'PUT').toUpperCase(),
205
+ inline_content: content,
206
+ json_commit: spec.jsonCommit === true,
207
+ });
208
+ }
209
+
142
210
  function normalizeUrl(config, value, purpose) {
143
211
  if (!value) return null;
144
212
  let url;
@@ -219,7 +287,23 @@ async function resolveMetadata(config, resourceType, resourceId, options = {}) {
219
287
  if (typeof client.listAllTools === 'function') {
220
288
  const tools = options.tools || await client.listAllTools(options);
221
289
  const found = tools.find((tool) => tool && toolNameMatches(String(tool.name || ''), METADATA_TOOL));
222
- if (!found) throw new WorktreeError('MCP_TOOL_UNAVAILABLE', `${METADATA_TOOL} is not available.`);
290
+ if (!found) {
291
+ const { TOOL_NAMES, openToolSession } = require('../mcp/tools');
292
+ const session = await openToolSession(config, [
293
+ TOOL_NAMES.apiSearch, TOOL_NAMES.apiDescribe, TOOL_NAMES.apiCall,
294
+ ], { ...options, client });
295
+ return finalMetadata(config, resourceType, resourceId, session, options);
296
+ }
297
+ toolName = found.name;
298
+ } else if (Array.isArray(options.tools)) {
299
+ const found = options.tools.find((tool) => tool && toolNameMatches(String(tool.name || ''), METADATA_TOOL));
300
+ if (!found) {
301
+ const { TOOL_NAMES, openToolSession } = require('../mcp/tools');
302
+ const session = await openToolSession(config, [
303
+ TOOL_NAMES.apiSearch, TOOL_NAMES.apiDescribe, TOOL_NAMES.apiCall,
304
+ ], { ...options, client });
305
+ return finalMetadata(config, resourceType, resourceId, session, options);
306
+ }
223
307
  toolName = found.name;
224
308
  }
225
309
  const result = await client.toolsCall(toolName, {
@@ -233,21 +317,26 @@ function authHeaders(config, extra = {}) {
233
317
  return { Authorization: `Bearer ${config.token || config.sat}`, ...extra };
234
318
  }
235
319
 
236
- async function readBounded(response, maximum = MAX_ERROR_BYTES) {
320
+ async function readBounded(response, maximum = MAX_ERROR_BYTES, rejectOverflow = false) {
237
321
  if (!response.body) return '';
238
322
  const reader = response.body.getReader();
239
323
  const chunks = [];
240
324
  let size = 0;
241
- let ended = false;
242
- while (size < maximum) {
243
- const { done, value } = await reader.read();
244
- if (done) { ended = true; break; }
245
- const chunk = Buffer.from(value);
246
- const remaining = maximum - size;
247
- chunks.push(chunk.subarray(0, remaining));
248
- size += Math.min(chunk.length, remaining);
249
- }
250
- if (!ended) await reader.cancel().catch(() => {});
325
+ try {
326
+ while (true) {
327
+ const { done, value } = await reader.read();
328
+ if (done) break;
329
+ const chunk = Buffer.from(value);
330
+ if (size + chunk.length > maximum) {
331
+ if (rejectOverflow) throw new WorktreeError('RESPONSE_TOO_LARGE',
332
+ 'DraftGo response exceeds the explicit ' + maximum + '-byte limit; local content was preserved.', { max_bytes: maximum });
333
+ chunks.push(chunk.subarray(0, maximum - size));
334
+ break;
335
+ }
336
+ chunks.push(chunk);
337
+ size += chunk.length;
338
+ }
339
+ } finally { await reader.cancel().catch(() => {}); }
251
340
  return Buffer.concat(chunks).toString('utf8');
252
341
  }
253
342
 
@@ -264,10 +353,15 @@ async function errorForResponse(response, token) {
264
353
  }
265
354
 
266
355
  async function download(config, metadata, options = {}) {
356
+ const raw = metadata.raw && typeof metadata.raw === 'object' ? metadata.raw : {};
357
+ if (typeof raw.inline_content === 'string') {
358
+ return new Response(raw.inline_content, { headers: { 'content-type': metadata.content_type } });
359
+ }
267
360
  const response = await (options.fetch || fetch)(metadata.download_url, {
268
361
  method: 'GET',
269
362
  headers: authHeaders(config, { Accept: '*/*' }),
270
363
  signal: options.signal,
364
+ redirect: 'error',
271
365
  });
272
366
  if (!response.ok) throw await errorForResponse(response, config.token || config.sat);
273
367
  return response;
@@ -294,15 +388,31 @@ async function commit(config, metadata, localPath, current, options = {}) {
294
388
  if (metadata.base_version != null) headers['DraftGo-Base-Version'] = String(metadata.base_version);
295
389
  if (metadata.base_revision != null) headers['DraftGo-Base-Revision'] = String(metadata.base_revision);
296
390
 
391
+ let body;
392
+ let duplex;
393
+ if (metadata.raw && metadata.raw.json_commit === true) {
394
+ const content = fs.readFileSync(localPath, 'utf8');
395
+ body = JSON.stringify({
396
+ content,
397
+ ...(metadata.base_revision != null ? { base_revision: metadata.base_revision } : {}),
398
+ ...(metadata.base_revision == null && metadata.content_hash ? { base_hash: metadata.content_hash } : {}),
399
+ });
400
+ headers['Content-Type'] = 'application/json';
401
+ headers['Content-Length'] = String(Buffer.byteLength(body, 'utf8'));
402
+ } else {
403
+ body = fs.createReadStream(localPath);
404
+ duplex = 'half';
405
+ }
297
406
  const response = await (options.fetch || fetch)(metadata.commit_url, {
298
407
  method: metadata.commit_method,
299
408
  headers,
300
- body: fs.createReadStream(localPath),
301
- duplex: 'half',
409
+ body,
410
+ ...(duplex ? { duplex } : {}),
302
411
  signal: options.signal,
412
+ redirect: 'error',
303
413
  });
304
414
  if (!response.ok) throw await errorForResponse(response, config.token || config.sat);
305
- const text = await readBounded(response, MAX_JSON_BYTES);
415
+ const text = await readBounded(response, MAX_JSON_BYTES, true);
306
416
  if (!text.trim()) return {};
307
417
  return parseJsonText(text, 'DraftGo commit endpoint');
308
418
  }
@@ -314,6 +424,8 @@ function nodeReadable(response) {
314
424
 
315
425
  module.exports = {
316
426
  METADATA_TOOL,
427
+ readBounded,
428
+ MAX_CHECKOUT_JSON_BYTES,
317
429
  toolNameMatches,
318
430
  unwrapToolResult,
319
431
  unwrapProtectedResult,