gipity 1.0.428 → 1.0.429

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1166,7 +1166,7 @@ var require_command = __commonJS({
1166
1166
  "node_modules/commander/lib/command.js"(exports) {
1167
1167
  var EventEmitter = __require("node:events").EventEmitter;
1168
1168
  var childProcess = __require("node:child_process");
1169
- var path4 = __require("node:path");
1169
+ var path5 = __require("node:path");
1170
1170
  var fs6 = __require("node:fs");
1171
1171
  var process2 = __require("node:process");
1172
1172
  var { Argument: Argument2, humanReadableArgName } = require_argument();
@@ -2166,9 +2166,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
2166
2166
  let launchWithNode = false;
2167
2167
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
2168
2168
  function findFile(baseDir, baseName) {
2169
- const localBin = path4.resolve(baseDir, baseName);
2169
+ const localBin = path5.resolve(baseDir, baseName);
2170
2170
  if (fs6.existsSync(localBin)) return localBin;
2171
- if (sourceExt.includes(path4.extname(baseName))) return void 0;
2171
+ if (sourceExt.includes(path5.extname(baseName))) return void 0;
2172
2172
  const foundExt = sourceExt.find(
2173
2173
  (ext) => fs6.existsSync(`${localBin}${ext}`)
2174
2174
  );
@@ -2186,17 +2186,17 @@ Expecting one of '${allowedValues.join("', '")}'`);
2186
2186
  } catch {
2187
2187
  resolvedScriptPath = this._scriptPath;
2188
2188
  }
2189
- executableDir = path4.resolve(
2190
- path4.dirname(resolvedScriptPath),
2189
+ executableDir = path5.resolve(
2190
+ path5.dirname(resolvedScriptPath),
2191
2191
  executableDir
2192
2192
  );
2193
2193
  }
2194
2194
  if (executableDir) {
2195
2195
  let localFile = findFile(executableDir, executableFile);
2196
2196
  if (!localFile && !subcommand._executableFile && this._scriptPath) {
2197
- const legacyName = path4.basename(
2197
+ const legacyName = path5.basename(
2198
2198
  this._scriptPath,
2199
- path4.extname(this._scriptPath)
2199
+ path5.extname(this._scriptPath)
2200
2200
  );
2201
2201
  if (legacyName !== this._name) {
2202
2202
  localFile = findFile(
@@ -2207,7 +2207,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2207
2207
  }
2208
2208
  executableFile = localFile || executableFile;
2209
2209
  }
2210
- launchWithNode = sourceExt.includes(path4.extname(executableFile));
2210
+ launchWithNode = sourceExt.includes(path5.extname(executableFile));
2211
2211
  let proc;
2212
2212
  if (process2.platform !== "win32") {
2213
2213
  if (launchWithNode) {
@@ -3054,7 +3054,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
3054
3054
  * @return {Command}
3055
3055
  */
3056
3056
  nameFromFilename(filename) {
3057
- this._name = path4.basename(filename, path4.extname(filename));
3057
+ this._name = path5.basename(filename, path5.extname(filename));
3058
3058
  return this;
3059
3059
  }
3060
3060
  /**
@@ -3068,9 +3068,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
3068
3068
  * @param {string} [path]
3069
3069
  * @return {(string|null|Command)}
3070
3070
  */
3071
- executableDir(path5) {
3072
- if (path5 === void 0) return this._executableDir;
3073
- this._executableDir = path5;
3071
+ executableDir(path6) {
3072
+ if (path6 === void 0) return this._executableDir;
3073
+ this._executableDir = path6;
3074
3074
  return this;
3075
3075
  }
3076
3076
  /**
@@ -3639,7 +3639,7 @@ var require_ignore = __commonJS({
3639
3639
  // path matching.
3640
3640
  // - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
3641
3641
  // @returns {TestResult} true if a file is ignored
3642
- test(path4, checkUnignored, mode) {
3642
+ test(path5, checkUnignored, mode) {
3643
3643
  let ignored = false;
3644
3644
  let unignored = false;
3645
3645
  let matchedRule;
@@ -3648,7 +3648,7 @@ var require_ignore = __commonJS({
3648
3648
  if (unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) {
3649
3649
  return;
3650
3650
  }
3651
- const matched = rule[mode].test(path4);
3651
+ const matched = rule[mode].test(path5);
3652
3652
  if (!matched) {
3653
3653
  return;
3654
3654
  }
@@ -3669,17 +3669,17 @@ var require_ignore = __commonJS({
3669
3669
  var throwError = (message, Ctor) => {
3670
3670
  throw new Ctor(message);
3671
3671
  };
3672
- var checkPath = (path4, originalPath, doThrow) => {
3673
- if (!isString(path4)) {
3672
+ var checkPath = (path5, originalPath, doThrow) => {
3673
+ if (!isString(path5)) {
3674
3674
  return doThrow(
3675
3675
  `path must be a string, but got \`${originalPath}\``,
3676
3676
  TypeError
3677
3677
  );
3678
3678
  }
3679
- if (!path4) {
3679
+ if (!path5) {
3680
3680
  return doThrow(`path must not be empty`, TypeError);
3681
3681
  }
3682
- if (checkPath.isNotRelative(path4)) {
3682
+ if (checkPath.isNotRelative(path5)) {
3683
3683
  const r = "`path.relative()`d";
3684
3684
  return doThrow(
3685
3685
  `path should be a ${r} string, but got "${originalPath}"`,
@@ -3688,7 +3688,7 @@ var require_ignore = __commonJS({
3688
3688
  }
3689
3689
  return true;
3690
3690
  };
3691
- var isNotRelative = (path4) => REGEX_TEST_INVALID_PATH.test(path4);
3691
+ var isNotRelative = (path5) => REGEX_TEST_INVALID_PATH.test(path5);
3692
3692
  checkPath.isNotRelative = isNotRelative;
3693
3693
  checkPath.convert = (p) => p;
3694
3694
  var Ignore = class {
@@ -3718,19 +3718,19 @@ var require_ignore = __commonJS({
3718
3718
  }
3719
3719
  // @returns {TestResult}
3720
3720
  _test(originalPath, cache, checkUnignored, slices) {
3721
- const path4 = originalPath && checkPath.convert(originalPath);
3721
+ const path5 = originalPath && checkPath.convert(originalPath);
3722
3722
  checkPath(
3723
- path4,
3723
+ path5,
3724
3724
  originalPath,
3725
3725
  this._strictPathCheck ? throwError : RETURN_FALSE
3726
3726
  );
3727
- return this._t(path4, cache, checkUnignored, slices);
3727
+ return this._t(path5, cache, checkUnignored, slices);
3728
3728
  }
3729
- checkIgnore(path4) {
3730
- if (!REGEX_TEST_TRAILING_SLASH.test(path4)) {
3731
- return this.test(path4);
3729
+ checkIgnore(path5) {
3730
+ if (!REGEX_TEST_TRAILING_SLASH.test(path5)) {
3731
+ return this.test(path5);
3732
3732
  }
3733
- const slices = path4.split(SLASH).filter(Boolean);
3733
+ const slices = path5.split(SLASH).filter(Boolean);
3734
3734
  slices.pop();
3735
3735
  if (slices.length) {
3736
3736
  const parent = this._t(
@@ -3743,18 +3743,18 @@ var require_ignore = __commonJS({
3743
3743
  return parent;
3744
3744
  }
3745
3745
  }
3746
- return this._rules.test(path4, false, MODE_CHECK_IGNORE);
3746
+ return this._rules.test(path5, false, MODE_CHECK_IGNORE);
3747
3747
  }
3748
- _t(path4, cache, checkUnignored, slices) {
3749
- if (path4 in cache) {
3750
- return cache[path4];
3748
+ _t(path5, cache, checkUnignored, slices) {
3749
+ if (path5 in cache) {
3750
+ return cache[path5];
3751
3751
  }
3752
3752
  if (!slices) {
3753
- slices = path4.split(SLASH).filter(Boolean);
3753
+ slices = path5.split(SLASH).filter(Boolean);
3754
3754
  }
3755
3755
  slices.pop();
3756
3756
  if (!slices.length) {
3757
- return cache[path4] = this._rules.test(path4, checkUnignored, MODE_IGNORE);
3757
+ return cache[path5] = this._rules.test(path5, checkUnignored, MODE_IGNORE);
3758
3758
  }
3759
3759
  const parent = this._t(
3760
3760
  slices.join(SLASH) + SLASH,
@@ -3762,29 +3762,29 @@ var require_ignore = __commonJS({
3762
3762
  checkUnignored,
3763
3763
  slices
3764
3764
  );
3765
- return cache[path4] = parent.ignored ? parent : this._rules.test(path4, checkUnignored, MODE_IGNORE);
3765
+ return cache[path5] = parent.ignored ? parent : this._rules.test(path5, checkUnignored, MODE_IGNORE);
3766
3766
  }
3767
- ignores(path4) {
3768
- return this._test(path4, this._ignoreCache, false).ignored;
3767
+ ignores(path5) {
3768
+ return this._test(path5, this._ignoreCache, false).ignored;
3769
3769
  }
3770
3770
  createFilter() {
3771
- return (path4) => !this.ignores(path4);
3771
+ return (path5) => !this.ignores(path5);
3772
3772
  }
3773
3773
  filter(paths) {
3774
3774
  return makeArray(paths).filter(this.createFilter());
3775
3775
  }
3776
3776
  // @returns {TestResult}
3777
- test(path4) {
3778
- return this._test(path4, this._testCache, true);
3777
+ test(path5) {
3778
+ return this._test(path5, this._testCache, true);
3779
3779
  }
3780
3780
  };
3781
3781
  var factory = (options) => new Ignore(options);
3782
- var isPathValid = (path4) => checkPath(path4 && checkPath.convert(path4), path4, RETURN_FALSE);
3782
+ var isPathValid = (path5) => checkPath(path5 && checkPath.convert(path5), path5, RETURN_FALSE);
3783
3783
  var setupWindows = () => {
3784
3784
  const makePosix = (str) => /^\\\\\?\\/.test(str) || /["<>|\u0000-\u001F]+/u.test(str) ? str : str.replace(/\\/g, "/");
3785
3785
  checkPath.convert = makePosix;
3786
3786
  const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
3787
- checkPath.isNotRelative = (path4) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path4) || isNotRelative(path4);
3787
+ checkPath.isNotRelative = (path5) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path5) || isNotRelative(path5);
3788
3788
  };
3789
3789
  if (
3790
3790
  // Detect `process` so that it can run in browsers.
@@ -3944,10 +3944,10 @@ function prompt(question) {
3944
3944
  return Promise.reject(new Error(`prompt() called without a TTY: ${question.trim()}`));
3945
3945
  }
3946
3946
  const rl2 = createInterface({ input: process.stdin, output: process.stdout });
3947
- return new Promise((resolve18) => {
3947
+ return new Promise((resolve19) => {
3948
3948
  rl2.question(question, (answer) => {
3949
3949
  rl2.close();
3950
- resolve18(answer.trim());
3950
+ resolve19(answer.trim());
3951
3951
  });
3952
3952
  });
3953
3953
  }
@@ -3977,7 +3977,7 @@ async function confirm(question, opts = {}) {
3977
3977
  const wasRaw = stdin.isRaw ?? false;
3978
3978
  stdin.setRawMode(true);
3979
3979
  stdin.resume();
3980
- return new Promise((resolve18) => {
3980
+ return new Promise((resolve19) => {
3981
3981
  stdin.once("data", (key) => {
3982
3982
  stdin.setRawMode(wasRaw);
3983
3983
  stdin.pause();
@@ -3992,12 +3992,12 @@ async function confirm(question, opts = {}) {
3992
3992
  else if (k7 === "n") answer = false;
3993
3993
  else answer = defaultYes;
3994
3994
  console.log(answer ? "y" : "n");
3995
- resolve18(answer);
3995
+ resolve19(answer);
3996
3996
  });
3997
3997
  });
3998
3998
  }
3999
3999
  function pickOne(label2, max, defaultIdx = 1) {
4000
- return new Promise((resolve18) => {
4000
+ return new Promise((resolve19) => {
4001
4001
  process.stdout.write(` ${bold(label2)} (1-${max}) [${bold(String(defaultIdx))}]: `);
4002
4002
  const { stdin } = process;
4003
4003
  const wasRaw = stdin.isRaw ?? false;
@@ -4013,15 +4013,15 @@ function pickOne(label2, max, defaultIdx = 1) {
4013
4013
  }
4014
4014
  if (ch === "\r" || ch === "\n") {
4015
4015
  console.log(String(defaultIdx));
4016
- return resolve18(defaultIdx);
4016
+ return resolve19(defaultIdx);
4017
4017
  }
4018
4018
  const n = parseInt(ch, 10);
4019
4019
  if (n >= 1 && n <= max) {
4020
4020
  console.log(String(n));
4021
- return resolve18(n);
4021
+ return resolve19(n);
4022
4022
  }
4023
4023
  console.log(String(defaultIdx));
4024
- resolve18(defaultIdx);
4024
+ resolve19(defaultIdx);
4025
4025
  });
4026
4026
  });
4027
4027
  }
@@ -5370,8 +5370,8 @@ var require_streamx = __commonJS({
5370
5370
  return this;
5371
5371
  },
5372
5372
  next() {
5373
- return new Promise(function(resolve18, reject) {
5374
- promiseResolve = resolve18;
5373
+ return new Promise(function(resolve19, reject) {
5374
+ promiseResolve = resolve19;
5375
5375
  promiseReject = reject;
5376
5376
  const data = stream.read();
5377
5377
  if (data !== null) ondata(data);
@@ -5401,11 +5401,11 @@ var require_streamx = __commonJS({
5401
5401
  }
5402
5402
  function destroy(err) {
5403
5403
  stream.destroy(err);
5404
- return new Promise((resolve18, reject) => {
5405
- if (stream._duplexState & DESTROYED) return resolve18({ value: void 0, done: true });
5404
+ return new Promise((resolve19, reject) => {
5405
+ if (stream._duplexState & DESTROYED) return resolve19({ value: void 0, done: true });
5406
5406
  stream.once("close", function() {
5407
5407
  if (err) reject(err);
5408
- else resolve18({ value: void 0, done: true });
5408
+ else resolve19({ value: void 0, done: true });
5409
5409
  });
5410
5410
  });
5411
5411
  }
@@ -5449,8 +5449,8 @@ var require_streamx = __commonJS({
5449
5449
  const writes = pending + (ws2._duplexState & WRITE_WRITING ? 1 : 0);
5450
5450
  if (writes === 0) return Promise.resolve(true);
5451
5451
  if (state.drains === null) state.drains = [];
5452
- return new Promise((resolve18) => {
5453
- state.drains.push({ writes, resolve: resolve18 });
5452
+ return new Promise((resolve19) => {
5453
+ state.drains.push({ writes, resolve: resolve19 });
5454
5454
  });
5455
5455
  }
5456
5456
  write(data) {
@@ -5555,10 +5555,10 @@ var require_streamx = __commonJS({
5555
5555
  cb2(null);
5556
5556
  }
5557
5557
  function pipelinePromise(...streams) {
5558
- return new Promise((resolve18, reject) => {
5558
+ return new Promise((resolve19, reject) => {
5559
5559
  return pipeline(...streams, (err) => {
5560
5560
  if (err) return reject(err);
5561
- resolve18();
5561
+ resolve19();
5562
5562
  });
5563
5563
  });
5564
5564
  }
@@ -6213,16 +6213,16 @@ var require_extract = __commonJS({
6213
6213
  entryCallback = null;
6214
6214
  cb2(err);
6215
6215
  }
6216
- function onnext(resolve18, reject) {
6216
+ function onnext(resolve19, reject) {
6217
6217
  if (error2) {
6218
6218
  return reject(error2);
6219
6219
  }
6220
6220
  if (entryStream) {
6221
- resolve18({ value: entryStream, done: false });
6221
+ resolve19({ value: entryStream, done: false });
6222
6222
  entryStream = null;
6223
6223
  return;
6224
6224
  }
6225
- promiseResolve = resolve18;
6225
+ promiseResolve = resolve19;
6226
6226
  promiseReject = reject;
6227
6227
  consumeCallback(null);
6228
6228
  if (extract3._finished && promiseResolve) {
@@ -6250,11 +6250,11 @@ var require_extract = __commonJS({
6250
6250
  function destroy(err) {
6251
6251
  extract3.destroy(err);
6252
6252
  consumeCallback(err);
6253
- return new Promise((resolve18, reject) => {
6254
- if (extract3.destroyed) return resolve18({ value: void 0, done: true });
6253
+ return new Promise((resolve19, reject) => {
6254
+ if (extract3.destroyed) return resolve19({ value: void 0, done: true });
6255
6255
  extract3.once("close", function() {
6256
6256
  if (err) reject(err);
6257
- else resolve18({ value: void 0, done: true });
6257
+ else resolve19({ value: void 0, done: true });
6258
6258
  });
6259
6259
  });
6260
6260
  }
@@ -6662,16 +6662,16 @@ async function getAuthHeader() {
6662
6662
  const headers = await getHeaders();
6663
6663
  return headers.Authorization;
6664
6664
  }
6665
- async function request(method, path4, body, retrying = false) {
6665
+ async function request(method, path5, body, retrying = false) {
6666
6666
  const headers = await getHeaders();
6667
- const url = `${baseUrl()}${path4}`;
6667
+ const url = `${baseUrl()}${path5}`;
6668
6668
  const res = await fetchWithTimeout(url, {
6669
6669
  method,
6670
6670
  headers,
6671
6671
  body: body ? JSON.stringify(body) : void 0
6672
- }, REQUEST_TIMEOUT_MS, `${method} ${path4}`);
6672
+ }, REQUEST_TIMEOUT_MS, `${method} ${path5}`);
6673
6673
  if (await shouldRetryAfter401(res.status, retrying)) {
6674
- return request(method, path4, body, true);
6674
+ return request(method, path5, body, true);
6675
6675
  }
6676
6676
  if (!res.ok) {
6677
6677
  const json = await res.json().catch(() => ({ error: { code: "UNKNOWN", message: res.statusText } }));
@@ -6680,22 +6680,22 @@ async function request(method, path4, body, retrying = false) {
6680
6680
  }
6681
6681
  return res.json();
6682
6682
  }
6683
- function get(path4) {
6684
- return request("GET", path4);
6683
+ function get(path5) {
6684
+ return request("GET", path5);
6685
6685
  }
6686
- function post(path4, body) {
6687
- return request("POST", path4, body);
6686
+ function post(path5, body) {
6687
+ return request("POST", path5, body);
6688
6688
  }
6689
- async function postForTarEntries(path4, body, retried = false) {
6689
+ async function postForTarEntries(path5, body, retried = false) {
6690
6690
  const headers = await getHeaders();
6691
- const url = `${baseUrl()}${path4}`;
6691
+ const url = `${baseUrl()}${path5}`;
6692
6692
  const res = await fetch(url, {
6693
6693
  method: "POST",
6694
6694
  headers,
6695
6695
  body: body ? JSON.stringify(body) : void 0
6696
6696
  });
6697
6697
  if (await shouldRetryAfter401(res.status, retried)) {
6698
- return postForTarEntries(path4, body, true);
6698
+ return postForTarEntries(path5, body, true);
6699
6699
  }
6700
6700
  if (!res.ok) {
6701
6701
  const json = await res.json().catch(() => ({ error: { code: "UNKNOWN", message: res.statusText } }));
@@ -6705,7 +6705,7 @@ async function postForTarEntries(path4, body, retried = false) {
6705
6705
  if (!res.body) throw new ApiError(500, "EMPTY_RESPONSE", "Server returned no body");
6706
6706
  const extract3 = tar.extract();
6707
6707
  const entries = [];
6708
- const done = new Promise((resolve18, reject) => {
6708
+ const done = new Promise((resolve19, reject) => {
6709
6709
  extract3.on("entry", (header, stream, next) => {
6710
6710
  const chunks = [];
6711
6711
  stream.on("data", (c) => chunks.push(c));
@@ -6716,21 +6716,21 @@ async function postForTarEntries(path4, body, retried = false) {
6716
6716
  stream.on("error", reject);
6717
6717
  stream.resume();
6718
6718
  });
6719
- extract3.on("finish", () => resolve18());
6719
+ extract3.on("finish", () => resolve19());
6720
6720
  extract3.on("error", reject);
6721
6721
  });
6722
6722
  Readable.fromWeb(res.body).pipe(extract3);
6723
6723
  await done;
6724
6724
  return entries;
6725
6725
  }
6726
- function put(path4, body) {
6727
- return request("PUT", path4, body);
6726
+ function put(path5, body) {
6727
+ return request("PUT", path5, body);
6728
6728
  }
6729
- function patch(path4, body) {
6730
- return request("PATCH", path4, body);
6729
+ function patch(path5, body) {
6730
+ return request("PATCH", path5, body);
6731
6731
  }
6732
- function del(path4, body) {
6733
- return request("DELETE", path4, body);
6732
+ function del(path5, body) {
6733
+ return request("DELETE", path5, body);
6734
6734
  }
6735
6735
  async function sendMessage(message) {
6736
6736
  const config = requireConfig();
@@ -6743,26 +6743,26 @@ async function sendMessage(message) {
6743
6743
  }
6744
6744
  return res.data.content;
6745
6745
  }
6746
- async function download(path4, retried = false) {
6747
- const url = `${baseUrl()}${path4}`;
6746
+ async function download(path5, retried = false) {
6747
+ const url = `${baseUrl()}${path5}`;
6748
6748
  const res = await fetch(url, {
6749
6749
  headers: { ...clientHeaders(), "Authorization": `Bearer ${await bearerToken()}` }
6750
6750
  });
6751
6751
  if (await shouldRetryAfter401(res.status, retried)) {
6752
- return download(path4, true);
6752
+ return download(path5, true);
6753
6753
  }
6754
6754
  if (!res.ok) {
6755
6755
  throw new ApiError(res.status, "DOWNLOAD_ERROR", with401Hint(res.status, `Download failed: ${res.statusText}`));
6756
6756
  }
6757
6757
  return Buffer.from(await res.arrayBuffer());
6758
6758
  }
6759
- async function downloadWithHeaders(path4, retried = false) {
6760
- const url = `${baseUrl()}${path4}`;
6759
+ async function downloadWithHeaders(path5, retried = false) {
6760
+ const url = `${baseUrl()}${path5}`;
6761
6761
  const res = await fetch(url, {
6762
6762
  headers: { ...clientHeaders(), "Authorization": `Bearer ${await bearerToken()}` }
6763
6763
  });
6764
6764
  if (await shouldRetryAfter401(res.status, retried)) {
6765
- return downloadWithHeaders(path4, true);
6765
+ return downloadWithHeaders(path5, true);
6766
6766
  }
6767
6767
  if (!res.ok) {
6768
6768
  const json = await res.json().catch(() => ({ error: { code: "UNKNOWN", message: res.statusText } }));
@@ -6771,8 +6771,8 @@ async function downloadWithHeaders(path4, retried = false) {
6771
6771
  }
6772
6772
  return { buffer: Buffer.from(await res.arrayBuffer()), headers: res.headers };
6773
6773
  }
6774
- async function postBinary(path4, body, contentType = "application/zip", retried = false) {
6775
- const url = `${baseUrl()}${path4}`;
6774
+ async function postBinary(path5, body, contentType = "application/zip", retried = false) {
6775
+ const url = `${baseUrl()}${path5}`;
6776
6776
  const headers = {
6777
6777
  ...clientHeaders(),
6778
6778
  "Authorization": `Bearer ${await bearerToken()}`,
@@ -6782,9 +6782,9 @@ async function postBinary(path4, body, contentType = "application/zip", retried
6782
6782
  method: "POST",
6783
6783
  headers,
6784
6784
  body: new Uint8Array(body)
6785
- }, putTimeoutMs(body.length), `POST ${path4}`);
6785
+ }, putTimeoutMs(body.length), `POST ${path5}`);
6786
6786
  if (await shouldRetryAfter401(res.status, retried)) {
6787
- return postBinary(path4, body, contentType, true);
6787
+ return postBinary(path5, body, contentType, true);
6788
6788
  }
6789
6789
  if (!res.ok) {
6790
6790
  const json = await res.json().catch(() => ({ error: { code: "UNKNOWN", message: res.statusText } }));
@@ -6793,9 +6793,9 @@ async function postBinary(path4, body, contentType = "application/zip", retried
6793
6793
  }
6794
6794
  return { status: res.status, json: await res.json() };
6795
6795
  }
6796
- async function downloadStream(path4, retried = false) {
6796
+ async function downloadStream(path5, retried = false) {
6797
6797
  const { Readable: Readable2 } = await import("stream");
6798
- const url = `${baseUrl()}${path4}`;
6798
+ const url = `${baseUrl()}${path5}`;
6799
6799
  const ac2 = new AbortController();
6800
6800
  const headerTimer = setTimeout(() => ac2.abort(), DOWNLOAD_HEADER_TIMEOUT_MS);
6801
6801
  let res;
@@ -6813,7 +6813,7 @@ async function downloadStream(path4, retried = false) {
6813
6813
  clearTimeout(headerTimer);
6814
6814
  }
6815
6815
  if (await shouldRetryAfter401(res.status, retried)) {
6816
- return downloadStream(path4, true);
6816
+ return downloadStream(path5, true);
6817
6817
  }
6818
6818
  if (!res.ok) {
6819
6819
  throw new ApiError(res.status, "DOWNLOAD_ERROR", with401Hint(res.status, `Download failed: ${res.statusText}`));
@@ -6862,8 +6862,8 @@ async function getAccountSlug() {
6862
6862
  accountSlugCache = res.data.accountSlug;
6863
6863
  return accountSlugCache;
6864
6864
  }
6865
- async function publicPost(path4, body, extraHeaders) {
6866
- const url = `${baseUrl()}${path4}`;
6865
+ async function publicPost(path5, body, extraHeaders) {
6866
+ const url = `${baseUrl()}${path5}`;
6867
6867
  const res = await fetch(url, {
6868
6868
  method: "POST",
6869
6869
  headers: { ...clientHeaders(), "Content-Type": "application/json", ...extraHeaders },
@@ -6973,15 +6973,15 @@ function getConfigPath() {
6973
6973
  return cachedPath;
6974
6974
  }
6975
6975
  function getProjectRoot() {
6976
- const path4 = getConfigPath();
6977
- return path4 ? dirname2(path4) : null;
6976
+ const path5 = getConfigPath();
6977
+ return path5 ? dirname2(path5) : null;
6978
6978
  }
6979
6979
  function getConfig() {
6980
6980
  if (cached3) return cached3;
6981
- const path4 = getConfigPath();
6982
- if (!path4) return null;
6981
+ const path5 = getConfigPath();
6982
+ if (!path5) return null;
6983
6983
  try {
6984
- cached3 = JSON.parse(readFileSync3(path4, "utf-8"));
6984
+ cached3 = JSON.parse(readFileSync3(path5, "utf-8"));
6985
6985
  return cached3;
6986
6986
  } catch {
6987
6987
  return null;
@@ -7063,21 +7063,21 @@ function clearConfigCache() {
7063
7063
  cachedPath = null;
7064
7064
  }
7065
7065
  function saveConfig(data) {
7066
- const path4 = getConfigPath();
7067
- if (!path4) {
7066
+ const path5 = getConfigPath();
7067
+ if (!path5) {
7068
7068
  throw new Error(
7069
7069
  "saveConfig: no .gipity.json found to update. Use saveConfigAt(dir, \u2026) to create a new project config."
7070
7070
  );
7071
7071
  }
7072
- writeFileSync2(path4, JSON.stringify(data, null, 2) + "\n");
7072
+ writeFileSync2(path5, JSON.stringify(data, null, 2) + "\n");
7073
7073
  cached3 = data;
7074
- cachedPath = path4;
7074
+ cachedPath = path5;
7075
7075
  }
7076
7076
  function saveConfigAt(dir, data) {
7077
- const path4 = resolve2(dir, CONFIG_FILE);
7078
- writeFileSync2(path4, JSON.stringify(data, null, 2) + "\n");
7077
+ const path5 = resolve2(dir, CONFIG_FILE);
7078
+ writeFileSync2(path5, JSON.stringify(data, null, 2) + "\n");
7079
7079
  cached3 = data;
7080
- cachedPath = path4;
7080
+ cachedPath = path5;
7081
7081
  }
7082
7082
  function shouldIgnore(filePath, ignorePatterns) {
7083
7083
  if (ignorePatterns.length === 0) return false;
@@ -31860,7 +31860,7 @@ init_config();
31860
31860
  init_utils();
31861
31861
  import { readFileSync as readFileSync25 } from "fs";
31862
31862
  import { fileURLToPath as fileURLToPath3 } from "url";
31863
- import { dirname as dirname13, resolve as resolve17 } from "path";
31863
+ import { dirname as dirname13, resolve as resolve18 } from "path";
31864
31864
 
31865
31865
  // src/helpers/output.ts
31866
31866
  var frameOpen = false;
@@ -31900,16 +31900,16 @@ function printResult(text, opts, jsonData) {
31900
31900
  }
31901
31901
  console.log(text);
31902
31902
  }
31903
- function pluckField(data, path4) {
31904
- return path4.split(".").reduce(
31903
+ function pluckField(data, path5) {
31904
+ return path5.split(".").reduce(
31905
31905
  (acc, key) => acc == null ? void 0 : acc[key],
31906
31906
  data
31907
31907
  );
31908
31908
  }
31909
- function emitField(data, path4) {
31910
- const value = pluckField(data, path4);
31909
+ function emitField(data, path5) {
31910
+ const value = pluckField(data, path5);
31911
31911
  if (value === void 0) {
31912
- console.error(`Field not found: ${path4}`);
31912
+ console.error(`Field not found: ${path5}`);
31913
31913
  process.exit(1);
31914
31914
  }
31915
31915
  console.log(typeof value === "object" && value !== null ? JSON.stringify(value) : String(value));
@@ -32171,13 +32171,13 @@ async function flushBugQueue() {
32171
32171
  let delivered = 0;
32172
32172
  for (const file of readdirSync(QUEUE_DIR)) {
32173
32173
  if (!file.endsWith(".json")) continue;
32174
- const path4 = join2(QUEUE_DIR, file);
32174
+ const path5 = join2(QUEUE_DIR, file);
32175
32175
  let report;
32176
32176
  try {
32177
- report = JSON.parse(readFileSync4(path4, "utf-8"));
32177
+ report = JSON.parse(readFileSync4(path5, "utf-8"));
32178
32178
  } catch {
32179
32179
  try {
32180
- unlinkSync2(path4);
32180
+ unlinkSync2(path5);
32181
32181
  } catch {
32182
32182
  }
32183
32183
  continue;
@@ -32189,7 +32189,7 @@ async function flushBugQueue() {
32189
32189
  summary: report.summary,
32190
32190
  detail: report.detail
32191
32191
  });
32192
- unlinkSync2(path4);
32192
+ unlinkSync2(path5);
32193
32193
  delivered++;
32194
32194
  } catch {
32195
32195
  }
@@ -32347,32 +32347,32 @@ var MIME_BY_EXT = {
32347
32347
  ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
32348
32348
  ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
32349
32349
  };
32350
- function guessMime(path4) {
32351
- return MIME_BY_EXT[extname(path4).toLowerCase()] ?? "application/octet-stream";
32350
+ function guessMime(path5) {
32351
+ return MIME_BY_EXT[extname(path5).toLowerCase()] ?? "application/octet-stream";
32352
32352
  }
32353
- async function hashFile(path4) {
32353
+ async function hashFile(path5) {
32354
32354
  const hash = createHash("sha256");
32355
32355
  let size = 0;
32356
- const stream = createReadStream(path4);
32357
- await new Promise((resolve18, reject) => {
32356
+ const stream = createReadStream(path5);
32357
+ await new Promise((resolve19, reject) => {
32358
32358
  stream.on("data", (chunk) => {
32359
32359
  const buf = chunk;
32360
32360
  hash.update(buf);
32361
32361
  size += buf.length;
32362
32362
  });
32363
- stream.on("end", () => resolve18());
32363
+ stream.on("end", () => resolve19());
32364
32364
  stream.on("error", reject);
32365
32365
  });
32366
32366
  return { sha256: hash.digest("hex"), size };
32367
32367
  }
32368
- function readRange(path4, start, end) {
32369
- return new Promise((resolve18, reject) => {
32368
+ function readRange(path5, start, end) {
32369
+ return new Promise((resolve19, reject) => {
32370
32370
  const chunks = [];
32371
- const stream = createReadStream(path4, { start, end });
32371
+ const stream = createReadStream(path5, { start, end });
32372
32372
  stream.on("data", (c) => {
32373
32373
  chunks.push(c);
32374
32374
  });
32375
- stream.on("end", () => resolve18(Buffer.concat(chunks)));
32375
+ stream.on("end", () => resolve19(Buffer.concat(chunks)));
32376
32376
  stream.on("error", reject);
32377
32377
  });
32378
32378
  }
@@ -32395,10 +32395,10 @@ async function withRetry(label2, fn, attempts = 3) {
32395
32395
  throw lastErr;
32396
32396
  }
32397
32397
  var UploadConflictError = class extends Error {
32398
- constructor(currentServerVersion, path4) {
32399
- super(`Version mismatch for ${path4}: current serverVersion is ${currentServerVersion}`);
32398
+ constructor(currentServerVersion, path5) {
32399
+ super(`Version mismatch for ${path5}: current serverVersion is ${currentServerVersion}`);
32400
32400
  this.currentServerVersion = currentServerVersion;
32401
- this.path = path4;
32401
+ this.path = path5;
32402
32402
  this.name = "UploadConflictError";
32403
32403
  }
32404
32404
  currentServerVersion;
@@ -32645,10 +32645,10 @@ function stripGipityHooks(settings) {
32645
32645
  if (Object.keys(hooks).length === 0) delete settings.hooks;
32646
32646
  return changed;
32647
32647
  }
32648
- function readSettingsFile(path4) {
32649
- if (!existsSync4(path4)) return {};
32648
+ function readSettingsFile(path5) {
32649
+ if (!existsSync4(path5)) return {};
32650
32650
  try {
32651
- return JSON.parse(readFileSync5(path4, "utf-8"));
32651
+ return JSON.parse(readFileSync5(path5, "utf-8"));
32652
32652
  } catch {
32653
32653
  return {};
32654
32654
  }
@@ -32767,12 +32767,12 @@ function applySkillsBlock(existing) {
32767
32767
  return next.endsWith("\n") ? next : next + "\n";
32768
32768
  }
32769
32769
  function writeSkillsFile(relPath, wrap) {
32770
- const path4 = resolve3(process.cwd(), relPath);
32771
- mkdirSync3(dirname3(path4), { recursive: true });
32772
- const existing = existsSync4(path4) ? readFileSync5(path4, "utf-8") : null;
32770
+ const path5 = resolve3(process.cwd(), relPath);
32771
+ mkdirSync3(dirname3(path5), { recursive: true });
32772
+ const existing = existsSync4(path5) ? readFileSync5(path5, "utf-8") : null;
32773
32773
  const baseNext = applySkillsBlock(existing);
32774
32774
  const next = wrap && existing === null ? wrap(baseNext) : baseNext;
32775
- if (next !== existing) writeFileSync4(path4, next);
32775
+ if (next !== existing) writeFileSync4(path5, next);
32776
32776
  }
32777
32777
  function setupClaudeMd() {
32778
32778
  writeSkillsFile(PRIMER_FILES.claude);
@@ -32811,10 +32811,10 @@ function applyAiderConf(existing) {
32811
32811
  }
32812
32812
  function setupAiderMd() {
32813
32813
  writeSkillsFile(PRIMER_FILES.aider);
32814
- const path4 = resolve3(process.cwd(), AIDER_CONF_FILE);
32815
- const existing = existsSync4(path4) ? readFileSync5(path4, "utf-8") : null;
32814
+ const path5 = resolve3(process.cwd(), AIDER_CONF_FILE);
32815
+ const existing = existsSync4(path5) ? readFileSync5(path5, "utf-8") : null;
32816
32816
  const next = applyAiderConf(existing);
32817
- if (next !== null) writeFileSync4(path4, next);
32817
+ if (next !== null) writeFileSync4(path5, next);
32818
32818
  }
32819
32819
  function setupGeminiMd() {
32820
32820
  writeSkillsFile(PRIMER_FILES.gemini);
@@ -32888,12 +32888,12 @@ var LOCK_WAIT_MS2 = 3e4;
32888
32888
  var LOCK_POLL_MS2 = 500;
32889
32889
  var LOCK_HEARTBEAT_MS = 15e3;
32890
32890
  var LOCK_STALE_MS = 9e4;
32891
- function isLockReclaimable(path4, now = Date.now()) {
32891
+ function isLockReclaimable(path5, now = Date.now()) {
32892
32892
  let raw;
32893
32893
  let mtimeMs;
32894
32894
  try {
32895
- raw = readFileSync6(path4, "utf-8").trim();
32896
- mtimeMs = statSync(path4).mtimeMs;
32895
+ raw = readFileSync6(path5, "utf-8").trim();
32896
+ mtimeMs = statSync(path5).mtimeMs;
32897
32897
  } catch {
32898
32898
  return false;
32899
32899
  }
@@ -32907,8 +32907,8 @@ function isLockReclaimable(path4, now = Date.now()) {
32907
32907
  return now - mtimeMs > LOCK_STALE_MS;
32908
32908
  }
32909
32909
  async function acquireLock(progress) {
32910
- const path4 = lockPath();
32911
- mkdirSync4(dirname4(path4), { recursive: true });
32910
+ const path5 = lockPath();
32911
+ mkdirSync4(dirname4(path5), { recursive: true });
32912
32912
  const start = Date.now();
32913
32913
  let waitSpinner = null;
32914
32914
  const settleWait = (ok) => {
@@ -32919,12 +32919,12 @@ async function acquireLock(progress) {
32919
32919
  };
32920
32920
  while (true) {
32921
32921
  try {
32922
- const fd2 = openSync2(path4, "wx");
32922
+ const fd2 = openSync2(path5, "wx");
32923
32923
  writeFileSync5(fd2, String(process.pid));
32924
32924
  closeSync2(fd2);
32925
32925
  const beat = setInterval(() => {
32926
32926
  try {
32927
- utimesSync(path4, /* @__PURE__ */ new Date(), /* @__PURE__ */ new Date());
32927
+ utimesSync(path5, /* @__PURE__ */ new Date(), /* @__PURE__ */ new Date());
32928
32928
  } catch {
32929
32929
  }
32930
32930
  }, LOCK_HEARTBEAT_MS);
@@ -32933,14 +32933,14 @@ async function acquireLock(progress) {
32933
32933
  return () => {
32934
32934
  clearInterval(beat);
32935
32935
  try {
32936
- unlinkSync3(path4);
32936
+ unlinkSync3(path5);
32937
32937
  } catch {
32938
32938
  }
32939
32939
  };
32940
32940
  } catch {
32941
- if (isLockReclaimable(path4)) {
32941
+ if (isLockReclaimable(path5)) {
32942
32942
  try {
32943
- unlinkSync3(path4);
32943
+ unlinkSync3(path5);
32944
32944
  } catch {
32945
32945
  }
32946
32946
  continue;
@@ -32948,7 +32948,7 @@ async function acquireLock(progress) {
32948
32948
  if (Date.now() - start > LOCK_WAIT_MS2) {
32949
32949
  settleWait(false);
32950
32950
  throw new Error(
32951
- `Another sync is in progress (${path4}). Waited ${LOCK_WAIT_MS2 / 1e3}s. Remove the file manually if you're sure no sync is running.`
32951
+ `Another sync is in progress (${path5}). Waited ${LOCK_WAIT_MS2 / 1e3}s. Remove the file manually if you're sure no sync is running.`
32952
32952
  );
32953
32953
  }
32954
32954
  if (!waitSpinner) {
@@ -32959,10 +32959,10 @@ async function acquireLock(progress) {
32959
32959
  }
32960
32960
  }
32961
32961
  function readBaseline(projectGuid) {
32962
- const path4 = syncStatePath();
32963
- if (!existsSync5(path4)) return { projectGuid, files: {}, lastFullSync: null };
32962
+ const path5 = syncStatePath();
32963
+ if (!existsSync5(path5)) return { projectGuid, files: {}, lastFullSync: null };
32964
32964
  try {
32965
- const parsed = JSON.parse(readFileSync6(path4, "utf-8"));
32965
+ const parsed = JSON.parse(readFileSync6(path5, "utf-8"));
32966
32966
  if (parsed.projectGuid !== projectGuid) {
32967
32967
  return { projectGuid, files: {}, lastFullSync: null };
32968
32968
  }
@@ -32977,9 +32977,9 @@ function readBaseline(projectGuid) {
32977
32977
  }
32978
32978
  }
32979
32979
  function writeBaseline(b7) {
32980
- const path4 = syncStatePath();
32981
- mkdirSync4(dirname4(path4), { recursive: true });
32982
- writeFileSync5(path4, JSON.stringify(b7, null, 2));
32980
+ const path5 = syncStatePath();
32981
+ mkdirSync4(dirname4(path5), { recursive: true });
32982
+ writeFileSync5(path5, JSON.stringify(b7, null, 2));
32983
32983
  }
32984
32984
  function walkLocal(root, ignorePatterns, baseline) {
32985
32985
  const result = /* @__PURE__ */ new Map();
@@ -33014,11 +33014,11 @@ function walkLocal(root, ignorePatterns, baseline) {
33014
33014
  return result;
33015
33015
  }
33016
33016
  async function ensureLocalHashes(root, local, paths) {
33017
- for (const path4 of paths) {
33018
- const info2 = local.get(path4);
33017
+ for (const path5 of paths) {
33018
+ const info2 = local.get(path5);
33019
33019
  if (!info2 || info2.sha256) continue;
33020
33020
  try {
33021
- const { sha256: sha2562 } = await hashFile(join4(root, path4));
33021
+ const { sha256: sha2562 } = await hashFile(join4(root, path5));
33022
33022
  info2.sha256 = sha2562;
33023
33023
  } catch {
33024
33024
  }
@@ -33051,9 +33051,9 @@ async function fetchRemote(projectGuid) {
33051
33051
  const out = /* @__PURE__ */ new Map();
33052
33052
  for (const f of res.data) {
33053
33053
  if (f.type !== "file") continue;
33054
- const path4 = normalizeTreePath(f.path);
33055
- out.set(path4, {
33056
- path: path4,
33054
+ const path5 = normalizeTreePath(f.path);
33055
+ out.set(path5, {
33056
+ path: path5,
33057
33057
  size: f.size,
33058
33058
  sha256: f.contentHash,
33059
33059
  serverVersion: f.serverVersion,
@@ -33065,7 +33065,7 @@ async function fetchRemote(projectGuid) {
33065
33065
  function extractTarToMap(stream, idleMs, onBytes, keep) {
33066
33066
  const extract3 = tar2.extract();
33067
33067
  const files = /* @__PURE__ */ new Map();
33068
- return new Promise((resolve18, reject) => {
33068
+ return new Promise((resolve19, reject) => {
33069
33069
  let settled = false;
33070
33070
  let idle;
33071
33071
  const arm = () => {
@@ -33101,7 +33101,7 @@ function extractTarToMap(stream, idleMs, onBytes, keep) {
33101
33101
  });
33102
33102
  entryStream.resume();
33103
33103
  });
33104
- extract3.on("finish", () => done(resolve18, files));
33104
+ extract3.on("finish", () => done(resolve19, files));
33105
33105
  extract3.on("error", (e) => done(reject, e));
33106
33106
  stream.on("error", (e) => done(reject, e));
33107
33107
  arm();
@@ -33112,12 +33112,12 @@ async function downloadAll(projectGuid, onBytes) {
33112
33112
  const stream = await downloadStream(`/projects/${projectGuid}/files/tree?content=tar`);
33113
33113
  return extractTarToMap(stream, DOWNLOAD_IDLE_MS, onBytes);
33114
33114
  }
33115
- async function fetchOneOutcome(projectGuid, path4, expectedSha) {
33115
+ async function fetchOneOutcome(projectGuid, path5, expectedSha) {
33116
33116
  try {
33117
33117
  const stream = await downloadStream(
33118
- `/projects/${projectGuid}/files/tree?content=tar&path=${encodeURIComponent(path4)}`
33118
+ `/projects/${projectGuid}/files/tree?content=tar&path=${encodeURIComponent(path5)}`
33119
33119
  );
33120
- const want = normalizeTreePath(path4);
33120
+ const want = normalizeTreePath(path5);
33121
33121
  const files = await extractTarToMap(stream, DOWNLOAD_IDLE_MS, void 0, (p) => p === want);
33122
33122
  const buf = files.get(want) ?? null;
33123
33123
  if (!buf) return { kind: "transient" };
@@ -33131,8 +33131,8 @@ async function fetchOneOutcome(projectGuid, path4, expectedSha) {
33131
33131
  return { kind: "transient" };
33132
33132
  }
33133
33133
  }
33134
- async function fetchOne(projectGuid, path4, expectedSha) {
33135
- const out = await fetchOneOutcome(projectGuid, path4, expectedSha);
33134
+ async function fetchOne(projectGuid, path5, expectedSha) {
33135
+ const out = await fetchOneOutcome(projectGuid, path5, expectedSha);
33136
33136
  return out.kind === "ok" ? out.buf : null;
33137
33137
  }
33138
33138
  function classifyLocal(info2, base) {
@@ -33153,11 +33153,11 @@ function classifyRemote(info2, base) {
33153
33153
  if (iSha && bSha) return iSha === bSha ? "unchanged" : "modified";
33154
33154
  return info2.size === base.size ? "unchanged" : "modified";
33155
33155
  }
33156
- function conflictedCopyName(path4) {
33156
+ function conflictedCopyName(path5) {
33157
33157
  const ts2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:T]/g, "-").replace(/\..+$/, "");
33158
33158
  const host = hostname().replace(/[^A-Za-z0-9._-]/g, "_");
33159
- const ext = extname2(path4);
33160
- const base = ext ? path4.slice(0, -ext.length) : path4;
33159
+ const ext = extname2(path5);
33160
+ const base = ext ? path5.slice(0, -ext.length) : path5;
33161
33161
  return `${base} (conflict from ${host} ${ts2})${ext}`;
33162
33162
  }
33163
33163
  function plan(local, remote, baseline) {
@@ -33177,47 +33177,47 @@ function plan(local, remote, baseline) {
33177
33177
  }
33178
33178
  };
33179
33179
  const allPaths = /* @__PURE__ */ new Set([...local.keys(), ...remote.keys(), ...Object.keys(baseline)]);
33180
- for (const path4 of allPaths) {
33181
- const L7 = local.get(path4);
33182
- const R = remote.get(path4);
33183
- const B7 = baseline[path4];
33180
+ for (const path5 of allPaths) {
33181
+ const L7 = local.get(path5);
33182
+ const R = remote.get(path5);
33183
+ const B7 = baseline[path5];
33184
33184
  const lSide = classifyLocal(L7, B7);
33185
33185
  const rSide = classifyRemote(R, B7);
33186
33186
  if (lSide === "absent" && rSide === "absent") continue;
33187
33187
  const shasMatch = L7?.sha256 && R?.sha256 && L7.sha256 === R.sha256;
33188
33188
  if (lSide === "absent" && rSide === "added") {
33189
- actions.push({ path: path4, kind: "download", remoteSize: R.size });
33189
+ actions.push({ path: path5, kind: "download", remoteSize: R.size });
33190
33190
  continue;
33191
33191
  }
33192
33192
  if (lSide === "added" && rSide === "absent") {
33193
- actions.push({ path: path4, kind: "upload", localSize: L7.size, expectedServerVersion: null });
33193
+ actions.push({ path: path5, kind: "upload", localSize: L7.size, expectedServerVersion: null });
33194
33194
  continue;
33195
33195
  }
33196
33196
  if (lSide === "unchanged" && rSide === "unchanged") continue;
33197
33197
  if (lSide === "unchanged" && rSide === "modified") {
33198
33198
  if (B7 && R.serverVersion === B7.serverVersion && R.sha256 === B7.sha256) continue;
33199
- actions.push({ path: path4, kind: "download", remoteSize: R.size });
33199
+ actions.push({ path: path5, kind: "download", remoteSize: R.size });
33200
33200
  continue;
33201
33201
  }
33202
33202
  if (lSide === "unchanged" && rSide === "deleted") {
33203
- actions.push({ path: path4, kind: "delete-local", localSize: L7.size });
33203
+ actions.push({ path: path5, kind: "delete-local", localSize: L7.size });
33204
33204
  continue;
33205
33205
  }
33206
33206
  if (lSide === "modified" && rSide === "unchanged") {
33207
- actions.push({ path: path4, kind: "upload", localSize: L7.size, expectedServerVersion: B7.serverVersion });
33207
+ actions.push({ path: path5, kind: "upload", localSize: L7.size, expectedServerVersion: B7.serverVersion });
33208
33208
  continue;
33209
33209
  }
33210
33210
  if (lSide === "modified" && rSide === "modified") {
33211
33211
  if (shasMatch) {
33212
- adoptIfNeeded(path4, L7, R);
33212
+ adoptIfNeeded(path5, L7, R);
33213
33213
  continue;
33214
33214
  }
33215
33215
  actions.push({
33216
- path: path4,
33216
+ path: path5,
33217
33217
  kind: "conflict",
33218
33218
  localSize: L7.size,
33219
33219
  remoteSize: R.size,
33220
- renamedLocalTo: conflictedCopyName(path4),
33220
+ renamedLocalTo: conflictedCopyName(path5),
33221
33221
  expectedServerVersion: B7.serverVersion,
33222
33222
  reason: "both sides modified since last sync"
33223
33223
  });
@@ -33225,7 +33225,7 @@ function plan(local, remote, baseline) {
33225
33225
  }
33226
33226
  if (lSide === "modified" && rSide === "deleted") {
33227
33227
  actions.push({
33228
- path: path4,
33228
+ path: path5,
33229
33229
  kind: "upload",
33230
33230
  localSize: L7.size,
33231
33231
  expectedServerVersion: null,
@@ -33235,32 +33235,32 @@ function plan(local, remote, baseline) {
33235
33235
  }
33236
33236
  if (lSide === "added" && rSide === "added") {
33237
33237
  if (shasMatch) {
33238
- adoptIfNeeded(path4, L7, R);
33238
+ adoptIfNeeded(path5, L7, R);
33239
33239
  continue;
33240
33240
  }
33241
33241
  actions.push({
33242
- path: path4,
33242
+ path: path5,
33243
33243
  kind: "conflict",
33244
33244
  localSize: L7.size,
33245
33245
  remoteSize: R.size,
33246
- renamedLocalTo: conflictedCopyName(path4),
33246
+ renamedLocalTo: conflictedCopyName(path5),
33247
33247
  expectedServerVersion: null,
33248
33248
  reason: "both sides created the same path with different content"
33249
33249
  });
33250
33250
  continue;
33251
33251
  }
33252
33252
  if (lSide === "deleted" && rSide === "absent") {
33253
- baselineDrops.push(path4);
33253
+ baselineDrops.push(path5);
33254
33254
  continue;
33255
33255
  }
33256
33256
  if (lSide === "deleted" && rSide === "unchanged") {
33257
- actions.push({ path: path4, kind: "delete-remote", remoteSize: R.size, expectedServerVersion: R.serverVersion });
33257
+ actions.push({ path: path5, kind: "delete-remote", remoteSize: R.size, expectedServerVersion: R.serverVersion });
33258
33258
  continue;
33259
33259
  }
33260
33260
  if (lSide === "deleted" && rSide === "modified") {
33261
33261
  if (B7 && R.serverVersion === B7.serverVersion && R.sha256 === B7.sha256) continue;
33262
33262
  actions.push({
33263
- path: path4,
33263
+ path: path5,
33264
33264
  kind: "download",
33265
33265
  remoteSize: R.size,
33266
33266
  reason: "local deleted but remote modified - remote preserved"
@@ -33268,7 +33268,7 @@ function plan(local, remote, baseline) {
33268
33268
  continue;
33269
33269
  }
33270
33270
  if (lSide === "deleted" && rSide === "deleted") {
33271
- baselineDrops.push(path4);
33271
+ baselineDrops.push(path5);
33272
33272
  continue;
33273
33273
  }
33274
33274
  }
@@ -33325,9 +33325,9 @@ Plan deletes ${totalDeletes} files (${Math.round(fraction * 100)}% of the tree).
33325
33325
  }
33326
33326
  var GIPITY_IGNORE_FILE = ".gipityignore";
33327
33327
  function readGipityIgnore(root) {
33328
- const path4 = join4(root, GIPITY_IGNORE_FILE);
33329
- if (!existsSync5(path4)) return [];
33330
- return readFileSync6(path4, "utf8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => line.replace(/^\.\//, "").replace(/^\//, ""));
33328
+ const path5 = join4(root, GIPITY_IGNORE_FILE);
33329
+ if (!existsSync5(path5)) return [];
33330
+ return readFileSync6(path5, "utf8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => line.replace(/^\.\//, "").replace(/^\//, ""));
33331
33331
  }
33332
33332
  function effectiveIgnore(root, configIgnore) {
33333
33333
  const base = configIgnore && configIgnore.length ? configIgnore : DEFAULT_SYNC_IGNORE;
@@ -33343,8 +33343,8 @@ function isLocalTreeClean() {
33343
33343
  const local = walkLocal(root, effectiveIgnore(root, config.ignore), baseline.files);
33344
33344
  const baselineFiles = baseline.files;
33345
33345
  if (local.size !== Object.keys(baselineFiles).length) return false;
33346
- for (const [path4, info2] of local) {
33347
- const prior = baselineFiles[path4];
33346
+ for (const [path5, info2] of local) {
33347
+ const prior = baselineFiles[path5];
33348
33348
  if (!prior || prior.size !== info2.size || prior.mtime !== info2.mtime) return false;
33349
33349
  }
33350
33350
  return true;
@@ -33372,14 +33372,14 @@ async function syncInner(projectGuid, root, ignore2, opts, interactive) {
33372
33372
  const local = walkLocal(root, ignore2, baseline.files);
33373
33373
  p?.phase("Checking Gipity for changes\u2026");
33374
33374
  const remote = await fetchRemote(projectGuid);
33375
- for (const path4 of [...remote.keys()]) {
33376
- if (shouldIgnore(path4, ignore2)) remote.delete(path4);
33375
+ for (const path5 of [...remote.keys()]) {
33376
+ if (shouldIgnore(path5, ignore2)) remote.delete(path5);
33377
33377
  }
33378
33378
  const needHash = [];
33379
- for (const [path4, l7] of local) {
33379
+ for (const [path5, l7] of local) {
33380
33380
  if (l7.sha256) continue;
33381
- const r = remote.get(path4);
33382
- if (r?.sha256 || baseline.files[path4]) needHash.push(path4);
33381
+ const r = remote.get(path5);
33382
+ if (r?.sha256 || baseline.files[path5]) needHash.push(path5);
33383
33383
  }
33384
33384
  if (needHash.length) p?.phase(`Hashing ${needHash.length} file${needHash.length === 1 ? "" : "s"}\u2026`);
33385
33385
  await ensureLocalHashes(root, local, needHash);
@@ -33843,8 +33843,8 @@ async function syncInner(projectGuid, root, ignore2, opts, interactive) {
33843
33843
  delete baseline.deletesSkippedStreak;
33844
33844
  }
33845
33845
  cleanupEmptyDirs(root, dirsTouchedByDelete);
33846
- for (const { path: path4, entry } of planned.baselineAdopts) baseline.files[path4] = entry;
33847
- for (const path4 of planned.baselineDrops) delete baseline.files[path4];
33846
+ for (const { path: path5, entry } of planned.baselineAdopts) baseline.files[path5] = entry;
33847
+ for (const path5 of planned.baselineDrops) delete baseline.files[path5];
33848
33848
  baseline.lastFullSync = (/* @__PURE__ */ new Date()).toISOString();
33849
33849
  writeBaseline(baseline);
33850
33850
  p?.finish();
@@ -34837,11 +34837,11 @@ import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
34837
34837
  import { join as join9, resolve as resolve8 } from "path";
34838
34838
  import { homedir as homedir7 } from "os";
34839
34839
  function checkGipityPlugin() {
34840
- const path4 = join9(homedir7(), ".claude", "settings.json");
34840
+ const path5 = join9(homedir7(), ".claude", "settings.json");
34841
34841
  let settings = {};
34842
- if (existsSync9(path4)) {
34842
+ if (existsSync9(path5)) {
34843
34843
  try {
34844
- settings = JSON.parse(readFileSync10(path4, "utf-8"));
34844
+ settings = JSON.parse(readFileSync10(path5, "utf-8"));
34845
34845
  } catch {
34846
34846
  }
34847
34847
  }
@@ -35104,9 +35104,48 @@ init_config();
35104
35104
 
35105
35105
  // src/page-fixtures.ts
35106
35106
  init_api();
35107
- import { readFileSync as readFileSync11, statSync as statSync5 } from "node:fs";
35108
- import { basename as basename3 } from "node:path";
35109
- async function uploadPublicFixture(projectGuid, localPath) {
35107
+ init_config();
35108
+ import { readFileSync as readFileSync11, statSync as statSync5, existsSync as existsSync10, readdirSync as readdirSync6 } from "node:fs";
35109
+ import { basename as basename3, resolve as resolve11, relative as relative3, join as join11 } from "node:path";
35110
+ var FIND_SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", ".gipity", "dist", "build", ".next", "coverage"]);
35111
+ function findByBasename(root, name, limit = 3) {
35112
+ const hits = [];
35113
+ const queue = [root];
35114
+ let visited = 0;
35115
+ while (queue.length && hits.length < limit && visited < 500) {
35116
+ const dir = queue.shift();
35117
+ visited++;
35118
+ let entries;
35119
+ try {
35120
+ entries = readdirSync6(dir, { withFileTypes: true });
35121
+ } catch {
35122
+ continue;
35123
+ }
35124
+ for (const e of entries) {
35125
+ if (e.isDirectory()) {
35126
+ if (!FIND_SKIP.has(e.name)) queue.push(join11(dir, e.name));
35127
+ } else if (e.name === name && hits.length < limit) {
35128
+ hits.push(join11(dir, e.name));
35129
+ }
35130
+ }
35131
+ }
35132
+ return hits;
35133
+ }
35134
+ function assertLocalAsset(flag, localPath) {
35135
+ const abs = resolve11(localPath);
35136
+ if (existsSync10(abs)) return;
35137
+ const root = getProjectRoot() ?? process.cwd();
35138
+ const elsewhere = findByBasename(root, basename3(localPath)).filter((p) => p !== abs);
35139
+ const found = elsewhere.length ? `
35140
+ That file DOES exist here \u2014 pass this path instead:
35141
+ ${elsewhere.map((p) => ` ${flag} ${relative3(process.cwd(), p) || p}`).join("\n")}` : `
35142
+ Nothing named "${basename3(localPath)}" under ${root} either. Generate a frame with \`gipity generate image "<description>" -o ${localPath}\`, or check the path.`;
35143
+ throw new Error(
35144
+ `${flag} ${localPath}: no such file \u2014 looked for ${abs} (relative paths resolve against the current directory, ${process.cwd()}).${found}`
35145
+ );
35146
+ }
35147
+ async function uploadPublicFixture(projectGuid, localPath, flag = "--fixture") {
35148
+ assertLocalAsset(flag, localPath);
35110
35149
  const name = basename3(localPath);
35111
35150
  const size = statSync5(localPath).size;
35112
35151
  const contentType = guessMime(localPath);
@@ -35134,6 +35173,21 @@ async function uploadPublicFixture(projectGuid, localPath) {
35134
35173
  async function deleteFixture(projectGuid, guid) {
35135
35174
  await del(`/api/${projectGuid}/uploads/${guid}`);
35136
35175
  }
35176
+ var CAMERA_EXTS = [".png", ".jpg", ".jpeg", ".webp", ".mp4", ".webm", ".y4m", ".mjpeg"];
35177
+ function assertCameraFile(localPath) {
35178
+ assertLocalAsset("--camera", localPath);
35179
+ const ext = localPath.slice(localPath.lastIndexOf(".")).toLowerCase();
35180
+ if (CAMERA_EXTS.includes(ext)) return;
35181
+ throw new Error(
35182
+ `--camera ${basename3(localPath)}: unsupported file type "${ext || "(none)"}" \u2014 the camera feed must be an image or video (${CAMERA_EXTS.join(", ")}).
35183
+ A still image is played as a looping single-frame feed, which is what a gesture/pose/object model needs.
35184
+ No frame to hand? Generate one: gipity generate image "a hand making a closed fist, palm to camera, plain background"`
35185
+ );
35186
+ }
35187
+ async function uploadCameraFeed(projectGuid, localPath) {
35188
+ assertCameraFile(localPath);
35189
+ return uploadPublicFixture(projectGuid, localPath, "--camera");
35190
+ }
35137
35191
 
35138
35192
  // src/commands/page-eval.ts
35139
35193
  var EVAL_NO_VALUE_HINT = "The eval ran but returned no JSON-serializable value. A statement body with no `return`, an assignment, a void call, or a DOM node/function all serialize to null. End the script with an expression \u2014 or an explicit `return` \u2014 that yields plain data, e.g. `return { label: input.value, count: items.length }` or `return JSON.stringify(payload)`.";
@@ -35156,6 +35210,21 @@ function normalizeEvalResult(raw) {
35156
35210
  }
35157
35211
  return { result: raw, noValue: false };
35158
35212
  }
35213
+ var EVAL_EMPTY_STATE_HINT = "The eval returned an empty value \u2014 the script ran, but the state it read was not there at that instant. A fixed --wait samples ONE moment; transient state (a round result, a toast, a detection) can be gone by then. Poll INSIDE the body and return the moment the condition holds (raise --timeout if the sequence needs longer), or gate the read on the app's own signal with --wait-for '<selector>'.";
35214
+ var CAMERA_FRAME_CHECK = "in the eval body, run the model on the frame YOURSELF (grab the app's <video> element, or the app's own detector on it) and return its RAW output alongside the app's state. Model saw nothing => the frame is at fault: regenerate it (subject filling the frame, plain background, even light), do not re-run the same one. Model saw it but the app's state is empty => the bug is in the app's wiring, not the picture.";
35215
+ var EVAL_CAMERA_EMPTY_HINT = `The eval returned an empty value on a --camera run. --camera loops ONE still image, and the model is deterministic on it, so a longer --wait/--timeout re-runs the identical inference and returns the identical nothing \u2014 do not escalate. Two suspects: the frame, or the app's wiring. Tell them apart in one run: ${CAMERA_FRAME_CHECK} (If the app only starts its pipeline on a click, a headless run never clicks \u2014 start it on load and gate this eval on the app's own ready signal with --wait-for '<selector>'.)`;
35216
+ function isEmptyStateResult(result) {
35217
+ let parsed;
35218
+ try {
35219
+ parsed = JSON.parse(result);
35220
+ } catch {
35221
+ return false;
35222
+ }
35223
+ if (Array.isArray(parsed)) return parsed.length === 0;
35224
+ if (!parsed || typeof parsed !== "object") return false;
35225
+ const values = Object.values(parsed);
35226
+ return values.every((v7) => v7 === null || v7 === void 0 || v7 === "");
35227
+ }
35159
35228
  var EXPR_ECHO_MAX_CHARS = 120;
35160
35229
  function summarizeExpr(expr) {
35161
35230
  const lines = expr.split("\n");
@@ -35178,6 +35247,71 @@ function capWaitMs(rawWait, url) {
35178
35247
  ));
35179
35248
  return MAX_WAIT_MS;
35180
35249
  }
35250
+ var MAX_EVAL_STEPS = 4;
35251
+ var WAIT_FOR_MAX_MS = 3e4;
35252
+ function capWaitForTimeoutMs(raw) {
35253
+ const parsed = parseInt(raw, 10);
35254
+ const ms2 = Number.isFinite(parsed) && parsed >= 0 ? parsed : 5e3;
35255
+ if (ms2 <= WAIT_FOR_MAX_MS) return ms2;
35256
+ console.error(warning(
35257
+ `--wait-timeout ${ms2}ms exceeds the ${WAIT_FOR_MAX_MS}ms cap on the selector gate \u2014 using ${WAIT_FOR_MAX_MS}ms. A state that takes longer than that to appear is not a settling delay: have the page reach it on load, or watch for it INSIDE the script (--timeout ${EVAL_SCRIPT_BUDGET_MAX_MS}).`
35258
+ ));
35259
+ return WAIT_FOR_MAX_MS;
35260
+ }
35261
+ function parsesAsExpression(s) {
35262
+ try {
35263
+ new Function(`return (${s}
35264
+ );`);
35265
+ return true;
35266
+ } catch {
35267
+ return false;
35268
+ }
35269
+ }
35270
+ function withPrelude(script, prelude) {
35271
+ if (!prelude) return script;
35272
+ return parsesAsExpression(script) ? `${prelude}
35273
+ return (${script});` : `${prelude}
35274
+ ${script}`;
35275
+ }
35276
+ var CAMERA_DEFAULT_WAIT_MS = 15e3;
35277
+ var CAMERA_SETUP_BUDGET_MS = 3e4;
35278
+ var EVAL_SCRIPT_BUDGET_MS = 3e4;
35279
+ var EVAL_SCRIPT_BUDGET_CAMERA_MS = 45e3;
35280
+ var EVAL_SCRIPT_BUDGET_MAX_MS = 9e4;
35281
+ function capScriptBudgetMs(rawTimeout, hasMedia) {
35282
+ const fallback = hasMedia ? EVAL_SCRIPT_BUDGET_CAMERA_MS : EVAL_SCRIPT_BUDGET_MS;
35283
+ if (rawTimeout === void 0) return fallback;
35284
+ const parsed = parseInt(rawTimeout, 10);
35285
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
35286
+ if (parsed > EVAL_SCRIPT_BUDGET_MAX_MS) {
35287
+ console.error(warning(
35288
+ `--timeout ${parsed}ms exceeds the ${EVAL_SCRIPT_BUDGET_MAX_MS}ms max in-page budget \u2014 using ${EVAL_SCRIPT_BUDGET_MAX_MS}ms. A body that needs longer than that is doing its waiting in the wrong place: start the slow work on page load and absorb it in the pre-eval window (--wait-for '<ready-selector>'), keeping the body to a quick read.`
35289
+ ));
35290
+ return EVAL_SCRIPT_BUDGET_MAX_MS;
35291
+ }
35292
+ return Math.max(1e3, parsed);
35293
+ }
35294
+ function evalWorkBudgetMs(o) {
35295
+ const script = o.scriptBudgetMs ?? (o.hasCamera ? EVAL_SCRIPT_BUDGET_CAMERA_MS : EVAL_SCRIPT_BUDGET_MS);
35296
+ const legs = Math.max(1, o.legs ?? 1);
35297
+ const settle = o.waitMs + (o.waitForTimeoutMs ?? 0);
35298
+ const firstPass = settle + script * legs;
35299
+ return firstPass + (o.hasReload ? settle + script : 0) + (o.hasCamera ? CAMERA_SETUP_BUDGET_MS : 0);
35300
+ }
35301
+ function budgetOverrunHint(reason, usedBudgetMs) {
35302
+ if (!/in-page budget/i.test(reason)) return null;
35303
+ if (usedBudgetMs >= EVAL_SCRIPT_BUDGET_MAX_MS) {
35304
+ return `This run already used the maximum in-page budget (--timeout ${EVAL_SCRIPT_BUDGET_MAX_MS}). More time is not the answer: have the page start its slow work on load, wait it out in the pre-eval window (--wait-for '<ready-selector>' --wait-timeout ${MAX_WAIT_MS}), and keep the body to a quick read.`;
35305
+ }
35306
+ return `That budget was ${Math.round(usedBudgetMs / 1e3)}s. Raise it with --timeout <ms> (up to ${EVAL_SCRIPT_BUDGET_MAX_MS}), e.g. --timeout ${EVAL_SCRIPT_BUDGET_MAX_MS} \u2014 or gate on a ready signal instead: --wait-for '<selector>' --wait-timeout ${MAX_WAIT_MS}.`;
35307
+ }
35308
+ function slowRenderMessage(fps, o) {
35309
+ if (o.camera) {
35310
+ const frames = Math.max(1, Math.round(fps * (o.waitMs / 1e3)));
35311
+ return `${warning("\u26A0 Slow render:")} page painted at ${fps} fps, so the app's vision pipeline ran on roughly ${bold(`${frames} frame${frames === 1 ? "" : "s"}`)} during the ${Math.round(o.waitMs / 1e3)}s before this eval (it infers once per painted frame). ${bold("That is enough:")} --camera loops your still image, so every one of those frames is the SAME pixels and the model returns the SAME answer on each \u2014 re-running with a bigger --wait/--timeout cannot change the result. ${bold("Do not escalate the wait.")} If a detection landed, it is real. If nothing was detected, the suspect is the ${bold("frame")} (a model needs the whole subject in shot \u2014 a tight crop, an odd angle or a busy background reads as nothing) or the ${bold("app")} (frames never reach the model) \u2014 never the frame rate. Settle it in ONE run: ${CAMERA_FRAME_CHECK}`;
35312
+ }
35313
+ return `${warning("\u26A0 Slow render:")} page painted at ${fps} fps. Waiting on real time (setTimeout) advances animation/physics time far slower than it looks \u2014 assertions after a wall-clock wait can report a false negative. Step the app's own loop deterministically instead (3D templates: ${bold("core.advance(seconds)")}).`;
35314
+ }
35181
35315
  async function pollEvalResult(evalJobId, expectedWorkMs) {
35182
35316
  const deadline = Date.now() + expectedWorkMs + 6e4;
35183
35317
  let missCount = 0;
@@ -35196,10 +35330,13 @@ async function pollEvalResult(evalJobId, expectedWorkMs) {
35196
35330
  if (rec.status === "error") throw new ApiError(rec.httpStatus, rec.code, rec.reason);
35197
35331
  await sleep(1e3);
35198
35332
  }
35199
- throw new ApiError(504, "EVAL_TIMEOUT", "Eval did not finish in time; narrow the expression or lower --wait");
35333
+ throw new ApiError(
35334
+ 504,
35335
+ "EVAL_TIMEOUT",
35336
+ `the browser did not report back within ${Math.round((expectedWorkMs + 6e4) / 1e3)}s \u2014 that budget already covers --wait, --wait-for, the eval body and any --camera setup, so this is the browser itself being slow or stuck (a cold sandbox, a heavy page), not your expression being too big. Lowering --wait does NOT help and on a --camera run actively hurts (the wait is what lets the vision model load \u2014 cut it and the eval reads an empty page instead). Just run the same command again: the sandbox is warm on the second hit.`
35337
+ );
35200
35338
  }
35201
- var EVAL_EXEC_BUDGET_MS = 2e4;
35202
- function evalExecTimeoutMessage(result) {
35339
+ function evalExecTimeoutMessage(result, budgetMs) {
35203
35340
  let parsed;
35204
35341
  try {
35205
35342
  parsed = JSON.parse(result);
@@ -35208,10 +35345,18 @@ function evalExecTimeoutMessage(result) {
35208
35345
  }
35209
35346
  if (!parsed || parsed.success !== false || typeof parsed.error !== "string") return null;
35210
35347
  if (!/CDP command timed out:\s*Runtime\.evaluate/i.test(parsed.error)) return null;
35211
- return `the expression hit the ~${EVAL_EXEC_BUDGET_MS / 1e3}s in-page execution budget \u2014 the eval body (including its own await/setTimeout pauses) ran longer than that. This budget is the time the expression itself is allowed to run; it is separate from --wait, which only sleeps BEFORE the eval and cannot extend it. Split a long interactive check into several shorter 'page eval' calls (e.g. one per state to verify), keeping each body's in-page waits well under ${EVAL_EXEC_BUDGET_MS / 1e3}s.`;
35348
+ const budget = Math.round(budgetMs / 1e3);
35349
+ return `the script hit its ${budget}s in-page budget \u2014 the body (including its own await/setTimeout pauses) ran longer than that. --wait sleeps BEFORE the script and does not extend it.
35350
+ ` + (budgetOverrunHint("in-page budget", budgetMs) ?? "") + `
35351
+ If the slow part is a ONE-TIME page init (a WASM/model download, a big asset, a first-frame pipeline warm-up), do NOT split the body across several 'page eval' calls \u2014 every call is a fresh page load that re-pays that init, so each one hits this same wall. Have the page kick it off on load (not behind a click) and absorb it in the pre-eval window instead.`;
35212
35352
  }
35213
35353
  var JS_DECOY_FLAGS = ["--js", "--javascript", "--script", "--code", "--expr", "--eval", "--exec"];
35214
- var pageEvalCommand = new Command("eval").description("Evaluate JS in a real browser on a page (DOM, computed styles, element rects; inline expr or --file script). ONE client per call - to verify realtime/presence across concurrent clients use `page test --observe` instead").argument("<url>", "URL to load").argument("[expr]", "JavaScript to evaluate in page context (inline expression or statement body with return/await; result is JSON-serialized). Omit when using --file. Time budget: the body has ~20s to finish after page load - keep driver scripts within it.").option("--file <path>", "Read the script body from a file instead of the inline <expr> arg (mutually exclusive). Runs as an async function body, so top-level return/await work. Same ~20s post-load budget as <expr>.").option(
35354
+ var pageEvalCommand = new Command("eval").description("Evaluate JS in a real browser on a page (DOM, computed styles, element rects; inline expr or --file script). ONE client per call - to verify realtime/presence across concurrent clients use `page test --observe` instead").argument("<url>", "URL to load").argument("[expr]", `JavaScript to evaluate in page context (inline expression or statement body with return/await; result is JSON-serialized). Omit when using --file. Time budget: the body has ${EVAL_SCRIPT_BUDGET_MS / 1e3}s to finish after page load (${EVAL_SCRIPT_BUDGET_CAMERA_MS / 1e3}s with --camera) - raise it with --timeout, max ${EVAL_SCRIPT_BUDGET_MAX_MS / 1e3}s.`).option("--file <path>", `Read the script body from a file instead of the inline <expr> arg (mutually exclusive). Runs as an async function body, so top-level return/await work. Same post-load budget as <expr> (--timeout).`).option(
35355
+ "--step <expr>",
35356
+ `Run another expression against the SAME loaded page, after <expr> (repeat, max ${MAX_EVAL_STEPS}). Whatever that page load paid for \u2014 a vision model coming up, a game booting, a socket connecting \u2014 stays up for every step, so an N-part check costs ONE page load instead of N. Each step gets its own in-page budget and its own reported result.`,
35357
+ (val, prev) => [...prev, val],
35358
+ []
35359
+ ).option(
35215
35360
  "--fixture <path>",
35216
35361
  "Host a local file and expose it to the eval as `fixtureUrl` (and under `fixtures` by basename) to fetch in-page. For verifying a render/parse path against a real binary (an MP3, an image) - no size limit, auto-deleted after the run. Repeat for several files (single-value so it never swallows the inline <expr>).",
35217
35362
  (val, prev) => [...prev, val],
@@ -35219,7 +35364,13 @@ var pageEvalCommand = new Command("eval").description("Evaluate JS in a real bro
35219
35364
  ).option(
35220
35365
  "--reload <expr>",
35221
35366
  "After the first eval, reload the page IN PLACE (localStorage/sessionStorage/cookies preserved) and evaluate this second expression against the post-reload DOM. One command verifies persisted state survives a reload: seed/assert state with <expr>, then assert the restored UI here."
35222
- ).option("--reload-file <path>", "Read the post-reload expression from a file instead of inline --reload (mutually exclusive)").option("--wait <ms>", "Sleep this many ms after DOMContentLoaded before evaluating (lets late async work settle; max 30000)", "500").option("--wait-for <selector>", "Wait until this CSS selector appears before evaluating (deterministic; replaces --wait)").option("--wait-timeout <ms>", "Max ms to wait for --wait-for before giving up", "5000").option("--auth", "Evaluate signed in as you (your Gipity account), so a page behind a Sign-in-with-Gipity login is reachable. Only works for apps using Sign in with Gipity, hosted on *.gipity.ai.").option("--json", "Output as JSON").action((url, exprArg, opts) => run("Page eval", async () => {
35367
+ ).option("--reload-file <path>", "Read the post-reload expression from a file instead of inline --reload (mutually exclusive)").option(
35368
+ "--camera <path>",
35369
+ `Play a local image or video (.png/.jpg/.webp/.mp4/.webm/.y4m/.mjpeg) as the browser's WEBCAM feed, so a camera app's real pipeline (getUserMedia \u2192 MediaPipe/YOLOX \u2192 your app logic) runs headlessly on a frame you choose. Implies --fake-media and waits ${CAMERA_DEFAULT_WAIT_MS / 1e3}s for the vision model to load. No frame handy? gipity generate image "a hand making a closed fist, palm to camera".`
35370
+ ).option("--fake-media", "Grant a synthetic microphone + camera and auto-accept the getUserMedia prompt, so a voice/camera app runs headlessly instead of hitting its no-camera path. The feed is a built-in test pattern (and a tone) \u2014 nothing a vision model can recognize; to drive a vision app use --camera <path> instead.").option("--wait <ms>", "Sleep this many ms after DOMContentLoaded before evaluating (lets late async work settle; max 30000). With --wait-for, it elapses AFTER the selector appears - gate, then settle.", "500").option("--wait-for <selector>", `Wait until this CSS selector appears before evaluating, then evaluate (deterministic - beats guessing a --wait). Gate on the state you are ASSERTING, not just a ready flag: '#verdict:not(:empty)' returns the instant the round lands, where a fixed wait snapshots mid-sequence. Same flag on page inspect/screenshot. Max ${WAIT_FOR_MAX_MS}ms (--wait-timeout).`).option("--wait-timeout <ms>", `Max ms to wait for --wait-for before giving up (max ${WAIT_FOR_MAX_MS})`, "5000").option(
35371
+ "--timeout <ms>",
35372
+ `How long the script itself may run IN the page - its own await/setTimeout pauses count (default ${EVAL_SCRIPT_BUDGET_MS}, ${EVAL_SCRIPT_BUDGET_CAMERA_MS} with --camera/--fake-media; max ${EVAL_SCRIPT_BUDGET_MAX_MS}). Raise it to trace a sequence that unfolds over time (a game round, an animation). Distinct from --wait, which only sleeps BEFORE the script.`
35373
+ ).option("--auth", "Evaluate signed in as you (your Gipity account), so a page behind a Sign-in-with-Gipity login is reachable. Only works for apps using Sign in with Gipity, hosted on *.gipity.ai.").option("--json", "Output as JSON").action((url, exprArg, opts) => run("Page eval", async () => {
35223
35374
  const decoy = JS_DECOY_FLAGS.find((f) => opts[f.slice(2)] !== void 0);
35224
35375
  if (decoy) {
35225
35376
  pageEvalCommand.error(
@@ -35258,17 +35409,44 @@ var pageEvalCommand = new Command("eval").description("Evaluate JS in a real bro
35258
35409
  pageEvalCommand.error(`error: Cannot read file: ${opts.reloadFile}`);
35259
35410
  }
35260
35411
  }
35261
- const waitMs = capWaitMs(opts.wait, url);
35262
- const parsedTimeout = parseInt(opts.waitTimeout, 10);
35263
- const waitForTimeoutMs = Number.isFinite(parsedTimeout) && parsedTimeout >= 0 ? parsedTimeout : 5e3;
35412
+ const steps = opts.step ?? [];
35413
+ if (steps.length > MAX_EVAL_STEPS) {
35414
+ pageEvalCommand.error(
35415
+ `error: at most ${MAX_EVAL_STEPS} --step expressions ride on one page load (got ${steps.length}) \u2014 do more per step, or split this into two evals`
35416
+ );
35417
+ }
35418
+ let waitMs = capWaitMs(opts.wait, url);
35419
+ const waitForTimeoutMs = capWaitForTimeoutMs(opts.waitTimeout);
35420
+ const hasMedia = !!opts.camera || !!opts.fakeMedia;
35421
+ const scriptBudgetMs = capScriptBudgetMs(opts.timeout, hasMedia);
35422
+ const explicitWait = pageEvalCommand.getOptionValueSource("wait") !== "default";
35423
+ const chosenWait = explicitWait || !!opts.waitFor;
35424
+ if (opts.camera && !chosenWait) {
35425
+ waitMs = CAMERA_DEFAULT_WAIT_MS;
35426
+ console.error(muted(
35427
+ `--camera: waiting ${CAMERA_DEFAULT_WAIT_MS / 1e3}s before the eval so the camera and the app's vision model finish loading (they cannot warm up inside the eval body \u2014 that has a ${scriptBudgetMs / 1e3}s budget of its own, --timeout to change it). Override with --wait <ms>, or use --wait-for '<ready-selector>' to stop as soon as the app is ready.`
35428
+ ));
35429
+ }
35430
+ const settleAfterGateMs = opts.waitFor && explicitWait ? waitMs : 0;
35431
+ const sentWaitMs = settleAfterGateMs > 0 ? 0 : waitMs;
35432
+ const sentScriptBudgetMs = Math.min(scriptBudgetMs + settleAfterGateMs, EVAL_SCRIPT_BUDGET_MAX_MS);
35264
35433
  const fixturePaths = opts.fixture ?? [];
35265
35434
  const hosted = [];
35435
+ let camera;
35266
35436
  let projectGuid;
35267
35437
  let sentExpr = expr;
35438
+ let sentSteps = steps;
35439
+ if (opts.camera) assertCameraFile(opts.camera);
35268
35440
  try {
35269
- if (fixturePaths.length) {
35441
+ if (fixturePaths.length || opts.camera) {
35270
35442
  const { config } = await resolveProjectContext({});
35271
35443
  projectGuid = config.projectGuid;
35444
+ }
35445
+ if (opts.camera) {
35446
+ console.log(muted(`Hosting camera feed ${opts.camera}\u2026`));
35447
+ camera = await uploadCameraFeed(projectGuid, opts.camera);
35448
+ }
35449
+ if (fixturePaths.length) {
35272
35450
  for (const p of fixturePaths) {
35273
35451
  console.log(muted(`Hosting fixture ${p}\u2026`));
35274
35452
  hosted.push(await uploadPublicFixture(projectGuid, p));
@@ -35276,30 +35454,63 @@ var pageEvalCommand = new Command("eval").description("Evaluate JS in a real bro
35276
35454
  const map = {};
35277
35455
  for (const h of hosted) map[h.name] = h.url;
35278
35456
  const prelude = `const fixtures=${JSON.stringify(map)};const fixtureUrl=${JSON.stringify(hosted[0].url)};`;
35279
- sentExpr = opts.file ? `${prelude}
35280
- ${expr}` : `${prelude}
35281
- return (${expr});`;
35457
+ sentExpr = withPrelude(sentExpr, prelude);
35458
+ sentSteps = sentSteps.map((s) => withPrelude(s, prelude));
35459
+ }
35460
+ if (settleAfterGateMs > 0) {
35461
+ sentExpr = withPrelude(sentExpr, `await new Promise((r) => setTimeout(r, ${settleAfterGateMs}));`);
35282
35462
  }
35283
35463
  const kickoff = await post("/tools/browser/eval", {
35284
35464
  url,
35285
35465
  expr: sentExpr,
35286
- waitMs,
35466
+ waitMs: sentWaitMs,
35467
+ steps: sentSteps.length ? sentSteps : void 0,
35287
35468
  reloadExpr,
35288
35469
  waitForSelector: opts.waitFor || void 0,
35289
35470
  waitForTimeoutMs: opts.waitFor ? waitForTimeoutMs : void 0,
35290
- auth: opts.auth || void 0
35471
+ timeoutMs: sentScriptBudgetMs,
35472
+ auth: opts.auth || void 0,
35473
+ // A camera feed needs the synthetic devices in place to be played into,
35474
+ // so --camera implies --fake-media rather than failing on the pairing.
35475
+ fakeMedia: opts.fakeMedia || !!camera || void 0,
35476
+ cameraUrl: camera?.url
35291
35477
  });
35292
- const d = await pollEvalResult(kickoff.data.evalJobId, reloadExpr !== void 0 ? waitMs * 2 : waitMs);
35478
+ let d;
35479
+ try {
35480
+ d = await pollEvalResult(kickoff.data.evalJobId, evalWorkBudgetMs({
35481
+ waitMs: sentWaitMs,
35482
+ waitForTimeoutMs: opts.waitFor ? waitForTimeoutMs : 0,
35483
+ hasReload: reloadExpr !== void 0,
35484
+ hasCamera: !!camera,
35485
+ scriptBudgetMs: sentScriptBudgetMs,
35486
+ legs: 1 + sentSteps.length
35487
+ }));
35488
+ } catch (err) {
35489
+ const msg = err?.message ?? "";
35490
+ const hint = budgetOverrunHint(msg, scriptBudgetMs);
35491
+ if (!hint) throw err;
35492
+ throw new Error(`${msg}
35493
+ ${hint}`);
35494
+ }
35293
35495
  const { result, noValue } = normalizeEvalResult(d.result);
35294
35496
  const reload = d.reloadResult !== void 0 ? normalizeEvalResult(d.reloadResult) : void 0;
35295
- const execTimeout = evalExecTimeoutMessage(d.result);
35497
+ const execTimeout = evalExecTimeoutMessage(d.result, scriptBudgetMs);
35296
35498
  if (execTimeout) throw new Error(execTimeout);
35499
+ const stepOut = steps.map((stepExpr, i) => {
35500
+ const raw = d.stepResults?.[i];
35501
+ const norm = raw !== void 0 ? normalizeEvalResult(raw) : void 0;
35502
+ return { expr: stepExpr, result: norm?.result ?? "", noValue: norm?.noValue ?? true };
35503
+ });
35504
+ const emptyState2 = !noValue && isEmptyStateResult(result);
35505
+ const emptyHint = camera ? EVAL_CAMERA_EMPTY_HINT : EVAL_EMPTY_STATE_HINT;
35297
35506
  if (opts.json) {
35298
35507
  console.log(JSON.stringify({
35299
35508
  ...d,
35300
35509
  result,
35510
+ ...stepOut.length ? { stepResults: stepOut.map((s) => s.result) } : {},
35301
35511
  ...reload ? { reloadResult: reload.result } : {},
35302
- ...noValue ? { hint: EVAL_NO_VALUE_HINT } : {}
35512
+ ...noValue ? { hint: EVAL_NO_VALUE_HINT } : {},
35513
+ ...emptyState2 ? { hint: emptyHint } : {}
35303
35514
  }));
35304
35515
  return;
35305
35516
  }
@@ -35308,19 +35519,29 @@ return (${expr});`;
35308
35519
  console.log(`${warning("\u26A0 Navigation incomplete:")} ${d.note || "page did not reach full load"}`);
35309
35520
  }
35310
35521
  if (d.slowRender) {
35311
- console.log(`${warning("\u26A0 Slow render:")} page painted at ${d.slowRender.fps} fps. Waiting on real time (setTimeout) advances animation/physics time far slower than it looks \u2014 assertions after a wall-clock wait can report a false negative. Step the app's own loop deterministically instead (3D templates: ${bold("core.advance(seconds)")}).`);
35522
+ console.log(slowRenderMessage(d.slowRender.fps, { camera: !!camera, waitMs }));
35312
35523
  }
35313
35524
  if (d.auth?.requested) {
35314
35525
  const who = getAuth()?.email;
35315
35526
  console.log(d.auth.established ? `${muted("Auth:")} ${success("session established")}${who ? muted(` as ${who}`) : ""} ${muted("(what the page renders with it is app-defined)")}` : `${warning("Auth: session NOT established")}${d.auth.detail ? ` \u2014 ${d.auth.detail}` : ""} ${muted("(this is the anonymous view)")}`);
35316
35527
  }
35528
+ if (camera) console.log(`${muted("Camera:")} ${camera.name} ${muted("(played as the webcam feed; getUserMedia resolves)")}`);
35317
35529
  if (hosted.length) console.log(`${muted("Fixtures:")} ${hosted.map((h) => h.name).join(", ")}`);
35318
35530
  console.log(opts.file ? `${muted("Script:")} ${opts.file}` : `${muted("Expression:")} ${summarizeExpr(expr)}`);
35319
35531
  console.log(`
35320
35532
  ${result.trim() ? result : muted("(empty result)")}`);
35321
35533
  if (noValue) console.log(muted(`
35322
35534
  ${EVAL_NO_VALUE_HINT}`));
35535
+ if (emptyState2) console.log(muted(`
35536
+ ${emptyHint}`));
35323
35537
  if (d.truncated) console.log(muted("\n(result truncated to fit context - narrow the expression for the full value)"));
35538
+ for (let i = 0; i < stepOut.length; i++) {
35539
+ const s = stepOut[i];
35540
+ console.log(`
35541
+ ${bold(`Step ${i + 1}`)} ${muted(summarizeExpr(s.expr))} ${muted("(same page load)")}`);
35542
+ console.log(s.result.trim() ? s.result : muted("(empty result)"));
35543
+ if (s.noValue) console.log(muted(EVAL_NO_VALUE_HINT));
35544
+ }
35324
35545
  if (reload) {
35325
35546
  console.log(`
35326
35547
  ${bold("After reload")} ${muted("(page reloaded in place \u2014 storage preserved)")}`);
@@ -35329,7 +35550,7 @@ ${bold("After reload")} ${muted("(page reloaded in place \u2014 storage preserve
35329
35550
  if (d.reloadTruncated) console.log(muted("(reload result truncated to fit context - narrow the expression for the full value)"));
35330
35551
  }
35331
35552
  } finally {
35332
- for (const h of hosted) {
35553
+ for (const h of [...hosted, ...camera ? [camera] : []]) {
35333
35554
  try {
35334
35555
  await deleteFixture(projectGuid, h.guid);
35335
35556
  } catch (err) {
@@ -35355,15 +35576,52 @@ Examples:
35355
35576
  "localStorage.setItem('todo','milk'); document.title" \\
35356
35577
  --reload "({ restored: localStorage.getItem('todo'), heading: document.querySelector('h1')?.textContent })"
35357
35578
 
35579
+ # Camera app (MediaPipe / YOLOX / any getUserMedia app): play a real image as
35580
+ # the webcam so the app's OWN pipeline runs on a frame you control \u2014 no need to
35581
+ # stub the model or export internals just to test around a missing camera.
35582
+ # --camera waits ${CAMERA_DEFAULT_WAIT_MS / 1e3}s before evaluating (model load + first frame); the script
35583
+ # itself then gets ${EVAL_SCRIPT_BUDGET_CAMERA_MS / 1e3}s in the page (--timeout, max ${EVAL_SCRIPT_BUDGET_MAX_MS / 1e3}s).
35584
+ gipity generate image "a hand making a closed fist, palm to camera, plain background" -o fist.png
35585
+ gipity page eval "https://dev.gipity.ai/me/app/" --camera fist.png \\
35586
+ "document.querySelector('#detected-gesture').textContent"
35587
+ # Camera app that only starts on a click? Start it on page load instead (a headless
35588
+ # run has no user), and expose a ready signal so the wait ends the moment it's up:
35589
+ gipity page eval "https://dev.gipity.ai/me/app/" --camera fist.png \\
35590
+ --wait-for "#detected-gesture:not(:empty)" --wait-timeout ${WAIT_FOR_MAX_MS} \\
35591
+ "document.querySelector('#detected-gesture').textContent"
35592
+ # Several assertions about the SAME page? Use --step, not several commands: a camera
35593
+ # page load pays for getUserMedia + the vision model ONCE and every step reuses it.
35594
+ gipity page eval "https://dev.gipity.ai/me/app/" --camera fist.png \\
35595
+ --wait-for '[data-vision="ready"]' \\
35596
+ "window.__vision.gesture()" \\
35597
+ --step "document.getElementById('see').textContent" \\
35598
+ --step "({ score: score.textContent, verdict: verdict.textContent })"
35599
+
35358
35600
  Module resolution: dynamic import() specifiers starting with ./ or ../ resolve
35359
35601
  against the PAGE URL, so import('./packages/i18n/index.js') loads the app's own
35360
35602
  module without hand-building the deployed /account/project/ path. Absolute paths
35361
35603
  and full URLs pass through unchanged.
35362
35604
 
35363
- The eval body runs under a ~20s in-page execution budget (its own await/setTimeout
35364
- pauses count; --wait only sleeps BEFORE the eval and does not extend it). For a long
35365
- interactive sequence, split it into several shorter evals (one per state to verify)
35366
- rather than one body with many long waits.
35605
+ Time budget: the script runs under a ${EVAL_SCRIPT_BUDGET_MS / 1e3}s in-page budget (${EVAL_SCRIPT_BUDGET_CAMERA_MS / 1e3}s with --camera/--fake-media),
35606
+ counting its own await/setTimeout pauses. Two separate knobs:
35607
+ --timeout <ms> how long the SCRIPT may run in the page (max ${EVAL_SCRIPT_BUDGET_MAX_MS}) \u2014 raise this to
35608
+ trace a sequence that unfolds over time (a game round, an animation)
35609
+ --wait / --wait-for how long to settle BEFORE the script runs \u2014 a blind sleep, a
35610
+ selector gate, or both (gate first, then sleep). Neither extends --timeout.
35611
+ Which one:
35612
+ - waiting for the app to REACH a state before you read it \u2192 --wait-for '<selector>'
35613
+ (gate on the end state \u2014 '#verdict:not(:empty)' \u2014 not just a ready flag; a bare
35614
+ --wait samples one arbitrary instant and can land mid-sequence). Passing both means
35615
+ gate first, then settle --wait ms.
35616
+ - watching state evolve (poll until a result lands, record a trace) \u2192 --timeout
35617
+ - a slow ONE-TIME init (WASM/model download, big asset) \u2192 do NOT split the body across
35618
+ calls; every call re-loads the page and re-pays it. Start that work on page load and
35619
+ absorb it in the pre-eval window: --wait <ms> (max ${MAX_WAIT_MS}) or, better,
35620
+ --wait-for '<ready-selector>' --wait-timeout ${WAIT_FOR_MAX_MS}.
35621
+
35622
+ Several things to check on one page? Pass --step (max ${MAX_EVAL_STEPS}) instead of running
35623
+ 'page eval' N times: the steps run in order against the ONE loaded page, so a slow
35624
+ boot (vision model, game, socket) is paid once, not once per assertion.
35367
35625
 
35368
35626
  Testing realtime/shared state across clients?
35369
35627
  Separate 'page eval' calls run sequentially (one finishes before the next
@@ -35378,7 +35636,7 @@ init_auth();
35378
35636
  init_colors();
35379
35637
  init_utils();
35380
35638
  import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
35381
- import { dirname as dirname7, join as join11, resolve as resolvePath } from "path";
35639
+ import { dirname as dirname7, join as join12, resolve as resolvePath } from "path";
35382
35640
  var DEVICE_PRESETS = {
35383
35641
  default: { width: 1280, height: 720 },
35384
35642
  desktop: { width: 1920, height: 1080 },
@@ -35401,8 +35659,17 @@ function printAuthLine(auth) {
35401
35659
  }
35402
35660
  function printActionErrorLine(actionError) {
35403
35661
  if (!actionError) return;
35662
+ if (/wait-for:/.test(actionError)) {
35663
+ console.log(`${warning("\u26A0 --wait-for never matched:")} ${actionError.replace(/^EvalError:\s*/, "")} ${muted("(the image below is the state the page WAS in when the gate gave up)")}`);
35664
+ return;
35665
+ }
35404
35666
  console.log(`${warning("\u26A0 --action failed:")} ${actionError} ${muted("(this image shows the page BEFORE the action ran)")}`);
35405
35667
  }
35668
+ function augmentSandboxTimeout(message) {
35669
+ if (!/did not respond within/i.test(message)) return message;
35670
+ return `${message}
35671
+ A pre-capture script was set (--action and/or the --wait-for gate), and its runtime is spent INSIDE that same sandbox budget \u2014 a script that waits on slow work (a WASM/model download, a network fetch, a long animation) can exhaust the budget on its own. Keep --action to the interaction itself (a click, a keypress), let the page do the slow work on load, and absorb that with --wait <ms> or a tighter --wait-for '<selector>' gate instead.`;
35672
+ }
35406
35673
  function fmtPerformance(p) {
35407
35674
  const parts = [
35408
35675
  `TTFB ${fmtMs(p.ttfb)}`,
@@ -35430,7 +35697,7 @@ function dimSuffix(vp2) {
35430
35697
  }
35431
35698
  function defaultScreenshotDir() {
35432
35699
  const root = getProjectRoot();
35433
- return join11(root ?? ".", ".gipity", "screenshots");
35700
+ return join12(root ?? ".", "screenshots");
35434
35701
  }
35435
35702
  function timestampSlug(d = /* @__PURE__ */ new Date()) {
35436
35703
  const p = (n) => String(n).padStart(2, "0");
@@ -35485,17 +35752,51 @@ function appendOption(value, previous = []) {
35485
35752
  return [...previous, value];
35486
35753
  }
35487
35754
  var ACTION_DECOY_FLAGS = ["--eval", "--js", "--javascript", "--script", "--code", "--exec"];
35488
- var pageScreenshotCommand = new Command("screenshot").description("Screenshot a web page").argument("<url>", "URL to screenshot").option("--post-load-delay <ms>", "Delay after DOMContentLoaded before capture, in ms (default: 1000)").option("--action <js>", `Run JS in the page before capturing \u2014 e.g. click a button to enter a state ("document.getElementById('play').click()"). Runs as an async function body, so const/await and app-relative import('./\u2026') work. Runs after the post-load delay, then settles again before the shot. If it throws, the capture still happens and the failure is reported.`).option("--full", "Capture the full scrollable page (default: viewport only). Scrolls the page through first so scroll-reveal/fade-in-on-scroll (IntersectionObserver) sections render into the shot instead of capturing blank.").option("-o, --output <file>", "Output path (single viewport only; default .gipity/screenshots/ss-<host>-<timestamp>.png)").option("--device <names>", `Device preset(s): ${Object.keys(DEVICE_PRESETS).join(", ")} (comma-separated or repeat flag). mobile/tablet emulate a real touch device \u2014 touch events, mobile user-agent, DPR \u2014 so touch-gated mobile UI actually renders.`, appendOption, []).option("--viewport <dims>", "Raw viewport(s): WxH or WxH@dpr (comma-separated or repeat flag)", appendOption, []).option("--no-reload-between", "Skip reload between viewports (faster, lower fidelity - only safe for static pages)").option("--fake-media", "Grant a synthetic microphone + camera and auto-accept the getUserMedia prompt, so voice/camera apps render headlessly (audio is a built-in tone, not real speech)").option("--auth", "Capture the page signed in as you (your Gipity account), so UI behind a Sign-in-with-Gipity login is shown. Only works for apps using Sign in with Gipity, hosted on *.gipity.ai.").option("--json", "Output JSON metadata instead of a friendly summary").addOption(new Option("--wait <ms>", "Alias for --post-load-delay").hideHelp()).addOption(new Option("--full-page", "Alias for --full").hideHelp()).action((url, opts) => run("Page screenshot", async () => {
35755
+ function buildWaitForGate(selector, timeoutMs) {
35756
+ const sel = JSON.stringify(selector);
35757
+ return `await (async () => { const __t0 = Date.now(); while (Date.now() - __t0 < ${timeoutMs}) { try { if (document.querySelector(${sel})) return; } catch (e) { throw new Error('wait-for: invalid selector ' + ${sel}); } await new Promise((r) => setTimeout(r, 100)); } throw new Error('wait-for: nothing matched ' + ${sel} + ' within ${timeoutMs}ms \u2014 the page never reached that state (raise --wait-timeout, fix the selector, or check the app actually gets there headlessly)'); })();`;
35758
+ }
35759
+ var WAIT_FOR_MAX_MS2 = 15e3;
35760
+ var WAIT_FOR_DEFAULT_MS = 15e3;
35761
+ var MAX_POST_LOAD_DELAY_MS = 3e4;
35762
+ var CAMERA_DEFAULT_DELAY_MS = 15e3;
35763
+ var pageScreenshotCommand = new Command("screenshot").description("Screenshot a web page").argument("<url>", "URL to screenshot").option("--wait <ms>", `Sleep this many ms after DOMContentLoaded before capturing (default 1000, max ${MAX_POST_LOAD_DELAY_MS}). With --camera it defaults to ${CAMERA_DEFAULT_DELAY_MS} so the vision model is up. Same flag as page eval/inspect.`).option("--wait-for <selector>", `Wait until this CSS selector appears before capturing, then capture (deterministic - beats guessing a --wait). Same flag as page eval/inspect. Gate on what you are photographing ('[data-vision="ready"]', '#verdict:not(:empty)'). Max ${WAIT_FOR_MAX_MS2}ms (--wait-timeout).`).option("--wait-timeout <ms>", `Max ms to wait for --wait-for before giving up (default ${WAIT_FOR_DEFAULT_MS}, max ${WAIT_FOR_MAX_MS2}). Timing out is reported - the capture still happens, so you see the state it got stuck in.`).option("--action <js>", `Run JS in the page before capturing \u2014 e.g. click a button to enter a state ("document.getElementById('play').click()"). Runs as an async function body, so const/await and app-relative import('./\u2026') work. Runs after the post-load delay, then settles again before the shot. If it throws, the capture still happens and the failure is reported.`).option("--full", "Capture the full scrollable page (default: viewport only). Scrolls the page through first so scroll-reveal/fade-in-on-scroll (IntersectionObserver) sections render into the shot instead of capturing blank.").option("-o, --output <file>", "Output path (single viewport only; default .gipity/screenshots/ss-<host>-<timestamp>.png)").option("--device <names>", `Device preset(s): ${Object.keys(DEVICE_PRESETS).join(", ")} (comma-separated or repeat flag). mobile/tablet emulate a real touch device \u2014 touch events, mobile user-agent, DPR \u2014 so touch-gated mobile UI actually renders.`, appendOption, []).option("--viewport <dims>", "Raw viewport(s): WxH or WxH@dpr (comma-separated or repeat flag)", appendOption, []).option("--no-reload-between", "Skip reload between viewports (faster, lower fidelity - only safe for static pages)").option("--fake-media", "Grant a synthetic microphone + camera and auto-accept the getUserMedia prompt, so voice/camera apps render headlessly. The video feed is a built-in test pattern \u2014 to capture what the app does with a REAL frame (a hand, a face, an object), use --camera <path> instead.").option("--camera <path>", "Play a local image or video (.png/.jpg/.webp/.mp4/.webm/.y4m/.mjpeg) as the browser's WEBCAM feed, then capture \u2014 so the shot shows the app reacting to a frame you chose (detected gesture, boxes, labels). Implies --fake-media.").option("--auth", "Capture the page signed in as you (your Gipity account), so UI behind a Sign-in-with-Gipity login is shown. Only works for apps using Sign in with Gipity, hosted on *.gipity.ai.").option("--ephemeral", "Skip the project screenshot history: do not persist this capture to Gipity (screenshots/ in the project). Local file is still written.").option("--json", "Output JSON metadata instead of a friendly summary").addOption(new Option("--post-load-delay <ms>", "Alias for --wait").hideHelp()).addOption(new Option("--full-page", "Alias for --full").hideHelp()).action((url, opts) => run("Page screenshot", async () => {
35489
35764
  const decoy = ACTION_DECOY_FLAGS.find((f) => opts[f.slice(2)] !== void 0);
35490
35765
  if (decoy) {
35491
35766
  pageScreenshotCommand.error(
35492
35767
  `error: ${decoy} is not a flag on screenshot \u2014 use --action "<js>" to run JavaScript in the page before the capture, e.g. gipity page screenshot "<url>" --action "document.getElementById('play').click()"`
35493
35768
  );
35494
35769
  }
35495
- const delayRaw = opts.postLoadDelay ?? opts.wait ?? "1000";
35496
- const postLoadDelayMs = delayRaw !== void 0 ? parseInt(String(delayRaw), 10) : void 0;
35497
- if (postLoadDelayMs !== void 0 && (!Number.isFinite(postLoadDelayMs) || postLoadDelayMs < 0)) {
35498
- throw new Error("--post-load-delay must be a non-negative integer (ms)");
35770
+ const delayRaw = opts.wait ?? opts.postLoadDelay;
35771
+ const chosenDelay = delayRaw !== void 0;
35772
+ let postLoadDelayMs = chosenDelay ? parseInt(String(delayRaw), 10) : 1e3;
35773
+ if (!Number.isFinite(postLoadDelayMs) || postLoadDelayMs < 0) {
35774
+ throw new Error("--wait must be a non-negative integer (ms)");
35775
+ }
35776
+ if (postLoadDelayMs > MAX_POST_LOAD_DELAY_MS) {
35777
+ console.error(warning(
35778
+ `--wait ${postLoadDelayMs}ms exceeds the ${MAX_POST_LOAD_DELAY_MS}ms cap \u2014 using ${MAX_POST_LOAD_DELAY_MS}ms. Waiting longer is rarely the fix: gate on the state you want with --wait-for '<selector>' instead of guessing a duration.`
35779
+ ));
35780
+ postLoadDelayMs = MAX_POST_LOAD_DELAY_MS;
35781
+ }
35782
+ const waitForTimeoutRaw = opts.waitTimeout !== void 0 ? parseInt(String(opts.waitTimeout), 10) : WAIT_FOR_DEFAULT_MS;
35783
+ if (!Number.isFinite(waitForTimeoutRaw) || waitForTimeoutRaw < 0) {
35784
+ throw new Error("--wait-timeout must be a non-negative integer (ms)");
35785
+ }
35786
+ const waitForTimeoutMs = Math.min(waitForTimeoutRaw, WAIT_FOR_MAX_MS2);
35787
+ if (waitForTimeoutRaw > WAIT_FOR_MAX_MS2) {
35788
+ console.error(warning(
35789
+ `--wait-timeout ${waitForTimeoutRaw}ms exceeds the ${WAIT_FOR_MAX_MS2}ms the capture's browser budget covers \u2014 using ${WAIT_FOR_MAX_MS2}ms. A state that takes longer than that to appear is not a screenshot problem: watch for it with \`gipity page eval <url> --wait-for '<selector>' --wait-timeout 30000\` (a wider budget), then capture it.`
35790
+ ));
35791
+ }
35792
+ if (opts.waitTimeout !== void 0 && !opts.waitFor) {
35793
+ throw new Error("--wait-timeout only means something with --wait-for '<selector>' (it bounds that gate)");
35794
+ }
35795
+ if (opts.camera && !chosenDelay && !opts.waitFor) {
35796
+ postLoadDelayMs = CAMERA_DEFAULT_DELAY_MS;
35797
+ console.error(muted(
35798
+ `--camera: waiting ${CAMERA_DEFAULT_DELAY_MS / 1e3}s before the capture so the camera and the app's vision model finish loading. Override with --wait <ms>, or use --wait-for '<ready-selector>' to shoot the moment it's ready.`
35799
+ ));
35499
35800
  }
35500
35801
  const deviceNames = splitCsv(opts.device);
35501
35802
  const viewportStrs = splitCsv(opts.viewport);
@@ -35507,18 +35808,54 @@ var pageScreenshotCommand = new Command("screenshot").description("Screenshot a
35507
35808
  throw new Error("--output can only be used with a single viewport");
35508
35809
  }
35509
35810
  const userSpecifiedViewports = customViewports.length > 0;
35811
+ const slug = slugFromUrl(url);
35812
+ const ts2 = timestampSlug();
35813
+ const shotName = (vp2) => defaultFilename(slug, ts2, vp2 && userSpecifiedViewports ? dimSuffix(vp2) : void 0);
35814
+ const names = userSpecifiedViewports ? customViewports.map(shotName) : [shotName()];
35815
+ const projectGuid = getConfig()?.projectGuid;
35816
+ const save = !opts.ephemeral && projectGuid ? { project_guid: projectGuid, names } : void 0;
35817
+ let camera;
35818
+ if (opts.camera) {
35819
+ assertCameraFile(opts.camera);
35820
+ if (!projectGuid) throw new Error("--camera needs a linked project (the frame is hosted for the browser to fetch) \u2014 run `gipity link` first.");
35821
+ console.log(muted(`Hosting camera feed ${opts.camera}\u2026`));
35822
+ camera = await uploadCameraFeed(projectGuid, opts.camera);
35823
+ }
35824
+ const preCapture = [
35825
+ opts.waitFor ? buildWaitForGate(opts.waitFor, waitForTimeoutMs) : "",
35826
+ opts.action ?? ""
35827
+ ].filter(Boolean).join("\n");
35510
35828
  const body = {
35511
35829
  url,
35512
35830
  postLoadDelayMs,
35513
35831
  full: !!(opts.full || opts.fullPage),
35514
35832
  reloadBetween: opts.reloadBetween !== false,
35515
35833
  ...userSpecifiedViewports ? { viewports: customViewports } : {},
35516
- ...opts.fakeMedia ? { fakeMedia: true } : {},
35834
+ // A camera feed is played into the synthetic devices, so --camera implies
35835
+ // --fake-media rather than failing on the pairing.
35836
+ ...opts.fakeMedia || camera ? { fakeMedia: true } : {},
35837
+ ...camera ? { cameraUrl: camera.url } : {},
35517
35838
  ...opts.auth ? { auth: true } : {},
35518
- ...opts.action ? { action: opts.action } : {}
35839
+ ...preCapture ? { action: preCapture } : {},
35840
+ ...save ? { save } : {}
35519
35841
  };
35520
35842
  const doShoot = () => postForTarEntries("/tools/browser/screenshot", body);
35521
- const entries = opts.json ? await doShoot() : await withSpinner("Capturing\u2026", doShoot, { done: null });
35843
+ let entries;
35844
+ try {
35845
+ try {
35846
+ entries = opts.json ? await doShoot() : await withSpinner("Capturing\u2026", doShoot, { done: null });
35847
+ } catch (err) {
35848
+ throw preCapture ? new Error(augmentSandboxTimeout(err.message)) : err;
35849
+ }
35850
+ } finally {
35851
+ if (camera) {
35852
+ try {
35853
+ await deleteFixture(projectGuid, camera.guid);
35854
+ } catch (err) {
35855
+ console.error(warning(`\u26A0 Could not auto-delete camera feed "${camera.name}" (${camera.guid}) \u2014 still hosted at ${camera.url}: ${err.message}`));
35856
+ }
35857
+ }
35858
+ }
35522
35859
  const metaEntry = entries.find((e) => e.name === "meta.json");
35523
35860
  if (!metaEntry) throw new Error("Server response missing meta.json");
35524
35861
  const meta = JSON.parse(metaEntry.buffer.toString("utf8"));
@@ -35526,14 +35863,10 @@ var pageScreenshotCommand = new Command("screenshot").description("Screenshot a
35526
35863
  if (pngs.length !== meta.screenshots.length) {
35527
35864
  throw new Error(`Server returned ${pngs.length} PNGs but ${meta.screenshots.length} metadata entries`);
35528
35865
  }
35529
- const slug = slugFromUrl(url);
35530
- const ts2 = timestampSlug();
35531
35866
  const dir = defaultScreenshotDir();
35532
35867
  const savedFiles = [];
35533
35868
  for (let i = 0; i < pngs.length; i++) {
35534
- const shot = meta.screenshots[i];
35535
- const suffix = userSpecifiedViewports ? dimSuffix(shot.viewport) : void 0;
35536
- const target = opts.output ? opts.output : join11(dir, defaultFilename(slug, ts2, suffix));
35869
+ const target = opts.output ? opts.output : join12(dir, names[i] ?? shotName(meta.screenshots[i].viewport));
35537
35870
  mkdirSync7(dirname7(target), { recursive: true });
35538
35871
  writeFileSync8(target, pngs[i].buffer);
35539
35872
  savedFiles.push(resolvePath(target));
@@ -35561,11 +35894,20 @@ var pageScreenshotCommand = new Command("screenshot").description("Screenshot a
35561
35894
  height: s.height,
35562
35895
  size_bytes: s.screenshotSizeBytes,
35563
35896
  full_page: meta.full,
35564
- phase: s.phase
35897
+ phase: s.phase,
35898
+ ...s.vfs ? { gipity: s.vfs } : {}
35565
35899
  }))
35566
35900
  }));
35567
35901
  return;
35568
35902
  }
35903
+ const printVfsLine = (s) => {
35904
+ if (!s.vfs) return;
35905
+ if ("error" in s.vfs) {
35906
+ console.log(`${warning("\u26A0 Gipity save failed:")} ${s.vfs.error} ${muted("(local file is fine)")}`);
35907
+ } else {
35908
+ console.log(`${label("Gipity history")} ${s.vfs.path} ${muted("(synced, not deployed)")}`);
35909
+ }
35910
+ };
35569
35911
  if (meta.screenshots.length === 1) {
35570
35912
  const s = meta.screenshots[0];
35571
35913
  console.log(`${brand("Screenshot")} ${bold(url)}`);
@@ -35580,6 +35922,7 @@ var pageScreenshotCommand = new Command("screenshot").description("Screenshot a
35580
35922
  console.log(`${label("Screenshot size")} ${sizePart}`);
35581
35923
  if (s.width && s.height) console.log(`${label("Screenshot dims")} ${s.width} \xD7 ${s.height}`);
35582
35924
  console.log(`${label("Screenshot file")} ${success(savedFiles[0])}`);
35925
+ printVfsLine(s);
35583
35926
  return;
35584
35927
  }
35585
35928
  console.log(`${brand("Loading")} ${bold(url)} ${muted(`once \u2192 ${meta.screenshots.length} viewports`)}`);
@@ -35599,6 +35942,7 @@ ${brand("@ " + dims)}${deviceTag}`);
35599
35942
  console.log(`${label("Screenshot size")} ${sizePart}`);
35600
35943
  if (s.width && s.height) console.log(`${label("Screenshot dims")} ${s.width} \xD7 ${s.height}`);
35601
35944
  console.log(`${label("Screenshot file")} ${success(savedFiles[i])}`);
35945
+ printVfsLine(s);
35602
35946
  }
35603
35947
  }));
35604
35948
  for (const f of ACTION_DECOY_FLAGS) pageScreenshotCommand.addOption(new Option(`${f} <value>`).hideHelp());
@@ -35609,11 +35953,21 @@ Examples:
35609
35953
  gipity page screenshot "https://dev.gipity.ai/me/app/" --device mobile,desktop
35610
35954
  gipity page screenshot "https://dev.gipity.ai/me/app/" \\
35611
35955
  --action "document.getElementById('play').click()" # capture an in-game frame
35956
+ # Camera / vision app: play a real frame in as the webcam and shoot once the app
35957
+ # says it's ready \u2014 same --camera / --wait-for flags as 'page eval', no guessed delay:
35958
+ gipity page screenshot "https://dev.gipity.ai/me/app/" --camera fist.png \\
35959
+ --wait-for '[data-vision="ready"]'
35960
+
35961
+ Waiting for the page to reach a state before the shot?
35962
+ --wait-for '<selector>' gates the capture on the app's own signal (deterministic).
35963
+ Reach for --wait <ms> only when there is nothing to gate on \u2014 a guessed duration
35964
+ either shoots too early or wastes the difference.
35612
35965
 
35613
35966
  Capturing a state that needs an interaction (start a game, open a menu, dismiss a modal)?
35614
- Use --action to run JS in the page before the shot \u2014 it fires after the post-load
35615
- delay, then settles again so the result has painted. Do NOT hand-roll a 'page eval'
35616
- that returns a base64 image: the eval result is capped (~16KB) and truncates the PNG.
35967
+ Use --action to run JS in the page before the shot \u2014 it fires after the wait
35968
+ (and after any --wait-for gate), then settles again so the result has painted. Do
35969
+ NOT hand-roll a 'page eval' that returns a base64 image: the eval result is capped
35970
+ (~16KB) and truncates the PNG.
35617
35971
 
35618
35972
  Capturing an off-screen region or reading element data?
35619
35973
  \u2022 --full captures the ENTIRE scrollable page (then crop to the region).
@@ -35638,7 +35992,7 @@ function shortUrl(url, truncate = true, maxLen = 100) {
35638
35992
  const tailLen = Math.floor(keep / 2);
35639
35993
  return result.slice(0, headLen) + "\u2026" + result.slice(-tailLen);
35640
35994
  }
35641
- var pageInspectCommand = new Command("inspect").description("Inspect a web page (console, failed resources, timing, layout overflow). ONE passive load - to verify realtime/presence across concurrent clients use `page test --observe`").argument("<url>", "URL to inspect").option("--wait <ms>", "Sleep this many ms after DOMContentLoaded before capturing (lets late async/LCP work settle; max 30000)", "500").option("--wait-for <selector>", "Wait until this CSS selector appears before capturing (deterministic; replaces --wait)").option("--wait-timeout <ms>", "Max ms to wait for --wait-for before giving up", "5000").option("--json", "Output as JSON").option("--no-reprobe", "Skip the automatic second probe that filters one-time transient console errors - roughly halves inspect time on a page with any console error, at the cost of possibly reporting a cold-load transient as real").option("--no-truncate", "Show full URLs instead of truncating long ones with middle-ellipsis").option("--all", "Include render-blocking, large resources, oversized images, overflow culprits, and LCP detail").option("--fake-media", "Grant a synthetic microphone + camera and auto-accept the getUserMedia prompt, so voice/camera apps run headlessly (audio is a built-in tone, not real speech)").option("--device <name>", `Inspect as a real touch device: ${TOUCH_DEVICES.join(", ")}. Emulates touch events, mobile user-agent and DPR, so a touch-gated mobile layout is what gets inspected.`).option("--auth", "Load the page signed in as you (your Gipity account), so pages behind a Sign-in-with-Gipity login are reachable. Only works for apps using Sign in with Gipity, hosted on *.gipity.ai.").addOption(new Option("--screenshot [path]", "Capture a screenshot").hideHelp()).action((url, opts) => {
35995
+ var pageInspectCommand = new Command("inspect").description("Inspect a web page (console, failed resources, timing, layout overflow). ONE passive load - to verify realtime/presence across concurrent clients use `page test --observe`").argument("<url>", "URL to inspect").option("--wait <ms>", "Sleep this many ms after DOMContentLoaded before capturing (lets late async/LCP work settle; max 30000)", "500").option("--wait-for <selector>", "Wait until this CSS selector appears before capturing (deterministic; replaces --wait)").option("--wait-timeout <ms>", "Max ms to wait for --wait-for before giving up", "5000").option("--json", "Output as JSON").option("--no-reprobe", "Skip the automatic second probe that filters one-time transient console errors - roughly halves inspect time on a page with any console error, at the cost of possibly reporting a cold-load transient as real").option("--no-truncate", "Show full URLs instead of truncating long ones with middle-ellipsis").option("--all", "Include render-blocking, large resources, oversized images, overflow culprits, and LCP detail").option("--no-fake-media", "Inspect with NO camera or microphone present, so a getUserMedia app takes its no-device error path (use this only when that fallback IS what you're inspecting)").option("--device <name>", `Inspect as a real touch device: ${TOUCH_DEVICES.join(", ")}. Emulates touch events, mobile user-agent and DPR, so a touch-gated mobile layout is what gets inspected.`).option("--auth", "Load the page signed in as you (your Gipity account), so pages behind a Sign-in-with-Gipity login are reachable. Only works for apps using Sign in with Gipity, hosted on *.gipity.ai.").addOption(new Option("--screenshot [path]", "Capture a screenshot").hideHelp()).action((url, opts) => {
35642
35996
  if (opts.screenshot !== void 0) {
35643
35997
  console.error(error("page inspect does not capture screenshots. Use page screenshot:"));
35644
35998
  console.error(` gipity page screenshot ${url}${typeof opts.screenshot === "string" ? ` -o ${opts.screenshot}` : ""}`);
@@ -35657,7 +36011,7 @@ async function inspectPage(url, opts = {}) {
35657
36011
  waitMs,
35658
36012
  waitForSelector: opts.waitFor || void 0,
35659
36013
  waitForTimeoutMs: opts.waitFor ? waitForTimeoutMs : void 0,
35660
- fakeMedia: opts.fakeMedia || void 0,
36014
+ fakeMedia: opts.fakeMedia !== false ? true : void 0,
35661
36015
  device: opts.device ? resolveTouchDevice(opts.device) : void 0,
35662
36016
  auth: opts.auth || void 0
35663
36017
  };
@@ -35818,11 +36172,14 @@ function statusIcon(status) {
35818
36172
  if (status === "warning") return warning("\u26A0");
35819
36173
  return muted("\u2192");
35820
36174
  }
35821
- var deployCommand = new Command("deploy").description("Deploy to dev or prod").argument("[target]", "dev or prod", "dev").option("--source-dir <dir>", "Source directory to deploy from").option("--only <phases>", "Run only specific phases (comma-separated)").option("--force", "Re-run all phases (ignore checksums) and bypass the sync bulk-deletion guard").option("--no-sync", "Skip sync-up before deploy").option("--optimize", "Force Vite build optimization on (default for prod; use this to optimize a dev deploy too)").option("--no-optimize", 'Skip build optimization and upload files as-is - the escape hatch for plain-HTML apps whose <script src> tags are not type="module"').option("--json", "Output as JSON").option("--inspect [path]", "After a successful deploy, run `page inspect` on the deployed URL (or URL + path) in the same command - one build-loop step instead of two. With --json, emits two JSON lines: deploy, then inspect.").action((target, opts) => run("Deploy", async () => {
36175
+ var deployCommand = new Command("deploy").description("Deploy to dev or prod").argument("[target]", "dev or prod", "dev").option("--source-dir <dir>", "Source directory to deploy from").option("--only <phases>", "Run only specific phases (comma-separated)").option("--force", "Re-run all phases (ignore checksums) and bypass the sync bulk-deletion guard").option("--no-sync", "Skip sync-up before deploy").option("--optimize", "Force Vite build optimization on (default for prod; use this to optimize a dev deploy too)").option("--no-optimize", 'Skip build optimization and upload files as-is - the escape hatch for plain-HTML apps whose <script src> tags are not type="module"').option("--json", "Output as JSON").option("--inspect [path]", "After a successful deploy, run `page inspect` on the deployed URL (or URL + path) in the same command - one build-loop step instead of two. With --json, emits two JSON lines: deploy, then inspect.").option("--wait <ms>", "With --inspect: sleep this many ms before capturing (max 30000)").option("--wait-for <selector>", "With --inspect: wait until this CSS selector appears before capturing (deterministic; the app's own ready signal)").option("--wait-timeout <ms>", "With --inspect: max ms to wait for --wait-for before giving up", "5000").action((target, opts) => run("Deploy", async () => {
35822
36176
  if (target !== "dev" && target !== "prod") {
35823
36177
  console.error(error('Target must be "dev" or "prod"'));
35824
36178
  process.exit(1);
35825
36179
  }
36180
+ if (!opts.inspect && (opts.waitFor || deployCommand.getOptionValueSource("wait") !== void 0)) {
36181
+ opts.inspect = true;
36182
+ }
35826
36183
  const config = requireConfig();
35827
36184
  await syncBeforeAction(opts);
35828
36185
  const doDeploy = () => post(`/projects/${config.projectGuid}/deploy`, {
@@ -35843,7 +36200,12 @@ var deployCommand = new Command("deploy").description("Deploy to dev or prod").a
35843
36200
  const url = typeof opts.inspect === "string" ? new URL(opts.inspect, d.url).toString() : d.url;
35844
36201
  if (!opts.json) console.log("");
35845
36202
  try {
35846
- await inspectPage(url, { json: opts.json });
36203
+ await inspectPage(url, {
36204
+ json: opts.json,
36205
+ wait: opts.wait,
36206
+ waitFor: opts.waitFor,
36207
+ waitTimeout: opts.waitTimeout
36208
+ });
35847
36209
  } catch (err) {
35848
36210
  console.error(warning(`Inspect failed (deploy itself succeeded): ${err?.message ?? err}`));
35849
36211
  }
@@ -36066,8 +36428,8 @@ memoryCommand.command("delete <topic>").description("Delete a topic").option("--
36066
36428
  // src/commands/sandbox.ts
36067
36429
  init_api();
36068
36430
  init_config();
36069
- import { readFileSync as readFileSync13, existsSync as existsSync10, statSync as statSync6 } from "fs";
36070
- import { dirname as dirname8, extname as extname4, relative as relative3, resolve as resolve11 } from "path";
36431
+ import { readFileSync as readFileSync13, existsSync as existsSync11, statSync as statSync6 } from "fs";
36432
+ import { dirname as dirname8, extname as extname4, relative as relative4, resolve as resolve12 } from "path";
36071
36433
  init_colors();
36072
36434
  var LANG_MAP = {
36073
36435
  js: "javascript",
@@ -36180,7 +36542,7 @@ function resolveRelativeCwd() {
36180
36542
  const configPath = getConfigPath();
36181
36543
  if (!configPath) return void 0;
36182
36544
  const projectRoot = dirname8(configPath);
36183
- const rel2 = relative3(projectRoot, process.cwd());
36545
+ const rel2 = relative4(projectRoot, process.cwd());
36184
36546
  if (!rel2 || rel2.startsWith("..")) return void 0;
36185
36547
  return rel2.split(/[\\/]/).filter(Boolean).join("/");
36186
36548
  }
@@ -36239,7 +36601,7 @@ GCC/Rust).
36239
36601
  if (args.length >= 2 && INTERPRETERS[args[0].toLowerCase()] !== void 0) {
36240
36602
  langFromInterp = INTERPRETERS[args[0].toLowerCase()];
36241
36603
  const rest = args.slice(1).join(" ");
36242
- if (existsSync10(rest) && statSync6(rest).isFile()) filePath = rest;
36604
+ if (existsSync11(rest) && statSync6(rest).isFile()) filePath = rest;
36243
36605
  else inlineCode = rest;
36244
36606
  } else if (args.length === 1) {
36245
36607
  inlineCode = args[0];
@@ -36319,7 +36681,7 @@ GCC/Rust).
36319
36681
  }
36320
36682
  if (res.data.outputFiles && res.data.outputFiles.length > 0) {
36321
36683
  const projectRoot = getProjectRoot();
36322
- const landed = (f) => pulledLocal && !!projectRoot && existsSync10(resolve11(projectRoot, f));
36684
+ const landed = (f) => pulledLocal && !!projectRoot && existsSync11(resolve12(projectRoot, f));
36323
36685
  const onDisk = res.data.outputFiles.filter(landed);
36324
36686
  const notOnDisk = res.data.outputFiles.filter((f) => !landed(f));
36325
36687
  if (onDisk.length > 0) {
@@ -36436,7 +36798,7 @@ chatCommand.command("delete <guid>").description("Delete a chat").option("--json
36436
36798
  // src/commands/project.ts
36437
36799
  init_api();
36438
36800
  init_config();
36439
- import { join as join12 } from "path";
36801
+ import { join as join13 } from "path";
36440
36802
  import { mkdirSync as mkdirSync8 } from "fs";
36441
36803
  init_colors();
36442
36804
  init_utils();
@@ -36494,7 +36856,7 @@ projectCommand.command("create <name>").description("Create a project").option("
36494
36856
  }
36495
36857
  const res = await post("/projects", body);
36496
36858
  const project = res.data;
36497
- const dir = join12(getProjectsRoot(), project.slug);
36859
+ const dir = join13(getProjectsRoot(), project.slug);
36498
36860
  mkdirSync8(dir, { recursive: true });
36499
36861
  const accountSlug = await getAccountSlug();
36500
36862
  let agentGuid = "";
@@ -36750,7 +37112,7 @@ function printStepRuns(steps, emptyNote) {
36750
37112
  }
36751
37113
  }
36752
37114
  var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
36753
- var sleep2 = (ms2) => new Promise((resolve18) => setTimeout(resolve18, ms2));
37115
+ var sleep2 = (ms2) => new Promise((resolve19) => setTimeout(resolve19, ms2));
36754
37116
  async function waitForRun(wfGuid, prevGuid, timeoutSec) {
36755
37117
  const deadline = Date.now() + timeoutSec * 1e3;
36756
37118
  let runGuid;
@@ -37189,9 +37551,9 @@ init_config();
37189
37551
  init_utils();
37190
37552
  init_colors();
37191
37553
  var fileCommand = new Command("file").description("Browse remote files");
37192
- fileCommand.command("ls [path]").description("List files").option("--json", "Output as JSON").action((path4, opts) => run("List", async () => {
37554
+ fileCommand.command("ls [path]").description("List files").option("--json", "Output as JSON").action((path5, opts) => run("List", async () => {
37193
37555
  const { config } = await resolveProjectContext();
37194
- const query = path4 ? `?path=${encodeURIComponent(path4)}` : "";
37556
+ const query = path5 ? `?path=${encodeURIComponent(path5)}` : "";
37195
37557
  const res = await get(`/projects/${config.projectGuid}/files${query}`);
37196
37558
  printList(res.data, opts, "(empty)", (f) => {
37197
37559
  const size = f.type === "directory" ? "<DIR>" : formatSize(Number(f.size));
@@ -37199,10 +37561,10 @@ fileCommand.command("ls [path]").description("List files").option("--json", "Out
37199
37561
  return `${muted(size.padStart(10))} ${name}`;
37200
37562
  });
37201
37563
  }));
37202
- fileCommand.command("cat <path>").description("Read a file").option("--json", "Output as JSON").action((path4, opts) => run("Read", async () => {
37564
+ fileCommand.command("cat <path>").description("Read a file").option("--json", "Output as JSON").action((path5, opts) => run("Read", async () => {
37203
37565
  const { config } = await resolveProjectContext();
37204
37566
  const res = await get(
37205
- `/projects/${config.projectGuid}/files/read?path=${encodeURIComponent(path4)}`
37567
+ `/projects/${config.projectGuid}/files/read?path=${encodeURIComponent(path5)}`
37206
37568
  );
37207
37569
  if (opts.json) {
37208
37570
  console.log(JSON.stringify(res.data));
@@ -37210,9 +37572,9 @@ fileCommand.command("cat <path>").description("Read a file").option("--json", "O
37210
37572
  process.stdout.write(res.data.content);
37211
37573
  }
37212
37574
  }));
37213
- fileCommand.command("tree [path]").description("Show the file tree").option("--json", "Output as JSON").action((path4, opts) => run("Tree", async () => {
37575
+ fileCommand.command("tree [path]").description("Show the file tree").option("--json", "Output as JSON").action((path5, opts) => run("Tree", async () => {
37214
37576
  const { config } = await resolveProjectContext();
37215
- const query = path4 ? `?path=${encodeURIComponent(path4)}` : "";
37577
+ const query = path5 ? `?path=${encodeURIComponent(path5)}` : "";
37216
37578
  const res = await get(
37217
37579
  `/projects/${config.projectGuid}/files/tree${query}`
37218
37580
  );
@@ -37221,16 +37583,16 @@ fileCommand.command("tree [path]").description("Show the file tree").option("--j
37221
37583
  return `${f.path}${size}`;
37222
37584
  });
37223
37585
  }));
37224
- fileCommand.command("rm <path>").description("Delete a file or folder").option("--json", "Output as JSON").action((path4, opts) => run("Delete", async () => {
37586
+ fileCommand.command("rm <path>").description("Delete a file or folder").option("--json", "Output as JSON").action((path5, opts) => run("Delete", async () => {
37225
37587
  const { config } = await resolveProjectContext();
37226
37588
  const res = await del(
37227
- `/projects/${config.projectGuid}/files?path=${encodeURIComponent(path4)}`
37589
+ `/projects/${config.projectGuid}/files?path=${encodeURIComponent(path5)}`
37228
37590
  );
37229
- printResult(`Deleted: ${path4}`, opts, res);
37591
+ printResult(`Deleted: ${path5}`, opts, res);
37230
37592
  }));
37231
- fileCommand.command("versions <path>").description("List file versions").option("--limit <n>", "Max versions to return", "20").option("--json", "Output as JSON").action((path4, opts) => run("Versions", async () => {
37593
+ fileCommand.command("versions <path>").description("List file versions").option("--limit <n>", "Max versions to return", "20").option("--json", "Output as JSON").action((path5, opts) => run("Versions", async () => {
37232
37594
  const { config } = await resolveProjectContext();
37233
- const query = `?path=${encodeURIComponent(path4)}&limit=${opts.limit}`;
37595
+ const query = `?path=${encodeURIComponent(path5)}&limit=${opts.limit}`;
37234
37596
  const res = await get(
37235
37597
  `/projects/${config.projectGuid}/files/versions${query}`
37236
37598
  );
@@ -37240,11 +37602,11 @@ fileCommand.command("versions <path>").description("List file versions").option(
37240
37602
  return `${info(`v${v7.version}`)} ${muted(date)} ${formatSize(v7.size).padStart(8)} [${v7.source}]${marker}`;
37241
37603
  });
37242
37604
  }));
37243
- fileCommand.command("restore <path> <version>").description("Restore a file to a version").option("--json", "Output as JSON").action((path4, version, opts) => run("Restore", async () => {
37605
+ fileCommand.command("restore <path> <version>").description("Restore a file to a version").option("--json", "Output as JSON").action((path5, version, opts) => run("Restore", async () => {
37244
37606
  const { config } = await resolveProjectContext();
37245
37607
  const res = await post(
37246
37608
  `/projects/${config.projectGuid}/files/version-restore`,
37247
- { path: path4, version: Number(version) }
37609
+ { path: path5, version: Number(version) }
37248
37610
  );
37249
37611
  printResult(`Restored ${res.data.path} to v${res.data.version} (${formatSize(res.data.size)}).`, opts, res.data);
37250
37612
  }));
@@ -37371,8 +37733,8 @@ storageCommand.command("retention").description("View or adjust your file versio
37371
37733
  init_platform();
37372
37734
  init_auth();
37373
37735
  init_api();
37374
- import { join as join15, dirname as dirname10, resolve as resolve13, basename as basename4, sep as sep4 } from "path";
37375
- import { existsSync as existsSync12, mkdirSync as mkdirSync10, readFileSync as readFileSync16, writeFileSync as writeFileSync10, renameSync as renameSync2, readdirSync as readdirSync6, statSync as statSync7 } from "fs";
37736
+ import { join as join16, dirname as dirname10, resolve as resolve14, basename as basename4, sep as sep4 } from "path";
37737
+ import { existsSync as existsSync13, mkdirSync as mkdirSync10, readFileSync as readFileSync16, writeFileSync as writeFileSync10, renameSync as renameSync2, readdirSync as readdirSync7, statSync as statSync7 } from "fs";
37376
37738
  import { homedir as homedir11 } from "os";
37377
37739
  import { fileURLToPath as fileURLToPath2 } from "url";
37378
37740
 
@@ -37523,7 +37885,7 @@ import { readFileSync as readFileSync15 } from "node:fs";
37523
37885
 
37524
37886
  // src/relay/installers.ts
37525
37887
  import { homedir as homedir8, platform as osPlatform } from "os";
37526
- import { join as join13 } from "path";
37888
+ import { join as join14 } from "path";
37527
37889
  var UnsupportedPlatformError = class extends Error {
37528
37890
  constructor(plat) {
37529
37891
  super(`gipity relay install does not support ${plat}`);
@@ -37541,8 +37903,8 @@ function display(argvs) {
37541
37903
  }
37542
37904
  function launchdPlan(cliPath) {
37543
37905
  const label2 = "ai.gipity.relay";
37544
- const path4 = join13(homedir8(), "Library", "LaunchAgents", `${label2}.plist`);
37545
- const logDir = join13(homedir8(), "Library", "Logs", "Gipity");
37906
+ const path5 = join14(homedir8(), "Library", "LaunchAgents", `${label2}.plist`);
37907
+ const logDir = join14(homedir8(), "Library", "Logs", "Gipity");
37546
37908
  const uid = process.getuid?.() ?? 0;
37547
37909
  const target = `gui/${uid}`;
37548
37910
  const content = `<?xml version="1.0" encoding="UTF-8"?>
@@ -37566,18 +37928,18 @@ function launchdPlan(cliPath) {
37566
37928
  <dict>
37567
37929
  <key>SuccessfulExit</key><false/>
37568
37930
  </dict>
37569
- <key>StandardOutPath</key><string>${join13(logDir, "relay.out.log")}</string>
37570
- <key>StandardErrorPath</key><string>${join13(logDir, "relay.err.log")}</string>
37931
+ <key>StandardOutPath</key><string>${join14(logDir, "relay.out.log")}</string>
37932
+ <key>StandardErrorPath</key><string>${join14(logDir, "relay.err.log")}</string>
37571
37933
  <key>ProcessType</key><string>Background</string>
37572
37934
  </dict>
37573
37935
  </plist>
37574
37936
  `;
37575
- const enableCmds = [["launchctl", "bootstrap", target, path4]];
37576
- const disableCmds = [["launchctl", "bootout", target, path4]];
37937
+ const enableCmds = [["launchctl", "bootstrap", target, path5]];
37938
+ const disableCmds = [["launchctl", "bootout", target, path5]];
37577
37939
  const statusCmd = ["launchctl", "print", `${target}/${label2}`];
37578
37940
  return {
37579
37941
  platform: "darwin",
37580
- path: path4,
37942
+ path: path5,
37581
37943
  content,
37582
37944
  enableCmds,
37583
37945
  disableCmds,
@@ -37590,7 +37952,7 @@ function launchdPlan(cliPath) {
37590
37952
  }
37591
37953
  function systemdUserPlan(cliPath) {
37592
37954
  const unitName = "gipity-relay.service";
37593
- const path4 = join13(homedir8(), ".config", "systemd", "user", unitName);
37955
+ const path5 = join14(homedir8(), ".config", "systemd", "user", unitName);
37594
37956
  const content = `[Unit]
37595
37957
  Description=Gipity relay - local Claude Code control from the Gipity web CLI
37596
37958
  After=network-online.target
@@ -37615,7 +37977,7 @@ WantedBy=default.target
37615
37977
  const statusCmd = ["systemctl", "--user", "status", unitName];
37616
37978
  return {
37617
37979
  platform: "linux",
37618
- path: path4,
37980
+ path: path5,
37619
37981
  content,
37620
37982
  enableCmds,
37621
37983
  disableCmds,
@@ -37628,7 +37990,7 @@ WantedBy=default.target
37628
37990
  }
37629
37991
  function windowsTaskPlan(cliPath) {
37630
37992
  const taskName = "GipityRelay";
37631
- const path4 = join13(homedir8(), "AppData", "Local", "Gipity", "relay-task.xml");
37993
+ const path5 = join14(homedir8(), "AppData", "Local", "Gipity", "relay-task.xml");
37632
37994
  const runsViaNode = /\.[cm]?js$/i.test(cliPath);
37633
37995
  const command = runsViaNode ? process.execPath : cliPath;
37634
37996
  const argLine = runsViaNode ? `"${cliPath}" relay run` : "relay run";
@@ -37664,7 +38026,7 @@ function windowsTaskPlan(cliPath) {
37664
38026
  </Task>
37665
38027
  `;
37666
38028
  const enableCmds = [
37667
- ["schtasks", "/Create", "/TN", taskName, "/XML", path4, "/F"],
38029
+ ["schtasks", "/Create", "/TN", taskName, "/XML", path5, "/F"],
37668
38030
  ["schtasks", "/Run", "/TN", taskName]
37669
38031
  ];
37670
38032
  const disableCmds = [
@@ -37674,7 +38036,7 @@ function windowsTaskPlan(cliPath) {
37674
38036
  const statusCmd = ["schtasks", "/Query", "/TN", taskName, "/V", "/FO", "LIST"];
37675
38037
  return {
37676
38038
  platform: "win32",
37677
- path: path4,
38039
+ path: path5,
37678
38040
  content,
37679
38041
  enableCmds,
37680
38042
  disableCmds,
@@ -37690,7 +38052,7 @@ function windowsTaskPlan(cliPath) {
37690
38052
  init_platform();
37691
38053
  init_api();
37692
38054
  import { hostname as hostname3, userInfo, platform as osPlatform2 } from "os";
37693
- import { resolve as resolve12, dirname as dirname9 } from "path";
38055
+ import { resolve as resolve13, dirname as dirname9 } from "path";
37694
38056
  import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
37695
38057
 
37696
38058
  // src/relay/machine-id.ts
@@ -37703,9 +38065,9 @@ function rawMachineId() {
37703
38065
  if (override) return `override:${override}`;
37704
38066
  try {
37705
38067
  if (platform() === "linux") {
37706
- for (const path4 of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
38068
+ for (const path5 of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
37707
38069
  try {
37708
- const id2 = readFileSync14(path4, "utf-8").trim();
38070
+ const id2 = readFileSync14(path5, "utf-8").trim();
37709
38071
  if (id2) return `linux:${id2}`;
37710
38072
  } catch {
37711
38073
  }
@@ -37752,7 +38114,7 @@ function friendlyDeviceName() {
37752
38114
  return host || `My ${kind}`;
37753
38115
  }
37754
38116
  function resolveCliPath() {
37755
- return resolve12(process.argv[1] ?? "gipity");
38117
+ return resolve13(process.argv[1] ?? "gipity");
37756
38118
  }
37757
38119
  async function pairDevice(opts = {}) {
37758
38120
  const existing = getDevice();
@@ -38204,9 +38566,9 @@ function printFull(opts, outerW) {
38204
38566
 
38205
38567
  // src/claude-setup.ts
38206
38568
  import { execSync as execSync2 } from "child_process";
38207
- import { existsSync as existsSync11 } from "fs";
38569
+ import { existsSync as existsSync12 } from "fs";
38208
38570
  import { homedir as homedir10 } from "os";
38209
- import { join as join14 } from "path";
38571
+ import { join as join15 } from "path";
38210
38572
  var CLAUDE_PACKAGE = "@anthropic-ai/claude-code";
38211
38573
  function claudeInstallPlan(platformOverride) {
38212
38574
  const plat = platformOverride ?? process.platform;
@@ -38225,7 +38587,7 @@ function isClaudeInstalled(platformOverride) {
38225
38587
  }
38226
38588
  function isClaudeAuthenticated() {
38227
38589
  if (process.env.CLAUDE_CODE_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY) return true;
38228
- return existsSync11(join14(homedir10(), ".claude", ".credentials.json"));
38590
+ return existsSync12(join15(homedir10(), ".claude", ".credentials.json"));
38229
38591
  }
38230
38592
  function probeClaudeAuthenticated() {
38231
38593
  if (!isClaudeInstalled()) return false;
@@ -38255,8 +38617,8 @@ function ensureClaudeInstalled(opts = {}) {
38255
38617
  var __clDir = dirname10(fileURLToPath2(import.meta.url));
38256
38618
  function readOwnPkg(fromDir) {
38257
38619
  for (let d = fromDir; ; d = dirname10(d)) {
38258
- const p = join15(d, "package.json");
38259
- if (existsSync12(p)) {
38620
+ const p = join16(d, "package.json");
38621
+ if (existsSync13(p)) {
38260
38622
  try {
38261
38623
  const pkg2 = JSON.parse(readFileSync16(p, "utf-8"));
38262
38624
  if (pkg2.name === "gipity") return pkg2;
@@ -38307,12 +38669,12 @@ function localFsFallback(dir) {
38307
38669
  const topLevelEntries = [];
38308
38670
  const walk = (d, depth) => {
38309
38671
  try {
38310
- for (const name of readdirSync6(d).sort()) {
38672
+ for (const name of readdirSync7(d).sort()) {
38311
38673
  if (isSyncIgnored(name)) continue;
38312
38674
  let isDir = false;
38313
38675
  let size = 0;
38314
38676
  try {
38315
- const st2 = statSync7(join15(d, name));
38677
+ const st2 = statSync7(join16(d, name));
38316
38678
  isDir = st2.isDirectory();
38317
38679
  size = st2.isFile() ? st2.size : 0;
38318
38680
  } catch {
@@ -38321,7 +38683,7 @@ function localFsFallback(dir) {
38321
38683
  if (isDir) {
38322
38684
  folderCount++;
38323
38685
  if (depth === 0) topLevelEntries.push(`${name}/`);
38324
- walk(join15(d, name), depth + 1);
38686
+ walk(join16(d, name), depth + 1);
38325
38687
  } else {
38326
38688
  fileCount++;
38327
38689
  totalBytes += size;
@@ -38354,8 +38716,8 @@ function hasStreamJsonFlag(args) {
38354
38716
  }
38355
38717
  function markFolderTrusted(dir) {
38356
38718
  try {
38357
- const file = join15(homedir11(), ".claude.json");
38358
- const cfg = existsSync12(file) ? JSON.parse(readFileSync16(file, "utf-8")) : {};
38719
+ const file = join16(homedir11(), ".claude.json");
38720
+ const cfg = existsSync13(file) ? JSON.parse(readFileSync16(file, "utf-8")) : {};
38359
38721
  if (typeof cfg !== "object" || cfg === null) return;
38360
38722
  if (typeof cfg.projects !== "object" || cfg.projects === null) cfg.projects = {};
38361
38723
  const entry = typeof cfg.projects[dir] === "object" && cfg.projects[dir] !== null ? cfg.projects[dir] : {};
@@ -38468,7 +38830,7 @@ var claudeCommand = new Command("claude").description("Set up and run Claude Cod
38468
38830
  let existing = getConfig();
38469
38831
  let forceAdoptCwd = false;
38470
38832
  if (existing && !opts.newProject && !opts.project && !nonInteractive) {
38471
- const cwdHasConfig = existsSync12(resolve13(process.cwd(), ".gipity.json"));
38833
+ const cwdHasConfig = existsSync13(resolve14(process.cwd(), ".gipity.json"));
38472
38834
  if (!cwdHasConfig && isLikelyEmpty(process.cwd())) {
38473
38835
  const ancestorRoot = dirname10(getConfigPath());
38474
38836
  console.log(` ${bold("You are inside an existing Gipity project.")}
@@ -38517,7 +38879,7 @@ var claudeCommand = new Command("claude").description("Set up and run Claude Cod
38517
38879
  }
38518
38880
  project = found;
38519
38881
  }
38520
- const projectDir2 = opts.here ? process.cwd() : join15(getProjectsRoot(), project.slug);
38882
+ const projectDir2 = opts.here ? process.cwd() : join16(getProjectsRoot(), project.slug);
38521
38883
  mkdirSync10(projectDir2, { recursive: true });
38522
38884
  process.chdir(projectDir2);
38523
38885
  clearConfigCache();
@@ -38646,7 +39008,7 @@ var claudeCommand = new Command("claude").description("Set up and run Claude Cod
38646
39008
  } else {
38647
39009
  project = result.project;
38648
39010
  isNewProject = result.kind === "create-new";
38649
- const projectDir2 = join15(getProjectsRoot(), project.slug);
39011
+ const projectDir2 = join16(getProjectsRoot(), project.slug);
38650
39012
  mkdirSync10(projectDir2, { recursive: true });
38651
39013
  process.chdir(projectDir2);
38652
39014
  clearConfigCache();
@@ -39813,8 +40175,8 @@ Examples:
39813
40175
  init_colors();
39814
40176
  init_utils();
39815
40177
  import { createHash as createHash5 } from "crypto";
39816
- function expectedType(path4) {
39817
- const p = path4.toLowerCase().split("?")[0].split("#")[0];
40178
+ function expectedType(path5) {
40179
+ const p = path5.toLowerCase().split("?")[0].split("#")[0];
39818
40180
  if (p.endsWith(".json")) return "json";
39819
40181
  if (p.endsWith(".xml")) return "xml";
39820
40182
  if (p.endsWith(".html") || p.endsWith(".htm")) return "html";
@@ -39845,8 +40207,8 @@ function sha256(s) {
39845
40207
  function baseWithSlash(url) {
39846
40208
  return url.endsWith("/") ? url : url + "/";
39847
40209
  }
39848
- function resolveUrl(base, path4) {
39849
- return new URL(path4.replace(/^\/+/, ""), baseWithSlash(base)).toString();
40210
+ function resolveUrl(base, path5) {
40211
+ return new URL(path5.replace(/^\/+/, ""), baseWithSlash(base)).toString();
39850
40212
  }
39851
40213
  async function fetchRaw(url) {
39852
40214
  try {
@@ -39868,12 +40230,12 @@ async function probeShell(base) {
39868
40230
  isHtml: looksLikeHtml(r.body)
39869
40231
  };
39870
40232
  }
39871
- function classify(path4, r, shell) {
40233
+ function classify(path5, r, shell) {
39872
40234
  if (!r) return { status: null, contentType: null, bytes: 0, verdict: "MISSING", detail: "fetch failed (host unreachable)" };
39873
40235
  const bytes = Buffer.byteLength(r.body);
39874
40236
  const base = { status: r.status, contentType: r.contentType, bytes };
39875
40237
  if (r.status >= 400) return { ...base, verdict: "MISSING", detail: `HTTP ${r.status}` };
39876
- const expect = expectedType(path4);
40238
+ const expect = expectedType(path5);
39877
40239
  if (shell.served && shell.sha256 && sha256(r.body) === shell.sha256) {
39878
40240
  return { ...base, verdict: "MISSING", detail: `HTTP ${r.status} but served the SPA shell (file not deployed)` };
39879
40241
  }
@@ -40073,8 +40435,8 @@ fnCommand.command("call <name> [body]").description("Call a function").option("-
40073
40435
  const config = requireConfig();
40074
40436
  const raw = bodyArg || opts.data || "{}";
40075
40437
  const body = JSON.parse(raw);
40076
- const path4 = `/api/${config.projectGuid}/fn/${encodeURIComponent(name)}`;
40077
- const res = opts.anon ? await callAnon(config.projectGuid, name, body) : await post(path4, body);
40438
+ const path5 = `/api/${config.projectGuid}/fn/${encodeURIComponent(name)}`;
40439
+ const res = opts.anon ? await callAnon(config.projectGuid, name, body) : await post(path5, body);
40078
40440
  if (opts.field) {
40079
40441
  emitField(res.data, opts.field);
40080
40442
  return;
@@ -40127,8 +40489,8 @@ serviceCommand.command("list").description("List callable app services").action(
40127
40489
  }));
40128
40490
  serviceCommand.command("call <name> [body]").description("Call an app service by name (e.g. llm, image, music). Uses your logged-in session - no token wrangling.").option("--data <json>", "JSON request body (alternative to the positional [body])").option("--get", "Issue a GET (for listing endpoints like llm/models, tts/voices)").option("--json", "Output compact JSON").action((name, bodyArg, opts) => run("Call", async () => {
40129
40491
  const config = requireConfig();
40130
- const path4 = name.split("/").map(encodeURIComponent).join("/");
40131
- const url = `/api/${config.projectGuid}/services/${path4}`;
40492
+ const path5 = name.split("/").map(encodeURIComponent).join("/");
40493
+ const url = `/api/${config.projectGuid}/services/${path5}`;
40132
40494
  const res = opts.get ? await get(url) : await post(url, JSON.parse(bodyArg || opts.data || "{}"));
40133
40495
  console.log(opts.json ? JSON.stringify(res) : JSON.stringify(res, null, 2));
40134
40496
  }));
@@ -40557,11 +40919,11 @@ jobCommand.command("cancel <runGuid>").description("Cancel a queued or running j
40557
40919
  }
40558
40920
  }));
40559
40921
  jobCommand.command("run-local <name>").description("Run the job in a local Docker container against your local filesystem (no platform round-trip)").option("--image <image>", "Docker image to run inside", "easyclaw/sandbox:latest").option("--no-deps", "Skip pip/npm install even if a deps file is present").action(async (name, opts) => {
40560
- const { existsSync: existsSync22, statSync: statSync10 } = await import("node:fs");
40561
- const { resolve: resolve18, join: join24 } = await import("node:path");
40922
+ const { existsSync: existsSync23, statSync: statSync10 } = await import("node:fs");
40923
+ const { resolve: resolve19, join: join25 } = await import("node:path");
40562
40924
  const { spawnCommand: spawnCommand2, spawnSyncCommand: spawnSyncCommand2 } = await Promise.resolve().then(() => (init_platform(), platform_exports));
40563
- const jobDir = resolve18(process.cwd(), "jobs", name);
40564
- if (!existsSync22(jobDir) || !statSync10(jobDir).isDirectory()) {
40925
+ const jobDir = resolve19(process.cwd(), "jobs", name);
40926
+ if (!existsSync23(jobDir) || !statSync10(jobDir).isDirectory()) {
40565
40927
  console.error(error(`jobs/${name}/ not found in current directory`));
40566
40928
  process.exit(1);
40567
40929
  }
@@ -40582,7 +40944,7 @@ jobCommand.command("run-local <name>").description("Run the job in a local Docke
40582
40944
  },
40583
40945
  { file: "main.sh", runtime: "bash", entry: "bash" }
40584
40946
  ];
40585
- const picked = variants.find((v7) => existsSync22(join24(jobDir, v7.file)));
40947
+ const picked = variants.find((v7) => existsSync23(join25(jobDir, v7.file)));
40586
40948
  if (!picked) {
40587
40949
  console.error(error(`No main.py / main.js / main.sh found under jobs/${name}/`));
40588
40950
  process.exit(1);
@@ -40607,7 +40969,7 @@ jobCommand.command("run-local <name>").description("Run the job in a local Docke
40607
40969
  "-v",
40608
40970
  `${jobDir}:/work`
40609
40971
  ];
40610
- const needsDeps = opts.deps !== false && picked.depsFile && picked.installCmd && existsSync22(join24(jobDir, picked.depsFile));
40972
+ const needsDeps = opts.deps !== false && picked.depsFile && picked.installCmd && existsSync23(join25(jobDir, picked.depsFile));
40611
40973
  const handlerCmd = `${picked.entry} /work/${picked.file}`;
40612
40974
  let shellCmd;
40613
40975
  if (needsDeps && picked.installCmd) {
@@ -40783,30 +41145,59 @@ init_api();
40783
41145
  init_config();
40784
41146
  init_colors();
40785
41147
  import { mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "fs";
40786
- import { resolve as resolvePath2, dirname as dirname11, relative as relative4, isAbsolute } from "path";
41148
+ import { resolve as resolvePath2, dirname as dirname11, relative as relative5, isAbsolute } from "path";
40787
41149
 
40788
41150
  // src/provider-docs.ts
40789
41151
  var IMAGE_GEMINI_ASPECT_RATIOS = `1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 4:5, 5:4, 21:9`;
40790
41152
  var IMAGE_GEMINI_SIZES = `512, 1K, 2K, 4K`;
40791
- var IMAGE_MODELS_DOC = `openai: gpt-image-2, gpt-image-1.5. bfl: flux-2-pro, flux-2-flex, flux-2-max, flux-dev. gemini: gemini-2.5-flash-image, gemini-3.1-flash-image, gemini-3-pro-image`;
41153
+ var IMAGE_MODELS_DOC = `openai: gpt-image-2. bfl: flux-2-pro, flux-2-flex, flux-2-max, flux-2-klein-9b, flux-2-klein-4b. gemini: gemini-3.1-flash-lite-image, gemini-2.5-flash-image, gemini-3.1-flash-image, gemini-3-pro-image`;
40792
41154
  var TTS_PROVIDER_DESCRIPTIONS = {
40793
41155
  "elevenlabs": `ElevenLabs (many voices - use voice_set list to discover)`,
40794
41156
  "openai": `OpenAI (alloy, ash, ballad, coral, echo, fable, nova, onyx, sage, shimmer, verse)`,
40795
41157
  "gemini": `Gemini (30 voices: Kore, Puck, Zephyr, Charon, Fenrir, Leda, Orus, Aoede, and 22 more). Multi-speaker (up to 2) and 60+ languages`
40796
41158
  };
40797
- var VIDEO_MODELS_DOC = `veo-3.1-generate-preview (best quality, ~$0.40/sec), veo-3.1-fast-generate-preview (faster, ~$0.15/sec), veo-3.1-lite-generate-preview (budget, ~$0.07/sec)`;
41159
+ var VIDEO_MODELS_DOC = `veo-3.1-generate-preview (best quality, up to 4K, ~$0.40/sec), veo-3.1-fast-generate-preview (faster, ~$0.10/sec), veo-3.1-lite-generate-preview (budget, ~$0.05/sec), gemini-omni-flash-preview (3-10s @ 720p, conversational editing, ~$0.10/sec)`;
40798
41160
 
40799
41161
  // src/commands/generate.ts
40800
41162
  async function downloadFile(url, filename) {
40801
41163
  const res = await fetch(url);
40802
41164
  if (!res.ok) throw new Error(`Download failed: ${res.status}`);
40803
41165
  const buffer = Buffer.from(await res.arrayBuffer());
40804
- ensureOutputDir(filename);
40805
- writeFileSync11(filename, buffer);
40806
- const savedPath = resolvePath2(filename);
41166
+ const outPath = correctExtension(filename, buffer);
41167
+ ensureOutputDir(outPath);
41168
+ writeFileSync11(outPath, buffer);
41169
+ const savedPath = resolvePath2(outPath);
40807
41170
  await pushGenerated(savedPath);
40808
41171
  return savedPath;
40809
41172
  }
41173
+ function sniffExt(buf) {
41174
+ const ascii = (start, end) => buf.subarray(start, end).toString("latin1");
41175
+ if (buf.length < 4) return void 0;
41176
+ if (buf[0] === 255 && buf[1] === 216 && buf[2] === 255) return "jpg";
41177
+ if (ascii(0, 8) === "\x89PNG\r\n\n") return "png";
41178
+ if (ascii(0, 4) === "RIFF" && ascii(8, 12) === "WEBP") return "webp";
41179
+ if (ascii(0, 4) === "RIFF" && ascii(8, 12) === "WAVE") return "wav";
41180
+ if (ascii(0, 3) === "GIF") return "gif";
41181
+ if (ascii(4, 8) === "ftyp") return "mp4";
41182
+ if (buf[0] === 26 && buf[1] === 69 && buf[2] === 223 && buf[3] === 163) return "webm";
41183
+ if (ascii(0, 3) === "ID3" || buf[0] === 255 && (buf[1] & 224) === 224) return "mp3";
41184
+ return void 0;
41185
+ }
41186
+ var EXT_ALIASES = { jpeg: "jpg", htm: "html", yml: "yaml" };
41187
+ var canonicalExt = (ext) => EXT_ALIASES[ext] ?? ext;
41188
+ var formatName = (ext) => canonicalExt(ext) === "jpg" ? "JPEG" : ext.toUpperCase();
41189
+ function correctExtension(filename, buf) {
41190
+ const actual = sniffExt(buf);
41191
+ if (!actual) return filename;
41192
+ const dot = filename.lastIndexOf(".");
41193
+ const asked = dot > 0 ? filename.slice(dot + 1).toLowerCase() : "";
41194
+ if (canonicalExt(asked) === actual) return filename;
41195
+ const corrected = `${dot > 0 ? filename.slice(0, dot) : filename}.${actual}`;
41196
+ console.error(muted(
41197
+ `Note: the model returned ${formatName(actual)}, not ${asked ? formatName(asked) : "the requested format"} \u2014 saving as ${corrected} so the extension matches the actual bytes.`
41198
+ ));
41199
+ return corrected;
41200
+ }
40810
41201
  function ensureOutputDir(output) {
40811
41202
  const dir = dirname11(resolvePath2(output));
40812
41203
  try {
@@ -40818,7 +41209,7 @@ function ensureOutputDir(output) {
40818
41209
  async function pushGenerated(savedPath) {
40819
41210
  const configPath = getConfigPath();
40820
41211
  if (!configPath) return;
40821
- const rel2 = relative4(dirname11(configPath), savedPath);
41212
+ const rel2 = relative5(dirname11(configPath), savedPath);
40822
41213
  if (rel2.startsWith("..") || isAbsolute(rel2)) return;
40823
41214
  try {
40824
41215
  await pushFile(savedPath);
@@ -40861,10 +41252,10 @@ Examples:
40861
41252
  } else {
40862
41253
  const sizeKb = Math.round(result.size_bytes / 1024);
40863
41254
  console.log(`${muted(`Generated with ${result.provider}/${result.model} (${sizeKb}KB)`)}`);
40864
- console.log(success(`Saved to ${savedPath}`));
40865
41255
  if (result.seed !== void 0) {
40866
41256
  console.log(muted(`Seed ${result.seed} \u2014 pass --seed ${result.seed} to keep the next image coherent`));
40867
41257
  }
41258
+ console.log(success(`Saved to ${savedPath}`));
40868
41259
  }
40869
41260
  } catch (err) {
40870
41261
  printCommandError("Image generation", err);
@@ -41043,14 +41434,14 @@ var generateCommand = new Command("generate").description("Generate images, vide
41043
41434
  init_api();
41044
41435
  init_config();
41045
41436
  init_colors();
41046
- import { existsSync as existsSync13, readFileSync as readFileSync17 } from "fs";
41047
- import { join as join16 } from "path";
41437
+ import { existsSync as existsSync14, readFileSync as readFileSync17 } from "fs";
41438
+ import { join as join17 } from "path";
41048
41439
  function readInstalledKitReadme(name) {
41049
41440
  const root = getProjectRoot() ?? process.cwd();
41050
- const kitDir = join16(root, "src", "packages", name);
41051
- if (!existsSync13(join16(kitDir, "package.json"))) return null;
41052
- const readme = join16(kitDir, "README.md");
41053
- return existsSync13(readme) ? readFileSync17(readme, "utf-8") : null;
41441
+ const kitDir = join17(root, "src", "packages", name);
41442
+ if (!existsSync14(join17(kitDir, "package.json"))) return null;
41443
+ const readme = join17(kitDir, "README.md");
41444
+ return existsSync14(readme) ? readFileSync17(readme, "utf-8") : null;
41054
41445
  }
41055
41446
  var skillCommand = new Command("skill").description("Task docs - read the matching skill before building");
41056
41447
  skillCommand.command("list").description("List skills").option("--json", "Output as JSON").action((opts) => run("List", async () => {
@@ -41223,15 +41614,15 @@ var domainCommand = new Command("domain").description("Manage custom domains").a
41223
41614
  }));
41224
41615
 
41225
41616
  // src/commands/realtime.ts
41226
- import { existsSync as existsSync14 } from "fs";
41227
- import { dirname as dirname12, resolve as resolve14 } from "path";
41617
+ import { existsSync as existsSync15 } from "fs";
41618
+ import { dirname as dirname12, resolve as resolve15 } from "path";
41228
41619
  init_api();
41229
41620
  init_config();
41230
41621
  init_colors();
41231
41622
  function hasDeployManifest() {
41232
41623
  const cfgPath = getConfigPath();
41233
41624
  if (!cfgPath) return false;
41234
- return existsSync14(resolve14(dirname12(cfgPath), "gipity.yaml"));
41625
+ return existsSync15(resolve15(dirname12(cfgPath), "gipity.yaml"));
41235
41626
  }
41236
41627
  var roomCommand = new Command("room").description("Manage realtime rooms").argument("[action]", "list | create | delete | info", "list").argument("[name]", "room name (for create | delete | info)").option("--type <type>", "room type for create: state | relay", "state").option("--auth <level>", "auth level for create: public | user", "public").option("--max-clients <n>", "max clients for create (1-200)").option("--json", "Output as JSON").action((action, name, opts) => run("Realtime room", async () => {
41237
41628
  const config = requireConfig();
@@ -41364,10 +41755,10 @@ async function pollTestStatus(projectGuid, runGuid, opts) {
41364
41755
  if (!groups.has(key)) groups.set(key, []);
41365
41756
  groups.get(key).push(r);
41366
41757
  }
41367
- for (const [path4, results] of groups) {
41758
+ for (const [path5, results] of groups) {
41368
41759
  const existingPaths = new Set(data.results.slice(0, lastResultCount).map((r) => r.path));
41369
- if (!existingPaths.has(path4)) {
41370
- console.log(` ${dim(path4)}`);
41760
+ if (!existingPaths.has(path5)) {
41761
+ console.log(` ${dim(path5)}`);
41371
41762
  }
41372
41763
  for (const r of results) {
41373
41764
  const icon = r.isFlaky ? warning("~") : statusIcon2(r.status);
@@ -41407,7 +41798,7 @@ async function pollTestStatus(projectGuid, runGuid, opts) {
41407
41798
  }
41408
41799
  }
41409
41800
  }
41410
- await new Promise((resolve18) => setTimeout(resolve18, getPollInterval()));
41801
+ await new Promise((resolve19) => setTimeout(resolve19, getPollInterval()));
41411
41802
  }
41412
41803
  }
41413
41804
  var testCommand = new Command("test").description("Run tests").argument("[path]", 'Test path filter (e.g. "api", "e2e/portal")').option("--filter <path>", "Alias for the positional test path filter").option("--timeout <ms>", "Per-test timeout in ms", "30000").option("--retry <n>", "Retry failed tests N times", "0").option("--no-sync", "Skip sync-up before tests").option("--json", "Output as JSON").action((pathFilter, opts) => run("Test", async () => {
@@ -41617,21 +42008,21 @@ var locationCommand = new Command("location").description("Show location").argum
41617
42008
  }));
41618
42009
 
41619
42010
  // src/commands/doctor.ts
41620
- import { existsSync as existsSync16, readFileSync as readFileSync19, statSync as statSync8 } from "fs";
41621
- import { join as join18 } from "path";
42011
+ import { existsSync as existsSync17, readFileSync as readFileSync19, statSync as statSync8 } from "fs";
42012
+ import { join as join19 } from "path";
41622
42013
  import { homedir as homedir13 } from "os";
41623
42014
 
41624
42015
  // src/updater/state.ts
41625
- import { readFileSync as readFileSync18, writeFileSync as writeFileSync12, mkdirSync as mkdirSync12, existsSync as existsSync15 } from "fs";
41626
- import { join as join17 } from "path";
42016
+ import { readFileSync as readFileSync18, writeFileSync as writeFileSync12, mkdirSync as mkdirSync12, existsSync as existsSync16 } from "fs";
42017
+ import { join as join18 } from "path";
41627
42018
  import { homedir as homedir12 } from "os";
41628
- var GIPITY_DIR = join17(homedir12(), ".gipity");
41629
- var LOCAL_DIR = join17(GIPITY_DIR, "local");
41630
- var LOCAL_PKG_DIR = join17(LOCAL_DIR, "node_modules", "gipity");
41631
- var LOCAL_ENTRY = join17(LOCAL_PKG_DIR, "dist", "index.js");
41632
- var STATE_FILE = join17(GIPITY_DIR, "update-state.json");
41633
- var SETTINGS_FILE = join17(GIPITY_DIR, "settings.json");
41634
- var UPDATE_LOG = join17(GIPITY_DIR, "update.log");
42019
+ var GIPITY_DIR = join18(homedir12(), ".gipity");
42020
+ var LOCAL_DIR = join18(GIPITY_DIR, "local");
42021
+ var LOCAL_PKG_DIR = join18(LOCAL_DIR, "node_modules", "gipity");
42022
+ var LOCAL_ENTRY = join18(LOCAL_PKG_DIR, "dist", "index.js");
42023
+ var STATE_FILE = join18(GIPITY_DIR, "update-state.json");
42024
+ var SETTINGS_FILE = join18(GIPITY_DIR, "settings.json");
42025
+ var UPDATE_LOG = join18(GIPITY_DIR, "update.log");
41635
42026
  var DEFAULT_STATE = {
41636
42027
  installedVersion: null,
41637
42028
  lastCheckAt: 0,
@@ -41645,7 +42036,7 @@ function ensureDir() {
41645
42036
  mkdirSync12(GIPITY_DIR, { recursive: true });
41646
42037
  }
41647
42038
  function readState() {
41648
- if (!existsSync15(STATE_FILE)) return { ...DEFAULT_STATE };
42039
+ if (!existsSync16(STATE_FILE)) return { ...DEFAULT_STATE };
41649
42040
  try {
41650
42041
  return { ...DEFAULT_STATE, ...JSON.parse(readFileSync18(STATE_FILE, "utf-8")) };
41651
42042
  } catch {
@@ -41657,7 +42048,7 @@ function writeState(state) {
41657
42048
  writeFileSync12(STATE_FILE, JSON.stringify(state, null, 2));
41658
42049
  }
41659
42050
  function readSettings() {
41660
- if (!existsSync15(SETTINGS_FILE)) return { ...DEFAULT_SETTINGS };
42051
+ if (!existsSync16(SETTINGS_FILE)) return { ...DEFAULT_SETTINGS };
41661
42052
  try {
41662
42053
  return { ...DEFAULT_SETTINGS, ...JSON.parse(readFileSync18(SETTINGS_FILE, "utf-8")) };
41663
42054
  } catch {
@@ -41692,8 +42083,8 @@ function versionManagerNode(execPath, env = process.env, home = homedir13()) {
41692
42083
  return null;
41693
42084
  }
41694
42085
  function localVersion() {
41695
- const pkgPath = join18(LOCAL_PKG_DIR, "package.json");
41696
- if (!existsSync16(pkgPath)) return null;
42086
+ const pkgPath = join19(LOCAL_PKG_DIR, "package.json");
42087
+ if (!existsSync17(pkgPath)) return null;
41697
42088
  try {
41698
42089
  return JSON.parse(readFileSync19(pkgPath, "utf-8")).version ?? null;
41699
42090
  } catch {
@@ -41718,7 +42109,7 @@ function rel(t) {
41718
42109
  }
41719
42110
  function relayAutostartInstalled() {
41720
42111
  try {
41721
- return existsSync16(planFor({ cliPath: resolveCliPath() }).path);
42112
+ return existsSync17(planFor({ cliPath: resolveCliPath() }).path);
41722
42113
  } catch (err) {
41723
42114
  if (err instanceof UnsupportedPlatformError) return null;
41724
42115
  return null;
@@ -41756,7 +42147,7 @@ function gatherEnv(opts = {}) {
41756
42147
  const cli = {
41757
42148
  shim_version: shimVersion(),
41758
42149
  local_version: localVersion(),
41759
- local_install_ok: existsSync16(LOCAL_ENTRY),
42150
+ local_install_ok: existsSync17(LOCAL_ENTRY),
41760
42151
  auto_updates: !dis.disabled,
41761
42152
  updates_disabled_reason: dis.disabled ? dis.reason ?? null : null,
41762
42153
  last_check_at: updState.lastCheckAt,
@@ -41791,7 +42182,7 @@ var doctorCommand = new Command("doctor").description("Check install + environme
41791
42182
  const settings = readSettings();
41792
42183
  const dis = updatesDisabled();
41793
42184
  const local = localVersion();
41794
- const localOk = existsSync16(LOCAL_ENTRY);
42185
+ const localOk = existsSync17(LOCAL_ENTRY);
41795
42186
  console.log("");
41796
42187
  console.log(bold("CLI install"));
41797
42188
  console.log(`${muted("shim version ")} ${shimVersion()}`);
@@ -41799,17 +42190,17 @@ var doctorCommand = new Command("doctor").description("Check install + environme
41799
42190
  console.log(`${muted("local install ")} ${LOCAL_PKG_DIR}`);
41800
42191
  console.log("");
41801
42192
  console.log(`${muted("auto-updates ")} ${dis.disabled ? warning(`disabled (${dis.reason})`) : success("enabled")}`);
41802
- console.log(`${muted("settings file ")} ${existsSync16(SETTINGS_FILE) ? SETTINGS_FILE : dim("(default)")} autoUpdates=${settings.autoUpdates}`);
42193
+ console.log(`${muted("settings file ")} ${existsSync17(SETTINGS_FILE) ? SETTINGS_FILE : dim("(default)")} autoUpdates=${settings.autoUpdates}`);
41803
42194
  console.log(`${muted("last check ")} ${rel(state.lastCheckAt)}`);
41804
42195
  console.log(`${muted("last error ")} ${state.lastError ? error(state.lastError) : dim("none")}`);
41805
- console.log(`${muted("state file ")} ${existsSync16(STATE_FILE) ? STATE_FILE : dim("(none yet)")}`);
41806
- console.log(`${muted("update log ")} ${existsSync16(UPDATE_LOG) ? `${UPDATE_LOG} (${statSync8(UPDATE_LOG).size} bytes)` : dim("(none yet)")}`);
42196
+ console.log(`${muted("state file ")} ${existsSync17(STATE_FILE) ? STATE_FILE : dim("(none yet)")}`);
42197
+ console.log(`${muted("update log ")} ${existsSync17(UPDATE_LOG) ? `${UPDATE_LOG} (${statSync8(UPDATE_LOG).size} bytes)` : dim("(none yet)")}`);
41807
42198
  console.log("");
41808
42199
  console.log(dim("Force an update with: gipity update"));
41809
42200
  });
41810
42201
 
41811
42202
  // src/updater/check.ts
41812
- import { appendFileSync, existsSync as existsSync17 } from "fs";
42203
+ import { appendFileSync, existsSync as existsSync18 } from "fs";
41813
42204
  init_platform();
41814
42205
  var CHECK_INTERVAL_MS = 4 * 60 * 60 * 1e3;
41815
42206
  function log(line) {
@@ -41844,7 +42235,7 @@ function installVersion(version) {
41844
42235
  stdio: "ignore"
41845
42236
  });
41846
42237
  if (res.error) log(`npm spawn failed: ${res.error.message}`);
41847
- return res.status === 0 && existsSync17(LOCAL_ENTRY);
42238
+ return res.status === 0 && existsSync18(LOCAL_ENTRY);
41848
42239
  }
41849
42240
  async function runCheck(opts = {}) {
41850
42241
  const state = readState();
@@ -41918,17 +42309,17 @@ var updateCommand = new Command("update").description("Update the CLI").action(a
41918
42309
  init_api();
41919
42310
  init_utils();
41920
42311
  init_colors();
41921
- import { existsSync as existsSync20, readFileSync as readFileSync22, unlinkSync as unlinkSync7 } from "fs";
42312
+ import { existsSync as existsSync21, readFileSync as readFileSync22, unlinkSync as unlinkSync7 } from "fs";
41922
42313
  import { spawn as spawn2 } from "child_process";
41923
42314
 
41924
42315
  // src/relay/daemon.ts
41925
42316
  init_platform();
41926
42317
  init_config();
41927
- import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync14, existsSync as existsSync19, readFileSync as readFileSync21, writeFileSync as writeFileSync13, chmodSync as chmodSync3, closeSync as closeSync4, openSync as openSync4, unlinkSync as unlinkSync6 } from "fs";
42318
+ import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync14, existsSync as existsSync20, readFileSync as readFileSync21, writeFileSync as writeFileSync13, chmodSync as chmodSync3, closeSync as closeSync4, openSync as openSync4, unlinkSync as unlinkSync6 } from "fs";
41928
42319
  import { stat, readFile } from "fs/promises";
41929
42320
  import { createInterface as createInterface2 } from "readline";
41930
42321
  import { homedir as homedir15, hostname as hostname5, platform as osPlatform3, loadavg as loadavg2, freemem as freemem2, totalmem as totalmem2, cpus as cpus2 } from "os";
41931
- import { join as join21 } from "path";
42322
+ import { join as join22 } from "path";
41932
42323
  init_auth();
41933
42324
  init_api();
41934
42325
 
@@ -42218,6 +42609,214 @@ var IngestQueue = class {
42218
42609
  }
42219
42610
  };
42220
42611
 
42612
+ // src/relay/media-upload.ts
42613
+ import path4 from "path";
42614
+
42615
+ // src/relay/device-http.ts
42616
+ init_config();
42617
+ function apiBase() {
42618
+ return resolveApiBase();
42619
+ }
42620
+ function deviceToken() {
42621
+ const d = getDevice();
42622
+ if (!d) throw new Error("No device registered. Run: gipity login");
42623
+ return d.token;
42624
+ }
42625
+ function bridgeAbort(outer, inner) {
42626
+ if (outer.aborted) {
42627
+ inner.abort(outer.reason);
42628
+ return () => {
42629
+ };
42630
+ }
42631
+ const onAbort = () => {
42632
+ inner.abort(outer.reason);
42633
+ };
42634
+ outer.addEventListener("abort", onAbort, { once: true });
42635
+ return () => outer.removeEventListener("abort", onAbort);
42636
+ }
42637
+ async function deviceFetchBinary(method, path5, body, contentType, timeoutMs) {
42638
+ const controller = timeoutMs ? new AbortController() : null;
42639
+ const timer = controller ? setTimeout(() => controller.abort("timeout"), timeoutMs) : null;
42640
+ try {
42641
+ return await fetch(`${apiBase()}${path5}`, {
42642
+ method,
42643
+ headers: {
42644
+ "Authorization": `Bearer ${deviceToken()}`,
42645
+ "Content-Type": contentType
42646
+ },
42647
+ body: new Uint8Array(body),
42648
+ signal: controller?.signal
42649
+ });
42650
+ } finally {
42651
+ if (timer) clearTimeout(timer);
42652
+ }
42653
+ }
42654
+ async function deviceFetch(method, path5, body, timeoutMs, signal) {
42655
+ const controller = timeoutMs ? new AbortController() : null;
42656
+ const timer = controller ? setTimeout(() => controller.abort("timeout"), timeoutMs) : null;
42657
+ const detach = signal && controller ? bridgeAbort(signal, controller) : null;
42658
+ try {
42659
+ return await fetch(`${apiBase()}${path5}`, {
42660
+ method,
42661
+ headers: {
42662
+ "Authorization": `Bearer ${deviceToken()}`,
42663
+ "Content-Type": "application/json"
42664
+ },
42665
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
42666
+ signal: controller?.signal ?? signal
42667
+ });
42668
+ } finally {
42669
+ if (timer) clearTimeout(timer);
42670
+ if (detach) detach();
42671
+ }
42672
+ }
42673
+
42674
+ // src/relay/media-upload.ts
42675
+ var MIN_UPLOAD_BYTES = 4 * 1024;
42676
+ var MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
42677
+ var INLINE_SAFE_B64_CHARS = 12e4;
42678
+ var PATH_MAP_CAP = 500;
42679
+ var uploadViaDevice = async (convGuid, buf, opts) => {
42680
+ const qs = new URLSearchParams({ filename: opts.filename, media_type: opts.mediaType });
42681
+ if (opts.suggestedPath) qs.set("suggested_path", opts.suggestedPath);
42682
+ const res = await deviceFetchBinary(
42683
+ "POST",
42684
+ `/remote-sessions/${encodeURIComponent(convGuid)}/media?${qs.toString()}`,
42685
+ buf,
42686
+ opts.mediaType,
42687
+ 3e4
42688
+ );
42689
+ if (!res.ok) {
42690
+ const body = await res.text().catch(() => "");
42691
+ throw new Error(`HTTP ${res.status} ${body.slice(0, 120)}`);
42692
+ }
42693
+ const json = await res.json();
42694
+ return json.data;
42695
+ };
42696
+ function extForMime(mime) {
42697
+ switch (mime) {
42698
+ case "image/jpeg":
42699
+ return ".jpg";
42700
+ case "image/gif":
42701
+ return ".gif";
42702
+ case "image/webp":
42703
+ return ".webp";
42704
+ case "image/svg+xml":
42705
+ return ".svg";
42706
+ default:
42707
+ return ".png";
42708
+ }
42709
+ }
42710
+ function isBase64ImageBlock(b7) {
42711
+ return b7?.type === "image" && b7.source?.type === "base64" && typeof b7.source.data === "string" && b7.source.data.length > 0;
42712
+ }
42713
+ var ImageBlockRewriter = class {
42714
+ constructor(convGuid, onWarn, upload = uploadViaDevice) {
42715
+ this.convGuid = convGuid;
42716
+ this.onWarn = onWarn;
42717
+ this.upload = upload;
42718
+ }
42719
+ convGuid;
42720
+ onWarn;
42721
+ upload;
42722
+ /** tool_use_id → the file_path the tool was asked to read (for naming +
42723
+ * server-side dedup against the synced node at that path). */
42724
+ pathsByToolUseId = /* @__PURE__ */ new Map();
42725
+ /** Project root on this machine, once the dispatch resolves it — lets an
42726
+ * absolute Read path map to its project-relative VFS path. */
42727
+ cwd = null;
42728
+ setCwd(cwd) {
42729
+ this.cwd = cwd;
42730
+ }
42731
+ /** Rewrite every base64 image block in the batch to an image_ref. Never
42732
+ * throws — per-image failures degrade per the policy above. */
42733
+ async rewrite(entries) {
42734
+ const out = [];
42735
+ for (const entry of entries) {
42736
+ if (entry.kind === "tool_use") {
42737
+ this.recordToolPath(entry);
42738
+ out.push(entry);
42739
+ continue;
42740
+ }
42741
+ if (entry.kind !== "tool_result" || !Array.isArray(entry.content) || typeof entry.tool_use_id !== "string") {
42742
+ out.push(entry);
42743
+ continue;
42744
+ }
42745
+ const blocks = [];
42746
+ for (const block of entry.content) {
42747
+ blocks.push(isBase64ImageBlock(block) ? await this.rewriteBlock(block, entry.tool_use_id) : block);
42748
+ }
42749
+ out.push({ ...entry, content: blocks });
42750
+ }
42751
+ return out;
42752
+ }
42753
+ recordToolPath(entry) {
42754
+ if (typeof entry.tool_use_id !== "string") return;
42755
+ const input = entry.tool_input;
42756
+ const p = input && typeof input === "object" ? input.file_path ?? input.path ?? input.notebook_path : void 0;
42757
+ if (typeof p !== "string" || !p) return;
42758
+ if (this.pathsByToolUseId.size >= PATH_MAP_CAP) {
42759
+ const oldest = this.pathsByToolUseId.keys().next().value;
42760
+ if (oldest !== void 0) this.pathsByToolUseId.delete(oldest);
42761
+ }
42762
+ this.pathsByToolUseId.set(entry.tool_use_id, p);
42763
+ }
42764
+ async rewriteBlock(block, toolUseId) {
42765
+ const b64 = block.source.data;
42766
+ let buf;
42767
+ try {
42768
+ buf = Buffer.from(b64, "base64");
42769
+ } catch {
42770
+ return block;
42771
+ }
42772
+ if (buf.length < MIN_UPLOAD_BYTES) return block;
42773
+ const mediaType = block.source.media_type || "image/png";
42774
+ const sourcePath = this.pathsByToolUseId.get(toolUseId);
42775
+ const filename = sourcePath ? path4.basename(sourcePath) : `image${extForMime(mediaType)}`;
42776
+ const suggested = this.projectRelative(sourcePath);
42777
+ if (buf.length > MAX_UPLOAD_BYTES) {
42778
+ this.onWarn?.("transcript image exceeds upload cap - stubbed", { bytes: buf.length, filename });
42779
+ return this.stub(filename, buf.length, "too large to store");
42780
+ }
42781
+ try {
42782
+ const ref = await this.upload(this.convGuid, buf, {
42783
+ filename,
42784
+ mediaType,
42785
+ ...suggested ? { suggestedPath: suggested } : {}
42786
+ });
42787
+ return {
42788
+ type: "image_ref",
42789
+ url: ref.url,
42790
+ ...ref.thumb_url ? { thumb_url: ref.thumb_url } : {},
42791
+ media_type: mediaType,
42792
+ path: ref.path,
42793
+ ...ref.width != null ? { width: ref.width } : {},
42794
+ ...ref.height != null ? { height: ref.height } : {},
42795
+ bytes: ref.bytes
42796
+ };
42797
+ } catch (err) {
42798
+ this.onWarn?.("transcript image upload failed", { filename, err: err?.message });
42799
+ if (b64.length <= INLINE_SAFE_B64_CHARS) return block;
42800
+ return this.stub(filename, buf.length, `upload failed: ${err?.message || "unknown error"}`);
42801
+ }
42802
+ }
42803
+ stub(filename, bytes, reason) {
42804
+ return {
42805
+ type: "text",
42806
+ text: `[image ${filename} (${Math.round(bytes / 1024)} KB) omitted: ${reason}]`
42807
+ };
42808
+ }
42809
+ /** Map an absolute local path to its project-relative VFS path, when it
42810
+ * falls inside the dispatch's project root. */
42811
+ projectRelative(p) {
42812
+ if (!p || !this.cwd) return void 0;
42813
+ const abs = path4.resolve(p);
42814
+ const root = path4.resolve(this.cwd);
42815
+ if (!abs.startsWith(root + path4.sep)) return void 0;
42816
+ return abs.slice(root.length + 1).split(path4.sep).join("/");
42817
+ }
42818
+ };
42819
+
42221
42820
  // src/relay/redact.ts
42222
42821
  var REDACTION_MARKER = "[redacted]";
42223
42822
  var MIN_SECRET_LEN = 12;
@@ -42428,48 +43027,6 @@ var DeltaBatcher = class {
42428
43027
  // src/relay/daemon.ts
42429
43028
  import { randomUUID } from "crypto";
42430
43029
 
42431
- // src/relay/device-http.ts
42432
- init_config();
42433
- function apiBase() {
42434
- return resolveApiBase();
42435
- }
42436
- function deviceToken() {
42437
- const d = getDevice();
42438
- if (!d) throw new Error("No device registered. Run: gipity login");
42439
- return d.token;
42440
- }
42441
- function bridgeAbort(outer, inner) {
42442
- if (outer.aborted) {
42443
- inner.abort(outer.reason);
42444
- return () => {
42445
- };
42446
- }
42447
- const onAbort = () => {
42448
- inner.abort(outer.reason);
42449
- };
42450
- outer.addEventListener("abort", onAbort, { once: true });
42451
- return () => outer.removeEventListener("abort", onAbort);
42452
- }
42453
- async function deviceFetch(method, path4, body, timeoutMs, signal) {
42454
- const controller = timeoutMs ? new AbortController() : null;
42455
- const timer = controller ? setTimeout(() => controller.abort("timeout"), timeoutMs) : null;
42456
- const detach = signal && controller ? bridgeAbort(signal, controller) : null;
42457
- try {
42458
- return await fetch(`${apiBase()}${path4}`, {
42459
- method,
42460
- headers: {
42461
- "Authorization": `Bearer ${deviceToken()}`,
42462
- "Content-Type": "application/json"
42463
- },
42464
- body: body !== void 0 ? JSON.stringify(body) : void 0,
42465
- signal: controller?.signal ?? signal
42466
- });
42467
- } finally {
42468
- if (timer) clearTimeout(timer);
42469
- if (detach) detach();
42470
- }
42471
- }
42472
-
42473
43030
  // src/relay/agent-token.ts
42474
43031
  init_api();
42475
43032
  init_config();
@@ -42518,21 +43075,21 @@ async function revokeRelayAgentToken() {
42518
43075
  init_platform();
42519
43076
  init_client_context();
42520
43077
  import { cpus, freemem, totalmem, loadavg, release, uptime, homedir as homedir14 } from "os";
42521
- import { statfsSync, readdirSync as readdirSync7 } from "fs";
42522
- import { join as join19 } from "path";
43078
+ import { statfsSync, readdirSync as readdirSync8 } from "fs";
43079
+ import { join as join20 } from "path";
42523
43080
  var PROBE_TIMEOUT_MS = 4e3;
42524
43081
  function parseVersion(out) {
42525
43082
  const m = out.match(/\d+\.\d+(?:\.\d+)?(?:[-.\w]*)?/);
42526
43083
  return m ? m[0] : void 0;
42527
43084
  }
42528
43085
  function runProbe(cmd, args) {
42529
- return new Promise((resolve18) => {
43086
+ return new Promise((resolve19) => {
42530
43087
  let out = "";
42531
43088
  let settled = false;
42532
43089
  const done = (s) => {
42533
43090
  if (!settled) {
42534
43091
  settled = true;
42535
- resolve18(s);
43092
+ resolve19(s);
42536
43093
  }
42537
43094
  };
42538
43095
  let child;
@@ -42586,7 +43143,7 @@ async function detectGpu() {
42586
43143
  function diskUsage() {
42587
43144
  try {
42588
43145
  if (typeof statfsSync !== "function") return void 0;
42589
- const st2 = statfsSync(join19(homedir14(), "GipityProjects"), { bigint: false });
43146
+ const st2 = statfsSync(join20(homedir14(), "GipityProjects"), { bigint: false });
42590
43147
  if (!st2 || !st2.bsize) return void 0;
42591
43148
  return { total: st2.bsize * st2.blocks, free: st2.bsize * st2.bavail };
42592
43149
  } catch {
@@ -42600,7 +43157,7 @@ function diskUsage() {
42600
43157
  }
42601
43158
  function localProjectCount() {
42602
43159
  try {
42603
- return readdirSync7(join19(homedir14(), "GipityProjects"), { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).length;
43160
+ return readdirSync8(join20(homedir14(), "GipityProjects"), { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).length;
42604
43161
  } catch {
42605
43162
  return void 0;
42606
43163
  }
@@ -42729,8 +43286,8 @@ var SessionPool = class {
42729
43286
  const done = new Promise((r) => {
42730
43287
  markDone = r;
42731
43288
  });
42732
- return new Promise((resolve18, reject) => {
42733
- const turn = { onMessage: p.onMessage, resolve: resolve18, reject, interruptRequested: false, wasHot, done, markDone };
43289
+ return new Promise((resolve19, reject) => {
43290
+ const turn = { onMessage: p.onMessage, resolve: resolve19, reject, interruptRequested: false, wasHot, done, markDone };
42734
43291
  session.current = turn;
42735
43292
  session.state = "running";
42736
43293
  session.lastActivityAt = Date.now();
@@ -42907,7 +43464,7 @@ var SessionPool = class {
42907
43464
  // src/relay/daemon.ts
42908
43465
  init_config();
42909
43466
  init_api();
42910
- var RELAY_LOG_PATH = join21(homedir15(), ".gipity", "relay.log");
43467
+ var RELAY_LOG_PATH = join22(homedir15(), ".gipity", "relay.log");
42911
43468
  var HEARTBEAT_INTERVAL_MS = parseInt(process.env.GIPITY_RELAY_HEARTBEAT_MS || "60000", 10);
42912
43469
  var DIAGNOSTICS_INTERVAL_MS = parseInt(process.env.GIPITY_RELAY_DIAGNOSTICS_MS || String(24 * 60 * 60 * 1e3), 10);
42913
43470
  var LONG_POLL_TIMEOUT_MS = parseInt(process.env.GIPITY_RELAY_POLL_TIMEOUT_MS || "35000", 10);
@@ -43007,7 +43564,7 @@ function lockLogPerms(dir, file) {
43007
43564
  chmodSync3(dir, 448);
43008
43565
  } catch {
43009
43566
  }
43010
- if (!existsSync19(file)) {
43567
+ if (!existsSync20(file)) {
43011
43568
  try {
43012
43569
  closeSync4(openSync4(file, "a", 384));
43013
43570
  } catch {
@@ -43024,7 +43581,7 @@ function log2(level, msg, extra) {
43024
43581
  const pretty = `${C7.dim(hhmmss())} ${badge(level)} ${C7.bold(msg)}${formatExtra(extra)}`;
43025
43582
  process.stderr.write(pretty + "\n");
43026
43583
  try {
43027
- const dir = join21(homedir15(), ".gipity");
43584
+ const dir = join22(homedir15(), ".gipity");
43028
43585
  mkdirSync14(dir, { recursive: true });
43029
43586
  lockLogPerms(dir, RELAY_LOG_PATH);
43030
43587
  const json = JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level, msg, ...extra ?? {} });
@@ -43149,10 +43706,10 @@ async function heartbeatLoop(ctx) {
43149
43706
  }
43150
43707
  await Promise.race([
43151
43708
  sleep4(HEARTBEAT_INTERVAL_MS, ctx.abort.signal),
43152
- new Promise((resolve18) => {
43709
+ new Promise((resolve19) => {
43153
43710
  heartbeatPoke = () => {
43154
43711
  heartbeatPoke = null;
43155
- resolve18();
43712
+ resolve19();
43156
43713
  };
43157
43714
  })
43158
43715
  ]);
@@ -43196,9 +43753,9 @@ async function dispatchLoop(ctx, opts) {
43196
43753
  while (inflight.size >= MAX_CONCURRENT_DISPATCHES && !ctx.abort.signal.aborted) {
43197
43754
  await Promise.race([
43198
43755
  ...inflight,
43199
- new Promise((resolve18) => {
43200
- if (ctx.abort.signal.aborted) return resolve18();
43201
- ctx.abort.signal.addEventListener("abort", () => resolve18(), { once: true });
43756
+ new Promise((resolve19) => {
43757
+ if (ctx.abort.signal.aborted) return resolve19();
43758
+ ctx.abort.signal.addEventListener("abort", () => resolve19(), { once: true });
43202
43759
  })
43203
43760
  ]);
43204
43761
  }
@@ -43319,8 +43876,8 @@ async function postStreamDelta(convGuid, dispatchGuid, flush) {
43319
43876
  function toolHint(name, input) {
43320
43877
  if (!input || typeof input !== "object") return void 0;
43321
43878
  if (name === "Bash" && typeof input.command === "string") return input.command;
43322
- const path4 = input.file_path ?? input.path ?? input.pattern ?? input.query ?? input.url ?? input.description;
43323
- return typeof path4 === "string" ? path4 : void 0;
43879
+ const path5 = input.file_path ?? input.path ?? input.pattern ?? input.query ?? input.url ?? input.description;
43880
+ return typeof path5 === "string" ? path5 : void 0;
43324
43881
  }
43325
43882
  var PhaseTracker = class {
43326
43883
  phase = "starting";
@@ -43425,7 +43982,7 @@ function isSafeSessionId(s) {
43425
43982
  function transcriptPathFor(cwd, sessionId) {
43426
43983
  if (!isSafeSessionId(sessionId)) return null;
43427
43984
  const slug = cwd.replace(/[^a-zA-Z0-9]/g, "-");
43428
- return join21(homedir15(), ".claude", "projects", slug, `${sessionId}.jsonl`);
43985
+ return join22(homedir15(), ".claude", "projects", slug, `${sessionId}.jsonl`);
43429
43986
  }
43430
43987
  function formatDuration(ms2) {
43431
43988
  const totalSec = ms2 / 1e3;
@@ -43485,8 +44042,12 @@ async function handleDispatch(claimed) {
43485
44042
  d = { ...d, kind: "resume", remote_session_id: sid };
43486
44043
  }
43487
44044
  }
44045
+ const rewriter = new ImageBlockRewriter(
44046
+ d.conversation_guid,
44047
+ (msg, meta) => log2("warn", msg, { id: d.short_guid, ...meta })
44048
+ );
43488
44049
  const queue = new IngestQueue(
43489
- (entries) => postIngest(d.conversation_guid, entries),
44050
+ async (entries) => postIngest(d.conversation_guid, await rewriter.rewrite(entries)),
43490
44051
  { onWarn: (msg, meta) => log2("warn", msg, { id: d.short_guid, ...meta }) }
43491
44052
  );
43492
44053
  const pushSystem = (content) => {
@@ -43499,6 +44060,7 @@ async function handleDispatch(claimed) {
43499
44060
  let bootstrapped;
43500
44061
  try {
43501
44062
  ({ cwd, bootstrapped } = await resolveCwdForProject(d));
44063
+ rewriter.setCwd(cwd);
43502
44064
  log2("debug", "resolved project cwd", { id: d.short_guid, project: d.project_slug, cwd, bootstrapped });
43503
44065
  } catch (err) {
43504
44066
  log2("error", "could not resolve project cwd", { id: d.short_guid, err: err?.message });
@@ -43815,23 +44377,23 @@ async function resolveCwdForProject(d) {
43815
44377
  throw new Error(`Invalid project slug: ${JSON.stringify(d.project_slug)}`);
43816
44378
  }
43817
44379
  const root = getProjectsRoot();
43818
- const path4 = join21(root, d.project_slug);
43819
- const configPath = join21(path4, ".gipity.json");
43820
- if (existsSync19(configPath)) {
44380
+ const path5 = join22(root, d.project_slug);
44381
+ const configPath = join22(path5, ".gipity.json");
44382
+ if (existsSync20(configPath)) {
43821
44383
  try {
43822
44384
  const cfg = JSON.parse(readFileSync21(configPath, "utf-8"));
43823
- if (cfg.projectGuid === d.project_guid) return { cwd: path4, bootstrapped: false };
44385
+ if (cfg.projectGuid === d.project_guid) return { cwd: path5, bootstrapped: false };
43824
44386
  log2("warn", "project dir exists but guid mismatch - using it anyway", {
43825
- path: path4,
44387
+ path: path5,
43826
44388
  expected: d.project_guid,
43827
44389
  found: cfg.projectGuid
43828
44390
  });
43829
- return { cwd: path4, bootstrapped: false };
44391
+ return { cwd: path5, bootstrapped: false };
43830
44392
  } catch {
43831
44393
  }
43832
44394
  }
43833
- log2("info", "bootstrapping new project dir", { slug: d.project_slug, path: path4 });
43834
- mkdirSync14(path4, { recursive: true });
44395
+ log2("info", "bootstrapping new project dir", { slug: d.project_slug, path: path5 });
44396
+ mkdirSync14(path5, { recursive: true });
43835
44397
  const apiBase2 = getApiBaseOverride() || DEFAULT_API_BASE;
43836
44398
  writeFileSync13(configPath, JSON.stringify({
43837
44399
  projectGuid: d.project_guid,
@@ -43844,7 +44406,7 @@ async function resolveCwdForProject(d) {
43844
44406
  }, null, 2) + "\n");
43845
44407
  const origCwd = process.cwd();
43846
44408
  try {
43847
- process.chdir(path4);
44409
+ process.chdir(path5);
43848
44410
  setupClaudeHooks();
43849
44411
  setupClaudeMd();
43850
44412
  setupAgentsMd();
@@ -43852,7 +44414,7 @@ async function resolveCwdForProject(d) {
43852
44414
  } finally {
43853
44415
  process.chdir(origCwd);
43854
44416
  }
43855
- return { cwd: path4, bootstrapped: true };
44417
+ return { cwd: path5, bootstrapped: true };
43856
44418
  }
43857
44419
  var running = /* @__PURE__ */ new Map();
43858
44420
  var LAST_SESSION_TTL_MS = 10 * 6e4;
@@ -43913,7 +44475,7 @@ async function killRunningForConv(convGuid) {
43913
44475
  }
43914
44476
  }
43915
44477
  const graceTimers = [];
43916
- const escalate = new Promise((resolve18) => {
44478
+ const escalate = new Promise((resolve19) => {
43917
44479
  const t = setTimeout(() => {
43918
44480
  for (const e of matches) {
43919
44481
  log2("warn", "previous dispatch ignored SIGTERM - escalating to SIGKILL", { conv: convGuid });
@@ -43922,7 +44484,7 @@ async function killRunningForConv(convGuid) {
43922
44484
  } catch {
43923
44485
  }
43924
44486
  }
43925
- resolve18();
44487
+ resolve19();
43926
44488
  }, KILL_GRACE_MS);
43927
44489
  graceTimers.push(t);
43928
44490
  });
@@ -43933,7 +44495,7 @@ async function killRunningForConv(convGuid) {
43933
44495
  }
43934
44496
  async function spawnSync2(cwd, timeoutMs, onSpawn) {
43935
44497
  const cmd = process.env.GIPITY_RELAY_CLAUDE_CMD || resolveCommand("gipity");
43936
- return new Promise((resolve18, reject) => {
44498
+ return new Promise((resolve19, reject) => {
43937
44499
  const child = spawnCommand(cmd, ["sync", "--json"], {
43938
44500
  cwd,
43939
44501
  env: childEnv(),
@@ -43957,7 +44519,7 @@ async function spawnSync2(cwd, timeoutMs, onSpawn) {
43957
44519
  } catch {
43958
44520
  }
43959
44521
  try {
43960
- unlinkSync6(join21(cwd, ".gipity", "sync.lock"));
44522
+ unlinkSync6(join22(cwd, ".gipity", "sync.lock"));
43961
44523
  } catch {
43962
44524
  }
43963
44525
  finish(() => reject(new Error(`timed out after ${Math.round(timeoutMs / 1e3)}s`)));
@@ -43973,7 +44535,7 @@ async function spawnSync2(cwd, timeoutMs, onSpawn) {
43973
44535
  child.on("exit", (code) => finish(() => {
43974
44536
  if (code === 0) {
43975
44537
  log2("info", "sync done", { cwd, stdoutLen });
43976
- resolve18();
44538
+ resolve19();
43977
44539
  } else {
43978
44540
  reject(new Error(`gipity sync exited ${code}${stderrBuf ? `: ${stderrBuf.trim().slice(0, 300)}` : ""}`));
43979
44541
  }
@@ -43984,7 +44546,7 @@ async function spawnGipityClaude(args, cwd, d, queue, meta) {
43984
44546
  const cmd = process.env.GIPITY_RELAY_CLAUDE_CMD || resolveCommand("gipity");
43985
44547
  const fullArgs = [...args, "--output-format", "stream-json", "--verbose", "--include-partial-messages"];
43986
44548
  const env = childEnv({ GIPITY_CONVERSATION_GUID: d.conversation_guid, GIPITY_CAPTURE: "off" });
43987
- return new Promise((resolve18, reject) => {
44549
+ return new Promise((resolve19, reject) => {
43988
44550
  const child = spawnCommand(cmd, fullArgs, { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
43989
44551
  let resolveExited = () => {
43990
44552
  };
@@ -43992,10 +44554,17 @@ async function spawnGipityClaude(args, cwd, d, queue, meta) {
43992
44554
  resolveExited = r;
43993
44555
  });
43994
44556
  running.set(d.short_guid, { child, convGuid: d.conversation_guid, exited });
43995
- const q = queue ?? new IngestQueue(
43996
- (entries) => postIngest(d.conversation_guid, entries),
43997
- { onWarn: (msg, meta2) => log2("warn", msg, { id: d.short_guid, ...meta2 }) }
43998
- );
44557
+ const q = queue ?? (() => {
44558
+ const rw = new ImageBlockRewriter(
44559
+ d.conversation_guid,
44560
+ (msg, meta2) => log2("warn", msg, { id: d.short_guid, ...meta2 })
44561
+ );
44562
+ rw.setCwd(cwd);
44563
+ return new IngestQueue(
44564
+ async (entries) => postIngest(d.conversation_guid, await rw.rewrite(entries)),
44565
+ { onWarn: (msg, meta2) => log2("warn", msg, { id: d.short_guid, ...meta2 }) }
44566
+ );
44567
+ })();
43999
44568
  const ownQueue = !queue;
44000
44569
  const dispatchStartedAt = Date.now();
44001
44570
  let stdoutBytesTotal = 0;
@@ -44155,7 +44724,7 @@ async function spawnGipityClaude(args, cwd, d, queue, meta) {
44155
44724
  void postProgress(d.conversation_guid, buildProgressPayload(false));
44156
44725
  if (ownQueue) await q.close();
44157
44726
  cleanup();
44158
- resolve18({
44727
+ resolve19({
44159
44728
  exitCode: code ?? 1,
44160
44729
  killed,
44161
44730
  runtimeLimit,
@@ -44169,10 +44738,10 @@ async function runDispatchSync(d, cwd) {
44169
44738
  let killed = false;
44170
44739
  try {
44171
44740
  await spawnSync2(cwd, PROJECT_SYNC_TIMEOUT_MS, (child) => {
44172
- const exited = new Promise((resolve18) => {
44741
+ const exited = new Promise((resolve19) => {
44173
44742
  child.once("exit", (_code, signal) => {
44174
44743
  if (signal === "SIGTERM") killed = true;
44175
- resolve18();
44744
+ resolve19();
44176
44745
  });
44177
44746
  });
44178
44747
  running.set(d.short_guid, { child, convGuid: d.conversation_guid, exited });
@@ -44203,18 +44772,18 @@ function killDispatch(shortGuid) {
44203
44772
  return false;
44204
44773
  }
44205
44774
  function sleep4(ms2, signal) {
44206
- return new Promise((resolve18) => {
44207
- if (signal?.aborted) return resolve18();
44775
+ return new Promise((resolve19) => {
44776
+ if (signal?.aborted) return resolve19();
44208
44777
  let onAbort = null;
44209
44778
  const t = setTimeout(() => {
44210
44779
  if (onAbort && signal) signal.removeEventListener("abort", onAbort);
44211
- resolve18();
44780
+ resolve19();
44212
44781
  }, ms2);
44213
44782
  if (signal) {
44214
44783
  onAbort = () => {
44215
44784
  clearTimeout(t);
44216
44785
  signal.removeEventListener("abort", onAbort);
44217
- resolve18();
44786
+ resolve19();
44218
44787
  };
44219
44788
  signal.addEventListener("abort", onAbort, { once: true });
44220
44789
  }
@@ -44384,7 +44953,7 @@ relayCommand.command("run").description("Run the background service").option("-v
44384
44953
  });
44385
44954
  relayCommand.command("stop").description("Stop the background service").option("--force", "Force-stop if it doesn't exit cleanly within 5s").action(async (opts) => {
44386
44955
  const pidPath = getDaemonPidPath();
44387
- if (!existsSync20(pidPath)) {
44956
+ if (!existsSync21(pidPath)) {
44388
44957
  console.log(muted("Background service isn't running."));
44389
44958
  return;
44390
44959
  }
@@ -44512,7 +45081,7 @@ relayCommand.command("revoke").description("Revoke and forget this device").acti
44512
45081
  } catch {
44513
45082
  }
44514
45083
  const pidPath = getDaemonPidPath();
44515
- if (existsSync20(pidPath)) {
45084
+ if (existsSync21(pidPath)) {
44516
45085
  const pid = parseInt(readFileSync22(pidPath, "utf-8").trim(), 10);
44517
45086
  if (pid && !isNaN(pid)) {
44518
45087
  try {
@@ -44525,14 +45094,14 @@ relayCommand.command("revoke").description("Revoke and forget this device").acti
44525
45094
  console.log(muted(autostartRemoved ? "Auto-start removed and the background service was signalled to stop." : "The background service was signalled to stop."));
44526
45095
  });
44527
45096
  relayCommand.command("log").description("Tail the service log").option("-n, --lines <n>", "How many lines to print (default 100)", "100").option("-f, --follow", "Follow the log like `tail -f`").action((opts) => {
44528
- const path4 = RELAY_LOG_PATH;
44529
- if (!existsSync20(path4)) {
45097
+ const path5 = RELAY_LOG_PATH;
45098
+ if (!existsSync21(path5)) {
44530
45099
  console.log(muted("No log file yet. Start the service with `gipity relay run` (or install it)."));
44531
45100
  return;
44532
45101
  }
44533
45102
  const lines = parseInt(opts.lines, 10) || 100;
44534
45103
  try {
44535
- const all = readFileSync22(path4, "utf-8").split("\n");
45104
+ const all = readFileSync22(path5, "utf-8").split("\n");
44536
45105
  const tail = all.slice(-lines - 1).join("\n");
44537
45106
  process.stdout.write(tail);
44538
45107
  } catch (err) {
@@ -44545,7 +45114,7 @@ relayCommand.command("log").description("Tail the service log").option("-n, --li
44545
45114
  console.error(error("--follow is not supported on this platform yet."));
44546
45115
  process.exit(1);
44547
45116
  }
44548
- const child = spawn2(tailCmd, ["-f", "-n", "0", path4], { stdio: "inherit" });
45117
+ const child = spawn2(tailCmd, ["-f", "-n", "0", path5], { stdio: "inherit" });
44549
45118
  child.on("error", (err) => {
44550
45119
  console.error(error(err.code === "ENOENT" ? "`tail` not found - can't follow the log on this system." : `Failed to follow log: ${err.message}`));
44551
45120
  process.exit(1);
@@ -44605,12 +45174,12 @@ init_api();
44605
45174
  init_auth();
44606
45175
  init_utils();
44607
45176
  init_colors();
44608
- import { existsSync as existsSync21, rmSync as rmSync2, unlinkSync as unlinkSync8, readFileSync as readFileSync23, writeFileSync as writeFileSync14 } from "fs";
45177
+ import { existsSync as existsSync22, rmSync as rmSync2, unlinkSync as unlinkSync8, readFileSync as readFileSync23, writeFileSync as writeFileSync14 } from "fs";
44609
45178
  import { homedir as homedir16, platform as osPlatform4 } from "os";
44610
- import { join as join22, resolve as resolve16 } from "path";
45179
+ import { join as join23, resolve as resolve17 } from "path";
44611
45180
  function removeGipityPluginConfig() {
44612
- const settingsPath = join22(homedir16(), ".claude", "settings.json");
44613
- if (!existsSync21(settingsPath)) return false;
45181
+ const settingsPath = join23(homedir16(), ".claude", "settings.json");
45182
+ if (!existsSync22(settingsPath)) return false;
44614
45183
  let settings;
44615
45184
  try {
44616
45185
  settings = JSON.parse(readFileSync23(settingsPath, "utf-8"));
@@ -44636,8 +45205,8 @@ function removeInstallerPathLines() {
44636
45205
  const marker = "# Added by the Gipity installer";
44637
45206
  const touched = [];
44638
45207
  for (const name of [".bashrc", ".zshrc", ".profile"]) {
44639
- const rc2 = join22(homedir16(), name);
44640
- if (!existsSync21(rc2)) continue;
45208
+ const rc2 = join23(homedir16(), name);
45209
+ if (!existsSync22(rc2)) continue;
44641
45210
  let text;
44642
45211
  try {
44643
45212
  text = readFileSync23(rc2, "utf-8");
@@ -44662,7 +45231,7 @@ function tildify2(p) {
44662
45231
  return p === home || p.startsWith(home + "/") ? "~" + p.slice(home.length) : p;
44663
45232
  }
44664
45233
  function resolveCliPath2() {
44665
- return resolve16(process.argv[1] ?? "gipity");
45234
+ return resolve17(process.argv[1] ?? "gipity");
44666
45235
  }
44667
45236
  async function stopDaemon() {
44668
45237
  if (!isDaemonRunning()) return;
@@ -44696,7 +45265,7 @@ function removeServiceUnit() {
44696
45265
  const r = spawnSyncCommand(argv2[0], argv2.slice(1), { stdio: "ignore" });
44697
45266
  if (r.status !== 0) allOk = false;
44698
45267
  }
44699
- if (existsSync21(plan2.path)) {
45268
+ if (existsSync22(plan2.path)) {
44700
45269
  try {
44701
45270
  unlinkSync8(plan2.path);
44702
45271
  } catch {
@@ -44719,9 +45288,9 @@ async function revokeDeviceBestEffort() {
44719
45288
  }
44720
45289
  var uninstallCommand = new Command("uninstall").description("Uninstall Gipity").option("--yes", "Skip confirmation prompts").action(async (opts) => {
44721
45290
  const autoYes = opts.yes || getAutoConfirm();
44722
- const gipityDir = join22(homedir16(), ".gipity");
44723
- const launcherBin = join22(gipityDir, "launcher", "bin", "gipity");
44724
- const installedViaLauncher = existsSync21(launcherBin);
45291
+ const gipityDir = join23(homedir16(), ".gipity");
45292
+ const launcherBin = join23(gipityDir, "launcher", "bin", "gipity");
45293
+ const installedViaLauncher = existsSync22(launcherBin);
44725
45294
  console.log(`${bold("Gipity uninstall")} - this will:`);
44726
45295
  console.log(`\u2022 Stop the running relay daemon (if any)`);
44727
45296
  console.log(`\u2022 Remove the OS autostart service (launchd / systemd / Task Scheduler)`);
@@ -44756,7 +45325,7 @@ var uninstallCommand = new Command("uninstall").description("Uninstall Gipity").
44756
45325
  } else {
44757
45326
  console.log(`${muted("No Gipity entries in Claude Code settings.")}`);
44758
45327
  }
44759
- if (existsSync21(gipityDir)) {
45328
+ if (existsSync22(gipityDir)) {
44760
45329
  try {
44761
45330
  rmSync2(gipityDir, { recursive: true, force: true });
44762
45331
  console.log(`${success(`Removed ${gipityDir}/`)}`);
@@ -45283,7 +45852,7 @@ function collectRealFlags(argv2, program3) {
45283
45852
 
45284
45853
  // src/trace.ts
45285
45854
  import { openSync as openSync5, writeSync, mkdirSync as mkdirSync15 } from "fs";
45286
- import { join as join23 } from "path";
45855
+ import { join as join24 } from "path";
45287
45856
  import { homedir as homedir17 } from "os";
45288
45857
  var TRACE_KEY = /* @__PURE__ */ Symbol.for("gipity.traceOutput");
45289
45858
  function installOutputTrace(label2) {
@@ -45295,10 +45864,10 @@ function installOutputTrace(label2) {
45295
45864
  existing.emit({ event: "reenter", label: label2 });
45296
45865
  return;
45297
45866
  }
45298
- const dir = join23(process.env.GIPITY_DIR || join23(homedir17(), ".gipity"), "trace");
45867
+ const dir = join24(process.env.GIPITY_DIR || join24(homedir17(), ".gipity"), "trace");
45299
45868
  mkdirSync15(dir, { recursive: true });
45300
45869
  const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", "_").replace(/:/g, "-");
45301
- const fd2 = openSync5(join23(dir, `${stamp}-pid${process.pid}.jsonl`), "a");
45870
+ const fd2 = openSync5(join24(dir, `${stamp}-pid${process.pid}.jsonl`), "a");
45302
45871
  const emit = (rec) => {
45303
45872
  try {
45304
45873
  writeSync(fd2, JSON.stringify({ t: (/* @__PURE__ */ new Date()).toISOString(), ...rec }) + "\n");
@@ -45334,11 +45903,11 @@ function installOutputTrace(label2) {
45334
45903
  // src/index.ts
45335
45904
  installOutputTrace("index");
45336
45905
  var __dirname = dirname13(fileURLToPath3(import.meta.url));
45337
- var pkg = JSON.parse(readFileSync25(resolve17(__dirname, "../package.json"), "utf-8"));
45906
+ var pkg = JSON.parse(readFileSync25(resolve18(__dirname, "../package.json"), "utf-8"));
45338
45907
  function versionLabel() {
45339
45908
  const base = `v${pkg.version}`;
45340
45909
  try {
45341
- const info2 = JSON.parse(readFileSync25(resolve17(__dirname, "build-info.json"), "utf-8"));
45910
+ const info2 = JSON.parse(readFileSync25(resolve18(__dirname, "build-info.json"), "utf-8"));
45342
45911
  if (info2?.sha) return `${base} (dev ${info2.sha}${info2.dirty ? ", modified" : ""})`;
45343
45912
  } catch {
45344
45913
  }