gipity 1.0.425 → 1.0.427
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/banner.js +2 -2
- package/dist/bug-queue.js +77 -0
- package/dist/colors.js +2 -2
- package/dist/commands/add.js +6 -1
- package/dist/commands/bug.js +28 -5
- package/dist/commands/claude.js +1 -1
- package/dist/commands/credits.js +46 -0
- package/dist/commands/doctor.js +41 -2
- package/dist/commands/email.js +77 -8
- package/dist/commands/generate.js +27 -1
- package/dist/commands/login.js +7 -0
- package/dist/commands/logs.js +4 -1
- package/dist/commands/page-eval.js +10 -0
- package/dist/commands/page-screenshot.js +16 -0
- package/dist/commands/sandbox.js +23 -5
- package/dist/commands/status.js +14 -0
- package/dist/commands/workflow.js +3 -1
- package/dist/index.js +26407 -969
- package/dist/knowledge.js +2 -2
- package/dist/login-flow.js +6 -1
- package/dist/relay/daemon.js +345 -14
- package/dist/relay/session-pool.js +299 -0
- package/dist/relay/stream-json.js +21 -4
- package/dist/sync.js +61 -13
- package/package.json +3 -2
package/dist/banner.js
CHANGED
|
@@ -52,12 +52,12 @@ const INFRASTRUCTURE = [
|
|
|
52
52
|
'Compute', 'CDN', 'Storage', 'Inference', 'Hosting',
|
|
53
53
|
'Deploy', 'Uploads', 'Rollback',
|
|
54
54
|
];
|
|
55
|
-
// ── Egg color palette (shared) - gradient built around #
|
|
55
|
+
// ── Egg color palette (shared) - gradient built around #FEA60B ───────
|
|
56
56
|
// Colors route through fg() so they downgrade (256 / 16 / none) on terminals
|
|
57
57
|
// without truecolor support instead of rendering as misparsed garbage.
|
|
58
58
|
const _hi = fg(255, 215, 80); // golden highlight
|
|
59
59
|
const _lt = fg(254, 190, 45); // light
|
|
60
|
-
const _br = fg(254, 166,
|
|
60
|
+
const _br = fg(254, 166, 11); // base - #FEA60B
|
|
61
61
|
const _md = fg(218, 112, 8); // medium-dark
|
|
62
62
|
const _sh = fg(170, 68, 4); // shadow
|
|
63
63
|
// Wobbly egg - asymmetric left/right, organic feel (8 rows)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local queue for `gipity bug report` submissions that couldn't be delivered
|
|
3
|
+
* immediately (no network, session expired, server hiccup). The report is
|
|
4
|
+
* the one artifact that must survive the platform being the thing that's
|
|
5
|
+
* broken - a bug report failing to file because the session that just broke
|
|
6
|
+
* is also required to file it would silently lose the report. Queued entries
|
|
7
|
+
* are flushed opportunistically wherever there's a good "we're reconnected"
|
|
8
|
+
* signal: the next successful login, the top of the next `bug report`
|
|
9
|
+
* invocation, and `gipity status` when it finds a valid, unexpired session.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
import { homedir } from 'os';
|
|
14
|
+
import { post } from './api.js';
|
|
15
|
+
const QUEUE_DIR = join(process.env.GIPITY_DIR || join(homedir(), '.gipity'), 'bug-queue');
|
|
16
|
+
/** Persist a report that failed to submit so it survives to the next attempt. */
|
|
17
|
+
export function queueBugReport(report) {
|
|
18
|
+
mkdirSync(QUEUE_DIR, { recursive: true, mode: 0o700 });
|
|
19
|
+
const file = join(QUEUE_DIR, `${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json`);
|
|
20
|
+
writeFileSync(file, JSON.stringify(report, null, 2), { mode: 0o600 });
|
|
21
|
+
}
|
|
22
|
+
/** True when a submit failure is worth queuing for later delivery: the
|
|
23
|
+
* network was unreachable, the request timed out, the session/token was
|
|
24
|
+
* rejected, or the server had a transient failure. A validation error
|
|
25
|
+
* (bad category, missing field) would fail identically on retry, so those
|
|
26
|
+
* surface immediately instead of queuing silently forever. */
|
|
27
|
+
export function isRetryableFailure(err) {
|
|
28
|
+
if (err?.name === 'ApiError') {
|
|
29
|
+
return err.statusCode === 401 || err.statusCode === 408 || err.statusCode >= 500;
|
|
30
|
+
}
|
|
31
|
+
// fetch()-level network errors and the "Not authenticated" pre-check both
|
|
32
|
+
// have no statusCode - both mean "can't reach/use the platform right now".
|
|
33
|
+
return typeof err?.statusCode !== 'number';
|
|
34
|
+
}
|
|
35
|
+
/** Best-effort delivery of every queued report. Never throws - called
|
|
36
|
+
* opportunistically and a flush failure must not block the caller's actual
|
|
37
|
+
* command. Returns the count successfully delivered. */
|
|
38
|
+
export async function flushBugQueue() {
|
|
39
|
+
if (!existsSync(QUEUE_DIR))
|
|
40
|
+
return 0;
|
|
41
|
+
let delivered = 0;
|
|
42
|
+
for (const file of readdirSync(QUEUE_DIR)) {
|
|
43
|
+
if (!file.endsWith('.json'))
|
|
44
|
+
continue;
|
|
45
|
+
const path = join(QUEUE_DIR, file);
|
|
46
|
+
let report;
|
|
47
|
+
try {
|
|
48
|
+
report = JSON.parse(readFileSync(path, 'utf-8'));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
try {
|
|
52
|
+
unlinkSync(path);
|
|
53
|
+
}
|
|
54
|
+
catch { /* already gone */ }
|
|
55
|
+
continue; // corrupt entry - retrying it can never succeed
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
await post(`/api/${report.projectGuid}/services/bug-report/submit`, {
|
|
59
|
+
category: report.category,
|
|
60
|
+
severity: report.severity,
|
|
61
|
+
summary: report.summary,
|
|
62
|
+
detail: report.detail,
|
|
63
|
+
});
|
|
64
|
+
unlinkSync(path);
|
|
65
|
+
delivered++;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Still undeliverable - leave it queued for the next attempt. A queued
|
|
69
|
+
// report was only ever retryable at queue time (isRetryableFailure gates
|
|
70
|
+
// queuing); if the server later hard-rejects it permanently (e.g. the
|
|
71
|
+
// project was deleted), it sticks and retries forever. Accepted given
|
|
72
|
+
// bug-report volume is tiny - better than silently dropping it.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return delivered;
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=bug-queue.js.map
|
package/dist/colors.js
CHANGED
|
@@ -145,7 +145,7 @@ export const underline = makeStyle(4, 24);
|
|
|
145
145
|
// ── Gipity platform palette ────────────────────────────────────────────
|
|
146
146
|
// Colors sourced from platform/client/src/css/styles.css and
|
|
147
147
|
// platform/apps/gipitsm/src/css/tokens.css
|
|
148
|
-
export const brand = makeFg(254, 166,
|
|
148
|
+
export const brand = makeFg(254, 166, 11, bold); // Gipity orange #fea60b (→ bold on 16-color; no orange in palette)
|
|
149
149
|
export const error = makeFg(239, 68, 68); // #ef4444
|
|
150
150
|
export const warning = makeFg(245, 158, 11); // #f59e0b
|
|
151
151
|
export const success = makeFg(34, 197, 94); // #22c55e
|
|
@@ -156,7 +156,7 @@ export const faint = makeFg(111, 111, 120); // #6f6f78
|
|
|
156
156
|
export const accent = makeFg(178, 186, 250); // Light blue-purple
|
|
157
157
|
export const cyan = makeFg(9, 146, 179); // Teal
|
|
158
158
|
// ── Background variants ────────────────────────────────────────────────
|
|
159
|
-
export const bgBrand = makeBg(
|
|
159
|
+
export const bgBrand = makeBg(254, 166, 11);
|
|
160
160
|
export const bgError = makeBg(239, 68, 68);
|
|
161
161
|
export const bgSuccess = makeBg(34, 197, 94);
|
|
162
162
|
// ── Convenience combinators ────────────────────────────────────────────
|
package/dist/commands/add.js
CHANGED
|
@@ -209,8 +209,13 @@ export const addCommand = new Command('add')
|
|
|
209
209
|
// The spinner is TTY-only, so piped/agent transcripts would otherwise show
|
|
210
210
|
// a silent multi-second gap (favicon generation on a cold cache can take
|
|
211
211
|
// ~10s, cli#117) - leave one stderr marker so the wait reads as work.
|
|
212
|
+
// Favicons are generated for templates only, so don't claim them on a kit
|
|
213
|
+
// install (kits are quick; the slow first-add path is template scaffolding).
|
|
212
214
|
if (!opts.json && !process.stdout.isTTY) {
|
|
213
|
-
|
|
215
|
+
const isKit = KITS.some(k => k.key === name);
|
|
216
|
+
console.error(muted(isKit
|
|
217
|
+
? 'Installing kit (server-side install pipeline)...'
|
|
218
|
+
: 'Installing (server writes files + generates favicons; first add for a title can take ~10s)...'));
|
|
214
219
|
}
|
|
215
220
|
const doAdd = () => post(`/projects/${config.projectGuid}/add`, body);
|
|
216
221
|
const res = opts.json
|
package/dist/commands/bug.js
CHANGED
|
@@ -3,6 +3,7 @@ import { get, post } from '../api.js';
|
|
|
3
3
|
import { resolveProjectContext } from '../config.js';
|
|
4
4
|
import { bold, muted, success, warning } from '../colors.js';
|
|
5
5
|
import { run } from '../helpers/index.js';
|
|
6
|
+
import { flushBugQueue, isRetryableFailure, queueBugReport } from '../bug-queue.js';
|
|
6
7
|
// Kept in sync with @easyclaw/shared BUG_REPORT_CATEGORIES / _SEVERITIES (the CLI
|
|
7
8
|
// package can't import the server's shared package). The server validates
|
|
8
9
|
// authoritatively; these are for --help and a friendly client-side check.
|
|
@@ -38,12 +39,34 @@ bugCommand
|
|
|
38
39
|
throw new Error('--summary must be 7 words or fewer');
|
|
39
40
|
}
|
|
40
41
|
const { config } = await resolveProjectContext({ projectOverride: opts.project });
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
// Best-effort: deliver anything stranded by an earlier outage/session-expiry
|
|
43
|
+
// before filing the new one, so a single working connection clears the backlog.
|
|
44
|
+
const delivered = await flushBugQueue().catch(() => 0);
|
|
45
|
+
if (delivered > 0 && !opts.json) {
|
|
46
|
+
console.log(muted(`Delivered ${delivered} previously queued bug report${delivered === 1 ? '' : 's'}.`));
|
|
47
|
+
}
|
|
48
|
+
const payload = { category, severity, summary: opts.summary, detail: opts.detail };
|
|
49
|
+
try {
|
|
50
|
+
const res = await post(`/api/${config.projectGuid}/services/bug-report/submit`, payload);
|
|
51
|
+
if (opts.json) {
|
|
52
|
+
console.log(JSON.stringify(res.data));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
console.log(success(`✓ Bug report filed (${res.data.report_guid}) — queued for triage.`));
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
// The report itself is the record of "the platform broke" - don't lose it
|
|
59
|
+
// just because the breakage (dead session, no network) also blocks filing it.
|
|
60
|
+
if (!isRetryableFailure(err))
|
|
61
|
+
throw err;
|
|
62
|
+
queueBugReport({ projectGuid: config.projectGuid, ...payload });
|
|
63
|
+
if (opts.json) {
|
|
64
|
+
console.log(JSON.stringify({ queued: true }));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
console.log(warning('Bug report saved locally - no connection or session expired.'));
|
|
68
|
+
console.log(muted('It will be delivered automatically next time you log in or file another report.'));
|
|
45
69
|
}
|
|
46
|
-
console.log(success(`✓ Bug report filed (${res.data.report_guid}) — queued for triage.`));
|
|
47
70
|
}));
|
|
48
71
|
bugCommand
|
|
49
72
|
.command('retract <id>')
|
package/dist/commands/claude.js
CHANGED
|
@@ -71,7 +71,7 @@ import { getProjectsRoot } from '../relay/paths.js';
|
|
|
71
71
|
*
|
|
72
72
|
* Returns a best-effort local fallback if the API call fails (offline,
|
|
73
73
|
* auth missing, etc.) so the prompt still builds. */
|
|
74
|
-
async function fetchProjectStats(projectGuid, cwd) {
|
|
74
|
+
export async function fetchProjectStats(projectGuid, cwd) {
|
|
75
75
|
if (projectGuid) {
|
|
76
76
|
try {
|
|
77
77
|
const res = await get(`/projects/${encodeURIComponent(projectGuid)}/files/stats`);
|
package/dist/commands/credits.js
CHANGED
|
@@ -133,6 +133,7 @@ export const creditsCommand = new Command('credits')
|
|
|
133
133
|
}
|
|
134
134
|
else {
|
|
135
135
|
console.log(dim('Need more credits? Run `gipity credits list` to see credit packs, then `gipity credits buy <pack>`.'));
|
|
136
|
+
console.log(dim('Manage or cancel your subscription with `gipity credits manage`.'));
|
|
136
137
|
}
|
|
137
138
|
}));
|
|
138
139
|
// `credits list` compares every plan (with full limits) and the available
|
|
@@ -289,6 +290,51 @@ creditsCommand
|
|
|
289
290
|
throw err;
|
|
290
291
|
}
|
|
291
292
|
}));
|
|
293
|
+
// `credits manage` is the ONE manage/cancel command: it mints a Stripe
|
|
294
|
+
// Customer Portal link (cancel at period end, renew, update card, invoices).
|
|
295
|
+
// Like `buy`, the link is always printed so an agent can relay it; --open
|
|
296
|
+
// also launches a browser. Falls back to the pricing page on error.
|
|
297
|
+
creditsCommand
|
|
298
|
+
.command('manage')
|
|
299
|
+
.alias('cancel')
|
|
300
|
+
.description('Manage or cancel your subscription (opens the Stripe billing portal)')
|
|
301
|
+
.option('--open', 'Open the billing portal in your browser')
|
|
302
|
+
.option('--json', 'Output the portal URL as JSON')
|
|
303
|
+
// optsWithGlobals(): the parent `credits` command also declares --json, which
|
|
304
|
+
// commander binds to the parent - read merged opts so --json/--open are seen here.
|
|
305
|
+
.action((_opts, cmd) => run('Manage', async () => {
|
|
306
|
+
const opts = cmd.optsWithGlobals();
|
|
307
|
+
try {
|
|
308
|
+
const res = await post('/credits/portal', {});
|
|
309
|
+
const url = res.data.portalUrl;
|
|
310
|
+
if (opts.json) {
|
|
311
|
+
console.log(JSON.stringify({ portalUrl: url }));
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
console.log(` ${bold('Billing portal:')} ${success(url)}`);
|
|
315
|
+
console.log('');
|
|
316
|
+
console.log(dim('Open the link to cancel, renew, update your card, or view invoices. Cancelling takes effect at the end of the billing period — you keep your credits.'));
|
|
317
|
+
if (opts.open)
|
|
318
|
+
openInBrowser(url);
|
|
319
|
+
}
|
|
320
|
+
catch (err) {
|
|
321
|
+
// No billing account / payments not configured: point at the pricing page
|
|
322
|
+
// so the user is never dead-ended.
|
|
323
|
+
if (err instanceof ApiError) {
|
|
324
|
+
if (opts.json) {
|
|
325
|
+
console.log(JSON.stringify({ error: err.message, portalUrl: null, fallbackUrl: PRICING_URL }));
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
console.log(muted(`Couldn't open the billing portal (${err.message}).`));
|
|
329
|
+
console.log(`Manage your plan at ${brand(PRICING_URL)}`);
|
|
330
|
+
}
|
|
331
|
+
if (opts.open)
|
|
332
|
+
openInBrowser(PRICING_URL);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
throw err;
|
|
336
|
+
}
|
|
337
|
+
}));
|
|
292
338
|
creditsCommand
|
|
293
339
|
.command('usage')
|
|
294
340
|
.description('Show recent usage')
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { existsSync, readFileSync, statSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
+
import { homedir } from 'os';
|
|
4
5
|
import { LOCAL_PKG_DIR, LOCAL_ENTRY, STATE_FILE, SETTINGS_FILE, UPDATE_LOG, readState, readSettings, updatesDisabled } from '../updater/state.js';
|
|
5
6
|
import { bold, dim, success, warning, error as clrError, muted } from '../colors.js';
|
|
6
7
|
import { getAuth, sessionExpired } from '../auth.js';
|
|
@@ -9,6 +10,36 @@ import * as relayState from '../relay/state.js';
|
|
|
9
10
|
import { planFor, UnsupportedPlatformError } from '../relay/installers.js';
|
|
10
11
|
import { resolveCliPath } from '../relay/setup.js';
|
|
11
12
|
const NODE_MIN_MAJOR = 18;
|
|
13
|
+
/**
|
|
14
|
+
* If the `node` running this CLI lives under a version-manager install tree
|
|
15
|
+
* (nvm/fnm/asdf/volta), return the manager's name; otherwise null.
|
|
16
|
+
*
|
|
17
|
+
* Why it matters: `npm install -g gipity` drops the `gipity` binary in the
|
|
18
|
+
* *active node version's* bin dir. With a lazily-activated manager (nvm's
|
|
19
|
+
* default setup), that dir isn't on a fresh shell's PATH until the manager
|
|
20
|
+
* initializes, so a new terminal reports `gipity: command not found` and users
|
|
21
|
+
* think the install failed. (Plugin hooks used to break the same way; the
|
|
22
|
+
* plugin's sh launcher now resolves node itself, so this is only about the
|
|
23
|
+
* `gipity` binary's own discoverability.) `gipity doctor` surfaces it with a
|
|
24
|
+
* remediation instead of leaving it a mystery.
|
|
25
|
+
*/
|
|
26
|
+
export function versionManagerNode(execPath, env = process.env, home = homedir()) {
|
|
27
|
+
const norm = (s) => s.replace(/\\/g, '/').replace(/\/+$/, '') + '/';
|
|
28
|
+
const p = norm(execPath);
|
|
29
|
+
const h = norm(home);
|
|
30
|
+
const roots = [
|
|
31
|
+
['nvm', env.NVM_DIR ? norm(env.NVM_DIR) : `${h}.nvm/`],
|
|
32
|
+
['fnm', env.FNM_DIR ? norm(env.FNM_DIR) : `${h}.fnm/`],
|
|
33
|
+
['fnm', `${h}.local/share/fnm/`],
|
|
34
|
+
['asdf', env.ASDF_DIR ? norm(env.ASDF_DIR) : `${h}.asdf/`],
|
|
35
|
+
['volta', env.VOLTA_HOME ? norm(env.VOLTA_HOME) : `${h}.volta/`],
|
|
36
|
+
];
|
|
37
|
+
for (const [name, root] of roots) {
|
|
38
|
+
if (p.startsWith(root))
|
|
39
|
+
return name;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
12
43
|
function localVersion() {
|
|
13
44
|
const pkgPath = join(LOCAL_PKG_DIR, 'package.json');
|
|
14
45
|
if (!existsSync(pkgPath))
|
|
@@ -68,7 +99,11 @@ export function gatherEnv(opts = {}) {
|
|
|
68
99
|
const auth = getAuth();
|
|
69
100
|
const expired = auth ? sessionExpired() : false;
|
|
70
101
|
const device = relayState.getDevice();
|
|
71
|
-
const node = {
|
|
102
|
+
const node = {
|
|
103
|
+
ok: nodeMajor >= NODE_MIN_MAJOR,
|
|
104
|
+
version: process.versions.node,
|
|
105
|
+
version_manager: versionManagerNode(process.execPath),
|
|
106
|
+
};
|
|
72
107
|
const gipity = {
|
|
73
108
|
installed: true, // we're running it
|
|
74
109
|
version: shimVersion(),
|
|
@@ -118,7 +153,11 @@ export const doctorCommand = new Command('doctor')
|
|
|
118
153
|
console.log(bold('Gipity - doctor'));
|
|
119
154
|
console.log('');
|
|
120
155
|
console.log(bold('Environment'));
|
|
121
|
-
console.log(`${muted('node ')} ${env.node.version} ${env.node.ok ? success('✓') : clrError(`(need ${NODE_MIN_MAJOR}+)`)}`);
|
|
156
|
+
console.log(`${muted('node ')} ${env.node.version} ${env.node.ok ? success('✓') : clrError(`(need ${NODE_MIN_MAJOR}+)`)}${env.node.version_manager ? muted(` (via ${env.node.version_manager})`) : ''}`);
|
|
157
|
+
if (env.node.version_manager) {
|
|
158
|
+
console.log(`${muted(' ')} ${warning(`node is managed by ${env.node.version_manager}`)} - if a fresh terminal reports \`gipity: command not found\`, its bin dir isn't on your`);
|
|
159
|
+
console.log(`${muted(' ')} ${dim(`login PATH yet. Ensure ${env.node.version_manager} auto-activates in your shell rc, set it as the default version, or symlink node + gipity onto your base PATH.`)}`);
|
|
160
|
+
}
|
|
122
161
|
console.log(`${muted('gipity login ')} ${env.gipity.logged_in ? success(`logged in as ${env.gipity.email}`) : (env.gipity.session_expired ? warning(`session expired (${env.gipity.email})`) : warning('not logged in'))}`);
|
|
123
162
|
console.log(`${muted('claude code ')} installed ${yn(env.claude.installed)} · authenticated ${yn(env.claude.authenticated)}`);
|
|
124
163
|
const autostartLabel = env.relay.autostart === null ? muted('n/a') : yn(env.relay.autostart);
|
package/dist/commands/email.js
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { post } from '../api.js';
|
|
2
|
+
import { get, post } from '../api.js';
|
|
3
3
|
import { resolveProjectContext } from '../config.js';
|
|
4
|
-
import { error as clrError, success } from '../colors.js';
|
|
4
|
+
import { error as clrError, success, bold, muted, warning } from '../colors.js';
|
|
5
|
+
import { run } from '../helpers/index.js';
|
|
5
6
|
/** Commander collector: build an array from repeated flags (--to a --to b). */
|
|
6
7
|
function collect(value, prev) {
|
|
7
8
|
return [...prev, value];
|
|
8
9
|
}
|
|
10
|
+
// `email` is a command GROUP. Agent email is `email send`; the deployed app's
|
|
11
|
+
// email() service is exercised via `email test` / `email log`. (Keeping agent
|
|
12
|
+
// send under `send` rather than a bare-`email` default action avoids a
|
|
13
|
+
// parent/subcommand `--subject` option collision — and matches the `email`
|
|
14
|
+
// skill doc, which documents the CLI verb as `gipity email send`.)
|
|
9
15
|
export const emailCommand = new Command('email')
|
|
10
|
-
.description(
|
|
11
|
-
|
|
16
|
+
.description("Send email as the agent (send), or test/inspect a deployed app's email() sends (test/log)");
|
|
17
|
+
// --- gipity email send ---
|
|
18
|
+
// Agent email from gipity@gipity.ai (omit --to to self-send).
|
|
19
|
+
emailCommand
|
|
20
|
+
.command('send')
|
|
21
|
+
.description('Send an email as the agent (from gipity@gipity.ai; omit --to to self-send)')
|
|
12
22
|
.requiredOption('--subject <subject>', 'Email subject')
|
|
13
23
|
.requiredOption('--body <body>', 'Email body (plain text)')
|
|
14
24
|
.option('--to <email>', 'Recipient (repeatable; omit for self-send)', collect, [])
|
|
@@ -20,10 +30,7 @@ export const emailCommand = new Command('email')
|
|
|
20
30
|
.action(async (opts) => {
|
|
21
31
|
try {
|
|
22
32
|
await resolveProjectContext();
|
|
23
|
-
const payload = {
|
|
24
|
-
subject: opts.subject,
|
|
25
|
-
body: opts.body,
|
|
26
|
-
};
|
|
33
|
+
const payload = { subject: opts.subject, body: opts.body };
|
|
27
34
|
if (opts.to.length)
|
|
28
35
|
payload.to = opts.to;
|
|
29
36
|
if (opts.cc.length)
|
|
@@ -50,4 +57,66 @@ export const emailCommand = new Command('email')
|
|
|
50
57
|
process.exit(1);
|
|
51
58
|
}
|
|
52
59
|
});
|
|
60
|
+
// --- gipity email test <to> ---
|
|
61
|
+
// Exercises the deployed app's email() send path (platform-brokered, owner-billed)
|
|
62
|
+
// so the agent can verify email works after deploy — the parallel of `notify test`.
|
|
63
|
+
emailCommand
|
|
64
|
+
.command('test')
|
|
65
|
+
.description("Send a test email through your app's email() path (owner-billed)")
|
|
66
|
+
.argument('<to>', 'Recipient address')
|
|
67
|
+
.option('--subject <text>', 'Subject', 'Test email from Gipity')
|
|
68
|
+
.option('--text <text>', 'Body text', 'This is a test email sent through your app\'s email() service. ✉️')
|
|
69
|
+
.option('--reply-to <email>', 'Reply-To address')
|
|
70
|
+
.option('--from-name <name>', 'Sender display name (address stays gipity@gipity.ai)')
|
|
71
|
+
.option('--project <guid-or-slug>', 'Target a specific project instead of cwd / Home')
|
|
72
|
+
.option('--json', 'Output raw JSON')
|
|
73
|
+
.action((to, opts) => run('Email', async () => {
|
|
74
|
+
const { config } = await resolveProjectContext({ projectOverride: opts.project });
|
|
75
|
+
const payload = { to, subject: opts.subject, text: opts.text };
|
|
76
|
+
if (opts.replyTo)
|
|
77
|
+
payload.replyTo = opts.replyTo;
|
|
78
|
+
if (opts.fromName)
|
|
79
|
+
payload.fromName = opts.fromName;
|
|
80
|
+
const res = await post(`/api/${config.projectGuid}/services/email/send`, payload);
|
|
81
|
+
if (opts.json) {
|
|
82
|
+
console.log(JSON.stringify(res.data));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const { sent, skipped, results } = res.data;
|
|
86
|
+
if (sent > 0)
|
|
87
|
+
console.log(success(`✓ Sent to ${sent} recipient${sent === 1 ? '' : 's'}.`));
|
|
88
|
+
else
|
|
89
|
+
console.log(warning(`Nothing sent (${skipped} skipped).`));
|
|
90
|
+
for (const r of results)
|
|
91
|
+
console.log(muted(` ${r.to} — ${r.status}`));
|
|
92
|
+
}));
|
|
93
|
+
// --- gipity email log ---
|
|
94
|
+
// Recent email() sends from the credits ledger (operation = email_send).
|
|
95
|
+
emailCommand
|
|
96
|
+
.command('log')
|
|
97
|
+
.description('Recent email() sends from your app')
|
|
98
|
+
.option('--range <range>', 'Time range (24h, 7d, 30d)', '7d')
|
|
99
|
+
.option('--limit <n>', 'Max rows', '50')
|
|
100
|
+
.option('--project <guid-or-slug>', 'Target a specific project instead of cwd / Home')
|
|
101
|
+
.option('--json', 'Output raw JSON')
|
|
102
|
+
.action((opts) => run('Email', async () => {
|
|
103
|
+
const { config } = await resolveProjectContext({ projectOverride: opts.project });
|
|
104
|
+
const res = await get(`/account/logs/credits?operations=email_send&app_guid=${config.projectGuid}&range=${encodeURIComponent(opts.range)}&limit=${encodeURIComponent(opts.limit)}`);
|
|
105
|
+
if (opts.json) {
|
|
106
|
+
console.log(JSON.stringify(res.data));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const { totals, items } = res.data;
|
|
110
|
+
if (!items.length) {
|
|
111
|
+
console.log(muted('No email() sends in this range.'));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
console.log(bold(`${totals.n} send${totals.n === 1 ? '' : 's'} · ${totals.credits} credit${totals.credits === 1 ? '' : 's'}`));
|
|
115
|
+
for (const r of items) {
|
|
116
|
+
const when = new Date(r.created_at).toISOString().replace('T', ' ').slice(0, 16);
|
|
117
|
+
const to = r.detail?.to ?? '—';
|
|
118
|
+
const subj = r.detail?.subject ? ` · ${r.detail.subject}` : '';
|
|
119
|
+
console.log(` ${muted(when)} ${to}${muted(subj)}`);
|
|
120
|
+
}
|
|
121
|
+
}));
|
|
53
122
|
//# sourceMappingURL=email.js.map
|
|
@@ -2,7 +2,7 @@ import { Command } from 'commander';
|
|
|
2
2
|
import { post } from '../api.js';
|
|
3
3
|
import { resolveProjectContext, getConfigPath } from '../config.js';
|
|
4
4
|
import { pushFile } from '../sync.js';
|
|
5
|
-
import { writeFileSync } from 'fs';
|
|
5
|
+
import { mkdirSync, writeFileSync } from 'fs';
|
|
6
6
|
import { resolve as resolvePath, dirname, relative, isAbsolute } from 'path';
|
|
7
7
|
import { error as clrError, success, muted } from '../colors.js';
|
|
8
8
|
import { printCommandError } from '../helpers/command.js';
|
|
@@ -25,11 +25,27 @@ async function downloadFile(url, filename) {
|
|
|
25
25
|
if (!res.ok)
|
|
26
26
|
throw new Error(`Download failed: ${res.status}`);
|
|
27
27
|
const buffer = Buffer.from(await res.arrayBuffer());
|
|
28
|
+
ensureOutputDir(filename);
|
|
28
29
|
writeFileSync(filename, buffer);
|
|
29
30
|
const savedPath = resolvePath(filename);
|
|
30
31
|
await pushGenerated(savedPath);
|
|
31
32
|
return savedPath;
|
|
32
33
|
}
|
|
34
|
+
/** Create the parent directory of an -o path, so `-o src/audio/ding.mp3` works
|
|
35
|
+
* in a tree that has no `src/audio/` yet instead of dying on a raw ENOENT.
|
|
36
|
+
*
|
|
37
|
+
* Every action also calls this *before* its generate request: generation is
|
|
38
|
+
* billed per call, so an unwritable output path has to fail before we spend
|
|
39
|
+
* one, not after the bytes are already paid for and in hand. */
|
|
40
|
+
function ensureOutputDir(output) {
|
|
41
|
+
const dir = dirname(resolvePath(output));
|
|
42
|
+
try {
|
|
43
|
+
mkdirSync(dir, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
throw new Error(`can't create the output directory ${dir} (${err.code || err.message}). Pick a different -o path.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
33
49
|
/** Sync a freshly generated file up to the linked project (no-op when there's
|
|
34
50
|
* no local project, or the file was written outside the project tree). */
|
|
35
51
|
async function pushGenerated(savedPath) {
|
|
@@ -74,6 +90,8 @@ Examples:
|
|
|
74
90
|
.action(async (prompt, opts) => {
|
|
75
91
|
try {
|
|
76
92
|
const { config } = await resolveProjectContext();
|
|
93
|
+
if (opts.output)
|
|
94
|
+
ensureOutputDir(opts.output);
|
|
77
95
|
const doGenerate = () => post(`/projects/${config.projectGuid}/generate/image`, {
|
|
78
96
|
prompt,
|
|
79
97
|
provider: opts.provider,
|
|
@@ -135,6 +153,8 @@ Examples:
|
|
|
135
153
|
.action(async (prompt, opts) => {
|
|
136
154
|
try {
|
|
137
155
|
const { config } = await resolveProjectContext();
|
|
156
|
+
if (opts.output)
|
|
157
|
+
ensureOutputDir(opts.output);
|
|
138
158
|
const doGenerate = () => post(`/projects/${config.projectGuid}/generate/video`, {
|
|
139
159
|
prompt,
|
|
140
160
|
model: opts.model,
|
|
@@ -187,6 +207,8 @@ Examples:
|
|
|
187
207
|
.action(async (text, opts) => {
|
|
188
208
|
try {
|
|
189
209
|
const { config } = await resolveProjectContext();
|
|
210
|
+
if (opts.output)
|
|
211
|
+
ensureOutputDir(opts.output);
|
|
190
212
|
let speakers;
|
|
191
213
|
if (opts.speakers) {
|
|
192
214
|
try {
|
|
@@ -249,6 +271,8 @@ Examples:
|
|
|
249
271
|
.action(async (prompt, opts) => {
|
|
250
272
|
try {
|
|
251
273
|
const { config } = await resolveProjectContext();
|
|
274
|
+
if (opts.output)
|
|
275
|
+
ensureOutputDir(opts.output);
|
|
252
276
|
const doGenerate = () => post(`/projects/${config.projectGuid}/generate/music`, {
|
|
253
277
|
prompt,
|
|
254
278
|
duration_seconds: opts.duration,
|
|
@@ -299,6 +323,8 @@ Examples:
|
|
|
299
323
|
.action(async (text, opts) => {
|
|
300
324
|
try {
|
|
301
325
|
const { config } = await resolveProjectContext();
|
|
326
|
+
if (opts.output)
|
|
327
|
+
ensureOutputDir(opts.output);
|
|
302
328
|
const doGenerate = () => post(`/projects/${config.projectGuid}/generate/sound`, {
|
|
303
329
|
text,
|
|
304
330
|
duration_seconds: opts.duration,
|
package/dist/commands/login.js
CHANGED
|
@@ -3,6 +3,7 @@ import { saveAuth, getAuth } from '../auth.js';
|
|
|
3
3
|
import { publicPost } from '../api.js';
|
|
4
4
|
import { prompt, decodeJwtExp } from '../utils.js';
|
|
5
5
|
import { success, error as clrError, muted } from '../colors.js';
|
|
6
|
+
import { flushBugQueue } from '../bug-queue.js';
|
|
6
7
|
export const loginCommand = new Command('login')
|
|
7
8
|
.description('Log in or sign up')
|
|
8
9
|
.option('--email <email>', 'Email address')
|
|
@@ -59,5 +60,11 @@ async function verify(email, code) {
|
|
|
59
60
|
expiresAt,
|
|
60
61
|
});
|
|
61
62
|
console.log(success(`Logged in (${email}).`));
|
|
63
|
+
// A fresh session is the clearest "we're reconnected" signal - clear any bug
|
|
64
|
+
// reports that got stranded while this account's session was expired/offline.
|
|
65
|
+
const delivered = await flushBugQueue().catch(() => 0);
|
|
66
|
+
if (delivered > 0) {
|
|
67
|
+
console.log(muted(`Delivered ${delivered} queued bug report${delivered === 1 ? '' : 's'}.`));
|
|
68
|
+
}
|
|
62
69
|
}
|
|
63
70
|
//# sourceMappingURL=login.js.map
|
package/dist/commands/logs.js
CHANGED
|
@@ -26,7 +26,10 @@ logsCommand
|
|
|
26
26
|
for (const log of res.data) {
|
|
27
27
|
const time = new Date(log.created_at).toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit' });
|
|
28
28
|
const dur = log.duration_ms !== null ? `${log.duration_ms}ms`.padEnd(8) : ''.padEnd(8);
|
|
29
|
-
|
|
29
|
+
// The runtime writes 'ok' | 'error' | 'limit_exceeded'; it has never
|
|
30
|
+
// written 'success'. Matching on that dead value painted every healthy
|
|
31
|
+
// invocation with the warning colour, so a clean log read as a problem.
|
|
32
|
+
const statusColor = log.status === 'ok' ? success : log.status === 'error' ? clrError : warning;
|
|
30
33
|
const status = statusColor(log.status.padEnd(8));
|
|
31
34
|
const trigger = muted(log.trigger_type.padEnd(8));
|
|
32
35
|
const err = log.error_message ? ` ${clrError(`"${log.error_message}"`)}` : '';
|
|
@@ -275,6 +275,16 @@ export const pageEvalCommand = new Command('eval')
|
|
|
275
275
|
if (d.navigationIncomplete) {
|
|
276
276
|
console.log(`${warning('⚠ Navigation incomplete:')} ${d.note || 'page did not reach full load'}`);
|
|
277
277
|
}
|
|
278
|
+
// A slow-painting page makes any wall-clock wait silently wrong: the
|
|
279
|
+
// page's rAF loop is its clock, and engines cap the per-frame delta, so
|
|
280
|
+
// `setTimeout(2000)` may advance only a fraction of a second of app time.
|
|
281
|
+
// Without this line the eval returns a plausible-looking false negative.
|
|
282
|
+
if (d.slowRender) {
|
|
283
|
+
console.log(`${warning('⚠ Slow render:')} page painted at ${d.slowRender.fps} fps. `
|
|
284
|
+
+ `Waiting on real time (setTimeout) advances animation/physics time far slower than it looks — `
|
|
285
|
+
+ `assertions after a wall-clock wait can report a false negative. `
|
|
286
|
+
+ `Step the app's own loop deterministically instead (3D templates: ${bold('core.advance(seconds)')}).`);
|
|
287
|
+
}
|
|
278
288
|
// Auth state: without this line an agent can't distinguish "signed-in
|
|
279
289
|
// eval" from "--auth silently no-op'd against the anonymous page".
|
|
280
290
|
if (d.auth?.requested) {
|
|
@@ -150,6 +150,11 @@ function splitCsv(values) {
|
|
|
150
150
|
function appendOption(value, previous = []) {
|
|
151
151
|
return [...previous, value];
|
|
152
152
|
}
|
|
153
|
+
// Pre-capture scripting lives on --action, but agents reach for the JS-intent
|
|
154
|
+
// names first and an "unknown option" alone doesn't name the right flag. Capture
|
|
155
|
+
// the common guesses as hidden decoys (taking a value, so they swallow the
|
|
156
|
+
// script) and redirect precisely — same pattern as `page eval`'s JS_DECOY_FLAGS.
|
|
157
|
+
const ACTION_DECOY_FLAGS = ['--eval', '--js', '--javascript', '--script', '--code', '--exec'];
|
|
153
158
|
export const pageScreenshotCommand = new Command('screenshot')
|
|
154
159
|
.description('Screenshot a web page')
|
|
155
160
|
.argument('<url>', 'URL to screenshot')
|
|
@@ -172,6 +177,13 @@ export const pageScreenshotCommand = new Command('screenshot')
|
|
|
172
177
|
// rather than reject it as an unknown option and send them on a --help detour.
|
|
173
178
|
.addOption(new Option('--full-page', 'Alias for --full').hideHelp())
|
|
174
179
|
.action((url, opts) => run('Page screenshot', async () => {
|
|
180
|
+
// A JS-intent flag guess (captured as a hidden decoy below): name the real
|
|
181
|
+
// flag before any other arg-shape check fires.
|
|
182
|
+
const decoy = ACTION_DECOY_FLAGS.find((f) => opts[f.slice(2)] !== undefined);
|
|
183
|
+
if (decoy) {
|
|
184
|
+
pageScreenshotCommand.error(`error: ${decoy} is not a flag on screenshot — use --action "<js>" to run JavaScript in the page ` +
|
|
185
|
+
`before the capture, e.g. gipity page screenshot "<url>" --action "document.getElementById('play').click()"`);
|
|
186
|
+
}
|
|
175
187
|
// --wait is a hidden alias for --post-load-delay (agents reach for it because
|
|
176
188
|
// sibling `page inspect`/`eval` name the flag --wait). Canonical name wins if
|
|
177
189
|
// both given; fall back to the 1000ms default when neither is set.
|
|
@@ -308,6 +320,10 @@ export const pageScreenshotCommand = new Command('screenshot')
|
|
|
308
320
|
console.log(`${label('Screenshot file')} ${success(savedFiles[i])}`);
|
|
309
321
|
}
|
|
310
322
|
}));
|
|
323
|
+
// Register the JS-intent guesses as hidden decoys (value-taking, so they swallow
|
|
324
|
+
// the script) — the action turns any of them into the "--action" redirect above.
|
|
325
|
+
for (const f of ACTION_DECOY_FLAGS)
|
|
326
|
+
pageScreenshotCommand.addOption(new Option(`${f} <value>`).hideHelp());
|
|
311
327
|
// `screenshot` captures the page AFTER load + settle (+ optional --action). There
|
|
312
328
|
// is no scroll-to-a-position or wait-for-a-selector lever (agents reach for
|
|
313
329
|
// --scroll/--selector and get an unknown-option detour) — but `--full` does walk
|
package/dist/commands/sandbox.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { readFileSync, existsSync, statSync } from 'fs';
|
|
3
|
-
import { dirname, extname, relative } from 'path';
|
|
3
|
+
import { dirname, extname, relative, resolve } from 'path';
|
|
4
4
|
import { post } from '../api.js';
|
|
5
|
-
import { resolveProjectContext, getConfigPath, shouldIgnore } from '../config.js';
|
|
5
|
+
import { resolveProjectContext, getConfigPath, getProjectRoot, shouldIgnore } from '../config.js';
|
|
6
6
|
import { SCRATCH_IGNORE } from '../setup.js';
|
|
7
7
|
import { sync } from '../sync.js';
|
|
8
8
|
import { error as clrError, dim } from '../colors.js';
|
|
@@ -344,10 +344,28 @@ GCC/Rust).
|
|
|
344
344
|
console.error(res.data.stderr);
|
|
345
345
|
if (res.data.timedOut)
|
|
346
346
|
console.error(`[Timed out after ${res.data.durationMs}ms]`);
|
|
347
|
+
if (res.data.stdoutTruncated || res.data.stderrTruncated) {
|
|
348
|
+
console.error(dim('Note: output truncated at 256 KB. Write large results to a file instead of printing them.'));
|
|
349
|
+
}
|
|
347
350
|
if (res.data.outputFiles && res.data.outputFiles.length > 0) {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
+
// Only claim a file is "synced to this directory" once it is actually on
|
|
352
|
+
// local disk - a sync that ran is not the same as a file that landed.
|
|
353
|
+
const projectRoot = getProjectRoot();
|
|
354
|
+
const landed = (f) => pulledLocal && !!projectRoot && existsSync(resolve(projectRoot, f));
|
|
355
|
+
const onDisk = res.data.outputFiles.filter(landed);
|
|
356
|
+
const notOnDisk = res.data.outputFiles.filter((f) => !landed(f));
|
|
357
|
+
if (onDisk.length > 0) {
|
|
358
|
+
console.log('\nOutput files synced to this directory:');
|
|
359
|
+
for (const f of onDisk)
|
|
360
|
+
console.log(`${f}`);
|
|
361
|
+
}
|
|
362
|
+
if (notOnDisk.length > 0) {
|
|
363
|
+
console.log('\nOutput files saved to project:');
|
|
364
|
+
for (const f of notOnDisk)
|
|
365
|
+
console.log(`${f}`);
|
|
366
|
+
if (pulledLocal)
|
|
367
|
+
console.log(dim("Not pulled locally - run 'gipity sync' to fetch them."));
|
|
368
|
+
}
|
|
351
369
|
}
|
|
352
370
|
if (res.data.skippedOutputFiles && res.data.skippedOutputFiles.length > 0) {
|
|
353
371
|
console.log(dim('\nNot persisted (--no-sync-output):'));
|