gipity 1.1.5 → 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.
- package/dist/agents/agy.js +52 -0
- package/dist/agents/index.js +2 -0
- package/dist/api.js +19 -3
- package/dist/capture/sources/agy.js +88 -0
- package/dist/client-context.js +9 -0
- package/dist/commands/brand.js +125 -0
- package/dist/commands/build.js +1 -1
- package/dist/commands/fn.js +2 -8
- package/dist/commands/init.js +2 -2
- package/dist/commands/key.js +91 -0
- package/dist/commands/page-eval.js +42 -3
- package/dist/commands/records.js +57 -19
- package/dist/commands/token.js +6 -1
- package/dist/commands/uninstall.js +14 -1
- package/dist/hooks/capture-runner.js +20 -8
- package/dist/index.js +553 -105
- package/dist/knowledge.js +1 -1
- package/dist/project-setup.js +2 -0
- package/dist/relay/diagnostics.js +4 -2
- package/dist/setup.js +198 -16
- package/dist/template-vars.js +74 -0
- package/package.json +2 -2
|
@@ -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
|
package/dist/agents/index.js
CHANGED
|
@@ -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
|
|
369
|
+
export async function publicRequest(method, path, body, extraHeaders) {
|
|
370
370
|
const url = `${baseUrl()}${path}`;
|
|
371
371
|
const res = await fetch(url, {
|
|
372
|
-
method
|
|
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
|
|
@@ -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
|
package/dist/client-context.js
CHANGED
|
@@ -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
|
}
|
|
@@ -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
|
package/dist/commands/build.js
CHANGED
|
@@ -968,7 +968,7 @@ async function pickAgent(lastUsed) {
|
|
|
968
968
|
const notes = [];
|
|
969
969
|
if (a.key === lastUsed)
|
|
970
970
|
notes.push('last used');
|
|
971
|
-
if (a.key === 'codex' && !a.hooksSupportedOnPlatform(process.platform)) {
|
|
971
|
+
if ((a.key === 'codex' || a.key === 'agy') && !a.hooksSupportedOnPlatform(process.platform)) {
|
|
972
972
|
notes.push('session recording unavailable on Windows');
|
|
973
973
|
}
|
|
974
974
|
const note = notes.length ? ` ${muted(`(${notes.join(', ')})`)}` : '';
|
package/dist/commands/fn.js
CHANGED
|
@@ -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
|
-
|
|
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]')
|
package/dist/commands/init.js
CHANGED
|
@@ -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) {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { get, post, del } from '../api.js';
|
|
3
|
+
import { resolveProjectContext } from '../config.js';
|
|
4
|
+
import { bold, muted, success, warning } from '../colors.js';
|
|
5
|
+
import { run, printList } from '../helpers/index.js';
|
|
6
|
+
/**
|
|
7
|
+
* Project API keys - the revocable secret a script, cron, or agent sends as
|
|
8
|
+
* `X-Api-Key` to write to YOUR app without a browser login.
|
|
9
|
+
*
|
|
10
|
+
* These are per-project and app-facing; `gipity token` mints account-level
|
|
11
|
+
* agent tokens (gip_at_*) that drive the CLI itself. Without this command the
|
|
12
|
+
* only mint path was raw curl, so agents asked for "a key I can generate and
|
|
13
|
+
* revoke" built their own key table instead of using the platform's.
|
|
14
|
+
*/
|
|
15
|
+
export const keyCommand = new Command('key')
|
|
16
|
+
.description('Manage project API keys for scripts and agents (X-Api-Key)')
|
|
17
|
+
.addHelpText('after', `
|
|
18
|
+
Give a script, cron, or agent write access to your app without a login:
|
|
19
|
+
|
|
20
|
+
gipity key create "laptop importer" --role editor
|
|
21
|
+
# the script sends: X-Api-Key: <the key printed once above>
|
|
22
|
+
|
|
23
|
+
Inside a function the caller shows up as ctx.auth.via === 'api_key' with
|
|
24
|
+
ctx.auth.apiKeyName set to the key's name - stamp that on rows to tell
|
|
25
|
+
script-written entries from hand-entered ones. Never build your own key table.
|
|
26
|
+
|
|
27
|
+
Account-level tokens that run the CLI headlessly are a different thing: see
|
|
28
|
+
gipity token --help.`);
|
|
29
|
+
const fmtDate = (d) => (d ? new Date(d).toLocaleDateString() : 'never');
|
|
30
|
+
const projectOpt = ['--project <guid-or-slug>', 'Target a specific project instead of cwd / Home'];
|
|
31
|
+
async function projectGuid(opts) {
|
|
32
|
+
const { config } = await resolveProjectContext({ projectOverride: opts.project });
|
|
33
|
+
return config.projectGuid;
|
|
34
|
+
}
|
|
35
|
+
keyCommand
|
|
36
|
+
.command('create <name>')
|
|
37
|
+
.description('Mint a project API key (shown once). Give it a name you will recognize later')
|
|
38
|
+
.option('--role <role>', 'viewer | editor | owner (default: viewer)', 'viewer')
|
|
39
|
+
.option('--expires-days <n>', 'Days until the key expires (default: never)')
|
|
40
|
+
.option(...projectOpt)
|
|
41
|
+
.option('--json', 'Output as JSON')
|
|
42
|
+
.action((name, opts) => run('Create', async () => {
|
|
43
|
+
const body = { name, role: opts.role };
|
|
44
|
+
if (opts.expiresDays !== undefined) {
|
|
45
|
+
const days = parseInt(opts.expiresDays, 10);
|
|
46
|
+
if (!Number.isFinite(days) || days <= 0)
|
|
47
|
+
throw new Error('--expires-days must be a positive number of days');
|
|
48
|
+
body.expires_in_days = days;
|
|
49
|
+
}
|
|
50
|
+
const res = await post(`/projects/${await projectGuid(opts)}/api-keys`, body);
|
|
51
|
+
const k = res.data;
|
|
52
|
+
if (opts.json) {
|
|
53
|
+
console.log(JSON.stringify(k));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const expNote = k.expires_at ? ` (expires ${fmtDate(k.expires_at)})` : ' (never expires)';
|
|
57
|
+
console.log(success(`Created API key ${bold(k.short_guid)} "${k.name}" as ${k.role}${muted(expNote)}.`));
|
|
58
|
+
console.log('');
|
|
59
|
+
console.log(k.key);
|
|
60
|
+
console.log('');
|
|
61
|
+
console.log(muted('Send it from your script on every request:'));
|
|
62
|
+
console.log(muted(` curl -H "X-Api-Key: ${k.key}" ...`));
|
|
63
|
+
console.log(muted(`Revoke it any time: gipity key revoke ${k.short_guid}`));
|
|
64
|
+
console.log('');
|
|
65
|
+
console.log(warning('Copy it now - it will not be shown again.'));
|
|
66
|
+
}));
|
|
67
|
+
keyCommand
|
|
68
|
+
.command('list')
|
|
69
|
+
.alias('ls')
|
|
70
|
+
.description('List this project\'s API keys (values are never shown again)')
|
|
71
|
+
.option(...projectOpt)
|
|
72
|
+
.option('--json', 'Output as JSON')
|
|
73
|
+
.action((opts) => run('List', async () => {
|
|
74
|
+
const res = await get(`/projects/${await projectGuid(opts)}/api-keys`);
|
|
75
|
+
printList(res.data, opts, 'No API keys. Mint one with: gipity key create "my script" --role editor', (k) => `${bold(k.short_guid)} ${k.name} ${muted(`${k.role} ${k.prefix}… last used ${fmtDate(k.last_used_at)} expires ${fmtDate(k.expires_at)}`)}`);
|
|
76
|
+
}));
|
|
77
|
+
keyCommand
|
|
78
|
+
.command('revoke <short_guid>')
|
|
79
|
+
.alias('rm')
|
|
80
|
+
.description('Revoke a project API key (instant, irreversible)')
|
|
81
|
+
.option(...projectOpt)
|
|
82
|
+
.option('--json', 'Output as JSON')
|
|
83
|
+
.action((shortGuid, opts) => run('Revoke', async () => {
|
|
84
|
+
await del(`/projects/${await projectGuid(opts)}/api-keys/${encodeURIComponent(shortGuid)}`);
|
|
85
|
+
if (opts.json) {
|
|
86
|
+
console.log(JSON.stringify({ short_guid: shortGuid, revoked: true }));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
console.log(success(`Revoked API key ${bold(shortGuid)}.`));
|
|
90
|
+
}));
|
|
91
|
+
//# sourceMappingURL=key.js.map
|
|
@@ -257,6 +257,31 @@ export function budgetOverrunHint(reason, usedBudgetMs) {
|
|
|
257
257
|
`(up to ${EVAL_SCRIPT_BUDGET_MAX_MS}), e.g. --timeout ${EVAL_SCRIPT_BUDGET_MAX_MS} — ` +
|
|
258
258
|
`or gate on a ready signal instead: --wait-for '<selector>' --wait-timeout ${MAX_WAIT_MS}.`;
|
|
259
259
|
}
|
|
260
|
+
/** The server aborts a script whose page navigated out from under it, and tells
|
|
261
|
+
* the caller to "split the check into two evals". For the case that actually
|
|
262
|
+
* produces this (seed state, reload, assert the state came back), that advice
|
|
263
|
+
* sends the caller down the wrong road: two evals are two independent page
|
|
264
|
+
* sessions, so the second one has to re-seed before it can read anything, and
|
|
265
|
+
* the thing being verified (a real reload restoring real persisted state) is
|
|
266
|
+
* never exercised. `--reload` is the primitive for it: one page load, reloaded
|
|
267
|
+
* IN PLACE with storage preserved, second expression against the post-reload
|
|
268
|
+
* DOM. Name it here, at the exact moment the caller hand-rolled it. */
|
|
269
|
+
export function navigationAbortHint(reason) {
|
|
270
|
+
if (!/navigated or reloaded/i.test(reason))
|
|
271
|
+
return null;
|
|
272
|
+
return `If you were verifying that state SURVIVES a reload, that is what --reload is for (one command, one page): ` +
|
|
273
|
+
`gipity page eval "<url>" '<seed/assert expr>' --reload '<assert-restored expr>'. ` +
|
|
274
|
+
`It reloads the page in place (localStorage/sessionStorage/cookies preserved) and reports the second result ` +
|
|
275
|
+
`separately. Splitting into two 'page eval' calls does NOT do this: each call is its own browser session, so ` +
|
|
276
|
+
`the second one starts from empty storage and never exercises the restore path.`;
|
|
277
|
+
}
|
|
278
|
+
/** True when the submitted script already drives the app's clock itself
|
|
279
|
+
* (`core.advance(seconds)` on the 3D templates, the imported `advance(seconds)`
|
|
280
|
+
* on the 2D one). Such a script does not care what the renderer paints at, so
|
|
281
|
+
* the slow-render warning below has nothing to tell it. Exported for tests. */
|
|
282
|
+
export function stepsDeterministically(script) {
|
|
283
|
+
return /\badvance\s*\(/.test(script);
|
|
284
|
+
}
|
|
260
285
|
/** The headless browser paints slowly, and what that MEANS depends on what the
|
|
261
286
|
* page is doing — so the one-size warning was actively misleading on a --camera
|
|
262
287
|
* run. A vision app has no loop to step: its pipeline is driven by camera
|
|
@@ -273,6 +298,8 @@ export function budgetOverrunHint(reason, usedBudgetMs) {
|
|
|
273
298
|
* deterministic way to wait (--wait-for) instead of inviting a wall-clock
|
|
274
299
|
* escalation that is guaranteed to be wasted. Exported for tests. */
|
|
275
300
|
export function slowRenderMessage(fps, o) {
|
|
301
|
+
if (!o.camera && stepsDeterministically(o.script ?? ''))
|
|
302
|
+
return null;
|
|
276
303
|
if (o.camera) {
|
|
277
304
|
const frames = Math.max(1, Math.round(fps * (o.waitMs / 1000)));
|
|
278
305
|
return `${warning('⚠ Slow render:')} page painted at ${fps} fps, so the app's vision pipeline ran on roughly `
|
|
@@ -382,7 +409,7 @@ export const pageEvalCommand = new Command('eval')
|
|
|
382
409
|
.option('--file <path>', `Read the script body from a file instead of the inline <expr> arg (mutually exclusive), or --file - to read it from stdin (pipe a heredoc: --file - <<'EOF' ... EOF) with no tmp file. Runs as an async function body, so top-level return/await work. Same post-load budget as <expr> (--timeout).`)
|
|
383
410
|
.option('--step <expr>', `Run another expression against the SAME loaded page, after <expr> (repeat, max ${MAX_EVAL_STEPS}). Whatever that page load paid for — a vision model coming up, a game booting, a socket connecting — stays up for every step, so an N-part check costs ONE page load instead of N. Each step gets its own in-page budget and its own reported result.`, (val, prev) => [...prev, val], [])
|
|
384
411
|
.option('--fixture <path>', 'Host a local file and expose it to the eval as `fixtureUrl` (and under `fixtures` by basename) to fetch in-page. For verifying a render/parse path against a real binary (an MP3, an image) - no size limit, auto-deleted after the run. The hosted URL has permissive CORS, so `new Image(); img.crossOrigin="anonymous"; img.src=fixtureUrl` decodes untainted for a canvas/pixel read - this is the UPLOAD analog of --camera: feed a known photo/video to a file-upload vision app (web-vision-detect) and run its detector, no need to deploy a throwaway test asset into the app. Repeat for several files (single-value so it never swallows the inline <expr>).', (val, prev) => [...prev, val], [])
|
|
385
|
-
.option('--reload <expr>', 'After the first eval, reload the page IN PLACE (localStorage/sessionStorage/cookies preserved) and evaluate this second expression against the post-reload DOM. One command verifies persisted state survives a reload: seed/assert state with <expr>, then assert the restored UI here.')
|
|
412
|
+
.option('--reload <expr>', 'After the first eval, reload the page IN PLACE (localStorage/sessionStorage/cookies preserved) and evaluate this second expression against the post-reload DOM. One command verifies persisted state survives a reload ("remember it when I come back"): seed/assert state with <expr>, then assert the restored UI here. This is the ONLY way to check that - reloading inside the body aborts the eval (the result is lost with the old page), and two separate `page eval` calls are two fresh browser profiles, so the second starts from empty storage.')
|
|
386
413
|
.option('--reload-file <path>', 'Read the post-reload expression from a file instead of inline --reload (mutually exclusive)')
|
|
387
414
|
.option('--camera <path>', `Play a local image or video (.png/.jpg/.webp/.mp4/.webm/.y4m/.mjpeg) as the browser's WEBCAM feed, so a camera app's real pipeline (getUserMedia → MediaPipe/YOLOX → your app logic) runs headlessly on a frame you choose. Implies --fake-media and waits ${CAMERA_DEFAULT_WAIT_MS / 1000}s for the vision model to load. No frame handy? gipity generate image "a hand making a closed fist, palm to camera".`)
|
|
388
415
|
.option('--fake-media', 'Grant a synthetic microphone + camera and auto-accept the getUserMedia prompt, so a voice/camera app runs headlessly instead of hitting its no-camera path. The feed is a built-in test pattern (and a tone) — nothing a vision model can recognize; to drive a vision app use --camera <path> instead.')
|
|
@@ -589,7 +616,10 @@ export const pageEvalCommand = new Command('eval')
|
|
|
589
616
|
// raises it — and the flag is what the caller needs next. Name it, with
|
|
590
617
|
// the value THIS run used, so the retry is an edit rather than a guess.
|
|
591
618
|
const msg = err?.message ?? '';
|
|
592
|
-
const hint = budgetOverrunHint(msg, scriptBudgetMs)
|
|
619
|
+
const hint = budgetOverrunHint(msg, scriptBudgetMs)
|
|
620
|
+
// A script aborted by its own reload is nearly always a persistence
|
|
621
|
+
// check hand-rolled without --reload; point at the flag that does it.
|
|
622
|
+
?? (reloadExpr === undefined ? navigationAbortHint(msg) : null);
|
|
593
623
|
if (!hint)
|
|
594
624
|
throw err;
|
|
595
625
|
throw new Error(`${msg}\n${hint}`);
|
|
@@ -632,8 +662,17 @@ export const pageEvalCommand = new Command('eval')
|
|
|
632
662
|
// page's rAF loop is its clock, and engines cap the per-frame delta, so
|
|
633
663
|
// `setTimeout(2000)` may advance only a fraction of a second of app time.
|
|
634
664
|
// Without this line the eval returns a plausible-looking false negative.
|
|
665
|
+
// A script that already steps the loop itself is immune to all of that, so
|
|
666
|
+
// slowRenderMessage returns null for it rather than prescribing the fix it
|
|
667
|
+
// is already applying.
|
|
635
668
|
if (d.slowRender) {
|
|
636
|
-
|
|
669
|
+
const slow = slowRenderMessage(d.slowRender.fps, {
|
|
670
|
+
camera: !!camera,
|
|
671
|
+
waitMs,
|
|
672
|
+
script: [expr, ...steps, reloadExpr ?? ''].join('\n'),
|
|
673
|
+
});
|
|
674
|
+
if (slow)
|
|
675
|
+
console.log(slow);
|
|
637
676
|
}
|
|
638
677
|
// Identity line on EVERY run: without it an agent can't distinguish
|
|
639
678
|
// "signed-in eval" from "--auth silently no-op'd against the anonymous
|