@thxgg/steward 0.1.7 → 0.1.10
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/7fda7510-94bc-443a-a338-a8d2af142ed9.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 +617 -617
- package/.output/server/package.json +1 -1
- package/README.md +22 -4
- 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 +49 -25
- package/docs/MCP.md +50 -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
package/README.md
CHANGED
|
@@ -77,9 +77,7 @@ prd mcp
|
|
|
77
77
|
Steward exposes one MCP tool: `execute`.
|
|
78
78
|
|
|
79
79
|
```js
|
|
80
|
-
const
|
|
81
|
-
const repo = reposList[0]
|
|
82
|
-
if (!repo) return { error: 'No repos configured' }
|
|
80
|
+
const repo = await repos.current()
|
|
83
81
|
|
|
84
82
|
const prdList = await prds.list(repo.id)
|
|
85
83
|
if (prdList.length === 0) return { repo: repo.name, prds: 0 }
|
|
@@ -92,6 +90,26 @@ return {
|
|
|
92
90
|
}
|
|
93
91
|
```
|
|
94
92
|
|
|
93
|
+
Every call returns a structured envelope:
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"ok": true,
|
|
98
|
+
"result": {},
|
|
99
|
+
"logs": [],
|
|
100
|
+
"error": null,
|
|
101
|
+
"meta": {
|
|
102
|
+
"timeoutMs": 30000,
|
|
103
|
+
"durationMs": 10,
|
|
104
|
+
"truncatedResult": false,
|
|
105
|
+
"truncatedLogs": false,
|
|
106
|
+
"resultWasUndefined": false
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Use `steward.help()` inside `execute` for runtime API signatures and examples.
|
|
112
|
+
|
|
95
113
|
## APIs
|
|
96
114
|
|
|
97
115
|
Inside `execute`, these APIs are available:
|
|
@@ -99,7 +117,7 @@ Inside `execute`, these APIs are available:
|
|
|
99
117
|
- `repos` - register/list/remove repos and refresh discovered git repos
|
|
100
118
|
- `prds` - list/read PRD docs, tasks, progress, and task commit refs
|
|
101
119
|
- `git` - commit metadata, diffs, file diffs, and file contents
|
|
102
|
-
- `state` - direct PRD state get/upsert by repo id or
|
|
120
|
+
- `state` - direct PRD state get/upsert by repo id, path, or current repo
|
|
103
121
|
|
|
104
122
|
Detailed API docs and examples: `docs/MCP.md`
|
|
105
123
|
|
package/dist/host/src/api/git.js
CHANGED
|
@@ -1,12 +1,5 @@
|
|
|
1
1
|
import { getCommitDiff, getCommitInfo, getFileContent, getFileDiff, isGitRepo, validatePathInRepo } from '../../../server/utils/git.js';
|
|
2
|
-
import {
|
|
3
|
-
async function requireRepo(repoId) {
|
|
4
|
-
const repo = await getRepoById(repoId);
|
|
5
|
-
if (!repo) {
|
|
6
|
-
throw new Error('Repository not found');
|
|
7
|
-
}
|
|
8
|
-
return repo;
|
|
9
|
-
}
|
|
2
|
+
import { requireRepo } from './repo-context.js';
|
|
10
3
|
function resolveGitRepoPath(repo, repoPath) {
|
|
11
4
|
if (!repoPath) {
|
|
12
5
|
return repo.path;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
2
|
import { basename, join } from 'node:path';
|
|
3
3
|
import { resolveCommitRepo } from '../../../server/utils/git.js';
|
|
4
|
-
import { discoverGitRepos,
|
|
4
|
+
import { discoverGitRepos, getRepos, saveRepos } from '../../../server/utils/repos.js';
|
|
5
5
|
import { getPrdState, getPrdStateSummaries, migrateLegacyStateForRepo } from '../../../server/utils/prd-state.js';
|
|
6
|
+
import { requireRepo } from './repo-context.js';
|
|
6
7
|
function parseMetadata(content) {
|
|
7
8
|
const metadata = {};
|
|
8
9
|
const authorMatch = content.match(/\*{0,2}Author\*{0,2}:\*{0,2}\s*(.+?)(?:\n|$)/i);
|
|
@@ -30,13 +31,6 @@ function parseMetadata(content) {
|
|
|
30
31
|
}
|
|
31
32
|
return metadata;
|
|
32
33
|
}
|
|
33
|
-
async function requireRepo(repoId) {
|
|
34
|
-
const repo = await getRepoById(repoId);
|
|
35
|
-
if (!repo) {
|
|
36
|
-
throw new Error('Repository not found');
|
|
37
|
-
}
|
|
38
|
-
return repo;
|
|
39
|
-
}
|
|
40
34
|
async function readPrdFile(repo, prdSlug) {
|
|
41
35
|
const prdPath = join(repo.path, 'docs', 'prd', `${prdSlug}.md`);
|
|
42
36
|
try {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { getRepoById, getRepos } from '../../../server/utils/repos.js';
|
|
3
|
+
export class RepoLookupError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
details;
|
|
6
|
+
constructor(message, code, details) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.details = details;
|
|
10
|
+
this.name = 'RepoLookupError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function summarizeRepos(repos) {
|
|
14
|
+
return repos.map((repo) => ({
|
|
15
|
+
id: repo.id,
|
|
16
|
+
name: repo.name,
|
|
17
|
+
path: repo.path
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
function formatKnownRepos(knownRepos) {
|
|
21
|
+
return knownRepos
|
|
22
|
+
.map((repo) => `${repo.id} (${repo.name}) ${repo.path}`)
|
|
23
|
+
.join('; ');
|
|
24
|
+
}
|
|
25
|
+
export async function requireRepo(repoId) {
|
|
26
|
+
const repo = await getRepoById(repoId);
|
|
27
|
+
if (repo) {
|
|
28
|
+
return repo;
|
|
29
|
+
}
|
|
30
|
+
const allRepos = await getRepos();
|
|
31
|
+
const knownRepos = summarizeRepos(allRepos);
|
|
32
|
+
if (knownRepos.length === 0) {
|
|
33
|
+
throw new RepoLookupError(`Unknown repoId "${repoId}". No repositories are registered. Use repos.add(path) first.`, 'NO_REPOS', { repoId, knownRepos });
|
|
34
|
+
}
|
|
35
|
+
throw new RepoLookupError(`Unknown repoId "${repoId}". Known repositories: ${formatKnownRepos(knownRepos)}`, 'REPO_NOT_FOUND', { repoId, knownRepos });
|
|
36
|
+
}
|
|
37
|
+
export async function requireRepoByPath(repoPath) {
|
|
38
|
+
const absolutePath = resolve(repoPath);
|
|
39
|
+
const allRepos = await getRepos();
|
|
40
|
+
const repo = allRepos.find((candidate) => resolve(candidate.path) === absolutePath);
|
|
41
|
+
if (repo) {
|
|
42
|
+
return repo;
|
|
43
|
+
}
|
|
44
|
+
const knownRepos = summarizeRepos(allRepos);
|
|
45
|
+
if (knownRepos.length === 0) {
|
|
46
|
+
throw new RepoLookupError(`No registered repository found for path: ${absolutePath}. No repositories are registered. Use repos.add(path) first.`, 'NO_REPOS', { repoPath: absolutePath, knownRepos });
|
|
47
|
+
}
|
|
48
|
+
throw new RepoLookupError(`No registered repository found for path: ${absolutePath}. Known repositories: ${formatKnownRepos(knownRepos)}`, 'REPO_PATH_NOT_FOUND', { repoPath: absolutePath, knownRepos });
|
|
49
|
+
}
|
|
50
|
+
export async function requireCurrentRepo() {
|
|
51
|
+
const allRepos = await getRepos();
|
|
52
|
+
if (allRepos.length === 1) {
|
|
53
|
+
return allRepos[0];
|
|
54
|
+
}
|
|
55
|
+
const knownRepos = summarizeRepos(allRepos);
|
|
56
|
+
if (knownRepos.length === 0) {
|
|
57
|
+
throw new RepoLookupError('No repositories are registered. Use repos.add(path) first.', 'NO_REPOS', { knownRepos });
|
|
58
|
+
}
|
|
59
|
+
throw new RepoLookupError(`Cannot resolve a current repository because ${knownRepos.length} repositories are registered. Use an explicit repoId or by-path API. Known repositories: ${formatKnownRepos(knownRepos)}`, 'AMBIGUOUS_REPO', { knownRepos });
|
|
60
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { addRepo, discoverGitRepos, getRepoById, getRepos, removeRepo, saveRepos, validateRepoPath } from '../../../server/utils/repos.js';
|
|
2
2
|
import { migrateLegacyStateForRepo } from '../../../server/utils/prd-state.js';
|
|
3
|
+
import { requireCurrentRepo, requireRepo } from './repo-context.js';
|
|
3
4
|
export const repos = {
|
|
4
5
|
async list() {
|
|
5
6
|
return await getRepos();
|
|
@@ -7,6 +8,9 @@ export const repos = {
|
|
|
7
8
|
async get(repoId) {
|
|
8
9
|
return await getRepoById(repoId) ?? null;
|
|
9
10
|
},
|
|
11
|
+
async current() {
|
|
12
|
+
return await requireCurrentRepo();
|
|
13
|
+
},
|
|
10
14
|
async add(path, name) {
|
|
11
15
|
const validation = await validateRepoPath(path);
|
|
12
16
|
if (!validation.valid) {
|
|
@@ -17,6 +21,7 @@ export const repos = {
|
|
|
17
21
|
return repo;
|
|
18
22
|
},
|
|
19
23
|
async remove(repoId) {
|
|
24
|
+
await requireRepo(repoId);
|
|
20
25
|
const removed = await removeRepo(repoId);
|
|
21
26
|
if (!removed) {
|
|
22
27
|
throw new Error('Repository not found');
|
|
@@ -24,6 +29,7 @@ export const repos = {
|
|
|
24
29
|
return { removed: true };
|
|
25
30
|
},
|
|
26
31
|
async refreshGitRepos(repoId) {
|
|
32
|
+
await requireRepo(repoId);
|
|
27
33
|
const allRepos = await getRepos();
|
|
28
34
|
const repoIndex = allRepos.findIndex((repo) => repo.id === repoId);
|
|
29
35
|
if (repoIndex === -1) {
|
|
@@ -1,22 +1,5 @@
|
|
|
1
|
-
import { resolve } from 'node:path';
|
|
2
1
|
import { getPrdState, getPrdStateSummaries, migrateLegacyStateForRepo, upsertPrdState } from '../../../server/utils/prd-state.js';
|
|
3
|
-
import {
|
|
4
|
-
async function requireRepo(repoId) {
|
|
5
|
-
const repo = await getRepoById(repoId);
|
|
6
|
-
if (!repo) {
|
|
7
|
-
throw new Error('Repository not found');
|
|
8
|
-
}
|
|
9
|
-
return repo;
|
|
10
|
-
}
|
|
11
|
-
async function findRepoByPath(repoPath) {
|
|
12
|
-
const absolutePath = resolve(repoPath);
|
|
13
|
-
const repos = await getRepos();
|
|
14
|
-
const repo = repos.find((candidate) => resolve(candidate.path) === absolutePath);
|
|
15
|
-
if (!repo) {
|
|
16
|
-
throw new Error(`No registered repository found for path: ${absolutePath}`);
|
|
17
|
-
}
|
|
18
|
-
return repo;
|
|
19
|
-
}
|
|
2
|
+
import { requireCurrentRepo, requireRepo, requireRepoByPath } from './repo-context.js';
|
|
20
3
|
function mapStateUpdate(payload) {
|
|
21
4
|
return {
|
|
22
5
|
...(payload.tasks !== undefined && { tasks: payload.tasks }),
|
|
@@ -34,7 +17,12 @@ export const state = {
|
|
|
34
17
|
return await getPrdState(repo.id, slug);
|
|
35
18
|
},
|
|
36
19
|
async getByPath(repoPath, slug) {
|
|
37
|
-
const repo = await
|
|
20
|
+
const repo = await requireRepoByPath(repoPath);
|
|
21
|
+
await migrateLegacyStateForRepo(repo);
|
|
22
|
+
return await getPrdState(repo.id, slug);
|
|
23
|
+
},
|
|
24
|
+
async getCurrent(slug) {
|
|
25
|
+
const repo = await requireCurrentRepo();
|
|
38
26
|
await migrateLegacyStateForRepo(repo);
|
|
39
27
|
return await getPrdState(repo.id, slug);
|
|
40
28
|
},
|
|
@@ -45,7 +33,13 @@ export const state = {
|
|
|
45
33
|
return mapSummaryMap(summaries);
|
|
46
34
|
},
|
|
47
35
|
async summariesByPath(repoPath) {
|
|
48
|
-
const repo = await
|
|
36
|
+
const repo = await requireRepoByPath(repoPath);
|
|
37
|
+
await migrateLegacyStateForRepo(repo);
|
|
38
|
+
const summaries = await getPrdStateSummaries(repo.id);
|
|
39
|
+
return mapSummaryMap(summaries);
|
|
40
|
+
},
|
|
41
|
+
async summariesCurrent() {
|
|
42
|
+
const repo = await requireCurrentRepo();
|
|
49
43
|
await migrateLegacyStateForRepo(repo);
|
|
50
44
|
const summaries = await getPrdStateSummaries(repo.id);
|
|
51
45
|
return mapSummaryMap(summaries);
|
|
@@ -56,7 +50,12 @@ export const state = {
|
|
|
56
50
|
return { saved: true };
|
|
57
51
|
},
|
|
58
52
|
async upsertByPath(repoPath, slug, payload) {
|
|
59
|
-
const repo = await
|
|
53
|
+
const repo = await requireRepoByPath(repoPath);
|
|
54
|
+
await upsertPrdState(repo.id, slug, mapStateUpdate(payload));
|
|
55
|
+
return { saved: true };
|
|
56
|
+
},
|
|
57
|
+
async upsertCurrent(slug, payload) {
|
|
58
|
+
const repo = await requireCurrentRepo();
|
|
60
59
|
await upsertPrdState(repo.id, slug, mapStateUpdate(payload));
|
|
61
60
|
return { saved: true };
|
|
62
61
|
}
|
|
@@ -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
|
+
}
|