@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.
- package/AGENTS.md +1 -1
- package/CONTRIBUTING.md +6 -0
- package/README.md +3 -3
- package/cli/install.js +13 -1
- package/cli/postinstall.js +12 -0
- package/dist/rcode.js +24 -24
- package/package.json +1 -1
- package/rcode/bin/rcode-tools.cjs +33 -0
- package/rcode/brain/sources.yaml +10 -0
- package/rcode/command-aliases.yaml +16 -0
- package/rcode/commands/lazy.md +1 -6
- package/rcode/internal-workflows.yaml +26 -0
- package/rcode/skills/seo/on-page-seo-auditor/SKILL.md +8 -155
- package/rcode/skills/seo/on-page-seo-auditor/references.md +108 -0
- package/rcode/skills/seo/rank-and-rent-local-seo/SKILL.md +1 -1
- package/rcode/skills/seo/seo-audit/SKILL.md +6 -255
- package/rcode/skills/seo/seo-audit/references.md +257 -0
- package/rcode/skills/seo/seo-content-factory/SKILL.md +1 -1
- package/rcode/skills/seo/seo-content-writer/SKILL.md +7 -94
- package/rcode/skills/seo/seo-content-writer/references.md +48 -0
- package/rcode/skills/seo/seo-growth-orchestrator/SKILL.md +1 -1
- package/rcode/skills/seo/seo-site-builder/SKILL.md +1 -1
- package/rcode/skills/seo/technical-seo-checker/SKILL.md +8 -157
- package/rcode/skills/seo/technical-seo-checker/references.md +100 -0
- package/rcode/workflows/help.md +1 -0
- package/rcode/workflows/lazy.md +30 -0
- package/server/dashboard.js +39 -38
- package/server/lib/html/client/components/App.js +2 -0
- package/server/lib/html/client/components/CommandPalette.js +5 -0
- package/server/lib/html/client/components/RunConfirmDialog.js +60 -0
- package/server/lib/html/client/orchestrator.js +51 -3
- package/server/lib/html/client/store.js +4 -0
- package/server/lib/html/css.js +21 -0
- 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
|
-
*
|
|
367
|
-
*
|
|
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. */
|
package/server/lib/html/css.js
CHANGED
|
@@ -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;
|
package/server/orchestrator.js
CHANGED
|
@@ -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
|
-
|
|
339
|
-
|
|
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), {
|