gipity 1.1.1 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,7 @@ import { existsSync, readFileSync } from 'fs';
3
3
  import { join, resolve } from 'path';
4
4
  import { homedir } from 'os';
5
5
  import { getAuth, sessionExpired } from '../auth.js';
6
+ import { get, usingEnvToken, ApiError } from '../api.js';
6
7
  import { getConfig, liveUrl } from '../config.js';
7
8
  import { brand, success, warning, muted, error as clrError } from '../colors.js';
8
9
  import { GIPITY_PLUGIN_ID, GIPITY_MARKETPLACE_NAME, setupClaudeHooks, ensureGipityPlugin, ensureGipityPluginInstalled, userScopeInstallState } from '../setup.js';
@@ -38,6 +39,17 @@ function checkGipityPlugin() {
38
39
  const stale = missing.length === 1 && missing[0] === 'install' && install.exists;
39
40
  return { missing, ok: missing.length === 0, stale };
40
41
  }
42
+ async function probeAuth(loggedIn) {
43
+ if (!loggedIn && !usingEnvToken())
44
+ return 'none';
45
+ if (!usingEnvToken() && sessionExpired())
46
+ return 'expired';
47
+ // Cap the probe well below the API layer's 60s request timeout - status is
48
+ // a diagnostic command and must answer fast even when the network is dark.
49
+ const timeout = new Promise(res => setTimeout(() => res('unreachable'), 5000).unref?.());
50
+ const call = get('/users/me').then(() => 'ok', (err) => (err instanceof ApiError && err.statusCode === 401 ? 'rejected' : 'unreachable'));
51
+ return Promise.race([call, timeout]);
52
+ }
41
53
  // `whoami` is the name agents reach for first when they want the signed-in
42
54
  // identity (it's the unix spelling), and this is the command that prints it.
43
55
  // Aliasing costs one line and turns a guess into a hit.
@@ -58,6 +70,7 @@ export const statusCommand = new Command('status')
58
70
  // now go out. Skipped entirely (existsSync short-circuit) when the queue
59
71
  // is empty, which is the common case, so this stays a no-op most runs.
60
72
  const queueDelivered = (auth && !sessionExpired()) ? await flushBugQueue().catch(() => 0) : 0;
73
+ const probe = await probeAuth(!!auth);
61
74
  if (opts.json) {
62
75
  console.log(JSON.stringify({
63
76
  project: config ? {
@@ -69,9 +82,13 @@ export const statusCommand = new Command('status')
69
82
  } : null,
70
83
  // `valid` reflects the refresh token (the real session) - access
71
84
  // tokens auto-renew, so their expiry must not read as "invalid".
72
- auth: auth ? {
73
- email: auth.email,
74
- valid: !sessionExpired(),
85
+ // `probe` is what one live call just proved: 'rejected' means every
86
+ // authenticated command will fail even though `valid` reads true.
87
+ auth: (auth || usingEnvToken()) ? {
88
+ email: auth?.email,
89
+ source: usingEnvToken() ? 'agent-token' : 'session',
90
+ valid: usingEnvToken() ? probe !== 'rejected' : !sessionExpired(),
91
+ probe,
75
92
  } : null,
76
93
  plugin: hookCheck,
77
94
  }, null, 2));
@@ -88,14 +105,25 @@ export const statusCommand = new Command('status')
88
105
  if (config.agentGuid)
89
106
  console.log(`${muted('Agent:')} ${config.agentGuid}`);
90
107
  }
91
- if (!auth) {
108
+ if (usingEnvToken()) {
109
+ console.log(`${muted('Auth:')} ${probe === 'rejected'
110
+ ? warning('agent API token (GIPITY_TOKEN) rejected by the server — mint a new one: gipity skill read agent-deploy')
111
+ : success('agent API token (GIPITY_TOKEN)')}${probe === 'unreachable' ? ` ${muted('(unverified — API unreachable)')}` : ''}`);
112
+ }
113
+ else if (!auth) {
92
114
  console.log(`${muted('Auth:')} ${warning('not logged in. Run: gipity login')}`);
93
115
  }
94
- else if (sessionExpired()) {
95
- console.log(`${muted('Auth:')} ${warning(`session expired for ${auth.email}. Run: gipity login`)}`);
116
+ else if (probe === 'expired') {
117
+ console.log(`${muted('Auth:')} ${warning(`session expired for ${auth.email}. Run: gipity login (headless/CI: set GIPITY_TOKEN — gipity skill read agent-deploy)`)}`);
118
+ }
119
+ else if (probe === 'rejected') {
120
+ // Locally fresh but the server says no (refresh token rotated away or
121
+ // revoked). Without the live probe this printed a green identity while
122
+ // every authenticated command failed.
123
+ console.log(`${muted('Auth:')} ${warning(`session for ${auth.email} was rejected by the server. Run: gipity login (headless/CI: set GIPITY_TOKEN — gipity skill read agent-deploy)`)}`);
96
124
  }
97
125
  else {
98
- console.log(`${muted('Auth:')} ${success(auth.email)}`);
126
+ console.log(`${muted('Auth:')} ${success(auth.email)}${probe === 'unreachable' ? ` ${muted('(unverified — API unreachable)')}` : ''}`);
99
127
  }
100
128
  if (queueDelivered > 0) {
101
129
  console.log(`${muted('Bug queue:')} ${success(`delivered ${queueDelivered} queued bug report${queueDelivered === 1 ? '' : 's'}`)}`);
@@ -206,6 +206,13 @@ export const testCommand = new Command('test')
206
206
  console.log(clrError(`Run failed: ${data.errorMessage}`));
207
207
  console.log('');
208
208
  }
209
+ // What the isolated test DB reset did (tables truncated, test.preserve
210
+ // list, seeds re-applied) - so "expected N rows, got 0" reads as "the
211
+ // reset truncated that table", not as a mystery.
212
+ if (data.dbSummary) {
213
+ console.log(muted(`Test db: ${data.dbSummary}`));
214
+ console.log('');
215
+ }
209
216
  // Failures recap. Results stream incrementally above, but agents usually
210
217
  // read this output through a `tail` window, so anything printed early is
211
218
  // lost - restate every failure (name + assertion message) right before
@@ -28,6 +28,7 @@ export const FLAG_ALIASES = {
28
28
  '--src': '--source-dir',
29
29
  '--srcdir': '--source-dir',
30
30
  '--parallel': '--concurrency',
31
+ '--no-sync-output': '--discard-output',
31
32
  '--max': '--limit',
32
33
  '--from': '--since',
33
34
  '--after': '--since',
package/dist/index.js CHANGED
@@ -6615,7 +6615,8 @@ __export(api_exports, {
6615
6615
  put: () => put,
6616
6616
  putTimeoutMs: () => putTimeoutMs,
6617
6617
  putToPresignedUrl: () => putToPresignedUrl,
6618
- sendMessage: () => sendMessage
6618
+ sendMessage: () => sendMessage,
6619
+ usingEnvToken: () => usingEnvToken
6619
6620
  });
6620
6621
  import { Readable } from "stream";
6621
6622
  async function fetchWithTimeout(url, init, timeoutMs, label2) {
@@ -6644,7 +6645,7 @@ async function shouldRetryAfter401(status, retried) {
6644
6645
  return forceRefreshAccessToken();
6645
6646
  }
6646
6647
  function with401Hint(status, message) {
6647
- return status === 401 && !usingEnvToken() ? `${message} \u2014 run: gipity login` : message;
6648
+ return status === 401 && !usingEnvToken() ? `${message} \u2014 run: gipity login (headless/CI: set GIPITY_TOKEN instead \u2014 gipity skill read agent-deploy)` : message;
6648
6649
  }
6649
6650
  async function getHeaders() {
6650
6651
  return {
@@ -34987,6 +34988,7 @@ Working with an existing Gipity project:
34987
34988
 
34988
34989
  // src/commands/status.ts
34989
34990
  init_auth();
34991
+ init_api();
34990
34992
  init_config();
34991
34993
  init_colors();
34992
34994
  import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
@@ -35009,6 +35011,16 @@ function checkGipityPlugin() {
35009
35011
  const stale = missing.length === 1 && missing[0] === "install" && install.exists;
35010
35012
  return { missing, ok: missing.length === 0, stale };
35011
35013
  }
35014
+ async function probeAuth(loggedIn) {
35015
+ if (!loggedIn && !usingEnvToken()) return "none";
35016
+ if (!usingEnvToken() && sessionExpired()) return "expired";
35017
+ const timeout = new Promise((res) => setTimeout(() => res("unreachable"), 5e3).unref?.());
35018
+ const call = get("/users/me").then(
35019
+ () => "ok",
35020
+ (err) => err instanceof ApiError && err.statusCode === 401 ? "rejected" : "unreachable"
35021
+ );
35022
+ return Promise.race([call, timeout]);
35023
+ }
35012
35024
  var statusCommand = new Command("status").alias("whoami").description("Show project and login status").option("--json", "Output as JSON").option("--repair-hooks", "Re-enable the Gipity Claude Code plugin (hooks) if missing or disabled").action(async (opts) => {
35013
35025
  const config = getConfig();
35014
35026
  const auth = getAuth();
@@ -35016,6 +35028,7 @@ var statusCommand = new Command("status").alias("whoami").description("Show proj
35016
35028
  void cwd;
35017
35029
  const hookCheck = config ? checkGipityPlugin() : null;
35018
35030
  const queueDelivered = auth && !sessionExpired() ? await flushBugQueue().catch(() => 0) : 0;
35031
+ const probe = await probeAuth(!!auth);
35019
35032
  if (opts.json) {
35020
35033
  console.log(JSON.stringify({
35021
35034
  project: config ? {
@@ -35027,9 +35040,13 @@ var statusCommand = new Command("status").alias("whoami").description("Show proj
35027
35040
  } : null,
35028
35041
  // `valid` reflects the refresh token (the real session) - access
35029
35042
  // tokens auto-renew, so their expiry must not read as "invalid".
35030
- auth: auth ? {
35031
- email: auth.email,
35032
- valid: !sessionExpired()
35043
+ // `probe` is what one live call just proved: 'rejected' means every
35044
+ // authenticated command will fail even though `valid` reads true.
35045
+ auth: auth || usingEnvToken() ? {
35046
+ email: auth?.email,
35047
+ source: usingEnvToken() ? "agent-token" : "session",
35048
+ valid: usingEnvToken() ? probe !== "rejected" : !sessionExpired(),
35049
+ probe
35033
35050
  } : null,
35034
35051
  plugin: hookCheck
35035
35052
  }, null, 2));
@@ -35044,12 +35061,16 @@ var statusCommand = new Command("status").alias("whoami").description("Show proj
35044
35061
  console.log(`${muted("API:")} ${config.apiBase}`);
35045
35062
  if (config.agentGuid) console.log(`${muted("Agent:")} ${config.agentGuid}`);
35046
35063
  }
35047
- if (!auth) {
35064
+ if (usingEnvToken()) {
35065
+ console.log(`${muted("Auth:")} ${probe === "rejected" ? warning("agent API token (GIPITY_TOKEN) rejected by the server \u2014 mint a new one: gipity skill read agent-deploy") : success("agent API token (GIPITY_TOKEN)")}${probe === "unreachable" ? ` ${muted("(unverified \u2014 API unreachable)")}` : ""}`);
35066
+ } else if (!auth) {
35048
35067
  console.log(`${muted("Auth:")} ${warning("not logged in. Run: gipity login")}`);
35049
- } else if (sessionExpired()) {
35050
- console.log(`${muted("Auth:")} ${warning(`session expired for ${auth.email}. Run: gipity login`)}`);
35068
+ } else if (probe === "expired") {
35069
+ console.log(`${muted("Auth:")} ${warning(`session expired for ${auth.email}. Run: gipity login (headless/CI: set GIPITY_TOKEN \u2014 gipity skill read agent-deploy)`)}`);
35070
+ } else if (probe === "rejected") {
35071
+ console.log(`${muted("Auth:")} ${warning(`session for ${auth.email} was rejected by the server. Run: gipity login (headless/CI: set GIPITY_TOKEN \u2014 gipity skill read agent-deploy)`)}`);
35051
35072
  } else {
35052
- console.log(`${muted("Auth:")} ${success(auth.email)}`);
35073
+ console.log(`${muted("Auth:")} ${success(auth.email)}${probe === "unreachable" ? ` ${muted("(unverified \u2014 API unreachable)")}` : ""}`);
35053
35074
  }
35054
35075
  if (queueDelivered > 0) {
35055
35076
  console.log(`${muted("Bug queue:")} ${success(`delivered ${queueDelivered} queued bug report${queueDelivered === 1 ? "" : "s"}`)}`);
@@ -35466,7 +35487,7 @@ function slowRenderMessage(fps, o) {
35466
35487
  const frames = Math.max(1, Math.round(fps * (o.waitMs / 1e3)));
35467
35488
  return `${warning("\u26A0 Slow render:")} page painted at ${fps} fps, so the app's vision pipeline ran on roughly ${bold(`${frames} frame${frames === 1 ? "" : "s"}`)} during the ${Math.round(o.waitMs / 1e3)}s before this eval (it infers once per painted frame). ${bold("That is enough:")} --camera loops your still image, so every one of those frames is the SAME pixels and the model returns the SAME answer on each \u2014 re-running with a bigger --wait/--timeout cannot change the result. ${bold("Do not escalate the wait.")} If a detection landed, it is real. If nothing was detected, the suspect is the ${bold("frame")} (a model needs the whole subject in shot \u2014 a tight crop, an odd angle or a busy background reads as nothing) or the ${bold("app")} (frames never reach the model) \u2014 never the frame rate. Settle it in ONE run: ${CAMERA_FRAME_CHECK}`;
35468
35489
  }
35469
- return `${warning("\u26A0 Slow render:")} page painted at ${fps} fps. Waiting on real time (setTimeout) advances animation/physics time far slower than it looks \u2014 assertions after a wall-clock wait can report a false negative. Step the app's own loop deterministically instead (3D templates: ${bold("core.advance(seconds)")}).`;
35490
+ return `${warning("\u26A0 Slow render:")} page painted at ${fps} fps. Waiting on real time (setTimeout) advances animation/physics time far slower than it looks \u2014 assertions after a wall-clock wait can report a false negative. Step the app's own loop deterministically instead (3D templates: ${bold("core.advance(seconds)")}; 2D/Phaser template: ${bold("(await import('./js/config.js')).advance(seconds)")}).`;
35470
35491
  }
35471
35492
  async function pollEvalResult(evalJobId, expectedWorkMs) {
35472
35493
  const deadline = Date.now() + expectedWorkMs + 6e4;
@@ -35670,7 +35691,7 @@ ${hint}`);
35670
35691
  }));
35671
35692
  return;
35672
35693
  }
35673
- console.log(`${brand("Eval")} ${bold(d.url || url)}`);
35694
+ console.error(`${brand("Eval")} ${bold(d.url || url)}`);
35674
35695
  if (d.navigationIncomplete) {
35675
35696
  console.log(`${warning("\u26A0 Navigation incomplete:")} ${d.note || "page did not reach full load"}`);
35676
35697
  }
@@ -35685,7 +35706,7 @@ ${hint}`);
35685
35706
  }
35686
35707
  if (camera) console.log(`${muted("Camera:")} ${camera.name} ${muted("(played as the webcam feed; getUserMedia resolves)")}`);
35687
35708
  if (hosted.length) console.log(`${muted("Fixtures:")} ${hosted.map((h) => h.name).join(", ")}`);
35688
- console.log(opts.file ? `${muted("Script:")} ${opts.file}` : `${muted("Expression:")} ${summarizeExpr(expr)}`);
35709
+ console.error(opts.file ? `${muted("Script:")} ${opts.file}` : `${muted("Expression:")} ${summarizeExpr(expr)}`);
35689
35710
  console.log(`
35690
35711
  ${result.trim() ? result : muted("(empty result)")}`);
35691
35712
  if (noValue) console.log(muted(`
@@ -36433,6 +36454,10 @@ var deployCommand = new Command("deploy").description("Deploy to dev or prod").a
36433
36454
  } else {
36434
36455
  console.log(success(`\u2713 Deployed to ${target}`) + muted(` (${d.elapsedMs}ms)`));
36435
36456
  if (d.url) console.log(`${muted("Live:")} ${brand(d.url)}`);
36457
+ if (d.phases?.some((p) => p.name === "functions")) {
36458
+ console.log(`${muted("Functions:")} POST ${brand(`${config.apiBase}/api/${config.projectGuid}/fn/<name>`)}`);
36459
+ console.log(muted(" (same address on dev and prod; names: gipity fn list; verify the public path: gipity fn call <name> --anon)"));
36460
+ }
36436
36461
  await inspectAfter();
36437
36462
  }
36438
36463
  }));
@@ -36470,10 +36495,15 @@ dbCommand.command("query <sql>").description("Run SQL").option("--database <name
36470
36495
  console.log(columns.map((c) => String(row2[c] ?? "NULL")).join(" "));
36471
36496
  }
36472
36497
  }
36498
+ if (res.data.affectedRows !== void 0) {
36499
+ console.log(`Affected rows: ${res.data.affectedRows}`);
36500
+ }
36473
36501
  } else if (res.data.affectedRows !== void 0) {
36474
36502
  console.log(`Affected rows: ${res.data.affectedRows}`);
36475
36503
  } else if (res.data.results) {
36476
36504
  console.log(JSON.stringify(res.data.results, null, 2));
36505
+ } else {
36506
+ console.log(JSON.stringify(res.data, null, 2));
36477
36507
  }
36478
36508
  }
36479
36509
  }));
@@ -38348,10 +38378,6 @@ async function pairDevice(opts = {}) {
38348
38378
  };
38349
38379
  }
38350
38380
  if (existing && opts.force) {
38351
- try {
38352
- await post(`/remote-devices/${encodeURIComponent(existing.guid)}/revoke`, {});
38353
- } catch {
38354
- }
38355
38381
  clearDevice();
38356
38382
  }
38357
38383
  const name = (opts.name?.trim() || friendlyDeviceName()).trim();
@@ -38443,20 +38469,20 @@ async function runRelaySetup(opts) {
38443
38469
  console.log(` ${dim("To register this computer again \u2014 for example under a different name \u2014")}`);
38444
38470
  console.log(` ${dim("unregister it first, then re-run setup:")}`);
38445
38471
  console.log(` ${brand("gipity relay revoke")} ${dim("# unpairs this computer and removes the login service")}`);
38446
- console.log(` ${brand("gipity setup")} ${dim("# register it again (asks for a new name)")}`);
38472
+ console.log(` ${brand("gipity connect")} ${dim("# register it again (asks for a new name)")}`);
38447
38473
  console.log("");
38448
38474
  }
38449
38475
  return true;
38450
38476
  }
38451
38477
  if (opts.mode === "run-now") {
38452
38478
  console.log(` ${bold("Set up this computer as a relay")}`);
38453
- console.log(` ${dim("A relay runs Claude Code (or Codex) here so you can drive it from the web (")}${brand("gipity.ai")}${dim(") on any browser.")}`);
38454
- console.log(` ${dim("It uses your Claude or Codex subscription \u2014 the cheapest way to pay for tokens.")}`);
38479
+ console.log(` ${dim("A relay runs your coding agent (Claude Code, Codex, or Grok) here so you can drive it from the web (")}${brand("gipity.ai")}${dim(") on any browser.")}`);
38480
+ console.log(` ${dim("It uses your Claude, Codex, or Grok subscription \u2014 the cheapest way to pay for tokens.")}`);
38455
38481
  } else {
38456
- console.log(` ${bold("Remote control of Claude Code")}`);
38457
- console.log(` ${dim("Drive this Claude Code from the web (")}${brand("gipity.ai")}${dim(") on any browser (desktop or phone).")}`);
38482
+ console.log(` ${bold("Remote control of your coding agent")}`);
38483
+ console.log(` ${dim("Drive your coding agent on this computer from the web (")}${brand("gipity.ai")}${dim(") on any browser (desktop or phone).")}`);
38458
38484
  console.log("");
38459
- console.log(` ${dim("Enable now (takes 2 seconds) or turn on later with")} ${brand("gipity setup")}`);
38485
+ console.log(` ${dim("Enable now (takes 2 seconds) or turn on later with")} ${brand("gipity connect")}`);
38460
38486
  }
38461
38487
  console.log("");
38462
38488
  const promptText = opts.mode === "run-now" ? " Set up remote control on this computer?" : " Enable remote control?";
@@ -38481,7 +38507,7 @@ async function runRelaySetup(opts) {
38481
38507
  } catch (err) {
38482
38508
  console.error(`
38483
38509
  ${error(`Could not create device: ${err?.message || err}`)}`);
38484
- console.error(` ${dim("Skipping for now - we'll offer again next time. Or turn it on with `gipity setup`.")}`);
38510
+ console.error(` ${dim("Skipping for now - we'll offer again next time. Or turn it on with `gipity connect`.")}`);
38485
38511
  return false;
38486
38512
  }
38487
38513
  const startNow = await confirm(" Start the relay now (and on future `gipity build` runs)?", { default: "yes" });
@@ -38840,11 +38866,6 @@ var claudeCodeAdapter = {
38840
38866
  displayName: "Claude Code",
38841
38867
  providerName: "Anthropic",
38842
38868
  binary: "claude",
38843
- models: [
38844
- { id: "opus", label: "Opus" },
38845
- { id: "sonnet", label: "Sonnet" },
38846
- { id: "haiku", label: "Haiku" }
38847
- ],
38848
38869
  buildInteractiveArgs({ resume, model }) {
38849
38870
  const args = [];
38850
38871
  if (model) args.push("--model", model);
@@ -38882,10 +38903,6 @@ var codexAdapter = {
38882
38903
  displayName: "Codex",
38883
38904
  providerName: "OpenAI",
38884
38905
  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
38906
  buildInteractiveArgs({ resume, model }) {
38890
38907
  const args = [];
38891
38908
  if (model) args.push("--model", model);
@@ -38932,10 +38949,6 @@ var grokAdapter = {
38932
38949
  displayName: "Grok",
38933
38950
  providerName: "xAI",
38934
38951
  binary: "grok",
38935
- models: [
38936
- { id: "grok-4.5", label: "Grok 4.5" },
38937
- { id: "grok-code", label: "Grok Code" }
38938
- ],
38939
38952
  buildInteractiveArgs({ resume, model }) {
38940
38953
  const args = [];
38941
38954
  if (model) args.push("--model", model);
@@ -38998,8 +39011,7 @@ function readPrefs() {
38998
39011
  try {
38999
39012
  const parsed = JSON.parse(readFileSync16(PREFS_PATH, "utf-8"));
39000
39013
  return {
39001
- lastAgent: typeof parsed.lastAgent === "string" ? parsed.lastAgent : void 0,
39002
- lastModel: parsed.lastModel && typeof parsed.lastModel === "object" ? parsed.lastModel : void 0
39014
+ lastAgent: typeof parsed.lastAgent === "string" ? parsed.lastAgent : void 0
39003
39015
  };
39004
39016
  } catch {
39005
39017
  return {};
@@ -39008,11 +39020,7 @@ function readPrefs() {
39008
39020
  function writePrefs(update) {
39009
39021
  try {
39010
39022
  const current = readPrefs();
39011
- const next = {
39012
- ...current,
39013
- ...update,
39014
- lastModel: { ...current.lastModel, ...update.lastModel }
39015
- };
39023
+ const next = { ...current, ...update };
39016
39024
  mkdirSync11(join16(homedir11(), ".gipity"), { recursive: true });
39017
39025
  writeFileSync11(PREFS_PATH, JSON.stringify(next, null, 2) + "\n");
39018
39026
  } catch {
@@ -39154,7 +39162,7 @@ function suggestProjectName(existingSlugs) {
39154
39162
  return `project-${Date.now().toString(36).slice(-6)}`;
39155
39163
  }
39156
39164
  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);
39165
+ const cmd = new Command(name).description(cfg.presetAgent ? "Set up and run Claude Code (legacy alias of `gipity build`)" : "Start building from anywhere - pick a project, pick your coding agent, go").option("--setup-only", "Do the Gipity setup but skip launching the agent").option("--new-project", "Create a fresh Gipity project instead of using cwd or the picker").option("--name <name>", "Name for --new-project (default: project-NNN)").option("--project <slug>", "Open an existing project by slug or id").option("--here", "Use the current directory instead of ~/GipityProjects/<slug>/").option("--quiet", "Suppress the agent's live progress output (headless --new-project/--project runs)").option("--model <model>", "Model for the session, passed straight to the agent (e.g. opus) - omit it and the agent uses its own default").option("--bypass-approvals", "Skip the agent's interactive tool approvals (headless runs; the relay daemon sets this)").allowUnknownOption(true).allowExcessArguments(true);
39158
39166
  if (!cfg.presetAgent) {
39159
39167
  cmd.option("--agent <agent>", `Which coding agent to launch: ${AGENT_KEYS.join(", ")} (default: your last-used)`);
39160
39168
  }
@@ -39471,11 +39479,8 @@ async function runLaunch(cmdName, cmdCfg, opts) {
39471
39479
  }
39472
39480
  }
39473
39481
  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 } });
39482
+ if (!nonInteractive && !cmdCfg.presetAgent) {
39483
+ writePrefs({ lastAgent: adapter.key });
39479
39484
  }
39480
39485
  if (adapter.key !== "claude") {
39481
39486
  await launchNonClaudeAgent(adapter, {
@@ -39484,8 +39489,7 @@ async function runLaunch(cmdName, cmdCfg, opts) {
39484
39489
  rawArgs,
39485
39490
  nonInteractive,
39486
39491
  headlessOut,
39487
- runStart,
39488
- pickedModel
39492
+ runStart
39489
39493
  });
39490
39494
  return;
39491
39495
  }
@@ -39594,7 +39598,6 @@ Sending to Claude Code: ${userMsg}
39594
39598
  }
39595
39599
  } else {
39596
39600
  allArgs = initialPrompt ? [initialPrompt, ...claudeArgs] : claudeArgs;
39597
- if (pickedModel) allArgs.push("--model", pickedModel);
39598
39601
  }
39599
39602
  if (nonInteractive && (opts.newProject || opts.project)) {
39600
39603
  const hasPermFlag = allArgs.some((a) => a === "--permission-mode" || a.startsWith("--permission-mode=") || a === "--dangerously-skip-permissions");
@@ -39665,23 +39668,8 @@ async function pickAgent(lastUsed) {
39665
39668
  console.log("");
39666
39669
  return AGENT_ADAPTERS[choice - 1].key;
39667
39670
  }
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
39671
  async function launchNonClaudeAgent(adapter, ctx) {
39684
- const { opts, rawArgs, nonInteractive, headlessOut, runStart, pickedModel } = ctx;
39672
+ const { opts, rawArgs, nonInteractive, headlessOut, runStart } = ctx;
39685
39673
  if (!binaryOnPath(adapter.binary)) {
39686
39674
  let installed = false;
39687
39675
  if (adapter.ensureInstalled) {
@@ -39698,7 +39686,7 @@ async function launchNonClaudeAgent(adapter, ctx) {
39698
39686
  const valueFlags = ["--api-base", "--name", "--project", "--agent"];
39699
39687
  let message = null;
39700
39688
  let resume;
39701
- let model = pickedModel ?? void 0;
39689
+ let model;
39702
39690
  const extras = [];
39703
39691
  for (let i = 0; i < rawArgs.length; i++) {
39704
39692
  const arg = rawArgs[i];
@@ -39977,18 +39965,23 @@ import os from "os";
39977
39965
  init_api();
39978
39966
  init_config();
39979
39967
  init_colors();
39968
+
39969
+ // src/catalog.ts
39980
39970
  var STARTERS = [
39981
39971
  { key: "web-vision-cam", hint: "fullscreen camera app with on-device vision (MediaPipe)" },
39982
39972
  { key: "object-spotter", hint: "camera app that boxes, labels, and counts objects (YOLOX on-device)" },
39983
39973
  { key: "2d-game", hint: "2D games with Phaser 3 - platformer, arcade, puzzle" },
39984
39974
  { key: "3d-world", hint: "playable 3D multiplayer rocket-launcher demo" },
39985
- { key: "karaoke-captions", hint: "audio + lyrics -> word-synced karaoke captions (GPU job)" }
39975
+ { key: "karaoke-captions", hint: "audio + lyrics -> word-synced karaoke captions (GPU job)" },
39976
+ { key: "outreach-agent", hint: "AI-run outreach funnel - import contacts, draft + auto-send staged emails" },
39977
+ { key: "paid-app", hint: "storefront that charges real money - Stripe checkout, members area, billing" },
39978
+ { key: "notify-demo", hint: "web-push demo - enable notifications, send a real ping" }
39986
39979
  ];
39987
39980
  var BLANK = [
39988
39981
  { key: "web-simple", hint: "static frontend-only site - pages, dashboards, simple games" },
39989
39982
  { key: "web-fullstack", hint: "backend API + database wiring - frontend, functions, migrations; deploys green" },
39990
- { key: "api", hint: "pure API backend, no frontend - one example function + test" },
39991
- { key: "3d-engine", hint: "3D multiplayer wiring - Three.js + Rapier + Gipity Realtime" }
39983
+ { key: "3d-engine", hint: "3D multiplayer wiring - Three.js + Rapier + Gipity Realtime" },
39984
+ { key: "api", hint: "pure API backend, no frontend - one example function + test" }
39992
39985
  ];
39993
39986
  var KITS = [
39994
39987
  { key: "realtime", hint: "multiplayer / presence / shared state" },
@@ -39996,8 +39989,16 @@ var KITS = [
39996
39989
  { key: "web-vision-detect", hint: "browser object detection - YOLOX, WebGPU/WASM, custom models" },
39997
39990
  { key: "chatbot", hint: "drop-in chatbot - persona, guardrails, streaming responses" },
39998
39991
  { key: "audio-align", hint: "audio + lyrics -> word-level timing JSON (GPU job)" },
39999
- { key: "i18n", hint: "multi-language web apps - language picker, RTL, translations" }
39992
+ { key: "i18n", hint: "multi-language web apps - language picker, RTL, translations" },
39993
+ { key: "records", hint: "registry-driven data plane - generic CRUD, validation, search, audit spine" },
39994
+ { key: "views", hint: "registry-driven UI over records - table, forms, kanban" },
39995
+ { key: "agent-api", hint: "named API keys for agent/script writes through the records kit" },
39996
+ { key: "contacts", hint: "multi-source contact layer - LinkedIn/Gmail import, dedupe with provenance" },
39997
+ { key: "stripe", hint: "charge your users - Stripe checkout, subscriptions, brokered webhooks" },
39998
+ { key: "notify", hint: "web push notifications - platform-owned keys, works on iOS home screen" }
40000
39999
  ];
40000
+
40001
+ // src/commands/add.ts
40001
40002
  function catalogText() {
40002
40003
  const width = Math.max(...[...STARTERS, ...BLANK, ...KITS].map((e) => e.key.length));
40003
40004
  const row2 = (e) => ` ${e.key.padEnd(width)} ${muted(e.hint)}`;
@@ -40451,7 +40452,19 @@ init_api();
40451
40452
  init_config();
40452
40453
  init_colors();
40453
40454
  var logsCommand = new Command("logs").description("View logs");
40454
- logsCommand.command("fn <name>").description("Show function logs").option("--limit <n>", "Max entries", "20").option("--json", "Output as JSON").action((name, opts) => run("Logs", async () => {
40455
+ logsCommand.command("fn <name>").description("Show function logs").option("--limit <n>", "Max entries", "20").option("--json", "Output as JSON (top-level ARRAY of invocations, newest first)").addHelpText("after", `
40456
+ Each line: time, status (ok / error / limit_exceeded), duration, trigger, and
40457
+ svc:N when the invocation made N platform service calls (notify, email, LLM,
40458
+ image, ...). Captured console output is indented beneath its invocation.
40459
+
40460
+ --json emits a top-level ARRAY (not an object) of invocation rows:
40461
+ [{ id, status, duration_ms, trigger_type, error_message,
40462
+ limits_consumed: { max_queries, max_service_calls, ... }, logs: [...], created_at }]
40463
+
40464
+ To confirm an individual service call succeeded (and see provider, latency,
40465
+ and its own error message), use the per-call log instead:
40466
+ $ gipity logs app --type services
40467
+ `).action((name, opts) => run("Logs", async () => {
40455
40468
  const config = requireConfig();
40456
40469
  const limit = parseInt(opts.limit, 10) || 20;
40457
40470
  const res = await get(
@@ -40472,8 +40485,10 @@ logsCommand.command("fn <name>").description("Show function logs").option("--lim
40472
40485
  const statusColor = log3.status === "ok" ? success : log3.status === "error" ? error : warning;
40473
40486
  const status = statusColor(log3.status.padEnd(8));
40474
40487
  const trigger = muted(log3.trigger_type.padEnd(8));
40488
+ const svcCalls = log3.limits_consumed?.max_service_calls;
40489
+ const svc = svcCalls ? ` ${muted(`svc:${svcCalls}`)}` : "";
40475
40490
  const err = log3.error_message ? ` ${error(`"${log3.error_message}"`)}` : "";
40476
- console.log(`${muted(time)} ${status} ${dur} ${trigger}${err}`);
40491
+ console.log(`${muted(time)} ${status} ${dur} ${trigger}${svc}${err}`);
40477
40492
  for (const line of log3.logs ?? []) {
40478
40493
  const lvlColor = line.level === "error" ? error : line.level === "warn" ? warning : muted;
40479
40494
  const tag = line.level === "log" ? "" : `${line.level}: `;
@@ -41026,6 +41041,7 @@ recordsCommand.command("delete <table> <id>").description("Delete a record").act
41026
41041
 
41027
41042
  // src/commands/fn.ts
41028
41043
  init_api();
41044
+ init_auth();
41029
41045
  init_config();
41030
41046
  init_colors();
41031
41047
  init_utils();
@@ -41038,6 +41054,9 @@ fnCommand.command("list").description("List functions").option("--json", "Output
41038
41054
  return f.description ? `${line}
41039
41055
  ${muted(f.description)}` : line;
41040
41056
  });
41057
+ if (!opts.json && res.data.length > 0) {
41058
+ console.log(muted(`Endpoint: POST ${config.apiBase}/api/${config.projectGuid}/fn/<name> (same on dev and prod; public path: gipity fn call <name> --anon)`));
41059
+ }
41041
41060
  }));
41042
41061
  fnCommand.command("logs <name>").description("Show recent logs").option("--limit <n>", "Max entries", "20").option("--json", "Output as JSON").action((name, opts) => run("Logs", async () => {
41043
41062
  const config = requireConfig();
@@ -41047,8 +41066,10 @@ fnCommand.command("logs <name>").description("Show recent logs").option("--limit
41047
41066
  printList(res.data, opts, "No execution logs.", (log3) => {
41048
41067
  const dur = log3.duration_ms != null ? `${log3.duration_ms}ms` : "?";
41049
41068
  const ts2 = new Date(log3.created_at).toLocaleString();
41050
- const statusColor = log3.status === "success" ? success : log3.status === "error" ? error : muted;
41069
+ const statusColor = log3.status === "ok" ? success : log3.status === "error" ? error : muted;
41051
41070
  let line = `${statusColor(log3.status)} ${dur} ${muted(log3.trigger_type || "http")} ${muted(ts2)}`;
41071
+ const svcCalls = log3.limits_consumed?.max_service_calls;
41072
+ if (svcCalls) line += ` ${muted(`svc:${svcCalls}`)}`;
41052
41073
  if (log3.error_message) line += `
41053
41074
  ${error(`error: ${log3.error_message}`)}`;
41054
41075
  for (const entry of log3.logs ?? []) {
@@ -41078,6 +41099,12 @@ fnCommand.command("call <name> [body]").description("Call a function").option("-
41078
41099
  const raw = bodyArg || opts.data || "{}";
41079
41100
  const body = JSON.parse(raw);
41080
41101
  const path5 = `/api/${config.projectGuid}/fn/${encodeURIComponent(name)}`;
41102
+ if (opts.anon) {
41103
+ console.error(muted("Auth: anonymous visitor (the public path a signed-out user hits)"));
41104
+ } else {
41105
+ const who = getAuth()?.email;
41106
+ console.error(muted(`Auth: calling as ${who ?? "your signed-in account"} (the owner persona; use --anon for the public visitor path)`));
41107
+ }
41081
41108
  const res = opts.anon ? await callAnon(config.projectGuid, name, body) : await post(path5, body);
41082
41109
  if (opts.field) {
41083
41110
  emitField(res.data, opts.field);
@@ -42492,6 +42519,10 @@ var testCommand = new Command("test").description("Run tests").argument("[path]"
42492
42519
  console.log(error(`Run failed: ${data.errorMessage}`));
42493
42520
  console.log("");
42494
42521
  }
42522
+ if (data.dbSummary) {
42523
+ console.log(muted(`Test db: ${data.dbSummary}`));
42524
+ console.log("");
42525
+ }
42495
42526
  if (data.failed > 0) {
42496
42527
  console.log(error("Failures:"));
42497
42528
  for (const r of data.results.filter((r7) => r7.status === "failed")) {
@@ -45575,7 +45606,7 @@ function registerInstallCommands(relayCommand2) {
45575
45606
 
45576
45607
  // src/commands/relay.ts
45577
45608
  var relayCommand = new Command("relay").description("Pair with the web CLI");
45578
- relayCommand.command("setup").description("Pair this machine and start the relay - non-interactive (for installers/GUIs)").option("--name <name>", "Device name shown in the web CLI (default: this machine's hostname)").option("--no-start", "Pair only; do not start the relay daemon now").option("--no-autostart", "Skip the OS login service (use when a supervising app owns the daemon)").option("--force", "Re-pair even if already paired (revokes the old device first)").option("--json", "Machine-readable output").action(async (opts) => {
45609
+ relayCommand.command("setup").description("Pair this machine and start the relay - non-interactive (for installers/GUIs)").option("--name <name>", "Device name shown in the web CLI (default: this machine's hostname)").option("--no-start", "Pair only; do not start the relay daemon now").option("--no-autostart", "Skip the OS login service (use when a supervising app owns the daemon)").option("--force", "Re-pair even if already paired (re-registers this machine, rotating its token)").option("--json", "Machine-readable output").action(async (opts) => {
45579
45610
  const fail = (code, message) => {
45580
45611
  if (opts.json) {
45581
45612
  console.log(JSON.stringify({ ok: false, code, error: message }));
@@ -46526,6 +46557,7 @@ var FLAG_ALIASES = {
46526
46557
  "--src": "--source-dir",
46527
46558
  "--srcdir": "--source-dir",
46528
46559
  "--parallel": "--concurrency",
46560
+ "--no-sync-output": "--discard-output",
46529
46561
  "--max": "--limit",
46530
46562
  "--from": "--since",
46531
46563
  "--after": "--since",
package/dist/prefs.js CHANGED
@@ -1,18 +1,15 @@
1
1
  /**
2
2
  * Machine-level user preferences (~/.gipity/prefs.json) - the `gipity build`
3
- * picker's memory. Deliberately NOT in a project's .gipity.json: per-project
4
- * storage would re-ask the agent/model questions on every new project, which
5
- * defeats "hit enter twice".
3
+ * agent picker's memory. Deliberately NOT in a project's .gipity.json:
4
+ * per-project storage would re-ask the agent question on every new project,
5
+ * which defeats "hit enter twice".
6
6
  *
7
7
  * Shape:
8
- * {
9
- * "lastAgent": "claude",
10
- * "lastModel": { "claude": "opus", "codex": null } // per-agent so a
11
- * } // Codex session never
12
- * // clobbers the Claude pick
8
+ * { "lastAgent": "claude" }
13
9
  *
14
- * A `lastModel` of null/absent means "Agent default" - launch passes no
15
- * model flag and the agent's own model memory stays authoritative.
10
+ * The model is never stored: `gipity build` doesn't ask, so the agent's own
11
+ * default (and its own model memory) stays authoritative unless the user
12
+ * passes --model, which we forward verbatim.
16
13
  */
17
14
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
18
15
  import { homedir } from 'os';
@@ -25,7 +22,6 @@ export function readPrefs() {
25
22
  const parsed = JSON.parse(readFileSync(PREFS_PATH, 'utf-8'));
26
23
  return {
27
24
  lastAgent: typeof parsed.lastAgent === 'string' ? parsed.lastAgent : undefined,
28
- lastModel: parsed.lastModel && typeof parsed.lastModel === 'object' ? parsed.lastModel : undefined,
29
25
  };
30
26
  }
31
27
  catch {
@@ -35,11 +31,7 @@ export function readPrefs() {
35
31
  export function writePrefs(update) {
36
32
  try {
37
33
  const current = readPrefs();
38
- const next = {
39
- ...current,
40
- ...update,
41
- lastModel: { ...current.lastModel, ...update.lastModel },
42
- };
34
+ const next = { ...current, ...update };
43
35
  mkdirSync(join(homedir(), '.gipity'), { recursive: true });
44
36
  writeFileSync(PREFS_PATH, JSON.stringify(next, null, 2) + '\n');
45
37
  }