@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.
- package/.check-workspace.cjs +40 -0
- package/README.md +151 -0
- package/dist/astrale.js +59749 -0
- package/package.json +90 -0
- package/src/command.ts +40 -0
- package/src/commands/__tests__/admin-instance.test.ts +73 -0
- package/src/commands/__tests__/auth-login.test.ts +178 -0
- package/src/commands/__tests__/auth-token.test.ts +223 -0
- package/src/commands/__tests__/call.test.ts +72 -0
- package/src/commands/__tests__/domain-list.test.ts +74 -0
- package/src/commands/__tests__/help-contract.test.ts +136 -0
- package/src/commands/__tests__/install-identity-override.test.ts +65 -0
- package/src/commands/__tests__/instance-bookmark.test.ts +101 -0
- package/src/commands/__tests__/instance-create-hosts.test.ts +29 -0
- package/src/commands/__tests__/instance-list-rows.test.ts +63 -0
- package/src/commands/__tests__/logs.test.ts +117 -0
- package/src/commands/__tests__/ls.test.ts +25 -0
- package/src/commands/__tests__/setup-plan.test.ts +61 -0
- package/src/commands/admin/status.ts +61 -0
- package/src/commands/admin/use.ts +77 -0
- package/src/commands/auth/login.ts +82 -0
- package/src/commands/auth/logout.ts +50 -0
- package/src/commands/auth/status.ts +86 -0
- package/src/commands/auth/token.ts +162 -0
- package/src/commands/browser.ts +207 -0
- package/src/commands/call.ts +300 -0
- package/src/commands/describe.ts +182 -0
- package/src/commands/domain/install.ts +420 -0
- package/src/commands/domain/list.ts +154 -0
- package/src/commands/domain/publish.ts +155 -0
- package/src/commands/get.ts +60 -0
- package/src/commands/identity/create.ts +26 -0
- package/src/commands/identity/delete.ts +18 -0
- package/src/commands/identity/export.ts +66 -0
- package/src/commands/identity/import.ts +101 -0
- package/src/commands/identity/list.ts +56 -0
- package/src/commands/identity/register.ts +170 -0
- package/src/commands/identity/sync.ts +32 -0
- package/src/commands/identity/unsync.ts +24 -0
- package/src/commands/identity/use.ts +18 -0
- package/src/commands/identity/whoami.ts +34 -0
- package/src/commands/idp/add.ts +150 -0
- package/src/commands/idp/list.ts +57 -0
- package/src/commands/idp/refresh.ts +38 -0
- package/src/commands/idp/remove.ts +36 -0
- package/src/commands/idp/show.ts +29 -0
- package/src/commands/instance/active.ts +64 -0
- package/src/commands/instance/bookmark.ts +72 -0
- package/src/commands/instance/create.ts +69 -0
- package/src/commands/instance/delete.ts +72 -0
- package/src/commands/instance/forget.ts +26 -0
- package/src/commands/instance/list.ts +149 -0
- package/src/commands/instance/status.ts +42 -0
- package/src/commands/instance/use.ts +210 -0
- package/src/commands/logs.ts +347 -0
- package/src/commands/ls.ts +229 -0
- package/src/commands/query.ts +32 -0
- package/src/commands/setup.ts +54 -0
- package/src/commands/status.ts +60 -0
- package/src/commands/studio.ts +401 -0
- package/src/commands/token.ts +77 -0
- package/src/commands/update.ts +267 -0
- package/src/commands/use.ts +87 -0
- package/src/errors.ts +65 -0
- package/src/kernel/__tests__/auth.test.ts +77 -0
- package/src/kernel/__tests__/errors.test.ts +43 -0
- package/src/kernel/__tests__/remote-routing.test.ts +70 -0
- package/src/kernel/auth.ts +234 -0
- package/src/kernel/ca-fetch.ts +119 -0
- package/src/kernel/client.ts +191 -0
- package/src/kernel/errors.ts +280 -0
- package/src/kernel/expand.ts +217 -0
- package/src/kernel/index.ts +14 -0
- package/src/kernel/options.ts +22 -0
- package/src/kernel/remote-routing.ts +88 -0
- package/src/kernel/run.ts +63 -0
- package/src/kernel/types.ts +14 -0
- package/src/lib/__tests__/admin-target.test.ts +112 -0
- package/src/lib/__tests__/binary.test.ts +56 -0
- package/src/lib/__tests__/command-dx.test.ts +58 -0
- package/src/lib/__tests__/concurrency.test.ts +62 -0
- package/src/lib/__tests__/config.test.ts +53 -0
- package/src/lib/__tests__/design.test.ts +99 -0
- package/src/lib/__tests__/domain-identity.test.ts +60 -0
- package/src/lib/__tests__/format.test.ts +22 -0
- package/src/lib/__tests__/fs-atomic.test.ts +104 -0
- package/src/lib/__tests__/identity.test.ts +79 -0
- package/src/lib/__tests__/idp-session.driver.ts +53 -0
- package/src/lib/__tests__/idp-session.test.ts +357 -0
- package/src/lib/__tests__/idp.test.ts +385 -0
- package/src/lib/__tests__/instance-candidates.test.ts +73 -0
- package/src/lib/__tests__/instance-target.test.ts +183 -0
- package/src/lib/__tests__/instance.test.ts +136 -0
- package/src/lib/__tests__/keys.test.ts +129 -0
- package/src/lib/__tests__/local-status.test.ts +202 -0
- package/src/lib/__tests__/output.test.ts +150 -0
- package/src/lib/__tests__/panel.test.ts +40 -0
- package/src/lib/__tests__/port.test.ts +44 -0
- package/src/lib/__tests__/prompt.test.ts +25 -0
- package/src/lib/__tests__/sdk-deps.test.ts +68 -0
- package/src/lib/__tests__/self.test.ts +272 -0
- package/src/lib/__tests__/studio-server-deps.test.ts +74 -0
- package/src/lib/__tests__/table.test.ts +53 -0
- package/src/lib/__tests__/update.test.ts +246 -0
- package/src/lib/__tests__/use-target.test.ts +56 -0
- package/src/lib/__tests__/validation.test.ts +34 -0
- package/src/lib/admin-domain.ts +25 -0
- package/src/lib/admin-instance.ts +26 -0
- package/src/lib/admin-target.ts +217 -0
- package/src/lib/binary.ts +131 -0
- package/src/lib/browser.ts +150 -0
- package/src/lib/command-dx.ts +161 -0
- package/src/lib/concurrency.ts +31 -0
- package/src/lib/config.ts +45 -0
- package/src/lib/domain-identity.ts +49 -0
- package/src/lib/env.ts +49 -0
- package/src/lib/format.ts +4 -0
- package/src/lib/fs-atomic.ts +126 -0
- package/src/lib/identity.ts +256 -0
- package/src/lib/idp-session.ts +134 -0
- package/src/lib/idp.ts +876 -0
- package/src/lib/instance-candidates.ts +49 -0
- package/src/lib/instance-target.ts +182 -0
- package/src/lib/instance.ts +395 -0
- package/src/lib/keys.ts +294 -0
- package/src/lib/local-status.ts +152 -0
- package/src/lib/log.ts +116 -0
- package/src/lib/login-flow.ts +164 -0
- package/src/lib/meta.ts +86 -0
- package/src/lib/output.ts +222 -0
- package/src/lib/panel.ts +61 -0
- package/src/lib/paths.ts +11 -0
- package/src/lib/port.ts +41 -0
- package/src/lib/proc.ts +82 -0
- package/src/lib/prompt.ts +136 -0
- package/src/lib/provision-instance.ts +170 -0
- package/src/lib/sdk-deps.ts +104 -0
- package/src/lib/self.ts +166 -0
- package/src/lib/skills.ts +171 -0
- package/src/lib/table.ts +62 -0
- package/src/lib/update.ts +315 -0
- package/src/lib/use-target.ts +24 -0
- package/src/lib/validation.ts +59 -0
- package/src/program.ts +200 -0
- package/src/registry.ts +59 -0
- package/src/setup/__tests__/util.test.ts +29 -0
- package/src/setup/engine.ts +83 -0
- package/src/setup/render.ts +109 -0
- package/src/setup/steps/admin.ts +78 -0
- package/src/setup/steps/agent-browser.ts +81 -0
- package/src/setup/steps/auth.ts +54 -0
- package/src/setup/steps/domain.ts +68 -0
- package/src/setup/steps/index.ts +18 -0
- package/src/setup/steps/instance.ts +119 -0
- package/src/setup/steps/skills-bridge.ts +70 -0
- package/src/setup/steps/skills.ts +59 -0
- package/src/setup/types.ts +61 -0
- package/src/setup/util.ts +34 -0
- package/src/test-utils.ts +18 -0
- package/studio/client/dist/assets/index-DOwzZAEK.css +1 -0
- package/studio/client/dist/assets/index-wtU0Zxhy.js +183 -0
- package/studio/client/dist/index.html +13 -0
- package/studio/package.json +62 -0
- package/studio/server/agent/ask.ts +68 -0
- package/studio/server/agent/bridge-mcp.ts +182 -0
- package/studio/server/agent/bridge.ts +188 -0
- package/studio/server/agent/claude.ts +666 -0
- package/studio/server/agent/mock.ts +186 -0
- package/studio/server/agent/prompt.ts +202 -0
- package/studio/server/agent/registry.ts +29 -0
- package/studio/server/agent/runner.ts +484 -0
- package/studio/server/agent/schema-map.ts +112 -0
- package/studio/server/agent/types.ts +120 -0
- package/studio/server/api.ts +574 -0
- package/studio/server/cache.ts +138 -0
- package/studio/server/detect.ts +81 -0
- package/studio/server/domain.ts +70 -0
- package/studio/server/index.ts +136 -0
- package/studio/server/introspect/anatomy-extras.ts +398 -0
- package/studio/server/introspect/anatomy.ts +108 -0
- package/studio/server/introspect/bundle.ts +57 -0
- package/studio/server/introspect/core-extractor.ts +119 -0
- package/studio/server/introspect/core.ts +44 -0
- package/studio/server/introspect/diff.ts +133 -0
- package/studio/server/introspect/extractor.ts +102 -0
- package/studio/server/introspect/hash.ts +21 -0
- package/studio/server/introspect/overlay-tsmorph.ts +874 -0
- package/studio/server/introspect/overlay.ts +57 -0
- package/studio/server/introspect/runtime.ts +99 -0
- package/studio/server/introspect/schema-refs.ts +46 -0
- package/studio/server/lifecycle.ts +38 -0
- package/studio/server/sse.ts +57 -0
- package/studio/server/state/baseline.ts +211 -0
- package/studio/server/state/catalog.ts +117 -0
- package/studio/server/state/comments.ts +321 -0
- package/studio/server/state/context.ts +167 -0
- package/studio/server/state/copy.ts +156 -0
- package/studio/server/state/create.ts +156 -0
- package/studio/server/state/documents.ts +70 -0
- package/studio/server/state/env.ts +161 -0
- package/studio/server/state/git.ts +75 -0
- package/studio/server/state/handoff.ts +55 -0
- package/studio/server/state/harness-gateway.ts +181 -0
- package/studio/server/state/harness-token.ts +0 -0
- package/studio/server/state/instance.ts +244 -0
- package/studio/server/state/integrations.ts +55 -0
- package/studio/server/state/layout.ts +55 -0
- package/studio/server/state/settings.ts +39 -0
- package/studio/server/state/store.ts +97 -0
- package/studio/server/state/updates.ts +63 -0
- package/studio/server/state/usage.ts +37 -0
- package/studio/server/state/views.ts +138 -0
- package/studio/server/state/visibility.ts +33 -0
- package/studio/server/watch.ts +81 -0
- package/studio/server/workspace-state.ts +26 -0
- package/studio/server/workspace-watch.ts +101 -0
- package/studio/shared/types.ts +873 -0
- package/studio/tsconfig.json +23 -0
- 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
|
+
}
|