gipity 1.1.3 → 1.1.5

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.
Files changed (47) hide show
  1. package/dist/adopt-cwd.js +1 -1
  2. package/dist/banner.js +3 -5
  3. package/dist/catalog.js +1 -3
  4. package/dist/commands/add.js +1 -1
  5. package/dist/commands/build.js +111 -40
  6. package/dist/commands/chat.js +1 -1
  7. package/dist/commands/deploy.js +7 -3
  8. package/dist/commands/file.js +18 -0
  9. package/dist/commands/fn.js +6 -6
  10. package/dist/commands/generate.js +80 -16
  11. package/dist/commands/gmail.js +1 -1
  12. package/dist/commands/job.js +132 -10
  13. package/dist/commands/load.js +2 -2
  14. package/dist/commands/login.js +7 -2
  15. package/dist/commands/page-eval.js +82 -13
  16. package/dist/commands/page-fetch.js +14 -10
  17. package/dist/commands/page-inspect.js +28 -3
  18. package/dist/commands/page-screenshot.js +36 -7
  19. package/dist/commands/page-test.js +1 -1
  20. package/dist/commands/records.js +14 -1
  21. package/dist/commands/remove.js +1 -1
  22. package/dist/commands/sandbox.js +182 -10
  23. package/dist/commands/save.js +1 -1
  24. package/dist/commands/secrets.js +1 -1
  25. package/dist/commands/service.js +7 -2
  26. package/dist/commands/setup.js +4 -6
  27. package/dist/commands/status.js +41 -12
  28. package/dist/commands/storage.js +2 -2
  29. package/dist/commands/sync.js +15 -0
  30. package/dist/commands/test.js +33 -1
  31. package/dist/commands/update.js +4 -0
  32. package/dist/commands/upload.js +69 -121
  33. package/dist/commands/workflow.js +1 -1
  34. package/dist/helpers/body.js +117 -0
  35. package/dist/helpers/duration.js +34 -0
  36. package/dist/helpers/index.js +2 -0
  37. package/dist/index.js +1173 -620
  38. package/dist/knowledge.js +7 -7
  39. package/dist/login-flow.js +44 -2
  40. package/dist/relay/onboarding.js +26 -44
  41. package/dist/sync.js +6 -6
  42. package/dist/updater/bootstrap.js +27 -16
  43. package/dist/updater/check.js +86 -10
  44. package/dist/updater/install.js +82 -0
  45. package/dist/updater/shim.js +90 -27
  46. package/dist/utils.js +60 -3
  47. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -3929,6 +3929,41 @@ var init_colors = __esm({
3929
3929
 
3930
3930
  // src/utils.ts
3931
3931
  import { createInterface } from "readline";
3932
+ import { readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
3933
+ import { basename, join } from "node:path";
3934
+ function isWsl() {
3935
+ if (process.platform !== "linux") return false;
3936
+ if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) return true;
3937
+ try {
3938
+ return /microsoft/i.test(readFileSync("/proc/version", "utf-8"));
3939
+ } catch {
3940
+ return false;
3941
+ }
3942
+ }
3943
+ function findWindowsTwinProject(projectRoot, usersBase = "/mnt/c/Users") {
3944
+ try {
3945
+ const name = basename(projectRoot);
3946
+ if (!name) return null;
3947
+ const realRoot = realpathSync(projectRoot);
3948
+ for (const user of readdirSync(usersBase)) {
3949
+ if (WINDOWS_PSEUDO_USERS.has(user)) continue;
3950
+ const candidate = join(usersBase, user, "GipityProjects", name);
3951
+ try {
3952
+ if (!statSync(candidate).isDirectory()) continue;
3953
+ if (realpathSync(candidate) === realRoot) continue;
3954
+ return candidate;
3955
+ } catch {
3956
+ }
3957
+ }
3958
+ } catch {
3959
+ }
3960
+ return null;
3961
+ }
3962
+ function wslPathToWindows(p) {
3963
+ const m = p.match(/^\/mnt\/([a-z])\/(.*)$/);
3964
+ if (!m) return p;
3965
+ return `${m[1].toUpperCase()}:\\${m[2].replace(/\//g, "\\")}`;
3966
+ }
3932
3967
  function decodeJwtExp(token) {
3933
3968
  try {
3934
3969
  const parts = token.split(".");
@@ -3944,10 +3979,10 @@ function prompt(question) {
3944
3979
  return Promise.reject(new Error(`prompt() called without a TTY: ${question.trim()}`));
3945
3980
  }
3946
3981
  const rl2 = createInterface({ input: process.stdin, output: process.stdout });
3947
- return new Promise((resolve20) => {
3982
+ return new Promise((resolve19) => {
3948
3983
  rl2.question(question, (answer) => {
3949
3984
  rl2.close();
3950
- resolve20(answer.trim());
3985
+ resolve19(answer.trim());
3951
3986
  });
3952
3987
  });
3953
3988
  }
@@ -3972,12 +4007,12 @@ async function confirm(question, opts = {}) {
3972
4007
  return false;
3973
4008
  }
3974
4009
  const hint = defaultYes ? dim("[Y/n]") : dim("[y/N]");
3975
- process.stdout.write(`${question} ${hint} `);
4010
+ process.stdout.write(`${question} ${hint}: `);
3976
4011
  const { stdin } = process;
3977
4012
  const wasRaw = stdin.isRaw ?? false;
3978
4013
  stdin.setRawMode(true);
3979
4014
  stdin.resume();
3980
- return new Promise((resolve20) => {
4015
+ return new Promise((resolve19) => {
3981
4016
  stdin.once("data", (key) => {
3982
4017
  stdin.setRawMode(wasRaw);
3983
4018
  stdin.pause();
@@ -3992,12 +4027,12 @@ async function confirm(question, opts = {}) {
3992
4027
  else if (k7 === "n") answer = false;
3993
4028
  else answer = defaultYes;
3994
4029
  console.log(answer ? "y" : "n");
3995
- resolve20(answer);
4030
+ resolve19(answer);
3996
4031
  });
3997
4032
  });
3998
4033
  }
3999
4034
  function pickOne(label2, max, defaultIdx = 1) {
4000
- return new Promise((resolve20) => {
4035
+ return new Promise((resolve19) => {
4001
4036
  process.stdout.write(` ${bold(label2)} (1-${max}) [${bold(String(defaultIdx))}]: `);
4002
4037
  const { stdin } = process;
4003
4038
  const wasRaw = stdin.isRaw ?? false;
@@ -4013,15 +4048,15 @@ function pickOne(label2, max, defaultIdx = 1) {
4013
4048
  }
4014
4049
  if (ch === "\r" || ch === "\n") {
4015
4050
  console.log(String(defaultIdx));
4016
- return resolve20(defaultIdx);
4051
+ return resolve19(defaultIdx);
4017
4052
  }
4018
4053
  const n = parseInt(ch, 10);
4019
4054
  if (n >= 1 && n <= max) {
4020
4055
  console.log(String(n));
4021
- return resolve20(n);
4056
+ return resolve19(n);
4022
4057
  }
4023
4058
  console.log(String(defaultIdx));
4024
- resolve20(defaultIdx);
4059
+ resolve19(defaultIdx);
4025
4060
  });
4026
4061
  });
4027
4062
  }
@@ -4030,11 +4065,12 @@ function formatSize(bytes) {
4030
4065
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
4031
4066
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
4032
4067
  }
4033
- var _autoConfirm;
4068
+ var WINDOWS_PSEUDO_USERS, _autoConfirm;
4034
4069
  var init_utils = __esm({
4035
4070
  "src/utils.ts"() {
4036
4071
  "use strict";
4037
4072
  init_colors();
4073
+ WINDOWS_PSEUDO_USERS = /* @__PURE__ */ new Set(["All Users", "Default", "Default User", "Public", "desktop.ini"]);
4038
4074
  _autoConfirm = false;
4039
4075
  }
4040
4076
  });
@@ -4052,14 +4088,14 @@ __export(auth_exports, {
4052
4088
  saveAuth: () => saveAuth,
4053
4089
  sessionExpired: () => sessionExpired
4054
4090
  });
4055
- import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync, chmodSync, openSync, closeSync } from "fs";
4056
- import { join } from "path";
4091
+ import { readFileSync as readFileSync2, writeFileSync, mkdirSync, existsSync, unlinkSync, chmodSync, openSync, closeSync } from "fs";
4092
+ import { join as join2 } from "path";
4057
4093
  import { homedir } from "os";
4058
4094
  function getAuth() {
4059
4095
  if (cached) return cached;
4060
4096
  if (!existsSync(AUTH_FILE)) return null;
4061
4097
  try {
4062
- cached = JSON.parse(readFileSync(AUTH_FILE, "utf-8"));
4098
+ cached = JSON.parse(readFileSync2(AUTH_FILE, "utf-8"));
4063
4099
  return cached;
4064
4100
  } catch {
4065
4101
  return null;
@@ -4068,7 +4104,7 @@ function getAuth() {
4068
4104
  function readAuthFresh() {
4069
4105
  if (!existsSync(AUTH_FILE)) return null;
4070
4106
  try {
4071
- return JSON.parse(readFileSync(AUTH_FILE, "utf-8"));
4107
+ return JSON.parse(readFileSync2(AUTH_FILE, "utf-8"));
4072
4108
  } catch {
4073
4109
  return null;
4074
4110
  }
@@ -4119,7 +4155,7 @@ async function acquireRefreshLock() {
4119
4155
  };
4120
4156
  } catch {
4121
4157
  try {
4122
- const pid = parseInt(readFileSync(AUTH_LOCK_FILE, "utf-8").trim(), 10);
4158
+ const pid = parseInt(readFileSync2(AUTH_LOCK_FILE, "utf-8").trim(), 10);
4123
4159
  if (pid && !isNaN(pid)) {
4124
4160
  try {
4125
4161
  process.kill(pid, 0);
@@ -4209,9 +4245,9 @@ var init_auth = __esm({
4209
4245
  "src/auth.ts"() {
4210
4246
  "use strict";
4211
4247
  init_utils();
4212
- AUTH_DIR = process.env.GIPITY_DIR || join(homedir(), ".gipity");
4213
- AUTH_FILE = join(AUTH_DIR, "auth.json");
4214
- AUTH_LOCK_FILE = join(AUTH_DIR, "auth.lock");
4248
+ AUTH_DIR = process.env.GIPITY_DIR || join2(homedir(), ".gipity");
4249
+ AUTH_FILE = join2(AUTH_DIR, "auth.json");
4250
+ AUTH_LOCK_FILE = join2(AUTH_DIR, "auth.lock");
4215
4251
  cached = null;
4216
4252
  delay = (ms2) => new Promise((r) => setTimeout(r, ms2));
4217
4253
  LOCK_WAIT_MS = 1e4;
@@ -5370,8 +5406,8 @@ var require_streamx = __commonJS({
5370
5406
  return this;
5371
5407
  },
5372
5408
  next() {
5373
- return new Promise(function(resolve20, reject) {
5374
- promiseResolve = resolve20;
5409
+ return new Promise(function(resolve19, reject) {
5410
+ promiseResolve = resolve19;
5375
5411
  promiseReject = reject;
5376
5412
  const data = stream.read();
5377
5413
  if (data !== null) ondata(data);
@@ -5401,11 +5437,11 @@ var require_streamx = __commonJS({
5401
5437
  }
5402
5438
  function destroy(err) {
5403
5439
  stream.destroy(err);
5404
- return new Promise((resolve20, reject) => {
5405
- if (stream._duplexState & DESTROYED) return resolve20({ value: void 0, done: true });
5440
+ return new Promise((resolve19, reject) => {
5441
+ if (stream._duplexState & DESTROYED) return resolve19({ value: void 0, done: true });
5406
5442
  stream.once("close", function() {
5407
5443
  if (err) reject(err);
5408
- else resolve20({ value: void 0, done: true });
5444
+ else resolve19({ value: void 0, done: true });
5409
5445
  });
5410
5446
  });
5411
5447
  }
@@ -5449,8 +5485,8 @@ var require_streamx = __commonJS({
5449
5485
  const writes = pending + (ws2._duplexState & WRITE_WRITING ? 1 : 0);
5450
5486
  if (writes === 0) return Promise.resolve(true);
5451
5487
  if (state.drains === null) state.drains = [];
5452
- return new Promise((resolve20) => {
5453
- state.drains.push({ writes, resolve: resolve20 });
5488
+ return new Promise((resolve19) => {
5489
+ state.drains.push({ writes, resolve: resolve19 });
5454
5490
  });
5455
5491
  }
5456
5492
  write(data) {
@@ -5555,10 +5591,10 @@ var require_streamx = __commonJS({
5555
5591
  cb2(null);
5556
5592
  }
5557
5593
  function pipelinePromise(...streams) {
5558
- return new Promise((resolve20, reject) => {
5594
+ return new Promise((resolve19, reject) => {
5559
5595
  return pipeline(...streams, (err) => {
5560
5596
  if (err) return reject(err);
5561
- resolve20();
5597
+ resolve19();
5562
5598
  });
5563
5599
  });
5564
5600
  }
@@ -6213,16 +6249,16 @@ var require_extract = __commonJS({
6213
6249
  entryCallback = null;
6214
6250
  cb2(err);
6215
6251
  }
6216
- function onnext(resolve20, reject) {
6252
+ function onnext(resolve19, reject) {
6217
6253
  if (error2) {
6218
6254
  return reject(error2);
6219
6255
  }
6220
6256
  if (entryStream) {
6221
- resolve20({ value: entryStream, done: false });
6257
+ resolve19({ value: entryStream, done: false });
6222
6258
  entryStream = null;
6223
6259
  return;
6224
6260
  }
6225
- promiseResolve = resolve20;
6261
+ promiseResolve = resolve19;
6226
6262
  promiseReject = reject;
6227
6263
  consumeCallback(null);
6228
6264
  if (extract3._finished && promiseResolve) {
@@ -6250,11 +6286,11 @@ var require_extract = __commonJS({
6250
6286
  function destroy(err) {
6251
6287
  extract3.destroy(err);
6252
6288
  consumeCallback(err);
6253
- return new Promise((resolve20, reject) => {
6254
- if (extract3.destroyed) return resolve20({ value: void 0, done: true });
6289
+ return new Promise((resolve19, reject) => {
6290
+ if (extract3.destroyed) return resolve19({ value: void 0, done: true });
6255
6291
  extract3.once("close", function() {
6256
6292
  if (err) reject(err);
6257
- else resolve20({ value: void 0, done: true });
6293
+ else resolve19({ value: void 0, done: true });
6258
6294
  });
6259
6295
  });
6260
6296
  }
@@ -6535,13 +6571,13 @@ var require_tar_stream = __commonJS({
6535
6571
  });
6536
6572
 
6537
6573
  // src/client-context.ts
6538
- import { readFileSync as readFileSync2 } from "fs";
6574
+ import { readFileSync as readFileSync3 } from "fs";
6539
6575
  import { resolve, dirname } from "path";
6540
6576
  import { fileURLToPath } from "url";
6541
6577
  function cliVersion() {
6542
6578
  try {
6543
6579
  const dir = dirname(fileURLToPath(import.meta.url));
6544
- const pkg2 = JSON.parse(readFileSync2(resolve(dir, "../package.json"), "utf-8"));
6580
+ const pkg2 = JSON.parse(readFileSync3(resolve(dir, "../package.json"), "utf-8"));
6545
6581
  return typeof pkg2.version === "string" ? pkg2.version : "unknown";
6546
6582
  } catch {
6547
6583
  return "unknown";
@@ -6707,7 +6743,7 @@ async function postForTarEntries(path5, body, retried = false) {
6707
6743
  if (!res.body) throw new ApiError(500, "EMPTY_RESPONSE", "Server returned no body");
6708
6744
  const extract3 = tar.extract();
6709
6745
  const entries = [];
6710
- const done = new Promise((resolve20, reject) => {
6746
+ const done = new Promise((resolve19, reject) => {
6711
6747
  extract3.on("entry", (header, stream, next) => {
6712
6748
  const chunks = [];
6713
6749
  stream.on("data", (c) => chunks.push(c));
@@ -6718,7 +6754,7 @@ async function postForTarEntries(path5, body, retried = false) {
6718
6754
  stream.on("error", reject);
6719
6755
  stream.resume();
6720
6756
  });
6721
- extract3.on("finish", () => resolve20());
6757
+ extract3.on("finish", () => resolve19());
6722
6758
  extract3.on("error", reject);
6723
6759
  });
6724
6760
  Readable.fromWeb(res.body).pipe(extract3);
@@ -6928,7 +6964,7 @@ __export(config_exports, {
6928
6964
  setApiBaseOverride: () => setApiBaseOverride,
6929
6965
  shouldIgnore: () => shouldIgnore
6930
6966
  });
6931
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
6967
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
6932
6968
  import { dirname as dirname2, resolve as resolve2 } from "path";
6933
6969
  function setApiBaseOverride(url) {
6934
6970
  apiBaseOverride = url;
@@ -6986,7 +7022,7 @@ function getConfig() {
6986
7022
  const path5 = getConfigPath();
6987
7023
  if (!path5) return null;
6988
7024
  try {
6989
- cached3 = JSON.parse(readFileSync3(path5, "utf-8"));
7025
+ cached3 = JSON.parse(readFileSync4(path5, "utf-8"));
6990
7026
  return cached3;
6991
7027
  } catch {
6992
7028
  return null;
@@ -31863,9 +31899,9 @@ var {
31863
31899
  // src/index.ts
31864
31900
  init_config();
31865
31901
  init_utils();
31866
- import { readFileSync as readFileSync26 } from "fs";
31902
+ import { readFileSync as readFileSync28 } from "fs";
31867
31903
  import { fileURLToPath as fileURLToPath3 } from "url";
31868
- import { dirname as dirname13, resolve as resolve19 } from "path";
31904
+ import { dirname as dirname14, resolve as resolve18 } from "path";
31869
31905
 
31870
31906
  // src/helpers/output.ts
31871
31907
  var frameOpen = false;
@@ -31970,12 +32006,10 @@ Kits are reusable building blocks added to an existing app, not whole templates
31970
32006
  - \`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
32007
  - \`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
32008
  - \`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
32009
  - \`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
32010
  - \`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.`;
32011
+ - \`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.
32012
+ - \`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
32013
  var SKILLS_CONTENT = `# Gipity Integration
31980
32014
 
31981
32015
  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.
@@ -32083,12 +32117,14 @@ Write files locally - Gipity's editor hooks (installed into Claude Code, Grok, a
32083
32117
 
32084
32118
  To keep local-only material (research clones, scratch data, vendored references) in the project directory without syncing or deploying it, list it in a \`.gipityignore\` at the project root - gitignore-style, one pattern per line, \`#\` comments. Ignored paths are invisible to sync in both directions; anything that already synced before being ignored stays on the server until you delete it.
32085
32119
 
32120
+ **WSL trap: user-dropped files may be in a Windows-side twin.** On WSL, a same-named folder often exists at \`C:\\Users\\<name>\\GipityProjects\\<project>\` (visible as \`/mnt/c/Users/<name>/GipityProjects/<project>\`) - from Windows Explorer it looks like "the project folder", so users drop new files (audio, images) there. Nothing syncs from it. If the user says files are "in the project" but you can't find them, check that path before searching wider; \`gipity sync\` prints a notice when such a twin exists.
32121
+
32086
32122
  ### Where files go: deploy only ships \`src/\`
32087
32123
 
32088
32124
  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
32125
 
32090
32126
  - **\`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.
32127
+ - **\`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
32128
  - **\`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
32129
  - **\`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
32130
 
@@ -32104,7 +32140,7 @@ App services skills (load before calling \`/services/*\` endpoints):
32104
32140
  - \`app-audio\` - sound effects, music, transcription
32105
32141
  - \`app-auth\` - sign in with Gipity, popup vs redirect
32106
32142
  - \`app-files\` - uploads, variants, file listing
32107
- - \`app-image\` - text-to-image only (no input image / editing); providers, sizes, aspect ratios
32143
+ - \`app-image\` - text-to-image AND instruction editing (pass \`images\` to edit an uploaded photo); providers, sizes, aspect ratios
32108
32144
  - \`app-llm\` - chat completions, streaming, image input
32109
32145
  - \`app-location\` - user location & reverse geocoding for deployed apps (first-party - no third-party geocoder)
32110
32146
  - \`app-notify\` - web push notifications for deployed apps (incl. iOS home-screen web apps) - notify kit + injected notify() service, platform owns the keys
@@ -32119,6 +32155,7 @@ App development skills:
32119
32155
  - \`app-debugging\` - debug a deployed app: page inspect/eval, screenshots, function logs
32120
32156
  - \`app-development\` - functions, database, and API
32121
32157
  - \`app-import\` - import apps from GitHub/.gip bundles (incl. Vercel/Replit/Lovable porting) and export any project as a portable .gip - app_import tool, gipity save/load
32158
+ - \`app-records\` - Gipity Records: declared tables \u2192 validated CRUD, provenance, workflows, auto UI
32122
32159
  - \`app-testing\` - testing deployed app functions (ctx.fn.call/callAs, the isolated test DB)
32123
32160
  - \`deploy\` - the deploy pipeline & gipity.yaml manifest
32124
32161
  - \`jobs\` - long-running CPU + GPU compute jobs (Python / Node / bash)
@@ -32127,7 +32164,6 @@ App development skills:
32127
32164
  - \`web-ui-patterns\` - default Gipity look (theme tokens) + web UI recipes - feeds, copy-to-clipboard
32128
32165
 
32129
32166
  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
32167
  - \`audio-align\` - the audio-align kit: forced alignment of audio + lyrics into word-level timing JSON
32132
32168
  - \`chatbot\` - the chatbot kit: persona + scope guardrails + static knowledge, bubble widget or headless engine
32133
32169
 
@@ -32156,13 +32192,13 @@ init_colors();
32156
32192
 
32157
32193
  // src/bug-queue.ts
32158
32194
  init_api();
32159
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync4, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2 } from "fs";
32160
- import { join as join2 } from "path";
32195
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2 } from "fs";
32196
+ import { join as join3 } from "path";
32161
32197
  import { homedir as homedir2 } from "os";
32162
- var QUEUE_DIR = join2(process.env.GIPITY_DIR || join2(homedir2(), ".gipity"), "bug-queue");
32198
+ var QUEUE_DIR = join3(process.env.GIPITY_DIR || join3(homedir2(), ".gipity"), "bug-queue");
32163
32199
  function queueBugReport(report) {
32164
32200
  mkdirSync2(QUEUE_DIR, { recursive: true, mode: 448 });
32165
- const file = join2(QUEUE_DIR, `${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json`);
32201
+ const file = join3(QUEUE_DIR, `${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json`);
32166
32202
  writeFileSync3(file, JSON.stringify(report, null, 2), { mode: 384 });
32167
32203
  }
32168
32204
  function isRetryableFailure(err) {
@@ -32174,12 +32210,12 @@ function isRetryableFailure(err) {
32174
32210
  async function flushBugQueue() {
32175
32211
  if (!existsSync3(QUEUE_DIR)) return 0;
32176
32212
  let delivered = 0;
32177
- for (const file of readdirSync(QUEUE_DIR)) {
32213
+ for (const file of readdirSync2(QUEUE_DIR)) {
32178
32214
  if (!file.endsWith(".json")) continue;
32179
- const path5 = join2(QUEUE_DIR, file);
32215
+ const path5 = join3(QUEUE_DIR, file);
32180
32216
  let report;
32181
32217
  try {
32182
- report = JSON.parse(readFileSync4(path5, "utf-8"));
32218
+ report = JSON.parse(readFileSync5(path5, "utf-8"));
32183
32219
  } catch {
32184
32220
  try {
32185
32221
  unlinkSync2(path5);
@@ -32202,6 +32238,70 @@ async function flushBugQueue() {
32202
32238
  return delivered;
32203
32239
  }
32204
32240
 
32241
+ // src/login-flow.ts
32242
+ init_api();
32243
+ init_auth();
32244
+ init_config();
32245
+ init_utils();
32246
+ init_colors();
32247
+ function newAccountWouldBeUnexpected(email, priorAuth) {
32248
+ return !!getConfig() || priorAuth?.email.toLowerCase() === email.toLowerCase().trim();
32249
+ }
32250
+ function warnBeforeCodeIfUnexpectedNewAccount(isNewUser, email, indent = "") {
32251
+ if (isNewUser !== true) return;
32252
+ const priorAuth = getAuth();
32253
+ if (!newAccountWouldBeUnexpected(email, priorAuth)) return;
32254
+ const config = getConfig();
32255
+ console.log(`${indent}${warning(`No existing Gipity account for ${email} \u2014 entering the code will CREATE a new one.`)}`);
32256
+ if (config) {
32257
+ console.log(`${indent}${muted(`This directory is linked to project ${config.projectSlug} (account ${config.accountSlug}). If you meant to log into that account, stop and re-check the email before entering the code.`)}`);
32258
+ }
32259
+ }
32260
+ function warnIfUnexpectedNewAccount(isNewUser, email, priorAuth, indent = "") {
32261
+ if (isNewUser !== true) return;
32262
+ if (!newAccountWouldBeUnexpected(email, priorAuth)) return;
32263
+ const config = getConfig();
32264
+ console.log(`${indent}${warning(`Logged into a NEW, empty account for ${email} \u2014 no prior account existed for this email.`)}`);
32265
+ if (config) {
32266
+ console.log(`${indent}${muted(`This directory is linked to project ${config.projectSlug} (account ${config.accountSlug}), which the new account does not own \u2014 project / skill / sync commands will fail with "not found".`)}`);
32267
+ }
32268
+ console.log(`${indent}${muted("If you expected an existing account, run: gipity status \u2014 then gipity login again with the correct email.")}`);
32269
+ }
32270
+ async function interactiveLogin() {
32271
+ const email = await prompt(" Email: ");
32272
+ if (!email) {
32273
+ console.error(`
32274
+ ${error("Email required.")}`);
32275
+ process.exit(1);
32276
+ }
32277
+ const sendRes = await publicPost("/auth/login", { email });
32278
+ console.log(" Check your email for a 6-digit code.\n");
32279
+ warnBeforeCodeIfUnexpectedNewAccount(sendRes.isNewUser, email, " ");
32280
+ const code = await prompt(" Code: ");
32281
+ if (!code) {
32282
+ console.error(`
32283
+ ${error("Code required.")}`);
32284
+ process.exit(1);
32285
+ }
32286
+ const priorAuth = getAuth();
32287
+ const res = await publicPost("/auth/verify", { email, code });
32288
+ const exp = decodeJwtExp(res.accessToken);
32289
+ if (!exp) {
32290
+ console.error(`
32291
+ ${error("Invalid token received.")}`);
32292
+ process.exit(1);
32293
+ }
32294
+ const expiresAt = new Date(exp * 1e3).toISOString();
32295
+ saveAuth({ accessToken: res.accessToken, refreshToken: res.refreshToken, email, expiresAt });
32296
+ console.log(` ${success(`Logged in (${email}).`)}`);
32297
+ warnIfUnexpectedNewAccount(res.isNewUser, email, priorAuth, " ");
32298
+ const delivered = await flushBugQueue().catch(() => 0);
32299
+ if (delivered > 0) {
32300
+ console.log(` ${muted(`Delivered ${delivered} queued bug report${delivered === 1 ? "" : "s"}.`)}`);
32301
+ }
32302
+ return getAuth();
32303
+ }
32304
+
32205
32305
  // src/commands/login.ts
32206
32306
  var loginCommand = new Command("login").description("Log in or sign up").option("--email <email>", "Email address").option("--code <code>", "Verification code").action(async (opts) => {
32207
32307
  try {
@@ -32212,9 +32312,10 @@ var loginCommand = new Command("login").description("Log in or sign up").option(
32212
32312
  return;
32213
32313
  }
32214
32314
  if (email && !code) {
32215
- await publicPost("/auth/login", { email });
32315
+ const sendRes2 = await publicPost("/auth/login", { email });
32216
32316
  console.log("Check your email for a 6-digit code.");
32217
32317
  console.log(muted(`Then run: gipity login --email ${email} --code <code>`));
32318
+ warnBeforeCodeIfUnexpectedNewAccount(sendRes2.isNewUser, email);
32218
32319
  return;
32219
32320
  }
32220
32321
  console.log("Enter your email to log in or create an account.");
@@ -32225,9 +32326,10 @@ var loginCommand = new Command("login").description("Log in or sign up").option(
32225
32326
  console.error(error("Email required."));
32226
32327
  process.exit(1);
32227
32328
  }
32228
- await publicPost("/auth/login", { email });
32329
+ const sendRes = await publicPost("/auth/login", { email });
32229
32330
  console.log("");
32230
32331
  console.log("Check your email for a 6-digit code.");
32332
+ warnBeforeCodeIfUnexpectedNewAccount(sendRes.isNewUser, email);
32231
32333
  code = await prompt("Code: ");
32232
32334
  await verify(email, code);
32233
32335
  } catch (err) {
@@ -32236,6 +32338,7 @@ var loginCommand = new Command("login").description("Log in or sign up").option(
32236
32338
  }
32237
32339
  });
32238
32340
  async function verify(email, code) {
32341
+ const priorAuth = getAuth();
32239
32342
  const res = await publicPost("/auth/verify", { email, code });
32240
32343
  const exp = decodeJwtExp(res.accessToken);
32241
32344
  if (!exp) {
@@ -32250,6 +32353,7 @@ async function verify(email, code) {
32250
32353
  expiresAt
32251
32354
  });
32252
32355
  console.log(success(`Logged in (${email}).`));
32356
+ warnIfUnexpectedNewAccount(res.isNewUser, email, priorAuth);
32253
32357
  const delivered = await flushBugQueue().catch(() => 0);
32254
32358
  if (delivered > 0) {
32255
32359
  console.log(muted(`Delivered ${delivered} queued bug report${delivered === 1 ? "" : "s"}.`));
@@ -32295,8 +32399,8 @@ function run(label2, action) {
32295
32399
  init_api();
32296
32400
  init_config();
32297
32401
  init_utils();
32298
- import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync5, statSync, lstatSync, unlinkSync as unlinkSync3, readdirSync as readdirSync3, rmdirSync, readFileSync as readFileSync6, renameSync, openSync as openSync2, closeSync as closeSync2, utimesSync, realpathSync } from "fs";
32299
- import { join as join4, relative, dirname as dirname4, extname as extname2, resolve as resolve4, sep } from "path";
32402
+ import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync5, statSync as statSync2, lstatSync, unlinkSync as unlinkSync3, readdirSync as readdirSync4, rmdirSync, readFileSync as readFileSync7, renameSync, openSync as openSync2, closeSync as closeSync2, utimesSync, realpathSync as realpathSync2 } from "fs";
32403
+ import { join as join5, relative, dirname as dirname4, extname as extname2, resolve as resolve4, sep } from "path";
32300
32404
  import { hostname } from "os";
32301
32405
  import { createHash as createHash2 } from "crypto";
32302
32406
 
@@ -32359,25 +32463,25 @@ async function hashFile(path5) {
32359
32463
  const hash = createHash("sha256");
32360
32464
  let size = 0;
32361
32465
  const stream = createReadStream(path5);
32362
- await new Promise((resolve20, reject) => {
32466
+ await new Promise((resolve19, reject) => {
32363
32467
  stream.on("data", (chunk) => {
32364
32468
  const buf = chunk;
32365
32469
  hash.update(buf);
32366
32470
  size += buf.length;
32367
32471
  });
32368
- stream.on("end", () => resolve20());
32472
+ stream.on("end", () => resolve19());
32369
32473
  stream.on("error", reject);
32370
32474
  });
32371
32475
  return { sha256: hash.digest("hex"), size };
32372
32476
  }
32373
32477
  function readRange(path5, start, end) {
32374
- return new Promise((resolve20, reject) => {
32478
+ return new Promise((resolve19, reject) => {
32375
32479
  const chunks = [];
32376
32480
  const stream = createReadStream(path5, { start, end });
32377
32481
  stream.on("data", (c) => {
32378
32482
  chunks.push(c);
32379
32483
  });
32380
- stream.on("end", () => resolve20(Buffer.concat(chunks)));
32484
+ stream.on("end", () => resolve19(Buffer.concat(chunks)));
32381
32485
  stream.on("error", reject);
32382
32486
  });
32383
32487
  }
@@ -32410,9 +32514,9 @@ var UploadConflictError = class extends Error {
32410
32514
  path;
32411
32515
  };
32412
32516
  async function uploadOneFile(projectGuid, localPath, virtualPath, opts = {}) {
32413
- const { sha256: sha2562, size } = await hashFile(localPath);
32517
+ const { sha256, size } = await hashFile(localPath);
32414
32518
  const mime = opts.mime ?? guessMime(virtualPath);
32415
- const initBody = { path: virtualPath, size, sha256: sha2562, mime };
32519
+ const initBody = { path: virtualPath, size, sha256, mime };
32416
32520
  if (opts.expectedServerVersion !== void 0) {
32417
32521
  initBody.expected_server_version = opts.expectedServerVersion;
32418
32522
  }
@@ -32540,9 +32644,9 @@ async function uploadCompleteBatch(projectGuid, items) {
32540
32644
 
32541
32645
  // src/setup.ts
32542
32646
  init_platform();
32543
- import { resolve as resolve3, join as join3, dirname as dirname3 } from "path";
32647
+ import { resolve as resolve3, join as join4, dirname as dirname3 } from "path";
32544
32648
  import { homedir as homedir3, tmpdir } from "os";
32545
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, readFileSync as readFileSync5, mkdtempSync, rmSync, cpSync, readdirSync as readdirSync2 } from "fs";
32649
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, readFileSync as readFileSync6, mkdtempSync, rmSync, cpSync, readdirSync as readdirSync3 } from "fs";
32546
32650
  init_config();
32547
32651
  var PRIMER_FILES = {
32548
32652
  claude: "CLAUDE.md",
@@ -32657,14 +32761,14 @@ function stripGipityHooks(settings) {
32657
32761
  function readSettingsFile(path5) {
32658
32762
  if (!existsSync4(path5)) return {};
32659
32763
  try {
32660
- return JSON.parse(readFileSync5(path5, "utf-8"));
32764
+ return JSON.parse(readFileSync6(path5, "utf-8"));
32661
32765
  } catch {
32662
32766
  return {};
32663
32767
  }
32664
32768
  }
32665
32769
  function ensureGipityPlugin(force = false) {
32666
- const claudeDir = join3(homedir3(), ".claude");
32667
- const settingsPath = join3(claudeDir, "settings.json");
32770
+ const claudeDir = join4(homedir3(), ".claude");
32771
+ const settingsPath = join4(claudeDir, "settings.json");
32668
32772
  const settings = readSettingsFile(settingsPath);
32669
32773
  let changed = stripGipityHooks(settings);
32670
32774
  const marketplaces = settings.extraKnownMarketplaces ?? (settings.extraKnownMarketplaces = {});
@@ -32696,8 +32800,8 @@ function versionGte(have, want) {
32696
32800
  }
32697
32801
  function userScopeInstallState() {
32698
32802
  try {
32699
- const p = join3(homedir3(), ".claude", "plugins", "installed_plugins.json");
32700
- const data = JSON.parse(readFileSync5(p, "utf-8"));
32803
+ const p = join4(homedir3(), ".claude", "plugins", "installed_plugins.json");
32804
+ const data = JSON.parse(readFileSync6(p, "utf-8"));
32701
32805
  const entries = data?.plugins?.[GIPITY_PLUGIN_ID];
32702
32806
  if (!Array.isArray(entries)) return { exists: false, current: false };
32703
32807
  const userEntries = entries.filter((e) => e?.scope === "user");
@@ -32734,8 +32838,8 @@ function ensureGipityPluginInstalled() {
32734
32838
  }
32735
32839
  function grokInstallState() {
32736
32840
  try {
32737
- const p = join3(homedir3(), ".grok", "installed-plugins", "registry.json");
32738
- const data = JSON.parse(readFileSync5(p, "utf-8"));
32841
+ const p = join4(homedir3(), ".grok", "installed-plugins", "registry.json");
32842
+ const data = JSON.parse(readFileSync6(p, "utf-8"));
32739
32843
  const versions = [];
32740
32844
  for (const repo of Object.values(data?.repos ?? {})) {
32741
32845
  const v7 = repo?.plugins?.gipity?.version;
@@ -32760,12 +32864,12 @@ function ensureGrokPluginInstalled() {
32760
32864
  console.log("Installed the Gipity plugin for Grok (skills + file-sync hooks).");
32761
32865
  }
32762
32866
  }
32763
- var AGENTS_SKILLS_DIR = join3(homedir3(), ".agents", "skills");
32764
- var AGENT_HOOKS_DIR = join3(homedir3(), ".gipity", "agent-hooks");
32765
- var AGENT_SKILLS_MANIFEST = join3(homedir3(), ".gipity", "agent-skills.json");
32867
+ var AGENTS_SKILLS_DIR = join4(homedir3(), ".agents", "skills");
32868
+ var AGENT_HOOKS_DIR = join4(homedir3(), ".gipity", "agent-hooks");
32869
+ var AGENT_SKILLS_MANIFEST = join4(homedir3(), ".gipity", "agent-skills.json");
32766
32870
  function agentSkillsState() {
32767
32871
  try {
32768
- const m = JSON.parse(readFileSync5(AGENT_SKILLS_MANIFEST, "utf-8"));
32872
+ const m = JSON.parse(readFileSync6(AGENT_SKILLS_MANIFEST, "utf-8"));
32769
32873
  return {
32770
32874
  current: typeof m?.version === "string" && versionGte(m.version, GIPITY_PLUGIN_VERSION),
32771
32875
  skills: Array.isArray(m?.skills) ? m.skills : []
@@ -32777,36 +32881,36 @@ function agentSkillsState() {
32777
32881
  function ensureAgentSkillsInstalled() {
32778
32882
  if (agentSkillsState().current) return;
32779
32883
  if (!binaryOnPath("git")) return;
32780
- const tmp = mkdtempSync(join3(tmpdir(), "gipity-skills-"));
32884
+ const tmp = mkdtempSync(join4(tmpdir(), "gipity-skills-"));
32781
32885
  try {
32782
32886
  const clone = spawnSyncCommand(
32783
32887
  resolveCommand("git"),
32784
- ["clone", "--depth", "1", `https://github.com/${GIPITY_MARKETPLACE_REPO}.git`, join3(tmp, "repo")],
32888
+ ["clone", "--depth", "1", `https://github.com/${GIPITY_MARKETPLACE_REPO}.git`, join4(tmp, "repo")],
32785
32889
  { stdio: "ignore", timeout: 12e4 }
32786
32890
  );
32787
32891
  if (clone.status !== 0) return;
32788
- const repo = join3(tmp, "repo");
32892
+ const repo = join4(tmp, "repo");
32789
32893
  let version = GIPITY_PLUGIN_VERSION;
32790
32894
  try {
32791
- const manifest = JSON.parse(readFileSync5(join3(repo, ".claude-plugin", "plugin.json"), "utf-8"));
32895
+ const manifest = JSON.parse(readFileSync6(join4(repo, ".claude-plugin", "plugin.json"), "utf-8"));
32792
32896
  if (typeof manifest?.version === "string") version = manifest.version;
32793
32897
  } catch {
32794
32898
  }
32795
- const skillsSrc = join3(repo, "skills");
32899
+ const skillsSrc = join4(repo, "skills");
32796
32900
  const names = [];
32797
- for (const entry of readdirSync2(skillsSrc, { withFileTypes: true })) {
32901
+ for (const entry of readdirSync3(skillsSrc, { withFileTypes: true })) {
32798
32902
  if (!entry.isDirectory()) continue;
32799
- if (!existsSync4(join3(skillsSrc, entry.name, "SKILL.md"))) continue;
32903
+ if (!existsSync4(join4(skillsSrc, entry.name, "SKILL.md"))) continue;
32800
32904
  mkdirSync3(AGENTS_SKILLS_DIR, { recursive: true });
32801
- cpSync(join3(skillsSrc, entry.name), join3(AGENTS_SKILLS_DIR, entry.name), {
32905
+ cpSync(join4(skillsSrc, entry.name), join4(AGENTS_SKILLS_DIR, entry.name), {
32802
32906
  recursive: true,
32803
32907
  force: true
32804
32908
  });
32805
32909
  names.push(entry.name);
32806
32910
  }
32807
32911
  mkdirSync3(AGENT_HOOKS_DIR, { recursive: true });
32808
- for (const script of readdirSync2(join3(repo, "hooks", "scripts"))) {
32809
- cpSync(join3(repo, "hooks", "scripts", script), join3(AGENT_HOOKS_DIR, script), { force: true });
32912
+ for (const script of readdirSync3(join4(repo, "hooks", "scripts"))) {
32913
+ cpSync(join4(repo, "hooks", "scripts", script), join4(AGENT_HOOKS_DIR, script), { force: true });
32810
32914
  }
32811
32915
  writeFileSync4(AGENT_SKILLS_MANIFEST, JSON.stringify({ version, skills: names }, null, 2) + "\n");
32812
32916
  console.log(`Installed ${names.length} Gipity skills for Codex (~/.agents/skills).`);
@@ -32816,8 +32920,8 @@ function ensureAgentSkillsInstalled() {
32816
32920
  }
32817
32921
  }
32818
32922
  function applyCodexHooks(existing) {
32819
- const launcher = join3(AGENT_HOOKS_DIR, "launch.sh");
32820
- const cmd = (script, ...args) => [`sh "${launcher}" "${join3(AGENT_HOOKS_DIR, script)}"`, ...args].join(" ");
32923
+ const launcher = join4(AGENT_HOOKS_DIR, "launch.sh");
32924
+ const cmd = (script, ...args) => [`sh "${launcher}" "${join4(AGENT_HOOKS_DIR, script)}"`, ...args].join(" ");
32821
32925
  const wanted = [
32822
32926
  { event: "PostToolUse", matcher: "Edit|Write", command: cmd("sync-push.cjs"), timeout: 30 },
32823
32927
  { event: "UserPromptSubmit", command: cmd("sync-pull.cjs"), timeout: 300 },
@@ -32855,8 +32959,8 @@ function setupCodexHooks() {
32855
32959
  if (process.platform === "win32") return;
32856
32960
  const cwd = resolve3(process.cwd());
32857
32961
  if (cwd === resolve3(homedir3())) return;
32858
- const path5 = join3(cwd, ".codex", "hooks.json");
32859
- const existing = existsSync4(path5) ? readFileSync5(path5, "utf-8") : null;
32962
+ const path5 = join4(cwd, ".codex", "hooks.json");
32963
+ const existing = existsSync4(path5) ? readFileSync6(path5, "utf-8") : null;
32860
32964
  const next = applyCodexHooks(existing);
32861
32965
  if (next === null) return;
32862
32966
  mkdirSync3(dirname3(path5), { recursive: true });
@@ -32876,9 +32980,9 @@ function setupClaudeHooks() {
32876
32980
  ensureGipityPlugin();
32877
32981
  const cwd = resolve3(process.cwd());
32878
32982
  if (cwd === resolve3(homedir3())) return;
32879
- const claudeDir = join3(cwd, ".claude");
32983
+ const claudeDir = join4(cwd, ".claude");
32880
32984
  mkdirSync3(claudeDir, { recursive: true });
32881
- const settingsPath = join3(claudeDir, "settings.json");
32985
+ const settingsPath = join4(claudeDir, "settings.json");
32882
32986
  const settings = readSettingsFile(settingsPath);
32883
32987
  stripGipityHooks(settings);
32884
32988
  const perms = settings.permissions || {};
@@ -32925,7 +33029,7 @@ function applySkillsBlock(existing, apiBase2 = DEFAULT_API_BASE) {
32925
33029
  function writeSkillsFile(relPath, wrap) {
32926
33030
  const path5 = resolve3(process.cwd(), relPath);
32927
33031
  mkdirSync3(dirname3(path5), { recursive: true });
32928
- const existing = existsSync4(path5) ? readFileSync5(path5, "utf-8") : null;
33032
+ const existing = existsSync4(path5) ? readFileSync6(path5, "utf-8") : null;
32929
33033
  const baseNext = applySkillsBlock(existing, resolveApiBase());
32930
33034
  const next = wrap && existing === null ? wrap(baseNext) : baseNext;
32931
33035
  if (next !== existing) writeFileSync4(path5, next);
@@ -32968,7 +33072,7 @@ function applyAiderConf(existing) {
32968
33072
  function setupAiderMd() {
32969
33073
  writeSkillsFile(PRIMER_FILES.aider);
32970
33074
  const path5 = resolve3(process.cwd(), AIDER_CONF_FILE);
32971
- const existing = existsSync4(path5) ? readFileSync5(path5, "utf-8") : null;
33075
+ const existing = existsSync4(path5) ? readFileSync6(path5, "utf-8") : null;
32972
33076
  const next = applyAiderConf(existing);
32973
33077
  if (next !== null) writeFileSync4(path5, next);
32974
33078
  }
@@ -33010,7 +33114,7 @@ function setupGitignore() {
33010
33114
  const gitignorePath = resolve3(process.cwd(), ".gitignore");
33011
33115
  const entries = [".gipity/", ".gipity.json", ...SCRATCH_IGNORE];
33012
33116
  if (existsSync4(gitignorePath)) {
33013
- let content = readFileSync5(gitignorePath, "utf-8");
33117
+ let content = readFileSync6(gitignorePath, "utf-8");
33014
33118
  const lines = content.split(/\r?\n/);
33015
33119
  const toAdd = entries.filter((e) => !lines.includes(e));
33016
33120
  if (toAdd.length > 0) {
@@ -33038,11 +33142,11 @@ var BULK_DELETE_FRACTION = 0.25;
33038
33142
  var DOWNLOAD_IDLE_MS = 3e4;
33039
33143
  function syncStatePath() {
33040
33144
  const configPath = getConfigPath();
33041
- return join4(dirname4(configPath), ".gipity", "sync-state.json");
33145
+ return join5(dirname4(configPath), ".gipity", "sync-state.json");
33042
33146
  }
33043
33147
  function lockPath() {
33044
33148
  const configPath = getConfigPath();
33045
- return join4(dirname4(configPath), ".gipity", "sync.lock");
33149
+ return join5(dirname4(configPath), ".gipity", "sync.lock");
33046
33150
  }
33047
33151
  function projectDir() {
33048
33152
  const configPath = getConfigPath();
@@ -33056,8 +33160,8 @@ function isLockReclaimable(path5, now = Date.now()) {
33056
33160
  let raw;
33057
33161
  let mtimeMs;
33058
33162
  try {
33059
- raw = readFileSync6(path5, "utf-8").trim();
33060
- mtimeMs = statSync(path5).mtimeMs;
33163
+ raw = readFileSync7(path5, "utf-8").trim();
33164
+ mtimeMs = statSync2(path5).mtimeMs;
33061
33165
  } catch {
33062
33166
  return false;
33063
33167
  }
@@ -33116,7 +33220,7 @@ async function acquireLock(progress) {
33116
33220
  );
33117
33221
  }
33118
33222
  if (!waitSpinner) {
33119
- waitSpinner = progress?.spinner("Waiting for another sync to finish\u2026") ?? null;
33223
+ waitSpinner = progress?.spinner("Waiting for another sync to finish...") ?? null;
33120
33224
  }
33121
33225
  await new Promise((r) => setTimeout(r, LOCK_POLL_MS2));
33122
33226
  }
@@ -33126,7 +33230,7 @@ function readBaseline(projectGuid) {
33126
33230
  const path5 = syncStatePath();
33127
33231
  if (!existsSync5(path5)) return { projectGuid, files: {}, lastFullSync: null };
33128
33232
  try {
33129
- const parsed = JSON.parse(readFileSync6(path5, "utf-8"));
33233
+ const parsed = JSON.parse(readFileSync7(path5, "utf-8"));
33130
33234
  if (parsed.projectGuid !== projectGuid) {
33131
33235
  return { projectGuid, files: {}, lastFullSync: null };
33132
33236
  }
@@ -33150,25 +33254,25 @@ function walkLocal(root, ignorePatterns, baseline) {
33150
33254
  function walk(dir) {
33151
33255
  let entries;
33152
33256
  try {
33153
- entries = readdirSync3(dir, { withFileTypes: true });
33257
+ entries = readdirSync4(dir, { withFileTypes: true });
33154
33258
  } catch {
33155
33259
  return;
33156
33260
  }
33157
33261
  for (const entry of entries) {
33158
- const full = join4(dir, entry.name);
33262
+ const full = join5(dir, entry.name);
33159
33263
  const rel2 = relative(root, full).replace(/\\/g, "/");
33160
33264
  if (shouldIgnore(rel2, ignorePatterns)) continue;
33161
33265
  if (entry.isDirectory()) {
33162
- if (existsSync5(join4(full, CONFIG_FILE2))) continue;
33266
+ if (existsSync5(join5(full, CONFIG_FILE2))) continue;
33163
33267
  walk(full);
33164
33268
  } else if (entry.isFile()) {
33165
33269
  try {
33166
- const stat2 = statSync(full);
33270
+ const stat2 = statSync2(full);
33167
33271
  const size = stat2.size;
33168
33272
  const mtime = stat2.mtime.toISOString();
33169
33273
  const prior = baseline[rel2];
33170
- const sha2562 = prior && prior.size === size && prior.mtime === mtime ? prior.sha256 : void 0;
33171
- result.set(rel2, { size, mtime, sha256: sha2562 });
33274
+ const sha256 = prior && prior.size === size && prior.mtime === mtime ? prior.sha256 : void 0;
33275
+ result.set(rel2, { size, mtime, sha256 });
33172
33276
  } catch {
33173
33277
  }
33174
33278
  }
@@ -33182,8 +33286,8 @@ async function ensureLocalHashes(root, local, paths) {
33182
33286
  const info2 = local.get(path5);
33183
33287
  if (!info2 || info2.sha256) continue;
33184
33288
  try {
33185
- const { sha256: sha2562 } = await hashFile(join4(root, path5));
33186
- info2.sha256 = sha2562;
33289
+ const { sha256 } = await hashFile(join5(root, path5));
33290
+ info2.sha256 = sha256;
33187
33291
  } catch {
33188
33292
  }
33189
33293
  }
@@ -33198,10 +33302,10 @@ function resolveInRoot(root, relPath) {
33198
33302
  throw new Error(`Refusing path outside project root: ${relPath}`);
33199
33303
  }
33200
33304
  try {
33201
- const rootReal = realpathSync(rootResolved);
33305
+ const rootReal = realpathSync2(rootResolved);
33202
33306
  let ancestor = full;
33203
33307
  while (!existsSync5(ancestor) && dirname4(ancestor) !== ancestor) ancestor = dirname4(ancestor);
33204
- const ancestorReal = realpathSync(ancestor);
33308
+ const ancestorReal = realpathSync2(ancestor);
33205
33309
  if (ancestorReal !== rootReal && !ancestorReal.startsWith(rootReal + sep)) {
33206
33310
  throw new Error(`Refusing path outside project root (symlink escape): ${relPath}`);
33207
33311
  }
@@ -33229,7 +33333,7 @@ async function fetchRemote(projectGuid) {
33229
33333
  function extractTarToMap(stream, idleMs, onBytes, keep) {
33230
33334
  const extract3 = tar2.extract();
33231
33335
  const files = /* @__PURE__ */ new Map();
33232
- return new Promise((resolve20, reject) => {
33336
+ return new Promise((resolve19, reject) => {
33233
33337
  let settled = false;
33234
33338
  let idle;
33235
33339
  const arm = () => {
@@ -33265,7 +33369,7 @@ function extractTarToMap(stream, idleMs, onBytes, keep) {
33265
33369
  });
33266
33370
  entryStream.resume();
33267
33371
  });
33268
- extract3.on("finish", () => done(resolve20, files));
33372
+ extract3.on("finish", () => done(resolve19, files));
33269
33373
  extract3.on("error", (e) => done(reject, e));
33270
33374
  stream.on("error", (e) => done(reject, e));
33271
33375
  arm();
@@ -33489,9 +33593,9 @@ Plan deletes ${totalDeletes} files (${Math.round(fraction * 100)}% of the tree).
33489
33593
  }
33490
33594
  var GIPITY_IGNORE_FILE = ".gipityignore";
33491
33595
  function readGipityIgnore(root) {
33492
- const path5 = join4(root, GIPITY_IGNORE_FILE);
33596
+ const path5 = join5(root, GIPITY_IGNORE_FILE);
33493
33597
  if (!existsSync5(path5)) return [];
33494
- return readFileSync6(path5, "utf8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => line.replace(/^\.\//, "").replace(/^\//, ""));
33598
+ return readFileSync7(path5, "utf8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => line.replace(/^\.\//, "").replace(/^\//, ""));
33495
33599
  }
33496
33600
  function effectiveIgnore(root, configIgnore) {
33497
33601
  const base = configIgnore && configIgnore.length ? configIgnore : DEFAULT_SYNC_IGNORE;
@@ -33532,9 +33636,9 @@ async function syncInner(projectGuid, root, ignore2, opts, interactive) {
33532
33636
  const config = { projectGuid, ignore: ignore2 };
33533
33637
  const p = opts.progress;
33534
33638
  const baseline = readBaseline(projectGuid);
33535
- p?.phase("Scanning local files\u2026");
33639
+ p?.phase("Scanning local files...");
33536
33640
  const local = walkLocal(root, ignore2, baseline.files);
33537
- p?.phase("Checking Gipity for changes\u2026");
33641
+ p?.phase("Checking Gipity for changes...");
33538
33642
  const remote = await fetchRemote(projectGuid);
33539
33643
  for (const path5 of [...remote.keys()]) {
33540
33644
  if (shouldIgnore(path5, ignore2)) remote.delete(path5);
@@ -33545,7 +33649,7 @@ async function syncInner(projectGuid, root, ignore2, opts, interactive) {
33545
33649
  const r = remote.get(path5);
33546
33650
  if (r?.sha256 || baseline.files[path5]) needHash.push(path5);
33547
33651
  }
33548
- if (needHash.length) p?.phase(`Hashing ${needHash.length} file${needHash.length === 1 ? "" : "s"}\u2026`);
33652
+ if (needHash.length) p?.phase(`Hashing ${needHash.length} file${needHash.length === 1 ? "" : "s"}...`);
33549
33653
  await ensureLocalHashes(root, local, needHash);
33550
33654
  const planned = plan(local, remote, baseline.files);
33551
33655
  if (opts.plan) {
@@ -33631,13 +33735,13 @@ async function syncInner(projectGuid, root, ignore2, opts, interactive) {
33631
33735
  if (buf) downloadedBytes.set(a.path, buf);
33632
33736
  }
33633
33737
  } catch (err) {
33634
- errors.push(`Bulk download incomplete (${err.message}); recovering files individually\u2026`);
33738
+ errors.push(`Bulk download incomplete (${err.message}); recovering files individually...`);
33635
33739
  } finally {
33636
33740
  p?.finish();
33637
33741
  }
33638
33742
  const missing = wantedDownloads.filter((a) => !downloadedBytes.has(a.path));
33639
33743
  if (missing.length) {
33640
- p?.phase(`Recovering ${missing.length} file${missing.length === 1 ? "" : "s"} the bulk download dropped\u2026`);
33744
+ p?.phase(`Recovering ${missing.length} file${missing.length === 1 ? "" : "s"} the bulk download dropped...`);
33641
33745
  for (const a of missing) {
33642
33746
  const expectedSha = remote.get(a.path)?.sha256 ?? void 0;
33643
33747
  let buf = null;
@@ -33679,7 +33783,7 @@ async function syncInner(projectGuid, root, ignore2, opts, interactive) {
33679
33783
  if (currentBytes) {
33680
33784
  mkdirSync4(dirname4(full), { recursive: true });
33681
33785
  writeFileSync5(full, currentBytes);
33682
- const stat2 = statSync(full);
33786
+ const stat2 = statSync2(full);
33683
33787
  baseline.files[a.path] = {
33684
33788
  size: stat2.size,
33685
33789
  mtime: stat2.mtime.toISOString(),
@@ -33695,12 +33799,12 @@ async function syncInner(projectGuid, root, ignore2, opts, interactive) {
33695
33799
  renamedRel,
33696
33800
  { expectedServerVersion: null }
33697
33801
  );
33698
- const stat2 = statSync(renamedFull);
33699
- const { sha256: sha2562 } = await hashFile(renamedFull);
33802
+ const stat2 = statSync2(renamedFull);
33803
+ const { sha256 } = await hashFile(renamedFull);
33700
33804
  baseline.files[renamedRel] = {
33701
33805
  size: stat2.size,
33702
33806
  mtime: stat2.mtime.toISOString(),
33703
- sha256: sha2562,
33807
+ sha256,
33704
33808
  serverVersion: result.serverVersion
33705
33809
  };
33706
33810
  } catch (e) {
@@ -33736,7 +33840,7 @@ async function syncInner(projectGuid, root, ignore2, opts, interactive) {
33736
33840
  }
33737
33841
  mkdirSync4(dirname4(full), { recursive: true });
33738
33842
  writeFileSync5(full, buf);
33739
- const stat2 = statSync(full);
33843
+ const stat2 = statSync2(full);
33740
33844
  local.set(a.path, { size: stat2.size, mtime: stat2.mtime.toISOString(), sha256: void 0 });
33741
33845
  baseline.files[a.path] = {
33742
33846
  size: stat2.size,
@@ -33767,7 +33871,7 @@ async function syncInner(projectGuid, root, ignore2, opts, interactive) {
33767
33871
  if (buf) {
33768
33872
  mkdirSync4(dirname4(full), { recursive: true });
33769
33873
  writeFileSync5(full, buf);
33770
- const stat2 = statSync(full);
33874
+ const stat2 = statSync2(full);
33771
33875
  baseline.files[a.path] = {
33772
33876
  size: stat2.size,
33773
33877
  mtime: stat2.mtime.toISOString(),
@@ -33781,12 +33885,12 @@ async function syncInner(projectGuid, root, ignore2, opts, interactive) {
33781
33885
  const result = await uploadOneFile(config.projectGuid, renamed, a.renamedLocalTo, {
33782
33886
  expectedServerVersion: null
33783
33887
  });
33784
- const stat2 = statSync(renamed);
33785
- const { sha256: sha2562 } = await hashFile(renamed);
33888
+ const stat2 = statSync2(renamed);
33889
+ const { sha256 } = await hashFile(renamed);
33786
33890
  baseline.files[a.renamedLocalTo] = {
33787
33891
  size: stat2.size,
33788
33892
  mtime: stat2.mtime.toISOString(),
33789
- sha256: sha2562,
33893
+ sha256,
33790
33894
  serverVersion: result.serverVersion
33791
33895
  };
33792
33896
  } catch (err) {
@@ -33820,14 +33924,14 @@ async function syncInner(projectGuid, root, ignore2, opts, interactive) {
33820
33924
  continue;
33821
33925
  }
33822
33926
  try {
33823
- const stat2 = statSync(full);
33927
+ const stat2 = statSync2(full);
33824
33928
  if (stat2.size > UPLOAD_MAX_BYTES) {
33825
33929
  errors.push(`Upload failed for ${a.path}: file exceeds the 30 GB upload limit`);
33826
33930
  onBytes?.(a.localSize ?? 0);
33827
33931
  continue;
33828
33932
  }
33829
- const sha2562 = local.get(a.path)?.sha256 ?? (await hashFile(full)).sha256;
33830
- prepared.push({ a, full, size: stat2.size, mtime: stat2.mtime.toISOString(), sha256: sha2562 });
33933
+ const sha256 = local.get(a.path)?.sha256 ?? (await hashFile(full)).sha256;
33934
+ prepared.push({ a, full, size: stat2.size, mtime: stat2.mtime.toISOString(), sha256 });
33831
33935
  } catch (e) {
33832
33936
  errors.push(`Upload failed for ${a.path}: ${e.message}`);
33833
33937
  onBytes?.(a.localSize ?? 0);
@@ -33974,13 +34078,13 @@ async function syncInner(projectGuid, root, ignore2, opts, interactive) {
33974
34078
  const full = resolveInRoot(root, a.path);
33975
34079
  mkdirSync4(dirname4(full), { recursive: true });
33976
34080
  writeFileSync5(full, buf);
33977
- const stat2 = statSync(full);
33978
- const { sha256: sha2562 } = await hashFile(full);
34081
+ const stat2 = statSync2(full);
34082
+ const { sha256 } = await hashFile(full);
33979
34083
  const current = typeof err.data?.current_server_version === "number" ? err.data.current_server_version : null;
33980
34084
  baseline.files[a.path] = {
33981
34085
  size: stat2.size,
33982
34086
  mtime: stat2.mtime.toISOString(),
33983
- sha256: sha2562,
34087
+ sha256,
33984
34088
  serverVersion: current ?? a.expectedServerVersion ?? 0
33985
34089
  };
33986
34090
  errors.push(`Could not delete ${a.path}: server has a newer version - restored the server copy locally (delete it again and re-sync to confirm)`);
@@ -34024,7 +34128,7 @@ async function syncInner(projectGuid, root, ignore2, opts, interactive) {
34024
34128
  function cleanupEmptyDirs(root, emptiedDirs) {
34025
34129
  const isEmpty = (dir) => {
34026
34130
  try {
34027
- return readdirSync3(dir).length === 0;
34131
+ return readdirSync4(dir).length === 0;
34028
34132
  } catch {
34029
34133
  return false;
34030
34134
  }
@@ -34059,12 +34163,12 @@ async function pushFile(filePath) {
34059
34163
  const result = await uploadOneFile(config.projectGuid, filePath, rel2, {
34060
34164
  expectedServerVersion: baseEntry ? baseEntry.serverVersion : null
34061
34165
  });
34062
- const stat2 = statSync(filePath);
34063
- const { sha256: sha2562 } = await hashFile(filePath);
34166
+ const stat2 = statSync2(filePath);
34167
+ const { sha256 } = await hashFile(filePath);
34064
34168
  baseline.files[rel2] = {
34065
34169
  size: stat2.size,
34066
34170
  mtime: stat2.mtime.toISOString(),
34067
- sha256: sha2562,
34171
+ sha256,
34068
34172
  serverVersion: result.serverVersion
34069
34173
  };
34070
34174
  writeBaseline(baseline);
@@ -34251,6 +34355,89 @@ async function syncBeforeAction(opts) {
34251
34355
  }
34252
34356
  }
34253
34357
 
34358
+ // src/helpers/body.ts
34359
+ import { readFileSync as readFileSync8 } from "node:fs";
34360
+ function readStdin() {
34361
+ if (process.stdin.isTTY) return "";
34362
+ try {
34363
+ return readFileSync8(0, "utf-8");
34364
+ } catch {
34365
+ return "";
34366
+ }
34367
+ }
34368
+ function readFileField(spec) {
34369
+ const eq2 = spec.indexOf("=");
34370
+ if (eq2 === -1) {
34371
+ throw new Error(`Invalid --file '${spec}': expected field=@path (e.g. --file image=@receipt.png).`);
34372
+ }
34373
+ const field = spec.slice(0, eq2).trim();
34374
+ let path5 = spec.slice(eq2 + 1);
34375
+ if (path5.startsWith("@")) path5 = path5.slice(1);
34376
+ if (!field) throw new Error(`Invalid --file '${spec}': missing field name before '='.`);
34377
+ if (!path5) throw new Error(`Invalid --file '${spec}': missing file path after '='.`);
34378
+ try {
34379
+ return [field, { data: readFileSync8(path5).toString("base64"), media_type: guessMime(path5) }];
34380
+ } catch (e) {
34381
+ throw new Error(`Cannot read --file '${path5}': ${e.message}`);
34382
+ }
34383
+ }
34384
+ function resolveBody(raw, fileSpecs) {
34385
+ const body = resolveJsonBody(raw);
34386
+ if (!fileSpecs || fileSpecs.length === 0) return body;
34387
+ if (typeof body !== "object" || body === null || Array.isArray(body)) {
34388
+ throw new Error("--file needs an object body to attach into (got a non-object JSON body).");
34389
+ }
34390
+ const obj = body;
34391
+ for (const spec of fileSpecs) {
34392
+ const [field, attachment] = readFileField(spec);
34393
+ obj[field] = attachment;
34394
+ }
34395
+ return obj;
34396
+ }
34397
+ function resolveJsonBody(raw) {
34398
+ if (raw == null || raw === "") return {};
34399
+ let source;
34400
+ let origin;
34401
+ if (raw === "-" || raw === "@-") {
34402
+ source = readStdin();
34403
+ origin = "stdin";
34404
+ if (source.trim() === "") {
34405
+ throw new Error("No JSON on stdin (pipe a body in, e.g. `cat body.json | gipity fn call foo -d -`).");
34406
+ }
34407
+ } else if (raw.startsWith("@")) {
34408
+ const path5 = raw.slice(1);
34409
+ try {
34410
+ source = readFileSync8(path5, "utf-8");
34411
+ } catch (e) {
34412
+ throw new Error(`Cannot read body file '${path5}': ${e.message}`);
34413
+ }
34414
+ origin = `file '${path5}'`;
34415
+ } else {
34416
+ source = raw;
34417
+ origin = "inline JSON";
34418
+ }
34419
+ try {
34420
+ return JSON.parse(source);
34421
+ } catch (e) {
34422
+ throw new Error(`Invalid JSON in ${origin}: ${e.message}`);
34423
+ }
34424
+ }
34425
+
34426
+ // src/helpers/duration.ts
34427
+ var UNIT_TO_MS = { ms: 1, s: 1e3, m: 6e4 };
34428
+ function parseDuration(raw, unit) {
34429
+ if (raw === void 0) return null;
34430
+ const m = raw.trim().match(/^(\d+(?:\.\d+)?)\s*(ms|s|m)?$/i);
34431
+ if (!m) return null;
34432
+ const n = parseFloat(m[1]);
34433
+ if (!Number.isFinite(n)) return null;
34434
+ const suffix = m[2]?.toLowerCase();
34435
+ if (!suffix) return { value: n, hadSuffix: false };
34436
+ const ms2 = n * UNIT_TO_MS[suffix];
34437
+ const value = unit === "ms" ? ms2 : ms2 / 1e3;
34438
+ return { value, hadSuffix: true };
34439
+ }
34440
+
34254
34441
  // src/commands/token.ts
34255
34442
  var tokenCommand = new Command("token").description("Manage API tokens").addHelpText("after", "\nLong-lived agent API tokens (gip_at_*) for headless agents and CI.");
34256
34443
  var fmtDate = (d) => d ? new Date(d).toLocaleDateString() : "never";
@@ -34301,23 +34488,23 @@ tokenCommand.command("revoke <short_guid>").alias("rm").description("Revoke an a
34301
34488
  init_api();
34302
34489
  init_config();
34303
34490
  init_auth();
34304
- import { basename, resolve as resolve7, dirname as dirname5 } from "path";
34305
- import { existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
34491
+ import { basename as basename2, resolve as resolve7, dirname as dirname5 } from "path";
34492
+ import { existsSync as existsSync8, readFileSync as readFileSync11 } from "fs";
34306
34493
  init_colors();
34307
34494
  init_utils();
34308
34495
 
34309
34496
  // src/adopt-cwd.ts
34310
34497
  init_api();
34311
- import { readdirSync as readdirSync5, statSync as statSync3 } from "fs";
34312
- import { join as join8, resolve as resolve6, sep as sep2, parse as parsePath } from "path";
34498
+ import { readdirSync as readdirSync6, statSync as statSync4 } from "fs";
34499
+ import { join as join9, resolve as resolve6, sep as sep2, parse as parsePath } from "path";
34313
34500
  import { homedir as homedir6 } from "os";
34314
34501
 
34315
34502
  // src/project-setup.ts
34316
34503
  init_config();
34317
34504
 
34318
34505
  // src/template-vars.ts
34319
- import { promises as fs, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
34320
- import { join as join5, relative as relative2, extname as extname3 } from "path";
34506
+ import { promises as fs, readdirSync as readdirSync5, statSync as statSync3 } from "fs";
34507
+ import { join as join6, relative as relative2, extname as extname3 } from "path";
34321
34508
  var SUBSTITUTABLE_EXTS = /* @__PURE__ */ new Set([
34322
34509
  ".html",
34323
34510
  ".htm",
@@ -34383,18 +34570,18 @@ function* walkTextFiles(root) {
34383
34570
  const dir = stack.pop();
34384
34571
  let entries;
34385
34572
  try {
34386
- entries = readdirSync4(dir, { withFileTypes: true });
34573
+ entries = readdirSync5(dir, { withFileTypes: true });
34387
34574
  } catch {
34388
34575
  continue;
34389
34576
  }
34390
34577
  for (const entry of entries) {
34391
34578
  if (isSyncIgnored(entry.name)) continue;
34392
- const full = join5(dir, entry.name);
34579
+ const full = join6(dir, entry.name);
34393
34580
  if (entry.isDirectory()) {
34394
34581
  stack.push(full);
34395
34582
  } else if (entry.isFile()) {
34396
34583
  try {
34397
- if (!statSync2(full).isFile()) continue;
34584
+ if (!statSync3(full).isFile()) continue;
34398
34585
  } catch {
34399
34586
  continue;
34400
34587
  }
@@ -34411,7 +34598,7 @@ async function substituteDir(dir, vars) {
34411
34598
  for (const rel2 of walkTextFiles(dir)) {
34412
34599
  const ext = extname3(rel2).toLowerCase();
34413
34600
  if (!SUBSTITUTABLE_EXTS.has(ext)) continue;
34414
- const abs = join5(dir, rel2);
34601
+ const abs = join6(dir, rel2);
34415
34602
  let content;
34416
34603
  try {
34417
34604
  content = await fs.readFile(abs, "utf-8");
@@ -34477,18 +34664,18 @@ async function finalizeLocalProject(opts) {
34477
34664
  }
34478
34665
 
34479
34666
  // src/relay/paths.ts
34480
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
34667
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
34481
34668
  import { homedir as homedir4 } from "os";
34482
- import { join as join6, resolve as resolve5 } from "path";
34669
+ import { join as join7, resolve as resolve5 } from "path";
34483
34670
  function getProjectsRoot() {
34484
- const settingsPath = join6(homedir4(), ".gipity", "settings.json");
34485
- const defaultDir = join6(homedir4(), "GipityProjects");
34671
+ const settingsPath = join7(homedir4(), ".gipity", "settings.json");
34672
+ const defaultDir = join7(homedir4(), "GipityProjects");
34486
34673
  try {
34487
34674
  if (existsSync6(settingsPath)) {
34488
- const settings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
34675
+ const settings = JSON.parse(readFileSync9(settingsPath, "utf-8"));
34489
34676
  if (settings.projectsDir) return resolve5(settings.projectsDir);
34490
34677
  } else {
34491
- mkdirSync5(join6(homedir4(), ".gipity"), { recursive: true });
34678
+ mkdirSync5(join7(homedir4(), ".gipity"), { recursive: true });
34492
34679
  writeFileSync6(settingsPath, JSON.stringify({ projectsDir: defaultDir }, null, 2) + "\n");
34493
34680
  }
34494
34681
  } catch {
@@ -34497,11 +34684,11 @@ function getProjectsRoot() {
34497
34684
  }
34498
34685
 
34499
34686
  // 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";
34501
- import { join as join7 } from "path";
34687
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, existsSync as existsSync7, chmodSync as chmodSync2, unlinkSync as unlinkSync4 } from "fs";
34688
+ import { join as join8 } from "path";
34502
34689
  import { homedir as homedir5 } from "os";
34503
- var RELAY_DIR = process.env.GIPITY_DIR || join7(homedir5(), ".gipity");
34504
- var RELAY_FILE = join7(RELAY_DIR, "relay.json");
34690
+ var RELAY_DIR = process.env.GIPITY_DIR || join8(homedir5(), ".gipity");
34691
+ var RELAY_FILE = join8(RELAY_DIR, "relay.json");
34505
34692
  var FILE_MODE = 384;
34506
34693
  function emptyState() {
34507
34694
  return { device: null, paused: false };
@@ -34509,7 +34696,7 @@ function emptyState() {
34509
34696
  function loadState() {
34510
34697
  if (!existsSync7(RELAY_FILE)) return emptyState();
34511
34698
  try {
34512
- const raw = JSON.parse(readFileSync8(RELAY_FILE, "utf-8"));
34699
+ const raw = JSON.parse(readFileSync10(RELAY_FILE, "utf-8"));
34513
34700
  return {
34514
34701
  device: raw.device ?? null,
34515
34702
  paused: Boolean(raw.paused),
@@ -34594,7 +34781,7 @@ function diagnosticsConsented() {
34594
34781
  if (env && env !== "0" && env.toLowerCase() !== "false") return false;
34595
34782
  return loadState().diagnostics_consent !== false;
34596
34783
  }
34597
- var RELAY_PID_FILE = join7(RELAY_DIR, "relay.pid");
34784
+ var RELAY_PID_FILE = join8(RELAY_DIR, "relay.pid");
34598
34785
  function getDaemonPidPath() {
34599
34786
  return RELAY_PID_FILE;
34600
34787
  }
@@ -34611,7 +34798,7 @@ function clearDaemonPid() {
34611
34798
  function isDaemonRunning() {
34612
34799
  if (!existsSync7(RELAY_PID_FILE)) return false;
34613
34800
  try {
34614
- const raw = readFileSync8(RELAY_PID_FILE, "utf-8").trim();
34801
+ const raw = readFileSync10(RELAY_PID_FILE, "utf-8").trim();
34615
34802
  const pid = parseInt(raw, 10);
34616
34803
  if (!pid || isNaN(pid)) {
34617
34804
  try {
@@ -34651,17 +34838,17 @@ function scanForAdoption(cwd) {
34651
34838
  if (truncated) return;
34652
34839
  let entries;
34653
34840
  try {
34654
- entries = readdirSync5(dir);
34841
+ entries = readdirSync6(dir);
34655
34842
  } catch {
34656
34843
  return;
34657
34844
  }
34658
34845
  for (const name of entries) {
34659
34846
  if (truncated) return;
34660
34847
  if (isSyncIgnored(name)) continue;
34661
- const full = join8(dir, name);
34848
+ const full = join9(dir, name);
34662
34849
  let st2;
34663
34850
  try {
34664
- st2 = statSync3(full);
34851
+ st2 = statSync4(full);
34665
34852
  } catch {
34666
34853
  continue;
34667
34854
  }
@@ -34687,7 +34874,7 @@ function scanForAdoption(cwd) {
34687
34874
  function isLikelyEmpty(cwd) {
34688
34875
  let entries;
34689
34876
  try {
34690
- entries = readdirSync5(cwd);
34877
+ entries = readdirSync6(cwd);
34691
34878
  } catch {
34692
34879
  return true;
34693
34880
  }
@@ -34725,18 +34912,18 @@ function canAdoptCwd(cwd) {
34725
34912
  function countGitRepoChildren(dir) {
34726
34913
  let entries;
34727
34914
  try {
34728
- entries = readdirSync5(dir);
34915
+ entries = readdirSync6(dir);
34729
34916
  } catch {
34730
34917
  return 0;
34731
34918
  }
34732
34919
  let count = 0;
34733
34920
  for (const name of entries.slice(0, 50)) {
34734
34921
  if (name.startsWith(".")) continue;
34735
- const child = join8(dir, name);
34922
+ const child = join9(dir, name);
34736
34923
  try {
34737
- if (!statSync3(child).isDirectory()) continue;
34738
- const gitPath = join8(child, ".git");
34739
- if (statSync3(gitPath)) count++;
34924
+ if (!statSync4(child).isDirectory()) continue;
34925
+ const gitPath = join9(child, ".git");
34926
+ if (statSync4(gitPath)) count++;
34740
34927
  } catch {
34741
34928
  }
34742
34929
  if (count >= 3) return count;
@@ -34750,7 +34937,7 @@ function formatCwdLabel(cwd) {
34750
34937
  if (norm.startsWith(home + sep2)) {
34751
34938
  const tail = norm.slice(home.length + 1).split(sep2);
34752
34939
  if (tail.length <= 2) return "~/" + tail.join("/");
34753
- return "~/\u2026/" + tail.slice(-2).join("/");
34940
+ return "~/.../" + tail.slice(-2).join("/");
34754
34941
  }
34755
34942
  const parts = norm.split(sep2).filter(Boolean);
34756
34943
  if (parts.length <= 1) return norm;
@@ -34915,7 +35102,7 @@ Working with an existing Gipity project:
34915
35102
  process.exit(1);
34916
35103
  }
34917
35104
  }
34918
- const projectName = (name || basename(cwd)).slice(0, 100);
35105
+ const projectName = (name || basename2(cwd)).slice(0, 100);
34919
35106
  const projectSlug = slugify(projectName);
34920
35107
  if (!projectSlug) {
34921
35108
  console.error(error("Could not derive a valid project slug. Provide a name: gipity init my-app"));
@@ -34957,7 +35144,7 @@ Working with an existing Gipity project:
34957
35144
  if (adopted.applied > 0) console.log(`Synced ${adopted.applied} change${adopted.applied > 1 ? "s" : ""} with Gipity.`);
34958
35145
  if (opts.capture === false) {
34959
35146
  try {
34960
- const cfg = JSON.parse(readFileSync9(resolve7(cwd, ".gipity.json"), "utf-8"));
35147
+ const cfg = JSON.parse(readFileSync11(resolve7(cwd, ".gipity.json"), "utf-8"));
34961
35148
  cfg.captureHooks = false;
34962
35149
  saveConfigAt(cwd, cfg);
34963
35150
  } catch {
@@ -34991,15 +35178,15 @@ init_auth();
34991
35178
  init_api();
34992
35179
  init_config();
34993
35180
  init_colors();
34994
- import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
34995
- import { join as join9, resolve as resolve8 } from "path";
35181
+ import { existsSync as existsSync9, readFileSync as readFileSync12 } from "fs";
35182
+ import { join as join10, resolve as resolve8 } from "path";
34996
35183
  import { homedir as homedir7 } from "os";
34997
35184
  function checkGipityPlugin() {
34998
- const path5 = join9(homedir7(), ".claude", "settings.json");
35185
+ const path5 = join10(homedir7(), ".claude", "settings.json");
34999
35186
  let settings = {};
35000
35187
  if (existsSync9(path5)) {
35001
35188
  try {
35002
- settings = JSON.parse(readFileSync10(path5, "utf-8"));
35189
+ settings = JSON.parse(readFileSync12(path5, "utf-8"));
35003
35190
  } catch {
35004
35191
  }
35005
35192
  }
@@ -35012,12 +35199,14 @@ function checkGipityPlugin() {
35012
35199
  return { missing, ok: missing.length === 0, stale };
35013
35200
  }
35014
35201
  async function probeAuth(loggedIn) {
35015
- if (!loggedIn && !usingEnvToken()) return "none";
35016
- if (!usingEnvToken() && sessionExpired()) return "expired";
35017
- const timeout = new Promise((res) => setTimeout(() => res("unreachable"), 5e3).unref?.());
35202
+ if (!loggedIn && !usingEnvToken()) return { state: "none", account: null };
35203
+ if (!usingEnvToken() && sessionExpired()) return { state: "expired", account: null };
35204
+ const timeout = new Promise(
35205
+ (res) => setTimeout(() => res({ state: "unreachable", account: null }), 5e3).unref?.()
35206
+ );
35018
35207
  const call = get("/users/me").then(
35019
- () => "ok",
35020
- (err) => err instanceof ApiError && err.statusCode === 401 ? "rejected" : "unreachable"
35208
+ (res) => ({ state: "ok", account: res.data?.accountSlug ?? null }),
35209
+ (err) => ({ state: err instanceof ApiError && err.statusCode === 401 ? "rejected" : "unreachable", account: null })
35021
35210
  );
35022
35211
  return Promise.race([call, timeout]);
35023
35212
  }
@@ -35029,6 +35218,8 @@ var statusCommand = new Command("status").alias("whoami").description("Show proj
35029
35218
  const hookCheck = config ? checkGipityPlugin() : null;
35030
35219
  const queueDelivered = auth && !sessionExpired() ? await flushBugQueue().catch(() => 0) : 0;
35031
35220
  const probe = await probeAuth(!!auth);
35221
+ const accountMismatch = probe.state === "ok" && !!config && !!probe.account && probe.account !== config.accountSlug;
35222
+ const apiBaseInUse = resolveApiBase();
35032
35223
  if (opts.json) {
35033
35224
  console.log(JSON.stringify({
35034
35225
  project: config ? {
@@ -35036,17 +35227,21 @@ var statusCommand = new Command("status").alias("whoami").description("Show proj
35036
35227
  slug: config.projectSlug,
35037
35228
  account: config.accountSlug,
35038
35229
  apiBase: config.apiBase,
35230
+ apiBaseInUse,
35039
35231
  url: liveUrl(config)
35040
35232
  } : null,
35041
35233
  // `valid` reflects the refresh token (the real session) - access
35042
35234
  // tokens auto-renew, so their expiry must not read as "invalid".
35043
35235
  // `probe` is what one live call just proved: 'rejected' means every
35044
35236
  // authenticated command will fail even though `valid` reads true.
35237
+ // 'mismatch' overrides 'ok' when the live account isn't the one that
35238
+ // owns this project - `valid` still reads true (the token IS valid).
35045
35239
  auth: auth || usingEnvToken() ? {
35046
35240
  email: auth?.email,
35241
+ account: probe.account,
35047
35242
  source: usingEnvToken() ? "agent-token" : "session",
35048
- valid: usingEnvToken() ? probe !== "rejected" : !sessionExpired(),
35049
- probe
35243
+ valid: usingEnvToken() ? probe.state !== "rejected" : !sessionExpired(),
35244
+ probe: accountMismatch ? "mismatch" : probe.state
35050
35245
  } : null,
35051
35246
  plugin: hookCheck
35052
35247
  }, null, 2));
@@ -35059,18 +35254,25 @@ var statusCommand = new Command("status").alias("whoami").description("Show proj
35059
35254
  console.log(`${muted("Account:")} ${config.accountSlug}`);
35060
35255
  console.log(`${muted("Live:")} ${liveUrl(config)}`);
35061
35256
  console.log(`${muted("API:")} ${config.apiBase}`);
35257
+ if (apiBaseInUse !== config.apiBase) {
35258
+ console.log(`${muted("API (in use):")} ${warning(apiBaseInUse)} ${muted("(overrides .gipity.json \u2014 GIPITY_API_BASE / --api-base / allowlist)")}`);
35259
+ }
35062
35260
  if (config.agentGuid) console.log(`${muted("Agent:")} ${config.agentGuid}`);
35063
35261
  }
35064
35262
  if (usingEnvToken()) {
35065
- console.log(`${muted("Auth:")} ${probe === "rejected" ? warning("agent API token (GIPITY_TOKEN) rejected by the server \u2014 mint a new one: gipity skill read agent-deploy") : success("agent API token (GIPITY_TOKEN)")}${probe === "unreachable" ? ` ${muted("(unverified \u2014 API unreachable)")}` : ""}`);
35263
+ console.log(`${muted("Auth:")} ${probe.state === "rejected" ? warning("agent API token (GIPITY_TOKEN) rejected by the server \u2014 mint a new one: gipity skill read agent-deploy") : success("agent API token (GIPITY_TOKEN)")}${probe.state === "unreachable" ? ` ${muted("(unverified \u2014 API unreachable)")}` : ""}`);
35066
35264
  } else if (!auth) {
35067
35265
  console.log(`${muted("Auth:")} ${warning("not logged in. Run: gipity login")}`);
35068
- } else if (probe === "expired") {
35266
+ } else if (probe.state === "expired") {
35069
35267
  console.log(`${muted("Auth:")} ${warning(`session expired for ${auth.email}. Run: gipity login (headless/CI: set GIPITY_TOKEN \u2014 gipity skill read agent-deploy)`)}`);
35070
- } else if (probe === "rejected") {
35268
+ } else if (probe.state === "rejected") {
35071
35269
  console.log(`${muted("Auth:")} ${warning(`session for ${auth.email} was rejected by the server. Run: gipity login (headless/CI: set GIPITY_TOKEN \u2014 gipity skill read agent-deploy)`)}`);
35072
35270
  } else {
35073
- console.log(`${muted("Auth:")} ${success(auth.email)}${probe === "unreachable" ? ` ${muted("(unverified \u2014 API unreachable)")}` : ""}`);
35271
+ console.log(`${muted("Auth:")} ${success(auth.email)}${probe.state === "unreachable" ? ` ${muted("(unverified \u2014 API unreachable)")}` : ""}`);
35272
+ }
35273
+ if (accountMismatch) {
35274
+ console.log(`${muted("Account:")} ${warning(`logged-in account (${probe.account}) differs from this project's account (${config.accountSlug}). If you didn't expect this you may be logged into the wrong account \u2014 run: gipity login`)}`);
35275
+ console.log(muted("(If this project was shared with you via gipity rbac, this is expected.)"));
35074
35276
  }
35075
35277
  if (queueDelivered > 0) {
35076
35278
  console.log(`${muted("Bug queue:")} ${success(`delivered ${queueDelivered} queued bug report${queueDelivered === 1 ? "" : "s"}`)}`);
@@ -35101,7 +35303,10 @@ var statusCommand = new Command("status").alias("whoami").description("Show proj
35101
35303
  });
35102
35304
 
35103
35305
  // src/commands/sync.ts
35306
+ import { dirname as dirname6 } from "node:path";
35104
35307
  init_colors();
35308
+ init_utils();
35309
+ init_config();
35105
35310
  var syncCommand = new Command("sync").description("Sync files").addHelpText("after", "\nAdd a .gipityignore at the project root to exclude paths (gitignore-style).").option("--plan", "Print the plan without applying any changes").option("--force", "Bypass the bulk-deletion guard").option("--prune", "Remove files that exist on Gipity but not locally (applies the bulk deletes the guard defers)").option("--json", "Output as JSON").action(async (opts) => {
35106
35311
  try {
35107
35312
  const progress = opts.json ? void 0 : createProgressReporter();
@@ -35114,6 +35319,16 @@ var syncCommand = new Command("sync").description("Sync files").addHelpText("aft
35114
35319
  console.log(`
35115
35320
  ${result.applied} action${result.applied > 1 ? "s" : ""} applied.`);
35116
35321
  }
35322
+ if (isWsl()) {
35323
+ const configPath = getConfigPath();
35324
+ const twin = configPath ? findWindowsTwinProject(dirname6(configPath)) : null;
35325
+ if (twin) {
35326
+ console.log(muted(
35327
+ `
35328
+ Note: a Windows-side copy of this project exists at ${wslPathToWindows(twin)} and is NOT synced. If you added files there, move them into this folder so they sync.`
35329
+ ));
35330
+ }
35331
+ }
35117
35332
  if (result.deferredDeletes > 0) {
35118
35333
  console.log(muted(
35119
35334
  `
@@ -35159,106 +35374,60 @@ var pushCommand = new Command("push").description("Push one or more files").argu
35159
35374
  });
35160
35375
 
35161
35376
  // src/commands/upload.ts
35377
+ init_api();
35162
35378
  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";
35379
+ import { statSync as statSync5 } from "fs";
35380
+ import { basename as basename3 } from "path";
35165
35381
  init_utils();
35166
35382
  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) => {
35383
+ var uploadCommand = new Command("upload").description(`Upload a local file and print a durable, worker-reachable URL for it.
35384
+
35385
+ The URL works immediately - no \`gipity deploy\` - and any job or function can
35386
+ fetch it. Use it to hand a GPU/CPU job a real input file when testing end-to-end:
35387
+
35388
+ gipity upload song.mp3
35389
+ gipity job submit split-stems --data '{"audio_url":"<printed url>"}'
35390
+
35391
+ By default the file is PUBLIC: a plain \`media.gipity.ai\` CDN url that resolves
35392
+ from anywhere, so a cloud worker can always fetch it. Pass --private for a
35393
+ 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 () => {
35394
+ const { config } = await resolveProjectContext();
35395
+ let size;
35185
35396
  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);
35397
+ const st2 = statSync5(file);
35398
+ if (!st2.isFile()) throw new Error("not a regular file");
35399
+ size = st2.size;
35257
35400
  } catch (err) {
35258
- console.error(error(`Upload failed: ${err.message}`));
35259
- process.exit(1);
35401
+ throw new Error(`can't read ${file}: ${err.message}`);
35402
+ }
35403
+ if (size === 0) throw new Error(`${file} is empty - nothing to upload.`);
35404
+ const filename = basename3(file);
35405
+ const contentType = opts.contentType || guessMime(file);
35406
+ const doUpload = async () => {
35407
+ const init = await post(`/api/${config.projectGuid}/uploads/init`, {
35408
+ filename,
35409
+ content_type: contentType,
35410
+ size,
35411
+ public: !opts.private
35412
+ });
35413
+ const fields = await transferToS3(file, size, contentType, init.data);
35414
+ const completeBody = {
35415
+ upload_guid: init.data.upload_guid
35416
+ };
35417
+ if ("parts" in fields) completeBody.parts = fields.parts;
35418
+ const comp = await post(`/api/${config.projectGuid}/uploads/complete`, completeBody);
35419
+ return comp.data;
35420
+ };
35421
+ const data = opts.json ? await doUpload() : await withSpinner(`Uploading ${filename} (${formatSize(size)})...`, doUpload, { done: null });
35422
+ if (opts.json) {
35423
+ console.log(JSON.stringify(data));
35424
+ return;
35260
35425
  }
35261
- });
35426
+ console.log(success(`Uploaded ${data.name} (${formatSize(data.size)})`));
35427
+ console.log(` ${brand(data.url)}`);
35428
+ console.log(muted(` Durable, worker-reachable URL - pass it straight to a job/function as an input URL.`));
35429
+ console.log(muted(` ${data.is_public ? "public (CDN)" : "private (token-signed)"} \xB7 guid: ${data.guid}`));
35430
+ }));
35262
35431
 
35263
35432
  // src/commands/deploy.ts
35264
35433
  init_api();
@@ -35273,7 +35442,7 @@ init_colors();
35273
35442
  init_auth();
35274
35443
 
35275
35444
  // src/commands/page-eval.ts
35276
- import { readFileSync as readFileSync12 } from "node:fs";
35445
+ import { readFileSync as readFileSync14 } from "node:fs";
35277
35446
  init_api();
35278
35447
  init_colors();
35279
35448
  init_auth();
@@ -35282,8 +35451,8 @@ init_config();
35282
35451
  // src/page-fixtures.ts
35283
35452
  init_api();
35284
35453
  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";
35454
+ import { readFileSync as readFileSync13, statSync as statSync6, existsSync as existsSync10, readdirSync as readdirSync7 } from "node:fs";
35455
+ import { basename as basename4, resolve as resolve10, relative as relative3, join as join11 } from "node:path";
35287
35456
  var FIND_SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", ".gipity", "dist", "build", ".next", "coverage"]);
35288
35457
  function findByBasename(root, name, limit = 3) {
35289
35458
  const hits = [];
@@ -35309,22 +35478,22 @@ function findByBasename(root, name, limit = 3) {
35309
35478
  return hits;
35310
35479
  }
35311
35480
  function assertLocalAsset(flag, localPath) {
35312
- const abs = resolve11(localPath);
35481
+ const abs = resolve10(localPath);
35313
35482
  if (existsSync10(abs)) return;
35314
35483
  const root = getProjectRoot() ?? process.cwd();
35315
- const elsewhere = findByBasename(root, basename3(localPath)).filter((p) => p !== abs);
35484
+ const elsewhere = findByBasename(root, basename4(localPath)).filter((p) => p !== abs);
35316
35485
  const found = elsewhere.length ? `
35317
35486
  That file DOES exist here \u2014 pass this path instead:
35318
35487
  ${elsewhere.map((p) => ` ${flag} ${relative3(process.cwd(), p) || p}`).join("\n")}` : `
35319
- Nothing named "${basename3(localPath)}" under ${root} either. Generate a frame with \`gipity generate image "<description>" -o ${localPath}\`, or check the path.`;
35488
+ Nothing named "${basename4(localPath)}" under ${root} either. Generate a frame with \`gipity generate image "<description>" -o ${localPath}\`, or check the path.`;
35320
35489
  throw new Error(
35321
35490
  `${flag} ${localPath}: no such file \u2014 looked for ${abs} (relative paths resolve against the current directory, ${process.cwd()}).${found}`
35322
35491
  );
35323
35492
  }
35324
35493
  async function uploadPublicFixture(projectGuid, localPath, flag = "--fixture") {
35325
35494
  assertLocalAsset(flag, localPath);
35326
- const name = basename3(localPath);
35327
- const size = statSync5(localPath).size;
35495
+ const name = basename4(localPath);
35496
+ const size = statSync6(localPath).size;
35328
35497
  const contentType = guessMime(localPath);
35329
35498
  const init = await post(
35330
35499
  `/api/${projectGuid}/uploads/init`,
@@ -35336,7 +35505,7 @@ async function uploadPublicFixture(projectGuid, localPath, flag = "--fixture") {
35336
35505
  const res = await fetch(init.data.url, {
35337
35506
  method: "PUT",
35338
35507
  headers: { "Content-Type": contentType },
35339
- body: readFileSync11(localPath)
35508
+ body: readFileSync13(localPath)
35340
35509
  });
35341
35510
  if (!res.ok) {
35342
35511
  throw new Error(`upload of fixture "${name}" failed: ${res.status} ${res.statusText}`);
@@ -35356,7 +35525,7 @@ function assertCameraFile(localPath) {
35356
35525
  const ext = localPath.slice(localPath.lastIndexOf(".")).toLowerCase();
35357
35526
  if (CAMERA_EXTS.includes(ext)) return;
35358
35527
  throw new Error(
35359
- `--camera ${basename3(localPath)}: unsupported file type "${ext || "(none)"}" \u2014 the camera feed must be an image or video (${CAMERA_EXTS.join(", ")}).
35528
+ `--camera ${basename4(localPath)}: unsupported file type "${ext || "(none)"}" \u2014 the camera feed must be an image or video (${CAMERA_EXTS.join(", ")}).
35360
35529
  A still image is played as a looping single-frame feed, which is what a gesture/pose/object model needs.
35361
35530
  No frame to hand? Generate one: gipity generate image "a hand making a closed fist, palm to camera, plain background"`
35362
35531
  );
@@ -35409,7 +35578,7 @@ function summarizeExpr(expr) {
35409
35578
  const oneLine = meaningful.length <= 1;
35410
35579
  if (oneLine && expr.trim().length <= EXPR_ECHO_MAX_CHARS) return expr.trim();
35411
35580
  const first = (meaningful[0] ?? "").trim();
35412
- const head = first.length > EXPR_ECHO_MAX_CHARS ? `${first.slice(0, EXPR_ECHO_MAX_CHARS - 1)}\u2026` : first;
35581
+ const head = first.length > EXPR_ECHO_MAX_CHARS ? `${first.slice(0, EXPR_ECHO_MAX_CHARS - 3)}...` : first;
35413
35582
  const shape = oneLine ? `(${expr.trim().length} chars)` : `(+${meaningful.length - 1} more ${meaningful.length - 1 === 1 ? "line" : "lines"}, ${expr.trim().length} chars)`;
35414
35583
  return `${head} ${shape}`;
35415
35584
  }
@@ -35435,6 +35604,9 @@ function capWaitForTimeoutMs(raw) {
35435
35604
  ));
35436
35605
  return WAIT_FOR_MAX_MS;
35437
35606
  }
35607
+ function readScriptFile(pathArg) {
35608
+ return readFileSync14(pathArg === "-" ? 0 : pathArg, "utf8");
35609
+ }
35438
35610
  function parsesAsExpression(s) {
35439
35611
  try {
35440
35612
  new Function(`return (${s}
@@ -35455,6 +35627,7 @@ var CAMERA_SETUP_BUDGET_MS = 3e4;
35455
35627
  var EVAL_SCRIPT_BUDGET_MS = 3e4;
35456
35628
  var EVAL_SCRIPT_BUDGET_CAMERA_MS = 45e3;
35457
35629
  var EVAL_SCRIPT_BUDGET_MAX_MS = 9e4;
35630
+ var EVAL_SCRIPT_BUDGET_MIN_MS = 1e3;
35458
35631
  function capScriptBudgetMs(rawTimeout, hasMedia) {
35459
35632
  const fallback = hasMedia ? EVAL_SCRIPT_BUDGET_CAMERA_MS : EVAL_SCRIPT_BUDGET_MS;
35460
35633
  if (rawTimeout === void 0) return fallback;
@@ -35466,7 +35639,7 @@ function capScriptBudgetMs(rawTimeout, hasMedia) {
35466
35639
  ));
35467
35640
  return EVAL_SCRIPT_BUDGET_MAX_MS;
35468
35641
  }
35469
- return Math.max(1e3, parsed);
35642
+ return Math.max(EVAL_SCRIPT_BUDGET_MIN_MS, parsed);
35470
35643
  }
35471
35644
  function evalWorkBudgetMs(o) {
35472
35645
  const script = o.scriptBudgetMs ?? (o.hasCamera ? EVAL_SCRIPT_BUDGET_CAMERA_MS : EVAL_SCRIPT_BUDGET_MS);
@@ -35528,14 +35701,14 @@ function evalExecTimeoutMessage(result, budgetMs) {
35528
35701
  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
35702
  }
35530
35703
  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(
35704
+ 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' ... 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
35705
  "--step <expr>",
35533
35706
  `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
35707
  (val, prev) => [...prev, val],
35535
35708
  []
35536
35709
  ).option(
35537
35710
  "--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>).",
35711
+ '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
35712
  (val, prev) => [...prev, val],
35540
35713
  []
35541
35714
  ).option(
@@ -35546,7 +35719,7 @@ var pageEvalCommand = new Command("eval").description("Evaluate JS in a real bro
35546
35719
  `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
35720
  ).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
35721
  "--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.`
35722
+ `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
35723
  ).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
35724
  const decoy = JS_DECOY_FLAGS.find((f) => opts[f.slice(2)] !== void 0);
35552
35725
  if (decoy) {
@@ -35570,9 +35743,9 @@ var pageEvalCommand = new Command("eval").description("Evaluate JS in a real bro
35570
35743
  let expr = exprArg;
35571
35744
  if (opts.file) {
35572
35745
  try {
35573
- expr = readFileSync12(opts.file, "utf8");
35746
+ expr = readScriptFile(opts.file);
35574
35747
  } catch {
35575
- pageEvalCommand.error(`error: Cannot read file: ${opts.file}`);
35748
+ pageEvalCommand.error(opts.file === "-" ? `error: --file - reads the script from stdin, but stdin was empty/unreadable - pipe it in, e.g. gipity page eval "<url>" --file - <<'EOF' ... EOF` : `error: Cannot read file: ${opts.file}`);
35576
35749
  }
35577
35750
  }
35578
35751
  if (opts.reload !== void 0 && opts.reloadFile) {
@@ -35581,7 +35754,7 @@ var pageEvalCommand = new Command("eval").description("Evaluate JS in a real bro
35581
35754
  let reloadExpr = opts.reload;
35582
35755
  if (opts.reloadFile) {
35583
35756
  try {
35584
- reloadExpr = readFileSync12(opts.reloadFile, "utf8");
35757
+ reloadExpr = readScriptFile(opts.reloadFile);
35585
35758
  } catch {
35586
35759
  pageEvalCommand.error(`error: Cannot read file: ${opts.reloadFile}`);
35587
35760
  }
@@ -35592,6 +35765,20 @@ var pageEvalCommand = new Command("eval").description("Evaluate JS in a real bro
35592
35765
  `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
35766
  );
35594
35767
  }
35768
+ if (opts.timeout !== void 0) {
35769
+ const dur = parseDuration(opts.timeout, "ms");
35770
+ if (dur?.hadSuffix) opts.timeout = String(Math.round(dur.value));
35771
+ }
35772
+ if (opts.timeout !== void 0) {
35773
+ const t = parseInt(opts.timeout, 10);
35774
+ if (Number.isFinite(t) && t > 0 && t < EVAL_SCRIPT_BUDGET_MIN_MS) {
35775
+ const overMax = t * 1e3 > EVAL_SCRIPT_BUDGET_MAX_MS;
35776
+ const asMs = Math.min(t * 1e3, EVAL_SCRIPT_BUDGET_MAX_MS);
35777
+ throw new Error(
35778
+ `--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)` : ""}.`
35779
+ );
35780
+ }
35781
+ }
35595
35782
  let waitMs = capWaitMs(opts.wait, url);
35596
35783
  const waitForTimeoutMs = capWaitForTimeoutMs(opts.waitTimeout);
35597
35784
  const hasMedia = !!opts.camera || !!opts.fakeMedia;
@@ -35620,12 +35807,12 @@ var pageEvalCommand = new Command("eval").description("Evaluate JS in a real bro
35620
35807
  projectGuid = config.projectGuid;
35621
35808
  }
35622
35809
  if (opts.camera) {
35623
- console.log(muted(`Hosting camera feed ${opts.camera}\u2026`));
35810
+ console.log(muted(`Hosting camera feed ${opts.camera}...`));
35624
35811
  camera = await uploadCameraFeed(projectGuid, opts.camera);
35625
35812
  }
35626
35813
  if (fixturePaths.length) {
35627
35814
  for (const p of fixturePaths) {
35628
- console.log(muted(`Hosting fixture ${p}\u2026`));
35815
+ console.log(muted(`Hosting fixture ${p}...`));
35629
35816
  hosted.push(await uploadPublicFixture(projectGuid, p));
35630
35817
  }
35631
35818
  const map = {};
@@ -35706,7 +35893,7 @@ ${hint}`);
35706
35893
  }
35707
35894
  if (camera) console.log(`${muted("Camera:")} ${camera.name} ${muted("(played as the webcam feed; getUserMedia resolves)")}`);
35708
35895
  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)}`);
35896
+ console.error(opts.file ? `${muted("Script:")} ${opts.file === "-" ? "(stdin)" : opts.file}` : `${muted("Expression:")} ${summarizeExpr(expr)}`);
35710
35897
  console.log(`
35711
35898
  ${result.trim() ? result : muted("(empty result)")}`);
35712
35899
  if (noValue) console.log(muted(`
@@ -35739,7 +35926,7 @@ ${bold("After reload")} ${muted("(page reloaded in place \u2014 storage preserve
35739
35926
  }
35740
35927
  }));
35741
35928
  for (const f of JS_DECOY_FLAGS) pageEvalCommand.addOption(new Option(`${f} <value>`).hideHelp());
35742
- pageEvalCommand.addHelpText("after", `
35929
+ pageEvalCommand.addHelpText("after", (context) => context.error ? "" : `
35743
35930
  Examples:
35744
35931
  gipity page eval "https://dev.gipity.ai/me/app/" "document.title"
35745
35932
  # Functionally test a page's own code paths: save a script that drives the UI
@@ -35775,6 +35962,16 @@ Examples:
35775
35962
  "window.__vision.gesture()" \\
35776
35963
  --step "document.getElementById('see').textContent" \\
35777
35964
  --step "({ score: score.textContent, verdict: verdict.textContent })"
35965
+ # File-UPLOAD vision app (web-vision-detect, no camera)? --fixture is the analog of
35966
+ # --camera: it hosts your test photo at a CORS-permissive URL, so you feed the kit's
35967
+ # detector a known image and read back the labels - no deploying a throwaway asset into
35968
+ # the app tree (and no cleanup redeploy). crossOrigin='anonymous' keeps the canvas clean.
35969
+ gipity generate image "street scene: three people, two cars, one dog" -o street.jpg
35970
+ gipity page eval "https://dev.gipity.ai/me/app/" --fixture street.jpg \\
35971
+ "const { createDetector } = await import('./packages/web-vision-detect/index.js'); \\
35972
+ const det = await createDetector({ model: 'nano' }); \\
35973
+ const img = new Image(); img.crossOrigin = 'anonymous'; img.src = fixtureUrl; await img.decode(); \\
35974
+ const { detections } = await det.detect(img); return detections.map(d => d.label);"
35778
35975
 
35779
35976
  Module resolution: dynamic import() specifiers starting with ./ or ../ resolve
35780
35977
  against the PAGE URL, so import('./packages/i18n/index.js') loads the app's own
@@ -35942,14 +36139,25 @@ var WAIT_FOR_MAX_MS2 = 15e3;
35942
36139
  var WAIT_FOR_DEFAULT_MS = 15e3;
35943
36140
  var MAX_POST_LOAD_DELAY_MS = 3e4;
35944
36141
  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 () => {
36142
+ 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 - 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('./...') 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' ... EOF) with no tmp file. For a multi-step driver - click through a flow, wait for it to render - 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
36143
  const aliasFlag = ACTION_ALIAS_FLAGS.find((f) => opts[f.slice(2)] !== void 0);
35947
36144
  if (aliasFlag) {
35948
36145
  console.error(muted(
35949
36146
  `Note: treating ${aliasFlag} as --action \u2014 it runs your JS in the page before the capture. --action is the canonical flag.`
35950
36147
  ));
35951
36148
  }
35952
- const actionScript = [opts.action, ...ACTION_ALIAS_FLAGS.map((f) => opts[f.slice(2)])].filter(Boolean).join("\n");
36149
+ const inlineAction = [opts.action, ...ACTION_ALIAS_FLAGS.map((f) => opts[f.slice(2)])].filter(Boolean).join("\n");
36150
+ if (opts.file && inlineAction) {
36151
+ throw new Error("Pass either --file <path> or an inline --action script, not both");
36152
+ }
36153
+ let actionScript = inlineAction;
36154
+ if (opts.file) {
36155
+ try {
36156
+ actionScript = readScriptFile(opts.file);
36157
+ } catch {
36158
+ throw new Error(opts.file === "-" ? `--file - reads the pre-capture script from stdin, but stdin was empty/unreadable - pipe it in, e.g. gipity page screenshot "<url>" --file - <<'EOF' ... EOF` : `Cannot read file: ${opts.file}`);
36159
+ }
36160
+ }
35953
36161
  const delayRaw = opts.wait ?? opts.postLoadDelay;
35954
36162
  const chosenDelay = delayRaw !== void 0;
35955
36163
  let postLoadDelayMs = chosenDelay ? parseInt(String(delayRaw), 10) : 1e3;
@@ -36015,7 +36223,7 @@ var pageScreenshotCommand = new Command("screenshot").description("Screenshot a
36015
36223
  if (opts.camera) {
36016
36224
  assertCameraFile(opts.camera);
36017
36225
  if (!projectGuid) throw new Error("--camera needs a linked project (the frame is hosted for the browser to fetch) \u2014 run `gipity link` first.");
36018
- console.log(muted(`Hosting camera feed ${opts.camera}\u2026`));
36226
+ console.log(muted(`Hosting camera feed ${opts.camera}...`));
36019
36227
  camera = await uploadCameraFeed(projectGuid, opts.camera);
36020
36228
  }
36021
36229
  const preCapture = [
@@ -36040,7 +36248,7 @@ var pageScreenshotCommand = new Command("screenshot").description("Screenshot a
36040
36248
  let entries;
36041
36249
  try {
36042
36250
  try {
36043
- entries = opts.json ? await doShoot() : await withSpinner("Capturing\u2026", doShoot, { done: null });
36251
+ entries = opts.json ? await doShoot() : await withSpinner("Capturing...", doShoot, { done: null });
36044
36252
  } catch (err) {
36045
36253
  throw preCapture ? new Error(augmentSandboxTimeout(err.message)) : err;
36046
36254
  }
@@ -36162,9 +36370,17 @@ Waiting for the page to reach a state before the shot?
36162
36370
 
36163
36371
  Capturing a state that needs an interaction (start a game, open a menu, dismiss a modal)?
36164
36372
  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.
36373
+ (and after any --wait-for gate), then settles again so the result has painted. For
36374
+ a multi-step driver (click, wait, click) pipe it as a heredoc with --file - (same
36375
+ as 'page eval') instead of cramming it into one inline string:
36376
+ gipity page screenshot "https://dev.gipity.ai/me/app/" --file - <<'EOF'
36377
+ document.getElementById('load-sample').click();
36378
+ await new Promise(r => setTimeout(r, 500));
36379
+ document.getElementById('run').click();
36380
+ await new Promise(r => setTimeout(r, 2000));
36381
+ EOF
36382
+ Do NOT hand-roll a 'page eval' that returns a base64 image: the eval result is
36383
+ capped (~16KB) and truncates the PNG.
36168
36384
 
36169
36385
  Capturing an off-screen region or reading element data?
36170
36386
  \u2022 --full captures the ENTIRE scrollable page (then crop to the region).
@@ -36184,10 +36400,10 @@ function shortUrl(url, truncate = true, maxLen = 100) {
36184
36400
  result = url;
36185
36401
  }
36186
36402
  if (!truncate || result.length <= maxLen) return result;
36187
- const keep = maxLen - 1;
36403
+ const keep = maxLen - 3;
36188
36404
  const headLen = Math.ceil(keep / 2);
36189
36405
  const tailLen = Math.floor(keep / 2);
36190
- return result.slice(0, headLen) + "\u2026" + result.slice(-tailLen);
36406
+ return result.slice(0, headLen) + "..." + result.slice(-tailLen);
36191
36407
  }
36192
36408
  var pageInspectCommand = new Command("inspect").description("Inspect a web page (console, failed resources, timing, layout overflow). ONE passive load - to verify realtime/presence across concurrent clients use `page test --observe`").argument("<url>", "URL to inspect").option("--wait <ms>", "Sleep this many ms after DOMContentLoaded before capturing (lets late async/LCP work settle; max 30000)", "500").option("--wait-for <selector>", "Wait until this CSS selector appears before capturing (deterministic; replaces --wait)").option("--wait-timeout <ms>", "Max ms to wait for --wait-for before giving up", "5000").option("--json", "Output as JSON").option("--no-reprobe", "Skip the automatic second probe that filters one-time transient console errors - roughly halves inspect time on a page with any console error, at the cost of possibly reporting a cold-load transient as real").option("--no-truncate", "Show full URLs instead of truncating long ones with middle-ellipsis").option("--all", "Include render-blocking, large resources, oversized images, overflow culprits, and LCP detail").option("--no-fake-media", "Inspect with NO camera or microphone present, so a getUserMedia app takes its no-device error path (use this only when that fallback IS what you're inspecting)").option("--device <name>", `Inspect as a real touch device: ${TOUCH_DEVICES.join(", ")}. Emulates touch events, mobile user-agent and DPR, so a touch-gated mobile layout is what gets inspected.`).option("--auth", "Load the page signed in as you (your Gipity account), so pages behind a Sign-in-with-Gipity login are reachable. Only works for apps using Sign in with Gipity, hosted on *.gipity.ai. Without this flag the page loads as a genuinely anonymous, signed-out visitor \u2014 nothing carries over from earlier --auth runs.").addOption(new Option("--screenshot [path]", "Capture a screenshot").hideHelp()).action((url, opts) => {
36193
36409
  if (opts.screenshot !== void 0) {
@@ -36213,7 +36429,17 @@ async function inspectPage(url, opts = {}) {
36213
36429
  auth: opts.auth || void 0
36214
36430
  };
36215
36431
  const res = await post(`/tools/browser/inspect`, inspectBody);
36216
- const b7 = res.data;
36432
+ const b7 = res?.data;
36433
+ if (!b7 || typeof b7 !== "object") {
36434
+ if (opts.json) {
36435
+ console.log(JSON.stringify({ url, captured: false, note: "inspector returned no page data" }));
36436
+ return;
36437
+ }
36438
+ console.log(`${brand("Inspecting")} ${bold(url)}`);
36439
+ console.log(warning("Captured no page data \u2014 the page returned an empty response."));
36440
+ 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."));
36441
+ return;
36442
+ }
36217
36443
  const isPlatformLog = (entry) => {
36218
36444
  const urlPart = entry.replace(/\s*\([^)]*\)\s*$/, "");
36219
36445
  try {
@@ -36269,6 +36495,9 @@ async function inspectPage(url, opts = {}) {
36269
36495
  console.log(`${muted("Title:")} ${b7.title || "(none)"}`);
36270
36496
  console.log(`${muted("Elements:")} ${b7.elementCount || 0}`);
36271
36497
  console.log(`${muted("Page weight:")} ${info(formatSize(b7.totalBytes || 0))}`);
36498
+ if ((b7.elementCount || 0) === 0 && !b7.title) {
36499
+ 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).")}`);
36500
+ }
36272
36501
  console.log(`
36273
36502
  ${bold("Timing:")}`);
36274
36503
  console.log(`${muted("TTFB:")} ${timing.ttfb}ms`);
@@ -36388,7 +36617,7 @@ var deployCommand = new Command("deploy").description("Deploy to dev or prod").a
36388
36617
  force: opts.force,
36389
36618
  only: opts.only?.split(",").map((s) => s.trim())
36390
36619
  });
36391
- const res = opts.json ? await doDeploy() : await withSpinner(`Deploying to ${target}\u2026`, doDeploy, { done: null });
36620
+ const res = opts.json ? await doDeploy() : await withSpinner(`Deploying to ${target}...`, doDeploy, { done: null });
36392
36621
  const d = res.data;
36393
36622
  const inspectAfter = async () => {
36394
36623
  if (!opts.inspect) return;
@@ -36455,7 +36684,7 @@ var deployCommand = new Command("deploy").description("Deploy to dev or prod").a
36455
36684
  console.log(success(`\u2713 Deployed to ${target}`) + muted(` (${d.elapsedMs}ms)`));
36456
36685
  if (d.url) console.log(`${muted("Live:")} ${brand(d.url)}`);
36457
36686
  if (d.phases?.some((p) => p.name === "functions")) {
36458
- console.log(`${muted("Functions:")} POST ${brand(`${config.apiBase}/api/${config.projectGuid}/fn/<name>`)}`);
36687
+ console.log(`${muted("Functions:")} POST ${brand(`${resolveApiBase()}/api/${config.projectGuid}/fn/<name>`)}`);
36459
36688
  console.log(muted(" (same address on dev and prod; names: gipity fn list; verify the public path: gipity fn call <name> --anon)"));
36460
36689
  }
36461
36690
  await inspectAfter();
@@ -36640,8 +36869,8 @@ memoryCommand.command("delete <topic>").description("Delete a topic").option("--
36640
36869
  // src/commands/sandbox.ts
36641
36870
  init_api();
36642
36871
  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";
36872
+ import { readFileSync as readFileSync15, existsSync as existsSync11, statSync as statSync7, mkdirSync as mkdirSync8, writeFileSync as writeFileSync9 } from "fs";
36873
+ import { dirname as dirname8, extname as extname4, relative as relative4, resolve as resolve11, sep as sep3 } from "path";
36645
36874
  init_colors();
36646
36875
  var LANG_MAP = {
36647
36876
  js: "javascript",
@@ -36750,6 +36979,32 @@ function explainSplitArgs(args) {
36750
36979
  );
36751
36980
  return lines.join("\n");
36752
36981
  }
36982
+ var SCRATCH_DIR_PATTERNS = SCRATCH_IGNORE.map((p) => p.replace(/\/$/, "")).filter(Boolean).map((p) => p.replace(/[.]/g, "\\.").replace(/\*/g, "[A-Za-z0-9._-]*"));
36983
+ function scratchRefsInCode(code) {
36984
+ const dirs = SCRATCH_DIR_PATTERNS.join("|");
36985
+ if (!dirs) return [];
36986
+ const re2 = new RegExp(`(?:\\/work\\/|(?<![\\w/.]))(?:${dirs})\\/[A-Za-z0-9._\\-\\/]+`, "g");
36987
+ return [...new Set((code.match(re2) ?? []).map((m) => m.replace(/^\/work\//, "")))];
36988
+ }
36989
+ function isWriteTarget(code, p) {
36990
+ const path5 = p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
36991
+ const m = new RegExp(`(?:\\/work\\/)?${path5}(?![\\w./-])`).exec(code);
36992
+ if (!m) return false;
36993
+ const idx = m.index;
36994
+ const before = code.slice(Math.max(0, idx - 40), idx);
36995
+ if (/(?:^|[\s'"`;(])(?:-o|-of|--out(?:put)?[= ]|>>?|tee(?:\s+-a)?)\s*['"`]?(?:\/work\/)?$/.test(before)) return true;
36996
+ const segStart = Math.max(
36997
+ code.lastIndexOf("&&", idx),
36998
+ code.lastIndexOf("||", idx),
36999
+ code.lastIndexOf(";", idx),
37000
+ code.lastIndexOf("|", idx),
37001
+ code.lastIndexOf("\n", idx)
37002
+ );
37003
+ const seg = code.slice(segStart + 1, idx).replace(/^[\s&|;]+/, "");
37004
+ const cmd = /^\s*([A-Za-z0-9_./-]+)/.exec(seg)?.[1]?.split("/").pop()?.toLowerCase();
37005
+ const afterInputFlag = /(?:^|[\s'"`])(?:-i|--input)[= ]\s*['"`]?$/.test(before);
37006
+ return cmd === "ffmpeg" && !afterInputFlag;
37007
+ }
36753
37008
  function resolveRelativeCwd() {
36754
37009
  const configPath = getConfigPath();
36755
37010
  if (!configPath) return void 0;
@@ -36759,7 +37014,7 @@ function resolveRelativeCwd() {
36759
37014
  return rel2.split(/[\\/]/).filter(Boolean).join("/");
36760
37015
  }
36761
37016
  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(
37017
+ 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
37018
  "--input <path>",
36764
37019
  "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
37020
  (v7, prev) => [...prev ?? [], v7]
@@ -36803,6 +37058,8 @@ Examples:
36803
37058
  $ gipity sandbox run --file build_report.py
36804
37059
  $ gipity sandbox run python build_report.py # same thing, interpreter shorthand
36805
37060
  $ gipity sandbox run bash "echo hi; ffmpeg -version" # inline, language pinned
37061
+ $ gipity sandbox run bash -c "ffmpeg -version" # the bash -c idiom also works
37062
+ $ gipity sandbox run node -e "console.log(process.version)" # and node -e
36806
37063
 
36807
37064
  # Surgical: only these files are mirrored in
36808
37065
  $ gipity sandbox run --language bash \\
@@ -36820,17 +37077,20 @@ GCC/Rust).
36820
37077
  let inlineCode;
36821
37078
  let filePath = opts.file;
36822
37079
  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"));
37080
+ const flagCode = opts.code ?? opts.cmd ?? opts.eval;
37081
+ if (flagCode !== void 0) {
37082
+ const leadIsInterp = args.length === 1 && INTERPRETERS[args[0].toLowerCase()] !== void 0;
37083
+ if (args.length > (leadIsInterp ? 1 : 0)) {
37084
+ console.error(error("Pass the code once: either positionally or via --code/--cmd/-c/--eval/-e, not both"));
36826
37085
  process.exit(1);
36827
37086
  }
36828
- args = [opts.code];
37087
+ const interp = leadIsInterp ? args[0] : opts.cmd !== void 0 ? "bash" : opts.eval !== void 0 ? "node" : void 0;
37088
+ args = interp ? [interp, flagCode] : [flagCode];
36829
37089
  }
36830
37090
  if (args.length >= 2 && INTERPRETERS[args[0].toLowerCase()] !== void 0) {
36831
37091
  langFromInterp = INTERPRETERS[args[0].toLowerCase()];
36832
37092
  const rest = args.slice(1).join(" ");
36833
- if (existsSync11(rest) && statSync6(rest).isFile()) filePath = rest;
37093
+ if (existsSync11(rest) && statSync7(rest).isFile()) filePath = rest;
36834
37094
  else inlineCode = rest;
36835
37095
  } else if (args.length === 1) {
36836
37096
  inlineCode = args[0];
@@ -36839,7 +37099,9 @@ GCC/Rust).
36839
37099
  process.exit(1);
36840
37100
  }
36841
37101
  if (inlineCode !== void 0 && filePath) {
36842
- console.error(error("Pass either an inline <code> arg or --file <path>, not both"));
37102
+ console.error(error("Pass the code ONE way, not both: an inline <code> arg OR --file <path>."));
37103
+ console.error(dim(" inline: gipity sandbox run bash 'echo hi'"));
37104
+ console.error(dim(" file: gipity sandbox run --file script.sh"));
36843
37105
  process.exit(1);
36844
37106
  }
36845
37107
  if (inlineCode === void 0 && !filePath) {
@@ -36849,7 +37111,7 @@ GCC/Rust).
36849
37111
  let source = inlineCode;
36850
37112
  if (filePath) {
36851
37113
  try {
36852
- source = readFileSync13(filePath, "utf8");
37114
+ source = readFileSync15(filePath, "utf8");
36853
37115
  } catch {
36854
37116
  console.error(error(`Cannot read file: ${filePath}`));
36855
37117
  process.exit(1);
@@ -36862,7 +37124,8 @@ GCC/Rust).
36862
37124
  process.exit(1);
36863
37125
  }
36864
37126
  const { config } = await resolveProjectContext();
36865
- const timeout = parseInt(opts.timeout, 10);
37127
+ const durOpt = parseDuration(opts.timeout, "s");
37128
+ const timeout = durOpt ? Math.max(1, Math.round(durOpt.value)) : parseInt(opts.timeout, 10);
36866
37129
  const cwd = resolveRelativeCwd();
36867
37130
  const scratchInputs = (opts.input ?? []).filter(
36868
37131
  (p) => shouldIgnore(p.replace(/\\/g, "/").replace(/^\.\//, ""), SCRATCH_IGNORE)
@@ -36873,6 +37136,18 @@ GCC/Rust).
36873
37136
  console.error(dim(" Stage inputs at a real project path (src/, docs/, assets/) and delete them afterward."));
36874
37137
  process.exit(1);
36875
37138
  }
37139
+ if (source) {
37140
+ const scratchReads = scratchRefsInCode(source).filter(
37141
+ (p) => shouldIgnore(p, SCRATCH_IGNORE) && existsSync11(resolve11(process.cwd(), p)) && !isWriteTarget(source, p)
37142
+ );
37143
+ if (scratchReads.length) {
37144
+ console.error(error(`Scratch files are never mirrored into the sandbox, so it can't read: ${scratchReads.join(", ")}`));
37145
+ console.error(dim(` ${SCRATCH_IGNORE.join(", ")} are ignored by sync, so the sandbox never sees them.`));
37146
+ console.error(dim(" Stage the input at a real project path (src/, docs/, assets/) and delete it afterward."));
37147
+ console.error(dim(" (Writing OUTPUT under tmp/ is fine \u2014 scratch outputs come back to your local tmp/.)"));
37148
+ process.exit(1);
37149
+ }
37150
+ }
36876
37151
  if (getConfigPath()) {
36877
37152
  await sync({ interactive: false, progress: opts.json ? void 0 : createProgressReporter() });
36878
37153
  }
@@ -36888,17 +37163,17 @@ GCC/Rust).
36888
37163
  // wire name for --discard-output.)
36889
37164
  noSyncOutput: discardOutput.length ? discardOutput : void 0
36890
37165
  });
36891
- const res = opts.json ? await doRun() : await withSpinner("Running in sandbox\u2026", doRun, { done: null });
37166
+ const res = opts.json ? await doRun() : await withSpinner("Running in sandbox...", doRun, { done: null });
36892
37167
  const pulledLocal = !!(res.data.outputFiles?.length && getConfigPath());
36893
37168
  if (pulledLocal) {
36894
37169
  await sync({ interactive: false, progress: opts.json ? void 0 : createProgressReporter() });
36895
37170
  }
36896
37171
  const scratchWritten = [];
36897
37172
  if (res.data.scratchFiles?.length) {
36898
- const base = resolve12(getProjectRoot() ?? process.cwd());
37173
+ const base = resolve11(getProjectRoot() ?? process.cwd());
36899
37174
  for (const f of res.data.scratchFiles) {
36900
37175
  const rel2 = f.path.replace(/\\/g, "/");
36901
- const abs = resolve12(base, rel2);
37176
+ const abs = resolve11(base, rel2);
36902
37177
  if (abs !== base && !abs.startsWith(base + sep3)) continue;
36903
37178
  mkdirSync8(dirname8(abs), { recursive: true });
36904
37179
  writeFileSync9(abs, Buffer.from(f.contentBase64, "base64"));
@@ -36924,7 +37199,7 @@ GCC/Rust).
36924
37199
  }
36925
37200
  if (res.data.outputFiles && res.data.outputFiles.length > 0) {
36926
37201
  const projectRoot = getProjectRoot();
36927
- const landed = (f) => pulledLocal && !!projectRoot && existsSync11(resolve12(projectRoot, f));
37202
+ const landed = (f) => pulledLocal && !!projectRoot && existsSync11(resolve11(projectRoot, f));
36928
37203
  const onDisk = res.data.outputFiles.filter(landed);
36929
37204
  const notOnDisk = res.data.outputFiles.filter((f) => !landed(f));
36930
37205
  if (onDisk.length > 0) {
@@ -36936,6 +37211,12 @@ GCC/Rust).
36936
37211
  for (const f of notOnDisk) console.log(`${f}`);
36937
37212
  if (pulledLocal) console.log(dim("Not pulled locally - run 'gipity sync' to fetch them."));
36938
37213
  }
37214
+ const rootStrays = [...onDisk, ...notOnDisk].filter((f) => !f.includes("/"));
37215
+ if (rootStrays.length > 0) {
37216
+ console.log(dim(`
37217
+ Note: ${rootStrays.join(", ")} landed at the project root and will deploy with the files phase.`));
37218
+ 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."));
37219
+ }
36939
37220
  }
36940
37221
  if (scratchWritten.length > 0) {
36941
37222
  console.log("\nScratch outputs (local only - never synced or deployed):");
@@ -36945,7 +37226,27 @@ GCC/Rust).
36945
37226
  console.log(dim("\nDiscarded (--discard-output):"));
36946
37227
  for (const f of res.data.skippedOutputFiles) console.log(dim(`${f}`));
36947
37228
  }
37229
+ if (res.data.exitCode === 0) {
37230
+ const emptyScratch = (res.data.scratchFiles ?? []).filter((f) => f.contentBase64.length === 0).map((f) => f.path.replace(/\\/g, "/"));
37231
+ const root = getProjectRoot();
37232
+ const emptyOutputs = pulledLocal && root ? (res.data.outputFiles ?? []).filter((f) => {
37233
+ try {
37234
+ return statSync7(resolve11(root, f)).size === 0;
37235
+ } catch {
37236
+ return false;
37237
+ }
37238
+ }) : [];
37239
+ const empties = [.../* @__PURE__ */ new Set([...emptyScratch, ...emptyOutputs])];
37240
+ if (empties.length > 0) {
37241
+ 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.`));
37242
+ }
37243
+ }
36948
37244
  if (res.data.exitCode !== 0) {
37245
+ if (res.data.exitCode === 137) {
37246
+ console.error(error("The sandbox process was killed (exit 137) - it ran out of memory."));
37247
+ console.error(dim(" Shrink the job: for ffmpeg, downscale (-vf scale=640:-2) and add -preset ultrafast;"));
37248
+ console.error(dim(" for ImageMagick, lower -density/-resize. Process in smaller chunks if it still OOMs."));
37249
+ }
36949
37250
  process.exit(res.data.exitCode);
36950
37251
  }
36951
37252
  }
@@ -36962,7 +37263,7 @@ var chatCommand = new Command("chat").description("Send a message to your agent"
36962
37263
  const endpoint = useExisting ? `/conversations/${config.conversationGuid}/messages` : "/conversations";
36963
37264
  const body = useExisting ? { content: message, projectGuid: config.projectGuid } : { agentGuid: config.agentGuid, content: message, projectGuid: config.projectGuid };
36964
37265
  const doChat = () => post(endpoint, body);
36965
- const res = opts.json ? await doChat() : await withSpinner("Thinking\u2026", doChat, { done: null });
37266
+ const res = opts.json ? await doChat() : await withSpinner("Thinking...", doChat, { done: null });
36966
37267
  if (!oneOff && res.data.conversationGuid !== config.conversationGuid) {
36967
37268
  saveConfig({ ...config, conversationGuid: res.data.conversationGuid });
36968
37269
  }
@@ -37361,7 +37662,7 @@ function printStepRuns(steps, emptyNote) {
37361
37662
  }
37362
37663
  }
37363
37664
  var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
37364
- var sleep2 = (ms2) => new Promise((resolve20) => setTimeout(resolve20, ms2));
37665
+ var sleep2 = (ms2) => new Promise((resolve19) => setTimeout(resolve19, ms2));
37365
37666
  async function waitForRun(wfGuid, prevGuid, timeoutSec) {
37366
37667
  const deadline = Date.now() + timeoutSec * 1e3;
37367
37668
  let runGuid;
@@ -37448,7 +37749,7 @@ workflowCommand.command("run <name>").description("Trigger a workflow (add --wai
37448
37749
  }
37449
37750
  if (r.status !== "completed") process.exit(1);
37450
37751
  }));
37451
- workflowCommand.command("runs <name> [runGuid]").description("List recent runs, or pass a run guid (wr_\u2026) to see that run's per-step outputs").option("--json", "Output as JSON").action((name, runGuid, _opts, cmd) => run("Runs", async () => {
37752
+ workflowCommand.command("runs <name> [runGuid]").description("List recent runs, or pass a run guid (wr_...) to see that run's per-step outputs").option("--json", "Output as JSON").action((name, runGuid, _opts, cmd) => run("Runs", async () => {
37452
37753
  const opts = mergedOpts(cmd);
37453
37754
  const wf2 = await resolveWorkflow(name);
37454
37755
  if (runGuid) {
@@ -37821,6 +38122,19 @@ fileCommand.command("cat <path>").description("Read a file").option("--json", "O
37821
38122
  process.stdout.write(res.data.content);
37822
38123
  }
37823
38124
  }));
38125
+ 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 () => {
38126
+ const { config } = await resolveProjectContext();
38127
+ const qs = new URLSearchParams({ path: path5 });
38128
+ if (opts.download) qs.set("download", "1");
38129
+ const res = await get(
38130
+ `/projects/${config.projectGuid}/files/url?${qs.toString()}`
38131
+ );
38132
+ if (opts.json) {
38133
+ console.log(JSON.stringify(res.data));
38134
+ } else {
38135
+ console.log(res.data.url);
38136
+ }
38137
+ }));
37824
38138
  fileCommand.command("tree [path]").description("Show the file tree").option("--json", "Output as JSON").action((path5, opts) => run("Tree", async () => {
37825
38139
  const { config } = await resolveProjectContext();
37826
38140
  const query = path5 ? `?path=${encodeURIComponent(path5)}` : "";
@@ -37921,7 +38235,7 @@ function printUsage(d) {
37921
38235
  for (const p of d.projects) {
37922
38236
  const name = p.projectName ?? "(no project)";
37923
38237
  row(
37924
- name.length > 16 ? `${name.slice(0, 15)}\u2026` : name,
38238
+ name.length > 16 ? `${name.slice(0, 13)}...` : name,
37925
38239
  formatBytes(p.liveBytes),
37926
38240
  `${p.liveFiles.toLocaleString()} files`
37927
38241
  );
@@ -37930,7 +38244,7 @@ function printUsage(d) {
37930
38244
  const shownBytes = d.projects.reduce((n, p) => n + p.liveBytes, 0);
37931
38245
  const shownFiles = d.projects.reduce((n, p) => n + p.liveFiles, 0);
37932
38246
  row(
37933
- `\u2026and ${hidden.toLocaleString()} more`,
38247
+ `...and ${hidden.toLocaleString()} more`,
37934
38248
  formatBytes(totals.liveBytes - shownBytes),
37935
38249
  `${(totals.liveFiles - shownFiles).toLocaleString()} files`
37936
38250
  );
@@ -37982,50 +38296,11 @@ storageCommand.command("retention").description("View or adjust your file versio
37982
38296
  init_platform();
37983
38297
  init_auth();
37984
38298
  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";
38299
+ import { join as join17, dirname as dirname10, resolve as resolve13, basename as basename5, sep as sep5 } from "path";
38300
+ import { existsSync as existsSync14, mkdirSync as mkdirSync12, readFileSync as readFileSync18, writeFileSync as writeFileSync12, renameSync as renameSync2, readdirSync as readdirSync8, statSync as statSync8 } from "fs";
37987
38301
  import { homedir as homedir12 } from "os";
37988
38302
  import { fileURLToPath as fileURLToPath2 } from "url";
37989
38303
  import { randomUUID } from "crypto";
37990
-
37991
- // src/login-flow.ts
37992
- init_api();
37993
- init_auth();
37994
- init_utils();
37995
- init_colors();
37996
- async function interactiveLogin() {
37997
- const email = await prompt(" Email: ");
37998
- if (!email) {
37999
- console.error(`
38000
- ${error("Email required.")}`);
38001
- process.exit(1);
38002
- }
38003
- await publicPost("/auth/login", { email });
38004
- console.log(" Check your email for a 6-digit code.\n");
38005
- const code = await prompt(" Code: ");
38006
- if (!code) {
38007
- console.error(`
38008
- ${error("Code required.")}`);
38009
- process.exit(1);
38010
- }
38011
- const res = await publicPost("/auth/verify", { email, code });
38012
- const exp = decodeJwtExp(res.accessToken);
38013
- if (!exp) {
38014
- console.error(`
38015
- ${error("Invalid token received.")}`);
38016
- process.exit(1);
38017
- }
38018
- const expiresAt = new Date(exp * 1e3).toISOString();
38019
- saveAuth({ accessToken: res.accessToken, refreshToken: res.refreshToken, email, expiresAt });
38020
- console.log(` ${success(`Logged in (${email}).`)}`);
38021
- const delivered = await flushBugQueue().catch(() => 0);
38022
- if (delivered > 0) {
38023
- console.log(` ${muted(`Delivered ${delivered} queued bug report${delivered === 1 ? "" : "s"}.`)}`);
38024
- }
38025
- return getAuth();
38026
- }
38027
-
38028
- // src/commands/build.ts
38029
38304
  init_config();
38030
38305
 
38031
38306
  // src/prompts.ts
@@ -38131,7 +38406,6 @@ function buildFreshWrap(contextBlock, userMsg) {
38131
38406
  // src/relay/onboarding.ts
38132
38407
  init_utils();
38133
38408
  init_colors();
38134
- import { readFileSync as readFileSync15 } from "node:fs";
38135
38409
 
38136
38410
  // src/relay/installers.ts
38137
38411
  import { homedir as homedir8, platform as osPlatform } from "os";
@@ -38302,13 +38576,13 @@ function windowsTaskPlan(cliPath) {
38302
38576
  init_platform();
38303
38577
  init_api();
38304
38578
  import { hostname as hostname3, userInfo, platform as osPlatform2 } from "os";
38305
- import { resolve as resolve13, dirname as dirname9 } from "path";
38579
+ import { resolve as resolve12, dirname as dirname9 } from "path";
38306
38580
  import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
38307
38581
 
38308
38582
  // src/relay/machine-id.ts
38309
38583
  import { createHash as createHash3 } from "crypto";
38310
38584
  import { execFileSync } from "child_process";
38311
- import { readFileSync as readFileSync14 } from "fs";
38585
+ import { readFileSync as readFileSync16 } from "fs";
38312
38586
  import { hostname as hostname2, platform, arch } from "os";
38313
38587
  function rawMachineId() {
38314
38588
  const override = process.env.GIPITY_RELAY_MACHINE_ID?.trim();
@@ -38317,7 +38591,7 @@ function rawMachineId() {
38317
38591
  if (platform() === "linux") {
38318
38592
  for (const path5 of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
38319
38593
  try {
38320
- const id2 = readFileSync14(path5, "utf-8").trim();
38594
+ const id2 = readFileSync16(path5, "utf-8").trim();
38321
38595
  if (id2) return `linux:${id2}`;
38322
38596
  } catch {
38323
38597
  }
@@ -38364,7 +38638,7 @@ function friendlyDeviceName() {
38364
38638
  return host || `My ${kind}`;
38365
38639
  }
38366
38640
  function resolveCliPath() {
38367
- return resolve13(process.argv[1] ?? "gipity");
38641
+ return resolve12(process.argv[1] ?? "gipity");
38368
38642
  }
38369
38643
  async function pairDevice(opts = {}) {
38370
38644
  const existing = getDevice();
@@ -38437,15 +38711,6 @@ function removeAutostart(opts = {}) {
38437
38711
 
38438
38712
  // src/relay/onboarding.ts
38439
38713
  var ensureDaemonRunning = startDaemon;
38440
- function isWsl() {
38441
- if (process.platform !== "linux") return false;
38442
- if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) return true;
38443
- try {
38444
- return /microsoft/i.test(readFileSync15("/proc/version", "utf-8"));
38445
- } catch {
38446
- return false;
38447
- }
38448
- }
38449
38714
  async function runRelaySetup(opts) {
38450
38715
  const alreadyAnswered = getRelayEnabled() !== void 0;
38451
38716
  if (opts.mode === "offer-once" && alreadyAnswered) {
@@ -38461,12 +38726,12 @@ async function runRelaySetup(opts) {
38461
38726
  console.log("");
38462
38727
  console.log(` ${success("Registered as")} ${bold(existingDevice.name)} ${muted(`(${existingDevice.guid})`)}`);
38463
38728
  if (isPaused()) {
38464
- console.log(` ${dim("The relay is paused \u2014 resume it with")} ${brand("gipity relay resume")}${dim(".")}`);
38729
+ console.log(` ${dim("The relay is paused - resume it with")} ${brand("gipity relay resume")}${dim(".")}`);
38465
38730
  } else {
38466
38731
  console.log(` ${dim("The relay is running. Open")} ${brand("gipity.ai")}${dim(" and type `/claude` to drive Claude Code here.")}`);
38467
38732
  }
38468
38733
  console.log("");
38469
- console.log(` ${dim("To register this computer again \u2014 for example under a different name \u2014")}`);
38734
+ console.log(` ${dim("To register this computer again (for example under a different name),")}`);
38470
38735
  console.log(` ${dim("unregister it first, then re-run setup:")}`);
38471
38736
  console.log(` ${brand("gipity relay revoke")} ${dim("# unpairs this computer and removes the login service")}`);
38472
38737
  console.log(` ${brand("gipity connect")} ${dim("# register it again (asks for a new name)")}`);
@@ -38476,28 +38741,29 @@ async function runRelaySetup(opts) {
38476
38741
  }
38477
38742
  if (opts.mode === "run-now") {
38478
38743
  console.log(` ${bold("Set up this computer as a relay")}`);
38479
- console.log(` ${dim("A relay runs your coding agent (Claude Code, Codex, or Grok) here so you can drive it from the web (")}${brand("gipity.ai")}${dim(") on any browser.")}`);
38480
- console.log(` ${dim("It uses your Claude, Codex, or Grok subscription \u2014 the cheapest way to pay for tokens.")}`);
38744
+ console.log(` ${dim("Gipity drives the coding agents on this computer from the web (")}${brand("gipity.ai")}${dim(")")}`);
38745
+ console.log(` ${dim("on any browser, desktop or phone. Uses your Claude, Codex, or Grok")}`);
38746
+ console.log(` ${dim("subscription - the cheapest way to pay for tokens.")}`);
38481
38747
  } else {
38482
- console.log(` ${bold("Remote control of your coding agent")}`);
38483
- console.log(` ${dim("Drive your coding agent on this computer from the web (")}${brand("gipity.ai")}${dim(") on any browser (desktop or phone).")}`);
38484
- console.log("");
38485
- console.log(` ${dim("Enable now (takes 2 seconds) or turn on later with")} ${brand("gipity connect")}`);
38748
+ console.log(` ${bold("Optional:")} ${dim("Gipity can drive the coding agents on this computer from the")}`);
38749
+ console.log(` ${dim("web (")}${brand("gipity.ai")}${dim(") on any browser, desktop or phone. Like Claude Code remote")}`);
38750
+ console.log(` ${dim("control, but one pane for all your coding agents.")}`);
38486
38751
  }
38487
38752
  console.log("");
38488
- const promptText = opts.mode === "run-now" ? " Set up remote control on this computer?" : " Enable remote control?";
38753
+ const promptText = opts.mode === "run-now" ? " Set up remote control on this computer" : " Enable remote control";
38489
38754
  const enable = await confirm(promptText, { default: "yes" });
38490
38755
  if (!enable) {
38491
38756
  setRelayEnabled(false);
38492
- console.log(` ${muted("Skipped.")}`);
38757
+ console.log(` ${muted("Skipped. Turn on later with")} ${brand("gipity connect")}${muted(".")}`);
38493
38758
  console.log("");
38494
38759
  return false;
38495
38760
  }
38761
+ console.log("");
38496
38762
  const defaultName = friendlyDeviceName();
38497
38763
  const rawName = await prompt(` Device name [${bold(defaultName)}]: `);
38498
38764
  const name = (rawName || defaultName).trim();
38499
38765
  if (!name || name.length > 100) {
38500
- console.error(` ${error("Device name must be 1\u2013100 non-whitespace characters. Skipping.")}`);
38766
+ console.error(` ${error("Device name must be 1-100 non-whitespace characters. Skipping.")}`);
38501
38767
  setRelayEnabled(false);
38502
38768
  return false;
38503
38769
  }
@@ -38510,11 +38776,9 @@ async function runRelaySetup(opts) {
38510
38776
  console.error(` ${dim("Skipping for now - we'll offer again next time. Or turn it on with `gipity connect`.")}`);
38511
38777
  return false;
38512
38778
  }
38513
- const startNow = await confirm(" Start the relay now (and on future `gipity build` runs)?", { default: "yes" });
38514
- if (startNow) {
38515
- startDaemon();
38516
- }
38517
- const autostartOs = await confirm(" Also start at OS login (auto-start with Windows / macOS / Linux)?", { default: "yes" });
38779
+ startDaemon();
38780
+ console.log("");
38781
+ const autostartOs = await confirm(" Also start at OS login", { default: "yes" });
38518
38782
  if (autostartOs) {
38519
38783
  try {
38520
38784
  const res = installAutostart();
@@ -38526,8 +38790,6 @@ async function runRelaySetup(opts) {
38526
38790
  } else {
38527
38791
  console.log(` ${muted("Autostart install returned non-zero - you can run")} ${brand("gipity relay install")} ${muted("later.")}`);
38528
38792
  }
38529
- } else {
38530
- console.log(` ${success("Auto-start installed.")} ${dim(res.summary)}`);
38531
38793
  }
38532
38794
  } catch (err) {
38533
38795
  if (err instanceof UnsupportedPlatformError) {
@@ -38538,16 +38800,12 @@ async function runRelaySetup(opts) {
38538
38800
  }
38539
38801
  }
38540
38802
  if (getDiagnosticsConsent() === void 0) {
38541
- const diag = await confirm(
38542
- " Share anonymous diagnostics (CPU/GPU/memory/disk/versions) to improve reliability?",
38543
- { default: "yes" }
38544
- );
38803
+ console.log("");
38804
+ const diag = await confirm(" Share anonymous diagnostics info", { default: "yes" });
38545
38805
  setDiagnosticsConsent(diag);
38546
- console.log(` ${dim(diag ? "Thanks - no personal data or file paths are ever sent. Turn off anytime with `gipity relay diagnostics off`." : "Diagnostics off. Turn on anytime with `gipity relay diagnostics on`.")}`);
38547
38806
  }
38548
38807
  console.log("");
38549
- console.log(` ${success(`Registered as ${bold(device.name)} (${device.guid}).`)}`);
38550
- console.log(` ${dim("In the Gipity web CLI, type `/claude` to dispatch messages to this PC.")}`);
38808
+ console.log(` ${success(`Done! ${bold(device.name)} is set up. New chats from gipity.ai can now execute here.`)}`);
38551
38809
  console.log("");
38552
38810
  return true;
38553
38811
  }
@@ -38690,9 +38948,8 @@ function padR(s, width) {
38690
38948
  }
38691
38949
  function leadingEllipsify(s, maxW) {
38692
38950
  if (s.length <= maxW) return s;
38693
- if (maxW <= 1) return s.slice(-maxW);
38694
- if (maxW <= 3) return "\u2026".padStart(maxW);
38695
- return "\u2026" + s.slice(-(maxW - 1));
38951
+ if (maxW <= 4) return s.slice(-maxW);
38952
+ return "..." + s.slice(-(maxW - 3));
38696
38953
  }
38697
38954
  function center(s, width) {
38698
38955
  const gap = width - visLen(s);
@@ -39002,14 +39259,14 @@ function getAdapterBySource(source) {
39002
39259
  }
39003
39260
 
39004
39261
  // src/prefs.ts
39005
- import { existsSync as existsSync13, mkdirSync as mkdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync11 } from "fs";
39262
+ import { existsSync as existsSync13, mkdirSync as mkdirSync11, readFileSync as readFileSync17, writeFileSync as writeFileSync11 } from "fs";
39006
39263
  import { homedir as homedir11 } from "os";
39007
39264
  import { join as join16 } from "path";
39008
39265
  var PREFS_PATH = join16(homedir11(), ".gipity", "prefs.json");
39009
39266
  function readPrefs() {
39010
39267
  if (!existsSync13(PREFS_PATH)) return {};
39011
39268
  try {
39012
- const parsed = JSON.parse(readFileSync16(PREFS_PATH, "utf-8"));
39269
+ const parsed = JSON.parse(readFileSync17(PREFS_PATH, "utf-8"));
39013
39270
  return {
39014
39271
  lastAgent: typeof parsed.lastAgent === "string" ? parsed.lastAgent : void 0
39015
39272
  };
@@ -39034,7 +39291,7 @@ function readOwnPkg(fromDir) {
39034
39291
  const p = join17(d, "package.json");
39035
39292
  if (existsSync14(p)) {
39036
39293
  try {
39037
- const pkg2 = JSON.parse(readFileSync17(p, "utf-8"));
39294
+ const pkg2 = JSON.parse(readFileSync18(p, "utf-8"));
39038
39295
  if (pkg2.name === "gipity") return pkg2;
39039
39296
  } catch {
39040
39297
  }
@@ -39045,7 +39302,7 @@ function readOwnPkg(fromDir) {
39045
39302
  var __clPkg = readOwnPkg(__clDir);
39046
39303
  function reportSyncResult(result) {
39047
39304
  if (result.aborted) {
39048
- console.log(` ${warning("Merge cancelled \u2014 this folder was NOT synced with the project.")}`);
39305
+ console.log(` ${warning("Merge cancelled - this folder was NOT synced with the project.")}`);
39049
39306
  console.log(` ${muted("For a clean copy, quit and open the project in an empty folder. To merge anyway, run `gipity sync`.")}`);
39050
39307
  return;
39051
39308
  }
@@ -39053,9 +39310,9 @@ function reportSyncResult(result) {
39053
39310
  console.log(` Synced ${result.applied} change${result.applied > 1 ? "s" : ""} with Gipity.`);
39054
39311
  }
39055
39312
  if (result.errors.length) {
39056
- console.log(` ${warning(`Sync finished with ${result.errors.length} problem${result.errors.length === 1 ? "" : "s"} \u2014 your local copy may be incomplete:`)}`);
39313
+ console.log(` ${warning(`Sync finished with ${result.errors.length} problem${result.errors.length === 1 ? "" : "s"} - your local copy may be incomplete:`)}`);
39057
39314
  for (const e of result.errors.slice(0, 8)) console.log(` - ${e}`);
39058
- if (result.errors.length > 8) console.log(` \u2026and ${result.errors.length - 8} more.`);
39315
+ if (result.errors.length > 8) console.log(` ...and ${result.errors.length - 8} more.`);
39059
39316
  console.log(` ${muted("Nothing was deleted. Re-run `gipity sync` to finish the pull.")}`);
39060
39317
  }
39061
39318
  }
@@ -39088,7 +39345,7 @@ function localFsFallback(dir) {
39088
39345
  let isDir = false;
39089
39346
  let size = 0;
39090
39347
  try {
39091
- const st2 = statSync7(join17(d, name));
39348
+ const st2 = statSync8(join17(d, name));
39092
39349
  isDir = st2.isDirectory();
39093
39350
  size = st2.isFile() ? st2.size : 0;
39094
39351
  } catch {
@@ -39108,7 +39365,7 @@ function localFsFallback(dir) {
39108
39365
  }
39109
39366
  };
39110
39367
  walk(dir, 0);
39111
- const topLevel = topLevelEntries.length ? topLevelEntries.slice(0, 20).join(", ") + (topLevelEntries.length > 20 ? ", \u2026" : "") : "(empty directory)";
39368
+ const topLevel = topLevelEntries.length ? topLevelEntries.slice(0, 20).join(", ") + (topLevelEntries.length > 20 ? ", ..." : "") : "(empty directory)";
39112
39369
  return { fileCount, folderCount, totalBytes, topLevel };
39113
39370
  }
39114
39371
  async function buildProjectContextBlock2(opts) {
@@ -39131,7 +39388,7 @@ function hasStreamJsonFlag(args) {
39131
39388
  function markFolderTrusted(dir) {
39132
39389
  try {
39133
39390
  const file = join17(homedir12(), ".claude.json");
39134
- const cfg = existsSync14(file) ? JSON.parse(readFileSync17(file, "utf-8")) : {};
39391
+ const cfg = existsSync14(file) ? JSON.parse(readFileSync18(file, "utf-8")) : {};
39135
39392
  if (typeof cfg !== "object" || cfg === null) return;
39136
39393
  if (typeof cfg.projects !== "object" || cfg.projects === null) cfg.projects = {};
39137
39394
  const entry = typeof cfg.projects[dir] === "object" && cfg.projects[dir] !== null ? cfg.projects[dir] : {};
@@ -39256,7 +39513,7 @@ async function runLaunch(cmdName, cmdCfg, opts) {
39256
39513
  let existing = getConfig();
39257
39514
  let forceAdoptCwd = false;
39258
39515
  if (existing && !opts.newProject && !opts.project && !nonInteractive) {
39259
- const cwdHasConfig = existsSync14(resolve14(process.cwd(), ".gipity.json"));
39516
+ const cwdHasConfig = existsSync14(resolve13(process.cwd(), ".gipity.json"));
39260
39517
  if (!cwdHasConfig && isLikelyEmpty(process.cwd())) {
39261
39518
  const ancestorRoot = dirname10(getConfigPath());
39262
39519
  console.log(` ${bold("You are inside an existing Gipity project.")}
@@ -39334,6 +39591,7 @@ async function runLaunch(cmdName, cmdCfg, opts) {
39334
39591
  console.log(` ${warning("Could not sync files (will retry on next prompt):")} ${err.message}`);
39335
39592
  }
39336
39593
  setupProjectTools();
39594
+ if (!nonInteractive) console.log(` ${muted("Installed Gipity plugin, skills, and hooks.")}`);
39337
39595
  if (nonInteractive) {
39338
39596
  headlessNewProject = isNewProject;
39339
39597
  } else {
@@ -39406,7 +39664,7 @@ async function runLaunch(cmdName, cmdCfg, opts) {
39406
39664
  let agentGuid = "";
39407
39665
  const result = forceAdoptCwd ? { kind: "adopt-cwd" } : projects.length > 0 ? await pickOrCreateProject(projects, existingSlugs) : { kind: "create-new", project: await createNewProject(existingSlugs) };
39408
39666
  if (result.kind === "adopt-cwd") {
39409
- const cwdBase = basename4(process.cwd());
39667
+ const cwdBase = basename5(process.cwd());
39410
39668
  const adoptSlug = slugify(cwdBase) || "project";
39411
39669
  const adoptName = (cwdBase || adoptSlug).slice(0, 100);
39412
39670
  accountSlug = await getAccountSlug();
@@ -39458,6 +39716,7 @@ async function runLaunch(cmdName, cmdCfg, opts) {
39458
39716
  }
39459
39717
  initialPrompt = "";
39460
39718
  setupProjectTools();
39719
+ if (!nonInteractive) console.log(` ${muted("Installed Gipity plugin, skills, and hooks.")}`);
39461
39720
  console.log(` ${success(`Project "${project.name}" ready.`)}
39462
39721
  `);
39463
39722
  }
@@ -39553,9 +39812,8 @@ async function runLaunch(cmdName, cmdCfg, opts) {
39553
39812
  }
39554
39813
  if (!nonInteractive) {
39555
39814
  console.log(` ${bold("Launching Claude Code, powered by Gipity.")}`);
39556
- console.log(` ${muted("Just tell Claude what you'd like to build or do - everything Claude can do,")}`);
39557
- console.log(` ${muted("plus hosting, databases, and live deploys on Gipity.")}`);
39558
39815
  if (convGuidForHooks) {
39816
+ console.log("");
39559
39817
  console.log(` ${muted(`This session is saved to Gipity - watch it live at ${brand("prompt.gipity.ai")}. (--no-capture on init to disable.)`)}`);
39560
39818
  }
39561
39819
  console.log("");
@@ -39653,10 +39911,8 @@ async function pickAgent(lastUsed) {
39653
39911
  console.log(` ${bold("Which coding agent?")}
39654
39912
  `);
39655
39913
  AGENT_ADAPTERS.forEach((a, i) => {
39656
- const installed = binaryOnPath(a.binary) || a.key === "claude" && isClaudeInstalled();
39657
39914
  const notes = [];
39658
39915
  if (a.key === lastUsed) notes.push("last used");
39659
- if (!installed) notes.push(a.ensureInstalled ? "not installed - we'll install it" : "not installed");
39660
39916
  if (a.key === "codex" && !a.hooksSupportedOnPlatform(process.platform)) {
39661
39917
  notes.push("session recording unavailable on Windows");
39662
39918
  }
@@ -39769,9 +40025,8 @@ Sending to ${adapter.displayName}: ${message}
39769
40025
  } else {
39770
40026
  agentArgs = [...adapter.buildInteractiveArgs({ resume, model }), ...extras];
39771
40027
  console.log(` ${bold(`Launching ${adapter.displayName}, powered by Gipity.`)}`);
39772
- console.log(` ${muted(`Just tell ${adapter.displayName} what you'd like to build or do - plus hosting,`)}`);
39773
- console.log(` ${muted("databases, and live deploys on Gipity.")}`);
39774
40028
  if (convGuid) {
40029
+ console.log("");
39775
40030
  console.log(` ${muted(`This session is saved to Gipity - watch it live at ${brand("prompt.gipity.ai")}. (--no-capture on init to disable.)`)}`);
39776
40031
  }
39777
40032
  if (adapter.oneTimeSetupNote) {
@@ -39831,32 +40086,39 @@ async function pickOrCreateProject(projects, existingSlugs) {
39831
40086
  const cwd = process.cwd();
39832
40087
  const showAdopt = canAdoptCwd(cwd);
39833
40088
  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;
40089
+ const fitsAll = filtered.length <= 9 - reserved;
40090
+ const hasMore = !fitsAll;
40091
+ const searchSlot = hasMore ? reserved + 1 : 0;
40092
+ const projectBase = reserved + (hasMore ? 1 : 0);
40093
+ const recent = fitsAll ? filtered : filtered.slice(0, 9 - reserved - 2);
40094
+ const moreSlot = hasMore ? projectBase + recent.length + 1 : 0;
39837
40095
  while (true) {
39838
40096
  const newProjectLabel = formatNewProjectLabel(existingSlugs);
39839
40097
  console.log(` ${bold("Choose project to open:")}
39840
40098
  `);
39841
- console.log(` ${bold("1.")} Create new project ${muted(`(${newProjectLabel})`)}`);
40099
+ console.log(` ${bold("1.")} ${info("Create new project")} ${muted(`(${newProjectLabel})`)}`);
39842
40100
  if (showAdopt) {
39843
- console.log(` ${bold("2.")} Use this directory ${muted(`(${formatCwdLabel(cwd)})`)}`);
40101
+ console.log(` ${bold("2.")} ${info("Use this directory")} ${muted(`(${formatCwdLabel(cwd)})`)}`);
40102
+ }
40103
+ if (hasMore) {
40104
+ console.log(` ${bold(`${searchSlot}.`)} ${info("Search projects")} ${muted(`(${filtered.length} total)`)}`);
39844
40105
  }
39845
40106
  recent.forEach(
39846
- (p, i) => console.log(` ${bold(`${i + reserved + 1}.`)} ${p.name} ${muted(`(${p.slug})`)}`)
40107
+ (p, i) => console.log(` ${bold(`${projectBase + i + 1}.`)} ${p.name} ${muted(`(${p.slug})`)}`)
39847
40108
  );
39848
40109
  if (hasMore) {
39849
- console.log(` ${bold(`${recent.length + reserved + 1}.`)} Show all projects`);
40110
+ console.log(` ${bold(`${moreSlot}.`)} ${info("Show more")}`);
39850
40111
  }
39851
40112
  console.log("");
39852
- const maxOption = recent.length + reserved + (hasMore ? 1 : 0);
40113
+ const maxOption = projectBase + recent.length + (hasMore ? 1 : 0);
39853
40114
  const idx = await pickOne("Choose", maxOption, 1);
39854
- if (idx > reserved && idx <= recent.length + reserved) {
39855
- return { kind: "pick", project: recent[idx - reserved - 1] };
40115
+ if (idx > projectBase && idx <= projectBase + recent.length) {
40116
+ return { kind: "pick", project: recent[idx - projectBase - 1] };
39856
40117
  }
39857
- if (hasMore && idx === recent.length + reserved + 1) {
39858
- const picked = await pickFromAll(filtered);
40118
+ if (hasMore && (idx === searchSlot || idx === moreSlot)) {
40119
+ const picked = await browseProjects(filtered, { search: idx === searchSlot });
39859
40120
  if (picked) return { kind: "pick", project: picked };
40121
+ console.log("");
39860
40122
  continue;
39861
40123
  }
39862
40124
  if (showAdopt && idx === 2) {
@@ -39871,17 +40133,62 @@ async function pickOrCreateProject(projects, existingSlugs) {
39871
40133
  return { kind: "create-new", project };
39872
40134
  }
39873
40135
  }
39874
- async function pickFromAll(filtered) {
40136
+ var PROJECT_PAGE_SIZE = 10;
40137
+ function searchProjects(all, query) {
40138
+ const q = query.trim().toLowerCase();
40139
+ if (!q) return all;
40140
+ return all.filter((p) => p.name.toLowerCase().includes(q) || p.slug.toLowerCase().includes(q));
40141
+ }
40142
+ async function browseProjects(all, opts = {}) {
40143
+ let items = all;
40144
+ let heading = "All projects";
40145
+ const runSearch = async () => {
40146
+ const q = await prompt(` ${bold("Search projects:")} `);
40147
+ if (!q) return false;
40148
+ const matches = searchProjects(all, q);
40149
+ if (matches.length === 0) {
40150
+ console.log(` ${muted(`No projects match "${q}".`)}`);
40151
+ return false;
40152
+ }
40153
+ items = matches;
40154
+ heading = `Projects matching "${q}"`;
40155
+ return true;
40156
+ };
39875
40157
  console.log("");
39876
- console.log(` ${bold("All projects:")}
40158
+ if (opts.search && !await runSearch()) return null;
40159
+ let shown = 0;
40160
+ while (true) {
40161
+ if (shown < items.length) {
40162
+ const page = items.slice(shown, shown + PROJECT_PAGE_SIZE);
40163
+ if (shown === 0) console.log(` ${bold(`${heading}`)} ${muted(`(${items.length})`)}
39877
40164
  `);
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;
40165
+ page.forEach(
40166
+ (p, i) => console.log(` ${bold(`${String(shown + i + 1).padStart(3)}.`)} ${p.name} ${muted(`(${p.slug})`)}`)
40167
+ );
40168
+ shown += page.length;
40169
+ console.log("");
40170
+ }
40171
+ const more = shown < items.length;
40172
+ const hints = [
40173
+ more ? `Enter = next ${Math.min(PROJECT_PAGE_SIZE, items.length - shown)}` : null,
40174
+ "number = open",
40175
+ "s = search",
40176
+ "q = back"
40177
+ ].filter(Boolean).join(" \xB7 ");
40178
+ const answer = (await prompt(` ${bold("Choose")} ${muted(`(${hints})`)}: `)).toLowerCase();
40179
+ if (answer === "" && more) continue;
40180
+ if (answer === "" || answer === "q") return null;
40181
+ if (answer === "s") {
40182
+ if (await runSearch()) {
40183
+ shown = 0;
40184
+ console.log("");
40185
+ }
40186
+ continue;
40187
+ }
40188
+ const n = parseInt(answer, 10);
40189
+ if (Number.isInteger(n) && n >= 1 && n <= shown) return items[n - 1];
40190
+ console.log(` ${muted("Type one of the numbers above, Enter for more, s to search, or q to go back.")}`);
40191
+ }
39885
40192
  }
39886
40193
  function formatNewProjectLabel(existingSlugs) {
39887
40194
  const slug = suggestProjectName(existingSlugs);
@@ -39990,12 +40297,10 @@ var KITS = [
39990
40297
  { key: "chatbot", hint: "drop-in chatbot - persona, guardrails, streaming responses" },
39991
40298
  { key: "audio-align", hint: "audio + lyrics -> word-level timing JSON (GPU job)" },
39992
40299
  { 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
40300
  { key: "contacts", hint: "multi-source contact layer - LinkedIn/Gmail import, dedupe with provenance" },
39997
40301
  { 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" }
40302
+ { key: "notify", hint: "web push notifications - platform-owned keys, works on iOS home screen" },
40303
+ { key: "servicenow", hint: "ServiceNow tables as a data source - OAuth pull/write-back/real-time sync" }
39999
40304
  ];
40000
40305
 
40001
40306
  // src/commands/add.ts
@@ -40151,7 +40456,7 @@ var addCommand = new Command("add").description("Add a template or kit").argumen
40151
40456
  console.error(muted(isKit ? "Installing kit (server-side install pipeline)..." : "Installing (server writes files + generates favicons; first add for a title can take ~10s)..."));
40152
40457
  }
40153
40458
  const doAdd = () => post(`/projects/${config.projectGuid}/add`, body);
40154
- const res = opts.json ? await doAdd() : await withSpinner("Installing\u2026", doAdd, { done: null });
40459
+ const res = opts.json ? await doAdd() : await withSpinner("Installing...", doAdd, { done: null });
40155
40460
  const syncResult = await sync({ interactive: false, progress: opts.json ? void 0 : createProgressReporter() });
40156
40461
  const data = res.data;
40157
40462
  if (opts.json) {
@@ -40199,7 +40504,7 @@ var removeCommand = new Command("remove").description("Remove a kit").argument("
40199
40504
  }
40200
40505
  }
40201
40506
  const doRemove = () => post(`/projects/${config.projectGuid}/remove`, { name: kit });
40202
- const res = opts.json ? await doRemove() : await withSpinner("Removing\u2026", doRemove, { done: null });
40507
+ const res = opts.json ? await doRemove() : await withSpinner("Removing...", doRemove, { done: null });
40203
40508
  const data = res.data;
40204
40509
  const syncResult = await sync({
40205
40510
  interactive: false,
@@ -40249,7 +40554,7 @@ function dispositionFilename(headers) {
40249
40554
  var saveCommand = new Command("save").description("Save this app as a portable .gip bundle").argument("[output]", "Output .gip path, or a directory to write <slug>.gip into (default: ./<slug>.gip)").option("--json", "Output as JSON").addHelpText("after", "\nRestore anywhere with `gipity load <file>.gip` - it always creates a NEW project.").action((output, opts) => run("Save", async () => {
40250
40555
  const config = requireConfig();
40251
40556
  const doExport = () => downloadWithHeaders(`/projects/${config.projectGuid}/export`);
40252
- const { buffer, headers } = opts.json ? await doExport() : await withSpinner("Exporting\u2026", doExport, { done: null });
40557
+ const { buffer, headers } = opts.json ? await doExport() : await withSpinner("Exporting...", doExport, { done: null });
40253
40558
  const filename = dispositionFilename(headers) ?? `${config.projectSlug}.gip`;
40254
40559
  let dest = path2.resolve(output ?? filename);
40255
40560
  if (output && fs3.existsSync(dest) && fs3.statSync(dest).isDirectory()) {
@@ -40257,14 +40562,14 @@ var saveCommand = new Command("save").description("Save this app as a portable .
40257
40562
  }
40258
40563
  fs3.mkdirSync(path2.dirname(dest), { recursive: true });
40259
40564
  fs3.writeFileSync(dest, buffer);
40260
- const sha2562 = createHash4("sha256").update(buffer).digest("hex");
40565
+ const sha256 = createHash4("sha256").update(buffer).digest("hex");
40261
40566
  const skipped = Number(headers.get("x-gip-skipped") ?? 0);
40262
40567
  const entries = zipEntryCount(buffer);
40263
40568
  if (opts.json) {
40264
40569
  console.log(JSON.stringify({
40265
40570
  path: dest,
40266
40571
  bytes: buffer.length,
40267
- sha256: sha2562,
40572
+ sha256,
40268
40573
  ...entries !== null ? { entries } : {},
40269
40574
  skipped
40270
40575
  }));
@@ -40274,7 +40579,7 @@ var saveCommand = new Command("save").description("Save this app as a portable .
40274
40579
  console.log(` Path: ${dest}`);
40275
40580
  console.log(` Size: ${formatBytes(buffer.length)}`);
40276
40581
  if (entries !== null) console.log(` Entries: ${entries} (including the bundle manifest)`);
40277
- console.log(` SHA-256: ${sha2562}`);
40582
+ console.log(` SHA-256: ${sha256}`);
40278
40583
  if (skipped > 0) {
40279
40584
  console.log("");
40280
40585
  console.warn(warning(`${skipped} file(s) were skipped (unreadable or over 100 MB) - this bundle is partial.`));
@@ -40391,7 +40696,7 @@ var loadCommand = new Command("load").description("Create a new app from a .gip
40391
40696
  };
40392
40697
  }
40393
40698
  if (opts.inspect) {
40394
- const res = opts.json ? await doInspect() : await withSpinner("Inspecting\u2026", doInspect, { done: null });
40699
+ const res = opts.json ? await doInspect() : await withSpinner("Inspecting...", doInspect, { done: null });
40395
40700
  if (opts.json) {
40396
40701
  console.log(JSON.stringify(res.data));
40397
40702
  return;
@@ -40399,7 +40704,7 @@ var loadCommand = new Command("load").description("Create a new app from a .gip
40399
40704
  printInspect(res.data, source);
40400
40705
  return;
40401
40706
  }
40402
- const { data, partial } = opts.json ? await doImport() : await withSpinner("Importing\u2026", doImport, { done: null });
40707
+ const { data, partial } = opts.json ? await doImport() : await withSpinner("Importing...", doImport, { done: null });
40403
40708
  const project = data.project;
40404
40709
  const dir = path3.join(getProjectsRoot(), project.slug);
40405
40710
  fs4.mkdirSync(dir, { recursive: true });
@@ -40800,7 +41105,7 @@ ${error(`\u26A0 ${problems} error/crash line(s) flagged above`)}`
40800
41105
  );
40801
41106
  if (problems > 0) process.exitCode = 1;
40802
41107
  }
40803
- var pageTestCommand = new Command("test").description("Multi-client realtime check: load a URL in N concurrent headless clients; flag console errors, or drive an action and observe shared state (--observe)").argument("<url>", "Deployed URL to load in every client. {{label}}/{{i}} substitute per client in both modes (e.g. ?name=Bot{{i}}, or ?role={{label}} with --labels host,join), so one invocation can give each client a distinct role.").option("--clients <n>", "Number of headless clients to launch", "2").option("--stagger <s>", "Seconds between client starts (passive default 12; interactive default 0)").option("--wait <ms>", "Passive mode: ms each client stays open after load (max 30000)", "24000").option("--observe <expr>", "Interactive: JS expression sampled in each client to read shared state (e.g. presence count). Switches on interactive mode.").option("--action <expr>", "Interactive: one-time JS run in each client before observing (e.g. fill a name + submit). {{label}}/{{i}} are substituted per client.").option("--labels <csv>", "Per-client labels substituted for {{label}} in the URL/--action/--observe (default client-0, client-1, \u2026)").option("--hold <ms>", `Interactive: total observe window per client (${MIN_HOLD_MS}-${MAX_HOLD_MS}ms)`, "8000").option("--samples <k>", "Interactive: number of observations across the hold window (2-30)", "6").option("--wait-for <selector>", "Interactive: wait for this CSS selector before running --action (deterministic readiness gate)").option("--json", "Output as JSON").addHelpText("after", `
41108
+ var pageTestCommand = new Command("test").description("Multi-client realtime check: load a URL in N concurrent headless clients; flag console errors, or drive an action and observe shared state (--observe)").argument("<url>", "Deployed URL to load in every client. {{label}}/{{i}} substitute per client in both modes (e.g. ?name=Bot{{i}}, or ?role={{label}} with --labels host,join), so one invocation can give each client a distinct role.").option("--clients <n>", "Number of headless clients to launch", "2").option("--stagger <s>", "Seconds between client starts (passive default 12; interactive default 0)").option("--wait <ms>", "Passive mode: ms each client stays open after load (max 30000)", "24000").option("--observe <expr>", "Interactive: JS expression sampled in each client to read shared state (e.g. presence count). Switches on interactive mode.").option("--action <expr>", "Interactive: one-time JS run in each client before observing (e.g. fill a name + submit). {{label}}/{{i}} are substituted per client.").option("--labels <csv>", "Per-client labels substituted for {{label}} in the URL/--action/--observe (default client-0, client-1, ...)").option("--hold <ms>", `Interactive: total observe window per client (${MIN_HOLD_MS}-${MAX_HOLD_MS}ms)`, "8000").option("--samples <k>", "Interactive: number of observations across the hold window (2-30)", "6").option("--wait-for <selector>", "Interactive: wait for this CSS selector before running --action (deterministic readiness gate)").option("--json", "Output as JSON").addHelpText("after", `
40804
41109
  Examples:
40805
41110
  # Passive: load in 3 staggered clients, flag console errors
40806
41111
  gipity page test "https://dev.gipity.ai/me/app/" --clients 3 --stagger 8
@@ -40858,9 +41163,6 @@ function looksLikeHtml(body) {
40858
41163
  const head = body.slice(0, 300).toLowerCase();
40859
41164
  return /<!doctype html|<html[\s>]/.test(head);
40860
41165
  }
40861
- function sha256(s) {
40862
- return createHash5("sha256").update(s).digest("hex");
40863
- }
40864
41166
  function baseWithSlash(url) {
40865
41167
  return url.endsWith("/") ? url : url + "/";
40866
41168
  }
@@ -40870,8 +41172,14 @@ function resolveUrl(base, path5) {
40870
41172
  async function fetchRaw(url) {
40871
41173
  try {
40872
41174
  const res = await fetch(url, { redirect: "follow" });
40873
- const body = await res.text();
40874
- return { status: res.status, contentType: res.headers.get("content-type"), body };
41175
+ const buf = Buffer.from(await res.arrayBuffer());
41176
+ return {
41177
+ status: res.status,
41178
+ contentType: res.headers.get("content-type"),
41179
+ body: buf.toString("utf-8"),
41180
+ bytes: buf.length,
41181
+ sha256: createHash5("sha256").update(buf).digest("hex")
41182
+ };
40875
41183
  } catch {
40876
41184
  return null;
40877
41185
  }
@@ -40883,17 +41191,16 @@ async function probeShell(base) {
40883
41191
  return {
40884
41192
  served: r.status >= 200 && r.status < 300,
40885
41193
  status: r.status,
40886
- sha256: sha256(r.body),
41194
+ sha256: r.sha256,
40887
41195
  isHtml: looksLikeHtml(r.body)
40888
41196
  };
40889
41197
  }
40890
41198
  function classify(path5, r, shell) {
40891
41199
  if (!r) return { status: null, contentType: null, bytes: 0, verdict: "MISSING", detail: "fetch failed (host unreachable)" };
40892
- const bytes = Buffer.byteLength(r.body);
40893
- const base = { status: r.status, contentType: r.contentType, bytes };
41200
+ const base = { status: r.status, contentType: r.contentType, bytes: r.bytes };
40894
41201
  if (r.status >= 400) return { ...base, verdict: "MISSING", detail: `HTTP ${r.status}` };
40895
41202
  const expect = expectedType(path5);
40896
- if (shell.served && shell.sha256 && sha256(r.body) === shell.sha256) {
41203
+ if (shell.served && shell.sha256 && r.sha256 === shell.sha256) {
40897
41204
  return { ...base, verdict: "MISSING", detail: `HTTP ${r.status} but served the SPA shell (file not deployed)` };
40898
41205
  }
40899
41206
  if (expect && expect !== "html" && looksLikeHtml(r.body)) {
@@ -40909,7 +41216,7 @@ var TAG = {
40909
41216
  "MISSING": error("MISSING "),
40910
41217
  "WRONG-TYPE": warning("WRONG-TYPE")
40911
41218
  };
40912
- var pageFetchCommand = new Command("fetch").description("Verify deployed non-rendered files (llms.txt, AGENTS.md, robots.txt, served JSON\u2026) really exist - catches the static-host trap where a missing file returns 200 with the SPA shell instead of a 404").argument("<url>", "Deployed app base URL; file paths resolve relative to it").argument("<paths...>", "One or more file paths to verify, e.g. llms.txt AGENTS.md robots.txt").option("--json", "Output as JSON").action((url, paths, opts) => run("Page fetch", async () => {
41219
+ var pageFetchCommand = new Command("fetch").description("Verify deployed non-rendered files (llms.txt, AGENTS.md, robots.txt, served JSON...) really exist - catches the static-host trap where a missing file returns 200 with the SPA shell instead of a 404").argument("<url>", "Deployed app base URL; file paths resolve relative to it").argument("<paths...>", "One or more file paths to verify, e.g. llms.txt AGENTS.md robots.txt").option("--json", "Output as JSON").action((url, paths, opts) => run("Page fetch", async () => {
40913
41220
  const shell = await probeShell(url);
40914
41221
  const results = [];
40915
41222
  for (const p of paths) {
@@ -40958,7 +41265,7 @@ init_api();
40958
41265
  init_config();
40959
41266
  init_colors();
40960
41267
  init_utils();
40961
- var recordsCommand = new Command("records").description("Manage records-kit data (registry-driven CRUD)");
41268
+ var recordsCommand = new Command("records").description("Manage app records (Gipity Records - validated CRUD with audit history)");
40962
41269
  recordsCommand.command("list").description("List record tables").option("--json", "Output as JSON").action((opts) => run("List", async () => {
40963
41270
  const config = requireConfig();
40964
41271
  const res = await get(`/api/${config.projectGuid}/records-config`);
@@ -41017,6 +41324,16 @@ recordsCommand.command("get <table> <id>").description("Get a record").option("-
41017
41324
  const res = await get(`/api/${config.projectGuid}/records/${table}/${id2}`);
41018
41325
  console.log(opts.json ? JSON.stringify(res.data) : JSON.stringify(res.data, null, 2));
41019
41326
  }));
41327
+ 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 () => {
41328
+ const config = requireConfig();
41329
+ const res = await get(
41330
+ `/api/${config.projectGuid}/records/${table}/${id2}/history?limit=${encodeURIComponent(opts.limit)}`
41331
+ );
41332
+ printList(res.data, opts, "No history for this record.", (e) => {
41333
+ const summary = e.detail?.summary || `${e.action} ${e.entity_type} ${e.entity_id}`;
41334
+ return `${muted(e.created_at)} ${bold(e.source || "-")} ${summary}`;
41335
+ });
41336
+ }));
41020
41337
  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
41338
  const config = requireConfig();
41022
41339
  const data = JSON.parse(opts.data);
@@ -41055,7 +41372,7 @@ fnCommand.command("list").description("List functions").option("--json", "Output
41055
41372
  ${muted(f.description)}` : line;
41056
41373
  });
41057
41374
  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)`));
41375
+ console.log(muted(`Endpoint: POST ${resolveApiBase()}/api/${config.projectGuid}/fn/<name> (same on dev and prod; public path: gipity fn call <name> --anon)`));
41059
41376
  }
41060
41377
  }));
41061
41378
  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 +41411,9 @@ async function callAnon(projectGuid, name, body) {
41094
41411
  appToken ? { "X-App-Token": appToken } : void 0
41095
41412
  );
41096
41413
  }
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 () => {
41414
+ 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
41415
  const config = requireConfig();
41099
- const raw = bodyArg || opts.data || "{}";
41100
- const body = JSON.parse(raw);
41416
+ const body = resolveBody(bodyArg || opts.data, opts.file);
41101
41417
  const path5 = `/api/${config.projectGuid}/fn/${encodeURIComponent(name)}`;
41102
41418
  if (opts.anon) {
41103
41419
  console.error(muted("Auth: anonymous visitor (the public path a signed-out user hits)"));
@@ -41145,9 +41461,13 @@ var SERVICES = [
41145
41461
  { name: "location/ip", method: "POST", desc: "IP geolocation ({ ip? })" },
41146
41462
  { name: "location/geocode", method: "POST", desc: "Reverse geocode ({ lat, lon })" }
41147
41463
  ];
41148
- 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 () => {
41464
+ 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.\nPer-service docs: gipity skill read app-llm | app-tts | app-image | app-video | app-audio (transcribe, sound, music) | app-location");
41465
+ serviceCommand.command("list").description("List callable app services").option("--json", "Output the service list as JSON").action((opts) => run("Services", async () => {
41150
41466
  requireConfig();
41467
+ if (opts.json) {
41468
+ console.log(JSON.stringify(SERVICES));
41469
+ return;
41470
+ }
41151
41471
  const width = SERVICES.reduce((m, s) => Math.max(m, s.name.length), 0);
41152
41472
  console.log('Call one with `gipity service call <name> \'{"json":"body"}\'`');
41153
41473
  console.log(muted("(GET endpoints like llm/models, tts/voices take --get and no body)"));
@@ -41187,7 +41507,7 @@ secretsCommand.command("list").alias("ls").description("List secret names (never
41187
41507
  }
41188
41508
  console.log(bold(`${res.data.length} ${scope} secret${res.data.length === 1 ? "" : "s"}:`));
41189
41509
  for (const s of res.data) {
41190
- const masked = s.preview ? muted(`\u2026${s.preview}`) : muted("(hidden)");
41510
+ const masked = s.preview ? muted(`...${s.preview}`) : muted("(hidden)");
41191
41511
  console.log(` ${s.name} ${masked} ${muted(`updated ${new Date(s.updated_at).toLocaleDateString()}`)}`);
41192
41512
  }
41193
41513
  }));
@@ -41466,10 +41786,9 @@ jobCommand.command("list").description("List jobs").option("--json", "Output as
41466
41786
  ${muted(j7.description)}` : line;
41467
41787
  });
41468
41788
  }));
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 () => {
41789
+ 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
41790
  const config = requireConfig();
41471
- const raw = bodyArg || opts.data || "{}";
41472
- const input = JSON.parse(raw);
41791
+ const input = resolveBody(bodyArg || opts.data, opts.file);
41473
41792
  const body = { input };
41474
41793
  if (opts.idempotencyKey) body.idempotency_key = opts.idempotencyKey;
41475
41794
  const res = await post(
@@ -41481,6 +41800,10 @@ jobCommand.command("submit <name> [body]").description("Submit a job (returns a
41481
41800
  } else {
41482
41801
  const tag = res.data.replayed ? warning(" (replayed)") : "";
41483
41802
  console.log(`${bold(res.data.run_guid)} ${muted(res.data.status)}${tag}`);
41803
+ const TERMINAL = /* @__PURE__ */ new Set(["success", "failed", "cancelled", "canceled", "error"]);
41804
+ if (!TERMINAL.has(res.data.status)) {
41805
+ console.error(muted(`Block until it finishes: gipity job wait ${res.data.run_guid}`));
41806
+ }
41484
41807
  }
41485
41808
  }));
41486
41809
  jobCommand.command("status <runGuid>").description("Show status of a job run").option("--json", "Output as JSON").action((runGuid, opts) => run("Status", async () => {
@@ -41493,11 +41816,56 @@ jobCommand.command("status <runGuid>").description("Show status of a job run").o
41493
41816
  const r = res.data;
41494
41817
  const statusColor = r.status === "success" ? success : r.status === "failed" ? error : muted;
41495
41818
  console.log(`${statusColor(r.status)} ${muted(r.guid)}`);
41496
- if (r.progress_pct != null) console.log(`progress: ${Math.round(r.progress_pct * 100)}%${r.progress_message ? ` (${r.progress_message})` : ""}`);
41819
+ if (r.progress_pct != null) {
41820
+ const prog = `${Math.round(r.progress_pct * 100)}%${r.progress_message ? ` (${r.progress_message})` : ""}`;
41821
+ const failedish = r.status === "failed" || r.status === "cancelled" || r.status === "canceled" || r.status === "error";
41822
+ console.log(failedish ? `last progress before ${r.status}: ${prog}` : `progress: ${prog}`);
41823
+ }
41497
41824
  if (r.duration_ms != null) console.log(`duration: ${r.duration_ms}ms`);
41498
41825
  if (r.error) console.log(`${error("error:")} ${r.error}`);
41499
41826
  if (r.output) console.log(`output: ${JSON.stringify(r.output)}`);
41500
41827
  }));
41828
+ 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 () => {
41829
+ const config = requireConfig();
41830
+ const TERMINAL = /* @__PURE__ */ new Set(["success", "failed", "cancelled", "canceled", "error"]);
41831
+ const durOpt = parseDuration(opts.timeout, "s");
41832
+ const timeoutSec = durOpt ? Math.max(1, Math.round(durOpt.value)) : parseInt(opts.timeout, 10);
41833
+ const timeout = Number.isFinite(timeoutSec) && timeoutSec > 0 ? timeoutSec : 90;
41834
+ const interval = Math.max(1, parseInt(opts.interval, 10) || 3);
41835
+ const deadline = Date.now() + timeout * 1e3;
41836
+ const fetchRun = async () => (await get(`/projects/${config.projectGuid}/jobs/runs/${encodeURIComponent(runGuid)}`)).data;
41837
+ let r = await fetchRun();
41838
+ let lastProgress = "";
41839
+ while (!TERMINAL.has(r.status) && Date.now() < deadline) {
41840
+ if (!opts.json) {
41841
+ const prog = r.progress_pct != null ? `${Math.round(r.progress_pct * 100)}%${r.progress_message ? ` (${r.progress_message})` : ""}` : r.status;
41842
+ if (prog !== lastProgress) {
41843
+ console.error(muted(`... ${prog}`));
41844
+ lastProgress = prog;
41845
+ }
41846
+ }
41847
+ const remaining = deadline - Date.now();
41848
+ if (remaining <= 0) break;
41849
+ await new Promise((res) => setTimeout(res, Math.min(interval * 1e3, remaining)));
41850
+ r = await fetchRun();
41851
+ }
41852
+ const done = TERMINAL.has(r.status);
41853
+ if (opts.json) {
41854
+ console.log(JSON.stringify(done ? r : { ...r, waiting: true, timed_out: true }));
41855
+ } else if (!done) {
41856
+ const prog = r.progress_pct != null ? ` ${Math.round(r.progress_pct * 100)}%` : "";
41857
+ console.log(`${warning("still running")}${prog}${r.progress_message ? ` (${r.progress_message})` : ""} ${muted(runGuid)}`);
41858
+ console.log(muted(`Not finished after ${timeout}s. Run \`gipity job wait ${runGuid}\` again to keep waiting.`));
41859
+ } else {
41860
+ const statusColor = r.status === "success" ? success : error;
41861
+ console.log(`${statusColor(r.status)} ${muted(r.guid)}`);
41862
+ if (r.duration_ms != null) console.log(`duration: ${r.duration_ms}ms`);
41863
+ if (r.error) console.log(`${error("error:")} ${r.error}`);
41864
+ if (r.output) console.log(`output: ${JSON.stringify(r.output)}`);
41865
+ }
41866
+ if (!done) process.exit(2);
41867
+ if (r.status !== "success") process.exit(1);
41868
+ }));
41501
41869
  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
41870
  const config = requireConfig();
41503
41871
  const res = await get(
@@ -41512,19 +41880,44 @@ jobCommand.command("runs <name>").description("List recent runs for a job").opti
41512
41880
  ${error(`error: ${r.error}`)}` : line;
41513
41881
  });
41514
41882
  }));
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 () => {
41883
+ 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
41884
  const config = requireConfig();
41517
41885
  if (!opts.follow) {
41518
41886
  const res2 = await get(`/projects/${config.projectGuid}/jobs/runs/${encodeURIComponent(runGuid)}`);
41519
41887
  console.log(opts.json ? JSON.stringify(res2.data) : JSON.stringify(res2.data, null, 2));
41520
41888
  return;
41521
41889
  }
41890
+ let peekSeconds = 0;
41891
+ if (opts.timeout != null) {
41892
+ const dur = parseDuration(String(opts.timeout), "s");
41893
+ const secs = dur ? dur.value : parseFloat(String(opts.timeout));
41894
+ peekSeconds = Number.isFinite(secs) && secs > 0 ? secs : 0;
41895
+ }
41522
41896
  const url = `${getBaseUrl()}/projects/${config.projectGuid}/jobs/runs/${encodeURIComponent(runGuid)}/logs/stream`;
41523
41897
  const authHeader = await getAuthHeader();
41524
41898
  const headers = { "Accept": "text/event-stream" };
41525
41899
  if (authHeader) headers.Authorization = authHeader;
41526
- const res = await fetch(url, { headers });
41900
+ const controller = new AbortController();
41901
+ let peekTimer;
41902
+ let peekedOut = false;
41903
+ if (peekSeconds > 0) {
41904
+ peekTimer = setTimeout(() => {
41905
+ peekedOut = true;
41906
+ controller.abort();
41907
+ }, peekSeconds * 1e3);
41908
+ }
41909
+ let res;
41910
+ try {
41911
+ res = await fetch(url, { headers, signal: controller.signal });
41912
+ } catch (e) {
41913
+ if (peekedOut) {
41914
+ console.error(muted(`... 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.`));
41915
+ return;
41916
+ }
41917
+ throw e;
41918
+ }
41527
41919
  if (!res.ok || !res.body) {
41920
+ if (peekTimer) clearTimeout(peekTimer);
41528
41921
  console.error(error(`Failed to open stream: HTTP ${res.status}`));
41529
41922
  process.exit(1);
41530
41923
  }
@@ -41533,8 +41926,21 @@ jobCommand.command("logs <runGuid>").description("Stream live logs for a job run
41533
41926
  let buffer = "";
41534
41927
  let currentEvent = "";
41535
41928
  while (true) {
41536
- const { done, value } = await reader.read();
41537
- if (done) break;
41929
+ let chunk;
41930
+ try {
41931
+ chunk = await reader.read();
41932
+ } catch (e) {
41933
+ if (peekedOut) {
41934
+ console.error(muted(`... 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.`));
41935
+ return;
41936
+ }
41937
+ throw e;
41938
+ }
41939
+ const { done, value } = chunk;
41940
+ if (done) {
41941
+ if (peekTimer) clearTimeout(peekTimer);
41942
+ break;
41943
+ }
41538
41944
  buffer += decoder.decode(value, { stream: true });
41539
41945
  let nl2;
41540
41946
  while ((nl2 = buffer.indexOf("\n")) >= 0) {
@@ -41546,9 +41952,9 @@ jobCommand.command("logs <runGuid>").description("Stream live logs for a job run
41546
41952
  const dataRaw = line.slice(5).trim();
41547
41953
  if (currentEvent === "log") {
41548
41954
  try {
41549
- const { type, chunk } = JSON.parse(dataRaw);
41550
- if (type === "stderr") process.stderr.write(error(chunk));
41551
- else process.stdout.write(chunk);
41955
+ const { type, chunk: chunk2 } = JSON.parse(dataRaw);
41956
+ if (type === "stderr") process.stderr.write(error(chunk2));
41957
+ else process.stdout.write(chunk2);
41552
41958
  } catch {
41553
41959
  }
41554
41960
  } else if (currentEvent === "status") {
@@ -41588,11 +41994,11 @@ jobCommand.command("cancel <runGuid>").description("Cancel a queued or running j
41588
41994
  }
41589
41995
  }));
41590
41996
  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");
41997
+ const { existsSync: existsSync24, statSync: statSync12 } = await import("node:fs");
41998
+ const { resolve: resolve19, join: join27 } = await import("node:path");
41593
41999
  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()) {
42000
+ const jobDir = resolve19(process.cwd(), "jobs", name);
42001
+ if (!existsSync24(jobDir) || !statSync12(jobDir).isDirectory()) {
41596
42002
  console.error(error(`jobs/${name}/ not found in current directory`));
41597
42003
  process.exit(1);
41598
42004
  }
@@ -41613,7 +42019,7 @@ jobCommand.command("run-local <name>").description("Run the job in a local Docke
41613
42019
  },
41614
42020
  { file: "main.sh", runtime: "bash", entry: "bash" }
41615
42021
  ];
41616
- const picked = variants.find((v7) => existsSync24(join26(jobDir, v7.file)));
42022
+ const picked = variants.find((v7) => existsSync24(join27(jobDir, v7.file)));
41617
42023
  if (!picked) {
41618
42024
  console.error(error(`No main.py / main.js / main.sh found under jobs/${name}/`));
41619
42025
  process.exit(1);
@@ -41638,7 +42044,7 @@ jobCommand.command("run-local <name>").description("Run the job in a local Docke
41638
42044
  "-v",
41639
42045
  `${jobDir}:/work`
41640
42046
  ];
41641
- const needsDeps = opts.deps !== false && picked.depsFile && picked.installCmd && existsSync24(join26(jobDir, picked.depsFile));
42047
+ const needsDeps = opts.deps !== false && picked.depsFile && picked.installCmd && existsSync24(join27(jobDir, picked.depsFile));
41642
42048
  const handlerCmd = `${picked.entry} /work/${picked.file}`;
41643
42049
  let shellCmd;
41644
42050
  if (needsDeps && picked.installCmd) {
@@ -41813,8 +42219,8 @@ emailCommand.command("log").description("Recent email() sends from your app").op
41813
42219
  init_api();
41814
42220
  init_config();
41815
42221
  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";
42222
+ import { mkdirSync as mkdirSync13, writeFileSync as writeFileSync13, readFileSync as readFileSync19 } from "fs";
42223
+ import { resolve as resolvePath2, dirname as dirname11, relative as relative5, isAbsolute, basename as basename6 } from "path";
41818
42224
 
41819
42225
  // src/provider-docs.ts
41820
42226
  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 +42234,12 @@ var TTS_PROVIDER_DESCRIPTIONS = {
41828
42234
  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
42235
 
41830
42236
  // src/commands/generate.ts
41831
- async function downloadFile(url, filename) {
42237
+ async function downloadFile(url, filename, explicit) {
41832
42238
  const res = await fetch(url);
41833
42239
  if (!res.ok) throw new Error(`Download failed: ${res.status}`);
41834
42240
  const buffer = Buffer.from(await res.arrayBuffer());
41835
- const outPath = correctExtension(filename, buffer);
42241
+ const outPath = explicit ? filename : correctExtension(filename, buffer);
42242
+ if (explicit) noteFormatMismatch(filename, buffer);
41836
42243
  ensureOutputDir(outPath);
41837
42244
  writeFileSync13(outPath, buffer);
41838
42245
  const savedPath = resolvePath2(outPath);
@@ -41867,6 +42274,16 @@ function correctExtension(filename, buf) {
41867
42274
  ));
41868
42275
  return corrected;
41869
42276
  }
42277
+ function noteFormatMismatch(filename, buf) {
42278
+ const actual = sniffExt(buf);
42279
+ if (!actual) return;
42280
+ const dot = filename.lastIndexOf(".");
42281
+ const asked = dot > 0 ? filename.slice(dot + 1).toLowerCase() : "";
42282
+ if (!asked || canonicalExt(asked) === actual) return;
42283
+ console.error(muted(
42284
+ `Note: ${basename6(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.`
42285
+ ));
42286
+ }
41870
42287
  function ensureOutputDir(output) {
41871
42288
  const dir = dirname11(resolvePath2(output));
41872
42289
  try {
@@ -41886,22 +42303,46 @@ async function pushGenerated(savedPath) {
41886
42303
  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
42304
  }
41888
42305
  }
41889
- var imageCommand = new Command("image").description(`Generate an image from a text prompt using AI.
42306
+ function readInputImages(paths) {
42307
+ return paths.map((p) => {
42308
+ let buf;
42309
+ try {
42310
+ buf = readFileSync19(p);
42311
+ } catch (e) {
42312
+ throw new Error(`can't read --input image ${p} (${e.code || e.message}).`);
42313
+ }
42314
+ const mime = guessMime(p);
42315
+ if (!mime.startsWith("image/")) {
42316
+ throw new Error(`--input ${p} is not an image (${mime}). Edit inputs must be image files.`);
42317
+ }
42318
+ return { data: buf.toString("base64"), mime_type: mime };
42319
+ });
42320
+ }
42321
+ var imageCommand = new Command("image").description(`Generate an image from a text prompt, or EDIT existing images with --input.
41890
42322
 
41891
42323
  Models: ${IMAGE_MODELS_DOC}
41892
42324
 
42325
+ Editing (--input): pass one or more source images and the prompt becomes an edit
42326
+ instruction applied to them - "make it night time", "add a hat", "remove the car
42327
+ in the background", or compose several inputs together. Editing routes to Gemini
42328
+ automatically. This is the first-party way to exercise the image-edit flow from
42329
+ the CLI; the CLI reads the file bytes itself, so there is no base64/argv fiddling.
42330
+
41893
42331
  Gemini-specific options:
41894
42332
  --aspect-ratio Control output shape: ${IMAGE_GEMINI_ASPECT_RATIOS}
41895
42333
  --image-size Control resolution: ${IMAGE_GEMINI_SIZES} (default: 1K)
41896
42334
 
41897
42335
  Examples:
41898
42336
  gipity generate image "a cat wearing a top hat"
42337
+ gipity generate image "make it night time" --input photo.jpg -o edited.png
42338
+ gipity generate image "put the person in the first photo into the second scene" --input person.png --input scene.png
41899
42339
  gipity generate image "landscape sunset" --provider gemini --aspect-ratio 16:9 --image-size 2K
41900
42340
  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) => {
42341
+ 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
42342
  try {
41903
42343
  const { config } = await resolveProjectContext();
41904
42344
  if (opts.output) ensureOutputDir(opts.output);
42345
+ const inputImages = opts.input ? readInputImages(opts.input) : void 0;
41905
42346
  const doGenerate = () => post(`/projects/${config.projectGuid}/generate/image`, {
41906
42347
  prompt: prompt2,
41907
42348
  provider: opts.provider,
@@ -41910,12 +42351,14 @@ Examples:
41910
42351
  quality: opts.quality,
41911
42352
  aspect_ratio: opts.aspectRatio,
41912
42353
  image_size: opts.imageSize,
41913
- seed: Number.isFinite(opts.seed) ? opts.seed : void 0
42354
+ seed: Number.isFinite(opts.seed) ? opts.seed : void 0,
42355
+ input_images: inputImages
41914
42356
  });
41915
- const result = opts.json ? await doGenerate() : await withSpinner("Generating image\u2026", doGenerate, { done: null });
42357
+ const verb = inputImages ? "Editing image..." : "Generating image...";
42358
+ const result = opts.json ? await doGenerate() : await withSpinner(verb, doGenerate, { done: null });
41916
42359
  const ext = result.content_type.includes("png") ? "png" : "jpg";
41917
42360
  const filename = opts.output || `generated.${ext}`;
41918
- const savedPath = await downloadFile(result.url, filename);
42361
+ const savedPath = await downloadFile(result.url, filename, !!opts.output);
41919
42362
  if (opts.json) {
41920
42363
  console.log(JSON.stringify({ ...result, saved: savedPath }));
41921
42364
  } else {
@@ -41957,9 +42400,9 @@ Examples:
41957
42400
  aspect_ratio: opts.aspect,
41958
42401
  resolution: opts.resolution
41959
42402
  });
41960
- const result = opts.json ? await doGenerate() : await withSpinner("Generating video\u2026", doGenerate, { done: null });
42403
+ const result = opts.json ? await doGenerate() : await withSpinner("Generating video...", doGenerate, { done: null });
41961
42404
  const filename = opts.output || "generated.mp4";
41962
- const savedPath = await downloadFile(result.url, filename);
42405
+ const savedPath = await downloadFile(result.url, filename, !!opts.output);
41963
42406
  if (opts.json) {
41964
42407
  console.log(JSON.stringify({ ...result, saved: savedPath }));
41965
42408
  } else {
@@ -42005,9 +42448,9 @@ Examples:
42005
42448
  language: opts.language,
42006
42449
  speakers
42007
42450
  });
42008
- const result = opts.json ? await doGenerate() : await withSpinner("Generating speech\u2026", doGenerate, { done: null });
42451
+ const result = opts.json ? await doGenerate() : await withSpinner("Generating speech...", doGenerate, { done: null });
42009
42452
  const filename = opts.output || "speech.mp3";
42010
- const savedPath = await downloadFile(result.url, filename);
42453
+ const savedPath = await downloadFile(result.url, filename, !!opts.output);
42011
42454
  if (opts.json) {
42012
42455
  console.log(JSON.stringify({ ...result, saved: savedPath }));
42013
42456
  } else {
@@ -42044,9 +42487,9 @@ Examples:
42044
42487
  model: opts.model,
42045
42488
  instrumental: !opts.vocals
42046
42489
  });
42047
- const result = opts.json ? await doGenerate() : await withSpinner("Generating music\u2026", doGenerate, { done: null });
42490
+ const result = opts.json ? await doGenerate() : await withSpinner("Generating music...", doGenerate, { done: null });
42048
42491
  const filename = opts.output || "music.mp3";
42049
- const savedPath = await downloadFile(result.url, filename);
42492
+ const savedPath = await downloadFile(result.url, filename, !!opts.output);
42050
42493
  if (opts.json) {
42051
42494
  console.log(JSON.stringify({ ...result, saved: savedPath }));
42052
42495
  } else {
@@ -42082,9 +42525,9 @@ Examples:
42082
42525
  duration_seconds: opts.duration,
42083
42526
  prompt_influence: opts.influence
42084
42527
  });
42085
- const result = opts.json ? await doGenerate() : await withSpinner("Generating sound effect\u2026", doGenerate, { done: null });
42528
+ const result = opts.json ? await doGenerate() : await withSpinner("Generating sound effect...", doGenerate, { done: null });
42086
42529
  const filename = opts.output || "sound.mp3";
42087
- const savedPath = await downloadFile(result.url, filename);
42530
+ const savedPath = await downloadFile(result.url, filename, !!opts.output);
42088
42531
  if (opts.json) {
42089
42532
  console.log(JSON.stringify({ ...result, saved: savedPath }));
42090
42533
  } else {
@@ -42103,14 +42546,14 @@ var generateCommand = new Command("generate").description("Generate images, vide
42103
42546
  init_api();
42104
42547
  init_config();
42105
42548
  init_colors();
42106
- import { existsSync as existsSync15, readFileSync as readFileSync18 } from "fs";
42549
+ import { existsSync as existsSync15, readFileSync as readFileSync20 } from "fs";
42107
42550
  import { join as join18 } from "path";
42108
42551
  function readInstalledKitReadme(name) {
42109
42552
  const root = getProjectRoot() ?? process.cwd();
42110
42553
  const kitDir = join18(root, "src", "packages", name);
42111
42554
  if (!existsSync15(join18(kitDir, "package.json"))) return null;
42112
42555
  const readme = join18(kitDir, "README.md");
42113
- return existsSync15(readme) ? readFileSync18(readme, "utf-8") : null;
42556
+ return existsSync15(readme) ? readFileSync20(readme, "utf-8") : null;
42114
42557
  }
42115
42558
  var skillCommand = new Command("skill").description("Task docs - read the matching skill before building");
42116
42559
  skillCommand.command("list").description("List skills").option("--json", "Output as JSON").action((opts) => run("List", async () => {
@@ -42284,14 +42727,14 @@ var domainCommand = new Command("domain").description("Manage custom domains").a
42284
42727
 
42285
42728
  // src/commands/realtime.ts
42286
42729
  import { existsSync as existsSync16 } from "fs";
42287
- import { dirname as dirname12, resolve as resolve15 } from "path";
42730
+ import { dirname as dirname12, resolve as resolve14 } from "path";
42288
42731
  init_api();
42289
42732
  init_config();
42290
42733
  init_colors();
42291
42734
  function hasDeployManifest() {
42292
42735
  const cfgPath = getConfigPath();
42293
42736
  if (!cfgPath) return false;
42294
- return existsSync16(resolve15(dirname12(cfgPath), "gipity.yaml"));
42737
+ return existsSync16(resolve14(dirname12(cfgPath), "gipity.yaml"));
42295
42738
  }
42296
42739
  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
42740
  const config = requireConfig();
@@ -42391,6 +42834,16 @@ function printSkippedCandidates(skipped) {
42391
42834
  if (skipped.length > 10) console.log(muted(` ... and ${skipped.length - 10} more`));
42392
42835
  console.log(muted('Run frontend/kit module tests in the sandbox: gipity sandbox run bash "node <file>"'));
42393
42836
  }
42837
+ function undeployedFunctionsFromFailures(results) {
42838
+ const names = /* @__PURE__ */ new Set();
42839
+ for (const r of results) {
42840
+ if (r.status !== "failed" || !r.error) continue;
42841
+ if (!/404\b/.test(r.error) || !/not\s+found/i.test(r.error)) continue;
42842
+ const m = r.error.match(/Function ['"]?([\w-]+)['"]? (?:not found|failed)/i);
42843
+ if (m) names.add(m[1]);
42844
+ }
42845
+ return [...names];
42846
+ }
42394
42847
  var LONG_RUN_MS = 6e4;
42395
42848
  async function pollTestStatus(projectGuid, runGuid, opts) {
42396
42849
  const startTime = Date.now();
@@ -42459,7 +42912,7 @@ async function pollTestStatus(projectGuid, runGuid, opts) {
42459
42912
  const elapsed = Math.round((now - startTime) / 1e3);
42460
42913
  const progress = data.totalFiles === 0 ? "starting up" : `${data.completedFiles}/${data.totalFiles} files`;
42461
42914
  const tally = data.passed + data.failed > 0 ? ` (${data.passed} passed${data.failed > 0 ? `, ${data.failed} failed` : ""} so far)` : "";
42462
- console.log(muted(` \u2026 still running \u2014 ${progress}${tally}, ${elapsed}s elapsed`));
42915
+ console.log(muted(` ... still running - ${progress}${tally}, ${elapsed}s elapsed`));
42463
42916
  if (now - startTime >= LONG_RUN_MS && !longRunHintShown) {
42464
42917
  longRunHintShown = true;
42465
42918
  console.log(muted(" Note: progressing, not hung. LLM-backed tests can take minutes. To verify one function fast, use `gipity fn call <name>`; narrow this suite with `gipity test <path>`."));
@@ -42467,7 +42920,7 @@ async function pollTestStatus(projectGuid, runGuid, opts) {
42467
42920
  }
42468
42921
  }
42469
42922
  }
42470
- await new Promise((resolve20) => setTimeout(resolve20, getPollInterval()));
42923
+ await new Promise((resolve19) => setTimeout(resolve19, getPollInterval()));
42471
42924
  }
42472
42925
  }
42473
42926
  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 +42985,15 @@ var testCommand = new Command("test").description("Run tests").argument("[path]"
42532
42985
  }
42533
42986
  console.log("");
42534
42987
  }
42988
+ const undeployed = undeployedFunctionsFromFailures(data.results);
42989
+ if (undeployed.length > 0) {
42990
+ const list = undeployed.map((n) => `'${n}'`).join(", ");
42991
+ console.log(warning(
42992
+ `${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.`
42993
+ ));
42994
+ console.log(muted("Deploy first, then re-run: gipity deploy dev && gipity test"));
42995
+ console.log("");
42996
+ }
42535
42997
  const parts = [];
42536
42998
  if (data.passed > 0) parts.push(success(`${data.passed} passed`));
42537
42999
  if (data.failed > 0) parts.push(error(`${data.failed} failed`));
@@ -42681,12 +43143,12 @@ var locationCommand = new Command("location").description("Show location").argum
42681
43143
  }));
42682
43144
 
42683
43145
  // 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";
43146
+ import { existsSync as existsSync18, readFileSync as readFileSync22, statSync as statSync9 } from "fs";
43147
+ import { join as join20, resolve as resolve15 } from "path";
42686
43148
  import { homedir as homedir14 } from "os";
42687
43149
 
42688
43150
  // src/updater/state.ts
42689
- import { readFileSync as readFileSync19, writeFileSync as writeFileSync14, mkdirSync as mkdirSync14, existsSync as existsSync17 } from "fs";
43151
+ import { readFileSync as readFileSync21, writeFileSync as writeFileSync14, mkdirSync as mkdirSync14, existsSync as existsSync17 } from "fs";
42690
43152
  import { join as join19 } from "path";
42691
43153
  import { homedir as homedir13 } from "os";
42692
43154
  var GIPITY_DIR = join19(homedir13(), ".gipity");
@@ -42711,7 +43173,7 @@ function ensureDir() {
42711
43173
  function readState() {
42712
43174
  if (!existsSync17(STATE_FILE)) return { ...DEFAULT_STATE };
42713
43175
  try {
42714
- return { ...DEFAULT_STATE, ...JSON.parse(readFileSync19(STATE_FILE, "utf-8")) };
43176
+ return { ...DEFAULT_STATE, ...JSON.parse(readFileSync21(STATE_FILE, "utf-8")) };
42715
43177
  } catch {
42716
43178
  return { ...DEFAULT_STATE };
42717
43179
  }
@@ -42723,7 +43185,7 @@ function writeState(state) {
42723
43185
  function readSettings() {
42724
43186
  if (!existsSync17(SETTINGS_FILE)) return { ...DEFAULT_SETTINGS };
42725
43187
  try {
42726
- return { ...DEFAULT_SETTINGS, ...JSON.parse(readFileSync19(SETTINGS_FILE, "utf-8")) };
43188
+ return { ...DEFAULT_SETTINGS, ...JSON.parse(readFileSync21(SETTINGS_FILE, "utf-8")) };
42727
43189
  } catch {
42728
43190
  return { ...DEFAULT_SETTINGS };
42729
43191
  }
@@ -42759,7 +43221,7 @@ function localVersion() {
42759
43221
  const pkgPath = join20(LOCAL_PKG_DIR, "package.json");
42760
43222
  if (!existsSync18(pkgPath)) return null;
42761
43223
  try {
42762
- return JSON.parse(readFileSync20(pkgPath, "utf-8")).version ?? null;
43224
+ return JSON.parse(readFileSync22(pkgPath, "utf-8")).version ?? null;
42763
43225
  } catch {
42764
43226
  return null;
42765
43227
  }
@@ -42767,7 +43229,7 @@ function localVersion() {
42767
43229
  function shimVersion() {
42768
43230
  try {
42769
43231
  const url = new URL("../../package.json", import.meta.url);
42770
- return JSON.parse(readFileSync20(url, "utf-8")).version;
43232
+ return JSON.parse(readFileSync22(url, "utf-8")).version;
42771
43233
  } catch {
42772
43234
  return "unknown";
42773
43235
  }
@@ -42850,7 +43312,7 @@ var doctorCommand = new Command("doctor").description("Check install + environme
42850
43312
  console.log(`${muted("claude code ")} installed ${yn(env.claude.installed)} \xB7 authenticated ${yn(env.claude.authenticated)}`);
42851
43313
  const autostartLabel = env.relay.autostart === null ? muted("n/a") : yn(env.relay.autostart);
42852
43314
  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"))) {
43315
+ if (existsSync18(resolve15(process.cwd(), ".codex", "hooks.json"))) {
42854
43316
  console.log(`${muted("codex hooks ")} ${warning("written")} - session capture needs a one-time approval: run /hooks inside Codex in this project`);
42855
43317
  }
42856
43318
  console.log(`${muted("ready ")} ${env.ready ? success("yes") : warning("no - run `gipity build` (or the desktop app) to finish setup")}`);
@@ -42870,14 +43332,68 @@ var doctorCommand = new Command("doctor").description("Check install + environme
42870
43332
  console.log(`${muted("last check ")} ${rel(state.lastCheckAt)}`);
42871
43333
  console.log(`${muted("last error ")} ${state.lastError ? error(state.lastError) : dim("none")}`);
42872
43334
  console.log(`${muted("state file ")} ${existsSync18(STATE_FILE) ? STATE_FILE : dim("(none yet)")}`);
42873
- console.log(`${muted("update log ")} ${existsSync18(UPDATE_LOG) ? `${UPDATE_LOG} (${statSync8(UPDATE_LOG).size} bytes)` : dim("(none yet)")}`);
43335
+ console.log(`${muted("update log ")} ${existsSync18(UPDATE_LOG) ? `${UPDATE_LOG} (${statSync9(UPDATE_LOG).size} bytes)` : dim("(none yet)")}`);
42874
43336
  console.log("");
42875
43337
  console.log(dim("Force an update with: gipity update"));
42876
43338
  });
42877
43339
 
42878
43340
  // src/updater/check.ts
42879
- import { appendFileSync, existsSync as existsSync19 } from "fs";
43341
+ import { appendFileSync } from "fs";
43342
+
43343
+ // src/updater/install.ts
43344
+ import { existsSync as existsSync19, mkdirSync as mkdirSync15, rmSync as rmSync2, statSync as statSync10, unlinkSync as unlinkSync5, writeFileSync as writeFileSync15 } from "fs";
43345
+ import { dirname as dirname13, join as join21 } from "path";
42880
43346
  init_platform();
43347
+ var UPDATE_LOCK = join21(GIPITY_DIR, "update.lock");
43348
+ var LOCK_STALE_MS2 = 10 * 60 * 1e3;
43349
+ function npmInstallGipity(version) {
43350
+ const res = spawnSyncCommand(resolveCommand("npm"), ["install", "--no-audit", "--no-fund", "--ignore-scripts", `gipity@${version}`], {
43351
+ cwd: LOCAL_DIR,
43352
+ stdio: ["ignore", "ignore", "pipe"],
43353
+ encoding: "utf-8"
43354
+ });
43355
+ return {
43356
+ ok: res.status === 0 && existsSync19(LOCAL_ENTRY),
43357
+ status: res.status,
43358
+ stderr: (res.stderr || "").toString(),
43359
+ spawnError: res.error
43360
+ };
43361
+ }
43362
+ var WEDGE_CODES = /\b(ENOTEMPTY|EEXIST|ENOTDIR|EISDIR|EJSONPARSE)\b/;
43363
+ function isWedged(stderr) {
43364
+ return WEDGE_CODES.test(stderr);
43365
+ }
43366
+ function resetLocalTree(dir = LOCAL_DIR) {
43367
+ mkdirSync15(dir, { recursive: true });
43368
+ rmSync2(join21(dir, "node_modules"), { recursive: true, force: true });
43369
+ rmSync2(join21(dir, "package-lock.json"), { force: true });
43370
+ writeFileSync15(join21(dir, "package.json"), JSON.stringify({ name: "gipity-local", private: true, version: "0.0.0" }, null, 2));
43371
+ }
43372
+ function acquireUpdateLock(lockPath2 = UPDATE_LOCK) {
43373
+ mkdirSync15(dirname13(lockPath2), { recursive: true });
43374
+ try {
43375
+ writeFileSync15(lockPath2, String(process.pid), { flag: "wx" });
43376
+ return true;
43377
+ } catch {
43378
+ try {
43379
+ if (Date.now() - statSync10(lockPath2).mtimeMs > LOCK_STALE_MS2) {
43380
+ unlinkSync5(lockPath2);
43381
+ writeFileSync15(lockPath2, String(process.pid), { flag: "wx" });
43382
+ return true;
43383
+ }
43384
+ } catch {
43385
+ }
43386
+ return false;
43387
+ }
43388
+ }
43389
+ function releaseUpdateLock(lockPath2 = UPDATE_LOCK) {
43390
+ try {
43391
+ unlinkSync5(lockPath2);
43392
+ } catch {
43393
+ }
43394
+ }
43395
+
43396
+ // src/updater/check.ts
42881
43397
  var CHECK_INTERVAL_MS = 4 * 60 * 60 * 1e3;
42882
43398
  function log(line) {
42883
43399
  try {
@@ -42906,12 +43422,24 @@ async function fetchLatestVersion() {
42906
43422
  return json.version;
42907
43423
  }
42908
43424
  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);
43425
+ let res = npmInstallGipity(version);
43426
+ if (!res.ok && !res.spawnError && isWedged(res.stderr)) {
43427
+ log(`wedged install tree detected; wiping node_modules and retrying:
43428
+ ${res.stderr.trim().slice(-2e3)}`);
43429
+ resetLocalTree();
43430
+ res = npmInstallGipity(version);
43431
+ if (res.ok) log("clean reinstall succeeded");
43432
+ }
43433
+ if (res.ok) return { ok: true };
43434
+ if (res.spawnError) {
43435
+ log(`npm spawn failed: ${res.spawnError.message}`);
43436
+ return { ok: false, detail: res.spawnError.message };
43437
+ }
43438
+ if (res.stderr.trim()) log(`npm stderr (exit ${res.status}):
43439
+ ${res.stderr.trim().slice(-2e3)}`);
43440
+ if (res.status === 0) return { ok: false, detail: "npm succeeded but the installed package is missing dist/index.js" };
43441
+ const firstLine = res.stderr.split("\n").map((l7) => l7.trim()).find((l7) => l7.length > 0) || `npm exit ${res.status}`;
43442
+ return { ok: false, detail: firstLine.length > 160 ? firstLine.slice(0, 157) + "..." : firstLine };
42915
43443
  }
42916
43444
  async function runCheck(opts = {}) {
42917
43445
  const state = readState();
@@ -42944,17 +43472,27 @@ async function runCheck(opts = {}) {
42944
43472
  log(`up-to-date (current=${current}, latest=${latest})`);
42945
43473
  return { updated: false, reason: "up-to-date" };
42946
43474
  }
43475
+ if (!acquireUpdateLock()) {
43476
+ log("skipped: another update is already in progress");
43477
+ return { updated: false, reason: "another update is already in progress" };
43478
+ }
42947
43479
  log(`upgrading ${current} \u2192 ${latest}`);
42948
- const ok = installVersion(latest);
42949
43480
  state.lastCheckAt = Date.now();
42950
- if (ok) {
43481
+ writeState(state);
43482
+ let install;
43483
+ try {
43484
+ install = installVersion(latest);
43485
+ } finally {
43486
+ releaseUpdateLock();
43487
+ }
43488
+ if (install.ok) {
42951
43489
  state.installedVersion = latest;
42952
43490
  state.lastError = null;
42953
43491
  writeState(state);
42954
43492
  log(`upgraded to ${latest}`);
42955
43493
  return { updated: true, from: current, to: latest };
42956
43494
  }
42957
- state.lastError = `npm install gipity@${latest} failed`;
43495
+ state.lastError = `npm install gipity@${latest} failed` + (install.detail ? `: ${install.detail}` : "");
42958
43496
  writeState(state);
42959
43497
  log(state.lastError);
42960
43498
  return { updated: false, reason: state.lastError };
@@ -42978,6 +43516,10 @@ var updateCommand = new Command("update").description("Update the CLI").action(a
42978
43516
  console.log(success("Already on the latest version."));
42979
43517
  } else {
42980
43518
  console.log(warning(`No update applied: ${result.reason}`));
43519
+ if (result.reason?.startsWith("npm install")) {
43520
+ console.log(dim("Full npm output: ~/.gipity/update.log"));
43521
+ console.log(dim("If it keeps failing, delete ~/.gipity/local and run any gipity command to reinstall."));
43522
+ }
42981
43523
  }
42982
43524
  });
42983
43525
 
@@ -42985,17 +43527,17 @@ var updateCommand = new Command("update").description("Update the CLI").action(a
42985
43527
  init_api();
42986
43528
  init_utils();
42987
43529
  init_colors();
42988
- import { existsSync as existsSync22, readFileSync as readFileSync23, unlinkSync as unlinkSync7 } from "fs";
43530
+ import { existsSync as existsSync22, readFileSync as readFileSync25, unlinkSync as unlinkSync8 } from "fs";
42989
43531
  import { spawn as spawn2 } from "child_process";
42990
43532
 
42991
43533
  // src/relay/daemon.ts
42992
43534
  init_platform();
42993
43535
  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";
43536
+ 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
43537
  import { stat, readFile } from "fs/promises";
42996
43538
  import { createInterface as createInterface2 } from "readline";
42997
43539
  import { homedir as homedir16, hostname as hostname5, platform as osPlatform3, loadavg as loadavg2, freemem as freemem2, totalmem as totalmem2, cpus as cpus2 } from "os";
42998
- import { join as join23 } from "path";
43540
+ import { join as join24 } from "path";
42999
43541
  init_auth();
43000
43542
  init_api();
43001
43543
 
@@ -43752,20 +44294,20 @@ init_platform();
43752
44294
  init_client_context();
43753
44295
  import { cpus, freemem, totalmem, loadavg, release, uptime, homedir as homedir15 } from "os";
43754
44296
  import { statfsSync, readdirSync as readdirSync9 } from "fs";
43755
- import { join as join21 } from "path";
44297
+ import { join as join22 } from "path";
43756
44298
  var PROBE_TIMEOUT_MS = 4e3;
43757
44299
  function parseVersion(out) {
43758
44300
  const m = out.match(/\d+\.\d+(?:\.\d+)?(?:[-.\w]*)?/);
43759
44301
  return m ? m[0] : void 0;
43760
44302
  }
43761
44303
  function runProbe(cmd, args) {
43762
- return new Promise((resolve20) => {
44304
+ return new Promise((resolve19) => {
43763
44305
  let out = "";
43764
44306
  let settled = false;
43765
44307
  const done = (s) => {
43766
44308
  if (!settled) {
43767
44309
  settled = true;
43768
- resolve20(s);
44310
+ resolve19(s);
43769
44311
  }
43770
44312
  };
43771
44313
  let child;
@@ -43819,7 +44361,7 @@ async function detectGpu() {
43819
44361
  function diskUsage() {
43820
44362
  try {
43821
44363
  if (typeof statfsSync !== "function") return void 0;
43822
- const st2 = statfsSync(join21(homedir15(), "GipityProjects"), { bigint: false });
44364
+ const st2 = statfsSync(join22(homedir15(), "GipityProjects"), { bigint: false });
43823
44365
  if (!st2 || !st2.bsize) return void 0;
43824
44366
  return { total: st2.bsize * st2.blocks, free: st2.bsize * st2.bavail };
43825
44367
  } catch {
@@ -43833,7 +44375,7 @@ function diskUsage() {
43833
44375
  }
43834
44376
  function localProjectCount() {
43835
44377
  try {
43836
- return readdirSync9(join21(homedir15(), "GipityProjects"), { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).length;
44378
+ return readdirSync9(join22(homedir15(), "GipityProjects"), { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).length;
43837
44379
  } catch {
43838
44380
  return void 0;
43839
44381
  }
@@ -43964,8 +44506,8 @@ var SessionPool = class {
43964
44506
  const done = new Promise((r) => {
43965
44507
  markDone = r;
43966
44508
  });
43967
- return new Promise((resolve20, reject) => {
43968
- const turn = { onMessage: p.onMessage, resolve: resolve20, reject, interruptRequested: false, wasHot, done, markDone };
44509
+ return new Promise((resolve19, reject) => {
44510
+ const turn = { onMessage: p.onMessage, resolve: resolve19, reject, interruptRequested: false, wasHot, done, markDone };
43969
44511
  session.current = turn;
43970
44512
  session.state = "running";
43971
44513
  session.lastActivityAt = Date.now();
@@ -44142,7 +44684,7 @@ var SessionPool = class {
44142
44684
  // src/relay/daemon.ts
44143
44685
  init_config();
44144
44686
  init_api();
44145
- var RELAY_LOG_PATH = join23(homedir16(), ".gipity", "relay.log");
44687
+ var RELAY_LOG_PATH = join24(homedir16(), ".gipity", "relay.log");
44146
44688
  var HEARTBEAT_INTERVAL_MS = parseInt(process.env.GIPITY_RELAY_HEARTBEAT_MS || "60000", 10);
44147
44689
  var DIAGNOSTICS_INTERVAL_MS = parseInt(process.env.GIPITY_RELAY_DIAGNOSTICS_MS || String(24 * 60 * 60 * 1e3), 10);
44148
44690
  var LONG_POLL_TIMEOUT_MS = parseInt(process.env.GIPITY_RELAY_POLL_TIMEOUT_MS || "35000", 10);
@@ -44259,8 +44801,8 @@ function log2(level, msg, extra) {
44259
44801
  const pretty = `${C7.dim(hhmmss())} ${badge(level)} ${C7.bold(msg)}${formatExtra(extra)}`;
44260
44802
  process.stderr.write(pretty + "\n");
44261
44803
  try {
44262
- const dir = join23(homedir16(), ".gipity");
44263
- mkdirSync16(dir, { recursive: true });
44804
+ const dir = join24(homedir16(), ".gipity");
44805
+ mkdirSync17(dir, { recursive: true });
44264
44806
  lockLogPerms(dir, RELAY_LOG_PATH);
44265
44807
  const json = JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level, msg, ...extra ?? {} });
44266
44808
  appendFileSync3(RELAY_LOG_PATH, json + "\n");
@@ -44384,10 +44926,10 @@ async function heartbeatLoop(ctx) {
44384
44926
  }
44385
44927
  await Promise.race([
44386
44928
  sleep4(HEARTBEAT_INTERVAL_MS, ctx.abort.signal),
44387
- new Promise((resolve20) => {
44929
+ new Promise((resolve19) => {
44388
44930
  heartbeatPoke = () => {
44389
44931
  heartbeatPoke = null;
44390
- resolve20();
44932
+ resolve19();
44391
44933
  };
44392
44934
  })
44393
44935
  ]);
@@ -44431,9 +44973,9 @@ async function dispatchLoop(ctx, opts) {
44431
44973
  while (inflight.size >= MAX_CONCURRENT_DISPATCHES && !ctx.abort.signal.aborted) {
44432
44974
  await Promise.race([
44433
44975
  ...inflight,
44434
- new Promise((resolve20) => {
44435
- if (ctx.abort.signal.aborted) return resolve20();
44436
- ctx.abort.signal.addEventListener("abort", () => resolve20(), { once: true });
44976
+ new Promise((resolve19) => {
44977
+ if (ctx.abort.signal.aborted) return resolve19();
44978
+ ctx.abort.signal.addEventListener("abort", () => resolve19(), { once: true });
44437
44979
  })
44438
44980
  ]);
44439
44981
  }
@@ -44660,7 +45202,7 @@ function isSafeSessionId(s) {
44660
45202
  function transcriptPathFor(cwd, sessionId) {
44661
45203
  if (!isSafeSessionId(sessionId)) return null;
44662
45204
  const slug = cwd.replace(/[^a-zA-Z0-9]/g, "-");
44663
- return join23(homedir16(), ".claude", "projects", slug, `${sessionId}.jsonl`);
45205
+ return join24(homedir16(), ".claude", "projects", slug, `${sessionId}.jsonl`);
44664
45206
  }
44665
45207
  function formatDuration(ms2) {
44666
45208
  const totalSec = ms2 / 1e3;
@@ -45114,11 +45656,11 @@ async function resolveCwdForProject(d) {
45114
45656
  throw new Error(`Invalid project slug: ${JSON.stringify(d.project_slug)}`);
45115
45657
  }
45116
45658
  const root = getProjectsRoot();
45117
- const path5 = join23(root, d.project_slug);
45118
- const configPath = join23(path5, ".gipity.json");
45659
+ const path5 = join24(root, d.project_slug);
45660
+ const configPath = join24(path5, ".gipity.json");
45119
45661
  if (existsSync21(configPath)) {
45120
45662
  try {
45121
- const cfg = JSON.parse(readFileSync22(configPath, "utf-8"));
45663
+ const cfg = JSON.parse(readFileSync24(configPath, "utf-8"));
45122
45664
  if (cfg.projectGuid === d.project_guid) return { cwd: path5, bootstrapped: false };
45123
45665
  log2("warn", "project dir exists but guid mismatch - using it anyway", {
45124
45666
  path: path5,
@@ -45130,9 +45672,9 @@ async function resolveCwdForProject(d) {
45130
45672
  }
45131
45673
  }
45132
45674
  log2("info", "bootstrapping new project dir", { slug: d.project_slug, path: path5 });
45133
- mkdirSync16(path5, { recursive: true });
45675
+ mkdirSync17(path5, { recursive: true });
45134
45676
  const apiBase2 = getApiBaseOverride() || DEFAULT_API_BASE;
45135
- writeFileSync15(configPath, JSON.stringify({
45677
+ writeFileSync16(configPath, JSON.stringify({
45136
45678
  projectGuid: d.project_guid,
45137
45679
  projectSlug: d.project_slug,
45138
45680
  accountSlug: d.account_slug,
@@ -45209,7 +45751,7 @@ async function killRunningForConv(convGuid) {
45209
45751
  }
45210
45752
  }
45211
45753
  const graceTimers = [];
45212
- const escalate = new Promise((resolve20) => {
45754
+ const escalate = new Promise((resolve19) => {
45213
45755
  const t = setTimeout(() => {
45214
45756
  for (const e of matches) {
45215
45757
  log2("warn", "previous dispatch ignored SIGTERM - escalating to SIGKILL", { conv: convGuid });
@@ -45218,7 +45760,7 @@ async function killRunningForConv(convGuid) {
45218
45760
  } catch {
45219
45761
  }
45220
45762
  }
45221
- resolve20();
45763
+ resolve19();
45222
45764
  }, KILL_GRACE_MS);
45223
45765
  graceTimers.push(t);
45224
45766
  });
@@ -45229,7 +45771,7 @@ async function killRunningForConv(convGuid) {
45229
45771
  }
45230
45772
  async function spawnSync2(cwd, timeoutMs, onSpawn) {
45231
45773
  const cmd = process.env.GIPITY_RELAY_CLAUDE_CMD || resolveCommand("gipity");
45232
- return new Promise((resolve20, reject) => {
45774
+ return new Promise((resolve19, reject) => {
45233
45775
  const child = spawnCommand(cmd, ["sync", "--json"], {
45234
45776
  cwd,
45235
45777
  env: childEnv(),
@@ -45253,7 +45795,7 @@ async function spawnSync2(cwd, timeoutMs, onSpawn) {
45253
45795
  } catch {
45254
45796
  }
45255
45797
  try {
45256
- unlinkSync6(join23(cwd, ".gipity", "sync.lock"));
45798
+ unlinkSync7(join24(cwd, ".gipity", "sync.lock"));
45257
45799
  } catch {
45258
45800
  }
45259
45801
  finish(() => reject(new Error(`timed out after ${Math.round(timeoutMs / 1e3)}s`)));
@@ -45269,7 +45811,7 @@ async function spawnSync2(cwd, timeoutMs, onSpawn) {
45269
45811
  child.on("exit", (code) => finish(() => {
45270
45812
  if (code === 0) {
45271
45813
  log2("info", "sync done", { cwd, stdoutLen });
45272
- resolve20();
45814
+ resolve19();
45273
45815
  } else {
45274
45816
  reject(new Error(`gipity sync exited ${code}${stderrBuf ? `: ${stderrBuf.trim().slice(0, 300)}` : ""}`));
45275
45817
  }
@@ -45281,7 +45823,7 @@ async function spawnGipityClaude(args, cwd, d, queue, meta) {
45281
45823
  const cmd = process.env.GIPITY_RELAY_CLAUDE_CMD || resolveCommand("gipity");
45282
45824
  const fullArgs = streamJson ? [...args, "--output-format", "stream-json", "--verbose", "--include-partial-messages"] : [...args];
45283
45825
  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) => {
45826
+ return new Promise((resolve19, reject) => {
45285
45827
  const child = spawnCommand(cmd, fullArgs, { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
45286
45828
  let resolveExited = () => {
45287
45829
  };
@@ -45461,7 +46003,7 @@ async function spawnGipityClaude(args, cwd, d, queue, meta) {
45461
46003
  void postProgress(d.conversation_guid, buildProgressPayload(false));
45462
46004
  if (ownQueue) await q.close();
45463
46005
  cleanup();
45464
- resolve20({
46006
+ resolve19({
45465
46007
  exitCode: code ?? 1,
45466
46008
  killed,
45467
46009
  runtimeLimit,
@@ -45475,10 +46017,10 @@ async function runDispatchSync(d, cwd) {
45475
46017
  let killed = false;
45476
46018
  try {
45477
46019
  await spawnSync2(cwd, PROJECT_SYNC_TIMEOUT_MS, (child) => {
45478
- const exited = new Promise((resolve20) => {
46020
+ const exited = new Promise((resolve19) => {
45479
46021
  child.once("exit", (_code, signal) => {
45480
46022
  if (signal === "SIGTERM") killed = true;
45481
- resolve20();
46023
+ resolve19();
45482
46024
  });
45483
46025
  });
45484
46026
  running.set(d.short_guid, { child, convGuid: d.conversation_guid, exited });
@@ -45509,18 +46051,18 @@ function killDispatch(shortGuid) {
45509
46051
  return false;
45510
46052
  }
45511
46053
  function sleep4(ms2, signal) {
45512
- return new Promise((resolve20) => {
45513
- if (signal?.aborted) return resolve20();
46054
+ return new Promise((resolve19) => {
46055
+ if (signal?.aborted) return resolve19();
45514
46056
  let onAbort = null;
45515
46057
  const t = setTimeout(() => {
45516
46058
  if (onAbort && signal) signal.removeEventListener("abort", onAbort);
45517
- resolve20();
46059
+ resolve19();
45518
46060
  }, ms2);
45519
46061
  if (signal) {
45520
46062
  onAbort = () => {
45521
46063
  clearTimeout(t);
45522
46064
  signal.removeEventListener("abort", onAbort);
45523
- resolve20();
46065
+ resolve19();
45524
46066
  };
45525
46067
  signal.addEventListener("abort", onAbort, { once: true });
45526
46068
  }
@@ -45694,7 +46236,7 @@ relayCommand.command("stop").description("Stop the background service").option("
45694
46236
  console.log(muted("Background service isn't running."));
45695
46237
  return;
45696
46238
  }
45697
- const pid = parseInt(readFileSync23(pidPath, "utf-8").trim(), 10);
46239
+ const pid = parseInt(readFileSync25(pidPath, "utf-8").trim(), 10);
45698
46240
  if (!pid || isNaN(pid)) {
45699
46241
  console.error(error("PID file is empty or malformed."));
45700
46242
  process.exit(1);
@@ -45705,7 +46247,7 @@ relayCommand.command("stop").description("Stop the background service").option("
45705
46247
  if (err?.code === "ESRCH") {
45706
46248
  console.log(muted(`PID ${pid} not running - cleaning up stale PID file.`));
45707
46249
  try {
45708
- unlinkSync7(pidPath);
46250
+ unlinkSync8(pidPath);
45709
46251
  } catch {
45710
46252
  }
45711
46253
  return;
@@ -45819,7 +46361,7 @@ relayCommand.command("revoke").description("Revoke and forget this device").acti
45819
46361
  }
45820
46362
  const pidPath = getDaemonPidPath();
45821
46363
  if (existsSync22(pidPath)) {
45822
- const pid = parseInt(readFileSync23(pidPath, "utf-8").trim(), 10);
46364
+ const pid = parseInt(readFileSync25(pidPath, "utf-8").trim(), 10);
45823
46365
  if (pid && !isNaN(pid)) {
45824
46366
  try {
45825
46367
  process.kill(pid, "SIGTERM");
@@ -45838,7 +46380,7 @@ relayCommand.command("log").description("Tail the service log").option("-n, --li
45838
46380
  }
45839
46381
  const lines = parseInt(opts.lines, 10) || 100;
45840
46382
  try {
45841
- const all = readFileSync23(path5, "utf-8").split("\n");
46383
+ const all = readFileSync25(path5, "utf-8").split("\n");
45842
46384
  const tail = all.slice(-lines - 1).join("\n");
45843
46385
  process.stdout.write(tail);
45844
46386
  } catch (err) {
@@ -45891,9 +46433,7 @@ var connectCommand = new Command("connect").alias("setup").description("Connect
45891
46433
  console.log("");
45892
46434
  const enabled = await runRelaySetup({ mode: "run-now" });
45893
46435
  if (enabled) {
45894
- const running2 = isRelayEnabled() && !isPaused();
45895
- console.log(` ${success("Done")} \u2014 your relay ${running2 ? "is running in the background" : "is set up"} and will start with your computer.`);
45896
- console.log(` ${muted("Open")} ${brand("gipity.ai")} ${muted("and start a chat to drive your coding agent here. Manage it with `gipity relay status`.")}`);
46436
+ console.log(` ${muted("Manage it anytime with `gipity relay status`.")}`);
45897
46437
  } else {
45898
46438
  console.log(` ${muted("No relay set up. Run `gipity connect` again anytime, or `gipity build` to start building.")}`);
45899
46439
  }
@@ -45911,15 +46451,15 @@ init_api();
45911
46451
  init_auth();
45912
46452
  init_utils();
45913
46453
  init_colors();
45914
- import { existsSync as existsSync23, rmSync as rmSync3, unlinkSync as unlinkSync8, readFileSync as readFileSync24, writeFileSync as writeFileSync16 } from "fs";
46454
+ import { existsSync as existsSync23, rmSync as rmSync4, unlinkSync as unlinkSync9, readFileSync as readFileSync26, writeFileSync as writeFileSync17 } from "fs";
45915
46455
  import { homedir as homedir17, platform as osPlatform4 } from "os";
45916
- import { join as join24, resolve as resolve18 } from "path";
46456
+ import { join as join25, resolve as resolve17 } from "path";
45917
46457
  function removeGipityPluginConfig() {
45918
- const settingsPath = join24(homedir17(), ".claude", "settings.json");
46458
+ const settingsPath = join25(homedir17(), ".claude", "settings.json");
45919
46459
  if (!existsSync23(settingsPath)) return false;
45920
46460
  let settings;
45921
46461
  try {
45922
- settings = JSON.parse(readFileSync24(settingsPath, "utf-8"));
46462
+ settings = JSON.parse(readFileSync26(settingsPath, "utf-8"));
45923
46463
  } catch {
45924
46464
  return false;
45925
46465
  }
@@ -45934,7 +46474,7 @@ function removeGipityPluginConfig() {
45934
46474
  if (Object.keys(settings.extraKnownMarketplaces).length === 0) delete settings.extraKnownMarketplaces;
45935
46475
  changed = true;
45936
46476
  }
45937
- if (changed) writeFileSync16(settingsPath, JSON.stringify(settings, null, 2) + "\n");
46477
+ if (changed) writeFileSync17(settingsPath, JSON.stringify(settings, null, 2) + "\n");
45938
46478
  return changed;
45939
46479
  }
45940
46480
  function removeInstallerPathLines() {
@@ -45942,11 +46482,11 @@ function removeInstallerPathLines() {
45942
46482
  const marker = "# Added by the Gipity installer";
45943
46483
  const touched = [];
45944
46484
  for (const name of [".bashrc", ".zshrc", ".profile"]) {
45945
- const rc2 = join24(homedir17(), name);
46485
+ const rc2 = join25(homedir17(), name);
45946
46486
  if (!existsSync23(rc2)) continue;
45947
46487
  let text;
45948
46488
  try {
45949
- text = readFileSync24(rc2, "utf-8");
46489
+ text = readFileSync26(rc2, "utf-8");
45950
46490
  } catch {
45951
46491
  continue;
45952
46492
  }
@@ -45956,7 +46496,7 @@ function removeInstallerPathLines() {
45956
46496
  );
45957
46497
  if (kept.length === lines.length) continue;
45958
46498
  try {
45959
- writeFileSync16(rc2, kept.join("\n"));
46499
+ writeFileSync17(rc2, kept.join("\n"));
45960
46500
  touched.push(rc2);
45961
46501
  } catch {
45962
46502
  }
@@ -45968,7 +46508,7 @@ function tildify2(p) {
45968
46508
  return p === home || p.startsWith(home + "/") ? "~" + p.slice(home.length) : p;
45969
46509
  }
45970
46510
  function resolveCliPath2() {
45971
- return resolve18(process.argv[1] ?? "gipity");
46511
+ return resolve17(process.argv[1] ?? "gipity");
45972
46512
  }
45973
46513
  async function stopDaemon() {
45974
46514
  if (!isDaemonRunning()) return;
@@ -46004,7 +46544,7 @@ function removeServiceUnit() {
46004
46544
  }
46005
46545
  if (existsSync23(plan2.path)) {
46006
46546
  try {
46007
- unlinkSync8(plan2.path);
46547
+ unlinkSync9(plan2.path);
46008
46548
  } catch {
46009
46549
  }
46010
46550
  }
@@ -46025,8 +46565,8 @@ async function revokeDeviceBestEffort() {
46025
46565
  }
46026
46566
  var uninstallCommand = new Command("uninstall").description("Uninstall Gipity").option("--yes", "Skip confirmation prompts").action(async (opts) => {
46027
46567
  const autoYes = opts.yes || getAutoConfirm();
46028
- const gipityDir = join24(homedir17(), ".gipity");
46029
- const launcherBin = join24(gipityDir, "launcher", "bin", "gipity");
46568
+ const gipityDir = join25(homedir17(), ".gipity");
46569
+ const launcherBin = join25(gipityDir, "launcher", "bin", "gipity");
46030
46570
  const installedViaLauncher = existsSync23(launcherBin);
46031
46571
  console.log(`${bold("Gipity uninstall")} - this will:`);
46032
46572
  console.log(`\u2022 Stop the running relay daemon (if any)`);
@@ -46073,7 +46613,7 @@ var uninstallCommand = new Command("uninstall").description("Uninstall Gipity").
46073
46613
  if (agentSkills.skills.length) {
46074
46614
  for (const name of agentSkills.skills) {
46075
46615
  try {
46076
- rmSync3(join24(AGENTS_SKILLS_DIR, name), { recursive: true, force: true });
46616
+ rmSync4(join25(AGENTS_SKILLS_DIR, name), { recursive: true, force: true });
46077
46617
  } catch {
46078
46618
  }
46079
46619
  }
@@ -46081,7 +46621,7 @@ var uninstallCommand = new Command("uninstall").description("Uninstall Gipity").
46081
46621
  }
46082
46622
  if (existsSync23(gipityDir)) {
46083
46623
  try {
46084
- rmSync3(gipityDir, { recursive: true, force: true });
46624
+ rmSync4(gipityDir, { recursive: true, force: true });
46085
46625
  console.log(`${success(`Removed ${gipityDir}/`)}`);
46086
46626
  } catch (err) {
46087
46627
  console.error(`${error(`Could not remove ${gipityDir}: ${err?.message || err}`)}`);
@@ -46225,7 +46765,7 @@ gmailCommand.command("send").description("Send a new email from your Gmail").req
46225
46765
  const recap = opts.to.join(", ") + (opts.cc.length ? `, cc: ${opts.cc.join(", ")}` : "");
46226
46766
  printResult(`Sent from your Gmail to ${recap}.`, opts, { sent: true, to: opts.to });
46227
46767
  }));
46228
- gmailCommand.command("reply").description("Reply to a Gmail thread. Get thread-id + message-id from `gmail search`.").requiredOption("--thread-id <id>", "Thread ID (from `gmail search`)").requiredOption("--message-id <id>", "Message ID being replied to (In-Reply-To)").requiredOption("--to <email>", "Recipient (repeatable)", collect2, []).option("--cc <email>", "Cc recipient (repeatable)", collect2, []).option("--bcc <email>", "Bcc recipient (repeatable)", collect2, []).option("--reply-to <email>", "Reply-To header address").requiredOption("--subject <s>", 'Subject (usually "Re: \u2026" of the original)').requiredOption("--body <text>", "Reply body (you compose the quote header)").option("--json", "Output as JSON").action((opts) => run("Reply", async () => {
46768
+ gmailCommand.command("reply").description("Reply to a Gmail thread. Get thread-id + message-id from `gmail search`.").requiredOption("--thread-id <id>", "Thread ID (from `gmail search`)").requiredOption("--message-id <id>", "Message ID being replied to (In-Reply-To)").requiredOption("--to <email>", "Recipient (repeatable)", collect2, []).option("--cc <email>", "Cc recipient (repeatable)", collect2, []).option("--bcc <email>", "Bcc recipient (repeatable)", collect2, []).option("--reply-to <email>", "Reply-To header address").requiredOption("--subject <s>", 'Subject (usually "Re: ..." of the original)').requiredOption("--body <text>", "Reply body (you compose the quote header)").option("--json", "Output as JSON").action((opts) => run("Reply", async () => {
46229
46769
  if (!opts.to.length) throw new Error("--to is required (pass at least one)");
46230
46770
  const payload = {
46231
46771
  thread_id: opts.threadId,
@@ -46242,7 +46782,7 @@ gmailCommand.command("reply").description("Reply to a Gmail thread. Get thread-i
46242
46782
  }));
46243
46783
 
46244
46784
  // src/commands/text.ts
46245
- import { readFileSync as readFileSync25 } from "fs";
46785
+ import { readFileSync as readFileSync27 } from "fs";
46246
46786
  init_colors();
46247
46787
 
46248
46788
  // src/helpers/text-analysis.ts
@@ -46397,10 +46937,10 @@ function areAnagrams(a, b7) {
46397
46937
 
46398
46938
  // src/commands/text.ts
46399
46939
  function resolveInput(args, opts) {
46400
- if (opts.file) return readFileSync25(opts.file, "utf-8");
46940
+ if (opts.file) return readFileSync27(opts.file, "utf-8");
46401
46941
  if (args.length) return args.join(" ");
46402
46942
  try {
46403
- return readFileSync25(0, "utf-8");
46943
+ return readFileSync27(0, "utf-8");
46404
46944
  } catch {
46405
46945
  return "";
46406
46946
  }
@@ -46606,8 +47146,8 @@ function collectRealFlags(argv2, program3) {
46606
47146
  }
46607
47147
 
46608
47148
  // src/trace.ts
46609
- import { openSync as openSync5, writeSync, mkdirSync as mkdirSync17 } from "fs";
46610
- import { join as join25 } from "path";
47149
+ import { openSync as openSync5, writeSync, mkdirSync as mkdirSync18 } from "fs";
47150
+ import { join as join26 } from "path";
46611
47151
  import { homedir as homedir18 } from "os";
46612
47152
  var TRACE_KEY = /* @__PURE__ */ Symbol.for("gipity.traceOutput");
46613
47153
  function installOutputTrace(label2) {
@@ -46619,10 +47159,10 @@ function installOutputTrace(label2) {
46619
47159
  existing.emit({ event: "reenter", label: label2 });
46620
47160
  return;
46621
47161
  }
46622
- const dir = join25(process.env.GIPITY_DIR || join25(homedir18(), ".gipity"), "trace");
46623
- mkdirSync17(dir, { recursive: true });
47162
+ const dir = join26(process.env.GIPITY_DIR || join26(homedir18(), ".gipity"), "trace");
47163
+ mkdirSync18(dir, { recursive: true });
46624
47164
  const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", "_").replace(/:/g, "-");
46625
- const fd2 = openSync5(join25(dir, `${stamp}-pid${process.pid}.jsonl`), "a");
47165
+ const fd2 = openSync5(join26(dir, `${stamp}-pid${process.pid}.jsonl`), "a");
46626
47166
  const emit = (rec) => {
46627
47167
  try {
46628
47168
  writeSync(fd2, JSON.stringify({ t: (/* @__PURE__ */ new Date()).toISOString(), ...rec }) + "\n");
@@ -46657,12 +47197,12 @@ function installOutputTrace(label2) {
46657
47197
 
46658
47198
  // src/index.ts
46659
47199
  installOutputTrace("index");
46660
- var __dirname = dirname13(fileURLToPath3(import.meta.url));
46661
- var pkg = JSON.parse(readFileSync26(resolve19(__dirname, "../package.json"), "utf-8"));
47200
+ var __dirname = dirname14(fileURLToPath3(import.meta.url));
47201
+ var pkg = JSON.parse(readFileSync28(resolve18(__dirname, "../package.json"), "utf-8"));
46662
47202
  function versionLabel() {
46663
47203
  const base = `v${pkg.version}`;
46664
47204
  try {
46665
- const info2 = JSON.parse(readFileSync26(resolve19(__dirname, "build-info.json"), "utf-8"));
47205
+ const info2 = JSON.parse(readFileSync28(resolve18(__dirname, "build-info.json"), "utf-8"));
46666
47206
  if (info2?.sha) return `${base} (dev ${info2.sha}${info2.dirty ? ", modified" : ""})`;
46667
47207
  } catch {
46668
47208
  }
@@ -46867,6 +47407,19 @@ function resolveTargetCommand(argv2) {
46867
47407
  }
46868
47408
  return cmd;
46869
47409
  }
47410
+ Command.prototype._excessArguments = function(receivedArgs) {
47411
+ if (this._allowExcessArguments) return;
47412
+ const expected = this.registeredArguments.length;
47413
+ const excess = receivedArgs.slice(expected);
47414
+ const forSubcommand = this.parent ? ` for '${this.name()}'` : "";
47415
+ const list = excess.map((a) => `'${a}'`).join(", ");
47416
+ let message = `error: unexpected extra argument${excess.length === 1 ? "" : "s"} ${list}${forSubcommand}.`;
47417
+ const kv2 = excess.map((a) => /^([A-Za-z][\w-]*)=/.exec(a)).find(Boolean);
47418
+ if (kv2) {
47419
+ message += ` Options are passed as \`--${kv2[1]} <value>\`, not \`${kv2[1]}=...\` - did you mean \`--${kv2[1]}\`?`;
47420
+ }
47421
+ this.error(message, { code: "commander.excessArguments" });
47422
+ };
46870
47423
  function enableHelpAfterError(cmd) {
46871
47424
  cmd.configureOutput({
46872
47425
  // Render the offending command's full help (via outputHelp, so addHelpText