@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.
@@ -1,35 +1,166 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * ModelReins MCP Channel — bridges a Claude Code session to ModelReins.
3
+ * @module modelreins-channel
4
+ * @version 1.1.0
4
5
  *
5
- * When installed in .mcp.json, this turns any Claude Code session into a
6
- * ModelReins worker with full local access (files, git, SSH, env vars).
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
- * The channel:
9
- * 1. Registers with ModelReins as a worker (PUT /presence)
10
- * 2. Polls for assigned jobs (GET /jobs?assigned_to=<name>&status=pending)
11
- * 3. Pushes job prompts into Claude's session via MCP notifications
12
- * 4. Captures Claude's responses and posts them back (POST /jobs/{id}/output)
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
- * Config (env vars or passed via args):
15
- * MODELREINS_URL — ModelReins server URL (default: http://localhost:8484)
16
- * MODELREINS_TOKEN — Auth token
17
- * MODELREINS_WORKER — Worker name (default: channel-<hostname>)
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": "node",
24
- * "args": ["path/to/modelreins-channel.mjs"],
35
+ * "command": "npx",
36
+ * "args": ["-y", "@mediagato/modelreins-channel"],
25
37
  * "env": {
26
- * "MODELREINS_URL": "https://app.modelreins.com",
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
- // ── Config ──────────────────────────────────────────────────────────────────
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 helpers ────────────────────────────────────────────────────────────
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.0.0' },
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 you receive a job, complete the task described in the prompt using your full local capabilities.',
91
- 'When done, use the modelreins_complete tool to report the result back.',
92
- 'If you cannot complete a job, use modelreins_complete with success=false and explain why.',
93
- 'Between jobs, you remain a normal Claude Code session — the user can still interact with you directly.',
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: report job completion back to ModelReins
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: 'Report job completion back to ModelReins dashboard. Call this when you finish a dispatched task.',
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: { type: 'string', description: 'The job ID from the <channel> tag' },
108
- summary: { type: 'string', description: 'Brief summary of what was done' },
109
- success: { type: 'boolean', description: 'Whether the task succeeded' },
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: 'Dispatch a new job to another ModelReins worker from this session.',
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: { type: 'string', description: 'The task prompt' },
121
- assigned_to: { type: 'string', description: 'Worker name or "auto"' },
122
- model_preference: { type: 'string', description: 'Model: auto/haiku/sonnet/opus' },
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: 'Check ModelReins fleet status — see active workers, running jobs, and schedules.',
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
- // Post completion output
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
- // Mark job done or failed
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
- return { content: [{ type: 'text', text: `Job #${job_id} ${success ? 'completed' : 'failed'}. Result posted to ModelReins.` }] };
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
- if (res.data?.success) {
166
- return { content: [{ type: 'text', text: `Dispatched job #${res.data.id} to ${res.data.assigned_to}` }] };
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
- return { content: [{ type: 'text', text: `Dispatch failed: ${JSON.stringify(res.data)}` }] };
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 = ['## ModelReins Fleet Status\n'];
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
- lines.push(`- ${w.instance}: ${w.status} (${w.model || '?'})`);
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 = workers.data?.jobs || jobs.data?.jobs || [];
550
+ const jList = jobs.data?.jobs || (Array.isArray(jobs.data) ? jobs.data : []);
189
551
  lines.push(`\n### Recent Jobs`);
190
- for (const j of jList.slice(0, 5)) {
191
- lines.push(`- #${j.id} ${j.status} → ${j.assigned_to} (${j.prompt?.substring(0, 60)}...)`);
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
- lines.push(`- ${s.name}: ${s.human_schedule || s.cron_expr} → ${s.assigned_to || 'auto'} (runs: ${s.run_count})`);
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
- // ── Connect ─────────────────────────────────────────────────────────────────
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
- // ── Heartbeat + Job Polling Loop ────────────────────────────────────────────
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
- details: currentJobId ? `working on #${currentJobId}` : 'idle, polling',
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
- if (currentJobId) return; // already working on something
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 > 0) {
233
- const job = jobs[0];
234
- currentJobId = job.id;
235
-
236
- // Claim the job
237
- await api('PUT', `/jobs/${job.id}`, { status: 'running' });
238
-
239
- // Push into Claude's session
240
- await mcp.notification({
241
- method: 'notifications/claude/channel',
242
- params: {
243
- content: job.prompt,
244
- meta: {
245
- job_id: String(job.id),
246
- priority: String(job.priority || 0),
247
- model_preference: job.model_preference || '',
248
- working_dir: job.working_dir || '',
249
- source: 'modelreins-dashboard',
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
- // Main loop
257
- setInterval(heartbeat, 30000);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mediagato/modelreins-channel",
3
- "version": "1.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "ModelReins MCP Channel — turn any Claude Code session into a ModelReins worker",
5
5
  "type": "module",
6
6
  "main": "modelreins-channel.mjs",