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
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';
|
|
@@ -44,6 +44,11 @@ dbCommand
|
|
|
44
44
|
console.log(columns.map(c => String(row[c] ?? 'NULL')).join('\t'));
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
+
// DML with RETURNING carries both rows and a count - the rows are the
|
|
48
|
+
// table above; still name how many rows the statement touched.
|
|
49
|
+
if (res.data.affectedRows !== undefined) {
|
|
50
|
+
console.log(`Affected rows: ${res.data.affectedRows}`);
|
|
51
|
+
}
|
|
47
52
|
}
|
|
48
53
|
else if (res.data.affectedRows !== undefined) {
|
|
49
54
|
console.log(`Affected rows: ${res.data.affectedRows}`);
|
|
@@ -51,6 +56,11 @@ dbCommand
|
|
|
51
56
|
else if (res.data.results) {
|
|
52
57
|
console.log(JSON.stringify(res.data.results, null, 2));
|
|
53
58
|
}
|
|
59
|
+
else {
|
|
60
|
+
// Unknown payload shape: print it raw rather than nothing - silence
|
|
61
|
+
// here reads as "the query returned no data", which is a lie.
|
|
62
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
63
|
+
}
|
|
54
64
|
}
|
|
55
65
|
}));
|
|
56
66
|
dbCommand
|
|
@@ -106,13 +116,20 @@ dbCommand
|
|
|
106
116
|
.description('Create a database')
|
|
107
117
|
.option('--json', 'Output as JSON')
|
|
108
118
|
.action((name, opts) => run('Create', async () => {
|
|
109
|
-
//
|
|
110
|
-
|
|
119
|
+
// Direct REST create - same endpoint `db drop` uses. This used to delegate
|
|
120
|
+
// to agent chat, which burned LLM tokens and, after the cost-consent gate
|
|
121
|
+
// landed server-side, could return an empty response having created
|
|
122
|
+
// nothing. Creating a database isn't destructive, so no confirmation.
|
|
123
|
+
const config = requireConfig();
|
|
124
|
+
await post(`/projects/${config.projectGuid}/db/manage`, {
|
|
125
|
+
action: 'create',
|
|
126
|
+
name,
|
|
127
|
+
});
|
|
111
128
|
if (opts.json) {
|
|
112
|
-
console.log(JSON.stringify({
|
|
129
|
+
console.log(JSON.stringify({ success: true, name }));
|
|
113
130
|
}
|
|
114
131
|
else {
|
|
115
|
-
console.log(
|
|
132
|
+
console.log(success(`Created database '${name}'.`));
|
|
116
133
|
}
|
|
117
134
|
}));
|
|
118
135
|
dbCommand
|
package/dist/commands/deploy.js
CHANGED
|
@@ -146,6 +146,13 @@ export const deployCommand = new Command('deploy')
|
|
|
146
146
|
// has to reconstruct the URL convention or guess a subdomain.
|
|
147
147
|
if (d.url)
|
|
148
148
|
console.log(`${muted('Live:')} ${brand(d.url)}`);
|
|
149
|
+
// Functions are served from the API host, NOT the static Live URL -
|
|
150
|
+
// for an API-only app the endpoint address IS the deliverable, so name
|
|
151
|
+
// it instead of leaving it to be inferred from a template comment.
|
|
152
|
+
if (d.phases?.some(p => p.name === 'functions')) {
|
|
153
|
+
console.log(`${muted('Functions:')} POST ${brand(`${config.apiBase}/api/${config.projectGuid}/fn/<name>`)}`);
|
|
154
|
+
console.log(muted(' (same address on dev and prod; names: gipity fn list; verify the public path: gipity fn call <name> --anon)'));
|
|
155
|
+
}
|
|
149
156
|
await inspectAfter();
|
|
150
157
|
}
|
|
151
158
|
}));
|
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/fn.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { get, post, del, publicPost } from '../api.js';
|
|
3
|
+
import { getAuth } from '../auth.js';
|
|
3
4
|
import { requireConfig } from '../config.js';
|
|
4
5
|
import { error as clrError, bold, muted, success, warning } from '../colors.js';
|
|
5
6
|
import { run, printList, emitField } from '../helpers/index.js';
|
|
@@ -17,6 +18,11 @@ fnCommand
|
|
|
17
18
|
const line = `${bold(f.name)} ${muted(`v${f.version}`)} ${muted(f.auth_level)} ${muted(`timeout=${f.timeout_ms}ms`)}`;
|
|
18
19
|
return f.description ? `${line}\n${muted(f.description)}` : line;
|
|
19
20
|
});
|
|
21
|
+
// The callable address is the deliverable for API-only apps - name it here
|
|
22
|
+
// rather than leaving it to be inferred from a template comment.
|
|
23
|
+
if (!opts.json && res.data.length > 0) {
|
|
24
|
+
console.log(muted(`Endpoint: POST ${config.apiBase}/api/${config.projectGuid}/fn/<name> (same on dev and prod; public path: gipity fn call <name> --anon)`));
|
|
25
|
+
}
|
|
20
26
|
}));
|
|
21
27
|
fnCommand
|
|
22
28
|
.command('logs <name>')
|
|
@@ -29,8 +35,15 @@ fnCommand
|
|
|
29
35
|
printList(res.data, opts, 'No execution logs.', log => {
|
|
30
36
|
const dur = log.duration_ms != null ? `${log.duration_ms}ms` : '?';
|
|
31
37
|
const ts = new Date(log.created_at).toLocaleString();
|
|
32
|
-
|
|
38
|
+
// The runtime writes 'ok' | 'error' | 'limit_exceeded' - never 'success'
|
|
39
|
+
// (same dead value already fixed in `logs fn`).
|
|
40
|
+
const statusColor = log.status === 'ok' ? success : log.status === 'error' ? clrError : muted;
|
|
33
41
|
let line = `${statusColor(log.status)} ${dur} ${muted(log.trigger_type || 'http')} ${muted(ts)}`;
|
|
42
|
+
// Platform service calls made inside the invocation (notify, email,
|
|
43
|
+
// LLM, ...) - per-call outcomes live in `gipity logs app --type services`.
|
|
44
|
+
const svcCalls = log.limits_consumed?.max_service_calls;
|
|
45
|
+
if (svcCalls)
|
|
46
|
+
line += ` ${muted(`svc:${svcCalls}`)}`;
|
|
34
47
|
if (log.error_message)
|
|
35
48
|
line += `\n${clrError(`error: ${log.error_message}`)}`;
|
|
36
49
|
// Captured console.log/warn/error output for this invocation.
|
|
@@ -69,6 +82,16 @@ fnCommand
|
|
|
69
82
|
const raw = bodyArg || opts.data || '{}';
|
|
70
83
|
const body = JSON.parse(raw);
|
|
71
84
|
const path = `/api/${config.projectGuid}/fn/${encodeURIComponent(name)}`;
|
|
85
|
+
// Name the calling identity (stderr, so stdout stays parseable). Without
|
|
86
|
+
// this, a "test the anonymous path" call silently runs authenticated as
|
|
87
|
+
// the signed-in owner and the public path never gets exercised.
|
|
88
|
+
if (opts.anon) {
|
|
89
|
+
console.error(muted('Auth: anonymous visitor (the public path a signed-out user hits)'));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
const who = getAuth()?.email;
|
|
93
|
+
console.error(muted(`Auth: calling as ${who ?? 'your signed-in account'} (the owner persona; use --anon for the public visitor path)`));
|
|
94
|
+
}
|
|
72
95
|
const res = opts.anon
|
|
73
96
|
? await callAnon(config.projectGuid, name, body)
|
|
74
97
|
: await post(path, body);
|
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) {
|
package/dist/commands/logs.js
CHANGED
|
@@ -9,7 +9,20 @@ logsCommand
|
|
|
9
9
|
.command('fn <name>')
|
|
10
10
|
.description('Show function logs')
|
|
11
11
|
.option('--limit <n>', 'Max entries', '20')
|
|
12
|
-
.option('--json', 'Output as JSON')
|
|
12
|
+
.option('--json', 'Output as JSON (top-level ARRAY of invocations, newest first)')
|
|
13
|
+
.addHelpText('after', `
|
|
14
|
+
Each line: time, status (ok / error / limit_exceeded), duration, trigger, and
|
|
15
|
+
svc:N when the invocation made N platform service calls (notify, email, LLM,
|
|
16
|
+
image, ...). Captured console output is indented beneath its invocation.
|
|
17
|
+
|
|
18
|
+
--json emits a top-level ARRAY (not an object) of invocation rows:
|
|
19
|
+
[{ id, status, duration_ms, trigger_type, error_message,
|
|
20
|
+
limits_consumed: { max_queries, max_service_calls, ... }, logs: [...], created_at }]
|
|
21
|
+
|
|
22
|
+
To confirm an individual service call succeeded (and see provider, latency,
|
|
23
|
+
and its own error message), use the per-call log instead:
|
|
24
|
+
$ gipity logs app --type services
|
|
25
|
+
`)
|
|
13
26
|
.action((name, opts) => run('Logs', async () => {
|
|
14
27
|
const config = requireConfig();
|
|
15
28
|
const limit = parseInt(opts.limit, 10) || 20;
|
|
@@ -32,8 +45,14 @@ logsCommand
|
|
|
32
45
|
const statusColor = log.status === 'ok' ? success : log.status === 'error' ? clrError : warning;
|
|
33
46
|
const status = statusColor(log.status.padEnd(8));
|
|
34
47
|
const trigger = muted(log.trigger_type.padEnd(8));
|
|
48
|
+
// Service calls made inside the invocation (notify, email, LLM, ...) are
|
|
49
|
+
// otherwise invisible here - surface the count so "did my send happen?"
|
|
50
|
+
// doesn't require spelunking --json limits counters. Per-call outcomes
|
|
51
|
+
// live in `gipity logs app --type services`.
|
|
52
|
+
const svcCalls = log.limits_consumed?.max_service_calls;
|
|
53
|
+
const svc = svcCalls ? ` ${muted(`svc:${svcCalls}`)}` : '';
|
|
35
54
|
const err = log.error_message ? ` ${clrError(`"${log.error_message}"`)}` : '';
|
|
36
|
-
console.log(`${muted(time)} ${status} ${dur} ${trigger}${err}`);
|
|
55
|
+
console.log(`${muted(time)} ${status} ${dur} ${trigger}${svc}${err}`);
|
|
37
56
|
// Captured console.log/warn/error output for this invocation, indented
|
|
38
57
|
// under its summary line. Empty for calls that printed nothing.
|
|
39
58
|
for (const line of log.logs ?? []) {
|
|
@@ -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
|
|
@@ -272,7 +274,8 @@ export function slowRenderMessage(fps, o) {
|
|
|
272
274
|
return `${warning('⚠ Slow render:')} page painted at ${fps} fps. `
|
|
273
275
|
+ `Waiting on real time (setTimeout) advances animation/physics time far slower than it looks — `
|
|
274
276
|
+ `assertions after a wall-clock wait can report a false negative. `
|
|
275
|
-
+ `Step the app's own loop deterministically instead (3D templates: ${bold('core.advance(seconds)')}
|
|
277
|
+
+ `Step the app's own loop deterministically instead (3D templates: ${bold('core.advance(seconds)')}; `
|
|
278
|
+
+ `2D/Phaser template: ${bold("(await import('./js/config.js')).advance(seconds)")}).`;
|
|
276
279
|
}
|
|
277
280
|
/** Poll the async eval job until it finishes. Eval runs server-side as a
|
|
278
281
|
* short-lived job (so a long --wait can't trip the gateway idle timeout);
|
|
@@ -346,7 +349,9 @@ export function evalExecTimeoutMessage(result, budgetMs) {
|
|
|
346
349
|
// a trap, since --json is a real flag that changes output but still leaves the
|
|
347
350
|
// script unset, sending the agent in a loop. Capture the common guesses as
|
|
348
351
|
// hidden decoy options so the action can redirect to the positional arg exactly.
|
|
349
|
-
|
|
352
|
+
// `--action` rides along: it's the sibling `page screenshot`'s spelling of
|
|
353
|
+
// "run JS on the page", so an agent that learned it there guesses it here.
|
|
354
|
+
const JS_DECOY_FLAGS = ['--js', '--javascript', '--script', '--code', '--expr', '--eval', '--exec', '--action'];
|
|
350
355
|
// The long-tail escape hatch alongside `page inspect`'s fixed bundle: when the
|
|
351
356
|
// curated metrics don't cover what you need (computed styles, element rects,
|
|
352
357
|
// visibility, z-index stacks), eval an expression in page context and get the
|
|
@@ -360,7 +365,7 @@ const JS_DECOY_FLAGS = ['--js', '--javascript', '--script', '--code', '--expr',
|
|
|
360
365
|
export const pageEvalCommand = new Command('eval')
|
|
361
366
|
.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
367
|
.argument('<url>', 'URL to load')
|
|
363
|
-
.argument('[expr]', `JavaScript to evaluate in page context (inline expression or statement body
|
|
368
|
+
.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
369
|
.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
370
|
.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
371
|
.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 +377,7 @@ export const pageEvalCommand = new Command('eval')
|
|
|
372
377
|
.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
378
|
.option('--wait-timeout <ms>', `Max ms to wait for --wait-for before giving up (max ${WAIT_FOR_MAX_MS})`, '5000')
|
|
374
379
|
.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.')
|
|
380
|
+
.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
381
|
.option('--json', 'Output as JSON')
|
|
377
382
|
.action((url, exprArg, opts) => run('Page eval', async () => {
|
|
378
383
|
// A JS-intent flag guess (captured as a hidden decoy below): redirect to the
|
|
@@ -470,6 +475,9 @@ export const pageEvalCommand = new Command('eval')
|
|
|
470
475
|
// cleaned up, without shifting the fixture map the caller indexes into.
|
|
471
476
|
let camera;
|
|
472
477
|
let projectGuid;
|
|
478
|
+
// Completion-value semantics (a body ending in a bare expression returns it,
|
|
479
|
+
// REPL-style) are applied SERVER-side to every leg — one source of truth for
|
|
480
|
+
// every caller; the CLI sends the script as written.
|
|
473
481
|
let sentExpr = expr;
|
|
474
482
|
let sentSteps = steps;
|
|
475
483
|
// Validate the camera file locally first: a wrong file type should cost one
|
|
@@ -563,7 +571,10 @@ export const pageEvalCommand = new Command('eval')
|
|
|
563
571
|
}));
|
|
564
572
|
return;
|
|
565
573
|
}
|
|
566
|
-
|
|
574
|
+
// Context echo (which URL, which script) goes to stderr: the caller
|
|
575
|
+
// already knows what it sent, and keeping stdout = qualifiers + result
|
|
576
|
+
// means `2>/dev/null` (or any stdout capture) yields just the answer.
|
|
577
|
+
console.error(`${brand('Eval')} ${bold(d.url || url)}`);
|
|
567
578
|
if (d.navigationIncomplete) {
|
|
568
579
|
console.log(`${warning('⚠ Navigation incomplete:')} ${d.note || 'page did not reach full load'}`);
|
|
569
580
|
}
|
|
@@ -574,19 +585,23 @@ export const pageEvalCommand = new Command('eval')
|
|
|
574
585
|
if (d.slowRender) {
|
|
575
586
|
console.log(slowRenderMessage(d.slowRender.fps, { camera: !!camera, waitMs }));
|
|
576
587
|
}
|
|
577
|
-
//
|
|
578
|
-
// eval" from "--auth silently no-op'd against the anonymous
|
|
588
|
+
// Identity line on EVERY run: without it an agent can't distinguish
|
|
589
|
+
// "signed-in eval" from "--auth silently no-op'd against the anonymous
|
|
590
|
+
// page" — or, worse, assume an unflagged run carried a signed-in session.
|
|
579
591
|
if (d.auth?.requested) {
|
|
580
592
|
const who = getAuth()?.email;
|
|
581
593
|
console.log(d.auth.established
|
|
582
594
|
? `${muted('Auth:')} ${success('session established')}${who ? muted(` as ${who}`) : ''} ${muted('(what the page renders with it is app-defined)')}`
|
|
583
595
|
: `${warning('Auth: session NOT established')}${d.auth.detail ? ` — ${d.auth.detail}` : ''} ${muted('(this is the anonymous view)')}`);
|
|
584
596
|
}
|
|
597
|
+
else {
|
|
598
|
+
console.log(muted('Auth: anonymous visitor (signed out; pass --auth to run as your Gipity account)'));
|
|
599
|
+
}
|
|
585
600
|
if (camera)
|
|
586
601
|
console.log(`${muted('Camera:')} ${camera.name} ${muted('(played as the webcam feed; getUserMedia resolves)')}`);
|
|
587
602
|
if (hosted.length)
|
|
588
603
|
console.log(`${muted('Fixtures:')} ${hosted.map((h) => h.name).join(', ')}`);
|
|
589
|
-
console.
|
|
604
|
+
console.error(opts.file ? `${muted('Script:')} ${opts.file}` : `${muted('Expression:')} ${summarizeExpr(expr)}`);
|
|
590
605
|
console.log(`\n${result.trim() ? result : muted('(empty result)')}`);
|
|
591
606
|
if (noValue)
|
|
592
607
|
console.log(muted(`\n${EVAL_NO_VALUE_HINT}`));
|
|
@@ -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))}`);
|