tutticode 0.1.0 → 0.2.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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- /* tutticode v0.1.0 | https://tuttidaw.com */
2
+ /* tutticode v0.2.0 | https://tuttidaw.com */
3
3
  "use strict";
4
4
  var __create = Object.create;
5
5
  var __defProp = Object.defineProperty;
@@ -2310,7 +2310,7 @@ var require_websocket = __commonJS({
2310
2310
  var http = require("http");
2311
2311
  var net = require("net");
2312
2312
  var tls = require("tls");
2313
- var { randomBytes: randomBytes2, createHash: createHash2 } = require("crypto");
2313
+ var { randomBytes: randomBytes4, createHash: createHash3 } = require("crypto");
2314
2314
  var { Duplex, Readable } = require("stream");
2315
2315
  var { URL: URL2 } = require("url");
2316
2316
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -2865,8 +2865,8 @@ var require_websocket = __commonJS({
2865
2865
  }
2866
2866
  }
2867
2867
  const defaultPort = isSecure ? 443 : 80;
2868
- const key = randomBytes2(16).toString("base64");
2869
- const request = isSecure ? https.request : http.request;
2868
+ const key = randomBytes4(16).toString("base64");
2869
+ const request2 = isSecure ? https.request : http.request;
2870
2870
  const protocolSet = /* @__PURE__ */ new Set();
2871
2871
  let perMessageDeflate;
2872
2872
  opts.createConnection = opts.createConnection || (isSecure ? tlsConnect : netConnect);
@@ -2944,12 +2944,12 @@ var require_websocket = __commonJS({
2944
2944
  if (opts.auth && !options.headers.authorization) {
2945
2945
  options.headers.authorization = "Basic " + Buffer.from(opts.auth).toString("base64");
2946
2946
  }
2947
- req = websocket._req = request(opts);
2947
+ req = websocket._req = request2(opts);
2948
2948
  if (websocket._redirects) {
2949
2949
  websocket.emit("redirect", websocket.url, req);
2950
2950
  }
2951
2951
  } else {
2952
- req = websocket._req = request(opts);
2952
+ req = websocket._req = request2(opts);
2953
2953
  }
2954
2954
  if (opts.timeout) {
2955
2955
  req.on("timeout", () => {
@@ -2998,7 +2998,7 @@ var require_websocket = __commonJS({
2998
2998
  abortHandshake(websocket, socket, "Invalid Upgrade header");
2999
2999
  return;
3000
3000
  }
3001
- const digest = createHash2("sha1").update(key + GUID).digest("base64");
3001
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
3002
3002
  if (res.headers["sec-websocket-accept"] !== digest) {
3003
3003
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
3004
3004
  return;
@@ -3387,7 +3387,7 @@ var require_websocket_server = __commonJS({
3387
3387
  var EventEmitter4 = require("events");
3388
3388
  var http = require("http");
3389
3389
  var { Duplex } = require("stream");
3390
- var { createHash: createHash2 } = require("crypto");
3390
+ var { createHash: createHash3 } = require("crypto");
3391
3391
  var extension2 = require_extension();
3392
3392
  var PerMessageDeflate2 = require_permessage_deflate();
3393
3393
  var subprotocol2 = require_subprotocol();
@@ -3702,7 +3702,7 @@ var require_websocket_server = __commonJS({
3702
3702
  }
3703
3703
  if (this._state > RUNNING)
3704
3704
  return abortHandshake(socket, 503);
3705
- const digest = createHash2("sha1").update(key + GUID).digest("base64");
3705
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
3706
3706
  const headers = [
3707
3707
  "HTTP/1.1 101 Switching Protocols",
3708
3708
  "Upgrade: websocket",
@@ -3791,12 +3791,703 @@ var require_websocket_server = __commonJS({
3791
3791
  });
3792
3792
 
3793
3793
  // src/bin.ts
3794
- var import_node_path9 = require("node:path");
3794
+ var import_node_path12 = require("node:path");
3795
3795
 
3796
3796
  // src/cli.ts
3797
- var import_node_fs8 = require("node:fs");
3797
+ var import_node_fs11 = require("node:fs");
3798
3798
  var import_node_readline2 = require("node:readline");
3799
3799
 
3800
+ // ../../node_modules/.pnpm/uqr@0.1.3/node_modules/uqr/dist/index.mjs
3801
+ var QrCodeDataType = /* @__PURE__ */ ((QrCodeDataType2) => {
3802
+ QrCodeDataType2[QrCodeDataType2["Border"] = -1] = "Border";
3803
+ QrCodeDataType2[QrCodeDataType2["Data"] = 0] = "Data";
3804
+ QrCodeDataType2[QrCodeDataType2["Function"] = 1] = "Function";
3805
+ QrCodeDataType2[QrCodeDataType2["Position"] = 2] = "Position";
3806
+ QrCodeDataType2[QrCodeDataType2["Timing"] = 3] = "Timing";
3807
+ QrCodeDataType2[QrCodeDataType2["Alignment"] = 4] = "Alignment";
3808
+ return QrCodeDataType2;
3809
+ })(QrCodeDataType || {});
3810
+ var LOW = [0, 1];
3811
+ var MEDIUM = [1, 0];
3812
+ var QUARTILE = [2, 3];
3813
+ var HIGH = [3, 2];
3814
+ var EccMap = {
3815
+ L: LOW,
3816
+ M: MEDIUM,
3817
+ Q: QUARTILE,
3818
+ H: HIGH
3819
+ };
3820
+ var NUMERIC_REGEX = /^\d*$/;
3821
+ var ALPHANUMERIC_REGEX = /^[A-Z0-9 $%*+./:-]*$/;
3822
+ var ALPHANUMERIC_CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
3823
+ var MIN_VERSION = 1;
3824
+ var MAX_VERSION = 40;
3825
+ var PENALTY_N1 = 3;
3826
+ var PENALTY_N2 = 3;
3827
+ var PENALTY_N3 = 40;
3828
+ var PENALTY_N4 = 10;
3829
+ var ECC_CODEWORDS_PER_BLOCK = [
3830
+ // Version: (note that index 0 is for padding, and is set to an illegal value)
3831
+ // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level
3832
+ [-1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
3833
+ // Low
3834
+ [-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28],
3835
+ // Medium
3836
+ [-1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
3837
+ // Quartile
3838
+ [-1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]
3839
+ // High
3840
+ ];
3841
+ var NUM_ERROR_CORRECTION_BLOCKS = [
3842
+ // Version: (note that index 0 is for padding, and is set to an illegal value)
3843
+ // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level
3844
+ [-1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25],
3845
+ // Low
3846
+ [-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49],
3847
+ // Medium
3848
+ [-1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68],
3849
+ // Quartile
3850
+ [-1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81]
3851
+ // High
3852
+ ];
3853
+ var QrCode = class {
3854
+ /* -- Constructor (low level) and fields -- */
3855
+ // Creates a new QR Code with the given version number,
3856
+ // error correction level, data codeword bytes, and mask number.
3857
+ // This is a low-level API that most users should not use directly.
3858
+ // A mid-level API is the encodeSegments() function.
3859
+ constructor(version, ecc, dataCodewords, msk) {
3860
+ this.version = version;
3861
+ this.ecc = ecc;
3862
+ if (version < MIN_VERSION || version > MAX_VERSION)
3863
+ throw new RangeError("Version value out of range");
3864
+ if (msk < -1 || msk > 7)
3865
+ throw new RangeError("Mask value out of range");
3866
+ this.size = version * 4 + 17;
3867
+ const row = Array.from({ length: this.size }).fill(false);
3868
+ for (let i = 0; i < this.size; i++) {
3869
+ this.modules.push(row.slice());
3870
+ this.types.push(row.map(() => 0));
3871
+ }
3872
+ this.drawFunctionPatterns();
3873
+ const allCodewords = this.addEccAndInterleave(dataCodewords);
3874
+ this.drawCodewords(allCodewords);
3875
+ if (msk === -1) {
3876
+ let minPenalty = 1e9;
3877
+ for (let i = 0; i < 8; i++) {
3878
+ this.applyMask(i);
3879
+ this.drawFormatBits(i);
3880
+ const penalty = this.getPenaltyScore();
3881
+ if (penalty < minPenalty) {
3882
+ msk = i;
3883
+ minPenalty = penalty;
3884
+ }
3885
+ this.applyMask(i);
3886
+ }
3887
+ }
3888
+ this.mask = msk;
3889
+ this.applyMask(msk);
3890
+ this.drawFormatBits(msk);
3891
+ }
3892
+ /* -- Fields -- */
3893
+ // The width and height of this QR Code, measured in modules, between
3894
+ // 21 and 177 (inclusive). This is equal to version * 4 + 17.
3895
+ size;
3896
+ // The index of the mask pattern used in this QR Code, which is between 0 and 7 (inclusive).
3897
+ // Even if a QR Code is created with automatic masking requested (mask = -1),
3898
+ // the resulting object still has a mask value between 0 and 7.
3899
+ mask;
3900
+ // The modules of this QR Code (false = light, true = dark).
3901
+ // Immutable after constructor finishes. Accessed through getModule().
3902
+ modules = [];
3903
+ types = [];
3904
+ /* -- Accessor methods -- */
3905
+ // Returns the color of the module (pixel) at the given coordinates, which is false
3906
+ // for light or true for dark. The top left corner has the coordinates (x=0, y=0).
3907
+ // If the given coordinates are out of bounds, then false (light) is returned.
3908
+ getModule(x, y) {
3909
+ return x >= 0 && x < this.size && y >= 0 && y < this.size && this.modules[y][x];
3910
+ }
3911
+ /* -- Private helper methods for constructor: Drawing function modules -- */
3912
+ // Reads this object's version field, and draws and marks all function modules.
3913
+ drawFunctionPatterns() {
3914
+ for (let i = 0; i < this.size; i++) {
3915
+ this.setFunctionModule(6, i, i % 2 === 0, QrCodeDataType.Timing);
3916
+ this.setFunctionModule(i, 6, i % 2 === 0, QrCodeDataType.Timing);
3917
+ }
3918
+ this.drawFinderPattern(3, 3);
3919
+ this.drawFinderPattern(this.size - 4, 3);
3920
+ this.drawFinderPattern(3, this.size - 4);
3921
+ const alignPatPos = this.getAlignmentPatternPositions();
3922
+ const numAlign = alignPatPos.length;
3923
+ for (let i = 0; i < numAlign; i++) {
3924
+ for (let j = 0; j < numAlign; j++) {
3925
+ if (!(i === 0 && j === 0 || i === 0 && j === numAlign - 1 || i === numAlign - 1 && j === 0))
3926
+ this.drawAlignmentPattern(alignPatPos[i], alignPatPos[j]);
3927
+ }
3928
+ }
3929
+ this.drawFormatBits(0);
3930
+ this.drawVersion();
3931
+ }
3932
+ // Draws two copies of the format bits (with its own error correction code)
3933
+ // based on the given mask and this object's error correction level field.
3934
+ drawFormatBits(mask) {
3935
+ const data = this.ecc[1] << 3 | mask;
3936
+ let rem = data;
3937
+ for (let i = 0; i < 10; i++)
3938
+ rem = rem << 1 ^ (rem >>> 9) * 1335;
3939
+ const bits = (data << 10 | rem) ^ 21522;
3940
+ for (let i = 0; i <= 5; i++)
3941
+ this.setFunctionModule(8, i, getBit(bits, i));
3942
+ this.setFunctionModule(8, 7, getBit(bits, 6));
3943
+ this.setFunctionModule(8, 8, getBit(bits, 7));
3944
+ this.setFunctionModule(7, 8, getBit(bits, 8));
3945
+ for (let i = 9; i < 15; i++)
3946
+ this.setFunctionModule(14 - i, 8, getBit(bits, i));
3947
+ for (let i = 0; i < 8; i++)
3948
+ this.setFunctionModule(this.size - 1 - i, 8, getBit(bits, i));
3949
+ for (let i = 8; i < 15; i++)
3950
+ this.setFunctionModule(8, this.size - 15 + i, getBit(bits, i));
3951
+ this.setFunctionModule(8, this.size - 8, true);
3952
+ }
3953
+ // Draws two copies of the version bits (with its own error correction code),
3954
+ // based on this object's version field, iff 7 <= version <= 40.
3955
+ drawVersion() {
3956
+ if (this.version < 7)
3957
+ return;
3958
+ let rem = this.version;
3959
+ for (let i = 0; i < 12; i++)
3960
+ rem = rem << 1 ^ (rem >>> 11) * 7973;
3961
+ const bits = this.version << 12 | rem;
3962
+ for (let i = 0; i < 18; i++) {
3963
+ const color = getBit(bits, i);
3964
+ const a = this.size - 11 + i % 3;
3965
+ const b = Math.floor(i / 3);
3966
+ this.setFunctionModule(a, b, color);
3967
+ this.setFunctionModule(b, a, color);
3968
+ }
3969
+ }
3970
+ // Draws a 9*9 finder pattern including the border separator,
3971
+ // with the center module at (x, y). Modules can be out of bounds.
3972
+ drawFinderPattern(x, y) {
3973
+ for (let dy = -4; dy <= 4; dy++) {
3974
+ for (let dx = -4; dx <= 4; dx++) {
3975
+ const dist = Math.max(Math.abs(dx), Math.abs(dy));
3976
+ const xx = x + dx;
3977
+ const yy = y + dy;
3978
+ if (xx >= 0 && xx < this.size && yy >= 0 && yy < this.size)
3979
+ this.setFunctionModule(xx, yy, dist !== 2 && dist !== 4, QrCodeDataType.Position);
3980
+ }
3981
+ }
3982
+ }
3983
+ // Draws a 5*5 alignment pattern, with the center module
3984
+ // at (x, y). All modules must be in bounds.
3985
+ drawAlignmentPattern(x, y) {
3986
+ for (let dy = -2; dy <= 2; dy++) {
3987
+ for (let dx = -2; dx <= 2; dx++) {
3988
+ this.setFunctionModule(
3989
+ x + dx,
3990
+ y + dy,
3991
+ Math.max(Math.abs(dx), Math.abs(dy)) !== 1,
3992
+ QrCodeDataType.Alignment
3993
+ );
3994
+ }
3995
+ }
3996
+ }
3997
+ // Sets the color of a module and marks it as a function module.
3998
+ // Only used by the constructor. Coordinates must be in bounds.
3999
+ setFunctionModule(x, y, isDark, type = QrCodeDataType.Function) {
4000
+ this.modules[y][x] = isDark;
4001
+ this.types[y][x] = type;
4002
+ }
4003
+ /* -- Private helper methods for constructor: Codewords and masking -- */
4004
+ // Returns a new byte string representing the given data with the appropriate error correction
4005
+ // codewords appended to it, based on this object's version and error correction level.
4006
+ addEccAndInterleave(data) {
4007
+ const ver = this.version;
4008
+ const ecl = this.ecc;
4009
+ if (data.length !== getNumDataCodewords(ver, ecl))
4010
+ throw new RangeError("Invalid argument");
4011
+ const numBlocks = NUM_ERROR_CORRECTION_BLOCKS[ecl[0]][ver];
4012
+ const blockEccLen = ECC_CODEWORDS_PER_BLOCK[ecl[0]][ver];
4013
+ const rawCodewords = Math.floor(getNumRawDataModules(ver) / 8);
4014
+ const numShortBlocks = numBlocks - rawCodewords % numBlocks;
4015
+ const shortBlockLen = Math.floor(rawCodewords / numBlocks);
4016
+ const blocks = [];
4017
+ const rsDiv = reedSolomonComputeDivisor(blockEccLen);
4018
+ for (let i = 0, k = 0; i < numBlocks; i++) {
4019
+ const dat = data.slice(k, k + shortBlockLen - blockEccLen + (i < numShortBlocks ? 0 : 1));
4020
+ k += dat.length;
4021
+ const ecc = reedSolomonComputeRemainder(dat, rsDiv);
4022
+ if (i < numShortBlocks)
4023
+ dat.push(0);
4024
+ blocks.push(dat.concat(ecc));
4025
+ }
4026
+ const result = [];
4027
+ for (let i = 0; i < blocks[0].length; i++) {
4028
+ blocks.forEach((block, j) => {
4029
+ if (i !== shortBlockLen - blockEccLen || j >= numShortBlocks)
4030
+ result.push(block[i]);
4031
+ });
4032
+ }
4033
+ return result;
4034
+ }
4035
+ // Draws the given sequence of 8-bit codewords (data and error correction) onto the entire
4036
+ // data area of this QR Code. Function modules need to be marked off before this is called.
4037
+ drawCodewords(data) {
4038
+ if (data.length !== Math.floor(getNumRawDataModules(this.version) / 8))
4039
+ throw new RangeError("Invalid argument");
4040
+ let i = 0;
4041
+ for (let right = this.size - 1; right >= 1; right -= 2) {
4042
+ if (right === 6)
4043
+ right = 5;
4044
+ for (let vert = 0; vert < this.size; vert++) {
4045
+ for (let j = 0; j < 2; j++) {
4046
+ const x = right - j;
4047
+ const upward = (right + 1 & 2) === 0;
4048
+ const y = upward ? this.size - 1 - vert : vert;
4049
+ if (!this.types[y][x] && i < data.length * 8) {
4050
+ this.modules[y][x] = getBit(data[i >>> 3], 7 - (i & 7));
4051
+ i++;
4052
+ }
4053
+ }
4054
+ }
4055
+ }
4056
+ }
4057
+ // XORs the codeword modules in this QR Code with the given mask pattern.
4058
+ // The function modules must be marked and the codeword bits must be drawn
4059
+ // before masking. Due to the arithmetic of XOR, calling applyMask() with
4060
+ // the same mask value a second time will undo the mask. A final well-formed
4061
+ // QR Code needs exactly one (not zero, two, etc.) mask applied.
4062
+ applyMask(mask) {
4063
+ if (mask < 0 || mask > 7)
4064
+ throw new RangeError("Mask value out of range");
4065
+ for (let y = 0; y < this.size; y++) {
4066
+ for (let x = 0; x < this.size; x++) {
4067
+ let invert;
4068
+ switch (mask) {
4069
+ case 0:
4070
+ invert = (x + y) % 2 === 0;
4071
+ break;
4072
+ case 1:
4073
+ invert = y % 2 === 0;
4074
+ break;
4075
+ case 2:
4076
+ invert = x % 3 === 0;
4077
+ break;
4078
+ case 3:
4079
+ invert = (x + y) % 3 === 0;
4080
+ break;
4081
+ case 4:
4082
+ invert = (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0;
4083
+ break;
4084
+ case 5:
4085
+ invert = x * y % 2 + x * y % 3 === 0;
4086
+ break;
4087
+ case 6:
4088
+ invert = (x * y % 2 + x * y % 3) % 2 === 0;
4089
+ break;
4090
+ case 7:
4091
+ invert = ((x + y) % 2 + x * y % 3) % 2 === 0;
4092
+ break;
4093
+ default:
4094
+ throw new Error("Unreachable");
4095
+ }
4096
+ if (!this.types[y][x] && invert)
4097
+ this.modules[y][x] = !this.modules[y][x];
4098
+ }
4099
+ }
4100
+ }
4101
+ // Calculates and returns the penalty score based on state of this QR Code's current modules.
4102
+ // This is used by the automatic mask choice algorithm to find the mask pattern that yields the lowest score.
4103
+ getPenaltyScore() {
4104
+ let result = 0;
4105
+ for (let y = 0; y < this.size; y++) {
4106
+ let runColor = false;
4107
+ let runX = 0;
4108
+ const runHistory = [0, 0, 0, 0, 0, 0, 0];
4109
+ for (let x = 0; x < this.size; x++) {
4110
+ if (this.modules[y][x] === runColor) {
4111
+ runX++;
4112
+ if (runX === 5)
4113
+ result += PENALTY_N1;
4114
+ else if (runX > 5)
4115
+ result++;
4116
+ } else {
4117
+ this.finderPenaltyAddHistory(runX, runHistory);
4118
+ if (!runColor)
4119
+ result += this.finderPenaltyCountPatterns(runHistory) * PENALTY_N3;
4120
+ runColor = this.modules[y][x];
4121
+ runX = 1;
4122
+ }
4123
+ }
4124
+ result += this.finderPenaltyTerminateAndCount(runColor, runX, runHistory) * PENALTY_N3;
4125
+ }
4126
+ for (let x = 0; x < this.size; x++) {
4127
+ let runColor = false;
4128
+ let runY = 0;
4129
+ const runHistory = [0, 0, 0, 0, 0, 0, 0];
4130
+ for (let y = 0; y < this.size; y++) {
4131
+ if (this.modules[y][x] === runColor) {
4132
+ runY++;
4133
+ if (runY === 5)
4134
+ result += PENALTY_N1;
4135
+ else if (runY > 5)
4136
+ result++;
4137
+ } else {
4138
+ this.finderPenaltyAddHistory(runY, runHistory);
4139
+ if (!runColor)
4140
+ result += this.finderPenaltyCountPatterns(runHistory) * PENALTY_N3;
4141
+ runColor = this.modules[y][x];
4142
+ runY = 1;
4143
+ }
4144
+ }
4145
+ result += this.finderPenaltyTerminateAndCount(runColor, runY, runHistory) * PENALTY_N3;
4146
+ }
4147
+ for (let y = 0; y < this.size - 1; y++) {
4148
+ for (let x = 0; x < this.size - 1; x++) {
4149
+ const color = this.modules[y][x];
4150
+ if (color === this.modules[y][x + 1] && color === this.modules[y + 1][x] && color === this.modules[y + 1][x + 1]) {
4151
+ result += PENALTY_N2;
4152
+ }
4153
+ }
4154
+ }
4155
+ let dark = 0;
4156
+ for (const row of this.modules)
4157
+ dark = row.reduce((sum, color) => sum + (color ? 1 : 0), dark);
4158
+ const total = this.size * this.size;
4159
+ const k = Math.ceil(Math.abs(dark * 20 - total * 10) / total) - 1;
4160
+ result += k * PENALTY_N4;
4161
+ return result;
4162
+ }
4163
+ /* -- Private helper functions -- */
4164
+ // Returns an ascending list of positions of alignment patterns for this version number.
4165
+ // Each position is in the range [0,177), and are used on both the x and y axes.
4166
+ // This could be implemented as lookup table of 40 variable-length lists of integers.
4167
+ getAlignmentPatternPositions() {
4168
+ if (this.version === 1) {
4169
+ return [];
4170
+ } else {
4171
+ const numAlign = Math.floor(this.version / 7) + 2;
4172
+ const step = this.version === 32 ? 26 : Math.ceil((this.version * 4 + 4) / (numAlign * 2 - 2)) * 2;
4173
+ const result = [6];
4174
+ for (let pos = this.size - 7; result.length < numAlign; pos -= step)
4175
+ result.splice(1, 0, pos);
4176
+ return result;
4177
+ }
4178
+ }
4179
+ // Can only be called immediately after a light run is added, and
4180
+ // returns either 0, 1, or 2. A helper function for getPenaltyScore().
4181
+ finderPenaltyCountPatterns(runHistory) {
4182
+ const n = runHistory[1];
4183
+ const core = n > 0 && runHistory[2] === n && runHistory[3] === n * 3 && runHistory[4] === n && runHistory[5] === n;
4184
+ return (core && runHistory[0] >= n * 4 && runHistory[6] >= n ? 1 : 0) + (core && runHistory[6] >= n * 4 && runHistory[0] >= n ? 1 : 0);
4185
+ }
4186
+ // Must be called at the end of a line (row or column) of modules. A helper function for getPenaltyScore().
4187
+ finderPenaltyTerminateAndCount(currentRunColor, currentRunLength, runHistory) {
4188
+ if (currentRunColor) {
4189
+ this.finderPenaltyAddHistory(currentRunLength, runHistory);
4190
+ currentRunLength = 0;
4191
+ }
4192
+ currentRunLength += this.size;
4193
+ this.finderPenaltyAddHistory(currentRunLength, runHistory);
4194
+ return this.finderPenaltyCountPatterns(runHistory);
4195
+ }
4196
+ // Pushes the given value to the front and drops the last value. A helper function for getPenaltyScore().
4197
+ finderPenaltyAddHistory(currentRunLength, runHistory) {
4198
+ if (runHistory[0] === 0)
4199
+ currentRunLength += this.size;
4200
+ runHistory.pop();
4201
+ runHistory.unshift(currentRunLength);
4202
+ }
4203
+ };
4204
+ function appendBits(val, len, bb) {
4205
+ if (len < 0 || len > 31 || val >>> len !== 0)
4206
+ throw new RangeError("Value out of range");
4207
+ for (let i = len - 1; i >= 0; i--)
4208
+ bb.push(val >>> i & 1);
4209
+ }
4210
+ function getBit(x, i) {
4211
+ return (x >>> i & 1) !== 0;
4212
+ }
4213
+ var QrSegment = class {
4214
+ // Creates a new QR Code segment with the given attributes and data.
4215
+ // The character count (numChars) must agree with the mode and the bit buffer length,
4216
+ // but the constraint isn't checked. The given bit buffer is cloned and stored.
4217
+ constructor(mode, numChars, bitData) {
4218
+ this.mode = mode;
4219
+ this.numChars = numChars;
4220
+ this.bitData = bitData;
4221
+ if (numChars < 0)
4222
+ throw new RangeError("Invalid argument");
4223
+ this.bitData = bitData.slice();
4224
+ }
4225
+ /* -- Methods -- */
4226
+ // Returns a new copy of the data bits of this segment.
4227
+ getData() {
4228
+ return this.bitData.slice();
4229
+ }
4230
+ };
4231
+ var MODE_NUMERIC = [1, 10, 12, 14];
4232
+ var MODE_ALPHANUMERIC = [2, 9, 11, 13];
4233
+ var MODE_BYTE = [4, 8, 16, 16];
4234
+ function numCharCountBits(mode, ver) {
4235
+ return mode[Math.floor((ver + 7) / 17) + 1];
4236
+ }
4237
+ function makeBytes(data) {
4238
+ const bb = [];
4239
+ for (const b of data)
4240
+ appendBits(b, 8, bb);
4241
+ return new QrSegment(MODE_BYTE, data.length, bb);
4242
+ }
4243
+ function makeNumeric(digits) {
4244
+ if (!isNumeric(digits))
4245
+ throw new RangeError("String contains non-numeric characters");
4246
+ const bb = [];
4247
+ for (let i = 0; i < digits.length; ) {
4248
+ const n = Math.min(digits.length - i, 3);
4249
+ appendBits(Number.parseInt(digits.substring(i, i + n), 10), n * 3 + 1, bb);
4250
+ i += n;
4251
+ }
4252
+ return new QrSegment(MODE_NUMERIC, digits.length, bb);
4253
+ }
4254
+ function makeAlphanumeric(text) {
4255
+ if (!isAlphanumeric(text))
4256
+ throw new RangeError("String contains unencodable characters in alphanumeric mode");
4257
+ const bb = [];
4258
+ let i;
4259
+ for (i = 0; i + 2 <= text.length; i += 2) {
4260
+ let temp = ALPHANUMERIC_CHARSET.indexOf(text.charAt(i)) * 45;
4261
+ temp += ALPHANUMERIC_CHARSET.indexOf(text.charAt(i + 1));
4262
+ appendBits(temp, 11, bb);
4263
+ }
4264
+ if (i < text.length)
4265
+ appendBits(ALPHANUMERIC_CHARSET.indexOf(text.charAt(i)), 6, bb);
4266
+ return new QrSegment(MODE_ALPHANUMERIC, text.length, bb);
4267
+ }
4268
+ function makeSegments(text) {
4269
+ if (text === "")
4270
+ return [];
4271
+ else if (isNumeric(text))
4272
+ return [makeNumeric(text)];
4273
+ else if (isAlphanumeric(text))
4274
+ return [makeAlphanumeric(text)];
4275
+ else
4276
+ return [makeBytes(toUtf8ByteArray(text))];
4277
+ }
4278
+ function isNumeric(text) {
4279
+ return NUMERIC_REGEX.test(text);
4280
+ }
4281
+ function isAlphanumeric(text) {
4282
+ return ALPHANUMERIC_REGEX.test(text);
4283
+ }
4284
+ function getTotalBits(segs, version) {
4285
+ let result = 0;
4286
+ for (const seg of segs) {
4287
+ const ccbits = numCharCountBits(seg.mode, version);
4288
+ if (seg.numChars >= 1 << ccbits)
4289
+ return Number.POSITIVE_INFINITY;
4290
+ result += 4 + ccbits + seg.bitData.length;
4291
+ }
4292
+ return result;
4293
+ }
4294
+ function toUtf8ByteArray(str) {
4295
+ str = encodeURI(str);
4296
+ const result = [];
4297
+ for (let i = 0; i < str.length; i++) {
4298
+ if (str.charAt(i) !== "%") {
4299
+ result.push(str.charCodeAt(i));
4300
+ } else {
4301
+ result.push(Number.parseInt(str.substring(i + 1, i + 3), 16));
4302
+ i += 2;
4303
+ }
4304
+ }
4305
+ return result;
4306
+ }
4307
+ function getNumRawDataModules(ver) {
4308
+ if (ver < MIN_VERSION || ver > MAX_VERSION)
4309
+ throw new RangeError("Version number out of range");
4310
+ let result = (16 * ver + 128) * ver + 64;
4311
+ if (ver >= 2) {
4312
+ const numAlign = Math.floor(ver / 7) + 2;
4313
+ result -= (25 * numAlign - 10) * numAlign - 55;
4314
+ if (ver >= 7)
4315
+ result -= 36;
4316
+ }
4317
+ return result;
4318
+ }
4319
+ function getNumDataCodewords(ver, ecl) {
4320
+ return Math.floor(getNumRawDataModules(ver) / 8) - ECC_CODEWORDS_PER_BLOCK[ecl[0]][ver] * NUM_ERROR_CORRECTION_BLOCKS[ecl[0]][ver];
4321
+ }
4322
+ function reedSolomonComputeDivisor(degree) {
4323
+ if (degree < 1 || degree > 255)
4324
+ throw new RangeError("Degree out of range");
4325
+ const result = [];
4326
+ for (let i = 0; i < degree - 1; i++)
4327
+ result.push(0);
4328
+ result.push(1);
4329
+ let root = 1;
4330
+ for (let i = 0; i < degree; i++) {
4331
+ for (let j = 0; j < result.length; j++) {
4332
+ result[j] = reedSolomonMultiply(result[j], root);
4333
+ if (j + 1 < result.length)
4334
+ result[j] ^= result[j + 1];
4335
+ }
4336
+ root = reedSolomonMultiply(root, 2);
4337
+ }
4338
+ return result;
4339
+ }
4340
+ function reedSolomonComputeRemainder(data, divisor) {
4341
+ const result = divisor.map((_) => 0);
4342
+ for (const b of data) {
4343
+ const factor = b ^ result.shift();
4344
+ result.push(0);
4345
+ divisor.forEach((coef, i) => result[i] ^= reedSolomonMultiply(coef, factor));
4346
+ }
4347
+ return result;
4348
+ }
4349
+ function reedSolomonMultiply(x, y) {
4350
+ if (x >>> 8 !== 0 || y >>> 8 !== 0)
4351
+ throw new RangeError("Byte out of range");
4352
+ let z = 0;
4353
+ for (let i = 7; i >= 0; i--) {
4354
+ z = z << 1 ^ (z >>> 7) * 285;
4355
+ z ^= (y >>> i & 1) * x;
4356
+ }
4357
+ return z;
4358
+ }
4359
+ function encodeSegments(segs, ecl, minVersion = 1, maxVersion = 40, mask = -1, boostEcl = true) {
4360
+ if (!(MIN_VERSION <= minVersion && minVersion <= maxVersion && maxVersion <= MAX_VERSION) || mask < -1 || mask > 7) {
4361
+ throw new RangeError("Invalid value");
4362
+ }
4363
+ let version;
4364
+ let dataUsedBits;
4365
+ for (version = minVersion; ; version++) {
4366
+ const dataCapacityBits2 = getNumDataCodewords(version, ecl) * 8;
4367
+ const usedBits = getTotalBits(segs, version);
4368
+ if (usedBits <= dataCapacityBits2) {
4369
+ dataUsedBits = usedBits;
4370
+ break;
4371
+ }
4372
+ if (version >= maxVersion)
4373
+ throw new RangeError("Data too long");
4374
+ }
4375
+ for (const newEcl of [MEDIUM, QUARTILE, HIGH]) {
4376
+ if (boostEcl && dataUsedBits <= getNumDataCodewords(version, newEcl) * 8)
4377
+ ecl = newEcl;
4378
+ }
4379
+ const bb = [];
4380
+ for (const seg of segs) {
4381
+ appendBits(seg.mode[0], 4, bb);
4382
+ appendBits(seg.numChars, numCharCountBits(seg.mode, version), bb);
4383
+ for (const b of seg.getData())
4384
+ bb.push(b);
4385
+ }
4386
+ const dataCapacityBits = getNumDataCodewords(version, ecl) * 8;
4387
+ appendBits(0, Math.min(4, dataCapacityBits - bb.length), bb);
4388
+ appendBits(0, (8 - bb.length % 8) % 8, bb);
4389
+ for (let padByte = 236; bb.length < dataCapacityBits; padByte ^= 236 ^ 17)
4390
+ appendBits(padByte, 8, bb);
4391
+ const dataCodewords = Array.from({ length: Math.ceil(bb.length / 8) }, () => 0);
4392
+ bb.forEach((b, i) => dataCodewords[i >>> 3] |= b << 7 - (i & 7));
4393
+ return new QrCode(version, ecl, dataCodewords, mask);
4394
+ }
4395
+ function encode(data, options) {
4396
+ const {
4397
+ ecc = "L",
4398
+ boostEcc = false,
4399
+ minVersion = 1,
4400
+ maxVersion = 40,
4401
+ maskPattern = -1,
4402
+ border = 1
4403
+ } = options || {};
4404
+ const segment = typeof data === "string" ? makeSegments(data) : Array.isArray(data) ? [makeBytes(data)] : void 0;
4405
+ if (!segment)
4406
+ throw new Error(`uqr only supports encoding string and binary data, but got: ${typeof data}`);
4407
+ const qr = encodeSegments(
4408
+ segment,
4409
+ EccMap[ecc],
4410
+ minVersion,
4411
+ maxVersion,
4412
+ maskPattern,
4413
+ boostEcc
4414
+ );
4415
+ const result = addBorder({
4416
+ version: qr.version,
4417
+ maskPattern: qr.mask,
4418
+ size: qr.size,
4419
+ data: qr.modules,
4420
+ types: qr.types
4421
+ }, border);
4422
+ if (options?.invert)
4423
+ result.data = result.data.map((row) => row.map((mod) => !mod));
4424
+ options?.onEncoded?.(result);
4425
+ return result;
4426
+ }
4427
+ function addBorder(input, border = 1) {
4428
+ if (!border)
4429
+ return input;
4430
+ const { size } = input;
4431
+ const newSize = size + border * 2;
4432
+ input.size = newSize;
4433
+ input.data.forEach((row) => {
4434
+ for (let i = 0; i < border; i++) {
4435
+ row.unshift(false);
4436
+ row.push(false);
4437
+ }
4438
+ });
4439
+ for (let i = 0; i < border; i++) {
4440
+ input.data.unshift(Array.from({ length: newSize }, (_) => false));
4441
+ input.data.push(Array.from({ length: newSize }, (_) => false));
4442
+ }
4443
+ const b = QrCodeDataType.Border;
4444
+ input.types.forEach((row) => {
4445
+ for (let i = 0; i < border; i++) {
4446
+ row.unshift(b);
4447
+ row.push(b);
4448
+ }
4449
+ });
4450
+ for (let i = 0; i < border; i++) {
4451
+ input.types.unshift(Array.from({ length: newSize }, (_) => b));
4452
+ input.types.push(Array.from({ length: newSize }, (_) => b));
4453
+ }
4454
+ return input;
4455
+ }
4456
+ function getDataAt(data, x, y, defaults = false) {
4457
+ if (x < 0 || y < 0 || x >= data.length || y >= data.length)
4458
+ return defaults;
4459
+ return data[y][x];
4460
+ }
4461
+ function renderUnicodeCompact(data, options = {}) {
4462
+ const palette = {
4463
+ WHITE_ALL: "\u2588",
4464
+ WHITE_BLACK: "\u2580",
4465
+ BLACK_WHITE: "\u2584",
4466
+ BLACK_ALL: " "
4467
+ };
4468
+ const result = encode(data, options);
4469
+ const WHITE = false;
4470
+ const BLACK = true;
4471
+ const at = (x, y) => getDataAt(result.data, x, y, true);
4472
+ const lines = [];
4473
+ let line = "";
4474
+ for (let row = 0; row < result.size; row += 2) {
4475
+ for (let col = 0; col < result.size; col++) {
4476
+ if (at(col, row) === WHITE && at(col, row + 1) === WHITE)
4477
+ line += palette.WHITE_ALL;
4478
+ else if (at(col, row) === WHITE && at(col, row + 1) === BLACK)
4479
+ line += palette.WHITE_BLACK;
4480
+ else if (at(col, row) === BLACK && at(col, row + 1) === WHITE)
4481
+ line += palette.BLACK_WHITE;
4482
+ else
4483
+ line += palette.BLACK_ALL;
4484
+ }
4485
+ lines.push(line);
4486
+ line = "";
4487
+ }
4488
+ return lines.join("\n");
4489
+ }
4490
+
3800
4491
  // ../tutticode-protocol/src/messages.ts
3801
4492
  var BRIDGE_PROTOCOL = 1;
3802
4493
  var BRIDGE_PATH = "/bridge";
@@ -3884,6 +4575,42 @@ function isFrameHeader(value) {
3884
4575
  return typeof v.id === "string" && typeof v.part === "string" && Number.isInteger(v.seq) && v.seq >= 0 && typeof v.last === "boolean";
3885
4576
  }
3886
4577
 
4578
+ // ../tutticode-protocol/src/auth.ts
4579
+ var TUTTICODE_SCOPE = "tutticode";
4580
+ var TUTTICODE_RESOURCE_PATH = "/api/tutticode";
4581
+ function tutticodeResourceUrl(origin) {
4582
+ return `${origin}${TUTTICODE_RESOURCE_PATH}`;
4583
+ }
4584
+ var TUTTICODE_CLIENT_ID = "tutticode-cli";
4585
+ var DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
4586
+ var TUTTICODE_OAUTH_PATHS = {
4587
+ token: "/api/oauth/token",
4588
+ revoke: "/api/oauth/revoke",
4589
+ deviceAuthorization: "/api/oauth/device_authorization",
4590
+ /** Browser pages (lane B). */
4591
+ login: "/code/login",
4592
+ activate: "/code/activate",
4593
+ /** The terms of use page (the site has no `/terms`; it redirects here). */
4594
+ terms: "/page/terms-of-use"
4595
+ };
4596
+ function tutticodeTermsUrl(origin) {
4597
+ return `${origin}${TUTTICODE_OAUTH_PATHS.terms}`;
4598
+ }
4599
+ var TUTTICODE_LOGIN_HINT_COMMAND = "tutticode login --device --no-wait";
4600
+ var DEFAULT_TUTTICODE_LIMITS = {
4601
+ enforce: false,
4602
+ anonMaxSeconds: 60,
4603
+ anonWatermarkAfterSeconds: 30,
4604
+ watermarkGapSeconds: 3,
4605
+ signedInMaxSeconds: 300,
4606
+ signedInDailyRenders: 1e3,
4607
+ minCliVersion: "0.2.0"
4608
+ };
4609
+ var VERSION = /^(\d{1,6})\.(\d{1,6})\.(\d{1,6})(?:-[0-9A-Za-z.-]{1,32})?$/;
4610
+ function isCliVersion(v) {
4611
+ return typeof v === "string" && VERSION.test(v);
4612
+ }
4613
+
3887
4614
  // ../tutticode-protocol/src/validate.ts
3888
4615
  var BRIDGE_MAX_CODE_CHARS = 2e5;
3889
4616
  var FORMATS = ["wav", "flac", "mp3"];
@@ -3932,11 +4659,28 @@ function validRender(m) {
3932
4659
  return "lufs must be -40..-5";
3933
4660
  if (!optional(m.truePeak, (v) => finite(v) && v >= -12 && v <= 0))
3934
4661
  return "truePeak must be -12..0";
3935
- for (const key of ["stems", "analyze"])
4662
+ for (const key of ["stems", "analyze", "limit", "brandTag"])
3936
4663
  if (!optional(m[key], (v) => typeof v === "boolean"))
3937
4664
  return `${key} must be a boolean`;
3938
4665
  return void 0;
3939
4666
  }
4667
+ var MAX_TOKEN_CHARS = 4096;
4668
+ var TOKEN = /^[A-Za-z0-9._~+/=-]+$/;
4669
+ var INSTALL_ID = /^[A-Za-z0-9_-]{16,64}$/;
4670
+ function validAuth(m) {
4671
+ if (!optional(
4672
+ m.accessToken,
4673
+ (v) => typeof v === "string" && v.length > 0 && v.length <= MAX_TOKEN_CHARS && TOKEN.test(v)
4674
+ ))
4675
+ return "auth.accessToken must be a token string";
4676
+ if (typeof m.installId !== "string" || !INSTALL_ID.test(m.installId))
4677
+ return "auth.installId must be 16..64 characters of [A-Za-z0-9_-]";
4678
+ if (!isCliVersion(m.cliVersion))
4679
+ return "auth.cliVersion must be major.minor.patch";
4680
+ if (m.client !== "cli" && m.client !== "mcp")
4681
+ return "auth.client must be cli or mcp";
4682
+ return void 0;
4683
+ }
3940
4684
  function validateCommand(raw) {
3941
4685
  if (!isObject(raw))
3942
4686
  return { ok: false, reason: "not an object" };
@@ -3955,6 +4699,19 @@ function validateCommand(raw) {
3955
4699
  return validCode(raw.code) ? { ok: true, value: raw } : { ok: false, reason: "open needs code" };
3956
4700
  case "cancel":
3957
4701
  return isId(raw.id) ? { ok: true, value: raw } : { ok: false, reason: "cancel needs an id" };
4702
+ case "auth": {
4703
+ const reason = validAuth(raw);
4704
+ if (reason)
4705
+ return { ok: false, reason };
4706
+ const auth = {
4707
+ type: "auth",
4708
+ ...raw.accessToken !== void 0 ? { accessToken: raw.accessToken } : {},
4709
+ installId: raw.installId,
4710
+ cliVersion: raw.cliVersion,
4711
+ client: raw.client
4712
+ };
4713
+ return { ok: true, value: auth };
4714
+ }
3958
4715
  default:
3959
4716
  return { ok: false, reason: `unknown command type ${String(raw.type)}` };
3960
4717
  }
@@ -3971,6 +4728,8 @@ function validateMessage(raw) {
3971
4728
  if (raw.type === "error") {
3972
4729
  if (typeof raw.code !== "string" || typeof raw.message !== "string")
3973
4730
  return { ok: false, reason: "error needs code and message" };
4731
+ if (!optional(raw.limit, validLimit))
4732
+ return { ok: false, reason: "error.limit must be {name, value, signedInValue}" };
3974
4733
  return { ok: true, value: raw };
3975
4734
  }
3976
4735
  if (!isId(raw.id))
@@ -3983,11 +4742,16 @@ function validateMessage(raw) {
3983
4742
  return { ok: false, reason: "rendered needs meta" };
3984
4743
  return { ok: true, value: raw };
3985
4744
  }
4745
+ function validLimit(v) {
4746
+ return isObject(v) && v.name === "length" && inRange(v.value, 0, MAX_RENDER_SECONDS) && inRange(v.signedInValue, 0, MAX_RENDER_SECONDS);
4747
+ }
3986
4748
  function bridgePageUrl(origin, locale, port, token) {
3987
4749
  return `${origin}/${locale}/code/bridge#p=${port}&t=${encodeURIComponent(token)}`;
3988
4750
  }
3989
4751
  var MAX_LABEL_CHARS = 64;
3990
4752
  var MAX_VERSION_CHARS = 128;
4753
+ var MAX_WARNINGS = 32;
4754
+ var MAX_WARNING_CHARS = 1e3;
3991
4755
  var MAX_EDIT_URL_CHARS = 1e6;
3992
4756
  var MAX_GRID_ENTRIES = 2e4;
3993
4757
  var MAX_RENDER_SECONDS = BRIDGE_MAX_SECONDS + 60;
@@ -4023,6 +4787,13 @@ function validateRenderMeta(raw, expect) {
4023
4787
  return no("meta.lufs / meta.truePeak out of range");
4024
4788
  if (!inRange(raw.gainDb, -100, 100))
4025
4789
  return no("meta.gainDb out of range");
4790
+ if (!optional(raw.limitDb, (v) => inRange(v, 0, 100)))
4791
+ return no("meta.limitDb out of range");
4792
+ if (!optional(
4793
+ raw.warnings,
4794
+ (v) => Array.isArray(v) && v.length <= MAX_WARNINGS && v.every((w) => typeof w === "string" && w.length <= MAX_WARNING_CHARS)
4795
+ ))
4796
+ return no("meta.warnings must be at most 32 short strings");
4026
4797
  const barTimes = grid(raw.barTimes);
4027
4798
  const beatTimes = grid(raw.beatTimes);
4028
4799
  if (!barTimes || !beatTimes)
@@ -4046,7 +4817,14 @@ function validateRenderMeta(raw, expect) {
4046
4817
  const voicePeak = level(v.truePeak, -200, 40);
4047
4818
  if (voiceLufs === void 0 || voicePeak === void 0)
4048
4819
  return no("meta.voices levels out of range");
4049
- voices.push({ label: v.label, lufs: voiceLufs, truePeak: voicePeak });
4820
+ if (!optional(v.gainDb, (g) => inRange(g, -100, 100)))
4821
+ return no("meta.voices gainDb out of range");
4822
+ voices.push({
4823
+ label: v.label,
4824
+ lufs: voiceLufs,
4825
+ truePeak: voicePeak,
4826
+ ...v.gainDb !== void 0 ? { gainDb: v.gainDb } : {}
4827
+ });
4050
4828
  }
4051
4829
  if (!isObject(raw.check))
4052
4830
  return no("meta.check must be a report");
@@ -4054,6 +4832,12 @@ function validateRenderMeta(raw, expect) {
4054
4832
  return no("meta.analysis must be an object");
4055
4833
  if (typeof raw.engineVersion !== "string" || raw.engineVersion.length > MAX_VERSION_CHARS)
4056
4834
  return no("meta.engineVersion malformed");
4835
+ let watermark;
4836
+ if (raw.watermark !== void 0) {
4837
+ watermark = validWatermark(raw.watermark, raw.seconds);
4838
+ if (!watermark)
4839
+ return no("meta.watermark must be {reason, at: ascending seconds within the render, removeWith}");
4840
+ }
4057
4841
  if (typeof raw.editUrl !== "string" || raw.editUrl.length > MAX_EDIT_URL_CHARS || !expect.origins.includes(urlOrigin(raw.editUrl) ?? ""))
4058
4842
  return no("meta.editUrl must be a link on the TuttiDAW site");
4059
4843
  return {
@@ -4067,6 +4851,8 @@ function validateRenderMeta(raw, expect) {
4067
4851
  lufs,
4068
4852
  truePeak,
4069
4853
  gainDb: raw.gainDb,
4854
+ ...raw.limitDb !== void 0 ? { limitDb: raw.limitDb } : {},
4855
+ ...raw.warnings !== void 0 ? { warnings: [...raw.warnings] } : {},
4070
4856
  barTimes,
4071
4857
  beatTimes,
4072
4858
  stems,
@@ -4074,10 +4860,28 @@ function validateRenderMeta(raw, expect) {
4074
4860
  ...raw.analysis !== void 0 ? { analysis: raw.analysis } : {},
4075
4861
  check: raw.check,
4076
4862
  editUrl: raw.editUrl,
4077
- engineVersion: raw.engineVersion
4863
+ engineVersion: raw.engineVersion,
4864
+ ...watermark ? { watermark } : {}
4078
4865
  }
4079
4866
  };
4080
4867
  }
4868
+ var MAX_WATERMARKS = 200;
4869
+ function validWatermark(v, seconds) {
4870
+ if (!isObject(v) || v.reason !== "anonymous")
4871
+ return void 0;
4872
+ if (typeof v.removeWith !== "string" || !v.removeWith || v.removeWith.length > 64)
4873
+ return void 0;
4874
+ if (!Array.isArray(v.at) || v.at.length === 0 || v.at.length > MAX_WATERMARKS)
4875
+ return void 0;
4876
+ const at = v.at;
4877
+ for (let i = 0; i < at.length; i++) {
4878
+ if (!inRange(at[i], 0, seconds))
4879
+ return void 0;
4880
+ if (i > 0 && at[i] <= at[i - 1])
4881
+ return void 0;
4882
+ }
4883
+ return { reason: "anonymous", at: [...at], removeWith: v.removeWith };
4884
+ }
4081
4885
  function urlOrigin(url) {
4082
4886
  try {
4083
4887
  const parsed = new URL(url);
@@ -4087,22 +4891,677 @@ function urlOrigin(url) {
4087
4891
  }
4088
4892
  }
4089
4893
 
4894
+ // src/credentials.ts
4895
+ var import_node_child_process = require("node:child_process");
4896
+ var import_node_crypto = require("node:crypto");
4897
+ var import_node_fs2 = require("node:fs");
4898
+ var import_node_os2 = require("node:os");
4899
+ var import_node_path2 = require("node:path");
4900
+
4901
+ // src/env.ts
4902
+ var import_node_fs = require("node:fs");
4903
+ var import_node_os = require("node:os");
4904
+ var import_node_path = require("node:path");
4905
+ var VERSION2 = true ? "0.2.0" : readPackageVersion();
4906
+ function envBrowserArgs() {
4907
+ const args = process.env.TUTTICODE_BROWSER_ARGS?.split(" ").filter(Boolean);
4908
+ return args?.length ? args : void 0;
4909
+ }
4910
+ function stateDir() {
4911
+ const dir = process.env.TUTTICODE_HOME || (0, import_node_path.join)((0, import_node_os.homedir)(), ".tutticode");
4912
+ (0, import_node_fs.mkdirSync)(dir, { recursive: true, mode: 448 });
4913
+ return dir;
4914
+ }
4915
+ var assetsDir;
4916
+ function skillsDir() {
4917
+ return process.env.TUTTICODE_SKILLS_DIR || assetsDir || // Running from the sources (tests): the app's public skills.
4918
+ (0, import_node_path.join)(__dirname, "..", "..", "..", "public", "skills");
4919
+ }
4920
+ function setSkillsDir(dir) {
4921
+ assetsDir = dir;
4922
+ }
4923
+ var LOG_ROLL_BYTES = 2 * 1024 * 1024;
4924
+ function createLogger(name, verbose) {
4925
+ let file2;
4926
+ try {
4927
+ const dir = (0, import_node_path.join)(stateDir(), "logs");
4928
+ (0, import_node_fs.mkdirSync)(dir, { recursive: true });
4929
+ file2 = (0, import_node_path.join)(dir, `${name}.log`);
4930
+ if (((0, import_node_fs.statSync)(file2, { throwIfNoEntry: false })?.size ?? 0) > LOG_ROLL_BYTES)
4931
+ (0, import_node_fs.renameSync)(file2, `${file2}.1`);
4932
+ } catch {
4933
+ file2 = void 0;
4934
+ }
4935
+ return (level2, message) => {
4936
+ const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${level2} [${process.pid}] ${message}`;
4937
+ if (verbose || level2 === "error")
4938
+ process.stderr.write(`tutticode: ${message}
4939
+ `);
4940
+ if (!file2)
4941
+ return;
4942
+ try {
4943
+ (0, import_node_fs.appendFileSync)(file2, `${line}
4944
+ `);
4945
+ } catch {
4946
+ }
4947
+ };
4948
+ }
4949
+
4950
+ // src/credentials.ts
4951
+ var CREDENTIALS_FILE = "credentials";
4952
+ function credentialsPath() {
4953
+ return (0, import_node_path2.join)(stateDir(), CREDENTIALS_FILE);
4954
+ }
4955
+ var EMPTY = { version: 1, accounts: {} };
4956
+ function readCredentials(path = credentialsPath()) {
4957
+ let raw;
4958
+ try {
4959
+ if ((0, import_node_fs2.lstatSync)(path, { throwIfNoEntry: false })?.isSymbolicLink())
4960
+ return EMPTY;
4961
+ raw = JSON.parse((0, import_node_fs2.readFileSync)(path, "utf8"));
4962
+ } catch {
4963
+ return EMPTY;
4964
+ }
4965
+ if (!isObject2(raw) || raw.version !== 1 || !isObject2(raw.accounts))
4966
+ return EMPTY;
4967
+ const accounts = {};
4968
+ for (const [origin, entry] of Object.entries(raw.accounts))
4969
+ if (validAccount(entry))
4970
+ accounts[origin] = entry;
4971
+ return { version: 1, accounts };
4972
+ }
4973
+ function readAccount(origin) {
4974
+ return readCredentials().accounts[origin];
4975
+ }
4976
+ function validAccount(v) {
4977
+ return isObject2(v) && v.kind === "login" && typeof v.accessToken === "string" && typeof v.refreshToken === "string" && typeof v.accessTokenExpiresAt === "string" && typeof v.clientId === "string" && typeof v.scope === "string";
4978
+ }
4979
+ var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
4980
+ function writeCredentials(file2) {
4981
+ const path = credentialsPath();
4982
+ if (!Object.keys(file2.accounts).length) {
4983
+ (0, import_node_fs2.rmSync)(path, { force: true });
4984
+ return;
4985
+ }
4986
+ writePrivateFile(path, `${JSON.stringify(file2, null, 2)}
4987
+ `);
4988
+ }
4989
+ function saveAccount(origin, account) {
4990
+ return withCredentialsLock(() => {
4991
+ const accounts = { ...readCredentials().accounts };
4992
+ if (account)
4993
+ accounts[origin] = account;
4994
+ else
4995
+ delete accounts[origin];
4996
+ writeCredentials({ version: 1, accounts });
4997
+ });
4998
+ }
4999
+ var PrivateDirError = class extends Error {
5000
+ };
5001
+ var privateDirs = /* @__PURE__ */ new Map();
5002
+ var fileId = (st) => `${st.dev}:${st.ino}`;
5003
+ function refusal(dir, why) {
5004
+ return new PrivateDirError(
5005
+ `${dir} ${why}, so tutticode will not keep a sign-in there. Point TUTTICODE_HOME at a folder you own that only you can use (or unset it for ~/.tutticode).`
5006
+ );
5007
+ }
5008
+ function ensurePrivateDir(dir = stateDir()) {
5009
+ const known = privateDirs.get(dir);
5010
+ const now = (0, import_node_fs2.lstatSync)(dir, { bigint: true, throwIfNoEntry: false });
5011
+ if (known && now && fileId(now) === known)
5012
+ return dir;
5013
+ privateDirs.set(dir, checkPrivateDir(dir, true));
5014
+ return dir;
5015
+ }
5016
+ function checkPrivateDir(dir, fix) {
5017
+ const st = (0, import_node_fs2.lstatSync)(dir, { bigint: true, throwIfNoEntry: false });
5018
+ if (!st)
5019
+ throw refusal(dir, "does not exist");
5020
+ if (st.isSymbolicLink())
5021
+ throw refusal(dir, "is a symbolic link or a junction");
5022
+ if (!st.isDirectory())
5023
+ throw refusal(dir, "is not a folder");
5024
+ const id = process.platform === "win32" ? checkWindowsDir(dir, fix) : checkPosixDir(dir, fix);
5025
+ if (id !== fileId(st))
5026
+ throw refusal(dir, "was replaced while it was checked");
5027
+ return id;
5028
+ }
5029
+ function checkPosixDir(dir, fix) {
5030
+ let fd;
5031
+ try {
5032
+ fd = (0, import_node_fs2.openSync)(
5033
+ dir,
5034
+ import_node_fs2.constants.O_RDONLY | import_node_fs2.constants.O_DIRECTORY | import_node_fs2.constants.O_NOFOLLOW
5035
+ );
5036
+ } catch {
5037
+ throw refusal(dir, "was replaced while it was checked");
5038
+ }
5039
+ try {
5040
+ let st = (0, import_node_fs2.fstatSync)(fd, { bigint: true });
5041
+ if (process.getuid && st.uid !== BigInt(process.getuid()))
5042
+ throw refusal(dir, "belongs to another user");
5043
+ if (fix && st.mode & BigInt(63)) {
5044
+ try {
5045
+ (0, import_node_fs2.fchmodSync)(fd, 448);
5046
+ } catch {
5047
+ throw refusal(dir, "cannot be made private (chmod 0700 failed)");
5048
+ }
5049
+ st = (0, import_node_fs2.fstatSync)(fd, { bigint: true });
5050
+ }
5051
+ if (st.mode & BigInt(63))
5052
+ throw refusal(dir, "is open to other users");
5053
+ return fileId(st);
5054
+ } finally {
5055
+ (0, import_node_fs2.closeSync)(fd);
5056
+ }
5057
+ }
5058
+ var WINDOWS_OWNERS = /* @__PURE__ */ new Set(["SY", "BA", "S-1-5-18", "S-1-5-32-544"]);
5059
+ function checkWindowsDir(dir, fix) {
5060
+ const sid = currentUserSid();
5061
+ const inspect = () => {
5062
+ const sec = windowsSecurity(dir);
5063
+ if (!sid || !sec)
5064
+ throw refusal(dir, "has an owner or access list tutticode cannot check");
5065
+ if (sec.reparse)
5066
+ throw refusal(dir, "is a symbolic link or a junction");
5067
+ if (sec.owner !== sid && !WINDOWS_OWNERS.has(sec.owner))
5068
+ throw refusal(dir, `belongs to another user (${sec.owner})`);
5069
+ return { access: daclAccess(sec.sddl, sid), protected: sec.protected };
5070
+ };
5071
+ let state = inspect();
5072
+ if (fix && (!state.access.ok || !state.protected)) {
5073
+ if (!restrictDirAcl(dir))
5074
+ throw refusal(dir, "could not be limited to your user (icacls failed)");
5075
+ state = inspect();
5076
+ }
5077
+ if (!state.access.ok)
5078
+ throw refusal(dir, `is ${state.access.detail}`);
5079
+ return fileId((0, import_node_fs2.lstatSync)(dir, { bigint: true }));
5080
+ }
5081
+ function writePrivateFile(path, text) {
5082
+ const dir = (0, import_node_path2.dirname)(path);
5083
+ ensurePrivateDir(dir);
5084
+ const checked = privateDirs.get(dir);
5085
+ const target = (0, import_node_fs2.lstatSync)(path, { throwIfNoEntry: false });
5086
+ if (target && !target.isFile())
5087
+ throw new PrivateDirError(
5088
+ `${path} is not a plain file (a link, a junction or a folder), so tutticode will not write the sign-in through it. Remove it and try again.`
5089
+ );
5090
+ const changed = () => new PrivateDirError(
5091
+ `${dir} changed while the sign-in was being written (replaced, or its owner or access list changed); nothing was written.`
5092
+ );
5093
+ const tmp = `${path}.${(0, import_node_crypto.randomBytes)(6).toString("hex")}.tmp`;
5094
+ const tmpId = () => {
5095
+ const st = (0, import_node_fs2.lstatSync)(tmp, { bigint: true, throwIfNoEntry: false });
5096
+ return st && fileId(st);
5097
+ };
5098
+ const fd = (0, import_node_fs2.openSync)(
5099
+ tmp,
5100
+ import_node_fs2.constants.O_WRONLY | import_node_fs2.constants.O_CREAT | import_node_fs2.constants.O_EXCL | (import_node_fs2.constants.O_NOFOLLOW ?? 0),
5101
+ 384
5102
+ );
5103
+ let open2 = true;
5104
+ try {
5105
+ const written = fileId((0, import_node_fs2.fstatSync)(fd, { bigint: true }));
5106
+ if (tmpId() !== written)
5107
+ throw changed();
5108
+ (0, import_node_fs2.writeFileSync)(fd, text);
5109
+ (0, import_node_fs2.fsyncSync)(fd);
5110
+ (0, import_node_fs2.closeSync)(fd);
5111
+ open2 = false;
5112
+ if (checkPrivateDir(dir, false) !== checked || tmpId() !== written)
5113
+ throw changed();
5114
+ if (process.platform === "win32" && !checkFileAccess(tmp)?.ok)
5115
+ throw new PrivateDirError(
5116
+ `Could not confirm that ${tmp} is private to your user; nothing was written.`
5117
+ );
5118
+ renameWithRetry(tmp, path);
5119
+ } catch (e) {
5120
+ if (open2)
5121
+ (0, import_node_fs2.closeSync)(fd);
5122
+ (0, import_node_fs2.rmSync)(tmp, { force: true });
5123
+ throw e;
5124
+ }
5125
+ }
5126
+ function renameWithRetry(from, to) {
5127
+ for (let attempt = 0; ; attempt++) {
5128
+ try {
5129
+ (0, import_node_fs2.renameSync)(from, to);
5130
+ return;
5131
+ } catch (e) {
5132
+ const code = e.code;
5133
+ if (attempt >= 20 || code !== "EPERM" && code !== "EBUSY")
5134
+ throw e;
5135
+ sleepSync(25);
5136
+ }
5137
+ }
5138
+ }
5139
+ function sleepSync(ms) {
5140
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
5141
+ }
5142
+ var system32 = (exe) => (0, import_node_path2.join)(process.env.SystemRoot ?? "C:\\Windows", "System32", exe);
5143
+ var userSid;
5144
+ function currentUserSid() {
5145
+ if (userSid !== void 0)
5146
+ return userSid;
5147
+ const out = (0, import_node_child_process.spawnSync)(
5148
+ system32("whoami.exe"),
5149
+ ["/user", "/fo", "csv", "/nh"],
5150
+ {
5151
+ encoding: "utf8",
5152
+ windowsHide: true
5153
+ }
5154
+ ).stdout;
5155
+ userSid = /"(S-1-[0-9-]+)"/.exec(out ?? "")?.[1] ?? null;
5156
+ return userSid;
5157
+ }
5158
+ function icaclsUser() {
5159
+ const sid = currentUserSid();
5160
+ return sid ? `*${sid}` : process.env.USERDOMAIN ? `${process.env.USERDOMAIN}\\${process.env.USERNAME}` : process.env.USERNAME ?? "";
5161
+ }
5162
+ var icacls = (args) => (0, import_node_child_process.spawnSync)(system32("icacls.exe"), args, {
5163
+ encoding: "utf8",
5164
+ windowsHide: true
5165
+ }).status === 0;
5166
+ function restrictDirAcl(dir) {
5167
+ const user = icaclsUser();
5168
+ return !!user && icacls([
5169
+ dir,
5170
+ "/inheritance:r",
5171
+ "/grant:r",
5172
+ `${user}:(OI)(CI)F`,
5173
+ "*S-1-5-18:(OI)(CI)F",
5174
+ "*S-1-5-32-544:(OI)(CI)F"
5175
+ ]);
5176
+ }
5177
+ var PS_SECURITY = "$p = $env:TUTTICODE_ACL_PATH; $a = [int][IO.File]::GetAttributes($p); $s = (New-Object System.Security.AccessControl.DirectorySecurity($p, 'Owner,Access')).GetSecurityDescriptorSddlForm('Owner,Access'); [Console]::Out.Write([string]$a + [char]10 + $s)";
5178
+ function windowsSecurity(path) {
5179
+ const out = (0, import_node_child_process.spawnSync)(
5180
+ system32("WindowsPowerShell\\v1.0\\powershell.exe"),
5181
+ ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", PS_SECURITY],
5182
+ {
5183
+ encoding: "utf8",
5184
+ windowsHide: true,
5185
+ env: { ...process.env, TUTTICODE_ACL_PATH: path }
5186
+ }
5187
+ );
5188
+ if (out.status !== 0)
5189
+ return void 0;
5190
+ const m = /^(\d+)\r?\n(O:(S-1-[0-9-]+|[A-Z]{2})D:[^\r\n]*)/.exec(
5191
+ out.stdout ?? ""
5192
+ );
5193
+ if (!m)
5194
+ return void 0;
5195
+ return {
5196
+ owner: m[3],
5197
+ sddl: m[2],
5198
+ protected: /D:[A-Z]*P/.test(m[2]),
5199
+ // FILE_ATTRIBUTE_REPARSE_POINT
5200
+ reparse: (Number(m[1]) & 1024) !== 0
5201
+ };
5202
+ }
5203
+ var ALLOWED_SIDS = /* @__PURE__ */ new Set(["SY", "BA", "OW", "S-1-5-18", "S-1-5-32-544"]);
5204
+ function sddlAllowTrustees(sddl) {
5205
+ const trustees = [];
5206
+ for (const ace of sddl.matchAll(/\(([^)]*)\)/g)) {
5207
+ const fields = ace[1].split(";");
5208
+ if (fields[0] === "A" && fields[5])
5209
+ trustees.push(fields[5]);
5210
+ }
5211
+ return trustees;
5212
+ }
5213
+ function checkFileAccess(path) {
5214
+ const st = (0, import_node_fs2.lstatSync)(path);
5215
+ if (st.isSymbolicLink())
5216
+ return { ok: false, detail: "a symbolic link or a junction" };
5217
+ if (process.platform !== "win32") {
5218
+ const mode = st.mode & 511;
5219
+ return mode & 63 ? { ok: false, detail: `mode ${mode.toString(8).padStart(4, "0")}` } : { ok: true, detail: "mode 0600" };
5220
+ }
5221
+ const sid = currentUserSid();
5222
+ if (!sid)
5223
+ return void 0;
5224
+ const dump = (0, import_node_path2.join)((0, import_node_os2.tmpdir)(), `tutticode-acl-${process.pid}.tmp`);
5225
+ try {
5226
+ const saved = (0, import_node_child_process.spawnSync)(system32("icacls.exe"), [path, "/save", dump], {
5227
+ windowsHide: true
5228
+ });
5229
+ if (saved.status !== 0)
5230
+ return void 0;
5231
+ return daclAccess((0, import_node_fs2.readFileSync)(dump).toString("utf16le"), sid);
5232
+ } finally {
5233
+ (0, import_node_fs2.rmSync)(dump, { force: true });
5234
+ }
5235
+ }
5236
+ function daclAccess(sddl, sid) {
5237
+ const others = sddlAllowTrustees(sddl).filter(
5238
+ (t) => t !== sid && !ALLOWED_SIDS.has(t)
5239
+ );
5240
+ return others.length ? { ok: false, detail: `also readable by ${others.join(", ")}` } : { ok: true, detail: "only you can read it" };
5241
+ }
5242
+ function fixFileAccess(path) {
5243
+ if (process.platform !== "win32") {
5244
+ (0, import_node_fs2.chmodSync)(path, 384);
5245
+ return true;
5246
+ }
5247
+ writePrivateFile(path, (0, import_node_fs2.readFileSync)(path, "utf8"));
5248
+ return checkFileAccess(path)?.ok ?? false;
5249
+ }
5250
+ var LOCK_STALE_MS = 3e4;
5251
+ async function withCredentialsLock(fn, options = {}) {
5252
+ const lock = (0, import_node_path2.join)(stateDir(), `${CREDENTIALS_FILE}.lock`);
5253
+ const staleMs = options.staleMs ?? LOCK_STALE_MS;
5254
+ for (; ; ) {
5255
+ if (takeLock(lock, staleMs))
5256
+ break;
5257
+ await new Promise((r2) => setTimeout(r2, options.pollMs ?? 100));
5258
+ }
5259
+ try {
5260
+ return await fn();
5261
+ } finally {
5262
+ (0, import_node_fs2.rmSync)(lock, { force: true });
5263
+ }
5264
+ }
5265
+ function takeLock(lock, staleMs) {
5266
+ const mtime = (0, import_node_fs2.statSync)(lock, { throwIfNoEntry: false })?.mtimeMs;
5267
+ if (mtime !== void 0 && Date.now() - mtime > staleMs)
5268
+ (0, import_node_fs2.rmSync)(lock, { force: true });
5269
+ try {
5270
+ const fd = (0, import_node_fs2.openSync)(lock, "wx", 384);
5271
+ (0, import_node_fs2.writeSync)(fd, String(process.pid));
5272
+ (0, import_node_fs2.closeSync)(fd);
5273
+ return true;
5274
+ } catch {
5275
+ return false;
5276
+ }
5277
+ }
5278
+ var INSTALL_ID2 = /^[A-Za-z0-9_-]{16,64}$/;
5279
+ function installId() {
5280
+ const path = (0, import_node_path2.join)(stateDir(), "install-id");
5281
+ try {
5282
+ const id2 = (0, import_node_fs2.readFileSync)(path, "utf8").trim();
5283
+ if (INSTALL_ID2.test(id2))
5284
+ return id2;
5285
+ } catch {
5286
+ }
5287
+ const id = (0, import_node_crypto.randomBytes)(16).toString("base64url");
5288
+ try {
5289
+ (0, import_node_fs2.writeFileSync)(path, `${id}
5290
+ `, { mode: 384, flag: "wx" });
5291
+ } catch (e) {
5292
+ if (e.code === "EEXIST")
5293
+ try {
5294
+ const theirs = (0, import_node_fs2.readFileSync)(path, "utf8").trim();
5295
+ if (INSTALL_ID2.test(theirs))
5296
+ return theirs;
5297
+ (0, import_node_fs2.writeFileSync)(path, `${id}
5298
+ `, { mode: 384 });
5299
+ } catch {
5300
+ }
5301
+ }
5302
+ return id;
5303
+ }
5304
+ function deviceName() {
5305
+ const os = process.platform === "win32" ? "Windows" : process.platform === "darwin" ? "macOS" : process.platform === "linux" ? "Linux" : process.platform;
5306
+ return `${(0, import_node_os2.hostname)()} (${os})`.slice(0, 100);
5307
+ }
5308
+
5309
+ // src/account.ts
5310
+ var REFRESH_MARGIN_MS = 5 * 60 * 1e3;
5311
+ var ACCOUNT_PATH = "/studio/account";
5312
+ var SIGN_IN_EXPIRED = "Your TuttiCode sign-in expired after a year: run `tutticode login`.";
5313
+ var OAuthError = class extends Error {
5314
+ constructor(error, message, status, body = {}) {
5315
+ super(message);
5316
+ this.error = error;
5317
+ this.status = status;
5318
+ this.body = body;
5319
+ }
5320
+ /** `TUTTICODE_TOKEN_EXPIRED` etc. when the server sent one. */
5321
+ get code() {
5322
+ return typeof this.body.code === "string" ? this.body.code : void 0;
5323
+ }
5324
+ };
5325
+ var ApiUnreachable = class extends Error {
5326
+ };
5327
+ async function request(origin, path, init, deps) {
5328
+ try {
5329
+ return await (deps.fetch ?? fetch)(`${origin}${path}`, {
5330
+ ...init,
5331
+ redirect: "error",
5332
+ signal: AbortSignal.timeout(deps.timeoutMs ?? 15e3)
5333
+ });
5334
+ } catch (e) {
5335
+ throw new ApiUnreachable(
5336
+ `${origin} did not answer (${e.message}).`
5337
+ );
5338
+ }
5339
+ }
5340
+ async function jsonBody(response) {
5341
+ try {
5342
+ const body = await response.json();
5343
+ return typeof body === "object" && body !== null ? body : {};
5344
+ } catch {
5345
+ return {};
5346
+ }
5347
+ }
5348
+ async function postOAuth(origin, path, params, deps = {}) {
5349
+ const response = await request(
5350
+ origin,
5351
+ path,
5352
+ {
5353
+ method: "POST",
5354
+ headers: {
5355
+ "Content-Type": "application/json",
5356
+ Accept: "application/json"
5357
+ },
5358
+ body: JSON.stringify({ client_id: TUTTICODE_CLIENT_ID, ...params })
5359
+ },
5360
+ deps
5361
+ );
5362
+ const body = await jsonBody(response);
5363
+ if (response.ok)
5364
+ return body;
5365
+ if (response.status >= 500)
5366
+ throw new ApiUnreachable(`${origin} answered ${response.status}.`);
5367
+ const error = typeof body.error === "string" ? body.error : "invalid_request";
5368
+ if (response.status === 429 && error !== "slow_down")
5369
+ throw new ApiUnreachable(`${origin} is rate limiting (429).`);
5370
+ throw new OAuthError(
5371
+ error,
5372
+ typeof body.error_description === "string" ? body.error_description : error,
5373
+ response.status,
5374
+ body
5375
+ );
5376
+ }
5377
+ function tokenResponse(body) {
5378
+ if (typeof body.access_token !== "string" || typeof body.refresh_token !== "string" || typeof body.expires_in !== "number")
5379
+ throw new ApiUnreachable("The token endpoint sent no tokens.");
5380
+ return body;
5381
+ }
5382
+ function storedAccount(tokens, now, base = {}) {
5383
+ return {
5384
+ kind: "login",
5385
+ clientId: TUTTICODE_CLIENT_ID,
5386
+ ...base,
5387
+ accessToken: tokens.access_token,
5388
+ accessTokenExpiresAt: new Date(
5389
+ now + tokens.expires_in * 1e3
5390
+ ).toISOString(),
5391
+ refreshToken: tokens.refresh_token,
5392
+ // A refresh never extends the year; keep the first end we learnt.
5393
+ grantExpiresAt: base.grantExpiresAt ?? (tokens.refresh_token_expires_in ? new Date(now + tokens.refresh_token_expires_in * 1e3).toISOString() : void 0),
5394
+ scope: tokens.scope || TUTTICODE_SCOPE,
5395
+ createdAt: base.createdAt ?? new Date(now).toISOString()
5396
+ };
5397
+ }
5398
+ var TOKEN2 = /^[A-Za-z0-9._~+/=-]{1,4096}$/;
5399
+ function envToken() {
5400
+ const token = process.env.TUTTICODE_TOKEN?.trim();
5401
+ return token && TOKEN2.test(token) ? token : void 0;
5402
+ }
5403
+ var expiresWithin = (iso, ms, now) => !iso || !(Date.parse(iso) - now > ms);
5404
+ async function resolveToken(origin, deps = {}, options = {}) {
5405
+ const pat = options.envToken === false ? void 0 : envToken();
5406
+ if (pat)
5407
+ return { kind: "pat", accessToken: pat };
5408
+ const now = deps.now ?? Date.now;
5409
+ const stored = readAccount(origin);
5410
+ if (!stored)
5411
+ return { kind: "anonymous" };
5412
+ if (stored.grantExpiresAt && Date.parse(stored.grantExpiresAt) <= now())
5413
+ return { kind: "anonymous", ended: SIGN_IN_EXPIRED };
5414
+ if (!expiresWithin(stored.accessTokenExpiresAt, REFRESH_MARGIN_MS, now()))
5415
+ return { kind: "login", accessToken: stored.accessToken };
5416
+ return withCredentialsLock(async () => {
5417
+ const current = readAccount(origin);
5418
+ if (!current)
5419
+ return { kind: "anonymous" };
5420
+ if (!expiresWithin(current.accessTokenExpiresAt, REFRESH_MARGIN_MS, now()))
5421
+ return { kind: "login", accessToken: current.accessToken };
5422
+ try {
5423
+ const tokens = tokenResponse(
5424
+ await postOAuth(
5425
+ origin,
5426
+ TUTTICODE_OAUTH_PATHS.token,
5427
+ { grant_type: "refresh_token", refresh_token: current.refreshToken },
5428
+ deps
5429
+ )
5430
+ );
5431
+ writeCredentials({
5432
+ version: 1,
5433
+ accounts: {
5434
+ ...readCredentials().accounts,
5435
+ [origin]: storedAccount(tokens, now(), current)
5436
+ }
5437
+ });
5438
+ return { kind: "login", accessToken: tokens.access_token };
5439
+ } catch (e) {
5440
+ if (e instanceof OAuthError)
5441
+ return {
5442
+ kind: "anonymous",
5443
+ ended: e.code === "TUTTICODE_TOKEN_EXPIRED" ? SIGN_IN_EXPIRED : "Your TuttiCode sign-in is no longer valid (signed out on the account page, or replaced): run `tutticode login`."
5444
+ };
5445
+ return { kind: "login", accessToken: current.accessToken };
5446
+ }
5447
+ });
5448
+ }
5449
+ function bridgeAuth(token, client) {
5450
+ return {
5451
+ ...token.accessToken ? { accessToken: token.accessToken } : {},
5452
+ installId: installId(),
5453
+ cliVersion: VERSION2,
5454
+ client
5455
+ };
5456
+ }
5457
+ async function accountState(origin, deps = {}, options = {}) {
5458
+ const token = await resolveToken(origin, deps, options);
5459
+ if (!token.accessToken || token.kind === "anonymous")
5460
+ return token.ended ? {
5461
+ state: "rejected",
5462
+ tokenKind: "login",
5463
+ code: "TUTTICODE_TOKEN_EXPIRED",
5464
+ message: token.ended
5465
+ } : { state: "anonymous" };
5466
+ const tokenKind = token.kind;
5467
+ let response;
5468
+ try {
5469
+ response = await request(
5470
+ origin,
5471
+ "/api/tutticode/me",
5472
+ {
5473
+ headers: {
5474
+ Authorization: `Bearer ${token.accessToken}`,
5475
+ Accept: "application/json"
5476
+ }
5477
+ },
5478
+ deps
5479
+ );
5480
+ } catch (e) {
5481
+ return { state: "unreachable", tokenKind, message: e.message };
5482
+ }
5483
+ const body = await jsonBody(response);
5484
+ if (response.ok && isMe(body))
5485
+ return { state: "signed-in", tokenKind, me: body };
5486
+ if (response.status === 401 || response.status === 403)
5487
+ return {
5488
+ state: "rejected",
5489
+ tokenKind,
5490
+ ...typeof body.code === "string" ? { code: body.code } : {},
5491
+ message: typeof body.message === "string" ? body.message : `The server refused the ${tokenKind === "pat" ? "TUTTICODE_TOKEN" : "sign-in"} (${response.status}).`
5492
+ };
5493
+ return {
5494
+ state: "unreachable",
5495
+ tokenKind,
5496
+ message: `${origin}/api/tutticode/me answered ${response.status}.`
5497
+ };
5498
+ }
5499
+ function isMe(v) {
5500
+ const user = v.user;
5501
+ return typeof user === "object" && user !== null && typeof user.email === "string" && typeof v.plan === "string" && typeof v.limits === "object" && v.limits !== null;
5502
+ }
5503
+ function accountLimits(state) {
5504
+ if (state.state === "signed-in")
5505
+ return {
5506
+ maxSeconds: state.me.limits.maxSeconds,
5507
+ watermarkAfterSeconds: null
5508
+ };
5509
+ return {
5510
+ maxSeconds: DEFAULT_TUTTICODE_LIMITS.anonMaxSeconds,
5511
+ watermarkAfterSeconds: DEFAULT_TUTTICODE_LIMITS.anonWatermarkAfterSeconds
5512
+ };
5513
+ }
5514
+ var LOGIN_HINT = {
5515
+ command: TUTTICODE_LOGIN_HINT_COMMAND,
5516
+ note: "A free account lifts the limits. Run the command, ask the user to open the link it prints and approve the code; keep working meanwhile."
5517
+ };
5518
+ function signedInAs(me) {
5519
+ return `${me.user.email} (TuttiCode, ${me.plan.toLowerCase()})`;
5520
+ }
5521
+ async function revokeToken(origin, token, deps = {}) {
5522
+ try {
5523
+ const response = await request(
5524
+ origin,
5525
+ TUTTICODE_OAUTH_PATHS.revoke,
5526
+ {
5527
+ method: "POST",
5528
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
5529
+ body: new URLSearchParams({
5530
+ token,
5531
+ client_id: TUTTICODE_CLIENT_ID
5532
+ }).toString()
5533
+ },
5534
+ deps
5535
+ );
5536
+ return response.status < 500;
5537
+ } catch {
5538
+ return false;
5539
+ }
5540
+ }
5541
+
4090
5542
  // src/args.ts
4091
5543
  var import_node_util = require("node:util");
4092
5544
 
4093
5545
  // src/output.ts
4094
- var import_node_fs = require("node:fs");
4095
- var import_node_path = require("node:path");
5546
+ var import_node_fs3 = require("node:fs");
5547
+ var import_node_path3 = require("node:path");
4096
5548
  var EXIT_OK = 0;
4097
5549
  var EXIT_CODE_ERRORS = 1;
4098
5550
  var EXIT_BRIDGE = 2;
4099
5551
  var EXIT_USAGE = 3;
5552
+ var EXIT_ANONYMOUS = 4;
5553
+ var EXIT_LIMIT = 5;
5554
+ var EXIT_UPGRADE = 6;
4100
5555
  var CODE_ERRORS = /* @__PURE__ */ new Set(["compile", "too-long", "clip"]);
4101
5556
  function exitCodeFor(outcome) {
4102
5557
  switch (outcome.kind) {
4103
5558
  case "checked":
4104
5559
  return outcome.report.ok ? EXIT_OK : EXIT_CODE_ERRORS;
4105
5560
  case "error":
5561
+ if (outcome.code === "limit")
5562
+ return EXIT_LIMIT;
5563
+ if (outcome.code === "upgrade")
5564
+ return EXIT_UPGRADE;
4106
5565
  return CODE_ERRORS.has(outcome.code) ? EXIT_CODE_ERRORS : EXIT_BRIDGE;
4107
5566
  default:
4108
5567
  return EXIT_OK;
@@ -4117,29 +5576,29 @@ function safeFileStem(label) {
4117
5576
  return RESERVED_NAMES.test(cleaned) ? `${cleaned}_` : cleaned;
4118
5577
  }
4119
5578
  function formatFromPath(path) {
4120
- const ext = (0, import_node_path.extname)(path).slice(1).toLowerCase();
5579
+ const ext = (0, import_node_path3.extname)(path).slice(1).toLowerCase();
4121
5580
  return ext === "wav" || ext === "flac" || ext === "mp3" ? ext : void 0;
4122
5581
  }
4123
5582
  var FORMATS2 = ["wav", "flac", "mp3"];
4124
5583
  function writeFileNoFollow(path, bytes) {
4125
5584
  try {
4126
- if ((0, import_node_fs.lstatSync)(path).isSymbolicLink())
5585
+ if ((0, import_node_fs3.lstatSync)(path).isSymbolicLink())
4127
5586
  throw new Error(`Refusing to write through the symbolic link ${path}.`);
4128
5587
  } catch (e) {
4129
5588
  if (e.code !== "ENOENT")
4130
5589
  throw e;
4131
5590
  }
4132
- const fd = (0, import_node_fs.openSync)(
5591
+ const fd = (0, import_node_fs3.openSync)(
4133
5592
  path,
4134
- import_node_fs.constants.O_WRONLY | import_node_fs.constants.O_CREAT | import_node_fs.constants.O_TRUNC | // POSIX only (undefined on Windows): fail rather than follow a link
5593
+ import_node_fs3.constants.O_WRONLY | import_node_fs3.constants.O_CREAT | import_node_fs3.constants.O_TRUNC | // POSIX only (undefined on Windows): fail rather than follow a link
4135
5594
  // swapped in after the check above.
4136
- (import_node_fs.constants.O_NOFOLLOW ?? 0)
5595
+ (import_node_fs3.constants.O_NOFOLLOW ?? 0)
4137
5596
  );
4138
5597
  try {
4139
5598
  for (let at = 0; at < bytes.length; )
4140
- at += (0, import_node_fs.writeSync)(fd, bytes, at, bytes.length - at);
5599
+ at += (0, import_node_fs3.writeSync)(fd, bytes, at, bytes.length - at);
4141
5600
  } finally {
4142
- (0, import_node_fs.closeSync)(fd);
5601
+ (0, import_node_fs3.closeSync)(fd);
4143
5602
  }
4144
5603
  }
4145
5604
  function writeRender(meta, parts, out, stemsDir) {
@@ -4150,30 +5609,30 @@ function writeRender(meta, parts, out, stemsDir) {
4150
5609
  const mix = parts.get(MIX_PART);
4151
5610
  if (!mix)
4152
5611
  throw new Error("The render has no mix.");
4153
- const mixPath = (0, import_node_path.resolve)(out);
4154
- (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(mixPath), { recursive: true });
5612
+ const mixPath = (0, import_node_path3.resolve)(out);
5613
+ (0, import_node_fs3.mkdirSync)((0, import_node_path3.dirname)(mixPath), { recursive: true });
4155
5614
  writeFileNoFollow(mixPath, mix);
4156
5615
  const stems = {};
4157
5616
  if (meta.stems.length) {
4158
- const requested = (0, import_node_path.resolve)(
4159
- stemsDir ?? mixPath.slice(0, mixPath.length - (0, import_node_path.extname)(mixPath).length) + ".stems"
5617
+ const requested = (0, import_node_path3.resolve)(
5618
+ stemsDir ?? mixPath.slice(0, mixPath.length - (0, import_node_path3.extname)(mixPath).length) + ".stems"
4160
5619
  );
4161
- (0, import_node_fs.mkdirSync)(requested, { recursive: true });
4162
- const dir = (0, import_node_fs.realpathSync)(requested);
5620
+ (0, import_node_fs3.mkdirSync)(requested, { recursive: true });
5621
+ const dir = (0, import_node_fs3.realpathSync)(requested);
4163
5622
  const used = new Set(
4164
- [requested, dir].includes((0, import_node_path.dirname)(mixPath)) ? [(0, import_node_path.basename)(mixPath).toLowerCase()] : []
5623
+ [requested, dir].includes((0, import_node_path3.dirname)(mixPath)) ? [(0, import_node_path3.basename)(mixPath).toLowerCase()] : []
4165
5624
  );
4166
5625
  for (const label of meta.stems) {
4167
5626
  const bytes = parts.get(stemPart(label));
4168
5627
  if (!bytes)
4169
5628
  continue;
4170
5629
  const base = safeFileStem(label);
4171
- let file = `${base}.${meta.format}`;
4172
- for (let n = 2; used.has(file.toLowerCase()); n++)
4173
- file = `${base}-${n}.${meta.format}`;
4174
- used.add(file.toLowerCase());
4175
- const path = (0, import_node_path.resolve)(dir, file);
4176
- if ((0, import_node_path.dirname)(path) !== dir || (0, import_node_path.basename)(path) !== file)
5630
+ let file2 = `${base}.${meta.format}`;
5631
+ for (let n = 2; used.has(file2.toLowerCase()); n++)
5632
+ file2 = `${base}-${n}.${meta.format}`;
5633
+ used.add(file2.toLowerCase());
5634
+ const path = (0, import_node_path3.resolve)(dir, file2);
5635
+ if ((0, import_node_path3.dirname)(path) !== dir || (0, import_node_path3.basename)(path) !== file2)
4177
5636
  throw new Error(`Refusing to write a stem outside ${dir}.`);
4178
5637
  writeFileNoFollow(path, bytes);
4179
5638
  stems[label] = path;
@@ -4182,11 +5641,11 @@ function writeRender(meta, parts, out, stemsDir) {
4182
5641
  return { mix: mixPath, stems };
4183
5642
  }
4184
5643
  var fixed = (n, digits = 1) => Number.isFinite(n) ? n.toFixed(digits) : String(n);
4185
- function formatDiagnostics(report, file) {
5644
+ function formatDiagnostics(report, file2) {
4186
5645
  const lines = [];
4187
5646
  for (const d of report.diagnostics) {
4188
5647
  lines.push(
4189
- `${file}:${d.line}:${d.col} ${d.severity} ${d.code} ${d.message}`
5648
+ `${file2}:${d.line}:${d.col} ${d.severity} ${d.code} ${d.message}`
4190
5649
  );
4191
5650
  if (d.snippet)
4192
5651
  lines.push(` ${d.snippet}`);
@@ -4207,94 +5666,52 @@ function formatCheckSummary(report) {
4207
5666
  )} s)`;
4208
5667
  }
4209
5668
  function formatRenderSummary(meta, files) {
5669
+ const gain = meta.gainDb ? `, gain ${meta.gainDb > 0 ? "+" : ""}${fixed(meta.gainDb)} dB` : "";
5670
+ const limited = meta.limitDb ? `, limited up to ${fixed(meta.limitDb)} dB` : "";
4210
5671
  const lines = [
4211
5672
  `Wrote ${files.mix} (${fixed(meta.seconds)} s, ${meta.bars} bars at ${fixed(
4212
5673
  meta.bpm,
4213
5674
  0
4214
- )} BPM, ${fixed(meta.lufs)} LUFS, ${fixed(meta.truePeak)} dBTP${meta.gainDb ? `, gain ${meta.gainDb > 0 ? "+" : ""}${fixed(meta.gainDb)} dB` : ""})`
5675
+ )} BPM, ${fixed(meta.lufs)} LUFS, ${fixed(
5676
+ meta.truePeak
5677
+ )} dBTP${gain}${limited})`
4215
5678
  ];
5679
+ for (const warning of meta.warnings ?? [])
5680
+ lines.push(`Note: ${warning}`);
4216
5681
  const stems = Object.values(files.stems);
4217
5682
  if (stems.length)
4218
- lines.push(`Stems: ${stems.length} files in ${(0, import_node_path.dirname)(stems[0])}`);
5683
+ lines.push(`Stems: ${stems.length} files in ${(0, import_node_path3.dirname)(stems[0])}`);
4219
5684
  lines.push(`Edit in TuttiDAW: ${meta.editUrl}`);
4220
5685
  return lines;
4221
5686
  }
5687
+ var WATERMARK_LINE = "Voice tag 'register at TuttiDAW' mixed in throughout this render (renders over 30 s without sign-in); the stats include it. Remove it: tutticode login (free)";
4222
5688
 
4223
5689
  // src/server.ts
4224
- var import_node_crypto = require("node:crypto");
5690
+ var import_node_crypto2 = require("node:crypto");
4225
5691
  var import_node_http = require("node:http");
4226
5692
 
4227
5693
  // ../../node_modules/.pnpm/ws@8.21.3/node_modules/ws/wrapper.mjs
4228
5694
  var import_stream = __toESM(require_stream(), 1);
4229
5695
  var import_extension = __toESM(require_extension(), 1);
4230
5696
  var import_permessage_deflate = __toESM(require_permessage_deflate(), 1);
4231
- var import_receiver = __toESM(require_receiver(), 1);
4232
- var import_sender = __toESM(require_sender(), 1);
4233
- var import_subprotocol = __toESM(require_subprotocol(), 1);
4234
- var import_websocket = __toESM(require_websocket(), 1);
4235
- var import_websocket_server = __toESM(require_websocket_server(), 1);
4236
- var wrapper_default = import_websocket.default;
4237
-
4238
- // src/env.ts
4239
- var import_node_fs2 = require("node:fs");
4240
- var import_node_os = require("node:os");
4241
- var import_node_path2 = require("node:path");
4242
- var VERSION = true ? "0.1.0" : readPackageVersion();
4243
- function envBrowserArgs() {
4244
- const args = process.env.TUTTICODE_BROWSER_ARGS?.split(" ").filter(Boolean);
4245
- return args?.length ? args : void 0;
4246
- }
4247
- function stateDir() {
4248
- const dir = process.env.TUTTICODE_HOME || (0, import_node_path2.join)((0, import_node_os.homedir)(), ".tutticode");
4249
- (0, import_node_fs2.mkdirSync)(dir, { recursive: true });
4250
- return dir;
4251
- }
4252
- var assetsDir;
4253
- function skillsDir() {
4254
- return process.env.TUTTICODE_SKILLS_DIR || assetsDir || // Running from the sources (tests): the app's public skills.
4255
- (0, import_node_path2.join)(__dirname, "..", "..", "..", "public", "skills");
4256
- }
4257
- function setSkillsDir(dir) {
4258
- assetsDir = dir;
4259
- }
4260
- var LOG_ROLL_BYTES = 2 * 1024 * 1024;
4261
- function createLogger(name, verbose) {
4262
- let file;
4263
- try {
4264
- const dir = (0, import_node_path2.join)(stateDir(), "logs");
4265
- (0, import_node_fs2.mkdirSync)(dir, { recursive: true });
4266
- file = (0, import_node_path2.join)(dir, `${name}.log`);
4267
- if (((0, import_node_fs2.statSync)(file, { throwIfNoEntry: false })?.size ?? 0) > LOG_ROLL_BYTES)
4268
- (0, import_node_fs2.renameSync)(file, `${file}.1`);
4269
- } catch {
4270
- file = void 0;
4271
- }
4272
- return (level2, message) => {
4273
- const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${level2} [${process.pid}] ${message}`;
4274
- if (verbose || level2 === "error")
4275
- process.stderr.write(`tutticode: ${message}
4276
- `);
4277
- if (!file)
4278
- return;
4279
- try {
4280
- (0, import_node_fs2.appendFileSync)(file, `${line}
4281
- `);
4282
- } catch {
4283
- }
4284
- };
4285
- }
5697
+ var import_receiver = __toESM(require_receiver(), 1);
5698
+ var import_sender = __toESM(require_sender(), 1);
5699
+ var import_subprotocol = __toESM(require_subprotocol(), 1);
5700
+ var import_websocket = __toESM(require_websocket(), 1);
5701
+ var import_websocket_server = __toESM(require_websocket_server(), 1);
5702
+ var wrapper_default = import_websocket.default;
4286
5703
 
4287
5704
  // src/server.ts
4288
5705
  var CONTROL_PATH = "/control";
4289
5706
  function newToken() {
4290
- return (0, import_node_crypto.randomBytes)(16).toString("base64url");
5707
+ return (0, import_node_crypto2.randomBytes)(16).toString("base64url");
4291
5708
  }
4292
5709
  function tokensEqual(expected, presented) {
4293
5710
  if (typeof presented !== "string")
4294
5711
  return false;
4295
5712
  const a = Buffer.from(expected);
4296
5713
  const b = Buffer.from(presented);
4297
- return a.length === b.length && (0, import_node_crypto.timingSafeEqual)(a, b);
5714
+ return a.length === b.length && (0, import_node_crypto2.timingSafeEqual)(a, b);
4298
5715
  }
4299
5716
  function normalizeOrigin(value) {
4300
5717
  try {
@@ -4386,7 +5803,7 @@ var BridgeServer = class {
4386
5803
  return res.end(
4387
5804
  JSON.stringify({
4388
5805
  name: "tutticode",
4389
- version: VERSION,
5806
+ version: VERSION2,
4390
5807
  protocol: BRIDGE_PROTOCOL
4391
5808
  })
4392
5809
  );
@@ -4450,6 +5867,21 @@ function pathOf(req) {
4450
5867
  // src/args.ts
4451
5868
  var UsageError = class extends Error {
4452
5869
  };
5870
+ var accountOptions = {
5871
+ origin: { type: "string" },
5872
+ locale: { type: "string" },
5873
+ verbose: { type: "boolean", short: "v" }
5874
+ };
5875
+ function accountFlags(values) {
5876
+ const origin = values.origin;
5877
+ if (origin !== void 0 && !normalizeOrigin(origin))
5878
+ throw new UsageError(`--origin must be an http(s) origin (got ${origin}).`);
5879
+ return {
5880
+ origin,
5881
+ locale: values.locale,
5882
+ verbose: !!values.verbose
5883
+ };
5884
+ }
4453
5885
  var bridgeOptions = {
4454
5886
  origin: { type: "string" },
4455
5887
  "dev-origin": { type: "string", multiple: true },
@@ -4576,7 +6008,8 @@ function parseCli(argv) {
4576
6008
  case "doctor": {
4577
6009
  const { values, positionals } = parse(rest, {
4578
6010
  ...bridgeOptions,
4579
- json: { type: "boolean" }
6011
+ json: { type: "boolean" },
6012
+ fix: { type: "boolean" }
4580
6013
  });
4581
6014
  if (values.help)
4582
6015
  return { name: "help", topic: "doctor" };
@@ -4585,9 +6018,56 @@ function parseCli(argv) {
4585
6018
  return {
4586
6019
  name: "doctor",
4587
6020
  json: !!values.json,
6021
+ fix: !!values.fix,
4588
6022
  bridge: bridgeFlags(values)
4589
6023
  };
4590
6024
  }
6025
+ case "login": {
6026
+ const { values, positionals } = parse(rest, {
6027
+ ...accountOptions,
6028
+ device: { type: "boolean" },
6029
+ "no-wait": { type: "boolean" },
6030
+ json: { type: "boolean" }
6031
+ });
6032
+ if (values.help)
6033
+ return { name: "help", topic: "login" };
6034
+ if (positionals.length)
6035
+ throw new UsageError("login takes no arguments.");
6036
+ const noWait = !!values["no-wait"];
6037
+ if (values.json && !noWait)
6038
+ throw new UsageError("--json goes with --device --no-wait.");
6039
+ return {
6040
+ name: "login",
6041
+ // Only the device grant can finish without this process.
6042
+ device: !!values.device || noWait,
6043
+ noWait,
6044
+ json: !!values.json,
6045
+ account: accountFlags(values)
6046
+ };
6047
+ }
6048
+ case "logout": {
6049
+ const { values, positionals } = parse(rest, accountOptions);
6050
+ if (values.help)
6051
+ return { name: "help", topic: "logout" };
6052
+ if (positionals.length)
6053
+ throw new UsageError("logout takes no arguments.");
6054
+ return { name: "logout", account: accountFlags(values) };
6055
+ }
6056
+ case "whoami": {
6057
+ const { values, positionals } = parse(rest, {
6058
+ ...accountOptions,
6059
+ json: { type: "boolean" }
6060
+ });
6061
+ if (values.help)
6062
+ return { name: "help", topic: "whoami" };
6063
+ if (positionals.length)
6064
+ throw new UsageError("whoami takes no arguments.");
6065
+ return {
6066
+ name: "whoami",
6067
+ json: !!values.json,
6068
+ account: accountFlags(values)
6069
+ };
6070
+ }
4591
6071
  case "setup": {
4592
6072
  const { values, positionals } = parse(rest, {
4593
6073
  yes: { type: "boolean", short: "y" },
@@ -4632,6 +6112,8 @@ function parseCli(argv) {
4632
6112
  }
4633
6113
  case "__daemon":
4634
6114
  return { name: "daemon", config: rest[0] ?? "" };
6115
+ case "__login-poll":
6116
+ return { name: "login-poll", file: rest[0] ?? "" };
4635
6117
  default:
4636
6118
  throw new UsageError(`Unknown command ${name}. Run tutticode --help.`);
4637
6119
  }
@@ -4653,12 +6135,14 @@ function parseRender(rest) {
4653
6135
  tail: { type: "string" },
4654
6136
  lufs: { type: "string" },
4655
6137
  "true-peak": { type: "string" },
6138
+ limit: { type: "boolean" },
4656
6139
  "no-analyze": { type: "boolean" },
6140
+ "no-brand-tag": { type: "boolean" },
4657
6141
  json: { type: "boolean" }
4658
6142
  });
4659
6143
  if (values.help)
4660
6144
  return { name: "help", topic: "render" };
4661
- const file = oneFile(positionals, "render");
6145
+ const file2 = oneFile(positionals, "render");
4662
6146
  const out = values.out;
4663
6147
  if (!out)
4664
6148
  throw new UsageError(
@@ -4707,11 +6191,13 @@ function parseRender(rest) {
4707
6191
  ...optional2("tail", number(values, "tail", 0, 60)),
4708
6192
  ...optional2("lufs", number(values, "lufs", -40, -5)),
4709
6193
  ...optional2("truePeak", number(values, "true-peak", -12, 0)),
4710
- ...values["no-analyze"] ? { analyze: false } : {}
6194
+ ...values.limit ? { limit: true } : {},
6195
+ ...values["no-analyze"] ? { analyze: false } : {},
6196
+ ...values["no-brand-tag"] ? { brandTag: false } : {}
4711
6197
  };
4712
6198
  return {
4713
6199
  name: "render",
4714
- file,
6200
+ file: file2,
4715
6201
  out,
4716
6202
  stemsDir: values.stems,
4717
6203
  json: !!values.json,
@@ -4731,10 +6217,19 @@ Usage:
4731
6217
  tutticode open <file|-> open the program in TuttiCode
4732
6218
  tutticode mcp MCP stdio server (code_check, code_render, ...)
4733
6219
  tutticode doctor is everything in place?
6220
+ tutticode login sign in (free): longer renders, no voice tag
6221
+ tutticode logout sign out on this computer
6222
+ tutticode whoami who renders, with which limits
4734
6223
  tutticode setup optional browser policy + skills
4735
6224
  tutticode skills install install the agent skills
4736
6225
 
4737
- Exit codes: 0 ok, 1 errors in the code, 2 bridge/browser problem, 3 usage.
6226
+ Once TuttiDAW switches its limits on (being rolled out), renders without
6227
+ sign-in are limited to 60 s, and renders over 30 s carry a spoken "register
6228
+ at TuttiDAW" tag. TUTTICODE_TOKEN (an access token from the
6229
+ account page) signs in CI.
6230
+
6231
+ Exit codes: 0 ok, 1 errors in the code, 2 bridge/browser problem, 3 usage,
6232
+ 4 not signed in (whoami), 5 over a limit without sign-in, 6 upgrade tutticode.
4738
6233
  Run tutticode <command> --help for its flags.`,
4739
6234
  check: `tutticode check <file|-> [--bpm N] [--json] [bridge flags]
4740
6235
 
@@ -4751,21 +6246,52 @@ program has errors.`,
4751
6246
  --stems <dir> also write one file per voice into <dir>
4752
6247
  --fade-in S --fade-out S --tail S seconds
4753
6248
  --lufs N --true-peak N static gain to a loudness; fails (clip)
4754
- rather than limiting
6249
+ when the true peak would pass the
6250
+ ceiling (default -1 dBTP). Without
6251
+ --lufs a render over the ceiling is
6252
+ lowered (one static gain, reported)
6253
+ --limit true-peak limiter instead of failing
6254
+ (clip) or lowering; reported
4755
6255
  --no-analyze skip the per-voice analysis
6256
+ --no-brand-tag no "Made with TuttiCode" file metadata
6257
+ (signed in only; TUTTICODE_BRAND_TAG=0)
4756
6258
  --json print the RenderMeta + written files
4757
6259
  --timeout S give up after S seconds (default 600)`,
4758
6260
  open: `tutticode open <file|-> [bridge flags]
4759
6261
 
4760
- Opens the program in a visible TuttiCode tab (tuttidaw.com/code).`,
6262
+ Opens the program in a visible TuttiCode tab (tuttidaw.com/code). With
6263
+ --headless it opens nothing and prints the link.`,
4761
6264
  mcp: `tutticode mcp [bridge flags]
4762
6265
 
4763
6266
  MCP stdio server: code_check, code_render, code_compare, code_open,
4764
- code_reference; resources = the TuttiCode skill references.
6267
+ code_reference, code_account; resources = the TuttiCode skill references.
6268
+ Signs the page in like the CLI (tutticode login, or TUTTICODE_TOKEN).
4765
6269
  claude mcp add tutticode -- tutticode mcp`,
4766
- doctor: `tutticode doctor [--json] [bridge flags]
6270
+ doctor: `tutticode doctor [--json] [--fix] [bridge flags]
4767
6271
 
4768
- Checks the browser, the page and the connection, and prints the engine version.`,
6272
+ Checks the browser, the page and the connection, prints the engine version
6273
+ and who renders (signed in / anonymous, the credentials file's permissions).
6274
+ --fix makes the credentials file private again.`,
6275
+ login: `tutticode login [--device [--no-wait] [--json]] [--origin <url>] [--locale <l>]
6276
+
6277
+ Signs in with a free TuttiDAW account: renders up to 5 minutes, no voice tag.
6278
+ Opens your browser; approve there and come back. The sign-in can only render
6279
+ with TuttiCode (it cannot open your projects, account or billing) and lasts a
6280
+ year.
6281
+ --device no browser here (SSH, a server): prints a code to approve at
6282
+ tuttidaw.com/code/activate on any device
6283
+ --no-wait with --device: print the code and return at once; tutticode
6284
+ finishes the sign-in in the background once it is approved.
6285
+ For agents: tutticode login --device --no-wait --json
6286
+ Needs a terminal unless --no-wait.`,
6287
+ logout: `tutticode logout [--origin <url>]
6288
+
6289
+ Signs this computer out (revokes its sign-in) and deletes the stored
6290
+ credentials. Other devices and access tokens: the account page.`,
6291
+ whoami: `tutticode whoami [--json] [--origin <url>]
6292
+
6293
+ Who renders: the masked email, plan, limits and today's usage. Exit 0 signed
6294
+ in, 4 not signed in, 2 when the sign-in was rejected.`,
4769
6295
  setup: `tutticode setup [--yes] [--undo] [--no-skills]
4770
6296
 
4771
6297
  Optionally lets tuttidaw.com reach tutticode in your everyday Chrome / Edge
@@ -4787,10 +6313,10 @@ agent found under your home directory).`,
4787
6313
  };
4788
6314
 
4789
6315
  // src/browser.ts
4790
- var import_node_child_process = require("node:child_process");
4791
- var import_node_fs3 = require("node:fs");
4792
- var import_node_os2 = require("node:os");
4793
- var import_node_path3 = require("node:path");
6316
+ var import_node_child_process2 = require("node:child_process");
6317
+ var import_node_fs4 = require("node:fs");
6318
+ var import_node_os3 = require("node:os");
6319
+ var import_node_path4 = require("node:path");
4794
6320
 
4795
6321
  // src/cdp.ts
4796
6322
  var import_node_events = require("node:events");
@@ -4895,8 +6421,8 @@ var CdpClient = class extends import_node_events.EventEmitter {
4895
6421
  var hostPlatform = () => ({
4896
6422
  platform: process.platform,
4897
6423
  env: process.env,
4898
- home: (0, import_node_os2.homedir)(),
4899
- exists: import_node_fs3.existsSync
6424
+ home: (0, import_node_os3.homedir)(),
6425
+ exists: import_node_fs4.existsSync
4900
6426
  });
4901
6427
  function browserCandidates(p = hostPlatform()) {
4902
6428
  if (p.platform === "win32") {
@@ -4908,20 +6434,20 @@ function browserCandidates(p = hostPlatform()) {
4908
6434
  return [
4909
6435
  ...roots.map((r2) => ({
4910
6436
  kind: "chrome",
4911
- path: (0, import_node_path3.join)(r2, "Google", "Chrome", "Application", "chrome.exe")
6437
+ path: (0, import_node_path4.join)(r2, "Google", "Chrome", "Application", "chrome.exe")
4912
6438
  })),
4913
6439
  ...roots.map((r2) => ({
4914
6440
  kind: "edge",
4915
- path: (0, import_node_path3.join)(r2, "Microsoft", "Edge", "Application", "msedge.exe")
6441
+ path: (0, import_node_path4.join)(r2, "Microsoft", "Edge", "Application", "msedge.exe")
4916
6442
  }))
4917
6443
  ];
4918
6444
  }
4919
6445
  if (p.platform === "darwin") {
4920
- const apps = ["/Applications", (0, import_node_path3.join)(p.home, "Applications")];
6446
+ const apps = ["/Applications", (0, import_node_path4.join)(p.home, "Applications")];
4921
6447
  return [
4922
6448
  ...apps.map((a) => ({
4923
6449
  kind: "chrome",
4924
- path: (0, import_node_path3.join)(
6450
+ path: (0, import_node_path4.join)(
4925
6451
  a,
4926
6452
  "Google Chrome.app",
4927
6453
  "Contents",
@@ -4931,7 +6457,7 @@ function browserCandidates(p = hostPlatform()) {
4931
6457
  })),
4932
6458
  ...apps.map((a) => ({
4933
6459
  kind: "edge",
4934
- path: (0, import_node_path3.join)(
6460
+ path: (0, import_node_path4.join)(
4935
6461
  a,
4936
6462
  "Microsoft Edge.app",
4937
6463
  "Contents",
@@ -4941,7 +6467,7 @@ function browserCandidates(p = hostPlatform()) {
4941
6467
  })),
4942
6468
  ...apps.map((a) => ({
4943
6469
  kind: "chromium",
4944
- path: (0, import_node_path3.join)(a, "Chromium.app", "Contents", "MacOS", "Chromium")
6470
+ path: (0, import_node_path4.join)(a, "Chromium.app", "Contents", "MacOS", "Chromium")
4945
6471
  }))
4946
6472
  ];
4947
6473
  }
@@ -4955,7 +6481,7 @@ function browserCandidates(p = hostPlatform()) {
4955
6481
  ["chromium", "chromium-browser"]
4956
6482
  ];
4957
6483
  return names.flatMap(
4958
- ([kind, name]) => dirs.map((d) => ({ kind, path: (0, import_node_path3.join)(d, name) }))
6484
+ ([kind, name]) => dirs.map((d) => ({ kind, path: (0, import_node_path4.join)(d, name) }))
4959
6485
  );
4960
6486
  }
4961
6487
  function findBrowser(wanted, p = hostPlatform()) {
@@ -4974,7 +6500,7 @@ function findBrowser(wanted, p = hostPlatform()) {
4974
6500
  }
4975
6501
  function openExternal(url) {
4976
6502
  const [command, args] = process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : process.platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
4977
- const child = (0, import_node_child_process.spawn)(command, args, {
6503
+ const child = (0, import_node_child_process2.spawn)(command, args, {
4978
6504
  detached: true,
4979
6505
  stdio: "ignore",
4980
6506
  windowsHide: true
@@ -5076,7 +6602,7 @@ var HeadlessBrowser = class {
5076
6602
  }
5077
6603
  }
5078
6604
  if (this.tempProfile) {
5079
- (0, import_node_fs3.rmSync)(this.tempProfile, { recursive: true, force: true });
6605
+ (0, import_node_fs4.rmSync)(this.tempProfile, { recursive: true, force: true });
5080
6606
  this.tempProfile = void 0;
5081
6607
  }
5082
6608
  }
@@ -5084,8 +6610,8 @@ var HeadlessBrowser = class {
5084
6610
  await this.close();
5085
6611
  this.consoleLines = [];
5086
6612
  const { browser, log } = this.options;
5087
- const shared = this.options.profileDir ?? (0, import_node_path3.join)(stateDir(), "browser-profile");
5088
- (0, import_node_fs3.mkdirSync)(shared, { recursive: true });
6613
+ const shared = this.options.profileDir ?? (0, import_node_path4.join)(stateDir(), "browser-profile");
6614
+ (0, import_node_fs4.mkdirSync)(shared, { recursive: true });
5089
6615
  try {
5090
6616
  this.cdp = await this.spawnBrowser(shared);
5091
6617
  } catch (e) {
@@ -5093,7 +6619,7 @@ var HeadlessBrowser = class {
5093
6619
  "warn",
5094
6620
  `shared browser profile unavailable (${e.message}); using a temporary one`
5095
6621
  );
5096
- this.tempProfile = (0, import_node_fs3.mkdtempSync)((0, import_node_path3.join)((0, import_node_os2.tmpdir)(), "tutticode-profile-"));
6622
+ this.tempProfile = (0, import_node_fs4.mkdtempSync)((0, import_node_path4.join)((0, import_node_os3.tmpdir)(), "tutticode-profile-"));
5097
6623
  this.cdp = await this.spawnBrowser(this.tempProfile);
5098
6624
  }
5099
6625
  this.cdp.on("event", (event) => this.onEvent(event));
@@ -5129,7 +6655,7 @@ var HeadlessBrowser = class {
5129
6655
  "about:blank"
5130
6656
  ];
5131
6657
  return new Promise((resolve3, reject) => {
5132
- const child = (0, import_node_child_process.spawn)(browser.path, args, {
6658
+ const child = (0, import_node_child_process2.spawn)(browser.path, args, {
5133
6659
  // fd 3: commands to the browser, fd 4: its replies and events.
5134
6660
  stdio: ["ignore", "ignore", "pipe", "pipe", "pipe"],
5135
6661
  windowsHide: true
@@ -5246,7 +6772,7 @@ function remoteDebuggingFree(args) {
5246
6772
  var delay = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
5247
6773
  function killTree(child) {
5248
6774
  if (process.platform === "win32" && child.pid)
5249
- (0, import_node_child_process.spawnSync)("taskkill", ["/T", "/F", "/PID", String(child.pid)], {
6775
+ (0, import_node_child_process2.spawnSync)("taskkill", ["/T", "/F", "/PID", String(child.pid)], {
5250
6776
  windowsHide: true
5251
6777
  });
5252
6778
  else
@@ -5254,18 +6780,19 @@ function killTree(child) {
5254
6780
  }
5255
6781
 
5256
6782
  // src/daemon.ts
5257
- var import_node_child_process2 = require("node:child_process");
5258
- var import_node_crypto3 = require("node:crypto");
5259
- var import_node_fs4 = require("node:fs");
5260
- var import_node_path4 = require("node:path");
6783
+ var import_node_child_process3 = require("node:child_process");
6784
+ var import_node_crypto4 = require("node:crypto");
6785
+ var import_node_fs5 = require("node:fs");
6786
+ var import_node_path5 = require("node:path");
5261
6787
 
5262
6788
  // src/session.ts
5263
- var import_node_crypto2 = require("node:crypto");
6789
+ var import_node_crypto3 = require("node:crypto");
5264
6790
  var import_node_events3 = require("node:events");
5265
6791
 
5266
6792
  // src/page-link.ts
5267
6793
  var import_node_events2 = require("node:events");
5268
6794
  var HELLO_TIMEOUT_MS = 15e3;
6795
+ var AUTH_TIMEOUT_MS = 1e4;
5269
6796
  var CANCEL_GRACE_MS = 5e3;
5270
6797
  function maxPartBytes(meta) {
5271
6798
  return Math.ceil((meta.seconds + 1) * meta.sampleRate) * 2 * 4 + 1024 * 1024;
@@ -5319,6 +6846,16 @@ var PageLink = class extends import_node_events2.EventEmitter {
5319
6846
  drain;
5320
6847
  closedReason;
5321
6848
  readyPromise;
6849
+ /** The token ('' = anonymous) of the last `auth` the page took. */
6850
+ authKey;
6851
+ authStatus;
6852
+ /** The page refused `auth` itself: stop sending it. */
6853
+ authUnsupported = false;
6854
+ /** The page's last `error auth` message. */
6855
+ authError;
6856
+ /** Tokens the page refused -> its message. */
6857
+ refused = /* @__PURE__ */ new Map();
6858
+ authWait;
5322
6859
  get open() {
5323
6860
  return this.closedReason === void 0;
5324
6861
  }
@@ -5330,6 +6867,73 @@ var PageLink = class extends import_node_events2.EventEmitter {
5330
6867
  get busy() {
5331
6868
  return !!this.current || !!this.drain;
5332
6869
  }
6870
+ /**
6871
+ * Sends `auth` unless this socket already carries the same token, and
6872
+ * waits for the page's verdict. A token the page refuses is dropped: the
6873
+ * socket signs in anonymously and the status says why. Never rejects.
6874
+ */
6875
+ async authenticate(auth, timeoutMs = AUTH_TIMEOUT_MS) {
6876
+ if (this.authUnsupported)
6877
+ return { signedIn: false, unsupported: true };
6878
+ const refused = auth.accessToken && this.refused.get(auth.accessToken);
6879
+ if (refused) {
6880
+ const { accessToken: _dropped2, ...anonymous2 } = auth;
6881
+ return {
6882
+ ...await this.authenticate(anonymous2, timeoutMs),
6883
+ rejected: refused
6884
+ };
6885
+ }
6886
+ const key = auth.accessToken ?? "";
6887
+ if (this.authKey === key && this.authStatus)
6888
+ return this.authStatus;
6889
+ this.authError = void 0;
6890
+ const answer = await this.sendAuth(auth, timeoutMs);
6891
+ if (answer !== "hello") {
6892
+ if (answer !== "closed") {
6893
+ this.authUnsupported = true;
6894
+ this.log(
6895
+ "warn",
6896
+ answer === "protocol" ? "the page does not know auth: rendering anonymously" : "the page did not answer auth: rendering anonymously"
6897
+ );
6898
+ }
6899
+ return { signedIn: false, unsupported: true };
6900
+ }
6901
+ const signedIn = !!this.hello?.signedIn;
6902
+ if (!auth.accessToken || signedIn) {
6903
+ this.authKey = key;
6904
+ return this.authStatus = { signedIn };
6905
+ }
6906
+ const rejected = this.authError ?? "The page did not accept the sign-in.";
6907
+ this.log("warn", `page refused the sign-in: ${rejected}`);
6908
+ this.refused.set(auth.accessToken, rejected);
6909
+ const { accessToken: _dropped, ...anonymous } = auth;
6910
+ const again = await this.sendAuth(anonymous, timeoutMs);
6911
+ if (again === "hello") {
6912
+ this.authKey = "";
6913
+ this.authStatus = { signedIn: !!this.hello?.signedIn };
6914
+ }
6915
+ return { signedIn: again === "hello" && !!this.hello?.signedIn, rejected };
6916
+ }
6917
+ sendAuth(auth, timeoutMs) {
6918
+ if (!this.open)
6919
+ return Promise.resolve("closed");
6920
+ return new Promise((resolve3) => {
6921
+ const done = (answer) => {
6922
+ clearTimeout(timer);
6923
+ this.off("close", onClose);
6924
+ this.authWait = void 0;
6925
+ resolve3(answer);
6926
+ };
6927
+ const onClose = () => done("closed");
6928
+ const timer = setTimeout(() => done("timeout"), timeoutMs);
6929
+ this.once("close", onClose);
6930
+ this.authWait = (answer) => {
6931
+ if (answer !== "auth")
6932
+ done(answer);
6933
+ };
6934
+ this.send({ type: "auth", ...auth });
6935
+ });
6936
+ }
5333
6937
  /** Runs one command. Never rejects: failures are an `error` outcome. */
5334
6938
  async run(command, options) {
5335
6939
  if (!this.open)
@@ -5445,8 +7049,13 @@ var PageLink = class extends import_node_events2.EventEmitter {
5445
7049
  }
5446
7050
  onMessage(message) {
5447
7051
  if (message.type === "hello") {
5448
- if (this.hello)
7052
+ if (this.hello) {
7053
+ if (message.protocol === BRIDGE_PROTOCOL) {
7054
+ this.hello = message;
7055
+ this.authWait?.("hello");
7056
+ }
5449
7057
  return;
7058
+ }
5450
7059
  if (message.protocol !== BRIDGE_PROTOCOL) {
5451
7060
  const why = `The page speaks bridge protocol ${message.protocol}, this tutticode speaks ${BRIDGE_PROTOCOL}: update tutticode (npm i -g tutticode@latest).`;
5452
7061
  this.emit("bad-hello", why);
@@ -5466,6 +7075,20 @@ var PageLink = class extends import_node_events2.EventEmitter {
5466
7075
  return;
5467
7076
  }
5468
7077
  const running = this.current;
7078
+ if (message.type === "error" && id === void 0 && message.code === "auth") {
7079
+ this.log("warn", `page error auth: ${message.message}`);
7080
+ this.authError = message.message;
7081
+ if (this.authKey) {
7082
+ this.authKey = void 0;
7083
+ this.authStatus = void 0;
7084
+ }
7085
+ this.authWait?.("auth");
7086
+ return;
7087
+ }
7088
+ if (message.type === "error" && id === void 0 && this.authWait) {
7089
+ this.log("warn", `page error ${message.code}: ${message.message}`);
7090
+ return this.authWait("protocol");
7091
+ }
5469
7092
  if (message.type === "error" && id === void 0) {
5470
7093
  this.log("warn", `page error ${message.code}: ${message.message}`);
5471
7094
  return this.fail(message.code, message.message, message.report);
@@ -5501,7 +7124,8 @@ var PageLink = class extends import_node_events2.EventEmitter {
5501
7124
  kind: "error",
5502
7125
  code: message.code,
5503
7126
  message: message.message,
5504
- ...message.report ? { report: message.report } : {}
7127
+ ...message.report ? { report: message.report } : {},
7128
+ ...message.limit ? { limit: message.limit } : {}
5505
7129
  });
5506
7130
  case "done": {
5507
7131
  if (!running.meta)
@@ -5633,7 +7257,13 @@ var Session = class extends import_node_events3.EventEmitter {
5633
7257
  const job = this.queue.then(async () => {
5634
7258
  try {
5635
7259
  const link = await this.ensurePage(options.signal);
5636
- return await link.run(command, options);
7260
+ if (!options.auth)
7261
+ return await link.run(command, options);
7262
+ const auth = await link.authenticate(
7263
+ options.auth,
7264
+ this.options.authTimeoutMs
7265
+ );
7266
+ return { ...await link.run(command, options), auth };
5637
7267
  } catch (e) {
5638
7268
  if (e instanceof BridgeError)
5639
7269
  return { kind: "error", code: e.code, message: e.message };
@@ -5781,14 +7411,14 @@ Page console:
5781
7411
  // src/daemon.ts
5782
7412
  function daemonKey(config) {
5783
7413
  const { verbose: _verbose, ...identity } = config;
5784
- return (0, import_node_crypto3.createHash)("sha256").update(
7414
+ return (0, import_node_crypto4.createHash)("sha256").update(
5785
7415
  JSON.stringify({
5786
7416
  ...identity,
5787
7417
  devOrigins: [...config.devOrigins ?? []].sort()
5788
7418
  })
5789
7419
  ).digest("hex").slice(0, 12);
5790
7420
  }
5791
- var daemonFile = (key) => (0, import_node_path4.join)(stateDir(), `daemon-${key}.json`);
7421
+ var daemonFile = (key) => (0, import_node_path5.join)(stateDir(), `daemon-${key}.json`);
5792
7422
  async function runDaemon(config, log) {
5793
7423
  const controlToken = newToken();
5794
7424
  let active = 0;
@@ -5816,22 +7446,22 @@ async function runDaemon(config, log) {
5816
7446
  const session = new Session(options);
5817
7447
  const port = await session.start();
5818
7448
  const key = daemonKey(config);
5819
- const file = {
7449
+ const file2 = {
5820
7450
  pid: process.pid,
5821
7451
  port,
5822
7452
  token: controlToken,
5823
- version: VERSION
7453
+ version: VERSION2
5824
7454
  };
5825
- (0, import_node_fs4.writeFileSync)(daemonFile(key), JSON.stringify(file), { mode: 384 });
7455
+ (0, import_node_fs5.writeFileSync)(daemonFile(key), JSON.stringify(file2), { mode: 384 });
5826
7456
  log("info", `daemon ${key} listening on 127.0.0.1:${port} (${config.mode})`);
5827
7457
  const shutdown = async (why) => {
5828
7458
  log("info", `daemon exiting: ${why}`);
5829
7459
  try {
5830
7460
  const current = JSON.parse(
5831
- (0, import_node_fs4.readFileSync)(daemonFile(key), "utf8")
7461
+ (0, import_node_fs5.readFileSync)(daemonFile(key), "utf8")
5832
7462
  );
5833
7463
  if (current.pid === process.pid)
5834
- (0, import_node_fs4.rmSync)(daemonFile(key), { force: true });
7464
+ (0, import_node_fs5.rmSync)(daemonFile(key), { force: true });
5835
7465
  } catch {
5836
7466
  }
5837
7467
  await session.close();
@@ -5852,14 +7482,14 @@ function serveControl(socket, session, setStatusSink) {
5852
7482
  socket.send(JSON.stringify(value));
5853
7483
  };
5854
7484
  socket.once("message", async (data) => {
5855
- let request;
7485
+ let request2;
5856
7486
  try {
5857
- request = JSON.parse(String(data));
7487
+ request2 = JSON.parse(String(data));
5858
7488
  } catch {
5859
7489
  return socket.close(1008, "bad request");
5860
7490
  }
5861
7491
  setStatusSink((message) => send({ type: "status", message }));
5862
- if (request.type === "info") {
7492
+ if (request2.type === "info") {
5863
7493
  try {
5864
7494
  await session.ensurePage(abort.signal);
5865
7495
  send({
@@ -5882,28 +7512,33 @@ function serveControl(socket, session, setStatusSink) {
5882
7512
  }
5883
7513
  return socket.close(1e3);
5884
7514
  }
5885
- if (request.type !== "run" || !request.command)
7515
+ if (request2.type !== "run" || !request2.command)
5886
7516
  return socket.close(1008, "bad request");
5887
- const outcome = await session.run(request.command, {
5888
- timeoutMs: request.timeoutMs ?? 6e5,
7517
+ const outcome = await session.run(request2.command, {
7518
+ timeoutMs: request2.timeoutMs ?? 6e5,
5889
7519
  signal: abort.signal,
5890
- onProgress: (progress) => send(progress)
7520
+ onProgress: (progress) => send(progress),
7521
+ ...request2.auth ? { auth: request2.auth } : {}
5891
7522
  });
5892
7523
  if (outcome.kind === "rendered") {
5893
- const id = request.command.type === "open" ? "open" : request.command.id;
7524
+ const id = request2.command.type === "open" ? "open" : request2.command.id;
5894
7525
  for (const [part, bytes] of outcome.parts)
5895
7526
  for (const frame of frameParts(id, part, bytes))
5896
7527
  socket.send(frame);
5897
7528
  send({
5898
7529
  type: "result",
5899
- outcome: { kind: "rendered", meta: outcome.meta }
7530
+ outcome: {
7531
+ kind: "rendered",
7532
+ meta: outcome.meta,
7533
+ ...outcome.auth ? { auth: outcome.auth } : {}
7534
+ }
5900
7535
  });
5901
7536
  } else
5902
7537
  send({ type: "result", outcome });
5903
7538
  socket.close(1e3);
5904
7539
  });
5905
7540
  }
5906
- async function runViaDaemon(options, command, timeoutMs, callbacks = {}) {
7541
+ async function runViaDaemon(options, command, timeoutMs, callbacks = {}, auth) {
5907
7542
  let socket;
5908
7543
  try {
5909
7544
  socket = await connectDaemon(options);
@@ -5912,7 +7547,7 @@ async function runViaDaemon(options, command, timeoutMs, callbacks = {}) {
5912
7547
  }
5913
7548
  return controlCall(
5914
7549
  socket,
5915
- { type: "run", command, timeoutMs },
7550
+ { type: "run", command, timeoutMs, ...auth ? { auth } : {} },
5916
7551
  callbacks
5917
7552
  );
5918
7553
  }
@@ -5925,7 +7560,7 @@ async function daemonInfo(options, callbacks = {}) {
5925
7560
  }
5926
7561
  return controlCall(socket, { type: "info" }, callbacks);
5927
7562
  }
5928
- function controlCall(socket, request, callbacks) {
7563
+ function controlCall(socket, request2, callbacks) {
5929
7564
  return new Promise((resolve3) => {
5930
7565
  const assembler = new PartAssembler();
5931
7566
  const parts = /* @__PURE__ */ new Map();
@@ -5984,7 +7619,7 @@ function controlCall(socket, request, callbacks) {
5984
7619
  })
5985
7620
  );
5986
7621
  socket.on("error", () => void 0);
5987
- socket.send(JSON.stringify(request));
7622
+ socket.send(JSON.stringify(request2));
5988
7623
  });
5989
7624
  }
5990
7625
  function openControlSocket(port, token, timeoutMs = 3e3) {
@@ -6001,7 +7636,7 @@ function openControlSocket(port, token, timeoutMs = 3e3) {
6001
7636
  }
6002
7637
  function readDaemonFile(key) {
6003
7638
  try {
6004
- return JSON.parse((0, import_node_fs4.readFileSync)(daemonFile(key), "utf8"));
7639
+ return JSON.parse((0, import_node_fs5.readFileSync)(daemonFile(key), "utf8"));
6005
7640
  } catch {
6006
7641
  return void 0;
6007
7642
  }
@@ -6013,7 +7648,7 @@ async function connectDaemon({
6013
7648
  }) {
6014
7649
  const key = daemonKey(config);
6015
7650
  const existing = readDaemonFile(key);
6016
- if (existing && existing.version === VERSION) {
7651
+ if (existing && existing.version === VERSION2) {
6017
7652
  try {
6018
7653
  return await openControlSocket(existing.port, existing.token);
6019
7654
  } catch {
@@ -6023,11 +7658,11 @@ async function connectDaemon({
6023
7658
  );
6024
7659
  }
6025
7660
  }
6026
- const lock = (0, import_node_path4.join)(stateDir(), `daemon-${key}.lock`);
6027
- const owner = (0, import_node_crypto3.randomUUID)();
6028
- if (takeLock(lock, owner)) {
7661
+ const lock = (0, import_node_path5.join)(stateDir(), `daemon-${key}.lock`);
7662
+ const owner = (0, import_node_crypto4.randomUUID)();
7663
+ if (takeLock2(lock, owner)) {
6029
7664
  try {
6030
- const child = (0, import_node_child_process2.spawn)(
7665
+ const child = (0, import_node_child_process3.spawn)(
6031
7666
  process.execPath,
6032
7667
  [
6033
7668
  entry,
@@ -6040,19 +7675,19 @@ async function connectDaemon({
6040
7675
  log("info", `spawned daemon ${key} (pid ${child.pid})`);
6041
7676
  return await waitForDaemon(key, existing?.pid, child.pid);
6042
7677
  } finally {
6043
- (0, import_node_fs4.rmSync)(lock, { force: true });
7678
+ (0, import_node_fs5.rmSync)(lock, { force: true });
6044
7679
  }
6045
7680
  }
6046
7681
  return waitForDaemon(key, existing?.pid);
6047
7682
  }
6048
- function takeLock(lock, owner) {
7683
+ function takeLock2(lock, owner) {
6049
7684
  try {
6050
- const age = Date.now() - ((0, import_node_fs4.statSync)(lock, { throwIfNoEntry: false })?.mtimeMs ?? 0);
7685
+ const age = Date.now() - ((0, import_node_fs5.statSync)(lock, { throwIfNoEntry: false })?.mtimeMs ?? 0);
6051
7686
  if (age > 2e4)
6052
- (0, import_node_fs4.rmSync)(lock, { force: true });
6053
- const fd = (0, import_node_fs4.openSync)(lock, "wx");
6054
- (0, import_node_fs4.writeFileSync)(fd, owner);
6055
- (0, import_node_fs4.closeSync)(fd);
7687
+ (0, import_node_fs5.rmSync)(lock, { force: true });
7688
+ const fd = (0, import_node_fs5.openSync)(lock, "wx");
7689
+ (0, import_node_fs5.writeFileSync)(fd, owner);
7690
+ (0, import_node_fs5.closeSync)(fd);
6056
7691
  return true;
6057
7692
  } catch {
6058
7693
  return false;
@@ -6061,10 +7696,10 @@ function takeLock(lock, owner) {
6061
7696
  async function waitForDaemon(key, stalePid, newPid) {
6062
7697
  const deadline = Date.now() + 15e3;
6063
7698
  while (Date.now() < deadline) {
6064
- const file = readDaemonFile(key);
6065
- if (file && file.pid !== stalePid && (newPid === void 0 || file.pid === newPid) && file.version === VERSION) {
7699
+ const file2 = readDaemonFile(key);
7700
+ if (file2 && file2.pid !== stalePid && (newPid === void 0 || file2.pid === newPid) && file2.version === VERSION2) {
6066
7701
  try {
6067
- return await openControlSocket(file.port, file.token);
7702
+ return await openControlSocket(file2.port, file2.token);
6068
7703
  } catch {
6069
7704
  }
6070
7705
  }
@@ -6075,6 +7710,370 @@ async function waitForDaemon(key, stalePid, newPid) {
6075
7710
  );
6076
7711
  }
6077
7712
 
7713
+ // src/login.ts
7714
+ var import_node_child_process4 = require("node:child_process");
7715
+ var import_node_crypto5 = require("node:crypto");
7716
+ var import_node_fs6 = require("node:fs");
7717
+ var import_node_http2 = require("node:http");
7718
+ var import_node_path6 = require("node:path");
7719
+ var LoginError = class extends Error {
7720
+ constructor(reason, message) {
7721
+ super(message);
7722
+ this.reason = reason;
7723
+ }
7724
+ };
7725
+ var LOOPBACK_TIMEOUT_MS = 5 * 60 * 1e3;
7726
+ function sitePage(origin, locale, path) {
7727
+ return `${origin}${locale === "en" ? "" : `/${locale}`}${path}`;
7728
+ }
7729
+ function pkcePair() {
7730
+ const verifier = (0, import_node_crypto5.randomBytes)(32).toString("base64url");
7731
+ const challenge = (0, import_node_crypto5.createHash)("sha256").update(verifier).digest("base64url");
7732
+ return { verifier, challenge };
7733
+ }
7734
+ function loginUrl(options) {
7735
+ const url = new URL(
7736
+ sitePage(options.origin, options.locale, TUTTICODE_OAUTH_PATHS.login)
7737
+ );
7738
+ url.search = new URLSearchParams({
7739
+ client_id: TUTTICODE_CLIENT_ID,
7740
+ redirect_uri: options.redirectUri,
7741
+ code_challenge: options.challenge,
7742
+ code_challenge_method: "S256",
7743
+ state: options.state,
7744
+ scope: TUTTICODE_SCOPE,
7745
+ resource: tutticodeResourceUrl(options.origin),
7746
+ device_name: options.deviceName
7747
+ }).toString();
7748
+ return url.toString();
7749
+ }
7750
+ var page = (title, text) => `<!doctype html><meta charset="utf-8"><title>${title}</title><p>${text}</p>`;
7751
+ async function startLoopback(options) {
7752
+ let resolveCode;
7753
+ let rejectCode;
7754
+ const code = new Promise((resolve3, reject) => {
7755
+ resolveCode = resolve3;
7756
+ rejectCode = reject;
7757
+ });
7758
+ code.catch(() => void 0);
7759
+ let settled = false;
7760
+ const finish = (fn) => {
7761
+ if (settled)
7762
+ return;
7763
+ settled = true;
7764
+ clearTimeout(timer);
7765
+ options.signal?.removeEventListener("abort", onAbort);
7766
+ fn();
7767
+ setImmediate(() => server.close());
7768
+ };
7769
+ const server = (0, import_node_http2.createServer)((req, res) => {
7770
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
7771
+ if (req.method !== "GET" || url.pathname !== "/callback") {
7772
+ res.writeHead(404).end();
7773
+ return;
7774
+ }
7775
+ const params = url.searchParams;
7776
+ if (settled || params.get("state") !== options.state) {
7777
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" }).end(
7778
+ page(
7779
+ "TuttiCode",
7780
+ "This sign-in link does not belong to the running tutticode login. Run tutticode login again."
7781
+ )
7782
+ );
7783
+ return;
7784
+ }
7785
+ const error = params.get("error");
7786
+ if (error) {
7787
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }).end(page("TuttiCode", "Sign-in cancelled. You can close this tab."));
7788
+ finish(
7789
+ () => rejectCode(
7790
+ error === "access_denied" ? new LoginError(
7791
+ "denied",
7792
+ "The sign-in was cancelled in the browser."
7793
+ ) : new LoginError("server", `The sign-in failed (${error}).`)
7794
+ )
7795
+ );
7796
+ return;
7797
+ }
7798
+ if (normalizeOrigin(params.get("iss") ?? "") !== options.issuer) {
7799
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" }).end(page("TuttiCode", "Sign-in refused: unexpected issuer."));
7800
+ finish(
7801
+ () => rejectCode(
7802
+ new LoginError(
7803
+ "issuer",
7804
+ `The sign-in came back from ${params.get("iss") ?? "an unnamed issuer"}, not ${options.issuer}.`
7805
+ )
7806
+ )
7807
+ );
7808
+ return;
7809
+ }
7810
+ const value = params.get("code");
7811
+ if (!value) {
7812
+ res.writeHead(400).end();
7813
+ return;
7814
+ }
7815
+ res.writeHead(302, { Location: options.doneUrl }).end();
7816
+ finish(() => resolveCode(value));
7817
+ });
7818
+ const onAbort = () => finish(() => rejectCode(new LoginError("cancelled", "Sign-in cancelled.")));
7819
+ const timer = setTimeout(
7820
+ () => finish(
7821
+ () => rejectCode(
7822
+ new LoginError(
7823
+ "timeout",
7824
+ `No answer from the browser within ${Math.round(
7825
+ (options.timeoutMs ?? LOOPBACK_TIMEOUT_MS) / 6e4
7826
+ )} minutes.`
7827
+ )
7828
+ )
7829
+ ),
7830
+ options.timeoutMs ?? LOOPBACK_TIMEOUT_MS
7831
+ );
7832
+ options.signal?.addEventListener("abort", onAbort, { once: true });
7833
+ await new Promise((resolve3, reject) => {
7834
+ server.once("error", reject);
7835
+ server.listen(0, "127.0.0.1", resolve3);
7836
+ });
7837
+ const port = server.address().port;
7838
+ return {
7839
+ port,
7840
+ redirectUri: `http://127.0.0.1:${port}/callback`,
7841
+ code,
7842
+ close: () => finish(() => rejectCode(new LoginError("cancelled", "Closed.")))
7843
+ };
7844
+ }
7845
+ async function loopbackLogin(options) {
7846
+ const { verifier, challenge } = pkcePair();
7847
+ const state = (0, import_node_crypto5.randomBytes)(24).toString("base64url");
7848
+ const loopback = await startLoopback({
7849
+ state,
7850
+ issuer: options.origin,
7851
+ doneUrl: sitePage(options.origin, options.locale, "/code/login/done"),
7852
+ timeoutMs: options.timeoutMs,
7853
+ signal: options.signal
7854
+ });
7855
+ const url = loginUrl({
7856
+ origin: options.origin,
7857
+ locale: options.locale,
7858
+ redirectUri: loopback.redirectUri,
7859
+ challenge,
7860
+ state,
7861
+ deviceName: options.deviceName
7862
+ });
7863
+ options.onUrl?.(url);
7864
+ try {
7865
+ await options.openBrowser(url);
7866
+ } catch {
7867
+ }
7868
+ const code = await loopback.code;
7869
+ return exchange(options.origin, options, {
7870
+ grant_type: "authorization_code",
7871
+ code,
7872
+ code_verifier: verifier,
7873
+ redirect_uri: loopback.redirectUri,
7874
+ resource: tutticodeResourceUrl(options.origin)
7875
+ });
7876
+ }
7877
+ async function exchange(origin, deps, params) {
7878
+ try {
7879
+ return tokenResponse(
7880
+ await postOAuth(origin, TUTTICODE_OAUTH_PATHS.token, params, deps)
7881
+ );
7882
+ } catch (e) {
7883
+ if (e instanceof OAuthError)
7884
+ throw new LoginError("server", `The sign-in failed: ${e.message}`);
7885
+ throw e;
7886
+ }
7887
+ }
7888
+ async function startDeviceLogin(origin, deviceName2, deps = {}) {
7889
+ let body;
7890
+ try {
7891
+ body = await postOAuth(
7892
+ origin,
7893
+ TUTTICODE_OAUTH_PATHS.deviceAuthorization,
7894
+ {
7895
+ scope: TUTTICODE_SCOPE,
7896
+ resource: tutticodeResourceUrl(origin),
7897
+ device_name: deviceName2
7898
+ },
7899
+ deps
7900
+ );
7901
+ } catch (e) {
7902
+ if (e instanceof OAuthError)
7903
+ throw new LoginError(
7904
+ "server",
7905
+ e.error === "slow_down" ? "Too many sign-in attempts from this network: try again in an hour." : `The sign-in could not start: ${e.message}`
7906
+ );
7907
+ throw e;
7908
+ }
7909
+ if (typeof body.device_code !== "string" || typeof body.user_code !== "string" || typeof body.verification_uri !== "string")
7910
+ throw new LoginError("server", "The server sent no device code.");
7911
+ return {
7912
+ deviceCode: body.device_code,
7913
+ userCode: body.user_code,
7914
+ verificationUri: body.verification_uri,
7915
+ ...typeof body.verification_uri_complete === "string" ? { verificationUriComplete: body.verification_uri_complete } : {},
7916
+ expiresIn: positive(body.expires_in, 600),
7917
+ interval: positive(body.interval, 5)
7918
+ };
7919
+ }
7920
+ var positive = (v, fallback) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : fallback;
7921
+ var SLOW_DOWN_STEP_S = 5;
7922
+ async function pollDeviceToken(origin, device, options = {}) {
7923
+ const now = options.now ?? Date.now;
7924
+ const sleep = options.sleep ?? ((ms) => new Promise((r2) => setTimeout(r2, ms)));
7925
+ const deadline = now() + device.expiresIn * 1e3;
7926
+ let interval = device.interval;
7927
+ for (; ; ) {
7928
+ await sleep(interval * 1e3);
7929
+ if (options.signal?.aborted)
7930
+ throw new LoginError("cancelled", "Sign-in cancelled.");
7931
+ if (options.stillWanted && !options.stillWanted())
7932
+ throw new LoginError("cancelled", "A newer sign-in replaced this one.");
7933
+ if (now() >= deadline)
7934
+ throw new LoginError(
7935
+ "expired",
7936
+ "The code expired before it was approved: run tutticode login --device again."
7937
+ );
7938
+ try {
7939
+ return tokenResponse(
7940
+ await postOAuth(
7941
+ origin,
7942
+ TUTTICODE_OAUTH_PATHS.token,
7943
+ {
7944
+ grant_type: DEVICE_CODE_GRANT_TYPE,
7945
+ device_code: device.deviceCode
7946
+ },
7947
+ options
7948
+ )
7949
+ );
7950
+ } catch (e) {
7951
+ if (!(e instanceof OAuthError))
7952
+ continue;
7953
+ switch (e.error) {
7954
+ case "authorization_pending":
7955
+ continue;
7956
+ case "slow_down": {
7957
+ const named = Number(e.body.interval);
7958
+ interval = Number.isFinite(named) && named > interval ? named : interval + SLOW_DOWN_STEP_S;
7959
+ continue;
7960
+ }
7961
+ case "access_denied":
7962
+ throw new LoginError("denied", "The sign-in was declined.");
7963
+ case "expired_token":
7964
+ throw new LoginError(
7965
+ "expired",
7966
+ "The code expired before it was approved: run tutticode login --device again."
7967
+ );
7968
+ default:
7969
+ throw new LoginError("server", `The sign-in failed: ${e.message}`);
7970
+ }
7971
+ }
7972
+ }
7973
+ }
7974
+ async function completeLogin(origin, tokens, deviceName2, deps = {}) {
7975
+ const now = (deps.now ?? Date.now)();
7976
+ const previous = readAccount(origin);
7977
+ try {
7978
+ await saveAccount(origin, storedAccount(tokens, now, { deviceName: deviceName2 }));
7979
+ } catch (e) {
7980
+ await revokeToken(origin, tokens.refresh_token, {
7981
+ ...deps,
7982
+ timeoutMs: 5e3
7983
+ });
7984
+ throw e;
7985
+ }
7986
+ const state = await accountState(origin, deps, { envToken: false });
7987
+ if (state.state === "signed-in") {
7988
+ const me = state.me;
7989
+ const current = readAccount(origin);
7990
+ if (current?.accessToken === tokens.access_token)
7991
+ await saveAccount(origin, {
7992
+ ...current,
7993
+ userId: me.user.id,
7994
+ email: me.user.email,
7995
+ nickname: me.user.nickname,
7996
+ plan: me.plan,
7997
+ ...me.expiresAt ? { grantExpiresAt: me.expiresAt } : {},
7998
+ ...me.deviceName ? { deviceName: me.deviceName } : {}
7999
+ });
8000
+ }
8001
+ if (previous && previous.refreshToken !== tokens.refresh_token)
8002
+ await revokeToken(origin, previous.refreshToken, {
8003
+ ...deps,
8004
+ timeoutMs: 5e3
8005
+ });
8006
+ return state;
8007
+ }
8008
+ function pendingLoginPath(origin) {
8009
+ const key = (0, import_node_crypto5.createHash)("sha256").update(origin).digest("hex").slice(0, 12);
8010
+ return (0, import_node_path6.join)(stateDir(), `login-pending-${key}.json`);
8011
+ }
8012
+ function writePendingLogin(pending) {
8013
+ const path = pendingLoginPath(pending.origin);
8014
+ writePrivateFile(path, JSON.stringify(pending));
8015
+ return path;
8016
+ }
8017
+ function readPendingLogin(path) {
8018
+ try {
8019
+ const value = JSON.parse((0, import_node_fs6.readFileSync)(path, "utf8"));
8020
+ return typeof value.deviceCode === "string" && typeof value.origin === "string" ? value : void 0;
8021
+ } catch {
8022
+ return void 0;
8023
+ }
8024
+ }
8025
+ function pendingLogin(origin, now = Date.now()) {
8026
+ const pending = readPendingLogin(pendingLoginPath(origin));
8027
+ return pending && Date.parse(pending.expiresAt) > now ? pending : void 0;
8028
+ }
8029
+ function spawnLoginPoller(entry, file2) {
8030
+ const child = (0, import_node_child_process4.spawn)(process.execPath, [entry, "__login-poll", file2], {
8031
+ detached: true,
8032
+ stdio: "ignore",
8033
+ windowsHide: true
8034
+ });
8035
+ child.unref();
8036
+ }
8037
+ async function runLoginPoller(file2, log, deps = {}) {
8038
+ const pending = readPendingLogin(file2);
8039
+ if (!pending) {
8040
+ log("warn", "login poller: no pending sign-in");
8041
+ return false;
8042
+ }
8043
+ const remaining = Math.max(
8044
+ 1,
8045
+ Math.round((Date.parse(pending.expiresAt) - Date.now()) / 1e3)
8046
+ );
8047
+ try {
8048
+ const tokens = await pollDeviceToken(
8049
+ pending.origin,
8050
+ {
8051
+ deviceCode: pending.deviceCode,
8052
+ expiresIn: remaining,
8053
+ interval: pending.interval
8054
+ },
8055
+ {
8056
+ ...deps,
8057
+ stillWanted: () => readPendingLogin(file2)?.deviceCode === pending.deviceCode
8058
+ }
8059
+ );
8060
+ const state = await completeLogin(
8061
+ pending.origin,
8062
+ tokens,
8063
+ pending.deviceName,
8064
+ deps
8065
+ );
8066
+ log("info", `login poller: signed in (${state.state})`);
8067
+ return true;
8068
+ } catch (e) {
8069
+ log("info", `login poller: ${e.message}`);
8070
+ return false;
8071
+ } finally {
8072
+ if (readPendingLogin(file2)?.deviceCode === pending.deviceCode)
8073
+ (0, import_node_fs6.rmSync)(file2, { force: true });
8074
+ }
8075
+ }
8076
+
6078
8077
  // src/mcp.ts
6079
8078
  var import_node_readline = require("node:readline");
6080
8079
 
@@ -6084,13 +8083,13 @@ var JSON_RPC_INVALID_REQUEST = -32600;
6084
8083
  var JSON_RPC_METHOD_NOT_FOUND = -32601;
6085
8084
  var JSON_RPC_INVALID_PARAMS = -32602;
6086
8085
  var JSON_RPC_INTERNAL_ERROR = -32603;
6087
- var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
8086
+ var isObject3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
6088
8087
  var isId2 = (v) => typeof v === "string" || typeof v === "number" && Number.isFinite(v);
6089
8088
  function classifyMessage(raw) {
6090
- if (!isObject2(raw) || raw.jsonrpc !== "2.0") {
8089
+ if (!isObject3(raw) || raw.jsonrpc !== "2.0") {
6091
8090
  return {
6092
8091
  kind: "invalid",
6093
- id: isObject2(raw) && isId2(raw.id) ? raw.id : null,
8092
+ id: isObject3(raw) && isId2(raw.id) ? raw.id : null,
6094
8093
  reason: "Not a JSON-RPC 2.0 message"
6095
8094
  };
6096
8095
  }
@@ -6104,7 +8103,7 @@ function classifyMessage(raw) {
6104
8103
  reason: "Missing method"
6105
8104
  };
6106
8105
  }
6107
- if (raw.params !== void 0 && !isObject2(raw.params)) {
8106
+ if (raw.params !== void 0 && !isObject3(raw.params)) {
6108
8107
  return {
6109
8108
  kind: "invalid",
6110
8109
  id: isId2(raw.id) ? raw.id : null,
@@ -6287,22 +8286,22 @@ function createMcpServer(options) {
6287
8286
  }
6288
8287
 
6289
8288
  // src/reference.ts
6290
- var import_node_fs5 = require("node:fs");
6291
- var import_node_path5 = require("node:path");
8289
+ var import_node_fs7 = require("node:fs");
8290
+ var import_node_path7 = require("node:path");
6292
8291
  var URI_PREFIX = "tutticode://skills/";
6293
8292
  var cache;
6294
8293
  function loadReference(dir = skillsDir()) {
6295
8294
  if (cache?.dir === dir)
6296
8295
  return cache.docs;
6297
8296
  const docs = [];
6298
- if ((0, import_node_fs5.existsSync)(dir))
6299
- for (const skill of (0, import_node_fs5.readdirSync)(dir).sort()) {
6300
- const root = (0, import_node_path5.join)(dir, skill);
6301
- if (!(0, import_node_fs5.statSync)(root).isDirectory() || !(0, import_node_fs5.existsSync)((0, import_node_path5.join)(root, "SKILL.md")))
8297
+ if ((0, import_node_fs7.existsSync)(dir))
8298
+ for (const skill of (0, import_node_fs7.readdirSync)(dir).sort()) {
8299
+ const root = (0, import_node_path7.join)(dir, skill);
8300
+ if (!(0, import_node_fs7.statSync)(root).isDirectory() || !(0, import_node_fs7.existsSync)((0, import_node_path7.join)(root, "SKILL.md")))
6302
8301
  continue;
6303
- for (const file of walk(root)) {
6304
- const path = (0, import_node_path5.relative)(root, file).split(import_node_path5.sep).join("/");
6305
- const text = (0, import_node_fs5.readFileSync)(file, "utf8").replace(/\r\n/g, "\n");
8302
+ for (const file2 of walk(root)) {
8303
+ const path = (0, import_node_path7.relative)(root, file2).split(import_node_path7.sep).join("/");
8304
+ const text = (0, import_node_fs7.readFileSync)(file2, "utf8").replace(/\r\n/g, "\n");
6306
8305
  docs.push({
6307
8306
  uri: `${URI_PREFIX}${skill}/${path}`,
6308
8307
  skill,
@@ -6316,9 +8315,9 @@ function loadReference(dir = skillsDir()) {
6316
8315
  return docs;
6317
8316
  }
6318
8317
  function walk(dir) {
6319
- return (0, import_node_fs5.readdirSync)(dir).sort().flatMap((name) => {
6320
- const path = (0, import_node_path5.join)(dir, name);
6321
- if ((0, import_node_fs5.statSync)(path).isDirectory())
8318
+ return (0, import_node_fs7.readdirSync)(dir).sort().flatMap((name) => {
8319
+ const path = (0, import_node_path7.join)(dir, name);
8320
+ if ((0, import_node_fs7.statSync)(path).isDirectory())
6322
8321
  return walk(path);
6323
8322
  return name.endsWith(".md") ? [path] : [];
6324
8323
  });
@@ -6398,8 +8397,8 @@ function count(haystack, needle) {
6398
8397
  }
6399
8398
 
6400
8399
  // src/tools.ts
6401
- var import_node_os3 = require("node:os");
6402
- var import_node_path6 = require("node:path");
8400
+ var import_node_os4 = require("node:os");
8401
+ var import_node_path8 = require("node:path");
6403
8402
 
6404
8403
  // ../mcp-protocol/src/code-tools.ts
6405
8404
  var CODE_CHECK_TOOL = "code_check";
@@ -6956,7 +8955,9 @@ function codeFragment(code) {
6956
8955
  // src/tools.ts
6957
8956
  var CODE_OPEN_TOOL = "code_open";
6958
8957
  var CODE_REFERENCE_TOOL = "code_reference";
6959
- var BACKEND_NOTE = "Runs locally: tutticode renders in your own browser with the tuttidaw.com/code engine (free, no account).";
8958
+ var CODE_ACCOUNT_TOOL = "code_account";
8959
+ var BACKEND_NOTE = "Runs locally: tutticode renders in your own browser with the tuttidaw.com/code engine (free).";
8960
+ var LIMITS_NOTE = `Once TuttiDAW switches its limits on (being rolled out; until then nothing is cut or tagged), renders without sign-in are limited to ${DEFAULT_TUTTICODE_LIMITS.anonMaxSeconds} s (error code "limit") and renders over ${DEFAULT_TUTTICODE_LIMITS.anonWatermarkAfterSeconds} s carry a spoken "register at TuttiDAW" tag, reported as \`watermark\`: it is not part of the program and is in the stats; never change the code to remove it, tell the user once that \`tutticode login\` (free) removes it.`;
6960
8961
  var codeProperty2 = codeCheckTool.inputSchema.properties.code;
6961
8962
  var renderProperties2 = {
6962
8963
  code: codeProperty2,
@@ -7015,13 +9016,17 @@ var renderProperties2 = {
7015
9016
  type: "number",
7016
9017
  minimum: -40,
7017
9018
  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.'
9019
+ 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
9020
  },
7020
9021
  truePeak: {
7021
9022
  type: "number",
7022
9023
  minimum: -12,
7023
9024
  maximum: 0,
7024
- description: "dBTP ceiling with lufs (default -1)."
9025
+ 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."
9026
+ },
9027
+ limit: {
9028
+ type: "boolean",
9029
+ 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
9030
  },
7026
9031
  out: {
7027
9032
  type: "string",
@@ -7030,6 +9035,10 @@ var renderProperties2 = {
7030
9035
  stemsDir: {
7031
9036
  type: "string",
7032
9037
  description: "Folder for the stems (default: next to out, <name>.stems)."
9038
+ },
9039
+ brandTag: {
9040
+ type: "boolean",
9041
+ description: 'false: no "Made with TuttiCode" metadata tag in the files. Honoured only when signed in (default true).'
7033
9042
  }
7034
9043
  };
7035
9044
  var localCheckTool = {
@@ -7039,7 +9048,7 @@ var localCheckTool = {
7039
9048
  var localRenderTool = {
7040
9049
  name: "code_render",
7041
9050
  title: "Render TuttiCode to a file",
7042
- description: `Render a TuttiCode program to an audio file on this computer (the same instruments and effects as tuttidaw.com/code) and measure it: integrated LUFS, true peak, octave bands, stereo width, groove, key, silence, per-bar loudness and each voice's level. Returns the written file paths (audio is never inlined), the beat grid (seconds of each bar and beat, for syncing cuts) and an editUrl - give it to the user so they can open the program in TuttiDAW and keep editing. Lengths up to ${BRIDGE_MAX_SECONDS} s. Call code_check first. ${BACKEND_NOTE}`,
9051
+ description: `Render a TuttiCode program to an audio file on this computer (the same instruments and effects as tuttidaw.com/code) and measure it: integrated LUFS, true peak, octave bands, stereo width, groove, key, silence, per-bar loudness and each voice's level. Returns the written file paths (audio is never inlined), the beat grid (seconds of each bar and beat, for syncing cuts) and an editUrl - give it to the user so they can open the program in TuttiDAW and keep editing. Lengths up to ${BRIDGE_MAX_SECONDS} s. Call code_check first. ${LIMITS_NOTE} ${BACKEND_NOTE}`,
7043
9052
  inputSchema: {
7044
9053
  type: "object",
7045
9054
  properties: renderProperties2,
@@ -7118,14 +9127,30 @@ var codeReferenceTool = {
7118
9127
  openWorldHint: false
7119
9128
  }
7120
9129
  };
9130
+ var codeAccountTool = {
9131
+ name: CODE_ACCOUNT_TOOL,
9132
+ title: "TuttiCode sign-in and limits",
9133
+ description: "Who renders and with which limits: signedIn, plan, limits.maxSeconds (longest render) and limits.watermarkAfterSeconds (renders longer than this carry a spoken tag; null = never), usageToday, and when not signed in a loginHint. Use it to plan within the limits (keep an anonymous draft at or under watermarkAfterSeconds). Never starts a sign-in: to sign in, run the loginHint command and ask the user to approve the code it prints.",
9134
+ inputSchema: {
9135
+ type: "object",
9136
+ properties: {},
9137
+ additionalProperties: false
9138
+ },
9139
+ annotations: {
9140
+ readOnlyHint: true,
9141
+ idempotentHint: true,
9142
+ openWorldHint: true
9143
+ }
9144
+ };
7121
9145
  var LOCAL_TOOLS = [
7122
9146
  localCheckTool,
7123
9147
  localRenderTool,
7124
9148
  localCompareTool,
7125
9149
  codeOpenTool,
7126
- codeReferenceTool
9150
+ codeReferenceTool,
9151
+ codeAccountTool
7127
9152
  ];
7128
- var MCP_INSTRUCTIONS = `tutticode turns TuttiCode (a superset of Strudel) into audio on this computer, rendering in the user's browser with the tuttidaw.com/code engine. Loop: write code -> code_check (fix every error) -> code_render (files + statistics) -> read the stats / code_compare -> revise. Use code_reference when unsure of a function. Always give the user the editUrl from code_render so they can keep editing in TuttiDAW.`;
9153
+ var MCP_INSTRUCTIONS = `tutticode turns TuttiCode (a superset of Strudel) into audio on this computer, rendering in the user's browser with the tuttidaw.com/code engine. Loop: write code -> code_check (fix every error) -> code_render (files + statistics) -> read the stats / code_compare -> revise. Use code_reference when unsure of a function. Always give the user the editUrl from code_render so they can keep editing in TuttiDAW. code_account says whether renders are signed in and their limits. On a "limit" error, tell the user its message in one sentence and carry on within the limit; a \`watermark\` in a report is the anonymous voice tag, not part of the program: do not change the code to remove it.`;
7129
9154
  var MAX_STORED = 64;
7130
9155
  var CHECK_TIMEOUT_MS = 9e4;
7131
9156
  var RENDER_TIMEOUT_MS = 15 * 6e4;
@@ -7144,7 +9169,7 @@ function createToolHandler(deps) {
7144
9169
  async function check2(args, signal) {
7145
9170
  const command = {
7146
9171
  type: "check",
7147
- id: (0, import_node_crypto2.randomUUID)(),
9172
+ id: (0, import_node_crypto3.randomUUID)(),
7148
9173
  code: args.code,
7149
9174
  ...args.bpm !== void 0 ? { bpm: args.bpm } : {}
7150
9175
  };
@@ -7176,7 +9201,7 @@ function createToolHandler(deps) {
7176
9201
  `format ${String(args.format)} does not match out ${out}.`
7177
9202
  );
7178
9203
  const format = args.format ?? outFormat ?? "wav";
7179
- const id = (0, import_node_crypto2.randomUUID)();
9204
+ const id = (0, import_node_crypto3.randomUUID)();
7180
9205
  const length = lengths.length ? { [lengths[0]]: args[lengths[0]] } : void 0;
7181
9206
  const command = {
7182
9207
  type: "render",
@@ -7195,7 +9220,9 @@ function createToolHandler(deps) {
7195
9220
  "fadeOut",
7196
9221
  "tail",
7197
9222
  "lufs",
7198
- "truePeak"
9223
+ "truePeak",
9224
+ "limit",
9225
+ "brandTag"
7199
9226
  ])
7200
9227
  if (args[key] !== void 0)
7201
9228
  command[key] = args[key];
@@ -7209,8 +9236,8 @@ function createToolHandler(deps) {
7209
9236
  });
7210
9237
  if (outcome.kind !== "rendered")
7211
9238
  return errorResult(outcome);
7212
- const path = out ?? (0, import_node_path6.join)(
7213
- deps.outDir ?? (0, import_node_path6.join)((0, import_node_os3.tmpdir)(), "tutticode"),
9239
+ const path = out ?? (0, import_node_path8.join)(
9240
+ deps.outDir ?? (0, import_node_path8.join)((0, import_node_os4.tmpdir)(), "tutticode"),
7214
9241
  `render-${id.slice(0, 8)}.${format}`
7215
9242
  );
7216
9243
  const files = writeRender(
@@ -7219,10 +9246,15 @@ function createToolHandler(deps) {
7219
9246
  path,
7220
9247
  typeof args.stemsDir === "string" ? args.stemsDir : void 0
7221
9248
  );
7222
- remember(id, { meta: outcome.meta, analysis: outcome.meta.analysis });
9249
+ remember(id, {
9250
+ meta: outcome.meta,
9251
+ analysis: outcome.meta.analysis,
9252
+ watermark: outcome.meta.watermark
9253
+ });
7223
9254
  const { meta } = outcome;
7224
9255
  return {
7225
9256
  id,
9257
+ watermarked: !!meta.watermark,
7226
9258
  report: {
7227
9259
  id,
7228
9260
  kind: "render",
@@ -7241,14 +9273,18 @@ function createToolHandler(deps) {
7241
9273
  loudness: {
7242
9274
  lufs: meta.lufs,
7243
9275
  truePeak: meta.truePeak,
7244
- gainDb: meta.gainDb
9276
+ gainDb: meta.gainDb,
9277
+ ...meta.limitDb ? { limitDb: meta.limitDb } : {}
7245
9278
  },
9279
+ ...meta.warnings?.length ? { warnings: meta.warnings } : {},
7246
9280
  voices: meta.voices,
7247
9281
  barTimes: meta.barTimes,
7248
9282
  beatTimes: meta.beatTimes,
7249
9283
  check: meta.check,
7250
9284
  ...meta.analysis ? { analysis: meta.analysis } : {},
7251
- engineVersion: meta.engineVersion
9285
+ engineVersion: meta.engineVersion,
9286
+ ...meta.watermark ? { watermark: meta.watermark, watermarkNote: WATERMARK_LINE } : {},
9287
+ ...signInNote(outcome.auth)
7252
9288
  }
7253
9289
  };
7254
9290
  }
@@ -7288,9 +9324,11 @@ function createToolHandler(deps) {
7288
9324
  return invalid("Give goals or againstRenderId.");
7289
9325
  let analysis;
7290
9326
  let rendered;
9327
+ let tagged = target === "render" && !!renders.get(String(args.againstRenderId))?.watermark;
7291
9328
  if (args.renderId !== void 0) {
7292
9329
  if (args.code !== void 0)
7293
9330
  return invalid("Give renderId or code, not both.");
9331
+ tagged ||= !!renders.get(String(args.renderId))?.watermark;
7294
9332
  analysis = renders.get(String(args.renderId))?.analysis;
7295
9333
  if (!analysis)
7296
9334
  return invalid(
@@ -7308,16 +9346,34 @@ function createToolHandler(deps) {
7308
9346
  rendered = await render2(renderArgs, signal);
7309
9347
  if (!("report" in rendered))
7310
9348
  return rendered;
9349
+ tagged ||= rendered.watermarked;
7311
9350
  analysis = rendered.report.analysis;
7312
9351
  if (!analysis)
7313
9352
  return invalid("The render came back without an analysis.");
7314
9353
  } else
7315
9354
  return invalid("Give renderId or code.");
7316
9355
  const result = compareAnalysis(analysis, goals, target);
9356
+ const note = tagged ? { watermarkNote: WATERMARK_LINE } : {};
7317
9357
  return textResult(
7318
- rendered && "report" in rendered ? { ...rendered.report, compare: result } : { id: args.renderId, compare: result }
9358
+ rendered && "report" in rendered ? { ...rendered.report, compare: result, ...note } : { id: args.renderId, compare: result, ...note }
7319
9359
  );
7320
9360
  }
9361
+ async function account() {
9362
+ const state = deps.account ? await deps.account() : { state: "anonymous" };
9363
+ const signedIn = state.state === "signed-in";
9364
+ return textResult({
9365
+ signedIn,
9366
+ tokenKind: signedIn ? state.tokenKind : "anonymous",
9367
+ ...signedIn ? {
9368
+ plan: state.me.plan,
9369
+ usageToday: state.me.usageToday,
9370
+ expiresAt: state.me.expiresAt
9371
+ } : {},
9372
+ limits: accountLimits(state),
9373
+ ...signedIn ? {} : { loginHint: LOGIN_HINT },
9374
+ ...state.state === "rejected" || state.state === "unreachable" ? { problem: state.message } : {}
9375
+ });
9376
+ }
7321
9377
  return async function callTool(name, args, signal) {
7322
9378
  switch (name) {
7323
9379
  case "code_check":
@@ -7352,6 +9408,8 @@ function createToolHandler(deps) {
7352
9408
  }
7353
9409
  case CODE_REFERENCE_TOOL:
7354
9410
  return reference(args);
9411
+ case CODE_ACCOUNT_TOOL:
9412
+ return account();
7355
9413
  default:
7356
9414
  return invalid(`Unknown tool ${name}.`);
7357
9415
  }
@@ -7385,12 +9443,26 @@ function errorResult(outcome) {
7385
9443
  return invalid(`Unexpected ${outcome.kind}.`);
7386
9444
  return textResult(
7387
9445
  {
7388
- error: { code: outcome.code, message: outcome.message },
7389
- ...outcome.report ? { report: outcome.report } : {}
9446
+ error: {
9447
+ code: outcome.code,
9448
+ message: outcome.message,
9449
+ ...outcome.limit ? { limit: outcome.limit } : {}
9450
+ },
9451
+ ...outcome.code === "limit" ? { loginHint: LOGIN_HINT } : {},
9452
+ ...outcome.report ? { report: outcome.report } : {},
9453
+ ...signInNote(outcome.auth)
7390
9454
  },
7391
9455
  true
7392
9456
  );
7393
9457
  }
9458
+ function signInNote(auth) {
9459
+ return auth?.rejected ? {
9460
+ signIn: {
9461
+ rejected: auth.rejected,
9462
+ note: "This ran without sign-in. Tell the user to run `tutticode login` again."
9463
+ }
9464
+ } : {};
9465
+ }
7394
9466
  var GOAL_KEYS = {
7395
9467
  style: (v) => typeof v === "string" && v.length > 0 && v.length <= 64,
7396
9468
  lufs: (v) => num(v, -70, 0),
@@ -7432,10 +9504,11 @@ function serveMcpStdio(options) {
7432
9504
  backend: options.backend,
7433
9505
  styles: options.styles,
7434
9506
  codeLink: options.codeLink,
7435
- canShow: options.canShow
9507
+ canShow: options.canShow,
9508
+ account: options.account
7436
9509
  });
7437
9510
  const server = createMcpServer({
7438
- serverInfo: { name: "tutticode", title: "TuttiCode", version: VERSION },
9511
+ serverInfo: { name: "tutticode", title: "TuttiCode", version: VERSION2 },
7439
9512
  instructions: MCP_INSTRUCTIONS,
7440
9513
  listTools: () => [...LOCAL_TOOLS],
7441
9514
  callTool: (name, args, ctx) => callTool(name, args, ctx.signal),
@@ -7473,7 +9546,7 @@ function serveMcpStdio(options) {
7473
9546
  });
7474
9547
  });
7475
9548
  }
7476
- function sessionBackend(create) {
9549
+ function sessionBackend(create, auth) {
7477
9550
  const sessions = /* @__PURE__ */ new Map();
7478
9551
  const get = (mode) => {
7479
9552
  let session = sessions.get(mode);
@@ -7484,21 +9557,57 @@ function sessionBackend(create) {
7484
9557
  return session;
7485
9558
  };
7486
9559
  return {
7487
- backend: (command, { mode, signal, timeoutMs }) => get(mode).run(command, { signal, timeoutMs }),
9560
+ backend: async (command, { mode, signal, timeoutMs }) => {
9561
+ const signIn = await auth?.();
9562
+ const outcome = await get(mode).run(command, {
9563
+ signal,
9564
+ timeoutMs,
9565
+ ...signIn ? { auth: signIn.auth } : {}
9566
+ });
9567
+ return signIn?.ended ? {
9568
+ ...outcome,
9569
+ auth: { signedIn: false, ...outcome.auth, rejected: signIn.ended }
9570
+ } : outcome;
9571
+ },
7488
9572
  close: async () => {
7489
9573
  await Promise.all([...sessions.values()].map((s) => s.close()));
7490
9574
  }
7491
9575
  };
7492
9576
  }
7493
9577
 
9578
+ // src/notices.ts
9579
+ var import_node_fs8 = require("node:fs");
9580
+ var import_node_path9 = require("node:path");
9581
+ var file = () => (0, import_node_path9.join)(stateDir(), "notices.json");
9582
+ function read() {
9583
+ try {
9584
+ const value = JSON.parse((0, import_node_fs8.readFileSync)(file(), "utf8"));
9585
+ return typeof value === "object" && value !== null ? value : {};
9586
+ } catch {
9587
+ return {};
9588
+ }
9589
+ }
9590
+ function firstTime(key, period, now = /* @__PURE__ */ new Date()) {
9591
+ const shown = read();
9592
+ const today = now.toISOString().slice(0, 10);
9593
+ const last = shown[key];
9594
+ if (last && (period === "install" || last === today))
9595
+ return false;
9596
+ try {
9597
+ (0, import_node_fs8.writeFileSync)(file(), JSON.stringify({ ...shown, [key]: today }));
9598
+ } catch {
9599
+ }
9600
+ return true;
9601
+ }
9602
+
7494
9603
  // src/setup.ts
7495
- var import_node_child_process3 = require("node:child_process");
7496
- var import_node_fs6 = require("node:fs");
7497
- var import_node_path7 = require("node:path");
9604
+ var import_node_child_process5 = require("node:child_process");
9605
+ var import_node_fs9 = require("node:fs");
9606
+ var import_node_path10 = require("node:path");
7498
9607
  var POLICY_NAME = "LocalNetworkAccessAllowedForUrls";
7499
9608
  var POLICY_URL = "https://tuttidaw.com";
7500
9609
  var exec = (command, args) => {
7501
- const r2 = (0, import_node_child_process3.spawnSync)(command, args, { encoding: "utf8", windowsHide: true });
9610
+ const r2 = (0, import_node_child_process5.spawnSync)(command, args, { encoding: "utf8", windowsHide: true });
7502
9611
  return { status: r2.status, stdout: r2.stdout ?? "" };
7503
9612
  };
7504
9613
  var WINDOWS_KEYS = [
@@ -7613,16 +9722,16 @@ function policySteps(undo, platform = process.platform, run = exec) {
7613
9722
  };
7614
9723
  });
7615
9724
  return LINUX_POLICY_FILES.map(
7616
- (file) => undo ? {
7617
- describe: `delete ${file}`,
9725
+ (file2) => undo ? {
9726
+ describe: `delete ${file2}`,
7618
9727
  run() {
7619
- (0, import_node_fs6.rmSync)(file, { force: true });
9728
+ (0, import_node_fs9.rmSync)(file2, { force: true });
7620
9729
  }
7621
9730
  } : {
7622
- describe: `write ${file} (${POLICY_NAME}: ["${POLICY_URL}"])`,
9731
+ describe: `write ${file2} (${POLICY_NAME}: ["${POLICY_URL}"])`,
7623
9732
  run() {
7624
- (0, import_node_fs6.mkdirSync)((0, import_node_path7.dirname)(file), { recursive: true });
7625
- (0, import_node_fs6.writeFileSync)(file, linuxPolicyJson());
9733
+ (0, import_node_fs9.mkdirSync)((0, import_node_path10.dirname)(file2), { recursive: true });
9734
+ (0, import_node_fs9.writeFileSync)(file2, linuxPolicyJson());
7626
9735
  }
7627
9736
  }
7628
9737
  );
@@ -7638,22 +9747,22 @@ for your user. It allows nothing else, and you can see it at chrome://policy
7638
9747
  (edge://policy). Undo any time with: tutticode setup --undo`;
7639
9748
 
7640
9749
  // src/skills.ts
7641
- var import_node_fs7 = require("node:fs");
7642
- var import_node_os4 = require("node:os");
7643
- var import_node_path8 = require("node:path");
7644
- function targetDir(target, home = (0, import_node_os4.homedir)()) {
7645
- return (0, import_node_path8.join)(home, `.${target}`, "skills");
9750
+ var import_node_fs10 = require("node:fs");
9751
+ var import_node_os5 = require("node:os");
9752
+ var import_node_path11 = require("node:path");
9753
+ function targetDir(target, home = (0, import_node_os5.homedir)()) {
9754
+ return (0, import_node_path11.join)(home, `.${target}`, "skills");
7646
9755
  }
7647
9756
  function shippedSkills(dir = skillsDir()) {
7648
- if (!(0, import_node_fs7.existsSync)(dir))
9757
+ if (!(0, import_node_fs10.existsSync)(dir))
7649
9758
  return [];
7650
- return (0, import_node_fs7.readdirSync)(dir).filter(
7651
- (name) => (0, import_node_fs7.statSync)((0, import_node_path8.join)(dir, name)).isDirectory() && (0, import_node_fs7.existsSync)((0, import_node_path8.join)(dir, name, "SKILL.md"))
9759
+ return (0, import_node_fs10.readdirSync)(dir).filter(
9760
+ (name) => (0, import_node_fs10.statSync)((0, import_node_path11.join)(dir, name)).isDirectory() && (0, import_node_fs10.existsSync)((0, import_node_path11.join)(dir, name, "SKILL.md"))
7652
9761
  ).sort();
7653
9762
  }
7654
- function defaultTargets(home = (0, import_node_os4.homedir)()) {
9763
+ function defaultTargets(home = (0, import_node_os5.homedir)()) {
7655
9764
  const found = ["claude", "copilot", "codex"].filter(
7656
- (t) => (0, import_node_fs7.existsSync)((0, import_node_path8.join)(home, `.${t}`))
9765
+ (t) => (0, import_node_fs10.existsSync)((0, import_node_path11.join)(home, `.${t}`))
7657
9766
  );
7658
9767
  return found.length ? [...found] : ["claude"];
7659
9768
  }
@@ -7664,9 +9773,9 @@ function installSkills(destinations, source = skillsDir()) {
7664
9773
  const installed = [];
7665
9774
  for (const destination of destinations) {
7666
9775
  for (const skill of skills) {
7667
- const path = (0, import_node_path8.resolve)(destination, skill);
7668
- (0, import_node_fs7.rmSync)(path, { recursive: true, force: true });
7669
- (0, import_node_fs7.cpSync)((0, import_node_path8.join)(source, skill), path, { recursive: true });
9776
+ const path = (0, import_node_path11.resolve)(destination, skill);
9777
+ (0, import_node_fs10.rmSync)(path, { recursive: true, force: true });
9778
+ (0, import_node_fs10.cpSync)((0, import_node_path11.join)(source, skill), path, { recursive: true });
7670
9779
  installed.push({ skill, path });
7671
9780
  }
7672
9781
  }
@@ -7909,7 +10018,19 @@ var afrobeats = {
7909
10018
  p90: [-8.9, -1.9, -2.9, -4.9, -6.9, -8.9, -9.9, -11.9, -12.9, -17.9],
7910
10019
  source: TONAL_BALANCE_SOURCE
7911
10020
  },
7912
- sidechain: "Compressor: brief gentle kick-to-bass duck only where the two overlap; retain the bounce.",
10021
+ sidechain: {
10022
+ ducks: [
10023
+ {
10024
+ trigger: "kick",
10025
+ targets: ["bass"],
10026
+ ratio: 3,
10027
+ attackMs: 1,
10028
+ releaseMs: 80,
10029
+ depthDb: 2
10030
+ }
10031
+ ],
10032
+ note: "Clears the kick, no pumping; keep the bounce."
10033
+ },
7913
10034
  reverb: "EQ Eight into light Compressor on vocal; Reverb short room for percussion and a darker plate for voice.",
7914
10035
  delay: "Echo dotted eighth on guitar or phrase endings; filter repeats away from the lead vocal.",
7915
10036
  stereo: "Center kick, bass and lead vocal; spread the guitar and shaker without widening the sub.",
@@ -8694,7 +10815,19 @@ var boomBap = {
8694
10815
  p90: [-12.2, -3.2, -2.2, -4.2, -6.2, -8.2, -9.2, -12.2, -16.2, -23.2],
8695
10816
  source: TONAL_BALANCE_SOURCE
8696
10817
  },
8697
- sidechain: "Compressor only to clear bass on the strongest kicks; no obvious EDM pumping.",
10818
+ sidechain: {
10819
+ ducks: [
10820
+ {
10821
+ trigger: "kick",
10822
+ targets: ["bass"],
10823
+ ratio: 3,
10824
+ attackMs: 1,
10825
+ releaseMs: 70,
10826
+ depthDb: 2
10827
+ }
10828
+ ],
10829
+ note: "Clears the kick, no EDM pumping."
10830
+ },
8698
10831
  reverb: "Redux lightly on the sample, Saturator for body, EQ Eight to trim hiss; short Reverb on snare, rap mostly dry.",
8699
10832
  delay: "Echo throws on selected rhyme endings, not across the whole verse.",
8700
10833
  stereo: "Centered kick/snare/bass/vocal; sample can be narrow with wider hook answers.",
@@ -9201,11 +11334,11 @@ var cinematic = {
9201
11334
  p90: [-11.4, -4.4, -3.4, -3.4, -5.4, -7.4, -8.4, -10.4, -12.4, -18.4],
9202
11335
  source: TONAL_BALANCE_SOURCE
9203
11336
  },
9204
- sidechain: "Compressor can briefly clear pad/sub for impacts; avoid audible dance pumping unless the cue is explicitly electronic.",
11337
+ sidechain: null,
9205
11338
  reverb: "EQ Eight before Hybrid Reverb keeps the hall clear; different predelays establish near piano and distant orchestra.",
9206
11339
  delay: "Echo can extend a pulse, but sync it so the ostinato does not become an accidental flam.",
9207
11340
  stereo: "Strings and upper brass broad, low drums/bass centered; keep dialogue space in the middle when scoring picture.",
9208
- lowEnd: "Saturator on low-drum substitutes adds weight; highpass reverb. Limiter catches isolated impacts, not the entire crescendo. Delivery loudness follows the actual brief."
11341
+ lowEnd: "Saturator on low-drum substitutes adds weight; highpass reverb. Thin pad/sub under impacts, no dance pumping. Limiter catches isolated impacts, not the entire crescendo. Delivery loudness follows the brief."
9209
11342
  },
9210
11343
  arrangement: {
9211
11344
  variationEveryBars: 4,
@@ -10556,7 +12689,7 @@ var disco = {
10556
12689
  p90: [-12.1, -3.1, -2.1, -5.1, -7.1, -8.1, -9.1, -10.1, -11.1, -17.1],
10557
12690
  source: TONAL_BALANCE_SOURCE
10558
12691
  },
10559
- sidechain: "Little or none for the live-band feel; shorten bass notes before ducking.",
12692
+ sidechain: null,
10560
12693
  reverb: "Reverb plate on strings and vocal, small room on drums.",
10561
12694
  delay: "Quiet Echo slap on vocal; avoid busy feedback against guitar sixteenths.",
10562
12695
  stereo: "Strings wide, rhythm guitar to one side with keys answering; bass and kick centered.",
@@ -10875,14 +13008,34 @@ var drumAndBass = {
10875
13008
  "Climbing Keys G Minor 140 bpm"
10876
13009
  ],
10877
13010
  mix: {
10878
- lufs: { min: -8, max: -5 },
13011
+ lufs: { min: -9, max: -7 },
10879
13012
  tonalBalance: {
10880
13013
  p10: [-14.5, -9, -9.5, -12.5, -15, -16, -16.5, -17, -19, -25.5],
10881
13014
  p50: [-9.5, -5, -6.5, -9.5, -12, -13, -13.5, -14, -15, -20.5],
10882
13015
  p90: [-4.5, -1, -3.5, -6.5, -9, -10, -10.5, -11, -11, -15.5],
10883
13016
  source: TONAL_BALANCE_SOURCE
10884
13017
  },
10885
- sidechain: "Pads/reese ducked by kick AND snare; the sub ducked only by the kick.",
13018
+ sidechain: {
13019
+ ducks: [
13020
+ {
13021
+ trigger: "kick",
13022
+ targets: ["bass", "pad"],
13023
+ ratio: 4,
13024
+ attackMs: 0.5,
13025
+ releaseMs: 100,
13026
+ depthDb: 4
13027
+ },
13028
+ {
13029
+ trigger: "snare",
13030
+ targets: ["pad"],
13031
+ ratio: 3,
13032
+ attackMs: 0.5,
13033
+ releaseMs: 120,
13034
+ depthDb: 3
13035
+ }
13036
+ ],
13037
+ note: "Pads duck to kick and snare; the sub only to the kick."
13038
+ },
10886
13039
  reverb: "Short, bright snare reverb; big wash on breakdown pads/vocals.",
10887
13040
  delay: "1/4 or 1/8 dotted on vocals; delay throws into drops.",
10888
13041
  stereo: "Sub and kick mono, reese mids wide, breaks slightly wide, pads very wide.",
@@ -11157,7 +13310,7 @@ var dubstep = {
11157
13310
  },
11158
13311
  starterClips: ["Dub Halftime 136 bpm", "Halftime Pattern 136 bpm"],
11159
13312
  mix: {
11160
- lufs: { min: -10, max: -6 },
13313
+ lufs: { min: -10, max: -8 },
11161
13314
  tonalBalance: {
11162
13315
  p10: [
11163
13316
  -11.1,
@@ -11175,7 +13328,19 @@ var dubstep = {
11175
13328
  p90: [-1.1, -0.1, -5.1, -9.1, -11.1, -11.1, -11.1, -13.1, -15.1, -21.1],
11176
13329
  source: TONAL_BALANCE_SOURCE
11177
13330
  },
11178
- sidechain: "Compressor ducks sub briefly for the kick; a separate gentle duck on mid-bass can expose the huge snare.",
13331
+ sidechain: {
13332
+ ducks: [
13333
+ {
13334
+ trigger: "kick",
13335
+ targets: ["bass"],
13336
+ ratio: 4,
13337
+ attackMs: 0.5,
13338
+ releaseMs: 60,
13339
+ depthDb: 4
13340
+ }
13341
+ ],
13342
+ note: "The sub ducks briefly for the kick only."
13343
+ },
11179
13344
  reverb: "Dark Reverb on snare and isolated bells; no reverb on the clean sub.",
11180
13345
  delay: "Dub-style Echo throws into the gaps, filtered and automated before the next bass call.",
11181
13346
  stereo: "Sub and kick mono; widen only the upper bass layer and atmospheric answers.",
@@ -11820,7 +13985,7 @@ var funk = {
11820
13985
  p90: [-13.5, -3.5, -2.5, -4.5, -6.5, -7.5, -8.5, -10.5, -12.5, -19.5],
11821
13986
  source: TONAL_BALANCE_SOURCE
11822
13987
  },
11823
- sidechain: "Usually none: note lengths and rests make space before compression.",
13988
+ sidechain: null,
11824
13989
  reverb: "Short Reverb room; keep guitar scratches and bass dry.",
11825
13990
  delay: "Brief Echo slap on a horn or vocal answer only.",
11826
13991
  stereo: "Bass, snare and lead centered; guitar and clav answer from opposite sides.",
@@ -11837,7 +14002,7 @@ var funk = {
11837
14002
  {
11838
14003
  kind: "breakdown",
11839
14004
  where: "Middle",
11840
- how: "Expose drums and bass; reintroduce clav before horns."
14005
+ how: "Expose the drums (and bass, in a bass feature); reintroduce clav before horns."
11841
14006
  },
11842
14007
  {
11843
14008
  kind: "variation",
@@ -12083,7 +14248,7 @@ var futureBass = {
12083
14248
  },
12084
14249
  starterClips: [],
12085
14250
  mix: {
12086
- lufs: { min: -10, max: -7 },
14251
+ lufs: { min: -10, max: -8 },
12087
14252
  tonalBalance: {
12088
14253
  p10: [
12089
14254
  -17.8,
@@ -12101,7 +14266,27 @@ var futureBass = {
12101
14266
  p90: [-7.8, -1.8, -4.8, -6.8, -6.8, -6.8, -7.8, -8.8, -9.8, -14.8],
12102
14267
  source: TONAL_BALANCE_SOURCE
12103
14268
  },
12104
- sidechain: "Compressor keyed by kick ducks chord stacks strongly; release follows the swell, usually around an eighth note. Keep the sub duck shorter.",
14269
+ sidechain: {
14270
+ ducks: [
14271
+ {
14272
+ trigger: "kick",
14273
+ targets: ["chords", "pad"],
14274
+ ratio: 8,
14275
+ attackMs: 0.5,
14276
+ releaseMs: 200,
14277
+ depthDb: 8
14278
+ },
14279
+ {
14280
+ trigger: "kick",
14281
+ targets: ["bass"],
14282
+ ratio: 4,
14283
+ attackMs: 0.5,
14284
+ releaseMs: 80,
14285
+ depthDb: 4
14286
+ }
14287
+ ],
14288
+ note: "Chord stacks pump hard, release follows the swell; the sub duck is shorter."
14289
+ },
12105
14290
  reverb: "EQ Eight clears chord low mids, Saturator adds body, Chorus-Ensemble widens upper layers, then Reverb; keep dry transients in front.",
12106
14291
  delay: "Echo dotted eighth on vocal chops; automate throws into gaps rather than washing out the next chord.",
12107
14292
  stereo: "Wide chords and chops above a centered sub/kick/snare; mono-check detuned layers.",
@@ -12373,7 +14558,19 @@ var gospel = {
12373
14558
  p90: [-13.1, -5.1, -4.1, -4.1, -5.1, -6.1, -7.1, -9.1, -11.1, -17.1],
12374
14559
  source: TONAL_BALANCE_SOURCE
12375
14560
  },
12376
- sidechain: "Only gentle Compressor ducking where bass masks the kick; no dance pumping.",
14561
+ sidechain: {
14562
+ ducks: [
14563
+ {
14564
+ trigger: "kick",
14565
+ targets: ["bass"],
14566
+ ratio: 3,
14567
+ attackMs: 1,
14568
+ releaseMs: 80,
14569
+ depthDb: 1.5
14570
+ }
14571
+ ],
14572
+ note: "Only where bass masks the kick; no dance pumping."
14573
+ },
12377
14574
  reverb: "EQ Eight then Compressor on lead/choir; shared Reverb hall with enough predelay to keep consonants clear.",
12378
14575
  delay: "Low Echo throws behind the leader; do not repeat over the choir response.",
12379
14576
  stereo: "Lead center, choir spread, organ behind the words; retain mono bass.",
@@ -12427,7 +14624,7 @@ var guofengPop = {
12427
14624
  feel: {
12428
14625
  kind: "straight",
12429
14626
  swingPercent: 50,
12430
- note: "Soft even-16th R&B-pop beat; the Chinese colour comes from the melody (pentatonic, slides, grace notes) and woodblock-like rim, not the kick and snare."
14627
+ note: "Soft even-16th R&B-pop beat; the Chinese colour is in the melody (pentatonic, slides, grace notes) and woodblock-like rim."
12431
14628
  },
12432
14629
  keys: ["D major", "E major", "G major", "B minor", "E minor"],
12433
14630
  forms: [
@@ -12732,7 +14929,7 @@ var guofengPop = {
12732
14929
  reverb: "Large hall on flute, zither and vocal (the misty space is the style), pre-delay for clear words; drums dry.",
12733
14930
  delay: "Dotted-1/8 Echo on flute and plucks; vocal throws on line ends.",
12734
14931
  stereo: "Zither and strings wide, flute off-centre, vocal centred on top.",
12735
- lowEnd: "Soft and round; high-pass plucks and flute ~150 Hz. Chain: vocal EQ Eight > Compressor > Hybrid Reverb; master Glue Compressor > Limiter."
14932
+ lowEnd: "Soft and round; high-pass plucks and flute ~150 Hz. Chain: vocal EQ Eight > Compressor > Hybrid Reverb; gentle master Glue."
12736
14933
  },
12737
14934
  arrangement: {
12738
14935
  variationEveryBars: 8,
@@ -12760,7 +14957,7 @@ var guofengPop = {
12760
14957
  ]
12761
14958
  },
12762
14959
  references: ["Jay Chou", "JJ Lin", "Silence Wang", "Henry Huo", "Isaac Hong"],
12763
- why: "Guofeng is C-pop with a Chinese accent: a pop/R&B beat and pop chords under an ornamented pentatonic melody and classical imagery; the instrumental hook announces the style. No Chinese instrument samples yet: Koto / Eastern Harp stand in for guzheng, Flute Mellow / Indian Flute for dizi, Violin Strings with slides for erhu."
14960
+ why: "Guofeng is C-pop with a Chinese accent: pop/R&B beat and chords under an ornamented pentatonic melody and classical imagery. No Chinese instrument samples yet: Koto / Eastern Harp stand in for guzheng, Flute Mellow / Indian Flute for dizi, Violin Strings with slides for erhu."
12764
14961
  };
12765
14962
 
12766
14963
  // ../../src/components/editor-ai/style/cards/house.ts
@@ -13058,14 +15255,34 @@ var house = {
13058
15255
  "House 90s C Minor 128 bpm"
13059
15256
  ],
13060
15257
  mix: {
13061
- lufs: { min: -9, max: -7 },
15258
+ lufs: { min: -10, max: -8 },
13062
15259
  tonalBalance: {
13063
15260
  p10: [-15.5, -8.5, -9, -12, -14.5, -16, -17, -18, -20, -26.5],
13064
15261
  p50: [-10.5, -4.5, -6, -9, -11.5, -13, -14, -15, -16, -21.5],
13065
15262
  p90: [-5.5, -0.5, -3, -6, -8.5, -10, -11, -12, -12, -16.5],
13066
15263
  source: TONAL_BALANCE_SOURCE
13067
15264
  },
13068
- sidechain: "Pads/chords ducked by the kick (medium, fast release ~1/8); bass light or none.",
15265
+ sidechain: {
15266
+ ducks: [
15267
+ {
15268
+ trigger: "kick",
15269
+ targets: ["chords", "pad"],
15270
+ ratio: 4,
15271
+ attackMs: 1,
15272
+ releaseMs: 200,
15273
+ depthDb: 5
15274
+ },
15275
+ {
15276
+ trigger: "kick",
15277
+ targets: ["bass"],
15278
+ ratio: 3,
15279
+ attackMs: 1,
15280
+ releaseMs: 80,
15281
+ depthDb: 2
15282
+ }
15283
+ ],
15284
+ note: "Chords/pads pump (release ~1/8); the offbeat bass only lightly."
15285
+ },
13069
15286
  reverb: "Short plate on clap, large hall on breakdown pads/vocals.",
13070
15287
  delay: "1/8 dotted or 3/16 on stabs and vocals, filtered.",
13071
15288
  stereo: "Kick, bass, clap centered; hats/shaker/perc spread wide.",
@@ -13697,7 +15914,7 @@ var jpopAnime = {
13697
15914
  "Downwards A Minor 141 bpm"
13698
15915
  ],
13699
15916
  mix: {
13700
- lufs: { min: -9, max: -6 },
15917
+ lufs: { min: -10, max: -8 },
13701
15918
  tonalBalance: {
13702
15919
  p10: [-20.5, -12, -9.5, -10.5, -12, -13, -14, -15, -18, -26],
13703
15920
  p50: [-15.5, -8, -6.5, -7.5, -9, -10, -11, -12, -14, -21],
@@ -13708,7 +15925,7 @@ var jpopAnime = {
13708
15925
  reverb: "Bright plate on vocal and snare, hall on strings.",
13709
15926
  delay: "Dotted-1/8 vocal delay tucked under.",
13710
15927
  stereo: "Doubled guitars hard L/R, strings wide, vocal centred on top.",
13711
- lowEnd: "Tight kick and bass; high-pass guitars ~100 Hz. Chain: guitars Amp > Cabinet; vocal Compressor > EQ Eight > Reverb; master Glue Compressor > Limiter, loud."
15928
+ lowEnd: "Tight kick and bass; high-pass guitars ~100 Hz. Chain: guitars Amp > Cabinet; vocal Compressor > EQ Eight > Reverb; master Glue Compressor > one Limiter (ceiling -1 dBTP)."
13712
15929
  },
13713
15930
  arrangement: {
13714
15931
  variationEveryBars: 4,
@@ -13766,19 +15983,19 @@ var kpop = {
13766
15983
  "\uCF00\uC774\uD31D",
13767
15984
  "\uC544\uC774\uB3CC"
13768
15985
  ],
13769
- summary: "Idol dance-pop, 95-130 BPM, genre per section: rap verse, pre-chorus build, EDM/trap chorus, dance break, key-up end.",
15986
+ summary: "Idol dance-pop, 95-130 BPM, genre per section: rap verse, build, EDM/trap chorus, dance break, key-up end.",
13770
15987
  tempo: { min: 95, max: 130, typical: 120 },
13771
15988
  meter: "4/4",
13772
15989
  feel: {
13773
15990
  kind: "straight",
13774
15991
  swingPercent: 50,
13775
- note: "Quantized, punchy; rap/trap parts go half-time with 32nd hat rolls."
15992
+ note: "Quantized; trap parts half-time with 32nd hat rolls."
13776
15993
  },
13777
15994
  keys: ["F# minor", "C# minor", "G minor", "A minor", "Eb major"],
13778
15995
  forms: [
13779
15996
  {
13780
15997
  id: "title-track",
13781
- name: "Title track: (V/Pre/Chorus/Post) x2, Bridge, Dance Break, key up",
15998
+ name: "Title track, key-up end",
13782
15999
  sections: [
13783
16000
  {
13784
16001
  name: "Intro",
@@ -13798,9 +16015,9 @@ var kpop = {
13798
16015
  {
13799
16016
  name: "Pre-Chorus",
13800
16017
  bars: 8,
13801
- energy: 0.65,
16018
+ energy: 0.55,
13802
16019
  roles: ["drums", "bass", "chords", "pad", "vocal", "fx"],
13803
- drums: "verse",
16020
+ drums: "build",
13804
16021
  bass: "pulse"
13805
16022
  },
13806
16023
  {
@@ -13830,9 +16047,9 @@ var kpop = {
13830
16047
  {
13831
16048
  name: "Pre-Chorus",
13832
16049
  bars: 8,
13833
- energy: 0.7,
16050
+ energy: 0.6,
13834
16051
  roles: ["drums", "bass", "chords", "pad", "vocal", "fx"],
13835
- drums: "verse",
16052
+ drums: "build",
13836
16053
  bass: "pulse"
13837
16054
  },
13838
16055
  {
@@ -13878,7 +16095,7 @@ var kpop = {
13878
16095
  },
13879
16096
  {
13880
16097
  id: "beat-switch",
13881
- name: "Beat switch: rap open, trap chorus, dance-groove last chorus",
16098
+ name: "Beat switch: trap chorus, dance last chorus",
13882
16099
  sections: [
13883
16100
  {
13884
16101
  name: "Intro",
@@ -13898,8 +16115,9 @@ var kpop = {
13898
16115
  {
13899
16116
  name: "Pre-Chorus",
13900
16117
  bars: 8,
13901
- energy: 0.65,
13902
- roles: ["chords", "pad", "vocal", "fx"]
16118
+ energy: 0.6,
16119
+ roles: ["drums", "chords", "pad", "vocal", "fx"],
16120
+ drums: "build"
13903
16121
  },
13904
16122
  {
13905
16123
  name: "Chorus",
@@ -13944,6 +16162,7 @@ var kpop = {
13944
16162
  kick: "X..x..x.X.x.....X..x..x.X.....o.",
13945
16163
  snare: "....X.......X.......X.......X...",
13946
16164
  clap: "....x.......x.......x.......x...",
16165
+ snap: "....g.......g..o....g.......g...",
13947
16166
  hatClosed: "o.x.o.xxo.x.o.x.o.x.o.xxo.x.o...",
13948
16167
  hatOpen: "..............................x."
13949
16168
  }
@@ -13977,18 +16196,29 @@ var kpop = {
13977
16196
  kick: "X..X..X...X.X...",
13978
16197
  snare: "....X.......X...",
13979
16198
  clap: "....x..x....x..x",
16199
+ snap: "....x.......x...",
13980
16200
  hatClosed: "..x...x...x...x."
13981
16201
  }
16202
+ },
16203
+ {
16204
+ id: "build",
16205
+ name: "Build: kick out, snare 4ths > 8ths > 16ths",
16206
+ stepsPerBeat: 4,
16207
+ lanes: {
16208
+ snare: "o...o...o...o...x.x.x.x.xxxxXXXX",
16209
+ clap: "....o.......o.......x.......x...",
16210
+ hatClosed: "oooooooooooooooooxoxoxoxxxxxXXXX"
16211
+ }
13982
16212
  }
13983
16213
  ],
13984
16214
  fills: [
13985
16215
  {
13986
16216
  id: "riser-roll",
13987
16217
  everyBars: 8,
13988
- stepsPerBeat: 4,
13989
- steps: 8,
13990
- lanes: { snare: "x.x.xxxx", clap: "......XX" },
13991
- note: "Pre-chorus end: snare roll, kick out."
16218
+ stepsPerBeat: 8,
16219
+ steps: 24,
16220
+ lanes: { snare: "o.o.x.x.xxxxXXXX........" },
16221
+ note: "Snare roll, kick out, last beat silent."
13992
16222
  },
13993
16223
  {
13994
16224
  id: "drop-gap",
@@ -14000,8 +16230,8 @@ var kpop = {
14000
16230
  }
14001
16231
  ],
14002
16232
  rules: [
14003
- "One groove per section: verse, trap rap, dance chorus, jersey dance break.",
14004
- "Stack snare+clap(+snap); tight, sample-replaced; hits mark choreography."
16233
+ "One groove per section (verse, build, trap, chorus, jersey).",
16234
+ "Stack snare+clap+snap; hits mark choreography."
14005
16235
  ]
14006
16236
  },
14007
16237
  bass: {
@@ -14030,7 +16260,7 @@ var kpop = {
14030
16260
  velocity: 110
14031
16261
  }
14032
16262
  ],
14033
- notes: "Synth bass on the dance kick; distorted 808 with slides in rap/trap; out in the bridge."
16263
+ notes: "Synth bass on the dance kick; 808 in rap/trap."
14034
16264
  },
14035
16265
  harmony: {
14036
16266
  modes: [
@@ -14049,7 +16279,7 @@ var kpop = {
14049
16279
  chords: ["i", "bVII", "bVI", "V"],
14050
16280
  weight: 2,
14051
16281
  barsPerChord: 1,
14052
- use: "major V pulls into the hook"
16282
+ use: "major V into the hook"
14053
16283
  },
14054
16284
  {
14055
16285
  chords: ["IVmaj7", "V", "iii7", "vi"],
@@ -14065,28 +16295,24 @@ var kpop = {
14065
16295
  }
14066
16296
  ],
14067
16297
  extensions: ["add9", "maj7", "7", "sus4"],
14068
- voicing: "Close synth/brass stabs, pumping offbeats in the chorus; open piano+strings in the bridge.",
14069
- harmonicRhythm: "1 chord/bar; rap and dance break vamp; last chorus up 1-2 semitones."
16298
+ voicing: "Close synth/brass stabs; open piano+strings in the bridge.",
16299
+ harmonicRhythm: "1 chord/bar; rap and dance break vamp."
14070
16300
  },
14071
16301
  melody: {
14072
16302
  range: { low: 57, high: 79 },
14073
16303
  density: "dense",
14074
16304
  notesPerBar: [4, 12],
14075
- rhythm: "16th verses and rap; chorus repeats a short (often English) title word; post-chorus chant or chop.",
16305
+ rhythm: "16th verses and rap; a short title word repeats in the chorus.",
14076
16306
  motifs: [
14077
16307
  "One-word title hook repeated 4x",
14078
16308
  "Pre-chorus climbs to a held note",
14079
- "Post-chorus: wordless synth or vocal-chop riff"
16309
+ "Post-chorus vocal-chop riff"
14080
16310
  ]
14081
16311
  },
14082
16312
  palette: {
14083
16313
  drums: {
14084
- keywords: ["punchy kick", "clap", "snap", "trap hats"],
14085
- presets: [
14086
- "drumrack:Crystal Clear Kit",
14087
- "drumrack:Street Mind Kit",
14088
- "drumrack:707 Core Kit"
14089
- ]
16314
+ keywords: ["punchy kick", "snap", "trap hats"],
16315
+ presets: ["drumrack:Street Mind Kit", "drumrack:Clint West Kit"]
14090
16316
  },
14091
16317
  bass: {
14092
16318
  keywords: ["808", "synth bass", "sub"],
@@ -14106,29 +16332,40 @@ var kpop = {
14106
16332
  },
14107
16333
  lead: {
14108
16334
  keywords: ["vocal chop", "pluck", "saw lead"],
14109
- presets: ["wavetable:Formant Vox", "meld:PolyPlucks"]
16335
+ presets: ["sampler:Vocal Chop Ah", "meld:PolyPlucks"]
14110
16336
  },
14111
16337
  fx: { keywords: ["riser", "impact", "downlifter", "reverse cymbal"] }
14112
16338
  },
14113
16339
  starterClips: [
14114
16340
  "Pop Riddim 124 bpm",
14115
- "Dancehall Trap 113 bpm",
14116
16341
  "LD Core Kit Pop 120 bpm",
14117
16342
  "Uplifting Club Vamp F Min 127 bpm"
14118
16343
  ],
14119
16344
  mix: {
14120
- lufs: { min: -9, max: -6.5 },
16345
+ lufs: { min: -11, max: -9 },
14121
16346
  tonalBalance: {
14122
16347
  p10: [-16.5, -9, -8.5, -11, -13, -14.5, -15.5, -16.5, -19, -26.5],
14123
16348
  p50: [-11.5, -5.5, -5.5, -8, -10, -11.5, -12.5, -13.5, -15, -21.5],
14124
16349
  p90: [-6.5, -1.5, -2.5, -5, -7, -8.5, -9.5, -10.5, -11, -16.5],
14125
16350
  source: TONAL_BALANCE_SOURCE
14126
16351
  },
14127
- sidechain: "Kick ducks pads/stabs (Compressor sidechain) and the 808.",
16352
+ sidechain: {
16353
+ ducks: [
16354
+ {
16355
+ trigger: "kick",
16356
+ targets: ["pad", "chords", "bass"],
16357
+ ratio: 4,
16358
+ attackMs: 1,
16359
+ releaseMs: 150,
16360
+ depthDb: 5
16361
+ }
16362
+ ],
16363
+ note: "Chorus pump; measured dip >= 1 dB."
16364
+ },
14128
16365
  reverb: "Bright vocal plate, dry rap; hall only in the bridge.",
14129
- delay: "Dotted-1/8 throws on hook words and ad-libs.",
14130
- stereo: "Wide doubles and gang vocals in the chorus; rap centred.",
14131
- lowEnd: "Mono sub; Saturator on the 808; Glue Compressor + Limiter on the master."
16366
+ delay: "Dotted-1/8 throws on hook words.",
16367
+ stereo: "Wide gang vocals in the chorus; rap centred.",
16368
+ lowEnd: "Mono sub; Saturator on the 808; gentle master Glue, 1 dB true-peak headroom."
14132
16369
  },
14133
16370
  arrangement: {
14134
16371
  variationEveryBars: 4,
@@ -14136,7 +16373,7 @@ var kpop = {
14136
16373
  {
14137
16374
  kind: "build",
14138
16375
  where: "Pre-chorus",
14139
- how: "Kick out, snare roll, Auto Filter sweep, riser."
16376
+ how: "Kick out (build pattern), snare roll, Auto Filter sweep, riser; 4-6 dB under the chorus; last beat silent."
14140
16377
  },
14141
16378
  {
14142
16379
  kind: "breakdown",
@@ -14146,7 +16383,7 @@ var kpop = {
14146
16383
  {
14147
16384
  kind: "drop",
14148
16385
  where: "Dance break",
14149
- how: "Instrumental beat switch for the choreography."
16386
+ how: "Instrumental beat switch."
14150
16387
  },
14151
16388
  {
14152
16389
  kind: "key-change",
@@ -14156,7 +16393,7 @@ var kpop = {
14156
16393
  ]
14157
16394
  },
14158
16395
  references: ["BTS", "BLACKPINK", "TWICE", "Stray Kids", "NewJeans"],
14159
- why: "A showcase: each section features another member and genre, so the groove keeps switching while one simple title hook holds the song together; builds, stops and the dance break frame the choreography."
16396
+ why: "The groove switches genre per section while one short title hook holds the song; builds, stops and the dance break frame the choreography."
14160
16397
  };
14161
16398
 
14162
16399
  // ../../src/components/editor-ai/style/cards/lofi-hip-hop.ts
@@ -14429,7 +16666,19 @@ var lofiHipHop = {
14429
16666
  p90: [-8.5, -2.5, -2, -4, -6, -8.5, -11.5, -15.5, -19.5, -28.5],
14430
16667
  source: TONAL_BALANCE_SOURCE
14431
16668
  },
14432
- sidechain: null,
16669
+ sidechain: {
16670
+ ducks: [
16671
+ {
16672
+ trigger: "kick",
16673
+ targets: ["keys", "pad"],
16674
+ ratio: 3,
16675
+ attackMs: 5,
16676
+ releaseMs: 180,
16677
+ depthDb: 3
16678
+ }
16679
+ ],
16680
+ note: "Gentle breathing: keys and pad dip with the kick."
16681
+ },
14433
16682
  reverb: "Small dark room on drums, short plate on keys; low wet levels.",
14434
16683
  delay: "Tape-style 1/8 dotted on chops/lead, low feedback, filtered repeats.",
14435
16684
  stereo: "Drums mostly mono; keys and textures wide.",
@@ -14816,7 +17065,19 @@ var mandopopUptempo = {
14816
17065
  p90: [-8, -2.5, -3, -5, -6.5, -8, -9, -10.5, -11.5, -17],
14817
17066
  source: TONAL_BALANCE_SOURCE
14818
17067
  },
14819
- sidechain: "Light kick ducking on pads and stabs in the chorus.",
17068
+ sidechain: {
17069
+ ducks: [
17070
+ {
17071
+ trigger: "kick",
17072
+ targets: ["pad", "chords"],
17073
+ ratio: 3,
17074
+ attackMs: 1,
17075
+ releaseMs: 150,
17076
+ depthDb: 3
17077
+ }
17078
+ ],
17079
+ note: "Light pump, chorus only."
17080
+ },
14820
17081
  reverb: "Plate on vocal and snare; hall only on the breakdown.",
14821
17082
  delay: "1/8 vocal echo under the chorus; 1/4 throws.",
14822
17083
  stereo: "Vocal loud and centred; doubles wide in the chorus.",
@@ -14878,7 +17139,7 @@ var modernPop = {
14878
17139
  feel: {
14879
17140
  kind: "straight",
14880
17141
  swingPercent: 50,
14881
- note: "Tight and quantized; small swing (swing16-54) only on hats if it feels stiff."
17142
+ note: "Tight, quantized; swing16-54 on hats only if stiff."
14882
17143
  },
14883
17144
  keys: ["C major", "G major", "D major", "A minor", "F# minor"],
14884
17145
  forms: [
@@ -15081,13 +17342,13 @@ var modernPop = {
15081
17342
  stepsPerBeat: 4,
15082
17343
  steps: 4,
15083
17344
  lanes: {},
15084
- note: "One beat of silence before the chorus (alternative to the snare build)."
17345
+ note: "A silent beat before the chorus."
15085
17346
  }
15086
17347
  ],
15087
17348
  rules: [
15088
17349
  "Pre-chorus builds: snare 8ths -> 16ths over the last 2 bars, or pull the kick out.",
15089
17350
  "Chorus adds a layer the verse lacks (claps, open hats, tambourine).",
15090
- "Keep velocities consistent; pop drums are tight, punchy and sample-replaced."
17351
+ "Tight, consistent velocities; punchy, sample-replaced."
15091
17352
  ]
15092
17353
  },
15093
17354
  bass: {
@@ -15109,7 +17370,7 @@ var modernPop = {
15109
17370
  velocity: 104
15110
17371
  }
15111
17372
  ],
15112
- notes: "Synth or picked bass doubling the kick on the chorus; plays roots, octave pops for lift."
17373
+ notes: "Synth or picked bass doubling the chorus kick; roots, octave pops for lift."
15113
17374
  },
15114
17375
  harmony: {
15115
17376
  modes: [
@@ -15145,7 +17406,7 @@ var modernPop = {
15145
17406
  }
15146
17407
  ],
15147
17408
  extensions: ["add9", "sus2", "sus4", "maj7"],
15148
- voicing: "Open triads with added 9ths; pads hold, piano/guitar plays 8th-note pulses; the chorus voicing moves up an octave.",
17409
+ voicing: "Open triads with add9; pads hold, piano/guitar pulses 8ths; chorus voicing an octave up.",
15149
17410
  harmonicRhythm: "1 chord per bar; same loop through verse and chorus, with energy doing the work."
15150
17411
  },
15151
17412
  melody: {
@@ -15214,11 +17475,23 @@ var modernPop = {
15214
17475
  p90: [-7.5, -2, -2.5, -5, -7, -8.5, -10, -11.5, -12.5, -18.5],
15215
17476
  source: TONAL_BALANCE_SOURCE
15216
17477
  },
15217
- sidechain: "Light kick ducking on pads/synths in dance-pop; none in ballads.",
17478
+ sidechain: {
17479
+ ducks: [
17480
+ {
17481
+ trigger: "kick",
17482
+ targets: ["pad", "chords"],
17483
+ ratio: 3,
17484
+ attackMs: 1,
17485
+ releaseMs: 150,
17486
+ depthDb: 3
17487
+ }
17488
+ ],
17489
+ note: "Light pump in dance-pop; none in ballads."
17490
+ },
15218
17491
  reverb: "Vocal plate, short room on drums; long reverb only as throws.",
15219
17492
  delay: "1/4 and 1/8 vocal throws at phrase ends.",
15220
- stereo: "Doubles/backing vocals and synths wide in the chorus, narrower verse; low end mono.",
15221
- lowEnd: "Kick and bass locked; highpass everything else around 100-150 Hz."
17493
+ stereo: "Doubles and synths wide in the chorus, narrower verse; low end mono.",
17494
+ lowEnd: "Kick and bass locked; highpass the rest at 100-150 Hz."
15222
17495
  },
15223
17496
  arrangement: {
15224
17497
  variationEveryBars: 8,
@@ -15257,7 +17530,7 @@ var modernPop = {
15257
17530
  "The Weeknd",
15258
17531
  "Ed Sheeran"
15259
17532
  ],
15260
- why: "Pop is built around one memorable hook. The chords barely change; the song gets its arc from contrast - sparse verses, a pre-chorus that builds tension, and a chorus that is louder, wider and higher. Every 8 bars something new arrives so attention never drops."
17533
+ why: "One memorable hook over barely-changing chords; the arc comes from contrast: sparse verses, a building pre-chorus, a louder, wider, higher chorus, something new every 8 bars."
15261
17534
  };
15262
17535
 
15263
17536
  // ../../src/components/editor-ai/style/cards/neo-soul.ts
@@ -15487,7 +17760,19 @@ var neoSoul = {
15487
17760
  p90: [-12.5, -3.5, -2.5, -3.5, -5.5, -8.5, -10.5, -13.5, -16.5, -23.5],
15488
17761
  source: TONAL_BALANCE_SOURCE
15489
17762
  },
15490
- sidechain: "Minimal kick-to-bass Compressor only if held notes mask the kick; avoid obvious pumping.",
17763
+ sidechain: {
17764
+ ducks: [
17765
+ {
17766
+ trigger: "kick",
17767
+ targets: ["bass"],
17768
+ ratio: 3,
17769
+ attackMs: 1,
17770
+ releaseMs: 80,
17771
+ depthDb: 1.5
17772
+ }
17773
+ ],
17774
+ note: "Only if held bass masks the kick; no audible pumping."
17775
+ },
15491
17776
  reverb: "Short dark Reverb room around the band; longer vocal plate audible only between phrases.",
15492
17777
  delay: "Filtered Echo throws on selected vocal endings.",
15493
17778
  stereo: "Keep lead and bass close and centered; electric piano medium-wide, backing vocals wider.",
@@ -15759,7 +18044,7 @@ var reggae = {
15759
18044
  p90: [-9.1, -1.1, -2.1, -5.1, -8.1, -10.1, -12.1, -14.1, -17.1, -23.1],
15760
18045
  source: TONAL_BALANCE_SOURCE
15761
18046
  },
15762
- sidechain: "Usually none; bass and sparse kick should coexist by arrangement and EQ Eight, not audible pumping.",
18047
+ sidechain: null,
15763
18048
  reverb: "Reverb on rim/organ for a short spring-like splash; EQ Eight cuts lows on the wet return.",
15764
18049
  delay: "Echo filtered tape-style throws; automate sends on isolated words or rim hits, then mute the dry source for dub space.",
15765
18050
  stereo: "Bass, kick and vocal centered; skank/organ apart; dub returns may travel wider.",
@@ -16023,7 +18308,19 @@ var reggaeton = {
16023
18308
  p90: [-6.6, -0.6, -3.6, -6.6, -8.6, -9.6, -9.6, -10.6, -11.6, -16.6],
16024
18309
  source: TONAL_BALANCE_SOURCE
16025
18310
  },
16026
- sidechain: "Compressor briefly ducks sub under kick; release before the next tresillo accent.",
18311
+ sidechain: {
18312
+ ducks: [
18313
+ {
18314
+ trigger: "kick",
18315
+ targets: ["bass"],
18316
+ ratio: 4,
18317
+ attackMs: 0.5,
18318
+ releaseMs: 60,
18319
+ depthDb: 3
18320
+ }
18321
+ ],
18322
+ note: "Released before the next tresillo accent."
18323
+ },
16027
18324
  reverb: "EQ Eight and Compressor keep the vocal forward; short Reverb on snare, little on bass.",
16028
18325
  delay: "Echo on hook endings with filtered repeats, ducked or muted under the next line.",
16029
18326
  stereo: "Center kick, snare, sub and lead; widen supporting plucks and vocal doubles only.",
@@ -16072,7 +18369,7 @@ var rnb = {
16072
18369
  kind: "swing",
16073
18370
  swingPercent: 57,
16074
18371
  grooveId: "swing16-57",
16075
- note: "Laid back: swung 16th hats and ghosts, snare a hair late, kick on the grid. Often programmed at double tempo with the snare on 3 - same feel."
18372
+ note: "Laid back: swung 16th hats and ghosts, snare a hair late, kick on the grid (double-time, snare on 3: same feel)."
16076
18373
  },
16077
18374
  keys: ["Eb major", "Ab major", "Db major", "F minor", "C minor"],
16078
18375
  forms: [
@@ -16213,7 +18510,7 @@ var rnb = {
16213
18510
  stepsPerBeat: 4,
16214
18511
  lanes: {
16215
18512
  kick: "X.........x.....",
16216
- clap: "....x.......x...",
18513
+ snap: "....x.......x...",
16217
18514
  hatClosed: "x..x..x.o..x..x."
16218
18515
  }
16219
18516
  }
@@ -16300,7 +18597,7 @@ var rnb = {
16300
18597
  { chords: ["vi9", "ii9", "Imaj7", "IVmaj7"], weight: 1, barsPerChord: 1 }
16301
18598
  ],
16302
18599
  extensions: ["maj7", "maj9", "9", "11", "13", "7sus4"],
16303
- voicing: "Rootless 4-note keys voicings (3-5-7-9 or 7-9-3-5) around middle C, spread pads above; chords often land a 16th early.",
18600
+ voicing: "Rootless 4-note keys (3-5-7-9 or 7-9-3-5) around middle C, pads above; chords often land a 16th early.",
16304
18601
  harmonicRhythm: "1 chord per bar, or a 2-chord vamp of 2 bars each."
16305
18602
  },
16306
18603
  melody: {
@@ -16369,11 +18666,23 @@ var rnb = {
16369
18666
  p90: [-5.5, -1, -4, -6.5, -8, -9.5, -10.5, -12, -13, -18.5],
16370
18667
  source: TONAL_BALANCE_SOURCE
16371
18668
  },
16372
- sidechain: "Duck the sub under the kick when both sustain, or let the 808 be the kick.",
18669
+ sidechain: {
18670
+ ducks: [
18671
+ {
18672
+ trigger: "kick",
18673
+ targets: ["bass"],
18674
+ ratio: 3,
18675
+ attackMs: 1,
18676
+ releaseMs: 80,
18677
+ depthDb: 3
18678
+ }
18679
+ ],
18680
+ note: "Where kick and sub sustain together; or let the 808 be the kick."
18681
+ },
16373
18682
  reverb: "Plate on the lead vocal, long hall on ad-libs and stacks; drums mostly dry.",
16374
18683
  delay: "1/4 and dotted-1/8 vocal throws, filtered; ping-pong on ad-libs.",
16375
18684
  stereo: "Lead vocal centred and intimate, harmony stack and keys wide.",
16376
- lowEnd: "The sub (30-60 Hz) carries the weight; Roar or Pedal adds harmonics so it reads on phones. Chain: vocal EQ Eight > Compressor > Reverb/Echo; keys Chorus; master Glue Compressor > Limiter."
18685
+ lowEnd: "The sub (30-60 Hz) carries the weight; Roar or Pedal adds harmonics so it reads on phones. Chain: vocal EQ Eight > Compressor > Reverb/Echo; keys Chorus; gentle master Glue."
16377
18686
  },
16378
18687
  arrangement: {
16379
18688
  variationEveryBars: 4,
@@ -16406,7 +18715,7 @@ var rnb = {
16406
18715
  ]
16407
18716
  },
16408
18717
  references: ["SZA", "H.E.R.", "Daniel Caesar", "David Tao", "Khalil Fong"],
16409
- why: "R&B feels intimate because it leaves space: a slow, laid-back beat, a deep sub, and lush 7th and 9th chords that never fully resolve. The voice is the star, with runs, stacked harmonies and ad-libs; the beat stays simple so every vocal detail is heard."
18718
+ why: "R&B is intimate because it leaves space: a laid-back beat, a deep sub, 7th/9th chords that never fully resolve. The voice is the star (runs, stacks, ad-libs); the beat stays simple."
16410
18719
  };
16411
18720
 
16412
18721
  // ../../src/components/editor-ai/style/cards/synth-pop.ts
@@ -16674,7 +18983,19 @@ var synthPop = {
16674
18983
  p90: [-12.3, -3.3, -3.3, -5.3, -6.3, -6.3, -7.3, -9.3, -12.3, -18.3],
16675
18984
  source: TONAL_BALANCE_SOURCE
16676
18985
  },
16677
- sidechain: "Light Compressor duck on the pad if necessary; vocal phrasing, not kick pumping, drives the song.",
18986
+ sidechain: {
18987
+ ducks: [
18988
+ {
18989
+ trigger: "kick",
18990
+ targets: ["pad"],
18991
+ ratio: 2,
18992
+ attackMs: 2,
18993
+ releaseMs: 150,
18994
+ depthDb: 2
18995
+ }
18996
+ ],
18997
+ note: "Barely there: phrasing, not pumping, drives the song."
18998
+ },
16678
18999
  reverb: "Reverb plate on snare/vocal; Gate the snare return for a short retro bloom.",
16679
19000
  delay: "Echo eighths or slap on the hook, ducked below lead words.",
16680
19001
  stereo: "Chorus-Ensemble on pads; center the lead vocal, bass and main hook.",
@@ -16960,7 +19281,19 @@ var synthwave = {
16960
19281
  p90: [-10.2, -2.2, -2.2, -5.2, -7.2, -8.2, -9.2, -11.2, -14.2, -20.2],
16961
19282
  source: TONAL_BALANCE_SOURCE
16962
19283
  },
16963
- sidechain: "Compressor ducks bass and pads lightly to each kick; a breath, not a modern EDM vacuum.",
19284
+ sidechain: {
19285
+ ducks: [
19286
+ {
19287
+ trigger: "kick",
19288
+ targets: ["bass", "pad"],
19289
+ ratio: 3,
19290
+ attackMs: 2,
19291
+ releaseMs: 160,
19292
+ depthDb: 3
19293
+ }
19294
+ ],
19295
+ note: "A breath, not an EDM vacuum."
19296
+ },
16964
19297
  reverb: "Reverb into Gate on the snare return; broad hall on pads.",
16965
19298
  delay: "Echo dotted eighth on arp gaps, lowpassed feedback.",
16966
19299
  stereo: "Chorus-Ensemble widens pads; lead and snare stay focused, sub mono.",
@@ -17247,7 +19580,19 @@ var techno = {
17247
19580
  p90: [-3.5, 0, -3.5, -7.5, -10.5, -11.5, -12.5, -13.5, -14.5, -20.5],
17248
19581
  source: TONAL_BALANCE_SOURCE
17249
19582
  },
17250
- sidechain: "Compressor ducks the rumble return strongly from the dry kick, recovering before the next beat.",
19583
+ sidechain: {
19584
+ ducks: [
19585
+ {
19586
+ trigger: "kick",
19587
+ targets: ["bass"],
19588
+ ratio: 6,
19589
+ attackMs: 0.5,
19590
+ releaseMs: 150,
19591
+ depthDb: 8
19592
+ }
19593
+ ],
19594
+ note: "The rumble ducks hard, back before the next kick."
19595
+ },
17251
19596
  reverb: "Separate Reverb rumble return, then EQ Eight to remove highs and subsonic build-up; dry kick remains clear.",
17252
19597
  delay: "Filtered dotted-eighth Echo on stabs; automate feedback at boundaries.",
17253
19598
  stereo: "Kick and rumble mono; higher percussion and dub reflections can spread.",
@@ -17507,14 +19852,26 @@ var trance = {
17507
19852
  },
17508
19853
  starterClips: ["909 Core Kit Off Hat 125 bpm", "Techno Heavy 133 bpm"],
17509
19854
  mix: {
17510
- lufs: { min: -9, max: -6 },
19855
+ lufs: { min: -10, max: -8 },
17511
19856
  tonalBalance: {
17512
19857
  p10: [-18, -9, -9, -13, -15, -15, -15, -16, -19, -26],
17513
19858
  p50: [-13, -5, -6, -10, -12, -12, -12, -13, -15, -21],
17514
19859
  p90: [-8, -1, -3, -7, -9, -9, -9, -10, -11, -16],
17515
19860
  source: TONAL_BALANCE_SOURCE
17516
19861
  },
17517
- sidechain: "Compressor ducks bass, pad and lead reverb to the kick; release recovers into the offbeat.",
19862
+ sidechain: {
19863
+ ducks: [
19864
+ {
19865
+ trigger: "kick",
19866
+ targets: ["bass", "pad"],
19867
+ ratio: 4,
19868
+ attackMs: 1,
19869
+ releaseMs: 140,
19870
+ depthDb: 5
19871
+ }
19872
+ ],
19873
+ note: "Bass and pad recover into the offbeat."
19874
+ },
17518
19875
  reverb: "Long Reverb hall in breakdowns; highpass the return and reduce it at the drop.",
17519
19876
  delay: "Echo dotted eighth on the lead, filtered so the dry hook stays legible.",
17520
19877
  stereo: "Wide detuned lead and pad above the lows; kick/sub mono. Check the hook in mono.",
@@ -17820,14 +20177,26 @@ var trap = {
17820
20177
  "Sinister Run G Min 142 bpm"
17821
20178
  ],
17822
20179
  mix: {
17823
- lufs: { min: -9, max: -6 },
20180
+ lufs: { min: -10, max: -8 },
17824
20181
  tonalBalance: {
17825
20182
  p10: [-12.5, -8, -10, -13, -15.5, -17, -18, -19, -21.5, -28.5],
17826
20183
  p50: [-7.5, -4, -7, -10, -12.5, -14, -15, -16, -17.5, -23.5],
17827
20184
  p90: [-2.5, 0, -4, -7, -9.5, -11, -12, -13, -13.5, -18.5],
17828
20185
  source: TONAL_BALANCE_SOURCE
17829
20186
  },
17830
- sidechain: "Duck the 808 by the kick (fast, short) or arrange them never to clash.",
20187
+ sidechain: {
20188
+ ducks: [
20189
+ {
20190
+ trigger: "kick",
20191
+ targets: ["bass"],
20192
+ ratio: 4,
20193
+ attackMs: 0.5,
20194
+ releaseMs: 50,
20195
+ depthDb: 4
20196
+ }
20197
+ ],
20198
+ note: "Fast and short, or arrange 808 and kick never to clash."
20199
+ },
17831
20200
  reverb: "Big dark reverb on the melody; vocal upfront and nearly dry (short room, de-essed); drums dry.",
17832
20201
  delay: "1/4 or 1/8 throws on vocal ends and melody.",
17833
20202
  stereo: "Melody/pads wide, 808/kick/clap mono, hats slightly off-center.",
@@ -18114,7 +20483,19 @@ var ukGarage = {
18114
20483
  p90: [-5.7, -0.7, -2.7, -6.7, -9.7, -10.7, -10.7, -11.7, -12.7, -17.7],
18115
20484
  source: TONAL_BALANCE_SOURCE
18116
20485
  },
18117
- sidechain: "Short Compressor duck only where kick meets bass; keep the long gaps full.",
20486
+ sidechain: {
20487
+ ducks: [
20488
+ {
20489
+ trigger: "kick",
20490
+ targets: ["bass"],
20491
+ ratio: 3,
20492
+ attackMs: 1,
20493
+ releaseMs: 60,
20494
+ depthDb: 2
20495
+ }
20496
+ ],
20497
+ note: "Clears the kick; keep the long gaps full."
20498
+ },
18118
20499
  reverb: "Small Reverb room on clap/rim; a darker longer return for isolated vocal chops.",
18119
20500
  delay: "Filtered Echo dotted eighth on chops; silence the feedback before busy hook entries.",
18120
20501
  stereo: "Vocal chops and hats can answer left/right; kick, snare and sub stay centered.",
@@ -18237,14 +20618,16 @@ async function dispatch(command, io) {
18237
20618
  const text = HELP[command.topic ?? "main"] ?? HELP.main;
18238
20619
  io.stdout.write(
18239
20620
  `${text}
18240
- ${command.topic && command.topic !== "setup" && command.topic !== "skills" ? `
20621
+ ${command.topic && !["setup", "skills", "login", "logout", "whoami"].includes(
20622
+ command.topic
20623
+ ) ? `
18241
20624
  ${HELP.bridge}
18242
20625
  ` : ""}`
18243
20626
  );
18244
20627
  return EXIT_OK;
18245
20628
  }
18246
20629
  case "version":
18247
- io.stdout.write(`${VERSION}
20630
+ io.stdout.write(`${VERSION2}
18248
20631
  `);
18249
20632
  return EXIT_OK;
18250
20633
  case "check":
@@ -18257,6 +20640,15 @@ ${HELP.bridge}
18257
20640
  return mcp(command.bridge, io);
18258
20641
  case "doctor":
18259
20642
  return doctor(command, io);
20643
+ case "login":
20644
+ return login(command, io);
20645
+ case "logout":
20646
+ return logout(command.account, io);
20647
+ case "whoami":
20648
+ return whoami(command, io);
20649
+ case "login-poll":
20650
+ await runLoginPoller(command.file, createLogger("login", false));
20651
+ return EXIT_OK;
18260
20652
  case "setup":
18261
20653
  return setup(command, io);
18262
20654
  case "skills-install": {
@@ -18319,33 +20711,62 @@ async function runJob(job, flags, mode, io, timeoutS) {
18319
20711
  const onSigint = () => abort.abort();
18320
20712
  process.once("SIGINT", onSigint);
18321
20713
  const onProgress = progressPrinter(io, flags.verbose);
20714
+ const auth = await jobAuth(accountOrigin(flags.origin), "cli", log);
20715
+ if (auth.ended)
20716
+ io.stderr.write(`tutticode: ${auth.ended}
20717
+ `);
18322
20718
  try {
20719
+ let outcome;
18323
20720
  if (flags.noDaemon || io.openPage || !io.entry) {
18324
20721
  const session = newSession(flags, mode, io, log);
18325
20722
  try {
18326
- return await session.run(job, {
20723
+ outcome = await session.run(job, {
18327
20724
  timeoutMs: timeoutS * 1e3,
18328
20725
  signal: abort.signal,
18329
- onProgress
20726
+ onProgress,
20727
+ auth: auth.auth
18330
20728
  });
18331
20729
  } finally {
18332
20730
  await session.close();
18333
20731
  }
18334
- }
18335
- return await runViaDaemon(
18336
- { config: daemonConfig(flags, mode), entry: io.entry, log },
18337
- job,
18338
- timeoutS * 1e3,
18339
- {
18340
- signal: abort.signal,
18341
- onProgress,
18342
- onStatus: statusWriter(flags, mode, io)
18343
- }
18344
- );
20732
+ } else
20733
+ outcome = await runViaDaemon(
20734
+ { config: daemonConfig(flags, mode), entry: io.entry, log },
20735
+ job,
20736
+ timeoutS * 1e3,
20737
+ {
20738
+ signal: abort.signal,
20739
+ onProgress,
20740
+ onStatus: statusWriter(flags, mode, io)
20741
+ },
20742
+ auth.auth
20743
+ );
20744
+ if (outcome.auth?.rejected)
20745
+ io.stderr.write(`tutticode: ${rejectedLine(outcome.auth)}
20746
+ `);
20747
+ return outcome;
18345
20748
  } finally {
18346
20749
  process.removeListener("SIGINT", onSigint);
18347
20750
  }
18348
20751
  }
20752
+ function accountOrigin(origin) {
20753
+ return normalizeOrigin(origin ?? "") ?? BRIDGE_ORIGINS[0];
20754
+ }
20755
+ async function jobAuth(origin, client, log) {
20756
+ try {
20757
+ const token = await resolveToken(origin);
20758
+ return {
20759
+ auth: bridgeAuth(token, client),
20760
+ ...token.ended ? { ended: token.ended } : {}
20761
+ };
20762
+ } catch (e) {
20763
+ log("warn", `sign-in unavailable: ${e.message}`);
20764
+ return { auth: bridgeAuth({ kind: "anonymous" }, client) };
20765
+ }
20766
+ }
20767
+ function rejectedLine(auth) {
20768
+ return `the page did not accept your sign-in (${auth.rejected}); this ran without sign-in. Run \`tutticode login\`.`;
20769
+ }
18349
20770
  function progressPrinter(io, verbose) {
18350
20771
  let last = "";
18351
20772
  return (p) => {
@@ -18358,27 +20779,32 @@ function progressPrinter(io, verbose) {
18358
20779
  last = line;
18359
20780
  };
18360
20781
  }
18361
- async function readProgram(file, io) {
18362
- if (file === "-") {
20782
+ async function readProgram(file2, io) {
20783
+ if (file2 === "-") {
18363
20784
  const chunks = [];
18364
20785
  for await (const chunk of io.stdin)
18365
20786
  chunks.push(Buffer.from(chunk));
18366
20787
  return Buffer.concat(chunks).toString("utf8");
18367
20788
  }
18368
20789
  try {
18369
- return (0, import_node_fs8.readFileSync)(file, "utf8");
20790
+ return (0, import_node_fs11.readFileSync)(file2, "utf8");
18370
20791
  } catch (e) {
18371
20792
  throw new UsageError(
18372
- `Cannot read ${file}: ${e.code ?? e.message}`
20793
+ `Cannot read ${file2}: ${e.code ?? e.message}`
18373
20794
  );
18374
20795
  }
18375
20796
  }
18376
- function printError(outcome, json, file, io) {
20797
+ function printError(outcome, json, file2, io) {
18377
20798
  if (json)
18378
20799
  io.stdout.write(
18379
20800
  `${JSON.stringify(
18380
20801
  {
18381
- error: { code: outcome.code, message: outcome.message },
20802
+ error: {
20803
+ code: outcome.code,
20804
+ message: outcome.message,
20805
+ ...outcome.limit ? { limit: outcome.limit } : {}
20806
+ },
20807
+ ...outcome.code === "limit" ? { loginHint: { command: TUTTICODE_LOGIN_HINT_COMMAND } } : {},
18382
20808
  ...outcome.report ? { report: outcome.report } : {}
18383
20809
  },
18384
20810
  null,
@@ -18387,7 +20813,7 @@ function printError(outcome, json, file, io) {
18387
20813
  `
18388
20814
  );
18389
20815
  if (outcome.report)
18390
- for (const line of formatDiagnostics(outcome.report, file))
20816
+ for (const line of formatDiagnostics(outcome.report, file2))
18391
20817
  io.stderr.write(`${line}
18392
20818
  `);
18393
20819
  io.stderr.write(`tutticode: ${outcome.code}: ${outcome.message}
@@ -18399,7 +20825,7 @@ async function check(command, io) {
18399
20825
  const outcome = await runJob(
18400
20826
  {
18401
20827
  type: "check",
18402
- id: (0, import_node_crypto2.randomUUID)(),
20828
+ id: (0, import_node_crypto3.randomUUID)(),
18403
20829
  code,
18404
20830
  ...command.bpm !== void 0 ? { bpm: command.bpm } : {}
18405
20831
  },
@@ -18430,8 +20856,15 @@ async function check(command, io) {
18430
20856
  }
18431
20857
  async function render(command, io) {
18432
20858
  const code = await readProgram(command.file, io);
20859
+ const brandTagOff = command.options.brandTag === false || process.env.TUTTICODE_BRAND_TAG === "0";
18433
20860
  const outcome = await runJob(
18434
- { type: "render", id: (0, import_node_crypto2.randomUUID)(), code, ...command.options },
20861
+ {
20862
+ type: "render",
20863
+ id: (0, import_node_crypto3.randomUUID)(),
20864
+ code,
20865
+ ...command.options,
20866
+ ...brandTagOff ? { brandTag: false } : {}
20867
+ },
18435
20868
  command.bridge,
18436
20869
  modeFor(command.bridge, io),
18437
20870
  io,
@@ -18462,10 +20895,45 @@ async function render(command, io) {
18462
20895
  io.stdout.write(`${line}
18463
20896
  `);
18464
20897
  }
20898
+ if (outcome.meta.watermark)
20899
+ io.stderr.write(`${WATERMARK_LINE}
20900
+ `);
20901
+ const anonymous = outcome.auth ? !outcome.auth.signedIn : false;
20902
+ if (anonymous && brandTagOff && firstTime("brand-tag-anonymous", "install"))
20903
+ io.stderr.write(
20904
+ 'The "Made with TuttiCode" file tag can be turned off after `tutticode login`.\n'
20905
+ );
20906
+ if (anonymous && io.interactive) {
20907
+ if (firstTime("terms", "install"))
20908
+ io.stderr.write(
20909
+ `TuttiCode renders are subject to ${tutticodeTermsUrl(
20910
+ accountOrigin(command.bridge.origin)
20911
+ )}
20912
+ `
20913
+ );
20914
+ if (firstTime("sign-in-hint", "day"))
20915
+ io.stderr.write(
20916
+ "Sign in free for longer renders without the voice tag: tutticode login\n"
20917
+ );
20918
+ }
18465
20919
  return EXIT_OK;
18466
20920
  }
18467
20921
  async function open(command, io) {
18468
20922
  const code = await readProgram(command.file, io);
20923
+ const flags = command.bridge;
20924
+ const url = codeLink(
20925
+ normalizeOrigin(flags.origin ?? "") ?? BRIDGE_ORIGINS[0],
20926
+ flags.locale ?? "en",
20927
+ code
20928
+ );
20929
+ if (flags.show === false) {
20930
+ io.stdout.write(
20931
+ `No browser window was opened (--headless). Open the program in TuttiCode:
20932
+ ${url}
20933
+ `
20934
+ );
20935
+ return EXIT_OK;
20936
+ }
18469
20937
  const outcome = await runJob(
18470
20938
  { type: "open", code },
18471
20939
  command.bridge,
@@ -18480,20 +20948,23 @@ async function open(command, io) {
18480
20948
  command.file,
18481
20949
  io
18482
20950
  );
18483
- io.stdout.write("Opened in TuttiCode.\n");
20951
+ io.stdout.write(`Opened in TuttiCode: ${url}
20952
+ `);
18484
20953
  return EXIT_OK;
18485
20954
  }
18486
20955
  async function mcp(flags, io) {
18487
20956
  const log = createLogger("mcp", flags.verbose);
20957
+ const origin = accountOrigin(flags.origin);
18488
20958
  const { backend, close } = sessionBackend(
18489
20959
  (mode) => newSession(
18490
20960
  flags,
18491
20961
  flags.show ? "show" : mode,
18492
20962
  { ...io, stderr: process.stderr },
18493
20963
  log
18494
- )
20964
+ ),
20965
+ () => jobAuth(origin, "mcp", log)
18495
20966
  );
18496
- log("info", `mcp server ${VERSION} starting`);
20967
+ log("info", `mcp server ${VERSION2} starting`);
18497
20968
  await serveMcpStdio({
18498
20969
  input: io.stdin,
18499
20970
  output: io.stdout,
@@ -18506,7 +20977,8 @@ async function mcp(flags, io) {
18506
20977
  code
18507
20978
  ),
18508
20979
  // --headless: no window, not even for code_open
18509
- canShow: flags.show !== false
20980
+ canShow: flags.show !== false,
20981
+ account: () => accountState(origin)
18510
20982
  });
18511
20983
  await close();
18512
20984
  return EXIT_OK;
@@ -18590,14 +21062,313 @@ async function doctor(command, io) {
18590
21062
  )}) in ${info.browser.version ?? "the browser"}` : `${info.browser.version ?? "the browser"} accepted no Local Network Access grant (it may not enforce it)`
18591
21063
  );
18592
21064
  }
21065
+ await accountChecks(accountOrigin(flags.origin), command.fix, add);
18593
21066
  const ok = results.every((r2) => r2.ok || r2.check === "local network access");
18594
21067
  if (command.json)
18595
21068
  io.stdout.write(
18596
- `${JSON.stringify({ ok, version: VERSION, checks: results }, null, 2)}
21069
+ `${JSON.stringify({ ok, version: VERSION2, checks: results }, null, 2)}
18597
21070
  `
18598
21071
  );
18599
21072
  return ok ? EXIT_OK : EXIT_BRIDGE;
18600
21073
  }
21074
+ async function accountChecks(origin, fix, add) {
21075
+ const state = await accountState(origin);
21076
+ switch (state.state) {
21077
+ case "signed-in":
21078
+ add(
21079
+ "account",
21080
+ true,
21081
+ `signed in as ${signedInAs(state.me)}${state.tokenKind === "pat" ? " with TUTTICODE_TOKEN" : ""}`
21082
+ );
21083
+ break;
21084
+ case "anonymous":
21085
+ add("account", true, `not signed in: ${anonymousLimitsText()}`);
21086
+ break;
21087
+ case "rejected":
21088
+ add(
21089
+ "account",
21090
+ false,
21091
+ `sign-in rejected (${state.message}): run tutticode login`
21092
+ );
21093
+ break;
21094
+ case "unreachable":
21095
+ add("account", false, `could not check the sign-in: ${state.message}`);
21096
+ }
21097
+ if (envToken())
21098
+ add(
21099
+ "token override",
21100
+ true,
21101
+ "TUTTICODE_TOKEN is set: it wins over the stored sign-in"
21102
+ );
21103
+ const path = credentialsPath();
21104
+ if (!(0, import_node_fs11.existsSync)(path))
21105
+ return;
21106
+ let access = checkFileAccess(path);
21107
+ if (fix && access && !access.ok) {
21108
+ try {
21109
+ fixFileAccess(path);
21110
+ } catch (e) {
21111
+ if (!(e instanceof PrivateDirError))
21112
+ throw e;
21113
+ add("credentials folder", false, e.message);
21114
+ }
21115
+ access = checkFileAccess(path);
21116
+ }
21117
+ if (!access)
21118
+ add("credentials file", true, `${path} (permissions not checked)`);
21119
+ else
21120
+ add(
21121
+ "credentials file",
21122
+ access.ok,
21123
+ access.ok ? `${path}: ${access.detail}` : `${path} is too open (${access.detail}): run tutticode doctor --fix`
21124
+ );
21125
+ }
21126
+ function anonymousLimitsText() {
21127
+ const limits = accountLimits({ state: "anonymous" });
21128
+ return `renders up to ${limits.maxSeconds} s, a voice tag over ${limits.watermarkAfterSeconds} s (sign in free: tutticode login)`;
21129
+ }
21130
+ async function login(command, io) {
21131
+ const origin = accountOrigin(command.account.origin);
21132
+ const locale = command.account.locale ?? "en";
21133
+ ensurePrivateDir();
21134
+ if (!command.noWait && !io.interactive) {
21135
+ io.stderr.write(
21136
+ `tutticode: login needs a person: run it in a terminal, or use ${TUTTICODE_LOGIN_HINT_COMMAND}
21137
+ `
21138
+ );
21139
+ return EXIT_USAGE;
21140
+ }
21141
+ if (envToken())
21142
+ io.stderr.write(
21143
+ "Note: TUTTICODE_TOKEN is set; it wins over this sign-in until you unset it.\n"
21144
+ );
21145
+ const name = deviceName();
21146
+ const abort = new AbortController();
21147
+ const onSigint = () => abort.abort();
21148
+ process.once("SIGINT", onSigint);
21149
+ try {
21150
+ let tokens;
21151
+ if (!command.device) {
21152
+ const remote = !io.openBrowser && (!!process.env.SSH_CONNECTION || process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY);
21153
+ tokens = await loopbackLogin({
21154
+ origin,
21155
+ locale,
21156
+ deviceName: name,
21157
+ signal: abort.signal,
21158
+ openBrowser: io.openBrowser ?? (remote ? () => void 0 : openExternal),
21159
+ onUrl: (url) => io.stderr.write(
21160
+ remote ? `No browser can be opened here. Open this link in a browser on this computer:
21161
+ ${url}
21162
+ Or sign in with a code on any device: tutticode login --device
21163
+ ` : `Opening your browser to sign in. If it does not open, go to:
21164
+ ${url}
21165
+ `
21166
+ )
21167
+ });
21168
+ } else {
21169
+ const device = await startDeviceLogin(origin, name);
21170
+ const complete = device.verificationUriComplete ?? device.verificationUri;
21171
+ if (command.noWait) {
21172
+ const file2 = writePendingLogin({
21173
+ origin,
21174
+ deviceCode: device.deviceCode,
21175
+ userCode: device.userCode,
21176
+ verificationUri: device.verificationUri,
21177
+ ...device.verificationUriComplete ? { verificationUriComplete: device.verificationUriComplete } : {},
21178
+ expiresAt: new Date(
21179
+ Date.now() + device.expiresIn * 1e3
21180
+ ).toISOString(),
21181
+ interval: device.interval,
21182
+ deviceName: name
21183
+ });
21184
+ if (io.spawnLoginPoller)
21185
+ io.spawnLoginPoller(file2);
21186
+ else if (io.entry)
21187
+ spawnLoginPoller(io.entry, file2);
21188
+ else
21189
+ throw new Error("Cannot start the background sign-in.");
21190
+ if (command.json)
21191
+ io.stdout.write(
21192
+ `${JSON.stringify(
21193
+ {
21194
+ verificationUri: device.verificationUri,
21195
+ verificationUriComplete: complete,
21196
+ userCode: device.userCode,
21197
+ expiresIn: device.expiresIn
21198
+ },
21199
+ null,
21200
+ 2
21201
+ )}
21202
+ `
21203
+ );
21204
+ else
21205
+ io.stdout.write(
21206
+ `To sign in, open ${complete}
21207
+ (or open ${device.verificationUri} and enter the code ${device.userCode})
21208
+ within ${Math.round(
21209
+ device.expiresIn / 60
21210
+ )} minutes. tutticode finishes the sign-in in the background once it is approved; until then renders run without sign-in.
21211
+ `
21212
+ );
21213
+ return EXIT_OK;
21214
+ }
21215
+ io.stderr.write(
21216
+ `To sign in, open ${device.verificationUri} on any device and enter the code
21217
+
21218
+ ${device.userCode}
21219
+
21220
+ or open ${complete}
21221
+ or scan it with a phone:
21222
+
21223
+ ${renderUnicodeCompact(
21224
+ complete,
21225
+ { border: 1 }
21226
+ )}
21227
+
21228
+ Waiting for approval (the code expires in ${Math.round(
21229
+ device.expiresIn / 60
21230
+ )} minutes)...
21231
+ `
21232
+ );
21233
+ tokens = await pollDeviceToken(origin, device, {
21234
+ ...io.loginPoll,
21235
+ signal: abort.signal
21236
+ });
21237
+ }
21238
+ const state = await completeLogin(origin, tokens, name);
21239
+ io.stdout.write(
21240
+ state.state === "signed-in" ? `Signed in as ${signedInAs(state.me)}
21241
+ ` : "Signed in.\n"
21242
+ );
21243
+ return EXIT_OK;
21244
+ } catch (e) {
21245
+ if (e instanceof LoginError || e instanceof ApiUnreachable) {
21246
+ io.stderr.write(`tutticode: ${e.message}
21247
+ `);
21248
+ return EXIT_BRIDGE;
21249
+ }
21250
+ throw e;
21251
+ } finally {
21252
+ process.removeListener("SIGINT", onSigint);
21253
+ }
21254
+ }
21255
+ async function logout(flags, io) {
21256
+ const origin = accountOrigin(flags.origin);
21257
+ const accountPage = `${origin}${ACCOUNT_PATH}`;
21258
+ (0, import_node_fs11.rmSync)(pendingLoginPath(origin), { force: true });
21259
+ if (envToken())
21260
+ io.stderr.write(
21261
+ `TUTTICODE_TOKEN is set: it keeps signing tutticode in until you unset it. Revoke it on the account page: ${accountPage}
21262
+ `
21263
+ );
21264
+ const stored = readAccount(origin);
21265
+ if (!stored) {
21266
+ io.stdout.write(`Not signed in on this computer (${origin}).
21267
+ `);
21268
+ return EXIT_OK;
21269
+ }
21270
+ const revoked = await revokeToken(origin, stored.refreshToken);
21271
+ await saveAccount(origin, void 0);
21272
+ if (!revoked)
21273
+ io.stderr.write(
21274
+ `Warning: ${origin} could not be reached to end the sign-in; it is deleted here. Revoke this device on the account page: ${accountPage}
21275
+ `
21276
+ );
21277
+ io.stdout.write(
21278
+ `Signed out${stored.email ? ` (${stored.email})` : ""}. Other devices and access tokens: ${accountPage}
21279
+ `
21280
+ );
21281
+ return EXIT_OK;
21282
+ }
21283
+ async function whoami(command, io) {
21284
+ const origin = accountOrigin(command.account.origin);
21285
+ const state = await accountState(origin);
21286
+ const pending = state.state === "signed-in" ? void 0 : pendingLogin(origin);
21287
+ if (command.json)
21288
+ io.stdout.write(`${JSON.stringify(whoamiJson(state, pending), null, 2)}
21289
+ `);
21290
+ else
21291
+ for (const line of whoamiLines(state, pending))
21292
+ io.stdout.write(`${line}
21293
+ `);
21294
+ if (state.state === "signed-in")
21295
+ return EXIT_OK;
21296
+ return state.state === "anonymous" ? EXIT_ANONYMOUS : EXIT_BRIDGE;
21297
+ }
21298
+ function whoamiJson(state, pending) {
21299
+ const pendingJson = pending ? {
21300
+ pendingLogin: {
21301
+ userCode: pending.userCode,
21302
+ verificationUri: pending.verificationUri,
21303
+ expiresAt: pending.expiresAt
21304
+ }
21305
+ } : {};
21306
+ switch (state.state) {
21307
+ case "signed-in": {
21308
+ const { me } = state;
21309
+ return {
21310
+ signedIn: true,
21311
+ tokenKind: state.tokenKind,
21312
+ user: me.user,
21313
+ plan: me.plan,
21314
+ limits: me.limits,
21315
+ usageToday: me.usageToday,
21316
+ expiresAt: me.expiresAt,
21317
+ deviceName: me.deviceName,
21318
+ ...me.termsUpdate ? { termsUpdate: me.termsUpdate } : {}
21319
+ };
21320
+ }
21321
+ case "anonymous":
21322
+ return {
21323
+ signedIn: false,
21324
+ tokenKind: "anonymous",
21325
+ limits: accountLimits(state),
21326
+ loginHint: LOGIN_HINT,
21327
+ ...pendingJson
21328
+ };
21329
+ default:
21330
+ return {
21331
+ signedIn: false,
21332
+ tokenKind: state.tokenKind,
21333
+ error: {
21334
+ code: state.state === "rejected" ? state.code ?? "rejected" : "unreachable",
21335
+ message: state.message
21336
+ },
21337
+ ...pendingJson
21338
+ };
21339
+ }
21340
+ }
21341
+ function whoamiLines(state, pending) {
21342
+ const waiting = pending ? [
21343
+ `A sign-in is waiting for approval: open ${pending.verificationUriComplete ?? pending.verificationUri} (code ${pending.userCode}).`
21344
+ ] : [];
21345
+ switch (state.state) {
21346
+ case "signed-in": {
21347
+ const { me } = state;
21348
+ const until = me.expiresAt ? `, until ${me.expiresAt.slice(0, 10)}` : "";
21349
+ const lines = [
21350
+ `Signed in as ${signedInAs(me)}${me.user.nickname ? ` - ${me.user.nickname}` : ""}`,
21351
+ state.tokenKind === "pat" ? `With TUTTICODE_TOKEN (an access token)${until}` : `Signed-in device: ${me.deviceName ?? "this computer"}${until}`,
21352
+ `Limits: renders up to ${me.limits.maxSeconds} s, no voice tag`,
21353
+ `Today: ${me.usageToday.renders} render${me.usageToday.renders === 1 ? "" : "s"}, ${Math.round(me.usageToday.seconds)} s of audio`
21354
+ ];
21355
+ if (me.termsUpdate && firstTime(`terms-update:${me.termsUpdate}`, "install"))
21356
+ lines.push(
21357
+ `The TuttiCode terms changed: ${me.termsUpdate} (the next tutticode login accepts them)`
21358
+ );
21359
+ return lines;
21360
+ }
21361
+ case "anonymous":
21362
+ return [`Not signed in: ${anonymousLimitsText()}.`, ...waiting];
21363
+ case "rejected":
21364
+ return [
21365
+ state.message.includes("tutticode login") ? state.message : `Your sign-in was rejected (${state.message}). Run tutticode login.`,
21366
+ ...waiting
21367
+ ];
21368
+ case "unreachable":
21369
+ return [`Could not check the sign-in: ${state.message}`];
21370
+ }
21371
+ }
18601
21372
  async function setup(command, io) {
18602
21373
  const steps = policySteps(command.undo);
18603
21374
  if (command.undo) {
@@ -18675,7 +21446,7 @@ function askOnTerminal(io) {
18675
21446
  }
18676
21447
 
18677
21448
  // src/bin.ts
18678
- setSkillsDir((0, import_node_path9.join)(__dirname, "skills"));
21449
+ setSkillsDir((0, import_node_path12.join)(__dirname, "skills"));
18679
21450
  main(process.argv.slice(2), {
18680
21451
  stdout: process.stdout,
18681
21452
  stderr: process.stderr,