@thxgg/steward 0.1.7 → 0.1.11
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/.output/nitro.json +1 -1
- package/.output/public/_nuxt/{Bo1Fdv48.js → BPaqwWyl.js} +2 -2
- package/.output/public/_nuxt/{DhQtydpF.js → C8LtDyY4.js} +1 -1
- package/.output/public/_nuxt/{D0zW6lUK.js → CQgu_W_k.js} +1 -1
- package/.output/public/_nuxt/{BRDbaJqY.js → CZKCADv6.js} +2 -2
- package/.output/public/_nuxt/{CEJOILWG.js → CeO4HNxC.js} +1 -1
- package/.output/public/_nuxt/{BNzFoVmP.js → Cs5ptsBk.js} +1 -1
- package/.output/public/_nuxt/{CFsNy2aC.js → CshyynD6.js} +1 -1
- package/.output/public/_nuxt/{DYDTtHLR.js → CzKPXRws.js} +1 -1
- package/.output/public/_nuxt/{BqmZq_gb.js → DOvbLsAq.js} +1 -1
- package/.output/public/_nuxt/{Bri1ZtcQ.js → DbloiS5Y.js} +1 -1
- package/.output/public/_nuxt/{B3hkJjmY.js → DcRwFvvS.js} +1 -1
- package/.output/public/_nuxt/builds/latest.json +1 -1
- package/.output/public/_nuxt/builds/meta/e2995e80-736c-47cd-8041-a131bab2f136.json +1 -0
- package/.output/public/_nuxt/{X6fIXIFO.js → vr7VLA9A.js} +1 -1
- package/.output/server/chunks/build/{_prd_-CnwhMRyf.mjs → _prd_-CkKfJB6U.mjs} +2 -2
- package/.output/server/chunks/build/_prd_-CkKfJB6U.mjs.map +1 -0
- package/.output/server/chunks/build/client.precomputed.mjs +1 -1
- package/.output/server/chunks/build/server.mjs +1 -1
- package/.output/server/chunks/nitro/nitro.mjs +684 -622
- package/.output/server/package.json +1 -1
- package/README.md +31 -4
- package/bin/prd +117 -0
- package/dist/host/src/api/git.js +1 -8
- package/dist/host/src/api/prds.js +2 -8
- package/dist/host/src/api/repo-context.js +60 -0
- package/dist/host/src/api/repos.js +6 -0
- package/dist/host/src/api/state.js +20 -21
- package/dist/host/src/executor.js +215 -29
- package/dist/host/src/help.js +124 -0
- package/dist/host/src/mcp.js +51 -25
- package/dist/server/utils/db.js +86 -1
- package/docs/MCP.md +64 -3
- package/package.json +1 -1
- package/.output/public/_nuxt/builds/meta/6683a0d9-9c02-4098-b750-bbbc0305261e.json +0 -1
- package/.output/server/chunks/build/_prd_-CnwhMRyf.mjs.map +0 -1
|
@@ -1,61 +1,215 @@
|
|
|
1
1
|
import vm from 'node:vm';
|
|
2
2
|
import { git, prds, repos, state } from './api/index.js';
|
|
3
|
+
import { getStewardHelp } from './help.js';
|
|
3
4
|
const MAX_OUTPUT_SIZE = 50_000;
|
|
4
5
|
const EXECUTION_TIMEOUT_MS = 30_000;
|
|
5
6
|
const MAX_TIMERS = 100;
|
|
7
|
+
const MAX_LOG_ENTRIES = 200;
|
|
8
|
+
const MAX_LOG_OUTPUT_SIZE = 20_000;
|
|
9
|
+
const MAX_LOG_ENTRY_SIZE = 2_000;
|
|
6
10
|
export class ExecutionError extends Error {
|
|
7
|
-
|
|
8
|
-
constructor(message,
|
|
11
|
+
options;
|
|
12
|
+
constructor(message, options) {
|
|
9
13
|
super(message);
|
|
10
|
-
this.
|
|
14
|
+
this.options = options;
|
|
11
15
|
this.name = 'ExecutionError';
|
|
12
16
|
}
|
|
13
17
|
}
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
return undefined;
|
|
17
|
-
}
|
|
18
|
-
let json;
|
|
18
|
+
function safeJsonStringify(value) {
|
|
19
|
+
const seen = new WeakSet();
|
|
19
20
|
try {
|
|
20
|
-
|
|
21
|
+
return JSON.stringify(value, (_key, currentValue) => {
|
|
22
|
+
if (typeof currentValue === 'bigint') {
|
|
23
|
+
return `${currentValue}n`;
|
|
24
|
+
}
|
|
25
|
+
if (typeof currentValue === 'function') {
|
|
26
|
+
const functionName = currentValue.name ? ` ${currentValue.name}` : '';
|
|
27
|
+
return `[Function${functionName}]`;
|
|
28
|
+
}
|
|
29
|
+
if (typeof currentValue === 'symbol') {
|
|
30
|
+
return currentValue.toString();
|
|
31
|
+
}
|
|
32
|
+
if (typeof currentValue === 'object' && currentValue !== null) {
|
|
33
|
+
if (seen.has(currentValue)) {
|
|
34
|
+
return '[Circular]';
|
|
35
|
+
}
|
|
36
|
+
seen.add(currentValue);
|
|
37
|
+
}
|
|
38
|
+
return currentValue;
|
|
39
|
+
});
|
|
21
40
|
}
|
|
22
41
|
catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function formatLogValue(value) {
|
|
46
|
+
if (typeof value === 'string') {
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
const json = safeJsonStringify(value);
|
|
50
|
+
if (json !== undefined) {
|
|
51
|
+
return json;
|
|
52
|
+
}
|
|
53
|
+
return String(value);
|
|
54
|
+
}
|
|
55
|
+
function truncateResult(result) {
|
|
56
|
+
if (result === undefined) {
|
|
23
57
|
return {
|
|
24
|
-
|
|
25
|
-
|
|
58
|
+
result: null,
|
|
59
|
+
truncatedResult: false,
|
|
60
|
+
resultWasUndefined: true
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const json = safeJsonStringify(result);
|
|
64
|
+
if (json === undefined) {
|
|
65
|
+
return {
|
|
66
|
+
result: {
|
|
67
|
+
_unserializable: true,
|
|
68
|
+
preview: String(result)
|
|
69
|
+
},
|
|
70
|
+
truncatedResult: false,
|
|
71
|
+
resultWasUndefined: false
|
|
26
72
|
};
|
|
27
73
|
}
|
|
28
74
|
if (json.length <= MAX_OUTPUT_SIZE) {
|
|
29
|
-
return
|
|
75
|
+
return {
|
|
76
|
+
result,
|
|
77
|
+
truncatedResult: false,
|
|
78
|
+
resultWasUndefined: false
|
|
79
|
+
};
|
|
30
80
|
}
|
|
31
81
|
return {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
82
|
+
result: {
|
|
83
|
+
_truncated: true,
|
|
84
|
+
size: json.length,
|
|
85
|
+
preview: json.slice(0, MAX_OUTPUT_SIZE),
|
|
86
|
+
message: `Output truncated (${json.length} chars, showing first ${MAX_OUTPUT_SIZE})`
|
|
87
|
+
},
|
|
88
|
+
truncatedResult: true,
|
|
89
|
+
resultWasUndefined: false
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function normalizeFailure(error) {
|
|
93
|
+
if (error instanceof ExecutionError) {
|
|
94
|
+
return {
|
|
95
|
+
code: error.options?.code || 'EXECUTION_ERROR',
|
|
96
|
+
message: error.message,
|
|
97
|
+
...(error.options?.stackTrace && { stack: error.options.stackTrace }),
|
|
98
|
+
...(error.options?.details !== undefined && { details: error.options.details })
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (error instanceof Error) {
|
|
102
|
+
const { code, details } = error;
|
|
103
|
+
return {
|
|
104
|
+
code: typeof code === 'string' ? code : 'EXECUTION_ERROR',
|
|
105
|
+
message: error.message,
|
|
106
|
+
...(error.stack && { stack: error.stack }),
|
|
107
|
+
...(details !== undefined && { details })
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
code: 'EXECUTION_ERROR',
|
|
112
|
+
message: String(error)
|
|
36
113
|
};
|
|
37
114
|
}
|
|
38
115
|
export async function execute(code) {
|
|
116
|
+
const startedAt = Date.now();
|
|
117
|
+
const logs = [];
|
|
118
|
+
let totalLogChars = 0;
|
|
119
|
+
let logsTruncated = false;
|
|
120
|
+
const appendLog = (level, args) => {
|
|
121
|
+
if (logs.length >= MAX_LOG_ENTRIES) {
|
|
122
|
+
logsTruncated = true;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
let message = args.map(formatLogValue).join(' ');
|
|
126
|
+
if (message.length > MAX_LOG_ENTRY_SIZE) {
|
|
127
|
+
message = `${message.slice(0, MAX_LOG_ENTRY_SIZE)}...`;
|
|
128
|
+
logsTruncated = true;
|
|
129
|
+
}
|
|
130
|
+
if (totalLogChars + message.length > MAX_LOG_OUTPUT_SIZE) {
|
|
131
|
+
logsTruncated = true;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
totalLogChars += message.length;
|
|
135
|
+
logs.push({
|
|
136
|
+
level,
|
|
137
|
+
message,
|
|
138
|
+
timestamp: new Date().toISOString()
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
const buildEnvelope = (params) => ({
|
|
142
|
+
ok: params.ok,
|
|
143
|
+
result: params.result,
|
|
144
|
+
logs,
|
|
145
|
+
error: params.error,
|
|
146
|
+
meta: {
|
|
147
|
+
timeoutMs: EXECUTION_TIMEOUT_MS,
|
|
148
|
+
durationMs: Date.now() - startedAt,
|
|
149
|
+
truncatedResult: params.truncatedResult,
|
|
150
|
+
truncatedLogs: logsTruncated,
|
|
151
|
+
resultWasUndefined: params.resultWasUndefined
|
|
152
|
+
}
|
|
153
|
+
});
|
|
39
154
|
if (!code || !code.trim()) {
|
|
40
|
-
|
|
155
|
+
const error = normalizeFailure(new ExecutionError('Code cannot be empty', { code: 'EMPTY_CODE' }));
|
|
156
|
+
return buildEnvelope({
|
|
157
|
+
ok: false,
|
|
158
|
+
result: null,
|
|
159
|
+
error,
|
|
160
|
+
truncatedResult: false,
|
|
161
|
+
resultWasUndefined: false
|
|
162
|
+
});
|
|
41
163
|
}
|
|
42
164
|
const timers = new Set();
|
|
165
|
+
let executionTimeout = null;
|
|
166
|
+
let asyncCallbackError = null;
|
|
167
|
+
const wrapTimerHandler = (handler) => {
|
|
168
|
+
return () => {
|
|
169
|
+
try {
|
|
170
|
+
handler();
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
const normalizedError = error instanceof Error
|
|
174
|
+
? error
|
|
175
|
+
: new Error(String(error));
|
|
176
|
+
asyncCallbackError = normalizedError;
|
|
177
|
+
appendLog('error', ['Timer callback error:', normalizedError.message]);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
const ensureTimerHandler = (handler) => {
|
|
182
|
+
if (typeof handler !== 'function') {
|
|
183
|
+
throw new ExecutionError('Timer handler must be a function', {
|
|
184
|
+
code: 'INVALID_TIMER_HANDLER'
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return wrapTimerHandler(handler);
|
|
188
|
+
};
|
|
43
189
|
const sandbox = {
|
|
44
190
|
repos,
|
|
45
191
|
prds,
|
|
46
192
|
git,
|
|
47
193
|
state,
|
|
194
|
+
steward: {
|
|
195
|
+
help: () => getStewardHelp()
|
|
196
|
+
},
|
|
48
197
|
console: {
|
|
49
|
-
log: (...args) =>
|
|
50
|
-
|
|
198
|
+
log: (...args) => appendLog('log', args),
|
|
199
|
+
info: (...args) => appendLog('info', args),
|
|
200
|
+
warn: (...args) => appendLog('warn', args),
|
|
201
|
+
error: (...args) => appendLog('error', args)
|
|
51
202
|
},
|
|
52
203
|
setTimeout: (handler, timeout) => {
|
|
53
204
|
if (timers.size >= MAX_TIMERS) {
|
|
54
|
-
throw new
|
|
205
|
+
throw new ExecutionError(`Timer limit exceeded (max ${MAX_TIMERS})`, {
|
|
206
|
+
code: 'TIMER_LIMIT'
|
|
207
|
+
});
|
|
55
208
|
}
|
|
209
|
+
const wrappedHandler = ensureTimerHandler(handler);
|
|
56
210
|
const timer = setTimeout(() => {
|
|
57
211
|
timers.delete(timer);
|
|
58
|
-
|
|
212
|
+
wrappedHandler();
|
|
59
213
|
}, timeout);
|
|
60
214
|
timers.add(timer);
|
|
61
215
|
return timer;
|
|
@@ -66,9 +220,12 @@ export async function execute(code) {
|
|
|
66
220
|
},
|
|
67
221
|
setInterval: (handler, timeout) => {
|
|
68
222
|
if (timers.size >= MAX_TIMERS) {
|
|
69
|
-
throw new
|
|
223
|
+
throw new ExecutionError(`Timer limit exceeded (max ${MAX_TIMERS})`, {
|
|
224
|
+
code: 'TIMER_LIMIT'
|
|
225
|
+
});
|
|
70
226
|
}
|
|
71
|
-
const
|
|
227
|
+
const wrappedHandler = ensureTimerHandler(handler);
|
|
228
|
+
const timer = setInterval(wrappedHandler, timeout);
|
|
72
229
|
timers.add(timer);
|
|
73
230
|
return timer;
|
|
74
231
|
},
|
|
@@ -88,18 +245,47 @@ export async function execute(code) {
|
|
|
88
245
|
filename: 'codemode.js'
|
|
89
246
|
});
|
|
90
247
|
const context = vm.createContext(sandbox);
|
|
91
|
-
const
|
|
248
|
+
const executionPromise = Promise.resolve(script.runInContext(context, {
|
|
92
249
|
timeout: EXECUTION_TIMEOUT_MS
|
|
250
|
+
}));
|
|
251
|
+
const timeoutPromise = new Promise((_resolve, reject) => {
|
|
252
|
+
executionTimeout = setTimeout(() => {
|
|
253
|
+
reject(new ExecutionError(`Execution timed out after ${EXECUTION_TIMEOUT_MS}ms`, {
|
|
254
|
+
code: 'TIMEOUT'
|
|
255
|
+
}));
|
|
256
|
+
}, EXECUTION_TIMEOUT_MS);
|
|
257
|
+
});
|
|
258
|
+
const rawResult = await Promise.race([executionPromise, timeoutPromise]);
|
|
259
|
+
if (asyncCallbackError instanceof Error) {
|
|
260
|
+
throw new ExecutionError(asyncCallbackError.message, {
|
|
261
|
+
code: 'ASYNC_CALLBACK_ERROR',
|
|
262
|
+
stackTrace: asyncCallbackError.stack
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
const truncated = truncateResult(rawResult);
|
|
266
|
+
return buildEnvelope({
|
|
267
|
+
ok: true,
|
|
268
|
+
result: truncated.result,
|
|
269
|
+
error: null,
|
|
270
|
+
truncatedResult: truncated.truncatedResult,
|
|
271
|
+
resultWasUndefined: truncated.resultWasUndefined
|
|
93
272
|
});
|
|
94
|
-
return truncateOutput(result);
|
|
95
273
|
}
|
|
96
274
|
catch (error) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
275
|
+
const failure = normalizeFailure(error);
|
|
276
|
+
appendLog('error', [`${failure.code}: ${failure.message}`]);
|
|
277
|
+
return buildEnvelope({
|
|
278
|
+
ok: false,
|
|
279
|
+
result: null,
|
|
280
|
+
error: failure,
|
|
281
|
+
truncatedResult: false,
|
|
282
|
+
resultWasUndefined: false
|
|
283
|
+
});
|
|
101
284
|
}
|
|
102
285
|
finally {
|
|
286
|
+
if (executionTimeout) {
|
|
287
|
+
clearTimeout(executionTimeout);
|
|
288
|
+
}
|
|
103
289
|
timers.forEach((timer) => {
|
|
104
290
|
clearTimeout(timer);
|
|
105
291
|
clearInterval(timer);
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
const HELP = {
|
|
2
|
+
version: 1,
|
|
3
|
+
envelope: {
|
|
4
|
+
ok: 'true on success, false on failure',
|
|
5
|
+
result: 'returned value from your code, or null when no return value exists',
|
|
6
|
+
logs: 'captured console output entries from this execution',
|
|
7
|
+
error: 'null on success, otherwise { code, message, stack?, details? }',
|
|
8
|
+
meta: {
|
|
9
|
+
timeoutMs: 'execution timeout limit in milliseconds',
|
|
10
|
+
durationMs: 'elapsed runtime in milliseconds',
|
|
11
|
+
truncatedResult: 'true when result is truncated to output limit',
|
|
12
|
+
truncatedLogs: 'true when logs are truncated to output limit',
|
|
13
|
+
resultWasUndefined: 'true when code finished without an explicit return value'
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
apis: {
|
|
17
|
+
repos: [
|
|
18
|
+
{ signature: 'repos.list()', description: 'List registered repositories' },
|
|
19
|
+
{ signature: 'repos.get(repoId)', description: 'Get one repository by id' },
|
|
20
|
+
{
|
|
21
|
+
signature: 'repos.current()',
|
|
22
|
+
description: 'Resolve current repository when exactly one is registered'
|
|
23
|
+
},
|
|
24
|
+
{ signature: 'repos.add(path, name?)', description: 'Register repository path' },
|
|
25
|
+
{ signature: 'repos.remove(repoId)', description: 'Remove repository by id' },
|
|
26
|
+
{
|
|
27
|
+
signature: 'repos.refreshGitRepos(repoId)',
|
|
28
|
+
description: 'Refresh discovered nested git repositories'
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
prds: [
|
|
32
|
+
{ signature: 'prds.list(repoId)', description: 'List PRDs for repository' },
|
|
33
|
+
{ signature: 'prds.getDocument(repoId, prdSlug)', description: 'Load PRD markdown document' },
|
|
34
|
+
{ signature: 'prds.getTasks(repoId, prdSlug)', description: 'Load tasks state for PRD' },
|
|
35
|
+
{ signature: 'prds.getProgress(repoId, prdSlug)', description: 'Load progress state for PRD' },
|
|
36
|
+
{
|
|
37
|
+
signature: 'prds.getTaskCommits(repoId, prdSlug, taskId)',
|
|
38
|
+
description: 'Resolve task commit references'
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
git: [
|
|
42
|
+
{ signature: 'git.getCommits(repoId, shas, repoPath?)', description: 'Load commit metadata' },
|
|
43
|
+
{ signature: 'git.getDiff(repoId, commit, repoPath?)', description: 'Load full commit diff' },
|
|
44
|
+
{
|
|
45
|
+
signature: 'git.getFileDiff(repoId, commit, file, repoPath?)',
|
|
46
|
+
description: 'Load diff hunks for one file'
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
signature: 'git.getFileContent(repoId, commit, file, repoPath?)',
|
|
50
|
+
description: 'Load file content at commit'
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
state: [
|
|
54
|
+
{ signature: 'state.get(repoId, slug)', description: 'Load stored state by repo id' },
|
|
55
|
+
{ signature: 'state.getByPath(repoPath, slug)', description: 'Load stored state by repo path' },
|
|
56
|
+
{
|
|
57
|
+
signature: 'state.getCurrent(slug)',
|
|
58
|
+
description: 'Load state for current repository when unambiguous'
|
|
59
|
+
},
|
|
60
|
+
{ signature: 'state.summaries(repoId)', description: 'Load PRD state summaries by repo id' },
|
|
61
|
+
{ signature: 'state.summariesByPath(repoPath)', description: 'Load PRD state summaries by path' },
|
|
62
|
+
{
|
|
63
|
+
signature: 'state.summariesCurrent()',
|
|
64
|
+
description: 'Load state summaries for current repository when unambiguous'
|
|
65
|
+
},
|
|
66
|
+
{ signature: 'state.upsert(repoId, slug, payload)', description: 'Save tasks/progress/notes by repo id' },
|
|
67
|
+
{
|
|
68
|
+
signature: 'state.upsertByPath(repoPath, slug, payload)',
|
|
69
|
+
description: 'Save tasks/progress/notes by repo path'
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
signature: 'state.upsertCurrent(slug, payload)',
|
|
73
|
+
description: 'Save state in current repository when unambiguous'
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
examples: [
|
|
78
|
+
{
|
|
79
|
+
title: 'List repos and PRDs',
|
|
80
|
+
code: `const allRepos = await repos.list()\n\nreturn await Promise.all(allRepos.map(async (repo) => ({\n id: repo.id,\n name: repo.name,\n prds: await prds.list(repo.id)\n})))`
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
title: 'Use current repo helper',
|
|
84
|
+
code: `const repo = await repos.current()\nconst slug = 'prd-viewer'\n\nreturn {\n repo,\n tasks: await prds.getTasks(repo.id, slug),\n progress: await prds.getProgress(repo.id, slug)\n}`
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
title: 'Upsert without repoId',
|
|
88
|
+
code: `await state.upsertCurrent('prd-viewer', {\n notes: '# Updated from MCP'\n})\n\nreturn { saved: true }`
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
};
|
|
92
|
+
function formatMethodList(methods) {
|
|
93
|
+
return methods
|
|
94
|
+
.map((method) => `- \`${method.signature}\` - ${method.description}`)
|
|
95
|
+
.join('\n');
|
|
96
|
+
}
|
|
97
|
+
export function getStewardHelp() {
|
|
98
|
+
return JSON.parse(JSON.stringify(HELP));
|
|
99
|
+
}
|
|
100
|
+
export function getExecuteToolDescription() {
|
|
101
|
+
return [
|
|
102
|
+
'Run codemode JavaScript with repos, prds, git, and state APIs.',
|
|
103
|
+
'',
|
|
104
|
+
'Execution always returns a structured JSON envelope:',
|
|
105
|
+
'`{ ok, result, logs, error, meta }`',
|
|
106
|
+
'',
|
|
107
|
+
'In-sandbox discovery helper:',
|
|
108
|
+
'- `steward.help()`',
|
|
109
|
+
'',
|
|
110
|
+
'Repository APIs:',
|
|
111
|
+
formatMethodList(HELP.apis.repos),
|
|
112
|
+
'',
|
|
113
|
+
'PRD APIs:',
|
|
114
|
+
formatMethodList(HELP.apis.prds),
|
|
115
|
+
'',
|
|
116
|
+
'Git APIs:',
|
|
117
|
+
formatMethodList(HELP.apis.git),
|
|
118
|
+
'',
|
|
119
|
+
'State APIs:',
|
|
120
|
+
formatMethodList(HELP.apis.state),
|
|
121
|
+
'',
|
|
122
|
+
'Use `return` in your code to set the envelope `result` field.'
|
|
123
|
+
].join('\n');
|
|
124
|
+
}
|
package/dist/host/src/mcp.js
CHANGED
|
@@ -1,57 +1,83 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
? `${error.message}\n\n${error.stackTrace}`
|
|
9
|
-
: error.message;
|
|
10
|
-
}
|
|
11
|
-
if (error instanceof Error) {
|
|
12
|
-
return error.stack
|
|
13
|
-
? `${error.message}\n\n${error.stack}`
|
|
14
|
-
: error.message;
|
|
15
|
-
}
|
|
16
|
-
return String(error);
|
|
17
|
-
}
|
|
18
|
-
function serializeResult(result) {
|
|
19
|
-
if (result === undefined) {
|
|
20
|
-
return 'undefined';
|
|
21
|
-
}
|
|
4
|
+
import { assertSqliteRuntimeSupport } from '../../server/utils/db.js';
|
|
5
|
+
import { execute } from './executor.js';
|
|
6
|
+
import { getExecuteToolDescription } from './help.js';
|
|
7
|
+
function serializeEnvelope(envelope) {
|
|
22
8
|
try {
|
|
23
|
-
return JSON.stringify(
|
|
9
|
+
return JSON.stringify(envelope, null, 2);
|
|
24
10
|
}
|
|
25
11
|
catch {
|
|
26
|
-
|
|
12
|
+
const fallback = {
|
|
13
|
+
ok: false,
|
|
14
|
+
result: null,
|
|
15
|
+
logs: [],
|
|
16
|
+
error: {
|
|
17
|
+
code: 'SERIALIZATION_ERROR',
|
|
18
|
+
message: 'Failed to serialize execution envelope'
|
|
19
|
+
},
|
|
20
|
+
meta: {
|
|
21
|
+
timeoutMs: 30_000,
|
|
22
|
+
durationMs: 0,
|
|
23
|
+
truncatedResult: false,
|
|
24
|
+
truncatedLogs: false,
|
|
25
|
+
resultWasUndefined: false
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
return JSON.stringify(fallback, null, 2);
|
|
27
29
|
}
|
|
28
30
|
}
|
|
31
|
+
function buildUnexpectedErrorEnvelope(error) {
|
|
32
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
33
|
+
const stack = error instanceof Error ? error.stack : undefined;
|
|
34
|
+
return {
|
|
35
|
+
ok: false,
|
|
36
|
+
result: null,
|
|
37
|
+
logs: [],
|
|
38
|
+
error: {
|
|
39
|
+
code: 'MCP_EXECUTION_FAILURE',
|
|
40
|
+
message,
|
|
41
|
+
...(stack && { stack })
|
|
42
|
+
},
|
|
43
|
+
meta: {
|
|
44
|
+
timeoutMs: 30_000,
|
|
45
|
+
durationMs: 0,
|
|
46
|
+
truncatedResult: false,
|
|
47
|
+
truncatedLogs: false,
|
|
48
|
+
resultWasUndefined: false
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
29
52
|
export async function runMcpServer() {
|
|
53
|
+
await assertSqliteRuntimeSupport();
|
|
30
54
|
const server = new McpServer({
|
|
31
55
|
name: 'steward',
|
|
32
56
|
version: '0.1.0'
|
|
33
57
|
});
|
|
34
|
-
server.tool('execute',
|
|
35
|
-
code: z.string().
|
|
58
|
+
server.tool('execute', getExecuteToolDescription(), {
|
|
59
|
+
code: z.string().optional()
|
|
36
60
|
}, async ({ code }) => {
|
|
37
61
|
try {
|
|
38
|
-
const
|
|
62
|
+
const envelope = await execute(code || '');
|
|
39
63
|
return {
|
|
64
|
+
isError: !envelope.ok,
|
|
40
65
|
content: [
|
|
41
66
|
{
|
|
42
67
|
type: 'text',
|
|
43
|
-
text:
|
|
68
|
+
text: serializeEnvelope(envelope)
|
|
44
69
|
}
|
|
45
70
|
]
|
|
46
71
|
};
|
|
47
72
|
}
|
|
48
73
|
catch (error) {
|
|
74
|
+
const envelope = buildUnexpectedErrorEnvelope(error);
|
|
49
75
|
return {
|
|
50
76
|
isError: true,
|
|
51
77
|
content: [
|
|
52
78
|
{
|
|
53
79
|
type: 'text',
|
|
54
|
-
text:
|
|
80
|
+
text: serializeEnvelope(envelope)
|
|
55
81
|
}
|
|
56
82
|
]
|
|
57
83
|
};
|
package/dist/server/utils/db.js
CHANGED
|
@@ -3,7 +3,19 @@ import { dirname, join } from 'node:path';
|
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
const DEFAULT_DATA_HOME = process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share');
|
|
5
5
|
const DEFAULT_DB_PATH = join(DEFAULT_DATA_HOME, 'prd', 'state.db');
|
|
6
|
+
const SQLITE_ENABLE_FLAG = '--experimental-sqlite';
|
|
7
|
+
const SQLITE_DISABLE_FLAG = '--no-experimental-sqlite';
|
|
6
8
|
let adapterPromise = null;
|
|
9
|
+
export class SqliteRuntimeError extends Error {
|
|
10
|
+
code;
|
|
11
|
+
details;
|
|
12
|
+
constructor(message, details) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = 'SqliteRuntimeError';
|
|
15
|
+
this.code = 'SQLITE_RUNTIME_UNSUPPORTED';
|
|
16
|
+
this.details = details;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
7
19
|
function coerceChanges(result) {
|
|
8
20
|
if (!result || typeof result !== 'object') {
|
|
9
21
|
return 0;
|
|
@@ -22,11 +34,84 @@ function resolveDbPath() {
|
|
|
22
34
|
}
|
|
23
35
|
return DEFAULT_DB_PATH;
|
|
24
36
|
}
|
|
37
|
+
function formatNodeRuntimeHint() {
|
|
38
|
+
const nodeOptions = process.env.NODE_OPTIONS || '';
|
|
39
|
+
const hasDisableFlag = process.execArgv.includes(SQLITE_DISABLE_FLAG)
|
|
40
|
+
|| nodeOptions.includes(SQLITE_DISABLE_FLAG);
|
|
41
|
+
if (hasDisableFlag) {
|
|
42
|
+
return `Remove ${SQLITE_DISABLE_FLAG} from NODE_OPTIONS or Node arguments.`;
|
|
43
|
+
}
|
|
44
|
+
return `Use Node.js with built-in sqlite support, or launch with ${SQLITE_ENABLE_FLAG}.`;
|
|
45
|
+
}
|
|
46
|
+
function mapSqliteImportError(error, dbPath) {
|
|
47
|
+
if (!(error instanceof Error)) {
|
|
48
|
+
return new SqliteRuntimeError('SQLite runtime support is unavailable in this process.', {
|
|
49
|
+
runtime: 'node',
|
|
50
|
+
dbPath,
|
|
51
|
+
nodeVersion: process.version,
|
|
52
|
+
execPath: process.execPath,
|
|
53
|
+
execArgv: [...process.execArgv],
|
|
54
|
+
nodeOptions: process.env.NODE_OPTIONS || '',
|
|
55
|
+
originalMessage: String(error)
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const code = error.code;
|
|
59
|
+
const isMissingBuiltin = typeof code === 'string'
|
|
60
|
+
&& code === 'ERR_UNKNOWN_BUILTIN_MODULE'
|
|
61
|
+
&& error.message.includes('node:sqlite');
|
|
62
|
+
const isMissingModule = typeof code === 'string'
|
|
63
|
+
&& code === 'ERR_MODULE_NOT_FOUND'
|
|
64
|
+
&& error.message.includes('node:sqlite');
|
|
65
|
+
if (!isMissingBuiltin && !isMissingModule) {
|
|
66
|
+
return error;
|
|
67
|
+
}
|
|
68
|
+
return new SqliteRuntimeError(`SQLite runtime support is unavailable for Steward (${process.version}). ${formatNodeRuntimeHint()}`, {
|
|
69
|
+
runtime: 'node',
|
|
70
|
+
dbPath,
|
|
71
|
+
nodeVersion: process.version,
|
|
72
|
+
execPath: process.execPath,
|
|
73
|
+
execArgv: [...process.execArgv],
|
|
74
|
+
nodeOptions: process.env.NODE_OPTIONS || '',
|
|
75
|
+
originalCode: typeof code === 'string' ? code : undefined,
|
|
76
|
+
originalMessage: error.message
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
async function loadNodeSqlite(dbPath) {
|
|
80
|
+
try {
|
|
81
|
+
return await import('node:sqlite');
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
throw mapSqliteImportError(error, dbPath);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
25
87
|
export function getDbPath() {
|
|
26
88
|
return resolveDbPath();
|
|
27
89
|
}
|
|
90
|
+
export async function assertSqliteRuntimeSupport() {
|
|
91
|
+
const dbPath = resolveDbPath();
|
|
92
|
+
const isBunRuntime = typeof globalThis.Bun !== 'undefined';
|
|
93
|
+
if (isBunRuntime) {
|
|
94
|
+
try {
|
|
95
|
+
const bunModuleName = 'bun:sqlite';
|
|
96
|
+
await import(bunModuleName);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
const code = error instanceof Error
|
|
101
|
+
? error.code
|
|
102
|
+
: undefined;
|
|
103
|
+
throw new SqliteRuntimeError('SQLite runtime support is unavailable in Bun runtime.', {
|
|
104
|
+
runtime: 'bun',
|
|
105
|
+
dbPath,
|
|
106
|
+
originalCode: typeof code === 'string' ? code : undefined,
|
|
107
|
+
originalMessage: error instanceof Error ? error.message : String(error)
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
await loadNodeSqlite(dbPath);
|
|
112
|
+
}
|
|
28
113
|
async function createNodeAdapter(dbPath) {
|
|
29
|
-
const sqliteModule = await
|
|
114
|
+
const sqliteModule = await loadNodeSqlite(dbPath);
|
|
30
115
|
const db = new sqliteModule.DatabaseSync(dbPath);
|
|
31
116
|
return {
|
|
32
117
|
exec(sql) {
|