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.
- package/README.md +21 -37
- package/package.json +3 -5
- package/resources/skill/SKILL.md +9 -5
- package/resources/skill/manifest.json +2 -5
- package/resources/skill/references/ai.md +41 -0
- package/resources/skill/references/app-api.md +2 -50
- package/resources/skill/references/architecture.md +1 -1
- package/resources/skill/references/chat-sdk.md +29 -37
- package/resources/skill/references/checkout.md +4 -4
- package/resources/skill/references/data.md +0 -46
- package/resources/skill/references/delivery.md +3 -3
- package/resources/skill/references/diagnostics.md +10 -11
- package/resources/skill/references/frontend.md +23 -20
- package/resources/skill/references/mcp.md +4 -14
- package/resources/skill/references/methods.md +15 -68
- package/resources/skill/references/modules.md +23 -44
- package/resources/skill/references/runtime.md +3 -20
- package/resources/skill/story/SKILL.md +2 -2
- package/src/apiContractCache.js +14 -6
- package/src/cli.js +0 -7
- package/src/commandRegistry.js +0 -6
- package/src/commands/api.js +87 -17
- package/src/commands/apiKey.js +2 -6
- package/src/commands/autoPush.js +15 -51
- package/src/commands/capabilities.js +22 -15
- package/src/commands/check.js +19 -53
- package/src/commands/checkout.js +1 -4
- package/src/commands/clean.js +1 -1
- package/src/commands/commit.js +1 -4
- package/src/commands/components.js +12 -8
- package/src/commands/conflict.js +4 -6
- package/src/commands/conflicts.js +1 -2
- package/src/commands/connect.js +0 -8
- package/src/commands/delete.js +15 -11
- package/src/commands/deploy.js +64 -26
- package/src/commands/diff.js +1 -4
- package/src/commands/group.js +2 -3
- package/src/commands/help.js +19 -41
- package/src/commands/init.js +13 -6
- package/src/commands/local.js +4 -1
- package/src/commands/map.js +138 -23
- package/src/commands/reconcile.js +1 -15
- package/src/commands/role.js +1 -2
- package/src/commands/status.js +12 -40
- package/src/commands/verify.js +8 -7
- package/src/commands/worklog.js +11 -5
- package/src/contractCompatibility.js +10 -2
- package/src/localRuntime/compose.js +41 -27
- package/src/localRuntime/detect.js +6 -6
- package/src/localRuntime/index.js +47 -47
- package/src/localRuntime/services.js +2 -39
- package/src/mcp/client.js +99 -134
- package/src/mcp/parallel.js +25 -2
- package/src/mcp/protocol.js +38 -9
- package/src/mcp/tools.js +10 -19
- package/src/projectConfig.js +1 -4
- package/src/{workspaceHealth.js → projectHealth.js} +5 -5
- package/src/projectMap.js +1 -1
- package/src/runtimeFiles.js +2 -1
- package/src/worklog.js +3 -2
- package/src/worktree/backend.js +127 -15
- package/src/worktree/index.js +64 -22
- package/src/worktree/locks.js +52 -0
- package/src/worktree/manifest.js +18 -4
- package/src/worktree/status.js +4 -2
- package/resources/custom-service-sdk/ai.go +0 -520
- package/resources/custom-service-sdk/ai_test.go +0 -156
- package/resources/custom-service-sdk/auth_test.go +0 -56
- package/resources/custom-service-sdk/billing.go +0 -596
- package/resources/custom-service-sdk/billing_test.go +0 -150
- package/resources/custom-service-sdk/go.mod +0 -3
- package/resources/custom-service-sdk/manifest.json +0 -77
- package/resources/custom-service-sdk/platform.go +0 -352
- package/resources/custom-service-sdk/platform_logger_test.go +0 -24
- package/resources/custom-service-sdk/registration_test.go +0 -39
- package/resources/custom-service-sdk/resources.go +0 -247
- package/resources/custom-service-sdk/resources_billing_test.go +0 -115
- package/resources/custom-service-sdk/resources_files_test.go +0 -57
- package/resources/custom-service-sdk/resources_scope_test.go +0 -92
- package/resources/custom-service-sdk/sdk.go +0 -209
- package/resources/skill/references/aihub.md +0 -116
- package/resources/skill/references/custom-services.md +0 -201
- package/src/commands/customService.js +0 -95
- package/src/commands/dataRange.js +0 -33
- package/src/commands/grant.js +0 -29
- package/src/commands/space.js +0 -41
- package/src/customServices.js +0 -484
package/src/mcp/protocol.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
51
|
+
module.exports = { TOOL_NAMES, TOOL_ALIASES, openToolSession, callStructured };
|
package/src/projectConfig.js
CHANGED
|
@@ -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 = {
|
|
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
|
|
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-
|
|
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-
|
|
27
|
-
if (relative.startsWith('artifacts/') && !registered.has(relative)) summary.warnings.push({ code: 'DG-
|
|
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 = {
|
|
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', '
|
|
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
|
|
package/src/runtimeFiles.js
CHANGED
|
@@ -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*(
|
|
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) {
|
package/src/worktree/backend.js
CHANGED
|
@@ -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)
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
|
301
|
-
duplex:
|
|
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,
|