gipity 1.1.5 → 1.1.7
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/catalog.js +2 -2
- package/dist/client-context.js +9 -0
- package/dist/commands/brand.js +125 -0
- package/dist/commands/build.js +1 -1
- package/dist/commands/db.js +61 -0
- 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 +70 -3
- package/dist/commands/page-screenshot.js +34 -6
- package/dist/commands/records.js +93 -23
- package/dist/commands/service.js +71 -7
- package/dist/commands/token.js +6 -1
- package/dist/commands/uninstall.js +14 -1
- package/dist/commands/workflow.js +59 -6
- package/dist/db-checkpoint.js +42 -0
- package/dist/hooks/capture-runner.js +20 -8
- package/dist/index.js +777 -137
- package/dist/knowledge.js +3 -3
- package/dist/project-setup.js +2 -0
- package/dist/relay/diagnostics.js +4 -2
- package/dist/relay/onboarding.js +3 -3
- package/dist/setup.js +198 -16
- package/dist/template-vars.js +74 -0
- package/dist/utils.js +16 -5
- 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/catalog.js
CHANGED
|
@@ -19,9 +19,9 @@ export const STARTERS = [
|
|
|
19
19
|
/** Visible blank-wiring templates. */
|
|
20
20
|
export const BLANK = [
|
|
21
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' },
|
|
22
|
+
{ key: 'web-fullstack', hint: 'backend API + database wiring - frontend, functions, migrations; deploys green empty' },
|
|
23
23
|
{ key: '3d-engine', hint: '3D multiplayer wiring - Three.js + Rapier + Gipity Realtime' },
|
|
24
|
-
{ key: 'api', hint: 'pure API backend, no frontend -
|
|
24
|
+
{ key: 'api', hint: 'pure API backend, no frontend - functions + tests, deploys green' },
|
|
25
25
|
];
|
|
26
26
|
/** Hidden templates - installable by exact key, omitted from listings. */
|
|
27
27
|
export const HIDDEN = [
|
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/db.js
CHANGED
|
@@ -4,6 +4,7 @@ import { requireConfig } from '../config.js';
|
|
|
4
4
|
import { error as clrError, success } from '../colors.js';
|
|
5
5
|
import { run, printList, emitField } from '../helpers/index.js';
|
|
6
6
|
import { confirm } from '../utils.js';
|
|
7
|
+
import { createCheckpoint, restoreCheckpoint, dropCheckpoint, resolveDatabase } from '../db-checkpoint.js';
|
|
7
8
|
export const dbCommand = new Command('db')
|
|
8
9
|
.description('Manage databases');
|
|
9
10
|
dbCommand
|
|
@@ -171,4 +172,64 @@ dbCommand
|
|
|
171
172
|
console.log(success(`Dropped database '${name}'.`));
|
|
172
173
|
}
|
|
173
174
|
}));
|
|
175
|
+
dbCommand
|
|
176
|
+
.command('checkpoint')
|
|
177
|
+
.description('Snapshot every table so a write test can be undone. Take one before exercising a real write path (page eval on the deployed app, fn call, workflow run), then `gipity db restore` to put the data back exactly as it was - no hand-written SQL to scrub test strings out of real content.')
|
|
178
|
+
.option('--database <name>', 'Database name')
|
|
179
|
+
.option('--drop', 'Discard the existing checkpoint without restoring (keep whatever the run wrote)')
|
|
180
|
+
.option('--json', 'Output as JSON')
|
|
181
|
+
.action((opts) => run('Checkpoint', async () => {
|
|
182
|
+
const config = requireConfig();
|
|
183
|
+
const dbName = await resolveDatabase(config.projectGuid, opts.database);
|
|
184
|
+
if (!dbName) {
|
|
185
|
+
console.error(clrError('No databases in this project - nothing to checkpoint.'));
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
if (opts.drop) {
|
|
189
|
+
const dropped = await dropCheckpoint(config.projectGuid, dbName);
|
|
190
|
+
if (opts.json) {
|
|
191
|
+
console.log(JSON.stringify(dropped));
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
console.log(dropped.tables.length === 0
|
|
195
|
+
? `No checkpoint in '${dbName}'.`
|
|
196
|
+
: success(`Discarded the checkpoint of ${dropped.tables.length} table(s) in '${dbName}'. Current data kept.`));
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const res = await createCheckpoint(config.projectGuid, dbName);
|
|
201
|
+
if (opts.json) {
|
|
202
|
+
console.log(JSON.stringify(res));
|
|
203
|
+
}
|
|
204
|
+
else if (res.tables.length === 0) {
|
|
205
|
+
console.log(`Database '${dbName}' has no tables - nothing to checkpoint.`);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
console.log(success(`Checkpointed ${res.tables.length} table(s), ${res.rows} row(s) in '${dbName}'.`));
|
|
209
|
+
console.log('Undo everything written since: gipity db restore');
|
|
210
|
+
}
|
|
211
|
+
}));
|
|
212
|
+
dbCommand
|
|
213
|
+
.command('restore')
|
|
214
|
+
.description('Roll every table back to the last `gipity db checkpoint` and drop the checkpoint. The undo for a live write test.')
|
|
215
|
+
.option('--database <name>', 'Database name')
|
|
216
|
+
.option('--keep', 'Keep the checkpoint after restoring, so you can run the same write test again')
|
|
217
|
+
.option('--json', 'Output as JSON')
|
|
218
|
+
.action((opts) => run('Restore', async () => {
|
|
219
|
+
const config = requireConfig();
|
|
220
|
+
const dbName = await resolveDatabase(config.projectGuid, opts.database);
|
|
221
|
+
if (!dbName) {
|
|
222
|
+
console.error(clrError('No databases in this project - nothing to restore.'));
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
const res = await restoreCheckpoint(config.projectGuid, dbName, { keep: opts.keep });
|
|
226
|
+
if (opts.json) {
|
|
227
|
+
console.log(JSON.stringify(res));
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
console.log(success(`Restored ${res.tables.length} table(s) to the checkpoint (${res.rows} row(s)) in '${dbName}'.`));
|
|
231
|
+
if (opts.keep)
|
|
232
|
+
console.log('Checkpoint kept - restore again any time.');
|
|
233
|
+
}
|
|
234
|
+
}));
|
|
174
235
|
//# sourceMappingURL=db.js.map
|
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
|