gipity 1.0.391 → 1.0.392
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/api.js +14 -12
- package/dist/auth.js +74 -40
- package/dist/commands/generate.js +34 -5
- package/dist/commands/page-inspect.js +33 -9
- package/dist/commands/page-screenshot.js +21 -0
- package/dist/commands/token.js +65 -0
- package/dist/config.js +6 -0
- package/dist/index.js +27 -3
- package/dist/knowledge.js +1 -0
- package/dist/relay/daemon.js +7 -1
- package/dist/sync.js +14 -2
- package/dist/utils.js +13 -1
- package/package.json +2 -2
package/dist/api.js
CHANGED
|
@@ -16,13 +16,23 @@ export class ApiError extends Error {
|
|
|
16
16
|
this.name = 'ApiError';
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
-
|
|
19
|
+
/** Resolve the Bearer token value. A GIPITY_TOKEN env var (a long-lived agent
|
|
20
|
+
* API token) takes precedence over the saved session — the persistent,
|
|
21
|
+
* login-free path for headless agents and CI. Falls back to the refreshed
|
|
22
|
+
* session access token. */
|
|
23
|
+
async function bearerToken() {
|
|
24
|
+
const envToken = process.env.GIPITY_TOKEN?.trim();
|
|
25
|
+
if (envToken)
|
|
26
|
+
return envToken;
|
|
20
27
|
await refreshTokenIfNeeded();
|
|
21
28
|
const auth = getAuth();
|
|
22
29
|
if (!auth)
|
|
23
30
|
throw new Error('Not authenticated. Run: gipity login');
|
|
31
|
+
return auth.accessToken;
|
|
32
|
+
}
|
|
33
|
+
async function getHeaders() {
|
|
24
34
|
return {
|
|
25
|
-
'Authorization': `Bearer ${
|
|
35
|
+
'Authorization': `Bearer ${await bearerToken()}`,
|
|
26
36
|
'Content-Type': 'application/json',
|
|
27
37
|
};
|
|
28
38
|
}
|
|
@@ -125,13 +135,9 @@ export async function sendMessage(message) {
|
|
|
125
135
|
}
|
|
126
136
|
/** Download a file as raw bytes (no JSON parsing) */
|
|
127
137
|
export async function download(path) {
|
|
128
|
-
await refreshTokenIfNeeded();
|
|
129
|
-
const auth = getAuth();
|
|
130
|
-
if (!auth)
|
|
131
|
-
throw new Error('Not authenticated. Run: gipity login');
|
|
132
138
|
const url = `${baseUrl()}${path}`;
|
|
133
139
|
const res = await fetch(url, {
|
|
134
|
-
headers: { 'Authorization': `Bearer ${
|
|
140
|
+
headers: { 'Authorization': `Bearer ${await bearerToken()}` },
|
|
135
141
|
});
|
|
136
142
|
if (!res.ok) {
|
|
137
143
|
throw new ApiError(res.status, 'DOWNLOAD_ERROR', `Download failed: ${res.statusText}`);
|
|
@@ -141,13 +147,9 @@ export async function download(path) {
|
|
|
141
147
|
/** Download a response as a Node.js Readable stream */
|
|
142
148
|
export async function downloadStream(path) {
|
|
143
149
|
const { Readable } = await import('stream');
|
|
144
|
-
await refreshTokenIfNeeded();
|
|
145
|
-
const auth = getAuth();
|
|
146
|
-
if (!auth)
|
|
147
|
-
throw new Error('Not authenticated. Run: gipity login');
|
|
148
150
|
const url = `${baseUrl()}${path}`;
|
|
149
151
|
const res = await fetch(url, {
|
|
150
|
-
headers: { 'Authorization': `Bearer ${
|
|
152
|
+
headers: { 'Authorization': `Bearer ${await bearerToken()}` },
|
|
151
153
|
});
|
|
152
154
|
if (!res.ok) {
|
|
153
155
|
throw new ApiError(res.status, 'DOWNLOAD_ERROR', `Download failed: ${res.statusText}`);
|
package/dist/auth.js
CHANGED
|
@@ -2,7 +2,11 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync, chmodSy
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
import { decodeJwtExp } from './utils.js';
|
|
5
|
-
|
|
5
|
+
// GIPITY_DIR lets a caller keep a SEPARATE auth context (its own auth.json) from
|
|
6
|
+
// the default ~/.gipity — e.g. GipRunner logging into a local dev server without
|
|
7
|
+
// clobbering your real (prod) login. Only the auth dir moves; HOME is untouched,
|
|
8
|
+
// so the `claude` subprocess and git/npm still use the real home.
|
|
9
|
+
const AUTH_DIR = process.env.GIPITY_DIR || join(homedir(), '.gipity');
|
|
6
10
|
const AUTH_FILE = join(AUTH_DIR, 'auth.json');
|
|
7
11
|
let cached = null;
|
|
8
12
|
export function getAuth() {
|
|
@@ -52,18 +56,10 @@ export function clearAuth() {
|
|
|
52
56
|
catch { /* already gone */ }
|
|
53
57
|
cached = null;
|
|
54
58
|
}
|
|
55
|
-
export function isExpired() {
|
|
56
|
-
const auth = getAuth();
|
|
57
|
-
if (!auth)
|
|
58
|
-
return true;
|
|
59
|
-
const expiresAt = new Date(auth.expiresAt).getTime();
|
|
60
|
-
const buffer = 5 * 60 * 1000; // 5 minute buffer
|
|
61
|
-
return Date.now() > expiresAt - buffer;
|
|
62
|
-
}
|
|
63
59
|
/** True only when re-login is genuinely required: the refresh token itself
|
|
64
|
-
* has expired. Access-token expiry (`expiresAt`
|
|
65
|
-
*
|
|
66
|
-
*
|
|
60
|
+
* has expired. Access-token expiry (`expiresAt`) is invisible to users —
|
|
61
|
+
* every API call renews it via refreshTokenIfNeeded() — so it must never be
|
|
62
|
+
* surfaced as a session warning. */
|
|
67
63
|
export function sessionExpired() {
|
|
68
64
|
const auth = getAuth();
|
|
69
65
|
if (!auth)
|
|
@@ -73,40 +69,78 @@ export function sessionExpired() {
|
|
|
73
69
|
return false; // undecodable - let the refresh path decide
|
|
74
70
|
return Date.now() > exp * 1000;
|
|
75
71
|
}
|
|
72
|
+
const delay = (ms) => new Promise(r => setTimeout(r, ms));
|
|
73
|
+
/** Renew the access token (5-min buffer) before an authenticated call, surviving
|
|
74
|
+
* the case that broke overnight fix-mode runs: MANY concurrent `gipity` processes
|
|
75
|
+
* (relay daemon, file-sync hook, parallel commands) sharing one ~/.gipity/auth.json.
|
|
76
|
+
* Refresh tokens are SINGLE-USE — the server rotates them, so when several siblings
|
|
77
|
+
* race to refresh the same token, the first wins and the rest get a 401. The old
|
|
78
|
+
* code trusted a stale in-process cache and, on that race, let the 401 reach the
|
|
79
|
+
* caller, whose handler called clearAuth() and DELETED the shared file — locking
|
|
80
|
+
* every sibling out mid-run ("Not logged in"). Fix: always read the file fresh, and
|
|
81
|
+
* retry the race/transient failures, re-reading each attempt so we ADOPT whatever
|
|
82
|
+
* token a sibling just rotated in rather than resubmitting the rotated-away one.
|
|
83
|
+
* Stays void / never throws / never clears auth: a genuine dead token still flows to
|
|
84
|
+
* the caller's existing 401 path (which messages "run: gipity login"). */
|
|
76
85
|
export async function refreshTokenIfNeeded() {
|
|
77
|
-
|
|
78
|
-
return;
|
|
79
|
-
const auth = getAuth();
|
|
86
|
+
const auth = readAuthFresh(); // never the cache — a sibling may have rotated
|
|
80
87
|
if (!auth)
|
|
81
|
-
return; // not logged in
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
return; // not logged in - caller throws the clean error
|
|
89
|
+
cached = auth;
|
|
90
|
+
const buffer = 5 * 60 * 1000; // refresh 5 min before the access token lapses
|
|
91
|
+
const fresh = (a) => Date.now() <= new Date(a.expiresAt).getTime() - buffer;
|
|
92
|
+
if (fresh(auth))
|
|
93
|
+
return;
|
|
94
|
+
// If the refresh token itself has expired, re-login is genuinely required; leave the
|
|
95
|
+
// expired auth in place so the caller's existing 401 path prompts `gipity login`.
|
|
96
|
+
const refreshExp = decodeJwtExp(auth.refreshToken);
|
|
97
|
+
if (refreshExp && Date.now() > refreshExp * 1000)
|
|
98
|
+
return;
|
|
99
|
+
const { resolveApiBase } = await import('./config.js');
|
|
100
|
+
const apiBase = resolveApiBase();
|
|
101
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
102
|
+
const cur = readAuthFresh(); // a sibling may have just refreshed for us
|
|
103
|
+
if (cur && fresh(cur)) {
|
|
104
|
+
cached = cur;
|
|
92
105
|
return;
|
|
93
106
|
}
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
107
|
+
const refreshToken = cur?.refreshToken ?? auth.refreshToken;
|
|
108
|
+
let res;
|
|
109
|
+
try {
|
|
110
|
+
res = await fetch(`${apiBase}/auth/refresh`, {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: { 'Content-Type': 'application/json' },
|
|
113
|
+
body: JSON.stringify({ refreshToken }),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
await delay(attempt * 300);
|
|
118
|
+
continue; // network blip - retry
|
|
119
|
+
}
|
|
120
|
+
if (res.ok) {
|
|
121
|
+
const json = await res.json().catch(() => null);
|
|
122
|
+
const exp = json && decodeJwtExp(json.accessToken);
|
|
123
|
+
if (!json || !exp) {
|
|
124
|
+
await delay(attempt * 300);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
saveAuth({ accessToken: json.accessToken, refreshToken: json.refreshToken, email: auth.email, expiresAt: new Date(exp * 1000).toISOString() });
|
|
98
128
|
return;
|
|
99
129
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
130
|
+
// 401/403 → the refresh token was rejected outright (it was rotated away by a
|
|
131
|
+
// sibling, or genuinely expired). Re-read once more in case a sibling's fresh
|
|
132
|
+
// token just landed; otherwise stop and let the caller's 401 path re-login.
|
|
133
|
+
if (res.status === 401 || res.status === 403) {
|
|
134
|
+
const after = readAuthFresh();
|
|
135
|
+
if (after && fresh(after)) {
|
|
136
|
+
cached = after;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
await delay(attempt * 300); // 5xx / unexpected → transient, retry
|
|
110
142
|
}
|
|
143
|
+
// Retries exhausted: leave the existing token. The caller's request will 401 and the
|
|
144
|
+
// existing handler messages the user — we never delete the shared auth.json here.
|
|
111
145
|
}
|
|
112
146
|
//# sourceMappingURL=auth.js.map
|
|
@@ -1,19 +1,48 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { post } from '../api.js';
|
|
3
|
-
import { resolveProjectContext } from '../config.js';
|
|
3
|
+
import { resolveProjectContext, getConfigPath } from '../config.js';
|
|
4
|
+
import { pushFile } from '../sync.js';
|
|
4
5
|
import { writeFileSync } from 'fs';
|
|
5
|
-
import { resolve as resolvePath } from 'path';
|
|
6
|
+
import { resolve as resolvePath, dirname, relative, isAbsolute } from 'path';
|
|
6
7
|
import { error as clrError, success, muted, info } from '../colors.js';
|
|
7
8
|
import { IMAGE_MODELS_DOC, IMAGE_GEMINI_ASPECT_RATIOS, IMAGE_GEMINI_SIZES, VIDEO_MODELS_DOC, TTS_PROVIDER_DESCRIPTIONS } from '../provider-docs.js';
|
|
8
|
-
/** Download a URL and save to a local file
|
|
9
|
-
*
|
|
9
|
+
/** Download a URL and save to a local file, then push it up to the project so
|
|
10
|
+
* the cloud (and anything that mirrors it) immediately matches local disk.
|
|
11
|
+
* Returns the absolute path written, so callers can report where it landed.
|
|
12
|
+
*
|
|
13
|
+
* The push matters because generated media is written straight to disk with
|
|
14
|
+
* writeFileSync, which does NOT trip the editor's file-sync hook the way an
|
|
15
|
+
* agent's own file write does. Without it the file is local-only until the
|
|
16
|
+
* next `gipity sync`, so `gipity sandbox run` - which mirrors the *server* -
|
|
17
|
+
* can't see a just-generated image to convert/optimize it. Pushing here closes
|
|
18
|
+
* that gap so the "sandbox auto-mirrors the project" contract holds right after
|
|
19
|
+
* generation. Best-effort: the local save already succeeded, so a push failure
|
|
20
|
+
* only warns and points at `gipity sync`. */
|
|
10
21
|
async function downloadFile(url, filename) {
|
|
11
22
|
const res = await fetch(url);
|
|
12
23
|
if (!res.ok)
|
|
13
24
|
throw new Error(`Download failed: ${res.status}`);
|
|
14
25
|
const buffer = Buffer.from(await res.arrayBuffer());
|
|
15
26
|
writeFileSync(filename, buffer);
|
|
16
|
-
|
|
27
|
+
const savedPath = resolvePath(filename);
|
|
28
|
+
await pushGenerated(savedPath);
|
|
29
|
+
return savedPath;
|
|
30
|
+
}
|
|
31
|
+
/** Sync a freshly generated file up to the linked project (no-op when there's
|
|
32
|
+
* no local project, or the file was written outside the project tree). */
|
|
33
|
+
async function pushGenerated(savedPath) {
|
|
34
|
+
const configPath = getConfigPath();
|
|
35
|
+
if (!configPath)
|
|
36
|
+
return; // not linked to a project - nothing to sync into
|
|
37
|
+
const rel = relative(dirname(configPath), savedPath);
|
|
38
|
+
if (rel.startsWith('..') || isAbsolute(rel))
|
|
39
|
+
return; // outside the project tree
|
|
40
|
+
try {
|
|
41
|
+
await pushFile(savedPath);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
console.error(muted(`Note: couldn't sync to the cloud automatically (${err.message}). Run \`gipity sync\` before referencing this file in \`gipity sandbox run\`.`));
|
|
45
|
+
}
|
|
17
46
|
}
|
|
18
47
|
// ── IMAGE ──────────────────────────────────────────────────────────────
|
|
19
48
|
const imageCommand = new Command('image')
|
|
@@ -6,6 +6,13 @@ import { run } from '../helpers/index.js';
|
|
|
6
6
|
import { capWaitMs } from './page-eval.js';
|
|
7
7
|
/** A console line is an error-level entry (page error or console.error). */
|
|
8
8
|
const isErrorLine = (line) => /^error:/i.test(line);
|
|
9
|
+
/** A message-less, cross-origin "Script error." The throwing <script> lacks
|
|
10
|
+
* CORS, so the browser strips its message/stack and the source is unknowable
|
|
11
|
+
* from the console alone — there is no own-code stack to chase. These can't be
|
|
12
|
+
* attributed to app code, so we surface them apart from real console errors
|
|
13
|
+
* rather than letting an unactionable (and sometimes growing) count read as a
|
|
14
|
+
* regression in the app the agent just wrote. */
|
|
15
|
+
const isMessagelessCrossOrigin = (line) => isErrorLine(line) && /message-less|cross-origin|Script error\.?/i.test(line);
|
|
9
16
|
function shortUrl(url, truncate = true, maxLen = 100) {
|
|
10
17
|
let result;
|
|
11
18
|
try {
|
|
@@ -55,13 +62,21 @@ export const pageInspectCommand = new Command('inspect')
|
|
|
55
62
|
};
|
|
56
63
|
const res = await post(`/tools/browser/inspect`, inspectBody);
|
|
57
64
|
const b = res.data;
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
+
// Pull message-less cross-origin "Script error." lines out first. They carry
|
|
66
|
+
// no source/stack, so they're never actionable as app-code defects, and on a
|
|
67
|
+
// Gipity-deployed page the platform's own injected SDK is itself a
|
|
68
|
+
// cross-origin script — so these are reported separately (not as app console
|
|
69
|
+
// errors, and not folded into the re-probe count) instead of misleading the
|
|
70
|
+
// agent into chasing its own code.
|
|
71
|
+
const crossOriginErrors = (b.console || []).filter(isMessagelessCrossOrigin);
|
|
72
|
+
b.console = (b.console || []).filter((l) => !isMessagelessCrossOrigin(l));
|
|
73
|
+
// Self-verify the remaining console errors before flagging them. A
|
|
74
|
+
// freshly-deployed page's first hit can throw a one-time, non-reproducible
|
|
75
|
+
// error from an asset still propagating — and reporting it as a real defect
|
|
76
|
+
// sends agents chasing a phantom. So when the first probe reports error-level
|
|
77
|
+
// console lines, re-probe once (the sticky session is now warm) and keep only
|
|
78
|
+
// the errors that recur; errors seen on a single probe are surfaced
|
|
79
|
+
// separately as transient noise.
|
|
65
80
|
let transientErrors = [];
|
|
66
81
|
if ((b.console || []).some(isErrorLine)) {
|
|
67
82
|
try {
|
|
@@ -76,7 +91,11 @@ export const pageInspectCommand = new Command('inspect')
|
|
|
76
91
|
}
|
|
77
92
|
}
|
|
78
93
|
if (opts.json) {
|
|
79
|
-
console.log(JSON.stringify(
|
|
94
|
+
console.log(JSON.stringify({
|
|
95
|
+
...b,
|
|
96
|
+
...(transientErrors.length ? { transientConsole: transientErrors } : {}),
|
|
97
|
+
...(crossOriginErrors.length ? { crossOriginConsole: crossOriginErrors } : {}),
|
|
98
|
+
}));
|
|
80
99
|
return;
|
|
81
100
|
}
|
|
82
101
|
const timing = b.timing || { ttfb: 0, domReady: 0, load: 0 };
|
|
@@ -112,7 +131,12 @@ export const pageInspectCommand = new Command('inspect')
|
|
|
112
131
|
for (const line of transientErrors) {
|
|
113
132
|
console.log(muted(line));
|
|
114
133
|
}
|
|
115
|
-
console.log(muted('One-time cold-load artifact (first hit of freshly-deployed assets
|
|
134
|
+
console.log(muted('One-time cold-load artifact (first hit of freshly-deployed assets) — not reproducible, not in your app code. Ignore unless it recurs.'));
|
|
135
|
+
}
|
|
136
|
+
// ── Cross-origin console errors (message-less; source hidden by the browser) ──
|
|
137
|
+
if (crossOriginErrors.length > 0) {
|
|
138
|
+
console.log(`\n${bold('Cross-origin console errors')} ${muted(`(${crossOriginErrors.length}, source hidden by the browser)`)}:`);
|
|
139
|
+
console.log(muted("Message-less — the throwing <script> lacks CORS, so the browser hides its source and there's no own-code stack to chase. Gipity's injected SDK is itself cross-origin, so if your app loads no third-party CDN scripts these are platform noise — ignore them. If your app DOES load a third-party <script>, add crossorigin=\"anonymous\" to that tag to surface the real error."));
|
|
116
140
|
}
|
|
117
141
|
// ── Failed Resources ──
|
|
118
142
|
// Browsers auto-request /favicon.ico at the site root for every page, so a
|
|
@@ -231,4 +231,25 @@ export const pageScreenshotCommand = new Command('screenshot')
|
|
|
231
231
|
console.log(`${label('Screenshot file')} ${success(savedFiles[i])}`);
|
|
232
232
|
}
|
|
233
233
|
}));
|
|
234
|
+
// `screenshot` captures the page AS IT LOADS — there is no flag to scroll, click,
|
|
235
|
+
// run a script, or wait for a selector before capture (agents reach for --eval/
|
|
236
|
+
// --script/--scroll/--selector and get an unknown-option detour). State this
|
|
237
|
+
// limitation and the two supported alternatives right here, so the help (rendered
|
|
238
|
+
// on any bad flag, and this 'after' block survives `| tail`/`| grep`) ends the
|
|
239
|
+
// hunt in one shot instead of sending the agent grepping `--help` for
|
|
240
|
+
// scroll/script/before/action. The real per-element capture lives server-side and
|
|
241
|
+
// isn't wired yet — until then, --full + crop or `page eval` cover the need.
|
|
242
|
+
pageScreenshotCommand.addHelpText('after', `
|
|
243
|
+
Examples:
|
|
244
|
+
gipity page screenshot "https://dev.gipity.ai/me/app/"
|
|
245
|
+
gipity page screenshot "https://dev.gipity.ai/me/app/" --full # whole scrollable page
|
|
246
|
+
gipity page screenshot "https://dev.gipity.ai/me/app/" --device mobile,desktop
|
|
247
|
+
|
|
248
|
+
Capturing a specific state (a slide, an off-screen element, a post-scroll view)?
|
|
249
|
+
screenshot captures the page as it loads — it does NOT scroll, click, wait for a
|
|
250
|
+
selector, or run a script before capture. To get the part you want:
|
|
251
|
+
• --full captures the ENTIRE scrollable page (then crop to the region).
|
|
252
|
+
• 'gipity page eval <url> "<expr>"' reads any (even off-screen) element's
|
|
253
|
+
data/state/rect without a picture — e.g. read the chart's bar values
|
|
254
|
+
directly instead of screenshotting the slide.`);
|
|
234
255
|
//# sourceMappingURL=page-screenshot.js.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { get, post, del } from '../api.js';
|
|
3
|
+
import { bold, muted, success, warning } from '../colors.js';
|
|
4
|
+
import { run, printList } from '../helpers/index.js';
|
|
5
|
+
export const tokenCommand = new Command('token')
|
|
6
|
+
.description('Manage agent API tokens (gip_at_*) for headless agents and CI');
|
|
7
|
+
const fmtDate = (d) => (d ? new Date(d).toLocaleDateString() : 'never');
|
|
8
|
+
tokenCommand
|
|
9
|
+
.command('create')
|
|
10
|
+
.description('Mint a long-lived agent API token (shown once)')
|
|
11
|
+
.option('--name <name>', 'Label for the token, e.g. "Hermes on my VPS"')
|
|
12
|
+
.option('--expires <days>', 'Days until the token expires (default: never)')
|
|
13
|
+
.option('--json', 'Output as JSON')
|
|
14
|
+
.action((opts) => run('Create', async () => {
|
|
15
|
+
const body = {};
|
|
16
|
+
if (opts.name)
|
|
17
|
+
body.name = opts.name;
|
|
18
|
+
if (opts.expires !== undefined) {
|
|
19
|
+
const days = parseInt(opts.expires, 10);
|
|
20
|
+
if (!Number.isFinite(days) || days <= 0)
|
|
21
|
+
throw new Error('--expires must be a positive number of days');
|
|
22
|
+
body.expiresInDays = days;
|
|
23
|
+
}
|
|
24
|
+
const res = await post('/auth/agent-tokens', body);
|
|
25
|
+
const { token, shortGuid, expiresAt } = res.data;
|
|
26
|
+
if (opts.json) {
|
|
27
|
+
console.log(JSON.stringify(res.data));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const expNote = expiresAt ? ` (expires ${fmtDate(expiresAt)})` : ' (never expires)';
|
|
31
|
+
console.log(success(`Created token ${bold(shortGuid)}${muted(expNote)}.`));
|
|
32
|
+
console.log('');
|
|
33
|
+
console.log(token);
|
|
34
|
+
console.log('');
|
|
35
|
+
console.log(muted('Use it from an agent, script, or CI — no login needed:'));
|
|
36
|
+
console.log(` export GIPITY_TOKEN=${token}`);
|
|
37
|
+
console.log('');
|
|
38
|
+
console.log(warning('Copy it now — it will not be shown again.'));
|
|
39
|
+
}));
|
|
40
|
+
tokenCommand
|
|
41
|
+
.command('list')
|
|
42
|
+
.alias('ls')
|
|
43
|
+
.description('List your active agent API tokens')
|
|
44
|
+
.option('--json', 'Output as JSON')
|
|
45
|
+
.action((opts) => run('List', async () => {
|
|
46
|
+
const res = await get('/auth/agent-tokens');
|
|
47
|
+
printList(res.data, opts, 'No agent API tokens.', (t) => {
|
|
48
|
+
const label = t.name ? ` ${t.name}` : '';
|
|
49
|
+
return `${bold(t.short_guid)}${label} ${muted(`created ${fmtDate(t.created_at)}`)} ${muted(`expires ${fmtDate(t.expires_at)}`)} ${muted(`last used ${fmtDate(t.last_used_at)}`)}`;
|
|
50
|
+
});
|
|
51
|
+
}));
|
|
52
|
+
tokenCommand
|
|
53
|
+
.command('revoke <short_guid>')
|
|
54
|
+
.alias('rm')
|
|
55
|
+
.description('Revoke an agent API token (instant, irreversible)')
|
|
56
|
+
.option('--json', 'Output as JSON')
|
|
57
|
+
.action((shortGuid, opts) => run('Revoke', async () => {
|
|
58
|
+
await del(`/auth/agent-tokens/${encodeURIComponent(shortGuid)}`);
|
|
59
|
+
if (opts.json) {
|
|
60
|
+
console.log(JSON.stringify({ shortGuid, revoked: true }));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
console.log(success(`Revoked token ${bold(shortGuid)}.`));
|
|
64
|
+
}));
|
|
65
|
+
//# sourceMappingURL=token.js.map
|
package/dist/config.js
CHANGED
|
@@ -45,6 +45,12 @@ export function resolveApiBase() {
|
|
|
45
45
|
const override = getApiBaseOverride();
|
|
46
46
|
if (override)
|
|
47
47
|
return override;
|
|
48
|
+
// GIPITY_API_BASE env is a trusted override (any host, like --api-base) so a
|
|
49
|
+
// caller can point the CLI at a local dev server without passing the flag on
|
|
50
|
+
// every command — e.g. GipRunner running builds against http://localhost:7201.
|
|
51
|
+
const fromEnv = process.env.GIPITY_API_BASE;
|
|
52
|
+
if (fromEnv)
|
|
53
|
+
return fromEnv;
|
|
48
54
|
const fromConfig = getConfig()?.apiBase;
|
|
49
55
|
if (fromConfig) {
|
|
50
56
|
if (isAllowedApiHost(fromConfig))
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { GIPITY_TAGLINE } from './knowledge.js';
|
|
|
10
10
|
import { getAuth, sessionExpired } from './auth.js';
|
|
11
11
|
import { loginCommand } from './commands/login.js';
|
|
12
12
|
import { logoutCommand } from './commands/logout.js';
|
|
13
|
+
import { tokenCommand } from './commands/token.js';
|
|
13
14
|
import { initCommand } from './commands/init.js';
|
|
14
15
|
import { statusCommand } from './commands/status.js';
|
|
15
16
|
import { syncCommand } from './commands/sync.js';
|
|
@@ -108,7 +109,7 @@ const filesGroup = [fileCommand, syncCommand, pushCommand, uploadCommand];
|
|
|
108
109
|
const appBuildingGroup = [testCommand, fnCommand, serviceCommand, jobCommand, dbCommand, logsCommand, workflowCommand, realtimeCommand, rbacCommand, auditCommand, recordsCommand];
|
|
109
110
|
const utilitiesGroup = [pageCommand, sandboxCommand, generateCommand, emailCommand, gmailCommand, locationCommand, textCommand];
|
|
110
111
|
const agentGroup = [chatCommand, memoryCommand, agentCommand, approvalCommand];
|
|
111
|
-
const setupGroup = [loginCommand, logoutCommand, creditsCommand, planCommand, doctorCommand, updateCommand, uninstallCommand];
|
|
112
|
+
const setupGroup = [loginCommand, logoutCommand, tokenCommand, creditsCommand, planCommand, doctorCommand, updateCommand, uninstallCommand];
|
|
112
113
|
const HELP_SECTIONS = [
|
|
113
114
|
{ title: 'Common', cmds: commonGroup },
|
|
114
115
|
{ title: 'Connect', cmds: connectGroup },
|
|
@@ -125,11 +126,15 @@ program
|
|
|
125
126
|
.version(pkg.version, '-v, --version')
|
|
126
127
|
.addOption(new Option('--api-base <url>', 'API base URL').hideHelp())
|
|
127
128
|
.option('-y, --yes', 'Skip confirmation prompts');
|
|
128
|
-
program.hook('preAction', () => {
|
|
129
|
+
program.hook('preAction', (_thisCommand, actionCommand) => {
|
|
129
130
|
const globalOpts = program.opts();
|
|
130
131
|
if (globalOpts.apiBase)
|
|
131
132
|
setApiBaseOverride(globalOpts.apiBase);
|
|
132
|
-
|
|
133
|
+
// Honor `-y`/`--yes` whether it came before the subcommand (the global flag)
|
|
134
|
+
// or after it (the per-command flag registered by enableYesEverywhere below),
|
|
135
|
+
// so both `gipity -y records delete ...` and `gipity records delete ... --yes`
|
|
136
|
+
// skip confirmation identically.
|
|
137
|
+
if (globalOpts.yes || actionCommand.opts().yes)
|
|
133
138
|
setAutoConfirm(true);
|
|
134
139
|
});
|
|
135
140
|
// Bracket non-JSON command output with leading/trailing blank lines centrally,
|
|
@@ -216,6 +221,25 @@ function enableHelpAfterError(cmd) {
|
|
|
216
221
|
enableHelpAfterError(sub);
|
|
217
222
|
}
|
|
218
223
|
enableHelpAfterError(program);
|
|
224
|
+
// ── `-y`/`--yes` accepted AFTER any subcommand, not only before it ──────
|
|
225
|
+
// The global `-y` lives on `program`, so Commander parses it only when it
|
|
226
|
+
// precedes the subcommand (`gipity -y records delete ...`). Agents and humans
|
|
227
|
+
// instinctively append it instead (`gipity records delete ... --yes`), which
|
|
228
|
+
// Commander would reject as an unknown option and dump help for. Register the
|
|
229
|
+
// flag on every leaf command so both positions work identically; the preAction
|
|
230
|
+
// hook honors whichever one was set. Skip commands that already declare their
|
|
231
|
+
// own `--yes` (e.g. `fn delete`, `db drop`, `remove`) to avoid a duplicate.
|
|
232
|
+
function enableYesEverywhere(cmd) {
|
|
233
|
+
if (cmd.commands.length > 0) {
|
|
234
|
+
for (const sub of cmd.commands)
|
|
235
|
+
enableYesEverywhere(sub);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const hasYes = cmd.options.some(o => o.long === '--yes' || o.short === '-y');
|
|
239
|
+
if (!hasYes)
|
|
240
|
+
cmd.addOption(new Option('-y, --yes', 'Skip confirmation prompts').hideHelp());
|
|
241
|
+
}
|
|
242
|
+
enableYesEverywhere(program);
|
|
219
243
|
// Auto-fetch related skill docs when --help is run on a doc-bearing TOP-LEVEL
|
|
220
244
|
// command (e.g. `gipity fn --help`, `gipity db --help`). It must NOT fire for a
|
|
221
245
|
// subcommand's help: `gipity db query --help` should render commander's own
|
package/dist/knowledge.js
CHANGED
|
@@ -122,6 +122,7 @@ App services skills (load before calling \`/services/*\` endpoints):
|
|
|
122
122
|
- \`app-video\` - Gipity Video: models, aspect, resolution
|
|
123
123
|
|
|
124
124
|
App development skills:
|
|
125
|
+
- \`agent-deploy\` - headless auth via agent API tokens (GIPITY_TOKEN) for unattended deploys
|
|
125
126
|
- \`app-debugging\` - debug a deployed app: page inspect/eval, screenshots, function logs
|
|
126
127
|
- \`app-development\` - functions, database, and API
|
|
127
128
|
- \`deploy\` - the deploy pipeline & gipity.yaml manifest
|
package/dist/relay/daemon.js
CHANGED
|
@@ -718,7 +718,13 @@ async function handleDispatch(d) {
|
|
|
718
718
|
// Future cleanup: see docs/feature-backlog/future-generate-to-vfs.md
|
|
719
719
|
// - server-side /generate/* should write directly to VFS and make
|
|
720
720
|
// this sync redundant for that case.
|
|
721
|
-
|
|
721
|
+
//
|
|
722
|
+
// Skip on `killed`: a kill-on-new-message replacement is already starting for
|
|
723
|
+
// this conv, and a bidirectional reconcile over the half-finished tree of the
|
|
724
|
+
// cancelled run is exactly the WS-00172 stale-state trap - it pushes/pulls a
|
|
725
|
+
// partial state that the resuming run then fights. The replacement dispatch
|
|
726
|
+
// runs its own sync; let it own the tree.
|
|
727
|
+
if (!spawnErr && !killed) {
|
|
722
728
|
try {
|
|
723
729
|
await spawnSync(cwd, PROJECT_SYNC_TIMEOUT_MS);
|
|
724
730
|
}
|
package/dist/sync.js
CHANGED
|
@@ -1030,9 +1030,18 @@ export async function pushFile(filePath) {
|
|
|
1030
1030
|
const rel = relative(root, filePath).replace(/\\/g, '/');
|
|
1031
1031
|
if (shouldIgnore(rel, effectiveIgnore(root, config.ignore)))
|
|
1032
1032
|
return;
|
|
1033
|
-
|
|
1034
|
-
|
|
1033
|
+
// Serialize against `gipity sync` and other concurrent pushes by holding the
|
|
1034
|
+
// same per-project lock `sync()` uses. Both paths read-modify-write the shared
|
|
1035
|
+
// baseline; without a common lock, a burst of PostToolUse pushes (each a
|
|
1036
|
+
// detached `gipity push`) racing the UserPromptSubmit/post-dispatch reconciles
|
|
1037
|
+
// drops baseline updates, and the 3-way merge then misreads our own just-pushed
|
|
1038
|
+
// edits as `modified×modified` conflicts (or pulls stale bytes over a live
|
|
1039
|
+
// edit). Read the baseline AFTER acquiring the lock so earlier pushes' writes
|
|
1040
|
+
// are visible. (WS-00172)
|
|
1041
|
+
const releaseLock = await acquireLock();
|
|
1035
1042
|
try {
|
|
1043
|
+
const baseline = readBaseline(config.projectGuid);
|
|
1044
|
+
const baseEntry = baseline.files[rel];
|
|
1036
1045
|
const result = await uploadOneFile(config.projectGuid, filePath, rel, {
|
|
1037
1046
|
expectedServerVersion: baseEntry ? baseEntry.serverVersion : null,
|
|
1038
1047
|
});
|
|
@@ -1051,5 +1060,8 @@ export async function pushFile(filePath) {
|
|
|
1051
1060
|
}
|
|
1052
1061
|
throw err;
|
|
1053
1062
|
}
|
|
1063
|
+
finally {
|
|
1064
|
+
releaseLock();
|
|
1065
|
+
}
|
|
1054
1066
|
}
|
|
1055
1067
|
//# sourceMappingURL=sync.js.map
|
package/dist/utils.js
CHANGED
|
@@ -47,6 +47,15 @@ export async function promptBoxed() {
|
|
|
47
47
|
let _autoConfirm = false;
|
|
48
48
|
export function setAutoConfirm(val) { _autoConfirm = val; }
|
|
49
49
|
export function getAutoConfirm() { return _autoConfirm; }
|
|
50
|
+
/** Reconstruct the current invocation with `--yes` appended, for self-correcting
|
|
51
|
+
* non-interactive confirmation hints. Shell-quotes args that need it. */
|
|
52
|
+
function rerunWithYes() {
|
|
53
|
+
const args = process.argv.slice(2);
|
|
54
|
+
if (!args.some(a => a === '--yes' || a === '-y'))
|
|
55
|
+
args.push('--yes');
|
|
56
|
+
const quote = (a) => (/[^\w@%+=:,./-]/.test(a) ? `'${a.replace(/'/g, `'\\''`)}'` : a);
|
|
57
|
+
return `gipity ${args.map(quote).join(' ')}`;
|
|
58
|
+
}
|
|
50
59
|
/** Ask for Y/n confirmation. Single-keypress - no Enter required.
|
|
51
60
|
*
|
|
52
61
|
* - `opts.default` controls which answer Enter / unknown-key selects. Defaults to `'no'`.
|
|
@@ -59,7 +68,10 @@ export async function confirm(question, opts = {}) {
|
|
|
59
68
|
if (opts.skip ?? _autoConfirm)
|
|
60
69
|
return true;
|
|
61
70
|
if (!process.stdin.isTTY) {
|
|
62
|
-
|
|
71
|
+
// Headless/agent context: no one can answer the prompt. Don't just say
|
|
72
|
+
// "use --yes" - echo the exact command to re-run so the fix is copy-paste,
|
|
73
|
+
// not a second guessing trip.
|
|
74
|
+
console.error(`Confirmation required (non-interactive). Re-run with --yes:\n ${rerunWithYes()}`);
|
|
63
75
|
return false;
|
|
64
76
|
}
|
|
65
77
|
const hint = defaultYes ? dim('[Y/n]') : dim('[y/N]');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gipity",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.392",
|
|
4
4
|
"description": "The full-stack platform tuned for AI agents. Database, storage, auth, functions, deploy, and drop-in kits - all agent-tuned. Pair with Claude Code or use standalone.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"gipity": "dist/updater/shim.js",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"build": "tsc && chmod +x dist/index.js dist/gipcc.js dist/gipccd.js dist/updater/shim.js dist/updater/check.js",
|
|
13
13
|
"dev": "tsc --watch",
|
|
14
14
|
"test": "npm run test:smoke",
|
|
15
|
-
"test:smoke": "tsc && node --test dist/__tests__/utils.test.js dist/__tests__/config.test.js dist/__tests__/sync.test.js dist/__tests__/sync-apply.test.js dist/__tests__/sync-lock.test.js dist/__tests__/push-cas.test.js dist/__tests__/upload.test.js dist/__tests__/progress.test.js dist/__tests__/updater.test.js dist/__tests__/cli-smoke.test.js dist/__tests__/claude-noninteractive.test.js dist/__tests__/claude-trust.test.js dist/__tests__/relay-state.test.js dist/__tests__/relay-daemon.test.js dist/__tests__/relay-installers.test.js dist/__tests__/relay-bridge-abort.test.js dist/__tests__/relay-redact.test.js dist/__tests__/relay-machine-id.test.js dist/__tests__/stream-json.test.js dist/__tests__/relay-ingest-contract.test.js dist/__tests__/prompts.test.js dist/__tests__/capture-transcript.test.js dist/__tests__/flag-aliases.test.js dist/__tests__/adopt-cwd.test.js dist/__tests__/cli-cmd-agent.test.js dist/__tests__/cli-cmd-approval.test.js dist/__tests__/cli-cmd-audit.test.js dist/__tests__/cli-cmd-chat.test.js dist/__tests__/cli-cmd-credits.test.js dist/__tests__/cli-cmd-db.test.js dist/__tests__/cli-cmd-deploy.test.js dist/__tests__/cli-cmd-domain.test.js dist/__tests__/cli-cmd-email.test.js dist/__tests__/cli-cmd-file.test.js dist/__tests__/cli-cmd-fn.test.js dist/__tests__/cli-cmd-service.test.js dist/__tests__/cli-cmd-job.test.js dist/__tests__/cli-cmd-generate.test.js dist/__tests__/cli-cmd-gmail.test.js dist/__tests__/cli-cmd-info.test.js dist/__tests__/cli-cmd-init.test.js dist/__tests__/cli-cmd-location.test.js dist/__tests__/cli-cmd-text.test.js dist/__tests__/cli-cmd-login.test.js dist/__tests__/cli-cmd-logout.test.js dist/__tests__/cli-cmd-logs.test.js dist/__tests__/cli-cmd-memory.test.js dist/__tests__/cli-cmd-page.test.js dist/__tests__/cli-cmd-plan.test.js dist/__tests__/cli-cmd-project.test.js dist/__tests__/cli-cmd-rbac.test.js dist/__tests__/cli-cmd-realtime.test.js dist/__tests__/cli-cmd-records.test.js dist/__tests__/cli-cmd-relay.test.js dist/__tests__/cli-cmd-sandbox.test.js dist/__tests__/cli-cmd-add.test.js dist/__tests__/cli-cmd-remove.test.js dist/__tests__/cli-cmd-skill.test.js dist/__tests__/cli-cmd-test.test.js dist/__tests__/cli-cmd-workflow.test.js dist/__tests__/setup-skills-block.test.js dist/__tests__/setup-hooks.test.js",
|
|
15
|
+
"test:smoke": "tsc && node --test dist/__tests__/utils.test.js dist/__tests__/config.test.js dist/__tests__/sync.test.js dist/__tests__/sync-apply.test.js dist/__tests__/sync-lock.test.js dist/__tests__/push-cas.test.js dist/__tests__/upload.test.js dist/__tests__/progress.test.js dist/__tests__/updater.test.js dist/__tests__/cli-smoke.test.js dist/__tests__/claude-noninteractive.test.js dist/__tests__/claude-trust.test.js dist/__tests__/relay-state.test.js dist/__tests__/relay-daemon.test.js dist/__tests__/relay-installers.test.js dist/__tests__/relay-bridge-abort.test.js dist/__tests__/relay-redact.test.js dist/__tests__/relay-machine-id.test.js dist/__tests__/stream-json.test.js dist/__tests__/relay-ingest-contract.test.js dist/__tests__/prompts.test.js dist/__tests__/capture-transcript.test.js dist/__tests__/flag-aliases.test.js dist/__tests__/adopt-cwd.test.js dist/__tests__/cli-cmd-agent.test.js dist/__tests__/cli-cmd-approval.test.js dist/__tests__/cli-cmd-audit.test.js dist/__tests__/cli-cmd-chat.test.js dist/__tests__/cli-cmd-credits.test.js dist/__tests__/cli-cmd-db.test.js dist/__tests__/cli-cmd-deploy.test.js dist/__tests__/cli-cmd-domain.test.js dist/__tests__/cli-cmd-email.test.js dist/__tests__/cli-cmd-file.test.js dist/__tests__/cli-cmd-fn.test.js dist/__tests__/cli-cmd-service.test.js dist/__tests__/cli-cmd-job.test.js dist/__tests__/cli-cmd-generate.test.js dist/__tests__/cli-cmd-gmail.test.js dist/__tests__/cli-cmd-info.test.js dist/__tests__/cli-cmd-init.test.js dist/__tests__/cli-cmd-location.test.js dist/__tests__/cli-cmd-text.test.js dist/__tests__/cli-cmd-login.test.js dist/__tests__/cli-cmd-logout.test.js dist/__tests__/cli-cmd-token.test.js dist/__tests__/cli-cmd-logs.test.js dist/__tests__/cli-cmd-memory.test.js dist/__tests__/cli-cmd-page.test.js dist/__tests__/cli-cmd-plan.test.js dist/__tests__/cli-cmd-project.test.js dist/__tests__/cli-cmd-rbac.test.js dist/__tests__/cli-cmd-realtime.test.js dist/__tests__/cli-cmd-records.test.js dist/__tests__/cli-cmd-relay.test.js dist/__tests__/cli-cmd-sandbox.test.js dist/__tests__/cli-cmd-add.test.js dist/__tests__/cli-cmd-remove.test.js dist/__tests__/cli-cmd-skill.test.js dist/__tests__/cli-cmd-test.test.js dist/__tests__/cli-cmd-workflow.test.js dist/__tests__/setup-skills-block.test.js dist/__tests__/setup-hooks.test.js",
|
|
16
16
|
"test:e2e": "tsc && GIPITY_E2E=1 node --test --test-timeout=180000 dist/__tests__/cli-e2e-live.test.js dist/__tests__/cli-e2e-services-media-live.test.js dist/__tests__/cli-e2e-workflow-live.test.js dist/__tests__/cli-e2e-sandbox-live.test.js dist/__tests__/cli-e2e-page-fetch-live.test.js dist/__tests__/cli-e2e-page-test-live.test.js",
|
|
17
17
|
"test:e2e:sandbox": "tsc && GIPITY_E2E=1 node --test --test-timeout=180000 dist/__tests__/cli-e2e-sandbox-live.test.js"
|
|
18
18
|
},
|