tutticode 0.1.0 → 0.1.1

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/README.md CHANGED
@@ -52,7 +52,7 @@ anywhere else (for example a project's `.claude/skills`).
52
52
  tutticode check song.tc # diagnostics, no audio
53
53
  tutticode render song.tc -o bed.wav --seconds 12 --fade-out 1.5 --lufs -16 --json
54
54
  tutticode render song.tc -o loop.flac --loops 4 --stems stems/
55
- tutticode open song.tc # open it in TuttiCode for a human
55
+ tutticode open song.tc # open it in TuttiCode for a human (--headless: prints the link)
56
56
  cat song.tc | tutticode check - # read the program from stdin
57
57
  ```
58
58
 
@@ -60,8 +60,12 @@ cat song.tc | tutticode check - # read the program from stdin
60
60
  `--format wav|flac|mp3` (default from the `-o` extension),
61
61
  `--sample-rate 44100|48000`, `--bit-depth 16|24`, `--stems <dir>`,
62
62
  `--fade-in s`, `--fade-out s`, `--tail s`, `--lufs N`, `--true-peak N`,
63
- `--json`, `--timeout s`. `--lufs` applies one static gain; when the true-peak
64
- ceiling cannot be met the render fails with `clip` instead of being limited.
63
+ `--limit`, `--json`, `--timeout s`. No file is written clipped: a render
64
+ whose true peak would pass the ceiling (`--true-peak`, default -1 dBTP) is
65
+ lowered by one static gain, and the CLI says so ("Lowered 4.6 dB to avoid
66
+ clipping"). `--lufs` applies one static gain; when the true-peak ceiling
67
+ cannot be met the render fails with `clip`, unless you add `--limit`: a
68
+ true-peak limiter on the mix (not the stems), reported as `limitDb`.
65
69
 
66
70
  `--json` prints the render report: length, bars, tempo, loudness and true
67
71
  peak, the gain applied, per-voice levels, the bar and beat grid (seconds, for
package/README.zh-CN.md CHANGED
@@ -45,16 +45,19 @@ Claude Code、Copilot CLI 和 Codex 分别从 `~/.claude/skills`、`~/.copilot/s
45
45
  tutticode check song.tc # 只检查,不出声音
46
46
  tutticode render song.tc -o bed.wav --seconds 12 --fade-out 1.5 --lufs -16 --json
47
47
  tutticode render song.tc -o loop.flac --loops 4 --stems stems/
48
- tutticode open song.tc # 在 TuttiCode 里打开给人看
48
+ tutticode open song.tc # 在 TuttiCode 里打开给人看(--headless:只打印链接)
49
49
  cat song.tc | tutticode check - # 从 stdin 读程序
50
50
  ```
51
51
 
52
52
  `render` 参数: `--seconds|--bars|--loops N`、`--bpm N`、
53
53
  `--format wav|flac|mp3`(默认按 `-o` 的扩展名)、`--sample-rate 44100|48000`、
54
54
  `--bit-depth 16|24`、`--stems <目录>`、`--fade-in 秒`、`--fade-out 秒`、
55
- `--tail 秒`、`--lufs N`、`--true-peak N`、`--json`、`--timeout 秒`。
55
+ `--tail 秒`、`--lufs N`、`--true-peak N`、`--limit`、`--json`、`--timeout 秒`。
56
+ 不会写出削波的文件:true peak 超过上限(`--true-peak`,默认 -1 dBTP)的渲染会
57
+ 整体降低一次静态增益,CLI 会说明("Lowered 4.6 dB to avoid clipping")。
56
58
  `--lufs` 只做一次静态增益;如果达不到 true peak 上限,会以 `clip` 失败,而不是
57
- 偷偷加限制器。
59
+ 偷偷加限制器;需要时加 `--limit`:对混音(不含分轨)加 true peak 限制器,
60
+ 报告里的 `limitDb` 是它最多压下的量。
58
61
 
59
62
  `--json` 输出渲染报告:时长、小节数、速度、响度和 true peak、施加的增益、每个
60
63
  声部的电平、小节和拍的时间点(秒,用来对齐剪辑点)、检查报告、分析结果、
@@ -125,6 +125,7 @@ Read only what the task needs:
125
125
  - `references/mini-notation.md`: Mini-notation. The rhythm language inside the quotes - sequences, rests, groups, alternation, polymeter, Euclidean rhythms and chance.
126
126
  - `references/time-and-structure.md`: Time and structure. Functions that speed up, shift, reverse, repeat and combine patterns, and arrange them into sections.
127
127
  - `references/harmony-and-scales.md`: Harmony and scales. Note names and octaves, scale degrees, transposition, chord symbols, voicings and arpeggios.
128
+ - `references/melodies-with-do-re-mi.md`: Melodies with do re mi. Write notes in solfège (do re mi fa sol la si) wherever a note name goes; strudel.cc gets letters.
128
129
  - `references/sounds.md`: Sounds. Drum kits and banks, synth waveforms, General MIDI instrument names, Ensemble sounds and your own samples.
129
130
  - `references/effect-controls.md`: Effect controls. Filters, reverb, delay, distortion, panning, envelopes and synth controls - and moving them with signals.
130
131
  - `references/sample-operations.md`: Sample operations. Play part of a sample, change its speed, cut it into slices, fit it to the tempo and choke it.
@@ -136,5 +137,5 @@ Read only what the task needs:
136
137
  - `references/functions.md`: every function, method and signal, with signature, description and example (generated from the engine).
137
138
  - `references/devices.md`: every instrument and effect for `.inst/.fx/.midifx`, with `references/devices/<id>.md` per device: param keys, ranges, units, presets (generated from the device catalogue).
138
139
 
139
- Language: PATTERN LANGUAGE 1.2.0, engine 15.
140
+ Language: PATTERN LANGUAGE 1.2.0, engine 16.
140
141
  Human guide: https://tuttidaw.com/code/learn; agents: https://tuttidaw.com/code/learn/for-agents
@@ -40,8 +40,9 @@ differences are marked *local* or *web*.
40
40
 
41
41
  *local* returns `files` (`audio`, `stems`: paths written on disk),
42
42
  `editUrl`, `window` (`bars`, `seconds`, `bpm`), `loudness` (`lufs`,
43
- `truePeak` of the delivered file, `gainDb` applied for a `lufs` target),
44
- `voices[]` (each voice's solo `lufs` and `truePeak`), `barTimes` /
43
+ `truePeak` of the delivered file, `gainDb` applied for a `lufs` target or
44
+ to avoid clipping, `limitDb` with `limit`), `warnings` (what was done to
45
+ the audio), `voices[]` (each voice's solo `lufs` and `truePeak`), `barTimes` /
45
46
  `beatTimes` (seconds of each bar and beat: sync video cuts to them),
46
47
  `check` and `analysis`. *web* returns `analysis`, a `listenUrl` (24 h) and
47
48
  signed audio links.
@@ -71,11 +72,19 @@ or mute (`_`) the voices you are not listening to.
71
72
  (whole loops ring out past their end). `tail` adds seconds after the
72
73
  length for reverbs and delays and makes the file exactly length + tail;
73
74
  `fadeIn` / `fadeOut` are linear, in seconds.
75
+ - No file is written clipped: without `lufs`, a render whose true peak
76
+ would pass `truePeak` (default -1 dBTP) is lowered by one static gain
77
+ (negative `gainDb`, a line in `warnings`); a stem that would clip on
78
+ its own is lowered further (its `gainDb` in `voices[]`). *web* lowers
79
+ the audio file the same way and says so in `warnings`; its `analysis`
80
+ measures the program before that gain.
74
81
  - `lufs` applies one static gain to that integrated loudness, the same on
75
- the stems. When the true peak would pass `truePeak` (default -1 dBTP)
76
- the render fails with code `clip` instead of limiting: ask for a lower
77
- `lufs`, or make the program less peaky (lower its loudest voice). Under
78
- 0.4 s there is no integrated loudness and `lufs` is ignored.
82
+ the stems. When the true peak would pass `truePeak` the render fails
83
+ with code `clip`: ask for a lower `lufs`, make the program less peaky
84
+ (lower its loudest voice), or add `limit: true` (`--limit`), a
85
+ true-peak limiter on the mix (not the stems) that reports the most it
86
+ took off in `limitDb`. Under 0.4 s there is no integrated loudness and
87
+ `lufs` is ignored.
79
88
  - Exit codes (CLI): 0 ok, 1 errors in the code or `clip`, 2 bridge or
80
89
  browser problem (run `tutticode doctor`), 3 usage.
81
90
 
@@ -99,7 +99,7 @@ n("0 2 4").scale("C:minor")
99
99
 
100
100
  ### `note(notes)`
101
101
 
102
- Plays pitches: note names (c4, eb3) or MIDI numbers (60).
102
+ Plays pitches: note names (c4, eb3), solfège (do re mi, sol4, sib) or MIDI numbers (60).
103
103
 
104
104
  ```js
105
105
  note("c3 e3 g3 b3")
@@ -0,0 +1,59 @@
1
+ # Melodies with do re mi
2
+
3
+ Write notes in solfège (do re mi fa sol la si) wherever a note name goes; strudel.cc gets letters.
4
+
5
+ ## Solfège is a note name
6
+
7
+ Wherever a note name goes, you can write its solfège name instead:
8
+ `do re mi fa sol la si`. TuttiCode uses fixed do: `do` is always C, `re` is D,
9
+ `sol` is G, whatever the key. `so` and `ti` work too (the same as `sol` and
10
+ `si`).
11
+
12
+ ```js
13
+ note("do re mi fa sol la si do4")
14
+ ```
15
+
16
+ Only the name differs: `note("do re mi")` plays exactly what `note("c d e")`
17
+ plays.
18
+
19
+ ## Sharps, flats and octaves
20
+
21
+ Add sharps, flats and octaves as you would to a letter: `do#4`, `mib` (E flat),
22
+ `sib3` (B flat), `fa#`. A name without an octave is in octave 3, like `c`.
23
+ Capitals work too: `Do`, `SOL`.
24
+
25
+ ```js
26
+ note("<do4 mib4 sol4 sib4> [fa#3 la3]").s("piano")
27
+ ```
28
+
29
+ ## Scales and chords
30
+
31
+ Scale roots take solfège: `.scale("re:minor")`. Stack names with commas to
32
+ play a chord.
33
+
34
+ ```js
35
+ n("0 2 4 <7 6>").scale("<re:dorian sol:mixolydian>")
36
+ ```
37
+
38
+ ```js
39
+ note("<[do3,mi3,sol3] [la2,do3,mi3] [fa2,la2,do3] [sol2,si2,re3]>").s("piano")
40
+ ```
41
+
42
+ Chord symbols (`chord("C^7")`, `.voicings()`) keep their letters: there `do`
43
+ already means D diminished (`D` and `o`).
44
+
45
+ ## Where a word is not a note
46
+
47
+ In `s()` and `sound()` words are sound names, so `s("do")` looks for a sound
48
+ called "do". `n()` is also a sample or soundfont index, so `n("do")` is a
49
+ word too: write `note("do")`. Solfège means a pitch only in `note()` /
50
+ `.note()`, `.scale()` roots, `.mode()` and `.anchor()`, and `.add()` /
51
+ `.sub()` / `.transpose()` on notes.
52
+
53
+ ## Opening in strudel.cc
54
+
55
+ strudel.cc reads letter names only. "Open in strudel.cc" rewrites solfège to
56
+ letters for you (`"do re mi"` becomes `"c d e"`), so the program plays the same
57
+ there.
58
+
59
+ Source: https://tuttidaw.com/code/learn/melodies-with-do-re-mi
@@ -52,19 +52,26 @@ Recipe index:
52
52
  | Broadcast (EBU R128) | -23 |
53
53
  | Short sound effects | -18..-26: peaky, see each recipe |
54
54
 
55
- - True peak ceiling -1 dBTP (the default). `lufs` is one static gain, never
56
- a limiter: a peaky program fails with `clip`. Then lower the target, or
57
- lower the loudest voice's `.gain` and render again. Where loudness
58
- matters (music beds at -14..-17), end the program with
59
- `master.fx("Limiter", {gainDb: 3, ceiling: -3})`: the program's own bus
60
- catches the peaks before the gain, as the music recipes do.
55
+ - True peak ceiling -1 dBTP (the default, `truePeak`). No file is written
56
+ clipped: without `lufs`, a render that would pass the ceiling is lowered
57
+ by one static gain (a negative `gainDb` and a line in `warnings`). `lufs`
58
+ is one static gain: a peaky program (most drum programs at -14..-16)
59
+ fails with `clip`. Then lower the target, lower the loudest voice's
60
+ `.gain` and render again, end the program with
61
+ `master.fx("Limiter", {gainDb: 3, ceiling: -3})` (the program's own bus
62
+ catches the peaks before the gain, as the music recipes do), or add
63
+ `limit: true` (`--limit`): the render's true-peak limiter holds the
64
+ ceiling and reports the most it took off (`limitDb`). More than about
65
+ 6 dB of it flattens the drums: then lower the target instead.
61
66
  - Exact durations: `seconds` cuts exactly. A one-shot effect is one bar
62
67
  long: `setcpm` sets the bar (`setcpm(60)` = 1 s, `setcpm(15)` = 4 s).
63
68
  - Video beds: `fadeIn` 1-3 s, `fadeOut` 2-5 s, `tail` for the reverb.
64
69
  - Loops for apps and games: `loops` N with `tail: 0`, no fades, so the file
65
70
  is exactly N loops and repeats in time; keep the last step short.
66
- - Stems: `stems: true` writes one file per voice (named by the labels); a
67
- `lufs` gain applies to the stems too, so they still sum to the mix.
71
+ - Stems: `stems: true` writes one file per voice (named by the labels); the
72
+ mix's static gain applies to the stems too, so they still sum to the mix.
73
+ A stem that would clip on its own is lowered further (its `gainDb` in
74
+ `voices`, a line in `warnings`); the limiter never touches stems.
68
75
 
69
76
  ## Quality bar: never deliver
70
77
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- /* tutticode v0.1.0 | https://tuttidaw.com */
2
+ /* tutticode v0.1.1 | https://tuttidaw.com */
3
3
  "use strict";
4
4
  var __create = Object.create;
5
5
  var __defProp = Object.defineProperty;
@@ -3932,7 +3932,7 @@ function validRender(m) {
3932
3932
  return "lufs must be -40..-5";
3933
3933
  if (!optional(m.truePeak, (v) => finite(v) && v >= -12 && v <= 0))
3934
3934
  return "truePeak must be -12..0";
3935
- for (const key of ["stems", "analyze"])
3935
+ for (const key of ["stems", "analyze", "limit"])
3936
3936
  if (!optional(m[key], (v) => typeof v === "boolean"))
3937
3937
  return `${key} must be a boolean`;
3938
3938
  return void 0;
@@ -3988,6 +3988,8 @@ function bridgePageUrl(origin, locale, port, token) {
3988
3988
  }
3989
3989
  var MAX_LABEL_CHARS = 64;
3990
3990
  var MAX_VERSION_CHARS = 128;
3991
+ var MAX_WARNINGS = 32;
3992
+ var MAX_WARNING_CHARS = 1e3;
3991
3993
  var MAX_EDIT_URL_CHARS = 1e6;
3992
3994
  var MAX_GRID_ENTRIES = 2e4;
3993
3995
  var MAX_RENDER_SECONDS = BRIDGE_MAX_SECONDS + 60;
@@ -4023,6 +4025,13 @@ function validateRenderMeta(raw, expect) {
4023
4025
  return no("meta.lufs / meta.truePeak out of range");
4024
4026
  if (!inRange(raw.gainDb, -100, 100))
4025
4027
  return no("meta.gainDb out of range");
4028
+ if (!optional(raw.limitDb, (v) => inRange(v, 0, 100)))
4029
+ return no("meta.limitDb out of range");
4030
+ if (!optional(
4031
+ raw.warnings,
4032
+ (v) => Array.isArray(v) && v.length <= MAX_WARNINGS && v.every((w) => typeof w === "string" && w.length <= MAX_WARNING_CHARS)
4033
+ ))
4034
+ return no("meta.warnings must be at most 32 short strings");
4026
4035
  const barTimes = grid(raw.barTimes);
4027
4036
  const beatTimes = grid(raw.beatTimes);
4028
4037
  if (!barTimes || !beatTimes)
@@ -4046,7 +4055,14 @@ function validateRenderMeta(raw, expect) {
4046
4055
  const voicePeak = level(v.truePeak, -200, 40);
4047
4056
  if (voiceLufs === void 0 || voicePeak === void 0)
4048
4057
  return no("meta.voices levels out of range");
4049
- voices.push({ label: v.label, lufs: voiceLufs, truePeak: voicePeak });
4058
+ if (!optional(v.gainDb, (g) => inRange(g, -100, 100)))
4059
+ return no("meta.voices gainDb out of range");
4060
+ voices.push({
4061
+ label: v.label,
4062
+ lufs: voiceLufs,
4063
+ truePeak: voicePeak,
4064
+ ...v.gainDb !== void 0 ? { gainDb: v.gainDb } : {}
4065
+ });
4050
4066
  }
4051
4067
  if (!isObject(raw.check))
4052
4068
  return no("meta.check must be a report");
@@ -4067,6 +4083,8 @@ function validateRenderMeta(raw, expect) {
4067
4083
  lufs,
4068
4084
  truePeak,
4069
4085
  gainDb: raw.gainDb,
4086
+ ...raw.limitDb !== void 0 ? { limitDb: raw.limitDb } : {},
4087
+ ...raw.warnings !== void 0 ? { warnings: [...raw.warnings] } : {},
4070
4088
  barTimes,
4071
4089
  beatTimes,
4072
4090
  stems,
@@ -4207,12 +4225,16 @@ function formatCheckSummary(report) {
4207
4225
  )} s)`;
4208
4226
  }
4209
4227
  function formatRenderSummary(meta, files) {
4228
+ const gain = meta.gainDb ? `, gain ${meta.gainDb > 0 ? "+" : ""}${fixed(meta.gainDb)} dB` : "";
4229
+ const limited = meta.limitDb ? `, limited up to ${fixed(meta.limitDb)} dB` : "";
4210
4230
  const lines = [
4211
4231
  `Wrote ${files.mix} (${fixed(meta.seconds)} s, ${meta.bars} bars at ${fixed(
4212
4232
  meta.bpm,
4213
4233
  0
4214
- )} BPM, ${fixed(meta.lufs)} LUFS, ${fixed(meta.truePeak)} dBTP${meta.gainDb ? `, gain ${meta.gainDb > 0 ? "+" : ""}${fixed(meta.gainDb)} dB` : ""})`
4234
+ )} BPM, ${fixed(meta.lufs)} LUFS, ${fixed(meta.truePeak)} dBTP${gain}${limited})`
4215
4235
  ];
4236
+ for (const warning of meta.warnings ?? [])
4237
+ lines.push(`Note: ${warning}`);
4216
4238
  const stems = Object.values(files.stems);
4217
4239
  if (stems.length)
4218
4240
  lines.push(`Stems: ${stems.length} files in ${(0, import_node_path.dirname)(stems[0])}`);
@@ -4239,7 +4261,7 @@ var wrapper_default = import_websocket.default;
4239
4261
  var import_node_fs2 = require("node:fs");
4240
4262
  var import_node_os = require("node:os");
4241
4263
  var import_node_path2 = require("node:path");
4242
- var VERSION = true ? "0.1.0" : readPackageVersion();
4264
+ var VERSION = true ? "0.1.1" : readPackageVersion();
4243
4265
  function envBrowserArgs() {
4244
4266
  const args = process.env.TUTTICODE_BROWSER_ARGS?.split(" ").filter(Boolean);
4245
4267
  return args?.length ? args : void 0;
@@ -4653,6 +4675,7 @@ function parseRender(rest) {
4653
4675
  tail: { type: "string" },
4654
4676
  lufs: { type: "string" },
4655
4677
  "true-peak": { type: "string" },
4678
+ limit: { type: "boolean" },
4656
4679
  "no-analyze": { type: "boolean" },
4657
4680
  json: { type: "boolean" }
4658
4681
  });
@@ -4707,6 +4730,7 @@ function parseRender(rest) {
4707
4730
  ...optional2("tail", number(values, "tail", 0, 60)),
4708
4731
  ...optional2("lufs", number(values, "lufs", -40, -5)),
4709
4732
  ...optional2("truePeak", number(values, "true-peak", -12, 0)),
4733
+ ...values.limit ? { limit: true } : {},
4710
4734
  ...values["no-analyze"] ? { analyze: false } : {}
4711
4735
  };
4712
4736
  return {
@@ -4751,13 +4775,19 @@ program has errors.`,
4751
4775
  --stems <dir> also write one file per voice into <dir>
4752
4776
  --fade-in S --fade-out S --tail S seconds
4753
4777
  --lufs N --true-peak N static gain to a loudness; fails (clip)
4754
- rather than limiting
4778
+ when the true peak would pass the
4779
+ ceiling (default -1 dBTP). Without
4780
+ --lufs a render over the ceiling is
4781
+ lowered (one static gain, reported)
4782
+ --limit true-peak limiter instead of failing
4783
+ (clip) or lowering; reported
4755
4784
  --no-analyze skip the per-voice analysis
4756
4785
  --json print the RenderMeta + written files
4757
4786
  --timeout S give up after S seconds (default 600)`,
4758
4787
  open: `tutticode open <file|-> [bridge flags]
4759
4788
 
4760
- Opens the program in a visible TuttiCode tab (tuttidaw.com/code).`,
4789
+ Opens the program in a visible TuttiCode tab (tuttidaw.com/code). With
4790
+ --headless it opens nothing and prints the link.`,
4761
4791
  mcp: `tutticode mcp [bridge flags]
4762
4792
 
4763
4793
  MCP stdio server: code_check, code_render, code_compare, code_open,
@@ -7015,13 +7045,17 @@ var renderProperties2 = {
7015
7045
  type: "number",
7016
7046
  minimum: -40,
7017
7047
  maximum: -5,
7018
- description: 'Static gain to this integrated loudness (e.g. -14 for streaming, -16 for podcasts / video beds). Fails with code "clip" rather than limiting when truePeak cannot be met.'
7048
+ description: 'Static gain to this integrated loudness (e.g. -14 for streaming, -16 for podcasts / video beds). Fails with code "clip" when truePeak cannot be met, unless limit is true.'
7019
7049
  },
7020
7050
  truePeak: {
7021
7051
  type: "number",
7022
7052
  minimum: -12,
7023
7053
  maximum: 0,
7024
- description: "dBTP ceiling with lufs (default -1)."
7054
+ description: "dBTP ceiling (default -1). Without lufs, a render that would pass it is lowered by one static gain (loudness.gainDb, warnings): no file is written clipped."
7055
+ },
7056
+ limit: {
7057
+ type: "boolean",
7058
+ description: 'Opt-in true-peak limiter: peaks over truePeak are limited instead of failing with "clip" (with lufs) or lowering the whole render (without). Use it to reach -14 / -16 LUFS on drum-heavy programs; loudness.limitDb reports the most it took off. Stems are not limited.'
7025
7059
  },
7026
7060
  out: {
7027
7061
  type: "string",
@@ -7195,7 +7229,8 @@ function createToolHandler(deps) {
7195
7229
  "fadeOut",
7196
7230
  "tail",
7197
7231
  "lufs",
7198
- "truePeak"
7232
+ "truePeak",
7233
+ "limit"
7199
7234
  ])
7200
7235
  if (args[key] !== void 0)
7201
7236
  command[key] = args[key];
@@ -7241,8 +7276,10 @@ function createToolHandler(deps) {
7241
7276
  loudness: {
7242
7277
  lufs: meta.lufs,
7243
7278
  truePeak: meta.truePeak,
7244
- gainDb: meta.gainDb
7279
+ gainDb: meta.gainDb,
7280
+ ...meta.limitDb ? { limitDb: meta.limitDb } : {}
7245
7281
  },
7282
+ ...meta.warnings?.length ? { warnings: meta.warnings } : {},
7246
7283
  voices: meta.voices,
7247
7284
  barTimes: meta.barTimes,
7248
7285
  beatTimes: meta.beatTimes,
@@ -18466,6 +18503,20 @@ async function render(command, io) {
18466
18503
  }
18467
18504
  async function open(command, io) {
18468
18505
  const code = await readProgram(command.file, io);
18506
+ const flags = command.bridge;
18507
+ const url = codeLink(
18508
+ normalizeOrigin(flags.origin ?? "") ?? BRIDGE_ORIGINS[0],
18509
+ flags.locale ?? "en",
18510
+ code
18511
+ );
18512
+ if (flags.show === false) {
18513
+ io.stdout.write(
18514
+ `No browser window was opened (--headless). Open the program in TuttiCode:
18515
+ ${url}
18516
+ `
18517
+ );
18518
+ return EXIT_OK;
18519
+ }
18469
18520
  const outcome = await runJob(
18470
18521
  { type: "open", code },
18471
18522
  command.bridge,
@@ -18480,7 +18531,8 @@ async function open(command, io) {
18480
18531
  command.file,
18481
18532
  io
18482
18533
  );
18483
- io.stdout.write("Opened in TuttiCode.\n");
18534
+ io.stdout.write(`Opened in TuttiCode: ${url}
18535
+ `);
18484
18536
  return EXIT_OK;
18485
18537
  }
18486
18538
  async function mcp(flags, io) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tutticode",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "private": false,
5
5
  "description": "Turn TuttiCode (tuttidaw.com/code pattern code) into audio files from any agent: a CLI and an MCP server that render in your own browser with the TuttiDAW engine.",
6
6
  "license": "MIT",