gipity 1.1.4 → 1.1.6

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 (52) hide show
  1. package/dist/adopt-cwd.js +1 -1
  2. package/dist/agents/agy.js +52 -0
  3. package/dist/agents/index.js +2 -0
  4. package/dist/api.js +19 -3
  5. package/dist/banner.js +3 -5
  6. package/dist/capture/sources/agy.js +88 -0
  7. package/dist/client-context.js +9 -0
  8. package/dist/commands/add.js +1 -1
  9. package/dist/commands/brand.js +125 -0
  10. package/dist/commands/build.js +13 -12
  11. package/dist/commands/chat.js +1 -1
  12. package/dist/commands/deploy.js +1 -1
  13. package/dist/commands/fn.js +2 -8
  14. package/dist/commands/generate.js +5 -5
  15. package/dist/commands/gmail.js +1 -1
  16. package/dist/commands/init.js +2 -2
  17. package/dist/commands/job.js +10 -5
  18. package/dist/commands/key.js +91 -0
  19. package/dist/commands/load.js +2 -2
  20. package/dist/commands/login.js +7 -2
  21. package/dist/commands/page-eval.js +47 -8
  22. package/dist/commands/page-fetch.js +14 -10
  23. package/dist/commands/page-inspect.js +2 -2
  24. package/dist/commands/page-screenshot.js +5 -5
  25. package/dist/commands/page-test.js +1 -1
  26. package/dist/commands/records.js +57 -19
  27. package/dist/commands/remove.js +1 -1
  28. package/dist/commands/sandbox.js +1 -1
  29. package/dist/commands/save.js +1 -1
  30. package/dist/commands/secrets.js +1 -1
  31. package/dist/commands/service.js +1 -1
  32. package/dist/commands/setup.js +4 -6
  33. package/dist/commands/status.js +41 -12
  34. package/dist/commands/storage.js +2 -2
  35. package/dist/commands/sync.js +15 -0
  36. package/dist/commands/test.js +1 -1
  37. package/dist/commands/token.js +6 -1
  38. package/dist/commands/uninstall.js +14 -1
  39. package/dist/commands/upload.js +1 -1
  40. package/dist/commands/workflow.js +1 -1
  41. package/dist/hooks/capture-runner.js +20 -8
  42. package/dist/index.js +1046 -518
  43. package/dist/knowledge.js +4 -1
  44. package/dist/login-flow.js +44 -2
  45. package/dist/project-setup.js +2 -0
  46. package/dist/relay/diagnostics.js +4 -2
  47. package/dist/relay/onboarding.js +26 -44
  48. package/dist/setup.js +198 -16
  49. package/dist/sync.js +6 -6
  50. package/dist/template-vars.js +74 -0
  51. package/dist/utils.js +60 -3
  52. 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(".");
@@ -3972,7 +4007,7 @@ 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);
@@ -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;
@@ -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";
@@ -6561,6 +6597,9 @@ function detectHarness() {
6561
6597
  }
6562
6598
  if (hasEnvPrefix("CODEX_")) return { harness: "codex" };
6563
6599
  if (hasEnvPrefix("GROK_")) return { harness: "grok", harnessSession: process.env.GROK_SESSION_ID };
6600
+ if (process.env.ANTIGRAVITY_CONVERSATION_ID) {
6601
+ return { harness: "agy", harnessSession: process.env.ANTIGRAVITY_CONVERSATION_ID };
6602
+ }
6564
6603
  if (process.env.CURSOR_TRACE_ID || hasEnvPrefix("CURSOR_") || (process.env.TERM_PROGRAM ?? "").toLowerCase().includes("cursor")) {
6565
6604
  return { harness: "cursor" };
6566
6605
  }
@@ -6607,11 +6646,13 @@ __export(api_exports, {
6607
6646
  getAccountSlug: () => getAccountSlug,
6608
6647
  getAuthHeader: () => getAuthHeader,
6609
6648
  getBaseUrl: () => getBaseUrl,
6649
+ mintAppToken: () => mintAppToken,
6610
6650
  patch: () => patch,
6611
6651
  post: () => post,
6612
6652
  postBinary: () => postBinary,
6613
6653
  postForTarEntries: () => postForTarEntries,
6614
6654
  publicPost: () => publicPost,
6655
+ publicRequest: () => publicRequest,
6615
6656
  put: () => put,
6616
6657
  putTimeoutMs: () => putTimeoutMs,
6617
6658
  putToPresignedUrl: () => putToPresignedUrl,
@@ -6867,12 +6908,12 @@ async function getAccountSlug() {
6867
6908
  accountSlugCache = res.data.accountSlug;
6868
6909
  return accountSlugCache;
6869
6910
  }
6870
- async function publicPost(path5, body, extraHeaders) {
6911
+ async function publicRequest(method, path5, body, extraHeaders) {
6871
6912
  const url = `${baseUrl()}${path5}`;
6872
6913
  const res = await fetch(url, {
6873
- method: "POST",
6914
+ method,
6874
6915
  headers: { ...clientHeaders(), "Content-Type": "application/json", ...extraHeaders },
6875
- body: JSON.stringify(body)
6916
+ body: body === void 0 ? void 0 : JSON.stringify(body)
6876
6917
  });
6877
6918
  if (!res.ok) {
6878
6919
  const json = await res.json().catch(() => ({ error: { code: "UNKNOWN", message: res.statusText } }));
@@ -6881,6 +6922,17 @@ async function publicPost(path5, body, extraHeaders) {
6881
6922
  }
6882
6923
  return res.json();
6883
6924
  }
6925
+ function publicPost(path5, body, extraHeaders) {
6926
+ return publicRequest("POST", path5, body, extraHeaders);
6927
+ }
6928
+ async function mintAppToken(projectGuid2) {
6929
+ try {
6930
+ const minted = await publicPost("/api/token", { app: projectGuid2 });
6931
+ return { "X-App-Token": minted.data.token };
6932
+ } catch {
6933
+ return void 0;
6934
+ }
6935
+ }
6884
6936
  var tar, ApiError, REQUEST_TIMEOUT_MS, DOWNLOAD_HEADER_TIMEOUT_MS, PUT_TIMEOUT_FLOOR_MS, PUT_MIN_BYTES_PER_SEC, accountSlugCache;
6885
6937
  var init_api = __esm({
6886
6938
  "src/api.ts"() {
@@ -6928,7 +6980,7 @@ __export(config_exports, {
6928
6980
  setApiBaseOverride: () => setApiBaseOverride,
6929
6981
  shouldIgnore: () => shouldIgnore
6930
6982
  });
6931
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
6983
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
6932
6984
  import { dirname as dirname2, resolve as resolve2 } from "path";
6933
6985
  function setApiBaseOverride(url) {
6934
6986
  apiBaseOverride = url;
@@ -6986,7 +7038,7 @@ function getConfig() {
6986
7038
  const path5 = getConfigPath();
6987
7039
  if (!path5) return null;
6988
7040
  try {
6989
- cached3 = JSON.parse(readFileSync3(path5, "utf-8"));
7041
+ cached3 = JSON.parse(readFileSync4(path5, "utf-8"));
6990
7042
  return cached3;
6991
7043
  } catch {
6992
7044
  return null;
@@ -31865,7 +31917,7 @@ init_config();
31865
31917
  init_utils();
31866
31918
  import { readFileSync as readFileSync28 } from "fs";
31867
31919
  import { fileURLToPath as fileURLToPath3 } from "url";
31868
- import { dirname as dirname13, resolve as resolve18 } from "path";
31920
+ import { dirname as dirname14, resolve as resolve18 } from "path";
31869
31921
 
31870
31922
  // src/helpers/output.ts
31871
31923
  var frameOpen = false;
@@ -32039,7 +32091,7 @@ mkdir -p ~/GipityProjects/<slug> && cd ~/GipityProjects/<slug> && gipity init <s
32039
32091
 
32040
32092
  ## CLI quick reference
32041
32093
 
32042
- Key commands: \`gipity add <template|kit>\`, \`gipity deploy dev\`, \`gipity sandbox run\`, \`gipity page inspect <url>\`, \`gipity page screenshot <url>\`, \`gipity db query "SQL"\`, \`gipity fn call <name>\`, \`gipity logs fn <name>\`, \`gipity secrets set <NAME> <value> [--account]\` (store an API key/token encrypted; read in functions via \`secrets.get('NAME')\` \u2014 never hardcode keys), \`gipity email send --to <addr> --subject <s> --body <b>\` (sends as \`gipity@gipity.ai\`; omit \`--to\` to self-send), \`gipity skill read <name>\`.
32094
+ Key commands: \`gipity add <template|kit>\`, \`gipity brand set --emoji <e>|--color <hex>\` (regenerate the app's icons + social share card), \`gipity deploy dev\`, \`gipity sandbox run\`, \`gipity page inspect <url>\`, \`gipity page screenshot <url>\`, \`gipity db query "SQL"\`, \`gipity fn call <name>\`, \`gipity logs fn <name>\`, \`gipity secrets set <NAME> <value> [--account]\` (store an API key/token encrypted; read in functions via \`secrets.get('NAME')\` \u2014 never hardcode keys), \`gipity email send --to <addr> --subject <s> --body <b>\` (sends as \`gipity@gipity.ai\`; omit \`--to\` to self-send), \`gipity skill read <name>\`.
32043
32095
  Rename for findability: \`gipity project rename <name>\` renames the current project's display name (the slug and deployed URLs never change); \`gipity chat rename <title>\` renames the current chat's tab title. Both are the display label users scan to switch between tabs \u2014 retitle a chat when the conversation clearly shifts to a new topic (sparingly, not every turn), and keep every project/chat title SHORT: 2-4 words, \u226440 characters, no trailing punctuation (e.g. "Stripe checkout", "Tetris game").
32044
32096
  Pull an existing remote project local (given its URL/slug): \`mkdir -p ~/GipityProjects/<slug> && cd ~/GipityProjects/<slug> && gipity init <slug>\` (adopts the matching project and syncs files down - this is the "clone").
32045
32097
  Move whole apps in/out: \`gipity save\` (export this project as a portable \`.gip\` bundle), \`gipity load <file.gip | github:owner/repo>\` (import as a NEW project; \`--inspect\` to preview), \`gipity github connect\` (1-2 click GitHub access for imports). Porting a Vercel/Replit/Lovable app? Load the \`app-import\` skill first.
@@ -32081,6 +32133,8 @@ Write files locally - Gipity's editor hooks (installed into Claude Code, Grok, a
32081
32133
 
32082
32134
  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.
32083
32135
 
32136
+ **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.
32137
+
32084
32138
  ### Where files go: deploy only ships \`src/\`
32085
32139
 
32086
32140
  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:
@@ -32117,6 +32171,7 @@ App development skills:
32117
32171
  - \`app-debugging\` - debug a deployed app: page inspect/eval, screenshots, function logs
32118
32172
  - \`app-development\` - functions, database, and API
32119
32173
  - \`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
32174
+ - \`app-records\` - Gipity Records: declared tables \u2192 validated CRUD, provenance, workflows, auto UI
32120
32175
  - \`app-testing\` - testing deployed app functions (ctx.fn.call/callAs, the isolated test DB)
32121
32176
  - \`deploy\` - the deploy pipeline & gipity.yaml manifest
32122
32177
  - \`jobs\` - long-running CPU + GPU compute jobs (Python / Node / bash)
@@ -32153,13 +32208,13 @@ init_colors();
32153
32208
 
32154
32209
  // src/bug-queue.ts
32155
32210
  init_api();
32156
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync4, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2 } from "fs";
32157
- import { join as join2 } from "path";
32211
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2 } from "fs";
32212
+ import { join as join3 } from "path";
32158
32213
  import { homedir as homedir2 } from "os";
32159
- var QUEUE_DIR = join2(process.env.GIPITY_DIR || join2(homedir2(), ".gipity"), "bug-queue");
32214
+ var QUEUE_DIR = join3(process.env.GIPITY_DIR || join3(homedir2(), ".gipity"), "bug-queue");
32160
32215
  function queueBugReport(report) {
32161
32216
  mkdirSync2(QUEUE_DIR, { recursive: true, mode: 448 });
32162
- const file = join2(QUEUE_DIR, `${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json`);
32217
+ const file = join3(QUEUE_DIR, `${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json`);
32163
32218
  writeFileSync3(file, JSON.stringify(report, null, 2), { mode: 384 });
32164
32219
  }
32165
32220
  function isRetryableFailure(err) {
@@ -32171,12 +32226,12 @@ function isRetryableFailure(err) {
32171
32226
  async function flushBugQueue() {
32172
32227
  if (!existsSync3(QUEUE_DIR)) return 0;
32173
32228
  let delivered = 0;
32174
- for (const file of readdirSync(QUEUE_DIR)) {
32229
+ for (const file of readdirSync2(QUEUE_DIR)) {
32175
32230
  if (!file.endsWith(".json")) continue;
32176
- const path5 = join2(QUEUE_DIR, file);
32231
+ const path5 = join3(QUEUE_DIR, file);
32177
32232
  let report;
32178
32233
  try {
32179
- report = JSON.parse(readFileSync4(path5, "utf-8"));
32234
+ report = JSON.parse(readFileSync5(path5, "utf-8"));
32180
32235
  } catch {
32181
32236
  try {
32182
32237
  unlinkSync2(path5);
@@ -32199,6 +32254,70 @@ async function flushBugQueue() {
32199
32254
  return delivered;
32200
32255
  }
32201
32256
 
32257
+ // src/login-flow.ts
32258
+ init_api();
32259
+ init_auth();
32260
+ init_config();
32261
+ init_utils();
32262
+ init_colors();
32263
+ function newAccountWouldBeUnexpected(email, priorAuth) {
32264
+ return !!getConfig() || priorAuth?.email.toLowerCase() === email.toLowerCase().trim();
32265
+ }
32266
+ function warnBeforeCodeIfUnexpectedNewAccount(isNewUser, email, indent = "") {
32267
+ if (isNewUser !== true) return;
32268
+ const priorAuth = getAuth();
32269
+ if (!newAccountWouldBeUnexpected(email, priorAuth)) return;
32270
+ const config = getConfig();
32271
+ console.log(`${indent}${warning(`No existing Gipity account for ${email} \u2014 entering the code will CREATE a new one.`)}`);
32272
+ if (config) {
32273
+ 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.`)}`);
32274
+ }
32275
+ }
32276
+ function warnIfUnexpectedNewAccount(isNewUser, email, priorAuth, indent = "") {
32277
+ if (isNewUser !== true) return;
32278
+ if (!newAccountWouldBeUnexpected(email, priorAuth)) return;
32279
+ const config = getConfig();
32280
+ console.log(`${indent}${warning(`Logged into a NEW, empty account for ${email} \u2014 no prior account existed for this email.`)}`);
32281
+ if (config) {
32282
+ 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".`)}`);
32283
+ }
32284
+ console.log(`${indent}${muted("If you expected an existing account, run: gipity status \u2014 then gipity login again with the correct email.")}`);
32285
+ }
32286
+ async function interactiveLogin() {
32287
+ const email = await prompt(" Email: ");
32288
+ if (!email) {
32289
+ console.error(`
32290
+ ${error("Email required.")}`);
32291
+ process.exit(1);
32292
+ }
32293
+ const sendRes = await publicPost("/auth/login", { email });
32294
+ console.log(" Check your email for a 6-digit code.\n");
32295
+ warnBeforeCodeIfUnexpectedNewAccount(sendRes.isNewUser, email, " ");
32296
+ const code = await prompt(" Code: ");
32297
+ if (!code) {
32298
+ console.error(`
32299
+ ${error("Code required.")}`);
32300
+ process.exit(1);
32301
+ }
32302
+ const priorAuth = getAuth();
32303
+ const res = await publicPost("/auth/verify", { email, code });
32304
+ const exp = decodeJwtExp(res.accessToken);
32305
+ if (!exp) {
32306
+ console.error(`
32307
+ ${error("Invalid token received.")}`);
32308
+ process.exit(1);
32309
+ }
32310
+ const expiresAt = new Date(exp * 1e3).toISOString();
32311
+ saveAuth({ accessToken: res.accessToken, refreshToken: res.refreshToken, email, expiresAt });
32312
+ console.log(` ${success(`Logged in (${email}).`)}`);
32313
+ warnIfUnexpectedNewAccount(res.isNewUser, email, priorAuth, " ");
32314
+ const delivered = await flushBugQueue().catch(() => 0);
32315
+ if (delivered > 0) {
32316
+ console.log(` ${muted(`Delivered ${delivered} queued bug report${delivered === 1 ? "" : "s"}.`)}`);
32317
+ }
32318
+ return getAuth();
32319
+ }
32320
+
32202
32321
  // src/commands/login.ts
32203
32322
  var loginCommand = new Command("login").description("Log in or sign up").option("--email <email>", "Email address").option("--code <code>", "Verification code").action(async (opts) => {
32204
32323
  try {
@@ -32209,9 +32328,10 @@ var loginCommand = new Command("login").description("Log in or sign up").option(
32209
32328
  return;
32210
32329
  }
32211
32330
  if (email && !code) {
32212
- await publicPost("/auth/login", { email });
32331
+ const sendRes2 = await publicPost("/auth/login", { email });
32213
32332
  console.log("Check your email for a 6-digit code.");
32214
32333
  console.log(muted(`Then run: gipity login --email ${email} --code <code>`));
32334
+ warnBeforeCodeIfUnexpectedNewAccount(sendRes2.isNewUser, email);
32215
32335
  return;
32216
32336
  }
32217
32337
  console.log("Enter your email to log in or create an account.");
@@ -32222,9 +32342,10 @@ var loginCommand = new Command("login").description("Log in or sign up").option(
32222
32342
  console.error(error("Email required."));
32223
32343
  process.exit(1);
32224
32344
  }
32225
- await publicPost("/auth/login", { email });
32345
+ const sendRes = await publicPost("/auth/login", { email });
32226
32346
  console.log("");
32227
32347
  console.log("Check your email for a 6-digit code.");
32348
+ warnBeforeCodeIfUnexpectedNewAccount(sendRes.isNewUser, email);
32228
32349
  code = await prompt("Code: ");
32229
32350
  await verify(email, code);
32230
32351
  } catch (err) {
@@ -32233,6 +32354,7 @@ var loginCommand = new Command("login").description("Log in or sign up").option(
32233
32354
  }
32234
32355
  });
32235
32356
  async function verify(email, code) {
32357
+ const priorAuth = getAuth();
32236
32358
  const res = await publicPost("/auth/verify", { email, code });
32237
32359
  const exp = decodeJwtExp(res.accessToken);
32238
32360
  if (!exp) {
@@ -32247,6 +32369,7 @@ async function verify(email, code) {
32247
32369
  expiresAt
32248
32370
  });
32249
32371
  console.log(success(`Logged in (${email}).`));
32372
+ warnIfUnexpectedNewAccount(res.isNewUser, email, priorAuth);
32250
32373
  const delivered = await flushBugQueue().catch(() => 0);
32251
32374
  if (delivered > 0) {
32252
32375
  console.log(muted(`Delivered ${delivered} queued bug report${delivered === 1 ? "" : "s"}.`));
@@ -32292,8 +32415,8 @@ function run(label2, action) {
32292
32415
  init_api();
32293
32416
  init_config();
32294
32417
  init_utils();
32295
- 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";
32296
- import { join as join4, relative, dirname as dirname4, extname as extname2, resolve as resolve4, sep } from "path";
32418
+ 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";
32419
+ import { join as join5, relative, dirname as dirname4, extname as extname2, resolve as resolve4, sep } from "path";
32297
32420
  import { hostname } from "os";
32298
32421
  import { createHash as createHash2 } from "crypto";
32299
32422
 
@@ -32406,17 +32529,17 @@ var UploadConflictError = class extends Error {
32406
32529
  currentServerVersion;
32407
32530
  path;
32408
32531
  };
32409
- async function uploadOneFile(projectGuid, localPath, virtualPath, opts = {}) {
32410
- const { sha256: sha2562, size } = await hashFile(localPath);
32532
+ async function uploadOneFile(projectGuid2, localPath, virtualPath, opts = {}) {
32533
+ const { sha256, size } = await hashFile(localPath);
32411
32534
  const mime = opts.mime ?? guessMime(virtualPath);
32412
- const initBody = { path: virtualPath, size, sha256: sha2562, mime };
32535
+ const initBody = { path: virtualPath, size, sha256, mime };
32413
32536
  if (opts.expectedServerVersion !== void 0) {
32414
32537
  initBody.expected_server_version = opts.expectedServerVersion;
32415
32538
  }
32416
32539
  let init;
32417
32540
  try {
32418
32541
  init = await post(
32419
- `/projects/${projectGuid}/files/upload-init`,
32542
+ `/projects/${projectGuid2}/files/upload-init`,
32420
32543
  initBody
32421
32544
  );
32422
32545
  } catch (err) {
@@ -32438,7 +32561,7 @@ async function uploadOneFile(projectGuid, localPath, virtualPath, opts = {}) {
32438
32561
  }
32439
32562
  let comp;
32440
32563
  try {
32441
- comp = await post(`/projects/${projectGuid}/files/upload-complete`, completeBody);
32564
+ comp = await post(`/projects/${projectGuid2}/files/upload-complete`, completeBody);
32442
32565
  } catch (err) {
32443
32566
  if (err instanceof ApiError && err.statusCode === 409) {
32444
32567
  const current = typeof err.data?.current_server_version === "number" ? err.data.current_server_version : null;
@@ -32520,16 +32643,16 @@ async function transferToS3(localPath, size, mime, data, opts = {}) {
32520
32643
  completed.sort((a, b7) => a.part_number - b7.part_number);
32521
32644
  return { parts: completed };
32522
32645
  }
32523
- async function uploadInitBatch(projectGuid, files) {
32646
+ async function uploadInitBatch(projectGuid2, files) {
32524
32647
  const res = await post(
32525
- `/projects/${projectGuid}/files/upload-init-batch`,
32648
+ `/projects/${projectGuid2}/files/upload-init-batch`,
32526
32649
  { files }
32527
32650
  );
32528
32651
  return res.data.results;
32529
32652
  }
32530
- async function uploadCompleteBatch(projectGuid, items) {
32653
+ async function uploadCompleteBatch(projectGuid2, items) {
32531
32654
  const res = await post(
32532
- `/projects/${projectGuid}/files/upload-complete-batch`,
32655
+ `/projects/${projectGuid2}/files/upload-complete-batch`,
32533
32656
  { items }
32534
32657
  );
32535
32658
  return res.data.results;
@@ -32537,15 +32660,17 @@ async function uploadCompleteBatch(projectGuid, items) {
32537
32660
 
32538
32661
  // src/setup.ts
32539
32662
  init_platform();
32540
- import { resolve as resolve3, join as join3, dirname as dirname3 } from "path";
32663
+ import { resolve as resolve3, join as join4, dirname as dirname3 } from "path";
32541
32664
  import { homedir as homedir3, tmpdir } from "os";
32542
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, readFileSync as readFileSync5, mkdtempSync, rmSync, cpSync, readdirSync as readdirSync2 } from "fs";
32665
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, readFileSync as readFileSync6, mkdtempSync, rmSync, cpSync, readdirSync as readdirSync3 } from "fs";
32543
32666
  init_config();
32544
32667
  var PRIMER_FILES = {
32545
32668
  claude: "CLAUDE.md",
32546
32669
  codex: "AGENTS.md",
32547
32670
  grok: "AGENTS.md",
32548
32671
  // Grok Build reads the AGENTS.md family (and CLAUDE.md) natively
32672
+ agy: "AGENTS.md",
32673
+ // Antigravity reads the same AGENTS.md/GEMINI.md rules family natively
32549
32674
  aider: "AGENTS.md",
32550
32675
  // shares the Codex primer; aider is pointed at it via .aider.conf.yml
32551
32676
  gemini: "GEMINI.md",
@@ -32561,6 +32686,7 @@ var DEFAULT_SYNC_IGNORE = [
32561
32686
  ".gipity/",
32562
32687
  ".claude/",
32563
32688
  ".codex/",
32689
+ ".agents/",
32564
32690
  ".gitignore",
32565
32691
  AIDER_CONF_FILE,
32566
32692
  // Home-directory junk: a project created inside a real home dir (or one that
@@ -32654,14 +32780,14 @@ function stripGipityHooks(settings) {
32654
32780
  function readSettingsFile(path5) {
32655
32781
  if (!existsSync4(path5)) return {};
32656
32782
  try {
32657
- return JSON.parse(readFileSync5(path5, "utf-8"));
32783
+ return JSON.parse(readFileSync6(path5, "utf-8"));
32658
32784
  } catch {
32659
32785
  return {};
32660
32786
  }
32661
32787
  }
32662
32788
  function ensureGipityPlugin(force = false) {
32663
- const claudeDir = join3(homedir3(), ".claude");
32664
- const settingsPath = join3(claudeDir, "settings.json");
32789
+ const claudeDir = join4(homedir3(), ".claude");
32790
+ const settingsPath = join4(claudeDir, "settings.json");
32665
32791
  const settings = readSettingsFile(settingsPath);
32666
32792
  let changed = stripGipityHooks(settings);
32667
32793
  const marketplaces = settings.extraKnownMarketplaces ?? (settings.extraKnownMarketplaces = {});
@@ -32693,8 +32819,8 @@ function versionGte(have, want) {
32693
32819
  }
32694
32820
  function userScopeInstallState() {
32695
32821
  try {
32696
- const p = join3(homedir3(), ".claude", "plugins", "installed_plugins.json");
32697
- const data = JSON.parse(readFileSync5(p, "utf-8"));
32822
+ const p = join4(homedir3(), ".claude", "plugins", "installed_plugins.json");
32823
+ const data = JSON.parse(readFileSync6(p, "utf-8"));
32698
32824
  const entries = data?.plugins?.[GIPITY_PLUGIN_ID];
32699
32825
  if (!Array.isArray(entries)) return { exists: false, current: false };
32700
32826
  const userEntries = entries.filter((e) => e?.scope === "user");
@@ -32731,8 +32857,8 @@ function ensureGipityPluginInstalled() {
32731
32857
  }
32732
32858
  function grokInstallState() {
32733
32859
  try {
32734
- const p = join3(homedir3(), ".grok", "installed-plugins", "registry.json");
32735
- const data = JSON.parse(readFileSync5(p, "utf-8"));
32860
+ const p = join4(homedir3(), ".grok", "installed-plugins", "registry.json");
32861
+ const data = JSON.parse(readFileSync6(p, "utf-8"));
32736
32862
  const versions = [];
32737
32863
  for (const repo of Object.values(data?.repos ?? {})) {
32738
32864
  const v7 = repo?.plugins?.gipity?.version;
@@ -32757,12 +32883,14 @@ function ensureGrokPluginInstalled() {
32757
32883
  console.log("Installed the Gipity plugin for Grok (skills + file-sync hooks).");
32758
32884
  }
32759
32885
  }
32760
- var AGENTS_SKILLS_DIR = join3(homedir3(), ".agents", "skills");
32761
- var AGENT_HOOKS_DIR = join3(homedir3(), ".gipity", "agent-hooks");
32762
- var AGENT_SKILLS_MANIFEST = join3(homedir3(), ".gipity", "agent-skills.json");
32763
- function agentSkillsState() {
32886
+ var AGENTS_SKILLS_DIR = join4(homedir3(), ".agents", "skills");
32887
+ var AGENT_HOOKS_DIR = join4(homedir3(), ".gipity", "agent-hooks");
32888
+ var AGENT_SKILLS_MANIFEST = join4(homedir3(), ".gipity", "agent-skills.json");
32889
+ var AGY_SKILLS_DIR = join4(homedir3(), ".gemini", "config", "skills");
32890
+ var AGY_SKILLS_MANIFEST = join4(homedir3(), ".gipity", "agy-skills.json");
32891
+ function skillsManifestState(manifestPath) {
32764
32892
  try {
32765
- const m = JSON.parse(readFileSync5(AGENT_SKILLS_MANIFEST, "utf-8"));
32893
+ const m = JSON.parse(readFileSync6(manifestPath, "utf-8"));
32766
32894
  return {
32767
32895
  current: typeof m?.version === "string" && versionGte(m.version, GIPITY_PLUGIN_VERSION),
32768
32896
  skills: Array.isArray(m?.skills) ? m.skills : []
@@ -32771,50 +32899,63 @@ function agentSkillsState() {
32771
32899
  return { current: false, skills: [] };
32772
32900
  }
32773
32901
  }
32774
- function ensureAgentSkillsInstalled() {
32775
- if (agentSkillsState().current) return;
32902
+ function agentSkillsState() {
32903
+ return skillsManifestState(AGENT_SKILLS_MANIFEST);
32904
+ }
32905
+ function agySkillsState() {
32906
+ return skillsManifestState(AGY_SKILLS_MANIFEST);
32907
+ }
32908
+ function installSkillsAndHooks(skillsDir, manifestPath, harnessLabel) {
32776
32909
  if (!binaryOnPath("git")) return;
32777
- const tmp = mkdtempSync(join3(tmpdir(), "gipity-skills-"));
32910
+ const tmp = mkdtempSync(join4(tmpdir(), "gipity-skills-"));
32778
32911
  try {
32779
32912
  const clone = spawnSyncCommand(
32780
32913
  resolveCommand("git"),
32781
- ["clone", "--depth", "1", `https://github.com/${GIPITY_MARKETPLACE_REPO}.git`, join3(tmp, "repo")],
32914
+ ["clone", "--depth", "1", `https://github.com/${GIPITY_MARKETPLACE_REPO}.git`, join4(tmp, "repo")],
32782
32915
  { stdio: "ignore", timeout: 12e4 }
32783
32916
  );
32784
32917
  if (clone.status !== 0) return;
32785
- const repo = join3(tmp, "repo");
32918
+ const repo = join4(tmp, "repo");
32786
32919
  let version = GIPITY_PLUGIN_VERSION;
32787
32920
  try {
32788
- const manifest = JSON.parse(readFileSync5(join3(repo, ".claude-plugin", "plugin.json"), "utf-8"));
32921
+ const manifest = JSON.parse(readFileSync6(join4(repo, ".claude-plugin", "plugin.json"), "utf-8"));
32789
32922
  if (typeof manifest?.version === "string") version = manifest.version;
32790
32923
  } catch {
32791
32924
  }
32792
- const skillsSrc = join3(repo, "skills");
32925
+ const skillsSrc = join4(repo, "skills");
32793
32926
  const names = [];
32794
- for (const entry of readdirSync2(skillsSrc, { withFileTypes: true })) {
32927
+ for (const entry of readdirSync3(skillsSrc, { withFileTypes: true })) {
32795
32928
  if (!entry.isDirectory()) continue;
32796
- if (!existsSync4(join3(skillsSrc, entry.name, "SKILL.md"))) continue;
32797
- mkdirSync3(AGENTS_SKILLS_DIR, { recursive: true });
32798
- cpSync(join3(skillsSrc, entry.name), join3(AGENTS_SKILLS_DIR, entry.name), {
32929
+ if (!existsSync4(join4(skillsSrc, entry.name, "SKILL.md"))) continue;
32930
+ mkdirSync3(skillsDir, { recursive: true });
32931
+ cpSync(join4(skillsSrc, entry.name), join4(skillsDir, entry.name), {
32799
32932
  recursive: true,
32800
32933
  force: true
32801
32934
  });
32802
32935
  names.push(entry.name);
32803
32936
  }
32804
32937
  mkdirSync3(AGENT_HOOKS_DIR, { recursive: true });
32805
- for (const script of readdirSync2(join3(repo, "hooks", "scripts"))) {
32806
- cpSync(join3(repo, "hooks", "scripts", script), join3(AGENT_HOOKS_DIR, script), { force: true });
32938
+ for (const script of readdirSync3(join4(repo, "hooks", "scripts"))) {
32939
+ cpSync(join4(repo, "hooks", "scripts", script), join4(AGENT_HOOKS_DIR, script), { force: true });
32807
32940
  }
32808
- writeFileSync4(AGENT_SKILLS_MANIFEST, JSON.stringify({ version, skills: names }, null, 2) + "\n");
32809
- console.log(`Installed ${names.length} Gipity skills for Codex (~/.agents/skills).`);
32941
+ writeFileSync4(manifestPath, JSON.stringify({ version, skills: names }, null, 2) + "\n");
32942
+ console.log(`Installed ${names.length} Gipity skills for ${harnessLabel} (${skillsDir}).`);
32810
32943
  } catch {
32811
32944
  } finally {
32812
32945
  rmSync(tmp, { recursive: true, force: true });
32813
32946
  }
32814
32947
  }
32948
+ function ensureAgentSkillsInstalled() {
32949
+ if (agentSkillsState().current) return;
32950
+ installSkillsAndHooks(AGENTS_SKILLS_DIR, AGENT_SKILLS_MANIFEST, "Codex");
32951
+ }
32952
+ function ensureAgySkillsInstalled() {
32953
+ if (agySkillsState().current) return;
32954
+ installSkillsAndHooks(AGY_SKILLS_DIR, AGY_SKILLS_MANIFEST, "Antigravity");
32955
+ }
32815
32956
  function applyCodexHooks(existing) {
32816
- const launcher = join3(AGENT_HOOKS_DIR, "launch.sh");
32817
- const cmd = (script, ...args) => [`sh "${launcher}" "${join3(AGENT_HOOKS_DIR, script)}"`, ...args].join(" ");
32957
+ const launcher = join4(AGENT_HOOKS_DIR, "launch.sh");
32958
+ const cmd = (script, ...args) => [`sh "${launcher}" "${join4(AGENT_HOOKS_DIR, script)}"`, ...args].join(" ");
32818
32959
  const wanted = [
32819
32960
  { event: "PostToolUse", matcher: "Edit|Write", command: cmd("sync-push.cjs"), timeout: 30 },
32820
32961
  { event: "UserPromptSubmit", command: cmd("sync-pull.cjs"), timeout: 300 },
@@ -32852,8 +32993,8 @@ function setupCodexHooks() {
32852
32993
  if (process.platform === "win32") return;
32853
32994
  const cwd = resolve3(process.cwd());
32854
32995
  if (cwd === resolve3(homedir3())) return;
32855
- const path5 = join3(cwd, ".codex", "hooks.json");
32856
- const existing = existsSync4(path5) ? readFileSync5(path5, "utf-8") : null;
32996
+ const path5 = join4(cwd, ".codex", "hooks.json");
32997
+ const existing = existsSync4(path5) ? readFileSync6(path5, "utf-8") : null;
32857
32998
  const next = applyCodexHooks(existing);
32858
32999
  if (next === null) return;
32859
33000
  mkdirSync3(dirname3(path5), { recursive: true });
@@ -32869,13 +33010,109 @@ function setupCodexIntegration() {
32869
33010
  ensureAgentSkillsInstalled();
32870
33011
  setupCodexHooks();
32871
33012
  }
33013
+ var AGY_HOOKS_SCRIPT = `#!/usr/bin/env node
33014
+ 'use strict';
33015
+ const { spawnSync } = require('child_process');
33016
+ const { join } = require('path');
33017
+
33018
+ const WRITE_TOOLS = new Set(['write_to_file', 'replace_file_content']);
33019
+
33020
+ function readStdin() {
33021
+ return new Promise((res) => {
33022
+ let data = '';
33023
+ process.stdin.setEncoding('utf-8');
33024
+ process.stdin.on('data', (c) => { data += c; });
33025
+ process.stdin.on('end', () => res(data));
33026
+ process.stdin.on('error', () => res(data));
33027
+ });
33028
+ }
33029
+
33030
+ async function main() {
33031
+ const event = process.argv[2];
33032
+ const raw = await readStdin();
33033
+ let payload = {};
33034
+ try { payload = JSON.parse(raw); } catch { /* keep {} */ }
33035
+
33036
+ try {
33037
+ if (event === 'post-tool-use') {
33038
+ const toolCall = payload.toolCall;
33039
+ const filePath = toolCall && toolCall.args && toolCall.args.TargetFile;
33040
+ if (toolCall && WRITE_TOOLS.has(toolCall.name) && typeof filePath === 'string' && filePath) {
33041
+ spawnSync(process.execPath, [join(__dirname, 'sync-push.cjs')], {
33042
+ input: JSON.stringify({ tool_input: { file_path: filePath } }),
33043
+ stdio: ['pipe', 'ignore', 'ignore'],
33044
+ windowsHide: true,
33045
+ });
33046
+ }
33047
+ spawnSync(process.execPath, [join(__dirname, 'capture.cjs'), 'agy', 'post-tool-use'], {
33048
+ input: raw,
33049
+ stdio: ['pipe', 'ignore', 'ignore'],
33050
+ windowsHide: true,
33051
+ });
33052
+ } else if (event === 'stop') {
33053
+ spawnSync(process.execPath, [join(__dirname, 'capture.cjs'), 'agy', 'stop'], {
33054
+ input: raw,
33055
+ stdio: ['pipe', 'ignore', 'ignore'],
33056
+ windowsHide: true,
33057
+ });
33058
+ }
33059
+ } catch { /* never let a side effect break the response below */ }
33060
+
33061
+ process.stdout.write('{}');
33062
+ }
33063
+
33064
+ main();
33065
+ `;
33066
+ function applyAgyHooks(existing) {
33067
+ const wrapper = join4(AGENT_HOOKS_DIR, "agy-hooks.cjs");
33068
+ const launcher = join4(AGENT_HOOKS_DIR, "launch.sh");
33069
+ const wrapCmd = (event) => `sh "${launcher}" "${wrapper}" ${event}`;
33070
+ const block = {
33071
+ PostToolUse: [
33072
+ { matcher: ".*", hooks: [{ type: "command", command: wrapCmd("post-tool-use"), timeout: 30 }] }
33073
+ ],
33074
+ Stop: [
33075
+ { hooks: [{ type: "command", command: wrapCmd("stop"), timeout: 60 }] }
33076
+ ]
33077
+ };
33078
+ let settings = {};
33079
+ if (existing !== null) {
33080
+ try {
33081
+ settings = JSON.parse(existing);
33082
+ } catch {
33083
+ return null;
33084
+ }
33085
+ }
33086
+ if (JSON.stringify(settings.gipity ?? null) === JSON.stringify(block)) return null;
33087
+ settings.gipity = block;
33088
+ return JSON.stringify(settings, null, 2) + "\n";
33089
+ }
33090
+ function setupAgyHooks() {
33091
+ if (process.platform === "win32") return;
33092
+ const cwd = resolve3(process.cwd());
33093
+ if (cwd === resolve3(homedir3())) return;
33094
+ mkdirSync3(AGENT_HOOKS_DIR, { recursive: true });
33095
+ writeFileSync4(join4(AGENT_HOOKS_DIR, "agy-hooks.cjs"), AGY_HOOKS_SCRIPT);
33096
+ const path5 = join4(cwd, ".agents", "hooks.json");
33097
+ const existing = existsSync4(path5) ? readFileSync6(path5, "utf-8") : null;
33098
+ const next = applyAgyHooks(existing);
33099
+ if (next === null) return;
33100
+ mkdirSync3(dirname3(path5), { recursive: true });
33101
+ writeFileSync4(path5, next);
33102
+ console.log(existing === null ? "Wrote Antigravity sync + session-capture hooks (.agents/hooks.json)." : "Updated Antigravity hooks (.agents/hooks.json).");
33103
+ }
33104
+ function setupAgyIntegration() {
33105
+ if (!binaryOnPath("agy")) return;
33106
+ ensureAgySkillsInstalled();
33107
+ setupAgyHooks();
33108
+ }
32872
33109
  function setupClaudeHooks() {
32873
33110
  ensureGipityPlugin();
32874
33111
  const cwd = resolve3(process.cwd());
32875
33112
  if (cwd === resolve3(homedir3())) return;
32876
- const claudeDir = join3(cwd, ".claude");
33113
+ const claudeDir = join4(cwd, ".claude");
32877
33114
  mkdirSync3(claudeDir, { recursive: true });
32878
- const settingsPath = join3(claudeDir, "settings.json");
33115
+ const settingsPath = join4(claudeDir, "settings.json");
32879
33116
  const settings = readSettingsFile(settingsPath);
32880
33117
  stripGipityHooks(settings);
32881
33118
  const perms = settings.permissions || {};
@@ -32922,7 +33159,7 @@ function applySkillsBlock(existing, apiBase2 = DEFAULT_API_BASE) {
32922
33159
  function writeSkillsFile(relPath, wrap) {
32923
33160
  const path5 = resolve3(process.cwd(), relPath);
32924
33161
  mkdirSync3(dirname3(path5), { recursive: true });
32925
- const existing = existsSync4(path5) ? readFileSync5(path5, "utf-8") : null;
33162
+ const existing = existsSync4(path5) ? readFileSync6(path5, "utf-8") : null;
32926
33163
  const baseNext = applySkillsBlock(existing, resolveApiBase());
32927
33164
  const next = wrap && existing === null ? wrap(baseNext) : baseNext;
32928
33165
  if (next !== existing) writeFileSync4(path5, next);
@@ -32965,7 +33202,7 @@ function applyAiderConf(existing) {
32965
33202
  function setupAiderMd() {
32966
33203
  writeSkillsFile(PRIMER_FILES.aider);
32967
33204
  const path5 = resolve3(process.cwd(), AIDER_CONF_FILE);
32968
- const existing = existsSync4(path5) ? readFileSync5(path5, "utf-8") : null;
33205
+ const existing = existsSync4(path5) ? readFileSync6(path5, "utf-8") : null;
32969
33206
  const next = applyAiderConf(existing);
32970
33207
  if (next !== null) writeFileSync4(path5, next);
32971
33208
  }
@@ -32990,6 +33227,7 @@ var SUPPORTED_TOOLS = [
32990
33227
  { key: "claude", label: "Claude Code (CLAUDE.md + Gipity plugin)", setup: setupClaudeMd, integrate: setupClaudeHooks },
32991
33228
  { key: "codex", label: "OpenAI Codex (AGENTS.md + skills + sync hooks)", setup: setupAgentsMd, integrate: setupCodexIntegration },
32992
33229
  { key: "grok", label: "Grok Build (AGENTS.md + Gipity plugin)", setup: setupAgentsMd, integrate: ensureGrokPluginInstalled },
33230
+ { key: "agy", label: "Antigravity (AGENTS.md + skills + sync hooks)", setup: setupAgentsMd, integrate: setupAgyIntegration },
32993
33231
  { key: "aider", label: "Aider (AGENTS.md + .aider.conf.yml)", setup: setupAiderMd, optIn: true },
32994
33232
  { key: "gemini", label: "Gemini CLI (GEMINI.md)", setup: setupGeminiMd },
32995
33233
  { key: "copilot", label: "GitHub Copilot (.github/copilot-instructions.md)", setup: setupCopilotMd },
@@ -33007,7 +33245,7 @@ function setupGitignore() {
33007
33245
  const gitignorePath = resolve3(process.cwd(), ".gitignore");
33008
33246
  const entries = [".gipity/", ".gipity.json", ...SCRATCH_IGNORE];
33009
33247
  if (existsSync4(gitignorePath)) {
33010
- let content = readFileSync5(gitignorePath, "utf-8");
33248
+ let content = readFileSync6(gitignorePath, "utf-8");
33011
33249
  const lines = content.split(/\r?\n/);
33012
33250
  const toAdd = entries.filter((e) => !lines.includes(e));
33013
33251
  if (toAdd.length > 0) {
@@ -33035,11 +33273,11 @@ var BULK_DELETE_FRACTION = 0.25;
33035
33273
  var DOWNLOAD_IDLE_MS = 3e4;
33036
33274
  function syncStatePath() {
33037
33275
  const configPath = getConfigPath();
33038
- return join4(dirname4(configPath), ".gipity", "sync-state.json");
33276
+ return join5(dirname4(configPath), ".gipity", "sync-state.json");
33039
33277
  }
33040
33278
  function lockPath() {
33041
33279
  const configPath = getConfigPath();
33042
- return join4(dirname4(configPath), ".gipity", "sync.lock");
33280
+ return join5(dirname4(configPath), ".gipity", "sync.lock");
33043
33281
  }
33044
33282
  function projectDir() {
33045
33283
  const configPath = getConfigPath();
@@ -33053,8 +33291,8 @@ function isLockReclaimable(path5, now = Date.now()) {
33053
33291
  let raw;
33054
33292
  let mtimeMs;
33055
33293
  try {
33056
- raw = readFileSync6(path5, "utf-8").trim();
33057
- mtimeMs = statSync(path5).mtimeMs;
33294
+ raw = readFileSync7(path5, "utf-8").trim();
33295
+ mtimeMs = statSync2(path5).mtimeMs;
33058
33296
  } catch {
33059
33297
  return false;
33060
33298
  }
@@ -33113,28 +33351,28 @@ async function acquireLock(progress) {
33113
33351
  );
33114
33352
  }
33115
33353
  if (!waitSpinner) {
33116
- waitSpinner = progress?.spinner("Waiting for another sync to finish\u2026") ?? null;
33354
+ waitSpinner = progress?.spinner("Waiting for another sync to finish...") ?? null;
33117
33355
  }
33118
33356
  await new Promise((r) => setTimeout(r, LOCK_POLL_MS2));
33119
33357
  }
33120
33358
  }
33121
33359
  }
33122
- function readBaseline(projectGuid) {
33360
+ function readBaseline(projectGuid2) {
33123
33361
  const path5 = syncStatePath();
33124
- if (!existsSync5(path5)) return { projectGuid, files: {}, lastFullSync: null };
33362
+ if (!existsSync5(path5)) return { projectGuid: projectGuid2, files: {}, lastFullSync: null };
33125
33363
  try {
33126
- const parsed = JSON.parse(readFileSync6(path5, "utf-8"));
33127
- if (parsed.projectGuid !== projectGuid) {
33128
- return { projectGuid, files: {}, lastFullSync: null };
33364
+ const parsed = JSON.parse(readFileSync7(path5, "utf-8"));
33365
+ if (parsed.projectGuid !== projectGuid2) {
33366
+ return { projectGuid: projectGuid2, files: {}, lastFullSync: null };
33129
33367
  }
33130
33368
  return {
33131
- projectGuid,
33369
+ projectGuid: projectGuid2,
33132
33370
  files: parsed.files ?? {},
33133
33371
  lastFullSync: parsed.lastFullSync ?? null,
33134
33372
  ...parsed.deletesSkippedStreak ? { deletesSkippedStreak: parsed.deletesSkippedStreak } : {}
33135
33373
  };
33136
33374
  } catch {
33137
- return { projectGuid, files: {}, lastFullSync: null };
33375
+ return { projectGuid: projectGuid2, files: {}, lastFullSync: null };
33138
33376
  }
33139
33377
  }
33140
33378
  function writeBaseline(b7) {
@@ -33147,25 +33385,25 @@ function walkLocal(root, ignorePatterns, baseline) {
33147
33385
  function walk(dir) {
33148
33386
  let entries;
33149
33387
  try {
33150
- entries = readdirSync3(dir, { withFileTypes: true });
33388
+ entries = readdirSync4(dir, { withFileTypes: true });
33151
33389
  } catch {
33152
33390
  return;
33153
33391
  }
33154
33392
  for (const entry of entries) {
33155
- const full = join4(dir, entry.name);
33393
+ const full = join5(dir, entry.name);
33156
33394
  const rel2 = relative(root, full).replace(/\\/g, "/");
33157
33395
  if (shouldIgnore(rel2, ignorePatterns)) continue;
33158
33396
  if (entry.isDirectory()) {
33159
- if (existsSync5(join4(full, CONFIG_FILE2))) continue;
33397
+ if (existsSync5(join5(full, CONFIG_FILE2))) continue;
33160
33398
  walk(full);
33161
33399
  } else if (entry.isFile()) {
33162
33400
  try {
33163
- const stat2 = statSync(full);
33401
+ const stat2 = statSync2(full);
33164
33402
  const size = stat2.size;
33165
33403
  const mtime = stat2.mtime.toISOString();
33166
33404
  const prior = baseline[rel2];
33167
- const sha2562 = prior && prior.size === size && prior.mtime === mtime ? prior.sha256 : void 0;
33168
- result.set(rel2, { size, mtime, sha256: sha2562 });
33405
+ const sha256 = prior && prior.size === size && prior.mtime === mtime ? prior.sha256 : void 0;
33406
+ result.set(rel2, { size, mtime, sha256 });
33169
33407
  } catch {
33170
33408
  }
33171
33409
  }
@@ -33179,8 +33417,8 @@ async function ensureLocalHashes(root, local, paths) {
33179
33417
  const info2 = local.get(path5);
33180
33418
  if (!info2 || info2.sha256) continue;
33181
33419
  try {
33182
- const { sha256: sha2562 } = await hashFile(join4(root, path5));
33183
- info2.sha256 = sha2562;
33420
+ const { sha256 } = await hashFile(join5(root, path5));
33421
+ info2.sha256 = sha256;
33184
33422
  } catch {
33185
33423
  }
33186
33424
  }
@@ -33195,10 +33433,10 @@ function resolveInRoot(root, relPath) {
33195
33433
  throw new Error(`Refusing path outside project root: ${relPath}`);
33196
33434
  }
33197
33435
  try {
33198
- const rootReal = realpathSync(rootResolved);
33436
+ const rootReal = realpathSync2(rootResolved);
33199
33437
  let ancestor = full;
33200
33438
  while (!existsSync5(ancestor) && dirname4(ancestor) !== ancestor) ancestor = dirname4(ancestor);
33201
- const ancestorReal = realpathSync(ancestor);
33439
+ const ancestorReal = realpathSync2(ancestor);
33202
33440
  if (ancestorReal !== rootReal && !ancestorReal.startsWith(rootReal + sep)) {
33203
33441
  throw new Error(`Refusing path outside project root (symlink escape): ${relPath}`);
33204
33442
  }
@@ -33207,8 +33445,8 @@ function resolveInRoot(root, relPath) {
33207
33445
  }
33208
33446
  return full;
33209
33447
  }
33210
- async function fetchRemote(projectGuid) {
33211
- const res = await get(`/projects/${projectGuid}/files/tree`);
33448
+ async function fetchRemote(projectGuid2) {
33449
+ const res = await get(`/projects/${projectGuid2}/files/tree`);
33212
33450
  const out = /* @__PURE__ */ new Map();
33213
33451
  for (const f of res.data) {
33214
33452
  if (f.type !== "file") continue;
@@ -33269,14 +33507,14 @@ function extractTarToMap(stream, idleMs, onBytes, keep) {
33269
33507
  stream.pipe(extract3);
33270
33508
  });
33271
33509
  }
33272
- async function downloadAll(projectGuid, onBytes) {
33273
- const stream = await downloadStream(`/projects/${projectGuid}/files/tree?content=tar`);
33510
+ async function downloadAll(projectGuid2, onBytes) {
33511
+ const stream = await downloadStream(`/projects/${projectGuid2}/files/tree?content=tar`);
33274
33512
  return extractTarToMap(stream, DOWNLOAD_IDLE_MS, onBytes);
33275
33513
  }
33276
- async function fetchOneOutcome(projectGuid, path5, expectedSha) {
33514
+ async function fetchOneOutcome(projectGuid2, path5, expectedSha) {
33277
33515
  try {
33278
33516
  const stream = await downloadStream(
33279
- `/projects/${projectGuid}/files/tree?content=tar&path=${encodeURIComponent(path5)}`
33517
+ `/projects/${projectGuid2}/files/tree?content=tar&path=${encodeURIComponent(path5)}`
33280
33518
  );
33281
33519
  const want = normalizeTreePath(path5);
33282
33520
  const files = await extractTarToMap(stream, DOWNLOAD_IDLE_MS, void 0, (p) => p === want);
@@ -33292,8 +33530,8 @@ async function fetchOneOutcome(projectGuid, path5, expectedSha) {
33292
33530
  return { kind: "transient" };
33293
33531
  }
33294
33532
  }
33295
- async function fetchOne(projectGuid, path5, expectedSha) {
33296
- const out = await fetchOneOutcome(projectGuid, path5, expectedSha);
33533
+ async function fetchOne(projectGuid2, path5, expectedSha) {
33534
+ const out = await fetchOneOutcome(projectGuid2, path5, expectedSha);
33297
33535
  return out.kind === "ok" ? out.buf : null;
33298
33536
  }
33299
33537
  function classifyLocal(info2, base) {
@@ -33486,9 +33724,9 @@ Plan deletes ${totalDeletes} files (${Math.round(fraction * 100)}% of the tree).
33486
33724
  }
33487
33725
  var GIPITY_IGNORE_FILE = ".gipityignore";
33488
33726
  function readGipityIgnore(root) {
33489
- const path5 = join4(root, GIPITY_IGNORE_FILE);
33727
+ const path5 = join5(root, GIPITY_IGNORE_FILE);
33490
33728
  if (!existsSync5(path5)) return [];
33491
- return readFileSync6(path5, "utf8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => line.replace(/^\.\//, "").replace(/^\//, ""));
33729
+ return readFileSync7(path5, "utf8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => line.replace(/^\.\//, "").replace(/^\//, ""));
33492
33730
  }
33493
33731
  function effectiveIgnore(root, configIgnore) {
33494
33732
  const base = configIgnore && configIgnore.length ? configIgnore : DEFAULT_SYNC_IGNORE;
@@ -33525,14 +33763,14 @@ async function sync(opts = {}) {
33525
33763
  releaseLock();
33526
33764
  }
33527
33765
  }
33528
- async function syncInner(projectGuid, root, ignore2, opts, interactive) {
33529
- const config = { projectGuid, ignore: ignore2 };
33766
+ async function syncInner(projectGuid2, root, ignore2, opts, interactive) {
33767
+ const config = { projectGuid: projectGuid2, ignore: ignore2 };
33530
33768
  const p = opts.progress;
33531
- const baseline = readBaseline(projectGuid);
33532
- p?.phase("Scanning local files\u2026");
33769
+ const baseline = readBaseline(projectGuid2);
33770
+ p?.phase("Scanning local files...");
33533
33771
  const local = walkLocal(root, ignore2, baseline.files);
33534
- p?.phase("Checking Gipity for changes\u2026");
33535
- const remote = await fetchRemote(projectGuid);
33772
+ p?.phase("Checking Gipity for changes...");
33773
+ const remote = await fetchRemote(projectGuid2);
33536
33774
  for (const path5 of [...remote.keys()]) {
33537
33775
  if (shouldIgnore(path5, ignore2)) remote.delete(path5);
33538
33776
  }
@@ -33542,7 +33780,7 @@ async function syncInner(projectGuid, root, ignore2, opts, interactive) {
33542
33780
  const r = remote.get(path5);
33543
33781
  if (r?.sha256 || baseline.files[path5]) needHash.push(path5);
33544
33782
  }
33545
- if (needHash.length) p?.phase(`Hashing ${needHash.length} file${needHash.length === 1 ? "" : "s"}\u2026`);
33783
+ if (needHash.length) p?.phase(`Hashing ${needHash.length} file${needHash.length === 1 ? "" : "s"}...`);
33546
33784
  await ensureLocalHashes(root, local, needHash);
33547
33785
  const planned = plan(local, remote, baseline.files);
33548
33786
  if (opts.plan) {
@@ -33628,13 +33866,13 @@ async function syncInner(projectGuid, root, ignore2, opts, interactive) {
33628
33866
  if (buf) downloadedBytes.set(a.path, buf);
33629
33867
  }
33630
33868
  } catch (err) {
33631
- errors.push(`Bulk download incomplete (${err.message}); recovering files individually\u2026`);
33869
+ errors.push(`Bulk download incomplete (${err.message}); recovering files individually...`);
33632
33870
  } finally {
33633
33871
  p?.finish();
33634
33872
  }
33635
33873
  const missing = wantedDownloads.filter((a) => !downloadedBytes.has(a.path));
33636
33874
  if (missing.length) {
33637
- p?.phase(`Recovering ${missing.length} file${missing.length === 1 ? "" : "s"} the bulk download dropped\u2026`);
33875
+ p?.phase(`Recovering ${missing.length} file${missing.length === 1 ? "" : "s"} the bulk download dropped...`);
33638
33876
  for (const a of missing) {
33639
33877
  const expectedSha = remote.get(a.path)?.sha256 ?? void 0;
33640
33878
  let buf = null;
@@ -33676,7 +33914,7 @@ async function syncInner(projectGuid, root, ignore2, opts, interactive) {
33676
33914
  if (currentBytes) {
33677
33915
  mkdirSync4(dirname4(full), { recursive: true });
33678
33916
  writeFileSync5(full, currentBytes);
33679
- const stat2 = statSync(full);
33917
+ const stat2 = statSync2(full);
33680
33918
  baseline.files[a.path] = {
33681
33919
  size: stat2.size,
33682
33920
  mtime: stat2.mtime.toISOString(),
@@ -33692,12 +33930,12 @@ async function syncInner(projectGuid, root, ignore2, opts, interactive) {
33692
33930
  renamedRel,
33693
33931
  { expectedServerVersion: null }
33694
33932
  );
33695
- const stat2 = statSync(renamedFull);
33696
- const { sha256: sha2562 } = await hashFile(renamedFull);
33933
+ const stat2 = statSync2(renamedFull);
33934
+ const { sha256 } = await hashFile(renamedFull);
33697
33935
  baseline.files[renamedRel] = {
33698
33936
  size: stat2.size,
33699
33937
  mtime: stat2.mtime.toISOString(),
33700
- sha256: sha2562,
33938
+ sha256,
33701
33939
  serverVersion: result.serverVersion
33702
33940
  };
33703
33941
  } catch (e) {
@@ -33733,7 +33971,7 @@ async function syncInner(projectGuid, root, ignore2, opts, interactive) {
33733
33971
  }
33734
33972
  mkdirSync4(dirname4(full), { recursive: true });
33735
33973
  writeFileSync5(full, buf);
33736
- const stat2 = statSync(full);
33974
+ const stat2 = statSync2(full);
33737
33975
  local.set(a.path, { size: stat2.size, mtime: stat2.mtime.toISOString(), sha256: void 0 });
33738
33976
  baseline.files[a.path] = {
33739
33977
  size: stat2.size,
@@ -33764,7 +34002,7 @@ async function syncInner(projectGuid, root, ignore2, opts, interactive) {
33764
34002
  if (buf) {
33765
34003
  mkdirSync4(dirname4(full), { recursive: true });
33766
34004
  writeFileSync5(full, buf);
33767
- const stat2 = statSync(full);
34005
+ const stat2 = statSync2(full);
33768
34006
  baseline.files[a.path] = {
33769
34007
  size: stat2.size,
33770
34008
  mtime: stat2.mtime.toISOString(),
@@ -33778,12 +34016,12 @@ async function syncInner(projectGuid, root, ignore2, opts, interactive) {
33778
34016
  const result = await uploadOneFile(config.projectGuid, renamed, a.renamedLocalTo, {
33779
34017
  expectedServerVersion: null
33780
34018
  });
33781
- const stat2 = statSync(renamed);
33782
- const { sha256: sha2562 } = await hashFile(renamed);
34019
+ const stat2 = statSync2(renamed);
34020
+ const { sha256 } = await hashFile(renamed);
33783
34021
  baseline.files[a.renamedLocalTo] = {
33784
34022
  size: stat2.size,
33785
34023
  mtime: stat2.mtime.toISOString(),
33786
- sha256: sha2562,
34024
+ sha256,
33787
34025
  serverVersion: result.serverVersion
33788
34026
  };
33789
34027
  } catch (err) {
@@ -33817,14 +34055,14 @@ async function syncInner(projectGuid, root, ignore2, opts, interactive) {
33817
34055
  continue;
33818
34056
  }
33819
34057
  try {
33820
- const stat2 = statSync(full);
34058
+ const stat2 = statSync2(full);
33821
34059
  if (stat2.size > UPLOAD_MAX_BYTES) {
33822
34060
  errors.push(`Upload failed for ${a.path}: file exceeds the 30 GB upload limit`);
33823
34061
  onBytes?.(a.localSize ?? 0);
33824
34062
  continue;
33825
34063
  }
33826
- const sha2562 = local.get(a.path)?.sha256 ?? (await hashFile(full)).sha256;
33827
- prepared.push({ a, full, size: stat2.size, mtime: stat2.mtime.toISOString(), sha256: sha2562 });
34064
+ const sha256 = local.get(a.path)?.sha256 ?? (await hashFile(full)).sha256;
34065
+ prepared.push({ a, full, size: stat2.size, mtime: stat2.mtime.toISOString(), sha256 });
33828
34066
  } catch (e) {
33829
34067
  errors.push(`Upload failed for ${a.path}: ${e.message}`);
33830
34068
  onBytes?.(a.localSize ?? 0);
@@ -33971,13 +34209,13 @@ async function syncInner(projectGuid, root, ignore2, opts, interactive) {
33971
34209
  const full = resolveInRoot(root, a.path);
33972
34210
  mkdirSync4(dirname4(full), { recursive: true });
33973
34211
  writeFileSync5(full, buf);
33974
- const stat2 = statSync(full);
33975
- const { sha256: sha2562 } = await hashFile(full);
34212
+ const stat2 = statSync2(full);
34213
+ const { sha256 } = await hashFile(full);
33976
34214
  const current = typeof err.data?.current_server_version === "number" ? err.data.current_server_version : null;
33977
34215
  baseline.files[a.path] = {
33978
34216
  size: stat2.size,
33979
34217
  mtime: stat2.mtime.toISOString(),
33980
- sha256: sha2562,
34218
+ sha256,
33981
34219
  serverVersion: current ?? a.expectedServerVersion ?? 0
33982
34220
  };
33983
34221
  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)`);
@@ -34021,7 +34259,7 @@ async function syncInner(projectGuid, root, ignore2, opts, interactive) {
34021
34259
  function cleanupEmptyDirs(root, emptiedDirs) {
34022
34260
  const isEmpty = (dir) => {
34023
34261
  try {
34024
- return readdirSync3(dir).length === 0;
34262
+ return readdirSync4(dir).length === 0;
34025
34263
  } catch {
34026
34264
  return false;
34027
34265
  }
@@ -34056,12 +34294,12 @@ async function pushFile(filePath) {
34056
34294
  const result = await uploadOneFile(config.projectGuid, filePath, rel2, {
34057
34295
  expectedServerVersion: baseEntry ? baseEntry.serverVersion : null
34058
34296
  });
34059
- const stat2 = statSync(filePath);
34060
- const { sha256: sha2562 } = await hashFile(filePath);
34297
+ const stat2 = statSync2(filePath);
34298
+ const { sha256 } = await hashFile(filePath);
34061
34299
  baseline.files[rel2] = {
34062
34300
  size: stat2.size,
34063
34301
  mtime: stat2.mtime.toISOString(),
34064
- sha256: sha2562,
34302
+ sha256,
34065
34303
  serverVersion: result.serverVersion
34066
34304
  };
34067
34305
  writeBaseline(baseline);
@@ -34249,11 +34487,11 @@ async function syncBeforeAction(opts) {
34249
34487
  }
34250
34488
 
34251
34489
  // src/helpers/body.ts
34252
- import { readFileSync as readFileSync7 } from "node:fs";
34490
+ import { readFileSync as readFileSync8 } from "node:fs";
34253
34491
  function readStdin() {
34254
34492
  if (process.stdin.isTTY) return "";
34255
34493
  try {
34256
- return readFileSync7(0, "utf-8");
34494
+ return readFileSync8(0, "utf-8");
34257
34495
  } catch {
34258
34496
  return "";
34259
34497
  }
@@ -34269,7 +34507,7 @@ function readFileField(spec) {
34269
34507
  if (!field) throw new Error(`Invalid --file '${spec}': missing field name before '='.`);
34270
34508
  if (!path5) throw new Error(`Invalid --file '${spec}': missing file path after '='.`);
34271
34509
  try {
34272
- return [field, { data: readFileSync7(path5).toString("base64"), media_type: guessMime(path5) }];
34510
+ return [field, { data: readFileSync8(path5).toString("base64"), media_type: guessMime(path5) }];
34273
34511
  } catch (e) {
34274
34512
  throw new Error(`Cannot read --file '${path5}': ${e.message}`);
34275
34513
  }
@@ -34300,7 +34538,7 @@ function resolveJsonBody(raw) {
34300
34538
  } else if (raw.startsWith("@")) {
34301
34539
  const path5 = raw.slice(1);
34302
34540
  try {
34303
- source = readFileSync7(path5, "utf-8");
34541
+ source = readFileSync8(path5, "utf-8");
34304
34542
  } catch (e) {
34305
34543
  throw new Error(`Cannot read body file '${path5}': ${e.message}`);
34306
34544
  }
@@ -34332,7 +34570,12 @@ function parseDuration(raw, unit) {
34332
34570
  }
34333
34571
 
34334
34572
  // src/commands/token.ts
34335
- var tokenCommand = new Command("token").description("Manage API tokens").addHelpText("after", "\nLong-lived agent API tokens (gip_at_*) for headless agents and CI.");
34573
+ var tokenCommand = new Command("token").description("Manage API tokens").addHelpText("after", `
34574
+ Long-lived agent API tokens (gip_at_*) for headless agents and CI - they drive
34575
+ the gipity CLI as YOU.
34576
+
34577
+ To let a script or cron write to one app instead, mint a project API key:
34578
+ gipity key create "my script" --role editor (sent as X-Api-Key).`);
34336
34579
  var fmtDate = (d) => d ? new Date(d).toLocaleDateString() : "never";
34337
34580
  tokenCommand.command("create").description("Mint a long-lived agent API token (shown once)").option("--name <name>", 'Label for the token, e.g. "Hermes on my VPS"').option("--expires <days>", "Days until the token expires (default: never)").option("--json", "Output as JSON").action((opts) => run("Create", async () => {
34338
34581
  const body = {};
@@ -34377,27 +34620,94 @@ tokenCommand.command("revoke <short_guid>").alias("rm").description("Revoke an a
34377
34620
  console.log(success(`Revoked token ${bold(shortGuid)}.`));
34378
34621
  }));
34379
34622
 
34623
+ // src/commands/key.ts
34624
+ init_api();
34625
+ init_config();
34626
+ init_colors();
34627
+ var keyCommand = new Command("key").description("Manage project API keys for scripts and agents (X-Api-Key)").addHelpText("after", `
34628
+ Give a script, cron, or agent write access to your app without a login:
34629
+
34630
+ gipity key create "laptop importer" --role editor
34631
+ # the script sends: X-Api-Key: <the key printed once above>
34632
+
34633
+ Inside a function the caller shows up as ctx.auth.via === 'api_key' with
34634
+ ctx.auth.apiKeyName set to the key's name - stamp that on rows to tell
34635
+ script-written entries from hand-entered ones. Never build your own key table.
34636
+
34637
+ Account-level tokens that run the CLI headlessly are a different thing: see
34638
+ gipity token --help.`);
34639
+ var fmtDate2 = (d) => d ? new Date(d).toLocaleDateString() : "never";
34640
+ var projectOpt = ["--project <guid-or-slug>", "Target a specific project instead of cwd / Home"];
34641
+ async function projectGuid(opts) {
34642
+ const { config } = await resolveProjectContext({ projectOverride: opts.project });
34643
+ return config.projectGuid;
34644
+ }
34645
+ keyCommand.command("create <name>").description("Mint a project API key (shown once). Give it a name you will recognize later").option("--role <role>", "viewer | editor | owner (default: viewer)", "viewer").option("--expires-days <n>", "Days until the key expires (default: never)").option(...projectOpt).option("--json", "Output as JSON").action((name, opts) => run("Create", async () => {
34646
+ const body = { name, role: opts.role };
34647
+ if (opts.expiresDays !== void 0) {
34648
+ const days = parseInt(opts.expiresDays, 10);
34649
+ if (!Number.isFinite(days) || days <= 0) throw new Error("--expires-days must be a positive number of days");
34650
+ body.expires_in_days = days;
34651
+ }
34652
+ const res = await post(
34653
+ `/projects/${await projectGuid(opts)}/api-keys`,
34654
+ body
34655
+ );
34656
+ const k7 = res.data;
34657
+ if (opts.json) {
34658
+ console.log(JSON.stringify(k7));
34659
+ return;
34660
+ }
34661
+ const expNote = k7.expires_at ? ` (expires ${fmtDate2(k7.expires_at)})` : " (never expires)";
34662
+ console.log(success(`Created API key ${bold(k7.short_guid)} "${k7.name}" as ${k7.role}${muted(expNote)}.`));
34663
+ console.log("");
34664
+ console.log(k7.key);
34665
+ console.log("");
34666
+ console.log(muted("Send it from your script on every request:"));
34667
+ console.log(muted(` curl -H "X-Api-Key: ${k7.key}" ...`));
34668
+ console.log(muted(`Revoke it any time: gipity key revoke ${k7.short_guid}`));
34669
+ console.log("");
34670
+ console.log(warning("Copy it now - it will not be shown again."));
34671
+ }));
34672
+ keyCommand.command("list").alias("ls").description("List this project's API keys (values are never shown again)").option(...projectOpt).option("--json", "Output as JSON").action((opts) => run("List", async () => {
34673
+ const res = await get(`/projects/${await projectGuid(opts)}/api-keys`);
34674
+ printList(
34675
+ res.data,
34676
+ opts,
34677
+ 'No API keys. Mint one with: gipity key create "my script" --role editor',
34678
+ (k7) => `${bold(k7.short_guid)} ${k7.name} ${muted(`${k7.role} ${k7.prefix}\u2026 last used ${fmtDate2(k7.last_used_at)} expires ${fmtDate2(k7.expires_at)}`)}`
34679
+ );
34680
+ }));
34681
+ keyCommand.command("revoke <short_guid>").alias("rm").description("Revoke a project API key (instant, irreversible)").option(...projectOpt).option("--json", "Output as JSON").action((shortGuid, opts) => run("Revoke", async () => {
34682
+ await del(`/projects/${await projectGuid(opts)}/api-keys/${encodeURIComponent(shortGuid)}`);
34683
+ if (opts.json) {
34684
+ console.log(JSON.stringify({ short_guid: shortGuid, revoked: true }));
34685
+ return;
34686
+ }
34687
+ console.log(success(`Revoked API key ${bold(shortGuid)}.`));
34688
+ }));
34689
+
34380
34690
  // src/commands/init.ts
34381
34691
  init_api();
34382
34692
  init_config();
34383
34693
  init_auth();
34384
- import { basename, resolve as resolve7, dirname as dirname5 } from "path";
34385
- import { existsSync as existsSync8, readFileSync as readFileSync10 } from "fs";
34694
+ import { basename as basename2, resolve as resolve7, dirname as dirname5 } from "path";
34695
+ import { existsSync as existsSync8, readFileSync as readFileSync11 } from "fs";
34386
34696
  init_colors();
34387
34697
  init_utils();
34388
34698
 
34389
34699
  // src/adopt-cwd.ts
34390
34700
  init_api();
34391
- import { readdirSync as readdirSync5, statSync as statSync3 } from "fs";
34392
- import { join as join8, resolve as resolve6, sep as sep2, parse as parsePath } from "path";
34701
+ import { readdirSync as readdirSync6, statSync as statSync4 } from "fs";
34702
+ import { join as join9, resolve as resolve6, sep as sep2, parse as parsePath } from "path";
34393
34703
  import { homedir as homedir6 } from "os";
34394
34704
 
34395
34705
  // src/project-setup.ts
34396
34706
  init_config();
34397
34707
 
34398
34708
  // src/template-vars.ts
34399
- import { promises as fs, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
34400
- import { join as join5, relative as relative2, extname as extname3 } from "path";
34709
+ import { promises as fs, readdirSync as readdirSync5, statSync as statSync3 } from "fs";
34710
+ import { join as join6, relative as relative2, extname as extname3 } from "path";
34401
34711
  var SUBSTITUTABLE_EXTS = /* @__PURE__ */ new Set([
34402
34712
  ".html",
34403
34713
  ".htm",
@@ -34422,6 +34732,80 @@ function escapeHtml(s) {
34422
34732
  function jsEscape(s) {
34423
34733
  return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
34424
34734
  }
34735
+ var ACCENT_PALETTE = [
34736
+ "#f59e0b",
34737
+ "#f97316",
34738
+ "#ef4444",
34739
+ "#ec4899",
34740
+ "#a855f7",
34741
+ "#6366f1",
34742
+ "#3b82f6",
34743
+ "#0ea5e9",
34744
+ "#14b8a6",
34745
+ "#22c55e",
34746
+ "#84cc16",
34747
+ "#eab308"
34748
+ ];
34749
+ function hashString(s) {
34750
+ let h = 2166136261;
34751
+ for (let i = 0; i < s.length; i++) {
34752
+ h ^= s.charCodeAt(i);
34753
+ h = Math.imul(h, 16777619);
34754
+ }
34755
+ return h >>> 0;
34756
+ }
34757
+ function darkTint(hex) {
34758
+ const n = parseInt(hex.slice(1), 16);
34759
+ const base = 1052692;
34760
+ const ch = (shift) => {
34761
+ const a = n >> shift & 255;
34762
+ const b7 = base >> shift & 255;
34763
+ return Math.round(b7 + (a - b7) * 0.14).toString(16).padStart(2, "0");
34764
+ };
34765
+ return `#${ch(16)}${ch(8)}${ch(0)}`;
34766
+ }
34767
+ function buildHeadBlock(v7) {
34768
+ const t = escapeHtml(v7.projectName);
34769
+ const d = v7.description ? escapeHtml(v7.description) : "";
34770
+ const url = v7.accountSlug && v7.projectSlug ? `https://app.gipity.ai/${v7.accountSlug}/${v7.projectSlug}/` : void 0;
34771
+ const themeColor = darkTint(ACCENT_PALETTE[hashString(v7.projectGuid || v7.projectName) % ACCENT_PALETTE.length]);
34772
+ const jsonLd = JSON.stringify({
34773
+ "@context": "https://schema.org",
34774
+ "@type": "WebApplication",
34775
+ name: v7.projectName,
34776
+ ...v7.description ? { description: v7.description } : {},
34777
+ ...url ? { url } : {}
34778
+ }, null, 2).replace(/<\//g, "<\\/");
34779
+ const lines = [
34780
+ `<title>${t}</title>`,
34781
+ ...d ? [`<meta name="description" content="${d}">`] : [],
34782
+ ...url ? [`<link rel="canonical" href="${url}">`] : [],
34783
+ `<meta property="og:title" content="${t}">`,
34784
+ `<meta property="og:type" content="website">`,
34785
+ ...d ? [`<meta property="og:description" content="${d}">`] : [],
34786
+ ...url ? [
34787
+ `<meta property="og:url" content="${url}">`,
34788
+ `<meta property="og:image" content="${url}images/og-image.png">`,
34789
+ `<meta property="og:image:width" content="1200">`,
34790
+ `<meta property="og:image:height" content="630">`
34791
+ ] : [],
34792
+ `<meta name="twitter:card" content="summary_large_image">`,
34793
+ `<meta name="twitter:title" content="${t}">`,
34794
+ ...d ? [`<meta name="twitter:description" content="${d}">`] : [],
34795
+ ...url ? [`<meta name="twitter:image" content="${url}images/og-image.png">`] : [],
34796
+ `<meta name="theme-color" content="${themeColor}">`,
34797
+ `<link rel="icon" type="image/png" sizes="192x192" href="./images/favicon-192.png">`,
34798
+ `<link rel="icon" type="image/png" sizes="512x512" href="./images/favicon-512.png">`,
34799
+ `<link rel="icon" type="image/x-icon" href="./images/favicon.ico">`,
34800
+ `<link rel="apple-touch-icon" href="./images/apple-touch-icon.png">`,
34801
+ `<link rel="manifest" href="./manifest.webmanifest">`,
34802
+ `<script type="application/ld+json">
34803
+ ${jsonLd}
34804
+ </script>`
34805
+ ];
34806
+ return lines.map((l7) => `
34807
+ ${l7}`).join("");
34808
+ }
34425
34809
  function buildTemplateVars(v7) {
34426
34810
  const safeTitle = escapeHtml(v7.projectName);
34427
34811
  const safeDesc = v7.description ? escapeHtml(v7.description) : "";
@@ -34437,6 +34821,9 @@ function buildTemplateVars(v7) {
34437
34821
  "{{JS_TITLE}}": jsEscape(v7.projectName),
34438
34822
  "{{PROJECT_GUID}}": v7.projectGuid,
34439
34823
  "{{DATABASE}}": slug,
34824
+ "{{HEAD_BLOCK}}": buildHeadBlock(v7),
34825
+ // Legacy per-tag placeholders — current templates carry only {{HEAD_BLOCK}},
34826
+ // but older local template copies may still reference these.
34440
34827
  "{{DESCRIPTION_META}}": v7.description ? `
34441
34828
  <meta name="description" content="${safeDesc}">` : "",
34442
34829
  "{{OG_DESCRIPTION}}": v7.description ? `
@@ -34463,18 +34850,18 @@ function* walkTextFiles(root) {
34463
34850
  const dir = stack.pop();
34464
34851
  let entries;
34465
34852
  try {
34466
- entries = readdirSync4(dir, { withFileTypes: true });
34853
+ entries = readdirSync5(dir, { withFileTypes: true });
34467
34854
  } catch {
34468
34855
  continue;
34469
34856
  }
34470
34857
  for (const entry of entries) {
34471
34858
  if (isSyncIgnored(entry.name)) continue;
34472
- const full = join5(dir, entry.name);
34859
+ const full = join6(dir, entry.name);
34473
34860
  if (entry.isDirectory()) {
34474
34861
  stack.push(full);
34475
34862
  } else if (entry.isFile()) {
34476
34863
  try {
34477
- if (!statSync2(full).isFile()) continue;
34864
+ if (!statSync3(full).isFile()) continue;
34478
34865
  } catch {
34479
34866
  continue;
34480
34867
  }
@@ -34491,7 +34878,7 @@ async function substituteDir(dir, vars) {
34491
34878
  for (const rel2 of walkTextFiles(dir)) {
34492
34879
  const ext = extname3(rel2).toLowerCase();
34493
34880
  if (!SUBSTITUTABLE_EXTS.has(ext)) continue;
34494
- const abs = join5(dir, rel2);
34881
+ const abs = join6(dir, rel2);
34495
34882
  let content;
34496
34883
  try {
34497
34884
  content = await fs.readFile(abs, "utf-8");
@@ -34531,7 +34918,9 @@ async function finalizeLocalProject(opts) {
34531
34918
  try {
34532
34919
  const sub = await substituteDir(opts.dir, {
34533
34920
  projectGuid: opts.projectGuid,
34534
- projectName: opts.projectName
34921
+ projectName: opts.projectName,
34922
+ accountSlug: opts.accountSlug,
34923
+ projectSlug: opts.projectSlug
34535
34924
  });
34536
34925
  if (sub.changed.length) {
34537
34926
  console.log(muted(`Resolved template vars in ${sub.changed.length} file${sub.changed.length > 1 ? "s" : ""}.`));
@@ -34557,18 +34946,18 @@ async function finalizeLocalProject(opts) {
34557
34946
  }
34558
34947
 
34559
34948
  // src/relay/paths.ts
34560
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
34949
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
34561
34950
  import { homedir as homedir4 } from "os";
34562
- import { join as join6, resolve as resolve5 } from "path";
34951
+ import { join as join7, resolve as resolve5 } from "path";
34563
34952
  function getProjectsRoot() {
34564
- const settingsPath = join6(homedir4(), ".gipity", "settings.json");
34565
- const defaultDir = join6(homedir4(), "GipityProjects");
34953
+ const settingsPath = join7(homedir4(), ".gipity", "settings.json");
34954
+ const defaultDir = join7(homedir4(), "GipityProjects");
34566
34955
  try {
34567
34956
  if (existsSync6(settingsPath)) {
34568
- const settings = JSON.parse(readFileSync8(settingsPath, "utf-8"));
34957
+ const settings = JSON.parse(readFileSync9(settingsPath, "utf-8"));
34569
34958
  if (settings.projectsDir) return resolve5(settings.projectsDir);
34570
34959
  } else {
34571
- mkdirSync5(join6(homedir4(), ".gipity"), { recursive: true });
34960
+ mkdirSync5(join7(homedir4(), ".gipity"), { recursive: true });
34572
34961
  writeFileSync6(settingsPath, JSON.stringify({ projectsDir: defaultDir }, null, 2) + "\n");
34573
34962
  }
34574
34963
  } catch {
@@ -34577,11 +34966,11 @@ function getProjectsRoot() {
34577
34966
  }
34578
34967
 
34579
34968
  // src/relay/state.ts
34580
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, existsSync as existsSync7, chmodSync as chmodSync2, unlinkSync as unlinkSync4 } from "fs";
34581
- import { join as join7 } from "path";
34969
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, existsSync as existsSync7, chmodSync as chmodSync2, unlinkSync as unlinkSync4 } from "fs";
34970
+ import { join as join8 } from "path";
34582
34971
  import { homedir as homedir5 } from "os";
34583
- var RELAY_DIR = process.env.GIPITY_DIR || join7(homedir5(), ".gipity");
34584
- var RELAY_FILE = join7(RELAY_DIR, "relay.json");
34972
+ var RELAY_DIR = process.env.GIPITY_DIR || join8(homedir5(), ".gipity");
34973
+ var RELAY_FILE = join8(RELAY_DIR, "relay.json");
34585
34974
  var FILE_MODE = 384;
34586
34975
  function emptyState() {
34587
34976
  return { device: null, paused: false };
@@ -34589,7 +34978,7 @@ function emptyState() {
34589
34978
  function loadState() {
34590
34979
  if (!existsSync7(RELAY_FILE)) return emptyState();
34591
34980
  try {
34592
- const raw = JSON.parse(readFileSync9(RELAY_FILE, "utf-8"));
34981
+ const raw = JSON.parse(readFileSync10(RELAY_FILE, "utf-8"));
34593
34982
  return {
34594
34983
  device: raw.device ?? null,
34595
34984
  paused: Boolean(raw.paused),
@@ -34674,7 +35063,7 @@ function diagnosticsConsented() {
34674
35063
  if (env && env !== "0" && env.toLowerCase() !== "false") return false;
34675
35064
  return loadState().diagnostics_consent !== false;
34676
35065
  }
34677
- var RELAY_PID_FILE = join7(RELAY_DIR, "relay.pid");
35066
+ var RELAY_PID_FILE = join8(RELAY_DIR, "relay.pid");
34678
35067
  function getDaemonPidPath() {
34679
35068
  return RELAY_PID_FILE;
34680
35069
  }
@@ -34691,7 +35080,7 @@ function clearDaemonPid() {
34691
35080
  function isDaemonRunning() {
34692
35081
  if (!existsSync7(RELAY_PID_FILE)) return false;
34693
35082
  try {
34694
- const raw = readFileSync9(RELAY_PID_FILE, "utf-8").trim();
35083
+ const raw = readFileSync10(RELAY_PID_FILE, "utf-8").trim();
34695
35084
  const pid = parseInt(raw, 10);
34696
35085
  if (!pid || isNaN(pid)) {
34697
35086
  try {
@@ -34731,17 +35120,17 @@ function scanForAdoption(cwd) {
34731
35120
  if (truncated) return;
34732
35121
  let entries;
34733
35122
  try {
34734
- entries = readdirSync5(dir);
35123
+ entries = readdirSync6(dir);
34735
35124
  } catch {
34736
35125
  return;
34737
35126
  }
34738
35127
  for (const name of entries) {
34739
35128
  if (truncated) return;
34740
35129
  if (isSyncIgnored(name)) continue;
34741
- const full = join8(dir, name);
35130
+ const full = join9(dir, name);
34742
35131
  let st2;
34743
35132
  try {
34744
- st2 = statSync3(full);
35133
+ st2 = statSync4(full);
34745
35134
  } catch {
34746
35135
  continue;
34747
35136
  }
@@ -34767,7 +35156,7 @@ function scanForAdoption(cwd) {
34767
35156
  function isLikelyEmpty(cwd) {
34768
35157
  let entries;
34769
35158
  try {
34770
- entries = readdirSync5(cwd);
35159
+ entries = readdirSync6(cwd);
34771
35160
  } catch {
34772
35161
  return true;
34773
35162
  }
@@ -34805,18 +35194,18 @@ function canAdoptCwd(cwd) {
34805
35194
  function countGitRepoChildren(dir) {
34806
35195
  let entries;
34807
35196
  try {
34808
- entries = readdirSync5(dir);
35197
+ entries = readdirSync6(dir);
34809
35198
  } catch {
34810
35199
  return 0;
34811
35200
  }
34812
35201
  let count = 0;
34813
35202
  for (const name of entries.slice(0, 50)) {
34814
35203
  if (name.startsWith(".")) continue;
34815
- const child = join8(dir, name);
35204
+ const child = join9(dir, name);
34816
35205
  try {
34817
- if (!statSync3(child).isDirectory()) continue;
34818
- const gitPath = join8(child, ".git");
34819
- if (statSync3(gitPath)) count++;
35206
+ if (!statSync4(child).isDirectory()) continue;
35207
+ const gitPath = join9(child, ".git");
35208
+ if (statSync4(gitPath)) count++;
34820
35209
  } catch {
34821
35210
  }
34822
35211
  if (count >= 3) return count;
@@ -34830,7 +35219,7 @@ function formatCwdLabel(cwd) {
34830
35219
  if (norm.startsWith(home + sep2)) {
34831
35220
  const tail = norm.slice(home.length + 1).split(sep2);
34832
35221
  if (tail.length <= 2) return "~/" + tail.join("/");
34833
- return "~/\u2026/" + tail.slice(-2).join("/");
35222
+ return "~/.../" + tail.slice(-2).join("/");
34834
35223
  }
34835
35224
  const parts = norm.split(sep2).filter(Boolean);
34836
35225
  if (parts.length <= 1) return norm;
@@ -34917,7 +35306,7 @@ function resolveTools(forFlag) {
34917
35306
  (t) => requested.includes(t.key) || !t.optIn && requested.includes("all")
34918
35307
  );
34919
35308
  }
34920
- var initCommand = new Command("init").description("Link this directory to a project").addHelpText("after", "\nWrites CLAUDE.md/AGENTS.md primer files so your AI coding tool understands Gipity, and installs the Gipity skills + file-sync hooks into the agent CLIs found on this machine (Claude Code, Codex, Grok).").argument("[name]", "Project name/slug (defaults to current directory name)").option("--agent <guid>", "Agent GUID to use").option("--no-capture", "Don't record Claude Code sessions in this directory to your Gipity project (sets captureHooks: false in .gipity.json)").option(
35309
+ var initCommand = new Command("init").description("Link this directory to a project").addHelpText("after", "\nWrites CLAUDE.md/AGENTS.md primer files so your AI coding tool understands Gipity, and installs the Gipity skills + file-sync hooks into the agent CLIs found on this machine (Claude Code, Codex, Grok, Antigravity).").argument("[name]", "Project name/slug (defaults to current directory name)").option("--agent <guid>", "Agent GUID to use").option("--no-capture", "Don't record Claude Code sessions in this directory to your Gipity project (sets captureHooks: false in .gipity.json)").option(
34921
35310
  "--for <tools>",
34922
35311
  `Which AI tool primer files to write (comma-separated). Default: all except aider (opt-in - it also writes .aider.conf.yml). Choices: ${TOOL_KEYS.join(", ")}, all`
34923
35312
  ).addHelpText("after", `
@@ -34995,7 +35384,7 @@ Working with an existing Gipity project:
34995
35384
  process.exit(1);
34996
35385
  }
34997
35386
  }
34998
- const projectName = (name || basename(cwd)).slice(0, 100);
35387
+ const projectName = (name || basename2(cwd)).slice(0, 100);
34999
35388
  const projectSlug = slugify(projectName);
35000
35389
  if (!projectSlug) {
35001
35390
  console.error(error("Could not derive a valid project slug. Provide a name: gipity init my-app"));
@@ -35037,7 +35426,7 @@ Working with an existing Gipity project:
35037
35426
  if (adopted.applied > 0) console.log(`Synced ${adopted.applied} change${adopted.applied > 1 ? "s" : ""} with Gipity.`);
35038
35427
  if (opts.capture === false) {
35039
35428
  try {
35040
- const cfg = JSON.parse(readFileSync10(resolve7(cwd, ".gipity.json"), "utf-8"));
35429
+ const cfg = JSON.parse(readFileSync11(resolve7(cwd, ".gipity.json"), "utf-8"));
35041
35430
  cfg.captureHooks = false;
35042
35431
  saveConfigAt(cwd, cfg);
35043
35432
  } catch {
@@ -35045,7 +35434,7 @@ Working with an existing Gipity project:
35045
35434
  }
35046
35435
  console.log(success(`Wrote primer files: ${primerSummary}.`));
35047
35436
  if (wantsClaude) {
35048
- console.log(success("Ready! Run your coding agent here (claude, codex, grok), or `gipity build` to launch one with a picker."));
35437
+ console.log(success("Ready! Run your coding agent here (claude, codex, grok, agy), or `gipity build` to launch one with a picker."));
35049
35438
  if (opts.capture === false) {
35050
35439
  console.log(muted("Session recording is off for this project (captureHooks: false in .gipity.json)."));
35051
35440
  } else {
@@ -35071,15 +35460,15 @@ init_auth();
35071
35460
  init_api();
35072
35461
  init_config();
35073
35462
  init_colors();
35074
- import { existsSync as existsSync9, readFileSync as readFileSync11 } from "fs";
35075
- import { join as join9, resolve as resolve8 } from "path";
35463
+ import { existsSync as existsSync9, readFileSync as readFileSync12 } from "fs";
35464
+ import { join as join10, resolve as resolve8 } from "path";
35076
35465
  import { homedir as homedir7 } from "os";
35077
35466
  function checkGipityPlugin() {
35078
- const path5 = join9(homedir7(), ".claude", "settings.json");
35467
+ const path5 = join10(homedir7(), ".claude", "settings.json");
35079
35468
  let settings = {};
35080
35469
  if (existsSync9(path5)) {
35081
35470
  try {
35082
- settings = JSON.parse(readFileSync11(path5, "utf-8"));
35471
+ settings = JSON.parse(readFileSync12(path5, "utf-8"));
35083
35472
  } catch {
35084
35473
  }
35085
35474
  }
@@ -35092,12 +35481,14 @@ function checkGipityPlugin() {
35092
35481
  return { missing, ok: missing.length === 0, stale };
35093
35482
  }
35094
35483
  async function probeAuth(loggedIn) {
35095
- if (!loggedIn && !usingEnvToken()) return "none";
35096
- if (!usingEnvToken() && sessionExpired()) return "expired";
35097
- const timeout = new Promise((res) => setTimeout(() => res("unreachable"), 5e3).unref?.());
35484
+ if (!loggedIn && !usingEnvToken()) return { state: "none", account: null };
35485
+ if (!usingEnvToken() && sessionExpired()) return { state: "expired", account: null };
35486
+ const timeout = new Promise(
35487
+ (res) => setTimeout(() => res({ state: "unreachable", account: null }), 5e3).unref?.()
35488
+ );
35098
35489
  const call = get("/users/me").then(
35099
- () => "ok",
35100
- (err) => err instanceof ApiError && err.statusCode === 401 ? "rejected" : "unreachable"
35490
+ (res) => ({ state: "ok", account: res.data?.accountSlug ?? null }),
35491
+ (err) => ({ state: err instanceof ApiError && err.statusCode === 401 ? "rejected" : "unreachable", account: null })
35101
35492
  );
35102
35493
  return Promise.race([call, timeout]);
35103
35494
  }
@@ -35109,6 +35500,8 @@ var statusCommand = new Command("status").alias("whoami").description("Show proj
35109
35500
  const hookCheck = config ? checkGipityPlugin() : null;
35110
35501
  const queueDelivered = auth && !sessionExpired() ? await flushBugQueue().catch(() => 0) : 0;
35111
35502
  const probe = await probeAuth(!!auth);
35503
+ const accountMismatch = probe.state === "ok" && !!config && !!probe.account && probe.account !== config.accountSlug;
35504
+ const apiBaseInUse = resolveApiBase();
35112
35505
  if (opts.json) {
35113
35506
  console.log(JSON.stringify({
35114
35507
  project: config ? {
@@ -35116,17 +35509,21 @@ var statusCommand = new Command("status").alias("whoami").description("Show proj
35116
35509
  slug: config.projectSlug,
35117
35510
  account: config.accountSlug,
35118
35511
  apiBase: config.apiBase,
35512
+ apiBaseInUse,
35119
35513
  url: liveUrl(config)
35120
35514
  } : null,
35121
35515
  // `valid` reflects the refresh token (the real session) - access
35122
35516
  // tokens auto-renew, so their expiry must not read as "invalid".
35123
35517
  // `probe` is what one live call just proved: 'rejected' means every
35124
35518
  // authenticated command will fail even though `valid` reads true.
35519
+ // 'mismatch' overrides 'ok' when the live account isn't the one that
35520
+ // owns this project - `valid` still reads true (the token IS valid).
35125
35521
  auth: auth || usingEnvToken() ? {
35126
35522
  email: auth?.email,
35523
+ account: probe.account,
35127
35524
  source: usingEnvToken() ? "agent-token" : "session",
35128
- valid: usingEnvToken() ? probe !== "rejected" : !sessionExpired(),
35129
- probe
35525
+ valid: usingEnvToken() ? probe.state !== "rejected" : !sessionExpired(),
35526
+ probe: accountMismatch ? "mismatch" : probe.state
35130
35527
  } : null,
35131
35528
  plugin: hookCheck
35132
35529
  }, null, 2));
@@ -35139,18 +35536,25 @@ var statusCommand = new Command("status").alias("whoami").description("Show proj
35139
35536
  console.log(`${muted("Account:")} ${config.accountSlug}`);
35140
35537
  console.log(`${muted("Live:")} ${liveUrl(config)}`);
35141
35538
  console.log(`${muted("API:")} ${config.apiBase}`);
35539
+ if (apiBaseInUse !== config.apiBase) {
35540
+ console.log(`${muted("API (in use):")} ${warning(apiBaseInUse)} ${muted("(overrides .gipity.json \u2014 GIPITY_API_BASE / --api-base / allowlist)")}`);
35541
+ }
35142
35542
  if (config.agentGuid) console.log(`${muted("Agent:")} ${config.agentGuid}`);
35143
35543
  }
35144
35544
  if (usingEnvToken()) {
35145
- 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)")}` : ""}`);
35545
+ 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)")}` : ""}`);
35146
35546
  } else if (!auth) {
35147
35547
  console.log(`${muted("Auth:")} ${warning("not logged in. Run: gipity login")}`);
35148
- } else if (probe === "expired") {
35548
+ } else if (probe.state === "expired") {
35149
35549
  console.log(`${muted("Auth:")} ${warning(`session expired for ${auth.email}. Run: gipity login (headless/CI: set GIPITY_TOKEN \u2014 gipity skill read agent-deploy)`)}`);
35150
- } else if (probe === "rejected") {
35550
+ } else if (probe.state === "rejected") {
35151
35551
  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)`)}`);
35152
35552
  } else {
35153
- console.log(`${muted("Auth:")} ${success(auth.email)}${probe === "unreachable" ? ` ${muted("(unverified \u2014 API unreachable)")}` : ""}`);
35553
+ console.log(`${muted("Auth:")} ${success(auth.email)}${probe.state === "unreachable" ? ` ${muted("(unverified \u2014 API unreachable)")}` : ""}`);
35554
+ }
35555
+ if (accountMismatch) {
35556
+ 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`)}`);
35557
+ console.log(muted("(If this project was shared with you via gipity rbac, this is expected.)"));
35154
35558
  }
35155
35559
  if (queueDelivered > 0) {
35156
35560
  console.log(`${muted("Bug queue:")} ${success(`delivered ${queueDelivered} queued bug report${queueDelivered === 1 ? "" : "s"}`)}`);
@@ -35181,7 +35585,10 @@ var statusCommand = new Command("status").alias("whoami").description("Show proj
35181
35585
  });
35182
35586
 
35183
35587
  // src/commands/sync.ts
35588
+ import { dirname as dirname6 } from "node:path";
35184
35589
  init_colors();
35590
+ init_utils();
35591
+ init_config();
35185
35592
  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) => {
35186
35593
  try {
35187
35594
  const progress = opts.json ? void 0 : createProgressReporter();
@@ -35194,6 +35601,16 @@ var syncCommand = new Command("sync").description("Sync files").addHelpText("aft
35194
35601
  console.log(`
35195
35602
  ${result.applied} action${result.applied > 1 ? "s" : ""} applied.`);
35196
35603
  }
35604
+ if (isWsl()) {
35605
+ const configPath = getConfigPath();
35606
+ const twin = configPath ? findWindowsTwinProject(dirname6(configPath)) : null;
35607
+ if (twin) {
35608
+ console.log(muted(
35609
+ `
35610
+ 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.`
35611
+ ));
35612
+ }
35613
+ }
35197
35614
  if (result.deferredDeletes > 0) {
35198
35615
  console.log(muted(
35199
35616
  `
@@ -35241,8 +35658,8 @@ var pushCommand = new Command("push").description("Push one or more files").argu
35241
35658
  // src/commands/upload.ts
35242
35659
  init_api();
35243
35660
  init_config();
35244
- import { statSync as statSync4 } from "fs";
35245
- import { basename as basename2 } from "path";
35661
+ import { statSync as statSync5 } from "fs";
35662
+ import { basename as basename3 } from "path";
35246
35663
  init_utils();
35247
35664
  init_colors();
35248
35665
  var uploadCommand = new Command("upload").description(`Upload a local file and print a durable, worker-reachable URL for it.
@@ -35259,14 +35676,14 @@ token-signed serve url instead (reachable only by holders of the url).`).argumen
35259
35676
  const { config } = await resolveProjectContext();
35260
35677
  let size;
35261
35678
  try {
35262
- const st2 = statSync4(file);
35679
+ const st2 = statSync5(file);
35263
35680
  if (!st2.isFile()) throw new Error("not a regular file");
35264
35681
  size = st2.size;
35265
35682
  } catch (err) {
35266
35683
  throw new Error(`can't read ${file}: ${err.message}`);
35267
35684
  }
35268
35685
  if (size === 0) throw new Error(`${file} is empty - nothing to upload.`);
35269
- const filename = basename2(file);
35686
+ const filename = basename3(file);
35270
35687
  const contentType = opts.contentType || guessMime(file);
35271
35688
  const doUpload = async () => {
35272
35689
  const init = await post(`/api/${config.projectGuid}/uploads/init`, {
@@ -35283,7 +35700,7 @@ token-signed serve url instead (reachable only by holders of the url).`).argumen
35283
35700
  const comp = await post(`/api/${config.projectGuid}/uploads/complete`, completeBody);
35284
35701
  return comp.data;
35285
35702
  };
35286
- const data = opts.json ? await doUpload() : await withSpinner(`Uploading ${filename} (${formatSize(size)})\u2026`, doUpload, { done: null });
35703
+ const data = opts.json ? await doUpload() : await withSpinner(`Uploading ${filename} (${formatSize(size)})...`, doUpload, { done: null });
35287
35704
  if (opts.json) {
35288
35705
  console.log(JSON.stringify(data));
35289
35706
  return;
@@ -35307,7 +35724,7 @@ init_colors();
35307
35724
  init_auth();
35308
35725
 
35309
35726
  // src/commands/page-eval.ts
35310
- import { readFileSync as readFileSync13 } from "node:fs";
35727
+ import { readFileSync as readFileSync14 } from "node:fs";
35311
35728
  init_api();
35312
35729
  init_colors();
35313
35730
  init_auth();
@@ -35316,8 +35733,8 @@ init_config();
35316
35733
  // src/page-fixtures.ts
35317
35734
  init_api();
35318
35735
  init_config();
35319
- import { readFileSync as readFileSync12, statSync as statSync5, existsSync as existsSync10, readdirSync as readdirSync6 } from "node:fs";
35320
- import { basename as basename3, resolve as resolve10, relative as relative3, join as join10 } from "node:path";
35736
+ import { readFileSync as readFileSync13, statSync as statSync6, existsSync as existsSync10, readdirSync as readdirSync7 } from "node:fs";
35737
+ import { basename as basename4, resolve as resolve10, relative as relative3, join as join11 } from "node:path";
35321
35738
  var FIND_SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", ".gipity", "dist", "build", ".next", "coverage"]);
35322
35739
  function findByBasename(root, name, limit = 3) {
35323
35740
  const hits = [];
@@ -35328,15 +35745,15 @@ function findByBasename(root, name, limit = 3) {
35328
35745
  visited++;
35329
35746
  let entries;
35330
35747
  try {
35331
- entries = readdirSync6(dir, { withFileTypes: true });
35748
+ entries = readdirSync7(dir, { withFileTypes: true });
35332
35749
  } catch {
35333
35750
  continue;
35334
35751
  }
35335
35752
  for (const e of entries) {
35336
35753
  if (e.isDirectory()) {
35337
- if (!FIND_SKIP.has(e.name)) queue.push(join10(dir, e.name));
35754
+ if (!FIND_SKIP.has(e.name)) queue.push(join11(dir, e.name));
35338
35755
  } else if (e.name === name && hits.length < limit) {
35339
- hits.push(join10(dir, e.name));
35756
+ hits.push(join11(dir, e.name));
35340
35757
  }
35341
35758
  }
35342
35759
  }
@@ -35346,22 +35763,22 @@ function assertLocalAsset(flag, localPath) {
35346
35763
  const abs = resolve10(localPath);
35347
35764
  if (existsSync10(abs)) return;
35348
35765
  const root = getProjectRoot() ?? process.cwd();
35349
- const elsewhere = findByBasename(root, basename3(localPath)).filter((p) => p !== abs);
35766
+ const elsewhere = findByBasename(root, basename4(localPath)).filter((p) => p !== abs);
35350
35767
  const found = elsewhere.length ? `
35351
35768
  That file DOES exist here \u2014 pass this path instead:
35352
35769
  ${elsewhere.map((p) => ` ${flag} ${relative3(process.cwd(), p) || p}`).join("\n")}` : `
35353
- Nothing named "${basename3(localPath)}" under ${root} either. Generate a frame with \`gipity generate image "<description>" -o ${localPath}\`, or check the path.`;
35770
+ Nothing named "${basename4(localPath)}" under ${root} either. Generate a frame with \`gipity generate image "<description>" -o ${localPath}\`, or check the path.`;
35354
35771
  throw new Error(
35355
35772
  `${flag} ${localPath}: no such file \u2014 looked for ${abs} (relative paths resolve against the current directory, ${process.cwd()}).${found}`
35356
35773
  );
35357
35774
  }
35358
- async function uploadPublicFixture(projectGuid, localPath, flag = "--fixture") {
35775
+ async function uploadPublicFixture(projectGuid2, localPath, flag = "--fixture") {
35359
35776
  assertLocalAsset(flag, localPath);
35360
- const name = basename3(localPath);
35361
- const size = statSync5(localPath).size;
35777
+ const name = basename4(localPath);
35778
+ const size = statSync6(localPath).size;
35362
35779
  const contentType = guessMime(localPath);
35363
35780
  const init = await post(
35364
- `/api/${projectGuid}/uploads/init`,
35781
+ `/api/${projectGuid2}/uploads/init`,
35365
35782
  { filename: name, content_type: contentType, size, public: true }
35366
35783
  );
35367
35784
  if (init.data.method !== "PUT" || !init.data.url) {
@@ -35370,19 +35787,19 @@ async function uploadPublicFixture(projectGuid, localPath, flag = "--fixture") {
35370
35787
  const res = await fetch(init.data.url, {
35371
35788
  method: "PUT",
35372
35789
  headers: { "Content-Type": contentType },
35373
- body: readFileSync12(localPath)
35790
+ body: readFileSync13(localPath)
35374
35791
  });
35375
35792
  if (!res.ok) {
35376
35793
  throw new Error(`upload of fixture "${name}" failed: ${res.status} ${res.statusText}`);
35377
35794
  }
35378
35795
  const done = await post(
35379
- `/api/${projectGuid}/uploads/complete`,
35796
+ `/api/${projectGuid2}/uploads/complete`,
35380
35797
  { upload_guid: init.data.upload_guid }
35381
35798
  );
35382
35799
  return { guid: done.data.guid, url: done.data.url, name, localPath };
35383
35800
  }
35384
- async function deleteFixture(projectGuid, guid) {
35385
- await del(`/api/${projectGuid}/uploads/${guid}`);
35801
+ async function deleteFixture(projectGuid2, guid) {
35802
+ await del(`/api/${projectGuid2}/uploads/${guid}`);
35386
35803
  }
35387
35804
  var CAMERA_EXTS = [".png", ".jpg", ".jpeg", ".webp", ".mp4", ".webm", ".y4m", ".mjpeg"];
35388
35805
  function assertCameraFile(localPath) {
@@ -35390,14 +35807,14 @@ function assertCameraFile(localPath) {
35390
35807
  const ext = localPath.slice(localPath.lastIndexOf(".")).toLowerCase();
35391
35808
  if (CAMERA_EXTS.includes(ext)) return;
35392
35809
  throw new Error(
35393
- `--camera ${basename3(localPath)}: unsupported file type "${ext || "(none)"}" \u2014 the camera feed must be an image or video (${CAMERA_EXTS.join(", ")}).
35810
+ `--camera ${basename4(localPath)}: unsupported file type "${ext || "(none)"}" \u2014 the camera feed must be an image or video (${CAMERA_EXTS.join(", ")}).
35394
35811
  A still image is played as a looping single-frame feed, which is what a gesture/pose/object model needs.
35395
35812
  No frame to hand? Generate one: gipity generate image "a hand making a closed fist, palm to camera, plain background"`
35396
35813
  );
35397
35814
  }
35398
- async function uploadCameraFeed(projectGuid, localPath) {
35815
+ async function uploadCameraFeed(projectGuid2, localPath) {
35399
35816
  assertCameraFile(localPath);
35400
- return uploadPublicFixture(projectGuid, localPath, "--camera");
35817
+ return uploadPublicFixture(projectGuid2, localPath, "--camera");
35401
35818
  }
35402
35819
 
35403
35820
  // src/commands/page-eval.ts
@@ -35443,7 +35860,7 @@ function summarizeExpr(expr) {
35443
35860
  const oneLine = meaningful.length <= 1;
35444
35861
  if (oneLine && expr.trim().length <= EXPR_ECHO_MAX_CHARS) return expr.trim();
35445
35862
  const first = (meaningful[0] ?? "").trim();
35446
- const head = first.length > EXPR_ECHO_MAX_CHARS ? `${first.slice(0, EXPR_ECHO_MAX_CHARS - 1)}\u2026` : first;
35863
+ const head = first.length > EXPR_ECHO_MAX_CHARS ? `${first.slice(0, EXPR_ECHO_MAX_CHARS - 3)}...` : first;
35447
35864
  const shape = oneLine ? `(${expr.trim().length} chars)` : `(+${meaningful.length - 1} more ${meaningful.length - 1 === 1 ? "line" : "lines"}, ${expr.trim().length} chars)`;
35448
35865
  return `${head} ${shape}`;
35449
35866
  }
@@ -35470,7 +35887,7 @@ function capWaitForTimeoutMs(raw) {
35470
35887
  return WAIT_FOR_MAX_MS;
35471
35888
  }
35472
35889
  function readScriptFile(pathArg) {
35473
- return readFileSync13(pathArg === "-" ? 0 : pathArg, "utf8");
35890
+ return readFileSync14(pathArg === "-" ? 0 : pathArg, "utf8");
35474
35891
  }
35475
35892
  function parsesAsExpression(s) {
35476
35893
  try {
@@ -35520,7 +35937,15 @@ function budgetOverrunHint(reason, usedBudgetMs) {
35520
35937
  }
35521
35938
  return `That budget was ${Math.round(usedBudgetMs / 1e3)}s. Raise it with --timeout <ms> (up to ${EVAL_SCRIPT_BUDGET_MAX_MS}), e.g. --timeout ${EVAL_SCRIPT_BUDGET_MAX_MS} \u2014 or gate on a ready signal instead: --wait-for '<selector>' --wait-timeout ${MAX_WAIT_MS}.`;
35522
35939
  }
35940
+ function navigationAbortHint(reason) {
35941
+ if (!/navigated or reloaded/i.test(reason)) return null;
35942
+ return `If you were verifying that state SURVIVES a reload, that is what --reload is for (one command, one page): gipity page eval "<url>" '<seed/assert expr>' --reload '<assert-restored expr>'. It reloads the page in place (localStorage/sessionStorage/cookies preserved) and reports the second result separately. Splitting into two 'page eval' calls does NOT do this: each call is its own browser session, so the second one starts from empty storage and never exercises the restore path.`;
35943
+ }
35944
+ function stepsDeterministically(script) {
35945
+ return /\badvance\s*\(/.test(script);
35946
+ }
35523
35947
  function slowRenderMessage(fps, o) {
35948
+ if (!o.camera && stepsDeterministically(o.script ?? "")) return null;
35524
35949
  if (o.camera) {
35525
35950
  const frames = Math.max(1, Math.round(fps * (o.waitMs / 1e3)));
35526
35951
  return `${warning("\u26A0 Slow render:")} page painted at ${fps} fps, so the app's vision pipeline ran on roughly ${bold(`${frames} frame${frames === 1 ? "" : "s"}`)} during the ${Math.round(o.waitMs / 1e3)}s before this eval (it infers once per painted frame). ${bold("That is enough:")} --camera loops your still image, so every one of those frames is the SAME pixels and the model returns the SAME answer on each \u2014 re-running with a bigger --wait/--timeout cannot change the result. ${bold("Do not escalate the wait.")} If a detection landed, it is real. If nothing was detected, the suspect is the ${bold("frame")} (a model needs the whole subject in shot \u2014 a tight crop, an odd angle or a busy background reads as nothing) or the ${bold("app")} (frames never reach the model) \u2014 never the frame rate. Settle it in ONE run: ${CAMERA_FRAME_CHECK}`;
@@ -35566,7 +35991,7 @@ function evalExecTimeoutMessage(result, budgetMs) {
35566
35991
  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.`;
35567
35992
  }
35568
35993
  var JS_DECOY_FLAGS = ["--js", "--javascript", "--script", "--code", "--expr", "--eval", "--exec", "--action"];
35569
- var pageEvalCommand = new Command("eval").description("Evaluate JS in a real browser on a page (DOM, computed styles, element rects; inline expr or --file script). ONE client per call - to verify realtime/presence across concurrent clients use `page test --observe` instead").argument("<url>", "URL to load").argument("[expr]", `JavaScript to evaluate in page context (inline expression or statement body; await works, and the trailing expression is returned automatically \u2014 REPL-style \u2014 so no explicit return is needed; result is JSON-serialized). Omit when using --file. Time budget: the body has ${EVAL_SCRIPT_BUDGET_MS / 1e3}s to finish after page load (${EVAL_SCRIPT_BUDGET_CAMERA_MS / 1e3}s with --camera) - raise it with --timeout, max ${EVAL_SCRIPT_BUDGET_MAX_MS / 1e3}s.`).option("--file <path>", `Read the script body from a file instead of the inline <expr> arg (mutually exclusive), or --file - to read it from stdin (pipe a heredoc: --file - <<'EOF' \u2026 EOF) with no tmp file. Runs as an async function body, so top-level return/await work. Same post-load budget as <expr> (--timeout).`).option(
35994
+ 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(
35570
35995
  "--step <expr>",
35571
35996
  `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.`,
35572
35997
  (val, prev) => [...prev, val],
@@ -35578,7 +36003,7 @@ var pageEvalCommand = new Command("eval").description("Evaluate JS in a real bro
35578
36003
  []
35579
36004
  ).option(
35580
36005
  "--reload <expr>",
35581
- "After the first eval, reload the page IN PLACE (localStorage/sessionStorage/cookies preserved) and evaluate this second expression against the post-reload DOM. One command verifies persisted state survives a reload: seed/assert state with <expr>, then assert the restored UI here."
36006
+ 'After the first eval, reload the page IN PLACE (localStorage/sessionStorage/cookies preserved) and evaluate this second expression against the post-reload DOM. One command verifies persisted state survives a reload ("remember it when I come back"): seed/assert state with <expr>, then assert the restored UI here. This is the ONLY way to check that - reloading inside the body aborts the eval (the result is lost with the old page), and two separate `page eval` calls are two fresh browser profiles, so the second starts from empty storage.'
35582
36007
  ).option("--reload-file <path>", "Read the post-reload expression from a file instead of inline --reload (mutually exclusive)").option(
35583
36008
  "--camera <path>",
35584
36009
  `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".`
@@ -35610,7 +36035,7 @@ var pageEvalCommand = new Command("eval").description("Evaluate JS in a real bro
35610
36035
  try {
35611
36036
  expr = readScriptFile(opts.file);
35612
36037
  } catch {
35613
- pageEvalCommand.error(opts.file === "-" ? `error: --file - reads the script from stdin, but stdin was empty/unreadable \u2014 pipe it in, e.g. gipity page eval "<url>" --file - <<'EOF' \u2026 EOF` : `error: Cannot read file: ${opts.file}`);
36038
+ 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}`);
35614
36039
  }
35615
36040
  }
35616
36041
  if (opts.reload !== void 0 && opts.reloadFile) {
@@ -35662,23 +36087,23 @@ var pageEvalCommand = new Command("eval").description("Evaluate JS in a real bro
35662
36087
  const fixturePaths = opts.fixture ?? [];
35663
36088
  const hosted = [];
35664
36089
  let camera;
35665
- let projectGuid;
36090
+ let projectGuid2;
35666
36091
  let sentExpr = expr;
35667
36092
  let sentSteps = steps;
35668
36093
  if (opts.camera) assertCameraFile(opts.camera);
35669
36094
  try {
35670
36095
  if (fixturePaths.length || opts.camera) {
35671
36096
  const { config } = await resolveProjectContext({});
35672
- projectGuid = config.projectGuid;
36097
+ projectGuid2 = config.projectGuid;
35673
36098
  }
35674
36099
  if (opts.camera) {
35675
- console.log(muted(`Hosting camera feed ${opts.camera}\u2026`));
35676
- camera = await uploadCameraFeed(projectGuid, opts.camera);
36100
+ console.log(muted(`Hosting camera feed ${opts.camera}...`));
36101
+ camera = await uploadCameraFeed(projectGuid2, opts.camera);
35677
36102
  }
35678
36103
  if (fixturePaths.length) {
35679
36104
  for (const p of fixturePaths) {
35680
- console.log(muted(`Hosting fixture ${p}\u2026`));
35681
- hosted.push(await uploadPublicFixture(projectGuid, p));
36105
+ console.log(muted(`Hosting fixture ${p}...`));
36106
+ hosted.push(await uploadPublicFixture(projectGuid2, p));
35682
36107
  }
35683
36108
  const map = {};
35684
36109
  for (const h of hosted) map[h.name] = h.url;
@@ -35716,7 +36141,7 @@ var pageEvalCommand = new Command("eval").description("Evaluate JS in a real bro
35716
36141
  }));
35717
36142
  } catch (err) {
35718
36143
  const msg = err?.message ?? "";
35719
- const hint = budgetOverrunHint(msg, scriptBudgetMs);
36144
+ const hint = budgetOverrunHint(msg, scriptBudgetMs) ?? (reloadExpr === void 0 ? navigationAbortHint(msg) : null);
35720
36145
  if (!hint) throw err;
35721
36146
  throw new Error(`${msg}
35722
36147
  ${hint}`);
@@ -35748,7 +36173,12 @@ ${hint}`);
35748
36173
  console.log(`${warning("\u26A0 Navigation incomplete:")} ${d.note || "page did not reach full load"}`);
35749
36174
  }
35750
36175
  if (d.slowRender) {
35751
- console.log(slowRenderMessage(d.slowRender.fps, { camera: !!camera, waitMs }));
36176
+ const slow = slowRenderMessage(d.slowRender.fps, {
36177
+ camera: !!camera,
36178
+ waitMs,
36179
+ script: [expr, ...steps, reloadExpr ?? ""].join("\n")
36180
+ });
36181
+ if (slow) console.log(slow);
35752
36182
  }
35753
36183
  if (d.auth?.requested) {
35754
36184
  const who = getAuth()?.email;
@@ -35783,7 +36213,7 @@ ${bold("After reload")} ${muted("(page reloaded in place \u2014 storage preserve
35783
36213
  } finally {
35784
36214
  for (const h of [...hosted, ...camera ? [camera] : []]) {
35785
36215
  try {
35786
- await deleteFixture(projectGuid, h.guid);
36216
+ await deleteFixture(projectGuid2, h.guid);
35787
36217
  } catch (err) {
35788
36218
  console.error(warning(`\u26A0 Could not auto-delete fixture "${h.name}" (${h.guid}) \u2014 still hosted at ${h.url}: ${err.message}`));
35789
36219
  }
@@ -35877,7 +36307,7 @@ init_auth();
35877
36307
  init_colors();
35878
36308
  init_utils();
35879
36309
  import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
35880
- import { dirname as dirname6, join as join11, resolve as resolvePath } from "path";
36310
+ import { dirname as dirname7, join as join12, resolve as resolvePath } from "path";
35881
36311
  var DEVICE_PRESETS = {
35882
36312
  default: { width: 1280, height: 720 },
35883
36313
  desktop: { width: 1920, height: 1080 },
@@ -35941,7 +36371,7 @@ function dimSuffix(vp2) {
35941
36371
  }
35942
36372
  function defaultScreenshotDir() {
35943
36373
  const root = getProjectRoot();
35944
- return join11(root ?? ".", "screenshots");
36374
+ return join12(root ?? ".", "screenshots");
35945
36375
  }
35946
36376
  function timestampSlug(d = /* @__PURE__ */ new Date()) {
35947
36377
  const p = (n) => String(n).padStart(2, "0");
@@ -36004,7 +36434,7 @@ var WAIT_FOR_MAX_MS2 = 15e3;
36004
36434
  var WAIT_FOR_DEFAULT_MS = 15e3;
36005
36435
  var MAX_POST_LOAD_DELAY_MS = 3e4;
36006
36436
  var CAMERA_DEFAULT_DELAY_MS = 15e3;
36007
- var pageScreenshotCommand = new Command("screenshot").description("Screenshot a web page").argument("<url>", "URL to screenshot").option("--device <names>", `Device preset(s): ${Object.keys(DEVICE_PRESETS).join(", ")} (comma-separated or repeat flag). mobile/tablet emulate a real touch device \u2014 touch events, mobile user-agent, DPR \u2014 so touch-gated mobile UI actually renders.`, appendOption, []).option("--viewport <dims>", "Raw viewport(s): WxH or WxH@dpr (comma-separated or repeat flag), e.g. --viewport 390x844 for a phone-sized window", appendOption, []).option("--wait <ms>", `Sleep this many ms after DOMContentLoaded before capturing (default 1000, max ${MAX_POST_LOAD_DELAY_MS}). With --camera it defaults to ${CAMERA_DEFAULT_DELAY_MS} so the vision model is up. Same flag as page eval/inspect.`).option("--wait-for <selector>", `Wait until this CSS selector appears before capturing, then capture (deterministic - beats guessing a --wait). Same flag as page eval/inspect. Gate on what you are photographing ('[data-vision="ready"]', '#verdict:not(:empty)'). Max ${WAIT_FOR_MAX_MS2}ms (--wait-timeout).`).option("--wait-timeout <ms>", `Max ms to wait for --wait-for before giving up (default ${WAIT_FOR_DEFAULT_MS}, max ${WAIT_FOR_MAX_MS2}). Timing out is reported - the capture still happens, so you see the state it got stuck in.`).option("--action <js>", `Run JS in the page before capturing \u2014 e.g. click a button to enter a state ("document.getElementById('play').click()"). Runs as an async function body, so const/await and app-relative import('./\u2026') work. Runs after the post-load delay, then settles again before the shot. If it throws, the capture still happens and the failure is reported.`).option("--file <path>", "Read the pre-capture script from a file instead of inline --action (mutually exclusive), or --file - to read it from stdin (pipe a heredoc: --file - <<'EOF' \u2026 EOF) with no tmp file. For a multi-step driver \u2014 click through a flow, wait for it to render \u2014 then capture. Same async-function-body semantics as --action; same --file flag as page eval.").option("--full", "Capture the full scrollable page (default: viewport only). Scrolls the page through first so scroll-reveal/fade-in-on-scroll (IntersectionObserver) sections render into the shot instead of capturing blank.").option("-o, --output <file>", "Output path (single viewport only; default .gipity/screenshots/ss-<host>-<timestamp>.png)").option("--no-reload-between", "Skip reload between viewports (faster, lower fidelity - only safe for static pages)").option("--fake-media", "Grant a synthetic microphone + camera and auto-accept the getUserMedia prompt, so voice/camera apps render headlessly. The video feed is a built-in test pattern \u2014 to capture what the app does with a REAL frame (a hand, a face, an object), use --camera <path> instead.").option("--camera <path>", "Play a local image or video (.png/.jpg/.webp/.mp4/.webm/.y4m/.mjpeg) as the browser's WEBCAM feed, then capture \u2014 so the shot shows the app reacting to a frame you chose (detected gesture, boxes, labels). Implies --fake-media.").option("--auth", "Capture the page signed in as you (your Gipity account), so UI behind a Sign-in-with-Gipity login is shown. Only works for apps using Sign in with Gipity, hosted on *.gipity.ai. Without this flag the page loads as a genuinely anonymous, signed-out visitor \u2014 nothing carries over from earlier --auth runs.").option("--ephemeral", "Skip the project screenshot history: do not persist this capture to Gipity (screenshots/ in the project). Local file is still written.").option("--json", "Output JSON metadata instead of a friendly summary").addOption(new Option("--post-load-delay <ms>", "Alias for --wait").hideHelp()).addOption(new Option("--full-page", "Alias for --full").hideHelp()).addOption(new Option("--width <px>", "Alias: --viewport WxH").hideHelp()).addOption(new Option("--height <px>", "Alias: --viewport WxH").hideHelp()).action((url, opts) => run("Page screenshot", async () => {
36437
+ 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 () => {
36008
36438
  const aliasFlag = ACTION_ALIAS_FLAGS.find((f) => opts[f.slice(2)] !== void 0);
36009
36439
  if (aliasFlag) {
36010
36440
  console.error(muted(
@@ -36020,7 +36450,7 @@ var pageScreenshotCommand = new Command("screenshot").description("Screenshot a
36020
36450
  try {
36021
36451
  actionScript = readScriptFile(opts.file);
36022
36452
  } catch {
36023
- throw new Error(opts.file === "-" ? `--file - reads the pre-capture script from stdin, but stdin was empty/unreadable \u2014 pipe it in, e.g. gipity page screenshot "<url>" --file - <<'EOF' \u2026 EOF` : `Cannot read file: ${opts.file}`);
36453
+ 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}`);
36024
36454
  }
36025
36455
  }
36026
36456
  const delayRaw = opts.wait ?? opts.postLoadDelay;
@@ -36082,14 +36512,14 @@ var pageScreenshotCommand = new Command("screenshot").description("Screenshot a
36082
36512
  const ts2 = timestampSlug();
36083
36513
  const shotName = (vp2) => defaultFilename(slug, ts2, vp2 && userSpecifiedViewports ? dimSuffix(vp2) : void 0);
36084
36514
  const names = userSpecifiedViewports ? customViewports.map(shotName) : [shotName()];
36085
- const projectGuid = getConfig()?.projectGuid;
36086
- const save = !opts.ephemeral && projectGuid ? { project_guid: projectGuid, names } : void 0;
36515
+ const projectGuid2 = getConfig()?.projectGuid;
36516
+ const save = !opts.ephemeral && projectGuid2 ? { project_guid: projectGuid2, names } : void 0;
36087
36517
  let camera;
36088
36518
  if (opts.camera) {
36089
36519
  assertCameraFile(opts.camera);
36090
- if (!projectGuid) throw new Error("--camera needs a linked project (the frame is hosted for the browser to fetch) \u2014 run `gipity link` first.");
36091
- console.log(muted(`Hosting camera feed ${opts.camera}\u2026`));
36092
- camera = await uploadCameraFeed(projectGuid, opts.camera);
36520
+ if (!projectGuid2) throw new Error("--camera needs a linked project (the frame is hosted for the browser to fetch) \u2014 run `gipity link` first.");
36521
+ console.log(muted(`Hosting camera feed ${opts.camera}...`));
36522
+ camera = await uploadCameraFeed(projectGuid2, opts.camera);
36093
36523
  }
36094
36524
  const preCapture = [
36095
36525
  opts.waitFor ? buildWaitForGate(opts.waitFor, waitForTimeoutMs) : "",
@@ -36113,14 +36543,14 @@ var pageScreenshotCommand = new Command("screenshot").description("Screenshot a
36113
36543
  let entries;
36114
36544
  try {
36115
36545
  try {
36116
- entries = opts.json ? await doShoot() : await withSpinner("Capturing\u2026", doShoot, { done: null });
36546
+ entries = opts.json ? await doShoot() : await withSpinner("Capturing...", doShoot, { done: null });
36117
36547
  } catch (err) {
36118
36548
  throw preCapture ? new Error(augmentSandboxTimeout(err.message)) : err;
36119
36549
  }
36120
36550
  } finally {
36121
36551
  if (camera) {
36122
36552
  try {
36123
- await deleteFixture(projectGuid, camera.guid);
36553
+ await deleteFixture(projectGuid2, camera.guid);
36124
36554
  } catch (err) {
36125
36555
  console.error(warning(`\u26A0 Could not auto-delete camera feed "${camera.name}" (${camera.guid}) \u2014 still hosted at ${camera.url}: ${err.message}`));
36126
36556
  }
@@ -36136,8 +36566,8 @@ var pageScreenshotCommand = new Command("screenshot").description("Screenshot a
36136
36566
  const dir = defaultScreenshotDir();
36137
36567
  const savedFiles = [];
36138
36568
  for (let i = 0; i < pngs.length; i++) {
36139
- const target = opts.output ? opts.output : join11(dir, names[i] ?? shotName(meta.screenshots[i].viewport));
36140
- mkdirSync7(dirname6(target), { recursive: true });
36569
+ const target = opts.output ? opts.output : join12(dir, names[i] ?? shotName(meta.screenshots[i].viewport));
36570
+ mkdirSync7(dirname7(target), { recursive: true });
36141
36571
  writeFileSync8(target, pngs[i].buffer);
36142
36572
  savedFiles.push(resolvePath(target));
36143
36573
  }
@@ -36265,10 +36695,10 @@ function shortUrl(url, truncate = true, maxLen = 100) {
36265
36695
  result = url;
36266
36696
  }
36267
36697
  if (!truncate || result.length <= maxLen) return result;
36268
- const keep = maxLen - 1;
36698
+ const keep = maxLen - 3;
36269
36699
  const headLen = Math.ceil(keep / 2);
36270
36700
  const tailLen = Math.floor(keep / 2);
36271
- return result.slice(0, headLen) + "\u2026" + result.slice(-tailLen);
36701
+ return result.slice(0, headLen) + "..." + result.slice(-tailLen);
36272
36702
  }
36273
36703
  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) => {
36274
36704
  if (opts.screenshot !== void 0) {
@@ -36482,7 +36912,7 @@ var deployCommand = new Command("deploy").description("Deploy to dev or prod").a
36482
36912
  force: opts.force,
36483
36913
  only: opts.only?.split(",").map((s) => s.trim())
36484
36914
  });
36485
- const res = opts.json ? await doDeploy() : await withSpinner(`Deploying to ${target}\u2026`, doDeploy, { done: null });
36915
+ const res = opts.json ? await doDeploy() : await withSpinner(`Deploying to ${target}...`, doDeploy, { done: null });
36486
36916
  const d = res.data;
36487
36917
  const inspectAfter = async () => {
36488
36918
  if (!opts.inspect) return;
@@ -36622,10 +37052,10 @@ dbCommand.command("list").description("List databases").option("--all", "List da
36622
37052
  grouped.get(key).push(db2);
36623
37053
  }
36624
37054
  let first = true;
36625
- for (const [projectGuid, dbs] of grouped) {
37055
+ for (const [projectGuid2, dbs] of grouped) {
36626
37056
  if (!first) console.log("");
36627
37057
  first = false;
36628
- const label2 = dbs[0].projectSlug || dbs[0].projectName || projectGuid;
37058
+ const label2 = dbs[0].projectSlug || dbs[0].projectName || projectGuid2;
36629
37059
  console.log(label2);
36630
37060
  for (const db2 of dbs) {
36631
37061
  console.log(`${db2.friendlyName}`);
@@ -36734,8 +37164,8 @@ memoryCommand.command("delete <topic>").description("Delete a topic").option("--
36734
37164
  // src/commands/sandbox.ts
36735
37165
  init_api();
36736
37166
  init_config();
36737
- import { readFileSync as readFileSync14, existsSync as existsSync11, statSync as statSync6, mkdirSync as mkdirSync8, writeFileSync as writeFileSync9 } from "fs";
36738
- import { dirname as dirname7, extname as extname4, relative as relative4, resolve as resolve11, sep as sep3 } from "path";
37167
+ import { readFileSync as readFileSync15, existsSync as existsSync11, statSync as statSync7, mkdirSync as mkdirSync8, writeFileSync as writeFileSync9 } from "fs";
37168
+ import { dirname as dirname8, extname as extname4, relative as relative4, resolve as resolve11, sep as sep3 } from "path";
36739
37169
  init_colors();
36740
37170
  var LANG_MAP = {
36741
37171
  js: "javascript",
@@ -36873,7 +37303,7 @@ function isWriteTarget(code, p) {
36873
37303
  function resolveRelativeCwd() {
36874
37304
  const configPath = getConfigPath();
36875
37305
  if (!configPath) return void 0;
36876
- const projectRoot = dirname7(configPath);
37306
+ const projectRoot = dirname8(configPath);
36877
37307
  const rel2 = relative4(projectRoot, process.cwd());
36878
37308
  if (!rel2 || rel2.startsWith("..")) return void 0;
36879
37309
  return rel2.split(/[\\/]/).filter(Boolean).join("/");
@@ -36955,7 +37385,7 @@ GCC/Rust).
36955
37385
  if (args.length >= 2 && INTERPRETERS[args[0].toLowerCase()] !== void 0) {
36956
37386
  langFromInterp = INTERPRETERS[args[0].toLowerCase()];
36957
37387
  const rest = args.slice(1).join(" ");
36958
- if (existsSync11(rest) && statSync6(rest).isFile()) filePath = rest;
37388
+ if (existsSync11(rest) && statSync7(rest).isFile()) filePath = rest;
36959
37389
  else inlineCode = rest;
36960
37390
  } else if (args.length === 1) {
36961
37391
  inlineCode = args[0];
@@ -36976,7 +37406,7 @@ GCC/Rust).
36976
37406
  let source = inlineCode;
36977
37407
  if (filePath) {
36978
37408
  try {
36979
- source = readFileSync14(filePath, "utf8");
37409
+ source = readFileSync15(filePath, "utf8");
36980
37410
  } catch {
36981
37411
  console.error(error(`Cannot read file: ${filePath}`));
36982
37412
  process.exit(1);
@@ -37028,7 +37458,7 @@ GCC/Rust).
37028
37458
  // wire name for --discard-output.)
37029
37459
  noSyncOutput: discardOutput.length ? discardOutput : void 0
37030
37460
  });
37031
- const res = opts.json ? await doRun() : await withSpinner("Running in sandbox\u2026", doRun, { done: null });
37461
+ const res = opts.json ? await doRun() : await withSpinner("Running in sandbox...", doRun, { done: null });
37032
37462
  const pulledLocal = !!(res.data.outputFiles?.length && getConfigPath());
37033
37463
  if (pulledLocal) {
37034
37464
  await sync({ interactive: false, progress: opts.json ? void 0 : createProgressReporter() });
@@ -37040,7 +37470,7 @@ GCC/Rust).
37040
37470
  const rel2 = f.path.replace(/\\/g, "/");
37041
37471
  const abs = resolve11(base, rel2);
37042
37472
  if (abs !== base && !abs.startsWith(base + sep3)) continue;
37043
- mkdirSync8(dirname7(abs), { recursive: true });
37473
+ mkdirSync8(dirname8(abs), { recursive: true });
37044
37474
  writeFileSync9(abs, Buffer.from(f.contentBase64, "base64"));
37045
37475
  scratchWritten.push(rel2);
37046
37476
  }
@@ -37096,7 +37526,7 @@ Note: ${rootStrays.join(", ")} landed at the project root and will deploy with t
37096
37526
  const root = getProjectRoot();
37097
37527
  const emptyOutputs = pulledLocal && root ? (res.data.outputFiles ?? []).filter((f) => {
37098
37528
  try {
37099
- return statSync6(resolve11(root, f)).size === 0;
37529
+ return statSync7(resolve11(root, f)).size === 0;
37100
37530
  } catch {
37101
37531
  return false;
37102
37532
  }
@@ -37128,7 +37558,7 @@ var chatCommand = new Command("chat").description("Send a message to your agent"
37128
37558
  const endpoint = useExisting ? `/conversations/${config.conversationGuid}/messages` : "/conversations";
37129
37559
  const body = useExisting ? { content: message, projectGuid: config.projectGuid } : { agentGuid: config.agentGuid, content: message, projectGuid: config.projectGuid };
37130
37560
  const doChat = () => post(endpoint, body);
37131
- const res = opts.json ? await doChat() : await withSpinner("Thinking\u2026", doChat, { done: null });
37561
+ const res = opts.json ? await doChat() : await withSpinner("Thinking...", doChat, { done: null });
37132
37562
  if (!oneOff && res.data.conversationGuid !== config.conversationGuid) {
37133
37563
  saveConfig({ ...config, conversationGuid: res.data.conversationGuid });
37134
37564
  }
@@ -37213,7 +37643,7 @@ chatCommand.command("delete <guid>").description("Delete a chat").option("--json
37213
37643
  // src/commands/project.ts
37214
37644
  init_api();
37215
37645
  init_config();
37216
- import { join as join12 } from "path";
37646
+ import { join as join13 } from "path";
37217
37647
  import { mkdirSync as mkdirSync9 } from "fs";
37218
37648
  init_colors();
37219
37649
  init_utils();
@@ -37271,7 +37701,7 @@ projectCommand.command("create <name>").description("Create a project").option("
37271
37701
  }
37272
37702
  const res = await post("/projects", body);
37273
37703
  const project = res.data;
37274
- const dir = join12(getProjectsRoot(), project.slug);
37704
+ const dir = join13(getProjectsRoot(), project.slug);
37275
37705
  mkdirSync9(dir, { recursive: true });
37276
37706
  const accountSlug = await getAccountSlug();
37277
37707
  let agentGuid = "";
@@ -37614,7 +38044,7 @@ workflowCommand.command("run <name>").description("Trigger a workflow (add --wai
37614
38044
  }
37615
38045
  if (r.status !== "completed") process.exit(1);
37616
38046
  }));
37617
- 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 () => {
38047
+ 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 () => {
37618
38048
  const opts = mergedOpts(cmd);
37619
38049
  const wf2 = await resolveWorkflow(name);
37620
38050
  if (runGuid) {
@@ -38100,7 +38530,7 @@ function printUsage(d) {
38100
38530
  for (const p of d.projects) {
38101
38531
  const name = p.projectName ?? "(no project)";
38102
38532
  row(
38103
- name.length > 16 ? `${name.slice(0, 15)}\u2026` : name,
38533
+ name.length > 16 ? `${name.slice(0, 13)}...` : name,
38104
38534
  formatBytes(p.liveBytes),
38105
38535
  `${p.liveFiles.toLocaleString()} files`
38106
38536
  );
@@ -38109,7 +38539,7 @@ function printUsage(d) {
38109
38539
  const shownBytes = d.projects.reduce((n, p) => n + p.liveBytes, 0);
38110
38540
  const shownFiles = d.projects.reduce((n, p) => n + p.liveFiles, 0);
38111
38541
  row(
38112
- `\u2026and ${hidden.toLocaleString()} more`,
38542
+ `...and ${hidden.toLocaleString()} more`,
38113
38543
  formatBytes(totals.liveBytes - shownBytes),
38114
38544
  `${(totals.liveFiles - shownFiles).toLocaleString()} files`
38115
38545
  );
@@ -38161,50 +38591,11 @@ storageCommand.command("retention").description("View or adjust your file versio
38161
38591
  init_platform();
38162
38592
  init_auth();
38163
38593
  init_api();
38164
- import { join as join16, dirname as dirname9, resolve as resolve13, basename as basename4, sep as sep5 } from "path";
38165
- import { existsSync as existsSync14, mkdirSync as mkdirSync12, readFileSync as readFileSync18, writeFileSync as writeFileSync12, renameSync as renameSync2, readdirSync as readdirSync7, statSync as statSync7 } from "fs";
38594
+ import { join as join17, dirname as dirname10, resolve as resolve13, basename as basename5, sep as sep5 } from "path";
38595
+ import { existsSync as existsSync14, mkdirSync as mkdirSync12, readFileSync as readFileSync18, writeFileSync as writeFileSync12, renameSync as renameSync2, readdirSync as readdirSync8, statSync as statSync8 } from "fs";
38166
38596
  import { homedir as homedir12 } from "os";
38167
38597
  import { fileURLToPath as fileURLToPath2 } from "url";
38168
38598
  import { randomUUID } from "crypto";
38169
-
38170
- // src/login-flow.ts
38171
- init_api();
38172
- init_auth();
38173
- init_utils();
38174
- init_colors();
38175
- async function interactiveLogin() {
38176
- const email = await prompt(" Email: ");
38177
- if (!email) {
38178
- console.error(`
38179
- ${error("Email required.")}`);
38180
- process.exit(1);
38181
- }
38182
- await publicPost("/auth/login", { email });
38183
- console.log(" Check your email for a 6-digit code.\n");
38184
- const code = await prompt(" Code: ");
38185
- if (!code) {
38186
- console.error(`
38187
- ${error("Code required.")}`);
38188
- process.exit(1);
38189
- }
38190
- const res = await publicPost("/auth/verify", { email, code });
38191
- const exp = decodeJwtExp(res.accessToken);
38192
- if (!exp) {
38193
- console.error(`
38194
- ${error("Invalid token received.")}`);
38195
- process.exit(1);
38196
- }
38197
- const expiresAt = new Date(exp * 1e3).toISOString();
38198
- saveAuth({ accessToken: res.accessToken, refreshToken: res.refreshToken, email, expiresAt });
38199
- console.log(` ${success(`Logged in (${email}).`)}`);
38200
- const delivered = await flushBugQueue().catch(() => 0);
38201
- if (delivered > 0) {
38202
- console.log(` ${muted(`Delivered ${delivered} queued bug report${delivered === 1 ? "" : "s"}.`)}`);
38203
- }
38204
- return getAuth();
38205
- }
38206
-
38207
- // src/commands/build.ts
38208
38599
  init_config();
38209
38600
 
38210
38601
  // src/prompts.ts
@@ -38310,11 +38701,10 @@ function buildFreshWrap(contextBlock, userMsg) {
38310
38701
  // src/relay/onboarding.ts
38311
38702
  init_utils();
38312
38703
  init_colors();
38313
- import { readFileSync as readFileSync16 } from "node:fs";
38314
38704
 
38315
38705
  // src/relay/installers.ts
38316
38706
  import { homedir as homedir8, platform as osPlatform } from "os";
38317
- import { join as join13 } from "path";
38707
+ import { join as join14 } from "path";
38318
38708
  var UnsupportedPlatformError = class extends Error {
38319
38709
  constructor(plat) {
38320
38710
  super(`gipity relay install does not support ${plat}`);
@@ -38332,8 +38722,8 @@ function display(argvs) {
38332
38722
  }
38333
38723
  function launchdPlan(cliPath) {
38334
38724
  const label2 = "ai.gipity.relay";
38335
- const path5 = join13(homedir8(), "Library", "LaunchAgents", `${label2}.plist`);
38336
- const logDir = join13(homedir8(), "Library", "Logs", "Gipity");
38725
+ const path5 = join14(homedir8(), "Library", "LaunchAgents", `${label2}.plist`);
38726
+ const logDir = join14(homedir8(), "Library", "Logs", "Gipity");
38337
38727
  const uid = process.getuid?.() ?? 0;
38338
38728
  const target = `gui/${uid}`;
38339
38729
  const content = `<?xml version="1.0" encoding="UTF-8"?>
@@ -38357,8 +38747,8 @@ function launchdPlan(cliPath) {
38357
38747
  <dict>
38358
38748
  <key>SuccessfulExit</key><false/>
38359
38749
  </dict>
38360
- <key>StandardOutPath</key><string>${join13(logDir, "relay.out.log")}</string>
38361
- <key>StandardErrorPath</key><string>${join13(logDir, "relay.err.log")}</string>
38750
+ <key>StandardOutPath</key><string>${join14(logDir, "relay.out.log")}</string>
38751
+ <key>StandardErrorPath</key><string>${join14(logDir, "relay.err.log")}</string>
38362
38752
  <key>ProcessType</key><string>Background</string>
38363
38753
  </dict>
38364
38754
  </plist>
@@ -38381,7 +38771,7 @@ function launchdPlan(cliPath) {
38381
38771
  }
38382
38772
  function systemdUserPlan(cliPath) {
38383
38773
  const unitName = "gipity-relay.service";
38384
- const path5 = join13(homedir8(), ".config", "systemd", "user", unitName);
38774
+ const path5 = join14(homedir8(), ".config", "systemd", "user", unitName);
38385
38775
  const content = `[Unit]
38386
38776
  Description=Gipity relay - local Claude Code control from the Gipity web CLI
38387
38777
  After=network-online.target
@@ -38419,7 +38809,7 @@ WantedBy=default.target
38419
38809
  }
38420
38810
  function windowsTaskPlan(cliPath) {
38421
38811
  const taskName = "GipityRelay";
38422
- const path5 = join13(homedir8(), "AppData", "Local", "Gipity", "relay-task.xml");
38812
+ const path5 = join14(homedir8(), "AppData", "Local", "Gipity", "relay-task.xml");
38423
38813
  const runsViaNode = /\.[cm]?js$/i.test(cliPath);
38424
38814
  const command = runsViaNode ? process.execPath : cliPath;
38425
38815
  const argLine = runsViaNode ? `"${cliPath}" relay run` : "relay run";
@@ -38481,13 +38871,13 @@ function windowsTaskPlan(cliPath) {
38481
38871
  init_platform();
38482
38872
  init_api();
38483
38873
  import { hostname as hostname3, userInfo, platform as osPlatform2 } from "os";
38484
- import { resolve as resolve12, dirname as dirname8 } from "path";
38874
+ import { resolve as resolve12, dirname as dirname9 } from "path";
38485
38875
  import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
38486
38876
 
38487
38877
  // src/relay/machine-id.ts
38488
38878
  import { createHash as createHash3 } from "crypto";
38489
38879
  import { execFileSync } from "child_process";
38490
- import { readFileSync as readFileSync15 } from "fs";
38880
+ import { readFileSync as readFileSync16 } from "fs";
38491
38881
  import { hostname as hostname2, platform, arch } from "os";
38492
38882
  function rawMachineId() {
38493
38883
  const override = process.env.GIPITY_RELAY_MACHINE_ID?.trim();
@@ -38496,7 +38886,7 @@ function rawMachineId() {
38496
38886
  if (platform() === "linux") {
38497
38887
  for (const path5 of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
38498
38888
  try {
38499
- const id2 = readFileSync15(path5, "utf-8").trim();
38889
+ const id2 = readFileSync16(path5, "utf-8").trim();
38500
38890
  if (id2) return `linux:${id2}`;
38501
38891
  } catch {
38502
38892
  }
@@ -38591,7 +38981,7 @@ function startDaemon() {
38591
38981
  function installAutostart(opts = {}) {
38592
38982
  const stdio = opts.stdio ?? "ignore";
38593
38983
  const plan2 = planFor({ cliPath: resolveCliPath() });
38594
- mkdirSync10(dirname8(plan2.path), { recursive: true });
38984
+ mkdirSync10(dirname9(plan2.path), { recursive: true });
38595
38985
  writeFileSync10(plan2.path, plan2.content);
38596
38986
  let ok = true;
38597
38987
  for (const argv2 of plan2.enableCmds) {
@@ -38616,15 +39006,6 @@ function removeAutostart(opts = {}) {
38616
39006
 
38617
39007
  // src/relay/onboarding.ts
38618
39008
  var ensureDaemonRunning = startDaemon;
38619
- function isWsl() {
38620
- if (process.platform !== "linux") return false;
38621
- if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) return true;
38622
- try {
38623
- return /microsoft/i.test(readFileSync16("/proc/version", "utf-8"));
38624
- } catch {
38625
- return false;
38626
- }
38627
- }
38628
39009
  async function runRelaySetup(opts) {
38629
39010
  const alreadyAnswered = getRelayEnabled() !== void 0;
38630
39011
  if (opts.mode === "offer-once" && alreadyAnswered) {
@@ -38640,12 +39021,12 @@ async function runRelaySetup(opts) {
38640
39021
  console.log("");
38641
39022
  console.log(` ${success("Registered as")} ${bold(existingDevice.name)} ${muted(`(${existingDevice.guid})`)}`);
38642
39023
  if (isPaused()) {
38643
- console.log(` ${dim("The relay is paused \u2014 resume it with")} ${brand("gipity relay resume")}${dim(".")}`);
39024
+ console.log(` ${dim("The relay is paused - resume it with")} ${brand("gipity relay resume")}${dim(".")}`);
38644
39025
  } else {
38645
39026
  console.log(` ${dim("The relay is running. Open")} ${brand("gipity.ai")}${dim(" and type `/claude` to drive Claude Code here.")}`);
38646
39027
  }
38647
39028
  console.log("");
38648
- console.log(` ${dim("To register this computer again \u2014 for example under a different name \u2014")}`);
39029
+ console.log(` ${dim("To register this computer again (for example under a different name),")}`);
38649
39030
  console.log(` ${dim("unregister it first, then re-run setup:")}`);
38650
39031
  console.log(` ${brand("gipity relay revoke")} ${dim("# unpairs this computer and removes the login service")}`);
38651
39032
  console.log(` ${brand("gipity connect")} ${dim("# register it again (asks for a new name)")}`);
@@ -38655,28 +39036,29 @@ async function runRelaySetup(opts) {
38655
39036
  }
38656
39037
  if (opts.mode === "run-now") {
38657
39038
  console.log(` ${bold("Set up this computer as a relay")}`);
38658
- 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.")}`);
38659
- console.log(` ${dim("It uses your Claude, Codex, or Grok subscription \u2014 the cheapest way to pay for tokens.")}`);
39039
+ console.log(` ${dim("Gipity drives the coding agents on this computer from the web (")}${brand("gipity.ai")}${dim(")")}`);
39040
+ console.log(` ${dim("on any browser, desktop or phone. Uses your Claude, Codex, or Grok")}`);
39041
+ console.log(` ${dim("subscription - the cheapest way to pay for tokens.")}`);
38660
39042
  } else {
38661
- console.log(` ${bold("Remote control of your coding agent")}`);
38662
- console.log(` ${dim("Drive your coding agent on this computer from the web (")}${brand("gipity.ai")}${dim(") on any browser (desktop or phone).")}`);
38663
- console.log("");
38664
- console.log(` ${dim("Enable now (takes 2 seconds) or turn on later with")} ${brand("gipity connect")}`);
39043
+ console.log(` ${bold("Optional:")} ${dim("Gipity can drive the coding agents on this computer from the")}`);
39044
+ console.log(` ${dim("web (")}${brand("gipity.ai")}${dim(") on any browser, desktop or phone. Like Claude Code remote")}`);
39045
+ console.log(` ${dim("control, but one pane for all your coding agents.")}`);
38665
39046
  }
38666
39047
  console.log("");
38667
- const promptText = opts.mode === "run-now" ? " Set up remote control on this computer?" : " Enable remote control?";
39048
+ const promptText = opts.mode === "run-now" ? " Set up remote control on this computer" : " Enable remote control";
38668
39049
  const enable = await confirm(promptText, { default: "yes" });
38669
39050
  if (!enable) {
38670
39051
  setRelayEnabled(false);
38671
- console.log(` ${muted("Skipped.")}`);
39052
+ console.log(` ${muted("Skipped. Turn on later with")} ${brand("gipity connect")}${muted(".")}`);
38672
39053
  console.log("");
38673
39054
  return false;
38674
39055
  }
39056
+ console.log("");
38675
39057
  const defaultName = friendlyDeviceName();
38676
39058
  const rawName = await prompt(` Device name [${bold(defaultName)}]: `);
38677
39059
  const name = (rawName || defaultName).trim();
38678
39060
  if (!name || name.length > 100) {
38679
- console.error(` ${error("Device name must be 1\u2013100 non-whitespace characters. Skipping.")}`);
39061
+ console.error(` ${error("Device name must be 1-100 non-whitespace characters. Skipping.")}`);
38680
39062
  setRelayEnabled(false);
38681
39063
  return false;
38682
39064
  }
@@ -38689,11 +39071,9 @@ async function runRelaySetup(opts) {
38689
39071
  console.error(` ${dim("Skipping for now - we'll offer again next time. Or turn it on with `gipity connect`.")}`);
38690
39072
  return false;
38691
39073
  }
38692
- const startNow = await confirm(" Start the relay now (and on future `gipity build` runs)?", { default: "yes" });
38693
- if (startNow) {
38694
- startDaemon();
38695
- }
38696
- const autostartOs = await confirm(" Also start at OS login (auto-start with Windows / macOS / Linux)?", { default: "yes" });
39074
+ startDaemon();
39075
+ console.log("");
39076
+ const autostartOs = await confirm(" Also start at OS login", { default: "yes" });
38697
39077
  if (autostartOs) {
38698
39078
  try {
38699
39079
  const res = installAutostart();
@@ -38705,8 +39085,6 @@ async function runRelaySetup(opts) {
38705
39085
  } else {
38706
39086
  console.log(` ${muted("Autostart install returned non-zero - you can run")} ${brand("gipity relay install")} ${muted("later.")}`);
38707
39087
  }
38708
- } else {
38709
- console.log(` ${success("Auto-start installed.")} ${dim(res.summary)}`);
38710
39088
  }
38711
39089
  } catch (err) {
38712
39090
  if (err instanceof UnsupportedPlatformError) {
@@ -38717,16 +39095,12 @@ async function runRelaySetup(opts) {
38717
39095
  }
38718
39096
  }
38719
39097
  if (getDiagnosticsConsent() === void 0) {
38720
- const diag = await confirm(
38721
- " Share anonymous diagnostics (CPU/GPU/memory/disk/versions) to improve reliability?",
38722
- { default: "yes" }
38723
- );
39098
+ console.log("");
39099
+ const diag = await confirm(" Share anonymous diagnostics info", { default: "yes" });
38724
39100
  setDiagnosticsConsent(diag);
38725
- 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`.")}`);
38726
39101
  }
38727
39102
  console.log("");
38728
- console.log(` ${success(`Registered as ${bold(device.name)} (${device.guid}).`)}`);
38729
- console.log(` ${dim("In the Gipity web CLI, type `/claude` to dispatch messages to this PC.")}`);
39103
+ console.log(` ${success(`Done! ${bold(device.name)} is set up. New chats from gipity.ai can now execute here.`)}`);
38730
39104
  console.log("");
38731
39105
  return true;
38732
39106
  }
@@ -38869,9 +39243,8 @@ function padR(s, width) {
38869
39243
  }
38870
39244
  function leadingEllipsify(s, maxW) {
38871
39245
  if (s.length <= maxW) return s;
38872
- if (maxW <= 1) return s.slice(-maxW);
38873
- if (maxW <= 3) return "\u2026".padStart(maxW);
38874
- return "\u2026" + s.slice(-(maxW - 1));
39246
+ if (maxW <= 4) return s.slice(-maxW);
39247
+ return "..." + s.slice(-(maxW - 3));
38875
39248
  }
38876
39249
  function center(s, width) {
38877
39250
  const gap = width - visLen(s);
@@ -38993,7 +39366,7 @@ function printFull(opts, outerW) {
38993
39366
  import { execSync as execSync2 } from "child_process";
38994
39367
  import { existsSync as existsSync12 } from "fs";
38995
39368
  import { homedir as homedir10 } from "os";
38996
- import { join as join14 } from "path";
39369
+ import { join as join15 } from "path";
38997
39370
  var CLAUDE_PACKAGE = "@anthropic-ai/claude-code";
38998
39371
  function claudeInstallPlan(platformOverride) {
38999
39372
  const plat = platformOverride ?? process.platform;
@@ -39012,7 +39385,7 @@ function isClaudeInstalled(platformOverride) {
39012
39385
  }
39013
39386
  function isClaudeAuthenticated() {
39014
39387
  if (process.env.CLAUDE_CODE_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY) return true;
39015
- return existsSync12(join14(homedir10(), ".claude", ".credentials.json"));
39388
+ return existsSync12(join15(homedir10(), ".claude", ".credentials.json"));
39016
39389
  }
39017
39390
  function probeClaudeAuthenticated() {
39018
39391
  if (!isClaudeInstalled()) return false;
@@ -39162,11 +39535,53 @@ var grokAdapter = {
39162
39535
  installHint: "curl -fsSL https://grok.x.ai/install.sh | sh (or see docs.x.ai/grok-build)"
39163
39536
  };
39164
39537
 
39538
+ // src/agents/agy.ts
39539
+ var MODEL_ID_TO_AGY_DISPLAY = {
39540
+ "gemini-3.5-flash": "Gemini 3.5 Flash (Medium)",
39541
+ "gemini-3.1-pro-preview": "Gemini 3.1 Pro (High)"
39542
+ };
39543
+ function translateModel(model) {
39544
+ return MODEL_ID_TO_AGY_DISPLAY[model] ?? model;
39545
+ }
39546
+ function projectArgs(resume) {
39547
+ return resume ? ["--conversation", resume] : ["--new-project"];
39548
+ }
39549
+ var agyAdapter = {
39550
+ key: "agy",
39551
+ source: "agy",
39552
+ displayName: "Antigravity",
39553
+ providerName: "Google",
39554
+ binary: "agy",
39555
+ buildInteractiveArgs({ resume, model }) {
39556
+ const args = [...projectArgs(resume)];
39557
+ if (model) args.push("--model", translateModel(model));
39558
+ return args;
39559
+ },
39560
+ buildHeadlessArgs({ message, resume, model, bypassApprovals }) {
39561
+ const args = ["-p", message, ...projectArgs(resume)];
39562
+ if (model) args.push("--model", translateModel(model));
39563
+ if (bypassApprovals) args.push("--dangerously-skip-permissions");
39564
+ return args;
39565
+ },
39566
+ sessionIdFromStreamEvent() {
39567
+ return null;
39568
+ },
39569
+ hooksSupportedOnPlatform(platform2) {
39570
+ return platform2 !== "win32";
39571
+ },
39572
+ // No stream-json ingest mapper for agy: hook capture carries the full
39573
+ // conversationId on every event (confirmed live, including headless -p),
39574
+ // so the daemon doesn't need to parse agy's stdout at all.
39575
+ daemonStreamCapture: false,
39576
+ installHint: "see https://antigravity.google (Google account sign-in required)"
39577
+ };
39578
+
39165
39579
  // src/agents/index.ts
39166
39580
  var AGENT_ADAPTERS = [
39167
39581
  claudeCodeAdapter,
39168
39582
  codexAdapter,
39169
- grokAdapter
39583
+ grokAdapter,
39584
+ agyAdapter
39170
39585
  ];
39171
39586
  var AGENT_KEYS = AGENT_ADAPTERS.map((a) => a.key);
39172
39587
  function getAdapter(key) {
@@ -39183,8 +39598,8 @@ function getAdapterBySource(source) {
39183
39598
  // src/prefs.ts
39184
39599
  import { existsSync as existsSync13, mkdirSync as mkdirSync11, readFileSync as readFileSync17, writeFileSync as writeFileSync11 } from "fs";
39185
39600
  import { homedir as homedir11 } from "os";
39186
- import { join as join15 } from "path";
39187
- var PREFS_PATH = join15(homedir11(), ".gipity", "prefs.json");
39601
+ import { join as join16 } from "path";
39602
+ var PREFS_PATH = join16(homedir11(), ".gipity", "prefs.json");
39188
39603
  function readPrefs() {
39189
39604
  if (!existsSync13(PREFS_PATH)) return {};
39190
39605
  try {
@@ -39200,17 +39615,17 @@ function writePrefs(update) {
39200
39615
  try {
39201
39616
  const current = readPrefs();
39202
39617
  const next = { ...current, ...update };
39203
- mkdirSync11(join15(homedir11(), ".gipity"), { recursive: true });
39618
+ mkdirSync11(join16(homedir11(), ".gipity"), { recursive: true });
39204
39619
  writeFileSync11(PREFS_PATH, JSON.stringify(next, null, 2) + "\n");
39205
39620
  } catch {
39206
39621
  }
39207
39622
  }
39208
39623
 
39209
39624
  // src/commands/build.ts
39210
- var __clDir = dirname9(fileURLToPath2(import.meta.url));
39625
+ var __clDir = dirname10(fileURLToPath2(import.meta.url));
39211
39626
  function readOwnPkg(fromDir) {
39212
- for (let d = fromDir; ; d = dirname9(d)) {
39213
- const p = join16(d, "package.json");
39627
+ for (let d = fromDir; ; d = dirname10(d)) {
39628
+ const p = join17(d, "package.json");
39214
39629
  if (existsSync14(p)) {
39215
39630
  try {
39216
39631
  const pkg2 = JSON.parse(readFileSync18(p, "utf-8"));
@@ -39218,13 +39633,13 @@ function readOwnPkg(fromDir) {
39218
39633
  } catch {
39219
39634
  }
39220
39635
  }
39221
- if (dirname9(d) === d) return {};
39636
+ if (dirname10(d) === d) return {};
39222
39637
  }
39223
39638
  }
39224
39639
  var __clPkg = readOwnPkg(__clDir);
39225
39640
  function reportSyncResult(result) {
39226
39641
  if (result.aborted) {
39227
- console.log(` ${warning("Merge cancelled \u2014 this folder was NOT synced with the project.")}`);
39642
+ console.log(` ${warning("Merge cancelled - this folder was NOT synced with the project.")}`);
39228
39643
  console.log(` ${muted("For a clean copy, quit and open the project in an empty folder. To merge anyway, run `gipity sync`.")}`);
39229
39644
  return;
39230
39645
  }
@@ -39232,16 +39647,16 @@ function reportSyncResult(result) {
39232
39647
  console.log(` Synced ${result.applied} change${result.applied > 1 ? "s" : ""} with Gipity.`);
39233
39648
  }
39234
39649
  if (result.errors.length) {
39235
- console.log(` ${warning(`Sync finished with ${result.errors.length} problem${result.errors.length === 1 ? "" : "s"} \u2014 your local copy may be incomplete:`)}`);
39650
+ console.log(` ${warning(`Sync finished with ${result.errors.length} problem${result.errors.length === 1 ? "" : "s"} - your local copy may be incomplete:`)}`);
39236
39651
  for (const e of result.errors.slice(0, 8)) console.log(` - ${e}`);
39237
- if (result.errors.length > 8) console.log(` \u2026and ${result.errors.length - 8} more.`);
39652
+ if (result.errors.length > 8) console.log(` ...and ${result.errors.length - 8} more.`);
39238
39653
  console.log(` ${muted("Nothing was deleted. Re-run `gipity sync` to finish the pull.")}`);
39239
39654
  }
39240
39655
  }
39241
- async function fetchProjectStats(projectGuid, cwd) {
39242
- if (projectGuid) {
39656
+ async function fetchProjectStats(projectGuid2, cwd) {
39657
+ if (projectGuid2) {
39243
39658
  try {
39244
- const res = await get(`/projects/${encodeURIComponent(projectGuid)}/files/stats`);
39659
+ const res = await get(`/projects/${encodeURIComponent(projectGuid2)}/files/stats`);
39245
39660
  const d = res.data;
39246
39661
  const topLevelNames = d.top_level.map((e) => e.type === "directory" ? `${e.name}/` : e.name);
39247
39662
  return {
@@ -39262,12 +39677,12 @@ function localFsFallback(dir) {
39262
39677
  const topLevelEntries = [];
39263
39678
  const walk = (d, depth) => {
39264
39679
  try {
39265
- for (const name of readdirSync7(d).sort()) {
39680
+ for (const name of readdirSync8(d).sort()) {
39266
39681
  if (isSyncIgnored(name)) continue;
39267
39682
  let isDir = false;
39268
39683
  let size = 0;
39269
39684
  try {
39270
- const st2 = statSync7(join16(d, name));
39685
+ const st2 = statSync8(join17(d, name));
39271
39686
  isDir = st2.isDirectory();
39272
39687
  size = st2.isFile() ? st2.size : 0;
39273
39688
  } catch {
@@ -39276,7 +39691,7 @@ function localFsFallback(dir) {
39276
39691
  if (isDir) {
39277
39692
  folderCount++;
39278
39693
  if (depth === 0) topLevelEntries.push(`${name}/`);
39279
- walk(join16(d, name), depth + 1);
39694
+ walk(join17(d, name), depth + 1);
39280
39695
  } else {
39281
39696
  fileCount++;
39282
39697
  totalBytes += size;
@@ -39287,7 +39702,7 @@ function localFsFallback(dir) {
39287
39702
  }
39288
39703
  };
39289
39704
  walk(dir, 0);
39290
- const topLevel = topLevelEntries.length ? topLevelEntries.slice(0, 20).join(", ") + (topLevelEntries.length > 20 ? ", \u2026" : "") : "(empty directory)";
39705
+ const topLevel = topLevelEntries.length ? topLevelEntries.slice(0, 20).join(", ") + (topLevelEntries.length > 20 ? ", ..." : "") : "(empty directory)";
39291
39706
  return { fileCount, folderCount, totalBytes, topLevel };
39292
39707
  }
39293
39708
  async function buildProjectContextBlock2(opts) {
@@ -39309,7 +39724,7 @@ function hasStreamJsonFlag(args) {
39309
39724
  }
39310
39725
  function markFolderTrusted(dir) {
39311
39726
  try {
39312
- const file = join16(homedir12(), ".claude.json");
39727
+ const file = join17(homedir12(), ".claude.json");
39313
39728
  const cfg = existsSync14(file) ? JSON.parse(readFileSync18(file, "utf-8")) : {};
39314
39729
  if (typeof cfg !== "object" || cfg === null) return;
39315
39730
  if (typeof cfg.projects !== "object" || cfg.projects === null) cfg.projects = {};
@@ -39437,7 +39852,7 @@ async function runLaunch(cmdName, cmdCfg, opts) {
39437
39852
  if (existing && !opts.newProject && !opts.project && !nonInteractive) {
39438
39853
  const cwdHasConfig = existsSync14(resolve13(process.cwd(), ".gipity.json"));
39439
39854
  if (!cwdHasConfig && isLikelyEmpty(process.cwd())) {
39440
- const ancestorRoot = dirname9(getConfigPath());
39855
+ const ancestorRoot = dirname10(getConfigPath());
39441
39856
  console.log(` ${bold("You are inside an existing Gipity project.")}
39442
39857
  `);
39443
39858
  console.log(` ${bold("1.")} Continue in ${brand(existing.projectSlug)} ${muted(`(rooted at ${formatCwdLabel(ancestorRoot)})`)}`);
@@ -39484,7 +39899,7 @@ async function runLaunch(cmdName, cmdCfg, opts) {
39484
39899
  }
39485
39900
  project = found;
39486
39901
  }
39487
- const projectDir2 = opts.here ? process.cwd() : join16(getProjectsRoot(), project.slug);
39902
+ const projectDir2 = opts.here ? process.cwd() : join17(getProjectsRoot(), project.slug);
39488
39903
  mkdirSync12(projectDir2, { recursive: true });
39489
39904
  process.chdir(projectDir2);
39490
39905
  clearConfigCache();
@@ -39513,6 +39928,7 @@ async function runLaunch(cmdName, cmdCfg, opts) {
39513
39928
  console.log(` ${warning("Could not sync files (will retry on next prompt):")} ${err.message}`);
39514
39929
  }
39515
39930
  setupProjectTools();
39931
+ if (!nonInteractive) console.log(` ${muted("Installed Gipity plugin, skills, and hooks.")}`);
39516
39932
  if (nonInteractive) {
39517
39933
  headlessNewProject = isNewProject;
39518
39934
  } else {
@@ -39526,7 +39942,7 @@ async function runLaunch(cmdName, cmdCfg, opts) {
39526
39942
  `);
39527
39943
  setupProjectTools();
39528
39944
  let doSync = true;
39529
- const projectRoot = dirname9(getConfigPath());
39945
+ const projectRoot = dirname10(getConfigPath());
39530
39946
  const scan = scanForAdoption(projectRoot);
39531
39947
  if (scan.tier === "refuse" && !nonInteractive) {
39532
39948
  const sizeStr = scan.truncated ? `>${formatBytes(ADOPT_THRESHOLDS.REFUSE_BYTES)}` : formatBytes(scan.bytes);
@@ -39585,7 +40001,7 @@ async function runLaunch(cmdName, cmdCfg, opts) {
39585
40001
  let agentGuid = "";
39586
40002
  const result = forceAdoptCwd ? { kind: "adopt-cwd" } : projects.length > 0 ? await pickOrCreateProject(projects, existingSlugs) : { kind: "create-new", project: await createNewProject(existingSlugs) };
39587
40003
  if (result.kind === "adopt-cwd") {
39588
- const cwdBase = basename4(process.cwd());
40004
+ const cwdBase = basename5(process.cwd());
39589
40005
  const adoptSlug = slugify(cwdBase) || "project";
39590
40006
  const adoptName = (cwdBase || adoptSlug).slice(0, 100);
39591
40007
  accountSlug = await getAccountSlug();
@@ -39607,7 +40023,7 @@ async function runLaunch(cmdName, cmdCfg, opts) {
39607
40023
  } else {
39608
40024
  project = result.project;
39609
40025
  isNewProject = result.kind === "create-new";
39610
- const projectDir2 = join16(getProjectsRoot(), project.slug);
40026
+ const projectDir2 = join17(getProjectsRoot(), project.slug);
39611
40027
  mkdirSync12(projectDir2, { recursive: true });
39612
40028
  process.chdir(projectDir2);
39613
40029
  clearConfigCache();
@@ -39637,6 +40053,7 @@ async function runLaunch(cmdName, cmdCfg, opts) {
39637
40053
  }
39638
40054
  initialPrompt = "";
39639
40055
  setupProjectTools();
40056
+ if (!nonInteractive) console.log(` ${muted("Installed Gipity plugin, skills, and hooks.")}`);
39640
40057
  console.log(` ${success(`Project "${project.name}" ready.`)}
39641
40058
  `);
39642
40059
  }
@@ -39732,9 +40149,8 @@ async function runLaunch(cmdName, cmdCfg, opts) {
39732
40149
  }
39733
40150
  if (!nonInteractive) {
39734
40151
  console.log(` ${bold("Launching Claude Code, powered by Gipity.")}`);
39735
- console.log(` ${muted("Just tell Claude what you'd like to build or do - everything Claude can do,")}`);
39736
- console.log(` ${muted("plus hosting, databases, and live deploys on Gipity.")}`);
39737
40152
  if (convGuidForHooks) {
40153
+ console.log("");
39738
40154
  console.log(` ${muted(`This session is saved to Gipity - watch it live at ${brand("prompt.gipity.ai")}. (--no-capture on init to disable.)`)}`);
39739
40155
  }
39740
40156
  console.log("");
@@ -39832,11 +40248,9 @@ async function pickAgent(lastUsed) {
39832
40248
  console.log(` ${bold("Which coding agent?")}
39833
40249
  `);
39834
40250
  AGENT_ADAPTERS.forEach((a, i) => {
39835
- const installed = binaryOnPath(a.binary) || a.key === "claude" && isClaudeInstalled();
39836
40251
  const notes = [];
39837
40252
  if (a.key === lastUsed) notes.push("last used");
39838
- if (!installed) notes.push(a.ensureInstalled ? "not installed - we'll install it" : "not installed");
39839
- if (a.key === "codex" && !a.hooksSupportedOnPlatform(process.platform)) {
40253
+ if ((a.key === "codex" || a.key === "agy") && !a.hooksSupportedOnPlatform(process.platform)) {
39840
40254
  notes.push("session recording unavailable on Windows");
39841
40255
  }
39842
40256
  const note = notes.length ? ` ${muted(`(${notes.join(", ")})`)}` : "";
@@ -39948,9 +40362,8 @@ Sending to ${adapter.displayName}: ${message}
39948
40362
  } else {
39949
40363
  agentArgs = [...adapter.buildInteractiveArgs({ resume, model }), ...extras];
39950
40364
  console.log(` ${bold(`Launching ${adapter.displayName}, powered by Gipity.`)}`);
39951
- console.log(` ${muted(`Just tell ${adapter.displayName} what you'd like to build or do - plus hosting,`)}`);
39952
- console.log(` ${muted("databases, and live deploys on Gipity.")}`);
39953
40365
  if (convGuid) {
40366
+ console.log("");
39954
40367
  console.log(` ${muted(`This session is saved to Gipity - watch it live at ${brand("prompt.gipity.ai")}. (--no-capture on init to disable.)`)}`);
39955
40368
  }
39956
40369
  if (adapter.oneTimeSetupNote) {
@@ -39989,7 +40402,7 @@ ${doneLine}
39989
40402
  });
39990
40403
  }
39991
40404
  function flushHeadlessCapture(adapter, sessionId, convGuid) {
39992
- const runner = join16(__clDir, "..", "hooks", "capture-runner.js");
40405
+ const runner = join17(__clDir, "..", "hooks", "capture-runner.js");
39993
40406
  if (!existsSync14(runner)) return;
39994
40407
  const payload = JSON.stringify({ session_id: sessionId, cwd: process.cwd() });
39995
40408
  const env = { ...process.env, GIPITY_CONVERSATION_GUID: convGuid };
@@ -40380,7 +40793,7 @@ var addCommand = new Command("add").description("Add a template or kit").argumen
40380
40793
  console.error(muted(isKit ? "Installing kit (server-side install pipeline)..." : "Installing (server writes files + generates favicons; first add for a title can take ~10s)..."));
40381
40794
  }
40382
40795
  const doAdd = () => post(`/projects/${config.projectGuid}/add`, body);
40383
- const res = opts.json ? await doAdd() : await withSpinner("Installing\u2026", doAdd, { done: null });
40796
+ const res = opts.json ? await doAdd() : await withSpinner("Installing...", doAdd, { done: null });
40384
40797
  const syncResult = await sync({ interactive: false, progress: opts.json ? void 0 : createProgressReporter() });
40385
40798
  const data = res.data;
40386
40799
  if (opts.json) {
@@ -40414,6 +40827,87 @@ Pulled ${syncResult.applied} files to local.`);
40414
40827
  }
40415
40828
  }));
40416
40829
 
40830
+ // src/commands/brand.ts
40831
+ init_api();
40832
+ init_config();
40833
+ init_colors();
40834
+ function printBrand(d) {
40835
+ console.log(`${bold("Glyph:")} ${d.resolved.glyph}${d.resolved.isEmoji ? muted(" (emoji)") : ""}`);
40836
+ console.log(`${bold("Accent:")} ${d.resolved.accent}${d.branding.primary_color ? "" : muted(" (derived from project)")}`);
40837
+ if (d.branding.app_name) console.log(`${bold("Name:")} ${d.branding.app_name}`);
40838
+ if (d.branding.tagline) console.log(`${bold("Tagline:")} ${d.branding.tagline}`);
40839
+ }
40840
+ var DEPLOY_HINT = "Deploy to publish the new assets: gipity deploy dev";
40841
+ var brandCommand = new Command("brand").description("The app's generated icons + share card (favicons, iOS icon, og-image)").addHelpText("after", `
40842
+ Examples:
40843
+ gipity brand Show the current brand
40844
+ gipity brand set --emoji \u{1F98D} Emoji icon + share card, regenerate all assets
40845
+ gipity brand set --glyph R --color "#3b82f6" Letter glyph on a blue accent
40846
+ gipity brand set --tagline "Bananas at 60fps" Subtitle on the share card
40847
+ gipity brand apply Re-render assets from the stored brand
40848
+ gipity brand apply --fix-head Migrate an older app: assets + splice the
40849
+ share/SEO tags into src/index.html
40850
+
40851
+ Assets are generated server-side into src/images/ (+ src/manifest.webmanifest)
40852
+ and are versioned like any other file - rollback restores previous ones.`);
40853
+ brandCommand.command("show", { isDefault: true }).description("Show the stored brand and the resolved render spec").option("--project <guid-or-slug>", "Target a specific project instead of cwd / Home").option("--json", "Output raw JSON").action((opts) => run("Brand", async () => {
40854
+ const { config } = await resolveProjectContext({ projectOverride: opts.project });
40855
+ const res = await get(`/projects/${config.projectGuid}/brand`);
40856
+ if (opts.json) {
40857
+ console.log(JSON.stringify(res.data));
40858
+ return;
40859
+ }
40860
+ printBrand(res.data);
40861
+ console.log(muted(`
40862
+ Assets: ${(res.data.assets ?? []).join(", ")}`));
40863
+ console.log(muted(`Change it: gipity brand set --emoji \u{1F3A8} | --glyph A | --color "#RRGGBB" | --tagline "..."`));
40864
+ }));
40865
+ brandCommand.command("set").description("Change brand fields and regenerate all assets").option("--emoji <emoji>", "Emoji glyph for the icon + share card (e.g. \u{1F98D})").option("--glyph <char>", "Letter/digit glyph (alias of --emoji, any single character)").option("--color <hex>", "Accent color, #RRGGBB").option("--name <name>", "Display name override (og/share title stays the page title)").option("--tagline <text>", "One-line subtitle on the share card (max 120 chars)").option("--fix-head", "Also splice the share/SEO head block into src/index.html (for apps installed before templates shipped it)").option("--no-regenerate", "Store the fields without re-rendering assets").option("--project <guid-or-slug>", "Target a specific project instead of cwd / Home").option("--json", "Output raw JSON").action((opts) => run("Brand", async () => {
40866
+ const { config } = await resolveProjectContext({ projectOverride: opts.project });
40867
+ const body = {};
40868
+ const glyph = opts.emoji ?? opts.glyph;
40869
+ if (glyph) body.icon_glyph = glyph;
40870
+ if (opts.color) body.primary_color = opts.color;
40871
+ if (opts.name) body.app_name = opts.name;
40872
+ if (opts.tagline) body.tagline = opts.tagline;
40873
+ if (opts.regenerate === false) body.regenerate = false;
40874
+ if (opts.fixHead) body.fix_head = true;
40875
+ if (!glyph && !opts.color && !opts.name && !opts.tagline) {
40876
+ console.error("Nothing to set - pass --emoji, --glyph, --color, --name, or --tagline. (gipity brand set --help; use gipity brand apply to just re-render)");
40877
+ process.exitCode = 1;
40878
+ return;
40879
+ }
40880
+ const res = await post(`/projects/${config.projectGuid}/brand`, body);
40881
+ if (opts.json) {
40882
+ console.log(JSON.stringify(res.data));
40883
+ return;
40884
+ }
40885
+ printBrand(res.data);
40886
+ reportFiles(res.data.files ?? []);
40887
+ }));
40888
+ brandCommand.command("apply").description("Re-render all brand assets from the stored brand (no field changes)").option("--fix-head", "Also splice the share/SEO head block into src/index.html (for apps installed before templates shipped it)").option("--project <guid-or-slug>", "Target a specific project instead of cwd / Home").option("--json", "Output raw JSON").action((opts) => run("Brand", async () => {
40889
+ const { config } = await resolveProjectContext({ projectOverride: opts.project });
40890
+ const body = opts.fixHead ? { fix_head: true } : {};
40891
+ const res = await post(`/projects/${config.projectGuid}/brand`, body);
40892
+ if (opts.json) {
40893
+ console.log(JSON.stringify(res.data));
40894
+ return;
40895
+ }
40896
+ reportFiles(res.data.files ?? []);
40897
+ }));
40898
+ function reportFiles(files) {
40899
+ if (!files.length) {
40900
+ console.log(muted("\nStored (assets not regenerated - run gipity brand apply)."));
40901
+ return;
40902
+ }
40903
+ console.log(success(`
40904
+ \u2713 Regenerated ${files.length} asset${files.length === 1 ? "" : "s"}.`));
40905
+ if (files.includes("src/index.html")) {
40906
+ console.log(success("\u2713 src/index.html head updated (page title/description preserved)."));
40907
+ }
40908
+ console.log(muted(` ${DEPLOY_HINT}`));
40909
+ }
40910
+
40417
40911
  // src/commands/remove.ts
40418
40912
  init_api();
40419
40913
  init_config();
@@ -40428,7 +40922,7 @@ var removeCommand = new Command("remove").description("Remove a kit").argument("
40428
40922
  }
40429
40923
  }
40430
40924
  const doRemove = () => post(`/projects/${config.projectGuid}/remove`, { name: kit });
40431
- const res = opts.json ? await doRemove() : await withSpinner("Removing\u2026", doRemove, { done: null });
40925
+ const res = opts.json ? await doRemove() : await withSpinner("Removing...", doRemove, { done: null });
40432
40926
  const data = res.data;
40433
40927
  const syncResult = await sync({
40434
40928
  interactive: false,
@@ -40478,7 +40972,7 @@ function dispositionFilename(headers) {
40478
40972
  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 () => {
40479
40973
  const config = requireConfig();
40480
40974
  const doExport = () => downloadWithHeaders(`/projects/${config.projectGuid}/export`);
40481
- const { buffer, headers } = opts.json ? await doExport() : await withSpinner("Exporting\u2026", doExport, { done: null });
40975
+ const { buffer, headers } = opts.json ? await doExport() : await withSpinner("Exporting...", doExport, { done: null });
40482
40976
  const filename = dispositionFilename(headers) ?? `${config.projectSlug}.gip`;
40483
40977
  let dest = path2.resolve(output ?? filename);
40484
40978
  if (output && fs3.existsSync(dest) && fs3.statSync(dest).isDirectory()) {
@@ -40486,14 +40980,14 @@ var saveCommand = new Command("save").description("Save this app as a portable .
40486
40980
  }
40487
40981
  fs3.mkdirSync(path2.dirname(dest), { recursive: true });
40488
40982
  fs3.writeFileSync(dest, buffer);
40489
- const sha2562 = createHash4("sha256").update(buffer).digest("hex");
40983
+ const sha256 = createHash4("sha256").update(buffer).digest("hex");
40490
40984
  const skipped = Number(headers.get("x-gip-skipped") ?? 0);
40491
40985
  const entries = zipEntryCount(buffer);
40492
40986
  if (opts.json) {
40493
40987
  console.log(JSON.stringify({
40494
40988
  path: dest,
40495
40989
  bytes: buffer.length,
40496
- sha256: sha2562,
40990
+ sha256,
40497
40991
  ...entries !== null ? { entries } : {},
40498
40992
  skipped
40499
40993
  }));
@@ -40503,7 +40997,7 @@ var saveCommand = new Command("save").description("Save this app as a portable .
40503
40997
  console.log(` Path: ${dest}`);
40504
40998
  console.log(` Size: ${formatBytes(buffer.length)}`);
40505
40999
  if (entries !== null) console.log(` Entries: ${entries} (including the bundle manifest)`);
40506
- console.log(` SHA-256: ${sha2562}`);
41000
+ console.log(` SHA-256: ${sha256}`);
40507
41001
  if (skipped > 0) {
40508
41002
  console.log("");
40509
41003
  console.warn(warning(`${skipped} file(s) were skipped (unreadable or over 100 MB) - this bundle is partial.`));
@@ -40620,7 +41114,7 @@ var loadCommand = new Command("load").description("Create a new app from a .gip
40620
41114
  };
40621
41115
  }
40622
41116
  if (opts.inspect) {
40623
- const res = opts.json ? await doInspect() : await withSpinner("Inspecting\u2026", doInspect, { done: null });
41117
+ const res = opts.json ? await doInspect() : await withSpinner("Inspecting...", doInspect, { done: null });
40624
41118
  if (opts.json) {
40625
41119
  console.log(JSON.stringify(res.data));
40626
41120
  return;
@@ -40628,7 +41122,7 @@ var loadCommand = new Command("load").description("Create a new app from a .gip
40628
41122
  printInspect(res.data, source);
40629
41123
  return;
40630
41124
  }
40631
- const { data, partial } = opts.json ? await doImport() : await withSpinner("Importing\u2026", doImport, { done: null });
41125
+ const { data, partial } = opts.json ? await doImport() : await withSpinner("Importing...", doImport, { done: null });
40632
41126
  const project = data.project;
40633
41127
  const dir = path3.join(getProjectsRoot(), project.slug);
40634
41128
  fs4.mkdirSync(dir, { recursive: true });
@@ -41029,7 +41523,7 @@ ${error(`\u26A0 ${problems} error/crash line(s) flagged above`)}`
41029
41523
  );
41030
41524
  if (problems > 0) process.exitCode = 1;
41031
41525
  }
41032
- 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", `
41526
+ 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", `
41033
41527
  Examples:
41034
41528
  # Passive: load in 3 staggered clients, flag console errors
41035
41529
  gipity page test "https://dev.gipity.ai/me/app/" --clients 3 --stagger 8
@@ -41087,9 +41581,6 @@ function looksLikeHtml(body) {
41087
41581
  const head = body.slice(0, 300).toLowerCase();
41088
41582
  return /<!doctype html|<html[\s>]/.test(head);
41089
41583
  }
41090
- function sha256(s) {
41091
- return createHash5("sha256").update(s).digest("hex");
41092
- }
41093
41584
  function baseWithSlash(url) {
41094
41585
  return url.endsWith("/") ? url : url + "/";
41095
41586
  }
@@ -41099,8 +41590,14 @@ function resolveUrl(base, path5) {
41099
41590
  async function fetchRaw(url) {
41100
41591
  try {
41101
41592
  const res = await fetch(url, { redirect: "follow" });
41102
- const body = await res.text();
41103
- return { status: res.status, contentType: res.headers.get("content-type"), body };
41593
+ const buf = Buffer.from(await res.arrayBuffer());
41594
+ return {
41595
+ status: res.status,
41596
+ contentType: res.headers.get("content-type"),
41597
+ body: buf.toString("utf-8"),
41598
+ bytes: buf.length,
41599
+ sha256: createHash5("sha256").update(buf).digest("hex")
41600
+ };
41104
41601
  } catch {
41105
41602
  return null;
41106
41603
  }
@@ -41112,17 +41609,16 @@ async function probeShell(base) {
41112
41609
  return {
41113
41610
  served: r.status >= 200 && r.status < 300,
41114
41611
  status: r.status,
41115
- sha256: sha256(r.body),
41612
+ sha256: r.sha256,
41116
41613
  isHtml: looksLikeHtml(r.body)
41117
41614
  };
41118
41615
  }
41119
41616
  function classify(path5, r, shell) {
41120
41617
  if (!r) return { status: null, contentType: null, bytes: 0, verdict: "MISSING", detail: "fetch failed (host unreachable)" };
41121
- const bytes = Buffer.byteLength(r.body);
41122
- const base = { status: r.status, contentType: r.contentType, bytes };
41618
+ const base = { status: r.status, contentType: r.contentType, bytes: r.bytes };
41123
41619
  if (r.status >= 400) return { ...base, verdict: "MISSING", detail: `HTTP ${r.status}` };
41124
41620
  const expect = expectedType(path5);
41125
- if (shell.served && shell.sha256 && sha256(r.body) === shell.sha256) {
41621
+ if (shell.served && shell.sha256 && r.sha256 === shell.sha256) {
41126
41622
  return { ...base, verdict: "MISSING", detail: `HTTP ${r.status} but served the SPA shell (file not deployed)` };
41127
41623
  }
41128
41624
  if (expect && expect !== "html" && looksLikeHtml(r.body)) {
@@ -41138,7 +41634,7 @@ var TAG = {
41138
41634
  "MISSING": error("MISSING "),
41139
41635
  "WRONG-TYPE": warning("WRONG-TYPE")
41140
41636
  };
41141
- 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 () => {
41637
+ 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 () => {
41142
41638
  const shell = await probeShell(url);
41143
41639
  const results = [];
41144
41640
  for (const p of paths) {
@@ -41184,10 +41680,31 @@ pageCommand.action(() => {
41184
41680
 
41185
41681
  // src/commands/records.ts
41186
41682
  init_api();
41683
+ init_auth();
41187
41684
  init_config();
41188
41685
  init_colors();
41189
41686
  init_utils();
41190
41687
  var recordsCommand = new Command("records").description("Manage app records (Gipity Records - validated CRUD with audit history)");
41688
+ var ANON_FLAG = "--anon";
41689
+ var ANON_HELP = "Call as an anonymous visitor (the public path a signed-out user hits) instead of as your signed-in account";
41690
+ async function recordsHttp(opts) {
41691
+ const config = requireConfig();
41692
+ if (!opts.anon) {
41693
+ const who = getAuth()?.email;
41694
+ console.error(muted(`Auth: calling as ${who ?? "your signed-in account"} (the owner persona; use ${ANON_FLAG} for the public visitor path)`));
41695
+ return { guid: config.projectGuid, get, post, put, patch, del };
41696
+ }
41697
+ console.error(muted("Auth: anonymous visitor (the public path a signed-out user hits)"));
41698
+ const headers = await mintAppToken(config.projectGuid);
41699
+ return {
41700
+ guid: config.projectGuid,
41701
+ get: (path5) => publicRequest("GET", path5, void 0, headers),
41702
+ post: (path5, body) => publicRequest("POST", path5, body, headers),
41703
+ put: (path5, body) => publicRequest("PUT", path5, body, headers),
41704
+ patch: (path5, body) => publicRequest("PATCH", path5, body, headers),
41705
+ del: (path5, body) => publicRequest("DELETE", path5, body, headers)
41706
+ };
41707
+ }
41191
41708
  recordsCommand.command("list").description("List record tables").option("--json", "Output as JSON").action((opts) => run("List", async () => {
41192
41709
  const config = requireConfig();
41193
41710
  const res = await get(`/api/${config.projectGuid}/records-config`);
@@ -41219,16 +41736,16 @@ recordsCommand.command("config <table>").description("Show or set a table's Reco
41219
41736
  res.data
41220
41737
  );
41221
41738
  }));
41222
- recordsCommand.command("query <table>").description("List records").option("--filter <filter>", 'Filter string (e.g., "status:eq:active")').option("--sort <sort>", 'Sort string (e.g., "created_at:desc")').option("--limit <n>", "Max rows", "20").option("--offset <n>", "Offset", "0").option("--fields <fields>", "Comma-separated column names").option("--json", "Output as JSON").action((table, opts) => run("Query", async () => {
41223
- const config = requireConfig();
41739
+ recordsCommand.command("query <table>").description("List records").option("--filter <filter>", 'Filter string (e.g., "status:eq:active")').option("--sort <sort>", 'Sort string (e.g., "created_at:desc")').option("--limit <n>", "Max rows", "20").option("--offset <n>", "Offset", "0").option("--fields <fields>", "Comma-separated column names").option(ANON_FLAG, ANON_HELP).option("--json", "Output as JSON").action((table, opts) => run("Query", async () => {
41740
+ const api = await recordsHttp(opts);
41224
41741
  const params = new URLSearchParams();
41225
41742
  if (opts.filter) params.set("filter", opts.filter);
41226
41743
  if (opts.sort) params.set("sort", opts.sort);
41227
41744
  params.set("limit", opts.limit);
41228
41745
  params.set("offset", opts.offset);
41229
41746
  if (opts.fields) params.set("fields", opts.fields);
41230
- const res = await get(
41231
- `/api/${config.projectGuid}/records/${table}?${params}`
41747
+ const res = await api.get(
41748
+ `/api/${api.guid}/records/${table}?${params}`
41232
41749
  );
41233
41750
  if (opts.json) {
41234
41751
  console.log(JSON.stringify(res));
@@ -41241,41 +41758,42 @@ recordsCommand.command("query <table>").description("List records").option("--fi
41241
41758
  console.log("");
41242
41759
  }
41243
41760
  }));
41244
- recordsCommand.command("get <table> <id>").description("Get a record").option("--json", "Output as JSON").action((table, id2, opts) => run("Get", async () => {
41245
- const config = requireConfig();
41246
- const res = await get(`/api/${config.projectGuid}/records/${table}/${id2}`);
41761
+ recordsCommand.command("get <table> <id>").description("Get a record").option(ANON_FLAG, ANON_HELP).option("--json", "Output as JSON").action((table, id2, opts) => run("Get", async () => {
41762
+ const api = await recordsHttp(opts);
41763
+ const res = await api.get(`/api/${api.guid}/records/${table}/${id2}`);
41247
41764
  console.log(opts.json ? JSON.stringify(res.data) : JSON.stringify(res.data, null, 2));
41248
41765
  }));
41249
- recordsCommand.command("history <table> <id>").description("Audit history for a record (who/what changed it, with English summaries)").option("--limit <n>", "Max events", "20").option("--json", "Output as JSON").action((table, id2, opts) => run("History", async () => {
41250
- const config = requireConfig();
41251
- const res = await get(
41252
- `/api/${config.projectGuid}/records/${table}/${id2}/history?limit=${encodeURIComponent(opts.limit)}`
41766
+ recordsCommand.command("history <table> [id]").description("Audit history (who/what changed it, with English summaries). Omit <id> for the whole table's feed.").option("--limit <n>", "Max events", "20").option(ANON_FLAG, ANON_HELP).option("--json", "Output as JSON").action((table, id2, opts) => run("History", async () => {
41767
+ const api = await recordsHttp(opts);
41768
+ const scope = id2 ? `${table}/${encodeURIComponent(id2)}` : table;
41769
+ const res = await api.get(
41770
+ `/api/${api.guid}/records/${scope}/history?limit=${encodeURIComponent(opts.limit)}`
41253
41771
  );
41254
- printList(res.data, opts, "No history for this record.", (e) => {
41772
+ printList(res.data, opts, id2 ? "No history for this record." : `No history for "${table}".`, (e) => {
41255
41773
  const summary = e.detail?.summary || `${e.action} ${e.entity_type} ${e.entity_id}`;
41256
41774
  return `${muted(e.created_at)} ${bold(e.source || "-")} ${summary}`;
41257
41775
  });
41258
41776
  }));
41259
- recordsCommand.command("create <table>").description("Create a record").requiredOption("--data <json>", "JSON object with field values").option("--json", "Output as JSON").action((table, opts) => run("Create", async () => {
41260
- const config = requireConfig();
41777
+ recordsCommand.command("create <table>").description("Create a record").requiredOption("--data <json>", "JSON object with field values").option(ANON_FLAG, ANON_HELP).option("--json", "Output as JSON").action((table, opts) => run("Create", async () => {
41778
+ const api = await recordsHttp(opts);
41261
41779
  const data = JSON.parse(opts.data);
41262
- const res = await post(`/api/${config.projectGuid}/records/${table}`, data);
41780
+ const res = await api.post(`/api/${api.guid}/records/${table}`, data);
41263
41781
  printResult(`Created: ${JSON.stringify(res.data)}`, opts, res.data);
41264
41782
  }));
41265
- recordsCommand.command("update <table> <id>").description("Update a record").requiredOption("--data <json>", "JSON object with fields to update").option("--json", "Output as JSON").action((table, id2, opts) => run("Update", async () => {
41266
- const config = requireConfig();
41783
+ recordsCommand.command("update <table> <id>").description("Update a record").requiredOption("--data <json>", "JSON object with fields to update").option(ANON_FLAG, ANON_HELP).option("--json", "Output as JSON").action((table, id2, opts) => run("Update", async () => {
41784
+ const api = await recordsHttp(opts);
41267
41785
  const data = JSON.parse(opts.data);
41268
- const res = await put(`/api/${config.projectGuid}/records/${table}/${id2}`, data);
41786
+ const res = await api.put(`/api/${api.guid}/records/${table}/${id2}`, data);
41269
41787
  printResult(`Updated: ${JSON.stringify(res.data)}`, opts, res.data);
41270
41788
  }));
41271
- recordsCommand.command("delete <table> <id>").description("Delete a record").action((table, id2) => run("Delete", async () => {
41789
+ recordsCommand.command("delete <table> <id>").description("Delete a record").option(ANON_FLAG, ANON_HELP).option("--json", "Output as JSON").action((table, id2, opts) => run("Delete", async () => {
41272
41790
  if (!await confirm(`Delete record ${id2} from "${table}"?`)) {
41273
- console.log("Cancelled.");
41791
+ printResult("Cancelled.", opts, { table, id: id2, deleted: false, cancelled: true });
41274
41792
  return;
41275
41793
  }
41276
- const config = requireConfig();
41277
- await del(`/api/${config.projectGuid}/records/${table}/${id2}`);
41278
- printResult("Deleted.", { json: false });
41794
+ const api = await recordsHttp(opts);
41795
+ const res = await api.del(`/api/${api.guid}/records/${table}/${id2}`);
41796
+ printResult("Deleted.", opts, res?.data ?? { table, id: id2, deleted: true });
41279
41797
  }));
41280
41798
 
41281
41799
  // src/commands/fn.ts
@@ -41320,17 +41838,11 @@ ${error(`error: ${log3.error_message}`)}`;
41320
41838
  return line;
41321
41839
  });
41322
41840
  }));
41323
- async function callAnon(projectGuid, name, body) {
41324
- let appToken;
41325
- try {
41326
- const minted = await publicPost("/api/token", { app: projectGuid });
41327
- appToken = minted.data.token;
41328
- } catch {
41329
- }
41841
+ async function callAnon(projectGuid2, name, body) {
41330
41842
  return publicPost(
41331
- `/api/${projectGuid}/fn/${encodeURIComponent(name)}`,
41843
+ `/api/${projectGuid2}/fn/${encodeURIComponent(name)}`,
41332
41844
  body,
41333
- appToken ? { "X-App-Token": appToken } : void 0
41845
+ await mintAppToken(projectGuid2)
41334
41846
  );
41335
41847
  }
41336
41848
  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 () => {
@@ -41383,7 +41895,7 @@ var SERVICES = [
41383
41895
  { name: "location/ip", method: "POST", desc: "IP geolocation ({ ip? })" },
41384
41896
  { name: "location/geocode", method: "POST", desc: "Reverse geocode ({ lat, lon })" }
41385
41897
  ];
41386
- var serviceCommand = new Command("service").description("Call an app service (llm, tts, image, transcribe, ...)").addHelpText("after", "\nCall a Gipity app service: LLM, image, music, TTS, and more.");
41898
+ 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");
41387
41899
  serviceCommand.command("list").description("List callable app services").option("--json", "Output the service list as JSON").action((opts) => run("Services", async () => {
41388
41900
  requireConfig();
41389
41901
  if (opts.json) {
@@ -41429,7 +41941,7 @@ secretsCommand.command("list").alias("ls").description("List secret names (never
41429
41941
  }
41430
41942
  console.log(bold(`${res.data.length} ${scope} secret${res.data.length === 1 ? "" : "s"}:`));
41431
41943
  for (const s of res.data) {
41432
- const masked = s.preview ? muted(`\u2026${s.preview}`) : muted("(hidden)");
41944
+ const masked = s.preview ? muted(`...${s.preview}`) : muted("(hidden)");
41433
41945
  console.log(` ${s.name} ${masked} ${muted(`updated ${new Date(s.updated_at).toLocaleDateString()}`)}`);
41434
41946
  }
41435
41947
  }));
@@ -41738,7 +42250,11 @@ jobCommand.command("status <runGuid>").description("Show status of a job run").o
41738
42250
  const r = res.data;
41739
42251
  const statusColor = r.status === "success" ? success : r.status === "failed" ? error : muted;
41740
42252
  console.log(`${statusColor(r.status)} ${muted(r.guid)}`);
41741
- if (r.progress_pct != null) console.log(`progress: ${Math.round(r.progress_pct * 100)}%${r.progress_message ? ` (${r.progress_message})` : ""}`);
42253
+ if (r.progress_pct != null) {
42254
+ const prog = `${Math.round(r.progress_pct * 100)}%${r.progress_message ? ` (${r.progress_message})` : ""}`;
42255
+ const failedish = r.status === "failed" || r.status === "cancelled" || r.status === "canceled" || r.status === "error";
42256
+ console.log(failedish ? `last progress before ${r.status}: ${prog}` : `progress: ${prog}`);
42257
+ }
41742
42258
  if (r.duration_ms != null) console.log(`duration: ${r.duration_ms}ms`);
41743
42259
  if (r.error) console.log(`${error("error:")} ${r.error}`);
41744
42260
  if (r.output) console.log(`output: ${JSON.stringify(r.output)}`);
@@ -41758,7 +42274,7 @@ jobCommand.command("wait <runGuid>").description("Block until a job run finishes
41758
42274
  if (!opts.json) {
41759
42275
  const prog = r.progress_pct != null ? `${Math.round(r.progress_pct * 100)}%${r.progress_message ? ` (${r.progress_message})` : ""}` : r.status;
41760
42276
  if (prog !== lastProgress) {
41761
- console.error(muted(`\u2026 ${prog}`));
42277
+ console.error(muted(`... ${prog}`));
41762
42278
  lastProgress = prog;
41763
42279
  }
41764
42280
  }
@@ -41829,7 +42345,7 @@ jobCommand.command("logs <runGuid>").description("Stream live logs for a job run
41829
42345
  res = await fetch(url, { headers, signal: controller.signal });
41830
42346
  } catch (e) {
41831
42347
  if (peekedOut) {
41832
- console.error(muted(`\u2026 still streaming after ${peekSeconds}s. Run \`gipity job logs ${runGuid} --timeout ${Math.round(peekSeconds)}\` again to keep watching, or \`gipity job wait ${runGuid}\` to block until it finishes.`));
42348
+ 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.`));
41833
42349
  return;
41834
42350
  }
41835
42351
  throw e;
@@ -41849,7 +42365,7 @@ jobCommand.command("logs <runGuid>").description("Stream live logs for a job run
41849
42365
  chunk = await reader.read();
41850
42366
  } catch (e) {
41851
42367
  if (peekedOut) {
41852
- console.error(muted(`\u2026 still streaming after ${peekSeconds}s. Run \`gipity job logs ${runGuid} --timeout ${Math.round(peekSeconds)}\` again to keep watching, or \`gipity job wait ${runGuid}\` to block until it finishes.`));
42368
+ 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.`));
41853
42369
  return;
41854
42370
  }
41855
42371
  throw e;
@@ -41912,11 +42428,11 @@ jobCommand.command("cancel <runGuid>").description("Cancel a queued or running j
41912
42428
  }
41913
42429
  }));
41914
42430
  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) => {
41915
- const { existsSync: existsSync24, statSync: statSync11 } = await import("node:fs");
41916
- const { resolve: resolve19, join: join26 } = await import("node:path");
42431
+ const { existsSync: existsSync24, statSync: statSync12 } = await import("node:fs");
42432
+ const { resolve: resolve19, join: join27 } = await import("node:path");
41917
42433
  const { spawnCommand: spawnCommand2, spawnSyncCommand: spawnSyncCommand2 } = await Promise.resolve().then(() => (init_platform(), platform_exports));
41918
42434
  const jobDir = resolve19(process.cwd(), "jobs", name);
41919
- if (!existsSync24(jobDir) || !statSync11(jobDir).isDirectory()) {
42435
+ if (!existsSync24(jobDir) || !statSync12(jobDir).isDirectory()) {
41920
42436
  console.error(error(`jobs/${name}/ not found in current directory`));
41921
42437
  process.exit(1);
41922
42438
  }
@@ -41937,7 +42453,7 @@ jobCommand.command("run-local <name>").description("Run the job in a local Docke
41937
42453
  },
41938
42454
  { file: "main.sh", runtime: "bash", entry: "bash" }
41939
42455
  ];
41940
- const picked = variants.find((v7) => existsSync24(join26(jobDir, v7.file)));
42456
+ const picked = variants.find((v7) => existsSync24(join27(jobDir, v7.file)));
41941
42457
  if (!picked) {
41942
42458
  console.error(error(`No main.py / main.js / main.sh found under jobs/${name}/`));
41943
42459
  process.exit(1);
@@ -41962,7 +42478,7 @@ jobCommand.command("run-local <name>").description("Run the job in a local Docke
41962
42478
  "-v",
41963
42479
  `${jobDir}:/work`
41964
42480
  ];
41965
- const needsDeps = opts.deps !== false && picked.depsFile && picked.installCmd && existsSync24(join26(jobDir, picked.depsFile));
42481
+ const needsDeps = opts.deps !== false && picked.depsFile && picked.installCmd && existsSync24(join27(jobDir, picked.depsFile));
41966
42482
  const handlerCmd = `${picked.entry} /work/${picked.file}`;
41967
42483
  let shellCmd;
41968
42484
  if (needsDeps && picked.installCmd) {
@@ -42138,7 +42654,7 @@ init_api();
42138
42654
  init_config();
42139
42655
  init_colors();
42140
42656
  import { mkdirSync as mkdirSync13, writeFileSync as writeFileSync13, readFileSync as readFileSync19 } from "fs";
42141
- import { resolve as resolvePath2, dirname as dirname10, relative as relative5, isAbsolute, basename as basename5 } from "path";
42657
+ import { resolve as resolvePath2, dirname as dirname11, relative as relative5, isAbsolute, basename as basename6 } from "path";
42142
42658
 
42143
42659
  // src/provider-docs.ts
42144
42660
  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`;
@@ -42199,11 +42715,11 @@ function noteFormatMismatch(filename, buf) {
42199
42715
  const asked = dot > 0 ? filename.slice(dot + 1).toLowerCase() : "";
42200
42716
  if (!asked || canonicalExt(asked) === actual) return;
42201
42717
  console.error(muted(
42202
- `Note: ${basename5(filename)} holds ${formatName(actual)} bytes though its name ends .${asked} \u2014 that's fine (browsers and image/video/edit tools sniff the bytes, not the name). Saved at the exact path you requested; reference it as-is.`
42718
+ `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.`
42203
42719
  ));
42204
42720
  }
42205
42721
  function ensureOutputDir(output) {
42206
- const dir = dirname10(resolvePath2(output));
42722
+ const dir = dirname11(resolvePath2(output));
42207
42723
  try {
42208
42724
  mkdirSync13(dir, { recursive: true });
42209
42725
  } catch (err) {
@@ -42213,7 +42729,7 @@ function ensureOutputDir(output) {
42213
42729
  async function pushGenerated(savedPath) {
42214
42730
  const configPath = getConfigPath();
42215
42731
  if (!configPath) return;
42216
- const rel2 = relative5(dirname10(configPath), savedPath);
42732
+ const rel2 = relative5(dirname11(configPath), savedPath);
42217
42733
  if (rel2.startsWith("..") || isAbsolute(rel2)) return;
42218
42734
  try {
42219
42735
  await pushFile(savedPath);
@@ -42272,7 +42788,7 @@ Examples:
42272
42788
  seed: Number.isFinite(opts.seed) ? opts.seed : void 0,
42273
42789
  input_images: inputImages
42274
42790
  });
42275
- const verb = inputImages ? "Editing image\u2026" : "Generating image\u2026";
42791
+ const verb = inputImages ? "Editing image..." : "Generating image...";
42276
42792
  const result = opts.json ? await doGenerate() : await withSpinner(verb, doGenerate, { done: null });
42277
42793
  const ext = result.content_type.includes("png") ? "png" : "jpg";
42278
42794
  const filename = opts.output || `generated.${ext}`;
@@ -42318,7 +42834,7 @@ Examples:
42318
42834
  aspect_ratio: opts.aspect,
42319
42835
  resolution: opts.resolution
42320
42836
  });
42321
- const result = opts.json ? await doGenerate() : await withSpinner("Generating video\u2026", doGenerate, { done: null });
42837
+ const result = opts.json ? await doGenerate() : await withSpinner("Generating video...", doGenerate, { done: null });
42322
42838
  const filename = opts.output || "generated.mp4";
42323
42839
  const savedPath = await downloadFile(result.url, filename, !!opts.output);
42324
42840
  if (opts.json) {
@@ -42366,7 +42882,7 @@ Examples:
42366
42882
  language: opts.language,
42367
42883
  speakers
42368
42884
  });
42369
- const result = opts.json ? await doGenerate() : await withSpinner("Generating speech\u2026", doGenerate, { done: null });
42885
+ const result = opts.json ? await doGenerate() : await withSpinner("Generating speech...", doGenerate, { done: null });
42370
42886
  const filename = opts.output || "speech.mp3";
42371
42887
  const savedPath = await downloadFile(result.url, filename, !!opts.output);
42372
42888
  if (opts.json) {
@@ -42405,7 +42921,7 @@ Examples:
42405
42921
  model: opts.model,
42406
42922
  instrumental: !opts.vocals
42407
42923
  });
42408
- const result = opts.json ? await doGenerate() : await withSpinner("Generating music\u2026", doGenerate, { done: null });
42924
+ const result = opts.json ? await doGenerate() : await withSpinner("Generating music...", doGenerate, { done: null });
42409
42925
  const filename = opts.output || "music.mp3";
42410
42926
  const savedPath = await downloadFile(result.url, filename, !!opts.output);
42411
42927
  if (opts.json) {
@@ -42443,7 +42959,7 @@ Examples:
42443
42959
  duration_seconds: opts.duration,
42444
42960
  prompt_influence: opts.influence
42445
42961
  });
42446
- const result = opts.json ? await doGenerate() : await withSpinner("Generating sound effect\u2026", doGenerate, { done: null });
42962
+ const result = opts.json ? await doGenerate() : await withSpinner("Generating sound effect...", doGenerate, { done: null });
42447
42963
  const filename = opts.output || "sound.mp3";
42448
42964
  const savedPath = await downloadFile(result.url, filename, !!opts.output);
42449
42965
  if (opts.json) {
@@ -42465,12 +42981,12 @@ init_api();
42465
42981
  init_config();
42466
42982
  init_colors();
42467
42983
  import { existsSync as existsSync15, readFileSync as readFileSync20 } from "fs";
42468
- import { join as join17 } from "path";
42984
+ import { join as join18 } from "path";
42469
42985
  function readInstalledKitReadme(name) {
42470
42986
  const root = getProjectRoot() ?? process.cwd();
42471
- const kitDir = join17(root, "src", "packages", name);
42472
- if (!existsSync15(join17(kitDir, "package.json"))) return null;
42473
- const readme = join17(kitDir, "README.md");
42987
+ const kitDir = join18(root, "src", "packages", name);
42988
+ if (!existsSync15(join18(kitDir, "package.json"))) return null;
42989
+ const readme = join18(kitDir, "README.md");
42474
42990
  return existsSync15(readme) ? readFileSync20(readme, "utf-8") : null;
42475
42991
  }
42476
42992
  var skillCommand = new Command("skill").description("Task docs - read the matching skill before building");
@@ -42645,14 +43161,14 @@ var domainCommand = new Command("domain").description("Manage custom domains").a
42645
43161
 
42646
43162
  // src/commands/realtime.ts
42647
43163
  import { existsSync as existsSync16 } from "fs";
42648
- import { dirname as dirname11, resolve as resolve14 } from "path";
43164
+ import { dirname as dirname12, resolve as resolve14 } from "path";
42649
43165
  init_api();
42650
43166
  init_config();
42651
43167
  init_colors();
42652
43168
  function hasDeployManifest() {
42653
43169
  const cfgPath = getConfigPath();
42654
43170
  if (!cfgPath) return false;
42655
- return existsSync16(resolve14(dirname11(cfgPath), "gipity.yaml"));
43171
+ return existsSync16(resolve14(dirname12(cfgPath), "gipity.yaml"));
42656
43172
  }
42657
43173
  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 () => {
42658
43174
  const config = requireConfig();
@@ -42763,7 +43279,7 @@ function undeployedFunctionsFromFailures(results) {
42763
43279
  return [...names];
42764
43280
  }
42765
43281
  var LONG_RUN_MS = 6e4;
42766
- async function pollTestStatus(projectGuid, runGuid, opts) {
43282
+ async function pollTestStatus(projectGuid2, runGuid, opts) {
42767
43283
  const startTime = Date.now();
42768
43284
  const getPollInterval = () => {
42769
43285
  const elapsed = Date.now() - startTime;
@@ -42785,7 +43301,7 @@ async function pollTestStatus(projectGuid, runGuid, opts) {
42785
43301
  if (Date.now() - startTime > POLL_HARD_CAP_MS) {
42786
43302
  throw new Error(`Test run ${runGuid} still not finished after ${Math.round(POLL_HARD_CAP_MS / 6e4)} minutes - giving up on the poll. Check it later with \`gipity test status ${runGuid}\` or re-run.`);
42787
43303
  }
42788
- const res = await get(`/projects/${projectGuid}/test/status/${runGuid}`);
43304
+ const res = await get(`/projects/${projectGuid2}/test/status/${runGuid}`);
42789
43305
  const data = res.data;
42790
43306
  if (!opts.json && data.results.length > lastResultCount) {
42791
43307
  const newResults = data.results.slice(lastResultCount);
@@ -42830,7 +43346,7 @@ async function pollTestStatus(projectGuid, runGuid, opts) {
42830
43346
  const elapsed = Math.round((now - startTime) / 1e3);
42831
43347
  const progress = data.totalFiles === 0 ? "starting up" : `${data.completedFiles}/${data.totalFiles} files`;
42832
43348
  const tally = data.passed + data.failed > 0 ? ` (${data.passed} passed${data.failed > 0 ? `, ${data.failed} failed` : ""} so far)` : "";
42833
- console.log(muted(` \u2026 still running \u2014 ${progress}${tally}, ${elapsed}s elapsed`));
43349
+ console.log(muted(` ... still running - ${progress}${tally}, ${elapsed}s elapsed`));
42834
43350
  if (now - startTime >= LONG_RUN_MS && !longRunHintShown) {
42835
43351
  longRunHintShown = true;
42836
43352
  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>`."));
@@ -43061,21 +43577,21 @@ var locationCommand = new Command("location").description("Show location").argum
43061
43577
  }));
43062
43578
 
43063
43579
  // src/commands/doctor.ts
43064
- import { existsSync as existsSync18, readFileSync as readFileSync22, statSync as statSync8 } from "fs";
43065
- import { join as join19, resolve as resolve15 } from "path";
43580
+ import { existsSync as existsSync18, readFileSync as readFileSync22, statSync as statSync9 } from "fs";
43581
+ import { join as join20, resolve as resolve15 } from "path";
43066
43582
  import { homedir as homedir14 } from "os";
43067
43583
 
43068
43584
  // src/updater/state.ts
43069
43585
  import { readFileSync as readFileSync21, writeFileSync as writeFileSync14, mkdirSync as mkdirSync14, existsSync as existsSync17 } from "fs";
43070
- import { join as join18 } from "path";
43586
+ import { join as join19 } from "path";
43071
43587
  import { homedir as homedir13 } from "os";
43072
- var GIPITY_DIR = join18(homedir13(), ".gipity");
43073
- var LOCAL_DIR = join18(GIPITY_DIR, "local");
43074
- var LOCAL_PKG_DIR = join18(LOCAL_DIR, "node_modules", "gipity");
43075
- var LOCAL_ENTRY = join18(LOCAL_PKG_DIR, "dist", "index.js");
43076
- var STATE_FILE = join18(GIPITY_DIR, "update-state.json");
43077
- var SETTINGS_FILE = join18(GIPITY_DIR, "settings.json");
43078
- var UPDATE_LOG = join18(GIPITY_DIR, "update.log");
43588
+ var GIPITY_DIR = join19(homedir13(), ".gipity");
43589
+ var LOCAL_DIR = join19(GIPITY_DIR, "local");
43590
+ var LOCAL_PKG_DIR = join19(LOCAL_DIR, "node_modules", "gipity");
43591
+ var LOCAL_ENTRY = join19(LOCAL_PKG_DIR, "dist", "index.js");
43592
+ var STATE_FILE = join19(GIPITY_DIR, "update-state.json");
43593
+ var SETTINGS_FILE = join19(GIPITY_DIR, "settings.json");
43594
+ var UPDATE_LOG = join19(GIPITY_DIR, "update.log");
43079
43595
  var DEFAULT_STATE = {
43080
43596
  installedVersion: null,
43081
43597
  lastCheckAt: 0,
@@ -43136,7 +43652,7 @@ function versionManagerNode(execPath, env = process.env, home = homedir14()) {
43136
43652
  return null;
43137
43653
  }
43138
43654
  function localVersion() {
43139
- const pkgPath = join19(LOCAL_PKG_DIR, "package.json");
43655
+ const pkgPath = join20(LOCAL_PKG_DIR, "package.json");
43140
43656
  if (!existsSync18(pkgPath)) return null;
43141
43657
  try {
43142
43658
  return JSON.parse(readFileSync22(pkgPath, "utf-8")).version ?? null;
@@ -43250,7 +43766,7 @@ var doctorCommand = new Command("doctor").description("Check install + environme
43250
43766
  console.log(`${muted("last check ")} ${rel(state.lastCheckAt)}`);
43251
43767
  console.log(`${muted("last error ")} ${state.lastError ? error(state.lastError) : dim("none")}`);
43252
43768
  console.log(`${muted("state file ")} ${existsSync18(STATE_FILE) ? STATE_FILE : dim("(none yet)")}`);
43253
- console.log(`${muted("update log ")} ${existsSync18(UPDATE_LOG) ? `${UPDATE_LOG} (${statSync8(UPDATE_LOG).size} bytes)` : dim("(none yet)")}`);
43769
+ console.log(`${muted("update log ")} ${existsSync18(UPDATE_LOG) ? `${UPDATE_LOG} (${statSync9(UPDATE_LOG).size} bytes)` : dim("(none yet)")}`);
43254
43770
  console.log("");
43255
43771
  console.log(dim("Force an update with: gipity update"));
43256
43772
  });
@@ -43259,10 +43775,10 @@ var doctorCommand = new Command("doctor").description("Check install + environme
43259
43775
  import { appendFileSync } from "fs";
43260
43776
 
43261
43777
  // src/updater/install.ts
43262
- import { existsSync as existsSync19, mkdirSync as mkdirSync15, rmSync as rmSync2, statSync as statSync9, unlinkSync as unlinkSync5, writeFileSync as writeFileSync15 } from "fs";
43263
- import { dirname as dirname12, join as join20 } from "path";
43778
+ import { existsSync as existsSync19, mkdirSync as mkdirSync15, rmSync as rmSync2, statSync as statSync10, unlinkSync as unlinkSync5, writeFileSync as writeFileSync15 } from "fs";
43779
+ import { dirname as dirname13, join as join21 } from "path";
43264
43780
  init_platform();
43265
- var UPDATE_LOCK = join20(GIPITY_DIR, "update.lock");
43781
+ var UPDATE_LOCK = join21(GIPITY_DIR, "update.lock");
43266
43782
  var LOCK_STALE_MS2 = 10 * 60 * 1e3;
43267
43783
  function npmInstallGipity(version) {
43268
43784
  const res = spawnSyncCommand(resolveCommand("npm"), ["install", "--no-audit", "--no-fund", "--ignore-scripts", `gipity@${version}`], {
@@ -43283,18 +43799,18 @@ function isWedged(stderr) {
43283
43799
  }
43284
43800
  function resetLocalTree(dir = LOCAL_DIR) {
43285
43801
  mkdirSync15(dir, { recursive: true });
43286
- rmSync2(join20(dir, "node_modules"), { recursive: true, force: true });
43287
- rmSync2(join20(dir, "package-lock.json"), { force: true });
43288
- writeFileSync15(join20(dir, "package.json"), JSON.stringify({ name: "gipity-local", private: true, version: "0.0.0" }, null, 2));
43802
+ rmSync2(join21(dir, "node_modules"), { recursive: true, force: true });
43803
+ rmSync2(join21(dir, "package-lock.json"), { force: true });
43804
+ writeFileSync15(join21(dir, "package.json"), JSON.stringify({ name: "gipity-local", private: true, version: "0.0.0" }, null, 2));
43289
43805
  }
43290
43806
  function acquireUpdateLock(lockPath2 = UPDATE_LOCK) {
43291
- mkdirSync15(dirname12(lockPath2), { recursive: true });
43807
+ mkdirSync15(dirname13(lockPath2), { recursive: true });
43292
43808
  try {
43293
43809
  writeFileSync15(lockPath2, String(process.pid), { flag: "wx" });
43294
43810
  return true;
43295
43811
  } catch {
43296
43812
  try {
43297
- if (Date.now() - statSync9(lockPath2).mtimeMs > LOCK_STALE_MS2) {
43813
+ if (Date.now() - statSync10(lockPath2).mtimeMs > LOCK_STALE_MS2) {
43298
43814
  unlinkSync5(lockPath2);
43299
43815
  writeFileSync15(lockPath2, String(process.pid), { flag: "wx" });
43300
43816
  return true;
@@ -43455,7 +43971,7 @@ import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync17, existsSync
43455
43971
  import { stat, readFile } from "fs/promises";
43456
43972
  import { createInterface as createInterface2 } from "readline";
43457
43973
  import { homedir as homedir16, hostname as hostname5, platform as osPlatform3, loadavg as loadavg2, freemem as freemem2, totalmem as totalmem2, cpus as cpus2 } from "os";
43458
- import { join as join23 } from "path";
43974
+ import { join as join24 } from "path";
43459
43975
  init_auth();
43460
43976
  init_api();
43461
43977
 
@@ -44211,8 +44727,8 @@ async function revokeRelayAgentToken() {
44211
44727
  init_platform();
44212
44728
  init_client_context();
44213
44729
  import { cpus, freemem, totalmem, loadavg, release, uptime, homedir as homedir15 } from "os";
44214
- import { statfsSync, readdirSync as readdirSync8 } from "fs";
44215
- import { join as join21 } from "path";
44730
+ import { statfsSync, readdirSync as readdirSync9 } from "fs";
44731
+ import { join as join22 } from "path";
44216
44732
  var PROBE_TIMEOUT_MS = 4e3;
44217
44733
  function parseVersion(out) {
44218
44734
  const m = out.match(/\d+\.\d+(?:\.\d+)?(?:[-.\w]*)?/);
@@ -44279,7 +44795,7 @@ async function detectGpu() {
44279
44795
  function diskUsage() {
44280
44796
  try {
44281
44797
  if (typeof statfsSync !== "function") return void 0;
44282
- const st2 = statfsSync(join21(homedir15(), "GipityProjects"), { bigint: false });
44798
+ const st2 = statfsSync(join22(homedir15(), "GipityProjects"), { bigint: false });
44283
44799
  if (!st2 || !st2.bsize) return void 0;
44284
44800
  return { total: st2.bsize * st2.blocks, free: st2.bsize * st2.bavail };
44285
44801
  } catch {
@@ -44293,7 +44809,7 @@ function diskUsage() {
44293
44809
  }
44294
44810
  function localProjectCount() {
44295
44811
  try {
44296
- return readdirSync8(join21(homedir15(), "GipityProjects"), { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).length;
44812
+ return readdirSync9(join22(homedir15(), "GipityProjects"), { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).length;
44297
44813
  } catch {
44298
44814
  return void 0;
44299
44815
  }
@@ -44306,10 +44822,11 @@ async function collectDiagnostics() {
44306
44822
  return [];
44307
44823
  }
44308
44824
  })();
44309
- const [claude, codex, grok, cursor, gpu] = await Promise.all([
44825
+ const [claude, codex, grok, agy, cursor, gpu] = await Promise.all([
44310
44826
  probeVersion("claude"),
44311
44827
  probeVersion("codex"),
44312
44828
  probeVersion("grok"),
44829
+ probeVersion("agy"),
44313
44830
  probeVersion("cursor"),
44314
44831
  detectGpu()
44315
44832
  ]);
@@ -44317,6 +44834,7 @@ async function collectDiagnostics() {
44317
44834
  if (claude) agents.claude_code = claude;
44318
44835
  if (codex) agents.codex = codex;
44319
44836
  if (grok) agents.grok = grok;
44837
+ if (agy) agents.agy = agy;
44320
44838
  if (cursor) agents.cursor = cursor;
44321
44839
  return {
44322
44840
  collected_at: (/* @__PURE__ */ new Date()).toISOString(),
@@ -44602,7 +45120,7 @@ var SessionPool = class {
44602
45120
  // src/relay/daemon.ts
44603
45121
  init_config();
44604
45122
  init_api();
44605
- var RELAY_LOG_PATH = join23(homedir16(), ".gipity", "relay.log");
45123
+ var RELAY_LOG_PATH = join24(homedir16(), ".gipity", "relay.log");
44606
45124
  var HEARTBEAT_INTERVAL_MS = parseInt(process.env.GIPITY_RELAY_HEARTBEAT_MS || "60000", 10);
44607
45125
  var DIAGNOSTICS_INTERVAL_MS = parseInt(process.env.GIPITY_RELAY_DIAGNOSTICS_MS || String(24 * 60 * 60 * 1e3), 10);
44608
45126
  var LONG_POLL_TIMEOUT_MS = parseInt(process.env.GIPITY_RELAY_POLL_TIMEOUT_MS || "35000", 10);
@@ -44719,7 +45237,7 @@ function log2(level, msg, extra) {
44719
45237
  const pretty = `${C7.dim(hhmmss())} ${badge(level)} ${C7.bold(msg)}${formatExtra(extra)}`;
44720
45238
  process.stderr.write(pretty + "\n");
44721
45239
  try {
44722
- const dir = join23(homedir16(), ".gipity");
45240
+ const dir = join24(homedir16(), ".gipity");
44723
45241
  mkdirSync17(dir, { recursive: true });
44724
45242
  lockLogPerms(dir, RELAY_LOG_PATH);
44725
45243
  const json = JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level, msg, ...extra ?? {} });
@@ -45120,7 +45638,7 @@ function isSafeSessionId(s) {
45120
45638
  function transcriptPathFor(cwd, sessionId) {
45121
45639
  if (!isSafeSessionId(sessionId)) return null;
45122
45640
  const slug = cwd.replace(/[^a-zA-Z0-9]/g, "-");
45123
- return join23(homedir16(), ".claude", "projects", slug, `${sessionId}.jsonl`);
45641
+ return join24(homedir16(), ".claude", "projects", slug, `${sessionId}.jsonl`);
45124
45642
  }
45125
45643
  function formatDuration(ms2) {
45126
45644
  const totalSec = ms2 / 1e3;
@@ -45574,8 +46092,8 @@ async function resolveCwdForProject(d) {
45574
46092
  throw new Error(`Invalid project slug: ${JSON.stringify(d.project_slug)}`);
45575
46093
  }
45576
46094
  const root = getProjectsRoot();
45577
- const path5 = join23(root, d.project_slug);
45578
- const configPath = join23(path5, ".gipity.json");
46095
+ const path5 = join24(root, d.project_slug);
46096
+ const configPath = join24(path5, ".gipity.json");
45579
46097
  if (existsSync21(configPath)) {
45580
46098
  try {
45581
46099
  const cfg = JSON.parse(readFileSync24(configPath, "utf-8"));
@@ -45713,7 +46231,7 @@ async function spawnSync2(cwd, timeoutMs, onSpawn) {
45713
46231
  } catch {
45714
46232
  }
45715
46233
  try {
45716
- unlinkSync7(join23(cwd, ".gipity", "sync.lock"));
46234
+ unlinkSync7(join24(cwd, ".gipity", "sync.lock"));
45717
46235
  } catch {
45718
46236
  }
45719
46237
  finish(() => reject(new Error(`timed out after ${Math.round(timeoutMs / 1e3)}s`)));
@@ -46351,9 +46869,7 @@ var connectCommand = new Command("connect").alias("setup").description("Connect
46351
46869
  console.log("");
46352
46870
  const enabled = await runRelaySetup({ mode: "run-now" });
46353
46871
  if (enabled) {
46354
- const running2 = isRelayEnabled() && !isPaused();
46355
- console.log(` ${success("Done")} \u2014 your relay ${running2 ? "is running in the background" : "is set up"} and will start with your computer.`);
46356
- console.log(` ${muted("Open")} ${brand("gipity.ai")} ${muted("and start a chat to drive your coding agent here. Manage it with `gipity relay status`.")}`);
46872
+ console.log(` ${muted("Manage it anytime with `gipity relay status`.")}`);
46357
46873
  } else {
46358
46874
  console.log(` ${muted("No relay set up. Run `gipity connect` again anytime, or `gipity build` to start building.")}`);
46359
46875
  }
@@ -46373,9 +46889,9 @@ init_utils();
46373
46889
  init_colors();
46374
46890
  import { existsSync as existsSync23, rmSync as rmSync4, unlinkSync as unlinkSync9, readFileSync as readFileSync26, writeFileSync as writeFileSync17 } from "fs";
46375
46891
  import { homedir as homedir17, platform as osPlatform4 } from "os";
46376
- import { join as join24, resolve as resolve17 } from "path";
46892
+ import { join as join25, resolve as resolve17 } from "path";
46377
46893
  function removeGipityPluginConfig() {
46378
- const settingsPath = join24(homedir17(), ".claude", "settings.json");
46894
+ const settingsPath = join25(homedir17(), ".claude", "settings.json");
46379
46895
  if (!existsSync23(settingsPath)) return false;
46380
46896
  let settings;
46381
46897
  try {
@@ -46402,7 +46918,7 @@ function removeInstallerPathLines() {
46402
46918
  const marker = "# Added by the Gipity installer";
46403
46919
  const touched = [];
46404
46920
  for (const name of [".bashrc", ".zshrc", ".profile"]) {
46405
- const rc2 = join24(homedir17(), name);
46921
+ const rc2 = join25(homedir17(), name);
46406
46922
  if (!existsSync23(rc2)) continue;
46407
46923
  let text;
46408
46924
  try {
@@ -46485,8 +47001,8 @@ async function revokeDeviceBestEffort() {
46485
47001
  }
46486
47002
  var uninstallCommand = new Command("uninstall").description("Uninstall Gipity").option("--yes", "Skip confirmation prompts").action(async (opts) => {
46487
47003
  const autoYes = opts.yes || getAutoConfirm();
46488
- const gipityDir = join24(homedir17(), ".gipity");
46489
- const launcherBin = join24(gipityDir, "launcher", "bin", "gipity");
47004
+ const gipityDir = join25(homedir17(), ".gipity");
47005
+ const launcherBin = join25(gipityDir, "launcher", "bin", "gipity");
46490
47006
  const installedViaLauncher = existsSync23(launcherBin);
46491
47007
  console.log(`${bold("Gipity uninstall")} - this will:`);
46492
47008
  console.log(`\u2022 Stop the running relay daemon (if any)`);
@@ -46533,12 +47049,22 @@ var uninstallCommand = new Command("uninstall").description("Uninstall Gipity").
46533
47049
  if (agentSkills.skills.length) {
46534
47050
  for (const name of agentSkills.skills) {
46535
47051
  try {
46536
- rmSync4(join24(AGENTS_SKILLS_DIR, name), { recursive: true, force: true });
47052
+ rmSync4(join25(AGENTS_SKILLS_DIR, name), { recursive: true, force: true });
46537
47053
  } catch {
46538
47054
  }
46539
47055
  }
46540
47056
  console.log(`${success(`Removed ${agentSkills.skills.length} Gipity skills from ~/.agents/skills.`)}`);
46541
47057
  }
47058
+ const agySkills = agySkillsState();
47059
+ if (agySkills.skills.length) {
47060
+ for (const name of agySkills.skills) {
47061
+ try {
47062
+ rmSync4(join25(AGY_SKILLS_DIR, name), { recursive: true, force: true });
47063
+ } catch {
47064
+ }
47065
+ }
47066
+ console.log(`${success(`Removed ${agySkills.skills.length} Gipity skills from ~/.gemini/config/skills.`)}`);
47067
+ }
46542
47068
  if (existsSync23(gipityDir)) {
46543
47069
  try {
46544
47070
  rmSync4(gipityDir, { recursive: true, force: true });
@@ -46685,7 +47211,7 @@ gmailCommand.command("send").description("Send a new email from your Gmail").req
46685
47211
  const recap = opts.to.join(", ") + (opts.cc.length ? `, cc: ${opts.cc.join(", ")}` : "");
46686
47212
  printResult(`Sent from your Gmail to ${recap}.`, opts, { sent: true, to: opts.to });
46687
47213
  }));
46688
- 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 () => {
47214
+ 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 () => {
46689
47215
  if (!opts.to.length) throw new Error("--to is required (pass at least one)");
46690
47216
  const payload = {
46691
47217
  thread_id: opts.threadId,
@@ -47067,7 +47593,7 @@ function collectRealFlags(argv2, program3) {
47067
47593
 
47068
47594
  // src/trace.ts
47069
47595
  import { openSync as openSync5, writeSync, mkdirSync as mkdirSync18 } from "fs";
47070
- import { join as join25 } from "path";
47596
+ import { join as join26 } from "path";
47071
47597
  import { homedir as homedir18 } from "os";
47072
47598
  var TRACE_KEY = /* @__PURE__ */ Symbol.for("gipity.traceOutput");
47073
47599
  function installOutputTrace(label2) {
@@ -47079,10 +47605,10 @@ function installOutputTrace(label2) {
47079
47605
  existing.emit({ event: "reenter", label: label2 });
47080
47606
  return;
47081
47607
  }
47082
- const dir = join25(process.env.GIPITY_DIR || join25(homedir18(), ".gipity"), "trace");
47608
+ const dir = join26(process.env.GIPITY_DIR || join26(homedir18(), ".gipity"), "trace");
47083
47609
  mkdirSync18(dir, { recursive: true });
47084
47610
  const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", "_").replace(/:/g, "-");
47085
- const fd2 = openSync5(join25(dir, `${stamp}-pid${process.pid}.jsonl`), "a");
47611
+ const fd2 = openSync5(join26(dir, `${stamp}-pid${process.pid}.jsonl`), "a");
47086
47612
  const emit = (rec) => {
47087
47613
  try {
47088
47614
  writeSync(fd2, JSON.stringify({ t: (/* @__PURE__ */ new Date()).toISOString(), ...rec }) + "\n");
@@ -47117,7 +47643,7 @@ function installOutputTrace(label2) {
47117
47643
 
47118
47644
  // src/index.ts
47119
47645
  installOutputTrace("index");
47120
- var __dirname = dirname13(fileURLToPath3(import.meta.url));
47646
+ var __dirname = dirname14(fileURLToPath3(import.meta.url));
47121
47647
  var pkg = JSON.parse(readFileSync28(resolve18(__dirname, "../package.json"), "utf-8"));
47122
47648
  function versionLabel() {
47123
47649
  const base = `v${pkg.version}`;
@@ -47158,8 +47684,8 @@ function configureHelp(cmd) {
47158
47684
  var program2 = new Command();
47159
47685
  program2.enablePositionalOptions();
47160
47686
  var startGroup = [statusCommand, initCommand, skillCommand, projectCommand];
47161
- var buildGroup = [addCommand, removeCommand, saveCommand, loadCommand, deployCommand, pageCommand, testCommand];
47162
- var backendGroup = [dbCommand, fnCommand, secretsCommand, logsCommand, jobCommand, workflowCommand];
47687
+ var buildGroup = [addCommand, removeCommand, brandCommand, saveCommand, loadCommand, deployCommand, pageCommand, testCommand];
47688
+ var backendGroup = [dbCommand, fnCommand, secretsCommand, keyCommand, logsCommand, jobCommand, workflowCommand];
47163
47689
  var servicesGroup = [serviceCommand, generateCommand, notifyCommand, paymentsCommand, realtimeCommand, recordsCommand, rbacCommand, auditCommand, domainCommand, tokenCommand];
47164
47690
  var filesGroup = [syncCommand, fileCommand, pushCommand, uploadCommand, storageCommand];
47165
47691
  var gipGroup = [chatCommand, memoryCommand, agentCommand, approvalCommand, gmailCommand];
@@ -47176,6 +47702,7 @@ var HELP_SECTIONS = [
47176
47702
  { title: "Connect & setup", cmds: connectGroup }
47177
47703
  ];
47178
47704
  var SKILL_DOCS = {
47705
+ brand: "web-app-basics",
47179
47706
  save: "app-import",
47180
47707
  load: "app-import",
47181
47708
  github: "app-import",
@@ -47188,6 +47715,7 @@ var SKILL_DOCS = {
47188
47715
  notify: "app-notify",
47189
47716
  payments: "app-payments",
47190
47717
  records: "app-records",
47718
+ key: "app-auth",
47191
47719
  realtime: "app-realtime",
47192
47720
  job: "jobs",
47193
47721
  sandbox: "sandbox-tools",
@@ -47336,7 +47864,7 @@ Command.prototype._excessArguments = function(receivedArgs) {
47336
47864
  let message = `error: unexpected extra argument${excess.length === 1 ? "" : "s"} ${list}${forSubcommand}.`;
47337
47865
  const kv2 = excess.map((a) => /^([A-Za-z][\w-]*)=/.exec(a)).find(Boolean);
47338
47866
  if (kv2) {
47339
- message += ` Options are passed as \`--${kv2[1]} <value>\`, not \`${kv2[1]}=\u2026\` - did you mean \`--${kv2[1]}\`?`;
47867
+ message += ` Options are passed as \`--${kv2[1]} <value>\`, not \`${kv2[1]}=...\` - did you mean \`--${kv2[1]}\`?`;
47340
47868
  }
47341
47869
  this.error(message, { code: "commander.excessArguments" });
47342
47870
  };