gipity 1.0.429 → 1.1.2

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 (45) hide show
  1. package/README.md +67 -29
  2. package/dist/agents/claude-code.js +48 -0
  3. package/dist/agents/codex.js +45 -0
  4. package/dist/agents/grok.js +50 -0
  5. package/dist/agents/index.js +23 -0
  6. package/dist/agents/types.js +18 -0
  7. package/dist/api.js +14 -4
  8. package/dist/capture/sources/codex.js +216 -0
  9. package/dist/capture/sources/grok.js +178 -0
  10. package/dist/catalog.js +46 -0
  11. package/dist/client-context.js +2 -0
  12. package/dist/commands/add.js +9 -22
  13. package/dist/commands/build.js +1330 -0
  14. package/dist/commands/chat.js +9 -3
  15. package/dist/commands/claude.js +4 -13
  16. package/dist/commands/db.js +22 -5
  17. package/dist/commands/deploy.js +7 -0
  18. package/dist/commands/doctor.js +8 -2
  19. package/dist/commands/fn.js +24 -1
  20. package/dist/commands/init.js +9 -15
  21. package/dist/commands/logs.js +21 -2
  22. package/dist/commands/page-eval.js +25 -10
  23. package/dist/commands/page-inspect.js +7 -3
  24. package/dist/commands/page-screenshot.js +62 -23
  25. package/dist/commands/project.js +1 -1
  26. package/dist/commands/relay-install.js +1 -1
  27. package/dist/commands/relay.js +3 -3
  28. package/dist/commands/sandbox.js +62 -16
  29. package/dist/commands/setup.js +16 -10
  30. package/dist/commands/status.js +35 -7
  31. package/dist/commands/test.js +7 -0
  32. package/dist/commands/uninstall.js +25 -3
  33. package/dist/flag-aliases.js +1 -0
  34. package/dist/hooks/capture-runner.js +127 -38
  35. package/dist/index.js +1118 -361
  36. package/dist/knowledge.js +3 -3
  37. package/dist/prefs.js +42 -0
  38. package/dist/project-setup.js +2 -10
  39. package/dist/relay/daemon.js +124 -16
  40. package/dist/relay/diagnostics.js +4 -2
  41. package/dist/relay/onboarding.js +9 -9
  42. package/dist/relay/setup.js +8 -8
  43. package/dist/relay/state.js +1 -1
  44. package/dist/setup.js +258 -17
  45. package/package.json +4 -3
package/dist/index.js CHANGED
@@ -3944,10 +3944,10 @@ function prompt(question) {
3944
3944
  return Promise.reject(new Error(`prompt() called without a TTY: ${question.trim()}`));
3945
3945
  }
3946
3946
  const rl2 = createInterface({ input: process.stdin, output: process.stdout });
3947
- return new Promise((resolve19) => {
3947
+ return new Promise((resolve20) => {
3948
3948
  rl2.question(question, (answer) => {
3949
3949
  rl2.close();
3950
- resolve19(answer.trim());
3950
+ resolve20(answer.trim());
3951
3951
  });
3952
3952
  });
3953
3953
  }
@@ -3977,7 +3977,7 @@ async function confirm(question, opts = {}) {
3977
3977
  const wasRaw = stdin.isRaw ?? false;
3978
3978
  stdin.setRawMode(true);
3979
3979
  stdin.resume();
3980
- return new Promise((resolve19) => {
3980
+ return new Promise((resolve20) => {
3981
3981
  stdin.once("data", (key) => {
3982
3982
  stdin.setRawMode(wasRaw);
3983
3983
  stdin.pause();
@@ -3992,12 +3992,12 @@ async function confirm(question, opts = {}) {
3992
3992
  else if (k7 === "n") answer = false;
3993
3993
  else answer = defaultYes;
3994
3994
  console.log(answer ? "y" : "n");
3995
- resolve19(answer);
3995
+ resolve20(answer);
3996
3996
  });
3997
3997
  });
3998
3998
  }
3999
3999
  function pickOne(label2, max, defaultIdx = 1) {
4000
- return new Promise((resolve19) => {
4000
+ return new Promise((resolve20) => {
4001
4001
  process.stdout.write(` ${bold(label2)} (1-${max}) [${bold(String(defaultIdx))}]: `);
4002
4002
  const { stdin } = process;
4003
4003
  const wasRaw = stdin.isRaw ?? false;
@@ -4013,15 +4013,15 @@ function pickOne(label2, max, defaultIdx = 1) {
4013
4013
  }
4014
4014
  if (ch === "\r" || ch === "\n") {
4015
4015
  console.log(String(defaultIdx));
4016
- return resolve19(defaultIdx);
4016
+ return resolve20(defaultIdx);
4017
4017
  }
4018
4018
  const n = parseInt(ch, 10);
4019
4019
  if (n >= 1 && n <= max) {
4020
4020
  console.log(String(n));
4021
- return resolve19(n);
4021
+ return resolve20(n);
4022
4022
  }
4023
4023
  console.log(String(defaultIdx));
4024
- resolve19(defaultIdx);
4024
+ resolve20(defaultIdx);
4025
4025
  });
4026
4026
  });
4027
4027
  }
@@ -5370,8 +5370,8 @@ var require_streamx = __commonJS({
5370
5370
  return this;
5371
5371
  },
5372
5372
  next() {
5373
- return new Promise(function(resolve19, reject) {
5374
- promiseResolve = resolve19;
5373
+ return new Promise(function(resolve20, reject) {
5374
+ promiseResolve = resolve20;
5375
5375
  promiseReject = reject;
5376
5376
  const data = stream.read();
5377
5377
  if (data !== null) ondata(data);
@@ -5401,11 +5401,11 @@ var require_streamx = __commonJS({
5401
5401
  }
5402
5402
  function destroy(err) {
5403
5403
  stream.destroy(err);
5404
- return new Promise((resolve19, reject) => {
5405
- if (stream._duplexState & DESTROYED) return resolve19({ value: void 0, done: true });
5404
+ return new Promise((resolve20, reject) => {
5405
+ if (stream._duplexState & DESTROYED) return resolve20({ value: void 0, done: true });
5406
5406
  stream.once("close", function() {
5407
5407
  if (err) reject(err);
5408
- else resolve19({ value: void 0, done: true });
5408
+ else resolve20({ value: void 0, done: true });
5409
5409
  });
5410
5410
  });
5411
5411
  }
@@ -5449,8 +5449,8 @@ var require_streamx = __commonJS({
5449
5449
  const writes = pending + (ws2._duplexState & WRITE_WRITING ? 1 : 0);
5450
5450
  if (writes === 0) return Promise.resolve(true);
5451
5451
  if (state.drains === null) state.drains = [];
5452
- return new Promise((resolve19) => {
5453
- state.drains.push({ writes, resolve: resolve19 });
5452
+ return new Promise((resolve20) => {
5453
+ state.drains.push({ writes, resolve: resolve20 });
5454
5454
  });
5455
5455
  }
5456
5456
  write(data) {
@@ -5555,10 +5555,10 @@ var require_streamx = __commonJS({
5555
5555
  cb2(null);
5556
5556
  }
5557
5557
  function pipelinePromise(...streams) {
5558
- return new Promise((resolve19, reject) => {
5558
+ return new Promise((resolve20, reject) => {
5559
5559
  return pipeline(...streams, (err) => {
5560
5560
  if (err) return reject(err);
5561
- resolve19();
5561
+ resolve20();
5562
5562
  });
5563
5563
  });
5564
5564
  }
@@ -6213,16 +6213,16 @@ var require_extract = __commonJS({
6213
6213
  entryCallback = null;
6214
6214
  cb2(err);
6215
6215
  }
6216
- function onnext(resolve19, reject) {
6216
+ function onnext(resolve20, reject) {
6217
6217
  if (error2) {
6218
6218
  return reject(error2);
6219
6219
  }
6220
6220
  if (entryStream) {
6221
- resolve19({ value: entryStream, done: false });
6221
+ resolve20({ value: entryStream, done: false });
6222
6222
  entryStream = null;
6223
6223
  return;
6224
6224
  }
6225
- promiseResolve = resolve19;
6225
+ promiseResolve = resolve20;
6226
6226
  promiseReject = reject;
6227
6227
  consumeCallback(null);
6228
6228
  if (extract3._finished && promiseResolve) {
@@ -6250,11 +6250,11 @@ var require_extract = __commonJS({
6250
6250
  function destroy(err) {
6251
6251
  extract3.destroy(err);
6252
6252
  consumeCallback(err);
6253
- return new Promise((resolve19, reject) => {
6254
- if (extract3.destroyed) return resolve19({ value: void 0, done: true });
6253
+ return new Promise((resolve20, reject) => {
6254
+ if (extract3.destroyed) return resolve20({ value: void 0, done: true });
6255
6255
  extract3.once("close", function() {
6256
6256
  if (err) reject(err);
6257
- else resolve19({ value: void 0, done: true });
6257
+ else resolve20({ value: void 0, done: true });
6258
6258
  });
6259
6259
  });
6260
6260
  }
@@ -6560,6 +6560,7 @@ function detectHarness() {
6560
6560
  return { harness: "claude-code", harnessVersion: version, harnessSession: process.env.CLAUDE_CODE_SESSION_ID };
6561
6561
  }
6562
6562
  if (hasEnvPrefix("CODEX_")) return { harness: "codex" };
6563
+ if (hasEnvPrefix("GROK_")) return { harness: "grok", harnessSession: process.env.GROK_SESSION_ID };
6563
6564
  if (process.env.CURSOR_TRACE_ID || hasEnvPrefix("CURSOR_") || (process.env.TERM_PROGRAM ?? "").toLowerCase().includes("cursor")) {
6564
6565
  return { harness: "cursor" };
6565
6566
  }
@@ -6614,7 +6615,8 @@ __export(api_exports, {
6614
6615
  put: () => put,
6615
6616
  putTimeoutMs: () => putTimeoutMs,
6616
6617
  putToPresignedUrl: () => putToPresignedUrl,
6617
- sendMessage: () => sendMessage
6618
+ sendMessage: () => sendMessage,
6619
+ usingEnvToken: () => usingEnvToken
6618
6620
  });
6619
6621
  import { Readable } from "stream";
6620
6622
  async function fetchWithTimeout(url, init, timeoutMs, label2) {
@@ -6643,7 +6645,7 @@ async function shouldRetryAfter401(status, retried) {
6643
6645
  return forceRefreshAccessToken();
6644
6646
  }
6645
6647
  function with401Hint(status, message) {
6646
- return status === 401 && !usingEnvToken() ? `${message} \u2014 run: gipity login` : message;
6648
+ return status === 401 && !usingEnvToken() ? `${message} \u2014 run: gipity login (headless/CI: set GIPITY_TOKEN instead \u2014 gipity skill read agent-deploy)` : message;
6647
6649
  }
6648
6650
  async function getHeaders() {
6649
6651
  return {
@@ -6705,7 +6707,7 @@ async function postForTarEntries(path5, body, retried = false) {
6705
6707
  if (!res.body) throw new ApiError(500, "EMPTY_RESPONSE", "Server returned no body");
6706
6708
  const extract3 = tar.extract();
6707
6709
  const entries = [];
6708
- const done = new Promise((resolve19, reject) => {
6710
+ const done = new Promise((resolve20, reject) => {
6709
6711
  extract3.on("entry", (header, stream, next) => {
6710
6712
  const chunks = [];
6711
6713
  stream.on("data", (c) => chunks.push(c));
@@ -6716,7 +6718,7 @@ async function postForTarEntries(path5, body, retried = false) {
6716
6718
  stream.on("error", reject);
6717
6719
  stream.resume();
6718
6720
  });
6719
- extract3.on("finish", () => resolve19());
6721
+ extract3.on("finish", () => resolve20());
6720
6722
  extract3.on("error", reject);
6721
6723
  });
6722
6724
  Readable.fromWeb(res.body).pipe(extract3);
@@ -6741,6 +6743,9 @@ async function sendMessage(message) {
6741
6743
  if (res.data.conversationGuid !== config.conversationGuid) {
6742
6744
  saveConfig({ ...config, conversationGuid: res.data.conversationGuid });
6743
6745
  }
6746
+ if (res.data.costWarning && !res.data.content) {
6747
+ return res.data.costWarning.message;
6748
+ }
6744
6749
  return res.data.content;
6745
6750
  }
6746
6751
  async function download(path5, retried = false) {
@@ -31858,9 +31863,9 @@ var {
31858
31863
  // src/index.ts
31859
31864
  init_config();
31860
31865
  init_utils();
31861
- import { readFileSync as readFileSync25 } from "fs";
31866
+ import { readFileSync as readFileSync26 } from "fs";
31862
31867
  import { fileURLToPath as fileURLToPath3 } from "url";
31863
- import { dirname as dirname13, resolve as resolve18 } from "path";
31868
+ import { dirname as dirname13, resolve as resolve19 } from "path";
31864
31869
 
31865
31870
  // src/helpers/output.ts
31866
31871
  var frameOpen = false;
@@ -31982,7 +31987,7 @@ Prefer the cheapest option that works - CLI and sandbox are instant and free, ap
31982
31987
  3. App services - runtime HTTP endpoints your deployed app calls directly at \`https://a.gipity.ai/api/<PROJECT_GUID>/services/*\`. Available: LLM, TTS, image, sound, music, transcribe, video, file upload, realtime, location, push notifications (Gipity Notify - \`gipity add notify\`, send from a function with the injected \`notify()\`; see \`app-notify\`). Load the matching skill (\`app-llm\`, \`app-tts\`, etc.) before writing service code - they have the schemas, auth pattern, and common-mistake guards. For one-off generation during development, prefer \`gipity generate <image|video|speech|sound|music>\` or \`gipity chat\` - direct generation always bills you (the owner), regardless of any service \`billing_mode\` (that setting only governs the deployed app's runtime calls; never flip it just to create assets). \`gipity generate\` saves to a generic file in the current directory by default (e.g. \`./generated.png\`) - pass \`-o <path>\` to write it straight into your source tree so it deploys (e.g. \`gipity generate image "hero banner" -o src/assets/images/hero.png\`) instead of generating at cwd and moving it.
31983
31988
  4. Delegate to Gip (\`gipity chat "<task>"\`) - only when the work genuinely needs agent reasoning or a tool not in the CLI, sandbox, or app services. Required for: Twitter/X search, Gmail, calendar, push notifications, video understanding, audio source isolation, cross-model second opinions, multi-step orchestration. Don't use \`gipity chat\` for anything the sandbox can do - it's slower and burns tokens.
31984
31989
 
31985
- You are the developer. Write files in this directory - the Gipity Claude Code plugin's hooks auto-sync them to Gipity. Don't run \`npm install\`, \`npm start\`, \`node\`, or \`python\` locally; there is no local runtime. Code runs in the Gipity sandbox.
31990
+ You are the developer. Write files in this directory - Gipity's editor hooks (installed by \`gipity init\` into Claude Code, Grok, and Codex) auto-sync them to Gipity. Don't run \`npm install\`, \`npm start\`, \`node\`, or \`python\` locally; there is no local runtime. Code runs in the Gipity sandbox.
31986
31991
 
31987
31992
  ## Use first-party services before reaching outside
31988
31993
 
@@ -32018,7 +32023,7 @@ The full "when to add a template" rule and the definition of done are spelled ou
32018
32023
 
32019
32024
  Build loop: \`gipity add\` \u2192 edit files \u2192 \`gipity deploy dev --inspect\` \u2192 fix any errors \u2192 repeat until the definition of done is met. \`deploy --inspect\` deploys and then runs the page-inspect report on the live URL in one command; use a standalone \`gipity page inspect <url>\` only when re-checking without deploying.
32020
32025
 
32021
- \`add\` writes real files to disk - Read a scaffolded file before your first Write/Edit to it, or the call fails \`"File has not been read yet"\`. Don't rewrite from memory of the template.
32026
+ \`add\` writes real files to disk - Read a scaffolded file with the Read tool before your first Write/Edit to it, or the call fails \`"File has not been read yet"\` (viewing it with \`cat\`/\`head\` in Bash does not count - the harness only tracks the Read tool). Don't rewrite from memory of the template.
32022
32027
 
32023
32028
  Make your file changes and verify they landed, then run \`gipity deploy dev\` once. \`0 uploaded, N unchanged\` means nothing changed on disk - fix the files, don't re-run deploy or probe the environment.
32024
32029
 
@@ -32074,7 +32079,7 @@ Every tool call returns its full output with that call. There is no output buffe
32074
32079
 
32075
32080
  This directory is the app root - it holds \`.gipity.json\` (and \`gipity.yaml\` for backends) and is already your cwd, so run app commands here. Don't \`cd\` to a git root (\`git rev-parse --show-toplevel\`): when the app is nested in a larger repo that resolves outside the app.
32076
32081
 
32077
- Write files locally - the Gipity Claude Code plugin's hooks auto-push every save to Gipity and auto-pull remote changes (images, audio from \`gipity chat\`) before each turn. Use \`gipity sync\` if things get out of sync (or if the plugin isn't installed - \`gipity status --repair-hooks\` re-enables it). Deletes are safe - use \`rollback\` with a datetime to undo, or \`file_version_restore\` for individual files.
32082
+ Write files locally - Gipity's editor hooks (installed into Claude Code, Grok, and Codex by \`gipity init\`) auto-push every save to Gipity and auto-pull remote changes (images, audio from \`gipity chat\`) before each turn. Use \`gipity sync\` if things get out of sync (or if the hooks aren't installed - \`gipity status --repair-hooks\` re-enables them). Deletes are safe - use \`rollback\` with a datetime to undo, or \`file_version_restore\` for individual files.
32078
32083
 
32079
32084
  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.
32080
32085
 
@@ -32290,7 +32295,7 @@ function run(label2, action) {
32290
32295
  init_api();
32291
32296
  init_config();
32292
32297
  init_utils();
32293
- import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync5, statSync, lstatSync, unlinkSync as unlinkSync3, readdirSync as readdirSync2, rmdirSync, readFileSync as readFileSync6, renameSync, openSync as openSync2, closeSync as closeSync2, utimesSync, realpathSync } from "fs";
32298
+ import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync5, statSync, lstatSync, unlinkSync as unlinkSync3, readdirSync as readdirSync3, rmdirSync, readFileSync as readFileSync6, renameSync, openSync as openSync2, closeSync as closeSync2, utimesSync, realpathSync } from "fs";
32294
32299
  import { join as join4, relative, dirname as dirname4, extname as extname2, resolve as resolve4, sep } from "path";
32295
32300
  import { hostname } from "os";
32296
32301
  import { createHash as createHash2 } from "crypto";
@@ -32354,25 +32359,25 @@ async function hashFile(path5) {
32354
32359
  const hash = createHash("sha256");
32355
32360
  let size = 0;
32356
32361
  const stream = createReadStream(path5);
32357
- await new Promise((resolve19, reject) => {
32362
+ await new Promise((resolve20, reject) => {
32358
32363
  stream.on("data", (chunk) => {
32359
32364
  const buf = chunk;
32360
32365
  hash.update(buf);
32361
32366
  size += buf.length;
32362
32367
  });
32363
- stream.on("end", () => resolve19());
32368
+ stream.on("end", () => resolve20());
32364
32369
  stream.on("error", reject);
32365
32370
  });
32366
32371
  return { sha256: hash.digest("hex"), size };
32367
32372
  }
32368
32373
  function readRange(path5, start, end) {
32369
- return new Promise((resolve19, reject) => {
32374
+ return new Promise((resolve20, reject) => {
32370
32375
  const chunks = [];
32371
32376
  const stream = createReadStream(path5, { start, end });
32372
32377
  stream.on("data", (c) => {
32373
32378
  chunks.push(c);
32374
32379
  });
32375
- stream.on("end", () => resolve19(Buffer.concat(chunks)));
32380
+ stream.on("end", () => resolve20(Buffer.concat(chunks)));
32376
32381
  stream.on("error", reject);
32377
32382
  });
32378
32383
  }
@@ -32536,11 +32541,14 @@ async function uploadCompleteBatch(projectGuid, items) {
32536
32541
  // src/setup.ts
32537
32542
  init_platform();
32538
32543
  import { resolve as resolve3, join as join3, dirname as dirname3 } from "path";
32539
- import { homedir as homedir3 } from "os";
32540
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, readFileSync as readFileSync5 } from "fs";
32544
+ import { homedir as homedir3, tmpdir } from "os";
32545
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, readFileSync as readFileSync5, mkdtempSync, rmSync, cpSync, readdirSync as readdirSync2 } from "fs";
32546
+ init_config();
32541
32547
  var PRIMER_FILES = {
32542
32548
  claude: "CLAUDE.md",
32543
32549
  codex: "AGENTS.md",
32550
+ grok: "AGENTS.md",
32551
+ // Grok Build reads the AGENTS.md family (and CLAUDE.md) natively
32544
32552
  aider: "AGENTS.md",
32545
32553
  // shares the Codex primer; aider is pointed at it via .aider.conf.yml
32546
32554
  gemini: "GEMINI.md",
@@ -32555,6 +32563,7 @@ var DEFAULT_SYNC_IGNORE = [
32555
32563
  ".gipity.json",
32556
32564
  ".gipity/",
32557
32565
  ".claude/",
32566
+ ".codex/",
32558
32567
  ".gitignore",
32559
32568
  AIDER_CONF_FILE,
32560
32569
  // Home-directory junk: a project created inside a real home dir (or one that
@@ -32615,7 +32624,7 @@ var GIPITY_PLUGIN_ID = "gipity@gipity";
32615
32624
  var GIPITY_MARKETPLACE_NAME = "gipity";
32616
32625
  var GIPITY_MARKETPLACE_REPO = "GipityAI/skills";
32617
32626
  var LEGACY_MARKETPLACE_REPO = "GipityAI/claude-plugin";
32618
- var GIPITY_PLUGIN_VERSION = "0.5.0";
32627
+ var GIPITY_PLUGIN_VERSION = "0.7.0";
32619
32628
  function isGipityManagedHookCommand(command) {
32620
32629
  return (
32621
32630
  // Capture hooks: bare absolute runner path or the fire-time launcher.
@@ -32702,8 +32711,8 @@ function userScopeInstallState() {
32702
32711
  return { exists: false, current: false };
32703
32712
  }
32704
32713
  }
32705
- function claudeOnPath() {
32706
- const probe = spawnSyncCommand(process.platform === "win32" ? "where" : "which", ["claude"], {
32714
+ function binaryOnPath(bin) {
32715
+ const probe = spawnSyncCommand(process.platform === "win32" ? "where" : "which", [bin], {
32707
32716
  encoding: "utf-8"
32708
32717
  });
32709
32718
  return probe.status === 0 && !!probe.stdout?.toString().trim();
@@ -32711,7 +32720,7 @@ function claudeOnPath() {
32711
32720
  function ensureGipityPluginInstalled() {
32712
32721
  const state = userScopeInstallState();
32713
32722
  if (state.current) return;
32714
- if (!claudeOnPath()) return;
32723
+ if (!binaryOnPath("claude")) return;
32715
32724
  const claudeCmd = resolveCommand("claude");
32716
32725
  spawnSyncCommand(claudeCmd, ["plugin", "marketplace", "update", GIPITY_MARKETPLACE_NAME], {
32717
32726
  stdio: "ignore",
@@ -32723,6 +32732,146 @@ function ensureGipityPluginInstalled() {
32723
32732
  timeout: 12e4
32724
32733
  });
32725
32734
  }
32735
+ function grokInstallState() {
32736
+ try {
32737
+ const p = join3(homedir3(), ".grok", "installed-plugins", "registry.json");
32738
+ const data = JSON.parse(readFileSync5(p, "utf-8"));
32739
+ const versions = [];
32740
+ for (const repo of Object.values(data?.repos ?? {})) {
32741
+ const v7 = repo?.plugins?.gipity?.version;
32742
+ if (typeof v7 === "string") versions.push(v7);
32743
+ }
32744
+ return {
32745
+ exists: versions.length > 0,
32746
+ current: versions.some((v7) => versionGte(v7, GIPITY_PLUGIN_VERSION))
32747
+ };
32748
+ } catch {
32749
+ return { exists: false, current: false };
32750
+ }
32751
+ }
32752
+ function ensureGrokPluginInstalled() {
32753
+ const state = grokInstallState();
32754
+ if (state.current) return;
32755
+ if (!binaryOnPath("grok")) return;
32756
+ const grokCmd = resolveCommand("grok");
32757
+ const verb = state.exists ? ["plugin", "update", "gipity"] : ["plugin", "install", GIPITY_MARKETPLACE_REPO, "--trust"];
32758
+ const res = spawnSyncCommand(grokCmd, verb, { stdio: "ignore", timeout: 12e4 });
32759
+ if (!state.exists && res.status === 0) {
32760
+ console.log("Installed the Gipity plugin for Grok (skills + file-sync hooks).");
32761
+ }
32762
+ }
32763
+ var AGENTS_SKILLS_DIR = join3(homedir3(), ".agents", "skills");
32764
+ var AGENT_HOOKS_DIR = join3(homedir3(), ".gipity", "agent-hooks");
32765
+ var AGENT_SKILLS_MANIFEST = join3(homedir3(), ".gipity", "agent-skills.json");
32766
+ function agentSkillsState() {
32767
+ try {
32768
+ const m = JSON.parse(readFileSync5(AGENT_SKILLS_MANIFEST, "utf-8"));
32769
+ return {
32770
+ current: typeof m?.version === "string" && versionGte(m.version, GIPITY_PLUGIN_VERSION),
32771
+ skills: Array.isArray(m?.skills) ? m.skills : []
32772
+ };
32773
+ } catch {
32774
+ return { current: false, skills: [] };
32775
+ }
32776
+ }
32777
+ function ensureAgentSkillsInstalled() {
32778
+ if (agentSkillsState().current) return;
32779
+ if (!binaryOnPath("git")) return;
32780
+ const tmp = mkdtempSync(join3(tmpdir(), "gipity-skills-"));
32781
+ try {
32782
+ const clone = spawnSyncCommand(
32783
+ resolveCommand("git"),
32784
+ ["clone", "--depth", "1", `https://github.com/${GIPITY_MARKETPLACE_REPO}.git`, join3(tmp, "repo")],
32785
+ { stdio: "ignore", timeout: 12e4 }
32786
+ );
32787
+ if (clone.status !== 0) return;
32788
+ const repo = join3(tmp, "repo");
32789
+ let version = GIPITY_PLUGIN_VERSION;
32790
+ try {
32791
+ const manifest = JSON.parse(readFileSync5(join3(repo, ".claude-plugin", "plugin.json"), "utf-8"));
32792
+ if (typeof manifest?.version === "string") version = manifest.version;
32793
+ } catch {
32794
+ }
32795
+ const skillsSrc = join3(repo, "skills");
32796
+ const names = [];
32797
+ for (const entry of readdirSync2(skillsSrc, { withFileTypes: true })) {
32798
+ if (!entry.isDirectory()) continue;
32799
+ if (!existsSync4(join3(skillsSrc, entry.name, "SKILL.md"))) continue;
32800
+ mkdirSync3(AGENTS_SKILLS_DIR, { recursive: true });
32801
+ cpSync(join3(skillsSrc, entry.name), join3(AGENTS_SKILLS_DIR, entry.name), {
32802
+ recursive: true,
32803
+ force: true
32804
+ });
32805
+ names.push(entry.name);
32806
+ }
32807
+ mkdirSync3(AGENT_HOOKS_DIR, { recursive: true });
32808
+ for (const script of readdirSync2(join3(repo, "hooks", "scripts"))) {
32809
+ cpSync(join3(repo, "hooks", "scripts", script), join3(AGENT_HOOKS_DIR, script), { force: true });
32810
+ }
32811
+ writeFileSync4(AGENT_SKILLS_MANIFEST, JSON.stringify({ version, skills: names }, null, 2) + "\n");
32812
+ console.log(`Installed ${names.length} Gipity skills for Codex (~/.agents/skills).`);
32813
+ } catch {
32814
+ } finally {
32815
+ rmSync(tmp, { recursive: true, force: true });
32816
+ }
32817
+ }
32818
+ function applyCodexHooks(existing) {
32819
+ const launcher = join3(AGENT_HOOKS_DIR, "launch.sh");
32820
+ const cmd = (script, ...args) => [`sh "${launcher}" "${join3(AGENT_HOOKS_DIR, script)}"`, ...args].join(" ");
32821
+ const wanted = [
32822
+ { event: "PostToolUse", matcher: "Edit|Write", command: cmd("sync-push.cjs"), timeout: 30 },
32823
+ { event: "UserPromptSubmit", command: cmd("sync-pull.cjs"), timeout: 300 },
32824
+ // Session capture: mirror the Codex session into the Gipity web CLI.
32825
+ { event: "SessionStart", command: cmd("capture.cjs", "codex", "session-start"), timeout: 30 },
32826
+ { event: "PostToolUse", command: cmd("capture.cjs", "codex", "post-tool-use"), timeout: 30 },
32827
+ { event: "Stop", command: cmd("capture.cjs", "codex", "stop"), timeout: 60 }
32828
+ ];
32829
+ let settings = {};
32830
+ if (existing !== null) {
32831
+ try {
32832
+ settings = JSON.parse(existing);
32833
+ } catch {
32834
+ return null;
32835
+ }
32836
+ }
32837
+ const hooks = settings.hooks ?? (settings.hooks = {});
32838
+ let changed = false;
32839
+ for (const w of wanted) {
32840
+ const groups = Array.isArray(hooks[w.event]) ? hooks[w.event] : hooks[w.event] = [];
32841
+ const present = groups.some(
32842
+ (g) => Array.isArray(g?.hooks) && g.hooks.some(
32843
+ (h) => typeof h?.command === "string" && h.command === w.command
32844
+ )
32845
+ );
32846
+ if (present) continue;
32847
+ const group = { hooks: [{ type: "command", command: w.command, timeout: w.timeout }] };
32848
+ if (w.matcher) group.matcher = w.matcher;
32849
+ groups.push(group);
32850
+ changed = true;
32851
+ }
32852
+ return changed ? JSON.stringify(settings, null, 2) + "\n" : null;
32853
+ }
32854
+ function setupCodexHooks() {
32855
+ if (process.platform === "win32") return;
32856
+ const cwd = resolve3(process.cwd());
32857
+ if (cwd === resolve3(homedir3())) return;
32858
+ const path5 = join3(cwd, ".codex", "hooks.json");
32859
+ const existing = existsSync4(path5) ? readFileSync5(path5, "utf-8") : null;
32860
+ const next = applyCodexHooks(existing);
32861
+ if (next === null) return;
32862
+ mkdirSync3(dirname3(path5), { recursive: true });
32863
+ writeFileSync4(path5, next);
32864
+ if (existing === null) {
32865
+ console.log("Wrote Codex sync + session-capture hooks (.codex/hooks.json) - approve them once with /hooks inside Codex.");
32866
+ } else {
32867
+ console.log("Updated Codex hooks (.codex/hooks.json) - if Codex asks, re-approve them via /hooks.");
32868
+ }
32869
+ }
32870
+ function setupCodexIntegration() {
32871
+ if (!binaryOnPath("codex")) return;
32872
+ ensureAgentSkillsInstalled();
32873
+ setupCodexHooks();
32874
+ }
32726
32875
  function setupClaudeHooks() {
32727
32876
  ensureGipityPlugin();
32728
32877
  const cwd = resolve3(process.cwd());
@@ -32744,14 +32893,21 @@ function setupClaudeHooks() {
32744
32893
  }
32745
32894
  var GIPITY_BLOCK_BEGIN = "<!-- BEGIN GIPITY INTEGRATION - auto-generated by gipity, do not edit this block -->";
32746
32895
  var GIPITY_BLOCK_END = "<!-- END GIPITY INTEGRATION -->";
32747
- function renderManagedBlock() {
32748
- const body = [SKILLS_CONTENT, BUILD_VS_NON_BUILD_RULE, DEFINITION_OF_DONE].join("\n\n");
32896
+ function renderManagedBlock(apiBase2) {
32897
+ let body = [SKILLS_CONTENT, BUILD_VS_NON_BUILD_RULE, DEFINITION_OF_DONE].join("\n\n");
32898
+ const base = apiBase2.replace(/\/+$/, "");
32899
+ if (base !== DEFAULT_API_BASE) {
32900
+ body = body.replaceAll(DEFAULT_API_BASE, base);
32901
+ const note = `> **Platform instance:** this project runs against the Gipity platform at \`${base}\`, not the public \`${DEFAULT_API_BASE}\`. The project and its data exist only on that instance; every API/service URL in this document already points there - never substitute \`a.gipity.ai\`.`;
32902
+ const headingEnd = body.indexOf("\n");
32903
+ body = body.slice(0, headingEnd + 1) + "\n" + note + "\n" + body.slice(headingEnd + 1);
32904
+ }
32749
32905
  return `${GIPITY_BLOCK_BEGIN}
32750
32906
  ${body}
32751
32907
  ${GIPITY_BLOCK_END}`;
32752
32908
  }
32753
- function applySkillsBlock(existing) {
32754
- const block = renderManagedBlock();
32909
+ function applySkillsBlock(existing, apiBase2 = DEFAULT_API_BASE) {
32910
+ const block = renderManagedBlock(apiBase2);
32755
32911
  if (existing === null) return block + "\n";
32756
32912
  let next;
32757
32913
  const beginIdx = existing.indexOf(GIPITY_BLOCK_BEGIN);
@@ -32770,7 +32926,7 @@ function writeSkillsFile(relPath, wrap) {
32770
32926
  const path5 = resolve3(process.cwd(), relPath);
32771
32927
  mkdirSync3(dirname3(path5), { recursive: true });
32772
32928
  const existing = existsSync4(path5) ? readFileSync5(path5, "utf-8") : null;
32773
- const baseNext = applySkillsBlock(existing);
32929
+ const baseNext = applySkillsBlock(existing, resolveApiBase());
32774
32930
  const next = wrap && existing === null ? wrap(baseNext) : baseNext;
32775
32931
  if (next !== existing) writeFileSync4(path5, next);
32776
32932
  }
@@ -32834,14 +32990,22 @@ ${block}`
32834
32990
  );
32835
32991
  }
32836
32992
  var SUPPORTED_TOOLS = [
32837
- { key: "claude", label: "Claude Code (CLAUDE.md)", setup: setupClaudeMd },
32838
- { key: "codex", label: "OpenAI Codex (AGENTS.md)", setup: setupAgentsMd },
32993
+ { key: "claude", label: "Claude Code (CLAUDE.md + Gipity plugin)", setup: setupClaudeMd, integrate: setupClaudeHooks },
32994
+ { key: "codex", label: "OpenAI Codex (AGENTS.md + skills + sync hooks)", setup: setupAgentsMd, integrate: setupCodexIntegration },
32995
+ { key: "grok", label: "Grok Build (AGENTS.md + Gipity plugin)", setup: setupAgentsMd, integrate: ensureGrokPluginInstalled },
32839
32996
  { key: "aider", label: "Aider (AGENTS.md + .aider.conf.yml)", setup: setupAiderMd, optIn: true },
32840
32997
  { key: "gemini", label: "Gemini CLI (GEMINI.md)", setup: setupGeminiMd },
32841
32998
  { key: "copilot", label: "GitHub Copilot (.github/copilot-instructions.md)", setup: setupCopilotMd },
32842
32999
  { key: "cursor", label: "Cursor (.cursor/rules/gipity.mdc)", setup: setupCursorMd }
32843
33000
  ];
32844
33001
  var DEFAULT_TOOLS = SUPPORTED_TOOLS.filter((t) => !t.optIn);
33002
+ function setupProjectTools(tools = DEFAULT_TOOLS) {
33003
+ for (const t of tools) {
33004
+ t.setup();
33005
+ t.integrate?.();
33006
+ }
33007
+ setupGitignore();
33008
+ }
32845
33009
  function setupGitignore() {
32846
33010
  const gitignorePath = resolve3(process.cwd(), ".gitignore");
32847
33011
  const entries = [".gipity/", ".gipity.json", ...SCRATCH_IGNORE];
@@ -32986,7 +33150,7 @@ function walkLocal(root, ignorePatterns, baseline) {
32986
33150
  function walk(dir) {
32987
33151
  let entries;
32988
33152
  try {
32989
- entries = readdirSync2(dir, { withFileTypes: true });
33153
+ entries = readdirSync3(dir, { withFileTypes: true });
32990
33154
  } catch {
32991
33155
  return;
32992
33156
  }
@@ -33065,7 +33229,7 @@ async function fetchRemote(projectGuid) {
33065
33229
  function extractTarToMap(stream, idleMs, onBytes, keep) {
33066
33230
  const extract3 = tar2.extract();
33067
33231
  const files = /* @__PURE__ */ new Map();
33068
- return new Promise((resolve19, reject) => {
33232
+ return new Promise((resolve20, reject) => {
33069
33233
  let settled = false;
33070
33234
  let idle;
33071
33235
  const arm = () => {
@@ -33101,7 +33265,7 @@ function extractTarToMap(stream, idleMs, onBytes, keep) {
33101
33265
  });
33102
33266
  entryStream.resume();
33103
33267
  });
33104
- extract3.on("finish", () => done(resolve19, files));
33268
+ extract3.on("finish", () => done(resolve20, files));
33105
33269
  extract3.on("error", (e) => done(reject, e));
33106
33270
  stream.on("error", (e) => done(reject, e));
33107
33271
  arm();
@@ -33860,7 +34024,7 @@ async function syncInner(projectGuid, root, ignore2, opts, interactive) {
33860
34024
  function cleanupEmptyDirs(root, emptiedDirs) {
33861
34025
  const isEmpty = (dir) => {
33862
34026
  try {
33863
- return readdirSync2(dir).length === 0;
34027
+ return readdirSync3(dir).length === 0;
33864
34028
  } catch {
33865
34029
  return false;
33866
34030
  }
@@ -34144,7 +34308,7 @@ init_utils();
34144
34308
 
34145
34309
  // src/adopt-cwd.ts
34146
34310
  init_api();
34147
- import { readdirSync as readdirSync4, statSync as statSync3 } from "fs";
34311
+ import { readdirSync as readdirSync5, statSync as statSync3 } from "fs";
34148
34312
  import { join as join8, resolve as resolve6, sep as sep2, parse as parsePath } from "path";
34149
34313
  import { homedir as homedir6 } from "os";
34150
34314
 
@@ -34152,7 +34316,7 @@ import { homedir as homedir6 } from "os";
34152
34316
  init_config();
34153
34317
 
34154
34318
  // src/template-vars.ts
34155
- import { promises as fs, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
34319
+ import { promises as fs, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
34156
34320
  import { join as join5, relative as relative2, extname as extname3 } from "path";
34157
34321
  var SUBSTITUTABLE_EXTS = /* @__PURE__ */ new Set([
34158
34322
  ".html",
@@ -34219,7 +34383,7 @@ function* walkTextFiles(root) {
34219
34383
  const dir = stack.pop();
34220
34384
  let entries;
34221
34385
  try {
34222
- entries = readdirSync3(dir, { withFileTypes: true });
34386
+ entries = readdirSync4(dir, { withFileTypes: true });
34223
34387
  } catch {
34224
34388
  continue;
34225
34389
  }
@@ -34308,10 +34472,7 @@ async function finalizeLocalProject(opts) {
34308
34472
  } catch (err) {
34309
34473
  if (opts.sync === "strict") throw err;
34310
34474
  }
34311
- const tools = opts.tools ?? DEFAULT_TOOLS;
34312
- if (tools.some((t) => t.key === "claude")) setupClaudeHooks();
34313
- for (const t of tools) t.setup();
34314
- setupGitignore();
34475
+ setupProjectTools(opts.tools ?? DEFAULT_TOOLS);
34315
34476
  return { applied };
34316
34477
  }
34317
34478
 
@@ -34490,7 +34651,7 @@ function scanForAdoption(cwd) {
34490
34651
  if (truncated) return;
34491
34652
  let entries;
34492
34653
  try {
34493
- entries = readdirSync4(dir);
34654
+ entries = readdirSync5(dir);
34494
34655
  } catch {
34495
34656
  return;
34496
34657
  }
@@ -34526,7 +34687,7 @@ function scanForAdoption(cwd) {
34526
34687
  function isLikelyEmpty(cwd) {
34527
34688
  let entries;
34528
34689
  try {
34529
- entries = readdirSync4(cwd);
34690
+ entries = readdirSync5(cwd);
34530
34691
  } catch {
34531
34692
  return true;
34532
34693
  }
@@ -34564,7 +34725,7 @@ function canAdoptCwd(cwd) {
34564
34725
  function countGitRepoChildren(dir) {
34565
34726
  let entries;
34566
34727
  try {
34567
- entries = readdirSync4(dir);
34728
+ entries = readdirSync5(dir);
34568
34729
  } catch {
34569
34730
  return 0;
34570
34731
  }
@@ -34676,14 +34837,15 @@ function resolveTools(forFlag) {
34676
34837
  (t) => requested.includes(t.key) || !t.optIn && requested.includes("all")
34677
34838
  );
34678
34839
  }
34679
- 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.").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(
34840
+ 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(
34680
34841
  "--for <tools>",
34681
34842
  `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`
34682
34843
  ).addHelpText("after", `
34683
34844
  Examples:
34684
34845
  $ gipity init Link cwd as a new project (slug = dir name).
34685
34846
  $ gipity init my-app Link cwd with an explicit slug.
34686
- $ gipity init --for codex Write only AGENTS.md (skip Claude/Cursor/etc).
34847
+ $ gipity init --for codex AGENTS.md + Codex skills/sync hooks only.
34848
+ $ gipity init --for grok AGENTS.md + the Gipity plugin in Grok only.
34687
34849
  $ gipity init --for cursor,gemini Write only the Cursor + Gemini primers.
34688
34850
  $ gipity init --for aider AGENTS.md + a read: entry in .aider.conf.yml
34689
34851
  (aider auto-reads nothing, so it's opt-in).
@@ -34704,9 +34866,6 @@ Working with an existing Gipity project:
34704
34866
  process.exit(1);
34705
34867
  }
34706
34868
  const wantsClaude = tools.some((t) => t.key === "claude");
34707
- const writeAllPrimers = () => {
34708
- for (const t of tools) t.setup();
34709
- };
34710
34869
  const primerSummary = tools.map((t) => t.label).join(", ");
34711
34870
  try {
34712
34871
  const auth = getAuth();
@@ -34718,9 +34877,7 @@ Working with an existing Gipity project:
34718
34877
  if (existsSync8(resolve7(cwd, ".gipity.json"))) {
34719
34878
  const existing = getConfig();
34720
34879
  console.log(`Already linked to ${info(`"${existing?.projectSlug ?? ""}"`)} ${muted(`(${existing?.projectGuid ?? ""})`)}`);
34721
- if (wantsClaude) setupClaudeHooks();
34722
- writeAllPrimers();
34723
- setupGitignore();
34880
+ setupProjectTools(tools);
34724
34881
  if (existing) {
34725
34882
  let changed = false;
34726
34883
  const cur = existing.ignore ?? (existing.ignore = []);
@@ -34769,7 +34926,7 @@ Working with an existing Gipity project:
34769
34926
  const sizeStr = scan.truncated ? `>${formatBytes(ADOPT_THRESHOLDS.REFUSE_BYTES)}` : formatBytes(scan.bytes);
34770
34927
  const fileStr = scan.truncated ? `>${ADOPT_THRESHOLDS.REFUSE_FILES}` : `${scan.files}`;
34771
34928
  console.error(error(`Directory has ${fileStr} files (${sizeStr}) - too large to adopt as a Gipity project.`));
34772
- console.error(muted('Move into a subdirectory, or use `gipity claude` and pick "Create new project".'));
34929
+ console.error(muted('Move into a subdirectory, or use `gipity build` and pick "Create new project".'));
34773
34930
  process.exit(1);
34774
34931
  }
34775
34932
  if (scan.tier === "moderate") {
@@ -34808,7 +34965,7 @@ Working with an existing Gipity project:
34808
34965
  }
34809
34966
  console.log(success(`Wrote primer files: ${primerSummary}.`));
34810
34967
  if (wantsClaude) {
34811
- console.log(success("Ready! Run `gipity claude` for Claude Code, or open this directory in your other AI coding tool."));
34968
+ console.log(success("Ready! Run your coding agent here (claude, codex, grok), or `gipity build` to launch one with a picker."));
34812
34969
  if (opts.capture === false) {
34813
34970
  console.log(muted("Session recording is off for this project (captureHooks: false in .gipity.json)."));
34814
34971
  } else {
@@ -34831,6 +34988,7 @@ Working with an existing Gipity project:
34831
34988
 
34832
34989
  // src/commands/status.ts
34833
34990
  init_auth();
34991
+ init_api();
34834
34992
  init_config();
34835
34993
  init_colors();
34836
34994
  import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
@@ -34853,6 +35011,16 @@ function checkGipityPlugin() {
34853
35011
  const stale = missing.length === 1 && missing[0] === "install" && install.exists;
34854
35012
  return { missing, ok: missing.length === 0, stale };
34855
35013
  }
35014
+ async function probeAuth(loggedIn) {
35015
+ if (!loggedIn && !usingEnvToken()) return "none";
35016
+ if (!usingEnvToken() && sessionExpired()) return "expired";
35017
+ const timeout = new Promise((res) => setTimeout(() => res("unreachable"), 5e3).unref?.());
35018
+ const call = get("/users/me").then(
35019
+ () => "ok",
35020
+ (err) => err instanceof ApiError && err.statusCode === 401 ? "rejected" : "unreachable"
35021
+ );
35022
+ return Promise.race([call, timeout]);
35023
+ }
34856
35024
  var statusCommand = new Command("status").alias("whoami").description("Show project and login status").option("--json", "Output as JSON").option("--repair-hooks", "Re-enable the Gipity Claude Code plugin (hooks) if missing or disabled").action(async (opts) => {
34857
35025
  const config = getConfig();
34858
35026
  const auth = getAuth();
@@ -34860,6 +35028,7 @@ var statusCommand = new Command("status").alias("whoami").description("Show proj
34860
35028
  void cwd;
34861
35029
  const hookCheck = config ? checkGipityPlugin() : null;
34862
35030
  const queueDelivered = auth && !sessionExpired() ? await flushBugQueue().catch(() => 0) : 0;
35031
+ const probe = await probeAuth(!!auth);
34863
35032
  if (opts.json) {
34864
35033
  console.log(JSON.stringify({
34865
35034
  project: config ? {
@@ -34871,9 +35040,13 @@ var statusCommand = new Command("status").alias("whoami").description("Show proj
34871
35040
  } : null,
34872
35041
  // `valid` reflects the refresh token (the real session) - access
34873
35042
  // tokens auto-renew, so their expiry must not read as "invalid".
34874
- auth: auth ? {
34875
- email: auth.email,
34876
- valid: !sessionExpired()
35043
+ // `probe` is what one live call just proved: 'rejected' means every
35044
+ // authenticated command will fail even though `valid` reads true.
35045
+ auth: auth || usingEnvToken() ? {
35046
+ email: auth?.email,
35047
+ source: usingEnvToken() ? "agent-token" : "session",
35048
+ valid: usingEnvToken() ? probe !== "rejected" : !sessionExpired(),
35049
+ probe
34877
35050
  } : null,
34878
35051
  plugin: hookCheck
34879
35052
  }, null, 2));
@@ -34888,12 +35061,16 @@ var statusCommand = new Command("status").alias("whoami").description("Show proj
34888
35061
  console.log(`${muted("API:")} ${config.apiBase}`);
34889
35062
  if (config.agentGuid) console.log(`${muted("Agent:")} ${config.agentGuid}`);
34890
35063
  }
34891
- if (!auth) {
35064
+ if (usingEnvToken()) {
35065
+ console.log(`${muted("Auth:")} ${probe === "rejected" ? warning("agent API token (GIPITY_TOKEN) rejected by the server \u2014 mint a new one: gipity skill read agent-deploy") : success("agent API token (GIPITY_TOKEN)")}${probe === "unreachable" ? ` ${muted("(unverified \u2014 API unreachable)")}` : ""}`);
35066
+ } else if (!auth) {
34892
35067
  console.log(`${muted("Auth:")} ${warning("not logged in. Run: gipity login")}`);
34893
- } else if (sessionExpired()) {
34894
- console.log(`${muted("Auth:")} ${warning(`session expired for ${auth.email}. Run: gipity login`)}`);
35068
+ } else if (probe === "expired") {
35069
+ console.log(`${muted("Auth:")} ${warning(`session expired for ${auth.email}. Run: gipity login (headless/CI: set GIPITY_TOKEN \u2014 gipity skill read agent-deploy)`)}`);
35070
+ } else if (probe === "rejected") {
35071
+ 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)`)}`);
34895
35072
  } else {
34896
- console.log(`${muted("Auth:")} ${success(auth.email)}`);
35073
+ console.log(`${muted("Auth:")} ${success(auth.email)}${probe === "unreachable" ? ` ${muted("(unverified \u2014 API unreachable)")}` : ""}`);
34897
35074
  }
34898
35075
  if (queueDelivered > 0) {
34899
35076
  console.log(`${muted("Bug queue:")} ${success(`delivered ${queueDelivered} queued bug report${queueDelivered === 1 ? "" : "s"}`)}`);
@@ -34983,7 +35160,7 @@ var pushCommand = new Command("push").description("Push one or more files").argu
34983
35160
 
34984
35161
  // src/commands/upload.ts
34985
35162
  init_config();
34986
- import { statSync as statSync4, readdirSync as readdirSync5 } from "fs";
35163
+ import { statSync as statSync4, readdirSync as readdirSync6 } from "fs";
34987
35164
  import { join as join10, basename as basename2, posix, resolve as resolve10, dirname as dirname6 } from "path";
34988
35165
  init_utils();
34989
35166
  init_colors();
@@ -34992,7 +35169,7 @@ function walkFiles(root) {
34992
35169
  const stack = [root];
34993
35170
  while (stack.length) {
34994
35171
  const dir = stack.pop();
34995
- for (const entry of readdirSync5(dir, { withFileTypes: true })) {
35172
+ for (const entry of readdirSync6(dir, { withFileTypes: true })) {
34996
35173
  const full = join10(dir, entry.name);
34997
35174
  if (entry.isDirectory()) stack.push(full);
34998
35175
  else if (entry.isFile()) out.push(full);
@@ -35105,7 +35282,7 @@ init_config();
35105
35282
  // src/page-fixtures.ts
35106
35283
  init_api();
35107
35284
  init_config();
35108
- import { readFileSync as readFileSync11, statSync as statSync5, existsSync as existsSync10, readdirSync as readdirSync6 } from "node:fs";
35285
+ import { readFileSync as readFileSync11, statSync as statSync5, existsSync as existsSync10, readdirSync as readdirSync7 } from "node:fs";
35109
35286
  import { basename as basename3, resolve as resolve11, relative as relative3, join as join11 } from "node:path";
35110
35287
  var FIND_SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", ".gipity", "dist", "build", ".next", "coverage"]);
35111
35288
  function findByBasename(root, name, limit = 3) {
@@ -35117,7 +35294,7 @@ function findByBasename(root, name, limit = 3) {
35117
35294
  visited++;
35118
35295
  let entries;
35119
35296
  try {
35120
- entries = readdirSync6(dir, { withFileTypes: true });
35297
+ entries = readdirSync7(dir, { withFileTypes: true });
35121
35298
  } catch {
35122
35299
  continue;
35123
35300
  }
@@ -35190,7 +35367,7 @@ async function uploadCameraFeed(projectGuid, localPath) {
35190
35367
  }
35191
35368
 
35192
35369
  // src/commands/page-eval.ts
35193
- var EVAL_NO_VALUE_HINT = "The eval ran but returned no JSON-serializable value. A statement body with no `return`, an assignment, a void call, or a DOM node/function all serialize to null. End the script with an expression \u2014 or an explicit `return` \u2014 that yields plain data, e.g. `return { label: input.value, count: items.length }` or `return JSON.stringify(payload)`.";
35370
+ var EVAL_NO_VALUE_HINT = "The eval ran but returned no JSON-serializable value. A body ending in a loop/if-block, a `return` of undefined, or a DOM node/function all serialize to null. End the script with an expression \u2014 or an explicit `return` \u2014 that yields plain data, e.g. `return { label: input.value, count: items.length }` or `return JSON.stringify(payload)`.";
35194
35371
  function normalizeEvalResult(raw) {
35195
35372
  const trimmed = (raw ?? "").trim();
35196
35373
  if (trimmed === "" || trimmed === "null" || trimmed === "undefined") {
@@ -35310,7 +35487,7 @@ function slowRenderMessage(fps, o) {
35310
35487
  const frames = Math.max(1, Math.round(fps * (o.waitMs / 1e3)));
35311
35488
  return `${warning("\u26A0 Slow render:")} page painted at ${fps} fps, so the app's vision pipeline ran on roughly ${bold(`${frames} frame${frames === 1 ? "" : "s"}`)} during the ${Math.round(o.waitMs / 1e3)}s before this eval (it infers once per painted frame). ${bold("That is enough:")} --camera loops your still image, so every one of those frames is the SAME pixels and the model returns the SAME answer on each \u2014 re-running with a bigger --wait/--timeout cannot change the result. ${bold("Do not escalate the wait.")} If a detection landed, it is real. If nothing was detected, the suspect is the ${bold("frame")} (a model needs the whole subject in shot \u2014 a tight crop, an odd angle or a busy background reads as nothing) or the ${bold("app")} (frames never reach the model) \u2014 never the frame rate. Settle it in ONE run: ${CAMERA_FRAME_CHECK}`;
35312
35489
  }
35313
- return `${warning("\u26A0 Slow render:")} page painted at ${fps} fps. Waiting on real time (setTimeout) advances animation/physics time far slower than it looks \u2014 assertions after a wall-clock wait can report a false negative. Step the app's own loop deterministically instead (3D templates: ${bold("core.advance(seconds)")}).`;
35490
+ return `${warning("\u26A0 Slow render:")} page painted at ${fps} fps. Waiting on real time (setTimeout) advances animation/physics time far slower than it looks \u2014 assertions after a wall-clock wait can report a false negative. Step the app's own loop deterministically instead (3D templates: ${bold("core.advance(seconds)")}; 2D/Phaser template: ${bold("(await import('./js/config.js')).advance(seconds)")}).`;
35314
35491
  }
35315
35492
  async function pollEvalResult(evalJobId, expectedWorkMs) {
35316
35493
  const deadline = Date.now() + expectedWorkMs + 6e4;
@@ -35350,8 +35527,8 @@ function evalExecTimeoutMessage(result, budgetMs) {
35350
35527
  ` + (budgetOverrunHint("in-page budget", budgetMs) ?? "") + `
35351
35528
  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.`;
35352
35529
  }
35353
- var JS_DECOY_FLAGS = ["--js", "--javascript", "--script", "--code", "--expr", "--eval", "--exec"];
35354
- var pageEvalCommand = new Command("eval").description("Evaluate JS in a real browser on a page (DOM, computed styles, element rects; inline expr or --file script). ONE client per call - to verify realtime/presence across concurrent clients use `page test --observe` instead").argument("<url>", "URL to load").argument("[expr]", `JavaScript to evaluate in page context (inline expression or statement body with return/await; result is JSON-serialized). Omit when using --file. Time budget: the body has ${EVAL_SCRIPT_BUDGET_MS / 1e3}s to finish after page load (${EVAL_SCRIPT_BUDGET_CAMERA_MS / 1e3}s with --camera) - raise it with --timeout, max ${EVAL_SCRIPT_BUDGET_MAX_MS / 1e3}s.`).option("--file <path>", `Read the script body from a file instead of the inline <expr> arg (mutually exclusive). Runs as an async function body, so top-level return/await work. Same post-load budget as <expr> (--timeout).`).option(
35530
+ var JS_DECOY_FLAGS = ["--js", "--javascript", "--script", "--code", "--expr", "--eval", "--exec", "--action"];
35531
+ var pageEvalCommand = new Command("eval").description("Evaluate JS in a real browser on a page (DOM, computed styles, element rects; inline expr or --file script). ONE client per call - to verify realtime/presence across concurrent clients use `page test --observe` instead").argument("<url>", "URL to load").argument("[expr]", `JavaScript to evaluate in page context (inline expression or statement body; await works, and the trailing expression is returned automatically \u2014 REPL-style \u2014 so no explicit return is needed; result is JSON-serialized). Omit when using --file. Time budget: the body has ${EVAL_SCRIPT_BUDGET_MS / 1e3}s to finish after page load (${EVAL_SCRIPT_BUDGET_CAMERA_MS / 1e3}s with --camera) - raise it with --timeout, max ${EVAL_SCRIPT_BUDGET_MAX_MS / 1e3}s.`).option("--file <path>", `Read the script body from a file instead of the inline <expr> arg (mutually exclusive). Runs as an async function body, so top-level return/await work. Same post-load budget as <expr> (--timeout).`).option(
35355
35532
  "--step <expr>",
35356
35533
  `Run another expression against the SAME loaded page, after <expr> (repeat, max ${MAX_EVAL_STEPS}). Whatever that page load paid for \u2014 a vision model coming up, a game booting, a socket connecting \u2014 stays up for every step, so an N-part check costs ONE page load instead of N. Each step gets its own in-page budget and its own reported result.`,
35357
35534
  (val, prev) => [...prev, val],
@@ -35370,7 +35547,7 @@ var pageEvalCommand = new Command("eval").description("Evaluate JS in a real bro
35370
35547
  ).option("--fake-media", "Grant a synthetic microphone + camera and auto-accept the getUserMedia prompt, so a voice/camera app runs headlessly instead of hitting its no-camera path. The feed is a built-in test pattern (and a tone) \u2014 nothing a vision model can recognize; to drive a vision app use --camera <path> instead.").option("--wait <ms>", "Sleep this many ms after DOMContentLoaded before evaluating (lets late async work settle; max 30000). With --wait-for, it elapses AFTER the selector appears - gate, then settle.", "500").option("--wait-for <selector>", `Wait until this CSS selector appears before evaluating, then evaluate (deterministic - beats guessing a --wait). Gate on the state you are ASSERTING, not just a ready flag: '#verdict:not(:empty)' returns the instant the round lands, where a fixed wait snapshots mid-sequence. Same flag on page inspect/screenshot. Max ${WAIT_FOR_MAX_MS}ms (--wait-timeout).`).option("--wait-timeout <ms>", `Max ms to wait for --wait-for before giving up (max ${WAIT_FOR_MAX_MS})`, "5000").option(
35371
35548
  "--timeout <ms>",
35372
35549
  `How long the script itself may run IN the page - its own await/setTimeout pauses count (default ${EVAL_SCRIPT_BUDGET_MS}, ${EVAL_SCRIPT_BUDGET_CAMERA_MS} with --camera/--fake-media; max ${EVAL_SCRIPT_BUDGET_MAX_MS}). Raise it to trace a sequence that unfolds over time (a game round, an animation). Distinct from --wait, which only sleeps BEFORE the script.`
35373
- ).option("--auth", "Evaluate signed in as you (your Gipity account), so a page behind a Sign-in-with-Gipity login is reachable. Only works for apps using Sign in with Gipity, hosted on *.gipity.ai.").option("--json", "Output as JSON").action((url, exprArg, opts) => run("Page eval", async () => {
35550
+ ).option("--auth", "Evaluate signed in as you (your Gipity account), so a page behind a Sign-in-with-Gipity login is reachable. Only works for apps using Sign in with Gipity, hosted on *.gipity.ai. Without this flag the page loads as a genuinely anonymous, signed-out visitor \u2014 nothing carries over from earlier --auth runs.").option("--json", "Output as JSON").action((url, exprArg, opts) => run("Page eval", async () => {
35374
35551
  const decoy = JS_DECOY_FLAGS.find((f) => opts[f.slice(2)] !== void 0);
35375
35552
  if (decoy) {
35376
35553
  pageEvalCommand.error(
@@ -35514,7 +35691,7 @@ ${hint}`);
35514
35691
  }));
35515
35692
  return;
35516
35693
  }
35517
- console.log(`${brand("Eval")} ${bold(d.url || url)}`);
35694
+ console.error(`${brand("Eval")} ${bold(d.url || url)}`);
35518
35695
  if (d.navigationIncomplete) {
35519
35696
  console.log(`${warning("\u26A0 Navigation incomplete:")} ${d.note || "page did not reach full load"}`);
35520
35697
  }
@@ -35524,10 +35701,12 @@ ${hint}`);
35524
35701
  if (d.auth?.requested) {
35525
35702
  const who = getAuth()?.email;
35526
35703
  console.log(d.auth.established ? `${muted("Auth:")} ${success("session established")}${who ? muted(` as ${who}`) : ""} ${muted("(what the page renders with it is app-defined)")}` : `${warning("Auth: session NOT established")}${d.auth.detail ? ` \u2014 ${d.auth.detail}` : ""} ${muted("(this is the anonymous view)")}`);
35704
+ } else {
35705
+ console.log(muted("Auth: anonymous visitor (signed out; pass --auth to run as your Gipity account)"));
35527
35706
  }
35528
35707
  if (camera) console.log(`${muted("Camera:")} ${camera.name} ${muted("(played as the webcam feed; getUserMedia resolves)")}`);
35529
35708
  if (hosted.length) console.log(`${muted("Fixtures:")} ${hosted.map((h) => h.name).join(", ")}`);
35530
- console.log(opts.file ? `${muted("Script:")} ${opts.file}` : `${muted("Expression:")} ${summarizeExpr(expr)}`);
35709
+ console.error(opts.file ? `${muted("Script:")} ${opts.file}` : `${muted("Expression:")} ${summarizeExpr(expr)}`);
35531
35710
  console.log(`
35532
35711
  ${result.trim() ? result : muted("(empty result)")}`);
35533
35712
  if (noValue) console.log(muted(`
@@ -35653,7 +35832,10 @@ function fmtMs(ms2) {
35653
35832
  return ms2 >= 1e3 ? `${(ms2 / 1e3).toFixed(1)}s` : `${ms2}ms`;
35654
35833
  }
35655
35834
  function printAuthLine(auth) {
35656
- if (!auth?.requested) return;
35835
+ if (!auth?.requested) {
35836
+ console.log(`${label("Auth")} ${muted("anonymous visitor (signed out; pass --auth to capture as your Gipity account)")}`);
35837
+ return;
35838
+ }
35657
35839
  const who = getAuth()?.email;
35658
35840
  console.log(auth.established ? `${label("Auth")} ${success("session established")}${who ? muted(` as ${who}`) : ""} ${muted("(what the page renders with it is app-defined)")}` : `${warning("Auth: session NOT established")}${auth.detail ? ` \u2014 ${auth.detail}` : ""} ${muted("(this is the anonymous view)")}`);
35659
35841
  }
@@ -35751,7 +35933,7 @@ function splitCsv(values) {
35751
35933
  function appendOption(value, previous = []) {
35752
35934
  return [...previous, value];
35753
35935
  }
35754
- var ACTION_DECOY_FLAGS = ["--eval", "--js", "--javascript", "--script", "--code", "--exec"];
35936
+ var ACTION_ALIAS_FLAGS = ["--eval", "--js", "--javascript", "--script", "--code", "--exec"];
35755
35937
  function buildWaitForGate(selector, timeoutMs) {
35756
35938
  const sel = JSON.stringify(selector);
35757
35939
  return `await (async () => { const __t0 = Date.now(); while (Date.now() - __t0 < ${timeoutMs}) { try { if (document.querySelector(${sel})) return; } catch (e) { throw new Error('wait-for: invalid selector ' + ${sel}); } await new Promise((r) => setTimeout(r, 100)); } throw new Error('wait-for: nothing matched ' + ${sel} + ' within ${timeoutMs}ms \u2014 the page never reached that state (raise --wait-timeout, fix the selector, or check the app actually gets there headlessly)'); })();`;
@@ -35760,13 +35942,14 @@ var WAIT_FOR_MAX_MS2 = 15e3;
35760
35942
  var WAIT_FOR_DEFAULT_MS = 15e3;
35761
35943
  var MAX_POST_LOAD_DELAY_MS = 3e4;
35762
35944
  var CAMERA_DEFAULT_DELAY_MS = 15e3;
35763
- var pageScreenshotCommand = new Command("screenshot").description("Screenshot a web page").argument("<url>", "URL to screenshot").option("--wait <ms>", `Sleep this many ms after DOMContentLoaded before capturing (default 1000, max ${MAX_POST_LOAD_DELAY_MS}). With --camera it defaults to ${CAMERA_DEFAULT_DELAY_MS} so the vision model is up. Same flag as page eval/inspect.`).option("--wait-for <selector>", `Wait until this CSS selector appears before capturing, then capture (deterministic - beats guessing a --wait). Same flag as page eval/inspect. Gate on what you are photographing ('[data-vision="ready"]', '#verdict:not(:empty)'). Max ${WAIT_FOR_MAX_MS2}ms (--wait-timeout).`).option("--wait-timeout <ms>", `Max ms to wait for --wait-for before giving up (default ${WAIT_FOR_DEFAULT_MS}, max ${WAIT_FOR_MAX_MS2}). Timing out is reported - the capture still happens, so you see the state it got stuck in.`).option("--action <js>", `Run JS in the page before capturing \u2014 e.g. click a button to enter a state ("document.getElementById('play').click()"). Runs as an async function body, so const/await and app-relative import('./\u2026') work. Runs after the post-load delay, then settles again before the shot. If it throws, the capture still happens and the failure is reported.`).option("--full", "Capture the full scrollable page (default: viewport only). Scrolls the page through first so scroll-reveal/fade-in-on-scroll (IntersectionObserver) sections render into the shot instead of capturing blank.").option("-o, --output <file>", "Output path (single viewport only; default .gipity/screenshots/ss-<host>-<timestamp>.png)").option("--device <names>", `Device preset(s): ${Object.keys(DEVICE_PRESETS).join(", ")} (comma-separated or repeat flag). mobile/tablet emulate a real touch device \u2014 touch events, mobile user-agent, DPR \u2014 so touch-gated mobile UI actually renders.`, appendOption, []).option("--viewport <dims>", "Raw viewport(s): WxH or WxH@dpr (comma-separated or repeat flag)", appendOption, []).option("--no-reload-between", "Skip reload between viewports (faster, lower fidelity - only safe for static pages)").option("--fake-media", "Grant a synthetic microphone + camera and auto-accept the getUserMedia prompt, so voice/camera apps render headlessly. The video feed is a built-in test pattern \u2014 to capture what the app does with a REAL frame (a hand, a face, an object), use --camera <path> instead.").option("--camera <path>", "Play a local image or video (.png/.jpg/.webp/.mp4/.webm/.y4m/.mjpeg) as the browser's WEBCAM feed, then capture \u2014 so the shot shows the app reacting to a frame you chose (detected gesture, boxes, labels). Implies --fake-media.").option("--auth", "Capture the page signed in as you (your Gipity account), so UI behind a Sign-in-with-Gipity login is shown. Only works for apps using Sign in with Gipity, hosted on *.gipity.ai.").option("--ephemeral", "Skip the project screenshot history: do not persist this capture to Gipity (screenshots/ in the project). Local file is still written.").option("--json", "Output JSON metadata instead of a friendly summary").addOption(new Option("--post-load-delay <ms>", "Alias for --wait").hideHelp()).addOption(new Option("--full-page", "Alias for --full").hideHelp()).action((url, opts) => run("Page screenshot", async () => {
35764
- const decoy = ACTION_DECOY_FLAGS.find((f) => opts[f.slice(2)] !== void 0);
35765
- if (decoy) {
35766
- pageScreenshotCommand.error(
35767
- `error: ${decoy} is not a flag on screenshot \u2014 use --action "<js>" to run JavaScript in the page before the capture, e.g. gipity page screenshot "<url>" --action "document.getElementById('play').click()"`
35768
- );
35945
+ var pageScreenshotCommand = new Command("screenshot").description("Screenshot a web page").argument("<url>", "URL to screenshot").option("--device <names>", `Device preset(s): ${Object.keys(DEVICE_PRESETS).join(", ")} (comma-separated or repeat flag). mobile/tablet emulate a real touch device \u2014 touch events, mobile user-agent, DPR \u2014 so touch-gated mobile UI actually renders.`, appendOption, []).option("--viewport <dims>", "Raw viewport(s): WxH or WxH@dpr (comma-separated or repeat flag), e.g. --viewport 390x844 for a phone-sized window", appendOption, []).option("--wait <ms>", `Sleep this many ms after DOMContentLoaded before capturing (default 1000, max ${MAX_POST_LOAD_DELAY_MS}). With --camera it defaults to ${CAMERA_DEFAULT_DELAY_MS} so the vision model is up. Same flag as page eval/inspect.`).option("--wait-for <selector>", `Wait until this CSS selector appears before capturing, then capture (deterministic - beats guessing a --wait). Same flag as page eval/inspect. Gate on what you are photographing ('[data-vision="ready"]', '#verdict:not(:empty)'). Max ${WAIT_FOR_MAX_MS2}ms (--wait-timeout).`).option("--wait-timeout <ms>", `Max ms to wait for --wait-for before giving up (default ${WAIT_FOR_DEFAULT_MS}, max ${WAIT_FOR_MAX_MS2}). Timing out is reported - the capture still happens, so you see the state it got stuck in.`).option("--action <js>", `Run JS in the page before capturing \u2014 e.g. click a button to enter a state ("document.getElementById('play').click()"). Runs as an async function body, so const/await and app-relative import('./\u2026') work. Runs after the post-load delay, then settles again before the shot. If it throws, the capture still happens and the failure is reported.`).option("--full", "Capture the full scrollable page (default: viewport only). Scrolls the page through first so scroll-reveal/fade-in-on-scroll (IntersectionObserver) sections render into the shot instead of capturing blank.").option("-o, --output <file>", "Output path (single viewport only; default .gipity/screenshots/ss-<host>-<timestamp>.png)").option("--no-reload-between", "Skip reload between viewports (faster, lower fidelity - only safe for static pages)").option("--fake-media", "Grant a synthetic microphone + camera and auto-accept the getUserMedia prompt, so voice/camera apps render headlessly. The video feed is a built-in test pattern \u2014 to capture what the app does with a REAL frame (a hand, a face, an object), use --camera <path> instead.").option("--camera <path>", "Play a local image or video (.png/.jpg/.webp/.mp4/.webm/.y4m/.mjpeg) as the browser's WEBCAM feed, then capture \u2014 so the shot shows the app reacting to a frame you chose (detected gesture, boxes, labels). Implies --fake-media.").option("--auth", "Capture the page signed in as you (your Gipity account), so UI behind a Sign-in-with-Gipity login is shown. Only works for apps using Sign in with Gipity, hosted on *.gipity.ai. Without this flag the page loads as a genuinely anonymous, signed-out visitor \u2014 nothing carries over from earlier --auth runs.").option("--ephemeral", "Skip the project screenshot history: do not persist this capture to Gipity (screenshots/ in the project). Local file is still written.").option("--json", "Output JSON metadata instead of a friendly summary").addOption(new Option("--post-load-delay <ms>", "Alias for --wait").hideHelp()).addOption(new Option("--full-page", "Alias for --full").hideHelp()).addOption(new Option("--width <px>", "Alias: --viewport WxH").hideHelp()).addOption(new Option("--height <px>", "Alias: --viewport WxH").hideHelp()).action((url, opts) => run("Page screenshot", async () => {
35946
+ const aliasFlag = ACTION_ALIAS_FLAGS.find((f) => opts[f.slice(2)] !== void 0);
35947
+ if (aliasFlag) {
35948
+ console.error(muted(
35949
+ `Note: treating ${aliasFlag} as --action \u2014 it runs your JS in the page before the capture. --action is the canonical flag.`
35950
+ ));
35769
35951
  }
35952
+ const actionScript = [opts.action, ...ACTION_ALIAS_FLAGS.map((f) => opts[f.slice(2)])].filter(Boolean).join("\n");
35770
35953
  const delayRaw = opts.wait ?? opts.postLoadDelay;
35771
35954
  const chosenDelay = delayRaw !== void 0;
35772
35955
  let postLoadDelayMs = chosenDelay ? parseInt(String(delayRaw), 10) : 1e3;
@@ -35800,6 +35983,20 @@ var pageScreenshotCommand = new Command("screenshot").description("Screenshot a
35800
35983
  }
35801
35984
  const deviceNames = splitCsv(opts.device);
35802
35985
  const viewportStrs = splitCsv(opts.viewport);
35986
+ if (opts.width !== void 0 || opts.height !== void 0) {
35987
+ if (opts.width === void 0 || opts.height === void 0) {
35988
+ pageScreenshotCommand.error(
35989
+ "error: --width and --height go together (both in px, equivalent to --viewport WxH) \u2014 or use a preset like --device mobile for a real handset (touch events, mobile user-agent, DPR)"
35990
+ );
35991
+ }
35992
+ if (!/^\d+$/.test(String(opts.width)) || !/^\d+$/.test(String(opts.height))) {
35993
+ pageScreenshotCommand.error("error: --width and --height must be integers (px)");
35994
+ }
35995
+ viewportStrs.push(`${opts.width}x${opts.height}`);
35996
+ if (parseInt(String(opts.width), 10) <= 500) {
35997
+ console.error(muted("Tip: --device mobile emulates a real handset (touch events, mobile user-agent, DPR) \u2014 a raw viewport only resizes the window."));
35998
+ }
35999
+ }
35803
36000
  const customViewports = [
35804
36001
  ...deviceNames.map(resolveDevice),
35805
36002
  ...viewportStrs.map(parseViewportString)
@@ -35823,7 +36020,7 @@ var pageScreenshotCommand = new Command("screenshot").description("Screenshot a
35823
36020
  }
35824
36021
  const preCapture = [
35825
36022
  opts.waitFor ? buildWaitForGate(opts.waitFor, waitForTimeoutMs) : "",
35826
- opts.action ?? ""
36023
+ actionScript
35827
36024
  ].filter(Boolean).join("\n");
35828
36025
  const body = {
35829
36026
  url,
@@ -35945,7 +36142,7 @@ ${brand("@ " + dims)}${deviceTag}`);
35945
36142
  printVfsLine(s);
35946
36143
  }
35947
36144
  }));
35948
- for (const f of ACTION_DECOY_FLAGS) pageScreenshotCommand.addOption(new Option(`${f} <value>`).hideHelp());
36145
+ for (const f of ACTION_ALIAS_FLAGS) pageScreenshotCommand.addOption(new Option(`${f} <js>`, "Alias for --action").hideHelp());
35949
36146
  pageScreenshotCommand.addHelpText("after", `
35950
36147
  Examples:
35951
36148
  gipity page screenshot "https://dev.gipity.ai/me/app/"
@@ -35992,7 +36189,7 @@ function shortUrl(url, truncate = true, maxLen = 100) {
35992
36189
  const tailLen = Math.floor(keep / 2);
35993
36190
  return result.slice(0, headLen) + "\u2026" + result.slice(-tailLen);
35994
36191
  }
35995
- var pageInspectCommand = new Command("inspect").description("Inspect a web page (console, failed resources, timing, layout overflow). ONE passive load - to verify realtime/presence across concurrent clients use `page test --observe`").argument("<url>", "URL to inspect").option("--wait <ms>", "Sleep this many ms after DOMContentLoaded before capturing (lets late async/LCP work settle; max 30000)", "500").option("--wait-for <selector>", "Wait until this CSS selector appears before capturing (deterministic; replaces --wait)").option("--wait-timeout <ms>", "Max ms to wait for --wait-for before giving up", "5000").option("--json", "Output as JSON").option("--no-reprobe", "Skip the automatic second probe that filters one-time transient console errors - roughly halves inspect time on a page with any console error, at the cost of possibly reporting a cold-load transient as real").option("--no-truncate", "Show full URLs instead of truncating long ones with middle-ellipsis").option("--all", "Include render-blocking, large resources, oversized images, overflow culprits, and LCP detail").option("--no-fake-media", "Inspect with NO camera or microphone present, so a getUserMedia app takes its no-device error path (use this only when that fallback IS what you're inspecting)").option("--device <name>", `Inspect as a real touch device: ${TOUCH_DEVICES.join(", ")}. Emulates touch events, mobile user-agent and DPR, so a touch-gated mobile layout is what gets inspected.`).option("--auth", "Load the page signed in as you (your Gipity account), so pages behind a Sign-in-with-Gipity login are reachable. Only works for apps using Sign in with Gipity, hosted on *.gipity.ai.").addOption(new Option("--screenshot [path]", "Capture a screenshot").hideHelp()).action((url, opts) => {
36192
+ 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) => {
35996
36193
  if (opts.screenshot !== void 0) {
35997
36194
  console.error(error("page inspect does not capture screenshots. Use page screenshot:"));
35998
36195
  console.error(` gipity page screenshot ${url}${typeof opts.screenshot === "string" ? ` -o ${opts.screenshot}` : ""}`);
@@ -36066,6 +36263,8 @@ async function inspectPage(url, opts = {}) {
36066
36263
  if (b7.auth?.requested) {
36067
36264
  const who = getAuth()?.email;
36068
36265
  console.log(b7.auth.established ? `${muted("Auth:")} ${success("session established")}${who ? muted(` as ${who}`) : ""} ${muted("(what the page renders with it is app-defined)")}` : `${warning("Auth: session NOT established")}${b7.auth.detail ? ` \u2014 ${b7.auth.detail}` : ""} ${muted("(this is the anonymous view)")}`);
36266
+ } else {
36267
+ console.log(muted("Auth: anonymous visitor (signed out; pass --auth to load as your Gipity account)"));
36069
36268
  }
36070
36269
  console.log(`${muted("Title:")} ${b7.title || "(none)"}`);
36071
36270
  console.log(`${muted("Elements:")} ${b7.elementCount || 0}`);
@@ -36255,6 +36454,10 @@ var deployCommand = new Command("deploy").description("Deploy to dev or prod").a
36255
36454
  } else {
36256
36455
  console.log(success(`\u2713 Deployed to ${target}`) + muted(` (${d.elapsedMs}ms)`));
36257
36456
  if (d.url) console.log(`${muted("Live:")} ${brand(d.url)}`);
36457
+ if (d.phases?.some((p) => p.name === "functions")) {
36458
+ console.log(`${muted("Functions:")} POST ${brand(`${config.apiBase}/api/${config.projectGuid}/fn/<name>`)}`);
36459
+ console.log(muted(" (same address on dev and prod; names: gipity fn list; verify the public path: gipity fn call <name> --anon)"));
36460
+ }
36258
36461
  await inspectAfter();
36259
36462
  }
36260
36463
  }));
@@ -36292,10 +36495,15 @@ dbCommand.command("query <sql>").description("Run SQL").option("--database <name
36292
36495
  console.log(columns.map((c) => String(row2[c] ?? "NULL")).join(" "));
36293
36496
  }
36294
36497
  }
36498
+ if (res.data.affectedRows !== void 0) {
36499
+ console.log(`Affected rows: ${res.data.affectedRows}`);
36500
+ }
36295
36501
  } else if (res.data.affectedRows !== void 0) {
36296
36502
  console.log(`Affected rows: ${res.data.affectedRows}`);
36297
36503
  } else if (res.data.results) {
36298
36504
  console.log(JSON.stringify(res.data.results, null, 2));
36505
+ } else {
36506
+ console.log(JSON.stringify(res.data, null, 2));
36299
36507
  }
36300
36508
  }
36301
36509
  }));
@@ -36336,11 +36544,15 @@ dbCommand.command("list").description("List databases").option("--all", "List da
36336
36544
  }
36337
36545
  }));
36338
36546
  dbCommand.command("create <name>").description("Create a database").option("--json", "Output as JSON").action((name, opts) => run("Create", async () => {
36339
- const response = await sendMessage(`Create a new database called "${name}" for this project. Confirm when done, no explanation.`);
36547
+ const config = requireConfig();
36548
+ await post(`/projects/${config.projectGuid}/db/manage`, {
36549
+ action: "create",
36550
+ name
36551
+ });
36340
36552
  if (opts.json) {
36341
- console.log(JSON.stringify({ response }));
36553
+ console.log(JSON.stringify({ success: true, name }));
36342
36554
  } else {
36343
- console.log(response);
36555
+ console.log(success(`Created database '${name}'.`));
36344
36556
  }
36345
36557
  }));
36346
36558
  dbCommand.command("drop <name>").description("Drop a database").option("--project <slug>", "Project slug (required if not in a project directory)").option("--yes", "Skip confirmation").option("--json", "Output as JSON").action((name, opts) => run("Drop", async () => {
@@ -36428,8 +36640,8 @@ memoryCommand.command("delete <topic>").description("Delete a topic").option("--
36428
36640
  // src/commands/sandbox.ts
36429
36641
  init_api();
36430
36642
  init_config();
36431
- import { readFileSync as readFileSync13, existsSync as existsSync11, statSync as statSync6 } from "fs";
36432
- import { dirname as dirname8, extname as extname4, relative as relative4, resolve as resolve12 } from "path";
36643
+ import { readFileSync as readFileSync13, existsSync as existsSync11, statSync as statSync6, mkdirSync as mkdirSync8, writeFileSync as writeFileSync9 } from "fs";
36644
+ import { dirname as dirname8, extname as extname4, relative as relative4, resolve as resolve12, sep as sep3 } from "path";
36433
36645
  init_colors();
36434
36646
  var LANG_MAP = {
36435
36647
  js: "javascript",
@@ -36547,13 +36759,13 @@ function resolveRelativeCwd() {
36547
36759
  return rel2.split(/[\\/]/).filter(Boolean).join("/");
36548
36760
  }
36549
36761
  var sandboxCommand = new Command("sandbox").description("Run code in a sandbox");
36550
- sandboxCommand.command("run [args...]").description("Run code").option("--language <language>", "Language: js, py, or bash (required unless pinned by an interpreter token or --file extension)").option("--file <path>", "Read the code body from a file instead of the inline <code> arg; --language is inferred from the extension when not given").option("--timeout <seconds>", "Execution timeout in seconds", "30").option(
36762
+ sandboxCommand.command("run [args...]").description("Run code").option("--language <language>", "Language: js, py, or bash (required unless pinned by an interpreter token or --file extension)").option("--file <path>", "Read the code body from a file instead of the inline <code> arg; --language is inferred from the extension when not given").addOption(new Option("--code <code>", "Alias for the positional inline <code> arg").hideHelp()).option("--timeout <seconds>", "Execution timeout in seconds", "30").option(
36551
36763
  "--input <path>",
36552
36764
  "Narrow to specific project files instead of auto-mirroring the whole tree (repeatable). Use this only for >1 GB projects or when you want surgical control.",
36553
36765
  (v7, prev) => [...prev ?? [], v7]
36554
36766
  ).option(
36555
- "--no-sync-output <glob>",
36556
- 'Do not persist run outputs matching this glob back to the project (repeatable). For byproducts you want to inspect once but never keep, e.g. --no-sync-output "docs/preview*". Supports *, **, and dir/ prefixes.',
36767
+ "--discard-output <glob>",
36768
+ "Discard run outputs matching this glob entirely - not saved, not returned (repeatable). To LOOK at an output without keeping it, write it under tmp/ instead: scratch outputs land on local disk only and never enter the project. Supports *, **, and dir/ prefixes.",
36557
36769
  (v7, prev) => [...prev, v7],
36558
36770
  []
36559
36771
  ).option("--json", "Output as JSON").addHelpText("after", `
@@ -36561,6 +36773,11 @@ By default the whole project is auto-mirrored into /work/ (up to 1 GB) -
36561
36773
  so your code can reference project files by their relative path, and any
36562
36774
  file you write lands back in the project. No manual copy needed.
36563
36775
 
36776
+ Exception: tmp/ is scratch. An output written under tmp/ comes back to
36777
+ your local tmp/ for inspection but is never saved to the project - use it
36778
+ for previews and other look-once artifacts (no cleanup needed). To drop
36779
+ an output entirely, --discard-output <glob>.
36780
+
36564
36781
  Use --input only for projects over the auto-mirror cap, or when you want
36565
36782
  to restrict what the sandbox sees.
36566
36783
 
@@ -36577,6 +36794,11 @@ Examples:
36577
36794
  $ gipity sandbox run --language python \\
36578
36795
  "import pandas as pd; print(pd.read_csv('data/sales.csv').describe())"
36579
36796
 
36797
+ # Preview a generated PDF without saving the preview: write it to tmp/
36798
+ $ gipity sandbox run bash \\
36799
+ "pdftoppm -png -r 90 docs/report.pdf tmp/preview"
36800
+ # -> tmp/preview-1.png lands on local disk only; Read it, done.
36801
+
36580
36802
  # Run a script file directly (language inferred from .py)
36581
36803
  $ gipity sandbox run --file build_report.py
36582
36804
  $ gipity sandbox run python build_report.py # same thing, interpreter shorthand
@@ -36598,6 +36820,13 @@ GCC/Rust).
36598
36820
  let inlineCode;
36599
36821
  let filePath = opts.file;
36600
36822
  let langFromInterp;
36823
+ if (opts.code !== void 0) {
36824
+ if (args.length) {
36825
+ console.error(error("Pass the code once: either positionally or via --code, not both"));
36826
+ process.exit(1);
36827
+ }
36828
+ args = [opts.code];
36829
+ }
36601
36830
  if (args.length >= 2 && INTERPRETERS[args[0].toLowerCase()] !== void 0) {
36602
36831
  langFromInterp = INTERPRETERS[args[0].toLowerCase()];
36603
36832
  const rest = args.slice(1).join(" ");
@@ -36627,9 +36856,9 @@ GCC/Rust).
36627
36856
  }
36628
36857
  }
36629
36858
  const language = resolveLanguage({ langFromInterp, langOpt: opts.language, filePath, inlineCode });
36630
- const noSyncOutput = opts.syncOutput ?? [];
36631
- if (noSyncOutput.some((g) => !g.trim())) {
36632
- console.error(error('--no-sync-output requires a non-empty glob (e.g. --no-sync-output "docs/preview*")'));
36859
+ const discardOutput = opts.discardOutput ?? [];
36860
+ if (discardOutput.some((g) => !g.trim())) {
36861
+ console.error(error('--discard-output requires a non-empty glob (e.g. --discard-output "*.ppm")'));
36633
36862
  process.exit(1);
36634
36863
  }
36635
36864
  const { config } = await resolveProjectContext();
@@ -36655,16 +36884,30 @@ GCC/Rust).
36655
36884
  cwd,
36656
36885
  // The filter must run SERVER-side (in the output extractor): skipping
36657
36886
  // only in the CLI would still write the files into project storage and
36658
- // make every later sync propose deleting them.
36659
- noSyncOutput: noSyncOutput.length ? noSyncOutput : void 0
36887
+ // make every later sync propose deleting them. (`noSyncOutput` is the
36888
+ // wire name for --discard-output.)
36889
+ noSyncOutput: discardOutput.length ? discardOutput : void 0
36660
36890
  });
36661
36891
  const res = opts.json ? await doRun() : await withSpinner("Running in sandbox\u2026", doRun, { done: null });
36662
36892
  const pulledLocal = !!(res.data.outputFiles?.length && getConfigPath());
36663
36893
  if (pulledLocal) {
36664
36894
  await sync({ interactive: false, progress: opts.json ? void 0 : createProgressReporter() });
36665
36895
  }
36896
+ const scratchWritten = [];
36897
+ if (res.data.scratchFiles?.length) {
36898
+ const base = resolve12(getProjectRoot() ?? process.cwd());
36899
+ for (const f of res.data.scratchFiles) {
36900
+ const rel2 = f.path.replace(/\\/g, "/");
36901
+ const abs = resolve12(base, rel2);
36902
+ if (abs !== base && !abs.startsWith(base + sep3)) continue;
36903
+ mkdirSync8(dirname8(abs), { recursive: true });
36904
+ writeFileSync9(abs, Buffer.from(f.contentBase64, "base64"));
36905
+ scratchWritten.push(rel2);
36906
+ }
36907
+ }
36666
36908
  if (opts.json) {
36667
- console.log(JSON.stringify({ ...res.data, filesSynced: pulledLocal }));
36909
+ const { scratchFiles: _omit, ...rest } = res.data;
36910
+ console.log(JSON.stringify({ ...rest, scratchFiles: scratchWritten, filesSynced: pulledLocal }));
36668
36911
  } else {
36669
36912
  if (res.data.autoMirrorSkipped) {
36670
36913
  console.error(dim(`Note: ${res.data.autoMirrorSkipped.reason}`));
@@ -36694,8 +36937,12 @@ GCC/Rust).
36694
36937
  if (pulledLocal) console.log(dim("Not pulled locally - run 'gipity sync' to fetch them."));
36695
36938
  }
36696
36939
  }
36940
+ if (scratchWritten.length > 0) {
36941
+ console.log("\nScratch outputs (local only - never synced or deployed):");
36942
+ for (const f of scratchWritten) console.log(`${f}`);
36943
+ }
36697
36944
  if (res.data.skippedOutputFiles && res.data.skippedOutputFiles.length > 0) {
36698
- console.log(dim("\nNot persisted (--no-sync-output):"));
36945
+ console.log(dim("\nDiscarded (--discard-output):"));
36699
36946
  for (const f of res.data.skippedOutputFiles) console.log(dim(`${f}`));
36700
36947
  }
36701
36948
  if (res.data.exitCode !== 0) {
@@ -36737,9 +36984,11 @@ ${syncResult.summary}`;
36737
36984
  ...a.remoteSize != null ? { size: a.remoteSize } : {}
36738
36985
  }));
36739
36986
  }
36987
+ const displayContent = res.data.content || res.data.costWarning?.message || "";
36740
36988
  if (opts.json) {
36741
36989
  console.log(JSON.stringify({
36742
- content: res.data.content,
36990
+ content: displayContent,
36991
+ costWarning: res.data.costWarning ?? null,
36743
36992
  toolsUsed: res.data.toolsUsed?.map((t) => ({
36744
36993
  tool: t.toolName,
36745
36994
  success: t.success,
@@ -36753,7 +37002,7 @@ ${syncResult.summary}`;
36753
37002
  syncedFiles: syncChanges
36754
37003
  }));
36755
37004
  } else {
36756
- console.log(res.data.content);
37005
+ console.log(displayContent);
36757
37006
  if (res.data.toolsUsed && res.data.toolsUsed.length > 0) {
36758
37007
  const toolNames = [...new Set(res.data.toolsUsed.map((t) => t.toolName))];
36759
37008
  console.log(`
@@ -36799,7 +37048,7 @@ chatCommand.command("delete <guid>").description("Delete a chat").option("--json
36799
37048
  init_api();
36800
37049
  init_config();
36801
37050
  import { join as join13 } from "path";
36802
- import { mkdirSync as mkdirSync8 } from "fs";
37051
+ import { mkdirSync as mkdirSync9 } from "fs";
36803
37052
  init_colors();
36804
37053
  init_utils();
36805
37054
  var projectCommand = new Command("project").description("Manage projects").argument("[name]", "Switch to project by name/slug").option("--json", "Output as JSON").action((name, opts) => run("Project", async () => {
@@ -36857,7 +37106,7 @@ projectCommand.command("create <name>").description("Create a project").option("
36857
37106
  const res = await post("/projects", body);
36858
37107
  const project = res.data;
36859
37108
  const dir = join13(getProjectsRoot(), project.slug);
36860
- mkdirSync8(dir, { recursive: true });
37109
+ mkdirSync9(dir, { recursive: true });
36861
37110
  const accountSlug = await getAccountSlug();
36862
37111
  let agentGuid = "";
36863
37112
  try {
@@ -36891,7 +37140,7 @@ projectCommand.command("create <name>").description("Create a project").option("
36891
37140
  if (process.env.GIPITY_NON_INTERACTIVE === "1") {
36892
37141
  console.log(`${muted("Next:")} switch to "${project.name}" in the sidebar.`);
36893
37142
  } else {
36894
- console.log(`${muted("Next:")} exit Claude (Ctrl+D), then run: ${brand("gipity claude")}`);
37143
+ console.log(`${muted("Next:")} exit Claude (Ctrl+D), then run: ${brand("gipity build")}`);
36895
37144
  console.log(`${muted("Pick")} "${project.name}" ${muted(`- it'll be at the top of the list.`)}`);
36896
37145
  }
36897
37146
  }));
@@ -37112,7 +37361,7 @@ function printStepRuns(steps, emptyNote) {
37112
37361
  }
37113
37362
  }
37114
37363
  var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
37115
- var sleep2 = (ms2) => new Promise((resolve19) => setTimeout(resolve19, ms2));
37364
+ var sleep2 = (ms2) => new Promise((resolve20) => setTimeout(resolve20, ms2));
37116
37365
  async function waitForRun(wfGuid, prevGuid, timeoutSec) {
37117
37366
  const deadline = Date.now() + timeoutSec * 1e3;
37118
37367
  let runGuid;
@@ -37729,14 +37978,15 @@ storageCommand.command("retention").description("View or adjust your file versio
37729
37978
  printRetention(data, opts.reset === true || hasSet);
37730
37979
  }));
37731
37980
 
37732
- // src/commands/claude.ts
37981
+ // src/commands/build.ts
37733
37982
  init_platform();
37734
37983
  init_auth();
37735
37984
  init_api();
37736
- import { join as join16, dirname as dirname10, resolve as resolve14, basename as basename4, sep as sep4 } from "path";
37737
- import { existsSync as existsSync13, mkdirSync as mkdirSync10, readFileSync as readFileSync16, writeFileSync as writeFileSync10, renameSync as renameSync2, readdirSync as readdirSync7, statSync as statSync7 } from "fs";
37738
- import { homedir as homedir11 } from "os";
37985
+ import { join as join17, dirname as dirname10, resolve as resolve14, basename as basename4, sep as sep5 } from "path";
37986
+ import { existsSync as existsSync14, mkdirSync as mkdirSync12, readFileSync as readFileSync17, writeFileSync as writeFileSync12, renameSync as renameSync2, readdirSync as readdirSync8, statSync as statSync7 } from "fs";
37987
+ import { homedir as homedir12 } from "os";
37739
37988
  import { fileURLToPath as fileURLToPath2 } from "url";
37989
+ import { randomUUID } from "crypto";
37740
37990
 
37741
37991
  // src/login-flow.ts
37742
37992
  init_api();
@@ -37775,7 +38025,7 @@ async function interactiveLogin() {
37775
38025
  return getAuth();
37776
38026
  }
37777
38027
 
37778
- // src/commands/claude.ts
38028
+ // src/commands/build.ts
37779
38029
  init_config();
37780
38030
 
37781
38031
  // src/prompts.ts
@@ -38053,7 +38303,7 @@ init_platform();
38053
38303
  init_api();
38054
38304
  import { hostname as hostname3, userInfo, platform as osPlatform2 } from "os";
38055
38305
  import { resolve as resolve13, dirname as dirname9 } from "path";
38056
- import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
38306
+ import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
38057
38307
 
38058
38308
  // src/relay/machine-id.ts
38059
38309
  import { createHash as createHash3 } from "crypto";
@@ -38128,10 +38378,6 @@ async function pairDevice(opts = {}) {
38128
38378
  };
38129
38379
  }
38130
38380
  if (existing && opts.force) {
38131
- try {
38132
- await post(`/remote-devices/${encodeURIComponent(existing.guid)}/revoke`, {});
38133
- } catch {
38134
- }
38135
38381
  clearDevice();
38136
38382
  }
38137
38383
  const name = (opts.name?.trim() || friendlyDeviceName()).trim();
@@ -38166,8 +38412,8 @@ function startDaemon() {
38166
38412
  function installAutostart(opts = {}) {
38167
38413
  const stdio = opts.stdio ?? "ignore";
38168
38414
  const plan2 = planFor({ cliPath: resolveCliPath() });
38169
- mkdirSync9(dirname9(plan2.path), { recursive: true });
38170
- writeFileSync9(plan2.path, plan2.content);
38415
+ mkdirSync10(dirname9(plan2.path), { recursive: true });
38416
+ writeFileSync10(plan2.path, plan2.content);
38171
38417
  let ok = true;
38172
38418
  for (const argv2 of plan2.enableCmds) {
38173
38419
  const r = spawnSyncCommand(argv2[0], argv2.slice(1), { stdio });
@@ -38223,20 +38469,20 @@ async function runRelaySetup(opts) {
38223
38469
  console.log(` ${dim("To register this computer again \u2014 for example under a different name \u2014")}`);
38224
38470
  console.log(` ${dim("unregister it first, then re-run setup:")}`);
38225
38471
  console.log(` ${brand("gipity relay revoke")} ${dim("# unpairs this computer and removes the login service")}`);
38226
- console.log(` ${brand("gipity setup")} ${dim("# register it again (asks for a new name)")}`);
38472
+ console.log(` ${brand("gipity connect")} ${dim("# register it again (asks for a new name)")}`);
38227
38473
  console.log("");
38228
38474
  }
38229
38475
  return true;
38230
38476
  }
38231
38477
  if (opts.mode === "run-now") {
38232
38478
  console.log(` ${bold("Set up this computer as a relay")}`);
38233
- console.log(` ${dim("A relay runs Claude Code (or Codex) here so you can drive it from the web (")}${brand("gipity.ai")}${dim(") on any browser.")}`);
38234
- console.log(` ${dim("It uses your Claude or Codex subscription \u2014 the cheapest way to pay for tokens.")}`);
38479
+ console.log(` ${dim("A relay runs your coding agent (Claude Code, Codex, or Grok) here so you can drive it from the web (")}${brand("gipity.ai")}${dim(") on any browser.")}`);
38480
+ console.log(` ${dim("It uses your Claude, Codex, or Grok subscription \u2014 the cheapest way to pay for tokens.")}`);
38235
38481
  } else {
38236
- console.log(` ${bold("Remote control of Claude Code")}`);
38237
- console.log(` ${dim("Drive this Claude Code from the web (")}${brand("gipity.ai")}${dim(") on any browser (desktop or phone).")}`);
38482
+ console.log(` ${bold("Remote control of your coding agent")}`);
38483
+ console.log(` ${dim("Drive your coding agent on this computer from the web (")}${brand("gipity.ai")}${dim(") on any browser (desktop or phone).")}`);
38238
38484
  console.log("");
38239
- console.log(` ${dim("Enable now (takes 2 seconds) or turn on later with")} ${brand("gipity setup")}`);
38485
+ console.log(` ${dim("Enable now (takes 2 seconds) or turn on later with")} ${brand("gipity connect")}`);
38240
38486
  }
38241
38487
  console.log("");
38242
38488
  const promptText = opts.mode === "run-now" ? " Set up remote control on this computer?" : " Enable remote control?";
@@ -38261,10 +38507,10 @@ async function runRelaySetup(opts) {
38261
38507
  } catch (err) {
38262
38508
  console.error(`
38263
38509
  ${error(`Could not create device: ${err?.message || err}`)}`);
38264
- console.error(` ${dim("Skipping for now - we'll offer again next time. Or turn it on with `gipity setup`.")}`);
38510
+ console.error(` ${dim("Skipping for now - we'll offer again next time. Or turn it on with `gipity connect`.")}`);
38265
38511
  return false;
38266
38512
  }
38267
- const startNow = await confirm(" Start the relay now (and on future `gipity claude` runs)?", { default: "yes" });
38513
+ const startNow = await confirm(" Start the relay now (and on future `gipity build` runs)?", { default: "yes" });
38268
38514
  if (startNow) {
38269
38515
  startDaemon();
38270
38516
  }
@@ -38275,7 +38521,7 @@ async function runRelaySetup(opts) {
38275
38521
  if (!res.ok) {
38276
38522
  if (isWsl()) {
38277
38523
  console.log(` ${muted("Auto-start needs systemd, which this WSL distro has off. The relay still runs -")}`);
38278
- console.log(` ${muted("it started just now and `gipity claude` restarts it. For boot-time auto-start:")}`);
38524
+ console.log(` ${muted("it started just now and `gipity build` restarts it. For boot-time auto-start:")}`);
38279
38525
  console.log(` ${muted("add [boot] systemd=true to /etc/wsl.conf, restart WSL, then run")} ${brand("gipity relay install")}${muted(".")}`);
38280
38526
  } else {
38281
38527
  console.log(` ${muted("Autostart install returned non-zero - you can run")} ${brand("gipity relay install")} ${muted("later.")}`);
@@ -38309,19 +38555,19 @@ async function maybeOfferRelayOn() {
38309
38555
  await runRelaySetup({ mode: "offer-once" });
38310
38556
  }
38311
38557
 
38312
- // src/commands/claude.ts
38558
+ // src/commands/build.ts
38313
38559
  init_utils();
38314
38560
  init_colors();
38315
38561
 
38316
38562
  // src/banner.ts
38317
38563
  init_colors();
38318
38564
  import { homedir as homedir9 } from "os";
38319
- import { sep as sep3 } from "path";
38565
+ import { sep as sep4 } from "path";
38320
38566
  function tildify(p) {
38321
38567
  const home = homedir9();
38322
38568
  if (!home) return p;
38323
38569
  if (p === home) return "~";
38324
- if (p.startsWith(home + sep3)) return "~" + p.slice(home.length);
38570
+ if (p.startsWith(home + sep4)) return "~" + p.slice(home.length);
38325
38571
  return p;
38326
38572
  }
38327
38573
  var AI_MODELS = [
@@ -38613,14 +38859,182 @@ function ensureClaudeInstalled(opts = {}) {
38613
38859
  return { installed: isClaudeInstalled(), alreadyPresent: false };
38614
38860
  }
38615
38861
 
38616
- // src/commands/claude.ts
38862
+ // src/agents/claude-code.ts
38863
+ var claudeCodeAdapter = {
38864
+ key: "claude",
38865
+ source: "claude_code",
38866
+ displayName: "Claude Code",
38867
+ providerName: "Anthropic",
38868
+ binary: "claude",
38869
+ buildInteractiveArgs({ resume, model }) {
38870
+ const args = [];
38871
+ if (model) args.push("--model", model);
38872
+ if (resume) args.push("--resume", resume);
38873
+ return args;
38874
+ },
38875
+ buildHeadlessArgs({ message, resume, model, bypassApprovals, jsonStream }) {
38876
+ const args = ["-p", message];
38877
+ if (bypassApprovals) args.push("--permission-mode", "bypassPermissions");
38878
+ if (model) args.push("--model", model);
38879
+ if (resume) args.push("--resume", resume);
38880
+ if (jsonStream) args.push("--output-format", "stream-json", "--verbose", "--include-partial-messages");
38881
+ return args;
38882
+ },
38883
+ sessionIdFromStreamEvent(event) {
38884
+ if (event && typeof event.session_id === "string" && event.session_id) return event.session_id;
38885
+ return null;
38886
+ },
38887
+ hooksSupportedOnPlatform() {
38888
+ return true;
38889
+ },
38890
+ // The daemon parses Claude's stream-json into ingest entries itself
38891
+ // (hook capture stands down via GIPITY_CAPTURE=off on dispatches).
38892
+ daemonStreamCapture: true,
38893
+ ensureInstalled() {
38894
+ return ensureClaudeInstalled().installed;
38895
+ },
38896
+ installHint: `npm install -g ${CLAUDE_PACKAGE}`
38897
+ };
38898
+
38899
+ // src/agents/codex.ts
38900
+ var codexAdapter = {
38901
+ key: "codex",
38902
+ source: "codex",
38903
+ displayName: "Codex",
38904
+ providerName: "OpenAI",
38905
+ binary: "codex",
38906
+ buildInteractiveArgs({ resume, model }) {
38907
+ const args = [];
38908
+ if (model) args.push("--model", model);
38909
+ if (resume) args.push("resume", resume);
38910
+ return args;
38911
+ },
38912
+ buildHeadlessArgs({ message, resume, model, bypassApprovals, jsonStream }) {
38913
+ const args = resume ? ["exec", "resume", resume, message] : ["exec", message];
38914
+ if (model) args.push("--model", model);
38915
+ if (bypassApprovals) {
38916
+ args.push(
38917
+ "-s",
38918
+ "workspace-write",
38919
+ "-c",
38920
+ "sandbox_workspace_write.network_access=true",
38921
+ "--dangerously-bypass-hook-trust",
38922
+ "--skip-git-repo-check"
38923
+ );
38924
+ }
38925
+ if (jsonStream) args.push("--json");
38926
+ return args;
38927
+ },
38928
+ sessionIdFromStreamEvent(event) {
38929
+ if (event?.type === "thread.started" && typeof event.thread_id === "string") {
38930
+ return event.thread_id;
38931
+ }
38932
+ return null;
38933
+ },
38934
+ hooksSupportedOnPlatform(platform2) {
38935
+ return platform2 !== "win32";
38936
+ },
38937
+ // No stream-json ingest mapper for Codex: relay dispatches keep hook
38938
+ // capture ON (GIPITY_CONVERSATION_GUID binds it) and the transcript
38939
+ // parser mirrors the session; the daemon tracks byte-level progress only.
38940
+ daemonStreamCapture: false,
38941
+ oneTimeSetupNote: "Codex runs project hooks only after a one-time approval: run /hooks inside Codex and approve the Gipity entries, or session recording stays off.",
38942
+ installHint: "npm install -g @openai/codex"
38943
+ };
38944
+
38945
+ // src/agents/grok.ts
38946
+ var grokAdapter = {
38947
+ key: "grok",
38948
+ source: "grok",
38949
+ displayName: "Grok",
38950
+ providerName: "xAI",
38951
+ binary: "grok",
38952
+ buildInteractiveArgs({ resume, model }) {
38953
+ const args = [];
38954
+ if (model) args.push("--model", model);
38955
+ if (resume) args.push("--resume", resume);
38956
+ return args;
38957
+ },
38958
+ buildHeadlessArgs({ message, resume, model, bypassApprovals, jsonStream }) {
38959
+ const args = ["-p", message];
38960
+ if (model) args.push("--model", model);
38961
+ if (resume) args.push("--resume", resume);
38962
+ if (bypassApprovals) args.push("--always-approve");
38963
+ if (jsonStream) args.push("--output-format", "streaming-json");
38964
+ return args;
38965
+ },
38966
+ sessionIdFromStreamEvent(event) {
38967
+ const fromParams = event?.params?.sessionId;
38968
+ if (typeof fromParams === "string" && fromParams) return fromParams;
38969
+ if (typeof event?.sessionId === "string" && event.sessionId) return event.sessionId;
38970
+ return null;
38971
+ },
38972
+ hooksSupportedOnPlatform() {
38973
+ return true;
38974
+ },
38975
+ daemonStreamCapture: false,
38976
+ // Verified live (2026-07-13): grok -p fires NO plugin hooks - not even
38977
+ // SessionStart/Stop - so headless capture is launcher-driven: pin the
38978
+ // session id, then replay ~/.grok/sessions/<cwd>/<sid>/chat_history.jsonl
38979
+ // through the capture runner after the run.
38980
+ headlessCapture: {
38981
+ sessionIdArgs: (sessionId) => ["--session-id", sessionId]
38982
+ },
38983
+ installHint: "curl -fsSL https://grok.x.ai/install.sh | sh (or see docs.x.ai/grok-build)"
38984
+ };
38985
+
38986
+ // src/agents/index.ts
38987
+ var AGENT_ADAPTERS = [
38988
+ claudeCodeAdapter,
38989
+ codexAdapter,
38990
+ grokAdapter
38991
+ ];
38992
+ var AGENT_KEYS = AGENT_ADAPTERS.map((a) => a.key);
38993
+ function getAdapter(key) {
38994
+ const found = AGENT_ADAPTERS.find((a) => a.key === key);
38995
+ if (!found) throw new Error(`Unknown agent "${key}". Valid: ${AGENT_KEYS.join(", ")}`);
38996
+ return found;
38997
+ }
38998
+ function getAdapterBySource(source) {
38999
+ const found = AGENT_ADAPTERS.find((a) => a.source === source);
39000
+ if (!found) throw new Error(`No agent adapter for source "${source}"`);
39001
+ return found;
39002
+ }
39003
+
39004
+ // src/prefs.ts
39005
+ import { existsSync as existsSync13, mkdirSync as mkdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync11 } from "fs";
39006
+ import { homedir as homedir11 } from "os";
39007
+ import { join as join16 } from "path";
39008
+ var PREFS_PATH = join16(homedir11(), ".gipity", "prefs.json");
39009
+ function readPrefs() {
39010
+ if (!existsSync13(PREFS_PATH)) return {};
39011
+ try {
39012
+ const parsed = JSON.parse(readFileSync16(PREFS_PATH, "utf-8"));
39013
+ return {
39014
+ lastAgent: typeof parsed.lastAgent === "string" ? parsed.lastAgent : void 0
39015
+ };
39016
+ } catch {
39017
+ return {};
39018
+ }
39019
+ }
39020
+ function writePrefs(update) {
39021
+ try {
39022
+ const current = readPrefs();
39023
+ const next = { ...current, ...update };
39024
+ mkdirSync11(join16(homedir11(), ".gipity"), { recursive: true });
39025
+ writeFileSync11(PREFS_PATH, JSON.stringify(next, null, 2) + "\n");
39026
+ } catch {
39027
+ }
39028
+ }
39029
+
39030
+ // src/commands/build.ts
38617
39031
  var __clDir = dirname10(fileURLToPath2(import.meta.url));
38618
39032
  function readOwnPkg(fromDir) {
38619
39033
  for (let d = fromDir; ; d = dirname10(d)) {
38620
- const p = join16(d, "package.json");
38621
- if (existsSync13(p)) {
39034
+ const p = join17(d, "package.json");
39035
+ if (existsSync14(p)) {
38622
39036
  try {
38623
- const pkg2 = JSON.parse(readFileSync16(p, "utf-8"));
39037
+ const pkg2 = JSON.parse(readFileSync17(p, "utf-8"));
38624
39038
  if (pkg2.name === "gipity") return pkg2;
38625
39039
  } catch {
38626
39040
  }
@@ -38669,12 +39083,12 @@ function localFsFallback(dir) {
38669
39083
  const topLevelEntries = [];
38670
39084
  const walk = (d, depth) => {
38671
39085
  try {
38672
- for (const name of readdirSync7(d).sort()) {
39086
+ for (const name of readdirSync8(d).sort()) {
38673
39087
  if (isSyncIgnored(name)) continue;
38674
39088
  let isDir = false;
38675
39089
  let size = 0;
38676
39090
  try {
38677
- const st2 = statSync7(join16(d, name));
39091
+ const st2 = statSync7(join17(d, name));
38678
39092
  isDir = st2.isDirectory();
38679
39093
  size = st2.isFile() ? st2.size : 0;
38680
39094
  } catch {
@@ -38683,7 +39097,7 @@ function localFsFallback(dir) {
38683
39097
  if (isDir) {
38684
39098
  folderCount++;
38685
39099
  if (depth === 0) topLevelEntries.push(`${name}/`);
38686
- walk(join16(d, name), depth + 1);
39100
+ walk(join17(d, name), depth + 1);
38687
39101
  } else {
38688
39102
  fileCount++;
38689
39103
  totalBytes += size;
@@ -38716,8 +39130,8 @@ function hasStreamJsonFlag(args) {
38716
39130
  }
38717
39131
  function markFolderTrusted(dir) {
38718
39132
  try {
38719
- const file = join16(homedir11(), ".claude.json");
38720
- const cfg = existsSync13(file) ? JSON.parse(readFileSync16(file, "utf-8")) : {};
39133
+ const file = join17(homedir12(), ".claude.json");
39134
+ const cfg = existsSync14(file) ? JSON.parse(readFileSync17(file, "utf-8")) : {};
38721
39135
  if (typeof cfg !== "object" || cfg === null) return;
38722
39136
  if (typeof cfg.projects !== "object" || cfg.projects === null) cfg.projects = {};
38723
39137
  const entry = typeof cfg.projects[dir] === "object" && cfg.projects[dir] !== null ? cfg.projects[dir] : {};
@@ -38725,7 +39139,7 @@ function markFolderTrusted(dir) {
38725
39139
  entry.hasTrustDialogAccepted = true;
38726
39140
  cfg.projects[dir] = entry;
38727
39141
  const tmp = `${file}.gipity-tmp`;
38728
- writeFileSync10(tmp, JSON.stringify(cfg, null, 2));
39142
+ writeFileSync12(tmp, JSON.stringify(cfg, null, 2));
38729
39143
  renameSync2(tmp, file);
38730
39144
  } catch {
38731
39145
  }
@@ -38747,10 +39161,22 @@ function suggestProjectName(existingSlugs) {
38747
39161
  }
38748
39162
  return `project-${Date.now().toString(36).slice(-6)}`;
38749
39163
  }
38750
- var claudeCommand = new Command("claude").description("Set up and run Claude Code").option("--setup-only", "Do the Gipity setup but skip launching the agent").option("--new-project", "Create a fresh Gipity project instead of using cwd or the picker").option("--name <name>", "Name for --new-project (default: project-NNN)").option("--project <slug>", "Open an existing project by slug or id").option("--here", "Use the current directory instead of ~/GipityProjects/<slug>/").option("--quiet", "Suppress Claude's live progress output (headless --new-project/--project runs)").option("--model <model>", "Model for the Claude session, forwarded to claude (e.g. sonnet, opus, or a full id like claude-sonnet-5)").allowUnknownOption(true).allowExcessArguments(true).action(async (opts) => {
39164
+ function createLaunchCommand(name, cfg = {}) {
39165
+ const cmd = new Command(name).description(cfg.presetAgent ? "Set up and run Claude Code (legacy alias of `gipity build`)" : "Start building from anywhere - pick a project, pick your coding agent, go").option("--setup-only", "Do the Gipity setup but skip launching the agent").option("--new-project", "Create a fresh Gipity project instead of using cwd or the picker").option("--name <name>", "Name for --new-project (default: project-NNN)").option("--project <slug>", "Open an existing project by slug or id").option("--here", "Use the current directory instead of ~/GipityProjects/<slug>/").option("--quiet", "Suppress the agent's live progress output (headless --new-project/--project runs)").option("--model <model>", "Model for the session, passed straight to the agent (e.g. opus) - omit it and the agent uses its own default").option("--bypass-approvals", "Skip the agent's interactive tool approvals (headless runs; the relay daemon sets this)").allowUnknownOption(true).allowExcessArguments(true);
39166
+ if (!cfg.presetAgent) {
39167
+ cmd.option("--agent <agent>", `Which coding agent to launch: ${AGENT_KEYS.join(", ")} (default: your last-used)`);
39168
+ }
39169
+ cmd.action(async (opts) => {
39170
+ await runLaunch(name, cfg, opts);
39171
+ });
39172
+ return cmd;
39173
+ }
39174
+ var buildCommand = createLaunchCommand("build");
39175
+ var claudeCommand = createLaunchCommand("claude", { presetAgent: "claude" });
39176
+ async function runLaunch(cmdName, cmdCfg, opts) {
38751
39177
  try {
38752
39178
  const runStart = Date.now();
38753
- const rawArgs = process.argv.slice(process.argv.indexOf("claude") + 1);
39179
+ const rawArgs = process.argv.slice(process.argv.indexOf(cmdName) + 1);
38754
39180
  if (rawArgs[0] === "install") {
38755
39181
  const r = ensureClaudeInstalled({ force: rawArgs.includes("--force") });
38756
39182
  if (r.alreadyPresent) console.log(` ${success("Claude Code already installed.")}`);
@@ -38791,7 +39217,7 @@ var claudeCommand = new Command("claude").description("Set up and run Claude Cod
38791
39217
  process.exit(1);
38792
39218
  }
38793
39219
  if (nonInteractive && !getConfig() && !opts.newProject && !opts.project) {
38794
- console.error(` ${error("No Gipity project in cwd. Run `gipity claude` (interactive) first, or pass --new-project / --project <slug>.")}`);
39220
+ console.error(` ${error("No Gipity project in cwd. Run `gipity build` (interactive) first, or pass --new-project / --project <slug>.")}`);
38795
39221
  process.exit(1);
38796
39222
  }
38797
39223
  if (!nonInteractive) {
@@ -38830,7 +39256,7 @@ var claudeCommand = new Command("claude").description("Set up and run Claude Cod
38830
39256
  let existing = getConfig();
38831
39257
  let forceAdoptCwd = false;
38832
39258
  if (existing && !opts.newProject && !opts.project && !nonInteractive) {
38833
- const cwdHasConfig = existsSync13(resolve14(process.cwd(), ".gipity.json"));
39259
+ const cwdHasConfig = existsSync14(resolve14(process.cwd(), ".gipity.json"));
38834
39260
  if (!cwdHasConfig && isLikelyEmpty(process.cwd())) {
38835
39261
  const ancestorRoot = dirname10(getConfigPath());
38836
39262
  console.log(` ${bold("You are inside an existing Gipity project.")}
@@ -38879,8 +39305,8 @@ var claudeCommand = new Command("claude").description("Set up and run Claude Cod
38879
39305
  }
38880
39306
  project = found;
38881
39307
  }
38882
- const projectDir2 = opts.here ? process.cwd() : join16(getProjectsRoot(), project.slug);
38883
- mkdirSync10(projectDir2, { recursive: true });
39308
+ const projectDir2 = opts.here ? process.cwd() : join17(getProjectsRoot(), project.slug);
39309
+ mkdirSync12(projectDir2, { recursive: true });
38884
39310
  process.chdir(projectDir2);
38885
39311
  clearConfigCache();
38886
39312
  let agentGuid = "";
@@ -38907,10 +39333,7 @@ var claudeCommand = new Command("claude").description("Set up and run Claude Cod
38907
39333
  } catch (err) {
38908
39334
  console.log(` ${warning("Could not sync files (will retry on next prompt):")} ${err.message}`);
38909
39335
  }
38910
- setupClaudeHooks();
38911
- setupClaudeMd();
38912
- setupAgentsMd();
38913
- setupGitignore();
39336
+ setupProjectTools();
38914
39337
  if (nonInteractive) {
38915
39338
  headlessNewProject = isNewProject;
38916
39339
  } else {
@@ -38922,10 +39345,7 @@ var claudeCommand = new Command("claude").description("Set up and run Claude Cod
38922
39345
  console.log(` Project: ${brand(existing.projectSlug)} ${muted(`(${existing.projectGuid})`)}`);
38923
39346
  console.log(` ${success("Already set up.")}
38924
39347
  `);
38925
- setupClaudeHooks();
38926
- setupClaudeMd();
38927
- setupAgentsMd();
38928
- setupGitignore();
39348
+ setupProjectTools();
38929
39349
  let doSync = true;
38930
39350
  const projectRoot = dirname10(getConfigPath());
38931
39351
  const scan = scanForAdoption(projectRoot);
@@ -39008,8 +39428,8 @@ var claudeCommand = new Command("claude").description("Set up and run Claude Cod
39008
39428
  } else {
39009
39429
  project = result.project;
39010
39430
  isNewProject = result.kind === "create-new";
39011
- const projectDir2 = join16(getProjectsRoot(), project.slug);
39012
- mkdirSync10(projectDir2, { recursive: true });
39431
+ const projectDir2 = join17(getProjectsRoot(), project.slug);
39432
+ mkdirSync12(projectDir2, { recursive: true });
39013
39433
  process.chdir(projectDir2);
39014
39434
  clearConfigCache();
39015
39435
  try {
@@ -39037,15 +39457,40 @@ var claudeCommand = new Command("claude").description("Set up and run Claude Cod
39037
39457
  }
39038
39458
  }
39039
39459
  initialPrompt = "";
39040
- setupClaudeHooks();
39041
- setupClaudeMd();
39042
- setupAgentsMd();
39043
- setupGitignore();
39460
+ setupProjectTools();
39044
39461
  console.log(` ${success(`Project "${project.name}" ready.`)}
39045
39462
  `);
39046
39463
  }
39047
39464
  if (opts.setupOnly) {
39048
- console.log(` Done. cd ${process.cwd()} && gipity claude`);
39465
+ console.log(` Done. cd ${process.cwd()} && gipity ${cmdName}`);
39466
+ return;
39467
+ }
39468
+ const prefs = readPrefs();
39469
+ let agentKey = cmdCfg.presetAgent ?? (typeof opts.agent === "string" ? opts.agent.toLowerCase() : void 0);
39470
+ if (agentKey && !AGENT_KEYS.includes(agentKey)) {
39471
+ console.error(` ${error(`Unknown agent "${agentKey}".`)} ${muted(`Valid: ${AGENT_KEYS.join(", ")}`)}`);
39472
+ process.exit(1);
39473
+ }
39474
+ if (!agentKey) {
39475
+ if (nonInteractive) {
39476
+ agentKey = prefs.lastAgent && AGENT_KEYS.includes(prefs.lastAgent) ? prefs.lastAgent : "claude";
39477
+ } else {
39478
+ agentKey = await pickAgent(prefs.lastAgent);
39479
+ }
39480
+ }
39481
+ const adapter = getAdapter(agentKey);
39482
+ if (!nonInteractive && !cmdCfg.presetAgent) {
39483
+ writePrefs({ lastAgent: adapter.key });
39484
+ }
39485
+ if (adapter.key !== "claude") {
39486
+ await launchNonClaudeAgent(adapter, {
39487
+ cmdName,
39488
+ opts,
39489
+ rawArgs,
39490
+ nonInteractive,
39491
+ headlessOut,
39492
+ runStart
39493
+ });
39049
39494
  return;
39050
39495
  }
39051
39496
  if (!isClaudeInstalled()) {
@@ -39071,7 +39516,7 @@ var claudeCommand = new Command("claude").description("Set up and run Claude Cod
39071
39516
  if (resumeSid) {
39072
39517
  try {
39073
39518
  const found = await get(
39074
- `/conversations/claude-code/by-session/${encodeURIComponent(resumeSid)}`
39519
+ `/conversations/remote/by-session/${encodeURIComponent(resumeSid)}?source=claude_code`
39075
39520
  );
39076
39521
  convGuidForHooks = found.data.conversation_guid;
39077
39522
  } catch {
@@ -39079,8 +39524,8 @@ var claudeCommand = new Command("claude").description("Set up and run Claude Cod
39079
39524
  }
39080
39525
  if (!convGuidForHooks && cfg?.projectGuid) {
39081
39526
  const created = await post(
39082
- "/conversations/claude-code",
39083
- { project_guid: cfg.projectGuid, device_guid: device.guid, origin: "local" }
39527
+ "/conversations/remote",
39528
+ { project_guid: cfg.projectGuid, device_guid: device.guid, source: "claude_code", origin: "local" }
39084
39529
  );
39085
39530
  convGuidForHooks = created.data.conversation_guid;
39086
39531
  }
@@ -39091,10 +39536,10 @@ var claudeCommand = new Command("claude").description("Set up and run Claude Cod
39091
39536
  }
39092
39537
  }
39093
39538
  }
39094
- const claudeIdx = process.argv.indexOf("claude");
39095
- const rawClaudeArgs = process.argv.slice(claudeIdx + 1);
39096
- const gipityBooleanFlags = /* @__PURE__ */ new Set(["--setup-only", "--new-project", "--here", "--quiet"]);
39097
- const gipityValueFlags = ["--api-base", "--name", "--project"];
39539
+ const cmdIdx = process.argv.indexOf(cmdName);
39540
+ const rawClaudeArgs = process.argv.slice(cmdIdx + 1);
39541
+ const gipityBooleanFlags = /* @__PURE__ */ new Set(["--setup-only", "--new-project", "--here", "--quiet", "--bypass-approvals"]);
39542
+ const gipityValueFlags = ["--api-base", "--name", "--project", "--agent"];
39098
39543
  const claudeArgs = [];
39099
39544
  for (let i = 0; i < rawClaudeArgs.length; i++) {
39100
39545
  const arg = rawClaudeArgs[i];
@@ -39110,6 +39555,9 @@ var claudeCommand = new Command("claude").description("Set up and run Claude Cod
39110
39555
  console.log(` ${bold("Launching Claude Code, powered by Gipity.")}`);
39111
39556
  console.log(` ${muted("Just tell Claude what you'd like to build or do - everything Claude can do,")}`);
39112
39557
  console.log(` ${muted("plus hosting, databases, and live deploys on Gipity.")}`);
39558
+ if (convGuidForHooks) {
39559
+ console.log(` ${muted(`This session is saved to Gipity - watch it live at ${brand("prompt.gipity.ai")}. (--no-capture on init to disable.)`)}`);
39560
+ }
39113
39561
  console.log("");
39114
39562
  }
39115
39563
  let allArgs;
@@ -39186,8 +39634,11 @@ Sending to Claude Code: ${userMsg}
39186
39634
  ${doneLine}
39187
39635
 
39188
39636
  `);
39189
- else console.log(`
39637
+ else {
39638
+ console.log(`
39190
39639
  ${doneLine}`);
39640
+ console.log(` ${muted(`Next time: gipity build from anywhere, or cd ${process.cwd()} && claude.`)}`);
39641
+ }
39191
39642
  process.exit(code ?? 0);
39192
39643
  });
39193
39644
  } catch (err) {
@@ -39195,7 +39646,186 @@ ${doneLine}
39195
39646
  ${error(`Error: ${err.message}`)}`);
39196
39647
  process.exit(1);
39197
39648
  }
39198
- });
39649
+ }
39650
+ async function pickAgent(lastUsed) {
39651
+ const defaultKey = lastUsed && AGENT_KEYS.includes(lastUsed) ? lastUsed : "claude";
39652
+ const defaultIdx = AGENT_ADAPTERS.findIndex((a) => a.key === defaultKey) + 1;
39653
+ console.log(` ${bold("Which coding agent?")}
39654
+ `);
39655
+ AGENT_ADAPTERS.forEach((a, i) => {
39656
+ const installed = binaryOnPath(a.binary) || a.key === "claude" && isClaudeInstalled();
39657
+ const notes = [];
39658
+ if (a.key === lastUsed) notes.push("last used");
39659
+ if (!installed) notes.push(a.ensureInstalled ? "not installed - we'll install it" : "not installed");
39660
+ if (a.key === "codex" && !a.hooksSupportedOnPlatform(process.platform)) {
39661
+ notes.push("session recording unavailable on Windows");
39662
+ }
39663
+ const note = notes.length ? ` ${muted(`(${notes.join(", ")})`)}` : "";
39664
+ console.log(` ${bold(`${i + 1}.`)} ${a.displayName} ${muted(`(${a.providerName})`)}${note}`);
39665
+ });
39666
+ console.log("");
39667
+ const choice = await pickOne("Choose", AGENT_ADAPTERS.length, defaultIdx);
39668
+ console.log("");
39669
+ return AGENT_ADAPTERS[choice - 1].key;
39670
+ }
39671
+ async function launchNonClaudeAgent(adapter, ctx) {
39672
+ const { opts, rawArgs, nonInteractive, headlessOut, runStart } = ctx;
39673
+ if (!binaryOnPath(adapter.binary)) {
39674
+ let installed = false;
39675
+ if (adapter.ensureInstalled) {
39676
+ console.log(` ${adapter.displayName} not found - installing it now...`);
39677
+ installed = adapter.ensureInstalled();
39678
+ }
39679
+ if (!installed && !binaryOnPath(adapter.binary)) {
39680
+ console.error(` ${error(`${adapter.displayName} is not installed.`)} Install it with: ${adapter.installHint}`);
39681
+ console.error(` ${muted(`Then run: gipity ${ctx.cmdName} --agent ${adapter.key}`)}`);
39682
+ process.exit(1);
39683
+ }
39684
+ }
39685
+ const booleanFlags = /* @__PURE__ */ new Set(["--setup-only", "--new-project", "--here", "--quiet", "--bypass-approvals"]);
39686
+ const valueFlags = ["--api-base", "--name", "--project", "--agent"];
39687
+ let message = null;
39688
+ let resume;
39689
+ let model;
39690
+ const extras = [];
39691
+ for (let i = 0; i < rawArgs.length; i++) {
39692
+ const arg = rawArgs[i];
39693
+ if (booleanFlags.has(arg)) continue;
39694
+ if (valueFlags.includes(arg)) {
39695
+ i++;
39696
+ continue;
39697
+ }
39698
+ if (valueFlags.some((f) => arg.startsWith(`${f}=`))) continue;
39699
+ if (arg === "-p" || arg === "--print") {
39700
+ message = rawArgs[++i] ?? "";
39701
+ continue;
39702
+ }
39703
+ if (arg.startsWith("-p=") || arg.startsWith("--print=")) {
39704
+ message = arg.slice(arg.indexOf("=") + 1);
39705
+ continue;
39706
+ }
39707
+ if (arg === "--model") {
39708
+ model = rawArgs[++i];
39709
+ continue;
39710
+ }
39711
+ if (arg.startsWith("--model=")) {
39712
+ model = arg.slice("--model=".length);
39713
+ continue;
39714
+ }
39715
+ if (arg === "--resume") {
39716
+ resume = rawArgs[++i];
39717
+ continue;
39718
+ }
39719
+ if (arg.startsWith("--resume=")) {
39720
+ resume = arg.slice("--resume=".length);
39721
+ continue;
39722
+ }
39723
+ extras.push(arg);
39724
+ }
39725
+ let convGuid = process.env.GIPITY_CONVERSATION_GUID ?? null;
39726
+ if (!convGuid) {
39727
+ const device = getDevice();
39728
+ const config = getConfig();
39729
+ if (device && config?.projectGuid) {
39730
+ try {
39731
+ if (resume) {
39732
+ try {
39733
+ const found = await get(
39734
+ `/conversations/remote/by-session/${encodeURIComponent(resume)}?source=${encodeURIComponent(adapter.source)}`
39735
+ );
39736
+ convGuid = found.data.conversation_guid;
39737
+ } catch {
39738
+ }
39739
+ }
39740
+ if (!convGuid) {
39741
+ const created = await post(
39742
+ "/conversations/remote",
39743
+ { project_guid: config.projectGuid, device_guid: device.guid, source: adapter.source, origin: "local" }
39744
+ );
39745
+ convGuid = created.data.conversation_guid;
39746
+ }
39747
+ } catch (err) {
39748
+ if (!nonInteractive) {
39749
+ console.error(` ${error(`Could not create Gipity conversation: ${err?.message || err}`)}`);
39750
+ }
39751
+ }
39752
+ }
39753
+ }
39754
+ let agentArgs;
39755
+ let syntheticSid = null;
39756
+ if (nonInteractive) {
39757
+ const bypass = Boolean(opts.bypassApprovals || opts.newProject || opts.project);
39758
+ agentArgs = [
39759
+ ...adapter.buildHeadlessArgs({ message: message ?? "", resume, model, bypassApprovals: bypass, jsonStream: false }),
39760
+ ...extras
39761
+ ];
39762
+ if (adapter.headlessCapture) {
39763
+ syntheticSid = resume ?? randomUUID();
39764
+ if (!resume) agentArgs.push(...adapter.headlessCapture.sessionIdArgs(syntheticSid));
39765
+ }
39766
+ if (message) headlessOut(`
39767
+ Sending to ${adapter.displayName}: ${message}
39768
+ `);
39769
+ } else {
39770
+ agentArgs = [...adapter.buildInteractiveArgs({ resume, model }), ...extras];
39771
+ console.log(` ${bold(`Launching ${adapter.displayName}, powered by Gipity.`)}`);
39772
+ console.log(` ${muted(`Just tell ${adapter.displayName} what you'd like to build or do - plus hosting,`)}`);
39773
+ console.log(` ${muted("databases, and live deploys on Gipity.")}`);
39774
+ if (convGuid) {
39775
+ console.log(` ${muted(`This session is saved to Gipity - watch it live at ${brand("prompt.gipity.ai")}. (--no-capture on init to disable.)`)}`);
39776
+ }
39777
+ if (adapter.oneTimeSetupNote) {
39778
+ console.log(` ${warning(adapter.oneTimeSetupNote)}`);
39779
+ }
39780
+ console.log("");
39781
+ }
39782
+ const childEnv2 = { ...process.env };
39783
+ if (convGuid) childEnv2.GIPITY_CONVERSATION_GUID = convGuid;
39784
+ const agentCmd = resolveCommand(adapter.binary);
39785
+ const child = spawnCommand(agentCmd, agentArgs, {
39786
+ stdio: "inherit",
39787
+ cwd: process.cwd(),
39788
+ env: childEnv2
39789
+ });
39790
+ child.on("error", (err) => {
39791
+ console.error(`
39792
+ ${error(err.code === "ENOENT" ? `${adapter.displayName} not found. Install it: ${adapter.installHint}` : `Failed to launch ${adapter.displayName}: ${err.message}`)}`);
39793
+ process.exit(1);
39794
+ });
39795
+ child.on("exit", (code) => {
39796
+ if (syntheticSid && convGuid) {
39797
+ flushHeadlessCapture(adapter, syntheticSid, convGuid);
39798
+ }
39799
+ const doneLine = `Done (${formatElapsed2(Date.now() - runStart)})`;
39800
+ if (nonInteractive) process.stderr.write(`
39801
+ ${doneLine}
39802
+
39803
+ `);
39804
+ else {
39805
+ console.log(`
39806
+ ${doneLine}`);
39807
+ console.log(` ${muted(`Next time: gipity build from anywhere, or cd ${process.cwd()} && ${adapter.binary}.`)}`);
39808
+ }
39809
+ process.exit(code ?? 0);
39810
+ });
39811
+ }
39812
+ function flushHeadlessCapture(adapter, sessionId, convGuid) {
39813
+ const runner = join17(__clDir, "..", "hooks", "capture-runner.js");
39814
+ if (!existsSync14(runner)) return;
39815
+ const payload = JSON.stringify({ session_id: sessionId, cwd: process.cwd() });
39816
+ const env = { ...process.env, GIPITY_CONVERSATION_GUID: convGuid };
39817
+ for (const event of ["session-start", "stop"]) {
39818
+ try {
39819
+ spawnSyncCommand(process.execPath, [runner, adapter.key, event], {
39820
+ input: payload,
39821
+ env,
39822
+ timeout: 6e4,
39823
+ stdio: ["pipe", "ignore", "ignore"]
39824
+ });
39825
+ } catch {
39826
+ }
39827
+ }
39828
+ }
39199
39829
  async function pickOrCreateProject(projects, existingSlugs) {
39200
39830
  const filtered = projects.filter((p) => !p.is_default);
39201
39831
  const cwd = process.cwd();
@@ -39256,9 +39886,9 @@ async function pickFromAll(filtered) {
39256
39886
  function formatNewProjectLabel(existingSlugs) {
39257
39887
  const slug = suggestProjectName(existingSlugs);
39258
39888
  const root = getProjectsRoot();
39259
- const home = homedir11();
39260
- const display2 = root === home || root.startsWith(home + sep4) ? "~" + root.slice(home.length) : root;
39261
- return `${display2}${sep4}${slug}`;
39889
+ const home = homedir12();
39890
+ const display2 = root === home || root.startsWith(home + sep5) ? "~" + root.slice(home.length) : root;
39891
+ return `${display2}${sep5}${slug}`;
39262
39892
  }
39263
39893
  async function confirmAdoptCwd(cwd) {
39264
39894
  process.stdout.write(` ${muted("Scanning directory...")} `);
@@ -39335,18 +39965,23 @@ import os from "os";
39335
39965
  init_api();
39336
39966
  init_config();
39337
39967
  init_colors();
39968
+
39969
+ // src/catalog.ts
39338
39970
  var STARTERS = [
39339
39971
  { key: "web-vision-cam", hint: "fullscreen camera app with on-device vision (MediaPipe)" },
39340
39972
  { key: "object-spotter", hint: "camera app that boxes, labels, and counts objects (YOLOX on-device)" },
39341
39973
  { key: "2d-game", hint: "2D games with Phaser 3 - platformer, arcade, puzzle" },
39342
39974
  { key: "3d-world", hint: "playable 3D multiplayer rocket-launcher demo" },
39343
- { key: "karaoke-captions", hint: "audio + lyrics -> word-synced karaoke captions (GPU job)" }
39975
+ { key: "karaoke-captions", hint: "audio + lyrics -> word-synced karaoke captions (GPU job)" },
39976
+ { key: "outreach-agent", hint: "AI-run outreach funnel - import contacts, draft + auto-send staged emails" },
39977
+ { key: "paid-app", hint: "storefront that charges real money - Stripe checkout, members area, billing" },
39978
+ { key: "notify-demo", hint: "web-push demo - enable notifications, send a real ping" }
39344
39979
  ];
39345
39980
  var BLANK = [
39346
39981
  { key: "web-simple", hint: "static frontend-only site - pages, dashboards, simple games" },
39347
39982
  { key: "web-fullstack", hint: "backend API + database wiring - frontend, functions, migrations; deploys green" },
39348
- { key: "api", hint: "pure API backend, no frontend - one example function + test" },
39349
- { key: "3d-engine", hint: "3D multiplayer wiring - Three.js + Rapier + Gipity Realtime" }
39983
+ { key: "3d-engine", hint: "3D multiplayer wiring - Three.js + Rapier + Gipity Realtime" },
39984
+ { key: "api", hint: "pure API backend, no frontend - one example function + test" }
39350
39985
  ];
39351
39986
  var KITS = [
39352
39987
  { key: "realtime", hint: "multiplayer / presence / shared state" },
@@ -39354,8 +39989,16 @@ var KITS = [
39354
39989
  { key: "web-vision-detect", hint: "browser object detection - YOLOX, WebGPU/WASM, custom models" },
39355
39990
  { key: "chatbot", hint: "drop-in chatbot - persona, guardrails, streaming responses" },
39356
39991
  { key: "audio-align", hint: "audio + lyrics -> word-level timing JSON (GPU job)" },
39357
- { key: "i18n", hint: "multi-language web apps - language picker, RTL, translations" }
39992
+ { key: "i18n", hint: "multi-language web apps - language picker, RTL, translations" },
39993
+ { key: "records", hint: "registry-driven data plane - generic CRUD, validation, search, audit spine" },
39994
+ { key: "views", hint: "registry-driven UI over records - table, forms, kanban" },
39995
+ { key: "agent-api", hint: "named API keys for agent/script writes through the records kit" },
39996
+ { key: "contacts", hint: "multi-source contact layer - LinkedIn/Gmail import, dedupe with provenance" },
39997
+ { key: "stripe", hint: "charge your users - Stripe checkout, subscriptions, brokered webhooks" },
39998
+ { key: "notify", hint: "web push notifications - platform-owned keys, works on iOS home screen" }
39358
39999
  ];
40000
+
40001
+ // src/commands/add.ts
39359
40002
  function catalogText() {
39360
40003
  const width = Math.max(...[...STARTERS, ...BLANK, ...KITS].map((e) => e.key.length));
39361
40004
  const row2 = (e) => ` ${e.key.padEnd(width)} ${muted(e.hint)}`;
@@ -39809,7 +40452,19 @@ init_api();
39809
40452
  init_config();
39810
40453
  init_colors();
39811
40454
  var logsCommand = new Command("logs").description("View logs");
39812
- logsCommand.command("fn <name>").description("Show function logs").option("--limit <n>", "Max entries", "20").option("--json", "Output as JSON").action((name, opts) => run("Logs", async () => {
40455
+ logsCommand.command("fn <name>").description("Show function logs").option("--limit <n>", "Max entries", "20").option("--json", "Output as JSON (top-level ARRAY of invocations, newest first)").addHelpText("after", `
40456
+ Each line: time, status (ok / error / limit_exceeded), duration, trigger, and
40457
+ svc:N when the invocation made N platform service calls (notify, email, LLM,
40458
+ image, ...). Captured console output is indented beneath its invocation.
40459
+
40460
+ --json emits a top-level ARRAY (not an object) of invocation rows:
40461
+ [{ id, status, duration_ms, trigger_type, error_message,
40462
+ limits_consumed: { max_queries, max_service_calls, ... }, logs: [...], created_at }]
40463
+
40464
+ To confirm an individual service call succeeded (and see provider, latency,
40465
+ and its own error message), use the per-call log instead:
40466
+ $ gipity logs app --type services
40467
+ `).action((name, opts) => run("Logs", async () => {
39813
40468
  const config = requireConfig();
39814
40469
  const limit = parseInt(opts.limit, 10) || 20;
39815
40470
  const res = await get(
@@ -39830,8 +40485,10 @@ logsCommand.command("fn <name>").description("Show function logs").option("--lim
39830
40485
  const statusColor = log3.status === "ok" ? success : log3.status === "error" ? error : warning;
39831
40486
  const status = statusColor(log3.status.padEnd(8));
39832
40487
  const trigger = muted(log3.trigger_type.padEnd(8));
40488
+ const svcCalls = log3.limits_consumed?.max_service_calls;
40489
+ const svc = svcCalls ? ` ${muted(`svc:${svcCalls}`)}` : "";
39833
40490
  const err = log3.error_message ? ` ${error(`"${log3.error_message}"`)}` : "";
39834
- console.log(`${muted(time)} ${status} ${dur} ${trigger}${err}`);
40491
+ console.log(`${muted(time)} ${status} ${dur} ${trigger}${svc}${err}`);
39835
40492
  for (const line of log3.logs ?? []) {
39836
40493
  const lvlColor = line.level === "error" ? error : line.level === "warn" ? warning : muted;
39837
40494
  const tag = line.level === "log" ? "" : `${line.level}: `;
@@ -40384,6 +41041,7 @@ recordsCommand.command("delete <table> <id>").description("Delete a record").act
40384
41041
 
40385
41042
  // src/commands/fn.ts
40386
41043
  init_api();
41044
+ init_auth();
40387
41045
  init_config();
40388
41046
  init_colors();
40389
41047
  init_utils();
@@ -40396,6 +41054,9 @@ fnCommand.command("list").description("List functions").option("--json", "Output
40396
41054
  return f.description ? `${line}
40397
41055
  ${muted(f.description)}` : line;
40398
41056
  });
41057
+ if (!opts.json && res.data.length > 0) {
41058
+ console.log(muted(`Endpoint: POST ${config.apiBase}/api/${config.projectGuid}/fn/<name> (same on dev and prod; public path: gipity fn call <name> --anon)`));
41059
+ }
40399
41060
  }));
40400
41061
  fnCommand.command("logs <name>").description("Show recent logs").option("--limit <n>", "Max entries", "20").option("--json", "Output as JSON").action((name, opts) => run("Logs", async () => {
40401
41062
  const config = requireConfig();
@@ -40405,8 +41066,10 @@ fnCommand.command("logs <name>").description("Show recent logs").option("--limit
40405
41066
  printList(res.data, opts, "No execution logs.", (log3) => {
40406
41067
  const dur = log3.duration_ms != null ? `${log3.duration_ms}ms` : "?";
40407
41068
  const ts2 = new Date(log3.created_at).toLocaleString();
40408
- const statusColor = log3.status === "success" ? success : log3.status === "error" ? error : muted;
41069
+ const statusColor = log3.status === "ok" ? success : log3.status === "error" ? error : muted;
40409
41070
  let line = `${statusColor(log3.status)} ${dur} ${muted(log3.trigger_type || "http")} ${muted(ts2)}`;
41071
+ const svcCalls = log3.limits_consumed?.max_service_calls;
41072
+ if (svcCalls) line += ` ${muted(`svc:${svcCalls}`)}`;
40410
41073
  if (log3.error_message) line += `
40411
41074
  ${error(`error: ${log3.error_message}`)}`;
40412
41075
  for (const entry of log3.logs ?? []) {
@@ -40436,6 +41099,12 @@ fnCommand.command("call <name> [body]").description("Call a function").option("-
40436
41099
  const raw = bodyArg || opts.data || "{}";
40437
41100
  const body = JSON.parse(raw);
40438
41101
  const path5 = `/api/${config.projectGuid}/fn/${encodeURIComponent(name)}`;
41102
+ if (opts.anon) {
41103
+ console.error(muted("Auth: anonymous visitor (the public path a signed-out user hits)"));
41104
+ } else {
41105
+ const who = getAuth()?.email;
41106
+ console.error(muted(`Auth: calling as ${who ?? "your signed-in account"} (the owner persona; use --anon for the public visitor path)`));
41107
+ }
40439
41108
  const res = opts.anon ? await callAnon(config.projectGuid, name, body) : await post(path5, body);
40440
41109
  if (opts.field) {
40441
41110
  emitField(res.data, opts.field);
@@ -40919,11 +41588,11 @@ jobCommand.command("cancel <runGuid>").description("Cancel a queued or running j
40919
41588
  }
40920
41589
  }));
40921
41590
  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) => {
40922
- const { existsSync: existsSync23, statSync: statSync10 } = await import("node:fs");
40923
- const { resolve: resolve19, join: join25 } = await import("node:path");
41591
+ const { existsSync: existsSync24, statSync: statSync10 } = await import("node:fs");
41592
+ const { resolve: resolve20, join: join26 } = await import("node:path");
40924
41593
  const { spawnCommand: spawnCommand2, spawnSyncCommand: spawnSyncCommand2 } = await Promise.resolve().then(() => (init_platform(), platform_exports));
40925
- const jobDir = resolve19(process.cwd(), "jobs", name);
40926
- if (!existsSync23(jobDir) || !statSync10(jobDir).isDirectory()) {
41594
+ const jobDir = resolve20(process.cwd(), "jobs", name);
41595
+ if (!existsSync24(jobDir) || !statSync10(jobDir).isDirectory()) {
40927
41596
  console.error(error(`jobs/${name}/ not found in current directory`));
40928
41597
  process.exit(1);
40929
41598
  }
@@ -40944,7 +41613,7 @@ jobCommand.command("run-local <name>").description("Run the job in a local Docke
40944
41613
  },
40945
41614
  { file: "main.sh", runtime: "bash", entry: "bash" }
40946
41615
  ];
40947
- const picked = variants.find((v7) => existsSync23(join25(jobDir, v7.file)));
41616
+ const picked = variants.find((v7) => existsSync24(join26(jobDir, v7.file)));
40948
41617
  if (!picked) {
40949
41618
  console.error(error(`No main.py / main.js / main.sh found under jobs/${name}/`));
40950
41619
  process.exit(1);
@@ -40969,7 +41638,7 @@ jobCommand.command("run-local <name>").description("Run the job in a local Docke
40969
41638
  "-v",
40970
41639
  `${jobDir}:/work`
40971
41640
  ];
40972
- const needsDeps = opts.deps !== false && picked.depsFile && picked.installCmd && existsSync23(join25(jobDir, picked.depsFile));
41641
+ const needsDeps = opts.deps !== false && picked.depsFile && picked.installCmd && existsSync24(join26(jobDir, picked.depsFile));
40973
41642
  const handlerCmd = `${picked.entry} /work/${picked.file}`;
40974
41643
  let shellCmd;
40975
41644
  if (needsDeps && picked.installCmd) {
@@ -41144,7 +41813,7 @@ emailCommand.command("log").description("Recent email() sends from your app").op
41144
41813
  init_api();
41145
41814
  init_config();
41146
41815
  init_colors();
41147
- import { mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "fs";
41816
+ import { mkdirSync as mkdirSync13, writeFileSync as writeFileSync13 } from "fs";
41148
41817
  import { resolve as resolvePath2, dirname as dirname11, relative as relative5, isAbsolute } from "path";
41149
41818
 
41150
41819
  // src/provider-docs.ts
@@ -41165,7 +41834,7 @@ async function downloadFile(url, filename) {
41165
41834
  const buffer = Buffer.from(await res.arrayBuffer());
41166
41835
  const outPath = correctExtension(filename, buffer);
41167
41836
  ensureOutputDir(outPath);
41168
- writeFileSync11(outPath, buffer);
41837
+ writeFileSync13(outPath, buffer);
41169
41838
  const savedPath = resolvePath2(outPath);
41170
41839
  await pushGenerated(savedPath);
41171
41840
  return savedPath;
@@ -41201,7 +41870,7 @@ function correctExtension(filename, buf) {
41201
41870
  function ensureOutputDir(output) {
41202
41871
  const dir = dirname11(resolvePath2(output));
41203
41872
  try {
41204
- mkdirSync11(dir, { recursive: true });
41873
+ mkdirSync13(dir, { recursive: true });
41205
41874
  } catch (err) {
41206
41875
  throw new Error(`can't create the output directory ${dir} (${err.code || err.message}). Pick a different -o path.`);
41207
41876
  }
@@ -41434,14 +42103,14 @@ var generateCommand = new Command("generate").description("Generate images, vide
41434
42103
  init_api();
41435
42104
  init_config();
41436
42105
  init_colors();
41437
- import { existsSync as existsSync14, readFileSync as readFileSync17 } from "fs";
41438
- import { join as join17 } from "path";
42106
+ import { existsSync as existsSync15, readFileSync as readFileSync18 } from "fs";
42107
+ import { join as join18 } from "path";
41439
42108
  function readInstalledKitReadme(name) {
41440
42109
  const root = getProjectRoot() ?? process.cwd();
41441
- const kitDir = join17(root, "src", "packages", name);
41442
- if (!existsSync14(join17(kitDir, "package.json"))) return null;
41443
- const readme = join17(kitDir, "README.md");
41444
- return existsSync14(readme) ? readFileSync17(readme, "utf-8") : null;
42110
+ const kitDir = join18(root, "src", "packages", name);
42111
+ if (!existsSync15(join18(kitDir, "package.json"))) return null;
42112
+ const readme = join18(kitDir, "README.md");
42113
+ return existsSync15(readme) ? readFileSync18(readme, "utf-8") : null;
41445
42114
  }
41446
42115
  var skillCommand = new Command("skill").description("Task docs - read the matching skill before building");
41447
42116
  skillCommand.command("list").description("List skills").option("--json", "Output as JSON").action((opts) => run("List", async () => {
@@ -41614,7 +42283,7 @@ var domainCommand = new Command("domain").description("Manage custom domains").a
41614
42283
  }));
41615
42284
 
41616
42285
  // src/commands/realtime.ts
41617
- import { existsSync as existsSync15 } from "fs";
42286
+ import { existsSync as existsSync16 } from "fs";
41618
42287
  import { dirname as dirname12, resolve as resolve15 } from "path";
41619
42288
  init_api();
41620
42289
  init_config();
@@ -41622,7 +42291,7 @@ init_colors();
41622
42291
  function hasDeployManifest() {
41623
42292
  const cfgPath = getConfigPath();
41624
42293
  if (!cfgPath) return false;
41625
- return existsSync15(resolve15(dirname12(cfgPath), "gipity.yaml"));
42294
+ return existsSync16(resolve15(dirname12(cfgPath), "gipity.yaml"));
41626
42295
  }
41627
42296
  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 () => {
41628
42297
  const config = requireConfig();
@@ -41798,7 +42467,7 @@ async function pollTestStatus(projectGuid, runGuid, opts) {
41798
42467
  }
41799
42468
  }
41800
42469
  }
41801
- await new Promise((resolve19) => setTimeout(resolve19, getPollInterval()));
42470
+ await new Promise((resolve20) => setTimeout(resolve20, getPollInterval()));
41802
42471
  }
41803
42472
  }
41804
42473
  var testCommand = new Command("test").description("Run tests").argument("[path]", 'Test path filter (e.g. "api", "e2e/portal")').option("--filter <path>", "Alias for the positional test path filter").option("--timeout <ms>", "Per-test timeout in ms", "30000").option("--retry <n>", "Retry failed tests N times", "0").option("--no-sync", "Skip sync-up before tests").option("--json", "Output as JSON").action((pathFilter, opts) => run("Test", async () => {
@@ -41850,6 +42519,10 @@ var testCommand = new Command("test").description("Run tests").argument("[path]"
41850
42519
  console.log(error(`Run failed: ${data.errorMessage}`));
41851
42520
  console.log("");
41852
42521
  }
42522
+ if (data.dbSummary) {
42523
+ console.log(muted(`Test db: ${data.dbSummary}`));
42524
+ console.log("");
42525
+ }
41853
42526
  if (data.failed > 0) {
41854
42527
  console.log(error("Failures:"));
41855
42528
  for (const r of data.results.filter((r7) => r7.status === "failed")) {
@@ -42008,21 +42681,21 @@ var locationCommand = new Command("location").description("Show location").argum
42008
42681
  }));
42009
42682
 
42010
42683
  // src/commands/doctor.ts
42011
- import { existsSync as existsSync17, readFileSync as readFileSync19, statSync as statSync8 } from "fs";
42012
- import { join as join19 } from "path";
42013
- import { homedir as homedir13 } from "os";
42684
+ import { existsSync as existsSync18, readFileSync as readFileSync20, statSync as statSync8 } from "fs";
42685
+ import { join as join20, resolve as resolve16 } from "path";
42686
+ import { homedir as homedir14 } from "os";
42014
42687
 
42015
42688
  // src/updater/state.ts
42016
- import { readFileSync as readFileSync18, writeFileSync as writeFileSync12, mkdirSync as mkdirSync12, existsSync as existsSync16 } from "fs";
42017
- import { join as join18 } from "path";
42018
- import { homedir as homedir12 } from "os";
42019
- var GIPITY_DIR = join18(homedir12(), ".gipity");
42020
- var LOCAL_DIR = join18(GIPITY_DIR, "local");
42021
- var LOCAL_PKG_DIR = join18(LOCAL_DIR, "node_modules", "gipity");
42022
- var LOCAL_ENTRY = join18(LOCAL_PKG_DIR, "dist", "index.js");
42023
- var STATE_FILE = join18(GIPITY_DIR, "update-state.json");
42024
- var SETTINGS_FILE = join18(GIPITY_DIR, "settings.json");
42025
- var UPDATE_LOG = join18(GIPITY_DIR, "update.log");
42689
+ import { readFileSync as readFileSync19, writeFileSync as writeFileSync14, mkdirSync as mkdirSync14, existsSync as existsSync17 } from "fs";
42690
+ import { join as join19 } from "path";
42691
+ import { homedir as homedir13 } from "os";
42692
+ var GIPITY_DIR = join19(homedir13(), ".gipity");
42693
+ var LOCAL_DIR = join19(GIPITY_DIR, "local");
42694
+ var LOCAL_PKG_DIR = join19(LOCAL_DIR, "node_modules", "gipity");
42695
+ var LOCAL_ENTRY = join19(LOCAL_PKG_DIR, "dist", "index.js");
42696
+ var STATE_FILE = join19(GIPITY_DIR, "update-state.json");
42697
+ var SETTINGS_FILE = join19(GIPITY_DIR, "settings.json");
42698
+ var UPDATE_LOG = join19(GIPITY_DIR, "update.log");
42026
42699
  var DEFAULT_STATE = {
42027
42700
  installedVersion: null,
42028
42701
  lastCheckAt: 0,
@@ -42033,24 +42706,24 @@ var DEFAULT_SETTINGS = {
42033
42706
  autoUpdates: true
42034
42707
  };
42035
42708
  function ensureDir() {
42036
- mkdirSync12(GIPITY_DIR, { recursive: true });
42709
+ mkdirSync14(GIPITY_DIR, { recursive: true });
42037
42710
  }
42038
42711
  function readState() {
42039
- if (!existsSync16(STATE_FILE)) return { ...DEFAULT_STATE };
42712
+ if (!existsSync17(STATE_FILE)) return { ...DEFAULT_STATE };
42040
42713
  try {
42041
- return { ...DEFAULT_STATE, ...JSON.parse(readFileSync18(STATE_FILE, "utf-8")) };
42714
+ return { ...DEFAULT_STATE, ...JSON.parse(readFileSync19(STATE_FILE, "utf-8")) };
42042
42715
  } catch {
42043
42716
  return { ...DEFAULT_STATE };
42044
42717
  }
42045
42718
  }
42046
42719
  function writeState(state) {
42047
42720
  ensureDir();
42048
- writeFileSync12(STATE_FILE, JSON.stringify(state, null, 2));
42721
+ writeFileSync14(STATE_FILE, JSON.stringify(state, null, 2));
42049
42722
  }
42050
42723
  function readSettings() {
42051
- if (!existsSync16(SETTINGS_FILE)) return { ...DEFAULT_SETTINGS };
42724
+ if (!existsSync17(SETTINGS_FILE)) return { ...DEFAULT_SETTINGS };
42052
42725
  try {
42053
- return { ...DEFAULT_SETTINGS, ...JSON.parse(readFileSync18(SETTINGS_FILE, "utf-8")) };
42726
+ return { ...DEFAULT_SETTINGS, ...JSON.parse(readFileSync19(SETTINGS_FILE, "utf-8")) };
42054
42727
  } catch {
42055
42728
  return { ...DEFAULT_SETTINGS };
42056
42729
  }
@@ -42066,7 +42739,7 @@ function updatesDisabled() {
42066
42739
  init_colors();
42067
42740
  init_auth();
42068
42741
  var NODE_MIN_MAJOR = 18;
42069
- function versionManagerNode(execPath, env = process.env, home = homedir13()) {
42742
+ function versionManagerNode(execPath, env = process.env, home = homedir14()) {
42070
42743
  const norm = (s) => s.replace(/\\/g, "/").replace(/\/+$/, "") + "/";
42071
42744
  const p = norm(execPath);
42072
42745
  const h = norm(home);
@@ -42083,10 +42756,10 @@ function versionManagerNode(execPath, env = process.env, home = homedir13()) {
42083
42756
  return null;
42084
42757
  }
42085
42758
  function localVersion() {
42086
- const pkgPath = join19(LOCAL_PKG_DIR, "package.json");
42087
- if (!existsSync17(pkgPath)) return null;
42759
+ const pkgPath = join20(LOCAL_PKG_DIR, "package.json");
42760
+ if (!existsSync18(pkgPath)) return null;
42088
42761
  try {
42089
- return JSON.parse(readFileSync19(pkgPath, "utf-8")).version ?? null;
42762
+ return JSON.parse(readFileSync20(pkgPath, "utf-8")).version ?? null;
42090
42763
  } catch {
42091
42764
  return null;
42092
42765
  }
@@ -42094,7 +42767,7 @@ function localVersion() {
42094
42767
  function shimVersion() {
42095
42768
  try {
42096
42769
  const url = new URL("../../package.json", import.meta.url);
42097
- return JSON.parse(readFileSync19(url, "utf-8")).version;
42770
+ return JSON.parse(readFileSync20(url, "utf-8")).version;
42098
42771
  } catch {
42099
42772
  return "unknown";
42100
42773
  }
@@ -42109,7 +42782,7 @@ function rel(t) {
42109
42782
  }
42110
42783
  function relayAutostartInstalled() {
42111
42784
  try {
42112
- return existsSync17(planFor({ cliPath: resolveCliPath() }).path);
42785
+ return existsSync18(planFor({ cliPath: resolveCliPath() }).path);
42113
42786
  } catch (err) {
42114
42787
  if (err instanceof UnsupportedPlatformError) return null;
42115
42788
  return null;
@@ -42147,7 +42820,7 @@ function gatherEnv(opts = {}) {
42147
42820
  const cli = {
42148
42821
  shim_version: shimVersion(),
42149
42822
  local_version: localVersion(),
42150
- local_install_ok: existsSync17(LOCAL_ENTRY),
42823
+ local_install_ok: existsSync18(LOCAL_ENTRY),
42151
42824
  auto_updates: !dis.disabled,
42152
42825
  updates_disabled_reason: dis.disabled ? dis.reason ?? null : null,
42153
42826
  last_check_at: updState.lastCheckAt,
@@ -42177,12 +42850,15 @@ var doctorCommand = new Command("doctor").description("Check install + environme
42177
42850
  console.log(`${muted("claude code ")} installed ${yn(env.claude.installed)} \xB7 authenticated ${yn(env.claude.authenticated)}`);
42178
42851
  const autostartLabel = env.relay.autostart === null ? muted("n/a") : yn(env.relay.autostart);
42179
42852
  console.log(`${muted("relay ")} paired ${yn(env.relay.paired)} \xB7 running ${yn(env.relay.running)} \xB7 autostart ${autostartLabel}${env.relay.paused ? warning(" \xB7 paused") : ""}${env.relay.device ? muted(` (${env.relay.device.name})`) : ""}`);
42180
- console.log(`${muted("ready ")} ${env.ready ? success("yes") : warning("no - run `gipity claude` (or the desktop app) to finish setup")}`);
42853
+ if (existsSync18(resolve16(process.cwd(), ".codex", "hooks.json"))) {
42854
+ console.log(`${muted("codex hooks ")} ${warning("written")} - session capture needs a one-time approval: run /hooks inside Codex in this project`);
42855
+ }
42856
+ console.log(`${muted("ready ")} ${env.ready ? success("yes") : warning("no - run `gipity build` (or the desktop app) to finish setup")}`);
42181
42857
  const state = readState();
42182
42858
  const settings = readSettings();
42183
42859
  const dis = updatesDisabled();
42184
42860
  const local = localVersion();
42185
- const localOk = existsSync17(LOCAL_ENTRY);
42861
+ const localOk = existsSync18(LOCAL_ENTRY);
42186
42862
  console.log("");
42187
42863
  console.log(bold("CLI install"));
42188
42864
  console.log(`${muted("shim version ")} ${shimVersion()}`);
@@ -42190,17 +42866,17 @@ var doctorCommand = new Command("doctor").description("Check install + environme
42190
42866
  console.log(`${muted("local install ")} ${LOCAL_PKG_DIR}`);
42191
42867
  console.log("");
42192
42868
  console.log(`${muted("auto-updates ")} ${dis.disabled ? warning(`disabled (${dis.reason})`) : success("enabled")}`);
42193
- console.log(`${muted("settings file ")} ${existsSync17(SETTINGS_FILE) ? SETTINGS_FILE : dim("(default)")} autoUpdates=${settings.autoUpdates}`);
42869
+ console.log(`${muted("settings file ")} ${existsSync18(SETTINGS_FILE) ? SETTINGS_FILE : dim("(default)")} autoUpdates=${settings.autoUpdates}`);
42194
42870
  console.log(`${muted("last check ")} ${rel(state.lastCheckAt)}`);
42195
42871
  console.log(`${muted("last error ")} ${state.lastError ? error(state.lastError) : dim("none")}`);
42196
- console.log(`${muted("state file ")} ${existsSync17(STATE_FILE) ? STATE_FILE : dim("(none yet)")}`);
42197
- console.log(`${muted("update log ")} ${existsSync17(UPDATE_LOG) ? `${UPDATE_LOG} (${statSync8(UPDATE_LOG).size} bytes)` : dim("(none yet)")}`);
42872
+ console.log(`${muted("state file ")} ${existsSync18(STATE_FILE) ? STATE_FILE : dim("(none yet)")}`);
42873
+ console.log(`${muted("update log ")} ${existsSync18(UPDATE_LOG) ? `${UPDATE_LOG} (${statSync8(UPDATE_LOG).size} bytes)` : dim("(none yet)")}`);
42198
42874
  console.log("");
42199
42875
  console.log(dim("Force an update with: gipity update"));
42200
42876
  });
42201
42877
 
42202
42878
  // src/updater/check.ts
42203
- import { appendFileSync, existsSync as existsSync18 } from "fs";
42879
+ import { appendFileSync, existsSync as existsSync19 } from "fs";
42204
42880
  init_platform();
42205
42881
  var CHECK_INTERVAL_MS = 4 * 60 * 60 * 1e3;
42206
42882
  function log(line) {
@@ -42235,7 +42911,7 @@ function installVersion(version) {
42235
42911
  stdio: "ignore"
42236
42912
  });
42237
42913
  if (res.error) log(`npm spawn failed: ${res.error.message}`);
42238
- return res.status === 0 && existsSync18(LOCAL_ENTRY);
42914
+ return res.status === 0 && existsSync19(LOCAL_ENTRY);
42239
42915
  }
42240
42916
  async function runCheck(opts = {}) {
42241
42917
  const state = readState();
@@ -42309,17 +42985,17 @@ var updateCommand = new Command("update").description("Update the CLI").action(a
42309
42985
  init_api();
42310
42986
  init_utils();
42311
42987
  init_colors();
42312
- import { existsSync as existsSync21, readFileSync as readFileSync22, unlinkSync as unlinkSync7 } from "fs";
42988
+ import { existsSync as existsSync22, readFileSync as readFileSync23, unlinkSync as unlinkSync7 } from "fs";
42313
42989
  import { spawn as spawn2 } from "child_process";
42314
42990
 
42315
42991
  // src/relay/daemon.ts
42316
42992
  init_platform();
42317
42993
  init_config();
42318
- import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync14, existsSync as existsSync20, readFileSync as readFileSync21, writeFileSync as writeFileSync13, chmodSync as chmodSync3, closeSync as closeSync4, openSync as openSync4, unlinkSync as unlinkSync6 } from "fs";
42994
+ import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync16, existsSync as existsSync21, readFileSync as readFileSync22, writeFileSync as writeFileSync15, chmodSync as chmodSync3, closeSync as closeSync4, openSync as openSync4, unlinkSync as unlinkSync6 } from "fs";
42319
42995
  import { stat, readFile } from "fs/promises";
42320
42996
  import { createInterface as createInterface2 } from "readline";
42321
- import { homedir as homedir15, hostname as hostname5, platform as osPlatform3, loadavg as loadavg2, freemem as freemem2, totalmem as totalmem2, cpus as cpus2 } from "os";
42322
- import { join as join22 } from "path";
42997
+ import { homedir as homedir16, hostname as hostname5, platform as osPlatform3, loadavg as loadavg2, freemem as freemem2, totalmem as totalmem2, cpus as cpus2 } from "os";
42998
+ import { join as join23 } from "path";
42323
42999
  init_auth();
42324
43000
  init_api();
42325
43001
 
@@ -43025,7 +43701,7 @@ var DeltaBatcher = class {
43025
43701
  };
43026
43702
 
43027
43703
  // src/relay/daemon.ts
43028
- import { randomUUID } from "crypto";
43704
+ import { randomUUID as randomUUID2 } from "crypto";
43029
43705
 
43030
43706
  // src/relay/agent-token.ts
43031
43707
  init_api();
@@ -43074,22 +43750,22 @@ async function revokeRelayAgentToken() {
43074
43750
  // src/relay/diagnostics.ts
43075
43751
  init_platform();
43076
43752
  init_client_context();
43077
- import { cpus, freemem, totalmem, loadavg, release, uptime, homedir as homedir14 } from "os";
43078
- import { statfsSync, readdirSync as readdirSync8 } from "fs";
43079
- import { join as join20 } from "path";
43753
+ import { cpus, freemem, totalmem, loadavg, release, uptime, homedir as homedir15 } from "os";
43754
+ import { statfsSync, readdirSync as readdirSync9 } from "fs";
43755
+ import { join as join21 } from "path";
43080
43756
  var PROBE_TIMEOUT_MS = 4e3;
43081
43757
  function parseVersion(out) {
43082
43758
  const m = out.match(/\d+\.\d+(?:\.\d+)?(?:[-.\w]*)?/);
43083
43759
  return m ? m[0] : void 0;
43084
43760
  }
43085
43761
  function runProbe(cmd, args) {
43086
- return new Promise((resolve19) => {
43762
+ return new Promise((resolve20) => {
43087
43763
  let out = "";
43088
43764
  let settled = false;
43089
43765
  const done = (s) => {
43090
43766
  if (!settled) {
43091
43767
  settled = true;
43092
- resolve19(s);
43768
+ resolve20(s);
43093
43769
  }
43094
43770
  };
43095
43771
  let child;
@@ -43143,12 +43819,12 @@ async function detectGpu() {
43143
43819
  function diskUsage() {
43144
43820
  try {
43145
43821
  if (typeof statfsSync !== "function") return void 0;
43146
- const st2 = statfsSync(join20(homedir14(), "GipityProjects"), { bigint: false });
43822
+ const st2 = statfsSync(join21(homedir15(), "GipityProjects"), { bigint: false });
43147
43823
  if (!st2 || !st2.bsize) return void 0;
43148
43824
  return { total: st2.bsize * st2.blocks, free: st2.bsize * st2.bavail };
43149
43825
  } catch {
43150
43826
  try {
43151
- const st2 = statfsSync(homedir14(), { bigint: false });
43827
+ const st2 = statfsSync(homedir15(), { bigint: false });
43152
43828
  return { total: st2.bsize * st2.blocks, free: st2.bsize * st2.bavail };
43153
43829
  } catch {
43154
43830
  return void 0;
@@ -43157,7 +43833,7 @@ function diskUsage() {
43157
43833
  }
43158
43834
  function localProjectCount() {
43159
43835
  try {
43160
- return readdirSync8(join20(homedir14(), "GipityProjects"), { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).length;
43836
+ return readdirSync9(join21(homedir15(), "GipityProjects"), { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).length;
43161
43837
  } catch {
43162
43838
  return void 0;
43163
43839
  }
@@ -43170,15 +43846,17 @@ async function collectDiagnostics() {
43170
43846
  return [];
43171
43847
  }
43172
43848
  })();
43173
- const [claude, codex, cursor, gpu] = await Promise.all([
43849
+ const [claude, codex, grok, cursor, gpu] = await Promise.all([
43174
43850
  probeVersion("claude"),
43175
43851
  probeVersion("codex"),
43852
+ probeVersion("grok"),
43176
43853
  probeVersion("cursor"),
43177
43854
  detectGpu()
43178
43855
  ]);
43179
43856
  const agents = {};
43180
43857
  if (claude) agents.claude_code = claude;
43181
43858
  if (codex) agents.codex = codex;
43859
+ if (grok) agents.grok = grok;
43182
43860
  if (cursor) agents.cursor = cursor;
43183
43861
  return {
43184
43862
  collected_at: (/* @__PURE__ */ new Date()).toISOString(),
@@ -43286,8 +43964,8 @@ var SessionPool = class {
43286
43964
  const done = new Promise((r) => {
43287
43965
  markDone = r;
43288
43966
  });
43289
- return new Promise((resolve19, reject) => {
43290
- const turn = { onMessage: p.onMessage, resolve: resolve19, reject, interruptRequested: false, wasHot, done, markDone };
43967
+ return new Promise((resolve20, reject) => {
43968
+ const turn = { onMessage: p.onMessage, resolve: resolve20, reject, interruptRequested: false, wasHot, done, markDone };
43291
43969
  session.current = turn;
43292
43970
  session.state = "running";
43293
43971
  session.lastActivityAt = Date.now();
@@ -43464,7 +44142,7 @@ var SessionPool = class {
43464
44142
  // src/relay/daemon.ts
43465
44143
  init_config();
43466
44144
  init_api();
43467
- var RELAY_LOG_PATH = join22(homedir15(), ".gipity", "relay.log");
44145
+ var RELAY_LOG_PATH = join23(homedir16(), ".gipity", "relay.log");
43468
44146
  var HEARTBEAT_INTERVAL_MS = parseInt(process.env.GIPITY_RELAY_HEARTBEAT_MS || "60000", 10);
43469
44147
  var DIAGNOSTICS_INTERVAL_MS = parseInt(process.env.GIPITY_RELAY_DIAGNOSTICS_MS || String(24 * 60 * 60 * 1e3), 10);
43470
44148
  var LONG_POLL_TIMEOUT_MS = parseInt(process.env.GIPITY_RELAY_POLL_TIMEOUT_MS || "35000", 10);
@@ -43564,7 +44242,7 @@ function lockLogPerms(dir, file) {
43564
44242
  chmodSync3(dir, 448);
43565
44243
  } catch {
43566
44244
  }
43567
- if (!existsSync20(file)) {
44245
+ if (!existsSync21(file)) {
43568
44246
  try {
43569
44247
  closeSync4(openSync4(file, "a", 384));
43570
44248
  } catch {
@@ -43581,8 +44259,8 @@ function log2(level, msg, extra) {
43581
44259
  const pretty = `${C7.dim(hhmmss())} ${badge(level)} ${C7.bold(msg)}${formatExtra(extra)}`;
43582
44260
  process.stderr.write(pretty + "\n");
43583
44261
  try {
43584
- const dir = join22(homedir15(), ".gipity");
43585
- mkdirSync14(dir, { recursive: true });
44262
+ const dir = join23(homedir16(), ".gipity");
44263
+ mkdirSync16(dir, { recursive: true });
43586
44264
  lockLogPerms(dir, RELAY_LOG_PATH);
43587
44265
  const json = JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level, msg, ...extra ?? {} });
43588
44266
  appendFileSync3(RELAY_LOG_PATH, json + "\n");
@@ -43706,10 +44384,10 @@ async function heartbeatLoop(ctx) {
43706
44384
  }
43707
44385
  await Promise.race([
43708
44386
  sleep4(HEARTBEAT_INTERVAL_MS, ctx.abort.signal),
43709
- new Promise((resolve19) => {
44387
+ new Promise((resolve20) => {
43710
44388
  heartbeatPoke = () => {
43711
44389
  heartbeatPoke = null;
43712
- resolve19();
44390
+ resolve20();
43713
44391
  };
43714
44392
  })
43715
44393
  ]);
@@ -43753,9 +44431,9 @@ async function dispatchLoop(ctx, opts) {
43753
44431
  while (inflight.size >= MAX_CONCURRENT_DISPATCHES && !ctx.abort.signal.aborted) {
43754
44432
  await Promise.race([
43755
44433
  ...inflight,
43756
- new Promise((resolve19) => {
43757
- if (ctx.abort.signal.aborted) return resolve19();
43758
- ctx.abort.signal.addEventListener("abort", () => resolve19(), { once: true });
44434
+ new Promise((resolve20) => {
44435
+ if (ctx.abort.signal.aborted) return resolve20();
44436
+ ctx.abort.signal.addEventListener("abort", () => resolve20(), { once: true });
43759
44437
  })
43760
44438
  ]);
43761
44439
  }
@@ -43982,7 +44660,7 @@ function isSafeSessionId(s) {
43982
44660
  function transcriptPathFor(cwd, sessionId) {
43983
44661
  if (!isSafeSessionId(sessionId)) return null;
43984
44662
  const slug = cwd.replace(/[^a-zA-Z0-9]/g, "-");
43985
- return join22(homedir15(), ".claude", "projects", slug, `${sessionId}.jsonl`);
44663
+ return join23(homedir16(), ".claude", "projects", slug, `${sessionId}.jsonl`);
43986
44664
  }
43987
44665
  function formatDuration(ms2) {
43988
44666
  const totalSec = ms2 / 1e3;
@@ -44051,7 +44729,7 @@ async function handleDispatch(claimed) {
44051
44729
  { onWarn: (msg, meta) => log2("warn", msg, { id: d.short_guid, ...meta }) }
44052
44730
  );
44053
44731
  const pushSystem = (content) => {
44054
- queue.push({ kind: "system", content, ts: (/* @__PURE__ */ new Date()).toISOString(), source_uuid: randomUUID() });
44732
+ queue.push({ kind: "system", content, ts: (/* @__PURE__ */ new Date()).toISOString(), source_uuid: randomUUID2() });
44055
44733
  };
44056
44734
  const flushQueue = async () => {
44057
44735
  await queue.close(3e4);
@@ -44097,11 +44775,16 @@ async function handleDispatch(claimed) {
44097
44775
  }
44098
44776
  if (!syncFailed) pushSystem(bootstrapped ? "Project files synced." : "Attached files synced.");
44099
44777
  }
44778
+ const adapter = getAdapterBySource(d.remote_type ?? "claude_code");
44779
+ if (adapter.key !== "claude") {
44780
+ await handleNonClaudeDispatch(adapter, d, cwd, queue, pushSystem, flushQueue);
44781
+ return;
44782
+ }
44100
44783
  if (SESSION_POOL_ENABLED) {
44101
44784
  const handled = await tryHandleViaPool(d, cwd, queue, pushSystem, flushQueue);
44102
44785
  if (handled) return;
44103
44786
  }
44104
- const args = ["claude", "-p", d.message, "--permission-mode", "bypassPermissions"];
44787
+ const args = ["build", "--agent", "claude", "-p", d.message, "--permission-mode", "bypassPermissions"];
44105
44788
  args.push("--append-system-prompt", GIPITY_QUESTION_PROTOCOL);
44106
44789
  if (d.model) {
44107
44790
  args.push("--model", d.model);
@@ -44109,7 +44792,7 @@ async function handleDispatch(claimed) {
44109
44792
  if (d.kind === "resume" && d.remote_session_id) {
44110
44793
  args.push("--resume", d.remote_session_id);
44111
44794
  }
44112
- log2("debug", "spawning gipity claude", {
44795
+ log2("debug", "spawning gipity build --agent claude", {
44113
44796
  id: d.short_guid,
44114
44797
  cwd,
44115
44798
  args,
@@ -44158,8 +44841,8 @@ async function handleDispatch(claimed) {
44158
44841
  const header = `Running Claude Code - ${counts.join(" + ")} words${resumeNote}`;
44159
44842
  const ts2 = (/* @__PURE__ */ new Date()).toISOString();
44160
44843
  queue.push(
44161
- { kind: "prompt", prompt: d.message, ts: ts2, source_uuid: randomUUID() },
44162
- { kind: "system", content: header, ts: ts2, source_uuid: randomUUID() }
44844
+ { kind: "prompt", prompt: d.message, ts: ts2, source_uuid: randomUUID2() },
44845
+ { kind: "system", content: header, ts: ts2, source_uuid: randomUUID2() }
44163
44846
  );
44164
44847
  const t0 = Date.now();
44165
44848
  let exitCode = 1;
@@ -44206,7 +44889,61 @@ async function handleDispatch(claimed) {
44206
44889
  await ack(d.short_guid, "done", void 0, metrics);
44207
44890
  } else {
44208
44891
  log2("warn", "dispatch child exited nonzero", { id: d.short_guid, exitCode, ms: ms2 });
44209
- await ack(d.short_guid, "error", `gipity claude exited with code ${exitCode}${stderrNote}`);
44892
+ await ack(d.short_guid, "error", `Claude Code exited with code ${exitCode}${stderrNote}`);
44893
+ }
44894
+ }
44895
+ async function handleNonClaudeDispatch(adapter, d, cwd, queue, pushSystem, flushQueue) {
44896
+ const args = ["build", "--agent", adapter.key, "-p", d.message, "--bypass-approvals"];
44897
+ if (d.model) args.push("--model", d.model);
44898
+ if (d.kind === "resume" && d.remote_session_id) args.push("--resume", d.remote_session_id);
44899
+ log2("debug", `spawning gipity build --agent ${adapter.key}`, {
44900
+ id: d.short_guid,
44901
+ cwd,
44902
+ args,
44903
+ conv: d.conversation_guid,
44904
+ chain: d.kind === "resume" ? `resume ${d.remote_session_id}` : "start (fresh session)"
44905
+ });
44906
+ const words = d.message.trim().split(/\s+/).filter(Boolean).length;
44907
+ pushSystem(`Running ${adapter.displayName} - ${words.toLocaleString("en-US")} words`);
44908
+ const t0 = Date.now();
44909
+ let exitCode = 1;
44910
+ let spawnErr = null;
44911
+ let killed = false;
44912
+ let runtimeLimit = false;
44913
+ let stderrTail = "";
44914
+ try {
44915
+ const result = await spawnGipityClaude(args, cwd, d, queue, { streamJson: false });
44916
+ exitCode = result.exitCode;
44917
+ killed = result.killed;
44918
+ runtimeLimit = result.runtimeLimit ?? false;
44919
+ stderrTail = result.stderrTail ?? "";
44920
+ } catch (err) {
44921
+ spawnErr = err?.message || String(err);
44922
+ log2("error", "dispatch spawn failed", { id: d.short_guid, err: spawnErr });
44923
+ }
44924
+ const dur = formatDuration(Date.now() - t0);
44925
+ if (!spawnErr && !killed) {
44926
+ try {
44927
+ await spawnSync2(cwd, PROJECT_SYNC_TIMEOUT_MS);
44928
+ } catch (err) {
44929
+ log2("warn", "sync after dispatch failed", { id: d.short_guid, err: err?.message });
44930
+ }
44931
+ }
44932
+ const stderrNote = stderrTail ? `: ${stderrTail.slice(0, 300)}` : "";
44933
+ const tail = runtimeLimit ? `stopped after ${dur} (runtime limit)` : killed ? `cancelled (${dur})` : spawnErr ? `failed (${dur}: ${spawnErr})` : exitCode === 0 ? `finished (${dur})` : `failed (${dur}, exit ${exitCode}${stderrNote})`;
44934
+ pushSystem(`${adapter.displayName} ${tail}`);
44935
+ await flushQueue();
44936
+ if (runtimeLimit) {
44937
+ await ack(d.short_guid, "error", `${adapter.displayName} stopped after ${dur} (runtime limit)`);
44938
+ } else if (killed) {
44939
+ await ack(d.short_guid, "cancelled");
44940
+ } else if (spawnErr) {
44941
+ await ack(d.short_guid, "error", spawnErr);
44942
+ } else if (exitCode === 0) {
44943
+ log2("info", "dispatch done", { id: d.short_guid, agent: adapter.key });
44944
+ await ack(d.short_guid, "done");
44945
+ } else {
44946
+ await ack(d.short_guid, "error", `${adapter.displayName} exited with code ${exitCode}${stderrNote}`);
44210
44947
  }
44211
44948
  }
44212
44949
  async function wrapPoolMessage(d, cwd, resume) {
@@ -44248,8 +44985,8 @@ async function tryHandleViaPool(d, cwd, queue, pushSystem, flushQueue) {
44248
44985
  const words = d.message.trim().split(/\s+/).filter(Boolean).length;
44249
44986
  const ts0 = (/* @__PURE__ */ new Date()).toISOString();
44250
44987
  queue.push(
44251
- { kind: "prompt", prompt: d.message, ts: ts0, source_uuid: randomUUID() },
44252
- { kind: "system", content: `Running Claude Code - ${words.toLocaleString("en-US")} words${wasLive ? " (hot session)" : ""}`, ts: ts0, source_uuid: randomUUID() }
44988
+ { kind: "prompt", prompt: d.message, ts: ts0, source_uuid: randomUUID2() },
44989
+ { kind: "system", content: `Running Claude Code - ${words.toLocaleString("en-US")} words${wasLive ? " (hot session)" : ""}`, ts: ts0, source_uuid: randomUUID2() }
44253
44990
  );
44254
44991
  const phases = new PhaseTracker();
44255
44992
  const deltaAcc = new DeltaAccumulator(getRelaySecrets);
@@ -44304,7 +45041,7 @@ async function tryHandleViaPool(d, cwd, queue, pushSystem, flushQueue) {
44304
45041
  const ts2 = (/* @__PURE__ */ new Date()).toISOString();
44305
45042
  for (const e of entries) {
44306
45043
  e.ts = ts2;
44307
- if (!e.source_uuid) e.source_uuid = randomUUID();
45044
+ if (!e.source_uuid) e.source_uuid = randomUUID2();
44308
45045
  }
44309
45046
  queue.push(...entries);
44310
45047
  };
@@ -44377,11 +45114,11 @@ async function resolveCwdForProject(d) {
44377
45114
  throw new Error(`Invalid project slug: ${JSON.stringify(d.project_slug)}`);
44378
45115
  }
44379
45116
  const root = getProjectsRoot();
44380
- const path5 = join22(root, d.project_slug);
44381
- const configPath = join22(path5, ".gipity.json");
44382
- if (existsSync20(configPath)) {
45117
+ const path5 = join23(root, d.project_slug);
45118
+ const configPath = join23(path5, ".gipity.json");
45119
+ if (existsSync21(configPath)) {
44383
45120
  try {
44384
- const cfg = JSON.parse(readFileSync21(configPath, "utf-8"));
45121
+ const cfg = JSON.parse(readFileSync22(configPath, "utf-8"));
44385
45122
  if (cfg.projectGuid === d.project_guid) return { cwd: path5, bootstrapped: false };
44386
45123
  log2("warn", "project dir exists but guid mismatch - using it anyway", {
44387
45124
  path: path5,
@@ -44393,9 +45130,9 @@ async function resolveCwdForProject(d) {
44393
45130
  }
44394
45131
  }
44395
45132
  log2("info", "bootstrapping new project dir", { slug: d.project_slug, path: path5 });
44396
- mkdirSync14(path5, { recursive: true });
45133
+ mkdirSync16(path5, { recursive: true });
44397
45134
  const apiBase2 = getApiBaseOverride() || DEFAULT_API_BASE;
44398
- writeFileSync13(configPath, JSON.stringify({
45135
+ writeFileSync15(configPath, JSON.stringify({
44399
45136
  projectGuid: d.project_guid,
44400
45137
  projectSlug: d.project_slug,
44401
45138
  accountSlug: d.account_slug,
@@ -44407,10 +45144,7 @@ async function resolveCwdForProject(d) {
44407
45144
  const origCwd = process.cwd();
44408
45145
  try {
44409
45146
  process.chdir(path5);
44410
- setupClaudeHooks();
44411
- setupClaudeMd();
44412
- setupAgentsMd();
44413
- setupGitignore();
45147
+ setupProjectTools();
44414
45148
  } finally {
44415
45149
  process.chdir(origCwd);
44416
45150
  }
@@ -44475,7 +45209,7 @@ async function killRunningForConv(convGuid) {
44475
45209
  }
44476
45210
  }
44477
45211
  const graceTimers = [];
44478
- const escalate = new Promise((resolve19) => {
45212
+ const escalate = new Promise((resolve20) => {
44479
45213
  const t = setTimeout(() => {
44480
45214
  for (const e of matches) {
44481
45215
  log2("warn", "previous dispatch ignored SIGTERM - escalating to SIGKILL", { conv: convGuid });
@@ -44484,7 +45218,7 @@ async function killRunningForConv(convGuid) {
44484
45218
  } catch {
44485
45219
  }
44486
45220
  }
44487
- resolve19();
45221
+ resolve20();
44488
45222
  }, KILL_GRACE_MS);
44489
45223
  graceTimers.push(t);
44490
45224
  });
@@ -44495,7 +45229,7 @@ async function killRunningForConv(convGuid) {
44495
45229
  }
44496
45230
  async function spawnSync2(cwd, timeoutMs, onSpawn) {
44497
45231
  const cmd = process.env.GIPITY_RELAY_CLAUDE_CMD || resolveCommand("gipity");
44498
- return new Promise((resolve19, reject) => {
45232
+ return new Promise((resolve20, reject) => {
44499
45233
  const child = spawnCommand(cmd, ["sync", "--json"], {
44500
45234
  cwd,
44501
45235
  env: childEnv(),
@@ -44519,7 +45253,7 @@ async function spawnSync2(cwd, timeoutMs, onSpawn) {
44519
45253
  } catch {
44520
45254
  }
44521
45255
  try {
44522
- unlinkSync6(join22(cwd, ".gipity", "sync.lock"));
45256
+ unlinkSync6(join23(cwd, ".gipity", "sync.lock"));
44523
45257
  } catch {
44524
45258
  }
44525
45259
  finish(() => reject(new Error(`timed out after ${Math.round(timeoutMs / 1e3)}s`)));
@@ -44535,7 +45269,7 @@ async function spawnSync2(cwd, timeoutMs, onSpawn) {
44535
45269
  child.on("exit", (code) => finish(() => {
44536
45270
  if (code === 0) {
44537
45271
  log2("info", "sync done", { cwd, stdoutLen });
44538
- resolve19();
45272
+ resolve20();
44539
45273
  } else {
44540
45274
  reject(new Error(`gipity sync exited ${code}${stderrBuf ? `: ${stderrBuf.trim().slice(0, 300)}` : ""}`));
44541
45275
  }
@@ -44543,10 +45277,11 @@ async function spawnSync2(cwd, timeoutMs, onSpawn) {
44543
45277
  });
44544
45278
  }
44545
45279
  async function spawnGipityClaude(args, cwd, d, queue, meta) {
45280
+ const streamJson = meta?.streamJson !== false;
44546
45281
  const cmd = process.env.GIPITY_RELAY_CLAUDE_CMD || resolveCommand("gipity");
44547
- const fullArgs = [...args, "--output-format", "stream-json", "--verbose", "--include-partial-messages"];
44548
- const env = childEnv({ GIPITY_CONVERSATION_GUID: d.conversation_guid, GIPITY_CAPTURE: "off" });
44549
- return new Promise((resolve19, reject) => {
45282
+ const fullArgs = streamJson ? [...args, "--output-format", "stream-json", "--verbose", "--include-partial-messages"] : [...args];
45283
+ const env = streamJson ? childEnv({ GIPITY_CONVERSATION_GUID: d.conversation_guid, GIPITY_CAPTURE: "off" }) : childEnv({ GIPITY_CONVERSATION_GUID: d.conversation_guid });
45284
+ return new Promise((resolve20, reject) => {
44550
45285
  const child = spawnCommand(cmd, fullArgs, { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
44551
45286
  let resolveExited = () => {
44552
45287
  };
@@ -44675,16 +45410,18 @@ async function spawnGipityClaude(args, cwd, d, queue, meta) {
44675
45410
  const ts2 = (/* @__PURE__ */ new Date()).toISOString();
44676
45411
  for (const e of entries) {
44677
45412
  e.ts = ts2;
44678
- if (!e.source_uuid) e.source_uuid = randomUUID();
45413
+ if (!e.source_uuid) e.source_uuid = randomUUID2();
44679
45414
  }
44680
45415
  q.push(...entries);
44681
45416
  });
44682
45417
  child.stdout?.on("data", (chunk) => {
44683
45418
  stdoutBytesTotal += chunk.length;
44684
45419
  lastStdoutByteAt = Date.now();
44685
- splitter.push(chunk);
45420
+ if (streamJson) splitter.push(chunk);
45421
+ });
45422
+ child.stdout?.on("end", () => {
45423
+ if (streamJson) splitter.flush();
44686
45424
  });
44687
- child.stdout?.on("end", () => splitter.flush());
44688
45425
  const errPrefix = C7.dim("\u2502 ");
44689
45426
  const stderrTail = [];
44690
45427
  const errRl = child.stderr ? createInterface2({ input: child.stderr }) : null;
@@ -44710,7 +45447,7 @@ async function spawnGipityClaude(args, cwd, d, queue, meta) {
44710
45447
  if (signal === "SIGTERM" || signal === "SIGKILL") killed = true;
44711
45448
  if (finalResult) {
44712
45449
  finalResult.ts = (/* @__PURE__ */ new Date()).toISOString();
44713
- finalResult.source_uuid = randomUUID();
45450
+ finalResult.source_uuid = randomUUID2();
44714
45451
  q.push(finalResult);
44715
45452
  }
44716
45453
  if (unmapped.size > 0) {
@@ -44724,7 +45461,7 @@ async function spawnGipityClaude(args, cwd, d, queue, meta) {
44724
45461
  void postProgress(d.conversation_guid, buildProgressPayload(false));
44725
45462
  if (ownQueue) await q.close();
44726
45463
  cleanup();
44727
- resolve19({
45464
+ resolve20({
44728
45465
  exitCode: code ?? 1,
44729
45466
  killed,
44730
45467
  runtimeLimit,
@@ -44738,10 +45475,10 @@ async function runDispatchSync(d, cwd) {
44738
45475
  let killed = false;
44739
45476
  try {
44740
45477
  await spawnSync2(cwd, PROJECT_SYNC_TIMEOUT_MS, (child) => {
44741
- const exited = new Promise((resolve19) => {
45478
+ const exited = new Promise((resolve20) => {
44742
45479
  child.once("exit", (_code, signal) => {
44743
45480
  if (signal === "SIGTERM") killed = true;
44744
- resolve19();
45481
+ resolve20();
44745
45482
  });
44746
45483
  });
44747
45484
  running.set(d.short_guid, { child, convGuid: d.conversation_guid, exited });
@@ -44772,18 +45509,18 @@ function killDispatch(shortGuid) {
44772
45509
  return false;
44773
45510
  }
44774
45511
  function sleep4(ms2, signal) {
44775
- return new Promise((resolve19) => {
44776
- if (signal?.aborted) return resolve19();
45512
+ return new Promise((resolve20) => {
45513
+ if (signal?.aborted) return resolve20();
44777
45514
  let onAbort = null;
44778
45515
  const t = setTimeout(() => {
44779
45516
  if (onAbort && signal) signal.removeEventListener("abort", onAbort);
44780
- resolve19();
45517
+ resolve20();
44781
45518
  }, ms2);
44782
45519
  if (signal) {
44783
45520
  onAbort = () => {
44784
45521
  clearTimeout(t);
44785
45522
  signal.removeEventListener("abort", onAbort);
44786
- resolve19();
45523
+ resolve20();
44787
45524
  };
44788
45525
  signal.addEventListener("abort", onAbort, { once: true });
44789
45526
  }
@@ -44796,7 +45533,7 @@ init_colors();
44796
45533
  function requirePaired() {
44797
45534
  const device = getDevice();
44798
45535
  if (!device) {
44799
- console.error(`${error("No paired device.")} Run ${bold("gipity claude")} to pair this machine.`);
45536
+ console.error(`${error("No paired device.")} Run ${bold("gipity connect")} to pair this machine.`);
44800
45537
  process.exit(1);
44801
45538
  }
44802
45539
  return device;
@@ -44869,7 +45606,7 @@ function registerInstallCommands(relayCommand2) {
44869
45606
 
44870
45607
  // src/commands/relay.ts
44871
45608
  var relayCommand = new Command("relay").description("Pair with the web CLI");
44872
- relayCommand.command("setup").description("Pair this machine and start the relay - non-interactive (for installers/GUIs)").option("--name <name>", "Device name shown in the web CLI (default: this machine's hostname)").option("--no-start", "Pair only; do not start the relay daemon now").option("--no-autostart", "Skip the OS login service (use when a supervising app owns the daemon)").option("--force", "Re-pair even if already paired (revokes the old device first)").option("--json", "Machine-readable output").action(async (opts) => {
45609
+ relayCommand.command("setup").description("Pair this machine and start the relay - non-interactive (for installers/GUIs)").option("--name <name>", "Device name shown in the web CLI (default: this machine's hostname)").option("--no-start", "Pair only; do not start the relay daemon now").option("--no-autostart", "Skip the OS login service (use when a supervising app owns the daemon)").option("--force", "Re-pair even if already paired (re-registers this machine, rotating its token)").option("--json", "Machine-readable output").action(async (opts) => {
44873
45610
  const fail = (code, message) => {
44874
45611
  if (opts.json) {
44875
45612
  console.log(JSON.stringify({ ok: false, code, error: message }));
@@ -44937,7 +45674,7 @@ relayCommand.command("status").description("Show pairing status").option("--json
44937
45674
  return;
44938
45675
  }
44939
45676
  if (!s.device) {
44940
- console.log(`${muted("No paired device.")} Run ${brand("gipity claude")} to pair this machine.`);
45677
+ console.log(`${muted("No paired device.")} Run ${brand("gipity connect")} to pair this machine.`);
44941
45678
  return;
44942
45679
  }
44943
45680
  console.log(`${bold("Device:")} ${brand(s.device.name)} ${muted(`(${s.device.guid})`)}`);
@@ -44953,11 +45690,11 @@ relayCommand.command("run").description("Run the background service").option("-v
44953
45690
  });
44954
45691
  relayCommand.command("stop").description("Stop the background service").option("--force", "Force-stop if it doesn't exit cleanly within 5s").action(async (opts) => {
44955
45692
  const pidPath = getDaemonPidPath();
44956
- if (!existsSync21(pidPath)) {
45693
+ if (!existsSync22(pidPath)) {
44957
45694
  console.log(muted("Background service isn't running."));
44958
45695
  return;
44959
45696
  }
44960
- const pid = parseInt(readFileSync22(pidPath, "utf-8").trim(), 10);
45697
+ const pid = parseInt(readFileSync23(pidPath, "utf-8").trim(), 10);
44961
45698
  if (!pid || isNaN(pid)) {
44962
45699
  console.error(error("PID file is empty or malformed."));
44963
45700
  process.exit(1);
@@ -45081,8 +45818,8 @@ relayCommand.command("revoke").description("Revoke and forget this device").acti
45081
45818
  } catch {
45082
45819
  }
45083
45820
  const pidPath = getDaemonPidPath();
45084
- if (existsSync21(pidPath)) {
45085
- const pid = parseInt(readFileSync22(pidPath, "utf-8").trim(), 10);
45821
+ if (existsSync22(pidPath)) {
45822
+ const pid = parseInt(readFileSync23(pidPath, "utf-8").trim(), 10);
45086
45823
  if (pid && !isNaN(pid)) {
45087
45824
  try {
45088
45825
  process.kill(pid, "SIGTERM");
@@ -45095,13 +45832,13 @@ relayCommand.command("revoke").description("Revoke and forget this device").acti
45095
45832
  });
45096
45833
  relayCommand.command("log").description("Tail the service log").option("-n, --lines <n>", "How many lines to print (default 100)", "100").option("-f, --follow", "Follow the log like `tail -f`").action((opts) => {
45097
45834
  const path5 = RELAY_LOG_PATH;
45098
- if (!existsSync21(path5)) {
45835
+ if (!existsSync22(path5)) {
45099
45836
  console.log(muted("No log file yet. Start the service with `gipity relay run` (or install it)."));
45100
45837
  return;
45101
45838
  }
45102
45839
  const lines = parseInt(opts.lines, 10) || 100;
45103
45840
  try {
45104
- const all = readFileSync22(path5, "utf-8").split("\n");
45841
+ const all = readFileSync23(path5, "utf-8").split("\n");
45105
45842
  const tail = all.slice(-lines - 1).join("\n");
45106
45843
  process.stdout.write(tail);
45107
45844
  } catch (err) {
@@ -45127,7 +45864,7 @@ registerInstallCommands(relayCommand);
45127
45864
  function requirePaired2() {
45128
45865
  const device = getDevice();
45129
45866
  if (!device) {
45130
- console.error(`${error("No paired device.")} Run ${brand("gipity claude")} to pair this machine.`);
45867
+ console.error(`${error("No paired device.")} Run ${brand("gipity connect")} to pair this machine.`);
45131
45868
  process.exit(1);
45132
45869
  }
45133
45870
  return device;
@@ -45136,9 +45873,9 @@ function requirePaired2() {
45136
45873
  // src/commands/setup.ts
45137
45874
  init_auth();
45138
45875
  init_colors();
45139
- var setupCommand = new Command("setup").description("Set up this computer as a relay (no project, no launch)").action(async () => {
45876
+ var connectCommand = new Command("connect").alias("setup").description("Connect this computer to gipity.ai so the web CLI can drive it (no project, no launch)").action(async () => {
45140
45877
  try {
45141
- console.log(` ${bold("Gipity setup")} ${muted("- get this computer ready as a relay")}`);
45878
+ console.log(` ${bold("Gipity connect")} ${muted("- let gipity.ai drive this computer")}`);
45142
45879
  console.log("");
45143
45880
  let auth = getAuth();
45144
45881
  if (auth && !sessionExpired()) {
@@ -45156,9 +45893,9 @@ var setupCommand = new Command("setup").description("Set up this computer as a r
45156
45893
  if (enabled) {
45157
45894
  const running2 = isRelayEnabled() && !isPaused();
45158
45895
  console.log(` ${success("Done")} \u2014 your relay ${running2 ? "is running in the background" : "is set up"} and will start with your computer.`);
45159
- console.log(` ${muted("Open")} ${brand("gipity.ai")} ${muted("and start a chat to drive Claude Code here. Manage it with `gipity relay status`.")}`);
45896
+ console.log(` ${muted("Open")} ${brand("gipity.ai")} ${muted("and start a chat to drive your coding agent here. Manage it with `gipity relay status`.")}`);
45160
45897
  } else {
45161
- console.log(` ${muted("No relay set up. Run `gipity setup` again anytime, or `gipity claude` to build with Gipity.")}`);
45898
+ console.log(` ${muted("No relay set up. Run `gipity connect` again anytime, or `gipity build` to start building.")}`);
45162
45899
  }
45163
45900
  console.log("");
45164
45901
  } catch (err) {
@@ -45174,15 +45911,15 @@ init_api();
45174
45911
  init_auth();
45175
45912
  init_utils();
45176
45913
  init_colors();
45177
- import { existsSync as existsSync22, rmSync as rmSync2, unlinkSync as unlinkSync8, readFileSync as readFileSync23, writeFileSync as writeFileSync14 } from "fs";
45178
- import { homedir as homedir16, platform as osPlatform4 } from "os";
45179
- import { join as join23, resolve as resolve17 } from "path";
45914
+ import { existsSync as existsSync23, rmSync as rmSync3, unlinkSync as unlinkSync8, readFileSync as readFileSync24, writeFileSync as writeFileSync16 } from "fs";
45915
+ import { homedir as homedir17, platform as osPlatform4 } from "os";
45916
+ import { join as join24, resolve as resolve18 } from "path";
45180
45917
  function removeGipityPluginConfig() {
45181
- const settingsPath = join23(homedir16(), ".claude", "settings.json");
45182
- if (!existsSync22(settingsPath)) return false;
45918
+ const settingsPath = join24(homedir17(), ".claude", "settings.json");
45919
+ if (!existsSync23(settingsPath)) return false;
45183
45920
  let settings;
45184
45921
  try {
45185
- settings = JSON.parse(readFileSync23(settingsPath, "utf-8"));
45922
+ settings = JSON.parse(readFileSync24(settingsPath, "utf-8"));
45186
45923
  } catch {
45187
45924
  return false;
45188
45925
  }
@@ -45197,7 +45934,7 @@ function removeGipityPluginConfig() {
45197
45934
  if (Object.keys(settings.extraKnownMarketplaces).length === 0) delete settings.extraKnownMarketplaces;
45198
45935
  changed = true;
45199
45936
  }
45200
- if (changed) writeFileSync14(settingsPath, JSON.stringify(settings, null, 2) + "\n");
45937
+ if (changed) writeFileSync16(settingsPath, JSON.stringify(settings, null, 2) + "\n");
45201
45938
  return changed;
45202
45939
  }
45203
45940
  function removeInstallerPathLines() {
@@ -45205,11 +45942,11 @@ function removeInstallerPathLines() {
45205
45942
  const marker = "# Added by the Gipity installer";
45206
45943
  const touched = [];
45207
45944
  for (const name of [".bashrc", ".zshrc", ".profile"]) {
45208
- const rc2 = join23(homedir16(), name);
45209
- if (!existsSync22(rc2)) continue;
45945
+ const rc2 = join24(homedir17(), name);
45946
+ if (!existsSync23(rc2)) continue;
45210
45947
  let text;
45211
45948
  try {
45212
- text = readFileSync23(rc2, "utf-8");
45949
+ text = readFileSync24(rc2, "utf-8");
45213
45950
  } catch {
45214
45951
  continue;
45215
45952
  }
@@ -45219,7 +45956,7 @@ function removeInstallerPathLines() {
45219
45956
  );
45220
45957
  if (kept.length === lines.length) continue;
45221
45958
  try {
45222
- writeFileSync14(rc2, kept.join("\n"));
45959
+ writeFileSync16(rc2, kept.join("\n"));
45223
45960
  touched.push(rc2);
45224
45961
  } catch {
45225
45962
  }
@@ -45227,11 +45964,11 @@ function removeInstallerPathLines() {
45227
45964
  return touched;
45228
45965
  }
45229
45966
  function tildify2(p) {
45230
- const home = homedir16();
45967
+ const home = homedir17();
45231
45968
  return p === home || p.startsWith(home + "/") ? "~" + p.slice(home.length) : p;
45232
45969
  }
45233
45970
  function resolveCliPath2() {
45234
- return resolve17(process.argv[1] ?? "gipity");
45971
+ return resolve18(process.argv[1] ?? "gipity");
45235
45972
  }
45236
45973
  async function stopDaemon() {
45237
45974
  if (!isDaemonRunning()) return;
@@ -45265,7 +46002,7 @@ function removeServiceUnit() {
45265
46002
  const r = spawnSyncCommand(argv2[0], argv2.slice(1), { stdio: "ignore" });
45266
46003
  if (r.status !== 0) allOk = false;
45267
46004
  }
45268
- if (existsSync22(plan2.path)) {
46005
+ if (existsSync23(plan2.path)) {
45269
46006
  try {
45270
46007
  unlinkSync8(plan2.path);
45271
46008
  } catch {
@@ -45288,9 +46025,9 @@ async function revokeDeviceBestEffort() {
45288
46025
  }
45289
46026
  var uninstallCommand = new Command("uninstall").description("Uninstall Gipity").option("--yes", "Skip confirmation prompts").action(async (opts) => {
45290
46027
  const autoYes = opts.yes || getAutoConfirm();
45291
- const gipityDir = join23(homedir16(), ".gipity");
45292
- const launcherBin = join23(gipityDir, "launcher", "bin", "gipity");
45293
- const installedViaLauncher = existsSync22(launcherBin);
46028
+ const gipityDir = join24(homedir17(), ".gipity");
46029
+ const launcherBin = join24(gipityDir, "launcher", "bin", "gipity");
46030
+ const installedViaLauncher = existsSync23(launcherBin);
45294
46031
  console.log(`${bold("Gipity uninstall")} - this will:`);
45295
46032
  console.log(`\u2022 Stop the running relay daemon (if any)`);
45296
46033
  console.log(`\u2022 Remove the OS autostart service (launchd / systemd / Task Scheduler)`);
@@ -45325,9 +46062,26 @@ var uninstallCommand = new Command("uninstall").description("Uninstall Gipity").
45325
46062
  } else {
45326
46063
  console.log(`${muted("No Gipity entries in Claude Code settings.")}`);
45327
46064
  }
45328
- if (existsSync22(gipityDir)) {
46065
+ if (grokInstallState().exists) {
46066
+ spawnSyncCommand(resolveCommand("grok"), ["plugin", "uninstall", "gipity", "--confirm"], {
46067
+ stdio: "ignore",
46068
+ timeout: 6e4
46069
+ });
46070
+ console.log(`${success("Gipity plugin removed from Grok.")}`);
46071
+ }
46072
+ const agentSkills = agentSkillsState();
46073
+ if (agentSkills.skills.length) {
46074
+ for (const name of agentSkills.skills) {
46075
+ try {
46076
+ rmSync3(join24(AGENTS_SKILLS_DIR, name), { recursive: true, force: true });
46077
+ } catch {
46078
+ }
46079
+ }
46080
+ console.log(`${success(`Removed ${agentSkills.skills.length} Gipity skills from ~/.agents/skills.`)}`);
46081
+ }
46082
+ if (existsSync23(gipityDir)) {
45329
46083
  try {
45330
- rmSync2(gipityDir, { recursive: true, force: true });
46084
+ rmSync3(gipityDir, { recursive: true, force: true });
45331
46085
  console.log(`${success(`Removed ${gipityDir}/`)}`);
45332
46086
  } catch (err) {
45333
46087
  console.error(`${error(`Could not remove ${gipityDir}: ${err?.message || err}`)}`);
@@ -45488,7 +46242,7 @@ gmailCommand.command("reply").description("Reply to a Gmail thread. Get thread-i
45488
46242
  }));
45489
46243
 
45490
46244
  // src/commands/text.ts
45491
- import { readFileSync as readFileSync24 } from "fs";
46245
+ import { readFileSync as readFileSync25 } from "fs";
45492
46246
  init_colors();
45493
46247
 
45494
46248
  // src/helpers/text-analysis.ts
@@ -45643,10 +46397,10 @@ function areAnagrams(a, b7) {
45643
46397
 
45644
46398
  // src/commands/text.ts
45645
46399
  function resolveInput(args, opts) {
45646
- if (opts.file) return readFileSync24(opts.file, "utf-8");
46400
+ if (opts.file) return readFileSync25(opts.file, "utf-8");
45647
46401
  if (args.length) return args.join(" ");
45648
46402
  try {
45649
- return readFileSync24(0, "utf-8");
46403
+ return readFileSync25(0, "utf-8");
45650
46404
  } catch {
45651
46405
  return "";
45652
46406
  }
@@ -45803,6 +46557,7 @@ var FLAG_ALIASES = {
45803
46557
  "--src": "--source-dir",
45804
46558
  "--srcdir": "--source-dir",
45805
46559
  "--parallel": "--concurrency",
46560
+ "--no-sync-output": "--discard-output",
45806
46561
  "--max": "--limit",
45807
46562
  "--from": "--since",
45808
46563
  "--after": "--since",
@@ -45851,9 +46606,9 @@ function collectRealFlags(argv2, program3) {
45851
46606
  }
45852
46607
 
45853
46608
  // src/trace.ts
45854
- import { openSync as openSync5, writeSync, mkdirSync as mkdirSync15 } from "fs";
45855
- import { join as join24 } from "path";
45856
- import { homedir as homedir17 } from "os";
46609
+ import { openSync as openSync5, writeSync, mkdirSync as mkdirSync17 } from "fs";
46610
+ import { join as join25 } from "path";
46611
+ import { homedir as homedir18 } from "os";
45857
46612
  var TRACE_KEY = /* @__PURE__ */ Symbol.for("gipity.traceOutput");
45858
46613
  function installOutputTrace(label2) {
45859
46614
  if (process.env.GIPITY_TRACE_OUTPUT !== "1") return;
@@ -45864,10 +46619,10 @@ function installOutputTrace(label2) {
45864
46619
  existing.emit({ event: "reenter", label: label2 });
45865
46620
  return;
45866
46621
  }
45867
- const dir = join24(process.env.GIPITY_DIR || join24(homedir17(), ".gipity"), "trace");
45868
- mkdirSync15(dir, { recursive: true });
46622
+ const dir = join25(process.env.GIPITY_DIR || join25(homedir18(), ".gipity"), "trace");
46623
+ mkdirSync17(dir, { recursive: true });
45869
46624
  const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", "_").replace(/:/g, "-");
45870
- const fd2 = openSync5(join24(dir, `${stamp}-pid${process.pid}.jsonl`), "a");
46625
+ const fd2 = openSync5(join25(dir, `${stamp}-pid${process.pid}.jsonl`), "a");
45871
46626
  const emit = (rec) => {
45872
46627
  try {
45873
46628
  writeSync(fd2, JSON.stringify({ t: (/* @__PURE__ */ new Date()).toISOString(), ...rec }) + "\n");
@@ -45903,11 +46658,11 @@ function installOutputTrace(label2) {
45903
46658
  // src/index.ts
45904
46659
  installOutputTrace("index");
45905
46660
  var __dirname = dirname13(fileURLToPath3(import.meta.url));
45906
- var pkg = JSON.parse(readFileSync25(resolve18(__dirname, "../package.json"), "utf-8"));
46661
+ var pkg = JSON.parse(readFileSync26(resolve19(__dirname, "../package.json"), "utf-8"));
45907
46662
  function versionLabel() {
45908
46663
  const base = `v${pkg.version}`;
45909
46664
  try {
45910
- const info2 = JSON.parse(readFileSync25(resolve18(__dirname, "build-info.json"), "utf-8"));
46665
+ const info2 = JSON.parse(readFileSync26(resolve19(__dirname, "build-info.json"), "utf-8"));
45911
46666
  if (info2?.sha) return `${base} (dev ${info2.sha}${info2.dirty ? ", modified" : ""})`;
45912
46667
  } catch {
45913
46668
  }
@@ -45949,7 +46704,7 @@ var servicesGroup = [serviceCommand, generateCommand, notifyCommand, paymentsCom
45949
46704
  var filesGroup = [syncCommand, fileCommand, pushCommand, uploadCommand, storageCommand];
45950
46705
  var gipGroup = [chatCommand, memoryCommand, agentCommand, approvalCommand, gmailCommand];
45951
46706
  var utilitiesGroup = [sandboxCommand, emailCommand, locationCommand, textCommand, bugCommand];
45952
- var connectGroup = [loginCommand, logoutCommand, claudeCommand, setupCommand, relayCommand, githubCommand, creditsCommand, doctorCommand, updateCommand, uninstallCommand];
46707
+ var connectGroup = [loginCommand, logoutCommand, buildCommand, connectCommand, relayCommand, githubCommand, creditsCommand, doctorCommand, updateCommand, uninstallCommand];
45953
46708
  var HELP_SECTIONS = [
45954
46709
  { title: "Start here", cmds: startGroup },
45955
46710
  { title: "App build & ship", cmds: buildGroup },
@@ -46007,9 +46762,9 @@ program2.configureHelp({
46007
46762
  lines.push("");
46008
46763
  lines.push(bold("Quick start:"));
46009
46764
  lines.push(` ${brand("gipity login")} ${dim("- authenticate first if you haven't already")}`);
46010
- lines.push(` ${brand("gipity init")} ${dim("- link this dir + write CLAUDE.md/AGENTS.md primers for your AI tool")}`);
46011
- lines.push(` ${brand("gipity setup")} ${dim("- set up this computer as a relay to drive from gipity.ai (no launch)")}`);
46012
- lines.push(` ${brand("gipity claude")} ${dim("- or launch Claude Code with Gipity tools wired in")}`);
46765
+ lines.push(` ${brand("gipity init")} ${dim("- link this dir + wire up every coding agent on this machine")}`);
46766
+ lines.push(` ${brand("gipity connect")} ${dim("- connect this computer to gipity.ai so the web CLI can drive it")}`);
46767
+ lines.push(` ${brand("gipity build")} ${dim("- or start from anywhere: pick a project, pick your agent, go")}`);
46013
46768
  lines.push("");
46014
46769
  lines.push(bold("Usage:"));
46015
46770
  lines.push(` ${cmd.name()} [options] [command]`);
@@ -46043,6 +46798,8 @@ for (const cmd of HELP_SECTIONS.flatMap((s) => s.cmds)) {
46043
46798
  `);
46044
46799
  program2.addCommand(cmd);
46045
46800
  }
46801
+ configureHelp(claudeCommand);
46802
+ program2.addCommand(claudeCommand, { hidden: true });
46046
46803
  program2.helpCommand(false);
46047
46804
  function manifestCommand(c) {
46048
46805
  return {