gipity 1.0.429 → 1.1.1
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/claude-code.js +53 -0
- package/dist/agents/codex.js +49 -0
- package/dist/agents/grok.js +54 -0
- package/dist/agents/index.js +23 -0
- package/dist/agents/types.js +18 -0
- package/dist/api.js +7 -0
- package/dist/capture/sources/codex.js +216 -0
- package/dist/capture/sources/grok.js +178 -0
- package/dist/client-context.js +2 -0
- package/dist/commands/build.js +1352 -0
- package/dist/commands/chat.js +9 -3
- package/dist/commands/claude.js +4 -13
- package/dist/commands/db.js +12 -5
- package/dist/commands/doctor.js +8 -2
- package/dist/commands/init.js +9 -15
- package/dist/commands/page-eval.js +18 -7
- 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 +2 -2
- package/dist/commands/sandbox.js +62 -16
- package/dist/commands/setup.js +16 -10
- package/dist/commands/uninstall.js +25 -3
- package/dist/hooks/capture-runner.js +127 -38
- package/dist/index.js +1055 -330
- package/dist/knowledge.js +3 -3
- package/dist/prefs.js +50 -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 +2 -2
- package/dist/setup.js +258 -17
- package/package.json +4 -3
package/dist/commands/chat.js
CHANGED
|
@@ -50,9 +50,15 @@ export const chatCommand = new Command('chat')
|
|
|
50
50
|
...(a.remoteSize != null ? { size: a.remoteSize } : {}),
|
|
51
51
|
}));
|
|
52
52
|
}
|
|
53
|
+
// The server's cost-consent gate stops the agentic loop on big-build
|
|
54
|
+
// requests: content comes back empty with an explicit costWarning.
|
|
55
|
+
// Surface it - before this the CLI printed a blank line and the request
|
|
56
|
+
// silently did nothing. Re-running the command consents via chat reply.
|
|
57
|
+
const displayContent = res.data.content || res.data.costWarning?.message || '';
|
|
53
58
|
if (opts.json) {
|
|
54
59
|
console.log(JSON.stringify({
|
|
55
|
-
content:
|
|
60
|
+
content: displayContent,
|
|
61
|
+
costWarning: res.data.costWarning ?? null,
|
|
56
62
|
toolsUsed: res.data.toolsUsed?.map(t => ({
|
|
57
63
|
tool: t.toolName,
|
|
58
64
|
success: t.success,
|
|
@@ -67,8 +73,8 @@ export const chatCommand = new Command('chat')
|
|
|
67
73
|
}));
|
|
68
74
|
}
|
|
69
75
|
else {
|
|
70
|
-
// Show agent response
|
|
71
|
-
console.log(
|
|
76
|
+
// Show agent response (or the cost warning when the gate stopped the loop)
|
|
77
|
+
console.log(displayContent);
|
|
72
78
|
// Show tools used
|
|
73
79
|
if (res.data.toolsUsed && res.data.toolsUsed.length > 0) {
|
|
74
80
|
const toolNames = [...new Set(res.data.toolsUsed.map(t => t.toolName))];
|
package/dist/commands/claude.js
CHANGED
|
@@ -9,7 +9,7 @@ import { get, post, ApiError, getAccountSlug } from '../api.js';
|
|
|
9
9
|
import { interactiveLogin } from '../login-flow.js';
|
|
10
10
|
import { getConfig, saveConfigAt, clearConfigCache, getApiBaseOverride, DEFAULT_API_BASE, getConfigPath } from '../config.js';
|
|
11
11
|
import { sync } from '../sync.js';
|
|
12
|
-
import { slugify,
|
|
12
|
+
import { slugify, ensureGipityPluginInstalled, setupProjectTools, DEFAULT_SYNC_IGNORE, isSyncIgnored } from '../setup.js';
|
|
13
13
|
import { buildProjectContextBlock as buildProjectContextBlockText, buildNewProjectPrompt, buildResumeWrap, buildFreshWrap, } from '../prompts.js';
|
|
14
14
|
import * as relayState from '../relay/state.js';
|
|
15
15
|
import { maybeOfferRelayOn, ensureDaemonRunning } from '../relay/onboarding.js';
|
|
@@ -481,10 +481,7 @@ export const claudeCommand = new Command('claude')
|
|
|
481
481
|
catch (err) {
|
|
482
482
|
console.log(` ${warning('Could not sync files (will retry on next prompt):')} ${err.message}`);
|
|
483
483
|
}
|
|
484
|
-
|
|
485
|
-
setupClaudeMd();
|
|
486
|
-
setupAgentsMd();
|
|
487
|
-
setupGitignore();
|
|
484
|
+
setupProjectTools();
|
|
488
485
|
if (nonInteractive) {
|
|
489
486
|
// Headless: the -p message is wrapped later (Step 3). Just record
|
|
490
487
|
// whether to use the new-project framing for that wrap.
|
|
@@ -503,10 +500,7 @@ export const claudeCommand = new Command('claude')
|
|
|
503
500
|
// cwd already has (or inherits) a .gipity.json - run inside it.
|
|
504
501
|
console.log(` Project: ${brand(existing.projectSlug)} ${muted(`(${existing.projectGuid})`)}`);
|
|
505
502
|
console.log(` ${success('Already set up.')}\n`);
|
|
506
|
-
|
|
507
|
-
setupClaudeMd();
|
|
508
|
-
setupAgentsMd();
|
|
509
|
-
setupGitignore();
|
|
503
|
+
setupProjectTools();
|
|
510
504
|
// Warn before syncing a very large project tree. An oversized root
|
|
511
505
|
// (or a config accidentally rooted high up, e.g. at $HOME) makes the
|
|
512
506
|
// sync slow, and a silent multi-GB walk reads as a hang.
|
|
@@ -658,10 +652,7 @@ export const claudeCommand = new Command('claude')
|
|
|
658
652
|
// of done, and a welcome banner prints before launch - the user tells
|
|
659
653
|
// Claude directly what they want to build or do.
|
|
660
654
|
initialPrompt = '';
|
|
661
|
-
|
|
662
|
-
setupClaudeMd();
|
|
663
|
-
setupAgentsMd();
|
|
664
|
-
setupGitignore();
|
|
655
|
+
setupProjectTools();
|
|
665
656
|
console.log(` ${success(`Project "${project.name}" ready.`)}\n`);
|
|
666
657
|
}
|
|
667
658
|
// ── Step 3: Launch Claude Code ────────────────────────────────────
|
package/dist/commands/db.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { get, post
|
|
2
|
+
import { get, post } from '../api.js';
|
|
3
3
|
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';
|
|
@@ -106,13 +106,20 @@ dbCommand
|
|
|
106
106
|
.description('Create a database')
|
|
107
107
|
.option('--json', 'Output as JSON')
|
|
108
108
|
.action((name, opts) => run('Create', async () => {
|
|
109
|
-
//
|
|
110
|
-
|
|
109
|
+
// Direct REST create - same endpoint `db drop` uses. This used to delegate
|
|
110
|
+
// to agent chat, which burned LLM tokens and, after the cost-consent gate
|
|
111
|
+
// landed server-side, could return an empty response having created
|
|
112
|
+
// nothing. Creating a database isn't destructive, so no confirmation.
|
|
113
|
+
const config = requireConfig();
|
|
114
|
+
await post(`/projects/${config.projectGuid}/db/manage`, {
|
|
115
|
+
action: 'create',
|
|
116
|
+
name,
|
|
117
|
+
});
|
|
111
118
|
if (opts.json) {
|
|
112
|
-
console.log(JSON.stringify({
|
|
119
|
+
console.log(JSON.stringify({ success: true, name }));
|
|
113
120
|
}
|
|
114
121
|
else {
|
|
115
|
-
console.log(
|
|
122
|
+
console.log(success(`Created database '${name}'.`));
|
|
116
123
|
}
|
|
117
124
|
}));
|
|
118
125
|
dbCommand
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { existsSync, readFileSync, statSync } from 'fs';
|
|
3
|
-
import { join } from 'path';
|
|
3
|
+
import { join, resolve } from 'path';
|
|
4
4
|
import { homedir } from 'os';
|
|
5
5
|
import { LOCAL_PKG_DIR, LOCAL_ENTRY, STATE_FILE, SETTINGS_FILE, UPDATE_LOG, readState, readSettings, updatesDisabled } from '../updater/state.js';
|
|
6
6
|
import { bold, dim, success, warning, error as clrError, muted } from '../colors.js';
|
|
@@ -162,7 +162,13 @@ export const doctorCommand = new Command('doctor')
|
|
|
162
162
|
console.log(`${muted('claude code ')} installed ${yn(env.claude.installed)} · authenticated ${yn(env.claude.authenticated)}`);
|
|
163
163
|
const autostartLabel = env.relay.autostart === null ? muted('n/a') : yn(env.relay.autostart);
|
|
164
164
|
console.log(`${muted('relay ')} paired ${yn(env.relay.paired)} · running ${yn(env.relay.running)} · autostart ${autostartLabel}${env.relay.paused ? warning(' · paused') : ''}${env.relay.device ? muted(` (${env.relay.device.name})`) : ''}`);
|
|
165
|
-
|
|
165
|
+
// Codex approves project hooks interactively and stores the decision in
|
|
166
|
+
// its own opaque state - we can't verify it from here, so surface the
|
|
167
|
+
// one manual step whenever this project ships Codex hooks.
|
|
168
|
+
if (existsSync(resolve(process.cwd(), '.codex', 'hooks.json'))) {
|
|
169
|
+
console.log(`${muted('codex hooks ')} ${warning('written')} - session capture needs a one-time approval: run /hooks inside Codex in this project`);
|
|
170
|
+
}
|
|
171
|
+
console.log(`${muted('ready ')} ${env.ready ? success('yes') : warning('no - run `gipity build` (or the desktop app) to finish setup')}`);
|
|
166
172
|
// ── CLI install / update health ────────────────────────────────────
|
|
167
173
|
const state = readState();
|
|
168
174
|
const settings = readSettings();
|
package/dist/commands/init.js
CHANGED
|
@@ -4,7 +4,7 @@ import { existsSync, readFileSync } from 'fs';
|
|
|
4
4
|
import { getAccountSlug } from '../api.js';
|
|
5
5
|
import { getConfig, getConfigPath, saveConfigAt } from '../config.js';
|
|
6
6
|
import { getAuth } from '../auth.js';
|
|
7
|
-
import { slugify,
|
|
7
|
+
import { slugify, setupProjectTools, SUPPORTED_TOOLS, DEFAULT_TOOLS, DEFAULT_SYNC_IGNORE } from '../setup.js';
|
|
8
8
|
import { success, error as clrError, info, muted, bold } from '../colors.js';
|
|
9
9
|
import { confirm } from '../utils.js';
|
|
10
10
|
import { scanForAdoption, adoptCurrentDir, canAdoptCwd, formatBytes, formatCwdLabel, ADOPT_THRESHOLDS, } from '../adopt-cwd.js';
|
|
@@ -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.')
|
|
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).')
|
|
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)')
|
|
@@ -32,7 +32,8 @@ export const initCommand = new Command('init')
|
|
|
32
32
|
Examples:
|
|
33
33
|
$ gipity init Link cwd as a new project (slug = dir name).
|
|
34
34
|
$ gipity init my-app Link cwd with an explicit slug.
|
|
35
|
-
$ gipity init --for codex
|
|
35
|
+
$ gipity init --for codex AGENTS.md + Codex skills/sync hooks only.
|
|
36
|
+
$ gipity init --for grok AGENTS.md + the Gipity plugin in Grok only.
|
|
36
37
|
$ gipity init --for cursor,gemini Write only the Cursor + Gemini primers.
|
|
37
38
|
$ gipity init --for aider AGENTS.md + a read: entry in .aider.conf.yml
|
|
38
39
|
(aider auto-reads nothing, so it's opt-in).
|
|
@@ -57,10 +58,6 @@ Working with an existing Gipity project:
|
|
|
57
58
|
process.exit(1);
|
|
58
59
|
}
|
|
59
60
|
const wantsClaude = tools.some(t => t.key === 'claude');
|
|
60
|
-
const writeAllPrimers = () => {
|
|
61
|
-
for (const t of tools)
|
|
62
|
-
t.setup();
|
|
63
|
-
};
|
|
64
61
|
const primerSummary = tools.map(t => t.label).join(', ');
|
|
65
62
|
try {
|
|
66
63
|
// Check auth
|
|
@@ -77,12 +74,9 @@ Working with an existing Gipity project:
|
|
|
77
74
|
if (existsSync(resolve(cwd, '.gipity.json'))) {
|
|
78
75
|
const existing = getConfig();
|
|
79
76
|
console.log(`Already linked to ${info(`"${existing?.projectSlug ?? ''}"`)} ${muted(`(${existing?.projectGuid ?? ''})`)}`);
|
|
80
|
-
// Re-run setup in case hooks/skills are missing
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
setupClaudeHooks();
|
|
84
|
-
writeAllPrimers();
|
|
85
|
-
setupGitignore();
|
|
77
|
+
// Re-run setup in case primers/hooks/skills are missing - each tool's
|
|
78
|
+
// integration self-gates on its binary and current-version state.
|
|
79
|
+
setupProjectTools(tools);
|
|
86
80
|
// The config's ignore list was frozen at link time, so a workstation
|
|
87
81
|
// artifact introduced by a newer CLI (e.g. aider's .aider.conf.yml)
|
|
88
82
|
// would sync up as project content. Union in the current defaults.
|
|
@@ -143,7 +137,7 @@ Working with an existing Gipity project:
|
|
|
143
137
|
const sizeStr = scan.truncated ? `>${formatBytes(ADOPT_THRESHOLDS.REFUSE_BYTES)}` : formatBytes(scan.bytes);
|
|
144
138
|
const fileStr = scan.truncated ? `>${ADOPT_THRESHOLDS.REFUSE_FILES}` : `${scan.files}`;
|
|
145
139
|
console.error(clrError(`Directory has ${fileStr} files (${sizeStr}) - too large to adopt as a Gipity project.`));
|
|
146
|
-
console.error(muted('Move into a subdirectory, or use `gipity
|
|
140
|
+
console.error(muted('Move into a subdirectory, or use `gipity build` and pick "Create new project".'));
|
|
147
141
|
process.exit(1);
|
|
148
142
|
}
|
|
149
143
|
if (scan.tier === 'moderate') {
|
|
@@ -183,7 +177,7 @@ Working with an existing Gipity project:
|
|
|
183
177
|
}
|
|
184
178
|
console.log(success(`Wrote primer files: ${primerSummary}.`));
|
|
185
179
|
if (wantsClaude) {
|
|
186
|
-
console.log(success('Ready! Run
|
|
180
|
+
console.log(success('Ready! Run your coding agent here (claude, codex, grok), or `gipity build` to launch one with a picker.'));
|
|
187
181
|
// Recording happens by default (however Claude Code is launched), so
|
|
188
182
|
// say so up front - consent should be explicit, not discovered later.
|
|
189
183
|
if (opts.capture === false) {
|
|
@@ -8,8 +8,10 @@ import { resolveProjectContext } from '../config.js';
|
|
|
8
8
|
import { uploadPublicFixture, uploadCameraFeed, assertCameraFile, deleteFixture } from '../page-fixtures.js';
|
|
9
9
|
// Shown when an eval runs cleanly but returns nothing serializable. Turns a
|
|
10
10
|
// bare/opaque `null` into a deterministic, actionable nudge so the agent shapes
|
|
11
|
-
// a returnable value instead of guessing and retrying.
|
|
12
|
-
|
|
11
|
+
// a returnable value instead of guessing and retrying. (A body ending in a bare
|
|
12
|
+
// expression is auto-returned SERVER-side before this can fire — so this is the
|
|
13
|
+
// residue: block/loop endings, explicit undefined returns, DOM nodes, functions.)
|
|
14
|
+
export const EVAL_NO_VALUE_HINT = 'The eval ran but returned no JSON-serializable value. A body ending in a loop/if-block, a `return` of undefined, or a DOM node/function all serialize to null. ' +
|
|
13
15
|
'End the script with an expression — or an explicit `return` — that yields plain data, e.g. `return { label: input.value, count: items.length }` or `return JSON.stringify(payload)`.';
|
|
14
16
|
/** Normalize a raw eval result for display. The eval can come back as a useful
|
|
15
17
|
* serialized value, the literal `null`/`undefined`/empty string, or — when the
|
|
@@ -346,7 +348,9 @@ export function evalExecTimeoutMessage(result, budgetMs) {
|
|
|
346
348
|
// a trap, since --json is a real flag that changes output but still leaves the
|
|
347
349
|
// script unset, sending the agent in a loop. Capture the common guesses as
|
|
348
350
|
// hidden decoy options so the action can redirect to the positional arg exactly.
|
|
349
|
-
|
|
351
|
+
// `--action` rides along: it's the sibling `page screenshot`'s spelling of
|
|
352
|
+
// "run JS on the page", so an agent that learned it there guesses it here.
|
|
353
|
+
const JS_DECOY_FLAGS = ['--js', '--javascript', '--script', '--code', '--expr', '--eval', '--exec', '--action'];
|
|
350
354
|
// The long-tail escape hatch alongside `page inspect`'s fixed bundle: when the
|
|
351
355
|
// curated metrics don't cover what you need (computed styles, element rects,
|
|
352
356
|
// visibility, z-index stacks), eval an expression in page context and get the
|
|
@@ -360,7 +364,7 @@ const JS_DECOY_FLAGS = ['--js', '--javascript', '--script', '--code', '--expr',
|
|
|
360
364
|
export const pageEvalCommand = new Command('eval')
|
|
361
365
|
.description('Evaluate JS in a real browser on a page (DOM, computed styles, element rects; inline expr or --file script). ONE client per call - to verify realtime/presence across concurrent clients use `page test --observe` instead')
|
|
362
366
|
.argument('<url>', 'URL to load')
|
|
363
|
-
.argument('[expr]', `JavaScript to evaluate in page context (inline expression or statement body
|
|
367
|
+
.argument('[expr]', `JavaScript to evaluate in page context (inline expression or statement body; await works, and the trailing expression is returned automatically — REPL-style — so no explicit return is needed; result is JSON-serialized). Omit when using --file. Time budget: the body has ${EVAL_SCRIPT_BUDGET_MS / 1000}s to finish after page load (${EVAL_SCRIPT_BUDGET_CAMERA_MS / 1000}s with --camera) - raise it with --timeout, max ${EVAL_SCRIPT_BUDGET_MAX_MS / 1000}s.`)
|
|
364
368
|
.option('--file <path>', `Read the script body from a file instead of the inline <expr> arg (mutually exclusive). Runs as an async function body, so top-level return/await work. Same post-load budget as <expr> (--timeout).`)
|
|
365
369
|
.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], [])
|
|
366
370
|
.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. Repeat for several files (single-value so it never swallows the inline <expr>).', (val, prev) => [...prev, val], [])
|
|
@@ -372,7 +376,7 @@ export const pageEvalCommand = new Command('eval')
|
|
|
372
376
|
.option('--wait-for <selector>', `Wait until this CSS selector appears before evaluating, then evaluate (deterministic - beats guessing a --wait). Gate on the state you are ASSERTING, not just a ready flag: '#verdict:not(:empty)' returns the instant the round lands, where a fixed wait snapshots mid-sequence. Same flag on page inspect/screenshot. Max ${WAIT_FOR_MAX_MS}ms (--wait-timeout).`)
|
|
373
377
|
.option('--wait-timeout <ms>', `Max ms to wait for --wait-for before giving up (max ${WAIT_FOR_MAX_MS})`, '5000')
|
|
374
378
|
.option('--timeout <ms>', `How long the script itself may run IN the page - its own await/setTimeout pauses count (default ${EVAL_SCRIPT_BUDGET_MS}, ${EVAL_SCRIPT_BUDGET_CAMERA_MS} with --camera/--fake-media; max ${EVAL_SCRIPT_BUDGET_MAX_MS}). Raise it to trace a sequence that unfolds over time (a game round, an animation). Distinct from --wait, which only sleeps BEFORE the script.`)
|
|
375
|
-
.option('--auth', 'Evaluate signed in as you (your Gipity account), so a page behind a Sign-in-with-Gipity login is reachable. Only works for apps using Sign in with Gipity, hosted on *.gipity.ai.')
|
|
379
|
+
.option('--auth', 'Evaluate signed in as you (your Gipity account), so a page behind a Sign-in-with-Gipity login is reachable. 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.')
|
|
376
380
|
.option('--json', 'Output as JSON')
|
|
377
381
|
.action((url, exprArg, opts) => run('Page eval', async () => {
|
|
378
382
|
// A JS-intent flag guess (captured as a hidden decoy below): redirect to the
|
|
@@ -470,6 +474,9 @@ export const pageEvalCommand = new Command('eval')
|
|
|
470
474
|
// cleaned up, without shifting the fixture map the caller indexes into.
|
|
471
475
|
let camera;
|
|
472
476
|
let projectGuid;
|
|
477
|
+
// Completion-value semantics (a body ending in a bare expression returns it,
|
|
478
|
+
// REPL-style) are applied SERVER-side to every leg — one source of truth for
|
|
479
|
+
// every caller; the CLI sends the script as written.
|
|
473
480
|
let sentExpr = expr;
|
|
474
481
|
let sentSteps = steps;
|
|
475
482
|
// Validate the camera file locally first: a wrong file type should cost one
|
|
@@ -574,14 +581,18 @@ export const pageEvalCommand = new Command('eval')
|
|
|
574
581
|
if (d.slowRender) {
|
|
575
582
|
console.log(slowRenderMessage(d.slowRender.fps, { camera: !!camera, waitMs }));
|
|
576
583
|
}
|
|
577
|
-
//
|
|
578
|
-
// eval" from "--auth silently no-op'd against the anonymous
|
|
584
|
+
// Identity line on EVERY run: without it an agent can't distinguish
|
|
585
|
+
// "signed-in eval" from "--auth silently no-op'd against the anonymous
|
|
586
|
+
// page" — or, worse, assume an unflagged run carried a signed-in session.
|
|
579
587
|
if (d.auth?.requested) {
|
|
580
588
|
const who = getAuth()?.email;
|
|
581
589
|
console.log(d.auth.established
|
|
582
590
|
? `${muted('Auth:')} ${success('session established')}${who ? muted(` as ${who}`) : ''} ${muted('(what the page renders with it is app-defined)')}`
|
|
583
591
|
: `${warning('Auth: session NOT established')}${d.auth.detail ? ` — ${d.auth.detail}` : ''} ${muted('(this is the anonymous view)')}`);
|
|
584
592
|
}
|
|
593
|
+
else {
|
|
594
|
+
console.log(muted('Auth: anonymous visitor (signed out; pass --auth to run as your Gipity account)'));
|
|
595
|
+
}
|
|
585
596
|
if (camera)
|
|
586
597
|
console.log(`${muted('Camera:')} ${camera.name} ${muted('(played as the webcam feed; getUserMedia resolves)')}`);
|
|
587
598
|
if (hosted.length)
|
|
@@ -43,7 +43,7 @@ export const pageInspectCommand = new Command('inspect')
|
|
|
43
43
|
.option('--all', 'Include render-blocking, large resources, oversized images, overflow culprits, and LCP detail')
|
|
44
44
|
.option('--no-fake-media', "Inspect with NO camera or microphone present, so a getUserMedia app takes its no-device error path (use this only when that fallback IS what you're inspecting)")
|
|
45
45
|
.option('--device <name>', `Inspect as a real touch device: ${INSPECT_DEVICES.join(', ')}. Emulates touch events, mobile user-agent and DPR, so a touch-gated mobile layout is what gets inspected.`)
|
|
46
|
-
.option('--auth', 'Load the page signed in as you (your Gipity account), so pages behind a Sign-in-with-Gipity login are reachable. Only works for apps using Sign in with Gipity, hosted on *.gipity.ai.')
|
|
46
|
+
.option('--auth', 'Load the page signed in as you (your Gipity account), so pages behind a Sign-in-with-Gipity login are reachable. 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.')
|
|
47
47
|
// Hidden redirect: agents reach for `page inspect --screenshot`. We don't take
|
|
48
48
|
// an image here (`page screenshot` is the single path for that) — just point there.
|
|
49
49
|
.addOption(new Option('--screenshot [path]', 'Capture a screenshot').hideHelp())
|
|
@@ -164,14 +164,18 @@ export async function inspectPage(url, opts = {}) {
|
|
|
164
164
|
if (b.navigationIncomplete) {
|
|
165
165
|
console.log(`${warning('⚠ Navigation incomplete:')} ${b.note || 'page did not reach full load'}`);
|
|
166
166
|
}
|
|
167
|
-
//
|
|
168
|
-
// from "--auth silently no-op'd and I'm looking at the
|
|
167
|
+
// Identity line on EVERY run: without it an agent can't distinguish
|
|
168
|
+
// "signed-in render" from "--auth silently no-op'd and I'm looking at the
|
|
169
|
+
// anonymous page" — or assume an unflagged run carried a signed-in session.
|
|
169
170
|
if (b.auth?.requested) {
|
|
170
171
|
const who = getAuth()?.email;
|
|
171
172
|
console.log(b.auth.established
|
|
172
173
|
? `${muted('Auth:')} ${success('session established')}${who ? muted(` as ${who}`) : ''} ${muted('(what the page renders with it is app-defined)')}`
|
|
173
174
|
: `${warning('Auth: session NOT established')}${b.auth.detail ? ` — ${b.auth.detail}` : ''} ${muted('(this is the anonymous view)')}`);
|
|
174
175
|
}
|
|
176
|
+
else {
|
|
177
|
+
console.log(muted('Auth: anonymous visitor (signed out; pass --auth to load as your Gipity account)'));
|
|
178
|
+
}
|
|
175
179
|
console.log(`${muted('Title:')} ${b.title || '(none)'}`);
|
|
176
180
|
console.log(`${muted('Elements:')} ${b.elementCount || 0}`);
|
|
177
181
|
console.log(`${muted('Page weight:')} ${info(formatSize(b.totalBytes || 0))}`);
|
|
@@ -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
|
@@ -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;
|