@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.
Files changed (36) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/_nuxt/{Bo1Fdv48.js → BPaqwWyl.js} +2 -2
  3. package/.output/public/_nuxt/{DhQtydpF.js → C8LtDyY4.js} +1 -1
  4. package/.output/public/_nuxt/{D0zW6lUK.js → CQgu_W_k.js} +1 -1
  5. package/.output/public/_nuxt/{BRDbaJqY.js → CZKCADv6.js} +2 -2
  6. package/.output/public/_nuxt/{CEJOILWG.js → CeO4HNxC.js} +1 -1
  7. package/.output/public/_nuxt/{BNzFoVmP.js → Cs5ptsBk.js} +1 -1
  8. package/.output/public/_nuxt/{CFsNy2aC.js → CshyynD6.js} +1 -1
  9. package/.output/public/_nuxt/{DYDTtHLR.js → CzKPXRws.js} +1 -1
  10. package/.output/public/_nuxt/{BqmZq_gb.js → DOvbLsAq.js} +1 -1
  11. package/.output/public/_nuxt/{Bri1ZtcQ.js → DbloiS5Y.js} +1 -1
  12. package/.output/public/_nuxt/{B3hkJjmY.js → DcRwFvvS.js} +1 -1
  13. package/.output/public/_nuxt/builds/latest.json +1 -1
  14. package/.output/public/_nuxt/builds/meta/e2995e80-736c-47cd-8041-a131bab2f136.json +1 -0
  15. package/.output/public/_nuxt/{X6fIXIFO.js → vr7VLA9A.js} +1 -1
  16. package/.output/server/chunks/build/{_prd_-CnwhMRyf.mjs → _prd_-CkKfJB6U.mjs} +2 -2
  17. package/.output/server/chunks/build/_prd_-CkKfJB6U.mjs.map +1 -0
  18. package/.output/server/chunks/build/client.precomputed.mjs +1 -1
  19. package/.output/server/chunks/build/server.mjs +1 -1
  20. package/.output/server/chunks/nitro/nitro.mjs +684 -622
  21. package/.output/server/package.json +1 -1
  22. package/README.md +31 -4
  23. package/bin/prd +117 -0
  24. package/dist/host/src/api/git.js +1 -8
  25. package/dist/host/src/api/prds.js +2 -8
  26. package/dist/host/src/api/repo-context.js +60 -0
  27. package/dist/host/src/api/repos.js +6 -0
  28. package/dist/host/src/api/state.js +20 -21
  29. package/dist/host/src/executor.js +215 -29
  30. package/dist/host/src/help.js +124 -0
  31. package/dist/host/src/mcp.js +51 -25
  32. package/dist/server/utils/db.js +86 -1
  33. package/docs/MCP.md +64 -3
  34. package/package.json +1 -1
  35. package/.output/public/_nuxt/builds/meta/6683a0d9-9c02-4098-b750-bbbc0305261e.json +0 -1
  36. package/.output/server/chunks/build/_prd_-CnwhMRyf.mjs.map +0 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thxgg/steward-prod",
3
- "version": "0.1.7",
3
+ "version": "0.1.11",
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "dependencies": {
package/README.md CHANGED
@@ -41,6 +41,15 @@ Add to your MCP client config:
41
41
  }
42
42
  ```
43
43
 
44
+ Steward MCP requires a Node runtime with built-in sqlite support (`node:sqlite`) for `repos`, `prds`, and `state` APIs.
45
+ If you see `ERR_UNKNOWN_BUILTIN_MODULE: node:sqlite`, run with sqlite enabled:
46
+
47
+ ```bash
48
+ NODE_OPTIONS=--experimental-sqlite npx -y @thxgg/steward mcp
49
+ ```
50
+
51
+ Note: `execute` runs in a VM sandbox by design, so globals like `process` are intentionally not exposed.
52
+
44
53
  ### CLI
45
54
 
46
55
  ```bash
@@ -77,9 +86,7 @@ prd mcp
77
86
  Steward exposes one MCP tool: `execute`.
78
87
 
79
88
  ```js
80
- const reposList = await repos.list()
81
- const repo = reposList[0]
82
- if (!repo) return { error: 'No repos configured' }
89
+ const repo = await repos.current()
83
90
 
84
91
  const prdList = await prds.list(repo.id)
85
92
  if (prdList.length === 0) return { repo: repo.name, prds: 0 }
@@ -92,6 +99,26 @@ return {
92
99
  }
93
100
  ```
94
101
 
102
+ Every call returns a structured envelope:
103
+
104
+ ```json
105
+ {
106
+ "ok": true,
107
+ "result": {},
108
+ "logs": [],
109
+ "error": null,
110
+ "meta": {
111
+ "timeoutMs": 30000,
112
+ "durationMs": 10,
113
+ "truncatedResult": false,
114
+ "truncatedLogs": false,
115
+ "resultWasUndefined": false
116
+ }
117
+ }
118
+ ```
119
+
120
+ Use `steward.help()` inside `execute` for runtime API signatures and examples.
121
+
95
122
  ## APIs
96
123
 
97
124
  Inside `execute`, these APIs are available:
@@ -99,7 +126,7 @@ Inside `execute`, these APIs are available:
99
126
  - `repos` - register/list/remove repos and refresh discovered git repos
100
127
  - `prds` - list/read PRD docs, tasks, progress, and task commit refs
101
128
  - `git` - commit metadata, diffs, file diffs, and file contents
102
- - `state` - direct PRD state get/upsert by repo id or path
129
+ - `state` - direct PRD state get/upsert by repo id, path, or current repo
103
130
 
104
131
  Detailed API docs and examples: `docs/MCP.md`
105
132
 
package/bin/prd CHANGED
@@ -1,11 +1,128 @@
1
1
  #!/usr/bin/env node
2
+ import { spawnSync } from 'node:child_process'
2
3
  import { existsSync } from 'node:fs'
3
4
  import { dirname, resolve } from 'node:path'
4
5
  import { fileURLToPath, pathToFileURL } from 'node:url'
5
6
 
7
+ const SQLITE_ENABLE_FLAG = '--experimental-sqlite'
8
+ const SQLITE_DISABLE_FLAG = '--no-experimental-sqlite'
9
+ const SQLITE_REEXEC_ENV = 'STEWARD_SQLITE_REEXEC'
10
+
6
11
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
7
12
  const entryPath = resolve(packageRoot, 'dist', 'host', 'src', 'index.js')
8
13
 
14
+ function requiresSqliteRuntime(command) {
15
+ return command === 'mcp' || command === 'ui'
16
+ }
17
+
18
+ function isSqliteRuntimeError(error) {
19
+ if (!(error instanceof Error)) {
20
+ return false
21
+ }
22
+
23
+ const code = error.code
24
+ return typeof code === 'string'
25
+ && code === 'ERR_UNKNOWN_BUILTIN_MODULE'
26
+ && error.message.includes('node:sqlite')
27
+ }
28
+
29
+ function sanitizeNodeOptions(rawNodeOptions = '') {
30
+ const options = rawNodeOptions
31
+ .split(/\s+/)
32
+ .filter(Boolean)
33
+ .filter((option) => option !== SQLITE_ENABLE_FLAG && option !== SQLITE_DISABLE_FLAG)
34
+
35
+ return options.join(' ')
36
+ }
37
+
38
+ function buildReexecEnv() {
39
+ const env = { ...process.env }
40
+ const sanitizedNodeOptions = sanitizeNodeOptions(env.NODE_OPTIONS)
41
+
42
+ if (sanitizedNodeOptions.length > 0) {
43
+ env.NODE_OPTIONS = sanitizedNodeOptions
44
+ } else {
45
+ delete env.NODE_OPTIONS
46
+ }
47
+
48
+ env[SQLITE_REEXEC_ENV] = '1'
49
+ return env
50
+ }
51
+
52
+ function canEnableSqliteWithFlag() {
53
+ const probe = spawnSync(
54
+ process.execPath,
55
+ [SQLITE_ENABLE_FLAG, '-e', "import('node:sqlite').then(() => process.exit(0)).catch(() => process.exit(1))"],
56
+ {
57
+ stdio: 'ignore',
58
+ env: buildReexecEnv()
59
+ }
60
+ )
61
+
62
+ return probe.status === 0
63
+ }
64
+
65
+ function reexecWithSqliteFlag() {
66
+ const result = spawnSync(
67
+ process.execPath,
68
+ [SQLITE_ENABLE_FLAG, fileURLToPath(import.meta.url), ...process.argv.slice(2)],
69
+ {
70
+ stdio: 'inherit',
71
+ env: buildReexecEnv()
72
+ }
73
+ )
74
+
75
+ if (result.error) {
76
+ throw result.error
77
+ }
78
+
79
+ process.exit(result.status ?? 1)
80
+ }
81
+
82
+ async function ensureSqliteRuntime(command) {
83
+ if (!requiresSqliteRuntime(command)) {
84
+ return
85
+ }
86
+
87
+ try {
88
+ await import('node:sqlite')
89
+ return
90
+ } catch (error) {
91
+ if (!isSqliteRuntimeError(error)) {
92
+ throw error
93
+ }
94
+
95
+ const alreadyReexecuted = process.env[SQLITE_REEXEC_ENV] === '1'
96
+ if (!alreadyReexecuted && canEnableSqliteWithFlag()) {
97
+ reexecWithSqliteFlag()
98
+ return
99
+ }
100
+
101
+ const message = error instanceof Error ? error.message : String(error)
102
+ const nodeOptions = process.env.NODE_OPTIONS || ''
103
+ const hasDisableFlag = process.execArgv.includes(SQLITE_DISABLE_FLAG)
104
+ || nodeOptions.includes(SQLITE_DISABLE_FLAG)
105
+
106
+ console.error('Steward requires SQLite runtime support for repos/prds/state APIs.')
107
+ console.error(`Runtime: ${process.version} (${process.execPath})`)
108
+ if (nodeOptions) {
109
+ console.error(`NODE_OPTIONS: ${nodeOptions}`)
110
+ }
111
+ console.error(`Cause: ${message}`)
112
+
113
+ if (hasDisableFlag) {
114
+ console.error(`Hint: remove ${SQLITE_DISABLE_FLAG} from NODE_OPTIONS or runtime args and retry.`)
115
+ } else {
116
+ console.error(`Hint: use a Node runtime with sqlite support or launch with ${SQLITE_ENABLE_FLAG}.`)
117
+ }
118
+
119
+ process.exit(1)
120
+ }
121
+ }
122
+
123
+ const command = process.argv[2] || ''
124
+ await ensureSqliteRuntime(command)
125
+
9
126
  if (!existsSync(entryPath)) {
10
127
  console.error('Steward host runtime is not built.')
11
128
  console.error('Run `npm run build:host` in this package before invoking `prd`.')
@@ -1,12 +1,5 @@
1
1
  import { getCommitDiff, getCommitInfo, getFileContent, getFileDiff, isGitRepo, validatePathInRepo } from '../../../server/utils/git.js';
2
- import { getRepoById } from '../../../server/utils/repos.js';
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, getRepoById, getRepos, saveRepos } from '../../../server/utils/repos.js';
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 { getRepoById, getRepos } from '../../../server/utils/repos.js';
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 findRepoByPath(repoPath);
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 findRepoByPath(repoPath);
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 findRepoByPath(repoPath);
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
  }