@marver-design/marver 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +72 -0
- package/README.md +2 -2
- package/dist/{build-D9gimz6K.mjs → build-Dd91jjIg.mjs} +2 -2
- package/dist/cli.mjs +12 -3
- package/dist/{daemon-2Lft_lVV.mjs → daemon-B_8BmmSJ.mjs} +486 -15
- package/dist/{dev-CTtkqVo_.mjs → dev-Dv0GlYQq.mjs} +4 -4
- package/dist/{init-8Giknvy1.mjs → init-D2EQEbV6.mjs} +4 -3
- package/dist/{manifest-DJHU7qfu.mjs → manifest-DIsp3ldB.mjs} +44 -12
- package/dist/{plugin-BrBWk2Qn.mjs → plugin-CgLVYTR-.mjs} +100 -3
- package/dist/shot-BRgPaFCs.mjs +254 -0
- package/dist/shot-DkkwuCZ2.mjs +29 -0
- package/package.json +1 -1
- package/src/client/frame-host/main.tsx +8 -1
- package/src/client/shell/Comments.tsx +1 -1
- package/templates/instructions/jam.md +88 -6
|
@@ -2,6 +2,7 @@ import { r as deviceId, t as has } from "./ledger-BgA7nQoH.mjs";
|
|
|
2
2
|
import { n as replay } from "./events-BMtBvvgU.mjs";
|
|
3
3
|
import { appendEvents, listBoards, readLog } from "./comments-BZBKhKRO.mjs";
|
|
4
4
|
import { n as localProfile } from "./profile-BkiWglVE.mjs";
|
|
5
|
+
import { i as toFrameId } from "./manifest-DIsp3ldB.mjs";
|
|
5
6
|
import { workActivity } from "./work-CLrmY-vQ.mjs";
|
|
6
7
|
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync, writeSync } from "node:fs";
|
|
7
8
|
import { join } from "node:path";
|
|
@@ -176,7 +177,7 @@ function extractReanchors(text) {
|
|
|
176
177
|
*/
|
|
177
178
|
/** First key of an object, or undefined. */
|
|
178
179
|
const firstKey = (o) => o && typeof o === "object" ? Object.keys(o)[0] : void 0;
|
|
179
|
-
const cleanModel = (raw) => typeof raw === "string" ? raw.replace(/\[[^\]]*\]$/, "") || void 0 : void 0;
|
|
180
|
+
const cleanModel$1 = (raw) => typeof raw === "string" ? raw.replace(/\[[^\]]*\]$/, "") || void 0 : void 0;
|
|
180
181
|
const claudeAdapter = {
|
|
181
182
|
name: "claude",
|
|
182
183
|
supportsSubagents: true,
|
|
@@ -205,7 +206,7 @@ const claudeAdapter = {
|
|
|
205
206
|
const text = (o.message?.content ?? []).find((c) => c?.type === "text" && typeof c.text === "string" && c.text.trim());
|
|
206
207
|
return text ? {
|
|
207
208
|
text: String(text.text).trim(),
|
|
208
|
-
model: cleanModel(o.message?.model)
|
|
209
|
+
model: cleanModel$1(o.message?.model)
|
|
209
210
|
} : null;
|
|
210
211
|
} catch {
|
|
211
212
|
return null;
|
|
@@ -225,14 +226,14 @@ const claudeAdapter = {
|
|
|
225
226
|
if (o.type === "result") {
|
|
226
227
|
if (typeof o.result === "string") text = o.result.trim();
|
|
227
228
|
if (o.is_error) failed = true;
|
|
228
|
-
model ??= cleanModel(firstKey(o.modelUsage));
|
|
229
|
-
} else if (o.type === "assistant") model ??= cleanModel(o.message?.model);
|
|
229
|
+
model ??= cleanModel$1(firstKey(o.modelUsage));
|
|
230
|
+
} else if (o.type === "assistant") model ??= cleanModel$1(o.message?.model);
|
|
230
231
|
} catch {}
|
|
231
232
|
}
|
|
232
233
|
if (!text) try {
|
|
233
234
|
const j = JSON.parse(stdout);
|
|
234
235
|
if (typeof j.result === "string") text = j.result.trim();
|
|
235
|
-
model ??= cleanModel((typeof j.model === "string" ? j.model : void 0) ?? j.canonicalModel ?? firstKey(j.modelUsage));
|
|
236
|
+
model ??= cleanModel$1((typeof j.model === "string" ? j.model : void 0) ?? j.canonicalModel ?? firstKey(j.modelUsage));
|
|
236
237
|
} catch {
|
|
237
238
|
if (!sawEvents) text = stdout.trim();
|
|
238
239
|
}
|
|
@@ -305,6 +306,425 @@ const codexAdapter = {
|
|
|
305
306
|
}
|
|
306
307
|
};
|
|
307
308
|
//#endregion
|
|
309
|
+
//#region src/server/jam/adapter/cursor.ts
|
|
310
|
+
/**
|
|
311
|
+
* The Cursor adapter. Spawns `cursor-agent -p` headless with stream-json output - a
|
|
312
|
+
* Claude-Code-shaped stream (system/init carries the model, assistant events carry complete
|
|
313
|
+
* messages, a terminal `result` event carries the reply).
|
|
314
|
+
*
|
|
315
|
+
* The long binary name is deliberate: cursor's installer ships BOTH `cursor-agent` and a bare
|
|
316
|
+
* `agent`, and grok's installer symlinks `agent` too - on a machine with both, the short name
|
|
317
|
+
* is whichever installed last. `cursor-agent` is unambiguous.
|
|
318
|
+
*
|
|
319
|
+
* Jail: codex-parity, not claude-parity - cursor's print mode carries write AND shell
|
|
320
|
+
* tools, and the CLI has no per-run flag that removes the shell outright. So the OS
|
|
321
|
+
* sandbox is switched ON (`--sandbox enabled`, overriding any config that turned it off)
|
|
322
|
+
* to bound what commands can touch, and `--force` is never passed, so nothing gets blanket
|
|
323
|
+
* command approval. The packet carries untrusted text (synced collaborators' comments);
|
|
324
|
+
* the sandbox is what stands between it and the machine.
|
|
325
|
+
*/
|
|
326
|
+
const assistantText$2 = (o) => {
|
|
327
|
+
if (o.type !== "assistant") return void 0;
|
|
328
|
+
const t = (o.message?.content ?? []).find((c) => c?.type === "text" && typeof c.text === "string" && c.text.trim());
|
|
329
|
+
return t ? String(t.text).trim() : void 0;
|
|
330
|
+
};
|
|
331
|
+
const cursorAdapter = {
|
|
332
|
+
name: "cursor",
|
|
333
|
+
supportsSubagents: false,
|
|
334
|
+
spawnArgs(goal) {
|
|
335
|
+
return {
|
|
336
|
+
cmd: "cursor-agent",
|
|
337
|
+
args: [
|
|
338
|
+
"-p",
|
|
339
|
+
goal,
|
|
340
|
+
"--output-format",
|
|
341
|
+
"stream-json",
|
|
342
|
+
"--sandbox",
|
|
343
|
+
"enabled",
|
|
344
|
+
"--trust"
|
|
345
|
+
]
|
|
346
|
+
};
|
|
347
|
+
},
|
|
348
|
+
earlyText(line) {
|
|
349
|
+
try {
|
|
350
|
+
const o = JSON.parse(line);
|
|
351
|
+
const text = assistantText$2(o);
|
|
352
|
+
return text ? { text } : null;
|
|
353
|
+
} catch {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
parse(stdout, code) {
|
|
358
|
+
let text = "";
|
|
359
|
+
let lastAssistant = "";
|
|
360
|
+
let model;
|
|
361
|
+
let failed = false;
|
|
362
|
+
for (const line of stdout.split("\n")) {
|
|
363
|
+
const t = line.trim();
|
|
364
|
+
if (!t) continue;
|
|
365
|
+
try {
|
|
366
|
+
const o = JSON.parse(t);
|
|
367
|
+
if (o.type === "result") {
|
|
368
|
+
if (typeof o.result === "string" && o.result.trim()) text = o.result.trim();
|
|
369
|
+
if (o.is_error) failed = true;
|
|
370
|
+
} else if (o.type === "system" && o.subtype === "init" && typeof o.model === "string" && o.model !== "unknown") model = o.model;
|
|
371
|
+
else {
|
|
372
|
+
const at = assistantText$2(o);
|
|
373
|
+
if (at) lastAssistant = at;
|
|
374
|
+
}
|
|
375
|
+
} catch {}
|
|
376
|
+
}
|
|
377
|
+
if (!text) text = lastAssistant;
|
|
378
|
+
const { reply: visible, reanchors } = extractReanchors(text);
|
|
379
|
+
const reply = extractReplyBlock(visible);
|
|
380
|
+
return {
|
|
381
|
+
reply,
|
|
382
|
+
model,
|
|
383
|
+
reanchors,
|
|
384
|
+
ok: code === 0 && !failed && !!reply
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
//#endregion
|
|
389
|
+
//#region src/server/jam/adapter/droid.ts
|
|
390
|
+
/**
|
|
391
|
+
* The Factory droid adapter. Spawns `droid exec -o stream-json` headless - a
|
|
392
|
+
* Claude-Code-shaped stream (system/init carries the model, a terminal `result` event
|
|
393
|
+
* carries the reply and `is_error`). Factory marks stream-json deprecated but it works
|
|
394
|
+
* (v0.200.0); parse() also reads the `-o json` single-envelope shape, so if a future droid
|
|
395
|
+
* drops the stream the switch is one argv word, not a parser rewrite.
|
|
396
|
+
*
|
|
397
|
+
* Jail: `--auto low` approves file creation/edit, and `--disabled-tools` then REMOVES the
|
|
398
|
+
* shell (`Execute`) and the outbound connectors (Slack posting, connector search) outright -
|
|
399
|
+
* the packet carries untrusted text, and both are exfiltration channels. Tool names come
|
|
400
|
+
* from droid's own `--list-tools` (v0.200.0). Claude-parity: edits yes, shell no.
|
|
401
|
+
*/
|
|
402
|
+
const droidAdapter = {
|
|
403
|
+
name: "droid",
|
|
404
|
+
supportsSubagents: false,
|
|
405
|
+
spawnArgs(goal) {
|
|
406
|
+
return {
|
|
407
|
+
cmd: "droid",
|
|
408
|
+
args: [
|
|
409
|
+
"exec",
|
|
410
|
+
"-o",
|
|
411
|
+
"stream-json",
|
|
412
|
+
"--auto",
|
|
413
|
+
"low",
|
|
414
|
+
"--disabled-tools",
|
|
415
|
+
"Execute,Task,ProposeMission,StartMissionRun,GenerateDroid,slack_post_message,slack_post_file,ConnectorSearch",
|
|
416
|
+
"--disable-builtin-skills",
|
|
417
|
+
goal
|
|
418
|
+
]
|
|
419
|
+
};
|
|
420
|
+
},
|
|
421
|
+
earlyText(line) {
|
|
422
|
+
try {
|
|
423
|
+
const o = JSON.parse(line);
|
|
424
|
+
if (o.type !== "assistant") return null;
|
|
425
|
+
const t = (o.message?.content ?? []).find((c) => c?.type === "text" && typeof c.text === "string" && c.text.trim());
|
|
426
|
+
return t ? { text: String(t.text).trim() } : null;
|
|
427
|
+
} catch {
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
parse(stdout, code) {
|
|
432
|
+
let text = "";
|
|
433
|
+
let model;
|
|
434
|
+
let failed = false;
|
|
435
|
+
let sawEvents = false;
|
|
436
|
+
for (const line of stdout.split("\n")) {
|
|
437
|
+
const t = line.trim();
|
|
438
|
+
if (!t) continue;
|
|
439
|
+
try {
|
|
440
|
+
const o = JSON.parse(t);
|
|
441
|
+
sawEvents = true;
|
|
442
|
+
if (o.type === "result") {
|
|
443
|
+
if (typeof o.result === "string") text = o.result.trim();
|
|
444
|
+
if (o.is_error) failed = true;
|
|
445
|
+
} else if (o.type === "system" && o.subtype === "init" && typeof o.model === "string") model = o.model;
|
|
446
|
+
else if (o.type === "error") failed = true;
|
|
447
|
+
} catch {}
|
|
448
|
+
}
|
|
449
|
+
if (!sawEvents) try {
|
|
450
|
+
const j = JSON.parse(stdout);
|
|
451
|
+
if (typeof j.result === "string") text = j.result.trim();
|
|
452
|
+
if (j.is_error) failed = true;
|
|
453
|
+
} catch {}
|
|
454
|
+
const { reply: visible, reanchors } = extractReanchors(text);
|
|
455
|
+
const reply = extractReplyBlock(visible);
|
|
456
|
+
return {
|
|
457
|
+
reply,
|
|
458
|
+
model,
|
|
459
|
+
reanchors,
|
|
460
|
+
ok: code === 0 && !failed && !!reply
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
//#endregion
|
|
465
|
+
//#region src/server/jam/adapter/grok.ts
|
|
466
|
+
/**
|
|
467
|
+
* The grok adapter (xAI's Grok Build CLI). Spawns `grok -p` headless with
|
|
468
|
+
* streaming-messages-json - an Anthropic-Messages-shaped stream: system/init and assistant
|
|
469
|
+
* frames name the model, a terminal `result` event carries the reply and `is_error`.
|
|
470
|
+
*
|
|
471
|
+
* Jail: an explicit `--tools` ALLOWLIST of read/edit primitives only - so the shell, web,
|
|
472
|
+
* X-search, subagents, scheduler, MCP, and image tools are all simply absent, not denied by
|
|
473
|
+
* name. (A deny-list is a footgun: grok's real shell tool is `run_terminal_command`, and an
|
|
474
|
+
* earlier `--disallowed-tools run_terminal_cmd` left the shell fully live - verified. An
|
|
475
|
+
* allowlist can't miss a tool it doesn't name.) `--permission-mode acceptEdits` auto-applies
|
|
476
|
+
* those edits so the headless run never stalls; `--no-subagents` is belt-and-braces (the
|
|
477
|
+
* allowlist already omits spawn_subagent). The packet carries untrusted text, so this is the
|
|
478
|
+
* no-shell, no-network posture.
|
|
479
|
+
*
|
|
480
|
+
* grok injects NO env marker into processes it spawns (verified in the grok-build source),
|
|
481
|
+
* so detection finds it by PATH order only - naming `jam: "grok"` in config always works.
|
|
482
|
+
*/
|
|
483
|
+
const cleanModel = (raw) => typeof raw === "string" && raw && raw !== "unknown" ? raw : void 0;
|
|
484
|
+
const assistantText$1 = (o) => {
|
|
485
|
+
if (o.type !== "assistant") return void 0;
|
|
486
|
+
const t = (o.message?.content ?? []).find((c) => c?.type === "text" && typeof c.text === "string" && c.text.trim());
|
|
487
|
+
return t ? String(t.text).trim() : void 0;
|
|
488
|
+
};
|
|
489
|
+
const grokAdapter = {
|
|
490
|
+
name: "grok",
|
|
491
|
+
supportsSubagents: false,
|
|
492
|
+
spawnArgs(goal) {
|
|
493
|
+
return {
|
|
494
|
+
cmd: "grok",
|
|
495
|
+
args: [
|
|
496
|
+
"-p",
|
|
497
|
+
goal,
|
|
498
|
+
"--output-format",
|
|
499
|
+
"streaming-messages-json",
|
|
500
|
+
"--permission-mode",
|
|
501
|
+
"acceptEdits",
|
|
502
|
+
"--no-subagents",
|
|
503
|
+
"--tools",
|
|
504
|
+
"read_file,search_replace,list_dir,grep,todo_write"
|
|
505
|
+
]
|
|
506
|
+
};
|
|
507
|
+
},
|
|
508
|
+
earlyText(line) {
|
|
509
|
+
try {
|
|
510
|
+
const o = JSON.parse(line);
|
|
511
|
+
const text = assistantText$1(o);
|
|
512
|
+
return text ? {
|
|
513
|
+
text,
|
|
514
|
+
model: cleanModel(o.message?.model)
|
|
515
|
+
} : null;
|
|
516
|
+
} catch {
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
parse(stdout, code) {
|
|
521
|
+
let text = "";
|
|
522
|
+
let lastAssistant = "";
|
|
523
|
+
let model;
|
|
524
|
+
let failed = false;
|
|
525
|
+
for (const line of stdout.split("\n")) {
|
|
526
|
+
const t = line.trim();
|
|
527
|
+
if (!t) continue;
|
|
528
|
+
try {
|
|
529
|
+
const o = JSON.parse(t);
|
|
530
|
+
if (o.type === "result") {
|
|
531
|
+
if (typeof o.result === "string" && o.result.trim()) text = o.result.trim();
|
|
532
|
+
if (o.is_error) failed = true;
|
|
533
|
+
} else if (o.type === "system" && o.subtype === "init") model ??= cleanModel(o.model);
|
|
534
|
+
else {
|
|
535
|
+
const at = assistantText$1(o);
|
|
536
|
+
if (at) {
|
|
537
|
+
lastAssistant = at;
|
|
538
|
+
model ??= cleanModel(o.message?.model);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
} catch {}
|
|
542
|
+
}
|
|
543
|
+
if (!text) text = lastAssistant;
|
|
544
|
+
const { reply: visible, reanchors } = extractReanchors(text);
|
|
545
|
+
const reply = extractReplyBlock(visible);
|
|
546
|
+
return {
|
|
547
|
+
reply,
|
|
548
|
+
model,
|
|
549
|
+
reanchors,
|
|
550
|
+
ok: code === 0 && !failed && !!reply
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
//#endregion
|
|
555
|
+
//#region src/server/jam/adapter/opencode.ts
|
|
556
|
+
/**
|
|
557
|
+
* The opencode adapter. Spawns `opencode run --format json` headless - NDJSON events where
|
|
558
|
+
* finalized text parts are `{"type":"text","part":{type:"text",text}}` and errors are
|
|
559
|
+
* `{"type":"error","error":{name,data}}`. The LAST text part is the reply.
|
|
560
|
+
*
|
|
561
|
+
* Jail: headless opencode auto-REJECTS permission asks, and its `--auto` flag approves
|
|
562
|
+
* everything including arbitrary shell - so neither default works. The OPENCODE_PERMISSION
|
|
563
|
+
* env var (verified: it merges over the user's config for this one process) is DEFAULT-DENY
|
|
564
|
+
* with an explicit allowlist - so an unnamed or future tool is denied, not defaulted open.
|
|
565
|
+
* `--pure` runs without external plugins, which execute code OUTSIDE the tool-permission
|
|
566
|
+
* layer (a repo could otherwise ship a plugin the daemon's spawn would load and run).
|
|
567
|
+
* Claude-parity: edits yes, shell no. Subagents inherit it (verified live).
|
|
568
|
+
*
|
|
569
|
+
* opencode's stream names NO model in any event (verified against its schema) - so replies
|
|
570
|
+
* carry the harness without a model, same honest gap as codex. Never faked.
|
|
571
|
+
*/
|
|
572
|
+
const textPart = (o) => o.type === "text" && typeof o.part?.text === "string" && o.part.text.trim() ? String(o.part.text).trim() : void 0;
|
|
573
|
+
const opencodeAdapter = {
|
|
574
|
+
name: "opencode",
|
|
575
|
+
supportsSubagents: true,
|
|
576
|
+
spawnArgs(goal) {
|
|
577
|
+
return {
|
|
578
|
+
cmd: "opencode",
|
|
579
|
+
args: [
|
|
580
|
+
"run",
|
|
581
|
+
"--pure",
|
|
582
|
+
"--format",
|
|
583
|
+
"json",
|
|
584
|
+
goal
|
|
585
|
+
],
|
|
586
|
+
env: { OPENCODE_PERMISSION: "{\"*\":\"deny\",\"read\":\"allow\",\"edit\":\"allow\",\"glob\":\"allow\",\"grep\":\"allow\",\"list\":\"allow\",\"ls\":\"allow\",\"webfetch\":\"allow\",\"websearch\":\"allow\",\"task\":\"allow\",\"todowrite\":\"allow\",\"todoread\":\"allow\"}" }
|
|
587
|
+
};
|
|
588
|
+
},
|
|
589
|
+
earlyText(line) {
|
|
590
|
+
try {
|
|
591
|
+
const text = textPart(JSON.parse(line));
|
|
592
|
+
return text ? { text } : null;
|
|
593
|
+
} catch {
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
},
|
|
597
|
+
parse(stdout, code) {
|
|
598
|
+
let msgId;
|
|
599
|
+
let parts = [];
|
|
600
|
+
let failed = false;
|
|
601
|
+
for (const line of stdout.split("\n")) {
|
|
602
|
+
const t = line.trim();
|
|
603
|
+
if (!t) continue;
|
|
604
|
+
try {
|
|
605
|
+
const o = JSON.parse(t);
|
|
606
|
+
const part = textPart(o);
|
|
607
|
+
if (part) {
|
|
608
|
+
const id = typeof o.part?.messageID === "string" ? o.part.messageID : void 0;
|
|
609
|
+
if (id !== msgId) {
|
|
610
|
+
msgId = id;
|
|
611
|
+
parts = [];
|
|
612
|
+
}
|
|
613
|
+
parts.push(part);
|
|
614
|
+
} else if (o.type === "error") failed = true;
|
|
615
|
+
} catch {}
|
|
616
|
+
}
|
|
617
|
+
const { reply: visible, reanchors } = extractReanchors(parts.join("\n\n"));
|
|
618
|
+
const reply = extractReplyBlock(visible);
|
|
619
|
+
return {
|
|
620
|
+
reply,
|
|
621
|
+
reanchors,
|
|
622
|
+
ok: code === 0 && !failed && !!reply
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
//#endregion
|
|
627
|
+
//#region src/server/jam/adapter/pi.ts
|
|
628
|
+
/**
|
|
629
|
+
* The pi adapter (the pi coding agent). Spawns `pi -p --mode json` headless - NDJSON events
|
|
630
|
+
* where `message_end` carries each finished message and the terminal `agent_end` carries the
|
|
631
|
+
* authoritative full transcript. The reply is the last assistant text in `agent_end`
|
|
632
|
+
* (falling back to the last `message_end` when the stream was cut before it).
|
|
633
|
+
*
|
|
634
|
+
* Jail: pi has NO runtime permission system - the tool allowlist IS the jail, so bash is
|
|
635
|
+
* simply not on the list: read/edit/write/grep/find/ls. Claude-parity - edits yes, shell no
|
|
636
|
+
* (the packet carries untrusted text; a shell is an exfiltration channel).
|
|
637
|
+
* `--no-extensions --no-skills` keeps the headless run to those built-ins: extensions load
|
|
638
|
+
* arbitrary code outside the allowlist, which has no place in an unattended spawn.
|
|
639
|
+
*/
|
|
640
|
+
/** Assistant text + model of one message object (message_end payload or agent_end member). */
|
|
641
|
+
const assistantText = (m) => {
|
|
642
|
+
const msg = m;
|
|
643
|
+
if (!msg || msg.role !== "assistant") return null;
|
|
644
|
+
const text = (Array.isArray(msg.content) ? msg.content : []).filter((c) => c?.type === "text" && typeof c.text === "string").map((c) => String(c.text).trim()).filter(Boolean).join("\n\n");
|
|
645
|
+
if (!text && !msg.errorMessage) return null;
|
|
646
|
+
return {
|
|
647
|
+
text,
|
|
648
|
+
model: typeof msg.model === "string" && msg.model ? msg.model : void 0,
|
|
649
|
+
error: Boolean(msg.errorMessage) || msg.stopReason === "error"
|
|
650
|
+
};
|
|
651
|
+
};
|
|
652
|
+
//#endregion
|
|
653
|
+
//#region src/server/jam/adapter/index.ts
|
|
654
|
+
const adapters = {
|
|
655
|
+
claude: claudeAdapter,
|
|
656
|
+
codex: codexAdapter,
|
|
657
|
+
cursor: cursorAdapter,
|
|
658
|
+
droid: droidAdapter,
|
|
659
|
+
opencode: opencodeAdapter,
|
|
660
|
+
grok: grokAdapter,
|
|
661
|
+
pi: {
|
|
662
|
+
name: "pi",
|
|
663
|
+
supportsSubagents: false,
|
|
664
|
+
spawnArgs(goal) {
|
|
665
|
+
return {
|
|
666
|
+
cmd: "pi",
|
|
667
|
+
args: [
|
|
668
|
+
"-p",
|
|
669
|
+
"--mode",
|
|
670
|
+
"json",
|
|
671
|
+
"--no-extensions",
|
|
672
|
+
"--no-skills",
|
|
673
|
+
"--tools",
|
|
674
|
+
"read,edit,write,grep,find,ls",
|
|
675
|
+
goal
|
|
676
|
+
]
|
|
677
|
+
};
|
|
678
|
+
},
|
|
679
|
+
earlyText(line) {
|
|
680
|
+
try {
|
|
681
|
+
const o = JSON.parse(line);
|
|
682
|
+
if (o.type !== "message_end") return null;
|
|
683
|
+
const a = assistantText(o.message);
|
|
684
|
+
return a?.text && !a.error ? {
|
|
685
|
+
text: a.text,
|
|
686
|
+
model: a.model
|
|
687
|
+
} : null;
|
|
688
|
+
} catch {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
},
|
|
692
|
+
parse(stdout, code) {
|
|
693
|
+
let last = null;
|
|
694
|
+
let fromEnd = false;
|
|
695
|
+
let failed = false;
|
|
696
|
+
for (const line of stdout.split("\n")) {
|
|
697
|
+
const t = line.trim();
|
|
698
|
+
if (!t) continue;
|
|
699
|
+
try {
|
|
700
|
+
const o = JSON.parse(t);
|
|
701
|
+
if (o.type === "agent_end" && Array.isArray(o.messages)) for (let i = o.messages.length - 1; i >= 0; i--) {
|
|
702
|
+
const a = assistantText(o.messages[i]);
|
|
703
|
+
if (a) {
|
|
704
|
+
last = a;
|
|
705
|
+
fromEnd = true;
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
else if (o.type === "message_end" && !fromEnd) {
|
|
710
|
+
const a = assistantText(o.message);
|
|
711
|
+
if (a) last = a;
|
|
712
|
+
} else if (o.type === "error") failed = true;
|
|
713
|
+
} catch {}
|
|
714
|
+
}
|
|
715
|
+
if (last?.error) failed = true;
|
|
716
|
+
const { reply: visible, reanchors } = extractReanchors(last?.text ?? "");
|
|
717
|
+
const reply = extractReplyBlock(visible);
|
|
718
|
+
return {
|
|
719
|
+
reply,
|
|
720
|
+
model: last?.model,
|
|
721
|
+
reanchors,
|
|
722
|
+
ok: code === 0 && !failed && !!reply
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
//#endregion
|
|
308
728
|
//#region src/server/jam/journal.ts
|
|
309
729
|
/**
|
|
310
730
|
* The durable batch journal - design/.local/jam-jobs.json.
|
|
@@ -518,6 +938,28 @@ const fenceGroup = (pid) => {
|
|
|
518
938
|
if (pid) process.kill(-pid, "SIGKILL");
|
|
519
939
|
} catch {}
|
|
520
940
|
};
|
|
941
|
+
const FRAME_FILE = /\.(tsx|jsx|html)$/;
|
|
942
|
+
/** Every frame FILE under design/scenes, as `frame id -> mtimeMs`. Used to see which frames the
|
|
943
|
+
* agent creates or edits mid-job (so the working glow can follow the work, not sit on the frame
|
|
944
|
+
* the comment happened to be pinned to). Infra files (`_layout`, `_fixtures`) are skipped. */
|
|
945
|
+
const sceneFrameMtimes = (root) => {
|
|
946
|
+
const scenesDir = join(root, "design", "scenes");
|
|
947
|
+
const out = /* @__PURE__ */ new Map();
|
|
948
|
+
let entries;
|
|
949
|
+
try {
|
|
950
|
+
entries = readdirSync(scenesDir, { recursive: true });
|
|
951
|
+
} catch {
|
|
952
|
+
return out;
|
|
953
|
+
}
|
|
954
|
+
for (const rel of entries) {
|
|
955
|
+
const base = rel.split("/").pop() ?? rel;
|
|
956
|
+
if (base.startsWith("_") || !FRAME_FILE.test(base)) continue;
|
|
957
|
+
try {
|
|
958
|
+
out.set(toFrameId(`scenes/${rel}`), statSync(join(scenesDir, rel)).mtimeMs);
|
|
959
|
+
} catch {}
|
|
960
|
+
}
|
|
961
|
+
return out;
|
|
962
|
+
};
|
|
521
963
|
/** The early ack posts VERBATIM, so a first line that narrates the agent's plan instead of
|
|
522
964
|
* addressing the owner must not ship. Deliberately NARROW - "acknowledg" only ever appears
|
|
523
965
|
* in meta-talk (a real ack never names itself), and the openers are pure plan phrasing.
|
|
@@ -533,7 +975,7 @@ function createJam(root, cfg, adapter, log = () => {}, hooks = {}) {
|
|
|
533
975
|
let stopped = false;
|
|
534
976
|
const activeChildren = /* @__PURE__ */ new Set();
|
|
535
977
|
const runAgent = (goal, onSpawn, onEarly) => new Promise((resolve) => {
|
|
536
|
-
const { cmd, args } = adapter.spawnArgs(goal);
|
|
978
|
+
const { cmd, args, env } = adapter.spawnArgs(goal);
|
|
537
979
|
let child;
|
|
538
980
|
try {
|
|
539
981
|
child = spawn(cmd, args, {
|
|
@@ -543,7 +985,12 @@ function createJam(root, cfg, adapter, log = () => {}, hooks = {}) {
|
|
|
543
985
|
"ignore",
|
|
544
986
|
"pipe",
|
|
545
987
|
"ignore"
|
|
546
|
-
]
|
|
988
|
+
],
|
|
989
|
+
env: {
|
|
990
|
+
...process.env,
|
|
991
|
+
...env,
|
|
992
|
+
PWD: root
|
|
993
|
+
}
|
|
547
994
|
});
|
|
548
995
|
} catch {
|
|
549
996
|
return resolve({
|
|
@@ -587,7 +1034,9 @@ function createJam(root, cfg, adapter, log = () => {}, hooks = {}) {
|
|
|
587
1034
|
for (const line of lines) {
|
|
588
1035
|
const hit = adapter.earlyText(line);
|
|
589
1036
|
if (!hit) continue;
|
|
590
|
-
const
|
|
1037
|
+
const visible = extractReanchors(hit.text).reply;
|
|
1038
|
+
const block = extractReplyBlock(visible);
|
|
1039
|
+
const text = block !== visible.trim() ? block : block.split(/\n\s*\n/)[0].trim();
|
|
591
1040
|
if (!text || metaNarration(text)) continue;
|
|
592
1041
|
earlyFired = true;
|
|
593
1042
|
try {
|
|
@@ -688,8 +1137,30 @@ function createJam(root, cfg, adapter, log = () => {}, hooks = {}) {
|
|
|
688
1137
|
persist();
|
|
689
1138
|
const member = buildMember(p, replay(readLog(commentsDir, b.board)));
|
|
690
1139
|
const packet = buildPacket(b.batchId, [member]);
|
|
691
|
-
|
|
692
|
-
const
|
|
1140
|
+
const lit = /* @__PURE__ */ new Set();
|
|
1141
|
+
const light = (f) => {
|
|
1142
|
+
if (f && !lit.has(f)) {
|
|
1143
|
+
hooks.work?.(f, true);
|
|
1144
|
+
lit.add(f);
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
const clearGlow = () => {
|
|
1148
|
+
for (const f of lit) hooks.work?.(f, false);
|
|
1149
|
+
lit.clear();
|
|
1150
|
+
};
|
|
1151
|
+
const startMtimes = sceneFrameMtimes(root);
|
|
1152
|
+
const touched = /* @__PURE__ */ new Set();
|
|
1153
|
+
light(member.frame);
|
|
1154
|
+
const followWork = () => {
|
|
1155
|
+
for (const [id, mt] of sceneFrameMtimes(root)) if (startMtimes.get(id) !== mt) touched.add(id);
|
|
1156
|
+
for (const id of touched) light(id);
|
|
1157
|
+
if (member.frame && lit.has(member.frame) && !touched.has(member.frame) && touched.size > 0) {
|
|
1158
|
+
hooks.work?.(member.frame, false);
|
|
1159
|
+
lit.delete(member.frame);
|
|
1160
|
+
}
|
|
1161
|
+
for (const f of lit) hooks.work?.(f, true);
|
|
1162
|
+
};
|
|
1163
|
+
const beat = setInterval(followWork, 2e3);
|
|
693
1164
|
beat.unref?.();
|
|
694
1165
|
let earlyBody;
|
|
695
1166
|
let run;
|
|
@@ -707,20 +1178,20 @@ function createJam(root, cfg, adapter, log = () => {}, hooks = {}) {
|
|
|
707
1178
|
}
|
|
708
1179
|
logRun(b.batchId, run.raw);
|
|
709
1180
|
if (stopped) {
|
|
710
|
-
|
|
1181
|
+
clearGlow();
|
|
711
1182
|
return;
|
|
712
1183
|
}
|
|
713
1184
|
if (run.ok) {
|
|
714
1185
|
if (run.reply !== earlyBody) writeReply(b, p, run.reply, run.model);
|
|
715
1186
|
emitReanchors(b, run.reanchors);
|
|
716
1187
|
hooks.changed?.(b.board);
|
|
717
|
-
|
|
1188
|
+
clearGlow();
|
|
718
1189
|
finish(b);
|
|
719
1190
|
log(` jam: replied on ${b.board}${run.model ? ` (${run.model})` : ""}${run.reanchors.length ? ` · re-pinned ${run.reanchors.length}` : ""}`);
|
|
720
1191
|
} else if (b.attempts >= MAX_ATTEMPTS) {
|
|
721
|
-
writeReply(b, p, "I couldn't finish that one.
|
|
1192
|
+
writeReply(b, p, "I couldn't finish that one. The raw run log is in design/.local/jam-logs - the troubleshooting drill in design/instructions/jam.md reads it. Or just rephrase and try again.", run.model);
|
|
722
1193
|
hooks.changed?.(b.board);
|
|
723
|
-
|
|
1194
|
+
clearGlow();
|
|
724
1195
|
finish(b);
|
|
725
1196
|
log(` jam: gave up on ${b.board} after ${b.attempts} attempts`);
|
|
726
1197
|
} else {
|
|
@@ -848,7 +1319,7 @@ function createJam(root, cfg, adapter, log = () => {}, hooks = {}) {
|
|
|
848
1319
|
* one merged glow. Returns null when the adapter is unavailable or another dev server
|
|
849
1320
|
* already holds the repo lock (that one runs the loop; this one watches without it). */
|
|
850
1321
|
function startJam(root, cfg, log = () => {}, onChanged = () => {}) {
|
|
851
|
-
const adapter = cfg.agent
|
|
1322
|
+
const adapter = adapters[cfg.agent] ?? null;
|
|
852
1323
|
if (!adapter) {
|
|
853
1324
|
log(` jam: the "${cfg.agent}" adapter is not available yet; Live Jam is off`);
|
|
854
1325
|
return null;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as NAME, r as PKG } from "./cli.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-
|
|
2
|
+
import { l as detectHost, s as loadConfig } from "./manifest-DIsp3ldB.mjs";
|
|
3
|
+
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-CgLVYTR-.mjs";
|
|
4
4
|
import { basename, dirname, join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { createLogger, createServer, searchForWorkspaceRoot } from "vite";
|
|
@@ -171,7 +171,7 @@ async function dev(root, portFlag) {
|
|
|
171
171
|
});
|
|
172
172
|
}
|
|
173
173
|
if (config.jam) {
|
|
174
|
-
const { startJam } = await import("./daemon-
|
|
174
|
+
const { startJam } = await import("./daemon-B_8BmmSJ.mjs");
|
|
175
175
|
const jam = startJam(root, config.jam, (m) => console.log(m), (board) => server.ws.send("sh:jam-comment", { board }));
|
|
176
176
|
if (jam) {
|
|
177
177
|
const close = server.close.bind(server);
|
|
@@ -180,7 +180,7 @@ async function dev(root, portFlag) {
|
|
|
180
180
|
return close();
|
|
181
181
|
});
|
|
182
182
|
}
|
|
183
|
-
} else if (config.jamOff === "no-agent") console.log(` jam: no agent CLI on PATH (claude or
|
|
183
|
+
} else if (config.jamOff === "no-agent") console.log(` jam: no agent CLI on PATH (claude, codex, cursor, droid, opencode, grok, or pi) - install one and tag @${NAME} in a comment\n`);
|
|
184
184
|
return server;
|
|
185
185
|
}
|
|
186
186
|
//#endregion
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as NAME } from "./cli.mjs";
|
|
2
|
-
import { a as
|
|
2
|
+
import { a as writeManifest, c as detectAgent, l as detectHost, o as DEFAULTS, r as scanFrames, u as readJson } from "./manifest-DIsp3ldB.mjs";
|
|
3
3
|
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname, join, relative } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -188,7 +188,7 @@ function init(root, opts) {
|
|
|
188
188
|
console.log(`\n commit design/ - only .local/ is ignored`);
|
|
189
189
|
console.log(` uninstall = delete design/, remove the ${NAME} dependency${host.tsconfigSweepsDesign ? ", revert the \"design\" line in tsconfig exclude" : ""}`);
|
|
190
190
|
if (!noApp(host) && !existsSync(join(design, "DESIGN.md"))) console.log(`\n note: design/DESIGN.md (the brand doc) does not exist yet - have your agent create it from the app's tokens (instructions/brand.md, Path A) to reach the idle state.`);
|
|
191
|
-
if (!jamAgent) console.log(`\n note: Live Jam found no agent CLI on PATH (claude or
|
|
191
|
+
if (!jamAgent) console.log(`\n note: Live Jam found no agent CLI on PATH (claude, codex, cursor, droid, opencode, grok, or pi) - install one and it arms itself on the next \`${NAME} dev\`.`);
|
|
192
192
|
else if (created.includes("design/config.ts")) console.log(`\n Live Jam is on (${jamAgent}): tag @${NAME} in a canvas comment and your agent does the work, then replies in the thread.`);
|
|
193
193
|
console.log(`\n next: npx ${NAME} dev (or: npx ${NAME} canvas - same thing; canvas on http://localhost:${DEFAULTS.port} by default)\n`);
|
|
194
194
|
if (!noApp(host)) console.log(` then, to your agent: "Read design/AGENTS.md. This is our first session - follow design/instructions/welcome.md."\n`);
|
|
@@ -488,7 +488,8 @@ const jamBlock = (agent) => agent ? ` // Live Jam - tag @${NAME} in a canvas co
|
|
|
488
488
|
// the wrong tool, raise concurrency for more frames at once, \`jam: false\` to turn it off.
|
|
489
489
|
jam: { agent: ${JSON.stringify(agent)}, concurrency: 6 },` : ` // Live Jam - tag @${NAME} in a canvas comment and your coding agent picks the job up,
|
|
490
490
|
// edits the real frame, and replies in the thread. No agent CLI was on PATH when init ran;
|
|
491
|
-
// install claude
|
|
491
|
+
// install an agent CLI (claude, codex, cursor, droid, opencode, grok, or pi) and it arms
|
|
492
|
+
// itself, or name one here. \`jam: false\` turns it off.
|
|
492
493
|
// jam: { agent: "claude", concurrency: 6 },`;
|
|
493
494
|
const configTemplate = (mode, jamAgent) => `// ${NAME} config - OPTIONAL. Delete this file and everything still works on defaults.
|
|
494
495
|
// Theme lives in design/theme.css (it imports your app's real stylesheet) - not here.
|