@maka/maka-cli 5.141.0 → 5.143.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,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.141.0",
3
+ "version": "5.143.0",
4
4
  "type": "module",
5
5
  "summary": "A command line tool for scaffolding Meteor 3.x applications using either React.",
6
6
  "description": "A command line tool for scaffolding Meteor 3.x applications using React.",
@@ -49,6 +49,7 @@
49
49
  "@inquirer/prompts": "^8.5.2",
50
50
  "@maka/meteor-sdk": "^0.2.13",
51
51
  "@maka/tar": "^6.2.2",
52
+ "@modelcontextprotocol/sdk": "^1.30.0",
52
53
  "blessed": "^0.1.81",
53
54
  "chalk": "^4.1.2",
54
55
  "change-case": "^5.4.4",
@@ -70,7 +71,8 @@
70
71
  "tar": "^7.4.3",
71
72
  "uuid": "^14.0.1",
72
73
  "ws": "^8.21.3",
73
- "xtend": "^4.0.2"
74
+ "xtend": "^4.0.2",
75
+ "zod": "^4.5.4"
74
76
  },
75
77
  "devDependencies": {
76
78
  "@aws-sdk/types": "^3.974.4",
@@ -9,9 +9,8 @@ const EnvCommand = Command.create({
9
9
  description: `
10
10
  Set or read secrets (KEY VALUE pairs) in a config/<env>/process.env file.
11
11
 
12
- Values are encrypted in place with dotenvx (https://dotenvx.com) for every
13
- environment except local, which stays plaintext for easy local development.
14
- The encrypted process.env and its DOTENV_PUBLIC_KEY are safe to commit; the
12
+ Values are encrypted in place with dotenvx (https://dotenvx.com). The
13
+ encrypted process.env and its DOTENV_PUBLIC_KEY are safe to commit; the
15
14
  sibling .env.keys file holding the private key is gitignored and must never
16
15
  be committed.
17
16
  `,
@@ -23,9 +23,8 @@ Command.create({
23
23
  a MongoDB connection string with a query string:
24
24
  maka env:set --env production MONGO_URL "mongodb+srv://user:pass@host/db?retryWrites=true&w=majority"
25
25
 
26
- For every environment except local, the value is encrypted in place with
27
- dotenvx -- the file's DOTENV_PUBLIC_KEY is generated on first use if it
28
- doesn't already exist. local stays plaintext for easy local development.
26
+ The value is encrypted in place with dotenvx -- the file's DOTENV_PUBLIC_KEY
27
+ is generated on first use if it doesn't already exist.
29
28
  `,
30
29
  examples: [
31
30
  'maka env:set --env production MONGO_URL mongodb://localhost:27017/app',
@@ -47,8 +46,8 @@ Command.create({
47
46
  for (let i = 0; i < args.length; i += 2) {
48
47
  const key = args[i];
49
48
  const value = args[i + 1];
50
- await this.dotenvx.set(key, value, processEnvPath, { plain: env === 'local' });
51
- Log.success(`Set ${key} in config/${env}/process.env${(env === 'local') ? '' : ' (encrypted)'}`);
49
+ await this.dotenvx.set(key, value, processEnvPath);
50
+ Log.success(`Set ${key} in config/${env}/process.env (encrypted)`);
52
51
  }
53
52
  }
54
53
  catch (e) {
@@ -115,8 +115,15 @@ export async function fetchReviewQueue() {
115
115
  }
116
116
  /** Answer, ask, or annotate one item. `kind` defaults to a plain note;
117
117
  * a play-tester replying to a question posts an 'answer'. */
118
- export async function postBacklogComment(itemId, text, opts = {}) {
119
- const token = authToken();
118
+ export async function postBacklogComment(itemId, text, opts = {},
119
+ // ADMIN OVERRIDE (2026-09-08). The MCP admin server (mcp.sub.cmd.ts)
120
+ // reuses this for notes/questions posted as the service account,
121
+ // which holds no entry in ~/.maka/global-config.json for authToken()
122
+ // to find. Optional and trailing so every existing human-CLI call
123
+ // site is untouched -- omitted, this is exactly the function it always
124
+ // was.
125
+ tokenOverride) {
126
+ const token = tokenOverride ?? authToken();
120
127
  if (!token)
121
128
  return 'no-login';
122
129
  try {
@@ -375,8 +382,12 @@ export async function setRole(action, email, role) {
375
382
  * in-review first, then open. `all` asks for everything anyway; a
376
383
  * specific `status` is always honoured exactly.
377
384
  */
378
- export async function fetchBacklog(status, opts = {}) {
379
- const token = authToken();
385
+ export async function fetchBacklog(status, opts = {},
386
+ // ADMIN OVERRIDE, same reasoning as postBacklogComment's: the MCP
387
+ // admin server has no human login for authToken() to find. Optional
388
+ // and trailing so every existing call site is untouched.
389
+ tokenOverride) {
390
+ const token = tokenOverride ?? authToken();
380
391
  if (!token)
381
392
  return { outcome: 'no-login' };
382
393
  try {
@@ -395,4 +406,162 @@ export async function fetchBacklog(status, opts = {}) {
395
406
  return { outcome: 'offline' };
396
407
  }
397
408
  }
409
+ // ---------------------------------------------------------------------
410
+ // ADMIN CALLS (2026-09-08), added for the MCP admin server
411
+ // (sideQuest-backlog-mcp.sub.cmd.ts). Every function below takes its
412
+ // token EXPLICITLY rather than falling back to authToken() -- these are
413
+ // only ever called with the M2M service account's session, never a
414
+ // human's, and a silent fallback to whichever human happens to be
415
+ // logged in on this machine would be exactly the wrong default for an
416
+ // admin action. All of them are the same house shape as everything
417
+ // above: swallow errors, return an outcome, let the caller turn it into
418
+ // prose (or, here, into an MCP tool error).
419
+ // ---------------------------------------------------------------------
420
+ /** One item, in full -- including fields /list withholds from a caller
421
+ * who is not its reporter or an admin. `GET item/:id` is gated the same
422
+ * as /list (mayUseBacklog), so this never needs a special admin path
423
+ * server-side; the token being an admin's is what gets it the fuller
424
+ * read. */
425
+ export async function fetchBacklogItemById(itemId, token) {
426
+ try {
427
+ const res = await request('GET', `/${API_BASE}/item/${encodeURIComponent(itemId)}`, token);
428
+ if (res.statusCode === 401)
429
+ return { outcome: 'expired' };
430
+ if (res.statusCode === 403)
431
+ return { outcome: 'forbidden' };
432
+ if (res.statusCode === 404)
433
+ return { outcome: 'rejected' };
434
+ if (res.statusCode !== 200)
435
+ return { outcome: 'offline' };
436
+ const parsed = parseData(res.data);
437
+ return { outcome: 'ok', item: parsed?.item };
438
+ }
439
+ catch {
440
+ return { outcome: 'offline' };
441
+ }
442
+ }
443
+ /** THE ACTUAL RESOLUTION MOVE: attach evidence and/or advance status.
444
+ * Admin-only server-side (`PATCH item/:id`), and gated there exactly as
445
+ * documented in game-backlog-v1-rest-api.ts -- entering `in-review` or
446
+ * `complete` without commits is refused with a `message` this function
447
+ * passes straight through rather than flattening into a generic
448
+ * "rejected", because that message is the instruction for what to do
449
+ * differently (the MCP tool surfaces it verbatim to whoever is holding
450
+ * the AI's hand at that point). */
451
+ export async function patchBacklogItem(itemId, body, token) {
452
+ try {
453
+ const res = await request('PATCH', `/${API_BASE}/item/${encodeURIComponent(itemId)}`, token, body);
454
+ if (res.statusCode === 200 || res.statusCode === 201) {
455
+ const parsed = parseData(res.data);
456
+ return { outcome: 'ok', status: parsed?.status, commits: parsed?.commits };
457
+ }
458
+ if (res.statusCode === 401)
459
+ return { outcome: 'expired' };
460
+ if (res.statusCode === 403)
461
+ return { outcome: 'forbidden' };
462
+ if (res.statusCode === 400 || res.statusCode === 404) {
463
+ const parsed = parseData(res.data);
464
+ return { outcome: 'rejected', message: parsed?.message };
465
+ }
466
+ return { outcome: 'offline' };
467
+ }
468
+ catch {
469
+ return { outcome: 'offline' };
470
+ }
471
+ }
472
+ /** ASK THE SERVER TO DRAFT A BENCH for one item. Lands in `reproDraft`,
473
+ * never `repro` -- promoting a draft to a real bench is a separate,
474
+ * deliberate PATCH (patchBacklogItem with a `repro` body), same as the
475
+ * web board and the REST route's own doc comment describe. Admin-only,
476
+ * costs a model call server-side. */
477
+ export async function draftBacklogBench(itemId, token, verbs) {
478
+ try {
479
+ const res = await request('POST', `/${API_BASE}/item/${encodeURIComponent(itemId)}/bench-draft`, token, verbs ? { verbs } : undefined);
480
+ if (res.statusCode === 200 || res.statusCode === 201) {
481
+ const parsed = parseData(res.data);
482
+ return { outcome: 'ok', reproDraft: parsed?.reproDraft };
483
+ }
484
+ if (res.statusCode === 401)
485
+ return { outcome: 'expired' };
486
+ if (res.statusCode === 403)
487
+ return { outcome: 'forbidden' };
488
+ if (res.statusCode === 400 || res.statusCode === 404) {
489
+ const parsed = parseData(res.data);
490
+ return { outcome: 'rejected', message: parsed?.message, code: parsed?.code };
491
+ }
492
+ return { outcome: 'offline' };
493
+ }
494
+ catch {
495
+ return { outcome: 'offline' };
496
+ }
497
+ }
498
+ /** THE BATCH FORM -- named ids only, never a selector (see the route's
499
+ * own doc comment on why: a sweep over a spend path is exactly the
500
+ * failure mode the per-item rule exists to prevent). Capped at 20 by
501
+ * the server; this does not re-enforce that limit, so a caller sees the
502
+ * server's own refusal if it is exceeded. */
503
+ export async function draftBacklogBenches(ids, token) {
504
+ try {
505
+ const res = await request('POST', `/${API_BASE}/bench-drafts`, token, { ids });
506
+ if (res.statusCode === 200 || res.statusCode === 201) {
507
+ const parsed = parseData(res.data);
508
+ return { outcome: 'ok', drafted: parsed?.drafted, attempted: parsed?.attempted, results: parsed?.results };
509
+ }
510
+ if (res.statusCode === 401)
511
+ return { outcome: 'expired' };
512
+ if (res.statusCode === 403)
513
+ return { outcome: 'forbidden' };
514
+ if (res.statusCode === 400) {
515
+ const parsed = parseData(res.data);
516
+ return { outcome: 'rejected', message: parsed?.message };
517
+ }
518
+ return { outcome: 'offline' };
519
+ }
520
+ catch {
521
+ return { outcome: 'offline' };
522
+ }
523
+ }
524
+ /** RE-JUDGE ITEMS AGAINST THE RULEBOOKS. `all: true` re-grades the whole
525
+ * board (a model call per item -- see the route's own doc comment on
526
+ * why that is opt-in); the default only catches items whose first pass
527
+ * ran without session context and now has one. */
528
+ export async function revetBacklog(token, opts = {}) {
529
+ try {
530
+ const res = await request('POST', `/${API_BASE}/revet`, token, opts);
531
+ if (res.statusCode === 200 || res.statusCode === 201) {
532
+ const parsed = parseData(res.data);
533
+ return { outcome: 'ok', ...parsed };
534
+ }
535
+ if (res.statusCode === 401)
536
+ return { outcome: 'expired' };
537
+ if (res.statusCode === 403)
538
+ return { outcome: 'forbidden' };
539
+ return { outcome: 'offline' };
540
+ }
541
+ catch {
542
+ return { outcome: 'offline' };
543
+ }
544
+ }
545
+ /** THE M2M TOKEN EXCHANGE. Not gated behind authToken() at all -- there
546
+ * is no Meteor session yet at the point this is called, that is
547
+ * precisely what it establishes. Hits `/api/v1/auth`, not
548
+ * `/api/v1/game-backlog`; `request()` takes a raw path, so this is the
549
+ * one caller in the file that does not go through API_BASE. See
550
+ * utilities/service-account-auth.ts for what obtains the Auth0 token
551
+ * this expects and what caches the result. */
552
+ export async function mintServiceAccountToken(auth0Token) {
553
+ try {
554
+ const res = await request('POST', '/api/v1/auth/m2m-login', '', { auth0Token });
555
+ if (res.statusCode === 200) {
556
+ const parsed = parseData(res.data);
557
+ return { outcome: 'ok', authToken: parsed?.authToken, userId: parsed?.userId, when: parsed?.when };
558
+ }
559
+ if (res.statusCode === 401)
560
+ return { outcome: 'expired' };
561
+ return { outcome: 'offline' };
562
+ }
563
+ catch {
564
+ return { outcome: 'offline' };
565
+ }
566
+ }
398
567
  //# sourceMappingURL=backlog.js.map
@@ -0,0 +1,68 @@
1
+ import https from 'https';
2
+ import { mintServiceAccountToken } from './backlog.js';
3
+ /** Client-credentials grant, direct to Auth0. Throws on any failure --
4
+ * there is no partial-success shape worth returning here, and the
5
+ * caller (ensureServiceSession below) is the one place that decides
6
+ * what a failure means for the MCP server's own state. */
7
+ async function getAuth0AccessToken(creds) {
8
+ const body = new URLSearchParams({
9
+ grant_type: 'client_credentials',
10
+ client_id: creds.clientId,
11
+ client_secret: creds.clientSecret,
12
+ audience: creds.audience,
13
+ }).toString();
14
+ return new Promise((resolve, reject) => {
15
+ const req = https.request({
16
+ hostname: creds.domain,
17
+ port: 443,
18
+ path: '/oauth/token',
19
+ method: 'POST',
20
+ timeout: 10000,
21
+ headers: {
22
+ 'Content-Type': 'application/x-www-form-urlencoded',
23
+ 'Content-Length': Buffer.byteLength(body),
24
+ },
25
+ }, (res) => {
26
+ let data = '';
27
+ res.on('data', (chunk) => { data += chunk; });
28
+ res.on('end', () => {
29
+ if (res.statusCode !== 200) {
30
+ reject(new Error(`Auth0 token request failed (${res.statusCode}): ${data.slice(0, 300)}`));
31
+ return;
32
+ }
33
+ try {
34
+ const parsed = JSON.parse(data);
35
+ if (!parsed.access_token)
36
+ throw new Error('no access_token in Auth0 response');
37
+ resolve(parsed.access_token);
38
+ }
39
+ catch (e) {
40
+ reject(e instanceof Error ? e : new Error(String(e)));
41
+ }
42
+ });
43
+ });
44
+ req.on('timeout', () => req.destroy(new Error('Auth0 token request timeout')));
45
+ req.on('error', reject);
46
+ req.write(body);
47
+ req.end();
48
+ });
49
+ }
50
+ let cachedToken;
51
+ /** THE ONE ENTRY POINT mcp.sub.cmd.ts calls. Returns a Meteor
52
+ * x-auth-token for the service account, minting one on first use and
53
+ * reusing it after that. `force: true` is what a 401 from any admin
54
+ * call should trigger -- the cached token may have been revoked
55
+ * (logoutAll, expiry) and one retry with a freshly minted token is
56
+ * cheap insurance against surfacing a spurious failure. */
57
+ export async function ensureServiceSession(creds, opts = {}) {
58
+ if (cachedToken && !opts.force)
59
+ return cachedToken;
60
+ const auth0Token = await getAuth0AccessToken(creds);
61
+ const minted = await mintServiceAccountToken(auth0Token);
62
+ if (minted.outcome !== 'ok' || !minted.authToken) {
63
+ throw new Error(`Could not establish a service-account session (${minted.outcome}).`);
64
+ }
65
+ cachedToken = minted.authToken;
66
+ return cachedToken;
67
+ }
68
+ //# sourceMappingURL=service-account-auth.js.map
@@ -0,0 +1,154 @@
1
+ import path from 'path';
2
+ import { Command } from '../../command.js';
3
+ export function registerBacklogMcp(parent) {
4
+ return Command.create({
5
+ name: 'mcp',
6
+ usage: 'maka play:backlog:mcp [--env <env>]',
7
+ mustBeInMakaProject: true,
8
+ // NO maxArgLength HERE, DELIBERATELY. Command's own arg-length check
9
+ // is `args.length >= maxArgLength` (command.ts), so `maxArgLength: 0`
10
+ // refuses EVERY invocation regardless of what was typed -- an
11
+ // off-by-one in the shared framework, not something to work around
12
+ // per-command. No command in this codebase sets it to 0 for exactly
13
+ // that reason. This command takes no positionals; any stray one is
14
+ // simply ignored rather than rejected.
15
+ validOpts: [
16
+ {
17
+ name: 'env',
18
+ description: 'Which maka-cli.com config/<env>/process.env supplies the Auth0 M2M credentials (default: production).',
19
+ },
20
+ ],
21
+ shortDesc: 'Run an MCP server exposing game-backlog admin tools, authenticated as the M2M service account.',
22
+ description: `Starts a local MCP server (stdio transport) with tools for listing and reading
23
+ backlog items, attaching evidence and advancing status, drafting an AI bench, and posting
24
+ admin notes -- all against maka-cli.com's existing game-backlog REST API, authenticated as
25
+ the claude-agent@maka-cli.com service account via the Auth0 M2M token exchange
26
+ (api/v1/auth/m2m-login). Must be run with its working directory set to a maka-cli.com
27
+ checkout, since that project's config/<env>/process.env is where AUTHZERO_DOMAIN /
28
+ AUTHZERO_CLIENT_ID / AUTHZERO_SECRET / AUTHZERO_AUDIENCE live.`,
29
+ examples: [
30
+ 'maka play:backlog:mcp',
31
+ 'maka play:backlog:mcp --env local',
32
+ ],
33
+ }, async function (_args, opts) {
34
+ const env = opts.env ?? 'production';
35
+ if (!this.cfg.checkConfigExists(env)) {
36
+ // Pre-handshake failure -- nothing has touched stdout as MCP
37
+ // protocol yet, so a plain thrown Error (which Command's own
38
+ // wrapper reports via Log.error) is fine here.
39
+ throw new Error(`No configuration: ${env}, consider running "maka g:config ${env}"`);
40
+ }
41
+ const configPath = this.cfg.getAppConfigPath(env);
42
+ const processEnvPath = path.join(configPath, 'process.env');
43
+ const [domain, clientId, clientSecret, audience] = await Promise.all([
44
+ this.dotenvx.get('AUTHZERO_DOMAIN', processEnvPath),
45
+ this.dotenvx.get('AUTHZERO_CLIENT_ID', processEnvPath),
46
+ this.dotenvx.get('AUTHZERO_SECRET', processEnvPath),
47
+ this.dotenvx.get('AUTHZERO_AUDIENCE', processEnvPath),
48
+ ]);
49
+ if (!domain || !clientId || !clientSecret || !audience) {
50
+ throw new Error(`AUTHZERO_DOMAIN/CLIENT_ID/SECRET/AUDIENCE must all be set in config/${env}/process.env`);
51
+ }
52
+ const creds = { domain, clientId, clientSecret, audience };
53
+ // WHICH HOSTNAME THE HTTP TRANSPORT HITS is a SEPARATE switch from
54
+ // which config supplied these credentials -- utilities/backlog.ts's
55
+ // request() picks localhost vs www.maka-cli.com off `MAKA_DEV`
56
+ // (read once, at that module's first import), which is this CLI's
57
+ // own long-standing convention and has nothing to do with `--env`
58
+ // by default. Without this, `--env local` would mint a token
59
+ // against maka-cli.com's real local Auth0 credentials and then
60
+ // still call PRODUCTION with it. Only `local` flips it -- there is
61
+ // no separately-served "development" host in this cluster today
62
+ // (nothing deploys that environment; see maka-cli.com's CLAUDE.md),
63
+ // so that one still targets production same as the default.
64
+ if (env === 'local' && !process.env.MAKA_DEV) {
65
+ process.env.MAKA_DEV = 'True';
66
+ }
67
+ // Heavy/optional imports, lazily loaded inside the handler -- the
68
+ // same discipline sideQuest-backlog.sub.cmd.ts documents for the
69
+ // game graph, so `maka --help` and unrelated commands never pay for
70
+ // these.
71
+ const { McpServer } = await import('@modelcontextprotocol/sdk/server/mcp.js');
72
+ const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
73
+ const { z } = await import('zod');
74
+ const { ensureServiceSession } = await import('./sideQuest/utilities/service-account-auth.js');
75
+ const { fetchBacklog, fetchBacklogItemById, patchBacklogItem, draftBacklogBench, postBacklogComment, BACKLOG_STATUSES, } = await import('./sideQuest/utilities/backlog.js');
76
+ const statusEnum = BACKLOG_STATUSES;
77
+ /** Every tool call goes through this: reuse the cached session, and
78
+ * on the first `expired` outcome, mint a fresh one and retry
79
+ * exactly once. A second failure is surfaced rather than looping
80
+ * forever. */
81
+ async function withSession(call) {
82
+ const token = await ensureServiceSession(creds);
83
+ const result = await call(token);
84
+ if (result.outcome !== 'expired')
85
+ return result;
86
+ const fresh = await ensureServiceSession(creds, { force: true });
87
+ return call(fresh);
88
+ }
89
+ /** House shape for a tool result: JSON in, MCP text-content out. A
90
+ * non-'ok' outcome is reported as `isError` so the calling model
91
+ * sees a refusal it can act on (or explain to the human) rather
92
+ * than a raw protocol exception. */
93
+ function toolResult(value) {
94
+ return {
95
+ content: [{ type: 'text', text: JSON.stringify(value, null, 2) }],
96
+ isError: value.outcome !== 'ok',
97
+ };
98
+ }
99
+ const server = new McpServer({ name: 'maka-play-backlog', version: '1.0.0' });
100
+ server.registerTool('backlog_list', {
101
+ title: 'List backlog items',
102
+ description: 'List game-backlog items, optionally filtered by status. Closed (complete) items are excluded unless a status is given explicitly.',
103
+ inputSchema: {
104
+ status: z.enum(statusEnum).optional()
105
+ .describe('Filter to one status. Omit to see everything not yet complete.'),
106
+ },
107
+ }, async ({ status }) => toolResult(await withSession((token) => fetchBacklog(status, { all: false }, token))));
108
+ server.registerTool('backlog_get', {
109
+ title: 'Read one backlog item',
110
+ description: 'Read a single game-backlog item in full, by id.',
111
+ inputSchema: { itemId: z.string().describe('The backlog item id.') },
112
+ }, async ({ itemId }) => toolResult(await withSession((token) => fetchBacklogItemById(itemId, token))));
113
+ server.registerTool('backlog_comment', {
114
+ title: 'Comment on a backlog item',
115
+ description: 'Post an admin note or a question to the reporter on a backlog item. A "question" with awaitingReporter puts it in the reporter\'s review queue and tomorrow\'s digest; a "note" does neither.',
116
+ inputSchema: {
117
+ itemId: z.string(),
118
+ text: z.string().min(1),
119
+ kind: z.enum(['question', 'note']).default('note'),
120
+ awaitingReporter: z.boolean().optional()
121
+ .describe('Only meaningful with kind "question" -- puts this item in the reporter\'s queue.'),
122
+ },
123
+ }, async ({ itemId, text, kind, awaitingReporter }) => toolResult(await withSession(async (token) => ({
124
+ outcome: await postBacklogComment(itemId, text, { kind, awaitingReporter }, token),
125
+ }))));
126
+ server.registerTool('backlog_advance', {
127
+ title: 'Advance a backlog item (attach evidence / change status)',
128
+ description: `Move a backlog item's status and/or attach fix-commit evidence. This is the
129
+ actual resolution action -- it will be refused by the server (with a message explaining
130
+ why) if you try to enter "in-review" or "complete" without at least one commit attached,
131
+ or "complete" without the reporter's sign-off already on the item. This tool never signs
132
+ off or rejects a fix on the reporter's behalf -- only the person who filed the item can
133
+ do that, in-game.`,
134
+ inputSchema: {
135
+ itemId: z.string(),
136
+ status: z.enum(statusEnum),
137
+ commits: z.array(z.string()).optional()
138
+ .describe('Fix-commit SHAs to attach (merged with whatever is already there).'),
139
+ },
140
+ }, async ({ itemId, status, commits }) => toolResult(await withSession((token) => patchBacklogItem(itemId, { status, commits }, token))));
141
+ server.registerTool('backlog_draft_bench', {
142
+ title: 'Draft an AI bench for a backlog item',
143
+ description: 'Ask the server to AI-draft a two-room repro bench for one item (lands in reproDraft, not repro -- a human has to dry-run and promote it separately via backlog_advance with a repro body). Costs a model call server-side.',
144
+ inputSchema: {
145
+ itemId: z.string(),
146
+ verbs: z.array(z.string()).optional().describe('The verbs available to reference in the bench steps.'),
147
+ },
148
+ }, async ({ itemId, verbs }) => toolResult(await withSession((token) => draftBacklogBench(itemId, token, verbs))));
149
+ const transport = new StdioServerTransport();
150
+ await server.connect(transport);
151
+ console.error('[play:backlog:mcp] ready');
152
+ }, parent);
153
+ }
154
+ //# sourceMappingURL=sideQuest-backlog-mcp.sub.cmd.js.map
@@ -1,5 +1,6 @@
1
1
  import ShadowrunCommand, { ShadowrunCompat } from './sideQuest.sub.cmd.js';
2
2
  import { Command } from '../../command.js';
3
+ import { registerBacklogMcp } from './sideQuest-backlog-mcp.sub.cmd.js';
3
4
  // Registered under BOTH the new spelling (play:backlog) and the compat
4
5
  // sub (play:shadowrun:backlog) from ONE definition -- two commands, one
5
6
  // options object, one handler, so the pair cannot drift.
@@ -62,6 +63,9 @@ const registerBacklog = (parent) => Command.create({
62
63
  detachAsked,
63
64
  });
64
65
  }, parent);
65
- registerBacklog(ShadowrunCommand);
66
+ const backlogCmd = registerBacklog(ShadowrunCommand);
66
67
  registerBacklog(ShadowrunCompat);
68
+ // mcp: only under the live `play:` spelling -- a brand-new command has
69
+ // no compat surface to preserve under `play:shadowrun:backlog:mcp`.
70
+ registerBacklogMcp(backlogCmd);
67
71
  //# sourceMappingURL=sideQuest-backlog.sub.cmd.js.map
@@ -65,9 +65,7 @@ Generator.create({
65
65
  context,
66
66
  config
67
67
  });
68
- if (destinationKey !== 'local') {
69
- Log.notice(`config/${destinationKey}/process.env starts as plaintext; use "maka env:set --env ${destinationKey} KEY VALUE" to add secrets, which encrypts them with dotenvx on first use.`);
70
- }
68
+ Log.notice(`config/${destinationKey}/process.env starts as plaintext; use "maka env:set --env ${destinationKey} KEY VALUE" to add secrets, which encrypts them with dotenvx on first use.`);
71
69
  }
72
70
  catch (error) {
73
71
  Log.error(error);
@@ -1,5 +1,5 @@
1
1
  // Dotenvx wrapper: loads (and transparently decrypts) process.env-style files,
2
- // and sets/gets individual secrets, encrypting on first use for non-local envs.
2
+ // and sets/gets individual secrets, encrypting on first use.
3
3
  import path from 'path';
4
4
  import dotenvxPkg from '@dotenvx/dotenvx';
5
5
  import { Log } from '../log/log.class.js';
@@ -36,13 +36,12 @@ export class DOTENVX {
36
36
  }
37
37
  /**
38
38
  * Sets a single KEY=value pair into a .env-style file, creating the file's
39
- * keypair on first use if needed. Encrypted by default (matching encrypt());
40
- * pass plain: true to write it unencrypted (used for the local environment).
39
+ * keypair on first use if needed. Always encrypted.
41
40
  */
42
- async set(key, value, filePath, opts = {}) {
41
+ async set(key, value, filePath) {
43
42
  try {
44
43
  const envKeysFile = path.join(path.dirname(filePath), '.env.keys');
45
- await setDotenvxValue(key, value, { path: filePath, envKeysFile, encrypt: !opts.plain });
44
+ await setDotenvxValue(key, value, { path: filePath, envKeysFile, encrypt: true });
46
45
  }
47
46
  catch (e) {
48
47
  errorHandler('set()', e);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.141.0",
3
+ "version": "5.143.0",
4
4
  "type": "module",
5
5
  "summary": "A command line tool for scaffolding Meteor 3.x applications using either React.",
6
6
  "description": "A command line tool for scaffolding Meteor 3.x applications using React.",
@@ -49,6 +49,7 @@
49
49
  "@inquirer/prompts": "^8.5.2",
50
50
  "@maka/meteor-sdk": "^0.2.13",
51
51
  "@maka/tar": "^6.2.2",
52
+ "@modelcontextprotocol/sdk": "^1.30.0",
52
53
  "blessed": "^0.1.81",
53
54
  "chalk": "^4.1.2",
54
55
  "change-case": "^5.4.4",
@@ -70,7 +71,8 @@
70
71
  "tar": "^7.4.3",
71
72
  "uuid": "^14.0.1",
72
73
  "ws": "^8.21.3",
73
- "xtend": "^4.0.2"
74
+ "xtend": "^4.0.2",
75
+ "zod": "^4.5.4"
74
76
  },
75
77
  "devDependencies": {
76
78
  "@aws-sdk/types": "^3.974.4",