gipity 1.0.429 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3944,10 +3944,10 @@ function prompt(question) {
3944
3944
  return Promise.reject(new Error(`prompt() called without a TTY: ${question.trim()}`));
3945
3945
  }
3946
3946
  const rl2 = createInterface({ input: process.stdin, output: process.stdout });
3947
- return new Promise((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
  }
@@ -6705,7 +6706,7 @@ async function postForTarEntries(path5, body, retried = false) {
6705
6706
  if (!res.body) throw new ApiError(500, "EMPTY_RESPONSE", "Server returned no body");
6706
6707
  const extract3 = tar.extract();
6707
6708
  const entries = [];
6708
- const done = new Promise((resolve19, reject) => {
6709
+ const done = new Promise((resolve20, reject) => {
6709
6710
  extract3.on("entry", (header, stream, next) => {
6710
6711
  const chunks = [];
6711
6712
  stream.on("data", (c) => chunks.push(c));
@@ -6716,7 +6717,7 @@ async function postForTarEntries(path5, body, retried = false) {
6716
6717
  stream.on("error", reject);
6717
6718
  stream.resume();
6718
6719
  });
6719
- extract3.on("finish", () => resolve19());
6720
+ extract3.on("finish", () => resolve20());
6720
6721
  extract3.on("error", reject);
6721
6722
  });
6722
6723
  Readable.fromWeb(res.body).pipe(extract3);
@@ -6741,6 +6742,9 @@ async function sendMessage(message) {
6741
6742
  if (res.data.conversationGuid !== config.conversationGuid) {
6742
6743
  saveConfig({ ...config, conversationGuid: res.data.conversationGuid });
6743
6744
  }
6745
+ if (res.data.costWarning && !res.data.content) {
6746
+ return res.data.costWarning.message;
6747
+ }
6744
6748
  return res.data.content;
6745
6749
  }
6746
6750
  async function download(path5, retried = false) {
@@ -31858,9 +31862,9 @@ var {
31858
31862
  // src/index.ts
31859
31863
  init_config();
31860
31864
  init_utils();
31861
- import { readFileSync as readFileSync25 } from "fs";
31865
+ import { readFileSync as readFileSync26 } from "fs";
31862
31866
  import { fileURLToPath as fileURLToPath3 } from "url";
31863
- import { dirname as dirname13, resolve as resolve18 } from "path";
31867
+ import { dirname as dirname13, resolve as resolve19 } from "path";
31864
31868
 
31865
31869
  // src/helpers/output.ts
31866
31870
  var frameOpen = false;
@@ -31982,7 +31986,7 @@ Prefer the cheapest option that works - CLI and sandbox are instant and free, ap
31982
31986
  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
31987
  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
31988
 
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.
31989
+ 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
31990
 
31987
31991
  ## Use first-party services before reaching outside
31988
31992
 
@@ -32018,7 +32022,7 @@ The full "when to add a template" rule and the definition of done are spelled ou
32018
32022
 
32019
32023
  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
32024
 
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.
32025
+ \`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
32026
 
32023
32027
  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
32028
 
@@ -32074,7 +32078,7 @@ Every tool call returns its full output with that call. There is no output buffe
32074
32078
 
32075
32079
  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
32080
 
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.
32081
+ 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
32082
 
32079
32083
  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
32084
 
@@ -32290,7 +32294,7 @@ function run(label2, action) {
32290
32294
  init_api();
32291
32295
  init_config();
32292
32296
  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";
32297
+ 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
32298
  import { join as join4, relative, dirname as dirname4, extname as extname2, resolve as resolve4, sep } from "path";
32295
32299
  import { hostname } from "os";
32296
32300
  import { createHash as createHash2 } from "crypto";
@@ -32354,25 +32358,25 @@ async function hashFile(path5) {
32354
32358
  const hash = createHash("sha256");
32355
32359
  let size = 0;
32356
32360
  const stream = createReadStream(path5);
32357
- await new Promise((resolve19, reject) => {
32361
+ await new Promise((resolve20, reject) => {
32358
32362
  stream.on("data", (chunk) => {
32359
32363
  const buf = chunk;
32360
32364
  hash.update(buf);
32361
32365
  size += buf.length;
32362
32366
  });
32363
- stream.on("end", () => resolve19());
32367
+ stream.on("end", () => resolve20());
32364
32368
  stream.on("error", reject);
32365
32369
  });
32366
32370
  return { sha256: hash.digest("hex"), size };
32367
32371
  }
32368
32372
  function readRange(path5, start, end) {
32369
- return new Promise((resolve19, reject) => {
32373
+ return new Promise((resolve20, reject) => {
32370
32374
  const chunks = [];
32371
32375
  const stream = createReadStream(path5, { start, end });
32372
32376
  stream.on("data", (c) => {
32373
32377
  chunks.push(c);
32374
32378
  });
32375
- stream.on("end", () => resolve19(Buffer.concat(chunks)));
32379
+ stream.on("end", () => resolve20(Buffer.concat(chunks)));
32376
32380
  stream.on("error", reject);
32377
32381
  });
32378
32382
  }
@@ -32536,11 +32540,14 @@ async function uploadCompleteBatch(projectGuid, items) {
32536
32540
  // src/setup.ts
32537
32541
  init_platform();
32538
32542
  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";
32543
+ import { homedir as homedir3, tmpdir } from "os";
32544
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, readFileSync as readFileSync5, mkdtempSync, rmSync, cpSync, readdirSync as readdirSync2 } from "fs";
32545
+ init_config();
32541
32546
  var PRIMER_FILES = {
32542
32547
  claude: "CLAUDE.md",
32543
32548
  codex: "AGENTS.md",
32549
+ grok: "AGENTS.md",
32550
+ // Grok Build reads the AGENTS.md family (and CLAUDE.md) natively
32544
32551
  aider: "AGENTS.md",
32545
32552
  // shares the Codex primer; aider is pointed at it via .aider.conf.yml
32546
32553
  gemini: "GEMINI.md",
@@ -32555,6 +32562,7 @@ var DEFAULT_SYNC_IGNORE = [
32555
32562
  ".gipity.json",
32556
32563
  ".gipity/",
32557
32564
  ".claude/",
32565
+ ".codex/",
32558
32566
  ".gitignore",
32559
32567
  AIDER_CONF_FILE,
32560
32568
  // Home-directory junk: a project created inside a real home dir (or one that
@@ -32615,7 +32623,7 @@ var GIPITY_PLUGIN_ID = "gipity@gipity";
32615
32623
  var GIPITY_MARKETPLACE_NAME = "gipity";
32616
32624
  var GIPITY_MARKETPLACE_REPO = "GipityAI/skills";
32617
32625
  var LEGACY_MARKETPLACE_REPO = "GipityAI/claude-plugin";
32618
- var GIPITY_PLUGIN_VERSION = "0.5.0";
32626
+ var GIPITY_PLUGIN_VERSION = "0.7.0";
32619
32627
  function isGipityManagedHookCommand(command) {
32620
32628
  return (
32621
32629
  // Capture hooks: bare absolute runner path or the fire-time launcher.
@@ -32702,8 +32710,8 @@ function userScopeInstallState() {
32702
32710
  return { exists: false, current: false };
32703
32711
  }
32704
32712
  }
32705
- function claudeOnPath() {
32706
- const probe = spawnSyncCommand(process.platform === "win32" ? "where" : "which", ["claude"], {
32713
+ function binaryOnPath(bin) {
32714
+ const probe = spawnSyncCommand(process.platform === "win32" ? "where" : "which", [bin], {
32707
32715
  encoding: "utf-8"
32708
32716
  });
32709
32717
  return probe.status === 0 && !!probe.stdout?.toString().trim();
@@ -32711,7 +32719,7 @@ function claudeOnPath() {
32711
32719
  function ensureGipityPluginInstalled() {
32712
32720
  const state = userScopeInstallState();
32713
32721
  if (state.current) return;
32714
- if (!claudeOnPath()) return;
32722
+ if (!binaryOnPath("claude")) return;
32715
32723
  const claudeCmd = resolveCommand("claude");
32716
32724
  spawnSyncCommand(claudeCmd, ["plugin", "marketplace", "update", GIPITY_MARKETPLACE_NAME], {
32717
32725
  stdio: "ignore",
@@ -32723,6 +32731,146 @@ function ensureGipityPluginInstalled() {
32723
32731
  timeout: 12e4
32724
32732
  });
32725
32733
  }
32734
+ function grokInstallState() {
32735
+ try {
32736
+ const p = join3(homedir3(), ".grok", "installed-plugins", "registry.json");
32737
+ const data = JSON.parse(readFileSync5(p, "utf-8"));
32738
+ const versions = [];
32739
+ for (const repo of Object.values(data?.repos ?? {})) {
32740
+ const v7 = repo?.plugins?.gipity?.version;
32741
+ if (typeof v7 === "string") versions.push(v7);
32742
+ }
32743
+ return {
32744
+ exists: versions.length > 0,
32745
+ current: versions.some((v7) => versionGte(v7, GIPITY_PLUGIN_VERSION))
32746
+ };
32747
+ } catch {
32748
+ return { exists: false, current: false };
32749
+ }
32750
+ }
32751
+ function ensureGrokPluginInstalled() {
32752
+ const state = grokInstallState();
32753
+ if (state.current) return;
32754
+ if (!binaryOnPath("grok")) return;
32755
+ const grokCmd = resolveCommand("grok");
32756
+ const verb = state.exists ? ["plugin", "update", "gipity"] : ["plugin", "install", GIPITY_MARKETPLACE_REPO, "--trust"];
32757
+ const res = spawnSyncCommand(grokCmd, verb, { stdio: "ignore", timeout: 12e4 });
32758
+ if (!state.exists && res.status === 0) {
32759
+ console.log("Installed the Gipity plugin for Grok (skills + file-sync hooks).");
32760
+ }
32761
+ }
32762
+ var AGENTS_SKILLS_DIR = join3(homedir3(), ".agents", "skills");
32763
+ var AGENT_HOOKS_DIR = join3(homedir3(), ".gipity", "agent-hooks");
32764
+ var AGENT_SKILLS_MANIFEST = join3(homedir3(), ".gipity", "agent-skills.json");
32765
+ function agentSkillsState() {
32766
+ try {
32767
+ const m = JSON.parse(readFileSync5(AGENT_SKILLS_MANIFEST, "utf-8"));
32768
+ return {
32769
+ current: typeof m?.version === "string" && versionGte(m.version, GIPITY_PLUGIN_VERSION),
32770
+ skills: Array.isArray(m?.skills) ? m.skills : []
32771
+ };
32772
+ } catch {
32773
+ return { current: false, skills: [] };
32774
+ }
32775
+ }
32776
+ function ensureAgentSkillsInstalled() {
32777
+ if (agentSkillsState().current) return;
32778
+ if (!binaryOnPath("git")) return;
32779
+ const tmp = mkdtempSync(join3(tmpdir(), "gipity-skills-"));
32780
+ try {
32781
+ const clone = spawnSyncCommand(
32782
+ resolveCommand("git"),
32783
+ ["clone", "--depth", "1", `https://github.com/${GIPITY_MARKETPLACE_REPO}.git`, join3(tmp, "repo")],
32784
+ { stdio: "ignore", timeout: 12e4 }
32785
+ );
32786
+ if (clone.status !== 0) return;
32787
+ const repo = join3(tmp, "repo");
32788
+ let version = GIPITY_PLUGIN_VERSION;
32789
+ try {
32790
+ const manifest = JSON.parse(readFileSync5(join3(repo, ".claude-plugin", "plugin.json"), "utf-8"));
32791
+ if (typeof manifest?.version === "string") version = manifest.version;
32792
+ } catch {
32793
+ }
32794
+ const skillsSrc = join3(repo, "skills");
32795
+ const names = [];
32796
+ for (const entry of readdirSync2(skillsSrc, { withFileTypes: true })) {
32797
+ if (!entry.isDirectory()) continue;
32798
+ if (!existsSync4(join3(skillsSrc, entry.name, "SKILL.md"))) continue;
32799
+ mkdirSync3(AGENTS_SKILLS_DIR, { recursive: true });
32800
+ cpSync(join3(skillsSrc, entry.name), join3(AGENTS_SKILLS_DIR, entry.name), {
32801
+ recursive: true,
32802
+ force: true
32803
+ });
32804
+ names.push(entry.name);
32805
+ }
32806
+ mkdirSync3(AGENT_HOOKS_DIR, { recursive: true });
32807
+ for (const script of readdirSync2(join3(repo, "hooks", "scripts"))) {
32808
+ cpSync(join3(repo, "hooks", "scripts", script), join3(AGENT_HOOKS_DIR, script), { force: true });
32809
+ }
32810
+ writeFileSync4(AGENT_SKILLS_MANIFEST, JSON.stringify({ version, skills: names }, null, 2) + "\n");
32811
+ console.log(`Installed ${names.length} Gipity skills for Codex (~/.agents/skills).`);
32812
+ } catch {
32813
+ } finally {
32814
+ rmSync(tmp, { recursive: true, force: true });
32815
+ }
32816
+ }
32817
+ function applyCodexHooks(existing) {
32818
+ const launcher = join3(AGENT_HOOKS_DIR, "launch.sh");
32819
+ const cmd = (script, ...args) => [`sh "${launcher}" "${join3(AGENT_HOOKS_DIR, script)}"`, ...args].join(" ");
32820
+ const wanted = [
32821
+ { event: "PostToolUse", matcher: "Edit|Write", command: cmd("sync-push.cjs"), timeout: 30 },
32822
+ { event: "UserPromptSubmit", command: cmd("sync-pull.cjs"), timeout: 300 },
32823
+ // Session capture: mirror the Codex session into the Gipity web CLI.
32824
+ { event: "SessionStart", command: cmd("capture.cjs", "codex", "session-start"), timeout: 30 },
32825
+ { event: "PostToolUse", command: cmd("capture.cjs", "codex", "post-tool-use"), timeout: 30 },
32826
+ { event: "Stop", command: cmd("capture.cjs", "codex", "stop"), timeout: 60 }
32827
+ ];
32828
+ let settings = {};
32829
+ if (existing !== null) {
32830
+ try {
32831
+ settings = JSON.parse(existing);
32832
+ } catch {
32833
+ return null;
32834
+ }
32835
+ }
32836
+ const hooks = settings.hooks ?? (settings.hooks = {});
32837
+ let changed = false;
32838
+ for (const w of wanted) {
32839
+ const groups = Array.isArray(hooks[w.event]) ? hooks[w.event] : hooks[w.event] = [];
32840
+ const present = groups.some(
32841
+ (g) => Array.isArray(g?.hooks) && g.hooks.some(
32842
+ (h) => typeof h?.command === "string" && h.command === w.command
32843
+ )
32844
+ );
32845
+ if (present) continue;
32846
+ const group = { hooks: [{ type: "command", command: w.command, timeout: w.timeout }] };
32847
+ if (w.matcher) group.matcher = w.matcher;
32848
+ groups.push(group);
32849
+ changed = true;
32850
+ }
32851
+ return changed ? JSON.stringify(settings, null, 2) + "\n" : null;
32852
+ }
32853
+ function setupCodexHooks() {
32854
+ if (process.platform === "win32") return;
32855
+ const cwd = resolve3(process.cwd());
32856
+ if (cwd === resolve3(homedir3())) return;
32857
+ const path5 = join3(cwd, ".codex", "hooks.json");
32858
+ const existing = existsSync4(path5) ? readFileSync5(path5, "utf-8") : null;
32859
+ const next = applyCodexHooks(existing);
32860
+ if (next === null) return;
32861
+ mkdirSync3(dirname3(path5), { recursive: true });
32862
+ writeFileSync4(path5, next);
32863
+ if (existing === null) {
32864
+ console.log("Wrote Codex sync + session-capture hooks (.codex/hooks.json) - approve them once with /hooks inside Codex.");
32865
+ } else {
32866
+ console.log("Updated Codex hooks (.codex/hooks.json) - if Codex asks, re-approve them via /hooks.");
32867
+ }
32868
+ }
32869
+ function setupCodexIntegration() {
32870
+ if (!binaryOnPath("codex")) return;
32871
+ ensureAgentSkillsInstalled();
32872
+ setupCodexHooks();
32873
+ }
32726
32874
  function setupClaudeHooks() {
32727
32875
  ensureGipityPlugin();
32728
32876
  const cwd = resolve3(process.cwd());
@@ -32744,14 +32892,21 @@ function setupClaudeHooks() {
32744
32892
  }
32745
32893
  var GIPITY_BLOCK_BEGIN = "<!-- BEGIN GIPITY INTEGRATION - auto-generated by gipity, do not edit this block -->";
32746
32894
  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");
32895
+ function renderManagedBlock(apiBase2) {
32896
+ let body = [SKILLS_CONTENT, BUILD_VS_NON_BUILD_RULE, DEFINITION_OF_DONE].join("\n\n");
32897
+ const base = apiBase2.replace(/\/+$/, "");
32898
+ if (base !== DEFAULT_API_BASE) {
32899
+ body = body.replaceAll(DEFAULT_API_BASE, base);
32900
+ 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\`.`;
32901
+ const headingEnd = body.indexOf("\n");
32902
+ body = body.slice(0, headingEnd + 1) + "\n" + note + "\n" + body.slice(headingEnd + 1);
32903
+ }
32749
32904
  return `${GIPITY_BLOCK_BEGIN}
32750
32905
  ${body}
32751
32906
  ${GIPITY_BLOCK_END}`;
32752
32907
  }
32753
- function applySkillsBlock(existing) {
32754
- const block = renderManagedBlock();
32908
+ function applySkillsBlock(existing, apiBase2 = DEFAULT_API_BASE) {
32909
+ const block = renderManagedBlock(apiBase2);
32755
32910
  if (existing === null) return block + "\n";
32756
32911
  let next;
32757
32912
  const beginIdx = existing.indexOf(GIPITY_BLOCK_BEGIN);
@@ -32770,7 +32925,7 @@ function writeSkillsFile(relPath, wrap) {
32770
32925
  const path5 = resolve3(process.cwd(), relPath);
32771
32926
  mkdirSync3(dirname3(path5), { recursive: true });
32772
32927
  const existing = existsSync4(path5) ? readFileSync5(path5, "utf-8") : null;
32773
- const baseNext = applySkillsBlock(existing);
32928
+ const baseNext = applySkillsBlock(existing, resolveApiBase());
32774
32929
  const next = wrap && existing === null ? wrap(baseNext) : baseNext;
32775
32930
  if (next !== existing) writeFileSync4(path5, next);
32776
32931
  }
@@ -32834,14 +32989,22 @@ ${block}`
32834
32989
  );
32835
32990
  }
32836
32991
  var SUPPORTED_TOOLS = [
32837
- { key: "claude", label: "Claude Code (CLAUDE.md)", setup: setupClaudeMd },
32838
- { key: "codex", label: "OpenAI Codex (AGENTS.md)", setup: setupAgentsMd },
32992
+ { key: "claude", label: "Claude Code (CLAUDE.md + Gipity plugin)", setup: setupClaudeMd, integrate: setupClaudeHooks },
32993
+ { key: "codex", label: "OpenAI Codex (AGENTS.md + skills + sync hooks)", setup: setupAgentsMd, integrate: setupCodexIntegration },
32994
+ { key: "grok", label: "Grok Build (AGENTS.md + Gipity plugin)", setup: setupAgentsMd, integrate: ensureGrokPluginInstalled },
32839
32995
  { key: "aider", label: "Aider (AGENTS.md + .aider.conf.yml)", setup: setupAiderMd, optIn: true },
32840
32996
  { key: "gemini", label: "Gemini CLI (GEMINI.md)", setup: setupGeminiMd },
32841
32997
  { key: "copilot", label: "GitHub Copilot (.github/copilot-instructions.md)", setup: setupCopilotMd },
32842
32998
  { key: "cursor", label: "Cursor (.cursor/rules/gipity.mdc)", setup: setupCursorMd }
32843
32999
  ];
32844
33000
  var DEFAULT_TOOLS = SUPPORTED_TOOLS.filter((t) => !t.optIn);
33001
+ function setupProjectTools(tools = DEFAULT_TOOLS) {
33002
+ for (const t of tools) {
33003
+ t.setup();
33004
+ t.integrate?.();
33005
+ }
33006
+ setupGitignore();
33007
+ }
32845
33008
  function setupGitignore() {
32846
33009
  const gitignorePath = resolve3(process.cwd(), ".gitignore");
32847
33010
  const entries = [".gipity/", ".gipity.json", ...SCRATCH_IGNORE];
@@ -32986,7 +33149,7 @@ function walkLocal(root, ignorePatterns, baseline) {
32986
33149
  function walk(dir) {
32987
33150
  let entries;
32988
33151
  try {
32989
- entries = readdirSync2(dir, { withFileTypes: true });
33152
+ entries = readdirSync3(dir, { withFileTypes: true });
32990
33153
  } catch {
32991
33154
  return;
32992
33155
  }
@@ -33065,7 +33228,7 @@ async function fetchRemote(projectGuid) {
33065
33228
  function extractTarToMap(stream, idleMs, onBytes, keep) {
33066
33229
  const extract3 = tar2.extract();
33067
33230
  const files = /* @__PURE__ */ new Map();
33068
- return new Promise((resolve19, reject) => {
33231
+ return new Promise((resolve20, reject) => {
33069
33232
  let settled = false;
33070
33233
  let idle;
33071
33234
  const arm = () => {
@@ -33101,7 +33264,7 @@ function extractTarToMap(stream, idleMs, onBytes, keep) {
33101
33264
  });
33102
33265
  entryStream.resume();
33103
33266
  });
33104
- extract3.on("finish", () => done(resolve19, files));
33267
+ extract3.on("finish", () => done(resolve20, files));
33105
33268
  extract3.on("error", (e) => done(reject, e));
33106
33269
  stream.on("error", (e) => done(reject, e));
33107
33270
  arm();
@@ -33860,7 +34023,7 @@ async function syncInner(projectGuid, root, ignore2, opts, interactive) {
33860
34023
  function cleanupEmptyDirs(root, emptiedDirs) {
33861
34024
  const isEmpty = (dir) => {
33862
34025
  try {
33863
- return readdirSync2(dir).length === 0;
34026
+ return readdirSync3(dir).length === 0;
33864
34027
  } catch {
33865
34028
  return false;
33866
34029
  }
@@ -34144,7 +34307,7 @@ init_utils();
34144
34307
 
34145
34308
  // src/adopt-cwd.ts
34146
34309
  init_api();
34147
- import { readdirSync as readdirSync4, statSync as statSync3 } from "fs";
34310
+ import { readdirSync as readdirSync5, statSync as statSync3 } from "fs";
34148
34311
  import { join as join8, resolve as resolve6, sep as sep2, parse as parsePath } from "path";
34149
34312
  import { homedir as homedir6 } from "os";
34150
34313
 
@@ -34152,7 +34315,7 @@ import { homedir as homedir6 } from "os";
34152
34315
  init_config();
34153
34316
 
34154
34317
  // src/template-vars.ts
34155
- import { promises as fs, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
34318
+ import { promises as fs, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
34156
34319
  import { join as join5, relative as relative2, extname as extname3 } from "path";
34157
34320
  var SUBSTITUTABLE_EXTS = /* @__PURE__ */ new Set([
34158
34321
  ".html",
@@ -34219,7 +34382,7 @@ function* walkTextFiles(root) {
34219
34382
  const dir = stack.pop();
34220
34383
  let entries;
34221
34384
  try {
34222
- entries = readdirSync3(dir, { withFileTypes: true });
34385
+ entries = readdirSync4(dir, { withFileTypes: true });
34223
34386
  } catch {
34224
34387
  continue;
34225
34388
  }
@@ -34308,10 +34471,7 @@ async function finalizeLocalProject(opts) {
34308
34471
  } catch (err) {
34309
34472
  if (opts.sync === "strict") throw err;
34310
34473
  }
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();
34474
+ setupProjectTools(opts.tools ?? DEFAULT_TOOLS);
34315
34475
  return { applied };
34316
34476
  }
34317
34477
 
@@ -34490,7 +34650,7 @@ function scanForAdoption(cwd) {
34490
34650
  if (truncated) return;
34491
34651
  let entries;
34492
34652
  try {
34493
- entries = readdirSync4(dir);
34653
+ entries = readdirSync5(dir);
34494
34654
  } catch {
34495
34655
  return;
34496
34656
  }
@@ -34526,7 +34686,7 @@ function scanForAdoption(cwd) {
34526
34686
  function isLikelyEmpty(cwd) {
34527
34687
  let entries;
34528
34688
  try {
34529
- entries = readdirSync4(cwd);
34689
+ entries = readdirSync5(cwd);
34530
34690
  } catch {
34531
34691
  return true;
34532
34692
  }
@@ -34564,7 +34724,7 @@ function canAdoptCwd(cwd) {
34564
34724
  function countGitRepoChildren(dir) {
34565
34725
  let entries;
34566
34726
  try {
34567
- entries = readdirSync4(dir);
34727
+ entries = readdirSync5(dir);
34568
34728
  } catch {
34569
34729
  return 0;
34570
34730
  }
@@ -34676,14 +34836,15 @@ function resolveTools(forFlag) {
34676
34836
  (t) => requested.includes(t.key) || !t.optIn && requested.includes("all")
34677
34837
  );
34678
34838
  }
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(
34839
+ 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
34840
  "--for <tools>",
34681
34841
  `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
34842
  ).addHelpText("after", `
34683
34843
  Examples:
34684
34844
  $ gipity init Link cwd as a new project (slug = dir name).
34685
34845
  $ gipity init my-app Link cwd with an explicit slug.
34686
- $ gipity init --for codex Write only AGENTS.md (skip Claude/Cursor/etc).
34846
+ $ gipity init --for codex AGENTS.md + Codex skills/sync hooks only.
34847
+ $ gipity init --for grok AGENTS.md + the Gipity plugin in Grok only.
34687
34848
  $ gipity init --for cursor,gemini Write only the Cursor + Gemini primers.
34688
34849
  $ gipity init --for aider AGENTS.md + a read: entry in .aider.conf.yml
34689
34850
  (aider auto-reads nothing, so it's opt-in).
@@ -34704,9 +34865,6 @@ Working with an existing Gipity project:
34704
34865
  process.exit(1);
34705
34866
  }
34706
34867
  const wantsClaude = tools.some((t) => t.key === "claude");
34707
- const writeAllPrimers = () => {
34708
- for (const t of tools) t.setup();
34709
- };
34710
34868
  const primerSummary = tools.map((t) => t.label).join(", ");
34711
34869
  try {
34712
34870
  const auth = getAuth();
@@ -34718,9 +34876,7 @@ Working with an existing Gipity project:
34718
34876
  if (existsSync8(resolve7(cwd, ".gipity.json"))) {
34719
34877
  const existing = getConfig();
34720
34878
  console.log(`Already linked to ${info(`"${existing?.projectSlug ?? ""}"`)} ${muted(`(${existing?.projectGuid ?? ""})`)}`);
34721
- if (wantsClaude) setupClaudeHooks();
34722
- writeAllPrimers();
34723
- setupGitignore();
34879
+ setupProjectTools(tools);
34724
34880
  if (existing) {
34725
34881
  let changed = false;
34726
34882
  const cur = existing.ignore ?? (existing.ignore = []);
@@ -34769,7 +34925,7 @@ Working with an existing Gipity project:
34769
34925
  const sizeStr = scan.truncated ? `>${formatBytes(ADOPT_THRESHOLDS.REFUSE_BYTES)}` : formatBytes(scan.bytes);
34770
34926
  const fileStr = scan.truncated ? `>${ADOPT_THRESHOLDS.REFUSE_FILES}` : `${scan.files}`;
34771
34927
  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".'));
34928
+ console.error(muted('Move into a subdirectory, or use `gipity build` and pick "Create new project".'));
34773
34929
  process.exit(1);
34774
34930
  }
34775
34931
  if (scan.tier === "moderate") {
@@ -34808,7 +34964,7 @@ Working with an existing Gipity project:
34808
34964
  }
34809
34965
  console.log(success(`Wrote primer files: ${primerSummary}.`));
34810
34966
  if (wantsClaude) {
34811
- console.log(success("Ready! Run `gipity claude` for Claude Code, or open this directory in your other AI coding tool."));
34967
+ console.log(success("Ready! Run your coding agent here (claude, codex, grok), or `gipity build` to launch one with a picker."));
34812
34968
  if (opts.capture === false) {
34813
34969
  console.log(muted("Session recording is off for this project (captureHooks: false in .gipity.json)."));
34814
34970
  } else {
@@ -34983,7 +35139,7 @@ var pushCommand = new Command("push").description("Push one or more files").argu
34983
35139
 
34984
35140
  // src/commands/upload.ts
34985
35141
  init_config();
34986
- import { statSync as statSync4, readdirSync as readdirSync5 } from "fs";
35142
+ import { statSync as statSync4, readdirSync as readdirSync6 } from "fs";
34987
35143
  import { join as join10, basename as basename2, posix, resolve as resolve10, dirname as dirname6 } from "path";
34988
35144
  init_utils();
34989
35145
  init_colors();
@@ -34992,7 +35148,7 @@ function walkFiles(root) {
34992
35148
  const stack = [root];
34993
35149
  while (stack.length) {
34994
35150
  const dir = stack.pop();
34995
- for (const entry of readdirSync5(dir, { withFileTypes: true })) {
35151
+ for (const entry of readdirSync6(dir, { withFileTypes: true })) {
34996
35152
  const full = join10(dir, entry.name);
34997
35153
  if (entry.isDirectory()) stack.push(full);
34998
35154
  else if (entry.isFile()) out.push(full);
@@ -35105,7 +35261,7 @@ init_config();
35105
35261
  // src/page-fixtures.ts
35106
35262
  init_api();
35107
35263
  init_config();
35108
- import { readFileSync as readFileSync11, statSync as statSync5, existsSync as existsSync10, readdirSync as readdirSync6 } from "node:fs";
35264
+ import { readFileSync as readFileSync11, statSync as statSync5, existsSync as existsSync10, readdirSync as readdirSync7 } from "node:fs";
35109
35265
  import { basename as basename3, resolve as resolve11, relative as relative3, join as join11 } from "node:path";
35110
35266
  var FIND_SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", ".gipity", "dist", "build", ".next", "coverage"]);
35111
35267
  function findByBasename(root, name, limit = 3) {
@@ -35117,7 +35273,7 @@ function findByBasename(root, name, limit = 3) {
35117
35273
  visited++;
35118
35274
  let entries;
35119
35275
  try {
35120
- entries = readdirSync6(dir, { withFileTypes: true });
35276
+ entries = readdirSync7(dir, { withFileTypes: true });
35121
35277
  } catch {
35122
35278
  continue;
35123
35279
  }
@@ -35190,7 +35346,7 @@ async function uploadCameraFeed(projectGuid, localPath) {
35190
35346
  }
35191
35347
 
35192
35348
  // 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)`.";
35349
+ 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
35350
  function normalizeEvalResult(raw) {
35195
35351
  const trimmed = (raw ?? "").trim();
35196
35352
  if (trimmed === "" || trimmed === "null" || trimmed === "undefined") {
@@ -35350,8 +35506,8 @@ function evalExecTimeoutMessage(result, budgetMs) {
35350
35506
  ` + (budgetOverrunHint("in-page budget", budgetMs) ?? "") + `
35351
35507
  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
35508
  }
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(
35509
+ var JS_DECOY_FLAGS = ["--js", "--javascript", "--script", "--code", "--expr", "--eval", "--exec", "--action"];
35510
+ 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
35511
  "--step <expr>",
35356
35512
  `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
35513
  (val, prev) => [...prev, val],
@@ -35370,7 +35526,7 @@ var pageEvalCommand = new Command("eval").description("Evaluate JS in a real bro
35370
35526
  ).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
35527
  "--timeout <ms>",
35372
35528
  `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 () => {
35529
+ ).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
35530
  const decoy = JS_DECOY_FLAGS.find((f) => opts[f.slice(2)] !== void 0);
35375
35531
  if (decoy) {
35376
35532
  pageEvalCommand.error(
@@ -35524,6 +35680,8 @@ ${hint}`);
35524
35680
  if (d.auth?.requested) {
35525
35681
  const who = getAuth()?.email;
35526
35682
  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)")}`);
35683
+ } else {
35684
+ console.log(muted("Auth: anonymous visitor (signed out; pass --auth to run as your Gipity account)"));
35527
35685
  }
35528
35686
  if (camera) console.log(`${muted("Camera:")} ${camera.name} ${muted("(played as the webcam feed; getUserMedia resolves)")}`);
35529
35687
  if (hosted.length) console.log(`${muted("Fixtures:")} ${hosted.map((h) => h.name).join(", ")}`);
@@ -35653,7 +35811,10 @@ function fmtMs(ms2) {
35653
35811
  return ms2 >= 1e3 ? `${(ms2 / 1e3).toFixed(1)}s` : `${ms2}ms`;
35654
35812
  }
35655
35813
  function printAuthLine(auth) {
35656
- if (!auth?.requested) return;
35814
+ if (!auth?.requested) {
35815
+ console.log(`${label("Auth")} ${muted("anonymous visitor (signed out; pass --auth to capture as your Gipity account)")}`);
35816
+ return;
35817
+ }
35657
35818
  const who = getAuth()?.email;
35658
35819
  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
35820
  }
@@ -35751,7 +35912,7 @@ function splitCsv(values) {
35751
35912
  function appendOption(value, previous = []) {
35752
35913
  return [...previous, value];
35753
35914
  }
35754
- var ACTION_DECOY_FLAGS = ["--eval", "--js", "--javascript", "--script", "--code", "--exec"];
35915
+ var ACTION_ALIAS_FLAGS = ["--eval", "--js", "--javascript", "--script", "--code", "--exec"];
35755
35916
  function buildWaitForGate(selector, timeoutMs) {
35756
35917
  const sel = JSON.stringify(selector);
35757
35918
  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 +35921,14 @@ var WAIT_FOR_MAX_MS2 = 15e3;
35760
35921
  var WAIT_FOR_DEFAULT_MS = 15e3;
35761
35922
  var MAX_POST_LOAD_DELAY_MS = 3e4;
35762
35923
  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
- );
35924
+ 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 () => {
35925
+ const aliasFlag = ACTION_ALIAS_FLAGS.find((f) => opts[f.slice(2)] !== void 0);
35926
+ if (aliasFlag) {
35927
+ console.error(muted(
35928
+ `Note: treating ${aliasFlag} as --action \u2014 it runs your JS in the page before the capture. --action is the canonical flag.`
35929
+ ));
35769
35930
  }
35931
+ const actionScript = [opts.action, ...ACTION_ALIAS_FLAGS.map((f) => opts[f.slice(2)])].filter(Boolean).join("\n");
35770
35932
  const delayRaw = opts.wait ?? opts.postLoadDelay;
35771
35933
  const chosenDelay = delayRaw !== void 0;
35772
35934
  let postLoadDelayMs = chosenDelay ? parseInt(String(delayRaw), 10) : 1e3;
@@ -35800,6 +35962,20 @@ var pageScreenshotCommand = new Command("screenshot").description("Screenshot a
35800
35962
  }
35801
35963
  const deviceNames = splitCsv(opts.device);
35802
35964
  const viewportStrs = splitCsv(opts.viewport);
35965
+ if (opts.width !== void 0 || opts.height !== void 0) {
35966
+ if (opts.width === void 0 || opts.height === void 0) {
35967
+ pageScreenshotCommand.error(
35968
+ "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)"
35969
+ );
35970
+ }
35971
+ if (!/^\d+$/.test(String(opts.width)) || !/^\d+$/.test(String(opts.height))) {
35972
+ pageScreenshotCommand.error("error: --width and --height must be integers (px)");
35973
+ }
35974
+ viewportStrs.push(`${opts.width}x${opts.height}`);
35975
+ if (parseInt(String(opts.width), 10) <= 500) {
35976
+ console.error(muted("Tip: --device mobile emulates a real handset (touch events, mobile user-agent, DPR) \u2014 a raw viewport only resizes the window."));
35977
+ }
35978
+ }
35803
35979
  const customViewports = [
35804
35980
  ...deviceNames.map(resolveDevice),
35805
35981
  ...viewportStrs.map(parseViewportString)
@@ -35823,7 +35999,7 @@ var pageScreenshotCommand = new Command("screenshot").description("Screenshot a
35823
35999
  }
35824
36000
  const preCapture = [
35825
36001
  opts.waitFor ? buildWaitForGate(opts.waitFor, waitForTimeoutMs) : "",
35826
- opts.action ?? ""
36002
+ actionScript
35827
36003
  ].filter(Boolean).join("\n");
35828
36004
  const body = {
35829
36005
  url,
@@ -35945,7 +36121,7 @@ ${brand("@ " + dims)}${deviceTag}`);
35945
36121
  printVfsLine(s);
35946
36122
  }
35947
36123
  }));
35948
- for (const f of ACTION_DECOY_FLAGS) pageScreenshotCommand.addOption(new Option(`${f} <value>`).hideHelp());
36124
+ for (const f of ACTION_ALIAS_FLAGS) pageScreenshotCommand.addOption(new Option(`${f} <js>`, "Alias for --action").hideHelp());
35949
36125
  pageScreenshotCommand.addHelpText("after", `
35950
36126
  Examples:
35951
36127
  gipity page screenshot "https://dev.gipity.ai/me/app/"
@@ -35992,7 +36168,7 @@ function shortUrl(url, truncate = true, maxLen = 100) {
35992
36168
  const tailLen = Math.floor(keep / 2);
35993
36169
  return result.slice(0, headLen) + "\u2026" + result.slice(-tailLen);
35994
36170
  }
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) => {
36171
+ 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
36172
  if (opts.screenshot !== void 0) {
35997
36173
  console.error(error("page inspect does not capture screenshots. Use page screenshot:"));
35998
36174
  console.error(` gipity page screenshot ${url}${typeof opts.screenshot === "string" ? ` -o ${opts.screenshot}` : ""}`);
@@ -36066,6 +36242,8 @@ async function inspectPage(url, opts = {}) {
36066
36242
  if (b7.auth?.requested) {
36067
36243
  const who = getAuth()?.email;
36068
36244
  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)")}`);
36245
+ } else {
36246
+ console.log(muted("Auth: anonymous visitor (signed out; pass --auth to load as your Gipity account)"));
36069
36247
  }
36070
36248
  console.log(`${muted("Title:")} ${b7.title || "(none)"}`);
36071
36249
  console.log(`${muted("Elements:")} ${b7.elementCount || 0}`);
@@ -36336,11 +36514,15 @@ dbCommand.command("list").description("List databases").option("--all", "List da
36336
36514
  }
36337
36515
  }));
36338
36516
  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.`);
36517
+ const config = requireConfig();
36518
+ await post(`/projects/${config.projectGuid}/db/manage`, {
36519
+ action: "create",
36520
+ name
36521
+ });
36340
36522
  if (opts.json) {
36341
- console.log(JSON.stringify({ response }));
36523
+ console.log(JSON.stringify({ success: true, name }));
36342
36524
  } else {
36343
- console.log(response);
36525
+ console.log(success(`Created database '${name}'.`));
36344
36526
  }
36345
36527
  }));
36346
36528
  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 +36610,8 @@ memoryCommand.command("delete <topic>").description("Delete a topic").option("--
36428
36610
  // src/commands/sandbox.ts
36429
36611
  init_api();
36430
36612
  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";
36613
+ import { readFileSync as readFileSync13, existsSync as existsSync11, statSync as statSync6, mkdirSync as mkdirSync8, writeFileSync as writeFileSync9 } from "fs";
36614
+ import { dirname as dirname8, extname as extname4, relative as relative4, resolve as resolve12, sep as sep3 } from "path";
36433
36615
  init_colors();
36434
36616
  var LANG_MAP = {
36435
36617
  js: "javascript",
@@ -36547,13 +36729,13 @@ function resolveRelativeCwd() {
36547
36729
  return rel2.split(/[\\/]/).filter(Boolean).join("/");
36548
36730
  }
36549
36731
  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(
36732
+ 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
36733
  "--input <path>",
36552
36734
  "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
36735
  (v7, prev) => [...prev ?? [], v7]
36554
36736
  ).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.',
36737
+ "--discard-output <glob>",
36738
+ "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
36739
  (v7, prev) => [...prev, v7],
36558
36740
  []
36559
36741
  ).option("--json", "Output as JSON").addHelpText("after", `
@@ -36561,6 +36743,11 @@ By default the whole project is auto-mirrored into /work/ (up to 1 GB) -
36561
36743
  so your code can reference project files by their relative path, and any
36562
36744
  file you write lands back in the project. No manual copy needed.
36563
36745
 
36746
+ Exception: tmp/ is scratch. An output written under tmp/ comes back to
36747
+ your local tmp/ for inspection but is never saved to the project - use it
36748
+ for previews and other look-once artifacts (no cleanup needed). To drop
36749
+ an output entirely, --discard-output <glob>.
36750
+
36564
36751
  Use --input only for projects over the auto-mirror cap, or when you want
36565
36752
  to restrict what the sandbox sees.
36566
36753
 
@@ -36577,6 +36764,11 @@ Examples:
36577
36764
  $ gipity sandbox run --language python \\
36578
36765
  "import pandas as pd; print(pd.read_csv('data/sales.csv').describe())"
36579
36766
 
36767
+ # Preview a generated PDF without saving the preview: write it to tmp/
36768
+ $ gipity sandbox run bash \\
36769
+ "pdftoppm -png -r 90 docs/report.pdf tmp/preview"
36770
+ # -> tmp/preview-1.png lands on local disk only; Read it, done.
36771
+
36580
36772
  # Run a script file directly (language inferred from .py)
36581
36773
  $ gipity sandbox run --file build_report.py
36582
36774
  $ gipity sandbox run python build_report.py # same thing, interpreter shorthand
@@ -36598,6 +36790,13 @@ GCC/Rust).
36598
36790
  let inlineCode;
36599
36791
  let filePath = opts.file;
36600
36792
  let langFromInterp;
36793
+ if (opts.code !== void 0) {
36794
+ if (args.length) {
36795
+ console.error(error("Pass the code once: either positionally or via --code, not both"));
36796
+ process.exit(1);
36797
+ }
36798
+ args = [opts.code];
36799
+ }
36601
36800
  if (args.length >= 2 && INTERPRETERS[args[0].toLowerCase()] !== void 0) {
36602
36801
  langFromInterp = INTERPRETERS[args[0].toLowerCase()];
36603
36802
  const rest = args.slice(1).join(" ");
@@ -36627,9 +36826,9 @@ GCC/Rust).
36627
36826
  }
36628
36827
  }
36629
36828
  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*")'));
36829
+ const discardOutput = opts.discardOutput ?? [];
36830
+ if (discardOutput.some((g) => !g.trim())) {
36831
+ console.error(error('--discard-output requires a non-empty glob (e.g. --discard-output "*.ppm")'));
36633
36832
  process.exit(1);
36634
36833
  }
36635
36834
  const { config } = await resolveProjectContext();
@@ -36655,16 +36854,30 @@ GCC/Rust).
36655
36854
  cwd,
36656
36855
  // The filter must run SERVER-side (in the output extractor): skipping
36657
36856
  // 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
36857
+ // make every later sync propose deleting them. (`noSyncOutput` is the
36858
+ // wire name for --discard-output.)
36859
+ noSyncOutput: discardOutput.length ? discardOutput : void 0
36660
36860
  });
36661
36861
  const res = opts.json ? await doRun() : await withSpinner("Running in sandbox\u2026", doRun, { done: null });
36662
36862
  const pulledLocal = !!(res.data.outputFiles?.length && getConfigPath());
36663
36863
  if (pulledLocal) {
36664
36864
  await sync({ interactive: false, progress: opts.json ? void 0 : createProgressReporter() });
36665
36865
  }
36866
+ const scratchWritten = [];
36867
+ if (res.data.scratchFiles?.length) {
36868
+ const base = resolve12(getProjectRoot() ?? process.cwd());
36869
+ for (const f of res.data.scratchFiles) {
36870
+ const rel2 = f.path.replace(/\\/g, "/");
36871
+ const abs = resolve12(base, rel2);
36872
+ if (abs !== base && !abs.startsWith(base + sep3)) continue;
36873
+ mkdirSync8(dirname8(abs), { recursive: true });
36874
+ writeFileSync9(abs, Buffer.from(f.contentBase64, "base64"));
36875
+ scratchWritten.push(rel2);
36876
+ }
36877
+ }
36666
36878
  if (opts.json) {
36667
- console.log(JSON.stringify({ ...res.data, filesSynced: pulledLocal }));
36879
+ const { scratchFiles: _omit, ...rest } = res.data;
36880
+ console.log(JSON.stringify({ ...rest, scratchFiles: scratchWritten, filesSynced: pulledLocal }));
36668
36881
  } else {
36669
36882
  if (res.data.autoMirrorSkipped) {
36670
36883
  console.error(dim(`Note: ${res.data.autoMirrorSkipped.reason}`));
@@ -36694,8 +36907,12 @@ GCC/Rust).
36694
36907
  if (pulledLocal) console.log(dim("Not pulled locally - run 'gipity sync' to fetch them."));
36695
36908
  }
36696
36909
  }
36910
+ if (scratchWritten.length > 0) {
36911
+ console.log("\nScratch outputs (local only - never synced or deployed):");
36912
+ for (const f of scratchWritten) console.log(`${f}`);
36913
+ }
36697
36914
  if (res.data.skippedOutputFiles && res.data.skippedOutputFiles.length > 0) {
36698
- console.log(dim("\nNot persisted (--no-sync-output):"));
36915
+ console.log(dim("\nDiscarded (--discard-output):"));
36699
36916
  for (const f of res.data.skippedOutputFiles) console.log(dim(`${f}`));
36700
36917
  }
36701
36918
  if (res.data.exitCode !== 0) {
@@ -36737,9 +36954,11 @@ ${syncResult.summary}`;
36737
36954
  ...a.remoteSize != null ? { size: a.remoteSize } : {}
36738
36955
  }));
36739
36956
  }
36957
+ const displayContent = res.data.content || res.data.costWarning?.message || "";
36740
36958
  if (opts.json) {
36741
36959
  console.log(JSON.stringify({
36742
- content: res.data.content,
36960
+ content: displayContent,
36961
+ costWarning: res.data.costWarning ?? null,
36743
36962
  toolsUsed: res.data.toolsUsed?.map((t) => ({
36744
36963
  tool: t.toolName,
36745
36964
  success: t.success,
@@ -36753,7 +36972,7 @@ ${syncResult.summary}`;
36753
36972
  syncedFiles: syncChanges
36754
36973
  }));
36755
36974
  } else {
36756
- console.log(res.data.content);
36975
+ console.log(displayContent);
36757
36976
  if (res.data.toolsUsed && res.data.toolsUsed.length > 0) {
36758
36977
  const toolNames = [...new Set(res.data.toolsUsed.map((t) => t.toolName))];
36759
36978
  console.log(`
@@ -36799,7 +37018,7 @@ chatCommand.command("delete <guid>").description("Delete a chat").option("--json
36799
37018
  init_api();
36800
37019
  init_config();
36801
37020
  import { join as join13 } from "path";
36802
- import { mkdirSync as mkdirSync8 } from "fs";
37021
+ import { mkdirSync as mkdirSync9 } from "fs";
36803
37022
  init_colors();
36804
37023
  init_utils();
36805
37024
  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 +37076,7 @@ projectCommand.command("create <name>").description("Create a project").option("
36857
37076
  const res = await post("/projects", body);
36858
37077
  const project = res.data;
36859
37078
  const dir = join13(getProjectsRoot(), project.slug);
36860
- mkdirSync8(dir, { recursive: true });
37079
+ mkdirSync9(dir, { recursive: true });
36861
37080
  const accountSlug = await getAccountSlug();
36862
37081
  let agentGuid = "";
36863
37082
  try {
@@ -36891,7 +37110,7 @@ projectCommand.command("create <name>").description("Create a project").option("
36891
37110
  if (process.env.GIPITY_NON_INTERACTIVE === "1") {
36892
37111
  console.log(`${muted("Next:")} switch to "${project.name}" in the sidebar.`);
36893
37112
  } else {
36894
- console.log(`${muted("Next:")} exit Claude (Ctrl+D), then run: ${brand("gipity claude")}`);
37113
+ console.log(`${muted("Next:")} exit Claude (Ctrl+D), then run: ${brand("gipity build")}`);
36895
37114
  console.log(`${muted("Pick")} "${project.name}" ${muted(`- it'll be at the top of the list.`)}`);
36896
37115
  }
36897
37116
  }));
@@ -37112,7 +37331,7 @@ function printStepRuns(steps, emptyNote) {
37112
37331
  }
37113
37332
  }
37114
37333
  var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
37115
- var sleep2 = (ms2) => new Promise((resolve19) => setTimeout(resolve19, ms2));
37334
+ var sleep2 = (ms2) => new Promise((resolve20) => setTimeout(resolve20, ms2));
37116
37335
  async function waitForRun(wfGuid, prevGuid, timeoutSec) {
37117
37336
  const deadline = Date.now() + timeoutSec * 1e3;
37118
37337
  let runGuid;
@@ -37729,14 +37948,15 @@ storageCommand.command("retention").description("View or adjust your file versio
37729
37948
  printRetention(data, opts.reset === true || hasSet);
37730
37949
  }));
37731
37950
 
37732
- // src/commands/claude.ts
37951
+ // src/commands/build.ts
37733
37952
  init_platform();
37734
37953
  init_auth();
37735
37954
  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";
37955
+ import { join as join17, dirname as dirname10, resolve as resolve14, basename as basename4, sep as sep5 } from "path";
37956
+ import { existsSync as existsSync14, mkdirSync as mkdirSync12, readFileSync as readFileSync17, writeFileSync as writeFileSync12, renameSync as renameSync2, readdirSync as readdirSync8, statSync as statSync7 } from "fs";
37957
+ import { homedir as homedir12 } from "os";
37739
37958
  import { fileURLToPath as fileURLToPath2 } from "url";
37959
+ import { randomUUID } from "crypto";
37740
37960
 
37741
37961
  // src/login-flow.ts
37742
37962
  init_api();
@@ -37775,7 +37995,7 @@ async function interactiveLogin() {
37775
37995
  return getAuth();
37776
37996
  }
37777
37997
 
37778
- // src/commands/claude.ts
37998
+ // src/commands/build.ts
37779
37999
  init_config();
37780
38000
 
37781
38001
  // src/prompts.ts
@@ -38053,7 +38273,7 @@ init_platform();
38053
38273
  init_api();
38054
38274
  import { hostname as hostname3, userInfo, platform as osPlatform2 } from "os";
38055
38275
  import { resolve as resolve13, dirname as dirname9 } from "path";
38056
- import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
38276
+ import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
38057
38277
 
38058
38278
  // src/relay/machine-id.ts
38059
38279
  import { createHash as createHash3 } from "crypto";
@@ -38166,8 +38386,8 @@ function startDaemon() {
38166
38386
  function installAutostart(opts = {}) {
38167
38387
  const stdio = opts.stdio ?? "ignore";
38168
38388
  const plan2 = planFor({ cliPath: resolveCliPath() });
38169
- mkdirSync9(dirname9(plan2.path), { recursive: true });
38170
- writeFileSync9(plan2.path, plan2.content);
38389
+ mkdirSync10(dirname9(plan2.path), { recursive: true });
38390
+ writeFileSync10(plan2.path, plan2.content);
38171
38391
  let ok = true;
38172
38392
  for (const argv2 of plan2.enableCmds) {
38173
38393
  const r = spawnSyncCommand(argv2[0], argv2.slice(1), { stdio });
@@ -38264,7 +38484,7 @@ async function runRelaySetup(opts) {
38264
38484
  console.error(` ${dim("Skipping for now - we'll offer again next time. Or turn it on with `gipity setup`.")}`);
38265
38485
  return false;
38266
38486
  }
38267
- const startNow = await confirm(" Start the relay now (and on future `gipity claude` runs)?", { default: "yes" });
38487
+ const startNow = await confirm(" Start the relay now (and on future `gipity build` runs)?", { default: "yes" });
38268
38488
  if (startNow) {
38269
38489
  startDaemon();
38270
38490
  }
@@ -38275,7 +38495,7 @@ async function runRelaySetup(opts) {
38275
38495
  if (!res.ok) {
38276
38496
  if (isWsl()) {
38277
38497
  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:")}`);
38498
+ console.log(` ${muted("it started just now and `gipity build` restarts it. For boot-time auto-start:")}`);
38279
38499
  console.log(` ${muted("add [boot] systemd=true to /etc/wsl.conf, restart WSL, then run")} ${brand("gipity relay install")}${muted(".")}`);
38280
38500
  } else {
38281
38501
  console.log(` ${muted("Autostart install returned non-zero - you can run")} ${brand("gipity relay install")} ${muted("later.")}`);
@@ -38309,19 +38529,19 @@ async function maybeOfferRelayOn() {
38309
38529
  await runRelaySetup({ mode: "offer-once" });
38310
38530
  }
38311
38531
 
38312
- // src/commands/claude.ts
38532
+ // src/commands/build.ts
38313
38533
  init_utils();
38314
38534
  init_colors();
38315
38535
 
38316
38536
  // src/banner.ts
38317
38537
  init_colors();
38318
38538
  import { homedir as homedir9 } from "os";
38319
- import { sep as sep3 } from "path";
38539
+ import { sep as sep4 } from "path";
38320
38540
  function tildify(p) {
38321
38541
  const home = homedir9();
38322
38542
  if (!home) return p;
38323
38543
  if (p === home) return "~";
38324
- if (p.startsWith(home + sep3)) return "~" + p.slice(home.length);
38544
+ if (p.startsWith(home + sep4)) return "~" + p.slice(home.length);
38325
38545
  return p;
38326
38546
  }
38327
38547
  var AI_MODELS = [
@@ -38613,14 +38833,200 @@ function ensureClaudeInstalled(opts = {}) {
38613
38833
  return { installed: isClaudeInstalled(), alreadyPresent: false };
38614
38834
  }
38615
38835
 
38616
- // src/commands/claude.ts
38836
+ // src/agents/claude-code.ts
38837
+ var claudeCodeAdapter = {
38838
+ key: "claude",
38839
+ source: "claude_code",
38840
+ displayName: "Claude Code",
38841
+ providerName: "Anthropic",
38842
+ binary: "claude",
38843
+ models: [
38844
+ { id: "opus", label: "Opus" },
38845
+ { id: "sonnet", label: "Sonnet" },
38846
+ { id: "haiku", label: "Haiku" }
38847
+ ],
38848
+ buildInteractiveArgs({ resume, model }) {
38849
+ const args = [];
38850
+ if (model) args.push("--model", model);
38851
+ if (resume) args.push("--resume", resume);
38852
+ return args;
38853
+ },
38854
+ buildHeadlessArgs({ message, resume, model, bypassApprovals, jsonStream }) {
38855
+ const args = ["-p", message];
38856
+ if (bypassApprovals) args.push("--permission-mode", "bypassPermissions");
38857
+ if (model) args.push("--model", model);
38858
+ if (resume) args.push("--resume", resume);
38859
+ if (jsonStream) args.push("--output-format", "stream-json", "--verbose", "--include-partial-messages");
38860
+ return args;
38861
+ },
38862
+ sessionIdFromStreamEvent(event) {
38863
+ if (event && typeof event.session_id === "string" && event.session_id) return event.session_id;
38864
+ return null;
38865
+ },
38866
+ hooksSupportedOnPlatform() {
38867
+ return true;
38868
+ },
38869
+ // The daemon parses Claude's stream-json into ingest entries itself
38870
+ // (hook capture stands down via GIPITY_CAPTURE=off on dispatches).
38871
+ daemonStreamCapture: true,
38872
+ ensureInstalled() {
38873
+ return ensureClaudeInstalled().installed;
38874
+ },
38875
+ installHint: `npm install -g ${CLAUDE_PACKAGE}`
38876
+ };
38877
+
38878
+ // src/agents/codex.ts
38879
+ var codexAdapter = {
38880
+ key: "codex",
38881
+ source: "codex",
38882
+ displayName: "Codex",
38883
+ providerName: "OpenAI",
38884
+ binary: "codex",
38885
+ models: [
38886
+ { id: "gpt-5.1-codex", label: "GPT-5.1 Codex" },
38887
+ { id: "gpt-5.1-codex-mini", label: "GPT-5.1 Codex Mini" }
38888
+ ],
38889
+ buildInteractiveArgs({ resume, model }) {
38890
+ const args = [];
38891
+ if (model) args.push("--model", model);
38892
+ if (resume) args.push("resume", resume);
38893
+ return args;
38894
+ },
38895
+ buildHeadlessArgs({ message, resume, model, bypassApprovals, jsonStream }) {
38896
+ const args = resume ? ["exec", "resume", resume, message] : ["exec", message];
38897
+ if (model) args.push("--model", model);
38898
+ if (bypassApprovals) {
38899
+ args.push(
38900
+ "-s",
38901
+ "workspace-write",
38902
+ "-c",
38903
+ "sandbox_workspace_write.network_access=true",
38904
+ "--dangerously-bypass-hook-trust",
38905
+ "--skip-git-repo-check"
38906
+ );
38907
+ }
38908
+ if (jsonStream) args.push("--json");
38909
+ return args;
38910
+ },
38911
+ sessionIdFromStreamEvent(event) {
38912
+ if (event?.type === "thread.started" && typeof event.thread_id === "string") {
38913
+ return event.thread_id;
38914
+ }
38915
+ return null;
38916
+ },
38917
+ hooksSupportedOnPlatform(platform2) {
38918
+ return platform2 !== "win32";
38919
+ },
38920
+ // No stream-json ingest mapper for Codex: relay dispatches keep hook
38921
+ // capture ON (GIPITY_CONVERSATION_GUID binds it) and the transcript
38922
+ // parser mirrors the session; the daemon tracks byte-level progress only.
38923
+ daemonStreamCapture: false,
38924
+ 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.",
38925
+ installHint: "npm install -g @openai/codex"
38926
+ };
38927
+
38928
+ // src/agents/grok.ts
38929
+ var grokAdapter = {
38930
+ key: "grok",
38931
+ source: "grok",
38932
+ displayName: "Grok",
38933
+ providerName: "xAI",
38934
+ binary: "grok",
38935
+ models: [
38936
+ { id: "grok-4.5", label: "Grok 4.5" },
38937
+ { id: "grok-code", label: "Grok Code" }
38938
+ ],
38939
+ buildInteractiveArgs({ resume, model }) {
38940
+ const args = [];
38941
+ if (model) args.push("--model", model);
38942
+ if (resume) args.push("--resume", resume);
38943
+ return args;
38944
+ },
38945
+ buildHeadlessArgs({ message, resume, model, bypassApprovals, jsonStream }) {
38946
+ const args = ["-p", message];
38947
+ if (model) args.push("--model", model);
38948
+ if (resume) args.push("--resume", resume);
38949
+ if (bypassApprovals) args.push("--always-approve");
38950
+ if (jsonStream) args.push("--output-format", "streaming-json");
38951
+ return args;
38952
+ },
38953
+ sessionIdFromStreamEvent(event) {
38954
+ const fromParams = event?.params?.sessionId;
38955
+ if (typeof fromParams === "string" && fromParams) return fromParams;
38956
+ if (typeof event?.sessionId === "string" && event.sessionId) return event.sessionId;
38957
+ return null;
38958
+ },
38959
+ hooksSupportedOnPlatform() {
38960
+ return true;
38961
+ },
38962
+ daemonStreamCapture: false,
38963
+ // Verified live (2026-07-13): grok -p fires NO plugin hooks - not even
38964
+ // SessionStart/Stop - so headless capture is launcher-driven: pin the
38965
+ // session id, then replay ~/.grok/sessions/<cwd>/<sid>/chat_history.jsonl
38966
+ // through the capture runner after the run.
38967
+ headlessCapture: {
38968
+ sessionIdArgs: (sessionId) => ["--session-id", sessionId]
38969
+ },
38970
+ installHint: "curl -fsSL https://grok.x.ai/install.sh | sh (or see docs.x.ai/grok-build)"
38971
+ };
38972
+
38973
+ // src/agents/index.ts
38974
+ var AGENT_ADAPTERS = [
38975
+ claudeCodeAdapter,
38976
+ codexAdapter,
38977
+ grokAdapter
38978
+ ];
38979
+ var AGENT_KEYS = AGENT_ADAPTERS.map((a) => a.key);
38980
+ function getAdapter(key) {
38981
+ const found = AGENT_ADAPTERS.find((a) => a.key === key);
38982
+ if (!found) throw new Error(`Unknown agent "${key}". Valid: ${AGENT_KEYS.join(", ")}`);
38983
+ return found;
38984
+ }
38985
+ function getAdapterBySource(source) {
38986
+ const found = AGENT_ADAPTERS.find((a) => a.source === source);
38987
+ if (!found) throw new Error(`No agent adapter for source "${source}"`);
38988
+ return found;
38989
+ }
38990
+
38991
+ // src/prefs.ts
38992
+ import { existsSync as existsSync13, mkdirSync as mkdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync11 } from "fs";
38993
+ import { homedir as homedir11 } from "os";
38994
+ import { join as join16 } from "path";
38995
+ var PREFS_PATH = join16(homedir11(), ".gipity", "prefs.json");
38996
+ function readPrefs() {
38997
+ if (!existsSync13(PREFS_PATH)) return {};
38998
+ try {
38999
+ const parsed = JSON.parse(readFileSync16(PREFS_PATH, "utf-8"));
39000
+ return {
39001
+ lastAgent: typeof parsed.lastAgent === "string" ? parsed.lastAgent : void 0,
39002
+ lastModel: parsed.lastModel && typeof parsed.lastModel === "object" ? parsed.lastModel : void 0
39003
+ };
39004
+ } catch {
39005
+ return {};
39006
+ }
39007
+ }
39008
+ function writePrefs(update) {
39009
+ try {
39010
+ const current = readPrefs();
39011
+ const next = {
39012
+ ...current,
39013
+ ...update,
39014
+ lastModel: { ...current.lastModel, ...update.lastModel }
39015
+ };
39016
+ mkdirSync11(join16(homedir11(), ".gipity"), { recursive: true });
39017
+ writeFileSync11(PREFS_PATH, JSON.stringify(next, null, 2) + "\n");
39018
+ } catch {
39019
+ }
39020
+ }
39021
+
39022
+ // src/commands/build.ts
38617
39023
  var __clDir = dirname10(fileURLToPath2(import.meta.url));
38618
39024
  function readOwnPkg(fromDir) {
38619
39025
  for (let d = fromDir; ; d = dirname10(d)) {
38620
- const p = join16(d, "package.json");
38621
- if (existsSync13(p)) {
39026
+ const p = join17(d, "package.json");
39027
+ if (existsSync14(p)) {
38622
39028
  try {
38623
- const pkg2 = JSON.parse(readFileSync16(p, "utf-8"));
39029
+ const pkg2 = JSON.parse(readFileSync17(p, "utf-8"));
38624
39030
  if (pkg2.name === "gipity") return pkg2;
38625
39031
  } catch {
38626
39032
  }
@@ -38669,12 +39075,12 @@ function localFsFallback(dir) {
38669
39075
  const topLevelEntries = [];
38670
39076
  const walk = (d, depth) => {
38671
39077
  try {
38672
- for (const name of readdirSync7(d).sort()) {
39078
+ for (const name of readdirSync8(d).sort()) {
38673
39079
  if (isSyncIgnored(name)) continue;
38674
39080
  let isDir = false;
38675
39081
  let size = 0;
38676
39082
  try {
38677
- const st2 = statSync7(join16(d, name));
39083
+ const st2 = statSync7(join17(d, name));
38678
39084
  isDir = st2.isDirectory();
38679
39085
  size = st2.isFile() ? st2.size : 0;
38680
39086
  } catch {
@@ -38683,7 +39089,7 @@ function localFsFallback(dir) {
38683
39089
  if (isDir) {
38684
39090
  folderCount++;
38685
39091
  if (depth === 0) topLevelEntries.push(`${name}/`);
38686
- walk(join16(d, name), depth + 1);
39092
+ walk(join17(d, name), depth + 1);
38687
39093
  } else {
38688
39094
  fileCount++;
38689
39095
  totalBytes += size;
@@ -38716,8 +39122,8 @@ function hasStreamJsonFlag(args) {
38716
39122
  }
38717
39123
  function markFolderTrusted(dir) {
38718
39124
  try {
38719
- const file = join16(homedir11(), ".claude.json");
38720
- const cfg = existsSync13(file) ? JSON.parse(readFileSync16(file, "utf-8")) : {};
39125
+ const file = join17(homedir12(), ".claude.json");
39126
+ const cfg = existsSync14(file) ? JSON.parse(readFileSync17(file, "utf-8")) : {};
38721
39127
  if (typeof cfg !== "object" || cfg === null) return;
38722
39128
  if (typeof cfg.projects !== "object" || cfg.projects === null) cfg.projects = {};
38723
39129
  const entry = typeof cfg.projects[dir] === "object" && cfg.projects[dir] !== null ? cfg.projects[dir] : {};
@@ -38725,7 +39131,7 @@ function markFolderTrusted(dir) {
38725
39131
  entry.hasTrustDialogAccepted = true;
38726
39132
  cfg.projects[dir] = entry;
38727
39133
  const tmp = `${file}.gipity-tmp`;
38728
- writeFileSync10(tmp, JSON.stringify(cfg, null, 2));
39134
+ writeFileSync12(tmp, JSON.stringify(cfg, null, 2));
38729
39135
  renameSync2(tmp, file);
38730
39136
  } catch {
38731
39137
  }
@@ -38747,10 +39153,22 @@ function suggestProjectName(existingSlugs) {
38747
39153
  }
38748
39154
  return `project-${Date.now().toString(36).slice(-6)}`;
38749
39155
  }
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) => {
39156
+ function createLaunchCommand(name, cfg = {}) {
39157
+ 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 (e.g. opus, sonnet - or skip it to use your agent's own default)").option("--bypass-approvals", "Skip the agent's interactive tool approvals (headless runs; the relay daemon sets this)").allowUnknownOption(true).allowExcessArguments(true);
39158
+ if (!cfg.presetAgent) {
39159
+ cmd.option("--agent <agent>", `Which coding agent to launch: ${AGENT_KEYS.join(", ")} (default: your last-used)`);
39160
+ }
39161
+ cmd.action(async (opts) => {
39162
+ await runLaunch(name, cfg, opts);
39163
+ });
39164
+ return cmd;
39165
+ }
39166
+ var buildCommand = createLaunchCommand("build");
39167
+ var claudeCommand = createLaunchCommand("claude", { presetAgent: "claude" });
39168
+ async function runLaunch(cmdName, cmdCfg, opts) {
38751
39169
  try {
38752
39170
  const runStart = Date.now();
38753
- const rawArgs = process.argv.slice(process.argv.indexOf("claude") + 1);
39171
+ const rawArgs = process.argv.slice(process.argv.indexOf(cmdName) + 1);
38754
39172
  if (rawArgs[0] === "install") {
38755
39173
  const r = ensureClaudeInstalled({ force: rawArgs.includes("--force") });
38756
39174
  if (r.alreadyPresent) console.log(` ${success("Claude Code already installed.")}`);
@@ -38791,7 +39209,7 @@ var claudeCommand = new Command("claude").description("Set up and run Claude Cod
38791
39209
  process.exit(1);
38792
39210
  }
38793
39211
  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>.")}`);
39212
+ console.error(` ${error("No Gipity project in cwd. Run `gipity build` (interactive) first, or pass --new-project / --project <slug>.")}`);
38795
39213
  process.exit(1);
38796
39214
  }
38797
39215
  if (!nonInteractive) {
@@ -38830,7 +39248,7 @@ var claudeCommand = new Command("claude").description("Set up and run Claude Cod
38830
39248
  let existing = getConfig();
38831
39249
  let forceAdoptCwd = false;
38832
39250
  if (existing && !opts.newProject && !opts.project && !nonInteractive) {
38833
- const cwdHasConfig = existsSync13(resolve14(process.cwd(), ".gipity.json"));
39251
+ const cwdHasConfig = existsSync14(resolve14(process.cwd(), ".gipity.json"));
38834
39252
  if (!cwdHasConfig && isLikelyEmpty(process.cwd())) {
38835
39253
  const ancestorRoot = dirname10(getConfigPath());
38836
39254
  console.log(` ${bold("You are inside an existing Gipity project.")}
@@ -38879,8 +39297,8 @@ var claudeCommand = new Command("claude").description("Set up and run Claude Cod
38879
39297
  }
38880
39298
  project = found;
38881
39299
  }
38882
- const projectDir2 = opts.here ? process.cwd() : join16(getProjectsRoot(), project.slug);
38883
- mkdirSync10(projectDir2, { recursive: true });
39300
+ const projectDir2 = opts.here ? process.cwd() : join17(getProjectsRoot(), project.slug);
39301
+ mkdirSync12(projectDir2, { recursive: true });
38884
39302
  process.chdir(projectDir2);
38885
39303
  clearConfigCache();
38886
39304
  let agentGuid = "";
@@ -38907,10 +39325,7 @@ var claudeCommand = new Command("claude").description("Set up and run Claude Cod
38907
39325
  } catch (err) {
38908
39326
  console.log(` ${warning("Could not sync files (will retry on next prompt):")} ${err.message}`);
38909
39327
  }
38910
- setupClaudeHooks();
38911
- setupClaudeMd();
38912
- setupAgentsMd();
38913
- setupGitignore();
39328
+ setupProjectTools();
38914
39329
  if (nonInteractive) {
38915
39330
  headlessNewProject = isNewProject;
38916
39331
  } else {
@@ -38922,10 +39337,7 @@ var claudeCommand = new Command("claude").description("Set up and run Claude Cod
38922
39337
  console.log(` Project: ${brand(existing.projectSlug)} ${muted(`(${existing.projectGuid})`)}`);
38923
39338
  console.log(` ${success("Already set up.")}
38924
39339
  `);
38925
- setupClaudeHooks();
38926
- setupClaudeMd();
38927
- setupAgentsMd();
38928
- setupGitignore();
39340
+ setupProjectTools();
38929
39341
  let doSync = true;
38930
39342
  const projectRoot = dirname10(getConfigPath());
38931
39343
  const scan = scanForAdoption(projectRoot);
@@ -39008,8 +39420,8 @@ var claudeCommand = new Command("claude").description("Set up and run Claude Cod
39008
39420
  } else {
39009
39421
  project = result.project;
39010
39422
  isNewProject = result.kind === "create-new";
39011
- const projectDir2 = join16(getProjectsRoot(), project.slug);
39012
- mkdirSync10(projectDir2, { recursive: true });
39423
+ const projectDir2 = join17(getProjectsRoot(), project.slug);
39424
+ mkdirSync12(projectDir2, { recursive: true });
39013
39425
  process.chdir(projectDir2);
39014
39426
  clearConfigCache();
39015
39427
  try {
@@ -39037,15 +39449,44 @@ var claudeCommand = new Command("claude").description("Set up and run Claude Cod
39037
39449
  }
39038
39450
  }
39039
39451
  initialPrompt = "";
39040
- setupClaudeHooks();
39041
- setupClaudeMd();
39042
- setupAgentsMd();
39043
- setupGitignore();
39452
+ setupProjectTools();
39044
39453
  console.log(` ${success(`Project "${project.name}" ready.`)}
39045
39454
  `);
39046
39455
  }
39047
39456
  if (opts.setupOnly) {
39048
- console.log(` Done. cd ${process.cwd()} && gipity claude`);
39457
+ console.log(` Done. cd ${process.cwd()} && gipity ${cmdName}`);
39458
+ return;
39459
+ }
39460
+ const prefs = readPrefs();
39461
+ let agentKey = cmdCfg.presetAgent ?? (typeof opts.agent === "string" ? opts.agent.toLowerCase() : void 0);
39462
+ if (agentKey && !AGENT_KEYS.includes(agentKey)) {
39463
+ console.error(` ${error(`Unknown agent "${agentKey}".`)} ${muted(`Valid: ${AGENT_KEYS.join(", ")}`)}`);
39464
+ process.exit(1);
39465
+ }
39466
+ if (!agentKey) {
39467
+ if (nonInteractive) {
39468
+ agentKey = prefs.lastAgent && AGENT_KEYS.includes(prefs.lastAgent) ? prefs.lastAgent : "claude";
39469
+ } else {
39470
+ agentKey = await pickAgent(prefs.lastAgent);
39471
+ }
39472
+ }
39473
+ const adapter = getAdapter(agentKey);
39474
+ const modelFlagGiven = rawArgs.some((a) => a === "--model" || a.startsWith("--model="));
39475
+ let pickedModel = null;
39476
+ if (!nonInteractive && !modelFlagGiven && !cmdCfg.presetAgent) {
39477
+ pickedModel = await pickModel(adapter, prefs.lastModel?.[adapter.key] ?? null);
39478
+ writePrefs({ lastAgent: adapter.key, lastModel: { [adapter.key]: pickedModel } });
39479
+ }
39480
+ if (adapter.key !== "claude") {
39481
+ await launchNonClaudeAgent(adapter, {
39482
+ cmdName,
39483
+ opts,
39484
+ rawArgs,
39485
+ nonInteractive,
39486
+ headlessOut,
39487
+ runStart,
39488
+ pickedModel
39489
+ });
39049
39490
  return;
39050
39491
  }
39051
39492
  if (!isClaudeInstalled()) {
@@ -39071,7 +39512,7 @@ var claudeCommand = new Command("claude").description("Set up and run Claude Cod
39071
39512
  if (resumeSid) {
39072
39513
  try {
39073
39514
  const found = await get(
39074
- `/conversations/claude-code/by-session/${encodeURIComponent(resumeSid)}`
39515
+ `/conversations/remote/by-session/${encodeURIComponent(resumeSid)}?source=claude_code`
39075
39516
  );
39076
39517
  convGuidForHooks = found.data.conversation_guid;
39077
39518
  } catch {
@@ -39079,8 +39520,8 @@ var claudeCommand = new Command("claude").description("Set up and run Claude Cod
39079
39520
  }
39080
39521
  if (!convGuidForHooks && cfg?.projectGuid) {
39081
39522
  const created = await post(
39082
- "/conversations/claude-code",
39083
- { project_guid: cfg.projectGuid, device_guid: device.guid, origin: "local" }
39523
+ "/conversations/remote",
39524
+ { project_guid: cfg.projectGuid, device_guid: device.guid, source: "claude_code", origin: "local" }
39084
39525
  );
39085
39526
  convGuidForHooks = created.data.conversation_guid;
39086
39527
  }
@@ -39091,10 +39532,10 @@ var claudeCommand = new Command("claude").description("Set up and run Claude Cod
39091
39532
  }
39092
39533
  }
39093
39534
  }
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"];
39535
+ const cmdIdx = process.argv.indexOf(cmdName);
39536
+ const rawClaudeArgs = process.argv.slice(cmdIdx + 1);
39537
+ const gipityBooleanFlags = /* @__PURE__ */ new Set(["--setup-only", "--new-project", "--here", "--quiet", "--bypass-approvals"]);
39538
+ const gipityValueFlags = ["--api-base", "--name", "--project", "--agent"];
39098
39539
  const claudeArgs = [];
39099
39540
  for (let i = 0; i < rawClaudeArgs.length; i++) {
39100
39541
  const arg = rawClaudeArgs[i];
@@ -39110,6 +39551,9 @@ var claudeCommand = new Command("claude").description("Set up and run Claude Cod
39110
39551
  console.log(` ${bold("Launching Claude Code, powered by Gipity.")}`);
39111
39552
  console.log(` ${muted("Just tell Claude what you'd like to build or do - everything Claude can do,")}`);
39112
39553
  console.log(` ${muted("plus hosting, databases, and live deploys on Gipity.")}`);
39554
+ if (convGuidForHooks) {
39555
+ console.log(` ${muted(`This session is saved to Gipity - watch it live at ${brand("prompt.gipity.ai")}. (--no-capture on init to disable.)`)}`);
39556
+ }
39113
39557
  console.log("");
39114
39558
  }
39115
39559
  let allArgs;
@@ -39150,6 +39594,7 @@ Sending to Claude Code: ${userMsg}
39150
39594
  }
39151
39595
  } else {
39152
39596
  allArgs = initialPrompt ? [initialPrompt, ...claudeArgs] : claudeArgs;
39597
+ if (pickedModel) allArgs.push("--model", pickedModel);
39153
39598
  }
39154
39599
  if (nonInteractive && (opts.newProject || opts.project)) {
39155
39600
  const hasPermFlag = allArgs.some((a) => a === "--permission-mode" || a.startsWith("--permission-mode=") || a === "--dangerously-skip-permissions");
@@ -39186,8 +39631,11 @@ Sending to Claude Code: ${userMsg}
39186
39631
  ${doneLine}
39187
39632
 
39188
39633
  `);
39189
- else console.log(`
39634
+ else {
39635
+ console.log(`
39190
39636
  ${doneLine}`);
39637
+ console.log(` ${muted(`Next time: gipity build from anywhere, or cd ${process.cwd()} && claude.`)}`);
39638
+ }
39191
39639
  process.exit(code ?? 0);
39192
39640
  });
39193
39641
  } catch (err) {
@@ -39195,7 +39643,201 @@ ${doneLine}
39195
39643
  ${error(`Error: ${err.message}`)}`);
39196
39644
  process.exit(1);
39197
39645
  }
39198
- });
39646
+ }
39647
+ async function pickAgent(lastUsed) {
39648
+ const defaultKey = lastUsed && AGENT_KEYS.includes(lastUsed) ? lastUsed : "claude";
39649
+ const defaultIdx = AGENT_ADAPTERS.findIndex((a) => a.key === defaultKey) + 1;
39650
+ console.log(` ${bold("Which coding agent?")}
39651
+ `);
39652
+ AGENT_ADAPTERS.forEach((a, i) => {
39653
+ const installed = binaryOnPath(a.binary) || a.key === "claude" && isClaudeInstalled();
39654
+ const notes = [];
39655
+ if (a.key === lastUsed) notes.push("last used");
39656
+ if (!installed) notes.push(a.ensureInstalled ? "not installed - we'll install it" : "not installed");
39657
+ if (a.key === "codex" && !a.hooksSupportedOnPlatform(process.platform)) {
39658
+ notes.push("session recording unavailable on Windows");
39659
+ }
39660
+ const note = notes.length ? ` ${muted(`(${notes.join(", ")})`)}` : "";
39661
+ console.log(` ${bold(`${i + 1}.`)} ${a.displayName} ${muted(`(${a.providerName})`)}${note}`);
39662
+ });
39663
+ console.log("");
39664
+ const choice = await pickOne("Choose", AGENT_ADAPTERS.length, defaultIdx);
39665
+ console.log("");
39666
+ return AGENT_ADAPTERS[choice - 1].key;
39667
+ }
39668
+ async function pickModel(adapter, lastUsed) {
39669
+ console.log(` ${bold("Which model?")}
39670
+ `);
39671
+ const lastIsModel = lastUsed && adapter.models.some((m) => m.id === lastUsed);
39672
+ const defaultIdx = lastIsModel ? adapter.models.findIndex((m) => m.id === lastUsed) + 2 : 1;
39673
+ console.log(` ${bold("1.")} Agent default ${muted(defaultIdx === 1 ? "(recommended)" : "")}`);
39674
+ adapter.models.forEach((m, i) => {
39675
+ const note = m.id === lastUsed ? ` ${muted("(last used)")}` : "";
39676
+ console.log(` ${bold(`${i + 2}.`)} ${m.label}${note}`);
39677
+ });
39678
+ console.log("");
39679
+ const choice = await pickOne("Choose", adapter.models.length + 1, defaultIdx);
39680
+ console.log("");
39681
+ return choice === 1 ? null : adapter.models[choice - 2].id;
39682
+ }
39683
+ async function launchNonClaudeAgent(adapter, ctx) {
39684
+ const { opts, rawArgs, nonInteractive, headlessOut, runStart, pickedModel } = ctx;
39685
+ if (!binaryOnPath(adapter.binary)) {
39686
+ let installed = false;
39687
+ if (adapter.ensureInstalled) {
39688
+ console.log(` ${adapter.displayName} not found - installing it now...`);
39689
+ installed = adapter.ensureInstalled();
39690
+ }
39691
+ if (!installed && !binaryOnPath(adapter.binary)) {
39692
+ console.error(` ${error(`${adapter.displayName} is not installed.`)} Install it with: ${adapter.installHint}`);
39693
+ console.error(` ${muted(`Then run: gipity ${ctx.cmdName} --agent ${adapter.key}`)}`);
39694
+ process.exit(1);
39695
+ }
39696
+ }
39697
+ const booleanFlags = /* @__PURE__ */ new Set(["--setup-only", "--new-project", "--here", "--quiet", "--bypass-approvals"]);
39698
+ const valueFlags = ["--api-base", "--name", "--project", "--agent"];
39699
+ let message = null;
39700
+ let resume;
39701
+ let model = pickedModel ?? void 0;
39702
+ const extras = [];
39703
+ for (let i = 0; i < rawArgs.length; i++) {
39704
+ const arg = rawArgs[i];
39705
+ if (booleanFlags.has(arg)) continue;
39706
+ if (valueFlags.includes(arg)) {
39707
+ i++;
39708
+ continue;
39709
+ }
39710
+ if (valueFlags.some((f) => arg.startsWith(`${f}=`))) continue;
39711
+ if (arg === "-p" || arg === "--print") {
39712
+ message = rawArgs[++i] ?? "";
39713
+ continue;
39714
+ }
39715
+ if (arg.startsWith("-p=") || arg.startsWith("--print=")) {
39716
+ message = arg.slice(arg.indexOf("=") + 1);
39717
+ continue;
39718
+ }
39719
+ if (arg === "--model") {
39720
+ model = rawArgs[++i];
39721
+ continue;
39722
+ }
39723
+ if (arg.startsWith("--model=")) {
39724
+ model = arg.slice("--model=".length);
39725
+ continue;
39726
+ }
39727
+ if (arg === "--resume") {
39728
+ resume = rawArgs[++i];
39729
+ continue;
39730
+ }
39731
+ if (arg.startsWith("--resume=")) {
39732
+ resume = arg.slice("--resume=".length);
39733
+ continue;
39734
+ }
39735
+ extras.push(arg);
39736
+ }
39737
+ let convGuid = process.env.GIPITY_CONVERSATION_GUID ?? null;
39738
+ if (!convGuid) {
39739
+ const device = getDevice();
39740
+ const config = getConfig();
39741
+ if (device && config?.projectGuid) {
39742
+ try {
39743
+ if (resume) {
39744
+ try {
39745
+ const found = await get(
39746
+ `/conversations/remote/by-session/${encodeURIComponent(resume)}?source=${encodeURIComponent(adapter.source)}`
39747
+ );
39748
+ convGuid = found.data.conversation_guid;
39749
+ } catch {
39750
+ }
39751
+ }
39752
+ if (!convGuid) {
39753
+ const created = await post(
39754
+ "/conversations/remote",
39755
+ { project_guid: config.projectGuid, device_guid: device.guid, source: adapter.source, origin: "local" }
39756
+ );
39757
+ convGuid = created.data.conversation_guid;
39758
+ }
39759
+ } catch (err) {
39760
+ if (!nonInteractive) {
39761
+ console.error(` ${error(`Could not create Gipity conversation: ${err?.message || err}`)}`);
39762
+ }
39763
+ }
39764
+ }
39765
+ }
39766
+ let agentArgs;
39767
+ let syntheticSid = null;
39768
+ if (nonInteractive) {
39769
+ const bypass = Boolean(opts.bypassApprovals || opts.newProject || opts.project);
39770
+ agentArgs = [
39771
+ ...adapter.buildHeadlessArgs({ message: message ?? "", resume, model, bypassApprovals: bypass, jsonStream: false }),
39772
+ ...extras
39773
+ ];
39774
+ if (adapter.headlessCapture) {
39775
+ syntheticSid = resume ?? randomUUID();
39776
+ if (!resume) agentArgs.push(...adapter.headlessCapture.sessionIdArgs(syntheticSid));
39777
+ }
39778
+ if (message) headlessOut(`
39779
+ Sending to ${adapter.displayName}: ${message}
39780
+ `);
39781
+ } else {
39782
+ agentArgs = [...adapter.buildInteractiveArgs({ resume, model }), ...extras];
39783
+ console.log(` ${bold(`Launching ${adapter.displayName}, powered by Gipity.`)}`);
39784
+ console.log(` ${muted(`Just tell ${adapter.displayName} what you'd like to build or do - plus hosting,`)}`);
39785
+ console.log(` ${muted("databases, and live deploys on Gipity.")}`);
39786
+ if (convGuid) {
39787
+ console.log(` ${muted(`This session is saved to Gipity - watch it live at ${brand("prompt.gipity.ai")}. (--no-capture on init to disable.)`)}`);
39788
+ }
39789
+ if (adapter.oneTimeSetupNote) {
39790
+ console.log(` ${warning(adapter.oneTimeSetupNote)}`);
39791
+ }
39792
+ console.log("");
39793
+ }
39794
+ const childEnv2 = { ...process.env };
39795
+ if (convGuid) childEnv2.GIPITY_CONVERSATION_GUID = convGuid;
39796
+ const agentCmd = resolveCommand(adapter.binary);
39797
+ const child = spawnCommand(agentCmd, agentArgs, {
39798
+ stdio: "inherit",
39799
+ cwd: process.cwd(),
39800
+ env: childEnv2
39801
+ });
39802
+ child.on("error", (err) => {
39803
+ console.error(`
39804
+ ${error(err.code === "ENOENT" ? `${adapter.displayName} not found. Install it: ${adapter.installHint}` : `Failed to launch ${adapter.displayName}: ${err.message}`)}`);
39805
+ process.exit(1);
39806
+ });
39807
+ child.on("exit", (code) => {
39808
+ if (syntheticSid && convGuid) {
39809
+ flushHeadlessCapture(adapter, syntheticSid, convGuid);
39810
+ }
39811
+ const doneLine = `Done (${formatElapsed2(Date.now() - runStart)})`;
39812
+ if (nonInteractive) process.stderr.write(`
39813
+ ${doneLine}
39814
+
39815
+ `);
39816
+ else {
39817
+ console.log(`
39818
+ ${doneLine}`);
39819
+ console.log(` ${muted(`Next time: gipity build from anywhere, or cd ${process.cwd()} && ${adapter.binary}.`)}`);
39820
+ }
39821
+ process.exit(code ?? 0);
39822
+ });
39823
+ }
39824
+ function flushHeadlessCapture(adapter, sessionId, convGuid) {
39825
+ const runner = join17(__clDir, "..", "hooks", "capture-runner.js");
39826
+ if (!existsSync14(runner)) return;
39827
+ const payload = JSON.stringify({ session_id: sessionId, cwd: process.cwd() });
39828
+ const env = { ...process.env, GIPITY_CONVERSATION_GUID: convGuid };
39829
+ for (const event of ["session-start", "stop"]) {
39830
+ try {
39831
+ spawnSyncCommand(process.execPath, [runner, adapter.key, event], {
39832
+ input: payload,
39833
+ env,
39834
+ timeout: 6e4,
39835
+ stdio: ["pipe", "ignore", "ignore"]
39836
+ });
39837
+ } catch {
39838
+ }
39839
+ }
39840
+ }
39199
39841
  async function pickOrCreateProject(projects, existingSlugs) {
39200
39842
  const filtered = projects.filter((p) => !p.is_default);
39201
39843
  const cwd = process.cwd();
@@ -39256,9 +39898,9 @@ async function pickFromAll(filtered) {
39256
39898
  function formatNewProjectLabel(existingSlugs) {
39257
39899
  const slug = suggestProjectName(existingSlugs);
39258
39900
  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}`;
39901
+ const home = homedir12();
39902
+ const display2 = root === home || root.startsWith(home + sep5) ? "~" + root.slice(home.length) : root;
39903
+ return `${display2}${sep5}${slug}`;
39262
39904
  }
39263
39905
  async function confirmAdoptCwd(cwd) {
39264
39906
  process.stdout.write(` ${muted("Scanning directory...")} `);
@@ -40919,11 +41561,11 @@ jobCommand.command("cancel <runGuid>").description("Cancel a queued or running j
40919
41561
  }
40920
41562
  }));
40921
41563
  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");
41564
+ const { existsSync: existsSync24, statSync: statSync10 } = await import("node:fs");
41565
+ const { resolve: resolve20, join: join26 } = await import("node:path");
40924
41566
  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()) {
41567
+ const jobDir = resolve20(process.cwd(), "jobs", name);
41568
+ if (!existsSync24(jobDir) || !statSync10(jobDir).isDirectory()) {
40927
41569
  console.error(error(`jobs/${name}/ not found in current directory`));
40928
41570
  process.exit(1);
40929
41571
  }
@@ -40944,7 +41586,7 @@ jobCommand.command("run-local <name>").description("Run the job in a local Docke
40944
41586
  },
40945
41587
  { file: "main.sh", runtime: "bash", entry: "bash" }
40946
41588
  ];
40947
- const picked = variants.find((v7) => existsSync23(join25(jobDir, v7.file)));
41589
+ const picked = variants.find((v7) => existsSync24(join26(jobDir, v7.file)));
40948
41590
  if (!picked) {
40949
41591
  console.error(error(`No main.py / main.js / main.sh found under jobs/${name}/`));
40950
41592
  process.exit(1);
@@ -40969,7 +41611,7 @@ jobCommand.command("run-local <name>").description("Run the job in a local Docke
40969
41611
  "-v",
40970
41612
  `${jobDir}:/work`
40971
41613
  ];
40972
- const needsDeps = opts.deps !== false && picked.depsFile && picked.installCmd && existsSync23(join25(jobDir, picked.depsFile));
41614
+ const needsDeps = opts.deps !== false && picked.depsFile && picked.installCmd && existsSync24(join26(jobDir, picked.depsFile));
40973
41615
  const handlerCmd = `${picked.entry} /work/${picked.file}`;
40974
41616
  let shellCmd;
40975
41617
  if (needsDeps && picked.installCmd) {
@@ -41144,7 +41786,7 @@ emailCommand.command("log").description("Recent email() sends from your app").op
41144
41786
  init_api();
41145
41787
  init_config();
41146
41788
  init_colors();
41147
- import { mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "fs";
41789
+ import { mkdirSync as mkdirSync13, writeFileSync as writeFileSync13 } from "fs";
41148
41790
  import { resolve as resolvePath2, dirname as dirname11, relative as relative5, isAbsolute } from "path";
41149
41791
 
41150
41792
  // src/provider-docs.ts
@@ -41165,7 +41807,7 @@ async function downloadFile(url, filename) {
41165
41807
  const buffer = Buffer.from(await res.arrayBuffer());
41166
41808
  const outPath = correctExtension(filename, buffer);
41167
41809
  ensureOutputDir(outPath);
41168
- writeFileSync11(outPath, buffer);
41810
+ writeFileSync13(outPath, buffer);
41169
41811
  const savedPath = resolvePath2(outPath);
41170
41812
  await pushGenerated(savedPath);
41171
41813
  return savedPath;
@@ -41201,7 +41843,7 @@ function correctExtension(filename, buf) {
41201
41843
  function ensureOutputDir(output) {
41202
41844
  const dir = dirname11(resolvePath2(output));
41203
41845
  try {
41204
- mkdirSync11(dir, { recursive: true });
41846
+ mkdirSync13(dir, { recursive: true });
41205
41847
  } catch (err) {
41206
41848
  throw new Error(`can't create the output directory ${dir} (${err.code || err.message}). Pick a different -o path.`);
41207
41849
  }
@@ -41434,14 +42076,14 @@ var generateCommand = new Command("generate").description("Generate images, vide
41434
42076
  init_api();
41435
42077
  init_config();
41436
42078
  init_colors();
41437
- import { existsSync as existsSync14, readFileSync as readFileSync17 } from "fs";
41438
- import { join as join17 } from "path";
42079
+ import { existsSync as existsSync15, readFileSync as readFileSync18 } from "fs";
42080
+ import { join as join18 } from "path";
41439
42081
  function readInstalledKitReadme(name) {
41440
42082
  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;
42083
+ const kitDir = join18(root, "src", "packages", name);
42084
+ if (!existsSync15(join18(kitDir, "package.json"))) return null;
42085
+ const readme = join18(kitDir, "README.md");
42086
+ return existsSync15(readme) ? readFileSync18(readme, "utf-8") : null;
41445
42087
  }
41446
42088
  var skillCommand = new Command("skill").description("Task docs - read the matching skill before building");
41447
42089
  skillCommand.command("list").description("List skills").option("--json", "Output as JSON").action((opts) => run("List", async () => {
@@ -41614,7 +42256,7 @@ var domainCommand = new Command("domain").description("Manage custom domains").a
41614
42256
  }));
41615
42257
 
41616
42258
  // src/commands/realtime.ts
41617
- import { existsSync as existsSync15 } from "fs";
42259
+ import { existsSync as existsSync16 } from "fs";
41618
42260
  import { dirname as dirname12, resolve as resolve15 } from "path";
41619
42261
  init_api();
41620
42262
  init_config();
@@ -41622,7 +42264,7 @@ init_colors();
41622
42264
  function hasDeployManifest() {
41623
42265
  const cfgPath = getConfigPath();
41624
42266
  if (!cfgPath) return false;
41625
- return existsSync15(resolve15(dirname12(cfgPath), "gipity.yaml"));
42267
+ return existsSync16(resolve15(dirname12(cfgPath), "gipity.yaml"));
41626
42268
  }
41627
42269
  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
42270
  const config = requireConfig();
@@ -41798,7 +42440,7 @@ async function pollTestStatus(projectGuid, runGuid, opts) {
41798
42440
  }
41799
42441
  }
41800
42442
  }
41801
- await new Promise((resolve19) => setTimeout(resolve19, getPollInterval()));
42443
+ await new Promise((resolve20) => setTimeout(resolve20, getPollInterval()));
41802
42444
  }
41803
42445
  }
41804
42446
  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 () => {
@@ -42008,21 +42650,21 @@ var locationCommand = new Command("location").description("Show location").argum
42008
42650
  }));
42009
42651
 
42010
42652
  // 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";
42653
+ import { existsSync as existsSync18, readFileSync as readFileSync20, statSync as statSync8 } from "fs";
42654
+ import { join as join20, resolve as resolve16 } from "path";
42655
+ import { homedir as homedir14 } from "os";
42014
42656
 
42015
42657
  // 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");
42658
+ import { readFileSync as readFileSync19, writeFileSync as writeFileSync14, mkdirSync as mkdirSync14, existsSync as existsSync17 } from "fs";
42659
+ import { join as join19 } from "path";
42660
+ import { homedir as homedir13 } from "os";
42661
+ var GIPITY_DIR = join19(homedir13(), ".gipity");
42662
+ var LOCAL_DIR = join19(GIPITY_DIR, "local");
42663
+ var LOCAL_PKG_DIR = join19(LOCAL_DIR, "node_modules", "gipity");
42664
+ var LOCAL_ENTRY = join19(LOCAL_PKG_DIR, "dist", "index.js");
42665
+ var STATE_FILE = join19(GIPITY_DIR, "update-state.json");
42666
+ var SETTINGS_FILE = join19(GIPITY_DIR, "settings.json");
42667
+ var UPDATE_LOG = join19(GIPITY_DIR, "update.log");
42026
42668
  var DEFAULT_STATE = {
42027
42669
  installedVersion: null,
42028
42670
  lastCheckAt: 0,
@@ -42033,24 +42675,24 @@ var DEFAULT_SETTINGS = {
42033
42675
  autoUpdates: true
42034
42676
  };
42035
42677
  function ensureDir() {
42036
- mkdirSync12(GIPITY_DIR, { recursive: true });
42678
+ mkdirSync14(GIPITY_DIR, { recursive: true });
42037
42679
  }
42038
42680
  function readState() {
42039
- if (!existsSync16(STATE_FILE)) return { ...DEFAULT_STATE };
42681
+ if (!existsSync17(STATE_FILE)) return { ...DEFAULT_STATE };
42040
42682
  try {
42041
- return { ...DEFAULT_STATE, ...JSON.parse(readFileSync18(STATE_FILE, "utf-8")) };
42683
+ return { ...DEFAULT_STATE, ...JSON.parse(readFileSync19(STATE_FILE, "utf-8")) };
42042
42684
  } catch {
42043
42685
  return { ...DEFAULT_STATE };
42044
42686
  }
42045
42687
  }
42046
42688
  function writeState(state) {
42047
42689
  ensureDir();
42048
- writeFileSync12(STATE_FILE, JSON.stringify(state, null, 2));
42690
+ writeFileSync14(STATE_FILE, JSON.stringify(state, null, 2));
42049
42691
  }
42050
42692
  function readSettings() {
42051
- if (!existsSync16(SETTINGS_FILE)) return { ...DEFAULT_SETTINGS };
42693
+ if (!existsSync17(SETTINGS_FILE)) return { ...DEFAULT_SETTINGS };
42052
42694
  try {
42053
- return { ...DEFAULT_SETTINGS, ...JSON.parse(readFileSync18(SETTINGS_FILE, "utf-8")) };
42695
+ return { ...DEFAULT_SETTINGS, ...JSON.parse(readFileSync19(SETTINGS_FILE, "utf-8")) };
42054
42696
  } catch {
42055
42697
  return { ...DEFAULT_SETTINGS };
42056
42698
  }
@@ -42066,7 +42708,7 @@ function updatesDisabled() {
42066
42708
  init_colors();
42067
42709
  init_auth();
42068
42710
  var NODE_MIN_MAJOR = 18;
42069
- function versionManagerNode(execPath, env = process.env, home = homedir13()) {
42711
+ function versionManagerNode(execPath, env = process.env, home = homedir14()) {
42070
42712
  const norm = (s) => s.replace(/\\/g, "/").replace(/\/+$/, "") + "/";
42071
42713
  const p = norm(execPath);
42072
42714
  const h = norm(home);
@@ -42083,10 +42725,10 @@ function versionManagerNode(execPath, env = process.env, home = homedir13()) {
42083
42725
  return null;
42084
42726
  }
42085
42727
  function localVersion() {
42086
- const pkgPath = join19(LOCAL_PKG_DIR, "package.json");
42087
- if (!existsSync17(pkgPath)) return null;
42728
+ const pkgPath = join20(LOCAL_PKG_DIR, "package.json");
42729
+ if (!existsSync18(pkgPath)) return null;
42088
42730
  try {
42089
- return JSON.parse(readFileSync19(pkgPath, "utf-8")).version ?? null;
42731
+ return JSON.parse(readFileSync20(pkgPath, "utf-8")).version ?? null;
42090
42732
  } catch {
42091
42733
  return null;
42092
42734
  }
@@ -42094,7 +42736,7 @@ function localVersion() {
42094
42736
  function shimVersion() {
42095
42737
  try {
42096
42738
  const url = new URL("../../package.json", import.meta.url);
42097
- return JSON.parse(readFileSync19(url, "utf-8")).version;
42739
+ return JSON.parse(readFileSync20(url, "utf-8")).version;
42098
42740
  } catch {
42099
42741
  return "unknown";
42100
42742
  }
@@ -42109,7 +42751,7 @@ function rel(t) {
42109
42751
  }
42110
42752
  function relayAutostartInstalled() {
42111
42753
  try {
42112
- return existsSync17(planFor({ cliPath: resolveCliPath() }).path);
42754
+ return existsSync18(planFor({ cliPath: resolveCliPath() }).path);
42113
42755
  } catch (err) {
42114
42756
  if (err instanceof UnsupportedPlatformError) return null;
42115
42757
  return null;
@@ -42147,7 +42789,7 @@ function gatherEnv(opts = {}) {
42147
42789
  const cli = {
42148
42790
  shim_version: shimVersion(),
42149
42791
  local_version: localVersion(),
42150
- local_install_ok: existsSync17(LOCAL_ENTRY),
42792
+ local_install_ok: existsSync18(LOCAL_ENTRY),
42151
42793
  auto_updates: !dis.disabled,
42152
42794
  updates_disabled_reason: dis.disabled ? dis.reason ?? null : null,
42153
42795
  last_check_at: updState.lastCheckAt,
@@ -42177,12 +42819,15 @@ var doctorCommand = new Command("doctor").description("Check install + environme
42177
42819
  console.log(`${muted("claude code ")} installed ${yn(env.claude.installed)} \xB7 authenticated ${yn(env.claude.authenticated)}`);
42178
42820
  const autostartLabel = env.relay.autostart === null ? muted("n/a") : yn(env.relay.autostart);
42179
42821
  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")}`);
42822
+ if (existsSync18(resolve16(process.cwd(), ".codex", "hooks.json"))) {
42823
+ console.log(`${muted("codex hooks ")} ${warning("written")} - session capture needs a one-time approval: run /hooks inside Codex in this project`);
42824
+ }
42825
+ console.log(`${muted("ready ")} ${env.ready ? success("yes") : warning("no - run `gipity build` (or the desktop app) to finish setup")}`);
42181
42826
  const state = readState();
42182
42827
  const settings = readSettings();
42183
42828
  const dis = updatesDisabled();
42184
42829
  const local = localVersion();
42185
- const localOk = existsSync17(LOCAL_ENTRY);
42830
+ const localOk = existsSync18(LOCAL_ENTRY);
42186
42831
  console.log("");
42187
42832
  console.log(bold("CLI install"));
42188
42833
  console.log(`${muted("shim version ")} ${shimVersion()}`);
@@ -42190,17 +42835,17 @@ var doctorCommand = new Command("doctor").description("Check install + environme
42190
42835
  console.log(`${muted("local install ")} ${LOCAL_PKG_DIR}`);
42191
42836
  console.log("");
42192
42837
  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}`);
42838
+ console.log(`${muted("settings file ")} ${existsSync18(SETTINGS_FILE) ? SETTINGS_FILE : dim("(default)")} autoUpdates=${settings.autoUpdates}`);
42194
42839
  console.log(`${muted("last check ")} ${rel(state.lastCheckAt)}`);
42195
42840
  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)")}`);
42841
+ console.log(`${muted("state file ")} ${existsSync18(STATE_FILE) ? STATE_FILE : dim("(none yet)")}`);
42842
+ console.log(`${muted("update log ")} ${existsSync18(UPDATE_LOG) ? `${UPDATE_LOG} (${statSync8(UPDATE_LOG).size} bytes)` : dim("(none yet)")}`);
42198
42843
  console.log("");
42199
42844
  console.log(dim("Force an update with: gipity update"));
42200
42845
  });
42201
42846
 
42202
42847
  // src/updater/check.ts
42203
- import { appendFileSync, existsSync as existsSync18 } from "fs";
42848
+ import { appendFileSync, existsSync as existsSync19 } from "fs";
42204
42849
  init_platform();
42205
42850
  var CHECK_INTERVAL_MS = 4 * 60 * 60 * 1e3;
42206
42851
  function log(line) {
@@ -42235,7 +42880,7 @@ function installVersion(version) {
42235
42880
  stdio: "ignore"
42236
42881
  });
42237
42882
  if (res.error) log(`npm spawn failed: ${res.error.message}`);
42238
- return res.status === 0 && existsSync18(LOCAL_ENTRY);
42883
+ return res.status === 0 && existsSync19(LOCAL_ENTRY);
42239
42884
  }
42240
42885
  async function runCheck(opts = {}) {
42241
42886
  const state = readState();
@@ -42309,17 +42954,17 @@ var updateCommand = new Command("update").description("Update the CLI").action(a
42309
42954
  init_api();
42310
42955
  init_utils();
42311
42956
  init_colors();
42312
- import { existsSync as existsSync21, readFileSync as readFileSync22, unlinkSync as unlinkSync7 } from "fs";
42957
+ import { existsSync as existsSync22, readFileSync as readFileSync23, unlinkSync as unlinkSync7 } from "fs";
42313
42958
  import { spawn as spawn2 } from "child_process";
42314
42959
 
42315
42960
  // src/relay/daemon.ts
42316
42961
  init_platform();
42317
42962
  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";
42963
+ 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
42964
  import { stat, readFile } from "fs/promises";
42320
42965
  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";
42966
+ import { homedir as homedir16, hostname as hostname5, platform as osPlatform3, loadavg as loadavg2, freemem as freemem2, totalmem as totalmem2, cpus as cpus2 } from "os";
42967
+ import { join as join23 } from "path";
42323
42968
  init_auth();
42324
42969
  init_api();
42325
42970
 
@@ -43025,7 +43670,7 @@ var DeltaBatcher = class {
43025
43670
  };
43026
43671
 
43027
43672
  // src/relay/daemon.ts
43028
- import { randomUUID } from "crypto";
43673
+ import { randomUUID as randomUUID2 } from "crypto";
43029
43674
 
43030
43675
  // src/relay/agent-token.ts
43031
43676
  init_api();
@@ -43074,22 +43719,22 @@ async function revokeRelayAgentToken() {
43074
43719
  // src/relay/diagnostics.ts
43075
43720
  init_platform();
43076
43721
  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";
43722
+ import { cpus, freemem, totalmem, loadavg, release, uptime, homedir as homedir15 } from "os";
43723
+ import { statfsSync, readdirSync as readdirSync9 } from "fs";
43724
+ import { join as join21 } from "path";
43080
43725
  var PROBE_TIMEOUT_MS = 4e3;
43081
43726
  function parseVersion(out) {
43082
43727
  const m = out.match(/\d+\.\d+(?:\.\d+)?(?:[-.\w]*)?/);
43083
43728
  return m ? m[0] : void 0;
43084
43729
  }
43085
43730
  function runProbe(cmd, args) {
43086
- return new Promise((resolve19) => {
43731
+ return new Promise((resolve20) => {
43087
43732
  let out = "";
43088
43733
  let settled = false;
43089
43734
  const done = (s) => {
43090
43735
  if (!settled) {
43091
43736
  settled = true;
43092
- resolve19(s);
43737
+ resolve20(s);
43093
43738
  }
43094
43739
  };
43095
43740
  let child;
@@ -43143,12 +43788,12 @@ async function detectGpu() {
43143
43788
  function diskUsage() {
43144
43789
  try {
43145
43790
  if (typeof statfsSync !== "function") return void 0;
43146
- const st2 = statfsSync(join20(homedir14(), "GipityProjects"), { bigint: false });
43791
+ const st2 = statfsSync(join21(homedir15(), "GipityProjects"), { bigint: false });
43147
43792
  if (!st2 || !st2.bsize) return void 0;
43148
43793
  return { total: st2.bsize * st2.blocks, free: st2.bsize * st2.bavail };
43149
43794
  } catch {
43150
43795
  try {
43151
- const st2 = statfsSync(homedir14(), { bigint: false });
43796
+ const st2 = statfsSync(homedir15(), { bigint: false });
43152
43797
  return { total: st2.bsize * st2.blocks, free: st2.bsize * st2.bavail };
43153
43798
  } catch {
43154
43799
  return void 0;
@@ -43157,7 +43802,7 @@ function diskUsage() {
43157
43802
  }
43158
43803
  function localProjectCount() {
43159
43804
  try {
43160
- return readdirSync8(join20(homedir14(), "GipityProjects"), { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).length;
43805
+ return readdirSync9(join21(homedir15(), "GipityProjects"), { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).length;
43161
43806
  } catch {
43162
43807
  return void 0;
43163
43808
  }
@@ -43170,15 +43815,17 @@ async function collectDiagnostics() {
43170
43815
  return [];
43171
43816
  }
43172
43817
  })();
43173
- const [claude, codex, cursor, gpu] = await Promise.all([
43818
+ const [claude, codex, grok, cursor, gpu] = await Promise.all([
43174
43819
  probeVersion("claude"),
43175
43820
  probeVersion("codex"),
43821
+ probeVersion("grok"),
43176
43822
  probeVersion("cursor"),
43177
43823
  detectGpu()
43178
43824
  ]);
43179
43825
  const agents = {};
43180
43826
  if (claude) agents.claude_code = claude;
43181
43827
  if (codex) agents.codex = codex;
43828
+ if (grok) agents.grok = grok;
43182
43829
  if (cursor) agents.cursor = cursor;
43183
43830
  return {
43184
43831
  collected_at: (/* @__PURE__ */ new Date()).toISOString(),
@@ -43286,8 +43933,8 @@ var SessionPool = class {
43286
43933
  const done = new Promise((r) => {
43287
43934
  markDone = r;
43288
43935
  });
43289
- return new Promise((resolve19, reject) => {
43290
- const turn = { onMessage: p.onMessage, resolve: resolve19, reject, interruptRequested: false, wasHot, done, markDone };
43936
+ return new Promise((resolve20, reject) => {
43937
+ const turn = { onMessage: p.onMessage, resolve: resolve20, reject, interruptRequested: false, wasHot, done, markDone };
43291
43938
  session.current = turn;
43292
43939
  session.state = "running";
43293
43940
  session.lastActivityAt = Date.now();
@@ -43464,7 +44111,7 @@ var SessionPool = class {
43464
44111
  // src/relay/daemon.ts
43465
44112
  init_config();
43466
44113
  init_api();
43467
- var RELAY_LOG_PATH = join22(homedir15(), ".gipity", "relay.log");
44114
+ var RELAY_LOG_PATH = join23(homedir16(), ".gipity", "relay.log");
43468
44115
  var HEARTBEAT_INTERVAL_MS = parseInt(process.env.GIPITY_RELAY_HEARTBEAT_MS || "60000", 10);
43469
44116
  var DIAGNOSTICS_INTERVAL_MS = parseInt(process.env.GIPITY_RELAY_DIAGNOSTICS_MS || String(24 * 60 * 60 * 1e3), 10);
43470
44117
  var LONG_POLL_TIMEOUT_MS = parseInt(process.env.GIPITY_RELAY_POLL_TIMEOUT_MS || "35000", 10);
@@ -43564,7 +44211,7 @@ function lockLogPerms(dir, file) {
43564
44211
  chmodSync3(dir, 448);
43565
44212
  } catch {
43566
44213
  }
43567
- if (!existsSync20(file)) {
44214
+ if (!existsSync21(file)) {
43568
44215
  try {
43569
44216
  closeSync4(openSync4(file, "a", 384));
43570
44217
  } catch {
@@ -43581,8 +44228,8 @@ function log2(level, msg, extra) {
43581
44228
  const pretty = `${C7.dim(hhmmss())} ${badge(level)} ${C7.bold(msg)}${formatExtra(extra)}`;
43582
44229
  process.stderr.write(pretty + "\n");
43583
44230
  try {
43584
- const dir = join22(homedir15(), ".gipity");
43585
- mkdirSync14(dir, { recursive: true });
44231
+ const dir = join23(homedir16(), ".gipity");
44232
+ mkdirSync16(dir, { recursive: true });
43586
44233
  lockLogPerms(dir, RELAY_LOG_PATH);
43587
44234
  const json = JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level, msg, ...extra ?? {} });
43588
44235
  appendFileSync3(RELAY_LOG_PATH, json + "\n");
@@ -43706,10 +44353,10 @@ async function heartbeatLoop(ctx) {
43706
44353
  }
43707
44354
  await Promise.race([
43708
44355
  sleep4(HEARTBEAT_INTERVAL_MS, ctx.abort.signal),
43709
- new Promise((resolve19) => {
44356
+ new Promise((resolve20) => {
43710
44357
  heartbeatPoke = () => {
43711
44358
  heartbeatPoke = null;
43712
- resolve19();
44359
+ resolve20();
43713
44360
  };
43714
44361
  })
43715
44362
  ]);
@@ -43753,9 +44400,9 @@ async function dispatchLoop(ctx, opts) {
43753
44400
  while (inflight.size >= MAX_CONCURRENT_DISPATCHES && !ctx.abort.signal.aborted) {
43754
44401
  await Promise.race([
43755
44402
  ...inflight,
43756
- new Promise((resolve19) => {
43757
- if (ctx.abort.signal.aborted) return resolve19();
43758
- ctx.abort.signal.addEventListener("abort", () => resolve19(), { once: true });
44403
+ new Promise((resolve20) => {
44404
+ if (ctx.abort.signal.aborted) return resolve20();
44405
+ ctx.abort.signal.addEventListener("abort", () => resolve20(), { once: true });
43759
44406
  })
43760
44407
  ]);
43761
44408
  }
@@ -43982,7 +44629,7 @@ function isSafeSessionId(s) {
43982
44629
  function transcriptPathFor(cwd, sessionId) {
43983
44630
  if (!isSafeSessionId(sessionId)) return null;
43984
44631
  const slug = cwd.replace(/[^a-zA-Z0-9]/g, "-");
43985
- return join22(homedir15(), ".claude", "projects", slug, `${sessionId}.jsonl`);
44632
+ return join23(homedir16(), ".claude", "projects", slug, `${sessionId}.jsonl`);
43986
44633
  }
43987
44634
  function formatDuration(ms2) {
43988
44635
  const totalSec = ms2 / 1e3;
@@ -44051,7 +44698,7 @@ async function handleDispatch(claimed) {
44051
44698
  { onWarn: (msg, meta) => log2("warn", msg, { id: d.short_guid, ...meta }) }
44052
44699
  );
44053
44700
  const pushSystem = (content) => {
44054
- queue.push({ kind: "system", content, ts: (/* @__PURE__ */ new Date()).toISOString(), source_uuid: randomUUID() });
44701
+ queue.push({ kind: "system", content, ts: (/* @__PURE__ */ new Date()).toISOString(), source_uuid: randomUUID2() });
44055
44702
  };
44056
44703
  const flushQueue = async () => {
44057
44704
  await queue.close(3e4);
@@ -44097,11 +44744,16 @@ async function handleDispatch(claimed) {
44097
44744
  }
44098
44745
  if (!syncFailed) pushSystem(bootstrapped ? "Project files synced." : "Attached files synced.");
44099
44746
  }
44747
+ const adapter = getAdapterBySource(d.remote_type ?? "claude_code");
44748
+ if (adapter.key !== "claude") {
44749
+ await handleNonClaudeDispatch(adapter, d, cwd, queue, pushSystem, flushQueue);
44750
+ return;
44751
+ }
44100
44752
  if (SESSION_POOL_ENABLED) {
44101
44753
  const handled = await tryHandleViaPool(d, cwd, queue, pushSystem, flushQueue);
44102
44754
  if (handled) return;
44103
44755
  }
44104
- const args = ["claude", "-p", d.message, "--permission-mode", "bypassPermissions"];
44756
+ const args = ["build", "--agent", "claude", "-p", d.message, "--permission-mode", "bypassPermissions"];
44105
44757
  args.push("--append-system-prompt", GIPITY_QUESTION_PROTOCOL);
44106
44758
  if (d.model) {
44107
44759
  args.push("--model", d.model);
@@ -44109,7 +44761,7 @@ async function handleDispatch(claimed) {
44109
44761
  if (d.kind === "resume" && d.remote_session_id) {
44110
44762
  args.push("--resume", d.remote_session_id);
44111
44763
  }
44112
- log2("debug", "spawning gipity claude", {
44764
+ log2("debug", "spawning gipity build --agent claude", {
44113
44765
  id: d.short_guid,
44114
44766
  cwd,
44115
44767
  args,
@@ -44158,8 +44810,8 @@ async function handleDispatch(claimed) {
44158
44810
  const header = `Running Claude Code - ${counts.join(" + ")} words${resumeNote}`;
44159
44811
  const ts2 = (/* @__PURE__ */ new Date()).toISOString();
44160
44812
  queue.push(
44161
- { kind: "prompt", prompt: d.message, ts: ts2, source_uuid: randomUUID() },
44162
- { kind: "system", content: header, ts: ts2, source_uuid: randomUUID() }
44813
+ { kind: "prompt", prompt: d.message, ts: ts2, source_uuid: randomUUID2() },
44814
+ { kind: "system", content: header, ts: ts2, source_uuid: randomUUID2() }
44163
44815
  );
44164
44816
  const t0 = Date.now();
44165
44817
  let exitCode = 1;
@@ -44206,7 +44858,61 @@ async function handleDispatch(claimed) {
44206
44858
  await ack(d.short_guid, "done", void 0, metrics);
44207
44859
  } else {
44208
44860
  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}`);
44861
+ await ack(d.short_guid, "error", `Claude Code exited with code ${exitCode}${stderrNote}`);
44862
+ }
44863
+ }
44864
+ async function handleNonClaudeDispatch(adapter, d, cwd, queue, pushSystem, flushQueue) {
44865
+ const args = ["build", "--agent", adapter.key, "-p", d.message, "--bypass-approvals"];
44866
+ if (d.model) args.push("--model", d.model);
44867
+ if (d.kind === "resume" && d.remote_session_id) args.push("--resume", d.remote_session_id);
44868
+ log2("debug", `spawning gipity build --agent ${adapter.key}`, {
44869
+ id: d.short_guid,
44870
+ cwd,
44871
+ args,
44872
+ conv: d.conversation_guid,
44873
+ chain: d.kind === "resume" ? `resume ${d.remote_session_id}` : "start (fresh session)"
44874
+ });
44875
+ const words = d.message.trim().split(/\s+/).filter(Boolean).length;
44876
+ pushSystem(`Running ${adapter.displayName} - ${words.toLocaleString("en-US")} words`);
44877
+ const t0 = Date.now();
44878
+ let exitCode = 1;
44879
+ let spawnErr = null;
44880
+ let killed = false;
44881
+ let runtimeLimit = false;
44882
+ let stderrTail = "";
44883
+ try {
44884
+ const result = await spawnGipityClaude(args, cwd, d, queue, { streamJson: false });
44885
+ exitCode = result.exitCode;
44886
+ killed = result.killed;
44887
+ runtimeLimit = result.runtimeLimit ?? false;
44888
+ stderrTail = result.stderrTail ?? "";
44889
+ } catch (err) {
44890
+ spawnErr = err?.message || String(err);
44891
+ log2("error", "dispatch spawn failed", { id: d.short_guid, err: spawnErr });
44892
+ }
44893
+ const dur = formatDuration(Date.now() - t0);
44894
+ if (!spawnErr && !killed) {
44895
+ try {
44896
+ await spawnSync2(cwd, PROJECT_SYNC_TIMEOUT_MS);
44897
+ } catch (err) {
44898
+ log2("warn", "sync after dispatch failed", { id: d.short_guid, err: err?.message });
44899
+ }
44900
+ }
44901
+ const stderrNote = stderrTail ? `: ${stderrTail.slice(0, 300)}` : "";
44902
+ const tail = runtimeLimit ? `stopped after ${dur} (runtime limit)` : killed ? `cancelled (${dur})` : spawnErr ? `failed (${dur}: ${spawnErr})` : exitCode === 0 ? `finished (${dur})` : `failed (${dur}, exit ${exitCode}${stderrNote})`;
44903
+ pushSystem(`${adapter.displayName} ${tail}`);
44904
+ await flushQueue();
44905
+ if (runtimeLimit) {
44906
+ await ack(d.short_guid, "error", `${adapter.displayName} stopped after ${dur} (runtime limit)`);
44907
+ } else if (killed) {
44908
+ await ack(d.short_guid, "cancelled");
44909
+ } else if (spawnErr) {
44910
+ await ack(d.short_guid, "error", spawnErr);
44911
+ } else if (exitCode === 0) {
44912
+ log2("info", "dispatch done", { id: d.short_guid, agent: adapter.key });
44913
+ await ack(d.short_guid, "done");
44914
+ } else {
44915
+ await ack(d.short_guid, "error", `${adapter.displayName} exited with code ${exitCode}${stderrNote}`);
44210
44916
  }
44211
44917
  }
44212
44918
  async function wrapPoolMessage(d, cwd, resume) {
@@ -44248,8 +44954,8 @@ async function tryHandleViaPool(d, cwd, queue, pushSystem, flushQueue) {
44248
44954
  const words = d.message.trim().split(/\s+/).filter(Boolean).length;
44249
44955
  const ts0 = (/* @__PURE__ */ new Date()).toISOString();
44250
44956
  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() }
44957
+ { kind: "prompt", prompt: d.message, ts: ts0, source_uuid: randomUUID2() },
44958
+ { kind: "system", content: `Running Claude Code - ${words.toLocaleString("en-US")} words${wasLive ? " (hot session)" : ""}`, ts: ts0, source_uuid: randomUUID2() }
44253
44959
  );
44254
44960
  const phases = new PhaseTracker();
44255
44961
  const deltaAcc = new DeltaAccumulator(getRelaySecrets);
@@ -44304,7 +45010,7 @@ async function tryHandleViaPool(d, cwd, queue, pushSystem, flushQueue) {
44304
45010
  const ts2 = (/* @__PURE__ */ new Date()).toISOString();
44305
45011
  for (const e of entries) {
44306
45012
  e.ts = ts2;
44307
- if (!e.source_uuid) e.source_uuid = randomUUID();
45013
+ if (!e.source_uuid) e.source_uuid = randomUUID2();
44308
45014
  }
44309
45015
  queue.push(...entries);
44310
45016
  };
@@ -44377,11 +45083,11 @@ async function resolveCwdForProject(d) {
44377
45083
  throw new Error(`Invalid project slug: ${JSON.stringify(d.project_slug)}`);
44378
45084
  }
44379
45085
  const root = getProjectsRoot();
44380
- const path5 = join22(root, d.project_slug);
44381
- const configPath = join22(path5, ".gipity.json");
44382
- if (existsSync20(configPath)) {
45086
+ const path5 = join23(root, d.project_slug);
45087
+ const configPath = join23(path5, ".gipity.json");
45088
+ if (existsSync21(configPath)) {
44383
45089
  try {
44384
- const cfg = JSON.parse(readFileSync21(configPath, "utf-8"));
45090
+ const cfg = JSON.parse(readFileSync22(configPath, "utf-8"));
44385
45091
  if (cfg.projectGuid === d.project_guid) return { cwd: path5, bootstrapped: false };
44386
45092
  log2("warn", "project dir exists but guid mismatch - using it anyway", {
44387
45093
  path: path5,
@@ -44393,9 +45099,9 @@ async function resolveCwdForProject(d) {
44393
45099
  }
44394
45100
  }
44395
45101
  log2("info", "bootstrapping new project dir", { slug: d.project_slug, path: path5 });
44396
- mkdirSync14(path5, { recursive: true });
45102
+ mkdirSync16(path5, { recursive: true });
44397
45103
  const apiBase2 = getApiBaseOverride() || DEFAULT_API_BASE;
44398
- writeFileSync13(configPath, JSON.stringify({
45104
+ writeFileSync15(configPath, JSON.stringify({
44399
45105
  projectGuid: d.project_guid,
44400
45106
  projectSlug: d.project_slug,
44401
45107
  accountSlug: d.account_slug,
@@ -44407,10 +45113,7 @@ async function resolveCwdForProject(d) {
44407
45113
  const origCwd = process.cwd();
44408
45114
  try {
44409
45115
  process.chdir(path5);
44410
- setupClaudeHooks();
44411
- setupClaudeMd();
44412
- setupAgentsMd();
44413
- setupGitignore();
45116
+ setupProjectTools();
44414
45117
  } finally {
44415
45118
  process.chdir(origCwd);
44416
45119
  }
@@ -44475,7 +45178,7 @@ async function killRunningForConv(convGuid) {
44475
45178
  }
44476
45179
  }
44477
45180
  const graceTimers = [];
44478
- const escalate = new Promise((resolve19) => {
45181
+ const escalate = new Promise((resolve20) => {
44479
45182
  const t = setTimeout(() => {
44480
45183
  for (const e of matches) {
44481
45184
  log2("warn", "previous dispatch ignored SIGTERM - escalating to SIGKILL", { conv: convGuid });
@@ -44484,7 +45187,7 @@ async function killRunningForConv(convGuid) {
44484
45187
  } catch {
44485
45188
  }
44486
45189
  }
44487
- resolve19();
45190
+ resolve20();
44488
45191
  }, KILL_GRACE_MS);
44489
45192
  graceTimers.push(t);
44490
45193
  });
@@ -44495,7 +45198,7 @@ async function killRunningForConv(convGuid) {
44495
45198
  }
44496
45199
  async function spawnSync2(cwd, timeoutMs, onSpawn) {
44497
45200
  const cmd = process.env.GIPITY_RELAY_CLAUDE_CMD || resolveCommand("gipity");
44498
- return new Promise((resolve19, reject) => {
45201
+ return new Promise((resolve20, reject) => {
44499
45202
  const child = spawnCommand(cmd, ["sync", "--json"], {
44500
45203
  cwd,
44501
45204
  env: childEnv(),
@@ -44519,7 +45222,7 @@ async function spawnSync2(cwd, timeoutMs, onSpawn) {
44519
45222
  } catch {
44520
45223
  }
44521
45224
  try {
44522
- unlinkSync6(join22(cwd, ".gipity", "sync.lock"));
45225
+ unlinkSync6(join23(cwd, ".gipity", "sync.lock"));
44523
45226
  } catch {
44524
45227
  }
44525
45228
  finish(() => reject(new Error(`timed out after ${Math.round(timeoutMs / 1e3)}s`)));
@@ -44535,7 +45238,7 @@ async function spawnSync2(cwd, timeoutMs, onSpawn) {
44535
45238
  child.on("exit", (code) => finish(() => {
44536
45239
  if (code === 0) {
44537
45240
  log2("info", "sync done", { cwd, stdoutLen });
44538
- resolve19();
45241
+ resolve20();
44539
45242
  } else {
44540
45243
  reject(new Error(`gipity sync exited ${code}${stderrBuf ? `: ${stderrBuf.trim().slice(0, 300)}` : ""}`));
44541
45244
  }
@@ -44543,10 +45246,11 @@ async function spawnSync2(cwd, timeoutMs, onSpawn) {
44543
45246
  });
44544
45247
  }
44545
45248
  async function spawnGipityClaude(args, cwd, d, queue, meta) {
45249
+ const streamJson = meta?.streamJson !== false;
44546
45250
  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) => {
45251
+ const fullArgs = streamJson ? [...args, "--output-format", "stream-json", "--verbose", "--include-partial-messages"] : [...args];
45252
+ const env = streamJson ? childEnv({ GIPITY_CONVERSATION_GUID: d.conversation_guid, GIPITY_CAPTURE: "off" }) : childEnv({ GIPITY_CONVERSATION_GUID: d.conversation_guid });
45253
+ return new Promise((resolve20, reject) => {
44550
45254
  const child = spawnCommand(cmd, fullArgs, { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
44551
45255
  let resolveExited = () => {
44552
45256
  };
@@ -44675,16 +45379,18 @@ async function spawnGipityClaude(args, cwd, d, queue, meta) {
44675
45379
  const ts2 = (/* @__PURE__ */ new Date()).toISOString();
44676
45380
  for (const e of entries) {
44677
45381
  e.ts = ts2;
44678
- if (!e.source_uuid) e.source_uuid = randomUUID();
45382
+ if (!e.source_uuid) e.source_uuid = randomUUID2();
44679
45383
  }
44680
45384
  q.push(...entries);
44681
45385
  });
44682
45386
  child.stdout?.on("data", (chunk) => {
44683
45387
  stdoutBytesTotal += chunk.length;
44684
45388
  lastStdoutByteAt = Date.now();
44685
- splitter.push(chunk);
45389
+ if (streamJson) splitter.push(chunk);
45390
+ });
45391
+ child.stdout?.on("end", () => {
45392
+ if (streamJson) splitter.flush();
44686
45393
  });
44687
- child.stdout?.on("end", () => splitter.flush());
44688
45394
  const errPrefix = C7.dim("\u2502 ");
44689
45395
  const stderrTail = [];
44690
45396
  const errRl = child.stderr ? createInterface2({ input: child.stderr }) : null;
@@ -44710,7 +45416,7 @@ async function spawnGipityClaude(args, cwd, d, queue, meta) {
44710
45416
  if (signal === "SIGTERM" || signal === "SIGKILL") killed = true;
44711
45417
  if (finalResult) {
44712
45418
  finalResult.ts = (/* @__PURE__ */ new Date()).toISOString();
44713
- finalResult.source_uuid = randomUUID();
45419
+ finalResult.source_uuid = randomUUID2();
44714
45420
  q.push(finalResult);
44715
45421
  }
44716
45422
  if (unmapped.size > 0) {
@@ -44724,7 +45430,7 @@ async function spawnGipityClaude(args, cwd, d, queue, meta) {
44724
45430
  void postProgress(d.conversation_guid, buildProgressPayload(false));
44725
45431
  if (ownQueue) await q.close();
44726
45432
  cleanup();
44727
- resolve19({
45433
+ resolve20({
44728
45434
  exitCode: code ?? 1,
44729
45435
  killed,
44730
45436
  runtimeLimit,
@@ -44738,10 +45444,10 @@ async function runDispatchSync(d, cwd) {
44738
45444
  let killed = false;
44739
45445
  try {
44740
45446
  await spawnSync2(cwd, PROJECT_SYNC_TIMEOUT_MS, (child) => {
44741
- const exited = new Promise((resolve19) => {
45447
+ const exited = new Promise((resolve20) => {
44742
45448
  child.once("exit", (_code, signal) => {
44743
45449
  if (signal === "SIGTERM") killed = true;
44744
- resolve19();
45450
+ resolve20();
44745
45451
  });
44746
45452
  });
44747
45453
  running.set(d.short_guid, { child, convGuid: d.conversation_guid, exited });
@@ -44772,18 +45478,18 @@ function killDispatch(shortGuid) {
44772
45478
  return false;
44773
45479
  }
44774
45480
  function sleep4(ms2, signal) {
44775
- return new Promise((resolve19) => {
44776
- if (signal?.aborted) return resolve19();
45481
+ return new Promise((resolve20) => {
45482
+ if (signal?.aborted) return resolve20();
44777
45483
  let onAbort = null;
44778
45484
  const t = setTimeout(() => {
44779
45485
  if (onAbort && signal) signal.removeEventListener("abort", onAbort);
44780
- resolve19();
45486
+ resolve20();
44781
45487
  }, ms2);
44782
45488
  if (signal) {
44783
45489
  onAbort = () => {
44784
45490
  clearTimeout(t);
44785
45491
  signal.removeEventListener("abort", onAbort);
44786
- resolve19();
45492
+ resolve20();
44787
45493
  };
44788
45494
  signal.addEventListener("abort", onAbort, { once: true });
44789
45495
  }
@@ -44796,7 +45502,7 @@ init_colors();
44796
45502
  function requirePaired() {
44797
45503
  const device = getDevice();
44798
45504
  if (!device) {
44799
- console.error(`${error("No paired device.")} Run ${bold("gipity claude")} to pair this machine.`);
45505
+ console.error(`${error("No paired device.")} Run ${bold("gipity connect")} to pair this machine.`);
44800
45506
  process.exit(1);
44801
45507
  }
44802
45508
  return device;
@@ -44937,7 +45643,7 @@ relayCommand.command("status").description("Show pairing status").option("--json
44937
45643
  return;
44938
45644
  }
44939
45645
  if (!s.device) {
44940
- console.log(`${muted("No paired device.")} Run ${brand("gipity claude")} to pair this machine.`);
45646
+ console.log(`${muted("No paired device.")} Run ${brand("gipity connect")} to pair this machine.`);
44941
45647
  return;
44942
45648
  }
44943
45649
  console.log(`${bold("Device:")} ${brand(s.device.name)} ${muted(`(${s.device.guid})`)}`);
@@ -44953,11 +45659,11 @@ relayCommand.command("run").description("Run the background service").option("-v
44953
45659
  });
44954
45660
  relayCommand.command("stop").description("Stop the background service").option("--force", "Force-stop if it doesn't exit cleanly within 5s").action(async (opts) => {
44955
45661
  const pidPath = getDaemonPidPath();
44956
- if (!existsSync21(pidPath)) {
45662
+ if (!existsSync22(pidPath)) {
44957
45663
  console.log(muted("Background service isn't running."));
44958
45664
  return;
44959
45665
  }
44960
- const pid = parseInt(readFileSync22(pidPath, "utf-8").trim(), 10);
45666
+ const pid = parseInt(readFileSync23(pidPath, "utf-8").trim(), 10);
44961
45667
  if (!pid || isNaN(pid)) {
44962
45668
  console.error(error("PID file is empty or malformed."));
44963
45669
  process.exit(1);
@@ -45081,8 +45787,8 @@ relayCommand.command("revoke").description("Revoke and forget this device").acti
45081
45787
  } catch {
45082
45788
  }
45083
45789
  const pidPath = getDaemonPidPath();
45084
- if (existsSync21(pidPath)) {
45085
- const pid = parseInt(readFileSync22(pidPath, "utf-8").trim(), 10);
45790
+ if (existsSync22(pidPath)) {
45791
+ const pid = parseInt(readFileSync23(pidPath, "utf-8").trim(), 10);
45086
45792
  if (pid && !isNaN(pid)) {
45087
45793
  try {
45088
45794
  process.kill(pid, "SIGTERM");
@@ -45095,13 +45801,13 @@ relayCommand.command("revoke").description("Revoke and forget this device").acti
45095
45801
  });
45096
45802
  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
45803
  const path5 = RELAY_LOG_PATH;
45098
- if (!existsSync21(path5)) {
45804
+ if (!existsSync22(path5)) {
45099
45805
  console.log(muted("No log file yet. Start the service with `gipity relay run` (or install it)."));
45100
45806
  return;
45101
45807
  }
45102
45808
  const lines = parseInt(opts.lines, 10) || 100;
45103
45809
  try {
45104
- const all = readFileSync22(path5, "utf-8").split("\n");
45810
+ const all = readFileSync23(path5, "utf-8").split("\n");
45105
45811
  const tail = all.slice(-lines - 1).join("\n");
45106
45812
  process.stdout.write(tail);
45107
45813
  } catch (err) {
@@ -45127,7 +45833,7 @@ registerInstallCommands(relayCommand);
45127
45833
  function requirePaired2() {
45128
45834
  const device = getDevice();
45129
45835
  if (!device) {
45130
- console.error(`${error("No paired device.")} Run ${brand("gipity claude")} to pair this machine.`);
45836
+ console.error(`${error("No paired device.")} Run ${brand("gipity connect")} to pair this machine.`);
45131
45837
  process.exit(1);
45132
45838
  }
45133
45839
  return device;
@@ -45136,9 +45842,9 @@ function requirePaired2() {
45136
45842
  // src/commands/setup.ts
45137
45843
  init_auth();
45138
45844
  init_colors();
45139
- var setupCommand = new Command("setup").description("Set up this computer as a relay (no project, no launch)").action(async () => {
45845
+ 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
45846
  try {
45141
- console.log(` ${bold("Gipity setup")} ${muted("- get this computer ready as a relay")}`);
45847
+ console.log(` ${bold("Gipity connect")} ${muted("- let gipity.ai drive this computer")}`);
45142
45848
  console.log("");
45143
45849
  let auth = getAuth();
45144
45850
  if (auth && !sessionExpired()) {
@@ -45156,9 +45862,9 @@ var setupCommand = new Command("setup").description("Set up this computer as a r
45156
45862
  if (enabled) {
45157
45863
  const running2 = isRelayEnabled() && !isPaused();
45158
45864
  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`.")}`);
45865
+ 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
45866
  } else {
45161
- console.log(` ${muted("No relay set up. Run `gipity setup` again anytime, or `gipity claude` to build with Gipity.")}`);
45867
+ console.log(` ${muted("No relay set up. Run `gipity connect` again anytime, or `gipity build` to start building.")}`);
45162
45868
  }
45163
45869
  console.log("");
45164
45870
  } catch (err) {
@@ -45174,15 +45880,15 @@ init_api();
45174
45880
  init_auth();
45175
45881
  init_utils();
45176
45882
  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";
45883
+ import { existsSync as existsSync23, rmSync as rmSync3, unlinkSync as unlinkSync8, readFileSync as readFileSync24, writeFileSync as writeFileSync16 } from "fs";
45884
+ import { homedir as homedir17, platform as osPlatform4 } from "os";
45885
+ import { join as join24, resolve as resolve18 } from "path";
45180
45886
  function removeGipityPluginConfig() {
45181
- const settingsPath = join23(homedir16(), ".claude", "settings.json");
45182
- if (!existsSync22(settingsPath)) return false;
45887
+ const settingsPath = join24(homedir17(), ".claude", "settings.json");
45888
+ if (!existsSync23(settingsPath)) return false;
45183
45889
  let settings;
45184
45890
  try {
45185
- settings = JSON.parse(readFileSync23(settingsPath, "utf-8"));
45891
+ settings = JSON.parse(readFileSync24(settingsPath, "utf-8"));
45186
45892
  } catch {
45187
45893
  return false;
45188
45894
  }
@@ -45197,7 +45903,7 @@ function removeGipityPluginConfig() {
45197
45903
  if (Object.keys(settings.extraKnownMarketplaces).length === 0) delete settings.extraKnownMarketplaces;
45198
45904
  changed = true;
45199
45905
  }
45200
- if (changed) writeFileSync14(settingsPath, JSON.stringify(settings, null, 2) + "\n");
45906
+ if (changed) writeFileSync16(settingsPath, JSON.stringify(settings, null, 2) + "\n");
45201
45907
  return changed;
45202
45908
  }
45203
45909
  function removeInstallerPathLines() {
@@ -45205,11 +45911,11 @@ function removeInstallerPathLines() {
45205
45911
  const marker = "# Added by the Gipity installer";
45206
45912
  const touched = [];
45207
45913
  for (const name of [".bashrc", ".zshrc", ".profile"]) {
45208
- const rc2 = join23(homedir16(), name);
45209
- if (!existsSync22(rc2)) continue;
45914
+ const rc2 = join24(homedir17(), name);
45915
+ if (!existsSync23(rc2)) continue;
45210
45916
  let text;
45211
45917
  try {
45212
- text = readFileSync23(rc2, "utf-8");
45918
+ text = readFileSync24(rc2, "utf-8");
45213
45919
  } catch {
45214
45920
  continue;
45215
45921
  }
@@ -45219,7 +45925,7 @@ function removeInstallerPathLines() {
45219
45925
  );
45220
45926
  if (kept.length === lines.length) continue;
45221
45927
  try {
45222
- writeFileSync14(rc2, kept.join("\n"));
45928
+ writeFileSync16(rc2, kept.join("\n"));
45223
45929
  touched.push(rc2);
45224
45930
  } catch {
45225
45931
  }
@@ -45227,11 +45933,11 @@ function removeInstallerPathLines() {
45227
45933
  return touched;
45228
45934
  }
45229
45935
  function tildify2(p) {
45230
- const home = homedir16();
45936
+ const home = homedir17();
45231
45937
  return p === home || p.startsWith(home + "/") ? "~" + p.slice(home.length) : p;
45232
45938
  }
45233
45939
  function resolveCliPath2() {
45234
- return resolve17(process.argv[1] ?? "gipity");
45940
+ return resolve18(process.argv[1] ?? "gipity");
45235
45941
  }
45236
45942
  async function stopDaemon() {
45237
45943
  if (!isDaemonRunning()) return;
@@ -45265,7 +45971,7 @@ function removeServiceUnit() {
45265
45971
  const r = spawnSyncCommand(argv2[0], argv2.slice(1), { stdio: "ignore" });
45266
45972
  if (r.status !== 0) allOk = false;
45267
45973
  }
45268
- if (existsSync22(plan2.path)) {
45974
+ if (existsSync23(plan2.path)) {
45269
45975
  try {
45270
45976
  unlinkSync8(plan2.path);
45271
45977
  } catch {
@@ -45288,9 +45994,9 @@ async function revokeDeviceBestEffort() {
45288
45994
  }
45289
45995
  var uninstallCommand = new Command("uninstall").description("Uninstall Gipity").option("--yes", "Skip confirmation prompts").action(async (opts) => {
45290
45996
  const autoYes = opts.yes || getAutoConfirm();
45291
- const gipityDir = join23(homedir16(), ".gipity");
45292
- const launcherBin = join23(gipityDir, "launcher", "bin", "gipity");
45293
- const installedViaLauncher = existsSync22(launcherBin);
45997
+ const gipityDir = join24(homedir17(), ".gipity");
45998
+ const launcherBin = join24(gipityDir, "launcher", "bin", "gipity");
45999
+ const installedViaLauncher = existsSync23(launcherBin);
45294
46000
  console.log(`${bold("Gipity uninstall")} - this will:`);
45295
46001
  console.log(`\u2022 Stop the running relay daemon (if any)`);
45296
46002
  console.log(`\u2022 Remove the OS autostart service (launchd / systemd / Task Scheduler)`);
@@ -45325,9 +46031,26 @@ var uninstallCommand = new Command("uninstall").description("Uninstall Gipity").
45325
46031
  } else {
45326
46032
  console.log(`${muted("No Gipity entries in Claude Code settings.")}`);
45327
46033
  }
45328
- if (existsSync22(gipityDir)) {
46034
+ if (grokInstallState().exists) {
46035
+ spawnSyncCommand(resolveCommand("grok"), ["plugin", "uninstall", "gipity", "--confirm"], {
46036
+ stdio: "ignore",
46037
+ timeout: 6e4
46038
+ });
46039
+ console.log(`${success("Gipity plugin removed from Grok.")}`);
46040
+ }
46041
+ const agentSkills = agentSkillsState();
46042
+ if (agentSkills.skills.length) {
46043
+ for (const name of agentSkills.skills) {
46044
+ try {
46045
+ rmSync3(join24(AGENTS_SKILLS_DIR, name), { recursive: true, force: true });
46046
+ } catch {
46047
+ }
46048
+ }
46049
+ console.log(`${success(`Removed ${agentSkills.skills.length} Gipity skills from ~/.agents/skills.`)}`);
46050
+ }
46051
+ if (existsSync23(gipityDir)) {
45329
46052
  try {
45330
- rmSync2(gipityDir, { recursive: true, force: true });
46053
+ rmSync3(gipityDir, { recursive: true, force: true });
45331
46054
  console.log(`${success(`Removed ${gipityDir}/`)}`);
45332
46055
  } catch (err) {
45333
46056
  console.error(`${error(`Could not remove ${gipityDir}: ${err?.message || err}`)}`);
@@ -45488,7 +46211,7 @@ gmailCommand.command("reply").description("Reply to a Gmail thread. Get thread-i
45488
46211
  }));
45489
46212
 
45490
46213
  // src/commands/text.ts
45491
- import { readFileSync as readFileSync24 } from "fs";
46214
+ import { readFileSync as readFileSync25 } from "fs";
45492
46215
  init_colors();
45493
46216
 
45494
46217
  // src/helpers/text-analysis.ts
@@ -45643,10 +46366,10 @@ function areAnagrams(a, b7) {
45643
46366
 
45644
46367
  // src/commands/text.ts
45645
46368
  function resolveInput(args, opts) {
45646
- if (opts.file) return readFileSync24(opts.file, "utf-8");
46369
+ if (opts.file) return readFileSync25(opts.file, "utf-8");
45647
46370
  if (args.length) return args.join(" ");
45648
46371
  try {
45649
- return readFileSync24(0, "utf-8");
46372
+ return readFileSync25(0, "utf-8");
45650
46373
  } catch {
45651
46374
  return "";
45652
46375
  }
@@ -45851,9 +46574,9 @@ function collectRealFlags(argv2, program3) {
45851
46574
  }
45852
46575
 
45853
46576
  // 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";
46577
+ import { openSync as openSync5, writeSync, mkdirSync as mkdirSync17 } from "fs";
46578
+ import { join as join25 } from "path";
46579
+ import { homedir as homedir18 } from "os";
45857
46580
  var TRACE_KEY = /* @__PURE__ */ Symbol.for("gipity.traceOutput");
45858
46581
  function installOutputTrace(label2) {
45859
46582
  if (process.env.GIPITY_TRACE_OUTPUT !== "1") return;
@@ -45864,10 +46587,10 @@ function installOutputTrace(label2) {
45864
46587
  existing.emit({ event: "reenter", label: label2 });
45865
46588
  return;
45866
46589
  }
45867
- const dir = join24(process.env.GIPITY_DIR || join24(homedir17(), ".gipity"), "trace");
45868
- mkdirSync15(dir, { recursive: true });
46590
+ const dir = join25(process.env.GIPITY_DIR || join25(homedir18(), ".gipity"), "trace");
46591
+ mkdirSync17(dir, { recursive: true });
45869
46592
  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");
46593
+ const fd2 = openSync5(join25(dir, `${stamp}-pid${process.pid}.jsonl`), "a");
45871
46594
  const emit = (rec) => {
45872
46595
  try {
45873
46596
  writeSync(fd2, JSON.stringify({ t: (/* @__PURE__ */ new Date()).toISOString(), ...rec }) + "\n");
@@ -45903,11 +46626,11 @@ function installOutputTrace(label2) {
45903
46626
  // src/index.ts
45904
46627
  installOutputTrace("index");
45905
46628
  var __dirname = dirname13(fileURLToPath3(import.meta.url));
45906
- var pkg = JSON.parse(readFileSync25(resolve18(__dirname, "../package.json"), "utf-8"));
46629
+ var pkg = JSON.parse(readFileSync26(resolve19(__dirname, "../package.json"), "utf-8"));
45907
46630
  function versionLabel() {
45908
46631
  const base = `v${pkg.version}`;
45909
46632
  try {
45910
- const info2 = JSON.parse(readFileSync25(resolve18(__dirname, "build-info.json"), "utf-8"));
46633
+ const info2 = JSON.parse(readFileSync26(resolve19(__dirname, "build-info.json"), "utf-8"));
45911
46634
  if (info2?.sha) return `${base} (dev ${info2.sha}${info2.dirty ? ", modified" : ""})`;
45912
46635
  } catch {
45913
46636
  }
@@ -45949,7 +46672,7 @@ var servicesGroup = [serviceCommand, generateCommand, notifyCommand, paymentsCom
45949
46672
  var filesGroup = [syncCommand, fileCommand, pushCommand, uploadCommand, storageCommand];
45950
46673
  var gipGroup = [chatCommand, memoryCommand, agentCommand, approvalCommand, gmailCommand];
45951
46674
  var utilitiesGroup = [sandboxCommand, emailCommand, locationCommand, textCommand, bugCommand];
45952
- var connectGroup = [loginCommand, logoutCommand, claudeCommand, setupCommand, relayCommand, githubCommand, creditsCommand, doctorCommand, updateCommand, uninstallCommand];
46675
+ var connectGroup = [loginCommand, logoutCommand, buildCommand, connectCommand, relayCommand, githubCommand, creditsCommand, doctorCommand, updateCommand, uninstallCommand];
45953
46676
  var HELP_SECTIONS = [
45954
46677
  { title: "Start here", cmds: startGroup },
45955
46678
  { title: "App build & ship", cmds: buildGroup },
@@ -46007,9 +46730,9 @@ program2.configureHelp({
46007
46730
  lines.push("");
46008
46731
  lines.push(bold("Quick start:"));
46009
46732
  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")}`);
46733
+ lines.push(` ${brand("gipity init")} ${dim("- link this dir + wire up every coding agent on this machine")}`);
46734
+ lines.push(` ${brand("gipity connect")} ${dim("- connect this computer to gipity.ai so the web CLI can drive it")}`);
46735
+ lines.push(` ${brand("gipity build")} ${dim("- or start from anywhere: pick a project, pick your agent, go")}`);
46013
46736
  lines.push("");
46014
46737
  lines.push(bold("Usage:"));
46015
46738
  lines.push(` ${cmd.name()} [options] [command]`);
@@ -46043,6 +46766,8 @@ for (const cmd of HELP_SECTIONS.flatMap((s) => s.cmds)) {
46043
46766
  `);
46044
46767
  program2.addCommand(cmd);
46045
46768
  }
46769
+ configureHelp(claudeCommand);
46770
+ program2.addCommand(claudeCommand, { hidden: true });
46046
46771
  program2.helpCommand(false);
46047
46772
  function manifestCommand(c) {
46048
46773
  return {