gipity 1.1.4 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/adopt-cwd.js +1 -1
  2. package/dist/agents/agy.js +52 -0
  3. package/dist/agents/index.js +2 -0
  4. package/dist/api.js +19 -3
  5. package/dist/banner.js +3 -5
  6. package/dist/capture/sources/agy.js +88 -0
  7. package/dist/client-context.js +9 -0
  8. package/dist/commands/add.js +1 -1
  9. package/dist/commands/brand.js +125 -0
  10. package/dist/commands/build.js +13 -12
  11. package/dist/commands/chat.js +1 -1
  12. package/dist/commands/deploy.js +1 -1
  13. package/dist/commands/fn.js +2 -8
  14. package/dist/commands/generate.js +5 -5
  15. package/dist/commands/gmail.js +1 -1
  16. package/dist/commands/init.js +2 -2
  17. package/dist/commands/job.js +10 -5
  18. package/dist/commands/key.js +91 -0
  19. package/dist/commands/load.js +2 -2
  20. package/dist/commands/login.js +7 -2
  21. package/dist/commands/page-eval.js +47 -8
  22. package/dist/commands/page-fetch.js +14 -10
  23. package/dist/commands/page-inspect.js +2 -2
  24. package/dist/commands/page-screenshot.js +5 -5
  25. package/dist/commands/page-test.js +1 -1
  26. package/dist/commands/records.js +57 -19
  27. package/dist/commands/remove.js +1 -1
  28. package/dist/commands/sandbox.js +1 -1
  29. package/dist/commands/save.js +1 -1
  30. package/dist/commands/secrets.js +1 -1
  31. package/dist/commands/service.js +1 -1
  32. package/dist/commands/setup.js +4 -6
  33. package/dist/commands/status.js +41 -12
  34. package/dist/commands/storage.js +2 -2
  35. package/dist/commands/sync.js +15 -0
  36. package/dist/commands/test.js +1 -1
  37. package/dist/commands/token.js +6 -1
  38. package/dist/commands/uninstall.js +14 -1
  39. package/dist/commands/upload.js +1 -1
  40. package/dist/commands/workflow.js +1 -1
  41. package/dist/hooks/capture-runner.js +20 -8
  42. package/dist/index.js +1046 -518
  43. package/dist/knowledge.js +4 -1
  44. package/dist/login-flow.js +44 -2
  45. package/dist/project-setup.js +2 -0
  46. package/dist/relay/diagnostics.js +4 -2
  47. package/dist/relay/onboarding.js +26 -44
  48. package/dist/setup.js +198 -16
  49. package/dist/sync.js +6 -6
  50. package/dist/template-vars.js +74 -0
  51. package/dist/utils.js +60 -3
  52. package/package.json +2 -2
package/dist/adopt-cwd.js CHANGED
@@ -167,7 +167,7 @@ export function formatCwdLabel(cwd) {
167
167
  const tail = norm.slice(home.length + 1).split(sep);
168
168
  if (tail.length <= 2)
169
169
  return '~/' + tail.join('/');
170
- return '~/…/' + tail.slice(-2).join('/');
170
+ return '~/.../' + tail.slice(-2).join('/');
171
171
  }
172
172
  // Outside home: show parent/this for non-root paths.
173
173
  const parts = norm.split(sep).filter(Boolean);
@@ -0,0 +1,52 @@
1
+ /** LLM_MODELS canonical id -> agy's own spaced --model display string, for the
2
+ * subset of the Gemini catalog agy actually offers. Anything not listed here
3
+ * passes through translateModel() unchanged - either it's already agy's own
4
+ * format (a human typed it directly), or it's a Gemini id agy doesn't have an
5
+ * equivalent for, in which case agy's own `--model` validation is the right
6
+ * place to reject it, not a guess made here. */
7
+ const MODEL_ID_TO_AGY_DISPLAY = {
8
+ 'gemini-3.5-flash': 'Gemini 3.5 Flash (Medium)',
9
+ 'gemini-3.1-pro-preview': 'Gemini 3.1 Pro (High)',
10
+ };
11
+ function translateModel(model) {
12
+ return MODEL_ID_TO_AGY_DISPLAY[model] ?? model;
13
+ }
14
+ function projectArgs(resume) {
15
+ return resume ? ['--conversation', resume] : ['--new-project'];
16
+ }
17
+ export const agyAdapter = {
18
+ key: 'agy',
19
+ source: 'agy',
20
+ displayName: 'Antigravity',
21
+ providerName: 'Google',
22
+ binary: 'agy',
23
+ buildInteractiveArgs({ resume, model }) {
24
+ const args = [...projectArgs(resume)];
25
+ if (model)
26
+ args.push('--model', translateModel(model));
27
+ return args;
28
+ },
29
+ buildHeadlessArgs({ message, resume, model, bypassApprovals }) {
30
+ const args = ['-p', message, ...projectArgs(resume)];
31
+ if (model)
32
+ args.push('--model', translateModel(model));
33
+ if (bypassApprovals)
34
+ args.push('--dangerously-skip-permissions');
35
+ return args;
36
+ },
37
+ sessionIdFromStreamEvent() {
38
+ // agy has no --json/--output-format flag (confirmed: `agy --help` lists
39
+ // none, and `-p` stdout is plain prose) - there is no stream to read a
40
+ // session id off. Capture relies entirely on hooks (see class comment).
41
+ return null;
42
+ },
43
+ hooksSupportedOnPlatform(platform) {
44
+ return platform !== 'win32'; // agy's hooks.json wiring is POSIX-only, like Codex's
45
+ },
46
+ // No stream-json ingest mapper for agy: hook capture carries the full
47
+ // conversationId on every event (confirmed live, including headless -p),
48
+ // so the daemon doesn't need to parse agy's stdout at all.
49
+ daemonStreamCapture: false,
50
+ installHint: 'see https://antigravity.google (Google account sign-in required)',
51
+ };
52
+ //# sourceMappingURL=agy.js.map
@@ -1,11 +1,13 @@
1
1
  import { claudeCodeAdapter } from './claude-code.js';
2
2
  import { codexAdapter } from './codex.js';
3
3
  import { grokAdapter } from './grok.js';
4
+ import { agyAdapter } from './agy.js';
4
5
  /** Picker/help order: Claude first (the default), then the others. */
5
6
  export const AGENT_ADAPTERS = [
6
7
  claudeCodeAdapter,
7
8
  codexAdapter,
8
9
  grokAdapter,
10
+ agyAdapter,
9
11
  ];
10
12
  export const AGENT_KEYS = AGENT_ADAPTERS.map(a => a.key);
11
13
  export function getAdapter(key) {
package/dist/api.js CHANGED
@@ -366,12 +366,12 @@ export async function getAccountSlug() {
366
366
  /** Unauthenticated request (for login/verify, and anonymous-visitor calls like
367
367
  * `fn call --anon`). extraHeaders lets callers attach non-auth headers such as
368
368
  * X-App-Token; no Authorization header is ever sent. */
369
- export async function publicPost(path, body, extraHeaders) {
369
+ export async function publicRequest(method, path, body, extraHeaders) {
370
370
  const url = `${baseUrl()}${path}`;
371
371
  const res = await fetch(url, {
372
- method: 'POST',
372
+ method,
373
373
  headers: { ...clientHeaders(), 'Content-Type': 'application/json', ...extraHeaders },
374
- body: JSON.stringify(body),
374
+ body: body === undefined ? undefined : JSON.stringify(body),
375
375
  });
376
376
  if (!res.ok) {
377
377
  const json = await res.json().catch(() => ({ error: { code: 'UNKNOWN', message: res.statusText } }));
@@ -380,4 +380,20 @@ export async function publicPost(path, body, extraHeaders) {
380
380
  }
381
381
  return res.json();
382
382
  }
383
+ export function publicPost(path, body, extraHeaders) {
384
+ return publicRequest('POST', path, body, extraHeaders);
385
+ }
386
+ /** Mint the short-lived public app token the browser SDK uses, so a CLI call can
387
+ * exercise the exact path a signed-out visitor hits. Best-effort: fully public
388
+ * surfaces work with no token at all, and an auth-gated one should 401 with the
389
+ * server's real reason rather than a CLI-invented one. */
390
+ export async function mintAppToken(projectGuid) {
391
+ try {
392
+ const minted = await publicPost('/api/token', { app: projectGuid });
393
+ return { 'X-App-Token': minted.data.token };
394
+ }
395
+ catch {
396
+ return undefined;
397
+ }
398
+ }
383
399
  //# sourceMappingURL=api.js.map
package/dist/banner.js CHANGED
@@ -115,15 +115,13 @@ function padR(s, width) {
115
115
  const gap = width - visLen(s);
116
116
  return gap > 0 ? s + ' '.repeat(gap) : s;
117
117
  }
118
- /** Truncate from the left with a leading ellipsis if longer than maxW (plain text). */
118
+ /** Truncate from the left with a leading "..." if longer than maxW (plain text). */
119
119
  function leadingEllipsify(s, maxW) {
120
120
  if (s.length <= maxW)
121
121
  return s;
122
- if (maxW <= 1)
122
+ if (maxW <= 4)
123
123
  return s.slice(-maxW);
124
- if (maxW <= 3)
125
- return '…'.padStart(maxW);
126
- return '…' + s.slice(-(maxW - 1));
124
+ return '...' + s.slice(-(maxW - 3));
127
125
  }
128
126
  function center(s, width) {
129
127
  const gap = width - visLen(s);
@@ -0,0 +1,88 @@
1
+ const USER_REQUEST_RE = /<USER_REQUEST>\s*([\s\S]*?)\s*<\/USER_REQUEST>/;
2
+ function extractUserRequest(content) {
3
+ const m = USER_REQUEST_RE.exec(content);
4
+ return m ? m[1].trim() : content.trim();
5
+ }
6
+ function positional(conversationId, idx) {
7
+ return `${conversationId}#${idx}`;
8
+ }
9
+ function watermarkIndex(afterUuid, conversationId) {
10
+ if (!afterUuid || !conversationId)
11
+ return null;
12
+ const prefix = `${conversationId}#`;
13
+ if (!afterUuid.startsWith(prefix))
14
+ return null;
15
+ const idx = parseInt(afterUuid.slice(prefix.length), 10);
16
+ return Number.isInteger(idx) && idx >= 0 ? idx : null;
17
+ }
18
+ function lineToEntries(parsed, conversationId, idx) {
19
+ const ts = typeof parsed?.created_at === 'string' ? parsed.created_at : undefined;
20
+ const type = parsed?.type;
21
+ const content = parsed?.content;
22
+ const uuid = positional(conversationId, idx);
23
+ if (type === 'USER_INPUT' && typeof content === 'string' && content) {
24
+ const prompt = extractUserRequest(content);
25
+ if (!prompt)
26
+ return [];
27
+ return [{ kind: 'prompt', prompt, source_uuid: uuid, ...(ts ? { ts } : {}) }];
28
+ }
29
+ if (type === 'PLANNER_RESPONSE') {
30
+ if (typeof content !== 'string' || !content)
31
+ return []; // thinking-only turn, nothing user-visible
32
+ return [{
33
+ kind: 'assistant',
34
+ text: content,
35
+ blocks: [{ type: 'text', text: content }],
36
+ source_uuid: uuid,
37
+ ...(ts ? { ts } : {}),
38
+ }];
39
+ }
40
+ if (type === 'CODE_ACTION') {
41
+ if (typeof content !== 'string' || !content)
42
+ return [];
43
+ return [{ kind: 'system', content, source_uuid: uuid, ...(ts ? { ts } : {}) }];
44
+ }
45
+ return []; // CONVERSATION_HISTORY, EPHEMERAL_MESSAGE, CHECKPOINT, SYSTEM_MESSAGE, unknown
46
+ }
47
+ /** Same contract as the other source parsers: entries after `afterUuid`, the
48
+ * new watermark, and whether the old watermark was still valid for this
49
+ * file. `conversationId` comes from the hook payload (capture-runner passes
50
+ * it as `hook.session_id`), not from the file content - agy's transcript
51
+ * lines carry no conversation/session id field of their own. */
52
+ export function parseTranscript(content, afterUuid, opts = {}) {
53
+ const conversationId = opts.conversationId ?? '';
54
+ const lines = content.split('\n');
55
+ // A watermark past EOF means the file was replaced by a shorter one under
56
+ // the same conversation id (mirrors the Codex parser's resume-rotation
57
+ // guard) - treat as not-found so the caller replays from the top instead of
58
+ // wedging forever.
59
+ let startAfter = watermarkIndex(afterUuid, conversationId);
60
+ if (startAfter !== null && startAfter >= lines.length)
61
+ startAfter = null;
62
+ const foundWatermark = afterUuid === null || startAfter !== null;
63
+ const out = [];
64
+ let lastIdx = startAfter;
65
+ for (let i = 0; i < lines.length; i++) {
66
+ const line = lines[i].trim();
67
+ if (!line)
68
+ continue;
69
+ if (startAfter !== null && i <= startAfter)
70
+ continue;
71
+ let parsed;
72
+ try {
73
+ parsed = JSON.parse(line);
74
+ }
75
+ catch {
76
+ continue;
77
+ }
78
+ for (const e of lineToEntries(parsed, conversationId, i))
79
+ out.push(e);
80
+ lastIdx = i;
81
+ }
82
+ return {
83
+ entries: out,
84
+ lastUuid: lastIdx === null ? afterUuid : positional(conversationId, lastIdx),
85
+ foundWatermark,
86
+ };
87
+ }
88
+ //# sourceMappingURL=agy.js.map
@@ -29,6 +29,15 @@ export function detectHarness() {
29
29
  return { harness: 'codex' };
30
30
  if (hasEnvPrefix('GROK_'))
31
31
  return { harness: 'grok', harnessSession: process.env.GROK_SESSION_ID };
32
+ // Antigravity (agy) injects ANTIGRAVITY_CONVERSATION_ID into every hook/tool
33
+ // subprocess it spawns (confirmed live). Check this exact var, not an
34
+ // 'ANTIGRAVITY_' prefix scan - the Antigravity IDE's own integrated terminal
35
+ // ambiently sets other ANTIGRAVITY_*-prefixed vars (e.g. ANTIGRAVITY_CLI_ALIAS)
36
+ // in every shell it spawns, which would false-positive for any user with the
37
+ // IDE open, even outside a real agy session/hook.
38
+ if (process.env.ANTIGRAVITY_CONVERSATION_ID) {
39
+ return { harness: 'agy', harnessSession: process.env.ANTIGRAVITY_CONVERSATION_ID };
40
+ }
32
41
  if (process.env.CURSOR_TRACE_ID || hasEnvPrefix('CURSOR_') || (process.env.TERM_PROGRAM ?? '').toLowerCase().includes('cursor')) {
33
42
  return { harness: 'cursor' };
34
43
  }
@@ -207,7 +207,7 @@ export const addCommand = new Command('add')
207
207
  const doAdd = () => post(`/projects/${config.projectGuid}/add`, body);
208
208
  const res = opts.json
209
209
  ? await doAdd()
210
- : await withSpinner('Installing', doAdd, { done: null });
210
+ : await withSpinner('Installing...', doAdd, { done: null });
211
211
  // Pull the created/installed files down to local.
212
212
  const syncResult = await sync({ interactive: false, progress: opts.json ? undefined : createProgressReporter() });
213
213
  const data = res.data;
@@ -0,0 +1,125 @@
1
+ /**
2
+ * `gipity brand` - view or change the app's generated visual identity:
3
+ * favicons, the iOS Home Screen icon, the PWA manifest, and the 1200x630
4
+ * social share card (og-image) that X/iMessage/Slack show when the app's
5
+ * URL is shared.
6
+ *
7
+ * The default identity is deterministic (accent color from the project guid,
8
+ * glyph from the title's first character, small Gipity egg badge). `brand set`
9
+ * overrides pieces of it - most usefully the glyph (an emoji reads far more
10
+ * "this is my app" than a letter) and the accent color - then regenerates
11
+ * every asset server-side in one shot. Deploy afterwards to publish.
12
+ */
13
+ import { Command } from 'commander';
14
+ import { get, post } from '../api.js';
15
+ import { resolveProjectContext } from '../config.js';
16
+ import { bold, muted, success } from '../colors.js';
17
+ import { run } from '../helpers/index.js';
18
+ function printBrand(d) {
19
+ console.log(`${bold('Glyph:')} ${d.resolved.glyph}${d.resolved.isEmoji ? muted(' (emoji)') : ''}`);
20
+ console.log(`${bold('Accent:')} ${d.resolved.accent}${d.branding.primary_color ? '' : muted(' (derived from project)')}`);
21
+ if (d.branding.app_name)
22
+ console.log(`${bold('Name:')} ${d.branding.app_name}`);
23
+ if (d.branding.tagline)
24
+ console.log(`${bold('Tagline:')} ${d.branding.tagline}`);
25
+ }
26
+ const DEPLOY_HINT = 'Deploy to publish the new assets: gipity deploy dev';
27
+ export const brandCommand = new Command('brand')
28
+ .description("The app's generated icons + share card (favicons, iOS icon, og-image)")
29
+ .addHelpText('after', `
30
+ Examples:
31
+ gipity brand Show the current brand
32
+ gipity brand set --emoji 🦍 Emoji icon + share card, regenerate all assets
33
+ gipity brand set --glyph R --color "#3b82f6" Letter glyph on a blue accent
34
+ gipity brand set --tagline "Bananas at 60fps" Subtitle on the share card
35
+ gipity brand apply Re-render assets from the stored brand
36
+ gipity brand apply --fix-head Migrate an older app: assets + splice the
37
+ share/SEO tags into src/index.html
38
+
39
+ Assets are generated server-side into src/images/ (+ src/manifest.webmanifest)
40
+ and are versioned like any other file - rollback restores previous ones.`);
41
+ brandCommand
42
+ .command('show', { isDefault: true })
43
+ .description('Show the stored brand and the resolved render spec')
44
+ .option('--project <guid-or-slug>', 'Target a specific project instead of cwd / Home')
45
+ .option('--json', 'Output raw JSON')
46
+ .action((opts) => run('Brand', async () => {
47
+ const { config } = await resolveProjectContext({ projectOverride: opts.project });
48
+ const res = await get(`/projects/${config.projectGuid}/brand`);
49
+ if (opts.json) {
50
+ console.log(JSON.stringify(res.data));
51
+ return;
52
+ }
53
+ printBrand(res.data);
54
+ console.log(muted(`\nAssets: ${(res.data.assets ?? []).join(', ')}`));
55
+ console.log(muted(`Change it: gipity brand set --emoji 🎨 | --glyph A | --color "#RRGGBB" | --tagline "..."`));
56
+ }));
57
+ brandCommand
58
+ .command('set')
59
+ .description('Change brand fields and regenerate all assets')
60
+ .option('--emoji <emoji>', 'Emoji glyph for the icon + share card (e.g. 🦍)')
61
+ .option('--glyph <char>', 'Letter/digit glyph (alias of --emoji, any single character)')
62
+ .option('--color <hex>', 'Accent color, #RRGGBB')
63
+ .option('--name <name>', 'Display name override (og/share title stays the page title)')
64
+ .option('--tagline <text>', 'One-line subtitle on the share card (max 120 chars)')
65
+ .option('--fix-head', 'Also splice the share/SEO head block into src/index.html (for apps installed before templates shipped it)')
66
+ .option('--no-regenerate', 'Store the fields without re-rendering assets')
67
+ .option('--project <guid-or-slug>', 'Target a specific project instead of cwd / Home')
68
+ .option('--json', 'Output raw JSON')
69
+ .action((opts) => run('Brand', async () => {
70
+ const { config } = await resolveProjectContext({ projectOverride: opts.project });
71
+ const body = {};
72
+ const glyph = opts.emoji ?? opts.glyph;
73
+ if (glyph)
74
+ body.icon_glyph = glyph;
75
+ if (opts.color)
76
+ body.primary_color = opts.color;
77
+ if (opts.name)
78
+ body.app_name = opts.name;
79
+ if (opts.tagline)
80
+ body.tagline = opts.tagline;
81
+ if (opts.regenerate === false)
82
+ body.regenerate = false;
83
+ if (opts.fixHead)
84
+ body.fix_head = true;
85
+ if (!glyph && !opts.color && !opts.name && !opts.tagline) {
86
+ console.error('Nothing to set - pass --emoji, --glyph, --color, --name, or --tagline. (gipity brand set --help; use gipity brand apply to just re-render)');
87
+ process.exitCode = 1;
88
+ return;
89
+ }
90
+ const res = await post(`/projects/${config.projectGuid}/brand`, body);
91
+ if (opts.json) {
92
+ console.log(JSON.stringify(res.data));
93
+ return;
94
+ }
95
+ printBrand(res.data);
96
+ reportFiles(res.data.files ?? []);
97
+ }));
98
+ brandCommand
99
+ .command('apply')
100
+ .description('Re-render all brand assets from the stored brand (no field changes)')
101
+ .option('--fix-head', 'Also splice the share/SEO head block into src/index.html (for apps installed before templates shipped it)')
102
+ .option('--project <guid-or-slug>', 'Target a specific project instead of cwd / Home')
103
+ .option('--json', 'Output raw JSON')
104
+ .action((opts) => run('Brand', async () => {
105
+ const { config } = await resolveProjectContext({ projectOverride: opts.project });
106
+ const body = opts.fixHead ? { fix_head: true } : {};
107
+ const res = await post(`/projects/${config.projectGuid}/brand`, body);
108
+ if (opts.json) {
109
+ console.log(JSON.stringify(res.data));
110
+ return;
111
+ }
112
+ reportFiles(res.data.files ?? []);
113
+ }));
114
+ function reportFiles(files) {
115
+ if (!files.length) {
116
+ console.log(muted('\nStored (assets not regenerated - run gipity brand apply).'));
117
+ return;
118
+ }
119
+ console.log(success(`\n✓ Regenerated ${files.length} asset${files.length === 1 ? '' : 's'}.`));
120
+ if (files.includes('src/index.html')) {
121
+ console.log(success('✓ src/index.html head updated (page title/description preserved).'));
122
+ }
123
+ console.log(muted(` ${DEPLOY_HINT}`));
124
+ }
125
+ //# sourceMappingURL=brand.js.map
@@ -50,7 +50,7 @@ const __clPkg = readOwnPkg(__clDir);
50
50
  * incomplete pull, so we can tell the user plainly that nothing was deleted. */
51
51
  function reportSyncResult(result) {
52
52
  if (result.aborted) {
53
- console.log(` ${warning('Merge cancelled this folder was NOT synced with the project.')}`);
53
+ console.log(` ${warning('Merge cancelled - this folder was NOT synced with the project.')}`);
54
54
  console.log(` ${muted('For a clean copy, quit and open the project in an empty folder. To merge anyway, run `gipity sync`.')}`);
55
55
  return;
56
56
  }
@@ -58,11 +58,11 @@ function reportSyncResult(result) {
58
58
  console.log(` Synced ${result.applied} change${result.applied > 1 ? 's' : ''} with Gipity.`);
59
59
  }
60
60
  if (result.errors.length) {
61
- console.log(` ${warning(`Sync finished with ${result.errors.length} problem${result.errors.length === 1 ? '' : 's'} your local copy may be incomplete:`)}`);
61
+ console.log(` ${warning(`Sync finished with ${result.errors.length} problem${result.errors.length === 1 ? '' : 's'} - your local copy may be incomplete:`)}`);
62
62
  for (const e of result.errors.slice(0, 8))
63
63
  console.log(` - ${e}`);
64
64
  if (result.errors.length > 8)
65
- console.log(` and ${result.errors.length - 8} more.`);
65
+ console.log(` ...and ${result.errors.length - 8} more.`);
66
66
  console.log(` ${muted('Nothing was deleted. Re-run `gipity sync` to finish the pull.')}`);
67
67
  }
68
68
  }
@@ -133,7 +133,7 @@ function localFsFallback(dir) {
133
133
  };
134
134
  walk(dir, 0);
135
135
  const topLevel = topLevelEntries.length
136
- ? topLevelEntries.slice(0, 20).join(', ') + (topLevelEntries.length > 20 ? ', ' : '')
136
+ ? topLevelEntries.slice(0, 20).join(', ') + (topLevelEntries.length > 20 ? ', ...' : '')
137
137
  : '(empty directory)';
138
138
  return { fileCount, folderCount, totalBytes, topLevel };
139
139
  }
@@ -502,6 +502,8 @@ async function runLaunch(cmdName, cmdCfg, opts) {
502
502
  console.log(` ${warning('Could not sync files (will retry on next prompt):')} ${err.message}`);
503
503
  }
504
504
  setupProjectTools();
505
+ if (!nonInteractive)
506
+ console.log(` ${muted('Installed Gipity plugin, skills, and hooks.')}`);
505
507
  if (nonInteractive) {
506
508
  // Headless: the -p message is wrapped later (Step 3). Just record
507
509
  // whether to use the new-project framing for that wrap.
@@ -673,6 +675,8 @@ async function runLaunch(cmdName, cmdCfg, opts) {
673
675
  // Claude directly what they want to build or do.
674
676
  initialPrompt = '';
675
677
  setupProjectTools();
678
+ if (!nonInteractive)
679
+ console.log(` ${muted('Installed Gipity plugin, skills, and hooks.')}`);
676
680
  console.log(` ${success(`Project "${project.name}" ready.`)}\n`);
677
681
  }
678
682
  // ── Step 3: Pick the coding agent + model ─────────────────────────
@@ -812,9 +816,8 @@ async function runLaunch(cmdName, cmdCfg, opts) {
812
816
  }
813
817
  if (!nonInteractive) {
814
818
  console.log(` ${bold('Launching Claude Code, powered by Gipity.')}`);
815
- console.log(` ${muted("Just tell Claude what you'd like to build or do - everything Claude can do,")}`);
816
- console.log(` ${muted('plus hosting, databases, and live deploys on Gipity.')}`);
817
819
  if (convGuidForHooks) {
820
+ console.log('');
818
821
  console.log(` ${muted(`This session is saved to Gipity - watch it live at ${brand('prompt.gipity.ai')}. (--no-capture on init to disable.)`)}`);
819
822
  }
820
823
  console.log('');
@@ -960,13 +963,12 @@ async function pickAgent(lastUsed) {
960
963
  const defaultIdx = AGENT_ADAPTERS.findIndex(a => a.key === defaultKey) + 1;
961
964
  console.log(` ${bold('Which coding agent?')}\n`);
962
965
  AGENT_ADAPTERS.forEach((a, i) => {
963
- const installed = binaryOnPath(a.binary) || (a.key === 'claude' && isClaudeInstalled());
966
+ // Keep the list clean: no install-state notes (picking an uninstalled
967
+ // agent installs it, or fails with the install hint at that point).
964
968
  const notes = [];
965
969
  if (a.key === lastUsed)
966
970
  notes.push('last used');
967
- if (!installed)
968
- notes.push(a.ensureInstalled ? "not installed - we'll install it" : 'not installed');
969
- if (a.key === 'codex' && !a.hooksSupportedOnPlatform(process.platform)) {
971
+ if ((a.key === 'codex' || a.key === 'agy') && !a.hooksSupportedOnPlatform(process.platform)) {
970
972
  notes.push('session recording unavailable on Windows');
971
973
  }
972
974
  const note = notes.length ? ` ${muted(`(${notes.join(', ')})`)}` : '';
@@ -1101,9 +1103,8 @@ async function launchNonClaudeAgent(adapter, ctx) {
1101
1103
  else {
1102
1104
  agentArgs = [...adapter.buildInteractiveArgs({ resume, model }), ...extras];
1103
1105
  console.log(` ${bold(`Launching ${adapter.displayName}, powered by Gipity.`)}`);
1104
- console.log(` ${muted(`Just tell ${adapter.displayName} what you'd like to build or do - plus hosting,`)}`);
1105
- console.log(` ${muted('databases, and live deploys on Gipity.')}`);
1106
1106
  if (convGuid) {
1107
+ console.log('');
1107
1108
  console.log(` ${muted(`This session is saved to Gipity - watch it live at ${brand('prompt.gipity.ai')}. (--no-capture on init to disable.)`)}`);
1108
1109
  }
1109
1110
  if (adapter.oneTimeSetupNote) {
@@ -25,7 +25,7 @@ export const chatCommand = new Command('chat')
25
25
  const doChat = () => post(endpoint, body);
26
26
  const res = opts.json
27
27
  ? await doChat()
28
- : await withSpinner('Thinking', doChat, { done: null });
28
+ : await withSpinner('Thinking...', doChat, { done: null });
29
29
  // Save conversation guid for continuity. Skipped in one-off mode: the
30
30
  // config was resolved from the server's Home project and there is no
31
31
  // local `.gipity.json` to update - persisting here would create one in
@@ -57,7 +57,7 @@ export const deployCommand = new Command('deploy')
57
57
  });
58
58
  const res = opts.json
59
59
  ? await doDeploy()
60
- : await withSpinner(`Deploying to ${target}…`, doDeploy, { done: null });
60
+ : await withSpinner(`Deploying to ${target}...`, doDeploy, { done: null });
61
61
  const d = res.data;
62
62
  // Deploy + verify in one command: after a successful deploy, run the
63
63
  // same inspect pipeline `page inspect` uses against the live URL. An
@@ -1,5 +1,5 @@
1
1
  import { Command } from 'commander';
2
- import { get, post, del, publicPost } from '../api.js';
2
+ import { get, post, del, publicPost, mintAppToken } from '../api.js';
3
3
  import { getAuth } from '../auth.js';
4
4
  import { requireConfig, resolveApiBase } from '../config.js';
5
5
  import { error as clrError, bold, muted, success, warning } from '../colors.js';
@@ -62,13 +62,7 @@ fnCommand
62
62
  * auth-gated function — the server's message is surfaced as-is, with no
63
63
  * "run: gipity login" misdirection. */
64
64
  async function callAnon(projectGuid, name, body) {
65
- let appToken;
66
- try {
67
- const minted = await publicPost('/api/token', { app: projectGuid });
68
- appToken = minted.data.token;
69
- }
70
- catch { /* public functions work without a token; auth-gated ones will 401 with the real reason */ }
71
- return publicPost(`/api/${projectGuid}/fn/${encodeURIComponent(name)}`, body, appToken ? { 'X-App-Token': appToken } : undefined);
65
+ return publicPost(`/api/${projectGuid}/fn/${encodeURIComponent(name)}`, body, await mintAppToken(projectGuid));
72
66
  }
73
67
  fnCommand
74
68
  .command('call <name> [body]')
@@ -219,7 +219,7 @@ Examples:
219
219
  seed: Number.isFinite(opts.seed) ? opts.seed : undefined,
220
220
  input_images: inputImages,
221
221
  });
222
- const verb = inputImages ? 'Editing image' : 'Generating image';
222
+ const verb = inputImages ? 'Editing image...' : 'Generating image...';
223
223
  const result = opts.json
224
224
  ? await doGenerate()
225
225
  : await withSpinner(verb, doGenerate, { done: null });
@@ -285,7 +285,7 @@ Examples:
285
285
  // Veo runs 30-120s; the bouncing bar + timer keeps the wait honest.
286
286
  const result = opts.json
287
287
  ? await doGenerate()
288
- : await withSpinner('Generating video', doGenerate, { done: null });
288
+ : await withSpinner('Generating video...', doGenerate, { done: null });
289
289
  const filename = opts.output || 'generated.mp4';
290
290
  const savedPath = await downloadFile(result.url, filename, !!opts.output);
291
291
  if (opts.json) {
@@ -349,7 +349,7 @@ Examples:
349
349
  });
350
350
  const result = opts.json
351
351
  ? await doGenerate()
352
- : await withSpinner('Generating speech', doGenerate, { done: null });
352
+ : await withSpinner('Generating speech...', doGenerate, { done: null });
353
353
  const filename = opts.output || 'speech.mp3';
354
354
  const savedPath = await downloadFile(result.url, filename, !!opts.output);
355
355
  if (opts.json) {
@@ -402,7 +402,7 @@ Examples:
402
402
  });
403
403
  const result = opts.json
404
404
  ? await doGenerate()
405
- : await withSpinner('Generating music', doGenerate, { done: null });
405
+ : await withSpinner('Generating music...', doGenerate, { done: null });
406
406
  const filename = opts.output || 'music.mp3';
407
407
  const savedPath = await downloadFile(result.url, filename, !!opts.output);
408
408
  if (opts.json) {
@@ -453,7 +453,7 @@ Examples:
453
453
  });
454
454
  const result = opts.json
455
455
  ? await doGenerate()
456
- : await withSpinner('Generating sound effect', doGenerate, { done: null });
456
+ : await withSpinner('Generating sound effect...', doGenerate, { done: null });
457
457
  const filename = opts.output || 'sound.mp3';
458
458
  const savedPath = await downloadFile(result.url, filename, !!opts.output);
459
459
  if (opts.json) {
@@ -61,7 +61,7 @@ gmailCommand
61
61
  .option('--cc <email>', 'Cc recipient (repeatable)', collect, [])
62
62
  .option('--bcc <email>', 'Bcc recipient (repeatable)', collect, [])
63
63
  .option('--reply-to <email>', 'Reply-To header address')
64
- .requiredOption('--subject <s>', 'Subject (usually "Re: " of the original)')
64
+ .requiredOption('--subject <s>', 'Subject (usually "Re: ..." of the original)')
65
65
  .requiredOption('--body <text>', 'Reply body (you compose the quote header)')
66
66
  .option('--json', 'Output as JSON')
67
67
  .action((opts) => run('Reply', async () => {
@@ -23,7 +23,7 @@ function resolveTools(forFlag) {
23
23
  }
24
24
  export const initCommand = new Command('init')
25
25
  .description('Link this directory to a project')
26
- .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).')
26
+ .addHelpText('after', '\nWrites CLAUDE.md/AGENTS.md primer files so your AI coding tool understands Gipity, and installs the Gipity skills + file-sync hooks into the agent CLIs found on this machine (Claude Code, Codex, Grok, Antigravity).')
27
27
  .argument('[name]', 'Project name/slug (defaults to current directory name)')
28
28
  .option('--agent <guid>', 'Agent GUID to use')
29
29
  .option('--no-capture', 'Don\'t record Claude Code sessions in this directory to your Gipity project (sets captureHooks: false in .gipity.json)')
@@ -177,7 +177,7 @@ Working with an existing Gipity project:
177
177
  }
178
178
  console.log(success(`Wrote primer files: ${primerSummary}.`));
179
179
  if (wantsClaude) {
180
- console.log(success('Ready! Run your coding agent here (claude, codex, grok), or `gipity build` to launch one with a picker.'));
180
+ console.log(success('Ready! Run your coding agent here (claude, codex, grok, agy), or `gipity build` to launch one with a picker.'));
181
181
  // Recording happens by default (however Claude Code is launched), so
182
182
  // say so up front - consent should be explicit, not discovered later.
183
183
  if (opts.capture === false) {
@@ -63,8 +63,13 @@ jobCommand
63
63
  const r = res.data;
64
64
  const statusColor = r.status === 'success' ? success : r.status === 'failed' ? clrError : muted;
65
65
  console.log(`${statusColor(r.status)} ${muted(r.guid)}`);
66
- if (r.progress_pct != null)
67
- console.log(`progress: ${Math.round(r.progress_pct * 100)}%${r.progress_message ? ` (${r.progress_message})` : ''}`);
66
+ if (r.progress_pct != null) {
67
+ const prog = `${Math.round(r.progress_pct * 100)}%${r.progress_message ? ` (${r.progress_message})` : ''}`;
68
+ // A failed run can have reported 100% (done) right before dying; a bare
69
+ // "progress: 100% (done)" next to status=failed reads as success. Label it.
70
+ const failedish = r.status === 'failed' || r.status === 'cancelled' || r.status === 'canceled' || r.status === 'error';
71
+ console.log(failedish ? `last progress before ${r.status}: ${prog}` : `progress: ${prog}`);
72
+ }
68
73
  if (r.duration_ms != null)
69
74
  console.log(`duration: ${r.duration_ms}ms`);
70
75
  if (r.error)
@@ -98,7 +103,7 @@ jobCommand
98
103
  ? `${Math.round(r.progress_pct * 100)}%${r.progress_message ? ` (${r.progress_message})` : ''}`
99
104
  : r.status;
100
105
  if (prog !== lastProgress) {
101
- console.error(muted(`… ${prog}`));
106
+ console.error(muted(`... ${prog}`));
102
107
  lastProgress = prog;
103
108
  }
104
109
  }
@@ -198,7 +203,7 @@ jobCommand
198
203
  }
199
204
  catch (e) {
200
205
  if (peekedOut) {
201
- console.error(muted(`… still streaming after ${peekSeconds}s. Run \`gipity job logs ${runGuid} --timeout ${Math.round(peekSeconds)}\` again to keep watching, or \`gipity job wait ${runGuid}\` to block until it finishes.`));
206
+ console.error(muted(`... still streaming after ${peekSeconds}s. Run \`gipity job logs ${runGuid} --timeout ${Math.round(peekSeconds)}\` again to keep watching, or \`gipity job wait ${runGuid}\` to block until it finishes.`));
202
207
  return;
203
208
  }
204
209
  throw e;
@@ -221,7 +226,7 @@ jobCommand
221
226
  catch (e) {
222
227
  // Peek window elapsed mid-stream: report cleanly and exit 0 (not an error).
223
228
  if (peekedOut) {
224
- console.error(muted(`… still streaming after ${peekSeconds}s. Run \`gipity job logs ${runGuid} --timeout ${Math.round(peekSeconds)}\` again to keep watching, or \`gipity job wait ${runGuid}\` to block until it finishes.`));
229
+ console.error(muted(`... still streaming after ${peekSeconds}s. Run \`gipity job logs ${runGuid} --timeout ${Math.round(peekSeconds)}\` again to keep watching, or \`gipity job wait ${runGuid}\` to block until it finishes.`));
225
230
  return;
226
231
  }
227
232
  throw e;