gipity 1.1.1 → 1.1.3

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/README.md CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  The full-stack platform tuned for AI agents.
4
4
 
5
- [Gipity](https://gipity.ai) is the platform: hosting, databases, file storage, deployment, workflows, code execution, and monitoring. Agent-tuned from idea to deploy. Use standalone, or pair with Claude Code to give your local agent cloud superpowers. Any model, any infra, always your code.
5
+ [Gipity](https://gipity.ai) is the platform: hosting, databases, file storage, deployment, workflows, code execution, and monitoring. Agent-tuned from idea to deploy. Use standalone, or pair with your coding agent - Claude Code, Codex, or Grok - to give it cloud superpowers. Any model, any infra, always your code.
6
6
 
7
- This CLI connects [Claude Code](https://claude.ai/claude-code) to Gipity's cloud platform - databases, deployment, browser testing, image gen, and 50+ other capabilities your local agent doesn't have. It also syncs files so Claude Code and the Gipity web agent share the same project.
7
+ This CLI connects your coding agent - [Claude Code](https://claude.ai/claude-code), Codex, or Grok - to Gipity's cloud platform: databases, deployment, browser testing, image gen, and 50+ other capabilities your local agent doesn't have. It also syncs files so your local agent and the Gipity web agent share the same project.
8
8
 
9
9
  ## Getting Started
10
10
 
11
- One line installs everything. It sets up Node 18+ (if you don't already have it) and the Gipity CLI, with no sudo required:
11
+ **Step 1 - install.** One line installs everything. It sets up Node 18+ (if you don't already have it) and the Gipity CLI, with no sudo required:
12
12
 
13
13
  ```bash
14
14
  # macOS / Linux / WSL
@@ -18,13 +18,42 @@ curl -fsSL https://gipity.ai/install.sh | bash
18
18
  irm https://gipity.ai/install.ps1 | iex
19
19
  ```
20
20
 
21
- Then launch your coding agent wired into Gipity:
21
+ **Step 2 - pick your path.** There are three, and they mix freely:
22
+
23
+ | You want to... | Run |
24
+ |----------------|-----|
25
+ | Start building right now, from anywhere | `gipity build` |
26
+ | Use your own workflow in your own directory | `gipity init`, then `claude` / `codex` / `grok` |
27
+ | Drive this computer from gipity.ai (phone, browser) | `gipity connect` |
28
+
29
+ ### `gipity build` - start from anywhere
30
+
31
+ One command does everything: logs you in (6-digit email code), lets you pick or create a project, lets you pick your coding agent (Claude Code, Codex, or Grok - it installs the agent if needed), and launches it with the whole Gipity stack wired up.
22
32
 
23
33
  ```bash
24
- gipity claude
34
+ gipity build
25
35
  ```
26
36
 
27
- `gipity claude` walks you through login, project setup, and launches Claude Code. Using Codex, Gemini, or Cursor instead? Run `gipity init`.
37
+ ### `gipity init` - bring your own workflow
38
+
39
+ Prefer launching your agent yourself? From any project directory:
40
+
41
+ ```bash
42
+ gipity init
43
+ claude # or codex, or grok - whatever you use
44
+ ```
45
+
46
+ `init` links the directory to a Gipity project, writes CLAUDE.md/AGENTS.md primers so your agent understands Gipity, and installs the Gipity skills + file-sync hooks into the agent CLIs found on your machine (Claude Code, Codex, Grok; Cursor and Gemini get primer files too).
47
+
48
+ ### `gipity connect` - drive it from the web
49
+
50
+ Want to start chats from gipity.ai (including your phone) and have them run the coding agent on this computer? Connect it once:
51
+
52
+ ```bash
53
+ gipity connect
54
+ ```
55
+
56
+ It logs you in, pairs the machine, starts the Gipity relay in the background, and installs the login service so it survives reboots - then stops, without launching anything. From then on, open gipity.ai and start a chat to drive your agent here. Manage it with `gipity relay status` / `pause` / `resume` / `revoke`.
28
57
 
29
58
  ### Prefer npm
30
59
 
@@ -47,15 +76,7 @@ gipity update # force an immediate update now
47
76
 
48
77
  To opt out: `export DISABLE_AUTOUPDATER=1` (matches Claude Code), or set `{ "autoUpdates": false }` in `~/.gipity/settings.json`. CI environments are auto-detected and skipped.
49
78
 
50
- ## Quick Start
51
-
52
- One command. It walks you through login, project setup, and drops you into Claude Code.
53
-
54
- ```bash
55
- gipity claude --dangerously-skip-permissions
56
- ```
57
-
58
- That's it. You'll see:
79
+ ## What `gipity build` looks like
59
80
 
60
81
  ```
61
82
  Welcome to Gipity
@@ -72,16 +93,29 @@ That's it. You'll see:
72
93
  2. Create new project
73
94
 
74
95
  Choose (1-2): 2
75
- Project name [project01]: cool-app
96
+ Project name [project-001]: cool-app
76
97
  Creating "cool-app"...
77
98
  Created.
78
99
 
79
- Launching Claude Code...
100
+ Which coding agent?
101
+ 1. Claude Code (Anthropic)
102
+ 2. Codex (OpenAI)
103
+ 3. Grok (xAI)
104
+
105
+ Launching Claude Code, powered by Gipity.
80
106
  ```
81
107
 
82
- If you're already logged in, it skips straight to project setup. If you already have a project in the current directory, it skips straight to launching Claude Code.
108
+ If you're already logged in, it skips straight to the project picker. If you're already inside a Gipity project directory, it uses that project. Your last-used agent is the default next time.
109
+
110
+ Projects live in `~/GipityProjects/{project-slug}/` - created automatically on first use. Any extra flags (like `--model opus`) pass straight through to the agent. Useful flags:
83
111
 
84
- Projects live in `~/GipityProjects/{project-slug}/` - created automatically on first use. Any extra flags (like `--dangerously-skip-permissions`, `--model opus`, etc.) get passed through to Claude.
112
+ ```bash
113
+ gipity build --agent codex # Skip the agent picker
114
+ gipity build --new-project --name app # Create a fresh project without the picker
115
+ gipity build --project my-app # Open a specific project
116
+ gipity build --here # Use the current directory, not ~/GipityProjects/
117
+ gipity build -p "add a contact form" # Headless one-shot (no interactive session)
118
+ ```
85
119
 
86
120
  ### The manual way
87
121
 
@@ -92,26 +126,28 @@ gipity login --email you@example.com
92
126
  gipity login --code 123456
93
127
  cd my-project
94
128
  gipity init
95
- claude
129
+ claude # or codex, or grok
96
130
  ```
97
131
 
98
- ## Claude Code Integration
132
+ ## Coding Agent Integration
133
+
134
+ This is the good part. When you run `gipity init` (or `gipity build`) in a project, it wires two hooks into your agent (Claude Code, Codex, and Grok all get them):
99
135
 
100
- This is the good part. When you run `gipity init` in a project, it sets up two hooks in `.claude/settings.json`:
136
+ **Auto-push** - Every time your agent writes or edits a file, it gets pushed to Gipity in the background. No extra steps.
101
137
 
102
- **Auto-push** - Every time Claude Code writes or edits a file, it gets pushed to Gipity in the background. No extra steps.
138
+ **Auto-pull** - Before each turn, your agent pulls any changes that happened remotely (like if your Gipity agent built something via chat). It sees what changed and can pick up where things left off.
103
139
 
104
- **Auto-pull** - Before each turn, Claude Code pulls any changes that happened remotely (like if your Gipity agent built something via chat). Claude sees what changed and can pick up where things left off.
140
+ That means your local agent and your Gipity agent share the same files, same project, same context. You get the best of both - hands-on coding locally, autonomous agent work on Gipity.
105
141
 
106
- That means Claude Code and your Gipity agent share the same files, same project, same context. You get the best of both - Claude Code for hands-on coding, Gipity for autonomous agent work.
142
+ Sessions are also recorded to your Gipity project so you can watch them live at prompt.gipity.ai. Opt out per project with `gipity init --no-capture`.
107
143
 
108
144
  ### What gets set up
109
145
 
110
146
  ```
111
147
  .gipity.json # Project config (which project, which agent)
112
148
  .gipity/ # Local sync state (gitignored)
113
- .claude/settings.json # Hooks for auto-push and auto-pull
114
- CLAUDE.md # Gipity commands reference for Claude Code
149
+ .claude/settings.json # Hooks for auto-push and auto-pull (per-agent equivalents for Codex/Grok)
150
+ CLAUDE.md / AGENTS.md # Gipity commands reference for your agent
115
151
  ```
116
152
 
117
153
  ### Manual sync
@@ -128,9 +164,11 @@ gipity sync down # Pull remote changes
128
164
 
129
165
  | Command | What it does |
130
166
  |---------|-------------|
131
- | `gipity claude` | Log in, pick a project, and launch Claude Code - all in one |
167
+ | `gipity build` | Log in, pick a project, pick your coding agent, and launch it - all in one |
168
+ | `gipity init` | Link this directory to a project and set up your coding agent |
169
+ | `gipity connect` | Connect this computer to gipity.ai so the web CLI can drive it |
170
+ | `gipity relay` | Manage the relay (status, pause, resume, revoke) |
132
171
  | `gipity login` | Authenticate with email + verification code |
133
- | `gipity init` | Set up a Gipity project and configure Claude Code |
134
172
  | `gipity status` | Show project, agent, and auth info |
135
173
  | `gipity sync` | Sync files between local and Gipity |
136
174
  | `gipity push <file>` | Push a single file |
@@ -5,11 +5,6 @@ export const claudeCodeAdapter = {
5
5
  displayName: 'Claude Code',
6
6
  providerName: 'Anthropic',
7
7
  binary: 'claude',
8
- models: [
9
- { id: 'opus', label: 'Opus' },
10
- { id: 'sonnet', label: 'Sonnet' },
11
- { id: 'haiku', label: 'Haiku' },
12
- ],
13
8
  buildInteractiveArgs({ resume, model }) {
14
9
  const args = [];
15
10
  if (model)
@@ -4,10 +4,6 @@ export const codexAdapter = {
4
4
  displayName: 'Codex',
5
5
  providerName: 'OpenAI',
6
6
  binary: 'codex',
7
- models: [
8
- { id: 'gpt-5.1-codex', label: 'GPT-5.1 Codex' },
9
- { id: 'gpt-5.1-codex-mini', label: 'GPT-5.1 Codex Mini' },
10
- ],
11
7
  buildInteractiveArgs({ resume, model }) {
12
8
  const args = [];
13
9
  if (model)
@@ -4,10 +4,6 @@ export const grokAdapter = {
4
4
  displayName: 'Grok',
5
5
  providerName: 'xAI',
6
6
  binary: 'grok',
7
- models: [
8
- { id: 'grok-4.5', label: 'Grok 4.5' },
9
- { id: 'grok-code', label: 'Grok Code' },
10
- ],
11
7
  buildInteractiveArgs({ resume, model }) {
12
8
  const args = [];
13
9
  if (model)
package/dist/api.js CHANGED
@@ -56,8 +56,8 @@ async function bearerToken() {
56
56
  /** True when a long-lived agent API token (GIPITY_TOKEN) is in play. Such a token
57
57
  * is static — it can't be refreshed — so the 401 self-heal path must skip it and
58
58
  * the "run: gipity login" hint must not be shown (that's the wrong recovery for
59
- * env-token callers). */
60
- function usingEnvToken() {
59
+ * env-token callers). Exported so `gipity status` can name the auth source. */
60
+ export function usingEnvToken() {
61
61
  return !!process.env.GIPITY_TOKEN?.trim();
62
62
  }
63
63
  // A transient 401 on a session token means the access token the server just saw
@@ -83,9 +83,12 @@ async function shouldRetryAfter401(status, retried) {
83
83
  /** Append a re-login hint to a 401 message for session users. A 401 that
84
84
  * survived the refresh-and-retry is an unrecoverable session (the refresh token
85
85
  * is gone too); env-token callers authenticate differently, so don't misdirect
86
- * them. */
86
+ * them. Names the headless path too — an unattended agent has no mailbox for
87
+ * the emailed login code, and dead-ending there stranded whole runs (cli#137). */
87
88
  function with401Hint(status, message) {
88
- return status === 401 && !usingEnvToken() ? `${message} — run: gipity login` : message;
89
+ return status === 401 && !usingEnvToken()
90
+ ? `${message} — run: gipity login (headless/CI: set GIPITY_TOKEN instead — gipity skill read agent-deploy)`
91
+ : message;
89
92
  }
90
93
  async function getHeaders() {
91
94
  return {
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The `gipity add` catalog - template + kit keys with terse list hints.
3
+ *
4
+ * AUTO-GENERATED - do not edit directly.
5
+ * Source: platform/packages/shared/src/constants.ts (TEMPLATES/KITS cliHint fields)
6
+ * Run `just build-knowledge` to refresh.
7
+ */
8
+ /** Visible starter templates (complete working demos). */
9
+ export const STARTERS = [
10
+ { key: 'web-vision-cam', hint: 'fullscreen camera app with on-device vision (MediaPipe)' },
11
+ { key: 'object-spotter', hint: 'camera app that boxes, labels, and counts objects (YOLOX on-device)' },
12
+ { key: '2d-game', hint: '2D games with Phaser 3 - platformer, arcade, puzzle' },
13
+ { key: '3d-world', hint: 'playable 3D multiplayer rocket-launcher demo' },
14
+ { key: 'karaoke-captions', hint: 'audio + lyrics -> word-synced karaoke captions (GPU job)' },
15
+ { key: 'outreach-agent', hint: 'AI-run outreach funnel - import contacts, draft + auto-send staged emails' },
16
+ { key: 'paid-app', hint: 'storefront that charges real money - Stripe checkout, members area, billing' },
17
+ { key: 'notify-demo', hint: 'web-push demo - enable notifications, send a real ping' },
18
+ ];
19
+ /** Visible blank-wiring templates. */
20
+ export const BLANK = [
21
+ { key: 'web-simple', hint: 'static frontend-only site - pages, dashboards, simple games' },
22
+ { key: 'web-fullstack', hint: 'backend API + database wiring - frontend, functions, migrations; deploys green' },
23
+ { key: '3d-engine', hint: '3D multiplayer wiring - Three.js + Rapier + Gipity Realtime' },
24
+ { key: 'api', hint: 'pure API backend, no frontend - one example function + test' },
25
+ ];
26
+ /** Hidden templates - installable by exact key, omitted from listings. */
27
+ export const HIDDEN = [
28
+ { key: 'app-itsm', hint: 'IT service management / helpdesk / ticketing' },
29
+ { key: 'monitor', hint: 'account observability dashboard (auto-installed per account)' },
30
+ ];
31
+ /** Kits - building blocks added into an existing app. */
32
+ export const KITS = [
33
+ { key: 'realtime', hint: 'multiplayer / presence / shared state' },
34
+ { key: 'web-vision-mediapipe', hint: 'browser camera vision - gesture, pose, object detection' },
35
+ { key: 'web-vision-detect', hint: 'browser object detection - YOLOX, WebGPU/WASM, custom models' },
36
+ { key: 'chatbot', hint: 'drop-in chatbot - persona, guardrails, streaming responses' },
37
+ { key: 'audio-align', hint: 'audio + lyrics -> word-level timing JSON (GPU job)' },
38
+ { key: 'i18n', hint: 'multi-language web apps - language picker, RTL, translations' },
39
+ { key: 'records', hint: 'registry-driven data plane - generic CRUD, validation, search, audit spine' },
40
+ { key: 'views', hint: 'registry-driven UI over records - table, forms, kanban' },
41
+ { key: 'agent-api', hint: 'named API keys for agent/script writes through the records kit' },
42
+ { key: 'contacts', hint: 'multi-source contact layer - LinkedIn/Gmail import, dedupe with provenance' },
43
+ { key: 'stripe', hint: 'charge your users - Stripe checkout, subscriptions, brokered webhooks' },
44
+ { key: 'notify', hint: 'web push notifications - platform-owned keys, works on iOS home screen' },
45
+ ];
46
+ //# sourceMappingURL=catalog.js.map
@@ -8,28 +8,15 @@ import { sync } from '../sync.js';
8
8
  import { success, muted, bold, warning, error as clrError } from '../colors.js';
9
9
  import { run } from '../helpers/index.js';
10
10
  import { createProgressReporter, withSpinner } from '../progress.js';
11
- const STARTERS = [
12
- { key: 'web-vision-cam', hint: 'fullscreen camera app with on-device vision (MediaPipe)' },
13
- { key: 'object-spotter', hint: 'camera app that boxes, labels, and counts objects (YOLOX on-device)' },
14
- { key: '2d-game', hint: '2D games with Phaser 3 - platformer, arcade, puzzle' },
15
- { key: '3d-world', hint: 'playable 3D multiplayer rocket-launcher demo' },
16
- { key: 'karaoke-captions', hint: 'audio + lyrics -> word-synced karaoke captions (GPU job)' },
17
- ];
18
- const BLANK = [
19
- { key: 'web-simple', hint: 'static frontend-only site - pages, dashboards, simple games' },
20
- { key: 'web-fullstack', hint: 'backend API + database wiring - frontend, functions, migrations; deploys green' },
21
- { key: 'api', hint: 'pure API backend, no frontend - one example function + test' },
22
- { key: '3d-engine', hint: '3D multiplayer wiring - Three.js + Rapier + Gipity Realtime' },
23
- ];
24
- const HIDDEN = [{ key: 'app-itsm', hint: 'IT service management / helpdesk / ticketing' }];
25
- const KITS = [
26
- { key: 'realtime', hint: 'multiplayer / presence / shared state' },
27
- { key: 'web-vision-mediapipe', hint: 'browser camera vision - gesture, pose, object detection' },
28
- { key: 'web-vision-detect', hint: 'browser object detection - YOLOX, WebGPU/WASM, custom models' },
29
- { key: 'chatbot', hint: 'drop-in chatbot - persona, guardrails, streaming responses' },
30
- { key: 'audio-align', hint: 'audio + lyrics -> word-level timing JSON (GPU job)' },
31
- { key: 'i18n', hint: 'multi-language web apps - language picker, RTL, translations' },
32
- ];
11
+ // Catalog GENERATED from platform/packages/shared (TEMPLATES + KITS cliHint
12
+ // fields) into src/catalog.ts by platform/scripts/build-knowledge.ts - the CLI
13
+ // ships as a standalone npm package and can't depend on the private shared
14
+ // workspace. The hand-mirrored copy that used to live here silently dropped
15
+ // every new catalog entry; edit constants.ts + `just build-knowledge` instead.
16
+ //
17
+ // Templates install a whole app (blank wiring or a working starter demo).
18
+ // Kits are reusable building blocks added into an existing app's src/packages/.
19
+ import { STARTERS, BLANK, KITS } from '../catalog.js';
33
20
  // The catalog block, rendered once and reused by the full help output
34
21
  // (`gipity add` / `gipity add --help`) and the bare listing (`gipity add
35
22
  // --list`) so they can never drift. Three sections, one entry per line, keys
@@ -233,9 +233,9 @@ function suggestProjectName(existingSlugs) {
233
233
  return `project-${Date.now().toString(36).slice(-6)}`;
234
234
  }
235
235
  /** Both launch commands share one implementation: `gipity build` (the
236
- * documented start-from-anywhere funnel: project picker + agent picker +
237
- * model picker) and the hidden legacy `gipity claude` (identical behavior
238
- * with the agent pinned to Claude and no agent/model questions - kept
236
+ * documented start-from-anywhere funnel: project picker + agent picker) and
237
+ * the hidden legacy `gipity claude` (identical behavior with the agent
238
+ * pinned to Claude and no agent question - kept
239
239
  * working forever because deployed relay daemons and the GUI installer
240
240
  * invoke it, but no longer advertised anywhere). */
241
241
  function createLaunchCommand(name, cfg = {}) {
@@ -250,7 +250,7 @@ function createLaunchCommand(name, cfg = {}) {
250
250
  // Forwarded to the agent via the unknown-arg passthrough below (NOT in the
251
251
  // gipity strip lists). Declared here only so it shows up in --help — without
252
252
  // this, callers can't discover that the session model is selectable.
253
- .option('--model <model>', 'Model for the session (e.g. opus, sonnet - or skip it to use your agent\'s own default)')
253
+ .option('--model <model>', 'Model for the session, passed straight to the agent (e.g. opus) - omit it and the agent uses its own default')
254
254
  .option('--bypass-approvals', 'Skip the agent\'s interactive tool approvals (headless runs; the relay daemon sets this)')
255
255
  .allowUnknownOption(true)
256
256
  .allowExcessArguments(true);
@@ -696,22 +696,18 @@ async function runLaunch(cmdName, cmdCfg, opts) {
696
696
  }
697
697
  }
698
698
  const adapter = getAdapter(agentKey);
699
- // Model: an explicit --model (in the passthrough args) wins and skips
700
- // the question. Interactive launches get the picker - option 1 is
701
- // "Agent default", which passes NO model flag so the agent's own model
702
- // memory stays authoritative. Headless runs never prompt. Only an
703
- // interactive answer is remembered (a daemon dispatch or the legacy
704
- // `gipity claude` shim must not clobber the user's picker memory).
705
- const modelFlagGiven = rawArgs.some(a => a === '--model' || a.startsWith('--model='));
706
- let pickedModel = null;
707
- if (!nonInteractive && !modelFlagGiven && !cmdCfg.presetAgent) {
708
- pickedModel = await pickModel(adapter, prefs.lastModel?.[adapter.key] ?? null);
709
- writePrefs({ lastAgent: adapter.key, lastModel: { [adapter.key]: pickedModel } });
699
+ // We never ask about the model: an explicit `--model` passes straight
700
+ // through to the agent, and with no flag the agent's own default (and
701
+ // its own model memory) stays authoritative. Only an interactive pick
702
+ // is remembered (a daemon dispatch or the legacy `gipity claude` shim
703
+ // must not clobber the user's agent memory).
704
+ if (!nonInteractive && !cmdCfg.presetAgent) {
705
+ writePrefs({ lastAgent: adapter.key });
710
706
  }
711
707
  // ── Step 4: Launch the agent ───────────────────────────────────────
712
708
  if (adapter.key !== 'claude') {
713
709
  await launchNonClaudeAgent(adapter, {
714
- cmdName, opts, rawArgs, nonInteractive, headlessOut, runStart, pickedModel,
710
+ cmdName, opts, rawArgs, nonInteractive, headlessOut, runStart,
715
711
  });
716
712
  return;
717
713
  }
@@ -873,12 +869,9 @@ async function runLaunch(cmdName, cmdCfg, opts) {
873
869
  }
874
870
  }
875
871
  else {
872
+ // Any `--model` the user passed is already in claudeArgs (passthrough);
873
+ // with no flag Claude picks its own default.
876
874
  allArgs = initialPrompt ? [initialPrompt, ...claudeArgs] : claudeArgs;
877
- // Model from the interactive picker (an explicit --model flag skipped
878
- // the picker and is already in claudeArgs; "Agent default" is null and
879
- // deliberately passes nothing).
880
- if (pickedModel)
881
- allArgs.push('--model', pickedModel);
882
875
  }
883
876
  // Single-command headless flows (`--new-project` / `--project` with -p)
884
877
  // have no human to approve tool use - auto-bypass so Claude can actually
@@ -957,10 +950,11 @@ async function runLaunch(cmdName, cmdCfg, opts) {
957
950
  process.exit(1);
958
951
  }
959
952
  }
960
- // ─── Agent + model pickers ────────────────────────────────────────────────
953
+ // ─── Agent picker ─────────────────────────────────────────────────────────
961
954
  // Same pick-a-number-or-hit-enter format as the project picker. The
962
- // enter-enter path must stay the visibly obvious one: default agent =
963
- // last-used (else Claude), default model = "Agent default".
955
+ // enter-enter path must stay the visibly obvious one: the default agent is
956
+ // the last-used one (else Claude). We deliberately don't ask about the model
957
+ // - the agent's own default is always right and a curated list only rots.
964
958
  async function pickAgent(lastUsed) {
965
959
  const defaultKey = lastUsed && AGENT_KEYS.includes(lastUsed) ? lastUsed : 'claude';
966
960
  const defaultIdx = AGENT_ADAPTERS.findIndex(a => a.key === defaultKey) + 1;
@@ -983,22 +977,6 @@ async function pickAgent(lastUsed) {
983
977
  console.log('');
984
978
  return AGENT_ADAPTERS[choice - 1].key;
985
979
  }
986
- /** Returns the model id to pass via --model, or null for "Agent default"
987
- * (no flag at all - the agent's own model memory stays authoritative). */
988
- async function pickModel(adapter, lastUsed) {
989
- console.log(` ${bold('Which model?')}\n`);
990
- const lastIsModel = lastUsed && adapter.models.some(m => m.id === lastUsed);
991
- const defaultIdx = lastIsModel ? adapter.models.findIndex(m => m.id === lastUsed) + 2 : 1;
992
- console.log(` ${bold('1.')} Agent default ${muted(defaultIdx === 1 ? '(recommended)' : '')}`);
993
- adapter.models.forEach((m, i) => {
994
- const note = m.id === lastUsed ? ` ${muted('(last used)')}` : '';
995
- console.log(` ${bold(`${i + 2}.`)} ${m.label}${note}`);
996
- });
997
- console.log('');
998
- const choice = await pickOne('Choose', adapter.models.length + 1, defaultIdx);
999
- console.log('');
1000
- return choice === 1 ? null : adapter.models[choice - 2].id;
1001
- }
1002
980
  // ─── Non-Claude agent launch (Codex, Grok) ────────────────────────────────
1003
981
  // The Claude path above carries years of Claude-specific plumbing (plugin
1004
982
  // install, folder trust, stream-json capture gating, -p context wrapping).
@@ -1008,7 +986,7 @@ async function pickModel(adapter, lastUsed) {
1008
986
  // (there is no stream-json ingest path for them, so GIPITY_CAPTURE is never
1009
987
  // turned off here).
1010
988
  async function launchNonClaudeAgent(adapter, ctx) {
1011
- const { opts, rawArgs, nonInteractive, headlessOut, runStart, pickedModel } = ctx;
989
+ const { opts, rawArgs, nonInteractive, headlessOut, runStart } = ctx;
1012
990
  // ── Binary ──────────────────────────────────────────────────────────────
1013
991
  if (!binaryOnPath(adapter.binary)) {
1014
992
  let installed = false;
@@ -1030,7 +1008,7 @@ async function launchNonClaudeAgent(adapter, ctx) {
1030
1008
  const valueFlags = ['--api-base', '--name', '--project', '--agent'];
1031
1009
  let message = null;
1032
1010
  let resume;
1033
- let model = pickedModel ?? undefined;
1011
+ let model;
1034
1012
  const extras = [];
1035
1013
  for (let i = 0; i < rawArgs.length; i++) {
1036
1014
  const arg = rawArgs[i];
@@ -44,6 +44,11 @@ dbCommand
44
44
  console.log(columns.map(c => String(row[c] ?? 'NULL')).join('\t'));
45
45
  }
46
46
  }
47
+ // DML with RETURNING carries both rows and a count - the rows are the
48
+ // table above; still name how many rows the statement touched.
49
+ if (res.data.affectedRows !== undefined) {
50
+ console.log(`Affected rows: ${res.data.affectedRows}`);
51
+ }
47
52
  }
48
53
  else if (res.data.affectedRows !== undefined) {
49
54
  console.log(`Affected rows: ${res.data.affectedRows}`);
@@ -51,6 +56,11 @@ dbCommand
51
56
  else if (res.data.results) {
52
57
  console.log(JSON.stringify(res.data.results, null, 2));
53
58
  }
59
+ else {
60
+ // Unknown payload shape: print it raw rather than nothing - silence
61
+ // here reads as "the query returned no data", which is a lie.
62
+ console.log(JSON.stringify(res.data, null, 2));
63
+ }
54
64
  }
55
65
  }));
56
66
  dbCommand
@@ -146,6 +146,13 @@ export const deployCommand = new Command('deploy')
146
146
  // has to reconstruct the URL convention or guess a subdomain.
147
147
  if (d.url)
148
148
  console.log(`${muted('Live:')} ${brand(d.url)}`);
149
+ // Functions are served from the API host, NOT the static Live URL -
150
+ // for an API-only app the endpoint address IS the deliverable, so name
151
+ // it instead of leaving it to be inferred from a template comment.
152
+ if (d.phases?.some(p => p.name === 'functions')) {
153
+ console.log(`${muted('Functions:')} POST ${brand(`${config.apiBase}/api/${config.projectGuid}/fn/<name>`)}`);
154
+ console.log(muted(' (same address on dev and prod; names: gipity fn list; verify the public path: gipity fn call <name> --anon)'));
155
+ }
149
156
  await inspectAfter();
150
157
  }
151
158
  }));
@@ -1,5 +1,6 @@
1
1
  import { Command } from 'commander';
2
2
  import { get, post, del, publicPost } from '../api.js';
3
+ import { getAuth } from '../auth.js';
3
4
  import { requireConfig } from '../config.js';
4
5
  import { error as clrError, bold, muted, success, warning } from '../colors.js';
5
6
  import { run, printList, emitField } from '../helpers/index.js';
@@ -17,6 +18,11 @@ fnCommand
17
18
  const line = `${bold(f.name)} ${muted(`v${f.version}`)} ${muted(f.auth_level)} ${muted(`timeout=${f.timeout_ms}ms`)}`;
18
19
  return f.description ? `${line}\n${muted(f.description)}` : line;
19
20
  });
21
+ // The callable address is the deliverable for API-only apps - name it here
22
+ // rather than leaving it to be inferred from a template comment.
23
+ if (!opts.json && res.data.length > 0) {
24
+ console.log(muted(`Endpoint: POST ${config.apiBase}/api/${config.projectGuid}/fn/<name> (same on dev and prod; public path: gipity fn call <name> --anon)`));
25
+ }
20
26
  }));
21
27
  fnCommand
22
28
  .command('logs <name>')
@@ -29,8 +35,15 @@ fnCommand
29
35
  printList(res.data, opts, 'No execution logs.', log => {
30
36
  const dur = log.duration_ms != null ? `${log.duration_ms}ms` : '?';
31
37
  const ts = new Date(log.created_at).toLocaleString();
32
- const statusColor = log.status === 'success' ? success : log.status === 'error' ? clrError : muted;
38
+ // The runtime writes 'ok' | 'error' | 'limit_exceeded' - never 'success'
39
+ // (same dead value already fixed in `logs fn`).
40
+ const statusColor = log.status === 'ok' ? success : log.status === 'error' ? clrError : muted;
33
41
  let line = `${statusColor(log.status)} ${dur} ${muted(log.trigger_type || 'http')} ${muted(ts)}`;
42
+ // Platform service calls made inside the invocation (notify, email,
43
+ // LLM, ...) - per-call outcomes live in `gipity logs app --type services`.
44
+ const svcCalls = log.limits_consumed?.max_service_calls;
45
+ if (svcCalls)
46
+ line += ` ${muted(`svc:${svcCalls}`)}`;
34
47
  if (log.error_message)
35
48
  line += `\n${clrError(`error: ${log.error_message}`)}`;
36
49
  // Captured console.log/warn/error output for this invocation.
@@ -69,6 +82,16 @@ fnCommand
69
82
  const raw = bodyArg || opts.data || '{}';
70
83
  const body = JSON.parse(raw);
71
84
  const path = `/api/${config.projectGuid}/fn/${encodeURIComponent(name)}`;
85
+ // Name the calling identity (stderr, so stdout stays parseable). Without
86
+ // this, a "test the anonymous path" call silently runs authenticated as
87
+ // the signed-in owner and the public path never gets exercised.
88
+ if (opts.anon) {
89
+ console.error(muted('Auth: anonymous visitor (the public path a signed-out user hits)'));
90
+ }
91
+ else {
92
+ const who = getAuth()?.email;
93
+ console.error(muted(`Auth: calling as ${who ?? 'your signed-in account'} (the owner persona; use --anon for the public visitor path)`));
94
+ }
72
95
  const res = opts.anon
73
96
  ? await callAnon(config.projectGuid, name, body)
74
97
  : await post(path, body);
@@ -9,7 +9,20 @@ logsCommand
9
9
  .command('fn <name>')
10
10
  .description('Show function logs')
11
11
  .option('--limit <n>', 'Max entries', '20')
12
- .option('--json', 'Output as JSON')
12
+ .option('--json', 'Output as JSON (top-level ARRAY of invocations, newest first)')
13
+ .addHelpText('after', `
14
+ Each line: time, status (ok / error / limit_exceeded), duration, trigger, and
15
+ svc:N when the invocation made N platform service calls (notify, email, LLM,
16
+ image, ...). Captured console output is indented beneath its invocation.
17
+
18
+ --json emits a top-level ARRAY (not an object) of invocation rows:
19
+ [{ id, status, duration_ms, trigger_type, error_message,
20
+ limits_consumed: { max_queries, max_service_calls, ... }, logs: [...], created_at }]
21
+
22
+ To confirm an individual service call succeeded (and see provider, latency,
23
+ and its own error message), use the per-call log instead:
24
+ $ gipity logs app --type services
25
+ `)
13
26
  .action((name, opts) => run('Logs', async () => {
14
27
  const config = requireConfig();
15
28
  const limit = parseInt(opts.limit, 10) || 20;
@@ -32,8 +45,14 @@ logsCommand
32
45
  const statusColor = log.status === 'ok' ? success : log.status === 'error' ? clrError : warning;
33
46
  const status = statusColor(log.status.padEnd(8));
34
47
  const trigger = muted(log.trigger_type.padEnd(8));
48
+ // Service calls made inside the invocation (notify, email, LLM, ...) are
49
+ // otherwise invisible here - surface the count so "did my send happen?"
50
+ // doesn't require spelunking --json limits counters. Per-call outcomes
51
+ // live in `gipity logs app --type services`.
52
+ const svcCalls = log.limits_consumed?.max_service_calls;
53
+ const svc = svcCalls ? ` ${muted(`svc:${svcCalls}`)}` : '';
35
54
  const err = log.error_message ? ` ${clrError(`"${log.error_message}"`)}` : '';
36
- console.log(`${muted(time)} ${status} ${dur} ${trigger}${err}`);
55
+ console.log(`${muted(time)} ${status} ${dur} ${trigger}${svc}${err}`);
37
56
  // Captured console.log/warn/error output for this invocation, indented
38
57
  // under its summary line. Empty for calls that printed nothing.
39
58
  for (const line of log.logs ?? []) {
@@ -274,7 +274,8 @@ export function slowRenderMessage(fps, o) {
274
274
  return `${warning('⚠ Slow render:')} page painted at ${fps} fps. `
275
275
  + `Waiting on real time (setTimeout) advances animation/physics time far slower than it looks — `
276
276
  + `assertions after a wall-clock wait can report a false negative. `
277
- + `Step the app's own loop deterministically instead (3D templates: ${bold('core.advance(seconds)')}).`;
277
+ + `Step the app's own loop deterministically instead (3D templates: ${bold('core.advance(seconds)')}; `
278
+ + `2D/Phaser template: ${bold("(await import('./js/config.js')).advance(seconds)")}).`;
278
279
  }
279
280
  /** Poll the async eval job until it finishes. Eval runs server-side as a
280
281
  * short-lived job (so a long --wait can't trip the gateway idle timeout);
@@ -570,7 +571,10 @@ export const pageEvalCommand = new Command('eval')
570
571
  }));
571
572
  return;
572
573
  }
573
- console.log(`${brand('Eval')} ${bold(d.url || url)}`);
574
+ // Context echo (which URL, which script) goes to stderr: the caller
575
+ // already knows what it sent, and keeping stdout = qualifiers + result
576
+ // means `2>/dev/null` (or any stdout capture) yields just the answer.
577
+ console.error(`${brand('Eval')} ${bold(d.url || url)}`);
574
578
  if (d.navigationIncomplete) {
575
579
  console.log(`${warning('⚠ Navigation incomplete:')} ${d.note || 'page did not reach full load'}`);
576
580
  }
@@ -597,7 +601,7 @@ export const pageEvalCommand = new Command('eval')
597
601
  console.log(`${muted('Camera:')} ${camera.name} ${muted('(played as the webcam feed; getUserMedia resolves)')}`);
598
602
  if (hosted.length)
599
603
  console.log(`${muted('Fixtures:')} ${hosted.map((h) => h.name).join(', ')}`);
600
- console.log(opts.file ? `${muted('Script:')} ${opts.file}` : `${muted('Expression:')} ${summarizeExpr(expr)}`);
604
+ console.error(opts.file ? `${muted('Script:')} ${opts.file}` : `${muted('Expression:')} ${summarizeExpr(expr)}`);
601
605
  console.log(`\n${result.trim() ? result : muted('(empty result)')}`);
602
606
  if (noValue)
603
607
  console.log(muted(`\n${EVAL_NO_VALUE_HINT}`));
@@ -29,7 +29,7 @@ relayCommand
29
29
  .option('--name <name>', 'Device name shown in the web CLI (default: this machine\'s hostname)')
30
30
  .option('--no-start', 'Pair only; do not start the relay daemon now')
31
31
  .option('--no-autostart', 'Skip the OS login service (use when a supervising app owns the daemon)')
32
- .option('--force', 'Re-pair even if already paired (revokes the old device first)')
32
+ .option('--force', 'Re-pair even if already paired (re-registers this machine, rotating its token)')
33
33
  .option('--json', 'Machine-readable output')
34
34
  .action(async (opts) => {
35
35
  const fail = (code, message) => {