gipity 1.0.426 → 1.0.428
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/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/page-eval.js +10 -0
- package/dist/commands/status.js +14 -0
- package/dist/index.js +26392 -969
- package/dist/knowledge.js +2 -2
- package/dist/login-flow.js +6 -1
- package/dist/relay/daemon.js +372 -26
- 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/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
|
|
@@ -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) {
|
package/dist/commands/status.js
CHANGED
|
@@ -6,6 +6,7 @@ import { getAuth, sessionExpired } from '../auth.js';
|
|
|
6
6
|
import { getConfig, liveUrl } from '../config.js';
|
|
7
7
|
import { brand, success, warning, muted, error as clrError } from '../colors.js';
|
|
8
8
|
import { GIPITY_PLUGIN_ID, GIPITY_MARKETPLACE_NAME, setupClaudeHooks, ensureGipityPlugin, ensureGipityPluginInstalled, userScopeInstallState } from '../setup.js';
|
|
9
|
+
import { flushBugQueue } from '../bug-queue.js';
|
|
9
10
|
/** Hooks ship in the Gipity Claude Code plugin now. "Installed" means three
|
|
10
11
|
* things must all hold: the user-scope settings register the marketplace and
|
|
11
12
|
* enable the plugin (declarative), AND Claude Code actually has a user-scope
|
|
@@ -37,7 +38,11 @@ function checkGipityPlugin() {
|
|
|
37
38
|
const stale = missing.length === 1 && missing[0] === 'install' && install.exists;
|
|
38
39
|
return { missing, ok: missing.length === 0, stale };
|
|
39
40
|
}
|
|
41
|
+
// `whoami` is the name agents reach for first when they want the signed-in
|
|
42
|
+
// identity (it's the unix spelling), and this is the command that prints it.
|
|
43
|
+
// Aliasing costs one line and turns a guess into a hit.
|
|
40
44
|
export const statusCommand = new Command('status')
|
|
45
|
+
.alias('whoami')
|
|
41
46
|
.description('Show project and login status')
|
|
42
47
|
.option('--json', 'Output as JSON')
|
|
43
48
|
.option('--repair-hooks', 'Re-enable the Gipity Claude Code plugin (hooks) if missing or disabled')
|
|
@@ -47,6 +52,12 @@ export const statusCommand = new Command('status')
|
|
|
47
52
|
const cwd = resolve(process.cwd());
|
|
48
53
|
void cwd;
|
|
49
54
|
const hookCheck = config ? checkGipityPlugin() : null;
|
|
55
|
+
// `status` is the command an agent reaches for to confirm "are we back?"
|
|
56
|
+
// after an outage/session-expiry - a valid, unexpired session here is the
|
|
57
|
+
// clearest signal we have that any bug reports stranded by that outage can
|
|
58
|
+
// now go out. Skipped entirely (existsSync short-circuit) when the queue
|
|
59
|
+
// is empty, which is the common case, so this stays a no-op most runs.
|
|
60
|
+
const queueDelivered = (auth && !sessionExpired()) ? await flushBugQueue().catch(() => 0) : 0;
|
|
50
61
|
if (opts.json) {
|
|
51
62
|
console.log(JSON.stringify({
|
|
52
63
|
project: config ? {
|
|
@@ -86,6 +97,9 @@ export const statusCommand = new Command('status')
|
|
|
86
97
|
else {
|
|
87
98
|
console.log(`${muted('Auth:')} ${success(auth.email)}`);
|
|
88
99
|
}
|
|
100
|
+
if (queueDelivered > 0) {
|
|
101
|
+
console.log(`${muted('Bug queue:')} ${success(`delivered ${queueDelivered} queued bug report${queueDelivered === 1 ? '' : 's'}`)}`);
|
|
102
|
+
}
|
|
89
103
|
if (hookCheck) {
|
|
90
104
|
if (hookCheck.ok) {
|
|
91
105
|
console.log(`${muted('Hooks:')} ${success(`Gipity plugin enabled (${GIPITY_PLUGIN_ID})`)}`);
|