@astrale-os/cli 0.4.0-alpha.13

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 (219) hide show
  1. package/.check-workspace.cjs +40 -0
  2. package/README.md +151 -0
  3. package/dist/astrale.js +59749 -0
  4. package/package.json +90 -0
  5. package/src/command.ts +40 -0
  6. package/src/commands/__tests__/admin-instance.test.ts +73 -0
  7. package/src/commands/__tests__/auth-login.test.ts +178 -0
  8. package/src/commands/__tests__/auth-token.test.ts +223 -0
  9. package/src/commands/__tests__/call.test.ts +72 -0
  10. package/src/commands/__tests__/domain-list.test.ts +74 -0
  11. package/src/commands/__tests__/help-contract.test.ts +136 -0
  12. package/src/commands/__tests__/install-identity-override.test.ts +65 -0
  13. package/src/commands/__tests__/instance-bookmark.test.ts +101 -0
  14. package/src/commands/__tests__/instance-create-hosts.test.ts +29 -0
  15. package/src/commands/__tests__/instance-list-rows.test.ts +63 -0
  16. package/src/commands/__tests__/logs.test.ts +117 -0
  17. package/src/commands/__tests__/ls.test.ts +25 -0
  18. package/src/commands/__tests__/setup-plan.test.ts +61 -0
  19. package/src/commands/admin/status.ts +61 -0
  20. package/src/commands/admin/use.ts +77 -0
  21. package/src/commands/auth/login.ts +82 -0
  22. package/src/commands/auth/logout.ts +50 -0
  23. package/src/commands/auth/status.ts +86 -0
  24. package/src/commands/auth/token.ts +162 -0
  25. package/src/commands/browser.ts +207 -0
  26. package/src/commands/call.ts +300 -0
  27. package/src/commands/describe.ts +182 -0
  28. package/src/commands/domain/install.ts +420 -0
  29. package/src/commands/domain/list.ts +154 -0
  30. package/src/commands/domain/publish.ts +155 -0
  31. package/src/commands/get.ts +60 -0
  32. package/src/commands/identity/create.ts +26 -0
  33. package/src/commands/identity/delete.ts +18 -0
  34. package/src/commands/identity/export.ts +66 -0
  35. package/src/commands/identity/import.ts +101 -0
  36. package/src/commands/identity/list.ts +56 -0
  37. package/src/commands/identity/register.ts +170 -0
  38. package/src/commands/identity/sync.ts +32 -0
  39. package/src/commands/identity/unsync.ts +24 -0
  40. package/src/commands/identity/use.ts +18 -0
  41. package/src/commands/identity/whoami.ts +34 -0
  42. package/src/commands/idp/add.ts +150 -0
  43. package/src/commands/idp/list.ts +57 -0
  44. package/src/commands/idp/refresh.ts +38 -0
  45. package/src/commands/idp/remove.ts +36 -0
  46. package/src/commands/idp/show.ts +29 -0
  47. package/src/commands/instance/active.ts +64 -0
  48. package/src/commands/instance/bookmark.ts +72 -0
  49. package/src/commands/instance/create.ts +69 -0
  50. package/src/commands/instance/delete.ts +72 -0
  51. package/src/commands/instance/forget.ts +26 -0
  52. package/src/commands/instance/list.ts +149 -0
  53. package/src/commands/instance/status.ts +42 -0
  54. package/src/commands/instance/use.ts +210 -0
  55. package/src/commands/logs.ts +347 -0
  56. package/src/commands/ls.ts +229 -0
  57. package/src/commands/query.ts +32 -0
  58. package/src/commands/setup.ts +54 -0
  59. package/src/commands/status.ts +60 -0
  60. package/src/commands/studio.ts +401 -0
  61. package/src/commands/token.ts +77 -0
  62. package/src/commands/update.ts +267 -0
  63. package/src/commands/use.ts +87 -0
  64. package/src/errors.ts +65 -0
  65. package/src/kernel/__tests__/auth.test.ts +77 -0
  66. package/src/kernel/__tests__/errors.test.ts +43 -0
  67. package/src/kernel/__tests__/remote-routing.test.ts +70 -0
  68. package/src/kernel/auth.ts +234 -0
  69. package/src/kernel/ca-fetch.ts +119 -0
  70. package/src/kernel/client.ts +191 -0
  71. package/src/kernel/errors.ts +280 -0
  72. package/src/kernel/expand.ts +217 -0
  73. package/src/kernel/index.ts +14 -0
  74. package/src/kernel/options.ts +22 -0
  75. package/src/kernel/remote-routing.ts +88 -0
  76. package/src/kernel/run.ts +63 -0
  77. package/src/kernel/types.ts +14 -0
  78. package/src/lib/__tests__/admin-target.test.ts +112 -0
  79. package/src/lib/__tests__/binary.test.ts +56 -0
  80. package/src/lib/__tests__/command-dx.test.ts +58 -0
  81. package/src/lib/__tests__/concurrency.test.ts +62 -0
  82. package/src/lib/__tests__/config.test.ts +53 -0
  83. package/src/lib/__tests__/design.test.ts +99 -0
  84. package/src/lib/__tests__/domain-identity.test.ts +60 -0
  85. package/src/lib/__tests__/format.test.ts +22 -0
  86. package/src/lib/__tests__/fs-atomic.test.ts +104 -0
  87. package/src/lib/__tests__/identity.test.ts +79 -0
  88. package/src/lib/__tests__/idp-session.driver.ts +53 -0
  89. package/src/lib/__tests__/idp-session.test.ts +357 -0
  90. package/src/lib/__tests__/idp.test.ts +385 -0
  91. package/src/lib/__tests__/instance-candidates.test.ts +73 -0
  92. package/src/lib/__tests__/instance-target.test.ts +183 -0
  93. package/src/lib/__tests__/instance.test.ts +136 -0
  94. package/src/lib/__tests__/keys.test.ts +129 -0
  95. package/src/lib/__tests__/local-status.test.ts +202 -0
  96. package/src/lib/__tests__/output.test.ts +150 -0
  97. package/src/lib/__tests__/panel.test.ts +40 -0
  98. package/src/lib/__tests__/port.test.ts +44 -0
  99. package/src/lib/__tests__/prompt.test.ts +25 -0
  100. package/src/lib/__tests__/sdk-deps.test.ts +68 -0
  101. package/src/lib/__tests__/self.test.ts +272 -0
  102. package/src/lib/__tests__/studio-server-deps.test.ts +74 -0
  103. package/src/lib/__tests__/table.test.ts +53 -0
  104. package/src/lib/__tests__/update.test.ts +246 -0
  105. package/src/lib/__tests__/use-target.test.ts +56 -0
  106. package/src/lib/__tests__/validation.test.ts +34 -0
  107. package/src/lib/admin-domain.ts +25 -0
  108. package/src/lib/admin-instance.ts +26 -0
  109. package/src/lib/admin-target.ts +217 -0
  110. package/src/lib/binary.ts +131 -0
  111. package/src/lib/browser.ts +150 -0
  112. package/src/lib/command-dx.ts +161 -0
  113. package/src/lib/concurrency.ts +31 -0
  114. package/src/lib/config.ts +45 -0
  115. package/src/lib/domain-identity.ts +49 -0
  116. package/src/lib/env.ts +49 -0
  117. package/src/lib/format.ts +4 -0
  118. package/src/lib/fs-atomic.ts +126 -0
  119. package/src/lib/identity.ts +256 -0
  120. package/src/lib/idp-session.ts +134 -0
  121. package/src/lib/idp.ts +876 -0
  122. package/src/lib/instance-candidates.ts +49 -0
  123. package/src/lib/instance-target.ts +182 -0
  124. package/src/lib/instance.ts +395 -0
  125. package/src/lib/keys.ts +294 -0
  126. package/src/lib/local-status.ts +152 -0
  127. package/src/lib/log.ts +116 -0
  128. package/src/lib/login-flow.ts +164 -0
  129. package/src/lib/meta.ts +86 -0
  130. package/src/lib/output.ts +222 -0
  131. package/src/lib/panel.ts +61 -0
  132. package/src/lib/paths.ts +11 -0
  133. package/src/lib/port.ts +41 -0
  134. package/src/lib/proc.ts +82 -0
  135. package/src/lib/prompt.ts +136 -0
  136. package/src/lib/provision-instance.ts +170 -0
  137. package/src/lib/sdk-deps.ts +104 -0
  138. package/src/lib/self.ts +166 -0
  139. package/src/lib/skills.ts +171 -0
  140. package/src/lib/table.ts +62 -0
  141. package/src/lib/update.ts +315 -0
  142. package/src/lib/use-target.ts +24 -0
  143. package/src/lib/validation.ts +59 -0
  144. package/src/program.ts +200 -0
  145. package/src/registry.ts +59 -0
  146. package/src/setup/__tests__/util.test.ts +29 -0
  147. package/src/setup/engine.ts +83 -0
  148. package/src/setup/render.ts +109 -0
  149. package/src/setup/steps/admin.ts +78 -0
  150. package/src/setup/steps/agent-browser.ts +81 -0
  151. package/src/setup/steps/auth.ts +54 -0
  152. package/src/setup/steps/domain.ts +68 -0
  153. package/src/setup/steps/index.ts +18 -0
  154. package/src/setup/steps/instance.ts +119 -0
  155. package/src/setup/steps/skills-bridge.ts +70 -0
  156. package/src/setup/steps/skills.ts +59 -0
  157. package/src/setup/types.ts +61 -0
  158. package/src/setup/util.ts +34 -0
  159. package/src/test-utils.ts +18 -0
  160. package/studio/client/dist/assets/index-DOwzZAEK.css +1 -0
  161. package/studio/client/dist/assets/index-wtU0Zxhy.js +183 -0
  162. package/studio/client/dist/index.html +13 -0
  163. package/studio/package.json +62 -0
  164. package/studio/server/agent/ask.ts +68 -0
  165. package/studio/server/agent/bridge-mcp.ts +182 -0
  166. package/studio/server/agent/bridge.ts +188 -0
  167. package/studio/server/agent/claude.ts +666 -0
  168. package/studio/server/agent/mock.ts +186 -0
  169. package/studio/server/agent/prompt.ts +202 -0
  170. package/studio/server/agent/registry.ts +29 -0
  171. package/studio/server/agent/runner.ts +484 -0
  172. package/studio/server/agent/schema-map.ts +112 -0
  173. package/studio/server/agent/types.ts +120 -0
  174. package/studio/server/api.ts +574 -0
  175. package/studio/server/cache.ts +138 -0
  176. package/studio/server/detect.ts +81 -0
  177. package/studio/server/domain.ts +70 -0
  178. package/studio/server/index.ts +136 -0
  179. package/studio/server/introspect/anatomy-extras.ts +398 -0
  180. package/studio/server/introspect/anatomy.ts +108 -0
  181. package/studio/server/introspect/bundle.ts +57 -0
  182. package/studio/server/introspect/core-extractor.ts +119 -0
  183. package/studio/server/introspect/core.ts +44 -0
  184. package/studio/server/introspect/diff.ts +133 -0
  185. package/studio/server/introspect/extractor.ts +102 -0
  186. package/studio/server/introspect/hash.ts +21 -0
  187. package/studio/server/introspect/overlay-tsmorph.ts +874 -0
  188. package/studio/server/introspect/overlay.ts +57 -0
  189. package/studio/server/introspect/runtime.ts +99 -0
  190. package/studio/server/introspect/schema-refs.ts +46 -0
  191. package/studio/server/lifecycle.ts +38 -0
  192. package/studio/server/sse.ts +57 -0
  193. package/studio/server/state/baseline.ts +211 -0
  194. package/studio/server/state/catalog.ts +117 -0
  195. package/studio/server/state/comments.ts +321 -0
  196. package/studio/server/state/context.ts +167 -0
  197. package/studio/server/state/copy.ts +156 -0
  198. package/studio/server/state/create.ts +156 -0
  199. package/studio/server/state/documents.ts +70 -0
  200. package/studio/server/state/env.ts +161 -0
  201. package/studio/server/state/git.ts +75 -0
  202. package/studio/server/state/handoff.ts +55 -0
  203. package/studio/server/state/harness-gateway.ts +181 -0
  204. package/studio/server/state/harness-token.ts +0 -0
  205. package/studio/server/state/instance.ts +244 -0
  206. package/studio/server/state/integrations.ts +55 -0
  207. package/studio/server/state/layout.ts +55 -0
  208. package/studio/server/state/settings.ts +39 -0
  209. package/studio/server/state/store.ts +97 -0
  210. package/studio/server/state/updates.ts +63 -0
  211. package/studio/server/state/usage.ts +37 -0
  212. package/studio/server/state/views.ts +138 -0
  213. package/studio/server/state/visibility.ts +33 -0
  214. package/studio/server/watch.ts +81 -0
  215. package/studio/server/workspace-state.ts +26 -0
  216. package/studio/server/workspace-watch.ts +101 -0
  217. package/studio/shared/types.ts +873 -0
  218. package/studio/tsconfig.json +23 -0
  219. package/tsconfig.json +14 -0
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en" class="dark">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Domain Studio</title>
7
+ <script type="module" crossorigin src="/assets/index-wtU0Zxhy.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-DOwzZAEK.css">
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ </body>
13
+ </html>
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@astrale-os/studio",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "Astrale Domain Studio — local GUI for authoring/inspecting a domain (launched by `astrale studio`)",
6
+ "bin": {
7
+ "astrale-studio": "./server/index.ts"
8
+ },
9
+ "files": [
10
+ "server",
11
+ "shared",
12
+ "client/dist"
13
+ ],
14
+ "type": "module",
15
+ "scripts": {
16
+ "build": "vite build",
17
+ "start": "bun server/index.ts",
18
+ "dev:client": "vite",
19
+ "typecheck": "tsgo --noEmit"
20
+ },
21
+ "dependencies": {
22
+ "@astrale-os/shell": ">=0.1.1 <1.0.0",
23
+ "@dagrejs/dagre": "^3.0.0",
24
+ "@radix-ui/react-collapsible": "^1.1.0",
25
+ "@radix-ui/react-dialog": "^1.1.6",
26
+ "@radix-ui/react-dropdown-menu": "^2.1.6",
27
+ "@radix-ui/react-hover-card": "^1.1.0",
28
+ "@radix-ui/react-popover": "^1.1.0",
29
+ "@radix-ui/react-scroll-area": "^1.2.3",
30
+ "@radix-ui/react-separator": "^1.1.2",
31
+ "@radix-ui/react-slot": "^1.1.2",
32
+ "@radix-ui/react-tabs": "^1.1.3",
33
+ "@radix-ui/react-tooltip": "^1.1.8",
34
+ "@tanstack/react-query": "^5.66.0",
35
+ "@xyflow/react": "^12.4.4",
36
+ "chokidar": "^4.0.3",
37
+ "class-variance-authority": "^0.7.1",
38
+ "clsx": "^2.1.1",
39
+ "cmdk": "^1.1.1",
40
+ "elkjs": "^0.11.1",
41
+ "lucide-react": "^0.475.0",
42
+ "react": "^19.2.0",
43
+ "react-dom": "^19.2.0",
44
+ "react-markdown": "^10.1.0",
45
+ "remark-gfm": "^4.0.1",
46
+ "sonner": "^2.0.1",
47
+ "tailwind-merge": "^3.0.1",
48
+ "ts-morph": "^25.0.1",
49
+ "zustand": "^5.0.3"
50
+ },
51
+ "devDependencies": {
52
+ "@tailwindcss/vite": "^4.0.6",
53
+ "@types/node": "^20.19.43",
54
+ "@types/react": "^19.2.0",
55
+ "@types/react-dom": "^19.2.0",
56
+ "@vitejs/plugin-react": "^5.0.0",
57
+ "bun-types": "^1.3.14",
58
+ "tailwindcss": "^4.0.6",
59
+ "typescript": "^5.7.3",
60
+ "vite": "^7.0.0"
61
+ }
62
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * agent/ask.ts — the "side question" channel. A quick, EPHEMERAL aside about one
3
+ * element: it FORKS the domain's live conversation (so it inherits the full context
4
+ * the main loop built up) but writes nothing back to the parent transcript — the
5
+ * main thread, its session, and comments.json are all untouched. Streams the
6
+ * answer; nothing is persisted.
7
+ *
8
+ * It deliberately bypasses the single-run-per-domain lock (the fork is isolated
9
+ * from the parent conversation) so you can ask while a main agent turn is running.
10
+ */
11
+ import type { AnchorRef } from '../../shared/types'
12
+ import type { DomainHandle } from '../domain'
13
+ import type { AskResult } from './types'
14
+
15
+ import { getBundle } from '../cache'
16
+ import { resolveHarnessEnv } from '../state/harness-gateway'
17
+ import { readSettings } from '../state/settings'
18
+ import { buildAskPrompt, buildAskSystemPrompt } from './prompt'
19
+ import { getHarness } from './registry'
20
+ import { forkableSession } from './runner'
21
+
22
+ export interface AskRequest {
23
+ anchor?: AnchorRef
24
+ excerpt?: string
25
+ question: string
26
+ }
27
+
28
+ export async function runAsk(
29
+ handle: DomainHandle,
30
+ body: AskRequest,
31
+ signal: AbortSignal,
32
+ onDelta: (text: string) => void,
33
+ ): Promise<AskResult> {
34
+ const harness = getHarness()
35
+ if (!harness.ask)
36
+ return {
37
+ text: '',
38
+ isError: true,
39
+ errorMessage: `${harness.label} does not support side questions`,
40
+ }
41
+ const question = String(body?.question ?? '').trim()
42
+ if (!question) return { text: '', isError: true, errorMessage: 'question is required' }
43
+
44
+ const ref = body.anchor?.ref ?? '(no target)'
45
+ const bundle = await getBundle(handle.id)
46
+ const prompt = buildAskPrompt({
47
+ anchorRef: ref,
48
+ excerpt: body.excerpt || ref,
49
+ question,
50
+ ir: bundle?.ir ?? null,
51
+ overlay: bundle?.overlay,
52
+ })
53
+ const settings = readSettings(handle.root)
54
+ const envResult = await resolveHarnessEnv(handle.root)
55
+ if (!envResult.ok)
56
+ return { text: '', isError: true, errorMessage: `model gateway auth failed — ${envResult.error}` }
57
+
58
+ return harness.ask({
59
+ root: handle.root,
60
+ prompt,
61
+ appendSystemPrompt: buildAskSystemPrompt(),
62
+ sessionId: forkableSession(handle.id), // fork the live conversation (undefined ⇒ fresh)
63
+ effort: settings.agentEffort,
64
+ env: envResult.env,
65
+ signal,
66
+ onDelta,
67
+ })
68
+ }
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * bridge-mcp.ts — a minimal stdio MCP server the harness spawns (via the
4
+ * generated --mcp-config). It exposes the Domain Studio write-back tools and
5
+ * forwards each call to the token-guarded HTTP bridge (api → agent/bridge/*),
6
+ * so an agent can reply to comment threads and post progress AS IT WORKS.
7
+ *
8
+ * Transport: newline-delimited JSON-RPC 2.0 on stdin/stdout. Nothing but
9
+ * protocol messages may touch stdout; diagnostics go to stderr.
10
+ */
11
+ const BASE = process.env.DOMAIN_STUDIO_BRIDGE_URL || ''
12
+ const TOKEN = process.env.DOMAIN_STUDIO_BRIDGE_TOKEN || ''
13
+
14
+ const TOOLS = [
15
+ {
16
+ name: 'list_open_threads',
17
+ description:
18
+ 'List the open comment threads that are awaiting your reply (id, anchor, file, latest message). Call this first to see what to address.',
19
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
20
+ route: 'threads',
21
+ },
22
+ {
23
+ name: 'reply_to_thread',
24
+ description:
25
+ 'Post a reply to one comment thread so the user sees it live. Say what you changed, answer a question, or ask one back. Pass `options` (a short list of concrete choices) to turn the reply into a multiple-choice question the user can pick from (or answer freely). Set resolve=true with a short closeNote ONLY when fully handled — never resolve a question you just asked.',
26
+ inputSchema: {
27
+ type: 'object',
28
+ properties: {
29
+ commentId: { type: 'string', description: 'the thread id from list_open_threads' },
30
+ text: {
31
+ type: 'string',
32
+ description: 'your reply (concise — a framing line when offering options)',
33
+ },
34
+ options: {
35
+ type: 'array',
36
+ items: { type: 'string' },
37
+ description: '2–5 short choices for the user to pick from; they may also answer freely',
38
+ },
39
+ resolve: { type: 'boolean', description: 'close the thread when done (not when asking)' },
40
+ closeNote: { type: 'string', description: 'one-line summary when resolving' },
41
+ },
42
+ required: ['commentId', 'text'],
43
+ additionalProperties: false,
44
+ },
45
+ route: 'reply',
46
+ },
47
+ {
48
+ name: 'resolve_thread',
49
+ description: 'Mark a comment thread resolved (closed) with an optional closeNote.',
50
+ inputSchema: {
51
+ type: 'object',
52
+ properties: { commentId: { type: 'string' }, closeNote: { type: 'string' } },
53
+ required: ['commentId'],
54
+ additionalProperties: false,
55
+ },
56
+ route: 'resolve',
57
+ },
58
+ {
59
+ name: 'post_progress',
60
+ description:
61
+ 'Post a short progress note shown in the studio activity panel (not tied to a thread).',
62
+ inputSchema: {
63
+ type: 'object',
64
+ properties: { text: { type: 'string' } },
65
+ required: ['text'],
66
+ additionalProperties: false,
67
+ },
68
+ route: 'progress',
69
+ },
70
+ {
71
+ name: 'raise_question',
72
+ description:
73
+ 'Open a NEW question thread anchored to a schema element (ref like "class.Monitor.property.url") when you need the user to decide something. Pass `options` for a multiple-choice question — the user picks one or answers freely.',
74
+ inputSchema: {
75
+ type: 'object',
76
+ properties: {
77
+ ref: {
78
+ type: 'string',
79
+ description: 'anchor ref, e.g. "class.Order" or "module.inventory/inventory"',
80
+ },
81
+ text: {
82
+ type: 'string',
83
+ description: 'the question (one framing line when offering options)',
84
+ },
85
+ file: { type: 'string' },
86
+ options: {
87
+ type: 'array',
88
+ items: { type: 'string' },
89
+ description: '2–5 short choices the user can pick from',
90
+ },
91
+ },
92
+ required: ['ref', 'text'],
93
+ additionalProperties: false,
94
+ },
95
+ route: 'raise_question',
96
+ },
97
+ ] as const
98
+
99
+ function send(msg: unknown): void {
100
+ process.stdout.write(`${JSON.stringify(msg)}\n`)
101
+ }
102
+ function reply(id: unknown, result: unknown): void {
103
+ send({ jsonrpc: '2.0', id, result })
104
+ }
105
+ function replyError(id: unknown, code: number, message: string): void {
106
+ send({ jsonrpc: '2.0', id, error: { code, message } })
107
+ }
108
+
109
+ async function callBridge(route: string, args: Record<string, unknown>): Promise<string> {
110
+ if (!BASE || !TOKEN) return 'bridge not configured'
111
+ try {
112
+ const res = await fetch(`${BASE}/${route}`, {
113
+ method: 'POST',
114
+ headers: { 'content-type': 'application/json' },
115
+ body: JSON.stringify({ token: TOKEN, ...args }),
116
+ })
117
+ const text = await res.text()
118
+ return res.ok ? text || 'ok' : `error ${res.status}: ${text}`
119
+ } catch (e: any) {
120
+ return `bridge call failed: ${e?.message ?? e}`
121
+ }
122
+ }
123
+
124
+ async function handle(req: any): Promise<void> {
125
+ const { id, method, params } = req
126
+ switch (method) {
127
+ case 'initialize':
128
+ reply(id, {
129
+ protocolVersion: params?.protocolVersion || '2025-06-18',
130
+ capabilities: { tools: {} },
131
+ serverInfo: { name: 'domain-studio', version: '0.1.0' },
132
+ })
133
+ return
134
+ case 'notifications/initialized':
135
+ case 'initialized':
136
+ return // notification, no response
137
+ case 'ping':
138
+ reply(id, {})
139
+ return
140
+ case 'tools/list':
141
+ reply(id, {
142
+ tools: TOOLS.map(({ name, description, inputSchema }) => ({
143
+ name,
144
+ description,
145
+ inputSchema,
146
+ })),
147
+ })
148
+ return
149
+ case 'tools/call': {
150
+ const tool = TOOLS.find((t) => t.name === params?.name)
151
+ if (!tool) {
152
+ replyError(id, -32602, `unknown tool: ${params?.name}`)
153
+ return
154
+ }
155
+ const out = await callBridge(tool.route, (params?.arguments ?? {}) as Record<string, unknown>)
156
+ reply(id, { content: [{ type: 'text', text: out }] })
157
+ return
158
+ }
159
+ default:
160
+ if (id !== undefined) replyError(id, -32601, `method not found: ${method}`)
161
+ }
162
+ }
163
+
164
+ let buf = ''
165
+ process.stdin.setEncoding('utf8')
166
+ process.stdin.on('data', (chunk: string) => {
167
+ buf += chunk
168
+ let nl: number
169
+ while ((nl = buf.indexOf('\n')) >= 0) {
170
+ const line = buf.slice(0, nl).trim()
171
+ buf = buf.slice(nl + 1)
172
+ if (!line) continue
173
+ let req: any
174
+ try {
175
+ req = JSON.parse(line)
176
+ } catch {
177
+ continue
178
+ }
179
+ void handle(req)
180
+ }
181
+ })
182
+ process.stdin.on('end', () => process.exit(0))
@@ -0,0 +1,188 @@
1
+ /**
2
+ * agent/bridge.ts — the per-run write-back channel. Lets the agent reply to
3
+ * threads LIVE (mid-turn) instead of only at end-of-turn. Two halves:
4
+ *
5
+ * 1. startBridge() mints a scoped token + writes an MCP config the harness
6
+ * loads (`--mcp-config`). The MCP server (bridge-mcp.ts) is a thin stdio
7
+ * proxy: each tool call POSTs to the token-guarded routes below.
8
+ * 2. handleBridge() applies those calls to the SAME state the GUI reads
9
+ * (comments.json), so replies appear in the threads immediately over SSE.
10
+ *
11
+ * This is additive: the runner ALSO merges the agent's end-of-turn machine-state
12
+ * block, and mergeReply dedupes by text so a live reply is never duplicated.
13
+ */
14
+ import { randomUUID } from 'node:crypto'
15
+ import { join } from 'node:path'
16
+
17
+ import type { StudioEvent } from '../../shared/types'
18
+ import type { DomainHandle } from '../domain'
19
+
20
+ import { addThreadEntry, readComments, setStatus, upsertComment } from '../state/comments'
21
+ import { removeState, statePath, writeJson } from '../state/store'
22
+
23
+ export interface Bridge {
24
+ enabled: boolean
25
+ mcpConfigPath?: string
26
+ onReply(cb: (commentId: string, text: string) => void): void
27
+ onProgress(cb: (text: string) => void): void
28
+ dispose(): void
29
+ }
30
+
31
+ interface RunBridge {
32
+ domainId: string
33
+ root: string
34
+ onReply: (commentId: string, text: string) => void
35
+ onProgress: (text: string) => void
36
+ }
37
+
38
+ const bridges = new Map<string, RunBridge>() // token → bridge
39
+
40
+ let studioPort = Number(process.env.PORT) || 4319
41
+ /** index.ts calls this once the server is listening so the MCP proxy knows where to POST. */
42
+ export function setBridgePort(port: number): void {
43
+ studioPort = port
44
+ }
45
+
46
+ const MCP_SERVER = join(import.meta.dir, 'bridge-mcp.ts')
47
+ // the studio runs under bun, so process.execPath is the absolute bun binary —
48
+ // robust when `bun` is only a shell function on the agent's PATH.
49
+ const BIN = process.env.DOMAIN_STUDIO_BRIDGE_BUN || process.execPath
50
+
51
+ export function startBridge(
52
+ handle: DomainHandle,
53
+ _getRunId: () => string,
54
+ _notify: (e: StudioEvent) => void,
55
+ ): Bridge {
56
+ const token = randomUUID()
57
+ const holder: RunBridge = {
58
+ domainId: handle.id,
59
+ root: handle.root,
60
+ onReply: () => {},
61
+ onProgress: () => {},
62
+ }
63
+ bridges.set(token, holder)
64
+
65
+ const base = `http://127.0.0.1:${studioPort}/api/domain/${encodeURIComponent(handle.id)}/agent/bridge`
66
+ const mcpRel = `.cache/agent/mcp-${token}.json`
67
+ writeJson(handle.root, mcpRel, {
68
+ mcpServers: {
69
+ 'domain-studio': {
70
+ command: BIN,
71
+ args: [MCP_SERVER],
72
+ env: { DOMAIN_STUDIO_BRIDGE_URL: base, DOMAIN_STUDIO_BRIDGE_TOKEN: token },
73
+ },
74
+ },
75
+ })
76
+ const mcpConfigPath = statePath(handle.root, mcpRel)
77
+
78
+ return {
79
+ enabled: true,
80
+ mcpConfigPath,
81
+ onReply: (cb) => {
82
+ holder.onReply = cb
83
+ },
84
+ onProgress: (cb) => {
85
+ holder.onProgress = cb
86
+ },
87
+ dispose: () => {
88
+ bridges.delete(token)
89
+ try {
90
+ removeState(handle.root, mcpRel)
91
+ } catch {
92
+ /* best-effort */
93
+ }
94
+ },
95
+ }
96
+ }
97
+
98
+ function ok(data: unknown): Response {
99
+ return new Response(JSON.stringify(data), {
100
+ status: 200,
101
+ headers: { 'content-type': 'application/json' },
102
+ })
103
+ }
104
+ function err(message: string, status = 400): Response {
105
+ return new Response(JSON.stringify({ error: message }), {
106
+ status,
107
+ headers: { 'content-type': 'application/json' },
108
+ })
109
+ }
110
+
111
+ /** Routes under /api/domain/:id/agent/bridge/<sub>. Token in the body, scoped to the run. */
112
+ export async function handleBridge(
113
+ handle: DomainHandle,
114
+ sub: string,
115
+ _req: Request,
116
+ body: any,
117
+ notify: (e: StudioEvent) => void,
118
+ ): Promise<Response> {
119
+ const token = String(body?.token ?? '')
120
+ const bridge = bridges.get(token)
121
+ if (!bridge || bridge.domainId !== handle.id) return err('invalid or expired bridge token', 401)
122
+ const root = handle.root
123
+
124
+ switch (sub) {
125
+ case 'threads': {
126
+ const open = readComments(root).comments.filter(
127
+ (c) => c.status === 'open' && c.thread.at(-1)?.role !== 'author',
128
+ )
129
+ return ok({
130
+ threads: open.map((c) => ({
131
+ id: c.id,
132
+ kind: c.kind,
133
+ anchor: c.anchorRefs?.[0]?.ref ?? null,
134
+ file: c.anchorRefs?.[0]?.file ?? null,
135
+ latest: c.thread.at(-1)?.text ?? '',
136
+ })),
137
+ })
138
+ }
139
+ case 'reply': {
140
+ const commentId = String(body.commentId ?? '')
141
+ const text = String(body.text ?? '').trim()
142
+ if (!commentId || !text) return err('commentId and text are required')
143
+ const c = addThreadEntry(root, commentId, {
144
+ role: 'author',
145
+ type: body.options ? 'choice' : 'text',
146
+ text,
147
+ options: body.options,
148
+ answer: body.answer,
149
+ })
150
+ if (!c) return err('unknown commentId', 404)
151
+ if (body.resolve) setStatus(root, commentId, 'closed', body.closeNote)
152
+ bridge.onReply(commentId, text)
153
+ notify({ type: 'comments', domainId: handle.id })
154
+ return ok({ ok: true, resolved: !!body.resolve })
155
+ }
156
+ case 'resolve': {
157
+ const commentId = String(body.commentId ?? '')
158
+ if (!commentId) return err('commentId is required')
159
+ const c = setStatus(root, commentId, 'closed', body.closeNote)
160
+ if (!c) return err('unknown commentId', 404)
161
+ bridge.onReply(commentId, body.closeNote ? `resolved: ${body.closeNote}` : 'resolved')
162
+ notify({ type: 'comments', domainId: handle.id })
163
+ return ok({ ok: true })
164
+ }
165
+ case 'progress': {
166
+ const text = String(body.text ?? '').trim()
167
+ if (text) bridge.onProgress(text)
168
+ return ok({ ok: true })
169
+ }
170
+ case 'raise_question': {
171
+ const ref = String(body.ref ?? '')
172
+ const text = String(body.text ?? '').trim()
173
+ if (!ref || !text) return err('ref and text are required')
174
+ const c = upsertComment(root, {
175
+ anchors: [body.label ?? ref],
176
+ anchorRefs: [{ ref, kind: (body.kind ?? 'schema') as any, file: body.file }],
177
+ text,
178
+ firstRole: 'author',
179
+ type: body.options ? 'choice' : 'text',
180
+ options: body.options,
181
+ })
182
+ notify({ type: 'comments', domainId: handle.id })
183
+ return ok({ ok: true, id: c.id })
184
+ }
185
+ default:
186
+ return err('unknown bridge route', 404)
187
+ }
188
+ }