@mediagato/modelreins-channel 1.0.0 → 4.1.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/modelreins-channel.mjs +872 -81
- package/package.json +1 -1
package/modelreins-channel.mjs
CHANGED
|
@@ -1,35 +1,166 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* @module modelreins-channel
|
|
4
|
+
* @version 1.1.0
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
6
|
+
* ModelReins MCP Channel
|
|
7
|
+
* ======================
|
|
8
|
+
* Bridges any Claude Code session — CLI or VSCode extension — to a ModelReins
|
|
9
|
+
* fleet. Once installed, this session becomes a named worker: it receives jobs
|
|
10
|
+
* from the dashboard, executes them with full local access (files, git, shell,
|
|
11
|
+
* env vars, browser), and reports results back in real time.
|
|
7
12
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
+
* How it works
|
|
14
|
+
* ------------
|
|
15
|
+
* 1. On startup, registers this session with ModelReins via PUT /presence.
|
|
16
|
+
* 2. Every MODELREINS_POLL_MS milliseconds, polls GET /jobs for assigned work.
|
|
17
|
+
* 3. When a job arrives, pushes it into Claude's context via MCP notifications
|
|
18
|
+
* (the `notifications/claude/channel` method — a Claude Code extension point).
|
|
19
|
+
* 4. Claude executes the task using its normal tools (Bash, Read, Edit, etc).
|
|
20
|
+
* 5. On completion, Claude calls modelreins_complete → result posted to dashboard.
|
|
13
21
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
22
|
+
* Between jobs, this is a fully normal Claude Code session. The user can still
|
|
23
|
+
* talk to Claude, open files, run commands. The channel is invisible until work
|
|
24
|
+
* arrives.
|
|
25
|
+
*
|
|
26
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
* QUICK START — Claude Code CLI
|
|
28
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
*
|
|
30
|
+
* Option A: project-scoped (.mcp.json in your repo root)
|
|
18
31
|
*
|
|
19
|
-
* .mcp.json entry:
|
|
20
32
|
* {
|
|
21
33
|
* "mcpServers": {
|
|
22
34
|
* "modelreins": {
|
|
23
|
-
* "command": "
|
|
24
|
-
* "args": ["
|
|
35
|
+
* "command": "npx",
|
|
36
|
+
* "args": ["-y", "@mediagato/modelreins-channel"],
|
|
25
37
|
* "env": {
|
|
26
|
-
* "MODELREINS_URL":
|
|
27
|
-
* "MODELREINS_TOKEN": "your-token",
|
|
38
|
+
* "MODELREINS_URL": "https://app.modelreins.com",
|
|
39
|
+
* "MODELREINS_TOKEN": "your-token-here",
|
|
28
40
|
* "MODELREINS_WORKER": "my-laptop"
|
|
29
41
|
* }
|
|
30
42
|
* }
|
|
31
43
|
* }
|
|
32
44
|
* }
|
|
45
|
+
*
|
|
46
|
+
* Option B: global (applies to every Claude Code session on this machine)
|
|
47
|
+
*
|
|
48
|
+
* claude mcp add modelreins \
|
|
49
|
+
* -e MODELREINS_URL=https://app.modelreins.com \
|
|
50
|
+
* -e MODELREINS_TOKEN=your-token \
|
|
51
|
+
* -e MODELREINS_WORKER=my-laptop \
|
|
52
|
+
* -- npx -y @mediagato/modelreins-channel
|
|
53
|
+
*
|
|
54
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
55
|
+
* QUICK START — VSCode (Claude Code extension)
|
|
56
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
57
|
+
*
|
|
58
|
+
* The Claude Code VSCode extension reads MCP config from two places:
|
|
59
|
+
*
|
|
60
|
+
* Option A: workspace-scoped — create .mcp.json in your repo root
|
|
61
|
+
* (same JSON format as the CLI option above — exact same file)
|
|
62
|
+
*
|
|
63
|
+
* Option B: user-scoped — add to VSCode settings.json
|
|
64
|
+
* Open: Cmd/Ctrl+Shift+P → "Open User Settings (JSON)"
|
|
65
|
+
* Add:
|
|
66
|
+
*
|
|
67
|
+
* "claude.mcpServers": {
|
|
68
|
+
* "modelreins": {
|
|
69
|
+
* "command": "npx",
|
|
70
|
+
* "args": ["-y", "@mediagato/modelreins-channel"],
|
|
71
|
+
* "env": {
|
|
72
|
+
* "MODELREINS_URL": "https://app.modelreins.com",
|
|
73
|
+
* "MODELREINS_TOKEN": "your-token-here",
|
|
74
|
+
* "MODELREINS_WORKER": "vscode-my-laptop"
|
|
75
|
+
* }
|
|
76
|
+
* }
|
|
77
|
+
* }
|
|
78
|
+
*
|
|
79
|
+
* After adding the config, reload VSCode. The modelreins server will appear
|
|
80
|
+
* in the Claude panel (bottom status bar or MCP server list). The worker
|
|
81
|
+
* should show as online in the ModelReins dashboard within ~30 seconds.
|
|
82
|
+
*
|
|
83
|
+
* Tip: give VSCode workers a distinct name like "vscode-laptop" so you can
|
|
84
|
+
* target them specifically from the dashboard (VSCode sessions have richer
|
|
85
|
+
* context about the open project than headless workers).
|
|
86
|
+
*
|
|
87
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
88
|
+
* ENVIRONMENT VARIABLES
|
|
89
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
90
|
+
*
|
|
91
|
+
* Required:
|
|
92
|
+
* MODELREINS_URL — Dashboard URL. Examples:
|
|
93
|
+
* https://app.modelreins.com (hosted)
|
|
94
|
+
* http://192.168.0.246:8484 (self-hosted LAN)
|
|
95
|
+
* http://localhost:8484 (local dev)
|
|
96
|
+
*
|
|
97
|
+
* MODELREINS_TOKEN — Worker auth token. Find it in the dashboard under
|
|
98
|
+
* Workers → Add Worker, or generate via the API.
|
|
99
|
+
*
|
|
100
|
+
* Optional:
|
|
101
|
+
* MODELREINS_WORKER — Worker name shown in the dashboard.
|
|
102
|
+
* Default: channel-<hostname>
|
|
103
|
+
* Tip: use a human name like "macbook-pro" or
|
|
104
|
+
* "vscode-workstation" for easy targeting.
|
|
105
|
+
*
|
|
106
|
+
* MODELREINS_WORKER_MODEL — Model label reported to the dashboard.
|
|
107
|
+
* Default: "claude-session"
|
|
108
|
+
* Override if you want the dashboard to show the
|
|
109
|
+
* specific Claude model (e.g. "claude-sonnet-4-6").
|
|
110
|
+
*
|
|
111
|
+
* MODELREINS_WORKER_TAGS — Comma-separated capability tags visible in the
|
|
112
|
+
* dashboard. Used for smart routing ("only send
|
|
113
|
+
* e2e test jobs to workers tagged 'playwright'").
|
|
114
|
+
* Default: "local,interactive,full-access"
|
|
115
|
+
* Examples: "playwright,node,python,docker"
|
|
116
|
+
* "vscode,local,typescript"
|
|
117
|
+
* "readonly,review-only"
|
|
118
|
+
*
|
|
119
|
+
* MODELREINS_POLL_MS — Job poll interval in milliseconds.
|
|
120
|
+
* Default: 5000 (5 seconds)
|
|
121
|
+
* Lower = more responsive, higher = less network.
|
|
122
|
+
* Minimum recommended: 2000.
|
|
123
|
+
*
|
|
124
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
125
|
+
* AVAILABLE TOOLS (visible to Claude in every session)
|
|
126
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
127
|
+
*
|
|
128
|
+
* modelreins_complete — Report a job as done or failed. Always call this
|
|
129
|
+
* when you finish a dispatched task. Never leave a
|
|
130
|
+
* job in "running" state — it blocks the queue.
|
|
131
|
+
*
|
|
132
|
+
* modelreins_dispatch — Send work to another worker while in session.
|
|
133
|
+
* Useful for splitting large tasks: handle the part
|
|
134
|
+
* you can do locally, delegate the rest to a headless
|
|
135
|
+
* worker with more model capacity.
|
|
136
|
+
*
|
|
137
|
+
* modelreins_status — Snapshot of the entire fleet: workers, recent jobs,
|
|
138
|
+
* active schedules. Useful for situational awareness
|
|
139
|
+
* before dispatching ("is worker-01 free right now?").
|
|
140
|
+
*
|
|
141
|
+
* modelreins_recruit — Generate a self-contained bootstrap prompt that
|
|
142
|
+
* installs and configures a new worker on any machine.
|
|
143
|
+
* Supports all providers and both CLI + VSCode setups.
|
|
144
|
+
* Paste the output into any Claude Code session and
|
|
145
|
+
* it will handle the entire install automatically.
|
|
146
|
+
*
|
|
147
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
148
|
+
* ARCHITECTURE NOTE
|
|
149
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
150
|
+
*
|
|
151
|
+
* This channel runs as a stdio MCP server — Claude Code spawns it as a child
|
|
152
|
+
* process and communicates over stdin/stdout using the MCP protocol. It has no
|
|
153
|
+
* HTTP server of its own. The only network traffic is outbound to ModelReins.
|
|
154
|
+
*
|
|
155
|
+
* The `notifications/claude/channel` method is a Claude Code extension to the
|
|
156
|
+
* MCP spec that injects text directly into Claude's context window. This is
|
|
157
|
+
* how job prompts arrive without the user needing to type anything.
|
|
158
|
+
*
|
|
159
|
+
* Job flow:
|
|
160
|
+
* Dashboard → POST /dispatch → ModelReins server assigns job →
|
|
161
|
+
* Channel polls → pulls job → notification → Claude executes →
|
|
162
|
+
* Claude calls modelreins_complete → POST /jobs/{id}/output + PUT /jobs/{id}
|
|
163
|
+
* → dashboard updates in real time.
|
|
33
164
|
*/
|
|
34
165
|
|
|
35
166
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
@@ -40,17 +171,62 @@ import {
|
|
|
40
171
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
41
172
|
import { hostname } from 'os';
|
|
42
173
|
|
|
43
|
-
// ──
|
|
174
|
+
// ── Configuration ─────────────────────────────────────────────────────────────
|
|
175
|
+
//
|
|
176
|
+
// All config is via environment variables so the same package.json / npx command
|
|
177
|
+
// works across every setup — no config files, no CLI flags to remember.
|
|
44
178
|
|
|
179
|
+
/** Base URL of the ModelReins server this channel connects to. */
|
|
45
180
|
const BASE_URL = process.env.MODELREINS_URL || 'http://localhost:8484';
|
|
181
|
+
|
|
182
|
+
/** Bearer token used for all API requests. Must match a token in the server DB. */
|
|
46
183
|
const TOKEN = process.env.MODELREINS_TOKEN || '';
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Name this worker reports to the dashboard. Defaults to channel-<hostname>
|
|
187
|
+
* so each machine gets a unique name automatically with no config required.
|
|
188
|
+
* Override to something human-readable like "macbook-pro" or "vscode-workstation".
|
|
189
|
+
*/
|
|
47
190
|
const WORKER_NAME = process.env.MODELREINS_WORKER || `channel-${hostname().toLowerCase()}`;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* How often (ms) to poll the server for new jobs.
|
|
194
|
+
* 5000ms is a good default — responsive without hammering the server.
|
|
195
|
+
* Decrease for latency-sensitive workflows, increase for battery-constrained machines.
|
|
196
|
+
*/
|
|
48
197
|
const POLL_MS = parseInt(process.env.MODELREINS_POLL_MS || '5000', 10);
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Model label reported to the dashboard for display purposes.
|
|
201
|
+
* Does not affect which model Claude actually uses — that's controlled by the
|
|
202
|
+
* Claude Code session / extension settings. Set this to match what you know
|
|
203
|
+
* the session will use (e.g. "claude-sonnet-4-6").
|
|
204
|
+
*/
|
|
49
205
|
const WORKER_MODEL = process.env.MODELREINS_WORKER_MODEL || 'claude-session';
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Comma-separated capability tags for smart routing.
|
|
209
|
+
* The dashboard and dispatch API can filter workers by tags:
|
|
210
|
+
* assigned_to: "tag:playwright" → only workers with that tag receive the job
|
|
211
|
+
* Default tags signal that this is a local interactive session with full filesystem access.
|
|
212
|
+
*/
|
|
50
213
|
const WORKER_TAGS = process.env.MODELREINS_WORKER_TAGS || 'local,interactive,full-access';
|
|
51
214
|
|
|
52
|
-
// ── HTTP
|
|
215
|
+
// ── HTTP Client ───────────────────────────────────────────────────────────────
|
|
216
|
+
//
|
|
217
|
+
// Thin wrapper around fetch — attaches auth, serializes body, swallows
|
|
218
|
+
// network errors so a momentary server blip doesn't crash the channel.
|
|
53
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Make an authenticated request to the ModelReins API.
|
|
222
|
+
*
|
|
223
|
+
* @param {'GET'|'POST'|'PUT'|'DELETE'} method HTTP method
|
|
224
|
+
* @param {string} path API path (e.g. '/jobs?limit=5')
|
|
225
|
+
* @param {object|null} body Request body (JSON-serialized)
|
|
226
|
+
* @returns {Promise<{status: number, data: any}>}
|
|
227
|
+
* status=0 means a network error occurred (server unreachable, timeout, etc).
|
|
228
|
+
* Always check status before using data.
|
|
229
|
+
*/
|
|
54
230
|
async function api(method, path, body = null) {
|
|
55
231
|
const url = `${BASE_URL}${path}`;
|
|
56
232
|
const opts = {
|
|
@@ -66,108 +242,288 @@ async function api(method, path, body = null) {
|
|
|
66
242
|
const data = await res.json();
|
|
67
243
|
return { status: res.status, data };
|
|
68
244
|
} catch (e) {
|
|
245
|
+
// Network errors (ECONNREFUSED, timeout, DNS failure) return status 0.
|
|
246
|
+
// The caller decides whether to retry or surface the error.
|
|
69
247
|
return { status: 0, data: { error: e.message } };
|
|
70
248
|
}
|
|
71
249
|
}
|
|
72
250
|
|
|
73
|
-
// ── State
|
|
251
|
+
// ── Session State ─────────────────────────────────────────────────────────────
|
|
74
252
|
|
|
253
|
+
/**
|
|
254
|
+
* ID of the job currently being worked on, or null if idle.
|
|
255
|
+
* Used to prevent accepting new jobs while one is in progress, and to
|
|
256
|
+
* report the correct status in heartbeats.
|
|
257
|
+
*/
|
|
75
258
|
let currentJobId = null;
|
|
76
|
-
let mcp = null;
|
|
77
259
|
|
|
78
|
-
// ── MCP Server
|
|
260
|
+
// ── MCP Server ────────────────────────────────────────────────────────────────
|
|
261
|
+
//
|
|
262
|
+
// The MCP server is the bridge between this process and Claude's context.
|
|
263
|
+
// Tools registered here appear in Claude's tool list and can be called at any
|
|
264
|
+
// point during a session. Notifications push content into Claude's context
|
|
265
|
+
// without user interaction.
|
|
79
266
|
|
|
80
|
-
mcp = new Server(
|
|
81
|
-
{ name: 'modelreins', version: '1.
|
|
267
|
+
const mcp = new Server(
|
|
268
|
+
{ name: 'modelreins', version: '1.1.0' },
|
|
82
269
|
{
|
|
83
270
|
capabilities: {
|
|
271
|
+
// Enable the Claude-specific channel notification method.
|
|
272
|
+
// This is what allows job prompts to be injected into the session.
|
|
84
273
|
experimental: { 'claude/channel': {} },
|
|
85
274
|
tools: {},
|
|
86
275
|
},
|
|
276
|
+
|
|
277
|
+
// System-level instructions Claude receives when the channel is connected.
|
|
278
|
+
// These prime Claude to handle incoming jobs correctly without the user
|
|
279
|
+
// having to explain the setup in every session.
|
|
87
280
|
instructions: [
|
|
88
281
|
`You are connected to ModelReins (${BASE_URL}) as worker "${WORKER_NAME}".`,
|
|
282
|
+
`Your capability tags: ${WORKER_TAGS}`,
|
|
283
|
+
'',
|
|
284
|
+
'RECEIVING JOBS:',
|
|
89
285
|
'Jobs from the ModelReins dashboard arrive as <channel source="modelreins" job_id="..." priority="..."> tags.',
|
|
90
|
-
'When
|
|
91
|
-
'
|
|
92
|
-
'
|
|
93
|
-
'
|
|
286
|
+
'When a job arrives, read the prompt inside the tag and execute it using your full local capabilities.',
|
|
287
|
+
'Treat dispatched jobs with the same care as user requests — use your tools, check your work.',
|
|
288
|
+
'',
|
|
289
|
+
'COMPLETING JOBS:',
|
|
290
|
+
'Always call modelreins_complete when you finish, whether the task succeeded or failed.',
|
|
291
|
+
'Never leave a job in "running" state — it blocks the queue and confuses the dashboard.',
|
|
292
|
+
'A good summary is 1-3 sentences: what you did, what changed, any caveats.',
|
|
293
|
+
'',
|
|
294
|
+
'BETWEEN JOBS:',
|
|
295
|
+
'You remain a fully normal Claude Code session. The user can still talk to you, open files,',
|
|
296
|
+
'run commands, etc. The ModelReins channel is invisible until work arrives.',
|
|
297
|
+
'',
|
|
298
|
+
'DISPATCHING TO OTHER WORKERS:',
|
|
299
|
+
'Use modelreins_dispatch to hand off subtasks. This is powerful: you can split a large job,',
|
|
300
|
+
'keep the local/interactive parts for yourself, and delegate batch work to headless workers.',
|
|
94
301
|
].join('\n'),
|
|
95
302
|
}
|
|
96
303
|
);
|
|
97
304
|
|
|
98
|
-
// Tool
|
|
305
|
+
// ── Tool Definitions ──────────────────────────────────────────────────────────
|
|
306
|
+
|
|
99
307
|
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
100
308
|
tools: [
|
|
309
|
+
|
|
310
|
+
// ── modelreins_complete ───────────────────────────────────────────────
|
|
101
311
|
{
|
|
102
312
|
name: 'modelreins_complete',
|
|
103
|
-
description:
|
|
313
|
+
description: [
|
|
314
|
+
'Report a dispatched job as done or failed back to the ModelReins dashboard.',
|
|
315
|
+
'',
|
|
316
|
+
'Call this at the end of EVERY job received via the <channel> tag, regardless of outcome.',
|
|
317
|
+
'Leaving a job in "running" state blocks the queue for that worker slot.',
|
|
318
|
+
'',
|
|
319
|
+
'The summary is shown in the dashboard job feed and stored in job history.',
|
|
320
|
+
'Keep it factual: what was done, what changed, any errors encountered.',
|
|
321
|
+
].join('\n'),
|
|
104
322
|
inputSchema: {
|
|
105
323
|
type: 'object',
|
|
106
324
|
properties: {
|
|
107
|
-
job_id: {
|
|
108
|
-
|
|
109
|
-
|
|
325
|
+
job_id: {
|
|
326
|
+
type: 'string',
|
|
327
|
+
description: 'The job ID from the <channel job_id="..."> tag.',
|
|
328
|
+
},
|
|
329
|
+
summary: {
|
|
330
|
+
type: 'string',
|
|
331
|
+
description: [
|
|
332
|
+
'What was accomplished (or why it failed). 1-3 sentences.',
|
|
333
|
+
'Examples:',
|
|
334
|
+
' "Refactored auth middleware — 142 lines, 3 functions extracted. All tests pass."',
|
|
335
|
+
' "Failed: could not connect to database. Check DB_URL env var."',
|
|
336
|
+
].join('\n'),
|
|
337
|
+
},
|
|
338
|
+
success: {
|
|
339
|
+
type: 'boolean',
|
|
340
|
+
description: 'true = job completed successfully, false = job failed.',
|
|
341
|
+
},
|
|
110
342
|
},
|
|
111
343
|
required: ['job_id', 'summary', 'success'],
|
|
112
344
|
},
|
|
113
345
|
},
|
|
346
|
+
|
|
347
|
+
// ── modelreins_dispatch ───────────────────────────────────────────────
|
|
114
348
|
{
|
|
115
349
|
name: 'modelreins_dispatch',
|
|
116
|
-
description:
|
|
350
|
+
description: [
|
|
351
|
+
'Dispatch a new job to another ModelReins worker from within this session.',
|
|
352
|
+
'',
|
|
353
|
+
'Use this to split large tasks: handle the local/interactive parts yourself,',
|
|
354
|
+
'delegate batch processing, test runs, or content generation to headless workers.',
|
|
355
|
+
'',
|
|
356
|
+
'Routing options:',
|
|
357
|
+
' assigned_to: "auto" — router picks the best available worker',
|
|
358
|
+
' assigned_to: "worker-01" — target a specific worker by name',
|
|
359
|
+
' assigned_to: "tag:playwright" — any worker tagged "playwright"',
|
|
360
|
+
'',
|
|
361
|
+
'Model preferences:',
|
|
362
|
+
' "auto" — worker uses its configured default',
|
|
363
|
+
' "haiku" — fast, cheap, good for simple tasks',
|
|
364
|
+
' "sonnet" — balanced, good for most code tasks',
|
|
365
|
+
' "opus" — most capable, use for complex architecture/reasoning',
|
|
366
|
+
].join('\n'),
|
|
117
367
|
inputSchema: {
|
|
118
368
|
type: 'object',
|
|
119
369
|
properties: {
|
|
120
|
-
prompt: {
|
|
121
|
-
|
|
122
|
-
|
|
370
|
+
prompt: {
|
|
371
|
+
type: 'string',
|
|
372
|
+
description: 'The task prompt. Be specific — headless workers have no context other than what you write here. Include file paths, expected outputs, and success criteria.',
|
|
373
|
+
},
|
|
374
|
+
assigned_to: {
|
|
375
|
+
type: 'string',
|
|
376
|
+
description: 'Worker name, "auto", or "tag:<tagname>". Default: "auto".',
|
|
377
|
+
},
|
|
378
|
+
model_preference: {
|
|
379
|
+
type: 'string',
|
|
380
|
+
description: 'Preferred model tier: auto / haiku / sonnet / opus. Default: auto.',
|
|
381
|
+
enum: ['auto', 'haiku', 'sonnet', 'opus'],
|
|
382
|
+
},
|
|
383
|
+
priority: {
|
|
384
|
+
type: 'number',
|
|
385
|
+
description: 'Job priority 0-10. Higher priority jobs are picked up first. Default: 0.',
|
|
386
|
+
},
|
|
123
387
|
},
|
|
124
388
|
required: ['prompt'],
|
|
125
389
|
},
|
|
126
390
|
},
|
|
391
|
+
|
|
392
|
+
// ── modelreins_status ─────────────────────────────────────────────────
|
|
127
393
|
{
|
|
128
394
|
name: 'modelreins_status',
|
|
129
|
-
description:
|
|
395
|
+
description: [
|
|
396
|
+
'Snapshot of the entire ModelReins fleet: online workers, recent jobs, active schedules.',
|
|
397
|
+
'',
|
|
398
|
+
'Use before dispatching to check worker availability.',
|
|
399
|
+
'Use after dispatching to confirm the job was picked up.',
|
|
400
|
+
'Use anytime for situational awareness of what the fleet is doing.',
|
|
401
|
+
].join('\n'),
|
|
130
402
|
inputSchema: {
|
|
131
403
|
type: 'object',
|
|
132
404
|
properties: {},
|
|
133
405
|
},
|
|
134
406
|
},
|
|
407
|
+
|
|
408
|
+
// ── modelreins_recruit ────────────────────────────────────────────────
|
|
409
|
+
{
|
|
410
|
+
name: 'modelreins_recruit',
|
|
411
|
+
description: [
|
|
412
|
+
'Generate a self-contained bootstrap prompt that installs and configures a ModelReins',
|
|
413
|
+
'worker on any machine — no prior knowledge required.',
|
|
414
|
+
'',
|
|
415
|
+
'The generated prompt is designed to be pasted into any Claude Code session (CLI or VSCode).',
|
|
416
|
+
'Claude will automatically:',
|
|
417
|
+
' - Detect which AI providers are available on the machine',
|
|
418
|
+
' - Present options for any providers not found',
|
|
419
|
+
' - Install the worker package',
|
|
420
|
+
' - Write the .env config',
|
|
421
|
+
' - Test the connection',
|
|
422
|
+
' - Register as a system service (survives reboots)',
|
|
423
|
+
' - Generate the correct MCP Channel config for CLI or VSCode',
|
|
424
|
+
'',
|
|
425
|
+
'This is how the fleet grows itself: one worker generates the prompt that recruits the next.',
|
|
426
|
+
].join('\n'),
|
|
427
|
+
inputSchema: {
|
|
428
|
+
type: 'object',
|
|
429
|
+
properties: {
|
|
430
|
+
worker_name: {
|
|
431
|
+
type: 'string',
|
|
432
|
+
description: 'Desired name for the new worker. If omitted, the prompt will auto-detect from the target machine hostname.',
|
|
433
|
+
},
|
|
434
|
+
providers: {
|
|
435
|
+
type: 'array',
|
|
436
|
+
items: { type: 'string' },
|
|
437
|
+
description: [
|
|
438
|
+
'Specific providers to include in the prompt.',
|
|
439
|
+
'If omitted, ALL providers are covered with auto-detection.',
|
|
440
|
+
'Valid values: claude, openai, gemini, ollama, lmstudio, openrouter, 1minai',
|
|
441
|
+
'Example: ["ollama", "lmstudio"] — generates a local-only, zero-API-key prompt.',
|
|
442
|
+
].join('\n'),
|
|
443
|
+
},
|
|
444
|
+
frontend: {
|
|
445
|
+
type: 'string',
|
|
446
|
+
description: [
|
|
447
|
+
'Target frontend environment. Affects which MCP config format is generated.',
|
|
448
|
+
' "cli" — Claude Code CLI (.mcp.json or global claude mcp add)',
|
|
449
|
+
' "vscode" — Claude Code VSCode extension (VSCode settings.json)',
|
|
450
|
+
' "both" — include setup steps for both (default)',
|
|
451
|
+
].join('\n'),
|
|
452
|
+
enum: ['cli', 'vscode', 'both'],
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
|
|
135
458
|
],
|
|
136
459
|
}));
|
|
137
460
|
|
|
461
|
+
// ── Tool Handlers ─────────────────────────────────────────────────────────────
|
|
462
|
+
|
|
138
463
|
mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
139
464
|
const { name, arguments: args } = req.params;
|
|
140
465
|
|
|
466
|
+
// ── modelreins_complete ───────────────────────────────────────────────────
|
|
467
|
+
|
|
141
468
|
if (name === 'modelreins_complete') {
|
|
142
469
|
const { job_id, summary, success } = args;
|
|
143
|
-
|
|
470
|
+
|
|
471
|
+
// Post the result text as a stdout output chunk. This streams into the
|
|
472
|
+
// dashboard's live output viewer in real time.
|
|
144
473
|
await api('POST', `/jobs/${job_id}/output`, {
|
|
145
474
|
stream: 'stdout',
|
|
146
475
|
content: summary,
|
|
147
476
|
});
|
|
148
|
-
|
|
477
|
+
|
|
478
|
+
// Update job status. exit_code follows Unix convention: 0 = success.
|
|
149
479
|
await api('PUT', `/jobs/${job_id}`, {
|
|
150
480
|
status: success ? 'done' : 'failed',
|
|
151
481
|
exit_code: success ? 0 : 1,
|
|
152
482
|
error: success ? null : summary,
|
|
153
483
|
});
|
|
484
|
+
|
|
485
|
+
// Clear current job slot so the poll loop can accept the next job.
|
|
154
486
|
currentJobId = null;
|
|
155
|
-
|
|
487
|
+
|
|
488
|
+
return {
|
|
489
|
+
content: [{
|
|
490
|
+
type: 'text',
|
|
491
|
+
text: `Job #${job_id} marked ${success ? 'done' : 'failed'}. Result posted to ModelReins dashboard.`,
|
|
492
|
+
}],
|
|
493
|
+
};
|
|
156
494
|
}
|
|
157
495
|
|
|
496
|
+
// ── modelreins_dispatch ───────────────────────────────────────────────────
|
|
497
|
+
|
|
158
498
|
if (name === 'modelreins_dispatch') {
|
|
159
|
-
const { prompt, assigned_to, model_preference } = args;
|
|
499
|
+
const { prompt, assigned_to, model_preference, priority } = args;
|
|
500
|
+
|
|
160
501
|
const res = await api('POST', '/dispatch', {
|
|
161
502
|
prompt,
|
|
162
503
|
assigned_to: assigned_to || 'auto',
|
|
163
504
|
model_preference: model_preference || null,
|
|
505
|
+
priority: priority ?? 0,
|
|
164
506
|
});
|
|
165
|
-
|
|
166
|
-
|
|
507
|
+
|
|
508
|
+
if (res.data?.success || res.data?.id) {
|
|
509
|
+
return {
|
|
510
|
+
content: [{
|
|
511
|
+
type: 'text',
|
|
512
|
+
text: `Dispatched job #${res.data.id} to ${res.data.assigned_to || assigned_to || 'auto'}. Track it in the dashboard.`,
|
|
513
|
+
}],
|
|
514
|
+
};
|
|
167
515
|
}
|
|
168
|
-
|
|
516
|
+
|
|
517
|
+
return {
|
|
518
|
+
content: [{
|
|
519
|
+
type: 'text',
|
|
520
|
+
text: `Dispatch failed (HTTP ${res.status}): ${JSON.stringify(res.data)}`,
|
|
521
|
+
}],
|
|
522
|
+
};
|
|
169
523
|
}
|
|
170
524
|
|
|
525
|
+
// ── modelreins_status ─────────────────────────────────────────────────────
|
|
526
|
+
|
|
171
527
|
if (name === 'modelreins_status') {
|
|
172
528
|
const [workers, jobs, schedules] = await Promise.all([
|
|
173
529
|
api('GET', '/workers'),
|
|
@@ -175,20 +531,31 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
175
531
|
api('GET', '/schedules'),
|
|
176
532
|
]);
|
|
177
533
|
|
|
178
|
-
const lines = [
|
|
534
|
+
const lines = [`## ModelReins Fleet — ${BASE_URL}\n`];
|
|
179
535
|
|
|
180
536
|
// Workers
|
|
181
537
|
const wList = workers.data?.workers || [];
|
|
182
|
-
lines.push(`### Workers (${wList.length})`);
|
|
538
|
+
lines.push(`### Workers (${wList.length} known)`);
|
|
539
|
+
if (wList.length === 0) {
|
|
540
|
+
lines.push(' No workers registered.');
|
|
541
|
+
}
|
|
183
542
|
for (const w of wList) {
|
|
184
|
-
|
|
543
|
+
const model = w.model || '?';
|
|
544
|
+
const status = w.status || 'unknown';
|
|
545
|
+
const jobs_running = w.jobs_running ? ` · ${w.jobs_running} running` : '';
|
|
546
|
+
lines.push(` ${w.instance.padEnd(24)} ${status.padEnd(10)} ${model}${jobs_running}`);
|
|
185
547
|
}
|
|
186
548
|
|
|
187
549
|
// Recent jobs
|
|
188
|
-
const jList =
|
|
550
|
+
const jList = jobs.data?.jobs || (Array.isArray(jobs.data) ? jobs.data : []);
|
|
189
551
|
lines.push(`\n### Recent Jobs`);
|
|
190
|
-
|
|
191
|
-
lines.push(
|
|
552
|
+
if (jList.length === 0) {
|
|
553
|
+
lines.push(' No recent jobs.');
|
|
554
|
+
}
|
|
555
|
+
for (const j of jList) {
|
|
556
|
+
const cost = j.cost_usd ? ` · $${j.cost_usd.toFixed(4)}` : '';
|
|
557
|
+
const dur = j.duration_ms ? ` · ${(j.duration_ms / 1000).toFixed(1)}s` : '';
|
|
558
|
+
lines.push(` #${String(j.id).padEnd(6)} ${j.status.padEnd(10)} → ${(j.assigned_to || 'auto').padEnd(20)} ${j.prompt?.substring(0, 50)}...${cost}${dur}`);
|
|
192
559
|
}
|
|
193
560
|
|
|
194
561
|
// Schedules
|
|
@@ -196,22 +563,433 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
196
563
|
if (sList.length > 0) {
|
|
197
564
|
lines.push(`\n### Schedules (${sList.length})`);
|
|
198
565
|
for (const s of sList) {
|
|
199
|
-
|
|
566
|
+
const next = s.next_run ? ` · next: ${s.next_run}` : '';
|
|
567
|
+
lines.push(` ${(s.name || s.id).padEnd(30)} ${(s.human_schedule || s.cron_expr).padEnd(25)} → ${s.assigned_to || 'auto'} (${s.run_count} runs)${next}`);
|
|
200
568
|
}
|
|
201
569
|
}
|
|
202
570
|
|
|
203
571
|
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
204
572
|
}
|
|
205
573
|
|
|
574
|
+
// ── modelreins_recruit ────────────────────────────────────────────────────
|
|
575
|
+
|
|
576
|
+
if (name === 'modelreins_recruit') {
|
|
577
|
+
const { worker_name, providers, frontend = 'both' } = args;
|
|
578
|
+
|
|
579
|
+
// ── Provider catalog ──────────────────────────────────────────────────
|
|
580
|
+
//
|
|
581
|
+
// Each entry describes how to detect the provider, what model options
|
|
582
|
+
// exist, and where to get credentials. The recruit prompt uses this to
|
|
583
|
+
// generate accurate, actionable instructions for any machine.
|
|
584
|
+
|
|
585
|
+
const ALL_PROVIDERS = {
|
|
586
|
+
claude: {
|
|
587
|
+
label: 'Claude (Anthropic)',
|
|
588
|
+
key: 'claude',
|
|
589
|
+
env: 'ANTHROPIC_API_KEY',
|
|
590
|
+
detect_env: true,
|
|
591
|
+
detect_cmd: null,
|
|
592
|
+
models: [
|
|
593
|
+
'claude-haiku-4-5 — fast, cheap (~$0.001/job for simple tasks)',
|
|
594
|
+
'claude-sonnet-4-6 — balanced (best for code, default choice)',
|
|
595
|
+
'claude-opus-4-6 — most capable (architecture, complex reasoning)',
|
|
596
|
+
],
|
|
597
|
+
note: 'Get your key at console.anthropic.com → API Keys.',
|
|
598
|
+
cost: 'pay-per-token',
|
|
599
|
+
},
|
|
600
|
+
openai: {
|
|
601
|
+
label: 'OpenAI',
|
|
602
|
+
key: 'openai',
|
|
603
|
+
env: 'OPENAI_API_KEY',
|
|
604
|
+
detect_env: true,
|
|
605
|
+
detect_cmd: null,
|
|
606
|
+
models: [
|
|
607
|
+
'gpt-4o-mini — fast, cheap',
|
|
608
|
+
'gpt-4o — balanced',
|
|
609
|
+
'o1-mini — reasoning tasks',
|
|
610
|
+
'o3-mini — advanced reasoning',
|
|
611
|
+
],
|
|
612
|
+
note: 'Get your key at platform.openai.com → API Keys.',
|
|
613
|
+
cost: 'pay-per-token',
|
|
614
|
+
},
|
|
615
|
+
gemini: {
|
|
616
|
+
label: 'Google Gemini',
|
|
617
|
+
key: 'gemini',
|
|
618
|
+
env: 'GEMINI_API_KEY',
|
|
619
|
+
detect_env: true,
|
|
620
|
+
detect_cmd: null,
|
|
621
|
+
models: [
|
|
622
|
+
'gemini-1.5-flash — fast, cheap, has free tier',
|
|
623
|
+
'gemini-1.5-pro — balanced',
|
|
624
|
+
'gemini-2.0-flash — latest fast model',
|
|
625
|
+
'gemini-2.5-pro — most capable',
|
|
626
|
+
],
|
|
627
|
+
note: 'Get your key at aistudio.google.com → Get API Key. Has a generous free tier.',
|
|
628
|
+
cost: 'pay-per-token (free tier available)',
|
|
629
|
+
},
|
|
630
|
+
openrouter: {
|
|
631
|
+
label: 'OpenRouter',
|
|
632
|
+
key: 'openrouter',
|
|
633
|
+
env: 'OPENROUTER_API_KEY',
|
|
634
|
+
detect_env: true,
|
|
635
|
+
detect_cmd: null,
|
|
636
|
+
models: [
|
|
637
|
+
'200+ models on one key — Claude, GPT, Gemma, Mistral, Llama, Qwen, and more.',
|
|
638
|
+
'Useful when you want provider redundancy or access to models not on direct APIs.',
|
|
639
|
+
],
|
|
640
|
+
note: 'Get your key at openrouter.ai → Keys. Pay-per-token, no minimums, no subscriptions.',
|
|
641
|
+
cost: 'pay-per-token, no minimum',
|
|
642
|
+
},
|
|
643
|
+
ollama: {
|
|
644
|
+
label: 'Ollama (local, completely free)',
|
|
645
|
+
key: 'ollama',
|
|
646
|
+
env: null,
|
|
647
|
+
detect_env: false,
|
|
648
|
+
detect_cmd: 'curl -s http://localhost:11434/api/tags 2>/dev/null',
|
|
649
|
+
models: [
|
|
650
|
+
'Any model you have pulled. Run `ollama list` to see installed models.',
|
|
651
|
+
'Good starting models: llama3.2, qwen2.5-coder, gemma3, devstral',
|
|
652
|
+
'Install models: ollama pull llama3.2',
|
|
653
|
+
],
|
|
654
|
+
note: 'No API key. Install Ollama from ollama.ai, then pull any model. Runs entirely on your GPU/CPU.',
|
|
655
|
+
cost: 'free — runs on your hardware',
|
|
656
|
+
},
|
|
657
|
+
lmstudio: {
|
|
658
|
+
label: 'LM Studio (local, completely free)',
|
|
659
|
+
key: 'lmstudio',
|
|
660
|
+
env: null,
|
|
661
|
+
detect_env: false,
|
|
662
|
+
detect_cmd: 'curl -s http://localhost:1234/v1/models 2>/dev/null',
|
|
663
|
+
models: [
|
|
664
|
+
'Any model loaded in LM Studio. The local server must be enabled (green toggle in LM Studio).',
|
|
665
|
+
'ModelReins connects to LM Studio\'s OpenAI-compatible API at localhost:1234.',
|
|
666
|
+
],
|
|
667
|
+
note: 'No API key. Download LM Studio from lmstudio.ai, load a model, enable local server.',
|
|
668
|
+
cost: 'free — runs on your hardware',
|
|
669
|
+
},
|
|
670
|
+
'1minai': {
|
|
671
|
+
label: '1minAI (economy cloud)',
|
|
672
|
+
key: '1minai',
|
|
673
|
+
env: 'ONEMINAI_API_KEY',
|
|
674
|
+
detect_env: true,
|
|
675
|
+
detect_cmd: null,
|
|
676
|
+
models: [
|
|
677
|
+
'Economy-tier cloud inference. Best for high-volume, low-complexity batch jobs.',
|
|
678
|
+
'Ultra-cheap per-token pricing.',
|
|
679
|
+
],
|
|
680
|
+
note: 'Get your key at 1min.ai.',
|
|
681
|
+
cost: 'pay-per-token, economy pricing',
|
|
682
|
+
},
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
const providerKeys = (providers && providers.length > 0)
|
|
686
|
+
? providers.filter(p => ALL_PROVIDERS[p])
|
|
687
|
+
: Object.keys(ALL_PROVIDERS);
|
|
688
|
+
|
|
689
|
+
const selectedProviders = providerKeys.map(k => ALL_PROVIDERS[k]);
|
|
690
|
+
|
|
691
|
+
// ── Build prompt sections ─────────────────────────────────────────────
|
|
692
|
+
|
|
693
|
+
const workerNameHint = worker_name || '<auto-detect from hostname>';
|
|
694
|
+
|
|
695
|
+
// Provider detection block
|
|
696
|
+
const detectionSteps = selectedProviders.map(p => {
|
|
697
|
+
if (p.detect_env) {
|
|
698
|
+
return ` ${p.label}: check if ${p.env} is in the environment`;
|
|
699
|
+
}
|
|
700
|
+
return ` ${p.label}: run \`${p.detect_cmd}\` — non-empty response means it is running`;
|
|
701
|
+
}).join('\n');
|
|
702
|
+
|
|
703
|
+
// Provider menu (shown when nothing is auto-detected)
|
|
704
|
+
const providerMenu = selectedProviders.map((p, i) => {
|
|
705
|
+
const envLine = p.env
|
|
706
|
+
? ` Set env var: ${p.env}=<your-key>`
|
|
707
|
+
: ' No API key required.';
|
|
708
|
+
const modelLines = p.models.map(m => ` - ${m}`).join('\n');
|
|
709
|
+
return ` ${i + 1}. ${p.label} (${p.cost})\n${modelLines}\n${envLine}\n ${p.note}`;
|
|
710
|
+
}).join('\n\n');
|
|
711
|
+
|
|
712
|
+
// MCP Channel config section — varies by frontend target
|
|
713
|
+
const mcpConfigSection = (() => {
|
|
714
|
+
const npxCmd = `npx -y @mediagato/modelreins-channel`;
|
|
715
|
+
const envBlock = `{
|
|
716
|
+
"MODELREINS_URL": "<your dashboard URL>",
|
|
717
|
+
"MODELREINS_TOKEN": "<your worker token>",
|
|
718
|
+
"MODELREINS_WORKER": "${workerNameHint}"
|
|
719
|
+
}`;
|
|
720
|
+
|
|
721
|
+
const cliConfig = `**Claude Code CLI — project-scoped (.mcp.json in your repo root)**
|
|
722
|
+
|
|
723
|
+
Create a file named \`.mcp.json\` in your project root:
|
|
724
|
+
\`\`\`json
|
|
725
|
+
{
|
|
726
|
+
"mcpServers": {
|
|
727
|
+
"modelreins": {
|
|
728
|
+
"command": "npx",
|
|
729
|
+
"args": ["-y", "@mediagato/modelreins-channel"],
|
|
730
|
+
"env": ${envBlock}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
\`\`\`
|
|
735
|
+
|
|
736
|
+
Or add it globally (applies to every Claude Code session on this machine):
|
|
737
|
+
\`\`\`bash
|
|
738
|
+
claude mcp add modelreins \\
|
|
739
|
+
-e MODELREINS_URL=<your-dashboard-url> \\
|
|
740
|
+
-e MODELREINS_TOKEN=<your-token> \\
|
|
741
|
+
-e MODELREINS_WORKER=${workerNameHint} \\
|
|
742
|
+
-- ${npxCmd}
|
|
743
|
+
\`\`\``;
|
|
744
|
+
|
|
745
|
+
const vscodeConfig = `**Claude Code VSCode Extension**
|
|
746
|
+
|
|
747
|
+
Option A — workspace-scoped: create \`.mcp.json\` in your repo root (same format as CLI above).
|
|
748
|
+
|
|
749
|
+
Option B — user-scoped: open VSCode settings.json
|
|
750
|
+
(Cmd/Ctrl+Shift+P → "Open User Settings (JSON)") and add:
|
|
751
|
+
|
|
752
|
+
\`\`\`json
|
|
753
|
+
"claude.mcpServers": {
|
|
754
|
+
"modelreins": {
|
|
755
|
+
"command": "npx",
|
|
756
|
+
"args": ["-y", "@mediagato/modelreins-channel"],
|
|
757
|
+
"env": ${envBlock}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
\`\`\`
|
|
761
|
+
|
|
762
|
+
After saving settings.json, reload VSCode (Cmd/Ctrl+Shift+P → "Developer: Reload Window").
|
|
763
|
+
The modelreins server will appear in the Claude panel. The worker goes online within ~30 seconds.
|
|
764
|
+
|
|
765
|
+
Tip: name VSCode workers "vscode-<hostname>" so they are easy to identify in the dashboard
|
|
766
|
+
and can be targeted specifically from dispatch (they have richer project context than headless workers).`;
|
|
767
|
+
|
|
768
|
+
if (frontend === 'cli') return cliConfig;
|
|
769
|
+
if (frontend === 'vscode') return vscodeConfig;
|
|
770
|
+
return `${cliConfig}\n\n---\n\n${vscodeConfig}`;
|
|
771
|
+
})();
|
|
772
|
+
|
|
773
|
+
// ── Final prompt assembly ─────────────────────────────────────────────
|
|
774
|
+
|
|
775
|
+
const prompt = `# ModelReins Worker Setup
|
|
776
|
+
|
|
777
|
+
You are setting up a ModelReins worker on this machine. Complete every step automatically.
|
|
778
|
+
Only pause to ask for required credentials if they are not already in the environment.
|
|
779
|
+
Do not ask for confirmation between steps — just work through them in order.
|
|
780
|
+
|
|
781
|
+
---
|
|
782
|
+
|
|
783
|
+
## Step 1 — Check Node.js
|
|
784
|
+
|
|
785
|
+
Run: \`node --version\`
|
|
786
|
+
|
|
787
|
+
If Node.js 18 or higher is not installed:
|
|
788
|
+
- macOS/Linux: install via https://nodejs.org or run \`nvm install 18\`
|
|
789
|
+
- Windows: download installer from https://nodejs.org
|
|
790
|
+
Stop here and tell the user to restart after installing Node.
|
|
791
|
+
|
|
792
|
+
---
|
|
793
|
+
|
|
794
|
+
## Step 2 — Detect available AI providers
|
|
795
|
+
|
|
796
|
+
Run these checks and record which providers are available:
|
|
797
|
+
|
|
798
|
+
${detectionSteps}
|
|
799
|
+
|
|
800
|
+
**If one or more providers are detected**, use the first detected one and proceed to Step 3.
|
|
801
|
+
Tell the user which provider was found and which model you will configure.
|
|
802
|
+
|
|
803
|
+
**If no providers are detected**, present this menu and ask the user to choose one:
|
|
804
|
+
|
|
805
|
+
${providerMenu}
|
|
806
|
+
|
|
807
|
+
Once the user chooses, collect the API key if required (ask in a single message).
|
|
808
|
+
|
|
809
|
+
---
|
|
810
|
+
|
|
811
|
+
## Step 3 — Collect connection details (ask in ONE message if not already known)
|
|
812
|
+
|
|
813
|
+
You need two values from the ModelReins dashboard:
|
|
814
|
+
1. **Dashboard URL** — e.g. \`https://app.modelreins.com\` or \`http://192.168.x.x:8484\`
|
|
815
|
+
2. **Worker token** — found in the dashboard under Workers → Add Worker
|
|
816
|
+
|
|
817
|
+
---
|
|
818
|
+
|
|
819
|
+
## Step 4 — Install the worker package
|
|
820
|
+
|
|
821
|
+
\`\`\`bash
|
|
822
|
+
npm install -g @mediagato/modelreins-worker
|
|
823
|
+
\`\`\`
|
|
824
|
+
|
|
825
|
+
---
|
|
826
|
+
|
|
827
|
+
## Step 5 — Write .env
|
|
828
|
+
|
|
829
|
+
Create a \`.env\` file in the current directory with these values:
|
|
830
|
+
|
|
831
|
+
\`\`\`env
|
|
832
|
+
# ModelReins connection
|
|
833
|
+
MODELREINS_URL=<dashboard URL from step 3>
|
|
834
|
+
MODELREINS_TOKEN=<worker token from step 3>
|
|
835
|
+
MODELREINS_WORKER_NAME=${workerNameHint}
|
|
836
|
+
|
|
837
|
+
# AI provider (set the one detected in step 2)
|
|
838
|
+
MODELREINS_PROVIDER=<provider key: claude | openai | gemini | openrouter | ollama | lmstudio | 1minai>
|
|
839
|
+
|
|
840
|
+
# API keys — include only the one you are using
|
|
841
|
+
# ANTHROPIC_API_KEY=...
|
|
842
|
+
# OPENAI_API_KEY=...
|
|
843
|
+
# GEMINI_API_KEY=...
|
|
844
|
+
# OPENROUTER_API_KEY=...
|
|
845
|
+
# ONEMINAI_API_KEY=...
|
|
846
|
+
|
|
847
|
+
# Local model options (only if using ollama or lmstudio)
|
|
848
|
+
# OLLAMA_MODEL=llama3.2
|
|
849
|
+
# LMSTUDIO_MODEL=<model name shown in LM Studio>
|
|
850
|
+
\`\`\`
|
|
851
|
+
|
|
852
|
+
---
|
|
853
|
+
|
|
854
|
+
## Step 6 — Test the connection
|
|
855
|
+
|
|
856
|
+
\`\`\`bash
|
|
857
|
+
npx @mediagato/modelreins-worker --test
|
|
858
|
+
\`\`\`
|
|
859
|
+
|
|
860
|
+
If the test fails:
|
|
861
|
+
- Check that MODELREINS_URL is reachable (try opening it in a browser)
|
|
862
|
+
- Verify the token matches one in the dashboard
|
|
863
|
+
- For Ollama/LM Studio: confirm the local server is running
|
|
864
|
+
- Fix the issue and re-run the test before continuing
|
|
865
|
+
|
|
866
|
+
---
|
|
867
|
+
|
|
868
|
+
## Step 7 — Register as a system service (survives reboots)
|
|
869
|
+
|
|
870
|
+
Detect the OS and create the appropriate service definition:
|
|
871
|
+
|
|
872
|
+
**macOS** — create ~/Library/LaunchAgents/com.modelreins.worker.plist:
|
|
873
|
+
\`\`\`xml
|
|
874
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
875
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
876
|
+
<plist version="1.0">
|
|
877
|
+
<dict>
|
|
878
|
+
<key>Label</key><string>com.modelreins.worker</string>
|
|
879
|
+
<key>ProgramArguments</key>
|
|
880
|
+
<array><string>npx</string><string>@mediagato/modelreins-worker</string></array>
|
|
881
|
+
<key>EnvironmentVariables</key>
|
|
882
|
+
<dict>
|
|
883
|
+
<key>MODELREINS_URL</key><string><dashboard-url></string>
|
|
884
|
+
<key>MODELREINS_TOKEN</key><string><token></string>
|
|
885
|
+
<key>MODELREINS_WORKER_NAME</key><string>${workerNameHint}</string>
|
|
886
|
+
<key>MODELREINS_PROVIDER</key><string><provider></string>
|
|
887
|
+
</dict>
|
|
888
|
+
<key>RunAtLoad</key><true/>
|
|
889
|
+
<key>KeepAlive</key><true/>
|
|
890
|
+
</dict>
|
|
891
|
+
</plist>
|
|
892
|
+
\`\`\`
|
|
893
|
+
Then run: \`launchctl load ~/Library/LaunchAgents/com.modelreins.worker.plist\`
|
|
894
|
+
|
|
895
|
+
**Linux (systemd)** — create /etc/systemd/system/modelreins-worker.service:
|
|
896
|
+
\`\`\`ini
|
|
897
|
+
[Unit]
|
|
898
|
+
Description=ModelReins Worker
|
|
899
|
+
After=network.target
|
|
900
|
+
|
|
901
|
+
[Service]
|
|
902
|
+
Type=simple
|
|
903
|
+
EnvironmentFile=<path-to-.env>
|
|
904
|
+
ExecStart=npx @mediagato/modelreins-worker
|
|
905
|
+
Restart=always
|
|
906
|
+
RestartSec=10
|
|
907
|
+
|
|
908
|
+
[Install]
|
|
909
|
+
WantedBy=multi-user.target
|
|
910
|
+
\`\`\`
|
|
911
|
+
Then run:
|
|
912
|
+
\`\`\`bash
|
|
913
|
+
systemctl daemon-reload
|
|
914
|
+
systemctl enable modelreins-worker
|
|
915
|
+
systemctl start modelreins-worker
|
|
916
|
+
\`\`\`
|
|
917
|
+
|
|
918
|
+
**Windows** — create a Task Scheduler entry:
|
|
919
|
+
\`\`\`powershell
|
|
920
|
+
$action = New-ScheduledTaskAction -Execute "npx" -Argument "@mediagato/modelreins-worker" -WorkingDirectory (Get-Location)
|
|
921
|
+
$trigger = New-ScheduledTaskTrigger -AtLogOn
|
|
922
|
+
Register-ScheduledTask -TaskName "ModelReins Worker" -Action $action -Trigger $trigger -RunLevel Highest
|
|
923
|
+
\`\`\`
|
|
924
|
+
|
|
925
|
+
---
|
|
926
|
+
|
|
927
|
+
## Step 8 — Install MCP Channel (optional but recommended)
|
|
928
|
+
|
|
929
|
+
The MCP Channel turns Claude Code itself into a fleet worker — your IDE session
|
|
930
|
+
receives and executes jobs from the dashboard with full local file access.
|
|
931
|
+
|
|
932
|
+
${mcpConfigSection}
|
|
933
|
+
|
|
934
|
+
---
|
|
935
|
+
|
|
936
|
+
## Step 9 — Confirm
|
|
937
|
+
|
|
938
|
+
Tell the user:
|
|
939
|
+
- Worker name registered
|
|
940
|
+
- Provider and model configured
|
|
941
|
+
- Dashboard URL to watch it come online
|
|
942
|
+
- Background service status (running / enabled / failed)
|
|
943
|
+
- MCP Channel: installed or skipped
|
|
944
|
+
|
|
945
|
+
The worker should appear as online in the ModelReins dashboard within 30 seconds.
|
|
946
|
+
The MCP Channel (if installed) will appear as a separate worker with the name you configured.`;
|
|
947
|
+
|
|
948
|
+
return {
|
|
949
|
+
content: [{
|
|
950
|
+
type: 'text',
|
|
951
|
+
text: [
|
|
952
|
+
`## ModelReins Worker Bootstrap Prompt`,
|
|
953
|
+
``,
|
|
954
|
+
`Paste this into any Claude Code session (CLI or VSCode) to recruit a new fleet member.`,
|
|
955
|
+
`Frontend target: ${frontend} | Providers: ${providerKeys.join(', ')}`,
|
|
956
|
+
`Dashboard: ${BASE_URL}`,
|
|
957
|
+
``,
|
|
958
|
+
`---`,
|
|
959
|
+
``,
|
|
960
|
+
prompt,
|
|
961
|
+
].join('\n'),
|
|
962
|
+
}],
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
|
|
206
966
|
throw new Error(`Unknown tool: ${name}`);
|
|
207
967
|
});
|
|
208
968
|
|
|
209
|
-
// ──
|
|
969
|
+
// ── Transport ─────────────────────────────────────────────────────────────────
|
|
970
|
+
//
|
|
971
|
+
// StdioServerTransport means Claude Code spawns this process and communicates
|
|
972
|
+
// over stdin/stdout. No ports, no HTTP server — the channel is entirely local
|
|
973
|
+
// from the OS's perspective.
|
|
210
974
|
|
|
211
975
|
await mcp.connect(new StdioServerTransport());
|
|
212
976
|
|
|
213
|
-
// ──
|
|
977
|
+
// ── Background Loop ───────────────────────────────────────────────────────────
|
|
978
|
+
//
|
|
979
|
+
// Two independent intervals run after MCP connects:
|
|
980
|
+
// heartbeat — tells ModelReins this worker is alive (every 30s)
|
|
981
|
+
// pollJobs — checks for assigned work (every POLL_MS)
|
|
982
|
+
//
|
|
983
|
+
// Both are fire-and-forget. Network errors are caught inside `api()` and
|
|
984
|
+
// silently dropped — a missed heartbeat is not fatal, and the next one will
|
|
985
|
+
// re-register the worker if presence expired.
|
|
214
986
|
|
|
987
|
+
/**
|
|
988
|
+
* Send a presence update to ModelReins.
|
|
989
|
+
* If currentJobId is set, reports "busy". Otherwise "active".
|
|
990
|
+
* Also reports model, tags, and worker type so the dashboard can display
|
|
991
|
+
* full worker context without a separate lookup.
|
|
992
|
+
*/
|
|
215
993
|
async function heartbeat() {
|
|
216
994
|
await api('PUT', '/presence', {
|
|
217
995
|
instance: WORKER_NAME,
|
|
@@ -219,43 +997,56 @@ async function heartbeat() {
|
|
|
219
997
|
project: 'channel',
|
|
220
998
|
worker_type: 'channel',
|
|
221
999
|
model: WORKER_MODEL,
|
|
222
|
-
|
|
1000
|
+
tags: WORKER_TAGS,
|
|
1001
|
+
details: currentJobId ? `working on job #${currentJobId}` : 'idle, polling',
|
|
223
1002
|
});
|
|
224
1003
|
}
|
|
225
1004
|
|
|
1005
|
+
/**
|
|
1006
|
+
* Poll for the next assigned job and inject it into Claude's context.
|
|
1007
|
+
* No-ops if a job is already running (one job at a time per channel session).
|
|
1008
|
+
*
|
|
1009
|
+
* When a job is found:
|
|
1010
|
+
* 1. Claims it by setting status → "running" (prevents other workers stealing it)
|
|
1011
|
+
* 2. Sends a `notifications/claude/channel` notification with the job prompt
|
|
1012
|
+
* 3. Claude's context window receives the job as a <channel> tag
|
|
1013
|
+
* 4. Claude reads it, executes the task, calls modelreins_complete when done
|
|
1014
|
+
*/
|
|
226
1015
|
async function pollJobs() {
|
|
227
|
-
|
|
1016
|
+
// One job at a time — don't accept new work while something is running.
|
|
1017
|
+
if (currentJobId) return;
|
|
228
1018
|
|
|
229
|
-
const res = await api('GET', `/jobs?status=pending&assigned_to=${WORKER_NAME}&limit=1`);
|
|
1019
|
+
const res = await api('GET', `/jobs?status=pending&assigned_to=${encodeURIComponent(WORKER_NAME)}&limit=1`);
|
|
230
1020
|
const jobs = res.data?.jobs || (Array.isArray(res.data) ? res.data : []);
|
|
231
1021
|
|
|
232
|
-
if (jobs.length
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
1022
|
+
if (jobs.length === 0) return;
|
|
1023
|
+
|
|
1024
|
+
const job = jobs[0];
|
|
1025
|
+
currentJobId = job.id;
|
|
1026
|
+
|
|
1027
|
+
// Claim the job immediately so no other worker picks it up.
|
|
1028
|
+
await api('PUT', `/jobs/${job.id}`, { status: 'running' });
|
|
1029
|
+
|
|
1030
|
+
// Inject the job prompt into Claude's context via the channel notification.
|
|
1031
|
+
// Claude Code intercepts this and presents it as a <channel> tag in the session.
|
|
1032
|
+
await mcp.notification({
|
|
1033
|
+
method: 'notifications/claude/channel',
|
|
1034
|
+
params: {
|
|
1035
|
+
content: job.prompt,
|
|
1036
|
+
meta: {
|
|
1037
|
+
job_id: String(job.id),
|
|
1038
|
+
priority: String(job.priority ?? 0),
|
|
1039
|
+
model_preference: job.model_preference || '',
|
|
1040
|
+
working_dir: job.working_dir || '',
|
|
1041
|
+
source: 'modelreins-dashboard',
|
|
251
1042
|
},
|
|
252
|
-
}
|
|
253
|
-
}
|
|
1043
|
+
},
|
|
1044
|
+
});
|
|
254
1045
|
}
|
|
255
1046
|
|
|
256
|
-
//
|
|
257
|
-
setInterval(heartbeat,
|
|
1047
|
+
// Kick off the background loops.
|
|
1048
|
+
setInterval(heartbeat, 30_000);
|
|
258
1049
|
setInterval(pollJobs, POLL_MS);
|
|
259
1050
|
|
|
260
|
-
// Initial heartbeat
|
|
1051
|
+
// Initial heartbeat fires immediately so the worker shows as online right away.
|
|
261
1052
|
heartbeat();
|
package/package.json
CHANGED