dominus-cli 2.1.1 → 2.2.0

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/CHANGELOG.md CHANGED
@@ -1,6 +1,31 @@
1
1
  # Changelog
2
2
 
3
- ## 2.1.1 (2026-07-13)
3
+ ## 2.2.0 (2026-07-13)
4
+
5
+ ### MCP-native multi-agent coordinator
6
+
7
+ - Added `run_parallel_task` to the published MCP server. It uses the MCP client's
8
+ model through standard Sampling, so Dominus still requires no AI provider key.
9
+ - Added deterministic path-based task partitioning. UI and generic roots are split
10
+ by immediate child branches and distributed across one to five workers.
11
+ - Workers run concurrently and receive bounded hierarchy plus live typed property
12
+ evidence for only their owned scopes. Workers are proposal-only and never receive
13
+ Studio mutation tools.
14
+ - Added strict worker proposal and coordinator decision schemas. Existing instance
15
+ refs must exactly match supplied ID/path pairs, and every mutation must remain
16
+ inside the worker's owned branches.
17
+ - Added ancestor/descendant conflict detection and coordinator-only operation
18
+ ordering. The coordinator may select complete validated proposals but cannot
19
+ invent, rewrite, or partially accept worker operations.
20
+ - Final writes use one atomic `studio_apply` batch. Dominus then reads the root and
21
+ affected properties back, checks creates and moves, and performs at most one
22
+ bounded repair pass before reporting verification evidence.
23
+ - Parallel planning refuses truncated hierarchy evidence, and generic property
24
+ mutations can no longer bypass revision checks by writing script `Source`.
25
+ - Added summary-level MCP logging events for worker starts/completions, proposal
26
+ decisions, application, and verification without exposing full model traces.
27
+ - Clients without Sampling receive a clear capability response and keep the normal
28
+ single-agent inspect-plan-apply-verify workflow.
4
29
 
5
30
  ### Studio connection reliability
6
31
 
@@ -16,8 +41,8 @@
16
41
  rejected by resolving names against the live Enum type's items.
17
42
  - Removed the stale binary `.rbxm` build artifact. The package and installer now
18
43
  use the single canonical `.rbxmx` plugin build.
19
- - Corrected documentation to describe the 16-tool MCP catalog and optional Studio
20
- restart behavior.
44
+ - Corrected documentation for the 17-tool MCP catalog, canonical plugin format,
45
+ and optional Studio restart behavior.
21
46
 
22
47
  ## 2.1.0 (2026-07-12)
23
48
 
@@ -101,95 +126,95 @@ Dominus has been rewritten as an MCP-first Roblox Studio engineering server.
101
126
  `instanceId` and/or `pathSegments`.
102
127
  - Script updates require `expectedRevision` from `studio_read_script`.
103
128
  - Existing UI is not replaced unless `replaceExisting: true` is supplied.
104
-
105
- ## 0.5.5 (2026-04-23)
106
-
107
- ### New MCP Tools (30 total, up from 24)
108
-
109
- - **`move_instance`** — Reparent an instance preserving all descendants. Sets ChangeHistoryService waypoint.
110
- - **`bulk_set_properties`** — Set properties on multiple instances in one call. Explicit paths or className filter mode.
111
- - **`find_replace_scripts`** — Find and replace across all scripts with optional Lua pattern matching and dry run preview.
112
- - **`undo`** — Undo last action (ChangeHistoryService). Like Ctrl+Z.
113
- - **`redo`** — Redo last undone action. Like Ctrl+Y.
114
-
115
- ### Improvements
116
-
117
- - **`get_properties` now supports compact mode** — Same filtering as `get_descendants_properties`. Strips read-only, nil, deprecated, and default-valued properties by default. The previous version returned all ~50 properties per instance even though most were noise.
118
- - **Fixed code injection in `clone_instance`, `group_instances`, `build_multiple`** — Instance names and paths with special characters (quotes, backslashes) no longer break generated Luau code.
119
- - **`build_multiple` now supports `rgb()` colors and `canCollide`** — Previously only handled `#hex` and BrickColor names; `rgb(255,0,0)` would silently fail.
120
- - **Better tool descriptions** — More specific descriptions help AI models pick the right tool. `get_properties` disambiguates vs `serialize_ui` and `get_descendants_properties`.
121
- - **Shared `luaStr()` and `resolvePath()` helpers** — Centralized safe string generation for all code-gen tools.
122
-
123
- ## 0.5.4 (2026-04-23)
124
-
125
- ### Fixes
126
-
127
- - **Rebuilt plugin** — 0.5.3 shipped a stale `Dominus.rbxm` missing the compact property filtering and protocol changes. This release includes the correctly built plugin.
128
-
129
- ## 0.5.3 (2026-04-23)
130
-
131
- ### Improvements
132
-
133
- - **Compact `get_descendants_properties`** — Now strips read-only, nil, deprecated, and default-valued properties by default, reducing response size by ~80%. Pass `compact: false` for the full dump.
134
- - **`serialize_ui` exposed as MCP tool** — Was previously only in the internal tool registry. Now available to all MCP clients (Copilot, Cursor, Claude, etc.) for converting existing Roblox UI into clean JSON trees.
135
- - **UI → Roact/React conversion guidance** — System prompt and internal memory now teach the agent the full workflow: `serialize_ui` → JSON tree → Roact/React component with correct type mappings (UDim2, Color3, Vector2, enums, FontFace).
136
- - **Smarter tool selection** — Agent now prefers `serialize_ui` over `get_descendants_properties` for UI conversion tasks, avoiding context bloat.
137
-
138
- ## 0.2.1 (2026-02-24)
139
-
140
- ### New
141
-
142
- - **Auto-update checker** — On startup, Dominus checks npm for newer versions
143
- and prints an upgrade notice. Cached for 4 hours, non-blocking, never throws.
144
-
145
- ### Bug Fixes
146
-
147
- - **MCP server crashes on EADDRINUSE** — `dominus mcp` would crash if port 18088
148
- was already held by a running `dominus start` (or another MCP instance). The MCP
149
- server now checks for an existing listener and connects as a relay client instead
150
- of trying to bind the same port.
151
-
152
- ### New MCP Tools (22 total, up from 14)
153
-
154
- - **`get_class_info`** — Look up the full schema of any Roblox class via
155
- ReflectionService (properties, methods, events with types).
156
- - **`create_ui`** — Create an entire UI tree in one call with a declarative JSON
157
- spec. Auto-coerces UDim2, Color3, Font, and enums. ~10x faster than `run_code`.
158
- - **`create_part`** — One-call Part creation with position, size, color, material,
159
- shape, transparency, anchored, and collision settings.
160
- - **`clone_instance`** — Clone an instance with optional rename, reparent, and
161
- position offset.
162
- - **`group_instances`** — Group multiple instances into a Model or Folder.
163
- - **`build_multiple`** — Batch-create many parts/instances in a single round-trip.
164
- - **`execute_run_test`** — Enter Run mode via `StudioTestService:ExecuteRunModeAsync()`
165
- with pass-through args and result capture.
166
- - **`execute_play_test`** — Enter Play mode via `StudioTestService:ExecutePlayModeAsync()`
167
- for full client-server simulation tests.
168
-
169
- ### New CLI Features
170
-
171
- - **`dominus mcp-install`** — Auto-install MCP config into Cursor, VS Code,
172
- Claude Desktop, Windsurf, and Claude Code with one command.
173
- - **`dominus mcp-setup`** — Print manual MCP configuration instructions.
174
- - **Relay client architecture** — `dominus start` can now connect as a relay
175
- client to an existing MCP-owned WebSocket server, allowing the TUI and MCP
176
- to coexist on the same port.
177
-
178
- ### Plugin Improvements
179
-
180
- - Enhanced `Properties.lua` with broader property type support.
181
- - Improved `Reflection.lua` for richer class metadata.
182
- - Updated `TestRunner.lua` with Run/Play mode test support.
183
- - New `UIBuilder.lua` module for server-side declarative UI construction.
184
-
185
- ### Other Changes
186
-
187
- - Multi-provider AI config improvements (OpenRouter, xAI, Gemini, custom).
188
- - TUI setup wizard for first-run API key and provider configuration.
189
- - Improved system prompt with builder-tool awareness.
190
- - WebSocket server now supports controller relay connections.
191
-
192
- ## 0.1.0 (2026-02-23)
193
-
194
- - Initial release — 14 MCP tools, TUI agent, persistent memory, Roblox Studio
195
- WebSocket bridge.
129
+
130
+ ## 0.5.5 (2026-04-23)
131
+
132
+ ### New MCP Tools (30 total, up from 24)
133
+
134
+ - **`move_instance`** — Reparent an instance preserving all descendants. Sets ChangeHistoryService waypoint.
135
+ - **`bulk_set_properties`** — Set properties on multiple instances in one call. Explicit paths or className filter mode.
136
+ - **`find_replace_scripts`** — Find and replace across all scripts with optional Lua pattern matching and dry run preview.
137
+ - **`undo`** — Undo last action (ChangeHistoryService). Like Ctrl+Z.
138
+ - **`redo`** — Redo last undone action. Like Ctrl+Y.
139
+
140
+ ### Improvements
141
+
142
+ - **`get_properties` now supports compact mode** — Same filtering as `get_descendants_properties`. Strips read-only, nil, deprecated, and default-valued properties by default. The previous version returned all ~50 properties per instance even though most were noise.
143
+ - **Fixed code injection in `clone_instance`, `group_instances`, `build_multiple`** — Instance names and paths with special characters (quotes, backslashes) no longer break generated Luau code.
144
+ - **`build_multiple` now supports `rgb()` colors and `canCollide`** — Previously only handled `#hex` and BrickColor names; `rgb(255,0,0)` would silently fail.
145
+ - **Better tool descriptions** — More specific descriptions help AI models pick the right tool. `get_properties` disambiguates vs `serialize_ui` and `get_descendants_properties`.
146
+ - **Shared `luaStr()` and `resolvePath()` helpers** — Centralized safe string generation for all code-gen tools.
147
+
148
+ ## 0.5.4 (2026-04-23)
149
+
150
+ ### Fixes
151
+
152
+ - **Rebuilt plugin** — 0.5.3 shipped a stale `Dominus.rbxm` missing the compact property filtering and protocol changes. This release includes the correctly built plugin.
153
+
154
+ ## 0.5.3 (2026-04-23)
155
+
156
+ ### Improvements
157
+
158
+ - **Compact `get_descendants_properties`** — Now strips read-only, nil, deprecated, and default-valued properties by default, reducing response size by ~80%. Pass `compact: false` for the full dump.
159
+ - **`serialize_ui` exposed as MCP tool** — Was previously only in the internal tool registry. Now available to all MCP clients (Copilot, Cursor, Claude, etc.) for converting existing Roblox UI into clean JSON trees.
160
+ - **UI → Roact/React conversion guidance** — System prompt and internal memory now teach the agent the full workflow: `serialize_ui` → JSON tree → Roact/React component with correct type mappings (UDim2, Color3, Vector2, enums, FontFace).
161
+ - **Smarter tool selection** — Agent now prefers `serialize_ui` over `get_descendants_properties` for UI conversion tasks, avoiding context bloat.
162
+
163
+ ## 0.2.1 (2026-02-24)
164
+
165
+ ### New
166
+
167
+ - **Auto-update checker** — On startup, Dominus checks npm for newer versions
168
+ and prints an upgrade notice. Cached for 4 hours, non-blocking, never throws.
169
+
170
+ ### Bug Fixes
171
+
172
+ - **MCP server crashes on EADDRINUSE** — `dominus mcp` would crash if port 18088
173
+ was already held by a running `dominus start` (or another MCP instance). The MCP
174
+ server now checks for an existing listener and connects as a relay client instead
175
+ of trying to bind the same port.
176
+
177
+ ### New MCP Tools (22 total, up from 14)
178
+
179
+ - **`get_class_info`** — Look up the full schema of any Roblox class via
180
+ ReflectionService (properties, methods, events with types).
181
+ - **`create_ui`** — Create an entire UI tree in one call with a declarative JSON
182
+ spec. Auto-coerces UDim2, Color3, Font, and enums. ~10x faster than `run_code`.
183
+ - **`create_part`** — One-call Part creation with position, size, color, material,
184
+ shape, transparency, anchored, and collision settings.
185
+ - **`clone_instance`** — Clone an instance with optional rename, reparent, and
186
+ position offset.
187
+ - **`group_instances`** — Group multiple instances into a Model or Folder.
188
+ - **`build_multiple`** — Batch-create many parts/instances in a single round-trip.
189
+ - **`execute_run_test`** — Enter Run mode via `StudioTestService:ExecuteRunModeAsync()`
190
+ with pass-through args and result capture.
191
+ - **`execute_play_test`** — Enter Play mode via `StudioTestService:ExecutePlayModeAsync()`
192
+ for full client-server simulation tests.
193
+
194
+ ### New CLI Features
195
+
196
+ - **`dominus mcp-install`** — Auto-install MCP config into Cursor, VS Code,
197
+ Claude Desktop, Windsurf, and Claude Code with one command.
198
+ - **`dominus mcp-setup`** — Print manual MCP configuration instructions.
199
+ - **Relay client architecture** — `dominus start` can now connect as a relay
200
+ client to an existing MCP-owned WebSocket server, allowing the TUI and MCP
201
+ to coexist on the same port.
202
+
203
+ ### Plugin Improvements
204
+
205
+ - Enhanced `Properties.lua` with broader property type support.
206
+ - Improved `Reflection.lua` for richer class metadata.
207
+ - Updated `TestRunner.lua` with Run/Play mode test support.
208
+ - New `UIBuilder.lua` module for server-side declarative UI construction.
209
+
210
+ ### Other Changes
211
+
212
+ - Multi-provider AI config improvements (OpenRouter, xAI, Gemini, custom).
213
+ - TUI setup wizard for first-run API key and provider configuration.
214
+ - Improved system prompt with builder-tool awareness.
215
+ - WebSocket server now supports controller relay connections.
216
+
217
+ ## 0.1.0 (2026-02-23)
218
+
219
+ - Initial release — 14 MCP tools, TUI agent, persistent memory, Roblox Studio
220
+ WebSocket bridge.
package/README.md CHANGED
@@ -8,7 +8,9 @@ Luau execution.
8
8
  ## What changed
9
9
 
10
10
  - MCP-only product: the old Ink chat CLI is no longer published.
11
- - 16 focused tools with input/output schemas, structured results, and risk hints.
11
+ - 17 focused tools with input/output schemas, structured results, and risk hints.
12
+ - MCP-native parallel coordination uses the host's model through Sampling, with
13
+ proposal-only workers and coordinator-owned Studio writes.
12
14
  - Stable instance refs survive dots in names, renames, reparenting, and duplicate
13
15
  sibling names for the life of the Studio session.
14
16
  - Script writes require a revision from `studio_read_script`.
@@ -63,7 +65,7 @@ For a version-pinned configuration that does not require a global install:
63
65
  "mcpServers": {
64
66
  "dominus": {
65
67
  "command": "npx",
66
- "args": ["-y", "-p", "dominus-cli@2.1.1", "dominus-mcp"]
68
+ "args": ["-y", "-p", "dominus-cli@2.2.0", "dominus-mcp"]
67
69
  }
68
70
  }
69
71
  }
@@ -94,6 +96,13 @@ For UI fidelity work, use `studio_snapshot_ui` after `studio_build_ui` and compa
94
96
  fonts, UDim2 values, colors, strokes, and hierarchy rather than relying on a
95
97
  successful write response alone.
96
98
 
99
+ For broad tasks with independent branches, use `run_parallel_task`. Dominus
100
+ partitions immediate child scopes, supplies each worker with hierarchy and typed
101
+ property evidence, runs up to five Sampling workers concurrently, validates their
102
+ proposals, and applies only the coordinator-approved atomic plan. Workers never
103
+ receive direct write access. Clients without Sampling continue to use the normal
104
+ single-agent workflow.
105
+
97
106
  ## Tool catalog
98
107
 
99
108
  Connection tools:
@@ -101,6 +110,10 @@ Connection tools:
101
110
  - `dominus_status`
102
111
  - `dominus_select_studio`
103
112
 
113
+ Parallel coordination:
114
+
115
+ - `run_parallel_task`
116
+
104
117
  Studio inspection:
105
118
 
106
119
  - `studio_get_tree`
@@ -70,6 +70,8 @@ for (const obsoleteName of ["Dominus2.rbxm", "Dominus.rbxm", "Dominus.rbxmx"]) {
70
70
  if (obsoletePath !== destination) fs3.rmSync(obsoletePath, { force: true });
71
71
  }
72
72
  console.log(`Installed Dominus 2 Studio plugin to ${destination}`);
73
- console.log("Restart Roblox Studio. Dominus 2 will authenticate automatically when dominus-mcp starts.");
73
+ console.log(
74
+ "Roblox Studio normally reloads Dominus 2 automatically. Restart Studio only if the toolbar does not reconnect."
75
+ );
74
76
  //# sourceMappingURL=install-plugin.js.map
75
77
  //# sourceMappingURL=install-plugin.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/config/config.ts","../src/transport/auth.ts","../src/install-plugin.ts"],"names":["path","os","fs"],"mappings":"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","file":"install-plugin.js","sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport os from 'node:os';\nimport type { ConfigAccess } from '../tools/types.js';\n\nconst DOMINUS_DIR = path.join(os.homedir(), '.dominus');\nconst CONFIG_PATH = path.join(DOMINUS_DIR, 'config.json');\nconst DB_PATH = path.join(DOMINUS_DIR, 'dominus.db');\nconst RULES_PATH = path.join(DOMINUS_DIR, 'rules.json');\n\nexport type AIProviderType =\n | 'openai'\n | 'anthropic'\n | 'gemini'\n | 'xai'\n | 'openrouter'\n | 'custom';\n\nexport interface ProviderConfig {\n name: string;\n baseUrl: string;\n envVar: string;\n defaultModel: string;\n}\n\nexport const PROVIDERS: Record<AIProviderType, ProviderConfig> = {\n openai: {\n name: 'OpenAI',\n baseUrl: 'https://api.openai.com/v1',\n envVar: 'OPENAI_API_KEY',\n defaultModel: 'gpt-5.3-codex',\n },\n anthropic: {\n name: 'Anthropic',\n baseUrl: 'https://api.anthropic.com/v1/',\n envVar: 'ANTHROPIC_API_KEY',\n defaultModel: 'claude-sonnet-4-6',\n },\n gemini: {\n name: 'Google Gemini',\n baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',\n envVar: 'GEMINI_API_KEY',\n defaultModel: 'gemini-3.1-pro',\n },\n xai: {\n name: 'xAI (Grok)',\n baseUrl: 'https://api.x.ai/v1',\n envVar: 'XAI_API_KEY',\n defaultModel: 'grok-4.2',\n },\n openrouter: {\n name: 'OpenRouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n envVar: 'OPENROUTER_API_KEY',\n defaultModel: 'anthropic/claude-sonnet-4-6',\n },\n custom: {\n name: 'Custom',\n baseUrl: '',\n envVar: '',\n defaultModel: '',\n },\n};\n\nexport interface DominusConfig {\n provider: AIProviderType;\n apiKey?: string;\n apiBaseUrl: string;\n model: string;\n port: number;\n maxToolIterations: number;\n maxVerifyRetries: number;\n tokenBudget: number;\n autoIndex: boolean;\n autoLearn: boolean;\n userName: string;\n openCloudApiKey?: string;\n}\n\nconst DEFAULTS: DominusConfig = {\n provider: 'openai',\n apiBaseUrl: '',\n model: '',\n port: 18088,\n maxToolIterations: 25,\n maxVerifyRetries: 3,\n tokenBudget: 128000,\n autoIndex: true,\n autoLearn: true,\n userName: 'Developer',\n};\n\n/**\n * Auto-detect provider from environment variables.\n * Returns the first provider whose env var is set, or null.\n */\nexport function detectProviderFromEnv(): { provider: AIProviderType; apiKey: string } | null {\n const priority: AIProviderType[] = ['openai', 'anthropic', 'gemini', 'xai', 'openrouter'];\n for (const p of priority) {\n const key = process.env[PROVIDERS[p].envVar];\n if (key) return { provider: p, apiKey: key };\n }\n return null;\n}\n\n/**\n * Resolve the effective base URL and model for a provider config.\n */\nexport function resolveProviderSettings(config: DominusConfig): {\n baseUrl: string;\n model: string;\n} {\n const providerInfo = PROVIDERS[config.provider];\n return {\n baseUrl: config.apiBaseUrl || providerInfo?.baseUrl || PROVIDERS.openrouter.baseUrl,\n model: config.model || providerInfo?.defaultModel || 'gpt-4o',\n };\n}\n\nexport interface DominusRules {\n do: string[];\n dont: string[];\n conventions: string[];\n context: string;\n}\n\nconst DEFAULT_RULES: DominusRules = {\n do: [],\n dont: [],\n conventions: [],\n context: '',\n};\n\nfunction ensureDir(): void {\n if (!fs.existsSync(DOMINUS_DIR)) {\n fs.mkdirSync(DOMINUS_DIR, { recursive: true });\n }\n}\n\nfunction readJson<T>(filePath: string, defaults: T): T {\n try {\n if (fs.existsSync(filePath)) {\n const raw = fs.readFileSync(filePath, 'utf-8');\n return { ...defaults, ...JSON.parse(raw) };\n }\n } catch {\n // Corrupted file; reset to defaults\n }\n return { ...defaults };\n}\n\nfunction writeJson(filePath: string, data: unknown): void {\n ensureDir();\n fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');\n}\n\nexport function loadConfig(): DominusConfig {\n ensureDir();\n return readJson(CONFIG_PATH, DEFAULTS);\n}\n\nexport function saveConfig(config: DominusConfig): void {\n writeJson(CONFIG_PATH, config);\n}\n\nexport function getConfigValue<K extends keyof DominusConfig>(key: K): DominusConfig[K] {\n const config = loadConfig();\n return config[key];\n}\n\nexport function setConfigValue<K extends keyof DominusConfig>(\n key: K,\n value: DominusConfig[K],\n): void {\n const config = loadConfig();\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (config as any)[key] = value;\n saveConfig(config);\n}\n\nexport function loadRules(): DominusRules {\n ensureDir();\n return readJson(RULES_PATH, DEFAULT_RULES);\n}\n\nexport function saveRules(rules: DominusRules): void {\n writeJson(RULES_PATH, rules);\n}\n\nexport function getDominusDir(): string {\n ensureDir();\n return DOMINUS_DIR;\n}\n\nexport function getDbPath(): string {\n ensureDir();\n return DB_PATH;\n}\n\nexport function createConfigAccess(): ConfigAccess {\n return {\n get<T>(key: string): T | undefined {\n const config = loadConfig() as unknown as Record<string, unknown>;\n return config[key] as T | undefined;\n },\n set(key: string, value: unknown): void {\n const config = loadConfig() as unknown as Record<string, unknown>;\n config[key] = value;\n saveConfig(config as unknown as DominusConfig);\n },\n getAll(): Record<string, unknown> {\n return loadConfig() as unknown as Record<string, unknown>;\n },\n };\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\nimport * as crypto from 'node:crypto';\nimport { getDominusDir } from '../config/config.js';\n\nconst TOKEN_FILE = 'bridge-token';\n\nexport function loadOrCreateBridgeToken(): string {\n const tokenPath = path.join(getDominusDir(), TOKEN_FILE);\n try {\n const existing = fs.readFileSync(tokenPath, 'utf8').trim();\n if (existing.length >= 32) return existing;\n } catch {\n // First run or an unreadable token is repaired below.\n }\n\n const token = crypto.randomBytes(32).toString('base64url');\n fs.writeFileSync(tokenPath, `${token}\\n`, { encoding: 'utf8', mode: 0o600 });\n try {\n fs.chmodSync(tokenPath, 0o600);\n } catch {\n // Windows ACLs do not map directly to POSIX modes.\n }\n return token;\n}\n\nexport function tokensEqual(actual: unknown, expected: string): boolean {\n if (typeof actual !== 'string') return false;\n const actualBuffer = Buffer.from(actual);\n const expectedBuffer = Buffer.from(expected);\n return actualBuffer.length === expectedBuffer.length\n && crypto.timingSafeEqual(actualBuffer, expectedBuffer);\n}\n","import fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { loadOrCreateBridgeToken } from './transport/auth.js';\n\nconst TOKEN_PLACEHOLDER = '__DOMINUS_BRIDGE_TOKEN__';\n\nfunction getPluginDirectory(): string {\n if (process.platform === 'win32') {\n const localAppData = process.env.LOCALAPPDATA;\n if (!localAppData) throw new Error('LOCALAPPDATA is not set');\n return path.join(localAppData, 'Roblox', 'Plugins');\n }\n if (process.platform === 'darwin') {\n return path.join(os.homedir(), 'Documents', 'Roblox', 'Plugins');\n }\n return path.join(os.homedir(), '.local', 'share', 'Roblox', 'Plugins');\n}\n\nconst distDirectory = path.dirname(fileURLToPath(import.meta.url));\nconst source = path.resolve(distDirectory, '..', 'plugin', 'Dominus.rbxmx');\nconst destinationDirectory = getPluginDirectory();\nconst destination = path.join(destinationDirectory, 'Dominus2.rbxmx');\n\nif (!fs.existsSync(source)) {\n throw new Error(`Packaged plugin not found at ${source}. Run pnpm build:plugin first.`);\n}\n\nconst template = fs.readFileSync(source, 'utf8');\nif (!template.includes(TOKEN_PLACEHOLDER)) {\n throw new Error('Packaged plugin template is missing its bridge token placeholder');\n}\nconst personalizedPlugin = template.replaceAll(TOKEN_PLACEHOLDER, loadOrCreateBridgeToken());\n\nfs.mkdirSync(destinationDirectory, { recursive: true });\nfs.writeFileSync(destination, personalizedPlugin, 'utf8');\nfor (const obsoleteName of ['Dominus2.rbxm', 'Dominus.rbxm', 'Dominus.rbxmx']) {\n const obsoletePath = path.join(destinationDirectory, obsoleteName);\n if (obsoletePath !== destination) fs.rmSync(obsoletePath, { force: true });\n}\nconsole.log(`Installed Dominus 2 Studio plugin to ${destination}`);\nconsole.log('Restart Roblox Studio. Dominus 2 will authenticate automatically when dominus-mcp starts.');\n"]}
1
+ {"version":3,"sources":["../src/config/config.ts","../src/transport/auth.ts","../src/install-plugin.ts"],"names":["path","os","fs"],"mappings":"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","file":"install-plugin.js","sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport os from 'node:os';\nimport type { ConfigAccess } from '../tools/types.js';\n\nconst DOMINUS_DIR = path.join(os.homedir(), '.dominus');\nconst CONFIG_PATH = path.join(DOMINUS_DIR, 'config.json');\nconst DB_PATH = path.join(DOMINUS_DIR, 'dominus.db');\nconst RULES_PATH = path.join(DOMINUS_DIR, 'rules.json');\n\nexport type AIProviderType =\n | 'openai'\n | 'anthropic'\n | 'gemini'\n | 'xai'\n | 'openrouter'\n | 'custom';\n\nexport interface ProviderConfig {\n name: string;\n baseUrl: string;\n envVar: string;\n defaultModel: string;\n}\n\nexport const PROVIDERS: Record<AIProviderType, ProviderConfig> = {\n openai: {\n name: 'OpenAI',\n baseUrl: 'https://api.openai.com/v1',\n envVar: 'OPENAI_API_KEY',\n defaultModel: 'gpt-5.3-codex',\n },\n anthropic: {\n name: 'Anthropic',\n baseUrl: 'https://api.anthropic.com/v1/',\n envVar: 'ANTHROPIC_API_KEY',\n defaultModel: 'claude-sonnet-4-6',\n },\n gemini: {\n name: 'Google Gemini',\n baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',\n envVar: 'GEMINI_API_KEY',\n defaultModel: 'gemini-3.1-pro',\n },\n xai: {\n name: 'xAI (Grok)',\n baseUrl: 'https://api.x.ai/v1',\n envVar: 'XAI_API_KEY',\n defaultModel: 'grok-4.2',\n },\n openrouter: {\n name: 'OpenRouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n envVar: 'OPENROUTER_API_KEY',\n defaultModel: 'anthropic/claude-sonnet-4-6',\n },\n custom: {\n name: 'Custom',\n baseUrl: '',\n envVar: '',\n defaultModel: '',\n },\n};\n\nexport interface DominusConfig {\n provider: AIProviderType;\n apiKey?: string;\n apiBaseUrl: string;\n model: string;\n port: number;\n maxToolIterations: number;\n maxVerifyRetries: number;\n tokenBudget: number;\n autoIndex: boolean;\n autoLearn: boolean;\n userName: string;\n openCloudApiKey?: string;\n}\n\nconst DEFAULTS: DominusConfig = {\n provider: 'openai',\n apiBaseUrl: '',\n model: '',\n port: 18088,\n maxToolIterations: 25,\n maxVerifyRetries: 3,\n tokenBudget: 128000,\n autoIndex: true,\n autoLearn: true,\n userName: 'Developer',\n};\n\n/**\n * Auto-detect provider from environment variables.\n * Returns the first provider whose env var is set, or null.\n */\nexport function detectProviderFromEnv(): { provider: AIProviderType; apiKey: string } | null {\n const priority: AIProviderType[] = ['openai', 'anthropic', 'gemini', 'xai', 'openrouter'];\n for (const p of priority) {\n const key = process.env[PROVIDERS[p].envVar];\n if (key) return { provider: p, apiKey: key };\n }\n return null;\n}\n\n/**\n * Resolve the effective base URL and model for a provider config.\n */\nexport function resolveProviderSettings(config: DominusConfig): {\n baseUrl: string;\n model: string;\n} {\n const providerInfo = PROVIDERS[config.provider];\n return {\n baseUrl: config.apiBaseUrl || providerInfo?.baseUrl || PROVIDERS.openrouter.baseUrl,\n model: config.model || providerInfo?.defaultModel || 'gpt-4o',\n };\n}\n\nexport interface DominusRules {\n do: string[];\n dont: string[];\n conventions: string[];\n context: string;\n}\n\nconst DEFAULT_RULES: DominusRules = {\n do: [],\n dont: [],\n conventions: [],\n context: '',\n};\n\nfunction ensureDir(): void {\n if (!fs.existsSync(DOMINUS_DIR)) {\n fs.mkdirSync(DOMINUS_DIR, { recursive: true });\n }\n}\n\nfunction readJson<T>(filePath: string, defaults: T): T {\n try {\n if (fs.existsSync(filePath)) {\n const raw = fs.readFileSync(filePath, 'utf-8');\n return { ...defaults, ...JSON.parse(raw) };\n }\n } catch {\n // Corrupted file; reset to defaults\n }\n return { ...defaults };\n}\n\nfunction writeJson(filePath: string, data: unknown): void {\n ensureDir();\n fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');\n}\n\nexport function loadConfig(): DominusConfig {\n ensureDir();\n return readJson(CONFIG_PATH, DEFAULTS);\n}\n\nexport function saveConfig(config: DominusConfig): void {\n writeJson(CONFIG_PATH, config);\n}\n\nexport function getConfigValue<K extends keyof DominusConfig>(key: K): DominusConfig[K] {\n const config = loadConfig();\n return config[key];\n}\n\nexport function setConfigValue<K extends keyof DominusConfig>(\n key: K,\n value: DominusConfig[K],\n): void {\n const config = loadConfig();\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (config as any)[key] = value;\n saveConfig(config);\n}\n\nexport function loadRules(): DominusRules {\n ensureDir();\n return readJson(RULES_PATH, DEFAULT_RULES);\n}\n\nexport function saveRules(rules: DominusRules): void {\n writeJson(RULES_PATH, rules);\n}\n\nexport function getDominusDir(): string {\n ensureDir();\n return DOMINUS_DIR;\n}\n\nexport function getDbPath(): string {\n ensureDir();\n return DB_PATH;\n}\n\nexport function createConfigAccess(): ConfigAccess {\n return {\n get<T>(key: string): T | undefined {\n const config = loadConfig() as unknown as Record<string, unknown>;\n return config[key] as T | undefined;\n },\n set(key: string, value: unknown): void {\n const config = loadConfig() as unknown as Record<string, unknown>;\n config[key] = value;\n saveConfig(config as unknown as DominusConfig);\n },\n getAll(): Record<string, unknown> {\n return loadConfig() as unknown as Record<string, unknown>;\n },\n };\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\nimport * as crypto from 'node:crypto';\nimport { getDominusDir } from '../config/config.js';\n\nconst TOKEN_FILE = 'bridge-token';\n\nexport function loadOrCreateBridgeToken(): string {\n const tokenPath = path.join(getDominusDir(), TOKEN_FILE);\n try {\n const existing = fs.readFileSync(tokenPath, 'utf8').trim();\n if (existing.length >= 32) return existing;\n } catch {\n // First run or an unreadable token is repaired below.\n }\n\n const token = crypto.randomBytes(32).toString('base64url');\n fs.writeFileSync(tokenPath, `${token}\\n`, { encoding: 'utf8', mode: 0o600 });\n try {\n fs.chmodSync(tokenPath, 0o600);\n } catch {\n // Windows ACLs do not map directly to POSIX modes.\n }\n return token;\n}\n\nexport function tokensEqual(actual: unknown, expected: string): boolean {\n if (typeof actual !== 'string') return false;\n const actualBuffer = Buffer.from(actual);\n const expectedBuffer = Buffer.from(expected);\n return actualBuffer.length === expectedBuffer.length\n && crypto.timingSafeEqual(actualBuffer, expectedBuffer);\n}\n","import fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { loadOrCreateBridgeToken } from './transport/auth.js';\n\nconst TOKEN_PLACEHOLDER = '__DOMINUS_BRIDGE_TOKEN__';\n\nfunction getPluginDirectory(): string {\n if (process.platform === 'win32') {\n const localAppData = process.env.LOCALAPPDATA;\n if (!localAppData) throw new Error('LOCALAPPDATA is not set');\n return path.join(localAppData, 'Roblox', 'Plugins');\n }\n if (process.platform === 'darwin') {\n return path.join(os.homedir(), 'Documents', 'Roblox', 'Plugins');\n }\n return path.join(os.homedir(), '.local', 'share', 'Roblox', 'Plugins');\n}\n\nconst distDirectory = path.dirname(fileURLToPath(import.meta.url));\nconst source = path.resolve(distDirectory, '..', 'plugin', 'Dominus.rbxmx');\nconst destinationDirectory = getPluginDirectory();\nconst destination = path.join(destinationDirectory, 'Dominus2.rbxmx');\n\nif (!fs.existsSync(source)) {\n throw new Error(`Packaged plugin not found at ${source}. Run pnpm build:plugin first.`);\n}\n\nconst template = fs.readFileSync(source, 'utf8');\nif (!template.includes(TOKEN_PLACEHOLDER)) {\n throw new Error('Packaged plugin template is missing its bridge token placeholder');\n}\nconst personalizedPlugin = template.replaceAll(TOKEN_PLACEHOLDER, loadOrCreateBridgeToken());\n\nfs.mkdirSync(destinationDirectory, { recursive: true });\nfs.writeFileSync(destination, personalizedPlugin, 'utf8');\nfor (const obsoleteName of ['Dominus2.rbxm', 'Dominus.rbxm', 'Dominus.rbxmx']) {\n const obsoletePath = path.join(destinationDirectory, obsoleteName);\n if (obsoletePath !== destination) fs.rmSync(obsoletePath, { force: true });\n}\nconsole.log(`Installed Dominus 2 Studio plugin to ${destination}`);\nconsole.log(\n 'Roblox Studio normally reloads Dominus 2 automatically. Restart Studio only if the toolbar does not reconnect.',\n);\n"]}