@hanzlaa/rcode 4.4.2 → 4.4.3

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.
Files changed (34) hide show
  1. package/AGENTS.md +1 -1
  2. package/CONTRIBUTING.md +6 -0
  3. package/README.md +3 -3
  4. package/cli/install.js +13 -1
  5. package/cli/postinstall.js +12 -0
  6. package/dist/rcode.js +24 -24
  7. package/package.json +1 -1
  8. package/rcode/bin/rcode-tools.cjs +33 -0
  9. package/rcode/brain/sources.yaml +10 -0
  10. package/rcode/command-aliases.yaml +16 -0
  11. package/rcode/commands/lazy.md +1 -6
  12. package/rcode/internal-workflows.yaml +26 -0
  13. package/rcode/skills/seo/on-page-seo-auditor/SKILL.md +8 -155
  14. package/rcode/skills/seo/on-page-seo-auditor/references.md +108 -0
  15. package/rcode/skills/seo/rank-and-rent-local-seo/SKILL.md +1 -1
  16. package/rcode/skills/seo/seo-audit/SKILL.md +6 -255
  17. package/rcode/skills/seo/seo-audit/references.md +257 -0
  18. package/rcode/skills/seo/seo-content-factory/SKILL.md +1 -1
  19. package/rcode/skills/seo/seo-content-writer/SKILL.md +7 -94
  20. package/rcode/skills/seo/seo-content-writer/references.md +48 -0
  21. package/rcode/skills/seo/seo-growth-orchestrator/SKILL.md +1 -1
  22. package/rcode/skills/seo/seo-site-builder/SKILL.md +1 -1
  23. package/rcode/skills/seo/technical-seo-checker/SKILL.md +8 -157
  24. package/rcode/skills/seo/technical-seo-checker/references.md +100 -0
  25. package/rcode/workflows/help.md +1 -0
  26. package/rcode/workflows/lazy.md +30 -0
  27. package/server/dashboard.js +39 -38
  28. package/server/lib/html/client/components/App.js +2 -0
  29. package/server/lib/html/client/components/CommandPalette.js +5 -0
  30. package/server/lib/html/client/components/RunConfirmDialog.js +60 -0
  31. package/server/lib/html/client/orchestrator.js +51 -3
  32. package/server/lib/html/client/store.js +4 -0
  33. package/server/lib/html/css.js +21 -0
  34. package/server/orchestrator.js +43 -2
@@ -303,6 +303,17 @@ function _poll() {
303
303
  * @param {{ runner?: string, model?: string }} [opts] — agent CLI selection
304
304
  */
305
305
  export function runAndOpenTerm(storyId, cmd, title, opts) {
306
+ // #916 — spawning an orchestrator session launches a real agent with
307
+ // permissions skipped. Gate it behind an explicit confirmation dialog
308
+ // instead of running on the first click. The dialog calls execRunAndOpenTerm
309
+ // on confirm.
310
+ setState({
311
+ runConfirm: { kind: 'story', storyId, cmd, title: title || storyId, opts: opts || null },
312
+ });
313
+ }
314
+
315
+ /** The actual spawn — invoked only after the user confirms (see #916). */
316
+ export function execRunAndOpenTerm(storyId, cmd, title, opts) {
306
317
  // Open the panel immediately (it shows "connecting" while the session starts).
307
318
  setState({
308
319
  terminal: {
@@ -362,9 +373,19 @@ export function stopStory(storyId) {
362
373
 
363
374
  // ── Command runner ────────────────────────────────────────────────────────────
364
375
  /**
365
- * Client-side allowlist — mirrors the server COMMAND_ALLOWLIST.
366
- * The server always re-validates; this list drives the picker dropdown only.
367
- * Update both when adding a new command.
376
+ * Client-side command-runner allowlist — mirrors the server COMMAND_ALLOWLIST.
377
+ *
378
+ * #932 this is INTENTIONALLY a small, curated subset of the ~117 rcode
379
+ * commands, NOT the full set. The command runner spawns a real agent with
380
+ * permissions skipped straight from the browser, so only commands that are
381
+ * SAFE to run unattended are exposed here: read-only status/inspection
382
+ * (status, progress, stats, health, diff, show, list-plans, help) plus the two
383
+ * idempotent setup commands (init, config). Destructive or long-running
384
+ * commands (execute, autonomous, ship, dev-story, …) are deliberately omitted
385
+ * — run those from your IDE where you can supervise them. The server
386
+ * re-validates against COMMAND_ALLOWLIST regardless; this list only drives the
387
+ * picker dropdown. Keep both in sync when adding a command, and only add one
388
+ * here if it is safe to run unattended.
368
389
  */
369
390
  export const ALLOWED_COMMANDS = [
370
391
  { cmd: '/rcode-init', label: 'init — initialise project workspace', category: 'Project' },
@@ -397,6 +418,16 @@ export const ALLOWED_COMMANDS = [
397
418
  * @param {{ runner?: string, model?: string }} [opts] — agent CLI selection
398
419
  */
399
420
  export function runCommandFromUI(cmd, opts) {
421
+ if (!cmd) return;
422
+ // #916 — gate command-runner spawns behind the same confirmation dialog.
423
+ const title = cmd + ' (command runner)';
424
+ setState({
425
+ runConfirm: { kind: 'command', cmd, title, opts: opts || null },
426
+ });
427
+ }
428
+
429
+ /** The actual command-runner spawn — invoked only after the user confirms. */
430
+ export function execRunCommandFromUI(cmd, opts) {
400
431
  if (!cmd) return;
401
432
  const slug = cmd.replace(/^\//, '').replace(/\//g, '-');
402
433
  const storyId = 'cmd-' + slug;
@@ -418,3 +449,20 @@ export function runCommandFromUI(cmd, opts) {
418
449
  })
419
450
  .catch(() => showToast('Could not reach orchestrator'));
420
451
  }
452
+
453
+ /** Confirm the pending run (from the #916 dialog) and dispatch the real spawn. */
454
+ export function confirmPendingRun() {
455
+ const rc = getState().runConfirm;
456
+ setState({ runConfirm: null });
457
+ if (!rc) return;
458
+ if (rc.kind === 'command') {
459
+ execRunCommandFromUI(rc.cmd, rc.opts);
460
+ } else {
461
+ execRunAndOpenTerm(rc.storyId, rc.cmd, rc.title, rc.opts);
462
+ }
463
+ }
464
+
465
+ /** Dismiss the pending-run confirmation without spawning. */
466
+ export function cancelPendingRun() {
467
+ setState({ runConfirm: null });
468
+ }
@@ -77,6 +77,10 @@ let _state = {
77
77
  // Runner-picker popover state (driven by components/RunnerPicker.js)
78
78
  // { open, x, y, run: { kind: 'session'|'command', storyId?, cmd, title? } }
79
79
  runnerPicker: null,
80
+ // #916 — pending orchestrator-run confirmation. When set, RunConfirmDialog
81
+ // renders and the spawn waits for explicit user approval.
82
+ // { kind: 'story'|'command', storyId?, cmd, title, opts }
83
+ runConfirm: null,
80
84
  };
81
85
 
82
86
  /** Registered subscriber functions. */
@@ -4573,6 +4573,13 @@ summary:focus-visible,
4573
4573
  padding: var(--space-6) var(--space-4);
4574
4574
  font-size: var(--text-sm);
4575
4575
  }
4576
+ .cmd-palette-footer {
4577
+ border-top: 1px solid var(--border);
4578
+ padding: var(--space-2) var(--space-3);
4579
+ color: var(--text-muted);
4580
+ font-size: var(--text-xs);
4581
+ line-height: 1.4;
4582
+ }
4576
4583
  /* ════════ Command palette (END) ════════ */
4577
4584
 
4578
4585
  /* ── Reject dialog ── */
@@ -4613,6 +4620,20 @@ summary:focus-visible,
4613
4620
  box-sizing: border-box;
4614
4621
  }
4615
4622
  .reject-dialog-input:focus { outline: none; border-color: var(--accent-primary); }
4623
+ .run-confirm-body {
4624
+ color: var(--text-secondary);
4625
+ font-size: var(--text-xs);
4626
+ line-height: 1.5;
4627
+ }
4628
+ .run-confirm-body p { margin: 0 0 var(--space-2); }
4629
+ .run-confirm-cmd {
4630
+ background: var(--bg-input);
4631
+ border: 1px solid var(--border);
4632
+ border-radius: var(--radius-2);
4633
+ padding: var(--space-2);
4634
+ word-break: break-all;
4635
+ }
4636
+ .run-confirm-cmd code { font-family: var(--font-mono, monospace); color: var(--text-primary); }
4616
4637
  .reject-dialog-actions {
4617
4638
  display: flex;
4618
4639
  justify-content: flex-end;
@@ -116,6 +116,12 @@ const RUNNERS = [
116
116
  // alias ('fable', 'opus', 'sonnet') or a full model id like fable-5.
117
117
  id: 'claude', label: 'Claude Code', bin: CLAUDE_BIN, modelFlag: '--model',
118
118
  models: ['fable-5', 'opus', 'sonnet', 'haiku'],
119
+ // #918 — orchestrated sessions run in a detached PTY with no human at the
120
+ // keyboard to answer permission prompts, so the agent is launched with
121
+ // --dangerously-skip-permissions. This is a real privilege grant: the
122
+ // agent can run any local command without a gate. Containment relies on
123
+ // (a) the loopback-only + token-gated API and (b) running in the project
124
+ // CWD. A visible warning is emitted at spawn time (see handleRun).
119
125
  args: (model, prompt) => model
120
126
  ? [prompt, '--dangerously-skip-permissions', '--model', model]
121
127
  : [prompt, '--dangerously-skip-permissions'],
@@ -332,11 +338,30 @@ function validStoryId(id) {
332
338
  && STORY_ID_RE.test(id);
333
339
  }
334
340
 
341
+ // Cap request bodies so a malicious or buggy caller can't exhaust memory by
342
+ // streaming an unbounded payload (#921). 1 MB is far more than any legitimate
343
+ // run/stop/reject body. On overflow we destroy the socket and resolve {} —
344
+ // the handler then rejects it as an invalid body.
345
+ const MAX_BODY_BYTES = 1 * 1024 * 1024;
346
+
335
347
  function parseBody(req) {
336
348
  return new Promise(resolve => {
337
349
  let buf = '';
338
- req.on('data', c => buf += c);
339
- req.on('end', () => { try { resolve(JSON.parse(buf)); } catch { resolve({}); } });
350
+ let size = 0;
351
+ let aborted = false;
352
+ req.on('data', c => {
353
+ if (aborted) return;
354
+ size += c.length;
355
+ if (size > MAX_BODY_BYTES) {
356
+ aborted = true;
357
+ try { req.destroy(); } catch {}
358
+ resolve({});
359
+ return;
360
+ }
361
+ buf += c;
362
+ });
363
+ req.on('end', () => { if (!aborted) { try { resolve(JSON.parse(buf)); } catch { resolve({}); } } });
364
+ req.on('error', () => { if (!aborted) { aborted = true; resolve({}); } });
340
365
  });
341
366
  }
342
367
 
@@ -492,6 +517,17 @@ async function handleRun(req, res) {
492
517
  json(res, 403, { error: 'command not in allowlist', cmd: reqCmd });
493
518
  return;
494
519
  }
520
+ } else if (typeof body.cmd === 'string' && body.cmd.trim() !== '') {
521
+ // #919 — non-cmd sessions (dev-runs: phase-N, sprint-N.M, task ids) may
522
+ // supply an explicit cmd, but it MUST be a slash command (e.g.
523
+ // "/rcode-dev-story phase-3"). Free-form prompt text is rejected so the
524
+ // allowlist isn't trivially bypassed by using a non-cmd- storyId. The
525
+ // default path (no body.cmd) uses "/rcode-dev-story <storyId>" — also a
526
+ // slash command — so this never blocks the normal flow.
527
+ if (!body.cmd.trim().startsWith('/')) {
528
+ json(res, 403, { error: 'non-command sessions must run a slash command (got free-form prompt)' });
529
+ return;
530
+ }
495
531
  }
496
532
 
497
533
  // Runner + model selection — STRICT validation against the registry.
@@ -533,6 +569,11 @@ async function handleRun(req, res) {
533
569
  const cmd = String(body.cmd || `/rcode-dev-story ${storyId}`);
534
570
  const cols = 120, rows = 30;
535
571
 
572
+ // #918 — make the privilege grant audible. Every orchestrated run launches
573
+ // the agent with permissions skipped; surface it in the server log so it's
574
+ // never silent.
575
+ console.warn(`[orchestrator] ⚠ spawning ${runner.id} for "${storyId}" with permissions SKIPPED — agent can run any local command in ${PROJECT_ROOT}`);
576
+
536
577
  let proc;
537
578
  try {
538
579
  proc = pty.spawn(runner.bin, runner.args(model, cmd), {