gipity 1.1.2 → 1.1.4

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
@@ -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((resolve20) => {
3947
+ return new Promise((resolve19) => {
3948
3948
  rl2.question(question, (answer) => {
3949
3949
  rl2.close();
3950
- resolve20(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((resolve20) => {
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
- resolve20(answer);
3995
+ resolve19(answer);
3996
3996
  });
3997
3997
  });
3998
3998
  }
3999
3999
  function pickOne(label2, max, defaultIdx = 1) {
4000
- return new Promise((resolve20) => {
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 resolve20(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 resolve20(n);
4021
+ return resolve19(n);
4022
4022
  }
4023
4023
  console.log(String(defaultIdx));
4024
- resolve20(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(resolve20, reject) {
5374
- promiseResolve = resolve20;
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((resolve20, reject) => {
5405
- if (stream._duplexState & DESTROYED) return resolve20({ 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 resolve20({ 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((resolve20) => {
5453
- state.drains.push({ writes, resolve: resolve20 });
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((resolve20, reject) => {
5558
+ return new Promise((resolve19, reject) => {
5559
5559
  return pipeline(...streams, (err) => {
5560
5560
  if (err) return reject(err);
5561
- resolve20();
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(resolve20, reject) {
6216
+ function onnext(resolve19, reject) {
6217
6217
  if (error2) {
6218
6218
  return reject(error2);
6219
6219
  }
6220
6220
  if (entryStream) {
6221
- resolve20({ value: entryStream, done: false });
6221
+ resolve19({ value: entryStream, done: false });
6222
6222
  entryStream = null;
6223
6223
  return;
6224
6224
  }
6225
- promiseResolve = resolve20;
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((resolve20, reject) => {
6254
- if (extract3.destroyed) return resolve20({ 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 resolve20({ value: void 0, done: true });
6257
+ else resolve19({ value: void 0, done: true });
6258
6258
  });
6259
6259
  });
6260
6260
  }
@@ -6707,7 +6707,7 @@ async function postForTarEntries(path5, body, retried = false) {
6707
6707
  if (!res.body) throw new ApiError(500, "EMPTY_RESPONSE", "Server returned no body");
6708
6708
  const extract3 = tar.extract();
6709
6709
  const entries = [];
6710
- const done = new Promise((resolve20, reject) => {
6710
+ const done = new Promise((resolve19, reject) => {
6711
6711
  extract3.on("entry", (header, stream, next) => {
6712
6712
  const chunks = [];
6713
6713
  stream.on("data", (c) => chunks.push(c));
@@ -6718,7 +6718,7 @@ async function postForTarEntries(path5, body, retried = false) {
6718
6718
  stream.on("error", reject);
6719
6719
  stream.resume();
6720
6720
  });
6721
- extract3.on("finish", () => resolve20());
6721
+ extract3.on("finish", () => resolve19());
6722
6722
  extract3.on("error", reject);
6723
6723
  });
6724
6724
  Readable.fromWeb(res.body).pipe(extract3);
@@ -31863,9 +31863,9 @@ var {
31863
31863
  // src/index.ts
31864
31864
  init_config();
31865
31865
  init_utils();
31866
- import { readFileSync as readFileSync26 } from "fs";
31866
+ import { readFileSync as readFileSync28 } from "fs";
31867
31867
  import { fileURLToPath as fileURLToPath3 } from "url";
31868
- import { dirname as dirname13, resolve as resolve19 } from "path";
31868
+ import { dirname as dirname13, resolve as resolve18 } from "path";
31869
31869
 
31870
31870
  // src/helpers/output.ts
31871
31871
  var frameOpen = false;
@@ -31970,12 +31970,10 @@ Kits are reusable building blocks added to an existing app, not whole templates
31970
31970
  - \`gipity add chatbot\` - Drop-in chatbot - configurable persona, scope guardrails, static knowledge (20k budget), streaming responses. Headless engine + bubble widget; bring your own UI if you want. Works in any app.
31971
31971
  - \`gipity add audio-align\` - Audio + lyrics -> word-level timing JSON. Demucs vocal isolation + MMS_FA forced alignment, runs as a Modal L4 GPU job (~$0.01 per 3-min song). For karaoke captions, subtitling, language learning, dubbing alignment.
31972
31972
  - \`gipity add i18n\` - Multi-language for web apps - language picker, locale persistence, RTL, plural/translation lookup. Scaffolds src/js/strings.js and wires it up; move your copy there and read it with t('key'). Web only.
31973
- - \`gipity add records\` - Registry-driven records: declare objects/fields as data, get generic CRUD functions with validation, full-text search, soft delete, ACTOR provenance, and an audit event spine - every write is transactional (row + event). Field types include relations ({id,label}), currency, emails/phones/links composites. Ships backend functions + migrations. Needs a database (web-fullstack/api template). See the \`app-records\` skill.
31974
- - \`gipity add views\` - Generic UI over records-kit objects: sortable/filterable table with full-text search, create/edit/delete forms with type-appropriate widgets, kanban board with drag-to-update. Renders entirely from the field registry - zero per-object UI code. Requires the records kit. See the \`app-records\` skill.
31975
- - \`gipity add agent-api\` - Make your app agent-operable: named API keys (kit_api_keys) let agents and scripts write through the records kit's single write path with AGENT/API actor attribution - machine writes land on the same audit spine as human edits. Requires the records kit. See the \`app-records\` skill.
31976
31973
  - \`gipity add contacts\` - Source-agnostic contact data layer for lead-gen/CRM apps: import people from LinkedIn CSV + Gmail + pasted lists, resolve duplicates into one person while keeping EVERY value from every source with provenance (multi-valued attributes, never overwrites). Exact email/URL auto-merge; fuzzy name+company goes to a human merge-review queue (reversible). Re-imports detect job changes and emit signals. User-definable tags, full-text search, and a transactional event spine. Ships backend functions + migrations. Needs a database (web-fullstack/api template).
31977
31974
  - \`gipity add stripe\` - Charge your app's end-users for one-time purchases and subscriptions via Stripe. Owner connects their own Stripe account through Gipity-hosted onboarding (no API keys to paste); money lands in their account, Gipity takes a small platform fee. Ships a buy-button / pricing component, a subscription-status helper for gating UI, a webhook-verified fulfillment function, and the payments/subscriptions tables. The platform brokers checkout + signature-verified webhooks. Needs a database (web-fullstack/api template).
31978
- - \`gipity add notify\` - Web push notifications for any web app, including iOS home-screen web apps (iOS 16.4+). The platform owns the VAPID keys, encryption, and delivery \u2014 no keys to paste, no crypto, no server. Ships a <gipity-notify-button>, a service worker, and a PWA manifest; the device subscribes itself and self-heals stale subscriptions. Send from a function with the injected notify() service (one flat credit per send, owner-billed) or test with \`gipity notify test\`. Works in any template \u2014 no database required.`;
31975
+ - \`gipity add notify\` - Web push notifications for any web app, including iOS home-screen web apps (iOS 16.4+). The platform owns the VAPID keys, encryption, and delivery \u2014 no keys to paste, no crypto, no server. Ships a <gipity-notify-button>, a service worker, and a PWA manifest; the device subscribes itself and self-heals stale subscriptions. Send from a function with the injected notify() service (one flat credit per send, owner-billed) or test with \`gipity notify test\`. Works in any template \u2014 no database required.
31976
+ - \`gipity add servicenow\` - Use ServiceNow tables as a data source: OAuth (client-credentials) authenticated polling pull into a local Postgres mirror (sn_records, any table, configurable), plus write-back into ServiceNow via the Table API. Optional real-time sync layers a ServiceNow Business Rule + Script Include on top, pushing changes to a webhook the instant they happen. Ships a cron workflow for the pull cycle, sn-pull/sn-write/sn-webhook functions, and setup scripts that automate the ServiceNow-side configuration. Needs a database (web-fullstack/api template).`;
31979
31977
  var SKILLS_CONTENT = `# Gipity Integration
31980
31978
 
31981
31979
  Gipity is the cloud platform your project runs on - hosting, databases, deployment, file storage, code execution, workflows, and monitoring. Gip is the cloud agent that runs on Gipity.
@@ -32088,7 +32086,7 @@ To keep local-only material (research clones, scratch data, vendored references)
32088
32086
  Deploy is opt-in, not opt-out: the \`files\` phase uploads **only** what's under \`src/\` (plus \`functions/\` and \`migrations/\` as backend, not CDN files). Anything else at the project root is kept but never deployed. Put each kind of file in the right bucket so scratch and reference material can't bloat a deploy:
32089
32087
 
32090
32088
  - **\`src/\`** - the app itself. Synced **and** deployed to the CDN. Only app code, assets, and pages belong here.
32091
- - **\`tmp/\`** - ephemeral scratch: file conversions, intermediate outputs, design staging. **Already ignored** (never synced, never deployed) - the one place to do throwaway work. Use this single root. (\`*_tmp/\` dirs and \`.gipityscratch/\` are auto-ignored too, as a safety net, so legacy scattered scratch like \`_vsd_tmp/\` can't leak - but write new scratch to \`tmp/\`, not scattered dirs.) Because it never syncs, \`tmp/\` is also never mirrored into the sandbox: **don't stage \`gipity sandbox run\` inputs here** - the sandbox won't see them. Stage inputs under \`src/\`/\`docs/\` and delete them after.
32089
+ - **\`tmp/\`** - ephemeral scratch: file conversions, intermediate outputs, design staging. **Already ignored** (never synced, never deployed) - the one place to do throwaway work. Use this single root. Leave scratch files where they are when you're done; there's nothing to clean up (no \`rm\` turn) - they're invisible to sync and deploy. (\`*_tmp/\` dirs and \`.gipityscratch/\` are auto-ignored too, as a safety net, so legacy scattered scratch like \`_vsd_tmp/\` can't leak - but write new scratch to \`tmp/\`, not scattered dirs.) Because it never syncs, \`tmp/\` is also never mirrored into the sandbox: **don't stage \`gipity sandbox run\` inputs here** - the sandbox won't see them. Stage inputs under \`src/\`/\`docs/\` and delete them after.
32092
32090
  - **\`docs/\`** - reference material you want to keep: UI/architecture diagrams, design decks, notes, ADRs. Synced and versioned on the server (backed up, rollback-able) but **never deployed**, because it's outside \`src/\`. This is the home for "keep forever, don't ship" artifacts.
32093
32091
  - **\`tests/\`** - \`*.test.js\` suites. Synced, run by \`gipity test\`, never deployed. \`gipity test list [path]\` lists the test files (and what a filter selects) without running them.
32094
32092
 
@@ -32104,7 +32102,7 @@ App services skills (load before calling \`/services/*\` endpoints):
32104
32102
  - \`app-audio\` - sound effects, music, transcription
32105
32103
  - \`app-auth\` - sign in with Gipity, popup vs redirect
32106
32104
  - \`app-files\` - uploads, variants, file listing
32107
- - \`app-image\` - text-to-image only (no input image / editing); providers, sizes, aspect ratios
32105
+ - \`app-image\` - text-to-image AND instruction editing (pass \`images\` to edit an uploaded photo); providers, sizes, aspect ratios
32108
32106
  - \`app-llm\` - chat completions, streaming, image input
32109
32107
  - \`app-location\` - user location & reverse geocoding for deployed apps (first-party - no third-party geocoder)
32110
32108
  - \`app-notify\` - web push notifications for deployed apps (incl. iOS home-screen web apps) - notify kit + injected notify() service, platform owns the keys
@@ -32127,7 +32125,6 @@ App development skills:
32127
32125
  - \`web-ui-patterns\` - default Gipity look (theme tokens) + web UI recipes - feeds, copy-to-clipboard
32128
32126
 
32129
32127
  Kit skills (reusable building blocks - \`gipity add <kit>\`):
32130
- - \`app-records\` - the records + views kits: registry-driven CRUD with search + audit history, and generated tables/kanban/forms (gipity add records)
32131
32128
  - \`audio-align\` - the audio-align kit: forced alignment of audio + lyrics into word-level timing JSON
32132
32129
  - \`chatbot\` - the chatbot kit: persona + scope guardrails + static knowledge, bubble widget or headless engine
32133
32130
 
@@ -32359,25 +32356,25 @@ async function hashFile(path5) {
32359
32356
  const hash = createHash("sha256");
32360
32357
  let size = 0;
32361
32358
  const stream = createReadStream(path5);
32362
- await new Promise((resolve20, reject) => {
32359
+ await new Promise((resolve19, reject) => {
32363
32360
  stream.on("data", (chunk) => {
32364
32361
  const buf = chunk;
32365
32362
  hash.update(buf);
32366
32363
  size += buf.length;
32367
32364
  });
32368
- stream.on("end", () => resolve20());
32365
+ stream.on("end", () => resolve19());
32369
32366
  stream.on("error", reject);
32370
32367
  });
32371
32368
  return { sha256: hash.digest("hex"), size };
32372
32369
  }
32373
32370
  function readRange(path5, start, end) {
32374
- return new Promise((resolve20, reject) => {
32371
+ return new Promise((resolve19, reject) => {
32375
32372
  const chunks = [];
32376
32373
  const stream = createReadStream(path5, { start, end });
32377
32374
  stream.on("data", (c) => {
32378
32375
  chunks.push(c);
32379
32376
  });
32380
- stream.on("end", () => resolve20(Buffer.concat(chunks)));
32377
+ stream.on("end", () => resolve19(Buffer.concat(chunks)));
32381
32378
  stream.on("error", reject);
32382
32379
  });
32383
32380
  }
@@ -33229,7 +33226,7 @@ async function fetchRemote(projectGuid) {
33229
33226
  function extractTarToMap(stream, idleMs, onBytes, keep) {
33230
33227
  const extract3 = tar2.extract();
33231
33228
  const files = /* @__PURE__ */ new Map();
33232
- return new Promise((resolve20, reject) => {
33229
+ return new Promise((resolve19, reject) => {
33233
33230
  let settled = false;
33234
33231
  let idle;
33235
33232
  const arm = () => {
@@ -33265,7 +33262,7 @@ function extractTarToMap(stream, idleMs, onBytes, keep) {
33265
33262
  });
33266
33263
  entryStream.resume();
33267
33264
  });
33268
- extract3.on("finish", () => done(resolve20, files));
33265
+ extract3.on("finish", () => done(resolve19, files));
33269
33266
  extract3.on("error", (e) => done(reject, e));
33270
33267
  stream.on("error", (e) => done(reject, e));
33271
33268
  arm();
@@ -34251,6 +34248,89 @@ async function syncBeforeAction(opts) {
34251
34248
  }
34252
34249
  }
34253
34250
 
34251
+ // src/helpers/body.ts
34252
+ import { readFileSync as readFileSync7 } from "node:fs";
34253
+ function readStdin() {
34254
+ if (process.stdin.isTTY) return "";
34255
+ try {
34256
+ return readFileSync7(0, "utf-8");
34257
+ } catch {
34258
+ return "";
34259
+ }
34260
+ }
34261
+ function readFileField(spec) {
34262
+ const eq2 = spec.indexOf("=");
34263
+ if (eq2 === -1) {
34264
+ throw new Error(`Invalid --file '${spec}': expected field=@path (e.g. --file image=@receipt.png).`);
34265
+ }
34266
+ const field = spec.slice(0, eq2).trim();
34267
+ let path5 = spec.slice(eq2 + 1);
34268
+ if (path5.startsWith("@")) path5 = path5.slice(1);
34269
+ if (!field) throw new Error(`Invalid --file '${spec}': missing field name before '='.`);
34270
+ if (!path5) throw new Error(`Invalid --file '${spec}': missing file path after '='.`);
34271
+ try {
34272
+ return [field, { data: readFileSync7(path5).toString("base64"), media_type: guessMime(path5) }];
34273
+ } catch (e) {
34274
+ throw new Error(`Cannot read --file '${path5}': ${e.message}`);
34275
+ }
34276
+ }
34277
+ function resolveBody(raw, fileSpecs) {
34278
+ const body = resolveJsonBody(raw);
34279
+ if (!fileSpecs || fileSpecs.length === 0) return body;
34280
+ if (typeof body !== "object" || body === null || Array.isArray(body)) {
34281
+ throw new Error("--file needs an object body to attach into (got a non-object JSON body).");
34282
+ }
34283
+ const obj = body;
34284
+ for (const spec of fileSpecs) {
34285
+ const [field, attachment] = readFileField(spec);
34286
+ obj[field] = attachment;
34287
+ }
34288
+ return obj;
34289
+ }
34290
+ function resolveJsonBody(raw) {
34291
+ if (raw == null || raw === "") return {};
34292
+ let source;
34293
+ let origin;
34294
+ if (raw === "-" || raw === "@-") {
34295
+ source = readStdin();
34296
+ origin = "stdin";
34297
+ if (source.trim() === "") {
34298
+ throw new Error("No JSON on stdin (pipe a body in, e.g. `cat body.json | gipity fn call foo -d -`).");
34299
+ }
34300
+ } else if (raw.startsWith("@")) {
34301
+ const path5 = raw.slice(1);
34302
+ try {
34303
+ source = readFileSync7(path5, "utf-8");
34304
+ } catch (e) {
34305
+ throw new Error(`Cannot read body file '${path5}': ${e.message}`);
34306
+ }
34307
+ origin = `file '${path5}'`;
34308
+ } else {
34309
+ source = raw;
34310
+ origin = "inline JSON";
34311
+ }
34312
+ try {
34313
+ return JSON.parse(source);
34314
+ } catch (e) {
34315
+ throw new Error(`Invalid JSON in ${origin}: ${e.message}`);
34316
+ }
34317
+ }
34318
+
34319
+ // src/helpers/duration.ts
34320
+ var UNIT_TO_MS = { ms: 1, s: 1e3, m: 6e4 };
34321
+ function parseDuration(raw, unit) {
34322
+ if (raw === void 0) return null;
34323
+ const m = raw.trim().match(/^(\d+(?:\.\d+)?)\s*(ms|s|m)?$/i);
34324
+ if (!m) return null;
34325
+ const n = parseFloat(m[1]);
34326
+ if (!Number.isFinite(n)) return null;
34327
+ const suffix = m[2]?.toLowerCase();
34328
+ if (!suffix) return { value: n, hadSuffix: false };
34329
+ const ms2 = n * UNIT_TO_MS[suffix];
34330
+ const value = unit === "ms" ? ms2 : ms2 / 1e3;
34331
+ return { value, hadSuffix: true };
34332
+ }
34333
+
34254
34334
  // src/commands/token.ts
34255
34335
  var tokenCommand = new Command("token").description("Manage API tokens").addHelpText("after", "\nLong-lived agent API tokens (gip_at_*) for headless agents and CI.");
34256
34336
  var fmtDate = (d) => d ? new Date(d).toLocaleDateString() : "never";
@@ -34302,7 +34382,7 @@ init_api();
34302
34382
  init_config();
34303
34383
  init_auth();
34304
34384
  import { basename, resolve as resolve7, dirname as dirname5 } from "path";
34305
- import { existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
34385
+ import { existsSync as existsSync8, readFileSync as readFileSync10 } from "fs";
34306
34386
  init_colors();
34307
34387
  init_utils();
34308
34388
 
@@ -34477,7 +34557,7 @@ async function finalizeLocalProject(opts) {
34477
34557
  }
34478
34558
 
34479
34559
  // src/relay/paths.ts
34480
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
34560
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
34481
34561
  import { homedir as homedir4 } from "os";
34482
34562
  import { join as join6, resolve as resolve5 } from "path";
34483
34563
  function getProjectsRoot() {
@@ -34485,7 +34565,7 @@ function getProjectsRoot() {
34485
34565
  const defaultDir = join6(homedir4(), "GipityProjects");
34486
34566
  try {
34487
34567
  if (existsSync6(settingsPath)) {
34488
- const settings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
34568
+ const settings = JSON.parse(readFileSync8(settingsPath, "utf-8"));
34489
34569
  if (settings.projectsDir) return resolve5(settings.projectsDir);
34490
34570
  } else {
34491
34571
  mkdirSync5(join6(homedir4(), ".gipity"), { recursive: true });
@@ -34497,7 +34577,7 @@ function getProjectsRoot() {
34497
34577
  }
34498
34578
 
34499
34579
  // src/relay/state.ts
34500
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, existsSync as existsSync7, chmodSync as chmodSync2, unlinkSync as unlinkSync4 } from "fs";
34580
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, existsSync as existsSync7, chmodSync as chmodSync2, unlinkSync as unlinkSync4 } from "fs";
34501
34581
  import { join as join7 } from "path";
34502
34582
  import { homedir as homedir5 } from "os";
34503
34583
  var RELAY_DIR = process.env.GIPITY_DIR || join7(homedir5(), ".gipity");
@@ -34509,7 +34589,7 @@ function emptyState() {
34509
34589
  function loadState() {
34510
34590
  if (!existsSync7(RELAY_FILE)) return emptyState();
34511
34591
  try {
34512
- const raw = JSON.parse(readFileSync8(RELAY_FILE, "utf-8"));
34592
+ const raw = JSON.parse(readFileSync9(RELAY_FILE, "utf-8"));
34513
34593
  return {
34514
34594
  device: raw.device ?? null,
34515
34595
  paused: Boolean(raw.paused),
@@ -34611,7 +34691,7 @@ function clearDaemonPid() {
34611
34691
  function isDaemonRunning() {
34612
34692
  if (!existsSync7(RELAY_PID_FILE)) return false;
34613
34693
  try {
34614
- const raw = readFileSync8(RELAY_PID_FILE, "utf-8").trim();
34694
+ const raw = readFileSync9(RELAY_PID_FILE, "utf-8").trim();
34615
34695
  const pid = parseInt(raw, 10);
34616
34696
  if (!pid || isNaN(pid)) {
34617
34697
  try {
@@ -34957,7 +35037,7 @@ Working with an existing Gipity project:
34957
35037
  if (adopted.applied > 0) console.log(`Synced ${adopted.applied} change${adopted.applied > 1 ? "s" : ""} with Gipity.`);
34958
35038
  if (opts.capture === false) {
34959
35039
  try {
34960
- const cfg = JSON.parse(readFileSync9(resolve7(cwd, ".gipity.json"), "utf-8"));
35040
+ const cfg = JSON.parse(readFileSync10(resolve7(cwd, ".gipity.json"), "utf-8"));
34961
35041
  cfg.captureHooks = false;
34962
35042
  saveConfigAt(cwd, cfg);
34963
35043
  } catch {
@@ -34991,7 +35071,7 @@ init_auth();
34991
35071
  init_api();
34992
35072
  init_config();
34993
35073
  init_colors();
34994
- import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
35074
+ import { existsSync as existsSync9, readFileSync as readFileSync11 } from "fs";
34995
35075
  import { join as join9, resolve as resolve8 } from "path";
34996
35076
  import { homedir as homedir7 } from "os";
34997
35077
  function checkGipityPlugin() {
@@ -34999,7 +35079,7 @@ function checkGipityPlugin() {
34999
35079
  let settings = {};
35000
35080
  if (existsSync9(path5)) {
35001
35081
  try {
35002
- settings = JSON.parse(readFileSync10(path5, "utf-8"));
35082
+ settings = JSON.parse(readFileSync11(path5, "utf-8"));
35003
35083
  } catch {
35004
35084
  }
35005
35085
  }
@@ -35159,106 +35239,60 @@ var pushCommand = new Command("push").description("Push one or more files").argu
35159
35239
  });
35160
35240
 
35161
35241
  // src/commands/upload.ts
35242
+ init_api();
35162
35243
  init_config();
35163
- import { statSync as statSync4, readdirSync as readdirSync6 } from "fs";
35164
- import { join as join10, basename as basename2, posix, resolve as resolve10, dirname as dirname6 } from "path";
35244
+ import { statSync as statSync4 } from "fs";
35245
+ import { basename as basename2 } from "path";
35165
35246
  init_utils();
35166
35247
  init_colors();
35167
- function walkFiles(root) {
35168
- const out = [];
35169
- const stack = [root];
35170
- while (stack.length) {
35171
- const dir = stack.pop();
35172
- for (const entry of readdirSync6(dir, { withFileTypes: true })) {
35173
- const full = join10(dir, entry.name);
35174
- if (entry.isDirectory()) stack.push(full);
35175
- else if (entry.isFile()) out.push(full);
35176
- }
35177
- }
35178
- return out;
35179
- }
35180
- function destFor(localFile, srcRoot, destRoot) {
35181
- const rel2 = localFile.slice(srcRoot.length).replace(/\\/g, "/").replace(/^\/+/, "");
35182
- return posix.join(destRoot.replace(/\\/g, "/").replace(/\/$/, ""), rel2);
35183
- }
35184
- var uploadCommand = new Command("upload").description("Upload files").argument("<src>", "Local source file or directory").argument("[dest]", "Destination path in the project (defaults to /)").option("-r, --recursive", "Upload a directory recursively").option("--mime <type>", "Override the content-type (default: detect from extension)").option("--concurrency <n>", `Parallel files (default ${UPLOAD_CONCURRENCY})`).option("--dry-run", "Print what would be uploaded; do not call the network").option("--project <guid-or-slug>", "Target a specific project instead of cwd / Home").action(async (src, destArg, opts) => {
35248
+ var uploadCommand = new Command("upload").description(`Upload a local file and print a durable, worker-reachable URL for it.
35249
+
35250
+ The URL works immediately - no \`gipity deploy\` - and any job or function can
35251
+ fetch it. Use it to hand a GPU/CPU job a real input file when testing end-to-end:
35252
+
35253
+ gipity upload song.mp3
35254
+ gipity job submit split-stems --data '{"audio_url":"<printed url>"}'
35255
+
35256
+ By default the file is PUBLIC: a plain \`media.gipity.ai\` CDN url that resolves
35257
+ from anywhere, so a cloud worker can always fetch it. Pass --private for a
35258
+ token-signed serve url instead (reachable only by holders of the url).`).argument("<file>", "Local file to upload").option("--private", "Store as a private token-signed serve URL instead of a public CDN url").option("--content-type <mime>", "Override the content type (default: detected from the file extension)").option("--json", "Output as JSON").action((file, opts) => run("Upload", async () => {
35259
+ const { config } = await resolveProjectContext();
35260
+ let size;
35185
35261
  try {
35186
- const { config } = await resolveProjectContext({ projectOverride: opts.project });
35187
- const dest = destArg ?? "/";
35188
- const srcStat = statSync4(src);
35189
- const planned = [];
35190
- if (srcStat.isDirectory()) {
35191
- if (!opts.recursive) {
35192
- throw new Error(`${src} is a directory - pass -r/--recursive to upload it`);
35193
- }
35194
- const srcAbs = resolve10(src);
35195
- const sliceRoot = dirname6(srcAbs);
35196
- for (const file of walkFiles(src)) {
35197
- planned.push({
35198
- localPath: file,
35199
- virtualPath: destFor(resolve10(file), sliceRoot, dest),
35200
- size: statSync4(file).size
35201
- });
35202
- }
35203
- } else if (srcStat.isFile()) {
35204
- const looksLikeDir = dest.endsWith("/") || opts.recursive === true;
35205
- const virtualPath = looksLikeDir ? posix.join(dest.replace(/\/$/, ""), basename2(src)) : dest;
35206
- planned.push({ localPath: src, virtualPath, size: srcStat.size });
35207
- } else {
35208
- throw new Error(`${src} is neither a regular file nor a directory`);
35209
- }
35210
- if (planned.length === 0) {
35211
- console.log("Nothing to upload (0 files).");
35212
- return;
35213
- }
35214
- const totalBytes = planned.reduce((s, f) => s + f.size, 0);
35215
- console.log(`Plan: ${planned.length} file${planned.length > 1 ? "s" : ""}, ${formatSize(totalBytes)}`);
35216
- for (const f of planned) {
35217
- console.log(`${f.localPath} ${dim("\u2192")} ${f.virtualPath} (${formatSize(f.size)})`);
35218
- }
35219
- if (opts.dryRun) {
35220
- console.log("\n--dry-run: skipping all network calls.");
35221
- return;
35222
- }
35223
- const concurrency = Math.max(1, parseInt(opts.concurrency ?? String(UPLOAD_CONCURRENCY), 10));
35224
- const uploadOpts = { mime: opts.mime };
35225
- let cursor = 0;
35226
- let uploaded = 0, skipped = 0, resumed = 0, failed = 0;
35227
- const workers = [];
35228
- for (let w = 0; w < Math.min(concurrency, planned.length); w++) {
35229
- workers.push((async () => {
35230
- while (true) {
35231
- const idx = cursor++;
35232
- if (idx >= planned.length) return;
35233
- const f = planned[idx];
35234
- try {
35235
- const result = await uploadOneFile(config.projectGuid, f.localPath, f.virtualPath, uploadOpts);
35236
- if (result.status === "skipped") {
35237
- skipped++;
35238
- console.log(`${dim("skip")} ${f.virtualPath} (already current)`);
35239
- } else if (result.status === "resumed") {
35240
- resumed++;
35241
- console.log(`${dim("resumed")} ${f.virtualPath} v${result.version}`);
35242
- } else {
35243
- uploaded++;
35244
- console.log(`${dim("uploaded")} ${f.virtualPath} v${result.version}`);
35245
- }
35246
- } catch (err) {
35247
- failed++;
35248
- console.error(error(` fail ${f.virtualPath}: ${err.message}`));
35249
- }
35250
- }
35251
- })());
35252
- }
35253
- await Promise.all(workers);
35254
- console.log(`
35255
- Uploaded: ${uploaded}, Resumed: ${resumed}, Skipped: ${skipped}, Failed: ${failed}`);
35256
- if (failed > 0) process.exit(1);
35262
+ const st2 = statSync4(file);
35263
+ if (!st2.isFile()) throw new Error("not a regular file");
35264
+ size = st2.size;
35257
35265
  } catch (err) {
35258
- console.error(error(`Upload failed: ${err.message}`));
35259
- process.exit(1);
35266
+ throw new Error(`can't read ${file}: ${err.message}`);
35267
+ }
35268
+ if (size === 0) throw new Error(`${file} is empty - nothing to upload.`);
35269
+ const filename = basename2(file);
35270
+ const contentType = opts.contentType || guessMime(file);
35271
+ const doUpload = async () => {
35272
+ const init = await post(`/api/${config.projectGuid}/uploads/init`, {
35273
+ filename,
35274
+ content_type: contentType,
35275
+ size,
35276
+ public: !opts.private
35277
+ });
35278
+ const fields = await transferToS3(file, size, contentType, init.data);
35279
+ const completeBody = {
35280
+ upload_guid: init.data.upload_guid
35281
+ };
35282
+ if ("parts" in fields) completeBody.parts = fields.parts;
35283
+ const comp = await post(`/api/${config.projectGuid}/uploads/complete`, completeBody);
35284
+ return comp.data;
35285
+ };
35286
+ const data = opts.json ? await doUpload() : await withSpinner(`Uploading ${filename} (${formatSize(size)})\u2026`, doUpload, { done: null });
35287
+ if (opts.json) {
35288
+ console.log(JSON.stringify(data));
35289
+ return;
35260
35290
  }
35261
- });
35291
+ console.log(success(`Uploaded ${data.name} (${formatSize(data.size)})`));
35292
+ console.log(` ${brand(data.url)}`);
35293
+ console.log(muted(` Durable, worker-reachable URL - pass it straight to a job/function as an input URL.`));
35294
+ console.log(muted(` ${data.is_public ? "public (CDN)" : "private (token-signed)"} \xB7 guid: ${data.guid}`));
35295
+ }));
35262
35296
 
35263
35297
  // src/commands/deploy.ts
35264
35298
  init_api();
@@ -35273,7 +35307,7 @@ init_colors();
35273
35307
  init_auth();
35274
35308
 
35275
35309
  // src/commands/page-eval.ts
35276
- import { readFileSync as readFileSync12 } from "node:fs";
35310
+ import { readFileSync as readFileSync13 } from "node:fs";
35277
35311
  init_api();
35278
35312
  init_colors();
35279
35313
  init_auth();
@@ -35282,8 +35316,8 @@ init_config();
35282
35316
  // src/page-fixtures.ts
35283
35317
  init_api();
35284
35318
  init_config();
35285
- import { readFileSync as readFileSync11, statSync as statSync5, existsSync as existsSync10, readdirSync as readdirSync7 } from "node:fs";
35286
- import { basename as basename3, resolve as resolve11, relative as relative3, join as join11 } from "node:path";
35319
+ import { readFileSync as readFileSync12, statSync as statSync5, existsSync as existsSync10, readdirSync as readdirSync6 } from "node:fs";
35320
+ import { basename as basename3, resolve as resolve10, relative as relative3, join as join10 } from "node:path";
35287
35321
  var FIND_SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", ".gipity", "dist", "build", ".next", "coverage"]);
35288
35322
  function findByBasename(root, name, limit = 3) {
35289
35323
  const hits = [];
@@ -35294,22 +35328,22 @@ function findByBasename(root, name, limit = 3) {
35294
35328
  visited++;
35295
35329
  let entries;
35296
35330
  try {
35297
- entries = readdirSync7(dir, { withFileTypes: true });
35331
+ entries = readdirSync6(dir, { withFileTypes: true });
35298
35332
  } catch {
35299
35333
  continue;
35300
35334
  }
35301
35335
  for (const e of entries) {
35302
35336
  if (e.isDirectory()) {
35303
- if (!FIND_SKIP.has(e.name)) queue.push(join11(dir, e.name));
35337
+ if (!FIND_SKIP.has(e.name)) queue.push(join10(dir, e.name));
35304
35338
  } else if (e.name === name && hits.length < limit) {
35305
- hits.push(join11(dir, e.name));
35339
+ hits.push(join10(dir, e.name));
35306
35340
  }
35307
35341
  }
35308
35342
  }
35309
35343
  return hits;
35310
35344
  }
35311
35345
  function assertLocalAsset(flag, localPath) {
35312
- const abs = resolve11(localPath);
35346
+ const abs = resolve10(localPath);
35313
35347
  if (existsSync10(abs)) return;
35314
35348
  const root = getProjectRoot() ?? process.cwd();
35315
35349
  const elsewhere = findByBasename(root, basename3(localPath)).filter((p) => p !== abs);
@@ -35336,7 +35370,7 @@ async function uploadPublicFixture(projectGuid, localPath, flag = "--fixture") {
35336
35370
  const res = await fetch(init.data.url, {
35337
35371
  method: "PUT",
35338
35372
  headers: { "Content-Type": contentType },
35339
- body: readFileSync11(localPath)
35373
+ body: readFileSync12(localPath)
35340
35374
  });
35341
35375
  if (!res.ok) {
35342
35376
  throw new Error(`upload of fixture "${name}" failed: ${res.status} ${res.statusText}`);
@@ -35435,6 +35469,9 @@ function capWaitForTimeoutMs(raw) {
35435
35469
  ));
35436
35470
  return WAIT_FOR_MAX_MS;
35437
35471
  }
35472
+ function readScriptFile(pathArg) {
35473
+ return readFileSync13(pathArg === "-" ? 0 : pathArg, "utf8");
35474
+ }
35438
35475
  function parsesAsExpression(s) {
35439
35476
  try {
35440
35477
  new Function(`return (${s}
@@ -35455,6 +35492,7 @@ var CAMERA_SETUP_BUDGET_MS = 3e4;
35455
35492
  var EVAL_SCRIPT_BUDGET_MS = 3e4;
35456
35493
  var EVAL_SCRIPT_BUDGET_CAMERA_MS = 45e3;
35457
35494
  var EVAL_SCRIPT_BUDGET_MAX_MS = 9e4;
35495
+ var EVAL_SCRIPT_BUDGET_MIN_MS = 1e3;
35458
35496
  function capScriptBudgetMs(rawTimeout, hasMedia) {
35459
35497
  const fallback = hasMedia ? EVAL_SCRIPT_BUDGET_CAMERA_MS : EVAL_SCRIPT_BUDGET_MS;
35460
35498
  if (rawTimeout === void 0) return fallback;
@@ -35466,7 +35504,7 @@ function capScriptBudgetMs(rawTimeout, hasMedia) {
35466
35504
  ));
35467
35505
  return EVAL_SCRIPT_BUDGET_MAX_MS;
35468
35506
  }
35469
- return Math.max(1e3, parsed);
35507
+ return Math.max(EVAL_SCRIPT_BUDGET_MIN_MS, parsed);
35470
35508
  }
35471
35509
  function evalWorkBudgetMs(o) {
35472
35510
  const script = o.scriptBudgetMs ?? (o.hasCamera ? EVAL_SCRIPT_BUDGET_CAMERA_MS : EVAL_SCRIPT_BUDGET_MS);
@@ -35528,14 +35566,14 @@ function evalExecTimeoutMessage(result, budgetMs) {
35528
35566
  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.`;
35529
35567
  }
35530
35568
  var JS_DECOY_FLAGS = ["--js", "--javascript", "--script", "--code", "--expr", "--eval", "--exec", "--action"];
35531
- 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; await works, and the trailing expression is returned automatically \u2014 REPL-style \u2014 so no explicit return is needed; 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(
35569
+ 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; await works, and the trailing expression is returned automatically \u2014 REPL-style \u2014 so no explicit return is needed; 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), or --file - to read it from stdin (pipe a heredoc: --file - <<'EOF' \u2026 EOF) with no tmp file. Runs as an async function body, so top-level return/await work. Same post-load budget as <expr> (--timeout).`).option(
35532
35570
  "--step <expr>",
35533
35571
  `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.`,
35534
35572
  (val, prev) => [...prev, val],
35535
35573
  []
35536
35574
  ).option(
35537
35575
  "--fixture <path>",
35538
- "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>).",
35576
+ '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. The hosted URL has permissive CORS, so `new Image(); img.crossOrigin="anonymous"; img.src=fixtureUrl` decodes untainted for a canvas/pixel read - this is the UPLOAD analog of --camera: feed a known photo/video to a file-upload vision app (web-vision-detect) and run its detector, no need to deploy a throwaway test asset into the app. Repeat for several files (single-value so it never swallows the inline <expr>).',
35539
35577
  (val, prev) => [...prev, val],
35540
35578
  []
35541
35579
  ).option(
@@ -35546,7 +35584,7 @@ var pageEvalCommand = new Command("eval").description("Evaluate JS in a real bro
35546
35584
  `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".`
35547
35585
  ).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(
35548
35586
  "--timeout <ms>",
35549
- `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.`
35587
+ `How long the script itself may run IN the page. Bare number = MILLISECONDS (so 90s = 90000, NOT 90); or pass an explicit unit that means the same on both this and \`sandbox run --timeout\` \u2014 --timeout 90s. Its own await/setTimeout pauses count (default ${EVAL_SCRIPT_BUDGET_MS}, ${EVAL_SCRIPT_BUDGET_CAMERA_MS} with --camera/--fake-media; floor 1000, 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.`
35550
35588
  ).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. Without this flag the page loads as a genuinely anonymous, signed-out visitor \u2014 nothing carries over from earlier --auth runs.").option("--json", "Output as JSON").action((url, exprArg, opts) => run("Page eval", async () => {
35551
35589
  const decoy = JS_DECOY_FLAGS.find((f) => opts[f.slice(2)] !== void 0);
35552
35590
  if (decoy) {
@@ -35570,9 +35608,9 @@ var pageEvalCommand = new Command("eval").description("Evaluate JS in a real bro
35570
35608
  let expr = exprArg;
35571
35609
  if (opts.file) {
35572
35610
  try {
35573
- expr = readFileSync12(opts.file, "utf8");
35611
+ expr = readScriptFile(opts.file);
35574
35612
  } catch {
35575
- pageEvalCommand.error(`error: Cannot read file: ${opts.file}`);
35613
+ pageEvalCommand.error(opts.file === "-" ? `error: --file - reads the script from stdin, but stdin was empty/unreadable \u2014 pipe it in, e.g. gipity page eval "<url>" --file - <<'EOF' \u2026 EOF` : `error: Cannot read file: ${opts.file}`);
35576
35614
  }
35577
35615
  }
35578
35616
  if (opts.reload !== void 0 && opts.reloadFile) {
@@ -35581,7 +35619,7 @@ var pageEvalCommand = new Command("eval").description("Evaluate JS in a real bro
35581
35619
  let reloadExpr = opts.reload;
35582
35620
  if (opts.reloadFile) {
35583
35621
  try {
35584
- reloadExpr = readFileSync12(opts.reloadFile, "utf8");
35622
+ reloadExpr = readScriptFile(opts.reloadFile);
35585
35623
  } catch {
35586
35624
  pageEvalCommand.error(`error: Cannot read file: ${opts.reloadFile}`);
35587
35625
  }
@@ -35592,6 +35630,20 @@ var pageEvalCommand = new Command("eval").description("Evaluate JS in a real bro
35592
35630
  `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`
35593
35631
  );
35594
35632
  }
35633
+ if (opts.timeout !== void 0) {
35634
+ const dur = parseDuration(opts.timeout, "ms");
35635
+ if (dur?.hadSuffix) opts.timeout = String(Math.round(dur.value));
35636
+ }
35637
+ if (opts.timeout !== void 0) {
35638
+ const t = parseInt(opts.timeout, 10);
35639
+ if (Number.isFinite(t) && t > 0 && t < EVAL_SCRIPT_BUDGET_MIN_MS) {
35640
+ const overMax = t * 1e3 > EVAL_SCRIPT_BUDGET_MAX_MS;
35641
+ const asMs = Math.min(t * 1e3, EVAL_SCRIPT_BUDGET_MAX_MS);
35642
+ throw new Error(
35643
+ `--timeout is in MILLISECONDS (got ${t}, under the ${EVAL_SCRIPT_BUDGET_MIN_MS}ms in-page floor). Unlike \`sandbox run --timeout\` (seconds), page eval's is ms \u2014 or pass an explicit unit that means the same on both: --timeout ${t}s. ${t} looks like seconds, so for ${t} second${t === 1 ? "" : "s"} pass --timeout ${t}s (= ${asMs}ms)${overMax ? ` (${t}s is over the ${EVAL_SCRIPT_BUDGET_MAX_MS / 1e3}s max, so this is the ceiling)` : ""}.`
35644
+ );
35645
+ }
35646
+ }
35595
35647
  let waitMs = capWaitMs(opts.wait, url);
35596
35648
  const waitForTimeoutMs = capWaitForTimeoutMs(opts.waitTimeout);
35597
35649
  const hasMedia = !!opts.camera || !!opts.fakeMedia;
@@ -35706,7 +35758,7 @@ ${hint}`);
35706
35758
  }
35707
35759
  if (camera) console.log(`${muted("Camera:")} ${camera.name} ${muted("(played as the webcam feed; getUserMedia resolves)")}`);
35708
35760
  if (hosted.length) console.log(`${muted("Fixtures:")} ${hosted.map((h) => h.name).join(", ")}`);
35709
- console.error(opts.file ? `${muted("Script:")} ${opts.file}` : `${muted("Expression:")} ${summarizeExpr(expr)}`);
35761
+ console.error(opts.file ? `${muted("Script:")} ${opts.file === "-" ? "(stdin)" : opts.file}` : `${muted("Expression:")} ${summarizeExpr(expr)}`);
35710
35762
  console.log(`
35711
35763
  ${result.trim() ? result : muted("(empty result)")}`);
35712
35764
  if (noValue) console.log(muted(`
@@ -35739,7 +35791,7 @@ ${bold("After reload")} ${muted("(page reloaded in place \u2014 storage preserve
35739
35791
  }
35740
35792
  }));
35741
35793
  for (const f of JS_DECOY_FLAGS) pageEvalCommand.addOption(new Option(`${f} <value>`).hideHelp());
35742
- pageEvalCommand.addHelpText("after", `
35794
+ pageEvalCommand.addHelpText("after", (context) => context.error ? "" : `
35743
35795
  Examples:
35744
35796
  gipity page eval "https://dev.gipity.ai/me/app/" "document.title"
35745
35797
  # Functionally test a page's own code paths: save a script that drives the UI
@@ -35775,6 +35827,16 @@ Examples:
35775
35827
  "window.__vision.gesture()" \\
35776
35828
  --step "document.getElementById('see').textContent" \\
35777
35829
  --step "({ score: score.textContent, verdict: verdict.textContent })"
35830
+ # File-UPLOAD vision app (web-vision-detect, no camera)? --fixture is the analog of
35831
+ # --camera: it hosts your test photo at a CORS-permissive URL, so you feed the kit's
35832
+ # detector a known image and read back the labels - no deploying a throwaway asset into
35833
+ # the app tree (and no cleanup redeploy). crossOrigin='anonymous' keeps the canvas clean.
35834
+ gipity generate image "street scene: three people, two cars, one dog" -o street.jpg
35835
+ gipity page eval "https://dev.gipity.ai/me/app/" --fixture street.jpg \\
35836
+ "const { createDetector } = await import('./packages/web-vision-detect/index.js'); \\
35837
+ const det = await createDetector({ model: 'nano' }); \\
35838
+ const img = new Image(); img.crossOrigin = 'anonymous'; img.src = fixtureUrl; await img.decode(); \\
35839
+ const { detections } = await det.detect(img); return detections.map(d => d.label);"
35778
35840
 
35779
35841
  Module resolution: dynamic import() specifiers starting with ./ or ../ resolve
35780
35842
  against the PAGE URL, so import('./packages/i18n/index.js') loads the app's own
@@ -35815,7 +35877,7 @@ init_auth();
35815
35877
  init_colors();
35816
35878
  init_utils();
35817
35879
  import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
35818
- import { dirname as dirname7, join as join12, resolve as resolvePath } from "path";
35880
+ import { dirname as dirname6, join as join11, resolve as resolvePath } from "path";
35819
35881
  var DEVICE_PRESETS = {
35820
35882
  default: { width: 1280, height: 720 },
35821
35883
  desktop: { width: 1920, height: 1080 },
@@ -35879,7 +35941,7 @@ function dimSuffix(vp2) {
35879
35941
  }
35880
35942
  function defaultScreenshotDir() {
35881
35943
  const root = getProjectRoot();
35882
- return join12(root ?? ".", "screenshots");
35944
+ return join11(root ?? ".", "screenshots");
35883
35945
  }
35884
35946
  function timestampSlug(d = /* @__PURE__ */ new Date()) {
35885
35947
  const p = (n) => String(n).padStart(2, "0");
@@ -35942,14 +36004,25 @@ var WAIT_FOR_MAX_MS2 = 15e3;
35942
36004
  var WAIT_FOR_DEFAULT_MS = 15e3;
35943
36005
  var MAX_POST_LOAD_DELAY_MS = 3e4;
35944
36006
  var CAMERA_DEFAULT_DELAY_MS = 15e3;
35945
- var pageScreenshotCommand = new Command("screenshot").description("Screenshot a web page").argument("<url>", "URL to screenshot").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), e.g. --viewport 390x844 for a phone-sized window", appendOption, []).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("--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. Without this flag the page loads as a genuinely anonymous, signed-out visitor \u2014 nothing carries over from earlier --auth runs.").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()).addOption(new Option("--width <px>", "Alias: --viewport WxH").hideHelp()).addOption(new Option("--height <px>", "Alias: --viewport WxH").hideHelp()).action((url, opts) => run("Page screenshot", async () => {
36007
+ var pageScreenshotCommand = new Command("screenshot").description("Screenshot a web page").argument("<url>", "URL to screenshot").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), e.g. --viewport 390x844 for a phone-sized window", appendOption, []).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("--file <path>", "Read the pre-capture script from a file instead of inline --action (mutually exclusive), or --file - to read it from stdin (pipe a heredoc: --file - <<'EOF' \u2026 EOF) with no tmp file. For a multi-step driver \u2014 click through a flow, wait for it to render \u2014 then capture. Same async-function-body semantics as --action; same --file flag as page eval.").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("--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. Without this flag the page loads as a genuinely anonymous, signed-out visitor \u2014 nothing carries over from earlier --auth runs.").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()).addOption(new Option("--width <px>", "Alias: --viewport WxH").hideHelp()).addOption(new Option("--height <px>", "Alias: --viewport WxH").hideHelp()).action((url, opts) => run("Page screenshot", async () => {
35946
36008
  const aliasFlag = ACTION_ALIAS_FLAGS.find((f) => opts[f.slice(2)] !== void 0);
35947
36009
  if (aliasFlag) {
35948
36010
  console.error(muted(
35949
36011
  `Note: treating ${aliasFlag} as --action \u2014 it runs your JS in the page before the capture. --action is the canonical flag.`
35950
36012
  ));
35951
36013
  }
35952
- const actionScript = [opts.action, ...ACTION_ALIAS_FLAGS.map((f) => opts[f.slice(2)])].filter(Boolean).join("\n");
36014
+ const inlineAction = [opts.action, ...ACTION_ALIAS_FLAGS.map((f) => opts[f.slice(2)])].filter(Boolean).join("\n");
36015
+ if (opts.file && inlineAction) {
36016
+ throw new Error("Pass either --file <path> or an inline --action script, not both");
36017
+ }
36018
+ let actionScript = inlineAction;
36019
+ if (opts.file) {
36020
+ try {
36021
+ actionScript = readScriptFile(opts.file);
36022
+ } catch {
36023
+ throw new Error(opts.file === "-" ? `--file - reads the pre-capture script from stdin, but stdin was empty/unreadable \u2014 pipe it in, e.g. gipity page screenshot "<url>" --file - <<'EOF' \u2026 EOF` : `Cannot read file: ${opts.file}`);
36024
+ }
36025
+ }
35953
36026
  const delayRaw = opts.wait ?? opts.postLoadDelay;
35954
36027
  const chosenDelay = delayRaw !== void 0;
35955
36028
  let postLoadDelayMs = chosenDelay ? parseInt(String(delayRaw), 10) : 1e3;
@@ -36063,8 +36136,8 @@ var pageScreenshotCommand = new Command("screenshot").description("Screenshot a
36063
36136
  const dir = defaultScreenshotDir();
36064
36137
  const savedFiles = [];
36065
36138
  for (let i = 0; i < pngs.length; i++) {
36066
- const target = opts.output ? opts.output : join12(dir, names[i] ?? shotName(meta.screenshots[i].viewport));
36067
- mkdirSync7(dirname7(target), { recursive: true });
36139
+ const target = opts.output ? opts.output : join11(dir, names[i] ?? shotName(meta.screenshots[i].viewport));
36140
+ mkdirSync7(dirname6(target), { recursive: true });
36068
36141
  writeFileSync8(target, pngs[i].buffer);
36069
36142
  savedFiles.push(resolvePath(target));
36070
36143
  }
@@ -36162,9 +36235,17 @@ Waiting for the page to reach a state before the shot?
36162
36235
 
36163
36236
  Capturing a state that needs an interaction (start a game, open a menu, dismiss a modal)?
36164
36237
  Use --action to run JS in the page before the shot \u2014 it fires after the wait
36165
- (and after any --wait-for gate), then settles again so the result has painted. Do
36166
- NOT hand-roll a 'page eval' that returns a base64 image: the eval result is capped
36167
- (~16KB) and truncates the PNG.
36238
+ (and after any --wait-for gate), then settles again so the result has painted. For
36239
+ a multi-step driver (click, wait, click) pipe it as a heredoc with --file - (same
36240
+ as 'page eval') instead of cramming it into one inline string:
36241
+ gipity page screenshot "https://dev.gipity.ai/me/app/" --file - <<'EOF'
36242
+ document.getElementById('load-sample').click();
36243
+ await new Promise(r => setTimeout(r, 500));
36244
+ document.getElementById('run').click();
36245
+ await new Promise(r => setTimeout(r, 2000));
36246
+ EOF
36247
+ Do NOT hand-roll a 'page eval' that returns a base64 image: the eval result is
36248
+ capped (~16KB) and truncates the PNG.
36168
36249
 
36169
36250
  Capturing an off-screen region or reading element data?
36170
36251
  \u2022 --full captures the ENTIRE scrollable page (then crop to the region).
@@ -36213,7 +36294,17 @@ async function inspectPage(url, opts = {}) {
36213
36294
  auth: opts.auth || void 0
36214
36295
  };
36215
36296
  const res = await post(`/tools/browser/inspect`, inspectBody);
36216
- const b7 = res.data;
36297
+ const b7 = res?.data;
36298
+ if (!b7 || typeof b7 !== "object") {
36299
+ if (opts.json) {
36300
+ console.log(JSON.stringify({ url, captured: false, note: "inspector returned no page data" }));
36301
+ return;
36302
+ }
36303
+ console.log(`${brand("Inspecting")} ${bold(url)}`);
36304
+ console.log(warning("Captured no page data \u2014 the page returned an empty response."));
36305
+ console.log(muted("The page may have failed to load, redirected away, or rendered nothing server-side. Retry once; if it persists pass --wait-for <selector> to wait for a specific element to appear, or --auth if it is behind Sign in with Gipity."));
36306
+ return;
36307
+ }
36217
36308
  const isPlatformLog = (entry) => {
36218
36309
  const urlPart = entry.replace(/\s*\([^)]*\)\s*$/, "");
36219
36310
  try {
@@ -36269,6 +36360,9 @@ async function inspectPage(url, opts = {}) {
36269
36360
  console.log(`${muted("Title:")} ${b7.title || "(none)"}`);
36270
36361
  console.log(`${muted("Elements:")} ${b7.elementCount || 0}`);
36271
36362
  console.log(`${muted("Page weight:")} ${info(formatSize(b7.totalBytes || 0))}`);
36363
+ if ((b7.elementCount || 0) === 0 && !b7.title) {
36364
+ console.log(`${warning("\u26A0 Rendered no DOM (0 elements, no title)")} ${muted("\u2014 the client-side render likely had not run yet (try --wait-for <selector>), the page failed to load, or it is behind a sign-in gate (try --auth).")}`);
36365
+ }
36272
36366
  console.log(`
36273
36367
  ${bold("Timing:")}`);
36274
36368
  console.log(`${muted("TTFB:")} ${timing.ttfb}ms`);
@@ -36455,7 +36549,7 @@ var deployCommand = new Command("deploy").description("Deploy to dev or prod").a
36455
36549
  console.log(success(`\u2713 Deployed to ${target}`) + muted(` (${d.elapsedMs}ms)`));
36456
36550
  if (d.url) console.log(`${muted("Live:")} ${brand(d.url)}`);
36457
36551
  if (d.phases?.some((p) => p.name === "functions")) {
36458
- console.log(`${muted("Functions:")} POST ${brand(`${config.apiBase}/api/${config.projectGuid}/fn/<name>`)}`);
36552
+ console.log(`${muted("Functions:")} POST ${brand(`${resolveApiBase()}/api/${config.projectGuid}/fn/<name>`)}`);
36459
36553
  console.log(muted(" (same address on dev and prod; names: gipity fn list; verify the public path: gipity fn call <name> --anon)"));
36460
36554
  }
36461
36555
  await inspectAfter();
@@ -36640,8 +36734,8 @@ memoryCommand.command("delete <topic>").description("Delete a topic").option("--
36640
36734
  // src/commands/sandbox.ts
36641
36735
  init_api();
36642
36736
  init_config();
36643
- import { readFileSync as readFileSync13, existsSync as existsSync11, statSync as statSync6, mkdirSync as mkdirSync8, writeFileSync as writeFileSync9 } from "fs";
36644
- import { dirname as dirname8, extname as extname4, relative as relative4, resolve as resolve12, sep as sep3 } from "path";
36737
+ import { readFileSync as readFileSync14, existsSync as existsSync11, statSync as statSync6, mkdirSync as mkdirSync8, writeFileSync as writeFileSync9 } from "fs";
36738
+ import { dirname as dirname7, extname as extname4, relative as relative4, resolve as resolve11, sep as sep3 } from "path";
36645
36739
  init_colors();
36646
36740
  var LANG_MAP = {
36647
36741
  js: "javascript",
@@ -36750,16 +36844,42 @@ function explainSplitArgs(args) {
36750
36844
  );
36751
36845
  return lines.join("\n");
36752
36846
  }
36847
+ var SCRATCH_DIR_PATTERNS = SCRATCH_IGNORE.map((p) => p.replace(/\/$/, "")).filter(Boolean).map((p) => p.replace(/[.]/g, "\\.").replace(/\*/g, "[A-Za-z0-9._-]*"));
36848
+ function scratchRefsInCode(code) {
36849
+ const dirs = SCRATCH_DIR_PATTERNS.join("|");
36850
+ if (!dirs) return [];
36851
+ const re2 = new RegExp(`(?:\\/work\\/|(?<![\\w/.]))(?:${dirs})\\/[A-Za-z0-9._\\-\\/]+`, "g");
36852
+ return [...new Set((code.match(re2) ?? []).map((m) => m.replace(/^\/work\//, "")))];
36853
+ }
36854
+ function isWriteTarget(code, p) {
36855
+ const path5 = p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
36856
+ const m = new RegExp(`(?:\\/work\\/)?${path5}(?![\\w./-])`).exec(code);
36857
+ if (!m) return false;
36858
+ const idx = m.index;
36859
+ const before = code.slice(Math.max(0, idx - 40), idx);
36860
+ if (/(?:^|[\s'"`;(])(?:-o|-of|--out(?:put)?[= ]|>>?|tee(?:\s+-a)?)\s*['"`]?(?:\/work\/)?$/.test(before)) return true;
36861
+ const segStart = Math.max(
36862
+ code.lastIndexOf("&&", idx),
36863
+ code.lastIndexOf("||", idx),
36864
+ code.lastIndexOf(";", idx),
36865
+ code.lastIndexOf("|", idx),
36866
+ code.lastIndexOf("\n", idx)
36867
+ );
36868
+ const seg = code.slice(segStart + 1, idx).replace(/^[\s&|;]+/, "");
36869
+ const cmd = /^\s*([A-Za-z0-9_./-]+)/.exec(seg)?.[1]?.split("/").pop()?.toLowerCase();
36870
+ const afterInputFlag = /(?:^|[\s'"`])(?:-i|--input)[= ]\s*['"`]?$/.test(before);
36871
+ return cmd === "ffmpeg" && !afterInputFlag;
36872
+ }
36753
36873
  function resolveRelativeCwd() {
36754
36874
  const configPath = getConfigPath();
36755
36875
  if (!configPath) return void 0;
36756
- const projectRoot = dirname8(configPath);
36876
+ const projectRoot = dirname7(configPath);
36757
36877
  const rel2 = relative4(projectRoot, process.cwd());
36758
36878
  if (!rel2 || rel2.startsWith("..")) return void 0;
36759
36879
  return rel2.split(/[\\/]/).filter(Boolean).join("/");
36760
36880
  }
36761
36881
  var sandboxCommand = new Command("sandbox").description("Run code in a sandbox");
36762
- sandboxCommand.command("run [args...]").description("Run code").option("--language <language>", "Language: js, py, or bash (required unless pinned by an interpreter token or --file extension)").option("--file <path>", "Read the code body from a file instead of the inline <code> arg; --language is inferred from the extension when not given").addOption(new Option("--code <code>", "Alias for the positional inline <code> arg").hideHelp()).option("--timeout <seconds>", "Execution timeout in seconds", "30").option(
36882
+ sandboxCommand.command("run [args...]").description("Run code").option("--language <language>", "Language: js, py, or bash (required unless pinned by an interpreter token or --file extension)").option("--file <path>", "Read the code body from a file instead of the inline <code> arg; --language is inferred from the extension when not given").addOption(new Option("--code <code>", "Alias for the positional inline <code> arg").hideHelp()).addOption(new Option("-c, --cmd <code>", 'Inline code (bash idiom: `sandbox run bash -c "<code>"`)').hideHelp()).addOption(new Option("-e, --eval <code>", 'Inline code (node idiom: `sandbox run node -e "<code>"`)').hideHelp()).option("--timeout <seconds>", "Execution timeout. Bare number = seconds; or pass an explicit unit that means the same on both this and `page eval --timeout`, e.g. --timeout 90s.", "30").option(
36763
36883
  "--input <path>",
36764
36884
  "Narrow to specific project files instead of auto-mirroring the whole tree (repeatable). Use this only for >1 GB projects or when you want surgical control.",
36765
36885
  (v7, prev) => [...prev ?? [], v7]
@@ -36803,6 +36923,8 @@ Examples:
36803
36923
  $ gipity sandbox run --file build_report.py
36804
36924
  $ gipity sandbox run python build_report.py # same thing, interpreter shorthand
36805
36925
  $ gipity sandbox run bash "echo hi; ffmpeg -version" # inline, language pinned
36926
+ $ gipity sandbox run bash -c "ffmpeg -version" # the bash -c idiom also works
36927
+ $ gipity sandbox run node -e "console.log(process.version)" # and node -e
36806
36928
 
36807
36929
  # Surgical: only these files are mirrored in
36808
36930
  $ gipity sandbox run --language bash \\
@@ -36820,12 +36942,15 @@ GCC/Rust).
36820
36942
  let inlineCode;
36821
36943
  let filePath = opts.file;
36822
36944
  let langFromInterp;
36823
- if (opts.code !== void 0) {
36824
- if (args.length) {
36825
- console.error(error("Pass the code once: either positionally or via --code, not both"));
36945
+ const flagCode = opts.code ?? opts.cmd ?? opts.eval;
36946
+ if (flagCode !== void 0) {
36947
+ const leadIsInterp = args.length === 1 && INTERPRETERS[args[0].toLowerCase()] !== void 0;
36948
+ if (args.length > (leadIsInterp ? 1 : 0)) {
36949
+ console.error(error("Pass the code once: either positionally or via --code/--cmd/-c/--eval/-e, not both"));
36826
36950
  process.exit(1);
36827
36951
  }
36828
- args = [opts.code];
36952
+ const interp = leadIsInterp ? args[0] : opts.cmd !== void 0 ? "bash" : opts.eval !== void 0 ? "node" : void 0;
36953
+ args = interp ? [interp, flagCode] : [flagCode];
36829
36954
  }
36830
36955
  if (args.length >= 2 && INTERPRETERS[args[0].toLowerCase()] !== void 0) {
36831
36956
  langFromInterp = INTERPRETERS[args[0].toLowerCase()];
@@ -36839,7 +36964,9 @@ GCC/Rust).
36839
36964
  process.exit(1);
36840
36965
  }
36841
36966
  if (inlineCode !== void 0 && filePath) {
36842
- console.error(error("Pass either an inline <code> arg or --file <path>, not both"));
36967
+ console.error(error("Pass the code ONE way, not both: an inline <code> arg OR --file <path>."));
36968
+ console.error(dim(" inline: gipity sandbox run bash 'echo hi'"));
36969
+ console.error(dim(" file: gipity sandbox run --file script.sh"));
36843
36970
  process.exit(1);
36844
36971
  }
36845
36972
  if (inlineCode === void 0 && !filePath) {
@@ -36849,7 +36976,7 @@ GCC/Rust).
36849
36976
  let source = inlineCode;
36850
36977
  if (filePath) {
36851
36978
  try {
36852
- source = readFileSync13(filePath, "utf8");
36979
+ source = readFileSync14(filePath, "utf8");
36853
36980
  } catch {
36854
36981
  console.error(error(`Cannot read file: ${filePath}`));
36855
36982
  process.exit(1);
@@ -36862,7 +36989,8 @@ GCC/Rust).
36862
36989
  process.exit(1);
36863
36990
  }
36864
36991
  const { config } = await resolveProjectContext();
36865
- const timeout = parseInt(opts.timeout, 10);
36992
+ const durOpt = parseDuration(opts.timeout, "s");
36993
+ const timeout = durOpt ? Math.max(1, Math.round(durOpt.value)) : parseInt(opts.timeout, 10);
36866
36994
  const cwd = resolveRelativeCwd();
36867
36995
  const scratchInputs = (opts.input ?? []).filter(
36868
36996
  (p) => shouldIgnore(p.replace(/\\/g, "/").replace(/^\.\//, ""), SCRATCH_IGNORE)
@@ -36873,6 +37001,18 @@ GCC/Rust).
36873
37001
  console.error(dim(" Stage inputs at a real project path (src/, docs/, assets/) and delete them afterward."));
36874
37002
  process.exit(1);
36875
37003
  }
37004
+ if (source) {
37005
+ const scratchReads = scratchRefsInCode(source).filter(
37006
+ (p) => shouldIgnore(p, SCRATCH_IGNORE) && existsSync11(resolve11(process.cwd(), p)) && !isWriteTarget(source, p)
37007
+ );
37008
+ if (scratchReads.length) {
37009
+ console.error(error(`Scratch files are never mirrored into the sandbox, so it can't read: ${scratchReads.join(", ")}`));
37010
+ console.error(dim(` ${SCRATCH_IGNORE.join(", ")} are ignored by sync, so the sandbox never sees them.`));
37011
+ console.error(dim(" Stage the input at a real project path (src/, docs/, assets/) and delete it afterward."));
37012
+ console.error(dim(" (Writing OUTPUT under tmp/ is fine \u2014 scratch outputs come back to your local tmp/.)"));
37013
+ process.exit(1);
37014
+ }
37015
+ }
36876
37016
  if (getConfigPath()) {
36877
37017
  await sync({ interactive: false, progress: opts.json ? void 0 : createProgressReporter() });
36878
37018
  }
@@ -36895,12 +37035,12 @@ GCC/Rust).
36895
37035
  }
36896
37036
  const scratchWritten = [];
36897
37037
  if (res.data.scratchFiles?.length) {
36898
- const base = resolve12(getProjectRoot() ?? process.cwd());
37038
+ const base = resolve11(getProjectRoot() ?? process.cwd());
36899
37039
  for (const f of res.data.scratchFiles) {
36900
37040
  const rel2 = f.path.replace(/\\/g, "/");
36901
- const abs = resolve12(base, rel2);
37041
+ const abs = resolve11(base, rel2);
36902
37042
  if (abs !== base && !abs.startsWith(base + sep3)) continue;
36903
- mkdirSync8(dirname8(abs), { recursive: true });
37043
+ mkdirSync8(dirname7(abs), { recursive: true });
36904
37044
  writeFileSync9(abs, Buffer.from(f.contentBase64, "base64"));
36905
37045
  scratchWritten.push(rel2);
36906
37046
  }
@@ -36924,7 +37064,7 @@ GCC/Rust).
36924
37064
  }
36925
37065
  if (res.data.outputFiles && res.data.outputFiles.length > 0) {
36926
37066
  const projectRoot = getProjectRoot();
36927
- const landed = (f) => pulledLocal && !!projectRoot && existsSync11(resolve12(projectRoot, f));
37067
+ const landed = (f) => pulledLocal && !!projectRoot && existsSync11(resolve11(projectRoot, f));
36928
37068
  const onDisk = res.data.outputFiles.filter(landed);
36929
37069
  const notOnDisk = res.data.outputFiles.filter((f) => !landed(f));
36930
37070
  if (onDisk.length > 0) {
@@ -36936,6 +37076,12 @@ GCC/Rust).
36936
37076
  for (const f of notOnDisk) console.log(`${f}`);
36937
37077
  if (pulledLocal) console.log(dim("Not pulled locally - run 'gipity sync' to fetch them."));
36938
37078
  }
37079
+ const rootStrays = [...onDisk, ...notOnDisk].filter((f) => !f.includes("/"));
37080
+ if (rootStrays.length > 0) {
37081
+ console.log(dim(`
37082
+ Note: ${rootStrays.join(", ")} landed at the project root and will deploy with the files phase.`));
37083
+ console.log(dim("For a throwaway probe or fixture, write it under tmp/ instead - scratch outputs stay on local disk and never sync or deploy."));
37084
+ }
36939
37085
  }
36940
37086
  if (scratchWritten.length > 0) {
36941
37087
  console.log("\nScratch outputs (local only - never synced or deployed):");
@@ -36945,7 +37091,27 @@ GCC/Rust).
36945
37091
  console.log(dim("\nDiscarded (--discard-output):"));
36946
37092
  for (const f of res.data.skippedOutputFiles) console.log(dim(`${f}`));
36947
37093
  }
37094
+ if (res.data.exitCode === 0) {
37095
+ const emptyScratch = (res.data.scratchFiles ?? []).filter((f) => f.contentBase64.length === 0).map((f) => f.path.replace(/\\/g, "/"));
37096
+ const root = getProjectRoot();
37097
+ const emptyOutputs = pulledLocal && root ? (res.data.outputFiles ?? []).filter((f) => {
37098
+ try {
37099
+ return statSync6(resolve11(root, f)).size === 0;
37100
+ } catch {
37101
+ return false;
37102
+ }
37103
+ }) : [];
37104
+ const empties = [.../* @__PURE__ */ new Set([...emptyScratch, ...emptyOutputs])];
37105
+ if (empties.length > 0) {
37106
+ console.error(dim(`Note: ${empties.join(", ")} was written but is empty (0 bytes) - the command exited 0 but likely failed silently (a broken filter, or an out-of-memory kill the shell swallowed). Re-check the command, or shrink the job if it may have run out of memory.`));
37107
+ }
37108
+ }
36948
37109
  if (res.data.exitCode !== 0) {
37110
+ if (res.data.exitCode === 137) {
37111
+ console.error(error("The sandbox process was killed (exit 137) - it ran out of memory."));
37112
+ console.error(dim(" Shrink the job: for ffmpeg, downscale (-vf scale=640:-2) and add -preset ultrafast;"));
37113
+ console.error(dim(" for ImageMagick, lower -density/-resize. Process in smaller chunks if it still OOMs."));
37114
+ }
36949
37115
  process.exit(res.data.exitCode);
36950
37116
  }
36951
37117
  }
@@ -37047,7 +37213,7 @@ chatCommand.command("delete <guid>").description("Delete a chat").option("--json
37047
37213
  // src/commands/project.ts
37048
37214
  init_api();
37049
37215
  init_config();
37050
- import { join as join13 } from "path";
37216
+ import { join as join12 } from "path";
37051
37217
  import { mkdirSync as mkdirSync9 } from "fs";
37052
37218
  init_colors();
37053
37219
  init_utils();
@@ -37105,7 +37271,7 @@ projectCommand.command("create <name>").description("Create a project").option("
37105
37271
  }
37106
37272
  const res = await post("/projects", body);
37107
37273
  const project = res.data;
37108
- const dir = join13(getProjectsRoot(), project.slug);
37274
+ const dir = join12(getProjectsRoot(), project.slug);
37109
37275
  mkdirSync9(dir, { recursive: true });
37110
37276
  const accountSlug = await getAccountSlug();
37111
37277
  let agentGuid = "";
@@ -37361,7 +37527,7 @@ function printStepRuns(steps, emptyNote) {
37361
37527
  }
37362
37528
  }
37363
37529
  var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
37364
- var sleep2 = (ms2) => new Promise((resolve20) => setTimeout(resolve20, ms2));
37530
+ var sleep2 = (ms2) => new Promise((resolve19) => setTimeout(resolve19, ms2));
37365
37531
  async function waitForRun(wfGuid, prevGuid, timeoutSec) {
37366
37532
  const deadline = Date.now() + timeoutSec * 1e3;
37367
37533
  let runGuid;
@@ -37821,6 +37987,19 @@ fileCommand.command("cat <path>").description("Read a file").option("--json", "O
37821
37987
  process.stdout.write(res.data.content);
37822
37988
  }
37823
37989
  }));
37990
+ fileCommand.command("url <path>").description("Durable public URL for a project file - no deploy needed. Reachable by browsers, GPU jobs, and external services the moment the file syncs; ideal for throwaway test fixtures (delete the file to revoke the link).").option("--download", "URL forces a download (Content-Disposition: attachment)").option("--json", "Output as JSON").action((path5, opts) => run("Url", async () => {
37991
+ const { config } = await resolveProjectContext();
37992
+ const qs = new URLSearchParams({ path: path5 });
37993
+ if (opts.download) qs.set("download", "1");
37994
+ const res = await get(
37995
+ `/projects/${config.projectGuid}/files/url?${qs.toString()}`
37996
+ );
37997
+ if (opts.json) {
37998
+ console.log(JSON.stringify(res.data));
37999
+ } else {
38000
+ console.log(res.data.url);
38001
+ }
38002
+ }));
37824
38003
  fileCommand.command("tree [path]").description("Show the file tree").option("--json", "Output as JSON").action((path5, opts) => run("Tree", async () => {
37825
38004
  const { config } = await resolveProjectContext();
37826
38005
  const query = path5 ? `?path=${encodeURIComponent(path5)}` : "";
@@ -37982,8 +38161,8 @@ storageCommand.command("retention").description("View or adjust your file versio
37982
38161
  init_platform();
37983
38162
  init_auth();
37984
38163
  init_api();
37985
- import { join as join17, dirname as dirname10, resolve as resolve14, basename as basename4, sep as sep5 } from "path";
37986
- import { existsSync as existsSync14, mkdirSync as mkdirSync12, readFileSync as readFileSync17, writeFileSync as writeFileSync12, renameSync as renameSync2, readdirSync as readdirSync8, statSync as statSync7 } from "fs";
38164
+ import { join as join16, dirname as dirname9, resolve as resolve13, basename as basename4, sep as sep5 } from "path";
38165
+ import { existsSync as existsSync14, mkdirSync as mkdirSync12, readFileSync as readFileSync18, writeFileSync as writeFileSync12, renameSync as renameSync2, readdirSync as readdirSync7, statSync as statSync7 } from "fs";
37987
38166
  import { homedir as homedir12 } from "os";
37988
38167
  import { fileURLToPath as fileURLToPath2 } from "url";
37989
38168
  import { randomUUID } from "crypto";
@@ -38131,11 +38310,11 @@ function buildFreshWrap(contextBlock, userMsg) {
38131
38310
  // src/relay/onboarding.ts
38132
38311
  init_utils();
38133
38312
  init_colors();
38134
- import { readFileSync as readFileSync15 } from "node:fs";
38313
+ import { readFileSync as readFileSync16 } from "node:fs";
38135
38314
 
38136
38315
  // src/relay/installers.ts
38137
38316
  import { homedir as homedir8, platform as osPlatform } from "os";
38138
- import { join as join14 } from "path";
38317
+ import { join as join13 } from "path";
38139
38318
  var UnsupportedPlatformError = class extends Error {
38140
38319
  constructor(plat) {
38141
38320
  super(`gipity relay install does not support ${plat}`);
@@ -38153,8 +38332,8 @@ function display(argvs) {
38153
38332
  }
38154
38333
  function launchdPlan(cliPath) {
38155
38334
  const label2 = "ai.gipity.relay";
38156
- const path5 = join14(homedir8(), "Library", "LaunchAgents", `${label2}.plist`);
38157
- const logDir = join14(homedir8(), "Library", "Logs", "Gipity");
38335
+ const path5 = join13(homedir8(), "Library", "LaunchAgents", `${label2}.plist`);
38336
+ const logDir = join13(homedir8(), "Library", "Logs", "Gipity");
38158
38337
  const uid = process.getuid?.() ?? 0;
38159
38338
  const target = `gui/${uid}`;
38160
38339
  const content = `<?xml version="1.0" encoding="UTF-8"?>
@@ -38178,8 +38357,8 @@ function launchdPlan(cliPath) {
38178
38357
  <dict>
38179
38358
  <key>SuccessfulExit</key><false/>
38180
38359
  </dict>
38181
- <key>StandardOutPath</key><string>${join14(logDir, "relay.out.log")}</string>
38182
- <key>StandardErrorPath</key><string>${join14(logDir, "relay.err.log")}</string>
38360
+ <key>StandardOutPath</key><string>${join13(logDir, "relay.out.log")}</string>
38361
+ <key>StandardErrorPath</key><string>${join13(logDir, "relay.err.log")}</string>
38183
38362
  <key>ProcessType</key><string>Background</string>
38184
38363
  </dict>
38185
38364
  </plist>
@@ -38202,7 +38381,7 @@ function launchdPlan(cliPath) {
38202
38381
  }
38203
38382
  function systemdUserPlan(cliPath) {
38204
38383
  const unitName = "gipity-relay.service";
38205
- const path5 = join14(homedir8(), ".config", "systemd", "user", unitName);
38384
+ const path5 = join13(homedir8(), ".config", "systemd", "user", unitName);
38206
38385
  const content = `[Unit]
38207
38386
  Description=Gipity relay - local Claude Code control from the Gipity web CLI
38208
38387
  After=network-online.target
@@ -38240,7 +38419,7 @@ WantedBy=default.target
38240
38419
  }
38241
38420
  function windowsTaskPlan(cliPath) {
38242
38421
  const taskName = "GipityRelay";
38243
- const path5 = join14(homedir8(), "AppData", "Local", "Gipity", "relay-task.xml");
38422
+ const path5 = join13(homedir8(), "AppData", "Local", "Gipity", "relay-task.xml");
38244
38423
  const runsViaNode = /\.[cm]?js$/i.test(cliPath);
38245
38424
  const command = runsViaNode ? process.execPath : cliPath;
38246
38425
  const argLine = runsViaNode ? `"${cliPath}" relay run` : "relay run";
@@ -38302,13 +38481,13 @@ function windowsTaskPlan(cliPath) {
38302
38481
  init_platform();
38303
38482
  init_api();
38304
38483
  import { hostname as hostname3, userInfo, platform as osPlatform2 } from "os";
38305
- import { resolve as resolve13, dirname as dirname9 } from "path";
38484
+ import { resolve as resolve12, dirname as dirname8 } from "path";
38306
38485
  import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
38307
38486
 
38308
38487
  // src/relay/machine-id.ts
38309
38488
  import { createHash as createHash3 } from "crypto";
38310
38489
  import { execFileSync } from "child_process";
38311
- import { readFileSync as readFileSync14 } from "fs";
38490
+ import { readFileSync as readFileSync15 } from "fs";
38312
38491
  import { hostname as hostname2, platform, arch } from "os";
38313
38492
  function rawMachineId() {
38314
38493
  const override = process.env.GIPITY_RELAY_MACHINE_ID?.trim();
@@ -38317,7 +38496,7 @@ function rawMachineId() {
38317
38496
  if (platform() === "linux") {
38318
38497
  for (const path5 of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
38319
38498
  try {
38320
- const id2 = readFileSync14(path5, "utf-8").trim();
38499
+ const id2 = readFileSync15(path5, "utf-8").trim();
38321
38500
  if (id2) return `linux:${id2}`;
38322
38501
  } catch {
38323
38502
  }
@@ -38364,7 +38543,7 @@ function friendlyDeviceName() {
38364
38543
  return host || `My ${kind}`;
38365
38544
  }
38366
38545
  function resolveCliPath() {
38367
- return resolve13(process.argv[1] ?? "gipity");
38546
+ return resolve12(process.argv[1] ?? "gipity");
38368
38547
  }
38369
38548
  async function pairDevice(opts = {}) {
38370
38549
  const existing = getDevice();
@@ -38412,7 +38591,7 @@ function startDaemon() {
38412
38591
  function installAutostart(opts = {}) {
38413
38592
  const stdio = opts.stdio ?? "ignore";
38414
38593
  const plan2 = planFor({ cliPath: resolveCliPath() });
38415
- mkdirSync10(dirname9(plan2.path), { recursive: true });
38594
+ mkdirSync10(dirname8(plan2.path), { recursive: true });
38416
38595
  writeFileSync10(plan2.path, plan2.content);
38417
38596
  let ok = true;
38418
38597
  for (const argv2 of plan2.enableCmds) {
@@ -38441,7 +38620,7 @@ function isWsl() {
38441
38620
  if (process.platform !== "linux") return false;
38442
38621
  if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) return true;
38443
38622
  try {
38444
- return /microsoft/i.test(readFileSync15("/proc/version", "utf-8"));
38623
+ return /microsoft/i.test(readFileSync16("/proc/version", "utf-8"));
38445
38624
  } catch {
38446
38625
  return false;
38447
38626
  }
@@ -38814,7 +38993,7 @@ function printFull(opts, outerW) {
38814
38993
  import { execSync as execSync2 } from "child_process";
38815
38994
  import { existsSync as existsSync12 } from "fs";
38816
38995
  import { homedir as homedir10 } from "os";
38817
- import { join as join15 } from "path";
38996
+ import { join as join14 } from "path";
38818
38997
  var CLAUDE_PACKAGE = "@anthropic-ai/claude-code";
38819
38998
  function claudeInstallPlan(platformOverride) {
38820
38999
  const plat = platformOverride ?? process.platform;
@@ -38833,7 +39012,7 @@ function isClaudeInstalled(platformOverride) {
38833
39012
  }
38834
39013
  function isClaudeAuthenticated() {
38835
39014
  if (process.env.CLAUDE_CODE_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY) return true;
38836
- return existsSync12(join15(homedir10(), ".claude", ".credentials.json"));
39015
+ return existsSync12(join14(homedir10(), ".claude", ".credentials.json"));
38837
39016
  }
38838
39017
  function probeClaudeAuthenticated() {
38839
39018
  if (!isClaudeInstalled()) return false;
@@ -39002,14 +39181,14 @@ function getAdapterBySource(source) {
39002
39181
  }
39003
39182
 
39004
39183
  // src/prefs.ts
39005
- import { existsSync as existsSync13, mkdirSync as mkdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync11 } from "fs";
39184
+ import { existsSync as existsSync13, mkdirSync as mkdirSync11, readFileSync as readFileSync17, writeFileSync as writeFileSync11 } from "fs";
39006
39185
  import { homedir as homedir11 } from "os";
39007
- import { join as join16 } from "path";
39008
- var PREFS_PATH = join16(homedir11(), ".gipity", "prefs.json");
39186
+ import { join as join15 } from "path";
39187
+ var PREFS_PATH = join15(homedir11(), ".gipity", "prefs.json");
39009
39188
  function readPrefs() {
39010
39189
  if (!existsSync13(PREFS_PATH)) return {};
39011
39190
  try {
39012
- const parsed = JSON.parse(readFileSync16(PREFS_PATH, "utf-8"));
39191
+ const parsed = JSON.parse(readFileSync17(PREFS_PATH, "utf-8"));
39013
39192
  return {
39014
39193
  lastAgent: typeof parsed.lastAgent === "string" ? parsed.lastAgent : void 0
39015
39194
  };
@@ -39021,25 +39200,25 @@ function writePrefs(update) {
39021
39200
  try {
39022
39201
  const current = readPrefs();
39023
39202
  const next = { ...current, ...update };
39024
- mkdirSync11(join16(homedir11(), ".gipity"), { recursive: true });
39203
+ mkdirSync11(join15(homedir11(), ".gipity"), { recursive: true });
39025
39204
  writeFileSync11(PREFS_PATH, JSON.stringify(next, null, 2) + "\n");
39026
39205
  } catch {
39027
39206
  }
39028
39207
  }
39029
39208
 
39030
39209
  // src/commands/build.ts
39031
- var __clDir = dirname10(fileURLToPath2(import.meta.url));
39210
+ var __clDir = dirname9(fileURLToPath2(import.meta.url));
39032
39211
  function readOwnPkg(fromDir) {
39033
- for (let d = fromDir; ; d = dirname10(d)) {
39034
- const p = join17(d, "package.json");
39212
+ for (let d = fromDir; ; d = dirname9(d)) {
39213
+ const p = join16(d, "package.json");
39035
39214
  if (existsSync14(p)) {
39036
39215
  try {
39037
- const pkg2 = JSON.parse(readFileSync17(p, "utf-8"));
39216
+ const pkg2 = JSON.parse(readFileSync18(p, "utf-8"));
39038
39217
  if (pkg2.name === "gipity") return pkg2;
39039
39218
  } catch {
39040
39219
  }
39041
39220
  }
39042
- if (dirname10(d) === d) return {};
39221
+ if (dirname9(d) === d) return {};
39043
39222
  }
39044
39223
  }
39045
39224
  var __clPkg = readOwnPkg(__clDir);
@@ -39083,12 +39262,12 @@ function localFsFallback(dir) {
39083
39262
  const topLevelEntries = [];
39084
39263
  const walk = (d, depth) => {
39085
39264
  try {
39086
- for (const name of readdirSync8(d).sort()) {
39265
+ for (const name of readdirSync7(d).sort()) {
39087
39266
  if (isSyncIgnored(name)) continue;
39088
39267
  let isDir = false;
39089
39268
  let size = 0;
39090
39269
  try {
39091
- const st2 = statSync7(join17(d, name));
39270
+ const st2 = statSync7(join16(d, name));
39092
39271
  isDir = st2.isDirectory();
39093
39272
  size = st2.isFile() ? st2.size : 0;
39094
39273
  } catch {
@@ -39097,7 +39276,7 @@ function localFsFallback(dir) {
39097
39276
  if (isDir) {
39098
39277
  folderCount++;
39099
39278
  if (depth === 0) topLevelEntries.push(`${name}/`);
39100
- walk(join17(d, name), depth + 1);
39279
+ walk(join16(d, name), depth + 1);
39101
39280
  } else {
39102
39281
  fileCount++;
39103
39282
  totalBytes += size;
@@ -39130,8 +39309,8 @@ function hasStreamJsonFlag(args) {
39130
39309
  }
39131
39310
  function markFolderTrusted(dir) {
39132
39311
  try {
39133
- const file = join17(homedir12(), ".claude.json");
39134
- const cfg = existsSync14(file) ? JSON.parse(readFileSync17(file, "utf-8")) : {};
39312
+ const file = join16(homedir12(), ".claude.json");
39313
+ const cfg = existsSync14(file) ? JSON.parse(readFileSync18(file, "utf-8")) : {};
39135
39314
  if (typeof cfg !== "object" || cfg === null) return;
39136
39315
  if (typeof cfg.projects !== "object" || cfg.projects === null) cfg.projects = {};
39137
39316
  const entry = typeof cfg.projects[dir] === "object" && cfg.projects[dir] !== null ? cfg.projects[dir] : {};
@@ -39256,9 +39435,9 @@ async function runLaunch(cmdName, cmdCfg, opts) {
39256
39435
  let existing = getConfig();
39257
39436
  let forceAdoptCwd = false;
39258
39437
  if (existing && !opts.newProject && !opts.project && !nonInteractive) {
39259
- const cwdHasConfig = existsSync14(resolve14(process.cwd(), ".gipity.json"));
39438
+ const cwdHasConfig = existsSync14(resolve13(process.cwd(), ".gipity.json"));
39260
39439
  if (!cwdHasConfig && isLikelyEmpty(process.cwd())) {
39261
- const ancestorRoot = dirname10(getConfigPath());
39440
+ const ancestorRoot = dirname9(getConfigPath());
39262
39441
  console.log(` ${bold("You are inside an existing Gipity project.")}
39263
39442
  `);
39264
39443
  console.log(` ${bold("1.")} Continue in ${brand(existing.projectSlug)} ${muted(`(rooted at ${formatCwdLabel(ancestorRoot)})`)}`);
@@ -39305,7 +39484,7 @@ async function runLaunch(cmdName, cmdCfg, opts) {
39305
39484
  }
39306
39485
  project = found;
39307
39486
  }
39308
- const projectDir2 = opts.here ? process.cwd() : join17(getProjectsRoot(), project.slug);
39487
+ const projectDir2 = opts.here ? process.cwd() : join16(getProjectsRoot(), project.slug);
39309
39488
  mkdirSync12(projectDir2, { recursive: true });
39310
39489
  process.chdir(projectDir2);
39311
39490
  clearConfigCache();
@@ -39347,7 +39526,7 @@ async function runLaunch(cmdName, cmdCfg, opts) {
39347
39526
  `);
39348
39527
  setupProjectTools();
39349
39528
  let doSync = true;
39350
- const projectRoot = dirname10(getConfigPath());
39529
+ const projectRoot = dirname9(getConfigPath());
39351
39530
  const scan = scanForAdoption(projectRoot);
39352
39531
  if (scan.tier === "refuse" && !nonInteractive) {
39353
39532
  const sizeStr = scan.truncated ? `>${formatBytes(ADOPT_THRESHOLDS.REFUSE_BYTES)}` : formatBytes(scan.bytes);
@@ -39428,7 +39607,7 @@ async function runLaunch(cmdName, cmdCfg, opts) {
39428
39607
  } else {
39429
39608
  project = result.project;
39430
39609
  isNewProject = result.kind === "create-new";
39431
- const projectDir2 = join17(getProjectsRoot(), project.slug);
39610
+ const projectDir2 = join16(getProjectsRoot(), project.slug);
39432
39611
  mkdirSync12(projectDir2, { recursive: true });
39433
39612
  process.chdir(projectDir2);
39434
39613
  clearConfigCache();
@@ -39810,7 +39989,7 @@ ${doneLine}
39810
39989
  });
39811
39990
  }
39812
39991
  function flushHeadlessCapture(adapter, sessionId, convGuid) {
39813
- const runner = join17(__clDir, "..", "hooks", "capture-runner.js");
39992
+ const runner = join16(__clDir, "..", "hooks", "capture-runner.js");
39814
39993
  if (!existsSync14(runner)) return;
39815
39994
  const payload = JSON.stringify({ session_id: sessionId, cwd: process.cwd() });
39816
39995
  const env = { ...process.env, GIPITY_CONVERSATION_GUID: convGuid };
@@ -39831,32 +40010,39 @@ async function pickOrCreateProject(projects, existingSlugs) {
39831
40010
  const cwd = process.cwd();
39832
40011
  const showAdopt = canAdoptCwd(cwd);
39833
40012
  const reserved = showAdopt ? 2 : 1;
39834
- const maxRecent = 9 - reserved - 1;
39835
- const recent = filtered.slice(0, maxRecent);
39836
- const hasMore = filtered.length > recent.length;
40013
+ const fitsAll = filtered.length <= 9 - reserved;
40014
+ const hasMore = !fitsAll;
40015
+ const searchSlot = hasMore ? reserved + 1 : 0;
40016
+ const projectBase = reserved + (hasMore ? 1 : 0);
40017
+ const recent = fitsAll ? filtered : filtered.slice(0, 9 - reserved - 2);
40018
+ const moreSlot = hasMore ? projectBase + recent.length + 1 : 0;
39837
40019
  while (true) {
39838
40020
  const newProjectLabel = formatNewProjectLabel(existingSlugs);
39839
40021
  console.log(` ${bold("Choose project to open:")}
39840
40022
  `);
39841
- console.log(` ${bold("1.")} Create new project ${muted(`(${newProjectLabel})`)}`);
40023
+ console.log(` ${bold("1.")} ${info("Create new project")} ${muted(`(${newProjectLabel})`)}`);
39842
40024
  if (showAdopt) {
39843
- console.log(` ${bold("2.")} Use this directory ${muted(`(${formatCwdLabel(cwd)})`)}`);
40025
+ console.log(` ${bold("2.")} ${info("Use this directory")} ${muted(`(${formatCwdLabel(cwd)})`)}`);
40026
+ }
40027
+ if (hasMore) {
40028
+ console.log(` ${bold(`${searchSlot}.`)} ${info("Search projects")} ${muted(`(${filtered.length} total)`)}`);
39844
40029
  }
39845
40030
  recent.forEach(
39846
- (p, i) => console.log(` ${bold(`${i + reserved + 1}.`)} ${p.name} ${muted(`(${p.slug})`)}`)
40031
+ (p, i) => console.log(` ${bold(`${projectBase + i + 1}.`)} ${p.name} ${muted(`(${p.slug})`)}`)
39847
40032
  );
39848
40033
  if (hasMore) {
39849
- console.log(` ${bold(`${recent.length + reserved + 1}.`)} Show all projects`);
40034
+ console.log(` ${bold(`${moreSlot}.`)} ${info("Show more")}`);
39850
40035
  }
39851
40036
  console.log("");
39852
- const maxOption = recent.length + reserved + (hasMore ? 1 : 0);
40037
+ const maxOption = projectBase + recent.length + (hasMore ? 1 : 0);
39853
40038
  const idx = await pickOne("Choose", maxOption, 1);
39854
- if (idx > reserved && idx <= recent.length + reserved) {
39855
- return { kind: "pick", project: recent[idx - reserved - 1] };
40039
+ if (idx > projectBase && idx <= projectBase + recent.length) {
40040
+ return { kind: "pick", project: recent[idx - projectBase - 1] };
39856
40041
  }
39857
- if (hasMore && idx === recent.length + reserved + 1) {
39858
- const picked = await pickFromAll(filtered);
40042
+ if (hasMore && (idx === searchSlot || idx === moreSlot)) {
40043
+ const picked = await browseProjects(filtered, { search: idx === searchSlot });
39859
40044
  if (picked) return { kind: "pick", project: picked };
40045
+ console.log("");
39860
40046
  continue;
39861
40047
  }
39862
40048
  if (showAdopt && idx === 2) {
@@ -39871,17 +40057,62 @@ async function pickOrCreateProject(projects, existingSlugs) {
39871
40057
  return { kind: "create-new", project };
39872
40058
  }
39873
40059
  }
39874
- async function pickFromAll(filtered) {
40060
+ var PROJECT_PAGE_SIZE = 10;
40061
+ function searchProjects(all, query) {
40062
+ const q = query.trim().toLowerCase();
40063
+ if (!q) return all;
40064
+ return all.filter((p) => p.name.toLowerCase().includes(q) || p.slug.toLowerCase().includes(q));
40065
+ }
40066
+ async function browseProjects(all, opts = {}) {
40067
+ let items = all;
40068
+ let heading = "All projects";
40069
+ const runSearch = async () => {
40070
+ const q = await prompt(` ${bold("Search projects:")} `);
40071
+ if (!q) return false;
40072
+ const matches = searchProjects(all, q);
40073
+ if (matches.length === 0) {
40074
+ console.log(` ${muted(`No projects match "${q}".`)}`);
40075
+ return false;
40076
+ }
40077
+ items = matches;
40078
+ heading = `Projects matching "${q}"`;
40079
+ return true;
40080
+ };
39875
40081
  console.log("");
39876
- console.log(` ${bold("All projects:")}
40082
+ if (opts.search && !await runSearch()) return null;
40083
+ let shown = 0;
40084
+ while (true) {
40085
+ if (shown < items.length) {
40086
+ const page = items.slice(shown, shown + PROJECT_PAGE_SIZE);
40087
+ if (shown === 0) console.log(` ${bold(`${heading}`)} ${muted(`(${items.length})`)}
39877
40088
  `);
39878
- console.log(` ${bold("1.")} Create new project`);
39879
- filtered.forEach((p, i) => console.log(` ${bold(`${i + 2}.`)} ${p.name} ${muted(`(${p.slug})`)}`));
39880
- console.log("");
39881
- const allChoice = await prompt(` Choose (1-${filtered.length + 1}): `);
39882
- const allIdx = parseInt(allChoice, 10);
39883
- if (allIdx >= 2 && allIdx <= filtered.length + 1) return filtered[allIdx - 2];
39884
- return null;
40089
+ page.forEach(
40090
+ (p, i) => console.log(` ${bold(`${String(shown + i + 1).padStart(3)}.`)} ${p.name} ${muted(`(${p.slug})`)}`)
40091
+ );
40092
+ shown += page.length;
40093
+ console.log("");
40094
+ }
40095
+ const more = shown < items.length;
40096
+ const hints = [
40097
+ more ? `Enter = next ${Math.min(PROJECT_PAGE_SIZE, items.length - shown)}` : null,
40098
+ "number = open",
40099
+ "s = search",
40100
+ "q = back"
40101
+ ].filter(Boolean).join(" \xB7 ");
40102
+ const answer = (await prompt(` ${bold("Choose")} ${muted(`(${hints})`)}: `)).toLowerCase();
40103
+ if (answer === "" && more) continue;
40104
+ if (answer === "" || answer === "q") return null;
40105
+ if (answer === "s") {
40106
+ if (await runSearch()) {
40107
+ shown = 0;
40108
+ console.log("");
40109
+ }
40110
+ continue;
40111
+ }
40112
+ const n = parseInt(answer, 10);
40113
+ if (Number.isInteger(n) && n >= 1 && n <= shown) return items[n - 1];
40114
+ console.log(` ${muted("Type one of the numbers above, Enter for more, s to search, or q to go back.")}`);
40115
+ }
39885
40116
  }
39886
40117
  function formatNewProjectLabel(existingSlugs) {
39887
40118
  const slug = suggestProjectName(existingSlugs);
@@ -39990,12 +40221,10 @@ var KITS = [
39990
40221
  { key: "chatbot", hint: "drop-in chatbot - persona, guardrails, streaming responses" },
39991
40222
  { key: "audio-align", hint: "audio + lyrics -> word-level timing JSON (GPU job)" },
39992
40223
  { key: "i18n", hint: "multi-language web apps - language picker, RTL, translations" },
39993
- { key: "records", hint: "registry-driven data plane - generic CRUD, validation, search, audit spine" },
39994
- { key: "views", hint: "registry-driven UI over records - table, forms, kanban" },
39995
- { key: "agent-api", hint: "named API keys for agent/script writes through the records kit" },
39996
40224
  { key: "contacts", hint: "multi-source contact layer - LinkedIn/Gmail import, dedupe with provenance" },
39997
40225
  { key: "stripe", hint: "charge your users - Stripe checkout, subscriptions, brokered webhooks" },
39998
- { key: "notify", hint: "web push notifications - platform-owned keys, works on iOS home screen" }
40226
+ { key: "notify", hint: "web push notifications - platform-owned keys, works on iOS home screen" },
40227
+ { key: "servicenow", hint: "ServiceNow tables as a data source - OAuth pull/write-back/real-time sync" }
39999
40228
  ];
40000
40229
 
40001
40230
  // src/commands/add.ts
@@ -40958,7 +41187,7 @@ init_api();
40958
41187
  init_config();
40959
41188
  init_colors();
40960
41189
  init_utils();
40961
- var recordsCommand = new Command("records").description("Manage records-kit data (registry-driven CRUD)");
41190
+ var recordsCommand = new Command("records").description("Manage app records (Gipity Records - validated CRUD with audit history)");
40962
41191
  recordsCommand.command("list").description("List record tables").option("--json", "Output as JSON").action((opts) => run("List", async () => {
40963
41192
  const config = requireConfig();
40964
41193
  const res = await get(`/api/${config.projectGuid}/records-config`);
@@ -41017,6 +41246,16 @@ recordsCommand.command("get <table> <id>").description("Get a record").option("-
41017
41246
  const res = await get(`/api/${config.projectGuid}/records/${table}/${id2}`);
41018
41247
  console.log(opts.json ? JSON.stringify(res.data) : JSON.stringify(res.data, null, 2));
41019
41248
  }));
41249
+ recordsCommand.command("history <table> <id>").description("Audit history for a record (who/what changed it, with English summaries)").option("--limit <n>", "Max events", "20").option("--json", "Output as JSON").action((table, id2, opts) => run("History", async () => {
41250
+ const config = requireConfig();
41251
+ const res = await get(
41252
+ `/api/${config.projectGuid}/records/${table}/${id2}/history?limit=${encodeURIComponent(opts.limit)}`
41253
+ );
41254
+ printList(res.data, opts, "No history for this record.", (e) => {
41255
+ const summary = e.detail?.summary || `${e.action} ${e.entity_type} ${e.entity_id}`;
41256
+ return `${muted(e.created_at)} ${bold(e.source || "-")} ${summary}`;
41257
+ });
41258
+ }));
41020
41259
  recordsCommand.command("create <table>").description("Create a record").requiredOption("--data <json>", "JSON object with field values").option("--json", "Output as JSON").action((table, opts) => run("Create", async () => {
41021
41260
  const config = requireConfig();
41022
41261
  const data = JSON.parse(opts.data);
@@ -41055,7 +41294,7 @@ fnCommand.command("list").description("List functions").option("--json", "Output
41055
41294
  ${muted(f.description)}` : line;
41056
41295
  });
41057
41296
  if (!opts.json && res.data.length > 0) {
41058
- console.log(muted(`Endpoint: POST ${config.apiBase}/api/${config.projectGuid}/fn/<name> (same on dev and prod; public path: gipity fn call <name> --anon)`));
41297
+ console.log(muted(`Endpoint: POST ${resolveApiBase()}/api/${config.projectGuid}/fn/<name> (same on dev and prod; public path: gipity fn call <name> --anon)`));
41059
41298
  }
41060
41299
  }));
41061
41300
  fnCommand.command("logs <name>").description("Show recent logs").option("--limit <n>", "Max entries", "20").option("--json", "Output as JSON").action((name, opts) => run("Logs", async () => {
@@ -41094,10 +41333,9 @@ async function callAnon(projectGuid, name, body) {
41094
41333
  appToken ? { "X-App-Token": appToken } : void 0
41095
41334
  );
41096
41335
  }
41097
- fnCommand.command("call <name> [body]").description("Call a function").option("--data <json>", "JSON request body").option("--anon", "Call as an anonymous visitor (the public path a signed-out user hits) instead of as your signed-in account").option("--field <path>", "Print only this field of the result (dot path, e.g. items.0.short_guid)").option("--json", "Output as JSON").action((name, bodyArg, opts) => run("Call", async () => {
41336
+ fnCommand.command("call <name> [body]").description("Call a function").option("-d, --data <json>", "JSON request body: inline JSON, @file to read a file, or @- / - for stdin").option("--file <field=@path>", "Attach a file as { data, media_type } under <field> (the vision/media service shape), repeatable, e.g. --file image=@receipt.png", (v7, acc) => (acc || []).concat(v7)).option("--anon", "Call as an anonymous visitor (the public path a signed-out user hits) instead of as your signed-in account").option("--field <path>", "Print only this field of the result (dot path, e.g. items.0.short_guid)").option("--json", "Output as JSON").action((name, bodyArg, opts) => run("Call", async () => {
41098
41337
  const config = requireConfig();
41099
- const raw = bodyArg || opts.data || "{}";
41100
- const body = JSON.parse(raw);
41338
+ const body = resolveBody(bodyArg || opts.data, opts.file);
41101
41339
  const path5 = `/api/${config.projectGuid}/fn/${encodeURIComponent(name)}`;
41102
41340
  if (opts.anon) {
41103
41341
  console.error(muted("Auth: anonymous visitor (the public path a signed-out user hits)"));
@@ -41146,8 +41384,12 @@ var SERVICES = [
41146
41384
  { name: "location/geocode", method: "POST", desc: "Reverse geocode ({ lat, lon })" }
41147
41385
  ];
41148
41386
  var serviceCommand = new Command("service").description("Call an app service (llm, tts, image, transcribe, ...)").addHelpText("after", "\nCall a Gipity app service: LLM, image, music, TTS, and more.");
41149
- serviceCommand.command("list").description("List callable app services").action(() => run("Services", async () => {
41387
+ serviceCommand.command("list").description("List callable app services").option("--json", "Output the service list as JSON").action((opts) => run("Services", async () => {
41150
41388
  requireConfig();
41389
+ if (opts.json) {
41390
+ console.log(JSON.stringify(SERVICES));
41391
+ return;
41392
+ }
41151
41393
  const width = SERVICES.reduce((m, s) => Math.max(m, s.name.length), 0);
41152
41394
  console.log('Call one with `gipity service call <name> \'{"json":"body"}\'`');
41153
41395
  console.log(muted("(GET endpoints like llm/models, tts/voices take --get and no body)"));
@@ -41466,10 +41708,9 @@ jobCommand.command("list").description("List jobs").option("--json", "Output as
41466
41708
  ${muted(j7.description)}` : line;
41467
41709
  });
41468
41710
  }));
41469
- jobCommand.command("submit <name> [body]").description("Submit a job (returns a run guid; poll with `job status <guid>`)").option("--data <json>", "JSON input body").option("--idempotency-key <key>", "Idempotency key (replays return the existing run)").option("--json", "Output as JSON").action((name, bodyArg, opts) => run("Submit", async () => {
41711
+ jobCommand.command("submit <name> [body]").description("Submit a job (returns a run guid; block on it with `job wait <guid>`)").option("-d, --data <json>", "JSON input body: inline JSON, @file to read a file, or @- / - for stdin").option("--file <field=@path>", "Attach a file as { data, media_type } under <field> (the vision/media service shape), repeatable, e.g. --file image=@scan.png", (v7, acc) => (acc || []).concat(v7)).option("--idempotency-key <key>", "Idempotency key (replays return the existing run)").option("--json", "Output as JSON").action((name, bodyArg, opts) => run("Submit", async () => {
41470
41712
  const config = requireConfig();
41471
- const raw = bodyArg || opts.data || "{}";
41472
- const input = JSON.parse(raw);
41713
+ const input = resolveBody(bodyArg || opts.data, opts.file);
41473
41714
  const body = { input };
41474
41715
  if (opts.idempotencyKey) body.idempotency_key = opts.idempotencyKey;
41475
41716
  const res = await post(
@@ -41481,6 +41722,10 @@ jobCommand.command("submit <name> [body]").description("Submit a job (returns a
41481
41722
  } else {
41482
41723
  const tag = res.data.replayed ? warning(" (replayed)") : "";
41483
41724
  console.log(`${bold(res.data.run_guid)} ${muted(res.data.status)}${tag}`);
41725
+ const TERMINAL = /* @__PURE__ */ new Set(["success", "failed", "cancelled", "canceled", "error"]);
41726
+ if (!TERMINAL.has(res.data.status)) {
41727
+ console.error(muted(`Block until it finishes: gipity job wait ${res.data.run_guid}`));
41728
+ }
41484
41729
  }
41485
41730
  }));
41486
41731
  jobCommand.command("status <runGuid>").description("Show status of a job run").option("--json", "Output as JSON").action((runGuid, opts) => run("Status", async () => {
@@ -41498,6 +41743,47 @@ jobCommand.command("status <runGuid>").description("Show status of a job run").o
41498
41743
  if (r.error) console.log(`${error("error:")} ${r.error}`);
41499
41744
  if (r.output) console.log(`output: ${JSON.stringify(r.output)}`);
41500
41745
  }));
41746
+ jobCommand.command("wait <runGuid>").description("Block until a job run finishes, or exit at --timeout so the shell never hangs (poll again to keep waiting)").option("--timeout <seconds>", "Max seconds to wait before giving up and reporting current progress. Bare number = seconds; an explicit unit (90s) means the same here and on `sandbox run --timeout`.", "90").option("--interval <seconds>", "Seconds between polls", "3").option("--json", "Output as JSON").action((runGuid, opts) => run("Wait", async () => {
41747
+ const config = requireConfig();
41748
+ const TERMINAL = /* @__PURE__ */ new Set(["success", "failed", "cancelled", "canceled", "error"]);
41749
+ const durOpt = parseDuration(opts.timeout, "s");
41750
+ const timeoutSec = durOpt ? Math.max(1, Math.round(durOpt.value)) : parseInt(opts.timeout, 10);
41751
+ const timeout = Number.isFinite(timeoutSec) && timeoutSec > 0 ? timeoutSec : 90;
41752
+ const interval = Math.max(1, parseInt(opts.interval, 10) || 3);
41753
+ const deadline = Date.now() + timeout * 1e3;
41754
+ const fetchRun = async () => (await get(`/projects/${config.projectGuid}/jobs/runs/${encodeURIComponent(runGuid)}`)).data;
41755
+ let r = await fetchRun();
41756
+ let lastProgress = "";
41757
+ while (!TERMINAL.has(r.status) && Date.now() < deadline) {
41758
+ if (!opts.json) {
41759
+ const prog = r.progress_pct != null ? `${Math.round(r.progress_pct * 100)}%${r.progress_message ? ` (${r.progress_message})` : ""}` : r.status;
41760
+ if (prog !== lastProgress) {
41761
+ console.error(muted(`\u2026 ${prog}`));
41762
+ lastProgress = prog;
41763
+ }
41764
+ }
41765
+ const remaining = deadline - Date.now();
41766
+ if (remaining <= 0) break;
41767
+ await new Promise((res) => setTimeout(res, Math.min(interval * 1e3, remaining)));
41768
+ r = await fetchRun();
41769
+ }
41770
+ const done = TERMINAL.has(r.status);
41771
+ if (opts.json) {
41772
+ console.log(JSON.stringify(done ? r : { ...r, waiting: true, timed_out: true }));
41773
+ } else if (!done) {
41774
+ const prog = r.progress_pct != null ? ` ${Math.round(r.progress_pct * 100)}%` : "";
41775
+ console.log(`${warning("still running")}${prog}${r.progress_message ? ` (${r.progress_message})` : ""} ${muted(runGuid)}`);
41776
+ console.log(muted(`Not finished after ${timeout}s. Run \`gipity job wait ${runGuid}\` again to keep waiting.`));
41777
+ } else {
41778
+ const statusColor = r.status === "success" ? success : error;
41779
+ console.log(`${statusColor(r.status)} ${muted(r.guid)}`);
41780
+ if (r.duration_ms != null) console.log(`duration: ${r.duration_ms}ms`);
41781
+ if (r.error) console.log(`${error("error:")} ${r.error}`);
41782
+ if (r.output) console.log(`output: ${JSON.stringify(r.output)}`);
41783
+ }
41784
+ if (!done) process.exit(2);
41785
+ if (r.status !== "success") process.exit(1);
41786
+ }));
41501
41787
  jobCommand.command("runs <name>").description("List recent runs for a job").option("--limit <n>", "Max entries", "20").option("--json", "Output as JSON").action((name, opts) => run("Runs", async () => {
41502
41788
  const config = requireConfig();
41503
41789
  const res = await get(
@@ -41512,19 +41798,44 @@ jobCommand.command("runs <name>").description("List recent runs for a job").opti
41512
41798
  ${error(`error: ${r.error}`)}` : line;
41513
41799
  });
41514
41800
  }));
41515
- jobCommand.command("logs <runGuid>").description("Stream live logs for a job run (--no-follow for a one-shot snapshot)").option("--follow", "Stream via SSE (default)", true).option("--no-follow", "One-shot snapshot of run state instead of streaming").option("--json", "Output as JSON").action((runGuid, opts) => run("Logs", async () => {
41801
+ jobCommand.command("logs <runGuid>").description("Stream live logs for a job run (--no-follow for a one-shot snapshot)").option("--follow", "Stream via SSE (default)", true).option("--no-follow", "One-shot snapshot of run state instead of streaming").option("--timeout <seconds>", "Stop streaming after N seconds and exit cleanly, so a bounded peek at the live log never hangs the shell (no shell `timeout` wrapper needed). Bare number = seconds; unit (20s) also accepted.").option("--json", "Output as JSON").action((runGuid, opts) => run("Logs", async () => {
41516
41802
  const config = requireConfig();
41517
41803
  if (!opts.follow) {
41518
41804
  const res2 = await get(`/projects/${config.projectGuid}/jobs/runs/${encodeURIComponent(runGuid)}`);
41519
41805
  console.log(opts.json ? JSON.stringify(res2.data) : JSON.stringify(res2.data, null, 2));
41520
41806
  return;
41521
41807
  }
41808
+ let peekSeconds = 0;
41809
+ if (opts.timeout != null) {
41810
+ const dur = parseDuration(String(opts.timeout), "s");
41811
+ const secs = dur ? dur.value : parseFloat(String(opts.timeout));
41812
+ peekSeconds = Number.isFinite(secs) && secs > 0 ? secs : 0;
41813
+ }
41522
41814
  const url = `${getBaseUrl()}/projects/${config.projectGuid}/jobs/runs/${encodeURIComponent(runGuid)}/logs/stream`;
41523
41815
  const authHeader = await getAuthHeader();
41524
41816
  const headers = { "Accept": "text/event-stream" };
41525
41817
  if (authHeader) headers.Authorization = authHeader;
41526
- const res = await fetch(url, { headers });
41818
+ const controller = new AbortController();
41819
+ let peekTimer;
41820
+ let peekedOut = false;
41821
+ if (peekSeconds > 0) {
41822
+ peekTimer = setTimeout(() => {
41823
+ peekedOut = true;
41824
+ controller.abort();
41825
+ }, peekSeconds * 1e3);
41826
+ }
41827
+ let res;
41828
+ try {
41829
+ res = await fetch(url, { headers, signal: controller.signal });
41830
+ } catch (e) {
41831
+ if (peekedOut) {
41832
+ console.error(muted(`\u2026 still streaming after ${peekSeconds}s. Run \`gipity job logs ${runGuid} --timeout ${Math.round(peekSeconds)}\` again to keep watching, or \`gipity job wait ${runGuid}\` to block until it finishes.`));
41833
+ return;
41834
+ }
41835
+ throw e;
41836
+ }
41527
41837
  if (!res.ok || !res.body) {
41838
+ if (peekTimer) clearTimeout(peekTimer);
41528
41839
  console.error(error(`Failed to open stream: HTTP ${res.status}`));
41529
41840
  process.exit(1);
41530
41841
  }
@@ -41533,8 +41844,21 @@ jobCommand.command("logs <runGuid>").description("Stream live logs for a job run
41533
41844
  let buffer = "";
41534
41845
  let currentEvent = "";
41535
41846
  while (true) {
41536
- const { done, value } = await reader.read();
41537
- if (done) break;
41847
+ let chunk;
41848
+ try {
41849
+ chunk = await reader.read();
41850
+ } catch (e) {
41851
+ if (peekedOut) {
41852
+ console.error(muted(`\u2026 still streaming after ${peekSeconds}s. Run \`gipity job logs ${runGuid} --timeout ${Math.round(peekSeconds)}\` again to keep watching, or \`gipity job wait ${runGuid}\` to block until it finishes.`));
41853
+ return;
41854
+ }
41855
+ throw e;
41856
+ }
41857
+ const { done, value } = chunk;
41858
+ if (done) {
41859
+ if (peekTimer) clearTimeout(peekTimer);
41860
+ break;
41861
+ }
41538
41862
  buffer += decoder.decode(value, { stream: true });
41539
41863
  let nl2;
41540
41864
  while ((nl2 = buffer.indexOf("\n")) >= 0) {
@@ -41546,9 +41870,9 @@ jobCommand.command("logs <runGuid>").description("Stream live logs for a job run
41546
41870
  const dataRaw = line.slice(5).trim();
41547
41871
  if (currentEvent === "log") {
41548
41872
  try {
41549
- const { type, chunk } = JSON.parse(dataRaw);
41550
- if (type === "stderr") process.stderr.write(error(chunk));
41551
- else process.stdout.write(chunk);
41873
+ const { type, chunk: chunk2 } = JSON.parse(dataRaw);
41874
+ if (type === "stderr") process.stderr.write(error(chunk2));
41875
+ else process.stdout.write(chunk2);
41552
41876
  } catch {
41553
41877
  }
41554
41878
  } else if (currentEvent === "status") {
@@ -41588,11 +41912,11 @@ jobCommand.command("cancel <runGuid>").description("Cancel a queued or running j
41588
41912
  }
41589
41913
  }));
41590
41914
  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) => {
41591
- const { existsSync: existsSync24, statSync: statSync10 } = await import("node:fs");
41592
- const { resolve: resolve20, join: join26 } = await import("node:path");
41915
+ const { existsSync: existsSync24, statSync: statSync11 } = await import("node:fs");
41916
+ const { resolve: resolve19, join: join26 } = await import("node:path");
41593
41917
  const { spawnCommand: spawnCommand2, spawnSyncCommand: spawnSyncCommand2 } = await Promise.resolve().then(() => (init_platform(), platform_exports));
41594
- const jobDir = resolve20(process.cwd(), "jobs", name);
41595
- if (!existsSync24(jobDir) || !statSync10(jobDir).isDirectory()) {
41918
+ const jobDir = resolve19(process.cwd(), "jobs", name);
41919
+ if (!existsSync24(jobDir) || !statSync11(jobDir).isDirectory()) {
41596
41920
  console.error(error(`jobs/${name}/ not found in current directory`));
41597
41921
  process.exit(1);
41598
41922
  }
@@ -41813,8 +42137,8 @@ emailCommand.command("log").description("Recent email() sends from your app").op
41813
42137
  init_api();
41814
42138
  init_config();
41815
42139
  init_colors();
41816
- import { mkdirSync as mkdirSync13, writeFileSync as writeFileSync13 } from "fs";
41817
- import { resolve as resolvePath2, dirname as dirname11, relative as relative5, isAbsolute } from "path";
42140
+ import { mkdirSync as mkdirSync13, writeFileSync as writeFileSync13, readFileSync as readFileSync19 } from "fs";
42141
+ import { resolve as resolvePath2, dirname as dirname10, relative as relative5, isAbsolute, basename as basename5 } from "path";
41818
42142
 
41819
42143
  // src/provider-docs.ts
41820
42144
  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`;
@@ -41828,11 +42152,12 @@ var TTS_PROVIDER_DESCRIPTIONS = {
41828
42152
  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)`;
41829
42153
 
41830
42154
  // src/commands/generate.ts
41831
- async function downloadFile(url, filename) {
42155
+ async function downloadFile(url, filename, explicit) {
41832
42156
  const res = await fetch(url);
41833
42157
  if (!res.ok) throw new Error(`Download failed: ${res.status}`);
41834
42158
  const buffer = Buffer.from(await res.arrayBuffer());
41835
- const outPath = correctExtension(filename, buffer);
42159
+ const outPath = explicit ? filename : correctExtension(filename, buffer);
42160
+ if (explicit) noteFormatMismatch(filename, buffer);
41836
42161
  ensureOutputDir(outPath);
41837
42162
  writeFileSync13(outPath, buffer);
41838
42163
  const savedPath = resolvePath2(outPath);
@@ -41867,8 +42192,18 @@ function correctExtension(filename, buf) {
41867
42192
  ));
41868
42193
  return corrected;
41869
42194
  }
42195
+ function noteFormatMismatch(filename, buf) {
42196
+ const actual = sniffExt(buf);
42197
+ if (!actual) return;
42198
+ const dot = filename.lastIndexOf(".");
42199
+ const asked = dot > 0 ? filename.slice(dot + 1).toLowerCase() : "";
42200
+ if (!asked || canonicalExt(asked) === actual) return;
42201
+ console.error(muted(
42202
+ `Note: ${basename5(filename)} holds ${formatName(actual)} bytes though its name ends .${asked} \u2014 that's fine (browsers and image/video/edit tools sniff the bytes, not the name). Saved at the exact path you requested; reference it as-is.`
42203
+ ));
42204
+ }
41870
42205
  function ensureOutputDir(output) {
41871
- const dir = dirname11(resolvePath2(output));
42206
+ const dir = dirname10(resolvePath2(output));
41872
42207
  try {
41873
42208
  mkdirSync13(dir, { recursive: true });
41874
42209
  } catch (err) {
@@ -41878,7 +42213,7 @@ function ensureOutputDir(output) {
41878
42213
  async function pushGenerated(savedPath) {
41879
42214
  const configPath = getConfigPath();
41880
42215
  if (!configPath) return;
41881
- const rel2 = relative5(dirname11(configPath), savedPath);
42216
+ const rel2 = relative5(dirname10(configPath), savedPath);
41882
42217
  if (rel2.startsWith("..") || isAbsolute(rel2)) return;
41883
42218
  try {
41884
42219
  await pushFile(savedPath);
@@ -41886,22 +42221,46 @@ async function pushGenerated(savedPath) {
41886
42221
  console.error(muted(`Note: couldn't sync to the cloud automatically (${err.message}). Run \`gipity sync\` before referencing this file in \`gipity sandbox run\`.`));
41887
42222
  }
41888
42223
  }
41889
- var imageCommand = new Command("image").description(`Generate an image from a text prompt using AI.
42224
+ function readInputImages(paths) {
42225
+ return paths.map((p) => {
42226
+ let buf;
42227
+ try {
42228
+ buf = readFileSync19(p);
42229
+ } catch (e) {
42230
+ throw new Error(`can't read --input image ${p} (${e.code || e.message}).`);
42231
+ }
42232
+ const mime = guessMime(p);
42233
+ if (!mime.startsWith("image/")) {
42234
+ throw new Error(`--input ${p} is not an image (${mime}). Edit inputs must be image files.`);
42235
+ }
42236
+ return { data: buf.toString("base64"), mime_type: mime };
42237
+ });
42238
+ }
42239
+ var imageCommand = new Command("image").description(`Generate an image from a text prompt, or EDIT existing images with --input.
41890
42240
 
41891
42241
  Models: ${IMAGE_MODELS_DOC}
41892
42242
 
42243
+ Editing (--input): pass one or more source images and the prompt becomes an edit
42244
+ instruction applied to them - "make it night time", "add a hat", "remove the car
42245
+ in the background", or compose several inputs together. Editing routes to Gemini
42246
+ automatically. This is the first-party way to exercise the image-edit flow from
42247
+ the CLI; the CLI reads the file bytes itself, so there is no base64/argv fiddling.
42248
+
41893
42249
  Gemini-specific options:
41894
42250
  --aspect-ratio Control output shape: ${IMAGE_GEMINI_ASPECT_RATIOS}
41895
42251
  --image-size Control resolution: ${IMAGE_GEMINI_SIZES} (default: 1K)
41896
42252
 
41897
42253
  Examples:
41898
42254
  gipity generate image "a cat wearing a top hat"
42255
+ gipity generate image "make it night time" --input photo.jpg -o edited.png
42256
+ gipity generate image "put the person in the first photo into the second scene" --input person.png --input scene.png
41899
42257
  gipity generate image "landscape sunset" --provider gemini --aspect-ratio 16:9 --image-size 2K
41900
42258
  gipity generate image "product photo" --provider openai --model gpt-image-2 --size 1536x1024 --quality high
41901
- gipity generate image "abstract art" --provider bfl --model flux-2-pro -o art.png`).argument("<prompt>", "Text description of the image to generate").option("--provider <provider>", "Image provider: openai, bfl, or gemini (default: bfl)").option("--model <model>", "Model ID (see provider list above)").option("--size <size>", 'Dimensions as WxH, e.g. "1024x1024" (OpenAI/BFL)').option("--quality <quality>", "Quality: low|medium|high|auto (gpt-image-2)").option("--aspect-ratio <ratio>", "Aspect ratio (Gemini only): 1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 4:5, 5:4, 21:9").option("--image-size <size>", "Output resolution (Gemini only): 512, 1K, 2K, 4K").option("--seed <n>", "Deterministic seed (BFL only): reuse one seed to keep a set of images visually coherent", (v7) => parseInt(v7, 10)).option("-o, --output <file>", "Output path (default ./generated.png). For an image your app ships, write it into the source tree so it deploys, e.g. -o src/assets/images/hero.png; the cwd default is fine for one-off generation.").option("--json", "Output as JSON").action(async (prompt2, opts) => {
42259
+ gipity generate image "abstract art" --provider bfl --model flux-2-pro -o art.png`).argument("<prompt>", "Text description of the image, or (with --input) the edit instruction to apply").option("--input <file>", "Source image to edit/compose (repeatable). With --input the prompt is an edit instruction; routes to Gemini.", (v7, acc) => (acc || []).concat(v7)).option("--provider <provider>", "Image provider: openai, bfl, or gemini (default: bfl)").option("--model <model>", "Model ID (see provider list above)").option("--size <size>", 'Dimensions as WxH, e.g. "1024x1024" (OpenAI/BFL)').option("--quality <quality>", "Quality: low|medium|high|auto (gpt-image-2)").option("--aspect-ratio <ratio>", "Aspect ratio (Gemini only): 1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 4:5, 5:4, 21:9").option("--image-size <size>", "Output resolution (Gemini only): 512, 1K, 2K, 4K").option("--seed <n>", "Deterministic seed (BFL only): reuse one seed to keep a set of images visually coherent", (v7) => parseInt(v7, 10)).option("-o, --output <file>", "Output path (default ./generated.png). For an image your app ships, write it into the source tree so it deploys, e.g. -o src/assets/images/hero.png; the cwd default is fine for one-off generation.").option("--json", "Output as JSON").action(async (prompt2, opts) => {
41902
42260
  try {
41903
42261
  const { config } = await resolveProjectContext();
41904
42262
  if (opts.output) ensureOutputDir(opts.output);
42263
+ const inputImages = opts.input ? readInputImages(opts.input) : void 0;
41905
42264
  const doGenerate = () => post(`/projects/${config.projectGuid}/generate/image`, {
41906
42265
  prompt: prompt2,
41907
42266
  provider: opts.provider,
@@ -41910,12 +42269,14 @@ Examples:
41910
42269
  quality: opts.quality,
41911
42270
  aspect_ratio: opts.aspectRatio,
41912
42271
  image_size: opts.imageSize,
41913
- seed: Number.isFinite(opts.seed) ? opts.seed : void 0
42272
+ seed: Number.isFinite(opts.seed) ? opts.seed : void 0,
42273
+ input_images: inputImages
41914
42274
  });
41915
- const result = opts.json ? await doGenerate() : await withSpinner("Generating image\u2026", doGenerate, { done: null });
42275
+ const verb = inputImages ? "Editing image\u2026" : "Generating image\u2026";
42276
+ const result = opts.json ? await doGenerate() : await withSpinner(verb, doGenerate, { done: null });
41916
42277
  const ext = result.content_type.includes("png") ? "png" : "jpg";
41917
42278
  const filename = opts.output || `generated.${ext}`;
41918
- const savedPath = await downloadFile(result.url, filename);
42279
+ const savedPath = await downloadFile(result.url, filename, !!opts.output);
41919
42280
  if (opts.json) {
41920
42281
  console.log(JSON.stringify({ ...result, saved: savedPath }));
41921
42282
  } else {
@@ -41959,7 +42320,7 @@ Examples:
41959
42320
  });
41960
42321
  const result = opts.json ? await doGenerate() : await withSpinner("Generating video\u2026", doGenerate, { done: null });
41961
42322
  const filename = opts.output || "generated.mp4";
41962
- const savedPath = await downloadFile(result.url, filename);
42323
+ const savedPath = await downloadFile(result.url, filename, !!opts.output);
41963
42324
  if (opts.json) {
41964
42325
  console.log(JSON.stringify({ ...result, saved: savedPath }));
41965
42326
  } else {
@@ -42007,7 +42368,7 @@ Examples:
42007
42368
  });
42008
42369
  const result = opts.json ? await doGenerate() : await withSpinner("Generating speech\u2026", doGenerate, { done: null });
42009
42370
  const filename = opts.output || "speech.mp3";
42010
- const savedPath = await downloadFile(result.url, filename);
42371
+ const savedPath = await downloadFile(result.url, filename, !!opts.output);
42011
42372
  if (opts.json) {
42012
42373
  console.log(JSON.stringify({ ...result, saved: savedPath }));
42013
42374
  } else {
@@ -42046,7 +42407,7 @@ Examples:
42046
42407
  });
42047
42408
  const result = opts.json ? await doGenerate() : await withSpinner("Generating music\u2026", doGenerate, { done: null });
42048
42409
  const filename = opts.output || "music.mp3";
42049
- const savedPath = await downloadFile(result.url, filename);
42410
+ const savedPath = await downloadFile(result.url, filename, !!opts.output);
42050
42411
  if (opts.json) {
42051
42412
  console.log(JSON.stringify({ ...result, saved: savedPath }));
42052
42413
  } else {
@@ -42084,7 +42445,7 @@ Examples:
42084
42445
  });
42085
42446
  const result = opts.json ? await doGenerate() : await withSpinner("Generating sound effect\u2026", doGenerate, { done: null });
42086
42447
  const filename = opts.output || "sound.mp3";
42087
- const savedPath = await downloadFile(result.url, filename);
42448
+ const savedPath = await downloadFile(result.url, filename, !!opts.output);
42088
42449
  if (opts.json) {
42089
42450
  console.log(JSON.stringify({ ...result, saved: savedPath }));
42090
42451
  } else {
@@ -42103,14 +42464,14 @@ var generateCommand = new Command("generate").description("Generate images, vide
42103
42464
  init_api();
42104
42465
  init_config();
42105
42466
  init_colors();
42106
- import { existsSync as existsSync15, readFileSync as readFileSync18 } from "fs";
42107
- import { join as join18 } from "path";
42467
+ import { existsSync as existsSync15, readFileSync as readFileSync20 } from "fs";
42468
+ import { join as join17 } from "path";
42108
42469
  function readInstalledKitReadme(name) {
42109
42470
  const root = getProjectRoot() ?? process.cwd();
42110
- const kitDir = join18(root, "src", "packages", name);
42111
- if (!existsSync15(join18(kitDir, "package.json"))) return null;
42112
- const readme = join18(kitDir, "README.md");
42113
- return existsSync15(readme) ? readFileSync18(readme, "utf-8") : null;
42471
+ const kitDir = join17(root, "src", "packages", name);
42472
+ if (!existsSync15(join17(kitDir, "package.json"))) return null;
42473
+ const readme = join17(kitDir, "README.md");
42474
+ return existsSync15(readme) ? readFileSync20(readme, "utf-8") : null;
42114
42475
  }
42115
42476
  var skillCommand = new Command("skill").description("Task docs - read the matching skill before building");
42116
42477
  skillCommand.command("list").description("List skills").option("--json", "Output as JSON").action((opts) => run("List", async () => {
@@ -42284,14 +42645,14 @@ var domainCommand = new Command("domain").description("Manage custom domains").a
42284
42645
 
42285
42646
  // src/commands/realtime.ts
42286
42647
  import { existsSync as existsSync16 } from "fs";
42287
- import { dirname as dirname12, resolve as resolve15 } from "path";
42648
+ import { dirname as dirname11, resolve as resolve14 } from "path";
42288
42649
  init_api();
42289
42650
  init_config();
42290
42651
  init_colors();
42291
42652
  function hasDeployManifest() {
42292
42653
  const cfgPath = getConfigPath();
42293
42654
  if (!cfgPath) return false;
42294
- return existsSync16(resolve15(dirname12(cfgPath), "gipity.yaml"));
42655
+ return existsSync16(resolve14(dirname11(cfgPath), "gipity.yaml"));
42295
42656
  }
42296
42657
  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 () => {
42297
42658
  const config = requireConfig();
@@ -42391,6 +42752,16 @@ function printSkippedCandidates(skipped) {
42391
42752
  if (skipped.length > 10) console.log(muted(` ... and ${skipped.length - 10} more`));
42392
42753
  console.log(muted('Run frontend/kit module tests in the sandbox: gipity sandbox run bash "node <file>"'));
42393
42754
  }
42755
+ function undeployedFunctionsFromFailures(results) {
42756
+ const names = /* @__PURE__ */ new Set();
42757
+ for (const r of results) {
42758
+ if (r.status !== "failed" || !r.error) continue;
42759
+ if (!/404\b/.test(r.error) || !/not\s+found/i.test(r.error)) continue;
42760
+ const m = r.error.match(/Function ['"]?([\w-]+)['"]? (?:not found|failed)/i);
42761
+ if (m) names.add(m[1]);
42762
+ }
42763
+ return [...names];
42764
+ }
42394
42765
  var LONG_RUN_MS = 6e4;
42395
42766
  async function pollTestStatus(projectGuid, runGuid, opts) {
42396
42767
  const startTime = Date.now();
@@ -42467,7 +42838,7 @@ async function pollTestStatus(projectGuid, runGuid, opts) {
42467
42838
  }
42468
42839
  }
42469
42840
  }
42470
- await new Promise((resolve20) => setTimeout(resolve20, getPollInterval()));
42841
+ await new Promise((resolve19) => setTimeout(resolve19, getPollInterval()));
42471
42842
  }
42472
42843
  }
42473
42844
  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 () => {
@@ -42532,6 +42903,15 @@ var testCommand = new Command("test").description("Run tests").argument("[path]"
42532
42903
  }
42533
42904
  console.log("");
42534
42905
  }
42906
+ const undeployed = undeployedFunctionsFromFailures(data.results);
42907
+ if (undeployed.length > 0) {
42908
+ const list = undeployed.map((n) => `'${n}'`).join(", ");
42909
+ console.log(warning(
42910
+ `${undeployed.length === 1 ? "Function" : "Functions"} ${list} 404'd: tests call functions against the DEPLOYED build, and ${undeployed.length === 1 ? "this one is" : "these are"} not deployed yet.`
42911
+ ));
42912
+ console.log(muted("Deploy first, then re-run: gipity deploy dev && gipity test"));
42913
+ console.log("");
42914
+ }
42535
42915
  const parts = [];
42536
42916
  if (data.passed > 0) parts.push(success(`${data.passed} passed`));
42537
42917
  if (data.failed > 0) parts.push(error(`${data.failed} failed`));
@@ -42681,21 +43061,21 @@ var locationCommand = new Command("location").description("Show location").argum
42681
43061
  }));
42682
43062
 
42683
43063
  // src/commands/doctor.ts
42684
- import { existsSync as existsSync18, readFileSync as readFileSync20, statSync as statSync8 } from "fs";
42685
- import { join as join20, resolve as resolve16 } from "path";
43064
+ import { existsSync as existsSync18, readFileSync as readFileSync22, statSync as statSync8 } from "fs";
43065
+ import { join as join19, resolve as resolve15 } from "path";
42686
43066
  import { homedir as homedir14 } from "os";
42687
43067
 
42688
43068
  // src/updater/state.ts
42689
- import { readFileSync as readFileSync19, writeFileSync as writeFileSync14, mkdirSync as mkdirSync14, existsSync as existsSync17 } from "fs";
42690
- import { join as join19 } from "path";
43069
+ import { readFileSync as readFileSync21, writeFileSync as writeFileSync14, mkdirSync as mkdirSync14, existsSync as existsSync17 } from "fs";
43070
+ import { join as join18 } from "path";
42691
43071
  import { homedir as homedir13 } from "os";
42692
- var GIPITY_DIR = join19(homedir13(), ".gipity");
42693
- var LOCAL_DIR = join19(GIPITY_DIR, "local");
42694
- var LOCAL_PKG_DIR = join19(LOCAL_DIR, "node_modules", "gipity");
42695
- var LOCAL_ENTRY = join19(LOCAL_PKG_DIR, "dist", "index.js");
42696
- var STATE_FILE = join19(GIPITY_DIR, "update-state.json");
42697
- var SETTINGS_FILE = join19(GIPITY_DIR, "settings.json");
42698
- var UPDATE_LOG = join19(GIPITY_DIR, "update.log");
43072
+ var GIPITY_DIR = join18(homedir13(), ".gipity");
43073
+ var LOCAL_DIR = join18(GIPITY_DIR, "local");
43074
+ var LOCAL_PKG_DIR = join18(LOCAL_DIR, "node_modules", "gipity");
43075
+ var LOCAL_ENTRY = join18(LOCAL_PKG_DIR, "dist", "index.js");
43076
+ var STATE_FILE = join18(GIPITY_DIR, "update-state.json");
43077
+ var SETTINGS_FILE = join18(GIPITY_DIR, "settings.json");
43078
+ var UPDATE_LOG = join18(GIPITY_DIR, "update.log");
42699
43079
  var DEFAULT_STATE = {
42700
43080
  installedVersion: null,
42701
43081
  lastCheckAt: 0,
@@ -42711,7 +43091,7 @@ function ensureDir() {
42711
43091
  function readState() {
42712
43092
  if (!existsSync17(STATE_FILE)) return { ...DEFAULT_STATE };
42713
43093
  try {
42714
- return { ...DEFAULT_STATE, ...JSON.parse(readFileSync19(STATE_FILE, "utf-8")) };
43094
+ return { ...DEFAULT_STATE, ...JSON.parse(readFileSync21(STATE_FILE, "utf-8")) };
42715
43095
  } catch {
42716
43096
  return { ...DEFAULT_STATE };
42717
43097
  }
@@ -42723,7 +43103,7 @@ function writeState(state) {
42723
43103
  function readSettings() {
42724
43104
  if (!existsSync17(SETTINGS_FILE)) return { ...DEFAULT_SETTINGS };
42725
43105
  try {
42726
- return { ...DEFAULT_SETTINGS, ...JSON.parse(readFileSync19(SETTINGS_FILE, "utf-8")) };
43106
+ return { ...DEFAULT_SETTINGS, ...JSON.parse(readFileSync21(SETTINGS_FILE, "utf-8")) };
42727
43107
  } catch {
42728
43108
  return { ...DEFAULT_SETTINGS };
42729
43109
  }
@@ -42756,10 +43136,10 @@ function versionManagerNode(execPath, env = process.env, home = homedir14()) {
42756
43136
  return null;
42757
43137
  }
42758
43138
  function localVersion() {
42759
- const pkgPath = join20(LOCAL_PKG_DIR, "package.json");
43139
+ const pkgPath = join19(LOCAL_PKG_DIR, "package.json");
42760
43140
  if (!existsSync18(pkgPath)) return null;
42761
43141
  try {
42762
- return JSON.parse(readFileSync20(pkgPath, "utf-8")).version ?? null;
43142
+ return JSON.parse(readFileSync22(pkgPath, "utf-8")).version ?? null;
42763
43143
  } catch {
42764
43144
  return null;
42765
43145
  }
@@ -42767,7 +43147,7 @@ function localVersion() {
42767
43147
  function shimVersion() {
42768
43148
  try {
42769
43149
  const url = new URL("../../package.json", import.meta.url);
42770
- return JSON.parse(readFileSync20(url, "utf-8")).version;
43150
+ return JSON.parse(readFileSync22(url, "utf-8")).version;
42771
43151
  } catch {
42772
43152
  return "unknown";
42773
43153
  }
@@ -42850,7 +43230,7 @@ var doctorCommand = new Command("doctor").description("Check install + environme
42850
43230
  console.log(`${muted("claude code ")} installed ${yn(env.claude.installed)} \xB7 authenticated ${yn(env.claude.authenticated)}`);
42851
43231
  const autostartLabel = env.relay.autostart === null ? muted("n/a") : yn(env.relay.autostart);
42852
43232
  console.log(`${muted("relay ")} paired ${yn(env.relay.paired)} \xB7 running ${yn(env.relay.running)} \xB7 autostart ${autostartLabel}${env.relay.paused ? warning(" \xB7 paused") : ""}${env.relay.device ? muted(` (${env.relay.device.name})`) : ""}`);
42853
- if (existsSync18(resolve16(process.cwd(), ".codex", "hooks.json"))) {
43233
+ if (existsSync18(resolve15(process.cwd(), ".codex", "hooks.json"))) {
42854
43234
  console.log(`${muted("codex hooks ")} ${warning("written")} - session capture needs a one-time approval: run /hooks inside Codex in this project`);
42855
43235
  }
42856
43236
  console.log(`${muted("ready ")} ${env.ready ? success("yes") : warning("no - run `gipity build` (or the desktop app) to finish setup")}`);
@@ -42876,8 +43256,62 @@ var doctorCommand = new Command("doctor").description("Check install + environme
42876
43256
  });
42877
43257
 
42878
43258
  // src/updater/check.ts
42879
- import { appendFileSync, existsSync as existsSync19 } from "fs";
43259
+ import { appendFileSync } from "fs";
43260
+
43261
+ // src/updater/install.ts
43262
+ import { existsSync as existsSync19, mkdirSync as mkdirSync15, rmSync as rmSync2, statSync as statSync9, unlinkSync as unlinkSync5, writeFileSync as writeFileSync15 } from "fs";
43263
+ import { dirname as dirname12, join as join20 } from "path";
42880
43264
  init_platform();
43265
+ var UPDATE_LOCK = join20(GIPITY_DIR, "update.lock");
43266
+ var LOCK_STALE_MS2 = 10 * 60 * 1e3;
43267
+ function npmInstallGipity(version) {
43268
+ const res = spawnSyncCommand(resolveCommand("npm"), ["install", "--no-audit", "--no-fund", "--ignore-scripts", `gipity@${version}`], {
43269
+ cwd: LOCAL_DIR,
43270
+ stdio: ["ignore", "ignore", "pipe"],
43271
+ encoding: "utf-8"
43272
+ });
43273
+ return {
43274
+ ok: res.status === 0 && existsSync19(LOCAL_ENTRY),
43275
+ status: res.status,
43276
+ stderr: (res.stderr || "").toString(),
43277
+ spawnError: res.error
43278
+ };
43279
+ }
43280
+ var WEDGE_CODES = /\b(ENOTEMPTY|EEXIST|ENOTDIR|EISDIR|EJSONPARSE)\b/;
43281
+ function isWedged(stderr) {
43282
+ return WEDGE_CODES.test(stderr);
43283
+ }
43284
+ function resetLocalTree(dir = LOCAL_DIR) {
43285
+ mkdirSync15(dir, { recursive: true });
43286
+ rmSync2(join20(dir, "node_modules"), { recursive: true, force: true });
43287
+ rmSync2(join20(dir, "package-lock.json"), { force: true });
43288
+ writeFileSync15(join20(dir, "package.json"), JSON.stringify({ name: "gipity-local", private: true, version: "0.0.0" }, null, 2));
43289
+ }
43290
+ function acquireUpdateLock(lockPath2 = UPDATE_LOCK) {
43291
+ mkdirSync15(dirname12(lockPath2), { recursive: true });
43292
+ try {
43293
+ writeFileSync15(lockPath2, String(process.pid), { flag: "wx" });
43294
+ return true;
43295
+ } catch {
43296
+ try {
43297
+ if (Date.now() - statSync9(lockPath2).mtimeMs > LOCK_STALE_MS2) {
43298
+ unlinkSync5(lockPath2);
43299
+ writeFileSync15(lockPath2, String(process.pid), { flag: "wx" });
43300
+ return true;
43301
+ }
43302
+ } catch {
43303
+ }
43304
+ return false;
43305
+ }
43306
+ }
43307
+ function releaseUpdateLock(lockPath2 = UPDATE_LOCK) {
43308
+ try {
43309
+ unlinkSync5(lockPath2);
43310
+ } catch {
43311
+ }
43312
+ }
43313
+
43314
+ // src/updater/check.ts
42881
43315
  var CHECK_INTERVAL_MS = 4 * 60 * 60 * 1e3;
42882
43316
  function log(line) {
42883
43317
  try {
@@ -42906,12 +43340,24 @@ async function fetchLatestVersion() {
42906
43340
  return json.version;
42907
43341
  }
42908
43342
  function installVersion(version) {
42909
- const res = spawnSyncCommand(resolveCommand("npm"), ["install", "--silent", "--no-audit", "--no-fund", "--ignore-scripts", `gipity@${version}`], {
42910
- cwd: LOCAL_DIR,
42911
- stdio: "ignore"
42912
- });
42913
- if (res.error) log(`npm spawn failed: ${res.error.message}`);
42914
- return res.status === 0 && existsSync19(LOCAL_ENTRY);
43343
+ let res = npmInstallGipity(version);
43344
+ if (!res.ok && !res.spawnError && isWedged(res.stderr)) {
43345
+ log(`wedged install tree detected; wiping node_modules and retrying:
43346
+ ${res.stderr.trim().slice(-2e3)}`);
43347
+ resetLocalTree();
43348
+ res = npmInstallGipity(version);
43349
+ if (res.ok) log("clean reinstall succeeded");
43350
+ }
43351
+ if (res.ok) return { ok: true };
43352
+ if (res.spawnError) {
43353
+ log(`npm spawn failed: ${res.spawnError.message}`);
43354
+ return { ok: false, detail: res.spawnError.message };
43355
+ }
43356
+ if (res.stderr.trim()) log(`npm stderr (exit ${res.status}):
43357
+ ${res.stderr.trim().slice(-2e3)}`);
43358
+ if (res.status === 0) return { ok: false, detail: "npm succeeded but the installed package is missing dist/index.js" };
43359
+ const firstLine = res.stderr.split("\n").map((l7) => l7.trim()).find((l7) => l7.length > 0) || `npm exit ${res.status}`;
43360
+ return { ok: false, detail: firstLine.length > 160 ? firstLine.slice(0, 157) + "..." : firstLine };
42915
43361
  }
42916
43362
  async function runCheck(opts = {}) {
42917
43363
  const state = readState();
@@ -42944,17 +43390,27 @@ async function runCheck(opts = {}) {
42944
43390
  log(`up-to-date (current=${current}, latest=${latest})`);
42945
43391
  return { updated: false, reason: "up-to-date" };
42946
43392
  }
43393
+ if (!acquireUpdateLock()) {
43394
+ log("skipped: another update is already in progress");
43395
+ return { updated: false, reason: "another update is already in progress" };
43396
+ }
42947
43397
  log(`upgrading ${current} \u2192 ${latest}`);
42948
- const ok = installVersion(latest);
42949
43398
  state.lastCheckAt = Date.now();
42950
- if (ok) {
43399
+ writeState(state);
43400
+ let install;
43401
+ try {
43402
+ install = installVersion(latest);
43403
+ } finally {
43404
+ releaseUpdateLock();
43405
+ }
43406
+ if (install.ok) {
42951
43407
  state.installedVersion = latest;
42952
43408
  state.lastError = null;
42953
43409
  writeState(state);
42954
43410
  log(`upgraded to ${latest}`);
42955
43411
  return { updated: true, from: current, to: latest };
42956
43412
  }
42957
- state.lastError = `npm install gipity@${latest} failed`;
43413
+ state.lastError = `npm install gipity@${latest} failed` + (install.detail ? `: ${install.detail}` : "");
42958
43414
  writeState(state);
42959
43415
  log(state.lastError);
42960
43416
  return { updated: false, reason: state.lastError };
@@ -42978,6 +43434,10 @@ var updateCommand = new Command("update").description("Update the CLI").action(a
42978
43434
  console.log(success("Already on the latest version."));
42979
43435
  } else {
42980
43436
  console.log(warning(`No update applied: ${result.reason}`));
43437
+ if (result.reason?.startsWith("npm install")) {
43438
+ console.log(dim("Full npm output: ~/.gipity/update.log"));
43439
+ console.log(dim("If it keeps failing, delete ~/.gipity/local and run any gipity command to reinstall."));
43440
+ }
42981
43441
  }
42982
43442
  });
42983
43443
 
@@ -42985,13 +43445,13 @@ var updateCommand = new Command("update").description("Update the CLI").action(a
42985
43445
  init_api();
42986
43446
  init_utils();
42987
43447
  init_colors();
42988
- import { existsSync as existsSync22, readFileSync as readFileSync23, unlinkSync as unlinkSync7 } from "fs";
43448
+ import { existsSync as existsSync22, readFileSync as readFileSync25, unlinkSync as unlinkSync8 } from "fs";
42989
43449
  import { spawn as spawn2 } from "child_process";
42990
43450
 
42991
43451
  // src/relay/daemon.ts
42992
43452
  init_platform();
42993
43453
  init_config();
42994
- import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync16, existsSync as existsSync21, readFileSync as readFileSync22, writeFileSync as writeFileSync15, chmodSync as chmodSync3, closeSync as closeSync4, openSync as openSync4, unlinkSync as unlinkSync6 } from "fs";
43454
+ import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync17, existsSync as existsSync21, readFileSync as readFileSync24, writeFileSync as writeFileSync16, chmodSync as chmodSync3, closeSync as closeSync4, openSync as openSync4, unlinkSync as unlinkSync7 } from "fs";
42995
43455
  import { stat, readFile } from "fs/promises";
42996
43456
  import { createInterface as createInterface2 } from "readline";
42997
43457
  import { homedir as homedir16, hostname as hostname5, platform as osPlatform3, loadavg as loadavg2, freemem as freemem2, totalmem as totalmem2, cpus as cpus2 } from "os";
@@ -43751,7 +44211,7 @@ async function revokeRelayAgentToken() {
43751
44211
  init_platform();
43752
44212
  init_client_context();
43753
44213
  import { cpus, freemem, totalmem, loadavg, release, uptime, homedir as homedir15 } from "os";
43754
- import { statfsSync, readdirSync as readdirSync9 } from "fs";
44214
+ import { statfsSync, readdirSync as readdirSync8 } from "fs";
43755
44215
  import { join as join21 } from "path";
43756
44216
  var PROBE_TIMEOUT_MS = 4e3;
43757
44217
  function parseVersion(out) {
@@ -43759,13 +44219,13 @@ function parseVersion(out) {
43759
44219
  return m ? m[0] : void 0;
43760
44220
  }
43761
44221
  function runProbe(cmd, args) {
43762
- return new Promise((resolve20) => {
44222
+ return new Promise((resolve19) => {
43763
44223
  let out = "";
43764
44224
  let settled = false;
43765
44225
  const done = (s) => {
43766
44226
  if (!settled) {
43767
44227
  settled = true;
43768
- resolve20(s);
44228
+ resolve19(s);
43769
44229
  }
43770
44230
  };
43771
44231
  let child;
@@ -43833,7 +44293,7 @@ function diskUsage() {
43833
44293
  }
43834
44294
  function localProjectCount() {
43835
44295
  try {
43836
- return readdirSync9(join21(homedir15(), "GipityProjects"), { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).length;
44296
+ return readdirSync8(join21(homedir15(), "GipityProjects"), { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).length;
43837
44297
  } catch {
43838
44298
  return void 0;
43839
44299
  }
@@ -43964,8 +44424,8 @@ var SessionPool = class {
43964
44424
  const done = new Promise((r) => {
43965
44425
  markDone = r;
43966
44426
  });
43967
- return new Promise((resolve20, reject) => {
43968
- const turn = { onMessage: p.onMessage, resolve: resolve20, reject, interruptRequested: false, wasHot, done, markDone };
44427
+ return new Promise((resolve19, reject) => {
44428
+ const turn = { onMessage: p.onMessage, resolve: resolve19, reject, interruptRequested: false, wasHot, done, markDone };
43969
44429
  session.current = turn;
43970
44430
  session.state = "running";
43971
44431
  session.lastActivityAt = Date.now();
@@ -44260,7 +44720,7 @@ function log2(level, msg, extra) {
44260
44720
  process.stderr.write(pretty + "\n");
44261
44721
  try {
44262
44722
  const dir = join23(homedir16(), ".gipity");
44263
- mkdirSync16(dir, { recursive: true });
44723
+ mkdirSync17(dir, { recursive: true });
44264
44724
  lockLogPerms(dir, RELAY_LOG_PATH);
44265
44725
  const json = JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level, msg, ...extra ?? {} });
44266
44726
  appendFileSync3(RELAY_LOG_PATH, json + "\n");
@@ -44384,10 +44844,10 @@ async function heartbeatLoop(ctx) {
44384
44844
  }
44385
44845
  await Promise.race([
44386
44846
  sleep4(HEARTBEAT_INTERVAL_MS, ctx.abort.signal),
44387
- new Promise((resolve20) => {
44847
+ new Promise((resolve19) => {
44388
44848
  heartbeatPoke = () => {
44389
44849
  heartbeatPoke = null;
44390
- resolve20();
44850
+ resolve19();
44391
44851
  };
44392
44852
  })
44393
44853
  ]);
@@ -44431,9 +44891,9 @@ async function dispatchLoop(ctx, opts) {
44431
44891
  while (inflight.size >= MAX_CONCURRENT_DISPATCHES && !ctx.abort.signal.aborted) {
44432
44892
  await Promise.race([
44433
44893
  ...inflight,
44434
- new Promise((resolve20) => {
44435
- if (ctx.abort.signal.aborted) return resolve20();
44436
- ctx.abort.signal.addEventListener("abort", () => resolve20(), { once: true });
44894
+ new Promise((resolve19) => {
44895
+ if (ctx.abort.signal.aborted) return resolve19();
44896
+ ctx.abort.signal.addEventListener("abort", () => resolve19(), { once: true });
44437
44897
  })
44438
44898
  ]);
44439
44899
  }
@@ -45118,7 +45578,7 @@ async function resolveCwdForProject(d) {
45118
45578
  const configPath = join23(path5, ".gipity.json");
45119
45579
  if (existsSync21(configPath)) {
45120
45580
  try {
45121
- const cfg = JSON.parse(readFileSync22(configPath, "utf-8"));
45581
+ const cfg = JSON.parse(readFileSync24(configPath, "utf-8"));
45122
45582
  if (cfg.projectGuid === d.project_guid) return { cwd: path5, bootstrapped: false };
45123
45583
  log2("warn", "project dir exists but guid mismatch - using it anyway", {
45124
45584
  path: path5,
@@ -45130,9 +45590,9 @@ async function resolveCwdForProject(d) {
45130
45590
  }
45131
45591
  }
45132
45592
  log2("info", "bootstrapping new project dir", { slug: d.project_slug, path: path5 });
45133
- mkdirSync16(path5, { recursive: true });
45593
+ mkdirSync17(path5, { recursive: true });
45134
45594
  const apiBase2 = getApiBaseOverride() || DEFAULT_API_BASE;
45135
- writeFileSync15(configPath, JSON.stringify({
45595
+ writeFileSync16(configPath, JSON.stringify({
45136
45596
  projectGuid: d.project_guid,
45137
45597
  projectSlug: d.project_slug,
45138
45598
  accountSlug: d.account_slug,
@@ -45209,7 +45669,7 @@ async function killRunningForConv(convGuid) {
45209
45669
  }
45210
45670
  }
45211
45671
  const graceTimers = [];
45212
- const escalate = new Promise((resolve20) => {
45672
+ const escalate = new Promise((resolve19) => {
45213
45673
  const t = setTimeout(() => {
45214
45674
  for (const e of matches) {
45215
45675
  log2("warn", "previous dispatch ignored SIGTERM - escalating to SIGKILL", { conv: convGuid });
@@ -45218,7 +45678,7 @@ async function killRunningForConv(convGuid) {
45218
45678
  } catch {
45219
45679
  }
45220
45680
  }
45221
- resolve20();
45681
+ resolve19();
45222
45682
  }, KILL_GRACE_MS);
45223
45683
  graceTimers.push(t);
45224
45684
  });
@@ -45229,7 +45689,7 @@ async function killRunningForConv(convGuid) {
45229
45689
  }
45230
45690
  async function spawnSync2(cwd, timeoutMs, onSpawn) {
45231
45691
  const cmd = process.env.GIPITY_RELAY_CLAUDE_CMD || resolveCommand("gipity");
45232
- return new Promise((resolve20, reject) => {
45692
+ return new Promise((resolve19, reject) => {
45233
45693
  const child = spawnCommand(cmd, ["sync", "--json"], {
45234
45694
  cwd,
45235
45695
  env: childEnv(),
@@ -45253,7 +45713,7 @@ async function spawnSync2(cwd, timeoutMs, onSpawn) {
45253
45713
  } catch {
45254
45714
  }
45255
45715
  try {
45256
- unlinkSync6(join23(cwd, ".gipity", "sync.lock"));
45716
+ unlinkSync7(join23(cwd, ".gipity", "sync.lock"));
45257
45717
  } catch {
45258
45718
  }
45259
45719
  finish(() => reject(new Error(`timed out after ${Math.round(timeoutMs / 1e3)}s`)));
@@ -45269,7 +45729,7 @@ async function spawnSync2(cwd, timeoutMs, onSpawn) {
45269
45729
  child.on("exit", (code) => finish(() => {
45270
45730
  if (code === 0) {
45271
45731
  log2("info", "sync done", { cwd, stdoutLen });
45272
- resolve20();
45732
+ resolve19();
45273
45733
  } else {
45274
45734
  reject(new Error(`gipity sync exited ${code}${stderrBuf ? `: ${stderrBuf.trim().slice(0, 300)}` : ""}`));
45275
45735
  }
@@ -45281,7 +45741,7 @@ async function spawnGipityClaude(args, cwd, d, queue, meta) {
45281
45741
  const cmd = process.env.GIPITY_RELAY_CLAUDE_CMD || resolveCommand("gipity");
45282
45742
  const fullArgs = streamJson ? [...args, "--output-format", "stream-json", "--verbose", "--include-partial-messages"] : [...args];
45283
45743
  const env = streamJson ? childEnv({ GIPITY_CONVERSATION_GUID: d.conversation_guid, GIPITY_CAPTURE: "off" }) : childEnv({ GIPITY_CONVERSATION_GUID: d.conversation_guid });
45284
- return new Promise((resolve20, reject) => {
45744
+ return new Promise((resolve19, reject) => {
45285
45745
  const child = spawnCommand(cmd, fullArgs, { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
45286
45746
  let resolveExited = () => {
45287
45747
  };
@@ -45461,7 +45921,7 @@ async function spawnGipityClaude(args, cwd, d, queue, meta) {
45461
45921
  void postProgress(d.conversation_guid, buildProgressPayload(false));
45462
45922
  if (ownQueue) await q.close();
45463
45923
  cleanup();
45464
- resolve20({
45924
+ resolve19({
45465
45925
  exitCode: code ?? 1,
45466
45926
  killed,
45467
45927
  runtimeLimit,
@@ -45475,10 +45935,10 @@ async function runDispatchSync(d, cwd) {
45475
45935
  let killed = false;
45476
45936
  try {
45477
45937
  await spawnSync2(cwd, PROJECT_SYNC_TIMEOUT_MS, (child) => {
45478
- const exited = new Promise((resolve20) => {
45938
+ const exited = new Promise((resolve19) => {
45479
45939
  child.once("exit", (_code, signal) => {
45480
45940
  if (signal === "SIGTERM") killed = true;
45481
- resolve20();
45941
+ resolve19();
45482
45942
  });
45483
45943
  });
45484
45944
  running.set(d.short_guid, { child, convGuid: d.conversation_guid, exited });
@@ -45509,18 +45969,18 @@ function killDispatch(shortGuid) {
45509
45969
  return false;
45510
45970
  }
45511
45971
  function sleep4(ms2, signal) {
45512
- return new Promise((resolve20) => {
45513
- if (signal?.aborted) return resolve20();
45972
+ return new Promise((resolve19) => {
45973
+ if (signal?.aborted) return resolve19();
45514
45974
  let onAbort = null;
45515
45975
  const t = setTimeout(() => {
45516
45976
  if (onAbort && signal) signal.removeEventListener("abort", onAbort);
45517
- resolve20();
45977
+ resolve19();
45518
45978
  }, ms2);
45519
45979
  if (signal) {
45520
45980
  onAbort = () => {
45521
45981
  clearTimeout(t);
45522
45982
  signal.removeEventListener("abort", onAbort);
45523
- resolve20();
45983
+ resolve19();
45524
45984
  };
45525
45985
  signal.addEventListener("abort", onAbort, { once: true });
45526
45986
  }
@@ -45694,7 +46154,7 @@ relayCommand.command("stop").description("Stop the background service").option("
45694
46154
  console.log(muted("Background service isn't running."));
45695
46155
  return;
45696
46156
  }
45697
- const pid = parseInt(readFileSync23(pidPath, "utf-8").trim(), 10);
46157
+ const pid = parseInt(readFileSync25(pidPath, "utf-8").trim(), 10);
45698
46158
  if (!pid || isNaN(pid)) {
45699
46159
  console.error(error("PID file is empty or malformed."));
45700
46160
  process.exit(1);
@@ -45705,7 +46165,7 @@ relayCommand.command("stop").description("Stop the background service").option("
45705
46165
  if (err?.code === "ESRCH") {
45706
46166
  console.log(muted(`PID ${pid} not running - cleaning up stale PID file.`));
45707
46167
  try {
45708
- unlinkSync7(pidPath);
46168
+ unlinkSync8(pidPath);
45709
46169
  } catch {
45710
46170
  }
45711
46171
  return;
@@ -45819,7 +46279,7 @@ relayCommand.command("revoke").description("Revoke and forget this device").acti
45819
46279
  }
45820
46280
  const pidPath = getDaemonPidPath();
45821
46281
  if (existsSync22(pidPath)) {
45822
- const pid = parseInt(readFileSync23(pidPath, "utf-8").trim(), 10);
46282
+ const pid = parseInt(readFileSync25(pidPath, "utf-8").trim(), 10);
45823
46283
  if (pid && !isNaN(pid)) {
45824
46284
  try {
45825
46285
  process.kill(pid, "SIGTERM");
@@ -45838,7 +46298,7 @@ relayCommand.command("log").description("Tail the service log").option("-n, --li
45838
46298
  }
45839
46299
  const lines = parseInt(opts.lines, 10) || 100;
45840
46300
  try {
45841
- const all = readFileSync23(path5, "utf-8").split("\n");
46301
+ const all = readFileSync25(path5, "utf-8").split("\n");
45842
46302
  const tail = all.slice(-lines - 1).join("\n");
45843
46303
  process.stdout.write(tail);
45844
46304
  } catch (err) {
@@ -45911,15 +46371,15 @@ init_api();
45911
46371
  init_auth();
45912
46372
  init_utils();
45913
46373
  init_colors();
45914
- import { existsSync as existsSync23, rmSync as rmSync3, unlinkSync as unlinkSync8, readFileSync as readFileSync24, writeFileSync as writeFileSync16 } from "fs";
46374
+ import { existsSync as existsSync23, rmSync as rmSync4, unlinkSync as unlinkSync9, readFileSync as readFileSync26, writeFileSync as writeFileSync17 } from "fs";
45915
46375
  import { homedir as homedir17, platform as osPlatform4 } from "os";
45916
- import { join as join24, resolve as resolve18 } from "path";
46376
+ import { join as join24, resolve as resolve17 } from "path";
45917
46377
  function removeGipityPluginConfig() {
45918
46378
  const settingsPath = join24(homedir17(), ".claude", "settings.json");
45919
46379
  if (!existsSync23(settingsPath)) return false;
45920
46380
  let settings;
45921
46381
  try {
45922
- settings = JSON.parse(readFileSync24(settingsPath, "utf-8"));
46382
+ settings = JSON.parse(readFileSync26(settingsPath, "utf-8"));
45923
46383
  } catch {
45924
46384
  return false;
45925
46385
  }
@@ -45934,7 +46394,7 @@ function removeGipityPluginConfig() {
45934
46394
  if (Object.keys(settings.extraKnownMarketplaces).length === 0) delete settings.extraKnownMarketplaces;
45935
46395
  changed = true;
45936
46396
  }
45937
- if (changed) writeFileSync16(settingsPath, JSON.stringify(settings, null, 2) + "\n");
46397
+ if (changed) writeFileSync17(settingsPath, JSON.stringify(settings, null, 2) + "\n");
45938
46398
  return changed;
45939
46399
  }
45940
46400
  function removeInstallerPathLines() {
@@ -45946,7 +46406,7 @@ function removeInstallerPathLines() {
45946
46406
  if (!existsSync23(rc2)) continue;
45947
46407
  let text;
45948
46408
  try {
45949
- text = readFileSync24(rc2, "utf-8");
46409
+ text = readFileSync26(rc2, "utf-8");
45950
46410
  } catch {
45951
46411
  continue;
45952
46412
  }
@@ -45956,7 +46416,7 @@ function removeInstallerPathLines() {
45956
46416
  );
45957
46417
  if (kept.length === lines.length) continue;
45958
46418
  try {
45959
- writeFileSync16(rc2, kept.join("\n"));
46419
+ writeFileSync17(rc2, kept.join("\n"));
45960
46420
  touched.push(rc2);
45961
46421
  } catch {
45962
46422
  }
@@ -45968,7 +46428,7 @@ function tildify2(p) {
45968
46428
  return p === home || p.startsWith(home + "/") ? "~" + p.slice(home.length) : p;
45969
46429
  }
45970
46430
  function resolveCliPath2() {
45971
- return resolve18(process.argv[1] ?? "gipity");
46431
+ return resolve17(process.argv[1] ?? "gipity");
45972
46432
  }
45973
46433
  async function stopDaemon() {
45974
46434
  if (!isDaemonRunning()) return;
@@ -46004,7 +46464,7 @@ function removeServiceUnit() {
46004
46464
  }
46005
46465
  if (existsSync23(plan2.path)) {
46006
46466
  try {
46007
- unlinkSync8(plan2.path);
46467
+ unlinkSync9(plan2.path);
46008
46468
  } catch {
46009
46469
  }
46010
46470
  }
@@ -46073,7 +46533,7 @@ var uninstallCommand = new Command("uninstall").description("Uninstall Gipity").
46073
46533
  if (agentSkills.skills.length) {
46074
46534
  for (const name of agentSkills.skills) {
46075
46535
  try {
46076
- rmSync3(join24(AGENTS_SKILLS_DIR, name), { recursive: true, force: true });
46536
+ rmSync4(join24(AGENTS_SKILLS_DIR, name), { recursive: true, force: true });
46077
46537
  } catch {
46078
46538
  }
46079
46539
  }
@@ -46081,7 +46541,7 @@ var uninstallCommand = new Command("uninstall").description("Uninstall Gipity").
46081
46541
  }
46082
46542
  if (existsSync23(gipityDir)) {
46083
46543
  try {
46084
- rmSync3(gipityDir, { recursive: true, force: true });
46544
+ rmSync4(gipityDir, { recursive: true, force: true });
46085
46545
  console.log(`${success(`Removed ${gipityDir}/`)}`);
46086
46546
  } catch (err) {
46087
46547
  console.error(`${error(`Could not remove ${gipityDir}: ${err?.message || err}`)}`);
@@ -46242,7 +46702,7 @@ gmailCommand.command("reply").description("Reply to a Gmail thread. Get thread-i
46242
46702
  }));
46243
46703
 
46244
46704
  // src/commands/text.ts
46245
- import { readFileSync as readFileSync25 } from "fs";
46705
+ import { readFileSync as readFileSync27 } from "fs";
46246
46706
  init_colors();
46247
46707
 
46248
46708
  // src/helpers/text-analysis.ts
@@ -46397,10 +46857,10 @@ function areAnagrams(a, b7) {
46397
46857
 
46398
46858
  // src/commands/text.ts
46399
46859
  function resolveInput(args, opts) {
46400
- if (opts.file) return readFileSync25(opts.file, "utf-8");
46860
+ if (opts.file) return readFileSync27(opts.file, "utf-8");
46401
46861
  if (args.length) return args.join(" ");
46402
46862
  try {
46403
- return readFileSync25(0, "utf-8");
46863
+ return readFileSync27(0, "utf-8");
46404
46864
  } catch {
46405
46865
  return "";
46406
46866
  }
@@ -46606,7 +47066,7 @@ function collectRealFlags(argv2, program3) {
46606
47066
  }
46607
47067
 
46608
47068
  // src/trace.ts
46609
- import { openSync as openSync5, writeSync, mkdirSync as mkdirSync17 } from "fs";
47069
+ import { openSync as openSync5, writeSync, mkdirSync as mkdirSync18 } from "fs";
46610
47070
  import { join as join25 } from "path";
46611
47071
  import { homedir as homedir18 } from "os";
46612
47072
  var TRACE_KEY = /* @__PURE__ */ Symbol.for("gipity.traceOutput");
@@ -46620,7 +47080,7 @@ function installOutputTrace(label2) {
46620
47080
  return;
46621
47081
  }
46622
47082
  const dir = join25(process.env.GIPITY_DIR || join25(homedir18(), ".gipity"), "trace");
46623
- mkdirSync17(dir, { recursive: true });
47083
+ mkdirSync18(dir, { recursive: true });
46624
47084
  const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", "_").replace(/:/g, "-");
46625
47085
  const fd2 = openSync5(join25(dir, `${stamp}-pid${process.pid}.jsonl`), "a");
46626
47086
  const emit = (rec) => {
@@ -46658,11 +47118,11 @@ function installOutputTrace(label2) {
46658
47118
  // src/index.ts
46659
47119
  installOutputTrace("index");
46660
47120
  var __dirname = dirname13(fileURLToPath3(import.meta.url));
46661
- var pkg = JSON.parse(readFileSync26(resolve19(__dirname, "../package.json"), "utf-8"));
47121
+ var pkg = JSON.parse(readFileSync28(resolve18(__dirname, "../package.json"), "utf-8"));
46662
47122
  function versionLabel() {
46663
47123
  const base = `v${pkg.version}`;
46664
47124
  try {
46665
- const info2 = JSON.parse(readFileSync26(resolve19(__dirname, "build-info.json"), "utf-8"));
47125
+ const info2 = JSON.parse(readFileSync28(resolve18(__dirname, "build-info.json"), "utf-8"));
46666
47126
  if (info2?.sha) return `${base} (dev ${info2.sha}${info2.dirty ? ", modified" : ""})`;
46667
47127
  } catch {
46668
47128
  }
@@ -46867,6 +47327,19 @@ function resolveTargetCommand(argv2) {
46867
47327
  }
46868
47328
  return cmd;
46869
47329
  }
47330
+ Command.prototype._excessArguments = function(receivedArgs) {
47331
+ if (this._allowExcessArguments) return;
47332
+ const expected = this.registeredArguments.length;
47333
+ const excess = receivedArgs.slice(expected);
47334
+ const forSubcommand = this.parent ? ` for '${this.name()}'` : "";
47335
+ const list = excess.map((a) => `'${a}'`).join(", ");
47336
+ let message = `error: unexpected extra argument${excess.length === 1 ? "" : "s"} ${list}${forSubcommand}.`;
47337
+ const kv2 = excess.map((a) => /^([A-Za-z][\w-]*)=/.exec(a)).find(Boolean);
47338
+ if (kv2) {
47339
+ message += ` Options are passed as \`--${kv2[1]} <value>\`, not \`${kv2[1]}=\u2026\` - did you mean \`--${kv2[1]}\`?`;
47340
+ }
47341
+ this.error(message, { code: "commander.excessArguments" });
47342
+ };
46870
47343
  function enableHelpAfterError(cmd) {
46871
47344
  cmd.configureOutput({
46872
47345
  // Render the offending command's full help (via outputHelp, so addHelpText