@supersuit/session-player 0.0.0-stage → 0.4.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 ADDED
@@ -0,0 +1,71 @@
1
+ # Changelog
2
+
3
+ ## 0.4.0 (2026-10-11)
4
+
5
+ - **Renamed `@supersuit/session-player`** (Gary, 2026-10-11: it plays recorded sessions; case studies and the coming Helm demo are content it plays). Same code as 0.3.2. Swap the package name in your imports; the exports, the scenario format, `SimulatorPlayer` and `<supersuit-simulator>` are unchanged. The CLI is `session-player`, and `simulator` still works. The repository is SupersuitUp/session-player (GitHub redirects the old address).
6
+
7
+ ## 0.3.2 (2026-10-11)
8
+
9
+ - The last `@supersuit/simulator`. The package is renamed `@supersuit/session-player` (Gary, 2026-10-11: it plays recorded sessions, and case studies and the coming Helm demo are content it plays). No code change; the README and description point to the new name.
10
+
11
+ ## 0.3.1 (2026-10-11)
12
+
13
+ - A moment never clips a line in half. In a short screen (the MacBook window on continentalworks.ai/demo) the moment's parts were squeezed by flexbox, so the answer and the person's line were cut mid-line. Each part now keeps its height, the big answer is a little smaller (up to 22 px, five lines), and the moment scrolls rather than clipping if it still does not fit. In moments the composer no longer repeats the person's line, which the moment already shows; it keeps its status and its Send and mic.
14
+
15
+ ## 0.3.0 (2026-10-11)
16
+
17
+ - Highlights (Wilson, 2026-10-11: "make it 40 seconds", "the key things, the most important moments for the human"): `speed` (embed `speed=3`) shortens the work between beats; `highlights` (embed `highlights`) shows where each turn landed, its last answer's first paragraph, with reading time that is never sped up; `steps` (embed) sets how many steps a turn keeps. A recast spec may name a turn's landing answer with `highlight: { "t6": 1 }`.
18
+ - Relief you can see: in highlights the panel is "Handled for <person>", only what got handled (artifacts of kind `other`) ticking off as it lands; the close card can list what Freeda handled (`close.handled`, carried and recast by `recast`). Measured facts only, never time saved.
19
+ - One moment per screen: `moments` (embed `moments`) replaces the scrolling log with the person's line small, where Freeda landed large, a counter ("3 of 8") and what is handled so far. Wilson: "the scroll down ... doesn't give me the TL;DR."
20
+ - An obvious Play, folded into the one Start (Wilson: "make sure the red button is a play button"): the Start demo card's button is now a large pulsing red Play with "Start demo" under it, the Autoplay choice still beside it. There is exactly one Play: the header keeps the Autoplay switch (on by default, named so), and the composer keeps Send and the mic, so no second Play competes with Start.
21
+ - Embeds take the cut too: `speed`, `highlights`, `moments` and `steps` on the iframe URL and as attributes on `<supersuit-simulator>`. A highlights cut still waits on the Start card unless the host opts in with `start=auto`.
22
+ - Board picks on autoplay follow `speed`, as the rest of the run does.
23
+
24
+ ## 0.2.3 (2026-10-11)
25
+
26
+ - **A Start demo button.** Nothing plays on load anywhere. The device shows a Start card over its screen: the scenario's title, one measured line (commands from the person, Freeda's minutes of work), a Start demo button, and the Autoplay choice beside it (on: it plays itself; off: you play the person). Sending a command or tapping the mic also counts as starting. Play again appears at the end, as before. A host opts into starting at once with `start: 'auto'`, `?start=auto` or `start="auto"` on the tag. Reduced motion is respected (no animation on the card).
27
+
28
+ ## 0.2.2 (2026-10-11)
29
+
30
+ - **Autoplay is on by default, and is called Autoplay.** The playback checkbox was labelled "auto", next to a device switch whose first option is also Auto, and it was unticked by default, so a viewer read Auto as not selected. Off, the viewer plays the person: each message waits, prefilled, for a tap on Send (or the mic), and each board waits for the viewer's pick, answered with what the person really chose. Turning it on or off says so in a toast. `autoplay={false}`, `?autoplay=off` or `autoplay="off"` on the tag start it off (the old bare `?autoplay` still means on).
31
+ - The device switch marks the active mode and, on Auto, what Auto resolved to: "Auto · iPhone", "Auto · MacBook", or "Auto · this phone". A forced mode is the marked one and Auto carries no hint.
32
+ - Toasts: a tap on the device switch says what is shown now and why ("Now simulating a MacBook: this session ran in Claude Code on a Mac."); a pick on a board says "Same call as X." or "In the real session, X chose Y." Quiet, polite to screen readers, over the demo with no layout shift, gone after about three seconds.
33
+
34
+ ## 0.2.1 (2026-10-11)
35
+
36
+ - A MacBook in a column too narrow for a believable laptop (under 560 px, such as the book's 434 px figure) keeps its Claude Code window and its "Simulating a MacBook" label and drops the lid, menu bar and base, rather than shrinking into a toy where the composer and the counter overlap.
37
+
38
+ ## 0.2.0 (2026-10-11)
39
+
40
+ - Device framing. On a desktop or tablet the demo is drawn inside a device: an iPhone (status bar with a real clock, Dynamic Island, side buttons), or a MacBook (lid, notch, menu bar with a real clock, a Claude Code window in a terminal theme, base) when the scenario ran on a Mac. On a phone the demo is the phone, full-bleed, as before.
41
+ - `device` mode: `auto` (default), `iphone` or `macbook`, set by the `device` prop, `?device=` on the embed URL, the `device` attribute on `<supersuit-simulator>`, or the switch on the demo itself (Auto / iPhone / MacBook).
42
+ - A MacBook forced on a phone is never a laptop shrunk into a phone: it is a desktop-style window at phone width with a persistent "Simulating a MacBook" pill. Whenever a frame is drawn, a quiet label outside the screen says what is simulated.
43
+ - Phone or desktop is decided from capability signals (`pointer: coarse`, `hover: none`, `maxTouchPoints`) and the viewport's shorter side, re-read on resize and rotation (`classifyViewer`, `resolveDevice` in `device.mjs`).
44
+ - The scenario format gains an optional `device: "iphone" | "macbook"`, validated; scenarios without it are unchanged and show as an iPhone. `convert` sets `macbook` for a session typed in a terminal (or the desktop app) on a Mac and leaves it unset when it cannot tell; `convert --device` overrides; `recast` keeps it.
45
+ - The player's main container is now named (`container: ss-sim`), so its wide-layout rule follows the MacBook window it sits in. Hosts that copy the player's browser files must also copy `device.mjs` (the player imports it).
46
+
47
+ ## 0.1.1 (2026-10-11)
48
+
49
+ - Renamed for readers: the Supersuit Simulator is now the **Supersuit Session demo** (the README, the package description, the CLI help and the default page title of `simulator serve`). The package name, the `simulator` CLI, the `supersuit-simulator/1` format, `SimulatorPlayer` and `<supersuit-simulator>` are unchanged, so no install or embed breaks.
50
+
51
+ ## 0.1.0 (2026-10-10)
52
+
53
+ - The scenario format (`supersuit-simulator/1`) and `validate`.
54
+ - `convert`: drafts a scenario from a Claude Code session transcript. Human turns only, interrupts marked as redirects, tool work condensed into steps timed by wall clock, plumbing tools folded into the next step, chapters from the session name, files and links collected as artifacts, credentials and home paths removed with every cut recorded.
55
+ - Questions the agent asked are kept as the whole board the person saw (`step.ask = { questions: [{ header, question, multiSelect, options: [{ label, description }], answer, chosen }], dismissed }`), read from the session's structured result, and the player draws them as that board: numbered options, the pick checked, a typed answer shown as the person's own words, a closed board marked.
56
+ - Steps and artifacts carry when they happened, so a replay interleaves them with replies.
57
+ - The player: `SimulatorPlayer` (`@supersuit/simulator/player`), mobile first, with persona picker, artifacts and timeline switches; `playback` condenses long turns without changing their totals.
58
+ - `tidy`: cleans the person's commands for display (typo fixes from a reviewer, screenshot paths to [screenshot]), refusing any fix that rewrites what was said; originals kept in `human.raw`, changes in `cuts`.
59
+ - Phone numbers and email addresses are removed from everything a scenario keeps, recorded as cuts.
60
+ - Artifacts are deliverables only (pages, media, documents), never code, config or folders; `tidy` takes `artifacts: { drop, add }` so a reviewer can add what the transcript cannot show (kind `other`, e.g. a release submitted to a store).
61
+ - `recast`: tells a different story from the same real run (new persona and subject, same commands, steps, timing and decisions), marked illustrative on screen, with a `forbid` list that refuses to finish while any word from the original survives.
62
+ - Boards are playable and match the native question board: pinned to the bottom of the phone in place of the composer, one question at a time with a tab per question. Playback pauses on each board, the viewer taps an option (or Other, or Type something else where the person closed it), each pick is answered with the real choice, and the recording resumes on the real path. Autoplay picks the real answers. Once answered, the conversation keeps a compact record (`Asked`): each question, what was chosen, and where the viewer picked differently.
63
+ - The mic "says" the command: tapping it streams the words into the composer like dictation (`dictate`), then sends. It is a 48 px circle, not a stretched oval. The counter reads "No work yet" before anything runs.
64
+ - Idle time is removed: thinking before a step counts at most 3 min, any one step at most 10 min, a question at most 5 min, and a timed-out command counts as no work. What was removed is kept as `step.idleMs`, recorded in cuts, summed by `summarize`, and named on the end screen.
65
+ - Images: what the person pasted is kept from the session (`human.images`); a reviewer attaches what they were shown (`tidy` `images`: on a step, a board, a single option, or the command), since a session often names images only through loop variables; `bundle` copies and shrinks every image into `assets/<id>/` beside the scenario; `recast` must map every image to a replacement by id or it refuses; the player shows option thumbnails, step galleries, pasted images on the command, and tap to enlarge. A step that showed images is never folded into quick steps.
66
+ - The close card (`CloseCard`): a celebration when the session ends, in measured numbers only: Freeda's work time and steps against the person's commands and minutes deciding, the chore freed (from a workflow catalog row), the edge points earned with their four counts, the things made, and the idle time left out. Never time saved. The facts live in an optional `close` block (`tidy` sets it; `recast` carries it, recasts its words, and may restate its measured numbers for the turns it keeps); `validate` refuses an edge total that is not the sum of its counts.
67
+ - Embeddable in any page or book: an `/embed` page for an `<iframe>` (query switches `scenario`, `artifacts`, `timeline`, `autoplay`, `height`), a `<supersuit-simulator>` custom element (`@supersuit/simulator/embed`), `scenarioId` and `height` props, and every overlay contained inside the player, never fixed to the host page's viewport.
68
+ - `bundle` writes a provenance recipe beside every image (mode derive: what it came from, what resized it, and the source's own recipe nested), so a bundled scenario passes a wiki's image-provenance gate.
69
+ - `recast` gives every image a neutral id (`<recast id>-image-<n>`, one per picture), since ids became file names and carried the original's subject.
70
+ - A session opening with a slash command shows the command as typed, not its tags; usage-limit and API-error notices are dropped (they are the harness, and carry the operator's plan and time zone); recast checks and recasts the close card's source notes.
71
+ - The `simulator` CLI: `convert`, `validate`, `tidy`, `recast`, `bundle`, `serve`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SupersuitUp
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,99 @@
1
- # Temporary Holding Version
1
+ # @supersuit/session-player
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Plays recorded agent sessions (case studies, and the coming Helm demo): step into the shoes of someone who needs a great deal of work done, give their commands on a simulated phone, and watch the work and the things it makes appear. Every scenario is built from a real agent session, with the person's corrections left in.
4
+
5
+ Renamed from `@supersuit/simulator` in 0.4.0 (2026-10-11): it plays recorded sessions, and case studies and the coming Helm demo are content it plays. To move, swap the package name in your imports; the exports, the scenario format (`supersuit-simulator/1`), `SimulatorPlayer` and `<supersuit-simulator>` are unchanged, and the CLI answers to both `session-player` and the old `simulator`.
6
+
7
+ This package holds the **scenario format**, the **converter** that drafts a scenario from a Claude Code session transcript, and the **player**, a React component that plays scenarios on a simulated phone.
8
+
9
+ ## Install
10
+
11
+ ```
12
+ npm install -g @supersuit/session-player
13
+ ```
14
+
15
+ ## Use
16
+
17
+ ```
18
+ simulator convert ~/.claude/projects/<dir>/<session-id>.jsonl --title "Releasing an album" --persona Gary --out album.json
19
+ simulator tidy album.json --fixes fixes.json --out album.json # {"t7": "corrected text"}
20
+ simulator recast album.json --spec ambient.json --out ambient.json # same run, different story
21
+ simulator bundle album.json # images into assets/<id>/
22
+ simulator validate album.json
23
+ simulator serve album.json # watch it at http://127.0.0.1:4460/
24
+ ```
25
+
26
+ `convert` keeps only what the person actually typed as turns (tool results, skill bodies, task notifications, messages from other sessions and interrupt markers are not turns), marks a turn that followed an interrupt as the person redirecting, condenses the agent's tool work into named steps timed by the wall clock, and collects what it delivered: pages it published and the documents, images and audio it made or pointed at (never code or folders, which already show as steps).
27
+
28
+ **A draft is not ready to publish.** The converter removes credential-shaped strings, phone numbers, email addresses and home-directory paths and records every edit in `cuts`, but it does not anonymize people. Do that, and review the draft, before anyone else sees it.
29
+
30
+ ## The format
31
+
32
+ `supersuit-simulator/1`, one JSON object:
33
+
34
+ | Field | What it holds |
35
+ |---|---|
36
+ | `persona` | `{ name, role, blurb }` |
37
+ | `source` | `{ kind: session \| persona, sessionId, date, illustrative }` |
38
+ | `chapters[]` | `{ title, turn }`, from the session's name changes |
39
+ | `turns[]` | `{ id, at, human { text, via, choices[] }, intervention, steps[] { label, tool, ms, count }, replies[] { text, at }, artifacts[] }` |
40
+ | `artifacts[]` | `{ id, kind: file \| link \| other, title, path \| url, turn, at }` |
41
+ | `cuts[]` | `{ turn, what, reason }`: every edit, so nothing is changed silently |
42
+
43
+ `device` (optional) is where the real session ran: `iphone` or `macbook`. The converter sets `macbook` when the person typed into a terminal (or the desktop app) on a Mac, and leaves it out when it cannot tell (Remote Control and SDK sessions); a scenario without it is shown as an iPhone.
44
+
45
+ `intervention` is one of `null`, `redirect`, `correct`, `approve`, `decide`. The converter sets only `redirect`; the rest are set by the person reviewing the draft.
46
+
47
+ ## The player
48
+
49
+ ```js
50
+ import { SimulatorPlayer } from '@supersuit/session-player/player';
51
+ <SimulatorPlayer scenario={album} /> // one scenario
52
+ <SimulatorPlayer scenarios={[dana, marcus]} /> // persona picker first
53
+ <SimulatorPlayer scenario={album} artifacts={false} timeline={false} /> // a limited embed
54
+ ```
55
+
56
+ Nothing plays until the viewer presses Start demo (a host may pass `start: 'auto'` or `?start=auto`). By default the session then plays itself (Autoplay). Untick Autoplay and the viewer plays the person, tapping each command (or the mic to say it), and watches the steps play, sped up from their real durations, with a running count of the work done for the persona. Decisions the person made show as their own cards; things made collect in a side panel on wide screens and a slide-up tray on phones. Needs React 18 or newer; no build step and no stylesheet to import.
57
+
58
+ ## Embedding it in a page or a book
59
+
60
+ Any page, including a book reader, can drop in an iframe:
61
+
62
+ ```html
63
+ <iframe src="https://<host>/embed?scenario=low-tide-tapes&timeline=off" style="width:100%;height:760px;border:0"></iframe>
64
+ ```
65
+
66
+ A page that runs modules can use the tag instead (it brings its own React through an import map or a bundler):
67
+
68
+ ```html
69
+ <supersuit-simulator src="/simulator/scenarios" scenario="low-tide-tapes" artifacts="off"></supersuit-simulator>
70
+ <script type="module">import { defineSimulatorElement } from '@supersuit/session-player/embed'; defineSimulatorElement();</script>
71
+ ```
72
+
73
+ ## A highlights cut
74
+
75
+ For a short page demo (about 40 seconds), pass `speed` (work between beats runs that many times faster), `highlights` (each turn shows only where it landed, its answer's first paragraph, with real reading time, and the panel ticks off what got handled), `moments` (one screen per moment instead of a scrolling log) and `steps` (how many steps a turn keeps). Same names as props, embed query switches (`?speed=3&highlights&moments&steps=1`) and tag attributes. A recast spec may name a turn's landing answer with `highlight: { "t6": 1 }`, and a scenario's `close.handled` lists what Freeda handled, in measured facts. The Start card still waits for the viewer.
76
+
77
+ ## Device framing
78
+
79
+ `device` is `auto` (the default), `iphone` or `macbook`: the `device` prop, `?device=` on the embed URL, or the `device` attribute on the tag. The viewer can also switch it on the demo (Auto / iPhone / MacBook).
80
+
81
+ | | on a phone | on a desktop or tablet |
82
+ | --- | --- | --- |
83
+ | `auto` | full-bleed: the demo is the phone | an iPhone, or a MacBook when the scenario's `device` is `macbook` |
84
+ | `iphone` | full-bleed | an iPhone |
85
+ | `macbook` | a desktop-style window at phone width, labelled "Simulating a MacBook"; never a laptop shrunk into a phone | a MacBook: lid, menu bar clock, a Claude Code window, base |
86
+
87
+ Whenever a device is drawn, a quiet label outside its screen says what is simulated. Phone or desktop is read from capability signals (coarse pointer, no hover, touch points) and the viewport's shorter side, again on every resize and rotation; a narrow desktop window is still a desktop and an iPad is a desktop unless it is in a narrow split view. Frames are CSS and SVG, sized from the height the player is given, so the same iframe height keeps fitting.
88
+
89
+ Nothing in the player is fixed to the viewport: the close card and the enlarged image stay inside the player's own box.
90
+
91
+ ## Library
92
+
93
+ ```js
94
+ import { convert, parseSession, validate } from '@supersuit/session-player';
95
+ const scenario = convert(parseSession(text), { title: 'Releasing an album' });
96
+ const problems = validate(scenario); // [] when valid
97
+ ```
98
+
99
+ Exit codes: 0 ok, 1 invalid or refused, 2 usage error. Node 20 or newer, no dependencies.
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+ // simulator: draft and check Supersuit Session demo scenarios.
3
+ import { readFileSync, writeFileSync } from 'node:fs';
4
+ import { convert, parseSession } from '../src/convert.mjs';
5
+ import { validate } from '../src/scenario.mjs';
6
+ import { createServer } from '../src/serve.mjs';
7
+ import { tidy } from '../src/tidy.mjs';
8
+ import { recast } from '../src/recast.mjs';
9
+ import { bundle } from '../src/bundle.mjs';
10
+ import { dirname, resolve } from 'node:path';
11
+
12
+ const HELP = `simulator - draft and check Supersuit Session demo scenarios
13
+
14
+ Usage:
15
+ simulator convert <session.jsonl> [--out <file>] [--title <t>] [--persona <name>] [--id <id>] [--device iphone|macbook] [--json]
16
+ Draft a scenario from a Claude Code session transcript. Writes the scenario to --out
17
+ (or stdout) and a one-line summary to stderr; --json puts the summary on stdout instead.
18
+ Records where the session ran (device) when the transcript can tell: typed in a terminal
19
+ on a Mac is a macbook; --device sets it when you know better.
20
+ simulator validate <scenario.json> [--json]
21
+ Check a scenario against the format. Exits 1 if anything is wrong.
22
+ simulator tidy <scenario.json> [--fixes <fixes.json>] [--out <file>]
23
+ Clean the person's commands for display: screenshot paths become [screenshot], and
24
+ fixes ({"t7": "corrected text"}) correct typos. Refuses a fix that rewrites what was
25
+ said. The original stays in human.raw; every change is recorded in cuts.
26
+ simulator recast <scenario.json> --spec <spec.json> [--out <file>]
27
+ Tell a different story from the same run: spec is { id, title, persona, keep: [turn ids],
28
+ replace: [[from, to]], forbid: [words] }. A "from" starting with re: is a regex (global,
29
+ case-insensitive). Refuses while any forbidden word survives. Marked illustrative.
30
+ simulator bundle <scenario.json> [--out <file>]
31
+ Copy every image into assets/<scenario id>/ beside the scenario, shrunk for the phone,
32
+ and point the scenario at the copies. Refuses if an image source is missing.
33
+ simulator serve <scenario.json>... [--port 4460] [--host 127.0.0.1]
34
+ Watch scenarios in the player in a browser. Add ?autoplay=off, ?artifacts=off,
35
+ ?timeline=off, ?device=iphone|macbook or ?start=auto to the URL to see the limited views an embed uses.
36
+
37
+ A draft is not a publishable scenario: people in it are not anonymized yet.
38
+ Exit codes: 0 ok, 1 invalid or refused, 2 usage error.`;
39
+
40
+ function args(argv) {
41
+ const pos = [];
42
+ const flags = {};
43
+ for (let i = 0; i < argv.length; i++) {
44
+ const a = argv[i];
45
+ if (a === '--json' || a === '--help' || a === '-h') flags[a.replace(/^-+/, '')] = true;
46
+ else if (a.startsWith('--')) flags[a.slice(2)] = argv[++i];
47
+ else pos.push(a);
48
+ }
49
+ return { pos, flags };
50
+ }
51
+
52
+ const { pos, flags } = args(process.argv.slice(2));
53
+ const [cmd, file] = pos;
54
+ if (flags.help || flags.h || !cmd) { console.log(HELP); process.exit(cmd || flags.help || flags.h ? 0 : 2); }
55
+
56
+ try {
57
+ if (cmd === 'convert') {
58
+ if (!file) throw Object.assign(new Error('convert needs a session file'), { code: 2 });
59
+ if (flags.device && !['iphone', 'macbook'].includes(flags.device)) throw Object.assign(new Error('--device must be iphone or macbook'), { code: 2 });
60
+ const sc = convert(parseSession(readFileSync(file, 'utf8')), {
61
+ title: flags.title, id: flags.id, persona: flags.persona ? { name: flags.persona } : undefined,
62
+ device: flags.device,
63
+ });
64
+ const problems = validate(sc);
65
+ const out = JSON.stringify(sc, null, 2) + '\n';
66
+ if (flags.out) writeFileSync(flags.out, out); else if (!flags.json) process.stdout.write(out);
67
+ const summary = { ok: problems.length === 0, turns: sc.turns.length, redirects: sc.turns.filter((t) => t.intervention).length, steps: sc.turns.reduce((n, t) => n + t.steps.length, 0), artifacts: sc.artifacts.length, cuts: sc.cuts.length, problems, out: flags.out ?? null };
68
+ if (flags.json) console.log(JSON.stringify(summary));
69
+ else console.error(`drafted ${summary.turns} turns, ${summary.steps} steps, ${summary.artifacts} artifacts, ${summary.cuts} cuts${problems.length ? `; PROBLEMS: ${problems.join('; ')}` : ''}`);
70
+ process.exit(problems.length ? 1 : 0);
71
+ } else if (cmd === 'validate') {
72
+ if (!file) throw Object.assign(new Error('validate needs a scenario file'), { code: 2 });
73
+ const problems = validate(JSON.parse(readFileSync(file, 'utf8')));
74
+ if (flags.json) console.log(JSON.stringify({ ok: problems.length === 0, problems }));
75
+ else console.log(problems.length ? problems.map((p) => `- ${p}`).join('\n') : 'valid');
76
+ process.exit(problems.length ? 1 : 0);
77
+ } else if (cmd === 'tidy') {
78
+ if (!file) throw Object.assign(new Error('tidy needs a scenario file'), { code: 2 });
79
+ const fixes = flags.fixes ? JSON.parse(readFileSync(flags.fixes, 'utf8')) : {};
80
+ const out = tidy(JSON.parse(readFileSync(file, 'utf8')), fixes);
81
+ const text = JSON.stringify(out, null, 2) + '\n';
82
+ if (flags.out) writeFileSync(flags.out, text); else process.stdout.write(text);
83
+ console.error(`tidied ${out.turns.filter((t) => t.human.raw).length} commands`);
84
+ } else if (cmd === 'recast') {
85
+ if (!file || !flags.spec) throw Object.assign(new Error('recast needs a scenario file and --spec'), { code: 2 });
86
+ const spec = JSON.parse(readFileSync(flags.spec, 'utf8'));
87
+ spec.replace = (spec.replace ?? []).map(([from, to]) => [from.startsWith('re:') ? new RegExp(from.slice(3), 'gi') : from, to]);
88
+ const out = recast(JSON.parse(readFileSync(file, 'utf8')), spec);
89
+ const problems = validate(out);
90
+ if (problems.length) throw new Error(`recast is not a valid scenario: ${problems[0]}`);
91
+ const text = JSON.stringify(out, null, 2) + '\n';
92
+ if (flags.out) writeFileSync(flags.out, text); else process.stdout.write(text);
93
+ console.error(`recast ${out.turns.length} turns as ${out.persona.name}`);
94
+ } else if (cmd === 'bundle') {
95
+ if (!file) throw Object.assign(new Error('bundle needs a scenario file'), { code: 2 });
96
+ const out = bundle(JSON.parse(readFileSync(file, 'utf8')), { dir: dirname(resolve(file)) });
97
+ writeFileSync(flags.out ?? file, JSON.stringify(out, null, 2) + '\n');
98
+ console.error(`bundled images into ${dirname(resolve(file))}/assets/${out.id}`);
99
+ } else if (cmd === 'serve') {
100
+ const files = pos.slice(1);
101
+ if (!files.length) throw Object.assign(new Error('serve needs at least one scenario file'), { code: 2 });
102
+ const scenarios = files.map((f) => {
103
+ const sc = JSON.parse(readFileSync(f, 'utf8'));
104
+ const problems = validate(sc);
105
+ if (problems.length) throw new Error(`${f} is not a valid scenario: ${problems[0]}`);
106
+ return sc;
107
+ });
108
+ const port = Number(flags.port ?? 4460);
109
+ const host = flags.host ?? '127.0.0.1';
110
+ createServer({ scenarios, assetsRoot: dirname(resolve(files[0])) }).listen(port, host, () => console.log(`simulator serving ${scenarios.length} scenario(s) at http://${host}:${port}/`));
111
+ } else {
112
+ throw Object.assign(new Error(`unknown command: ${cmd}`), { code: 2 });
113
+ }
114
+ } catch (e) {
115
+ console.error(`simulator: ${e.message}`);
116
+ process.exit(e.code === 2 ? 2 : 1);
117
+ }
package/package.json CHANGED
@@ -1,6 +1,57 @@
1
1
  {
2
2
  "name": "@supersuit/session-player",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.4.0",
4
+ "description": "Plays recorded agent sessions (case studies) on a simulated phone or MacBook: a scenario format, a converter from Claude Code transcripts, and a React player anyone can step into.",
5
+ "type": "module",
6
+ "bin": {
7
+ "session-player": "bin/simulator.mjs",
8
+ "simulator": "bin/simulator.mjs"
9
+ },
10
+ "exports": {
11
+ ".": "./src/index.mjs",
12
+ "./player": "./src/player.mjs",
13
+ "./embed": "./src/embed.mjs",
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "bin/",
18
+ "src/",
19
+ "README.md",
20
+ "CHANGELOG.md",
21
+ "LICENSE"
22
+ ],
23
+ "scripts": {
24
+ "test": "node --test test/*.test.mjs",
25
+ "smoke": "node scripts/smoke.mjs"
26
+ },
27
+ "engines": {
28
+ "node": ">=20"
29
+ },
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/SupersuitUp/session-player.git"
34
+ },
35
+ "keywords": [
36
+ "supersuit",
37
+ "simulator",
38
+ "claude-code",
39
+ "agents",
40
+ "replay"
41
+ ],
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "devDependencies": {
46
+ "react": "^19.3.0",
47
+ "react-dom": "^19.3.0"
48
+ },
49
+ "peerDependencies": {
50
+ "react": ">=18"
51
+ },
52
+ "peerDependenciesMeta": {
53
+ "react": {
54
+ "optional": true
55
+ }
56
+ }
57
+ }
package/src/bundle.mjs ADDED
@@ -0,0 +1,56 @@
1
+ // Makes a scenario self-contained: every image is copied into `<dir>/assets/<scenario id>/`,
2
+ // shrunk for the phone, and the scenario points at it by a relative path. Pasted images
3
+ // arrive as data URIs and are written out the same way. A source that is missing is refused,
4
+ // so a scenario never ships with a hole where a cover should be.
5
+
6
+ import { mkdirSync, writeFileSync, copyFileSync, existsSync, readFileSync } from 'node:fs';
7
+ import { execFileSync } from 'node:child_process';
8
+ import { join, extname } from 'node:path';
9
+ import { allImages } from './scenario.mjs';
10
+
11
+ const EXT = { 'image/png': '.png', 'image/jpeg': '.jpg', 'image/webp': '.webp', 'image/gif': '.gif' };
12
+
13
+ // Every copied image gets a `.recipe.json` beside it in the derive shape wikis check
14
+ // (mode, derivedFrom, generator, timestamp), with the source's own recipe nested as `source`
15
+ // when the source had one, so the chain back to the model and prompt is never cut.
16
+ function writeRecipe(file, { derivedFrom, generator, sourceFile }) {
17
+ const recipe = { mode: 'derive', asset: file.split('/').pop(), derivedFrom, generator, timestamp: new Date().toISOString() };
18
+ if (sourceFile && existsSync(`${sourceFile}.recipe.json`)) {
19
+ try { recipe.source = JSON.parse(readFileSync(`${sourceFile}.recipe.json`, 'utf8')); } catch { /* an unreadable source recipe is left out, not invented */ }
20
+ }
21
+ writeFileSync(`${file}.recipe.json`, JSON.stringify(recipe, null, 2) + '\n');
22
+ }
23
+
24
+ export function bundle(scenario, { dir, resize = true, max = 900 } = {}) {
25
+ const sc = structuredClone(scenario);
26
+ const rel = join('assets', sc.id);
27
+ mkdirSync(join(dir, rel), { recursive: true });
28
+ const done = new Map();
29
+ for (const img of allImages(sc)) {
30
+ if (img.src.startsWith('assets/')) continue;
31
+ if (done.has(img.src)) { img.src = done.get(img.src); continue; }
32
+ let name;
33
+ if (img.src.startsWith('data:')) {
34
+ const [, type, data] = img.src.match(/^data:([^;]+);base64,(.*)$/s) ?? [];
35
+ if (!data) throw new Error(`image ${img.id} has an unreadable data URI`);
36
+ name = `${img.id}${EXT[type] ?? '.png'}`;
37
+ writeFileSync(join(dir, rel, name), Buffer.from(data, 'base64'));
38
+ writeRecipe(join(dir, rel, name), { derivedFrom: `an image pasted into the session by the operator (${type})`, generator: '@supersuit/session-player convert + bundle (decoded from the transcript)' });
39
+ } else {
40
+ if (!existsSync(img.src)) throw new Error(`image ${img.id}: ${img.src} does not exist`);
41
+ if (resize && process.platform === 'darwin') {
42
+ name = `${img.id}.jpg`;
43
+ execFileSync('sips', ['-Z', String(max), '-s', 'format', 'jpeg', '-s', 'formatOptions', '80', img.src, '--out', join(dir, rel, name)], { stdio: 'ignore' });
44
+ writeRecipe(join(dir, rel, name), { derivedFrom: img.src, generator: `@supersuit/session-player bundle (sips -Z ${max}, jpeg quality 80)`, sourceFile: img.src });
45
+ } else {
46
+ name = `${img.id}${extname(img.src) || '.png'}`;
47
+ copyFileSync(img.src, join(dir, rel, name));
48
+ writeRecipe(join(dir, rel, name), { derivedFrom: img.src, generator: '@supersuit/session-player bundle (copied unchanged)', sourceFile: img.src });
49
+ }
50
+ }
51
+ const out = `${rel}/${name}`.split('\\').join('/');
52
+ done.set(img.src, out);
53
+ img.src = out;
54
+ }
55
+ return sc;
56
+ }