tutticode 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- /* tutticode v0.1.1 | https://tuttidaw.com */
2
+ /* tutticode v0.2.1 | 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", "limit"])
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,6 +4742,9 @@ 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
  }
@@ -4070,6 +4832,12 @@ function validateRenderMeta(raw, expect) {
4070
4832
  return no("meta.analysis must be an object");
4071
4833
  if (typeof raw.engineVersion !== "string" || raw.engineVersion.length > MAX_VERSION_CHARS)
4072
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
+ }
4073
4841
  if (typeof raw.editUrl !== "string" || raw.editUrl.length > MAX_EDIT_URL_CHARS || !expect.origins.includes(urlOrigin(raw.editUrl) ?? ""))
4074
4842
  return no("meta.editUrl must be a link on the TuttiDAW site");
4075
4843
  return {
@@ -4092,10 +4860,28 @@ function validateRenderMeta(raw, expect) {
4092
4860
  ...raw.analysis !== void 0 ? { analysis: raw.analysis } : {},
4093
4861
  check: raw.check,
4094
4862
  editUrl: raw.editUrl,
4095
- engineVersion: raw.engineVersion
4863
+ engineVersion: raw.engineVersion,
4864
+ ...watermark ? { watermark } : {}
4096
4865
  }
4097
4866
  };
4098
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
+ }
4099
4885
  function urlOrigin(url) {
4100
4886
  try {
4101
4887
  const parsed = new URL(url);
@@ -4105,22 +4891,677 @@ function urlOrigin(url) {
4105
4891
  }
4106
4892
  }
4107
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.1" : 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, platform = process.platform) => platform === "win32" ? `${st.ino}` : `${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
+
4108
5542
  // src/args.ts
4109
5543
  var import_node_util = require("node:util");
4110
5544
 
4111
5545
  // src/output.ts
4112
- var import_node_fs = require("node:fs");
4113
- var import_node_path = require("node:path");
5546
+ var import_node_fs3 = require("node:fs");
5547
+ var import_node_path3 = require("node:path");
4114
5548
  var EXIT_OK = 0;
4115
5549
  var EXIT_CODE_ERRORS = 1;
4116
5550
  var EXIT_BRIDGE = 2;
4117
5551
  var EXIT_USAGE = 3;
5552
+ var EXIT_ANONYMOUS = 4;
5553
+ var EXIT_LIMIT = 5;
5554
+ var EXIT_UPGRADE = 6;
4118
5555
  var CODE_ERRORS = /* @__PURE__ */ new Set(["compile", "too-long", "clip"]);
4119
5556
  function exitCodeFor(outcome) {
4120
5557
  switch (outcome.kind) {
4121
5558
  case "checked":
4122
5559
  return outcome.report.ok ? EXIT_OK : EXIT_CODE_ERRORS;
4123
5560
  case "error":
5561
+ if (outcome.code === "limit")
5562
+ return EXIT_LIMIT;
5563
+ if (outcome.code === "upgrade")
5564
+ return EXIT_UPGRADE;
4124
5565
  return CODE_ERRORS.has(outcome.code) ? EXIT_CODE_ERRORS : EXIT_BRIDGE;
4125
5566
  default:
4126
5567
  return EXIT_OK;
@@ -4135,29 +5576,29 @@ function safeFileStem(label) {
4135
5576
  return RESERVED_NAMES.test(cleaned) ? `${cleaned}_` : cleaned;
4136
5577
  }
4137
5578
  function formatFromPath(path) {
4138
- const ext = (0, import_node_path.extname)(path).slice(1).toLowerCase();
5579
+ const ext = (0, import_node_path3.extname)(path).slice(1).toLowerCase();
4139
5580
  return ext === "wav" || ext === "flac" || ext === "mp3" ? ext : void 0;
4140
5581
  }
4141
5582
  var FORMATS2 = ["wav", "flac", "mp3"];
4142
5583
  function writeFileNoFollow(path, bytes) {
4143
5584
  try {
4144
- if ((0, import_node_fs.lstatSync)(path).isSymbolicLink())
5585
+ if ((0, import_node_fs3.lstatSync)(path).isSymbolicLink())
4145
5586
  throw new Error(`Refusing to write through the symbolic link ${path}.`);
4146
5587
  } catch (e) {
4147
5588
  if (e.code !== "ENOENT")
4148
5589
  throw e;
4149
5590
  }
4150
- const fd = (0, import_node_fs.openSync)(
5591
+ const fd = (0, import_node_fs3.openSync)(
4151
5592
  path,
4152
- 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
4153
5594
  // swapped in after the check above.
4154
- (import_node_fs.constants.O_NOFOLLOW ?? 0)
5595
+ (import_node_fs3.constants.O_NOFOLLOW ?? 0)
4155
5596
  );
4156
5597
  try {
4157
5598
  for (let at = 0; at < bytes.length; )
4158
- 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);
4159
5600
  } finally {
4160
- (0, import_node_fs.closeSync)(fd);
5601
+ (0, import_node_fs3.closeSync)(fd);
4161
5602
  }
4162
5603
  }
4163
5604
  function writeRender(meta, parts, out, stemsDir) {
@@ -4168,30 +5609,30 @@ function writeRender(meta, parts, out, stemsDir) {
4168
5609
  const mix = parts.get(MIX_PART);
4169
5610
  if (!mix)
4170
5611
  throw new Error("The render has no mix.");
4171
- const mixPath = (0, import_node_path.resolve)(out);
4172
- (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 });
4173
5614
  writeFileNoFollow(mixPath, mix);
4174
5615
  const stems = {};
4175
5616
  if (meta.stems.length) {
4176
- const requested = (0, import_node_path.resolve)(
4177
- 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"
4178
5619
  );
4179
- (0, import_node_fs.mkdirSync)(requested, { recursive: true });
4180
- 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);
4181
5622
  const used = new Set(
4182
- [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()] : []
4183
5624
  );
4184
5625
  for (const label of meta.stems) {
4185
5626
  const bytes = parts.get(stemPart(label));
4186
5627
  if (!bytes)
4187
5628
  continue;
4188
5629
  const base = safeFileStem(label);
4189
- let file = `${base}.${meta.format}`;
4190
- for (let n = 2; used.has(file.toLowerCase()); n++)
4191
- file = `${base}-${n}.${meta.format}`;
4192
- used.add(file.toLowerCase());
4193
- const path = (0, import_node_path.resolve)(dir, file);
4194
- 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)
4195
5636
  throw new Error(`Refusing to write a stem outside ${dir}.`);
4196
5637
  writeFileNoFollow(path, bytes);
4197
5638
  stems[label] = path;
@@ -4200,11 +5641,11 @@ function writeRender(meta, parts, out, stemsDir) {
4200
5641
  return { mix: mixPath, stems };
4201
5642
  }
4202
5643
  var fixed = (n, digits = 1) => Number.isFinite(n) ? n.toFixed(digits) : String(n);
4203
- function formatDiagnostics(report, file) {
5644
+ function formatDiagnostics(report, file2) {
4204
5645
  const lines = [];
4205
5646
  for (const d of report.diagnostics) {
4206
5647
  lines.push(
4207
- `${file}:${d.line}:${d.col} ${d.severity} ${d.code} ${d.message}`
5648
+ `${file2}:${d.line}:${d.col} ${d.severity} ${d.code} ${d.message}`
4208
5649
  );
4209
5650
  if (d.snippet)
4210
5651
  lines.push(` ${d.snippet}`);
@@ -4231,92 +5672,46 @@ function formatRenderSummary(meta, files) {
4231
5672
  `Wrote ${files.mix} (${fixed(meta.seconds)} s, ${meta.bars} bars at ${fixed(
4232
5673
  meta.bpm,
4233
5674
  0
4234
- )} BPM, ${fixed(meta.lufs)} LUFS, ${fixed(meta.truePeak)} dBTP${gain}${limited})`
5675
+ )} BPM, ${fixed(meta.lufs)} LUFS, ${fixed(
5676
+ meta.truePeak
5677
+ )} dBTP${gain}${limited})`
4235
5678
  ];
4236
5679
  for (const warning of meta.warnings ?? [])
4237
5680
  lines.push(`Note: ${warning}`);
4238
5681
  const stems = Object.values(files.stems);
4239
5682
  if (stems.length)
4240
- 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])}`);
4241
5684
  lines.push(`Edit in TuttiDAW: ${meta.editUrl}`);
4242
5685
  return lines;
4243
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)";
5688
+
5689
+ // src/server.ts
5690
+ var import_node_crypto2 = require("node:crypto");
5691
+ var import_node_http = require("node:http");
5692
+
5693
+ // ../../node_modules/.pnpm/ws@8.21.3/node_modules/ws/wrapper.mjs
5694
+ var import_stream = __toESM(require_stream(), 1);
5695
+ var import_extension = __toESM(require_extension(), 1);
5696
+ var import_permessage_deflate = __toESM(require_permessage_deflate(), 1);
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;
4244
5703
 
4245
5704
  // src/server.ts
4246
- var import_node_crypto = require("node:crypto");
4247
- var import_node_http = require("node:http");
4248
-
4249
- // ../../node_modules/.pnpm/ws@8.21.3/node_modules/ws/wrapper.mjs
4250
- var import_stream = __toESM(require_stream(), 1);
4251
- var import_extension = __toESM(require_extension(), 1);
4252
- var import_permessage_deflate = __toESM(require_permessage_deflate(), 1);
4253
- var import_receiver = __toESM(require_receiver(), 1);
4254
- var import_sender = __toESM(require_sender(), 1);
4255
- var import_subprotocol = __toESM(require_subprotocol(), 1);
4256
- var import_websocket = __toESM(require_websocket(), 1);
4257
- var import_websocket_server = __toESM(require_websocket_server(), 1);
4258
- var wrapper_default = import_websocket.default;
4259
-
4260
- // src/env.ts
4261
- var import_node_fs2 = require("node:fs");
4262
- var import_node_os = require("node:os");
4263
- var import_node_path2 = require("node:path");
4264
- var VERSION = true ? "0.1.1" : readPackageVersion();
4265
- function envBrowserArgs() {
4266
- const args = process.env.TUTTICODE_BROWSER_ARGS?.split(" ").filter(Boolean);
4267
- return args?.length ? args : void 0;
4268
- }
4269
- function stateDir() {
4270
- const dir = process.env.TUTTICODE_HOME || (0, import_node_path2.join)((0, import_node_os.homedir)(), ".tutticode");
4271
- (0, import_node_fs2.mkdirSync)(dir, { recursive: true });
4272
- return dir;
4273
- }
4274
- var assetsDir;
4275
- function skillsDir() {
4276
- return process.env.TUTTICODE_SKILLS_DIR || assetsDir || // Running from the sources (tests): the app's public skills.
4277
- (0, import_node_path2.join)(__dirname, "..", "..", "..", "public", "skills");
4278
- }
4279
- function setSkillsDir(dir) {
4280
- assetsDir = dir;
4281
- }
4282
- var LOG_ROLL_BYTES = 2 * 1024 * 1024;
4283
- function createLogger(name, verbose) {
4284
- let file;
4285
- try {
4286
- const dir = (0, import_node_path2.join)(stateDir(), "logs");
4287
- (0, import_node_fs2.mkdirSync)(dir, { recursive: true });
4288
- file = (0, import_node_path2.join)(dir, `${name}.log`);
4289
- if (((0, import_node_fs2.statSync)(file, { throwIfNoEntry: false })?.size ?? 0) > LOG_ROLL_BYTES)
4290
- (0, import_node_fs2.renameSync)(file, `${file}.1`);
4291
- } catch {
4292
- file = void 0;
4293
- }
4294
- return (level2, message) => {
4295
- const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${level2} [${process.pid}] ${message}`;
4296
- if (verbose || level2 === "error")
4297
- process.stderr.write(`tutticode: ${message}
4298
- `);
4299
- if (!file)
4300
- return;
4301
- try {
4302
- (0, import_node_fs2.appendFileSync)(file, `${line}
4303
- `);
4304
- } catch {
4305
- }
4306
- };
4307
- }
4308
-
4309
- // src/server.ts
4310
5705
  var CONTROL_PATH = "/control";
4311
5706
  function newToken() {
4312
- return (0, import_node_crypto.randomBytes)(16).toString("base64url");
5707
+ return (0, import_node_crypto2.randomBytes)(16).toString("base64url");
4313
5708
  }
4314
5709
  function tokensEqual(expected, presented) {
4315
5710
  if (typeof presented !== "string")
4316
5711
  return false;
4317
5712
  const a = Buffer.from(expected);
4318
5713
  const b = Buffer.from(presented);
4319
- 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);
4320
5715
  }
4321
5716
  function normalizeOrigin(value) {
4322
5717
  try {
@@ -4408,7 +5803,7 @@ var BridgeServer = class {
4408
5803
  return res.end(
4409
5804
  JSON.stringify({
4410
5805
  name: "tutticode",
4411
- version: VERSION,
5806
+ version: VERSION2,
4412
5807
  protocol: BRIDGE_PROTOCOL
4413
5808
  })
4414
5809
  );
@@ -4472,6 +5867,21 @@ function pathOf(req) {
4472
5867
  // src/args.ts
4473
5868
  var UsageError = class extends Error {
4474
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
+ }
4475
5885
  var bridgeOptions = {
4476
5886
  origin: { type: "string" },
4477
5887
  "dev-origin": { type: "string", multiple: true },
@@ -4598,7 +6008,8 @@ function parseCli(argv) {
4598
6008
  case "doctor": {
4599
6009
  const { values, positionals } = parse(rest, {
4600
6010
  ...bridgeOptions,
4601
- json: { type: "boolean" }
6011
+ json: { type: "boolean" },
6012
+ fix: { type: "boolean" }
4602
6013
  });
4603
6014
  if (values.help)
4604
6015
  return { name: "help", topic: "doctor" };
@@ -4607,9 +6018,56 @@ function parseCli(argv) {
4607
6018
  return {
4608
6019
  name: "doctor",
4609
6020
  json: !!values.json,
6021
+ fix: !!values.fix,
4610
6022
  bridge: bridgeFlags(values)
4611
6023
  };
4612
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
+ }
4613
6071
  case "setup": {
4614
6072
  const { values, positionals } = parse(rest, {
4615
6073
  yes: { type: "boolean", short: "y" },
@@ -4654,6 +6112,8 @@ function parseCli(argv) {
4654
6112
  }
4655
6113
  case "__daemon":
4656
6114
  return { name: "daemon", config: rest[0] ?? "" };
6115
+ case "__login-poll":
6116
+ return { name: "login-poll", file: rest[0] ?? "" };
4657
6117
  default:
4658
6118
  throw new UsageError(`Unknown command ${name}. Run tutticode --help.`);
4659
6119
  }
@@ -4677,11 +6137,12 @@ function parseRender(rest) {
4677
6137
  "true-peak": { type: "string" },
4678
6138
  limit: { type: "boolean" },
4679
6139
  "no-analyze": { type: "boolean" },
6140
+ "no-brand-tag": { type: "boolean" },
4680
6141
  json: { type: "boolean" }
4681
6142
  });
4682
6143
  if (values.help)
4683
6144
  return { name: "help", topic: "render" };
4684
- const file = oneFile(positionals, "render");
6145
+ const file2 = oneFile(positionals, "render");
4685
6146
  const out = values.out;
4686
6147
  if (!out)
4687
6148
  throw new UsageError(
@@ -4731,11 +6192,12 @@ function parseRender(rest) {
4731
6192
  ...optional2("lufs", number(values, "lufs", -40, -5)),
4732
6193
  ...optional2("truePeak", number(values, "true-peak", -12, 0)),
4733
6194
  ...values.limit ? { limit: true } : {},
4734
- ...values["no-analyze"] ? { analyze: false } : {}
6195
+ ...values["no-analyze"] ? { analyze: false } : {},
6196
+ ...values["no-brand-tag"] ? { brandTag: false } : {}
4735
6197
  };
4736
6198
  return {
4737
6199
  name: "render",
4738
- file,
6200
+ file: file2,
4739
6201
  out,
4740
6202
  stemsDir: values.stems,
4741
6203
  json: !!values.json,
@@ -4755,10 +6217,19 @@ Usage:
4755
6217
  tutticode open <file|-> open the program in TuttiCode
4756
6218
  tutticode mcp MCP stdio server (code_check, code_render, ...)
4757
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
4758
6223
  tutticode setup optional browser policy + skills
4759
6224
  tutticode skills install install the agent skills
4760
6225
 
4761
- 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.
4762
6233
  Run tutticode <command> --help for its flags.`,
4763
6234
  check: `tutticode check <file|-> [--bpm N] [--json] [bridge flags]
4764
6235
 
@@ -4782,6 +6253,8 @@ program has errors.`,
4782
6253
  --limit true-peak limiter instead of failing
4783
6254
  (clip) or lowering; reported
4784
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)
4785
6258
  --json print the RenderMeta + written files
4786
6259
  --timeout S give up after S seconds (default 600)`,
4787
6260
  open: `tutticode open <file|-> [bridge flags]
@@ -4791,11 +6264,34 @@ Opens the program in a visible TuttiCode tab (tuttidaw.com/code). With
4791
6264
  mcp: `tutticode mcp [bridge flags]
4792
6265
 
4793
6266
  MCP stdio server: code_check, code_render, code_compare, code_open,
4794
- 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).
4795
6269
  claude mcp add tutticode -- tutticode mcp`,
4796
- doctor: `tutticode doctor [--json] [bridge flags]
6270
+ doctor: `tutticode doctor [--json] [--fix] [bridge flags]
6271
+
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>]
4797
6292
 
4798
- Checks the browser, the page and the connection, and prints the engine version.`,
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.`,
4799
6295
  setup: `tutticode setup [--yes] [--undo] [--no-skills]
4800
6296
 
4801
6297
  Optionally lets tuttidaw.com reach tutticode in your everyday Chrome / Edge
@@ -4817,10 +6313,10 @@ agent found under your home directory).`,
4817
6313
  };
4818
6314
 
4819
6315
  // src/browser.ts
4820
- var import_node_child_process = require("node:child_process");
4821
- var import_node_fs3 = require("node:fs");
4822
- var import_node_os2 = require("node:os");
4823
- 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");
4824
6320
 
4825
6321
  // src/cdp.ts
4826
6322
  var import_node_events = require("node:events");
@@ -4925,8 +6421,8 @@ var CdpClient = class extends import_node_events.EventEmitter {
4925
6421
  var hostPlatform = () => ({
4926
6422
  platform: process.platform,
4927
6423
  env: process.env,
4928
- home: (0, import_node_os2.homedir)(),
4929
- exists: import_node_fs3.existsSync
6424
+ home: (0, import_node_os3.homedir)(),
6425
+ exists: import_node_fs4.existsSync
4930
6426
  });
4931
6427
  function browserCandidates(p = hostPlatform()) {
4932
6428
  if (p.platform === "win32") {
@@ -4938,20 +6434,20 @@ function browserCandidates(p = hostPlatform()) {
4938
6434
  return [
4939
6435
  ...roots.map((r2) => ({
4940
6436
  kind: "chrome",
4941
- 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")
4942
6438
  })),
4943
6439
  ...roots.map((r2) => ({
4944
6440
  kind: "edge",
4945
- 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")
4946
6442
  }))
4947
6443
  ];
4948
6444
  }
4949
6445
  if (p.platform === "darwin") {
4950
- const apps = ["/Applications", (0, import_node_path3.join)(p.home, "Applications")];
6446
+ const apps = ["/Applications", (0, import_node_path4.join)(p.home, "Applications")];
4951
6447
  return [
4952
6448
  ...apps.map((a) => ({
4953
6449
  kind: "chrome",
4954
- path: (0, import_node_path3.join)(
6450
+ path: (0, import_node_path4.join)(
4955
6451
  a,
4956
6452
  "Google Chrome.app",
4957
6453
  "Contents",
@@ -4961,7 +6457,7 @@ function browserCandidates(p = hostPlatform()) {
4961
6457
  })),
4962
6458
  ...apps.map((a) => ({
4963
6459
  kind: "edge",
4964
- path: (0, import_node_path3.join)(
6460
+ path: (0, import_node_path4.join)(
4965
6461
  a,
4966
6462
  "Microsoft Edge.app",
4967
6463
  "Contents",
@@ -4971,7 +6467,7 @@ function browserCandidates(p = hostPlatform()) {
4971
6467
  })),
4972
6468
  ...apps.map((a) => ({
4973
6469
  kind: "chromium",
4974
- 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")
4975
6471
  }))
4976
6472
  ];
4977
6473
  }
@@ -4985,7 +6481,7 @@ function browserCandidates(p = hostPlatform()) {
4985
6481
  ["chromium", "chromium-browser"]
4986
6482
  ];
4987
6483
  return names.flatMap(
4988
- ([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) }))
4989
6485
  );
4990
6486
  }
4991
6487
  function findBrowser(wanted, p = hostPlatform()) {
@@ -5004,7 +6500,7 @@ function findBrowser(wanted, p = hostPlatform()) {
5004
6500
  }
5005
6501
  function openExternal(url) {
5006
6502
  const [command, args] = process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : process.platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
5007
- const child = (0, import_node_child_process.spawn)(command, args, {
6503
+ const child = (0, import_node_child_process2.spawn)(command, args, {
5008
6504
  detached: true,
5009
6505
  stdio: "ignore",
5010
6506
  windowsHide: true
@@ -5106,7 +6602,7 @@ var HeadlessBrowser = class {
5106
6602
  }
5107
6603
  }
5108
6604
  if (this.tempProfile) {
5109
- (0, import_node_fs3.rmSync)(this.tempProfile, { recursive: true, force: true });
6605
+ (0, import_node_fs4.rmSync)(this.tempProfile, { recursive: true, force: true });
5110
6606
  this.tempProfile = void 0;
5111
6607
  }
5112
6608
  }
@@ -5114,8 +6610,8 @@ var HeadlessBrowser = class {
5114
6610
  await this.close();
5115
6611
  this.consoleLines = [];
5116
6612
  const { browser, log } = this.options;
5117
- const shared = this.options.profileDir ?? (0, import_node_path3.join)(stateDir(), "browser-profile");
5118
- (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 });
5119
6615
  try {
5120
6616
  this.cdp = await this.spawnBrowser(shared);
5121
6617
  } catch (e) {
@@ -5123,7 +6619,7 @@ var HeadlessBrowser = class {
5123
6619
  "warn",
5124
6620
  `shared browser profile unavailable (${e.message}); using a temporary one`
5125
6621
  );
5126
- 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-"));
5127
6623
  this.cdp = await this.spawnBrowser(this.tempProfile);
5128
6624
  }
5129
6625
  this.cdp.on("event", (event) => this.onEvent(event));
@@ -5159,7 +6655,7 @@ var HeadlessBrowser = class {
5159
6655
  "about:blank"
5160
6656
  ];
5161
6657
  return new Promise((resolve3, reject) => {
5162
- const child = (0, import_node_child_process.spawn)(browser.path, args, {
6658
+ const child = (0, import_node_child_process2.spawn)(browser.path, args, {
5163
6659
  // fd 3: commands to the browser, fd 4: its replies and events.
5164
6660
  stdio: ["ignore", "ignore", "pipe", "pipe", "pipe"],
5165
6661
  windowsHide: true
@@ -5276,7 +6772,7 @@ function remoteDebuggingFree(args) {
5276
6772
  var delay = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
5277
6773
  function killTree(child) {
5278
6774
  if (process.platform === "win32" && child.pid)
5279
- (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)], {
5280
6776
  windowsHide: true
5281
6777
  });
5282
6778
  else
@@ -5284,18 +6780,19 @@ function killTree(child) {
5284
6780
  }
5285
6781
 
5286
6782
  // src/daemon.ts
5287
- var import_node_child_process2 = require("node:child_process");
5288
- var import_node_crypto3 = require("node:crypto");
5289
- var import_node_fs4 = require("node:fs");
5290
- 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");
5291
6787
 
5292
6788
  // src/session.ts
5293
- var import_node_crypto2 = require("node:crypto");
6789
+ var import_node_crypto3 = require("node:crypto");
5294
6790
  var import_node_events3 = require("node:events");
5295
6791
 
5296
6792
  // src/page-link.ts
5297
6793
  var import_node_events2 = require("node:events");
5298
6794
  var HELLO_TIMEOUT_MS = 15e3;
6795
+ var AUTH_TIMEOUT_MS = 1e4;
5299
6796
  var CANCEL_GRACE_MS = 5e3;
5300
6797
  function maxPartBytes(meta) {
5301
6798
  return Math.ceil((meta.seconds + 1) * meta.sampleRate) * 2 * 4 + 1024 * 1024;
@@ -5349,6 +6846,16 @@ var PageLink = class extends import_node_events2.EventEmitter {
5349
6846
  drain;
5350
6847
  closedReason;
5351
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;
5352
6859
  get open() {
5353
6860
  return this.closedReason === void 0;
5354
6861
  }
@@ -5360,6 +6867,73 @@ var PageLink = class extends import_node_events2.EventEmitter {
5360
6867
  get busy() {
5361
6868
  return !!this.current || !!this.drain;
5362
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
+ }
5363
6937
  /** Runs one command. Never rejects: failures are an `error` outcome. */
5364
6938
  async run(command, options) {
5365
6939
  if (!this.open)
@@ -5475,8 +7049,13 @@ var PageLink = class extends import_node_events2.EventEmitter {
5475
7049
  }
5476
7050
  onMessage(message) {
5477
7051
  if (message.type === "hello") {
5478
- if (this.hello)
7052
+ if (this.hello) {
7053
+ if (message.protocol === BRIDGE_PROTOCOL) {
7054
+ this.hello = message;
7055
+ this.authWait?.("hello");
7056
+ }
5479
7057
  return;
7058
+ }
5480
7059
  if (message.protocol !== BRIDGE_PROTOCOL) {
5481
7060
  const why = `The page speaks bridge protocol ${message.protocol}, this tutticode speaks ${BRIDGE_PROTOCOL}: update tutticode (npm i -g tutticode@latest).`;
5482
7061
  this.emit("bad-hello", why);
@@ -5496,6 +7075,20 @@ var PageLink = class extends import_node_events2.EventEmitter {
5496
7075
  return;
5497
7076
  }
5498
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
+ }
5499
7092
  if (message.type === "error" && id === void 0) {
5500
7093
  this.log("warn", `page error ${message.code}: ${message.message}`);
5501
7094
  return this.fail(message.code, message.message, message.report);
@@ -5531,7 +7124,8 @@ var PageLink = class extends import_node_events2.EventEmitter {
5531
7124
  kind: "error",
5532
7125
  code: message.code,
5533
7126
  message: message.message,
5534
- ...message.report ? { report: message.report } : {}
7127
+ ...message.report ? { report: message.report } : {},
7128
+ ...message.limit ? { limit: message.limit } : {}
5535
7129
  });
5536
7130
  case "done": {
5537
7131
  if (!running.meta)
@@ -5663,7 +7257,13 @@ var Session = class extends import_node_events3.EventEmitter {
5663
7257
  const job = this.queue.then(async () => {
5664
7258
  try {
5665
7259
  const link = await this.ensurePage(options.signal);
5666
- 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 };
5667
7267
  } catch (e) {
5668
7268
  if (e instanceof BridgeError)
5669
7269
  return { kind: "error", code: e.code, message: e.message };
@@ -5811,14 +7411,14 @@ Page console:
5811
7411
  // src/daemon.ts
5812
7412
  function daemonKey(config) {
5813
7413
  const { verbose: _verbose, ...identity } = config;
5814
- return (0, import_node_crypto3.createHash)("sha256").update(
7414
+ return (0, import_node_crypto4.createHash)("sha256").update(
5815
7415
  JSON.stringify({
5816
7416
  ...identity,
5817
7417
  devOrigins: [...config.devOrigins ?? []].sort()
5818
7418
  })
5819
7419
  ).digest("hex").slice(0, 12);
5820
7420
  }
5821
- 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`);
5822
7422
  async function runDaemon(config, log) {
5823
7423
  const controlToken = newToken();
5824
7424
  let active = 0;
@@ -5846,22 +7446,22 @@ async function runDaemon(config, log) {
5846
7446
  const session = new Session(options);
5847
7447
  const port = await session.start();
5848
7448
  const key = daemonKey(config);
5849
- const file = {
7449
+ const file2 = {
5850
7450
  pid: process.pid,
5851
7451
  port,
5852
7452
  token: controlToken,
5853
- version: VERSION
7453
+ version: VERSION2
5854
7454
  };
5855
- (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 });
5856
7456
  log("info", `daemon ${key} listening on 127.0.0.1:${port} (${config.mode})`);
5857
7457
  const shutdown = async (why) => {
5858
7458
  log("info", `daemon exiting: ${why}`);
5859
7459
  try {
5860
7460
  const current = JSON.parse(
5861
- (0, import_node_fs4.readFileSync)(daemonFile(key), "utf8")
7461
+ (0, import_node_fs5.readFileSync)(daemonFile(key), "utf8")
5862
7462
  );
5863
7463
  if (current.pid === process.pid)
5864
- (0, import_node_fs4.rmSync)(daemonFile(key), { force: true });
7464
+ (0, import_node_fs5.rmSync)(daemonFile(key), { force: true });
5865
7465
  } catch {
5866
7466
  }
5867
7467
  await session.close();
@@ -5882,14 +7482,14 @@ function serveControl(socket, session, setStatusSink) {
5882
7482
  socket.send(JSON.stringify(value));
5883
7483
  };
5884
7484
  socket.once("message", async (data) => {
5885
- let request;
7485
+ let request2;
5886
7486
  try {
5887
- request = JSON.parse(String(data));
7487
+ request2 = JSON.parse(String(data));
5888
7488
  } catch {
5889
7489
  return socket.close(1008, "bad request");
5890
7490
  }
5891
7491
  setStatusSink((message) => send({ type: "status", message }));
5892
- if (request.type === "info") {
7492
+ if (request2.type === "info") {
5893
7493
  try {
5894
7494
  await session.ensurePage(abort.signal);
5895
7495
  send({
@@ -5912,28 +7512,33 @@ function serveControl(socket, session, setStatusSink) {
5912
7512
  }
5913
7513
  return socket.close(1e3);
5914
7514
  }
5915
- if (request.type !== "run" || !request.command)
7515
+ if (request2.type !== "run" || !request2.command)
5916
7516
  return socket.close(1008, "bad request");
5917
- const outcome = await session.run(request.command, {
5918
- timeoutMs: request.timeoutMs ?? 6e5,
7517
+ const outcome = await session.run(request2.command, {
7518
+ timeoutMs: request2.timeoutMs ?? 6e5,
5919
7519
  signal: abort.signal,
5920
- onProgress: (progress) => send(progress)
7520
+ onProgress: (progress) => send(progress),
7521
+ ...request2.auth ? { auth: request2.auth } : {}
5921
7522
  });
5922
7523
  if (outcome.kind === "rendered") {
5923
- const id = request.command.type === "open" ? "open" : request.command.id;
7524
+ const id = request2.command.type === "open" ? "open" : request2.command.id;
5924
7525
  for (const [part, bytes] of outcome.parts)
5925
7526
  for (const frame of frameParts(id, part, bytes))
5926
7527
  socket.send(frame);
5927
7528
  send({
5928
7529
  type: "result",
5929
- outcome: { kind: "rendered", meta: outcome.meta }
7530
+ outcome: {
7531
+ kind: "rendered",
7532
+ meta: outcome.meta,
7533
+ ...outcome.auth ? { auth: outcome.auth } : {}
7534
+ }
5930
7535
  });
5931
7536
  } else
5932
7537
  send({ type: "result", outcome });
5933
7538
  socket.close(1e3);
5934
7539
  });
5935
7540
  }
5936
- async function runViaDaemon(options, command, timeoutMs, callbacks = {}) {
7541
+ async function runViaDaemon(options, command, timeoutMs, callbacks = {}, auth) {
5937
7542
  let socket;
5938
7543
  try {
5939
7544
  socket = await connectDaemon(options);
@@ -5942,7 +7547,7 @@ async function runViaDaemon(options, command, timeoutMs, callbacks = {}) {
5942
7547
  }
5943
7548
  return controlCall(
5944
7549
  socket,
5945
- { type: "run", command, timeoutMs },
7550
+ { type: "run", command, timeoutMs, ...auth ? { auth } : {} },
5946
7551
  callbacks
5947
7552
  );
5948
7553
  }
@@ -5955,7 +7560,7 @@ async function daemonInfo(options, callbacks = {}) {
5955
7560
  }
5956
7561
  return controlCall(socket, { type: "info" }, callbacks);
5957
7562
  }
5958
- function controlCall(socket, request, callbacks) {
7563
+ function controlCall(socket, request2, callbacks) {
5959
7564
  return new Promise((resolve3) => {
5960
7565
  const assembler = new PartAssembler();
5961
7566
  const parts = /* @__PURE__ */ new Map();
@@ -6014,7 +7619,7 @@ function controlCall(socket, request, callbacks) {
6014
7619
  })
6015
7620
  );
6016
7621
  socket.on("error", () => void 0);
6017
- socket.send(JSON.stringify(request));
7622
+ socket.send(JSON.stringify(request2));
6018
7623
  });
6019
7624
  }
6020
7625
  function openControlSocket(port, token, timeoutMs = 3e3) {
@@ -6031,7 +7636,7 @@ function openControlSocket(port, token, timeoutMs = 3e3) {
6031
7636
  }
6032
7637
  function readDaemonFile(key) {
6033
7638
  try {
6034
- return JSON.parse((0, import_node_fs4.readFileSync)(daemonFile(key), "utf8"));
7639
+ return JSON.parse((0, import_node_fs5.readFileSync)(daemonFile(key), "utf8"));
6035
7640
  } catch {
6036
7641
  return void 0;
6037
7642
  }
@@ -6043,7 +7648,7 @@ async function connectDaemon({
6043
7648
  }) {
6044
7649
  const key = daemonKey(config);
6045
7650
  const existing = readDaemonFile(key);
6046
- if (existing && existing.version === VERSION) {
7651
+ if (existing && existing.version === VERSION2) {
6047
7652
  try {
6048
7653
  return await openControlSocket(existing.port, existing.token);
6049
7654
  } catch {
@@ -6053,11 +7658,11 @@ async function connectDaemon({
6053
7658
  );
6054
7659
  }
6055
7660
  }
6056
- const lock = (0, import_node_path4.join)(stateDir(), `daemon-${key}.lock`);
6057
- const owner = (0, import_node_crypto3.randomUUID)();
6058
- 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)) {
6059
7664
  try {
6060
- const child = (0, import_node_child_process2.spawn)(
7665
+ const child = (0, import_node_child_process3.spawn)(
6061
7666
  process.execPath,
6062
7667
  [
6063
7668
  entry,
@@ -6070,19 +7675,19 @@ async function connectDaemon({
6070
7675
  log("info", `spawned daemon ${key} (pid ${child.pid})`);
6071
7676
  return await waitForDaemon(key, existing?.pid, child.pid);
6072
7677
  } finally {
6073
- (0, import_node_fs4.rmSync)(lock, { force: true });
7678
+ (0, import_node_fs5.rmSync)(lock, { force: true });
6074
7679
  }
6075
7680
  }
6076
7681
  return waitForDaemon(key, existing?.pid);
6077
7682
  }
6078
- function takeLock(lock, owner) {
7683
+ function takeLock2(lock, owner) {
6079
7684
  try {
6080
- 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);
6081
7686
  if (age > 2e4)
6082
- (0, import_node_fs4.rmSync)(lock, { force: true });
6083
- const fd = (0, import_node_fs4.openSync)(lock, "wx");
6084
- (0, import_node_fs4.writeFileSync)(fd, owner);
6085
- (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);
6086
7691
  return true;
6087
7692
  } catch {
6088
7693
  return false;
@@ -6091,10 +7696,10 @@ function takeLock(lock, owner) {
6091
7696
  async function waitForDaemon(key, stalePid, newPid) {
6092
7697
  const deadline = Date.now() + 15e3;
6093
7698
  while (Date.now() < deadline) {
6094
- const file = readDaemonFile(key);
6095
- 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) {
6096
7701
  try {
6097
- return await openControlSocket(file.port, file.token);
7702
+ return await openControlSocket(file2.port, file2.token);
6098
7703
  } catch {
6099
7704
  }
6100
7705
  }
@@ -6105,6 +7710,370 @@ async function waitForDaemon(key, stalePid, newPid) {
6105
7710
  );
6106
7711
  }
6107
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
+
6108
8077
  // src/mcp.ts
6109
8078
  var import_node_readline = require("node:readline");
6110
8079
 
@@ -6114,13 +8083,13 @@ var JSON_RPC_INVALID_REQUEST = -32600;
6114
8083
  var JSON_RPC_METHOD_NOT_FOUND = -32601;
6115
8084
  var JSON_RPC_INVALID_PARAMS = -32602;
6116
8085
  var JSON_RPC_INTERNAL_ERROR = -32603;
6117
- var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
8086
+ var isObject3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
6118
8087
  var isId2 = (v) => typeof v === "string" || typeof v === "number" && Number.isFinite(v);
6119
8088
  function classifyMessage(raw) {
6120
- if (!isObject2(raw) || raw.jsonrpc !== "2.0") {
8089
+ if (!isObject3(raw) || raw.jsonrpc !== "2.0") {
6121
8090
  return {
6122
8091
  kind: "invalid",
6123
- id: isObject2(raw) && isId2(raw.id) ? raw.id : null,
8092
+ id: isObject3(raw) && isId2(raw.id) ? raw.id : null,
6124
8093
  reason: "Not a JSON-RPC 2.0 message"
6125
8094
  };
6126
8095
  }
@@ -6134,7 +8103,7 @@ function classifyMessage(raw) {
6134
8103
  reason: "Missing method"
6135
8104
  };
6136
8105
  }
6137
- if (raw.params !== void 0 && !isObject2(raw.params)) {
8106
+ if (raw.params !== void 0 && !isObject3(raw.params)) {
6138
8107
  return {
6139
8108
  kind: "invalid",
6140
8109
  id: isId2(raw.id) ? raw.id : null,
@@ -6317,22 +8286,22 @@ function createMcpServer(options) {
6317
8286
  }
6318
8287
 
6319
8288
  // src/reference.ts
6320
- var import_node_fs5 = require("node:fs");
6321
- var import_node_path5 = require("node:path");
8289
+ var import_node_fs7 = require("node:fs");
8290
+ var import_node_path7 = require("node:path");
6322
8291
  var URI_PREFIX = "tutticode://skills/";
6323
8292
  var cache;
6324
8293
  function loadReference(dir = skillsDir()) {
6325
8294
  if (cache?.dir === dir)
6326
8295
  return cache.docs;
6327
8296
  const docs = [];
6328
- if ((0, import_node_fs5.existsSync)(dir))
6329
- for (const skill of (0, import_node_fs5.readdirSync)(dir).sort()) {
6330
- const root = (0, import_node_path5.join)(dir, skill);
6331
- 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")))
6332
8301
  continue;
6333
- for (const file of walk(root)) {
6334
- const path = (0, import_node_path5.relative)(root, file).split(import_node_path5.sep).join("/");
6335
- 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");
6336
8305
  docs.push({
6337
8306
  uri: `${URI_PREFIX}${skill}/${path}`,
6338
8307
  skill,
@@ -6346,9 +8315,9 @@ function loadReference(dir = skillsDir()) {
6346
8315
  return docs;
6347
8316
  }
6348
8317
  function walk(dir) {
6349
- return (0, import_node_fs5.readdirSync)(dir).sort().flatMap((name) => {
6350
- const path = (0, import_node_path5.join)(dir, name);
6351
- 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())
6352
8321
  return walk(path);
6353
8322
  return name.endsWith(".md") ? [path] : [];
6354
8323
  });
@@ -6428,8 +8397,8 @@ function count(haystack, needle) {
6428
8397
  }
6429
8398
 
6430
8399
  // src/tools.ts
6431
- var import_node_os3 = require("node:os");
6432
- var import_node_path6 = require("node:path");
8400
+ var import_node_os4 = require("node:os");
8401
+ var import_node_path8 = require("node:path");
6433
8402
 
6434
8403
  // ../mcp-protocol/src/code-tools.ts
6435
8404
  var CODE_CHECK_TOOL = "code_check";
@@ -6986,7 +8955,9 @@ function codeFragment(code) {
6986
8955
  // src/tools.ts
6987
8956
  var CODE_OPEN_TOOL = "code_open";
6988
8957
  var CODE_REFERENCE_TOOL = "code_reference";
6989
- 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.`;
6990
8961
  var codeProperty2 = codeCheckTool.inputSchema.properties.code;
6991
8962
  var renderProperties2 = {
6992
8963
  code: codeProperty2,
@@ -7064,6 +9035,10 @@ var renderProperties2 = {
7064
9035
  stemsDir: {
7065
9036
  type: "string",
7066
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).'
7067
9042
  }
7068
9043
  };
7069
9044
  var localCheckTool = {
@@ -7073,7 +9048,7 @@ var localCheckTool = {
7073
9048
  var localRenderTool = {
7074
9049
  name: "code_render",
7075
9050
  title: "Render TuttiCode to a file",
7076
- 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}`,
7077
9052
  inputSchema: {
7078
9053
  type: "object",
7079
9054
  properties: renderProperties2,
@@ -7152,14 +9127,30 @@ var codeReferenceTool = {
7152
9127
  openWorldHint: false
7153
9128
  }
7154
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
+ };
7155
9145
  var LOCAL_TOOLS = [
7156
9146
  localCheckTool,
7157
9147
  localRenderTool,
7158
9148
  localCompareTool,
7159
9149
  codeOpenTool,
7160
- codeReferenceTool
9150
+ codeReferenceTool,
9151
+ codeAccountTool
7161
9152
  ];
7162
- 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.`;
7163
9154
  var MAX_STORED = 64;
7164
9155
  var CHECK_TIMEOUT_MS = 9e4;
7165
9156
  var RENDER_TIMEOUT_MS = 15 * 6e4;
@@ -7178,7 +9169,7 @@ function createToolHandler(deps) {
7178
9169
  async function check2(args, signal) {
7179
9170
  const command = {
7180
9171
  type: "check",
7181
- id: (0, import_node_crypto2.randomUUID)(),
9172
+ id: (0, import_node_crypto3.randomUUID)(),
7182
9173
  code: args.code,
7183
9174
  ...args.bpm !== void 0 ? { bpm: args.bpm } : {}
7184
9175
  };
@@ -7210,7 +9201,7 @@ function createToolHandler(deps) {
7210
9201
  `format ${String(args.format)} does not match out ${out}.`
7211
9202
  );
7212
9203
  const format = args.format ?? outFormat ?? "wav";
7213
- const id = (0, import_node_crypto2.randomUUID)();
9204
+ const id = (0, import_node_crypto3.randomUUID)();
7214
9205
  const length = lengths.length ? { [lengths[0]]: args[lengths[0]] } : void 0;
7215
9206
  const command = {
7216
9207
  type: "render",
@@ -7230,7 +9221,8 @@ function createToolHandler(deps) {
7230
9221
  "tail",
7231
9222
  "lufs",
7232
9223
  "truePeak",
7233
- "limit"
9224
+ "limit",
9225
+ "brandTag"
7234
9226
  ])
7235
9227
  if (args[key] !== void 0)
7236
9228
  command[key] = args[key];
@@ -7244,8 +9236,8 @@ function createToolHandler(deps) {
7244
9236
  });
7245
9237
  if (outcome.kind !== "rendered")
7246
9238
  return errorResult(outcome);
7247
- const path = out ?? (0, import_node_path6.join)(
7248
- 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"),
7249
9241
  `render-${id.slice(0, 8)}.${format}`
7250
9242
  );
7251
9243
  const files = writeRender(
@@ -7254,10 +9246,15 @@ function createToolHandler(deps) {
7254
9246
  path,
7255
9247
  typeof args.stemsDir === "string" ? args.stemsDir : void 0
7256
9248
  );
7257
- 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
+ });
7258
9254
  const { meta } = outcome;
7259
9255
  return {
7260
9256
  id,
9257
+ watermarked: !!meta.watermark,
7261
9258
  report: {
7262
9259
  id,
7263
9260
  kind: "render",
@@ -7285,7 +9282,9 @@ function createToolHandler(deps) {
7285
9282
  beatTimes: meta.beatTimes,
7286
9283
  check: meta.check,
7287
9284
  ...meta.analysis ? { analysis: meta.analysis } : {},
7288
- engineVersion: meta.engineVersion
9285
+ engineVersion: meta.engineVersion,
9286
+ ...meta.watermark ? { watermark: meta.watermark, watermarkNote: WATERMARK_LINE } : {},
9287
+ ...signInNote(outcome.auth)
7289
9288
  }
7290
9289
  };
7291
9290
  }
@@ -7325,9 +9324,11 @@ function createToolHandler(deps) {
7325
9324
  return invalid("Give goals or againstRenderId.");
7326
9325
  let analysis;
7327
9326
  let rendered;
9327
+ let tagged = target === "render" && !!renders.get(String(args.againstRenderId))?.watermark;
7328
9328
  if (args.renderId !== void 0) {
7329
9329
  if (args.code !== void 0)
7330
9330
  return invalid("Give renderId or code, not both.");
9331
+ tagged ||= !!renders.get(String(args.renderId))?.watermark;
7331
9332
  analysis = renders.get(String(args.renderId))?.analysis;
7332
9333
  if (!analysis)
7333
9334
  return invalid(
@@ -7345,16 +9346,34 @@ function createToolHandler(deps) {
7345
9346
  rendered = await render2(renderArgs, signal);
7346
9347
  if (!("report" in rendered))
7347
9348
  return rendered;
9349
+ tagged ||= rendered.watermarked;
7348
9350
  analysis = rendered.report.analysis;
7349
9351
  if (!analysis)
7350
9352
  return invalid("The render came back without an analysis.");
7351
9353
  } else
7352
9354
  return invalid("Give renderId or code.");
7353
9355
  const result = compareAnalysis(analysis, goals, target);
9356
+ const note = tagged ? { watermarkNote: WATERMARK_LINE } : {};
7354
9357
  return textResult(
7355
- 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 }
7356
9359
  );
7357
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
+ }
7358
9377
  return async function callTool(name, args, signal) {
7359
9378
  switch (name) {
7360
9379
  case "code_check":
@@ -7389,6 +9408,8 @@ function createToolHandler(deps) {
7389
9408
  }
7390
9409
  case CODE_REFERENCE_TOOL:
7391
9410
  return reference(args);
9411
+ case CODE_ACCOUNT_TOOL:
9412
+ return account();
7392
9413
  default:
7393
9414
  return invalid(`Unknown tool ${name}.`);
7394
9415
  }
@@ -7422,12 +9443,26 @@ function errorResult(outcome) {
7422
9443
  return invalid(`Unexpected ${outcome.kind}.`);
7423
9444
  return textResult(
7424
9445
  {
7425
- error: { code: outcome.code, message: outcome.message },
7426
- ...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)
7427
9454
  },
7428
9455
  true
7429
9456
  );
7430
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
+ }
7431
9466
  var GOAL_KEYS = {
7432
9467
  style: (v) => typeof v === "string" && v.length > 0 && v.length <= 64,
7433
9468
  lufs: (v) => num(v, -70, 0),
@@ -7469,10 +9504,11 @@ function serveMcpStdio(options) {
7469
9504
  backend: options.backend,
7470
9505
  styles: options.styles,
7471
9506
  codeLink: options.codeLink,
7472
- canShow: options.canShow
9507
+ canShow: options.canShow,
9508
+ account: options.account
7473
9509
  });
7474
9510
  const server = createMcpServer({
7475
- serverInfo: { name: "tutticode", title: "TuttiCode", version: VERSION },
9511
+ serverInfo: { name: "tutticode", title: "TuttiCode", version: VERSION2 },
7476
9512
  instructions: MCP_INSTRUCTIONS,
7477
9513
  listTools: () => [...LOCAL_TOOLS],
7478
9514
  callTool: (name, args, ctx) => callTool(name, args, ctx.signal),
@@ -7510,7 +9546,7 @@ function serveMcpStdio(options) {
7510
9546
  });
7511
9547
  });
7512
9548
  }
7513
- function sessionBackend(create) {
9549
+ function sessionBackend(create, auth) {
7514
9550
  const sessions = /* @__PURE__ */ new Map();
7515
9551
  const get = (mode) => {
7516
9552
  let session = sessions.get(mode);
@@ -7521,21 +9557,57 @@ function sessionBackend(create) {
7521
9557
  return session;
7522
9558
  };
7523
9559
  return {
7524
- 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
+ },
7525
9572
  close: async () => {
7526
9573
  await Promise.all([...sessions.values()].map((s) => s.close()));
7527
9574
  }
7528
9575
  };
7529
9576
  }
7530
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
+
7531
9603
  // src/setup.ts
7532
- var import_node_child_process3 = require("node:child_process");
7533
- var import_node_fs6 = require("node:fs");
7534
- 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");
7535
9607
  var POLICY_NAME = "LocalNetworkAccessAllowedForUrls";
7536
9608
  var POLICY_URL = "https://tuttidaw.com";
7537
9609
  var exec = (command, args) => {
7538
- 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 });
7539
9611
  return { status: r2.status, stdout: r2.stdout ?? "" };
7540
9612
  };
7541
9613
  var WINDOWS_KEYS = [
@@ -7650,16 +9722,16 @@ function policySteps(undo, platform = process.platform, run = exec) {
7650
9722
  };
7651
9723
  });
7652
9724
  return LINUX_POLICY_FILES.map(
7653
- (file) => undo ? {
7654
- describe: `delete ${file}`,
9725
+ (file2) => undo ? {
9726
+ describe: `delete ${file2}`,
7655
9727
  run() {
7656
- (0, import_node_fs6.rmSync)(file, { force: true });
9728
+ (0, import_node_fs9.rmSync)(file2, { force: true });
7657
9729
  }
7658
9730
  } : {
7659
- describe: `write ${file} (${POLICY_NAME}: ["${POLICY_URL}"])`,
9731
+ describe: `write ${file2} (${POLICY_NAME}: ["${POLICY_URL}"])`,
7660
9732
  run() {
7661
- (0, import_node_fs6.mkdirSync)((0, import_node_path7.dirname)(file), { recursive: true });
7662
- (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());
7663
9735
  }
7664
9736
  }
7665
9737
  );
@@ -7675,22 +9747,22 @@ for your user. It allows nothing else, and you can see it at chrome://policy
7675
9747
  (edge://policy). Undo any time with: tutticode setup --undo`;
7676
9748
 
7677
9749
  // src/skills.ts
7678
- var import_node_fs7 = require("node:fs");
7679
- var import_node_os4 = require("node:os");
7680
- var import_node_path8 = require("node:path");
7681
- function targetDir(target, home = (0, import_node_os4.homedir)()) {
7682
- 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");
7683
9755
  }
7684
9756
  function shippedSkills(dir = skillsDir()) {
7685
- if (!(0, import_node_fs7.existsSync)(dir))
9757
+ if (!(0, import_node_fs10.existsSync)(dir))
7686
9758
  return [];
7687
- return (0, import_node_fs7.readdirSync)(dir).filter(
7688
- (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"))
7689
9761
  ).sort();
7690
9762
  }
7691
- function defaultTargets(home = (0, import_node_os4.homedir)()) {
9763
+ function defaultTargets(home = (0, import_node_os5.homedir)()) {
7692
9764
  const found = ["claude", "copilot", "codex"].filter(
7693
- (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}`))
7694
9766
  );
7695
9767
  return found.length ? [...found] : ["claude"];
7696
9768
  }
@@ -7701,9 +9773,9 @@ function installSkills(destinations, source = skillsDir()) {
7701
9773
  const installed = [];
7702
9774
  for (const destination of destinations) {
7703
9775
  for (const skill of skills) {
7704
- const path = (0, import_node_path8.resolve)(destination, skill);
7705
- (0, import_node_fs7.rmSync)(path, { recursive: true, force: true });
7706
- (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 });
7707
9779
  installed.push({ skill, path });
7708
9780
  }
7709
9781
  }
@@ -7946,7 +10018,19 @@ var afrobeats = {
7946
10018
  p90: [-8.9, -1.9, -2.9, -4.9, -6.9, -8.9, -9.9, -11.9, -12.9, -17.9],
7947
10019
  source: TONAL_BALANCE_SOURCE
7948
10020
  },
7949
- 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
+ },
7950
10034
  reverb: "EQ Eight into light Compressor on vocal; Reverb short room for percussion and a darker plate for voice.",
7951
10035
  delay: "Echo dotted eighth on guitar or phrase endings; filter repeats away from the lead vocal.",
7952
10036
  stereo: "Center kick, bass and lead vocal; spread the guitar and shaker without widening the sub.",
@@ -8731,7 +10815,19 @@ var boomBap = {
8731
10815
  p90: [-12.2, -3.2, -2.2, -4.2, -6.2, -8.2, -9.2, -12.2, -16.2, -23.2],
8732
10816
  source: TONAL_BALANCE_SOURCE
8733
10817
  },
8734
- 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
+ },
8735
10831
  reverb: "Redux lightly on the sample, Saturator for body, EQ Eight to trim hiss; short Reverb on snare, rap mostly dry.",
8736
10832
  delay: "Echo throws on selected rhyme endings, not across the whole verse.",
8737
10833
  stereo: "Centered kick/snare/bass/vocal; sample can be narrow with wider hook answers.",
@@ -9238,11 +11334,11 @@ var cinematic = {
9238
11334
  p90: [-11.4, -4.4, -3.4, -3.4, -5.4, -7.4, -8.4, -10.4, -12.4, -18.4],
9239
11335
  source: TONAL_BALANCE_SOURCE
9240
11336
  },
9241
- sidechain: "Compressor can briefly clear pad/sub for impacts; avoid audible dance pumping unless the cue is explicitly electronic.",
11337
+ sidechain: null,
9242
11338
  reverb: "EQ Eight before Hybrid Reverb keeps the hall clear; different predelays establish near piano and distant orchestra.",
9243
11339
  delay: "Echo can extend a pulse, but sync it so the ostinato does not become an accidental flam.",
9244
11340
  stereo: "Strings and upper brass broad, low drums/bass centered; keep dialogue space in the middle when scoring picture.",
9245
- 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."
9246
11342
  },
9247
11343
  arrangement: {
9248
11344
  variationEveryBars: 4,
@@ -10593,7 +12689,7 @@ var disco = {
10593
12689
  p90: [-12.1, -3.1, -2.1, -5.1, -7.1, -8.1, -9.1, -10.1, -11.1, -17.1],
10594
12690
  source: TONAL_BALANCE_SOURCE
10595
12691
  },
10596
- sidechain: "Little or none for the live-band feel; shorten bass notes before ducking.",
12692
+ sidechain: null,
10597
12693
  reverb: "Reverb plate on strings and vocal, small room on drums.",
10598
12694
  delay: "Quiet Echo slap on vocal; avoid busy feedback against guitar sixteenths.",
10599
12695
  stereo: "Strings wide, rhythm guitar to one side with keys answering; bass and kick centered.",
@@ -10912,14 +13008,34 @@ var drumAndBass = {
10912
13008
  "Climbing Keys G Minor 140 bpm"
10913
13009
  ],
10914
13010
  mix: {
10915
- lufs: { min: -8, max: -5 },
13011
+ lufs: { min: -9, max: -7 },
10916
13012
  tonalBalance: {
10917
13013
  p10: [-14.5, -9, -9.5, -12.5, -15, -16, -16.5, -17, -19, -25.5],
10918
13014
  p50: [-9.5, -5, -6.5, -9.5, -12, -13, -13.5, -14, -15, -20.5],
10919
13015
  p90: [-4.5, -1, -3.5, -6.5, -9, -10, -10.5, -11, -11, -15.5],
10920
13016
  source: TONAL_BALANCE_SOURCE
10921
13017
  },
10922
- 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
+ },
10923
13039
  reverb: "Short, bright snare reverb; big wash on breakdown pads/vocals.",
10924
13040
  delay: "1/4 or 1/8 dotted on vocals; delay throws into drops.",
10925
13041
  stereo: "Sub and kick mono, reese mids wide, breaks slightly wide, pads very wide.",
@@ -11194,7 +13310,7 @@ var dubstep = {
11194
13310
  },
11195
13311
  starterClips: ["Dub Halftime 136 bpm", "Halftime Pattern 136 bpm"],
11196
13312
  mix: {
11197
- lufs: { min: -10, max: -6 },
13313
+ lufs: { min: -10, max: -8 },
11198
13314
  tonalBalance: {
11199
13315
  p10: [
11200
13316
  -11.1,
@@ -11212,7 +13328,19 @@ var dubstep = {
11212
13328
  p90: [-1.1, -0.1, -5.1, -9.1, -11.1, -11.1, -11.1, -13.1, -15.1, -21.1],
11213
13329
  source: TONAL_BALANCE_SOURCE
11214
13330
  },
11215
- 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
+ },
11216
13344
  reverb: "Dark Reverb on snare and isolated bells; no reverb on the clean sub.",
11217
13345
  delay: "Dub-style Echo throws into the gaps, filtered and automated before the next bass call.",
11218
13346
  stereo: "Sub and kick mono; widen only the upper bass layer and atmospheric answers.",
@@ -11857,7 +13985,7 @@ var funk = {
11857
13985
  p90: [-13.5, -3.5, -2.5, -4.5, -6.5, -7.5, -8.5, -10.5, -12.5, -19.5],
11858
13986
  source: TONAL_BALANCE_SOURCE
11859
13987
  },
11860
- sidechain: "Usually none: note lengths and rests make space before compression.",
13988
+ sidechain: null,
11861
13989
  reverb: "Short Reverb room; keep guitar scratches and bass dry.",
11862
13990
  delay: "Brief Echo slap on a horn or vocal answer only.",
11863
13991
  stereo: "Bass, snare and lead centered; guitar and clav answer from opposite sides.",
@@ -11874,7 +14002,7 @@ var funk = {
11874
14002
  {
11875
14003
  kind: "breakdown",
11876
14004
  where: "Middle",
11877
- how: "Expose drums and bass; reintroduce clav before horns."
14005
+ how: "Expose the drums (and bass, in a bass feature); reintroduce clav before horns."
11878
14006
  },
11879
14007
  {
11880
14008
  kind: "variation",
@@ -12120,7 +14248,7 @@ var futureBass = {
12120
14248
  },
12121
14249
  starterClips: [],
12122
14250
  mix: {
12123
- lufs: { min: -10, max: -7 },
14251
+ lufs: { min: -10, max: -8 },
12124
14252
  tonalBalance: {
12125
14253
  p10: [
12126
14254
  -17.8,
@@ -12138,7 +14266,27 @@ var futureBass = {
12138
14266
  p90: [-7.8, -1.8, -4.8, -6.8, -6.8, -6.8, -7.8, -8.8, -9.8, -14.8],
12139
14267
  source: TONAL_BALANCE_SOURCE
12140
14268
  },
12141
- 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
+ },
12142
14290
  reverb: "EQ Eight clears chord low mids, Saturator adds body, Chorus-Ensemble widens upper layers, then Reverb; keep dry transients in front.",
12143
14291
  delay: "Echo dotted eighth on vocal chops; automate throws into gaps rather than washing out the next chord.",
12144
14292
  stereo: "Wide chords and chops above a centered sub/kick/snare; mono-check detuned layers.",
@@ -12410,7 +14558,19 @@ var gospel = {
12410
14558
  p90: [-13.1, -5.1, -4.1, -4.1, -5.1, -6.1, -7.1, -9.1, -11.1, -17.1],
12411
14559
  source: TONAL_BALANCE_SOURCE
12412
14560
  },
12413
- 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
+ },
12414
14574
  reverb: "EQ Eight then Compressor on lead/choir; shared Reverb hall with enough predelay to keep consonants clear.",
12415
14575
  delay: "Low Echo throws behind the leader; do not repeat over the choir response.",
12416
14576
  stereo: "Lead center, choir spread, organ behind the words; retain mono bass.",
@@ -12464,7 +14624,7 @@ var guofengPop = {
12464
14624
  feel: {
12465
14625
  kind: "straight",
12466
14626
  swingPercent: 50,
12467
- 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."
12468
14628
  },
12469
14629
  keys: ["D major", "E major", "G major", "B minor", "E minor"],
12470
14630
  forms: [
@@ -12769,7 +14929,7 @@ var guofengPop = {
12769
14929
  reverb: "Large hall on flute, zither and vocal (the misty space is the style), pre-delay for clear words; drums dry.",
12770
14930
  delay: "Dotted-1/8 Echo on flute and plucks; vocal throws on line ends.",
12771
14931
  stereo: "Zither and strings wide, flute off-centre, vocal centred on top.",
12772
- 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."
12773
14933
  },
12774
14934
  arrangement: {
12775
14935
  variationEveryBars: 8,
@@ -12797,7 +14957,7 @@ var guofengPop = {
12797
14957
  ]
12798
14958
  },
12799
14959
  references: ["Jay Chou", "JJ Lin", "Silence Wang", "Henry Huo", "Isaac Hong"],
12800
- 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."
12801
14961
  };
12802
14962
 
12803
14963
  // ../../src/components/editor-ai/style/cards/house.ts
@@ -13095,14 +15255,34 @@ var house = {
13095
15255
  "House 90s C Minor 128 bpm"
13096
15256
  ],
13097
15257
  mix: {
13098
- lufs: { min: -9, max: -7 },
15258
+ lufs: { min: -10, max: -8 },
13099
15259
  tonalBalance: {
13100
15260
  p10: [-15.5, -8.5, -9, -12, -14.5, -16, -17, -18, -20, -26.5],
13101
15261
  p50: [-10.5, -4.5, -6, -9, -11.5, -13, -14, -15, -16, -21.5],
13102
15262
  p90: [-5.5, -0.5, -3, -6, -8.5, -10, -11, -12, -12, -16.5],
13103
15263
  source: TONAL_BALANCE_SOURCE
13104
15264
  },
13105
- 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
+ },
13106
15286
  reverb: "Short plate on clap, large hall on breakdown pads/vocals.",
13107
15287
  delay: "1/8 dotted or 3/16 on stabs and vocals, filtered.",
13108
15288
  stereo: "Kick, bass, clap centered; hats/shaker/perc spread wide.",
@@ -13734,7 +15914,7 @@ var jpopAnime = {
13734
15914
  "Downwards A Minor 141 bpm"
13735
15915
  ],
13736
15916
  mix: {
13737
- lufs: { min: -9, max: -6 },
15917
+ lufs: { min: -10, max: -8 },
13738
15918
  tonalBalance: {
13739
15919
  p10: [-20.5, -12, -9.5, -10.5, -12, -13, -14, -15, -18, -26],
13740
15920
  p50: [-15.5, -8, -6.5, -7.5, -9, -10, -11, -12, -14, -21],
@@ -13745,7 +15925,7 @@ var jpopAnime = {
13745
15925
  reverb: "Bright plate on vocal and snare, hall on strings.",
13746
15926
  delay: "Dotted-1/8 vocal delay tucked under.",
13747
15927
  stereo: "Doubled guitars hard L/R, strings wide, vocal centred on top.",
13748
- 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)."
13749
15929
  },
13750
15930
  arrangement: {
13751
15931
  variationEveryBars: 4,
@@ -13803,19 +15983,19 @@ var kpop = {
13803
15983
  "\uCF00\uC774\uD31D",
13804
15984
  "\uC544\uC774\uB3CC"
13805
15985
  ],
13806
- 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.",
13807
15987
  tempo: { min: 95, max: 130, typical: 120 },
13808
15988
  meter: "4/4",
13809
15989
  feel: {
13810
15990
  kind: "straight",
13811
15991
  swingPercent: 50,
13812
- note: "Quantized, punchy; rap/trap parts go half-time with 32nd hat rolls."
15992
+ note: "Quantized; trap parts half-time with 32nd hat rolls."
13813
15993
  },
13814
15994
  keys: ["F# minor", "C# minor", "G minor", "A minor", "Eb major"],
13815
15995
  forms: [
13816
15996
  {
13817
15997
  id: "title-track",
13818
- name: "Title track: (V/Pre/Chorus/Post) x2, Bridge, Dance Break, key up",
15998
+ name: "Title track, key-up end",
13819
15999
  sections: [
13820
16000
  {
13821
16001
  name: "Intro",
@@ -13835,9 +16015,9 @@ var kpop = {
13835
16015
  {
13836
16016
  name: "Pre-Chorus",
13837
16017
  bars: 8,
13838
- energy: 0.65,
16018
+ energy: 0.55,
13839
16019
  roles: ["drums", "bass", "chords", "pad", "vocal", "fx"],
13840
- drums: "verse",
16020
+ drums: "build",
13841
16021
  bass: "pulse"
13842
16022
  },
13843
16023
  {
@@ -13867,9 +16047,9 @@ var kpop = {
13867
16047
  {
13868
16048
  name: "Pre-Chorus",
13869
16049
  bars: 8,
13870
- energy: 0.7,
16050
+ energy: 0.6,
13871
16051
  roles: ["drums", "bass", "chords", "pad", "vocal", "fx"],
13872
- drums: "verse",
16052
+ drums: "build",
13873
16053
  bass: "pulse"
13874
16054
  },
13875
16055
  {
@@ -13915,7 +16095,7 @@ var kpop = {
13915
16095
  },
13916
16096
  {
13917
16097
  id: "beat-switch",
13918
- name: "Beat switch: rap open, trap chorus, dance-groove last chorus",
16098
+ name: "Beat switch: trap chorus, dance last chorus",
13919
16099
  sections: [
13920
16100
  {
13921
16101
  name: "Intro",
@@ -13935,8 +16115,9 @@ var kpop = {
13935
16115
  {
13936
16116
  name: "Pre-Chorus",
13937
16117
  bars: 8,
13938
- energy: 0.65,
13939
- roles: ["chords", "pad", "vocal", "fx"]
16118
+ energy: 0.6,
16119
+ roles: ["drums", "chords", "pad", "vocal", "fx"],
16120
+ drums: "build"
13940
16121
  },
13941
16122
  {
13942
16123
  name: "Chorus",
@@ -13981,6 +16162,7 @@ var kpop = {
13981
16162
  kick: "X..x..x.X.x.....X..x..x.X.....o.",
13982
16163
  snare: "....X.......X.......X.......X...",
13983
16164
  clap: "....x.......x.......x.......x...",
16165
+ snap: "....g.......g..o....g.......g...",
13984
16166
  hatClosed: "o.x.o.xxo.x.o.x.o.x.o.xxo.x.o...",
13985
16167
  hatOpen: "..............................x."
13986
16168
  }
@@ -14014,18 +16196,29 @@ var kpop = {
14014
16196
  kick: "X..X..X...X.X...",
14015
16197
  snare: "....X.......X...",
14016
16198
  clap: "....x..x....x..x",
16199
+ snap: "....x.......x...",
14017
16200
  hatClosed: "..x...x...x...x."
14018
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
+ }
14019
16212
  }
14020
16213
  ],
14021
16214
  fills: [
14022
16215
  {
14023
16216
  id: "riser-roll",
14024
16217
  everyBars: 8,
14025
- stepsPerBeat: 4,
14026
- steps: 8,
14027
- lanes: { snare: "x.x.xxxx", clap: "......XX" },
14028
- 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."
14029
16222
  },
14030
16223
  {
14031
16224
  id: "drop-gap",
@@ -14037,8 +16230,8 @@ var kpop = {
14037
16230
  }
14038
16231
  ],
14039
16232
  rules: [
14040
- "One groove per section: verse, trap rap, dance chorus, jersey dance break.",
14041
- "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."
14042
16235
  ]
14043
16236
  },
14044
16237
  bass: {
@@ -14067,7 +16260,7 @@ var kpop = {
14067
16260
  velocity: 110
14068
16261
  }
14069
16262
  ],
14070
- 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."
14071
16264
  },
14072
16265
  harmony: {
14073
16266
  modes: [
@@ -14086,7 +16279,7 @@ var kpop = {
14086
16279
  chords: ["i", "bVII", "bVI", "V"],
14087
16280
  weight: 2,
14088
16281
  barsPerChord: 1,
14089
- use: "major V pulls into the hook"
16282
+ use: "major V into the hook"
14090
16283
  },
14091
16284
  {
14092
16285
  chords: ["IVmaj7", "V", "iii7", "vi"],
@@ -14102,28 +16295,24 @@ var kpop = {
14102
16295
  }
14103
16296
  ],
14104
16297
  extensions: ["add9", "maj7", "7", "sus4"],
14105
- voicing: "Close synth/brass stabs, pumping offbeats in the chorus; open piano+strings in the bridge.",
14106
- 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."
14107
16300
  },
14108
16301
  melody: {
14109
16302
  range: { low: 57, high: 79 },
14110
16303
  density: "dense",
14111
16304
  notesPerBar: [4, 12],
14112
- 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.",
14113
16306
  motifs: [
14114
16307
  "One-word title hook repeated 4x",
14115
16308
  "Pre-chorus climbs to a held note",
14116
- "Post-chorus: wordless synth or vocal-chop riff"
16309
+ "Post-chorus vocal-chop riff"
14117
16310
  ]
14118
16311
  },
14119
16312
  palette: {
14120
16313
  drums: {
14121
- keywords: ["punchy kick", "clap", "snap", "trap hats"],
14122
- presets: [
14123
- "drumrack:Crystal Clear Kit",
14124
- "drumrack:Street Mind Kit",
14125
- "drumrack:707 Core Kit"
14126
- ]
16314
+ keywords: ["punchy kick", "snap", "trap hats"],
16315
+ presets: ["drumrack:Street Mind Kit", "drumrack:Clint West Kit"]
14127
16316
  },
14128
16317
  bass: {
14129
16318
  keywords: ["808", "synth bass", "sub"],
@@ -14143,29 +16332,40 @@ var kpop = {
14143
16332
  },
14144
16333
  lead: {
14145
16334
  keywords: ["vocal chop", "pluck", "saw lead"],
14146
- presets: ["wavetable:Formant Vox", "meld:PolyPlucks"]
16335
+ presets: ["sampler:Vocal Chop Ah", "meld:PolyPlucks"]
14147
16336
  },
14148
16337
  fx: { keywords: ["riser", "impact", "downlifter", "reverse cymbal"] }
14149
16338
  },
14150
16339
  starterClips: [
14151
16340
  "Pop Riddim 124 bpm",
14152
- "Dancehall Trap 113 bpm",
14153
16341
  "LD Core Kit Pop 120 bpm",
14154
16342
  "Uplifting Club Vamp F Min 127 bpm"
14155
16343
  ],
14156
16344
  mix: {
14157
- lufs: { min: -9, max: -6.5 },
16345
+ lufs: { min: -11, max: -9 },
14158
16346
  tonalBalance: {
14159
16347
  p10: [-16.5, -9, -8.5, -11, -13, -14.5, -15.5, -16.5, -19, -26.5],
14160
16348
  p50: [-11.5, -5.5, -5.5, -8, -10, -11.5, -12.5, -13.5, -15, -21.5],
14161
16349
  p90: [-6.5, -1.5, -2.5, -5, -7, -8.5, -9.5, -10.5, -11, -16.5],
14162
16350
  source: TONAL_BALANCE_SOURCE
14163
16351
  },
14164
- 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
+ },
14165
16365
  reverb: "Bright vocal plate, dry rap; hall only in the bridge.",
14166
- delay: "Dotted-1/8 throws on hook words and ad-libs.",
14167
- stereo: "Wide doubles and gang vocals in the chorus; rap centred.",
14168
- 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."
14169
16369
  },
14170
16370
  arrangement: {
14171
16371
  variationEveryBars: 4,
@@ -14173,7 +16373,7 @@ var kpop = {
14173
16373
  {
14174
16374
  kind: "build",
14175
16375
  where: "Pre-chorus",
14176
- 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."
14177
16377
  },
14178
16378
  {
14179
16379
  kind: "breakdown",
@@ -14183,7 +16383,7 @@ var kpop = {
14183
16383
  {
14184
16384
  kind: "drop",
14185
16385
  where: "Dance break",
14186
- how: "Instrumental beat switch for the choreography."
16386
+ how: "Instrumental beat switch."
14187
16387
  },
14188
16388
  {
14189
16389
  kind: "key-change",
@@ -14193,7 +16393,7 @@ var kpop = {
14193
16393
  ]
14194
16394
  },
14195
16395
  references: ["BTS", "BLACKPINK", "TWICE", "Stray Kids", "NewJeans"],
14196
- 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."
14197
16397
  };
14198
16398
 
14199
16399
  // ../../src/components/editor-ai/style/cards/lofi-hip-hop.ts
@@ -14466,7 +16666,19 @@ var lofiHipHop = {
14466
16666
  p90: [-8.5, -2.5, -2, -4, -6, -8.5, -11.5, -15.5, -19.5, -28.5],
14467
16667
  source: TONAL_BALANCE_SOURCE
14468
16668
  },
14469
- 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
+ },
14470
16682
  reverb: "Small dark room on drums, short plate on keys; low wet levels.",
14471
16683
  delay: "Tape-style 1/8 dotted on chops/lead, low feedback, filtered repeats.",
14472
16684
  stereo: "Drums mostly mono; keys and textures wide.",
@@ -14853,7 +17065,19 @@ var mandopopUptempo = {
14853
17065
  p90: [-8, -2.5, -3, -5, -6.5, -8, -9, -10.5, -11.5, -17],
14854
17066
  source: TONAL_BALANCE_SOURCE
14855
17067
  },
14856
- 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
+ },
14857
17081
  reverb: "Plate on vocal and snare; hall only on the breakdown.",
14858
17082
  delay: "1/8 vocal echo under the chorus; 1/4 throws.",
14859
17083
  stereo: "Vocal loud and centred; doubles wide in the chorus.",
@@ -14915,7 +17139,7 @@ var modernPop = {
14915
17139
  feel: {
14916
17140
  kind: "straight",
14917
17141
  swingPercent: 50,
14918
- 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."
14919
17143
  },
14920
17144
  keys: ["C major", "G major", "D major", "A minor", "F# minor"],
14921
17145
  forms: [
@@ -15118,13 +17342,13 @@ var modernPop = {
15118
17342
  stepsPerBeat: 4,
15119
17343
  steps: 4,
15120
17344
  lanes: {},
15121
- note: "One beat of silence before the chorus (alternative to the snare build)."
17345
+ note: "A silent beat before the chorus."
15122
17346
  }
15123
17347
  ],
15124
17348
  rules: [
15125
17349
  "Pre-chorus builds: snare 8ths -> 16ths over the last 2 bars, or pull the kick out.",
15126
17350
  "Chorus adds a layer the verse lacks (claps, open hats, tambourine).",
15127
- "Keep velocities consistent; pop drums are tight, punchy and sample-replaced."
17351
+ "Tight, consistent velocities; punchy, sample-replaced."
15128
17352
  ]
15129
17353
  },
15130
17354
  bass: {
@@ -15146,7 +17370,7 @@ var modernPop = {
15146
17370
  velocity: 104
15147
17371
  }
15148
17372
  ],
15149
- 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."
15150
17374
  },
15151
17375
  harmony: {
15152
17376
  modes: [
@@ -15182,7 +17406,7 @@ var modernPop = {
15182
17406
  }
15183
17407
  ],
15184
17408
  extensions: ["add9", "sus2", "sus4", "maj7"],
15185
- 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.",
15186
17410
  harmonicRhythm: "1 chord per bar; same loop through verse and chorus, with energy doing the work."
15187
17411
  },
15188
17412
  melody: {
@@ -15251,11 +17475,23 @@ var modernPop = {
15251
17475
  p90: [-7.5, -2, -2.5, -5, -7, -8.5, -10, -11.5, -12.5, -18.5],
15252
17476
  source: TONAL_BALANCE_SOURCE
15253
17477
  },
15254
- 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
+ },
15255
17491
  reverb: "Vocal plate, short room on drums; long reverb only as throws.",
15256
17492
  delay: "1/4 and 1/8 vocal throws at phrase ends.",
15257
- stereo: "Doubles/backing vocals and synths wide in the chorus, narrower verse; low end mono.",
15258
- 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."
15259
17495
  },
15260
17496
  arrangement: {
15261
17497
  variationEveryBars: 8,
@@ -15294,7 +17530,7 @@ var modernPop = {
15294
17530
  "The Weeknd",
15295
17531
  "Ed Sheeran"
15296
17532
  ],
15297
- 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."
15298
17534
  };
15299
17535
 
15300
17536
  // ../../src/components/editor-ai/style/cards/neo-soul.ts
@@ -15524,7 +17760,19 @@ var neoSoul = {
15524
17760
  p90: [-12.5, -3.5, -2.5, -3.5, -5.5, -8.5, -10.5, -13.5, -16.5, -23.5],
15525
17761
  source: TONAL_BALANCE_SOURCE
15526
17762
  },
15527
- 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
+ },
15528
17776
  reverb: "Short dark Reverb room around the band; longer vocal plate audible only between phrases.",
15529
17777
  delay: "Filtered Echo throws on selected vocal endings.",
15530
17778
  stereo: "Keep lead and bass close and centered; electric piano medium-wide, backing vocals wider.",
@@ -15796,7 +18044,7 @@ var reggae = {
15796
18044
  p90: [-9.1, -1.1, -2.1, -5.1, -8.1, -10.1, -12.1, -14.1, -17.1, -23.1],
15797
18045
  source: TONAL_BALANCE_SOURCE
15798
18046
  },
15799
- sidechain: "Usually none; bass and sparse kick should coexist by arrangement and EQ Eight, not audible pumping.",
18047
+ sidechain: null,
15800
18048
  reverb: "Reverb on rim/organ for a short spring-like splash; EQ Eight cuts lows on the wet return.",
15801
18049
  delay: "Echo filtered tape-style throws; automate sends on isolated words or rim hits, then mute the dry source for dub space.",
15802
18050
  stereo: "Bass, kick and vocal centered; skank/organ apart; dub returns may travel wider.",
@@ -16060,7 +18308,19 @@ var reggaeton = {
16060
18308
  p90: [-6.6, -0.6, -3.6, -6.6, -8.6, -9.6, -9.6, -10.6, -11.6, -16.6],
16061
18309
  source: TONAL_BALANCE_SOURCE
16062
18310
  },
16063
- 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
+ },
16064
18324
  reverb: "EQ Eight and Compressor keep the vocal forward; short Reverb on snare, little on bass.",
16065
18325
  delay: "Echo on hook endings with filtered repeats, ducked or muted under the next line.",
16066
18326
  stereo: "Center kick, snare, sub and lead; widen supporting plucks and vocal doubles only.",
@@ -16109,7 +18369,7 @@ var rnb = {
16109
18369
  kind: "swing",
16110
18370
  swingPercent: 57,
16111
18371
  grooveId: "swing16-57",
16112
- 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)."
16113
18373
  },
16114
18374
  keys: ["Eb major", "Ab major", "Db major", "F minor", "C minor"],
16115
18375
  forms: [
@@ -16250,7 +18510,7 @@ var rnb = {
16250
18510
  stepsPerBeat: 4,
16251
18511
  lanes: {
16252
18512
  kick: "X.........x.....",
16253
- clap: "....x.......x...",
18513
+ snap: "....x.......x...",
16254
18514
  hatClosed: "x..x..x.o..x..x."
16255
18515
  }
16256
18516
  }
@@ -16337,7 +18597,7 @@ var rnb = {
16337
18597
  { chords: ["vi9", "ii9", "Imaj7", "IVmaj7"], weight: 1, barsPerChord: 1 }
16338
18598
  ],
16339
18599
  extensions: ["maj7", "maj9", "9", "11", "13", "7sus4"],
16340
- 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.",
16341
18601
  harmonicRhythm: "1 chord per bar, or a 2-chord vamp of 2 bars each."
16342
18602
  },
16343
18603
  melody: {
@@ -16406,11 +18666,23 @@ var rnb = {
16406
18666
  p90: [-5.5, -1, -4, -6.5, -8, -9.5, -10.5, -12, -13, -18.5],
16407
18667
  source: TONAL_BALANCE_SOURCE
16408
18668
  },
16409
- 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
+ },
16410
18682
  reverb: "Plate on the lead vocal, long hall on ad-libs and stacks; drums mostly dry.",
16411
18683
  delay: "1/4 and dotted-1/8 vocal throws, filtered; ping-pong on ad-libs.",
16412
18684
  stereo: "Lead vocal centred and intimate, harmony stack and keys wide.",
16413
- 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."
16414
18686
  },
16415
18687
  arrangement: {
16416
18688
  variationEveryBars: 4,
@@ -16443,7 +18715,7 @@ var rnb = {
16443
18715
  ]
16444
18716
  },
16445
18717
  references: ["SZA", "H.E.R.", "Daniel Caesar", "David Tao", "Khalil Fong"],
16446
- 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."
16447
18719
  };
16448
18720
 
16449
18721
  // ../../src/components/editor-ai/style/cards/synth-pop.ts
@@ -16711,7 +18983,19 @@ var synthPop = {
16711
18983
  p90: [-12.3, -3.3, -3.3, -5.3, -6.3, -6.3, -7.3, -9.3, -12.3, -18.3],
16712
18984
  source: TONAL_BALANCE_SOURCE
16713
18985
  },
16714
- 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
+ },
16715
18999
  reverb: "Reverb plate on snare/vocal; Gate the snare return for a short retro bloom.",
16716
19000
  delay: "Echo eighths or slap on the hook, ducked below lead words.",
16717
19001
  stereo: "Chorus-Ensemble on pads; center the lead vocal, bass and main hook.",
@@ -16997,7 +19281,19 @@ var synthwave = {
16997
19281
  p90: [-10.2, -2.2, -2.2, -5.2, -7.2, -8.2, -9.2, -11.2, -14.2, -20.2],
16998
19282
  source: TONAL_BALANCE_SOURCE
16999
19283
  },
17000
- 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
+ },
17001
19297
  reverb: "Reverb into Gate on the snare return; broad hall on pads.",
17002
19298
  delay: "Echo dotted eighth on arp gaps, lowpassed feedback.",
17003
19299
  stereo: "Chorus-Ensemble widens pads; lead and snare stay focused, sub mono.",
@@ -17284,7 +19580,19 @@ var techno = {
17284
19580
  p90: [-3.5, 0, -3.5, -7.5, -10.5, -11.5, -12.5, -13.5, -14.5, -20.5],
17285
19581
  source: TONAL_BALANCE_SOURCE
17286
19582
  },
17287
- 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
+ },
17288
19596
  reverb: "Separate Reverb rumble return, then EQ Eight to remove highs and subsonic build-up; dry kick remains clear.",
17289
19597
  delay: "Filtered dotted-eighth Echo on stabs; automate feedback at boundaries.",
17290
19598
  stereo: "Kick and rumble mono; higher percussion and dub reflections can spread.",
@@ -17544,14 +19852,26 @@ var trance = {
17544
19852
  },
17545
19853
  starterClips: ["909 Core Kit Off Hat 125 bpm", "Techno Heavy 133 bpm"],
17546
19854
  mix: {
17547
- lufs: { min: -9, max: -6 },
19855
+ lufs: { min: -10, max: -8 },
17548
19856
  tonalBalance: {
17549
19857
  p10: [-18, -9, -9, -13, -15, -15, -15, -16, -19, -26],
17550
19858
  p50: [-13, -5, -6, -10, -12, -12, -12, -13, -15, -21],
17551
19859
  p90: [-8, -1, -3, -7, -9, -9, -9, -10, -11, -16],
17552
19860
  source: TONAL_BALANCE_SOURCE
17553
19861
  },
17554
- 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
+ },
17555
19875
  reverb: "Long Reverb hall in breakdowns; highpass the return and reduce it at the drop.",
17556
19876
  delay: "Echo dotted eighth on the lead, filtered so the dry hook stays legible.",
17557
19877
  stereo: "Wide detuned lead and pad above the lows; kick/sub mono. Check the hook in mono.",
@@ -17857,14 +20177,26 @@ var trap = {
17857
20177
  "Sinister Run G Min 142 bpm"
17858
20178
  ],
17859
20179
  mix: {
17860
- lufs: { min: -9, max: -6 },
20180
+ lufs: { min: -10, max: -8 },
17861
20181
  tonalBalance: {
17862
20182
  p10: [-12.5, -8, -10, -13, -15.5, -17, -18, -19, -21.5, -28.5],
17863
20183
  p50: [-7.5, -4, -7, -10, -12.5, -14, -15, -16, -17.5, -23.5],
17864
20184
  p90: [-2.5, 0, -4, -7, -9.5, -11, -12, -13, -13.5, -18.5],
17865
20185
  source: TONAL_BALANCE_SOURCE
17866
20186
  },
17867
- 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
+ },
17868
20200
  reverb: "Big dark reverb on the melody; vocal upfront and nearly dry (short room, de-essed); drums dry.",
17869
20201
  delay: "1/4 or 1/8 throws on vocal ends and melody.",
17870
20202
  stereo: "Melody/pads wide, 808/kick/clap mono, hats slightly off-center.",
@@ -18151,7 +20483,19 @@ var ukGarage = {
18151
20483
  p90: [-5.7, -0.7, -2.7, -6.7, -9.7, -10.7, -10.7, -11.7, -12.7, -17.7],
18152
20484
  source: TONAL_BALANCE_SOURCE
18153
20485
  },
18154
- 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
+ },
18155
20499
  reverb: "Small Reverb room on clap/rim; a darker longer return for isolated vocal chops.",
18156
20500
  delay: "Filtered Echo dotted eighth on chops; silence the feedback before busy hook entries.",
18157
20501
  stereo: "Vocal chops and hats can answer left/right; kick, snare and sub stay centered.",
@@ -18274,14 +20618,16 @@ async function dispatch(command, io) {
18274
20618
  const text = HELP[command.topic ?? "main"] ?? HELP.main;
18275
20619
  io.stdout.write(
18276
20620
  `${text}
18277
- ${command.topic && command.topic !== "setup" && command.topic !== "skills" ? `
20621
+ ${command.topic && !["setup", "skills", "login", "logout", "whoami"].includes(
20622
+ command.topic
20623
+ ) ? `
18278
20624
  ${HELP.bridge}
18279
20625
  ` : ""}`
18280
20626
  );
18281
20627
  return EXIT_OK;
18282
20628
  }
18283
20629
  case "version":
18284
- io.stdout.write(`${VERSION}
20630
+ io.stdout.write(`${VERSION2}
18285
20631
  `);
18286
20632
  return EXIT_OK;
18287
20633
  case "check":
@@ -18294,6 +20640,15 @@ ${HELP.bridge}
18294
20640
  return mcp(command.bridge, io);
18295
20641
  case "doctor":
18296
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;
18297
20652
  case "setup":
18298
20653
  return setup(command, io);
18299
20654
  case "skills-install": {
@@ -18356,33 +20711,62 @@ async function runJob(job, flags, mode, io, timeoutS) {
18356
20711
  const onSigint = () => abort.abort();
18357
20712
  process.once("SIGINT", onSigint);
18358
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
+ `);
18359
20718
  try {
20719
+ let outcome;
18360
20720
  if (flags.noDaemon || io.openPage || !io.entry) {
18361
20721
  const session = newSession(flags, mode, io, log);
18362
20722
  try {
18363
- return await session.run(job, {
20723
+ outcome = await session.run(job, {
18364
20724
  timeoutMs: timeoutS * 1e3,
18365
20725
  signal: abort.signal,
18366
- onProgress
20726
+ onProgress,
20727
+ auth: auth.auth
18367
20728
  });
18368
20729
  } finally {
18369
20730
  await session.close();
18370
20731
  }
18371
- }
18372
- return await runViaDaemon(
18373
- { config: daemonConfig(flags, mode), entry: io.entry, log },
18374
- job,
18375
- timeoutS * 1e3,
18376
- {
18377
- signal: abort.signal,
18378
- onProgress,
18379
- onStatus: statusWriter(flags, mode, io)
18380
- }
18381
- );
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;
18382
20748
  } finally {
18383
20749
  process.removeListener("SIGINT", onSigint);
18384
20750
  }
18385
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
+ }
18386
20770
  function progressPrinter(io, verbose) {
18387
20771
  let last = "";
18388
20772
  return (p) => {
@@ -18395,27 +20779,32 @@ function progressPrinter(io, verbose) {
18395
20779
  last = line;
18396
20780
  };
18397
20781
  }
18398
- async function readProgram(file, io) {
18399
- if (file === "-") {
20782
+ async function readProgram(file2, io) {
20783
+ if (file2 === "-") {
18400
20784
  const chunks = [];
18401
20785
  for await (const chunk of io.stdin)
18402
20786
  chunks.push(Buffer.from(chunk));
18403
20787
  return Buffer.concat(chunks).toString("utf8");
18404
20788
  }
18405
20789
  try {
18406
- return (0, import_node_fs8.readFileSync)(file, "utf8");
20790
+ return (0, import_node_fs11.readFileSync)(file2, "utf8");
18407
20791
  } catch (e) {
18408
20792
  throw new UsageError(
18409
- `Cannot read ${file}: ${e.code ?? e.message}`
20793
+ `Cannot read ${file2}: ${e.code ?? e.message}`
18410
20794
  );
18411
20795
  }
18412
20796
  }
18413
- function printError(outcome, json, file, io) {
20797
+ function printError(outcome, json, file2, io) {
18414
20798
  if (json)
18415
20799
  io.stdout.write(
18416
20800
  `${JSON.stringify(
18417
20801
  {
18418
- 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 } } : {},
18419
20808
  ...outcome.report ? { report: outcome.report } : {}
18420
20809
  },
18421
20810
  null,
@@ -18424,7 +20813,7 @@ function printError(outcome, json, file, io) {
18424
20813
  `
18425
20814
  );
18426
20815
  if (outcome.report)
18427
- for (const line of formatDiagnostics(outcome.report, file))
20816
+ for (const line of formatDiagnostics(outcome.report, file2))
18428
20817
  io.stderr.write(`${line}
18429
20818
  `);
18430
20819
  io.stderr.write(`tutticode: ${outcome.code}: ${outcome.message}
@@ -18436,7 +20825,7 @@ async function check(command, io) {
18436
20825
  const outcome = await runJob(
18437
20826
  {
18438
20827
  type: "check",
18439
- id: (0, import_node_crypto2.randomUUID)(),
20828
+ id: (0, import_node_crypto3.randomUUID)(),
18440
20829
  code,
18441
20830
  ...command.bpm !== void 0 ? { bpm: command.bpm } : {}
18442
20831
  },
@@ -18467,8 +20856,15 @@ async function check(command, io) {
18467
20856
  }
18468
20857
  async function render(command, io) {
18469
20858
  const code = await readProgram(command.file, io);
20859
+ const brandTagOff = command.options.brandTag === false || process.env.TUTTICODE_BRAND_TAG === "0";
18470
20860
  const outcome = await runJob(
18471
- { 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
+ },
18472
20868
  command.bridge,
18473
20869
  modeFor(command.bridge, io),
18474
20870
  io,
@@ -18499,6 +20895,27 @@ async function render(command, io) {
18499
20895
  io.stdout.write(`${line}
18500
20896
  `);
18501
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
+ }
18502
20919
  return EXIT_OK;
18503
20920
  }
18504
20921
  async function open(command, io) {
@@ -18537,15 +20954,17 @@ ${url}
18537
20954
  }
18538
20955
  async function mcp(flags, io) {
18539
20956
  const log = createLogger("mcp", flags.verbose);
20957
+ const origin = accountOrigin(flags.origin);
18540
20958
  const { backend, close } = sessionBackend(
18541
20959
  (mode) => newSession(
18542
20960
  flags,
18543
20961
  flags.show ? "show" : mode,
18544
20962
  { ...io, stderr: process.stderr },
18545
20963
  log
18546
- )
20964
+ ),
20965
+ () => jobAuth(origin, "mcp", log)
18547
20966
  );
18548
- log("info", `mcp server ${VERSION} starting`);
20967
+ log("info", `mcp server ${VERSION2} starting`);
18549
20968
  await serveMcpStdio({
18550
20969
  input: io.stdin,
18551
20970
  output: io.stdout,
@@ -18558,7 +20977,8 @@ async function mcp(flags, io) {
18558
20977
  code
18559
20978
  ),
18560
20979
  // --headless: no window, not even for code_open
18561
- canShow: flags.show !== false
20980
+ canShow: flags.show !== false,
20981
+ account: () => accountState(origin)
18562
20982
  });
18563
20983
  await close();
18564
20984
  return EXIT_OK;
@@ -18642,14 +21062,313 @@ async function doctor(command, io) {
18642
21062
  )}) in ${info.browser.version ?? "the browser"}` : `${info.browser.version ?? "the browser"} accepted no Local Network Access grant (it may not enforce it)`
18643
21063
  );
18644
21064
  }
21065
+ await accountChecks(accountOrigin(flags.origin), command.fix, add);
18645
21066
  const ok = results.every((r2) => r2.ok || r2.check === "local network access");
18646
21067
  if (command.json)
18647
21068
  io.stdout.write(
18648
- `${JSON.stringify({ ok, version: VERSION, checks: results }, null, 2)}
21069
+ `${JSON.stringify({ ok, version: VERSION2, checks: results }, null, 2)}
18649
21070
  `
18650
21071
  );
18651
21072
  return ok ? EXIT_OK : EXIT_BRIDGE;
18652
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
+ }
18653
21372
  async function setup(command, io) {
18654
21373
  const steps = policySteps(command.undo);
18655
21374
  if (command.undo) {
@@ -18727,7 +21446,7 @@ function askOnTerminal(io) {
18727
21446
  }
18728
21447
 
18729
21448
  // src/bin.ts
18730
- setSkillsDir((0, import_node_path9.join)(__dirname, "skills"));
21449
+ setSkillsDir((0, import_node_path12.join)(__dirname, "skills"));
18731
21450
  main(process.argv.slice(2), {
18732
21451
  stdout: process.stdout,
18733
21452
  stderr: process.stderr,