@ottocode/server 0.1.265 → 0.1.267

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 (74) hide show
  1. package/package.json +3 -3
  2. package/src/routes/auth/copilot.ts +699 -0
  3. package/src/routes/auth/oauth.ts +578 -0
  4. package/src/routes/auth/onboarding.ts +45 -0
  5. package/src/routes/auth/providers.ts +189 -0
  6. package/src/routes/auth/service.ts +167 -0
  7. package/src/routes/auth/state.ts +23 -0
  8. package/src/routes/auth/status.ts +203 -0
  9. package/src/routes/auth/wallet.ts +229 -0
  10. package/src/routes/auth.ts +12 -2080
  11. package/src/routes/config/models-service.ts +411 -0
  12. package/src/routes/config/models.ts +6 -426
  13. package/src/routes/config/providers-service.ts +237 -0
  14. package/src/routes/config/providers.ts +10 -242
  15. package/src/routes/files/handlers.ts +297 -0
  16. package/src/routes/files/service.ts +313 -0
  17. package/src/routes/files.ts +12 -608
  18. package/src/routes/git/commit-service.ts +207 -0
  19. package/src/routes/git/commit.ts +6 -220
  20. package/src/routes/git/remote-service.ts +116 -0
  21. package/src/routes/git/remote.ts +8 -115
  22. package/src/routes/git/staging-service.ts +111 -0
  23. package/src/routes/git/staging.ts +10 -205
  24. package/src/routes/mcp/auth.ts +338 -0
  25. package/src/routes/mcp/lifecycle.ts +263 -0
  26. package/src/routes/mcp/servers.ts +212 -0
  27. package/src/routes/mcp/service.ts +664 -0
  28. package/src/routes/mcp/state.ts +13 -0
  29. package/src/routes/mcp.ts +6 -1233
  30. package/src/routes/ottorouter/billing.ts +593 -0
  31. package/src/routes/ottorouter/service.ts +92 -0
  32. package/src/routes/ottorouter/topup.ts +301 -0
  33. package/src/routes/ottorouter/wallet.ts +370 -0
  34. package/src/routes/ottorouter.ts +6 -1319
  35. package/src/routes/research/service.ts +339 -0
  36. package/src/routes/research.ts +12 -390
  37. package/src/routes/sessions/crud.ts +563 -0
  38. package/src/routes/sessions/queue.ts +242 -0
  39. package/src/routes/sessions/retry.ts +121 -0
  40. package/src/routes/sessions/service.ts +768 -0
  41. package/src/routes/sessions/share.ts +434 -0
  42. package/src/routes/sessions.ts +8 -1977
  43. package/src/routes/skills/service.ts +221 -0
  44. package/src/routes/skills/spec.ts +309 -0
  45. package/src/routes/skills.ts +31 -909
  46. package/src/routes/terminals/service.ts +326 -0
  47. package/src/routes/terminals.ts +19 -295
  48. package/src/routes/tunnel/service.ts +217 -0
  49. package/src/routes/tunnel.ts +29 -219
  50. package/src/runtime/agent/registry-prompts.ts +147 -0
  51. package/src/runtime/agent/registry.ts +6 -124
  52. package/src/runtime/agent/runner-errors.ts +116 -0
  53. package/src/runtime/agent/runner-reminders.ts +45 -0
  54. package/src/runtime/agent/runner-setup-model.ts +75 -0
  55. package/src/runtime/agent/runner-setup-prompt.ts +185 -0
  56. package/src/runtime/agent/runner-setup-tools.ts +103 -0
  57. package/src/runtime/agent/runner-setup-utils.ts +21 -0
  58. package/src/runtime/agent/runner-setup.ts +54 -288
  59. package/src/runtime/agent/runner-telemetry.ts +112 -0
  60. package/src/runtime/agent/runner-text.ts +108 -0
  61. package/src/runtime/agent/runner-tool-observer.ts +86 -0
  62. package/src/runtime/agent/runner.ts +79 -378
  63. package/src/runtime/prompt/builder.ts +5 -1
  64. package/src/runtime/prompt/capabilities.ts +13 -8
  65. package/src/runtime/provider/custom.ts +73 -0
  66. package/src/runtime/provider/index.ts +2 -85
  67. package/src/runtime/provider/reasoning-builders.ts +280 -0
  68. package/src/runtime/provider/reasoning.ts +67 -264
  69. package/src/tools/adapter/events.ts +116 -0
  70. package/src/tools/adapter/execution.ts +160 -0
  71. package/src/tools/adapter/pending.ts +37 -0
  72. package/src/tools/adapter/persistence.ts +166 -0
  73. package/src/tools/adapter/results.ts +97 -0
  74. package/src/tools/adapter.ts +124 -451
@@ -0,0 +1,217 @@
1
+ import type { Context } from 'hono';
2
+ import { streamSSE } from 'hono/streaming';
3
+ import {
4
+ generateQRCode,
5
+ isTunnelBinaryInstalled,
6
+ killStaleTunnels,
7
+ logger,
8
+ OttoTunnel,
9
+ } from '@ottocode/sdk';
10
+ import { getServerPort } from '../../state.ts';
11
+
12
+ type TunnelStatus = 'idle' | 'starting' | 'connected' | 'error';
13
+
14
+ let activeTunnel: OttoTunnel | null = null;
15
+ let tunnelUrl: string | null = null;
16
+ let tunnelStatus: TunnelStatus = 'idle';
17
+ let tunnelError: string | null = null;
18
+ let progressMessage: string | null = null;
19
+
20
+ export async function getTunnelStatus() {
21
+ const binaryInstalled = await isTunnelBinaryInstalled();
22
+
23
+ return {
24
+ status: tunnelStatus,
25
+ url: tunnelUrl,
26
+ error: tunnelError,
27
+ binaryInstalled,
28
+ isRunning: activeTunnel?.isRunning ?? false,
29
+ };
30
+ }
31
+
32
+ export async function startTunnel(requestedPort?: number) {
33
+ if (activeTunnel?.isRunning) {
34
+ return {
35
+ ok: true,
36
+ url: tunnelUrl,
37
+ message: 'Tunnel already running',
38
+ };
39
+ }
40
+
41
+ try {
42
+ const port = requestedPort || getServerPort() || 9100;
43
+
44
+ await killStaleTunnels();
45
+
46
+ tunnelStatus = 'starting';
47
+ tunnelError = null;
48
+ progressMessage = 'Initializing...';
49
+
50
+ activeTunnel = new OttoTunnel();
51
+
52
+ const url = await activeTunnel.start(port, (msg) => {
53
+ progressMessage = msg;
54
+ });
55
+
56
+ tunnelUrl = url;
57
+ tunnelStatus = 'connected';
58
+ progressMessage = null;
59
+
60
+ activeTunnel.on('error', (err) => {
61
+ logger.error('Tunnel error:', err);
62
+ tunnelError = err.message;
63
+ tunnelStatus = 'error';
64
+ });
65
+
66
+ activeTunnel.on('exit', () => {
67
+ tunnelStatus = 'idle';
68
+ tunnelUrl = null;
69
+ activeTunnel = null;
70
+ });
71
+
72
+ return {
73
+ ok: true,
74
+ url: tunnelUrl,
75
+ message: 'Tunnel started',
76
+ };
77
+ } catch (error) {
78
+ const message = error instanceof Error ? error.message : String(error);
79
+ tunnelStatus = 'error';
80
+ tunnelError = message;
81
+ progressMessage = null;
82
+
83
+ logger.error('Failed to start tunnel:', error);
84
+ return { ok: false, error: message };
85
+ }
86
+ }
87
+
88
+ export function registerExternalTunnel(url?: string) {
89
+ if (!url) {
90
+ return { ok: false, error: 'URL is required' };
91
+ }
92
+
93
+ tunnelUrl = url;
94
+ tunnelStatus = 'connected';
95
+ tunnelError = null;
96
+ progressMessage = null;
97
+
98
+ return {
99
+ ok: true,
100
+ url: tunnelUrl,
101
+ message: 'External tunnel registered',
102
+ };
103
+ }
104
+
105
+ export function stopTunnel() {
106
+ if (!activeTunnel) {
107
+ return { ok: true, message: 'No tunnel running' };
108
+ }
109
+
110
+ try {
111
+ activeTunnel.stop();
112
+ activeTunnel = null;
113
+ tunnelUrl = null;
114
+ tunnelStatus = 'idle';
115
+ tunnelError = null;
116
+
117
+ return { ok: true, message: 'Tunnel stopped' };
118
+ } catch (error) {
119
+ const message = error instanceof Error ? error.message : String(error);
120
+ return { ok: false, error: message };
121
+ }
122
+ }
123
+
124
+ export async function getTunnelQRCode() {
125
+ if (!tunnelUrl) {
126
+ return { ok: false, error: 'No tunnel URL available' };
127
+ }
128
+
129
+ try {
130
+ const qrCode = await generateQRCode(tunnelUrl);
131
+ return {
132
+ ok: true,
133
+ url: tunnelUrl,
134
+ qrCode,
135
+ };
136
+ } catch (error) {
137
+ const message = error instanceof Error ? error.message : String(error);
138
+ return { ok: false, error: message };
139
+ }
140
+ }
141
+
142
+ export async function handleTunnelStream(c: Context) {
143
+ return streamSSE(c as Context, async (stream) => {
144
+ const sendEvent = async (data: Record<string, unknown>) => {
145
+ try {
146
+ await stream.write(`data: ${JSON.stringify(data)}\n\n`);
147
+ } catch (error) {
148
+ logger.error('SSE error writing event', error);
149
+ }
150
+ };
151
+
152
+ await sendEvent({
153
+ type: 'status',
154
+ status: tunnelStatus,
155
+ url: tunnelUrl,
156
+ error: tunnelError,
157
+ progress: progressMessage,
158
+ });
159
+
160
+ const interval = setInterval(async () => {
161
+ await sendEvent({
162
+ type: 'status',
163
+ status: tunnelStatus,
164
+ url: tunnelUrl,
165
+ error: tunnelError,
166
+ progress: progressMessage,
167
+ });
168
+ }, 1000);
169
+
170
+ const onAbort = () => {
171
+ clearInterval(interval);
172
+ stream.close();
173
+ };
174
+
175
+ c.req.raw.signal.addEventListener('abort', onAbort, { once: true });
176
+
177
+ await new Promise<void>((resolve) => {
178
+ c.req.raw.signal.addEventListener('abort', () => resolve(), {
179
+ once: true,
180
+ });
181
+ });
182
+
183
+ clearInterval(interval);
184
+ });
185
+ }
186
+
187
+ export function stopActiveTunnel() {
188
+ if (activeTunnel) {
189
+ activeTunnel.stop();
190
+ activeTunnel = null;
191
+ tunnelUrl = null;
192
+ tunnelStatus = 'idle';
193
+ }
194
+ }
195
+
196
+ export function setExternalTunnel(tunnel: OttoTunnel, url: string) {
197
+ activeTunnel = tunnel;
198
+ tunnelUrl = url;
199
+ tunnelStatus = 'connected';
200
+ tunnelError = null;
201
+ progressMessage = null;
202
+
203
+ tunnel.on('error', (err) => {
204
+ tunnelError = err.message;
205
+ tunnelStatus = 'error';
206
+ });
207
+
208
+ tunnel.on('exit', () => {
209
+ tunnelStatus = 'idle';
210
+ tunnelUrl = null;
211
+ activeTunnel = null;
212
+ });
213
+ }
214
+
215
+ export function getActiveTunnelUrl(): string | null {
216
+ return tunnelUrl;
217
+ }
@@ -1,21 +1,18 @@
1
1
  import type { Hono } from 'hono';
2
- import type { Context } from 'hono';
3
- import { streamSSE } from 'hono/streaming';
4
- import {
5
- OttoTunnel,
6
- isTunnelBinaryInstalled,
7
- generateQRCode,
8
- killStaleTunnels,
9
- logger,
10
- } from '@ottocode/sdk';
11
- import { getServerPort } from '../state.ts';
12
2
  import { openApiRoute } from '../openapi/route.ts';
13
-
14
- let activeTunnel: OttoTunnel | null = null;
15
- let tunnelUrl: string | null = null;
16
- let tunnelStatus: 'idle' | 'starting' | 'connected' | 'error' = 'idle';
17
- let tunnelError: string | null = null;
18
- let progressMessage: string | null = null;
3
+ import {
4
+ getActiveTunnelUrl,
5
+ getTunnelQRCode,
6
+ getTunnelStatus,
7
+ handleTunnelStream,
8
+ registerExternalTunnel,
9
+ setExternalTunnel,
10
+ startTunnel,
11
+ stopActiveTunnel,
12
+ stopTunnel,
13
+ } from './tunnel/service.ts';
14
+
15
+ export { getActiveTunnelUrl, setExternalTunnel, stopActiveTunnel };
19
16
 
20
17
  export function registerTunnelRoutes(app: Hono) {
21
18
  openApiRoute(
@@ -60,17 +57,7 @@ export function registerTunnelRoutes(app: Hono) {
60
57
  },
61
58
  },
62
59
  },
63
- async (c) => {
64
- const binaryInstalled = await isTunnelBinaryInstalled();
65
-
66
- return c.json({
67
- status: tunnelStatus,
68
- url: tunnelUrl,
69
- error: tunnelError,
70
- binaryInstalled,
71
- isRunning: activeTunnel?.isRunning ?? false,
72
- });
73
- },
60
+ async (c) => c.json(await getTunnelStatus()),
74
61
  );
75
62
 
76
63
  openApiRoute(
@@ -125,66 +112,11 @@ export function registerTunnelRoutes(app: Hono) {
125
112
  },
126
113
  },
127
114
  async (c) => {
128
- if (activeTunnel?.isRunning) {
129
- return c.json({
130
- ok: true,
131
- url: tunnelUrl,
132
- message: 'Tunnel already running',
133
- });
134
- }
135
-
136
- try {
137
- const body = await c.req.json().catch(() => ({}));
138
- let port = body.port;
139
-
140
- // Use server's known port if not explicitly provided
141
- if (!port) {
142
- port = getServerPort() || 9100;
143
- }
144
-
145
- // Kill any stale tunnel processes first
146
- await killStaleTunnels();
147
-
148
- tunnelStatus = 'starting';
149
- tunnelError = null;
150
- progressMessage = 'Initializing...';
151
-
152
- activeTunnel = new OttoTunnel();
153
-
154
- const url = await activeTunnel.start(port, (msg) => {
155
- progressMessage = msg;
156
- });
157
-
158
- tunnelUrl = url;
159
- tunnelStatus = 'connected';
160
- progressMessage = null;
161
-
162
- activeTunnel.on('error', (err) => {
163
- logger.error('Tunnel error:', err);
164
- tunnelError = err.message;
165
- tunnelStatus = 'error';
166
- });
167
-
168
- activeTunnel.on('exit', () => {
169
- tunnelStatus = 'idle';
170
- tunnelUrl = null;
171
- activeTunnel = null;
172
- });
173
-
174
- return c.json({
175
- ok: true,
176
- url: tunnelUrl,
177
- message: 'Tunnel started',
178
- });
179
- } catch (error) {
180
- const message = error instanceof Error ? error.message : String(error);
181
- tunnelStatus = 'error';
182
- tunnelError = message;
183
- progressMessage = null;
184
-
185
- logger.error('Failed to start tunnel:', error);
186
- return c.json({ ok: false, error: message }, 500);
187
- }
115
+ const body: { port?: number } = await c.req
116
+ .json<{ port?: number }>()
117
+ .catch(() => ({}));
118
+ const result = await startTunnel(body.port);
119
+ return c.json(result, result.ok ? 200 : 500);
188
120
  },
189
121
  );
190
122
 
@@ -254,29 +186,11 @@ export function registerTunnelRoutes(app: Hono) {
254
186
  },
255
187
  },
256
188
  async (c) => {
257
- try {
258
- const body = await c.req.json().catch(() => ({}));
259
- const { url } = body;
260
-
261
- if (!url) {
262
- return c.json({ ok: false, error: 'URL is required' }, 400);
263
- }
264
-
265
- tunnelUrl = url;
266
- tunnelStatus = 'connected';
267
- tunnelError = null;
268
- progressMessage = null;
269
-
270
- return c.json({
271
- ok: true,
272
- url: tunnelUrl,
273
- message: 'External tunnel registered',
274
- });
275
- } catch (error) {
276
- const message = error instanceof Error ? error.message : String(error);
277
- logger.error('Failed to register external tunnel:', error);
278
- return c.json({ ok: false, error: message }, 500);
279
- }
189
+ const body: { url?: string } = await c.req
190
+ .json<{ url?: string }>()
191
+ .catch(() => ({}));
192
+ const result = registerExternalTunnel(body.url);
193
+ return c.json(result, result.ok ? 200 : 400);
280
194
  },
281
195
  );
282
196
 
@@ -310,23 +224,9 @@ export function registerTunnelRoutes(app: Hono) {
310
224
  },
311
225
  },
312
226
  },
313
- async (c) => {
314
- if (!activeTunnel) {
315
- return c.json({ ok: true, message: 'No tunnel running' });
316
- }
317
-
318
- try {
319
- activeTunnel.stop();
320
- activeTunnel = null;
321
- tunnelUrl = null;
322
- tunnelStatus = 'idle';
323
- tunnelError = null;
324
-
325
- return c.json({ ok: true, message: 'Tunnel stopped' });
326
- } catch (error) {
327
- const message = error instanceof Error ? error.message : String(error);
328
- return c.json({ ok: false, error: message }, 500);
329
- }
227
+ (c) => {
228
+ const result = stopTunnel();
229
+ return c.json(result, result.ok ? 200 : 500);
330
230
  },
331
231
  );
332
232
 
@@ -380,69 +280,11 @@ export function registerTunnelRoutes(app: Hono) {
380
280
  },
381
281
  },
382
282
  async (c) => {
383
- if (!tunnelUrl) {
384
- return c.json({ ok: false, error: 'No tunnel URL available' }, 400);
385
- }
386
-
387
- try {
388
- const qrCode = await generateQRCode(tunnelUrl);
389
- return c.json({
390
- ok: true,
391
- url: tunnelUrl,
392
- qrCode,
393
- });
394
- } catch (error) {
395
- const message = error instanceof Error ? error.message : String(error);
396
- return c.json({ ok: false, error: message }, 500);
397
- }
283
+ const result = await getTunnelQRCode();
284
+ return c.json(result, result.ok ? 200 : 400);
398
285
  },
399
286
  );
400
287
 
401
- const handleTunnelStream = async (c: Context) => {
402
- return streamSSE(c as Context, async (stream) => {
403
- const sendEvent = async (data: Record<string, unknown>) => {
404
- try {
405
- await stream.write(`data: ${JSON.stringify(data)}\n\n`);
406
- } catch (error) {
407
- logger.error('SSE error writing event', error);
408
- }
409
- };
410
-
411
- await sendEvent({
412
- type: 'status',
413
- status: tunnelStatus,
414
- url: tunnelUrl,
415
- error: tunnelError,
416
- progress: progressMessage,
417
- });
418
-
419
- const interval = setInterval(async () => {
420
- await sendEvent({
421
- type: 'status',
422
- status: tunnelStatus,
423
- url: tunnelUrl,
424
- error: tunnelError,
425
- progress: progressMessage,
426
- });
427
- }, 1000);
428
-
429
- const onAbort = () => {
430
- clearInterval(interval);
431
- stream.close();
432
- };
433
-
434
- c.req.raw.signal.addEventListener('abort', onAbort, { once: true });
435
-
436
- await new Promise<void>((resolve) => {
437
- c.req.raw.signal.addEventListener('abort', () => resolve(), {
438
- once: true,
439
- });
440
- });
441
-
442
- clearInterval(interval);
443
- });
444
- };
445
-
446
288
  const tunnelStreamRoute = {
447
289
  tags: ['tunnel'],
448
290
  summary: 'Subscribe to tunnel status stream',
@@ -479,35 +321,3 @@ export function registerTunnelRoutes(app: Hono) {
479
321
  handleTunnelStream,
480
322
  );
481
323
  }
482
-
483
- export function stopActiveTunnel() {
484
- if (activeTunnel) {
485
- activeTunnel.stop();
486
- activeTunnel = null;
487
- tunnelUrl = null;
488
- tunnelStatus = 'idle';
489
- }
490
- }
491
-
492
- export function setExternalTunnel(tunnel: OttoTunnel, url: string) {
493
- activeTunnel = tunnel;
494
- tunnelUrl = url;
495
- tunnelStatus = 'connected';
496
- tunnelError = null;
497
- progressMessage = null;
498
-
499
- tunnel.on('error', (err) => {
500
- tunnelError = err.message;
501
- tunnelStatus = 'error';
502
- });
503
-
504
- tunnel.on('exit', () => {
505
- tunnelStatus = 'idle';
506
- tunnelUrl = null;
507
- activeTunnel = null;
508
- });
509
- }
510
-
511
- export function getActiveTunnelUrl(): string | null {
512
- return tunnelUrl;
513
- }
@@ -0,0 +1,147 @@
1
+ import { getGlobalAgentsDir } from '@ottocode/sdk';
2
+ // Embed default agent prompts; only user overrides read from disk.
3
+ // eslint-disable-next-line @typescript-eslint/consistent-type-imports
4
+ import AGENT_BUILD from '@ottocode/sdk/prompts/agents/build.txt' with {
5
+ type: 'text',
6
+ };
7
+ // eslint-disable-next-line @typescript-eslint/consistent-type-imports
8
+ import AGENT_PLAN from '@ottocode/sdk/prompts/agents/plan.txt' with {
9
+ type: 'text',
10
+ };
11
+ // eslint-disable-next-line @typescript-eslint/consistent-type-imports
12
+ import AGENT_GENERAL from '@ottocode/sdk/prompts/agents/general.txt' with {
13
+ type: 'text',
14
+ };
15
+ // eslint-disable-next-line @typescript-eslint/consistent-type-imports
16
+ import AGENT_INIT from '@ottocode/sdk/prompts/agents/init.txt' with {
17
+ type: 'text',
18
+ };
19
+ import AGENT_RESEARCH from '@ottocode/sdk/prompts/agents/research.txt' with {
20
+ type: 'text',
21
+ };
22
+
23
+ type PromptResolution = {
24
+ prompt: string;
25
+ source: string;
26
+ };
27
+
28
+ const EMBEDDED_AGENT_PROMPTS: Record<string, string> = {
29
+ build: AGENT_BUILD,
30
+ plan: AGENT_PLAN,
31
+ general: AGENT_GENERAL,
32
+ init: AGENT_INIT,
33
+ research: AGENT_RESEARCH,
34
+ };
35
+
36
+ function normalizePath(path: string): string {
37
+ return path.replace(/\\/g, '/');
38
+ }
39
+
40
+ export function getAgentPromptCandidates(
41
+ projectRoot: string,
42
+ name: string,
43
+ ): string[] {
44
+ const globalAgentsDir = getGlobalAgentsDir();
45
+ return [
46
+ normalizePath(`${projectRoot}/.otto/agents/${name}/agent.md`),
47
+ normalizePath(`${projectRoot}/.otto/agents/${name}.md`),
48
+ normalizePath(`${projectRoot}/.otto/agents/${name}/agent.txt`),
49
+ normalizePath(`${projectRoot}/.otto/agents/${name}.txt`),
50
+ normalizePath(`${globalAgentsDir}/${name}/agent.md`),
51
+ normalizePath(`${globalAgentsDir}/${name}.md`),
52
+ normalizePath(`${globalAgentsDir}/${name}/agent.txt`),
53
+ normalizePath(`${globalAgentsDir}/${name}.txt`),
54
+ ];
55
+ }
56
+
57
+ async function readFirstNonEmptyFile(
58
+ candidates: string[],
59
+ sourcePrefix: string,
60
+ ): Promise<PromptResolution | undefined> {
61
+ for (const candidate of candidates) {
62
+ try {
63
+ const file = Bun.file(candidate);
64
+ if (!(await file.exists())) continue;
65
+ const text = await file.text();
66
+ if (!text.trim()) continue;
67
+ return {
68
+ prompt: text,
69
+ source: `${sourcePrefix}:${candidate}`,
70
+ };
71
+ } catch {}
72
+ }
73
+ return undefined;
74
+ }
75
+
76
+ function isPromptPathReference(value: string): boolean {
77
+ return (
78
+ /[.](md|txt)$/i.test(value) ||
79
+ value.startsWith('.') ||
80
+ value.startsWith('/') ||
81
+ value.startsWith('~/')
82
+ );
83
+ }
84
+
85
+ function getPromptReferenceCandidates(
86
+ projectRoot: string,
87
+ reference: string,
88
+ ): string[] {
89
+ if (reference.startsWith('~/')) {
90
+ const home = process.env.HOME || process.env.USERPROFILE || '';
91
+ return [normalizePath(`${home}/${reference.slice(2)}`)];
92
+ }
93
+ if (reference.startsWith('/')) return [normalizePath(reference)];
94
+ return [normalizePath(`${projectRoot}/${reference}`)];
95
+ }
96
+
97
+ async function resolveAgentsJsonPrompt(
98
+ projectRoot: string,
99
+ promptValue: string | undefined,
100
+ ): Promise<PromptResolution | undefined> {
101
+ const prompt = promptValue?.trim();
102
+ if (!prompt) return undefined;
103
+
104
+ if (!isPromptPathReference(prompt)) {
105
+ return {
106
+ prompt,
107
+ source: 'agents.json:inline',
108
+ };
109
+ }
110
+
111
+ return readFirstNonEmptyFile(
112
+ getPromptReferenceCandidates(projectRoot, prompt),
113
+ 'agents.json:file',
114
+ );
115
+ }
116
+
117
+ function resolveEmbeddedPrompt(name: string): PromptResolution {
118
+ const prompt = EMBEDDED_AGENT_PROMPTS[name]?.trim();
119
+ if (prompt) {
120
+ return {
121
+ prompt,
122
+ source: `fallback:embedded:${name}.txt`,
123
+ };
124
+ }
125
+
126
+ return {
127
+ prompt: (AGENT_BUILD || '').trim(),
128
+ source: 'fallback:embedded:build.txt',
129
+ };
130
+ }
131
+
132
+ export async function resolveAgentPrompt(args: {
133
+ projectRoot: string;
134
+ name: string;
135
+ entryPrompt?: string;
136
+ }): Promise<PromptResolution> {
137
+ const filePrompt = await readFirstNonEmptyFile(
138
+ getAgentPromptCandidates(args.projectRoot, args.name),
139
+ 'file',
140
+ );
141
+ const agentsJsonPrompt = await resolveAgentsJsonPrompt(
142
+ args.projectRoot,
143
+ args.entryPrompt,
144
+ );
145
+
146
+ return agentsJsonPrompt ?? filePrompt ?? resolveEmbeddedPrompt(args.name);
147
+ }