@thxgg/steward 0.1.15 → 0.1.17

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 (158) hide show
  1. package/.env.example +6 -0
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/_nuxt/{qKRNa41x.js → 4r0X30JV.js} +1 -1
  4. package/.output/public/_nuxt/{uTyw4SRK.js → BMdjSp24.js} +1 -1
  5. package/.output/public/_nuxt/{BubpH_wW.js → BSZqAKg4.js} +1 -1
  6. package/.output/public/_nuxt/{nYTZJhvT.js → BdjPva1I.js} +1 -1
  7. package/.output/public/_nuxt/Beeir9iR.js +1 -0
  8. package/.output/public/_nuxt/Bh3vsUvl.js +1 -0
  9. package/.output/public/_nuxt/{CMu9GKTH.js → BlTKcjLJ.js} +2 -2
  10. package/.output/public/_nuxt/{BDqHART1.js → By7gAVcL.js} +1 -1
  11. package/.output/public/_nuxt/CbJfCtEa.js +1 -0
  12. package/.output/public/_nuxt/CbkpNvIu.js +141 -0
  13. package/.output/public/_nuxt/CmhLcqDu.js +1 -0
  14. package/.output/public/_nuxt/DC6iPLz1.js +30 -0
  15. package/.output/public/_nuxt/{C_NevjZD.js → DD--ojY9.js} +1 -1
  16. package/.output/public/_nuxt/Detail.DSyVQNdr.css +1 -0
  17. package/.output/public/_nuxt/DhKWRjCh.js +60 -0
  18. package/.output/public/_nuxt/_prd_.BkpxMFSV.css +1 -0
  19. package/.output/public/_nuxt/builds/latest.json +1 -1
  20. package/.output/public/_nuxt/builds/meta/f3f42dbd-d501-442b-871c-3d06157e7aa1.json +1 -0
  21. package/.output/public/_nuxt/c1sXju8w.js +1 -0
  22. package/.output/public/_nuxt/eGCjCghR.js +1 -0
  23. package/.output/public/_nuxt/nX8Sf7cz.js +13 -0
  24. package/.output/server/chunks/_/git-api.mjs +100 -7
  25. package/.output/server/chunks/_/git-api.mjs.map +1 -1
  26. package/.output/server/chunks/_/git.mjs +3 -10
  27. package/.output/server/chunks/_/git.mjs.map +1 -1
  28. package/.output/server/chunks/_/prd-service.mjs +234 -0
  29. package/.output/server/chunks/_/prd-service.mjs.map +1 -0
  30. package/.output/server/chunks/_/task-graph.mjs +3 -3
  31. package/.output/server/chunks/_/task-graph.mjs.map +1 -1
  32. package/.output/server/chunks/_/watcher.mjs +26 -46
  33. package/.output/server/chunks/_/watcher.mjs.map +1 -1
  34. package/.output/server/chunks/build/{Detail-DC-KJQ1f.mjs → Detail-MGwP_u2d.mjs} +63 -34
  35. package/.output/server/chunks/build/Detail-MGwP_u2d.mjs.map +1 -0
  36. package/.output/server/chunks/build/DiffViewer-styles-1.mjs-BFsE2PCW.mjs +4 -0
  37. package/.output/server/chunks/build/DiffViewer-styles-1.mjs-BFsE2PCW.mjs.map +1 -0
  38. package/.output/server/chunks/build/DiffViewer-styles.D2bqX3nK.mjs +8 -0
  39. package/.output/server/chunks/build/DiffViewer-styles.D2bqX3nK.mjs.map +1 -0
  40. package/.output/server/chunks/build/DiffViewer-styles.FoV36wuV.mjs +10 -0
  41. package/.output/server/chunks/build/DiffViewer-styles.FoV36wuV.mjs.map +1 -0
  42. package/.output/server/chunks/build/Viewer-styles.D6wYWFb1.mjs +8 -0
  43. package/.output/server/chunks/build/Viewer-styles.D6wYWFb1.mjs.map +1 -0
  44. package/.output/server/chunks/build/{_prd_-C1C4GAhW.mjs → _prd_-C-Aj4fVa.mjs} +75 -33
  45. package/.output/server/chunks/build/_prd_-C-Aj4fVa.mjs.map +1 -0
  46. package/.output/server/chunks/build/client.precomputed.mjs +1 -1
  47. package/.output/server/chunks/build/{default-DWCOHHTE.mjs → default-Cao5eO80.mjs} +4 -3
  48. package/.output/server/chunks/build/default-Cao5eO80.mjs.map +1 -0
  49. package/.output/server/chunks/build/error-404-Bf6kdO80.mjs +2 -1
  50. package/.output/server/chunks/build/error-500-D_bcARXN.mjs +2 -1
  51. package/.output/server/chunks/build/{index-CckL_NBD.mjs → index-ByZO4Bvq.mjs} +2 -2
  52. package/.output/server/chunks/build/index-ByZO4Bvq.mjs.map +1 -0
  53. package/.output/server/chunks/build/{index-QVeSHT3L.mjs → index-ljj9uTXI.mjs} +8 -5
  54. package/.output/server/chunks/build/index-ljj9uTXI.mjs.map +1 -0
  55. package/.output/server/chunks/build/nuxt-link-SvT1nf8Z.mjs +1 -1
  56. package/.output/server/chunks/build/{repo-graph-CTEkxiYd.mjs → repo-graph-EuhMeFt7.mjs} +25 -10
  57. package/.output/server/chunks/build/repo-graph-EuhMeFt7.mjs.map +1 -0
  58. package/.output/server/chunks/build/server.mjs +7 -6
  59. package/.output/server/chunks/build/styles.mjs +4 -4
  60. package/.output/server/chunks/build/{usePrd-SqcxGyFU.mjs → usePrd-f7ylhIqs.mjs} +10 -34
  61. package/.output/server/chunks/build/usePrd-f7ylhIqs.mjs.map +1 -0
  62. package/.output/server/chunks/nitro/nitro.mjs +1149 -720
  63. package/.output/server/chunks/nitro/nitro.mjs.map +1 -1
  64. package/.output/server/chunks/routes/api/browse.get.mjs +12 -6
  65. package/.output/server/chunks/routes/api/browse.get.mjs.map +1 -1
  66. package/.output/server/chunks/routes/api/index.get.mjs +2 -1
  67. package/.output/server/chunks/routes/api/index.get.mjs.map +1 -1
  68. package/.output/server/chunks/routes/api/index.post.mjs +3 -2
  69. package/.output/server/chunks/routes/api/index.post.mjs.map +1 -1
  70. package/.output/server/chunks/routes/api/repos/_repoId/git/commits.get.mjs +11 -13
  71. package/.output/server/chunks/routes/api/repos/_repoId/git/commits.get.mjs.map +1 -1
  72. package/.output/server/chunks/routes/api/repos/_repoId/git/diff.get.mjs +11 -6
  73. package/.output/server/chunks/routes/api/repos/_repoId/git/diff.get.mjs.map +1 -1
  74. package/.output/server/chunks/routes/api/repos/_repoId/git/file-content.get.mjs +31 -12
  75. package/.output/server/chunks/routes/api/repos/_repoId/git/file-content.get.mjs.map +1 -1
  76. package/.output/server/chunks/routes/api/repos/_repoId/git/file-diff.get.mjs +13 -13
  77. package/.output/server/chunks/routes/api/repos/_repoId/git/file-diff.get.mjs.map +1 -1
  78. package/.output/server/chunks/routes/api/repos/_repoId/graph.get.mjs +5 -1
  79. package/.output/server/chunks/routes/api/repos/_repoId/graph.get.mjs.map +1 -1
  80. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/graph.get.mjs +14 -1
  81. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/graph.get.mjs.map +1 -1
  82. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/progress.get.mjs +20 -9
  83. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/progress.get.mjs.map +1 -1
  84. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/tasks/_taskId/commits.get.mjs +20 -85
  85. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/tasks/_taskId/commits.get.mjs.map +1 -1
  86. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/tasks.get.mjs +20 -9
  87. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/tasks.get.mjs.map +1 -1
  88. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug_.get.mjs +30 -50
  89. package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug_.get.mjs.map +1 -1
  90. package/.output/server/chunks/routes/api/repos/_repoId/prds.get.mjs +19 -49
  91. package/.output/server/chunks/routes/api/repos/_repoId/prds.get.mjs.map +1 -1
  92. package/.output/server/chunks/routes/api/repos/_repoId/refresh-git-repos.post.mjs +6 -13
  93. package/.output/server/chunks/routes/api/repos/_repoId/refresh-git-repos.post.mjs.map +1 -1
  94. package/.output/server/chunks/routes/api/repos/_repoId_.delete.mjs +2 -1
  95. package/.output/server/chunks/routes/api/repos/_repoId_.delete.mjs.map +1 -1
  96. package/.output/server/chunks/routes/api/runtime.get.mjs +3 -2
  97. package/.output/server/chunks/routes/api/runtime.get.mjs.map +1 -1
  98. package/.output/server/chunks/routes/api/watch.get.mjs +5 -4
  99. package/.output/server/chunks/routes/api/watch.get.mjs.map +1 -1
  100. package/.output/server/chunks/routes/renderer.mjs +1 -1
  101. package/.output/server/index.mjs +3 -2
  102. package/.output/server/index.mjs.map +1 -1
  103. package/.output/server/node_modules/zod/index.js +4 -0
  104. package/.output/server/node_modules/zod/package.json +118 -0
  105. package/.output/server/node_modules/zod/v3/ZodError.js +133 -0
  106. package/.output/server/node_modules/zod/v3/errors.js +9 -0
  107. package/.output/server/node_modules/zod/v3/external.js +6 -0
  108. package/.output/server/node_modules/zod/v3/helpers/errorUtil.js +6 -0
  109. package/.output/server/node_modules/zod/v3/helpers/parseUtil.js +109 -0
  110. package/.output/server/node_modules/zod/v3/helpers/typeAliases.js +1 -0
  111. package/.output/server/node_modules/zod/v3/helpers/util.js +133 -0
  112. package/.output/server/node_modules/zod/v3/locales/en.js +109 -0
  113. package/.output/server/node_modules/zod/v3/types.js +3693 -0
  114. package/.output/server/package.json +2 -1
  115. package/README.md +7 -2
  116. package/dist/host/src/api/prds.js +6 -172
  117. package/dist/host/src/api/repos.js +3 -16
  118. package/dist/host/src/api/state.js +7 -2
  119. package/dist/host/src/executor-runner.js +368 -0
  120. package/dist/host/src/executor.js +138 -260
  121. package/dist/host/src/index.js +7 -2
  122. package/dist/host/src/mcp.js +27 -1
  123. package/dist/host/src/ui.js +18 -3
  124. package/dist/server/utils/change-events.js +33 -0
  125. package/dist/server/utils/git.js +11 -16
  126. package/dist/server/utils/prd-service.js +235 -0
  127. package/dist/server/utils/prd-state.js +57 -45
  128. package/dist/server/utils/repos.js +58 -13
  129. package/dist/server/utils/state-schema.js +61 -0
  130. package/dist/server/utils/task-graph.js +2 -2
  131. package/package.json +2 -1
  132. package/.output/public/_nuxt/-k8zG74W.js +0 -61
  133. package/.output/public/_nuxt/B-5VWizU.js +0 -1
  134. package/.output/public/_nuxt/BMAq0QVD.js +0 -42
  135. package/.output/public/_nuxt/BPeTf9dd.js +0 -1
  136. package/.output/public/_nuxt/C2HGkiSP.js +0 -1
  137. package/.output/public/_nuxt/CVvrkZkq.js +0 -1
  138. package/.output/public/_nuxt/Detail.CzXXlavD.css +0 -1
  139. package/.output/public/_nuxt/_prd_.KTotLoF_.css +0 -1
  140. package/.output/public/_nuxt/builds/meta/c1a7997d-8d53-4718-ad03-a977e05e2523.json +0 -1
  141. package/.output/public/_nuxt/qt5OEWHC.js +0 -1
  142. package/.output/public/_nuxt/wbj-mIhK.js +0 -1
  143. package/.output/server/chunks/build/Detail-DC-KJQ1f.mjs.map +0 -1
  144. package/.output/server/chunks/build/DiffViewer-styles-1.mjs-D0sb4vsK.mjs +0 -4
  145. package/.output/server/chunks/build/DiffViewer-styles-1.mjs-D0sb4vsK.mjs.map +0 -1
  146. package/.output/server/chunks/build/DiffViewer-styles.CkSjCQ0r.mjs +0 -10
  147. package/.output/server/chunks/build/DiffViewer-styles.CkSjCQ0r.mjs.map +0 -1
  148. package/.output/server/chunks/build/DiffViewer-styles.FJJuYjYB.mjs +0 -8
  149. package/.output/server/chunks/build/DiffViewer-styles.FJJuYjYB.mjs.map +0 -1
  150. package/.output/server/chunks/build/Viewer-styles.CshnetGw.mjs +0 -8
  151. package/.output/server/chunks/build/Viewer-styles.CshnetGw.mjs.map +0 -1
  152. package/.output/server/chunks/build/_prd_-C1C4GAhW.mjs.map +0 -1
  153. package/.output/server/chunks/build/default-DWCOHHTE.mjs.map +0 -1
  154. package/.output/server/chunks/build/index-CckL_NBD.mjs.map +0 -1
  155. package/.output/server/chunks/build/index-QVeSHT3L.mjs.map +0 -1
  156. package/.output/server/chunks/build/repo-graph-CTEkxiYd.mjs.map +0 -1
  157. package/.output/server/chunks/build/usePrd-SqcxGyFU.mjs.map +0 -1
  158. package/.output/server/node_modules/shiki/dist/bundle-web.mjs +0 -366
@@ -1,103 +1,21 @@
1
- import vm from 'node:vm';
2
- import { git, prds, repos, state } from './api/index.js';
3
- import { getStewardHelp } from './help.js';
4
- const MAX_OUTPUT_SIZE = 50_000;
1
+ import { spawn } from 'node:child_process';
2
+ import { fileURLToPath } from 'node:url';
5
3
  const EXECUTION_TIMEOUT_MS = 30_000;
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;
10
- export class ExecutionError extends Error {
11
- options;
12
- constructor(message, options) {
13
- super(message);
14
- this.options = options;
15
- this.name = 'ExecutionError';
16
- }
17
- }
18
- function safeJsonStringify(value) {
19
- const seen = new WeakSet();
20
- try {
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
- });
40
- }
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) {
57
- return {
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
72
- };
73
- }
74
- if (json.length <= MAX_OUTPUT_SIZE) {
75
- return {
76
- result,
77
- truncatedResult: false,
78
- resultWasUndefined: false
79
- };
4
+ const MAX_STDIO_CAPTURE = 200_000;
5
+ function getForwardedNodeFlags() {
6
+ const forwarded = [];
7
+ for (const arg of process.execArgv) {
8
+ if (arg === '--experimental-sqlite' || arg === '--no-experimental-sqlite') {
9
+ forwarded.push(arg);
10
+ continue;
11
+ }
12
+ if (arg.startsWith('--experimental-sqlite=')) {
13
+ forwarded.push(arg);
14
+ }
80
15
  }
81
- return {
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
- };
16
+ return forwarded;
91
17
  }
92
18
  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
19
  if (error instanceof Error) {
102
20
  const { code, details } = error;
103
21
  return {
@@ -112,184 +30,144 @@ function normalizeFailure(error) {
112
30
  message: String(error)
113
31
  };
114
32
  }
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,
33
+ function buildFailureEnvelope(startedAt, code, message, details) {
34
+ return {
35
+ ok: false,
36
+ result: null,
37
+ logs: [],
38
+ error: {
39
+ code,
137
40
  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,
41
+ ...(details !== undefined && { details })
42
+ },
146
43
  meta: {
147
44
  timeoutMs: EXECUTION_TIMEOUT_MS,
148
45
  durationMs: Date.now() - startedAt,
149
- truncatedResult: params.truncatedResult,
150
- truncatedLogs: logsTruncated,
151
- resultWasUndefined: params.resultWasUndefined
152
- }
153
- });
154
- if (!code || !code.trim()) {
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
46
  truncatedResult: false,
47
+ truncatedLogs: false,
161
48
  resultWasUndefined: false
162
- });
163
- }
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
- };
49
+ }
180
50
  };
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
- });
51
+ }
52
+ function withDurationFallback(envelope, startedAt) {
53
+ const durationMs = Number.isFinite(envelope.meta.durationMs) && envelope.meta.durationMs >= 0
54
+ ? envelope.meta.durationMs
55
+ : Date.now() - startedAt;
56
+ return {
57
+ ...envelope,
58
+ meta: {
59
+ ...envelope.meta,
60
+ timeoutMs: EXECUTION_TIMEOUT_MS,
61
+ durationMs
186
62
  }
187
- return wrapTimerHandler(handler);
188
63
  };
189
- const sandbox = {
190
- repos,
191
- prds,
192
- git,
193
- state,
194
- steward: {
195
- help: () => getStewardHelp()
196
- },
197
- console: {
198
- log: (...args) => appendLog('log', args),
199
- info: (...args) => appendLog('info', args),
200
- warn: (...args) => appendLog('warn', args),
201
- error: (...args) => appendLog('error', args)
202
- },
203
- setTimeout: (handler, timeout) => {
204
- if (timers.size >= MAX_TIMERS) {
205
- throw new ExecutionError(`Timer limit exceeded (max ${MAX_TIMERS})`, {
206
- code: 'TIMER_LIMIT'
207
- });
64
+ }
65
+ function looksLikeExecutionEnvelope(value) {
66
+ if (!value || typeof value !== 'object') {
67
+ return false;
68
+ }
69
+ const candidate = value;
70
+ return typeof candidate.ok === 'boolean'
71
+ && Array.isArray(candidate.logs)
72
+ && candidate.meta !== undefined;
73
+ }
74
+ export async function execute(code) {
75
+ const startedAt = Date.now();
76
+ if (!code || !code.trim()) {
77
+ return buildFailureEnvelope(startedAt, 'EMPTY_CODE', 'Code cannot be empty');
78
+ }
79
+ const runnerPath = fileURLToPath(new URL('./executor-runner.js', import.meta.url));
80
+ const childArgs = [
81
+ ...getForwardedNodeFlags(),
82
+ '--max-old-space-size=256',
83
+ runnerPath
84
+ ];
85
+ return await new Promise((resolveEnvelope) => {
86
+ const child = spawn(process.execPath, childArgs, {
87
+ cwd: process.cwd(),
88
+ env: {
89
+ ...process.env,
90
+ STEWARD_EXECUTION_TIMEOUT_MS: String(EXECUTION_TIMEOUT_MS)
91
+ },
92
+ stdio: ['pipe', 'pipe', 'pipe']
93
+ });
94
+ let settled = false;
95
+ let stdout = '';
96
+ let stderr = '';
97
+ const finish = (envelope) => {
98
+ if (settled) {
99
+ return;
208
100
  }
209
- const wrappedHandler = ensureTimerHandler(handler);
210
- const timer = setTimeout(() => {
211
- timers.delete(timer);
212
- wrappedHandler();
213
- }, timeout);
214
- timers.add(timer);
215
- return timer;
216
- },
217
- clearTimeout: (timer) => {
218
- timers.delete(timer);
219
- clearTimeout(timer);
220
- },
221
- setInterval: (handler, timeout) => {
222
- if (timers.size >= MAX_TIMERS) {
223
- throw new ExecutionError(`Timer limit exceeded (max ${MAX_TIMERS})`, {
224
- code: 'TIMER_LIMIT'
225
- });
101
+ settled = true;
102
+ clearTimeout(killTimer);
103
+ resolveEnvelope(withDurationFallback(envelope, startedAt));
104
+ };
105
+ const captureOutput = (current, chunk) => {
106
+ if (current.length >= MAX_STDIO_CAPTURE) {
107
+ return current;
226
108
  }
227
- const wrappedHandler = ensureTimerHandler(handler);
228
- const timer = setInterval(wrappedHandler, timeout);
229
- timers.add(timer);
230
- return timer;
231
- },
232
- clearInterval: (timer) => {
233
- timers.delete(timer);
234
- clearInterval(timer);
235
- },
236
- Promise
237
- };
238
- const wrappedCode = `
239
- (async () => {
240
- ${code}
241
- })()
242
- `;
243
- try {
244
- const script = new vm.Script(wrappedCode, {
245
- filename: 'codemode.js'
109
+ const remaining = MAX_STDIO_CAPTURE - current.length;
110
+ return current + chunk.toString('utf-8', 0, remaining);
111
+ };
112
+ const killTimer = setTimeout(() => {
113
+ child.kill('SIGKILL');
114
+ finish(buildFailureEnvelope(startedAt, 'TIMEOUT', `Execution timed out after ${EXECUTION_TIMEOUT_MS}ms`));
115
+ }, EXECUTION_TIMEOUT_MS);
116
+ child.stdout.on('data', (chunk) => {
117
+ stdout = captureOutput(stdout, chunk);
246
118
  });
247
- const context = vm.createContext(sandbox);
248
- const executionPromise = Promise.resolve(script.runInContext(context, {
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);
119
+ child.stderr.on('data', (chunk) => {
120
+ stderr = captureOutput(stderr, chunk);
257
121
  });
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
122
+ child.on('error', (error) => {
123
+ const failure = normalizeFailure(error);
124
+ finish(buildFailureEnvelope(startedAt, failure.code, failure.message, failure.details));
272
125
  });
273
- }
274
- catch (error) {
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
126
+ child.on('close', (exitCode, signal) => {
127
+ if (settled) {
128
+ return;
129
+ }
130
+ const trimmedStdout = stdout.trim();
131
+ if (!trimmedStdout) {
132
+ const message = signal
133
+ ? `Execution process terminated by signal ${signal}`
134
+ : `Execution process exited with code ${exitCode ?? 0}`;
135
+ finish(buildFailureEnvelope(startedAt, 'EXECUTION_PROCESS_FAILURE', message, {
136
+ exitCode,
137
+ signal,
138
+ stderr: stderr.trim() || undefined
139
+ }));
140
+ return;
141
+ }
142
+ try {
143
+ const parsed = JSON.parse(trimmedStdout);
144
+ if (looksLikeExecutionEnvelope(parsed)) {
145
+ finish(parsed);
146
+ return;
147
+ }
148
+ finish(buildFailureEnvelope(startedAt, 'INVALID_ENVELOPE', 'Execution process returned an invalid envelope', {
149
+ outputPreview: trimmedStdout.slice(0, 2000),
150
+ stderr: stderr.trim() || undefined,
151
+ exitCode,
152
+ signal
153
+ }));
154
+ }
155
+ catch (error) {
156
+ const failure = normalizeFailure(error);
157
+ finish(buildFailureEnvelope(startedAt, 'INVALID_JSON', failure.message, {
158
+ outputPreview: trimmedStdout.slice(0, 2000),
159
+ stderr: stderr.trim() || undefined,
160
+ exitCode,
161
+ signal
162
+ }));
163
+ }
283
164
  });
284
- }
285
- finally {
286
- if (executionTimeout) {
287
- clearTimeout(executionTimeout);
165
+ try {
166
+ child.stdin.end(JSON.stringify({ code }));
288
167
  }
289
- timers.forEach((timer) => {
290
- clearTimeout(timer);
291
- clearInterval(timer);
292
- });
293
- timers.clear();
294
- }
168
+ catch (error) {
169
+ const failure = normalizeFailure(error);
170
+ finish(buildFailureEnvelope(startedAt, 'EXECUTION_PIPE_FAILURE', failure.message));
171
+ }
172
+ });
295
173
  }
@@ -4,7 +4,7 @@ function printUsage() {
4
4
  console.log(`prd - Steward CLI
5
5
 
6
6
  Usage:
7
- prd ui [--preview] [--port <port>] [--host <host>]
7
+ prd ui [--preview] [--port <port>] [--host <host>] [--allow-remote]
8
8
  prd mcp
9
9
 
10
10
  Commands:
@@ -15,6 +15,7 @@ Options:
15
15
  --preview Deprecated; ignored (kept for compatibility)
16
16
  --port <port> Port for ui mode
17
17
  --host <host> Host for ui mode
18
+ --allow-remote Allow non-loopback host binding
18
19
  -h, --help Show this help message
19
20
  `);
20
21
  }
@@ -26,7 +27,7 @@ function parsePort(value) {
26
27
  return parsed;
27
28
  }
28
29
  function parseUiArgs(args) {
29
- const options = { preview: false };
30
+ const options = { preview: false, allowRemote: false };
30
31
  for (let i = 0; i < args.length; i++) {
31
32
  const arg = args[i];
32
33
  if (arg === '--preview') {
@@ -51,6 +52,10 @@ function parseUiArgs(args) {
51
52
  i += 1;
52
53
  continue;
53
54
  }
55
+ if (arg === '--allow-remote') {
56
+ options.allowRemote = true;
57
+ continue;
58
+ }
54
59
  throw new Error(`Unknown option for ui: ${arg}`);
55
60
  }
56
61
  return options;
@@ -1,3 +1,6 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
1
4
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
5
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
6
  import { z } from 'zod';
@@ -50,11 +53,34 @@ function buildUnexpectedErrorEnvelope(error) {
50
53
  }
51
54
  };
52
55
  }
56
+ function resolvePackageVersion() {
57
+ let currentDir = dirname(fileURLToPath(import.meta.url));
58
+ while (true) {
59
+ const packageJsonPath = join(currentDir, 'package.json');
60
+ if (existsSync(packageJsonPath)) {
61
+ try {
62
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
63
+ if (typeof packageJson.version === 'string' && packageJson.version.trim().length > 0) {
64
+ return packageJson.version;
65
+ }
66
+ }
67
+ catch {
68
+ break;
69
+ }
70
+ }
71
+ const parentDir = dirname(currentDir);
72
+ if (parentDir === currentDir) {
73
+ break;
74
+ }
75
+ currentDir = parentDir;
76
+ }
77
+ return '0.0.0';
78
+ }
53
79
  export async function runMcpServer() {
54
80
  await assertSqliteRuntimeSupport();
55
81
  const server = new McpServer({
56
82
  name: 'steward',
57
- version: '0.1.0'
83
+ version: resolvePackageVersion()
58
84
  });
59
85
  registerStewardPrompts(server);
60
86
  server.tool('execute', getExecuteToolDescription(), {
@@ -2,6 +2,13 @@ import { existsSync } from 'node:fs';
2
2
  import { spawn } from 'node:child_process';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
+ const DEFAULT_UI_HOST = '127.0.0.1';
6
+ function isLoopbackHost(host) {
7
+ const normalizedHost = host.trim().toLowerCase();
8
+ return normalizedHost === '127.0.0.1'
9
+ || normalizedHost === 'localhost'
10
+ || normalizedHost === '::1';
11
+ }
5
12
  function findPackageRoot(startDir) {
6
13
  let currentDir = startDir;
7
14
  while (true) {
@@ -26,15 +33,23 @@ export async function runUi(options) {
26
33
  }
27
34
  const args = [serverEntrypoint];
28
35
  const env = { ...process.env };
36
+ const hostFromEnv = env.NITRO_HOST || env.HOST;
37
+ const requestedHost = (options.host || hostFromEnv || DEFAULT_UI_HOST).trim();
38
+ const allowRemote = options.allowRemote || env.STEWARD_ALLOW_REMOTE === '1';
39
+ if (!isLoopbackHost(requestedHost) && !allowRemote) {
40
+ throw new Error(`Refusing to bind UI to non-loopback host "${requestedHost}" without explicit opt-in. `
41
+ + 'Use --allow-remote or set STEWARD_ALLOW_REMOTE=1.');
42
+ }
29
43
  env.NODE_ENV = env.NODE_ENV || 'production';
30
44
  if (options.port !== undefined) {
31
45
  const port = String(options.port);
32
46
  env.PORT = port;
33
47
  env.NITRO_PORT = port;
34
48
  }
35
- if (options.host) {
36
- env.HOST = options.host;
37
- env.NITRO_HOST = options.host;
49
+ env.HOST = requestedHost;
50
+ env.NITRO_HOST = requestedHost;
51
+ if (allowRemote) {
52
+ env.STEWARD_ALLOW_REMOTE = '1';
38
53
  }
39
54
  const child = spawn(process.execPath, args, {
40
55
  cwd: packageRoot,
@@ -0,0 +1,33 @@
1
+ const listeners = new Set();
2
+ const pendingEvents = [];
3
+ let debounceTimer = null;
4
+ const DEBOUNCE_MS = 300;
5
+ function flushPendingEvents() {
6
+ const dedupedEvents = new Map();
7
+ for (const event of pendingEvents) {
8
+ dedupedEvents.set(`${event.repoId}:${event.category}:${event.path}`, event);
9
+ }
10
+ pendingEvents.length = 0;
11
+ debounceTimer = null;
12
+ for (const event of dedupedEvents.values()) {
13
+ for (const listener of listeners) {
14
+ listener(event);
15
+ }
16
+ }
17
+ }
18
+ export function emitChange(event) {
19
+ pendingEvents.push(event);
20
+ if (debounceTimer) {
21
+ clearTimeout(debounceTimer);
22
+ }
23
+ debounceTimer = setTimeout(flushPendingEvents, DEBOUNCE_MS);
24
+ }
25
+ export function addChangeListener(listener) {
26
+ listeners.add(listener);
27
+ return () => {
28
+ listeners.delete(listener);
29
+ };
30
+ }
31
+ export function getChangeListenerCount() {
32
+ return listeners.size;
33
+ }
@@ -208,15 +208,6 @@ export async function getCommitDiff(repoPath, sha) {
208
208
  if (!/^[0-9a-f]{4,40}$/i.test(sha)) {
209
209
  throw new Error(`Invalid commit SHA: ${sha}`);
210
210
  }
211
- // Get file status and stats
212
- const output = await execGit(repoPath, [
213
- 'show', sha,
214
- '--format=',
215
- '--name-status',
216
- '--numstat',
217
- ]);
218
- // Parse the output - first part is numstat, then name-status
219
- const lines = output.trim().split('\n').filter(l => l.trim());
220
211
  // We need to get both numstat and name-status info
221
212
  const numstatOutput = await execGit(repoPath, ['show', sha, '--format=', '--numstat']);
222
213
  const nameStatusOutput = await execGit(repoPath, ['show', sha, '--format=', '--name-status']);
@@ -344,12 +335,13 @@ function parseDiffHunks(diffOutput) {
344
335
  newLineNum = newStart;
345
336
  continue;
346
337
  }
347
- // Skip diff headers
348
- if (line.startsWith('diff --git') ||
349
- line.startsWith('index ') ||
350
- line.startsWith('---') ||
351
- line.startsWith('+++') ||
352
- line.startsWith('\\')) {
338
+ const isDiffHeader = line.startsWith('diff --git')
339
+ || line.startsWith('index ')
340
+ || /^--- (?:a\/.*|\/dev\/null)$/.test(line)
341
+ || /^\+\+\+ (?:b\/.*|\/dev\/null)$/.test(line)
342
+ || line === '\';
343
+ // Skip diff metadata lines
344
+ if (isDiffHeader) {
353
345
  continue;
354
346
  }
355
347
  // Parse diff lines
@@ -370,7 +362,7 @@ function parseDiffHunks(diffOutput) {
370
362
  };
371
363
  currentHunk.lines.push(diffLine);
372
364
  }
373
- else if (line.startsWith(' ') || line === '') {
365
+ else if (line.startsWith(' ')) {
374
366
  const diffLine = {
375
367
  type: 'context',
376
368
  content: line.substring(1),
@@ -386,6 +378,9 @@ function parseDiffHunks(diffOutput) {
386
378
  }
387
379
  return hunks;
388
380
  }
381
+ export function parseDiffHunksForTest(diffOutput) {
382
+ return parseDiffHunks(diffOutput);
383
+ }
389
384
  /**
390
385
  * Check if a file is binary by attempting to get its diff
391
386
  */