gipity 1.0.429 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -29
- package/dist/agents/claude-code.js +48 -0
- package/dist/agents/codex.js +45 -0
- package/dist/agents/grok.js +50 -0
- package/dist/agents/index.js +23 -0
- package/dist/agents/types.js +18 -0
- package/dist/api.js +14 -4
- package/dist/capture/sources/codex.js +216 -0
- package/dist/capture/sources/grok.js +178 -0
- package/dist/catalog.js +46 -0
- package/dist/client-context.js +2 -0
- package/dist/commands/add.js +9 -22
- package/dist/commands/build.js +1330 -0
- package/dist/commands/chat.js +9 -3
- package/dist/commands/claude.js +4 -13
- package/dist/commands/db.js +22 -5
- package/dist/commands/deploy.js +7 -0
- package/dist/commands/doctor.js +8 -2
- package/dist/commands/fn.js +24 -1
- package/dist/commands/init.js +9 -15
- package/dist/commands/logs.js +21 -2
- package/dist/commands/page-eval.js +25 -10
- package/dist/commands/page-inspect.js +7 -3
- package/dist/commands/page-screenshot.js +62 -23
- package/dist/commands/project.js +1 -1
- package/dist/commands/relay-install.js +1 -1
- package/dist/commands/relay.js +3 -3
- package/dist/commands/sandbox.js +62 -16
- package/dist/commands/setup.js +16 -10
- package/dist/commands/status.js +35 -7
- package/dist/commands/test.js +7 -0
- package/dist/commands/uninstall.js +25 -3
- package/dist/flag-aliases.js +1 -0
- package/dist/hooks/capture-runner.js +127 -38
- package/dist/index.js +1118 -361
- package/dist/knowledge.js +3 -3
- package/dist/prefs.js +42 -0
- package/dist/project-setup.js +2 -10
- package/dist/relay/daemon.js +124 -16
- package/dist/relay/diagnostics.js +4 -2
- package/dist/relay/onboarding.js +9 -9
- package/dist/relay/setup.js +8 -8
- package/dist/relay/state.js +1 -1
- package/dist/setup.js +258 -17
- package/package.json +4 -3
|
@@ -30,12 +30,15 @@ function label(text) {
|
|
|
30
30
|
function fmtMs(ms) {
|
|
31
31
|
return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${ms}ms`;
|
|
32
32
|
}
|
|
33
|
-
/**
|
|
34
|
-
* can't tell a signed-in capture from "--auth silently no-op'd and
|
|
35
|
-
* the anonymous page"
|
|
33
|
+
/** Identity line on EVERY capture (same format as page inspect/eval): without
|
|
34
|
+
* it an agent can't tell a signed-in capture from "--auth silently no-op'd and
|
|
35
|
+
* this is the anonymous page" — or assumes an unflagged capture carried a
|
|
36
|
+
* signed-in session. */
|
|
36
37
|
function printAuthLine(auth) {
|
|
37
|
-
if (!auth?.requested)
|
|
38
|
+
if (!auth?.requested) {
|
|
39
|
+
console.log(`${label('Auth')} ${muted('anonymous visitor (signed out; pass --auth to capture as your Gipity account)')}`);
|
|
38
40
|
return;
|
|
41
|
+
}
|
|
39
42
|
const who = getAuth()?.email;
|
|
40
43
|
console.log(auth.established
|
|
41
44
|
? `${label('Auth')} ${success('session established')}${who ? muted(` as ${who}`) : ''} ${muted('(what the page renders with it is app-defined)')}`
|
|
@@ -176,10 +179,13 @@ function appendOption(value, previous = []) {
|
|
|
176
179
|
return [...previous, value];
|
|
177
180
|
}
|
|
178
181
|
// Pre-capture scripting lives on --action, but agents reach for the JS-intent
|
|
179
|
-
// names first
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
|
|
182
|
+
// names first — --eval above all, the name they already know from `page eval`.
|
|
183
|
+
// The intent is unambiguous (run this JS in the page before the shot), so these
|
|
184
|
+
// are working hidden aliases, not errors — same accept-the-reflex-guess pattern
|
|
185
|
+
// as --full-page and --width/--height. (Supersedes the earlier decoy approach,
|
|
186
|
+
// which swallowed the script and errored with a redirect: when the guess is
|
|
187
|
+
// unambiguous, making it WORK beats making it a better error.)
|
|
188
|
+
const ACTION_ALIAS_FLAGS = ['--eval', '--js', '--javascript', '--script', '--code', '--exec'];
|
|
183
189
|
/** A capture is worthless if it fires before the app reaches the state you meant
|
|
184
190
|
* to photograph, and the only lever used to be a blind millisecond delay — so a
|
|
185
191
|
* camera/vision app got screenshotted with a guessed duration (`--wait 22000`).
|
|
@@ -216,6 +222,12 @@ export const CAMERA_DEFAULT_DELAY_MS = 15_000;
|
|
|
216
222
|
export const pageScreenshotCommand = new Command('screenshot')
|
|
217
223
|
.description('Screenshot a web page')
|
|
218
224
|
.argument('<url>', 'URL to screenshot')
|
|
225
|
+
// --device / --viewport lead the options: "how does this look on a phone?" is
|
|
226
|
+
// the single most common reason to screenshot, and agents routinely read the
|
|
227
|
+
// help through `--help | head -N` — buried below the timing flags, these two
|
|
228
|
+
// fell off the bottom and the run proceeded at desktop width.
|
|
229
|
+
.option('--device <names>', `Device preset(s): ${Object.keys(DEVICE_PRESETS).join(', ')} (comma-separated or repeat flag). mobile/tablet emulate a real touch device — touch events, mobile user-agent, DPR — so touch-gated mobile UI actually renders.`, appendOption, [])
|
|
230
|
+
.option('--viewport <dims>', 'Raw viewport(s): WxH or WxH@dpr (comma-separated or repeat flag), e.g. --viewport 390x844 for a phone-sized window', appendOption, [])
|
|
219
231
|
// No commander default: a default here would make the value always set, so the
|
|
220
232
|
// merge below could not tell "caller chose a delay" from "nobody did" — which
|
|
221
233
|
// is what --camera's model-load default and the --wait-for gate both hinge on.
|
|
@@ -225,27 +237,35 @@ export const pageScreenshotCommand = new Command('screenshot')
|
|
|
225
237
|
.option('--action <js>', 'Run JS in the page before capturing — e.g. click a button to enter a state ("document.getElementById(\'play\').click()"). Runs as an async function body, so const/await and app-relative import(\'./…\') work. Runs after the post-load delay, then settles again before the shot. If it throws, the capture still happens and the failure is reported.')
|
|
226
238
|
.option('--full', 'Capture the full scrollable page (default: viewport only). Scrolls the page through first so scroll-reveal/fade-in-on-scroll (IntersectionObserver) sections render into the shot instead of capturing blank.')
|
|
227
239
|
.option('-o, --output <file>', 'Output path (single viewport only; default .gipity/screenshots/ss-<host>-<timestamp>.png)')
|
|
228
|
-
.option('--device <names>', `Device preset(s): ${Object.keys(DEVICE_PRESETS).join(', ')} (comma-separated or repeat flag). mobile/tablet emulate a real touch device — touch events, mobile user-agent, DPR — so touch-gated mobile UI actually renders.`, appendOption, [])
|
|
229
|
-
.option('--viewport <dims>', 'Raw viewport(s): WxH or WxH@dpr (comma-separated or repeat flag)', appendOption, [])
|
|
230
240
|
.option('--no-reload-between', 'Skip reload between viewports (faster, lower fidelity - only safe for static pages)')
|
|
231
241
|
.option('--fake-media', 'Grant a synthetic microphone + camera and auto-accept the getUserMedia prompt, so voice/camera apps render headlessly. The video feed is a built-in test pattern — to capture what the app does with a REAL frame (a hand, a face, an object), use --camera <path> instead.')
|
|
232
242
|
.option('--camera <path>', 'Play a local image or video (.png/.jpg/.webp/.mp4/.webm/.y4m/.mjpeg) as the browser\'s WEBCAM feed, then capture — so the shot shows the app reacting to a frame you chose (detected gesture, boxes, labels). Implies --fake-media.')
|
|
233
|
-
.option('--auth', 'Capture the page signed in as you (your Gipity account), so UI behind a Sign-in-with-Gipity login is shown. Only works for apps using Sign in with Gipity, hosted on *.gipity.ai.')
|
|
243
|
+
.option('--auth', 'Capture the page signed in as you (your Gipity account), so UI behind a Sign-in-with-Gipity login is shown. Only works for apps using Sign in with Gipity, hosted on *.gipity.ai. Without this flag the page loads as a genuinely anonymous, signed-out visitor — nothing carries over from earlier --auth runs.')
|
|
234
244
|
.option('--ephemeral', 'Skip the project screenshot history: do not persist this capture to Gipity (screenshots/ in the project). Local file is still written.')
|
|
235
245
|
.option('--json', 'Output JSON metadata instead of a friendly summary')
|
|
236
246
|
.addOption(new Option('--post-load-delay <ms>', 'Alias for --wait').hideHelp())
|
|
247
|
+
// (--eval and the other JS-intent guesses are registered in one place from
|
|
248
|
+
// ACTION_ALIAS_FLAGS below — a second standalone registration here makes
|
|
249
|
+
// commander throw "conflicting flag" at startup.)
|
|
237
250
|
// `--full-page` is the Puppeteer/Playwright name for this (their `fullPage`),
|
|
238
251
|
// so agents reach for it by reflex. Accept it as a hidden alias for `--full`
|
|
239
252
|
// rather than reject it as an unknown option and send them on a --help detour.
|
|
240
253
|
.addOption(new Option('--full-page', 'Alias for --full').hideHelp())
|
|
254
|
+
// `--width 390 --height 844` is the setViewport vocabulary agents guess first
|
|
255
|
+
// when asked for a phone-sized shot. Accept the pair as a working alias for
|
|
256
|
+
// --viewport WxH instead of bouncing the guess into a --help detour.
|
|
257
|
+
.addOption(new Option('--width <px>', 'Alias: --viewport WxH').hideHelp())
|
|
258
|
+
.addOption(new Option('--height <px>', 'Alias: --viewport WxH').hideHelp())
|
|
241
259
|
.action((url, opts) => run('Page screenshot', async () => {
|
|
242
|
-
// A JS-intent
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
260
|
+
// A JS-intent alias guess (--eval et al., hidden): fold it into the action
|
|
261
|
+
// script so the reflex guess just works, and name the canonical flag once
|
|
262
|
+
// so the next call uses it.
|
|
263
|
+
const aliasFlag = ACTION_ALIAS_FLAGS.find((f) => opts[f.slice(2)] !== undefined);
|
|
264
|
+
if (aliasFlag) {
|
|
265
|
+
console.error(muted(`Note: treating ${aliasFlag} as --action — it runs your JS in the page before the capture. --action is the canonical flag.`));
|
|
248
266
|
}
|
|
267
|
+
const actionScript = [opts.action, ...ACTION_ALIAS_FLAGS.map((f) => opts[f.slice(2)])]
|
|
268
|
+
.filter(Boolean).join('\n');
|
|
249
269
|
// --wait is the canonical name (it is what `page inspect`/`page eval` call it);
|
|
250
270
|
// --post-load-delay stays as a hidden alias. Whether the caller named EITHER is
|
|
251
271
|
// what decides the defaults below, so keep the raw "was it set" signal.
|
|
@@ -284,6 +304,24 @@ export const pageScreenshotCommand = new Command('screenshot')
|
|
|
284
304
|
}
|
|
285
305
|
const deviceNames = splitCsv(opts.device);
|
|
286
306
|
const viewportStrs = splitCsv(opts.viewport);
|
|
307
|
+
// --width/--height: fold the alias pair into a --viewport entry. Half a
|
|
308
|
+
// pair is a mis-invocation worth one precise line, not a range error.
|
|
309
|
+
if (opts.width !== undefined || opts.height !== undefined) {
|
|
310
|
+
if (opts.width === undefined || opts.height === undefined) {
|
|
311
|
+
pageScreenshotCommand.error('error: --width and --height go together (both in px, equivalent to --viewport WxH) — ' +
|
|
312
|
+
'or use a preset like --device mobile for a real handset (touch events, mobile user-agent, DPR)');
|
|
313
|
+
}
|
|
314
|
+
if (!/^\d+$/.test(String(opts.width)) || !/^\d+$/.test(String(opts.height))) {
|
|
315
|
+
pageScreenshotCommand.error('error: --width and --height must be integers (px)');
|
|
316
|
+
}
|
|
317
|
+
viewportStrs.push(`${opts.width}x${opts.height}`);
|
|
318
|
+
// A raw phone-sized window is NOT a phone: touch-gated mobile UI still
|
|
319
|
+
// renders its desktop variant. Say so once, on the path where the caller
|
|
320
|
+
// was clearly after a phone.
|
|
321
|
+
if (parseInt(String(opts.width), 10) <= 500) {
|
|
322
|
+
console.error(muted('Tip: --device mobile emulates a real handset (touch events, mobile user-agent, DPR) — a raw viewport only resizes the window.'));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
287
325
|
const customViewports = [
|
|
288
326
|
...deviceNames.map(resolveDevice),
|
|
289
327
|
...viewportStrs.map(parseViewportString),
|
|
@@ -318,11 +356,12 @@ export const pageScreenshotCommand = new Command('screenshot')
|
|
|
318
356
|
}
|
|
319
357
|
// The pre-capture script the server runs after the post-load delay: the
|
|
320
358
|
// --wait-for gate first (so the shot waits for the state, deterministically),
|
|
321
|
-
// then the caller's --action
|
|
359
|
+
// then the caller's --action (with any alias-flag scripts folded in by
|
|
360
|
+
// actionScript above). All the same in-page primitive, so they
|
|
322
361
|
// compose into one script rather than needing a second server round-trip.
|
|
323
362
|
const preCapture = [
|
|
324
363
|
opts.waitFor ? buildWaitForGate(opts.waitFor, waitForTimeoutMs) : '',
|
|
325
|
-
|
|
364
|
+
actionScript,
|
|
326
365
|
].filter(Boolean).join('\n');
|
|
327
366
|
const body = {
|
|
328
367
|
url,
|
|
@@ -476,10 +515,10 @@ export const pageScreenshotCommand = new Command('screenshot')
|
|
|
476
515
|
printVfsLine(s);
|
|
477
516
|
}
|
|
478
517
|
}));
|
|
479
|
-
// Register the JS-intent guesses as hidden
|
|
480
|
-
// the script) — the action
|
|
481
|
-
for (const f of
|
|
482
|
-
pageScreenshotCommand.addOption(new Option(`${f} <
|
|
518
|
+
// Register the JS-intent guesses as hidden aliases for --action (value-taking,
|
|
519
|
+
// so they capture the script) — the action folds them into the pre-capture body.
|
|
520
|
+
for (const f of ACTION_ALIAS_FLAGS)
|
|
521
|
+
pageScreenshotCommand.addOption(new Option(`${f} <js>`, 'Alias for --action').hideHelp());
|
|
483
522
|
// `screenshot` captures the page AFTER load + settle (+ optional --wait-for gate
|
|
484
523
|
// and --action). There is no scroll-to-a-position lever (agents reach for
|
|
485
524
|
// --scroll and get an unknown-option detour) — but `--full` does walk the page
|
package/dist/commands/project.js
CHANGED
|
@@ -134,7 +134,7 @@ projectCommand
|
|
|
134
134
|
console.log(`${muted('Next:')} switch to "${project.name}" in the sidebar.`);
|
|
135
135
|
}
|
|
136
136
|
else {
|
|
137
|
-
console.log(`${muted('Next:')} exit Claude (Ctrl+D), then run: ${brand('gipity
|
|
137
|
+
console.log(`${muted('Next:')} exit Claude (Ctrl+D), then run: ${brand('gipity build')}`);
|
|
138
138
|
console.log(`${muted('Pick')} "${project.name}" ${muted(`- it'll be at the top of the list.`)}`);
|
|
139
139
|
}
|
|
140
140
|
}));
|
|
@@ -6,7 +6,7 @@ import { installAutostart, removeAutostart, resolveCliPath } from '../relay/setu
|
|
|
6
6
|
function requirePaired() {
|
|
7
7
|
const device = state.getDevice();
|
|
8
8
|
if (!device) {
|
|
9
|
-
console.error(`${clrError('No paired device.')} Run ${bold('gipity
|
|
9
|
+
console.error(`${clrError('No paired device.')} Run ${bold('gipity connect')} to pair this machine.`);
|
|
10
10
|
process.exit(1);
|
|
11
11
|
}
|
|
12
12
|
return device;
|
package/dist/commands/relay.js
CHANGED
|
@@ -29,7 +29,7 @@ relayCommand
|
|
|
29
29
|
.option('--name <name>', 'Device name shown in the web CLI (default: this machine\'s hostname)')
|
|
30
30
|
.option('--no-start', 'Pair only; do not start the relay daemon now')
|
|
31
31
|
.option('--no-autostart', 'Skip the OS login service (use when a supervising app owns the daemon)')
|
|
32
|
-
.option('--force', 'Re-pair even if already paired (
|
|
32
|
+
.option('--force', 'Re-pair even if already paired (re-registers this machine, rotating its token)')
|
|
33
33
|
.option('--json', 'Machine-readable output')
|
|
34
34
|
.action(async (opts) => {
|
|
35
35
|
const fail = (code, message) => {
|
|
@@ -121,7 +121,7 @@ relayCommand
|
|
|
121
121
|
return;
|
|
122
122
|
}
|
|
123
123
|
if (!s.device) {
|
|
124
|
-
console.log(`${muted('No paired device.')} Run ${brand('gipity
|
|
124
|
+
console.log(`${muted('No paired device.')} Run ${brand('gipity connect')} to pair this machine.`);
|
|
125
125
|
return;
|
|
126
126
|
}
|
|
127
127
|
console.log(`${bold('Device:')} ${brand(s.device.name)} ${muted(`(${s.device.guid})`)}`);
|
|
@@ -380,7 +380,7 @@ registerInstallCommands(relayCommand);
|
|
|
380
380
|
function requirePaired() {
|
|
381
381
|
const device = state.getDevice();
|
|
382
382
|
if (!device) {
|
|
383
|
-
console.error(`${clrError('No paired device.')} Run ${brand('gipity
|
|
383
|
+
console.error(`${clrError('No paired device.')} Run ${brand('gipity connect')} to pair this machine.`);
|
|
384
384
|
process.exit(1);
|
|
385
385
|
}
|
|
386
386
|
return device;
|
package/dist/commands/sandbox.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import { readFileSync, existsSync, statSync } from 'fs';
|
|
3
|
-
import { dirname, extname, relative, resolve } from 'path';
|
|
1
|
+
import { Command, Option } from 'commander';
|
|
2
|
+
import { readFileSync, existsSync, statSync, mkdirSync, writeFileSync } from 'fs';
|
|
3
|
+
import { dirname, extname, relative, resolve, sep } from 'path';
|
|
4
4
|
import { post } from '../api.js';
|
|
5
5
|
import { resolveProjectContext, getConfigPath, getProjectRoot, shouldIgnore } from '../config.js';
|
|
6
6
|
import { SCRATCH_IGNORE } from '../setup.js';
|
|
@@ -167,19 +167,28 @@ sandboxCommand
|
|
|
167
167
|
// an interpreter token, or a --file extension - see resolveLanguage().
|
|
168
168
|
.option('--language <language>', 'Language: js, py, or bash (required unless pinned by an interpreter token or --file extension)')
|
|
169
169
|
.option('--file <path>', 'Read the code body from a file instead of the inline <code> arg; --language is inferred from the extension when not given')
|
|
170
|
+
// `--code "<code>"` is the natural first guess for passing inline code (the
|
|
171
|
+
// positional arg is the canonical spelling). Accept it as a working alias
|
|
172
|
+
// rather than bouncing the guess into an unknown-option --help detour.
|
|
173
|
+
.addOption(new Option('--code <code>', 'Alias for the positional inline <code> arg').hideHelp())
|
|
170
174
|
.option('--timeout <seconds>', 'Execution timeout in seconds', '30')
|
|
171
175
|
.option('--input <path>', 'Narrow to specific project files instead of auto-mirroring the whole tree (repeatable). Use this only for >1 GB projects or when you want surgical control.', (v, prev) => [...(prev ?? []), v])
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
// the flag is repeatable.
|
|
176
|
-
.option('--
|
|
176
|
+
// Named "discard", not "no-sync": in Gipity vocabulary "sync" means the
|
|
177
|
+
// local<->cloud file sync, so a no-sync spelling reads as "give me the file
|
|
178
|
+
// locally, just don't sync it" - which is the tmp/ scratch path below, not
|
|
179
|
+
// this flag. Collected as an array so the flag is repeatable.
|
|
180
|
+
.option('--discard-output <glob>', 'Discard run outputs matching this glob entirely - not saved, not returned (repeatable). To LOOK at an output without keeping it, write it under tmp/ instead: scratch outputs land on local disk only and never enter the project. Supports *, **, and dir/ prefixes.', (v, prev) => [...prev, v], [])
|
|
177
181
|
.option('--json', 'Output as JSON')
|
|
178
182
|
.addHelpText('after', `
|
|
179
183
|
By default the whole project is auto-mirrored into /work/ (up to 1 GB) -
|
|
180
184
|
so your code can reference project files by their relative path, and any
|
|
181
185
|
file you write lands back in the project. No manual copy needed.
|
|
182
186
|
|
|
187
|
+
Exception: tmp/ is scratch. An output written under tmp/ comes back to
|
|
188
|
+
your local tmp/ for inspection but is never saved to the project - use it
|
|
189
|
+
for previews and other look-once artifacts (no cleanup needed). To drop
|
|
190
|
+
an output entirely, --discard-output <glob>.
|
|
191
|
+
|
|
183
192
|
Use --input only for projects over the auto-mirror cap, or when you want
|
|
184
193
|
to restrict what the sandbox sees.
|
|
185
194
|
|
|
@@ -196,6 +205,11 @@ Examples:
|
|
|
196
205
|
$ gipity sandbox run --language python \\
|
|
197
206
|
"import pandas as pd; print(pd.read_csv('data/sales.csv').describe())"
|
|
198
207
|
|
|
208
|
+
# Preview a generated PDF without saving the preview: write it to tmp/
|
|
209
|
+
$ gipity sandbox run bash \\
|
|
210
|
+
"pdftoppm -png -r 90 docs/report.pdf tmp/preview"
|
|
211
|
+
# -> tmp/preview-1.png lands on local disk only; Read it, done.
|
|
212
|
+
|
|
199
213
|
# Run a script file directly (language inferred from .py)
|
|
200
214
|
$ gipity sandbox run --file build_report.py
|
|
201
215
|
$ gipity sandbox run python build_report.py # same thing, interpreter shorthand
|
|
@@ -228,6 +242,14 @@ GCC/Rust).
|
|
|
228
242
|
let inlineCode;
|
|
229
243
|
let filePath = opts.file;
|
|
230
244
|
let langFromInterp;
|
|
245
|
+
// --code alias: fold it into the positional slot before the shape checks.
|
|
246
|
+
if (opts.code !== undefined) {
|
|
247
|
+
if (args.length) {
|
|
248
|
+
console.error(clrError('Pass the code once: either positionally or via --code, not both'));
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
args = [opts.code];
|
|
252
|
+
}
|
|
231
253
|
if (args.length >= 2 && INTERPRETERS[args[0].toLowerCase()] !== undefined) {
|
|
232
254
|
langFromInterp = INTERPRETERS[args[0].toLowerCase()];
|
|
233
255
|
const rest = args.slice(1).join(' ');
|
|
@@ -268,11 +290,11 @@ GCC/Rust).
|
|
|
268
290
|
// This runs BEFORE the project sync and the server round trip below, so a
|
|
269
291
|
// missing language costs nothing but the message.
|
|
270
292
|
const language = resolveLanguage({ langFromInterp, langOpt: opts.language, filePath, inlineCode });
|
|
271
|
-
// Validate --
|
|
293
|
+
// Validate --discard-output globs while we're still pre-network: an empty
|
|
272
294
|
// pattern (e.g. a quoting mishap) would silently match nothing server-side.
|
|
273
|
-
const
|
|
274
|
-
if (
|
|
275
|
-
console.error(clrError('--
|
|
295
|
+
const discardOutput = opts.discardOutput ?? [];
|
|
296
|
+
if (discardOutput.some((g) => !g.trim())) {
|
|
297
|
+
console.error(clrError('--discard-output requires a non-empty glob (e.g. --discard-output "*.ppm")'));
|
|
276
298
|
process.exit(1);
|
|
277
299
|
}
|
|
278
300
|
// Args are good - now it's worth resolving (and announcing) the project.
|
|
@@ -311,8 +333,9 @@ GCC/Rust).
|
|
|
311
333
|
cwd,
|
|
312
334
|
// The filter must run SERVER-side (in the output extractor): skipping
|
|
313
335
|
// only in the CLI would still write the files into project storage and
|
|
314
|
-
// make every later sync propose deleting them.
|
|
315
|
-
|
|
336
|
+
// make every later sync propose deleting them. (`noSyncOutput` is the
|
|
337
|
+
// wire name for --discard-output.)
|
|
338
|
+
noSyncOutput: discardOutput.length ? discardOutput : undefined,
|
|
316
339
|
});
|
|
317
340
|
const res = opts.json
|
|
318
341
|
? await doRun()
|
|
@@ -326,8 +349,26 @@ GCC/Rust).
|
|
|
326
349
|
if (pulledLocal) {
|
|
327
350
|
await sync({ interactive: false, progress: opts.json ? undefined : createProgressReporter() });
|
|
328
351
|
}
|
|
352
|
+
// Scratch outputs (tmp/ and friends) never enter the project: the server
|
|
353
|
+
// returns their bytes inline instead of persisting them, and we land them
|
|
354
|
+
// here. The scratch namespace is ignored by sync in both directions, so
|
|
355
|
+
// the file exists on local disk only - inspect it, no cleanup required.
|
|
356
|
+
const scratchWritten = [];
|
|
357
|
+
if (res.data.scratchFiles?.length) {
|
|
358
|
+
const base = resolve(getProjectRoot() ?? process.cwd());
|
|
359
|
+
for (const f of res.data.scratchFiles) {
|
|
360
|
+
const rel = f.path.replace(/\\/g, '/');
|
|
361
|
+
const abs = resolve(base, rel);
|
|
362
|
+
if (abs !== base && !abs.startsWith(base + sep))
|
|
363
|
+
continue; // traversal guard
|
|
364
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
365
|
+
writeFileSync(abs, Buffer.from(f.contentBase64, 'base64'));
|
|
366
|
+
scratchWritten.push(rel);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
329
369
|
if (opts.json) {
|
|
330
|
-
|
|
370
|
+
const { scratchFiles: _omit, ...rest } = res.data;
|
|
371
|
+
console.log(JSON.stringify({ ...rest, scratchFiles: scratchWritten, filesSynced: pulledLocal }));
|
|
331
372
|
}
|
|
332
373
|
else {
|
|
333
374
|
if (res.data.autoMirrorSkipped) {
|
|
@@ -367,8 +408,13 @@ GCC/Rust).
|
|
|
367
408
|
console.log(dim("Not pulled locally - run 'gipity sync' to fetch them."));
|
|
368
409
|
}
|
|
369
410
|
}
|
|
411
|
+
if (scratchWritten.length > 0) {
|
|
412
|
+
console.log('\nScratch outputs (local only - never synced or deployed):');
|
|
413
|
+
for (const f of scratchWritten)
|
|
414
|
+
console.log(`${f}`);
|
|
415
|
+
}
|
|
370
416
|
if (res.data.skippedOutputFiles && res.data.skippedOutputFiles.length > 0) {
|
|
371
|
-
console.log(dim('\
|
|
417
|
+
console.log(dim('\nDiscarded (--discard-output):'));
|
|
372
418
|
for (const f of res.data.skippedOutputFiles)
|
|
373
419
|
console.log(dim(`${f}`));
|
|
374
420
|
}
|
package/dist/commands/setup.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `gipity
|
|
2
|
+
* `gipity connect` — connect this computer to gipity.ai, then stop.
|
|
3
3
|
*
|
|
4
|
-
* Runs the SAME first steps as `gipity
|
|
4
|
+
* Runs the SAME first steps as `gipity build` (log in, then pair + start the
|
|
5
5
|
* relay + install the OS login service) but does NOT pick a project or launch
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* an agent. For the user who wants their machine drivable from the web CLI
|
|
7
|
+
* without being dropped into a coding session.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* Named `connect` because "setup" and "init" are synonyms in plain English -
|
|
10
|
+
* the machine-scoped verb needed its own word (and it matches `gipity
|
|
11
|
+
* payments connect`). `setup` stays as a hidden alias for muscle memory and
|
|
12
|
+
* old docs.
|
|
13
|
+
*
|
|
14
|
+
* Shares one implementation with `gipity build`: auth via `login-flow.ts`,
|
|
10
15
|
* relay setup via `relay/onboarding.ts` (`runRelaySetup`). Nothing to maintain
|
|
11
16
|
* twice.
|
|
12
17
|
*/
|
|
@@ -16,12 +21,13 @@ import { interactiveLogin } from '../login-flow.js';
|
|
|
16
21
|
import { runRelaySetup } from '../relay/onboarding.js';
|
|
17
22
|
import * as relayState from '../relay/state.js';
|
|
18
23
|
import { bold, brand, success, muted, error as clrError } from '../colors.js';
|
|
19
|
-
export const
|
|
20
|
-
.
|
|
24
|
+
export const connectCommand = new Command('connect')
|
|
25
|
+
.alias('setup') // legacy name, kept working but not advertised
|
|
26
|
+
.description('Connect this computer to gipity.ai so the web CLI can drive it (no project, no launch)')
|
|
21
27
|
.action(async () => {
|
|
22
28
|
try {
|
|
23
29
|
// Leading blank comes from the central output frame (installOutputFrame).
|
|
24
|
-
console.log(` ${bold('Gipity
|
|
30
|
+
console.log(` ${bold('Gipity connect')} ${muted('- let gipity.ai drive this computer')}`);
|
|
25
31
|
console.log('');
|
|
26
32
|
// ── Step 1: Auth ──────────────────────────────────────────────────
|
|
27
33
|
let auth = getAuth();
|
|
@@ -44,10 +50,10 @@ export const setupCommand = new Command('setup')
|
|
|
44
50
|
if (enabled) {
|
|
45
51
|
const running = relayState.isRelayEnabled() && !relayState.isPaused();
|
|
46
52
|
console.log(` ${success('Done')} — your relay ${running ? 'is running in the background' : 'is set up'} and will start with your computer.`);
|
|
47
|
-
console.log(` ${muted('Open')} ${brand('gipity.ai')} ${muted('and start a chat to drive
|
|
53
|
+
console.log(` ${muted('Open')} ${brand('gipity.ai')} ${muted('and start a chat to drive your coding agent here. Manage it with `gipity relay status`.')}`);
|
|
48
54
|
}
|
|
49
55
|
else {
|
|
50
|
-
console.log(` ${muted('No relay set up. Run `gipity
|
|
56
|
+
console.log(` ${muted('No relay set up. Run `gipity connect` again anytime, or `gipity build` to start building.')}`);
|
|
51
57
|
}
|
|
52
58
|
console.log('');
|
|
53
59
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync, readFileSync } from 'fs';
|
|
|
3
3
|
import { join, resolve } from 'path';
|
|
4
4
|
import { homedir } from 'os';
|
|
5
5
|
import { getAuth, sessionExpired } from '../auth.js';
|
|
6
|
+
import { get, usingEnvToken, ApiError } from '../api.js';
|
|
6
7
|
import { getConfig, liveUrl } from '../config.js';
|
|
7
8
|
import { brand, success, warning, muted, error as clrError } from '../colors.js';
|
|
8
9
|
import { GIPITY_PLUGIN_ID, GIPITY_MARKETPLACE_NAME, setupClaudeHooks, ensureGipityPlugin, ensureGipityPluginInstalled, userScopeInstallState } from '../setup.js';
|
|
@@ -38,6 +39,17 @@ function checkGipityPlugin() {
|
|
|
38
39
|
const stale = missing.length === 1 && missing[0] === 'install' && install.exists;
|
|
39
40
|
return { missing, ok: missing.length === 0, stale };
|
|
40
41
|
}
|
|
42
|
+
async function probeAuth(loggedIn) {
|
|
43
|
+
if (!loggedIn && !usingEnvToken())
|
|
44
|
+
return 'none';
|
|
45
|
+
if (!usingEnvToken() && sessionExpired())
|
|
46
|
+
return 'expired';
|
|
47
|
+
// Cap the probe well below the API layer's 60s request timeout - status is
|
|
48
|
+
// a diagnostic command and must answer fast even when the network is dark.
|
|
49
|
+
const timeout = new Promise(res => setTimeout(() => res('unreachable'), 5000).unref?.());
|
|
50
|
+
const call = get('/users/me').then(() => 'ok', (err) => (err instanceof ApiError && err.statusCode === 401 ? 'rejected' : 'unreachable'));
|
|
51
|
+
return Promise.race([call, timeout]);
|
|
52
|
+
}
|
|
41
53
|
// `whoami` is the name agents reach for first when they want the signed-in
|
|
42
54
|
// identity (it's the unix spelling), and this is the command that prints it.
|
|
43
55
|
// Aliasing costs one line and turns a guess into a hit.
|
|
@@ -58,6 +70,7 @@ export const statusCommand = new Command('status')
|
|
|
58
70
|
// now go out. Skipped entirely (existsSync short-circuit) when the queue
|
|
59
71
|
// is empty, which is the common case, so this stays a no-op most runs.
|
|
60
72
|
const queueDelivered = (auth && !sessionExpired()) ? await flushBugQueue().catch(() => 0) : 0;
|
|
73
|
+
const probe = await probeAuth(!!auth);
|
|
61
74
|
if (opts.json) {
|
|
62
75
|
console.log(JSON.stringify({
|
|
63
76
|
project: config ? {
|
|
@@ -69,9 +82,13 @@ export const statusCommand = new Command('status')
|
|
|
69
82
|
} : null,
|
|
70
83
|
// `valid` reflects the refresh token (the real session) - access
|
|
71
84
|
// tokens auto-renew, so their expiry must not read as "invalid".
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
85
|
+
// `probe` is what one live call just proved: 'rejected' means every
|
|
86
|
+
// authenticated command will fail even though `valid` reads true.
|
|
87
|
+
auth: (auth || usingEnvToken()) ? {
|
|
88
|
+
email: auth?.email,
|
|
89
|
+
source: usingEnvToken() ? 'agent-token' : 'session',
|
|
90
|
+
valid: usingEnvToken() ? probe !== 'rejected' : !sessionExpired(),
|
|
91
|
+
probe,
|
|
75
92
|
} : null,
|
|
76
93
|
plugin: hookCheck,
|
|
77
94
|
}, null, 2));
|
|
@@ -88,14 +105,25 @@ export const statusCommand = new Command('status')
|
|
|
88
105
|
if (config.agentGuid)
|
|
89
106
|
console.log(`${muted('Agent:')} ${config.agentGuid}`);
|
|
90
107
|
}
|
|
91
|
-
if (
|
|
108
|
+
if (usingEnvToken()) {
|
|
109
|
+
console.log(`${muted('Auth:')} ${probe === 'rejected'
|
|
110
|
+
? warning('agent API token (GIPITY_TOKEN) rejected by the server — mint a new one: gipity skill read agent-deploy')
|
|
111
|
+
: success('agent API token (GIPITY_TOKEN)')}${probe === 'unreachable' ? ` ${muted('(unverified — API unreachable)')}` : ''}`);
|
|
112
|
+
}
|
|
113
|
+
else if (!auth) {
|
|
92
114
|
console.log(`${muted('Auth:')} ${warning('not logged in. Run: gipity login')}`);
|
|
93
115
|
}
|
|
94
|
-
else if (
|
|
95
|
-
console.log(`${muted('Auth:')} ${warning(`session expired for ${auth.email}. Run: gipity login`)}`);
|
|
116
|
+
else if (probe === 'expired') {
|
|
117
|
+
console.log(`${muted('Auth:')} ${warning(`session expired for ${auth.email}. Run: gipity login (headless/CI: set GIPITY_TOKEN — gipity skill read agent-deploy)`)}`);
|
|
118
|
+
}
|
|
119
|
+
else if (probe === 'rejected') {
|
|
120
|
+
// Locally fresh but the server says no (refresh token rotated away or
|
|
121
|
+
// revoked). Without the live probe this printed a green identity while
|
|
122
|
+
// every authenticated command failed.
|
|
123
|
+
console.log(`${muted('Auth:')} ${warning(`session for ${auth.email} was rejected by the server. Run: gipity login (headless/CI: set GIPITY_TOKEN — gipity skill read agent-deploy)`)}`);
|
|
96
124
|
}
|
|
97
125
|
else {
|
|
98
|
-
console.log(`${muted('Auth:')} ${success(auth.email)}`);
|
|
126
|
+
console.log(`${muted('Auth:')} ${success(auth.email)}${probe === 'unreachable' ? ` ${muted('(unverified — API unreachable)')}` : ''}`);
|
|
99
127
|
}
|
|
100
128
|
if (queueDelivered > 0) {
|
|
101
129
|
console.log(`${muted('Bug queue:')} ${success(`delivered ${queueDelivered} queued bug report${queueDelivered === 1 ? '' : 's'}`)}`);
|
package/dist/commands/test.js
CHANGED
|
@@ -206,6 +206,13 @@ export const testCommand = new Command('test')
|
|
|
206
206
|
console.log(clrError(`Run failed: ${data.errorMessage}`));
|
|
207
207
|
console.log('');
|
|
208
208
|
}
|
|
209
|
+
// What the isolated test DB reset did (tables truncated, test.preserve
|
|
210
|
+
// list, seeds re-applied) - so "expected N rows, got 0" reads as "the
|
|
211
|
+
// reset truncated that table", not as a mystery.
|
|
212
|
+
if (data.dbSummary) {
|
|
213
|
+
console.log(muted(`Test db: ${data.dbSummary}`));
|
|
214
|
+
console.log('');
|
|
215
|
+
}
|
|
209
216
|
// Failures recap. Results stream incrementally above, but agents usually
|
|
210
217
|
// read this output through a `tail` window, so anything printed early is
|
|
211
218
|
// lost - restate every failure (name + assertion message) right before
|
|
@@ -12,14 +12,14 @@ import { Command } from 'commander';
|
|
|
12
12
|
import { existsSync, rmSync, unlinkSync, readFileSync, writeFileSync } from 'fs';
|
|
13
13
|
import { homedir, platform as osPlatform } from 'os';
|
|
14
14
|
import { join, resolve } from 'path';
|
|
15
|
-
import { spawnSyncCommand } from '../platform.js';
|
|
15
|
+
import { spawnSyncCommand, resolveCommand } from '../platform.js';
|
|
16
16
|
import { post } from '../api.js';
|
|
17
17
|
import { getAuth } from '../auth.js';
|
|
18
18
|
import { confirm, getAutoConfirm } from '../utils.js';
|
|
19
19
|
import { bold, brand, dim, success, error as clrError, muted } from '../colors.js';
|
|
20
20
|
import * as relayState from '../relay/state.js';
|
|
21
21
|
import { planFor, UnsupportedPlatformError } from '../relay/installers.js';
|
|
22
|
-
import { GIPITY_PLUGIN_ID, GIPITY_MARKETPLACE_NAME, stripGipityHooks } from '../setup.js';
|
|
22
|
+
import { GIPITY_PLUGIN_ID, GIPITY_MARKETPLACE_NAME, stripGipityHooks, grokInstallState, agentSkillsState, AGENTS_SKILLS_DIR } from '../setup.js';
|
|
23
23
|
/** Remove Gipity's entries from the user-scope Claude Code settings: the
|
|
24
24
|
* plugin enablement, the marketplace registration, and any legacy hook
|
|
25
25
|
* blocks older CLI versions wrote there. Surgical - everything else in the
|
|
@@ -219,7 +219,29 @@ export const uninstallCommand = new Command('uninstall')
|
|
|
219
219
|
else {
|
|
220
220
|
console.log(`${muted('No Gipity entries in Claude Code settings.')}`);
|
|
221
221
|
}
|
|
222
|
-
//
|
|
222
|
+
// 4b. Uninstall the Gipity plugin from Grok Build and remove the skills
|
|
223
|
+
// the CLI copied into the cross-agent ~/.agents/skills dir for Codex.
|
|
224
|
+
// Both best-effort: neither existing is the common case.
|
|
225
|
+
if (grokInstallState().exists) {
|
|
226
|
+
spawnSyncCommand(resolveCommand('grok'), ['plugin', 'uninstall', 'gipity', '--confirm'], {
|
|
227
|
+
stdio: 'ignore',
|
|
228
|
+
timeout: 60_000,
|
|
229
|
+
});
|
|
230
|
+
console.log(`${success('Gipity plugin removed from Grok.')}`);
|
|
231
|
+
}
|
|
232
|
+
const agentSkills = agentSkillsState();
|
|
233
|
+
if (agentSkills.skills.length) {
|
|
234
|
+
for (const name of agentSkills.skills) {
|
|
235
|
+
// Only names our manifest recorded - never someone else's skills.
|
|
236
|
+
try {
|
|
237
|
+
rmSync(join(AGENTS_SKILLS_DIR, name), { recursive: true, force: true });
|
|
238
|
+
}
|
|
239
|
+
catch { /* best-effort */ }
|
|
240
|
+
}
|
|
241
|
+
console.log(`${success(`Removed ${agentSkills.skills.length} Gipity skills from ~/.agents/skills.`)}`);
|
|
242
|
+
}
|
|
243
|
+
// 5. Wipe ~/.gipity/ (this also removes the agent-hooks scripts and the
|
|
244
|
+
// agent-skills manifest, which live under it).
|
|
223
245
|
if (existsSync(gipityDir)) {
|
|
224
246
|
try {
|
|
225
247
|
rmSync(gipityDir, { recursive: true, force: true });
|
package/dist/flag-aliases.js
CHANGED