gipity 1.0.391 → 1.0.394
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/agent.js +92 -1
- package/dist/commands/db.js +6 -2
- package/dist/commands/fn.js +6 -1
- package/dist/commands/generate.js +34 -5
- package/dist/commands/page-eval.js +20 -1
- package/dist/commands/page-inspect.js +67 -9
- package/dist/commands/page-screenshot.js +26 -0
- package/dist/commands/sandbox.js +11 -0
- package/dist/commands/token.js +65 -0
- package/dist/config.js +6 -0
- package/dist/helpers/index.js +1 -1
- package/dist/helpers/output.js +23 -0
- package/dist/index.js +27 -3
- package/dist/knowledge.js +19 -5
- package/dist/relay/daemon.js +7 -1
- package/dist/relay/state.js +8 -2
- 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
|
package/dist/commands/agent.js
CHANGED
|
@@ -68,12 +68,103 @@ agentCommand
|
|
|
68
68
|
else if (field === 'temp' || field === 'temperature')
|
|
69
69
|
body.temperature = parseFloat(value);
|
|
70
70
|
else {
|
|
71
|
-
console.error(clrError(`Unknown field: ${field}. Use: model, temp`));
|
|
71
|
+
console.error(clrError(`Unknown field: ${field}. Use: model, temp (for soul/goal use \`gipity agent soul|goal\`)`));
|
|
72
72
|
process.exit(1);
|
|
73
73
|
}
|
|
74
74
|
await put(`/agents/${config.agentGuid}`, body);
|
|
75
75
|
printResult(`Set ${field} = ${value}`, opts, { success: true, field, value });
|
|
76
76
|
}));
|
|
77
|
+
/** The active agent's guid, or a clear error - the brain commands all need one. */
|
|
78
|
+
function requireAgentGuid() {
|
|
79
|
+
const config = requireConfig();
|
|
80
|
+
if (!config.agentGuid) {
|
|
81
|
+
console.error(clrError('No active agent. Switch to one with: gipity agent <name>'));
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
return config.agentGuid;
|
|
85
|
+
}
|
|
86
|
+
// --- Brain: soul / goal / rules / learn ---
|
|
87
|
+
// These hit the account-scoped /account/agents surface (the same dual-auth,
|
|
88
|
+
// app-callable routes a deployed app uses), so the CLI, the web terminal, and an
|
|
89
|
+
// app all drive the agent's brain through one set of endpoints. No more
|
|
90
|
+
// hand-rolled `curl -X PUT a.gipity.ai/agents/:guid/soul` with a scraped token.
|
|
91
|
+
agentCommand
|
|
92
|
+
.command('soul [text...]')
|
|
93
|
+
.description("Show the current agent's soul, or set it (its voice/personality)")
|
|
94
|
+
.option('--json', 'Output as JSON')
|
|
95
|
+
.action((text, opts) => run('Soul', async () => {
|
|
96
|
+
const guid = requireAgentGuid();
|
|
97
|
+
if (text && text.length) {
|
|
98
|
+
const content = text.join(' ');
|
|
99
|
+
const res = await put(`/account/agents/${guid}/soul`, { content });
|
|
100
|
+
printResult('Soul updated.', opts, res.data);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
const res = await get(`/account/agents/${guid}/soul`);
|
|
104
|
+
printResult(res.data.content || '(no soul set)', opts, res.data);
|
|
105
|
+
}
|
|
106
|
+
}));
|
|
107
|
+
agentCommand
|
|
108
|
+
.command('goal [text...]')
|
|
109
|
+
.description("Show the current agent's goal, or set it")
|
|
110
|
+
.option('--clear', 'Clear the goal (back to a plain assistant)')
|
|
111
|
+
.option('--json', 'Output as JSON')
|
|
112
|
+
.action((text, opts) => run('Goal', async () => {
|
|
113
|
+
const guid = requireAgentGuid();
|
|
114
|
+
if (opts.clear) {
|
|
115
|
+
const res = await put(`/account/agents/${guid}/goal`, { goal: null });
|
|
116
|
+
printResult('Goal cleared.', opts, res.data);
|
|
117
|
+
}
|
|
118
|
+
else if (text && text.length) {
|
|
119
|
+
const goal = text.join(' ');
|
|
120
|
+
const res = await put(`/account/agents/${guid}/goal`, { goal });
|
|
121
|
+
printResult('Goal updated.', opts, res.data);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
const res = await get(`/account/agents/${guid}/goal`);
|
|
125
|
+
printResult(res.data.goal || '(no goal set)', opts, res.data);
|
|
126
|
+
}
|
|
127
|
+
}));
|
|
128
|
+
const rulesCommand = agentCommand
|
|
129
|
+
.command('rules')
|
|
130
|
+
.description("Show the agent's rules playbook (manual + learned)")
|
|
131
|
+
.option('--json', 'Output as JSON')
|
|
132
|
+
.action((opts) => run('Rules', async () => {
|
|
133
|
+
const guid = requireAgentGuid();
|
|
134
|
+
const res = await get(`/account/agents/${guid}/rules`);
|
|
135
|
+
printList(res.data, opts, 'No rules yet.', r => `[${r.source}] ${r.short_guid} ${r.text}`);
|
|
136
|
+
}));
|
|
137
|
+
rulesCommand
|
|
138
|
+
.command('add <text...>')
|
|
139
|
+
.description('Add a manual rule')
|
|
140
|
+
.option('--json', 'Output as JSON')
|
|
141
|
+
.action((text, opts) => run('Add', async () => {
|
|
142
|
+
const guid = requireAgentGuid();
|
|
143
|
+
const res = await post(`/account/agents/${guid}/rules`, { text: text.join(' ') });
|
|
144
|
+
printResult(`Added rule ${res.data[0].short_guid}.`, opts, res.data[0]);
|
|
145
|
+
}));
|
|
146
|
+
rulesCommand
|
|
147
|
+
.command('rm <rule-guid>')
|
|
148
|
+
.alias('delete')
|
|
149
|
+
.description('Deactivate a rule by its guid')
|
|
150
|
+
.option('--json', 'Output as JSON')
|
|
151
|
+
.action((ruleGuid, opts) => run('Remove', async () => {
|
|
152
|
+
const guid = requireAgentGuid();
|
|
153
|
+
await del(`/account/agents/${guid}/rules/${ruleGuid}`);
|
|
154
|
+
printResult(`Removed rule ${ruleGuid}.`, opts, { removed: ruleGuid });
|
|
155
|
+
}));
|
|
156
|
+
agentCommand
|
|
157
|
+
.command('learn')
|
|
158
|
+
.description("Teach the agent from one correction (distills a durable learned rule)")
|
|
159
|
+
.requiredOption('--original <text>', 'What the agent originally produced')
|
|
160
|
+
.requiredOption('--comment <text>', "Your correction / why it was wrong")
|
|
161
|
+
.option('--json', 'Output as JSON')
|
|
162
|
+
.action((opts) => run('Learn', async () => {
|
|
163
|
+
const guid = requireAgentGuid();
|
|
164
|
+
const res = await post(`/account/agents/${guid}/learn`, { original: opts.original, comment: opts.comment });
|
|
165
|
+
const d = res.data;
|
|
166
|
+
printResult(d.saved ? `Learned: ${d.rule.text}` : `No rule saved (${d.reason || 'too idiosyncratic to generalize'}).`, opts, d);
|
|
167
|
+
}));
|
|
77
168
|
agentCommand
|
|
78
169
|
.command('rename <new-name>')
|
|
79
170
|
.description('Rename the current agent')
|
package/dist/commands/db.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Command } from 'commander';
|
|
|
2
2
|
import { get, post, sendMessage } from '../api.js';
|
|
3
3
|
import { requireConfig } from '../config.js';
|
|
4
4
|
import { error as clrError, success } from '../colors.js';
|
|
5
|
-
import { run, printList } from '../helpers/index.js';
|
|
5
|
+
import { run, printList, emitField } from '../helpers/index.js';
|
|
6
6
|
import { confirm } from '../utils.js';
|
|
7
7
|
export const dbCommand = new Command('db')
|
|
8
8
|
.description('Manage databases');
|
|
@@ -10,6 +10,7 @@ dbCommand
|
|
|
10
10
|
.command('query <sql>')
|
|
11
11
|
.description('Run SQL')
|
|
12
12
|
.option('--database <name>', 'Database name')
|
|
13
|
+
.option('--field <path>', 'Print only this field of the result (dot path, e.g. rows.0.status)')
|
|
13
14
|
.option('--json', 'Output as JSON')
|
|
14
15
|
.action((sql, opts) => run('Query', async () => {
|
|
15
16
|
const config = requireConfig();
|
|
@@ -24,7 +25,10 @@ dbCommand
|
|
|
24
25
|
dbName = listRes.data[0].friendlyName;
|
|
25
26
|
}
|
|
26
27
|
const res = await post(`/projects/${config.projectGuid}/db/query`, { sql, database: dbName });
|
|
27
|
-
if (opts.
|
|
28
|
+
if (opts.field) {
|
|
29
|
+
emitField(res.data, opts.field);
|
|
30
|
+
}
|
|
31
|
+
else if (opts.json) {
|
|
28
32
|
console.log(JSON.stringify(res.data));
|
|
29
33
|
}
|
|
30
34
|
else {
|
package/dist/commands/fn.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Command } from 'commander';
|
|
|
2
2
|
import { get, post, del } from '../api.js';
|
|
3
3
|
import { requireConfig } from '../config.js';
|
|
4
4
|
import { error as clrError, bold, muted, success } from '../colors.js';
|
|
5
|
-
import { run, printList } from '../helpers/index.js';
|
|
5
|
+
import { run, printList, emitField } from '../helpers/index.js';
|
|
6
6
|
import { confirm } from '../utils.js';
|
|
7
7
|
export const fnCommand = new Command('fn')
|
|
8
8
|
.description('Manage functions');
|
|
@@ -38,12 +38,17 @@ fnCommand
|
|
|
38
38
|
.command('call <name> [body]')
|
|
39
39
|
.description('Call a function')
|
|
40
40
|
.option('--data <json>', 'JSON request body')
|
|
41
|
+
.option('--field <path>', 'Print only this field of the result (dot path, e.g. items.0.short_guid)')
|
|
41
42
|
.option('--json', 'Output as JSON')
|
|
42
43
|
.action((name, bodyArg, opts) => run('Call', async () => {
|
|
43
44
|
const config = requireConfig();
|
|
44
45
|
const raw = bodyArg || opts.data || '{}';
|
|
45
46
|
const body = JSON.parse(raw);
|
|
46
47
|
const res = await post(`/api/${config.projectGuid}/fn/${encodeURIComponent(name)}`, body);
|
|
48
|
+
if (opts.field) {
|
|
49
|
+
emitField(res.data, opts.field);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
47
52
|
console.log(opts.json ? JSON.stringify(res.data) : JSON.stringify(res.data, null, 2));
|
|
48
53
|
}));
|
|
49
54
|
fnCommand
|
|
@@ -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')
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { Command } from 'commander';
|
|
2
|
+
import { Command, Option } from 'commander';
|
|
3
3
|
import { post, get, ApiError } from '../api.js';
|
|
4
4
|
import { brand, bold, muted, warning } from '../colors.js';
|
|
5
5
|
import { run } from '../helpers/index.js';
|
|
@@ -122,6 +122,13 @@ export function evalExecTimeoutMessage(result) {
|
|
|
122
122
|
`and cannot extend it. Split a long interactive check into several shorter 'page eval' calls (e.g. ` +
|
|
123
123
|
`one per state to verify), keeping each body's in-page waits well under ${EVAL_EXEC_BUDGET_MS / 1000}s.`);
|
|
124
124
|
}
|
|
125
|
+
// Agents instinctively reach for a flag to pass the script (`--js`, `--script`,
|
|
126
|
+
// `--code`, …); the JS is actually the positional <expr> (or --file for a saved
|
|
127
|
+
// script). Without these, commander answers `--js` with "did you mean --json?" —
|
|
128
|
+
// a trap, since --json is a real flag that changes output but still leaves the
|
|
129
|
+
// script unset, sending the agent in a loop. Capture the common guesses as
|
|
130
|
+
// hidden decoy options so the action can redirect to the positional arg exactly.
|
|
131
|
+
const JS_DECOY_FLAGS = ['--js', '--javascript', '--script', '--code', '--expr', '--eval', '--exec'];
|
|
125
132
|
// The long-tail escape hatch alongside `page inspect`'s fixed bundle: when the
|
|
126
133
|
// curated metrics don't cover what you need (computed styles, element rects,
|
|
127
134
|
// visibility, z-index stacks), eval an expression in page context and get the
|
|
@@ -143,6 +150,13 @@ export const pageEvalCommand = new Command('eval')
|
|
|
143
150
|
.option('--wait-timeout <ms>', 'Max ms to wait for --wait-for before giving up', '5000')
|
|
144
151
|
.option('--json', 'Output as JSON')
|
|
145
152
|
.action((url, exprArg, opts) => run('Page eval', async () => {
|
|
153
|
+
// A JS-intent flag guess (captured as a hidden decoy below): redirect to the
|
|
154
|
+
// positional <expr> precisely, before the inline/--file shape checks fire.
|
|
155
|
+
const decoy = JS_DECOY_FLAGS.find((f) => opts[f.slice(2)] !== undefined);
|
|
156
|
+
if (decoy) {
|
|
157
|
+
pageEvalCommand.error(`error: ${decoy} is not a flag — pass the JavaScript as the positional <expr> argument ` +
|
|
158
|
+
`(or --file <path> for a saved script), e.g. gipity page eval "<url>" 'document.title'`);
|
|
159
|
+
}
|
|
146
160
|
// Arg-shape errors go through commander's error() so the enableHelpAfterError
|
|
147
161
|
// hook renders this command's help inline with the one-line error LAST
|
|
148
162
|
// (survives `| tail`), same as commander-detected errors like a missing url.
|
|
@@ -225,6 +239,11 @@ export const pageEvalCommand = new Command('eval')
|
|
|
225
239
|
}
|
|
226
240
|
}
|
|
227
241
|
}));
|
|
242
|
+
// Register the JS-intent flag guesses as hidden decoys (take a value so they
|
|
243
|
+
// swallow the script the agent passed) — the action turns any of them into the
|
|
244
|
+
// precise "JS is the positional arg" redirect above.
|
|
245
|
+
for (const f of JS_DECOY_FLAGS)
|
|
246
|
+
pageEvalCommand.addOption(new Option(`${f} <value>`).hideHelp());
|
|
228
247
|
// Each `page eval` call runs to completion before the next starts, so two evals
|
|
229
248
|
// fired back-to-back never coexist in time - they CANNOT test whether two live
|
|
230
249
|
// clients see each other (presence, shared state). For that, use the genuinely-
|
|
@@ -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,55 @@ 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
|
+
// ── Strip the platform's own instrumentation noise first ──
|
|
66
|
+
// Every deployed page loads Gipity's injected analytics SDK, which POSTs to
|
|
67
|
+
// Gipity's traffic/error log endpoints (`/api/<guid>/log/traffic|error`).
|
|
68
|
+
// Those are platform infrastructure, not the app's resources, so when one
|
|
69
|
+
// fails it surfaces as a failed resource on the Gipity host PLUS a generic,
|
|
70
|
+
// URL-less "Failed to load resource" console error — identical noise on
|
|
71
|
+
// essentially every deployed app. Drop both so an agent inspecting the app
|
|
72
|
+
// it just built sees only its own code's resources, not the platform's.
|
|
73
|
+
const isPlatformLog = (entry) => {
|
|
74
|
+
const urlPart = entry.replace(/\s*\([^)]*\)\s*$/, '');
|
|
75
|
+
try {
|
|
76
|
+
const u = new URL(urlPart);
|
|
77
|
+
return /(^|\.)gipity\.ai$/.test(u.hostname) && /\/log\/(traffic|error)$/.test(u.pathname);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const platformFailures = (b.failedResources || []).filter(isPlatformLog);
|
|
84
|
+
b.failedResources = (b.failedResources || []).filter((r) => !isPlatformLog(r));
|
|
85
|
+
// Each failed platform POST also emits exactly one generic, URL-less
|
|
86
|
+
// "Failed to load resource" console error. Drop one per platform failure —
|
|
87
|
+
// the text is identical, so removing by count is exact and any genuine app
|
|
88
|
+
// 404 keeps its own (indistinguishable) line.
|
|
89
|
+
let platformConsoleToDrop = platformFailures.length;
|
|
90
|
+
if (platformConsoleToDrop > 0) {
|
|
91
|
+
b.console = (b.console || []).filter((l) => {
|
|
92
|
+
if (platformConsoleToDrop > 0 && /^error:\s*Failed to load resource:/i.test(l)) {
|
|
93
|
+
platformConsoleToDrop--;
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
// Pull message-less cross-origin "Script error." lines out first. They carry
|
|
100
|
+
// no source/stack, so they're never actionable as app-code defects, and on a
|
|
101
|
+
// Gipity-deployed page the platform's own injected SDK is itself a
|
|
102
|
+
// cross-origin script — so these are reported separately (not as app console
|
|
103
|
+
// errors, and not folded into the re-probe count) instead of misleading the
|
|
104
|
+
// agent into chasing its own code.
|
|
105
|
+
const crossOriginErrors = (b.console || []).filter(isMessagelessCrossOrigin);
|
|
106
|
+
b.console = (b.console || []).filter((l) => !isMessagelessCrossOrigin(l));
|
|
107
|
+
// Self-verify the remaining console errors before flagging them. A
|
|
108
|
+
// freshly-deployed page's first hit can throw a one-time, non-reproducible
|
|
109
|
+
// error from an asset still propagating — and reporting it as a real defect
|
|
110
|
+
// sends agents chasing a phantom. So when the first probe reports error-level
|
|
111
|
+
// console lines, re-probe once (the sticky session is now warm) and keep only
|
|
112
|
+
// the errors that recur; errors seen on a single probe are surfaced
|
|
113
|
+
// separately as transient noise.
|
|
65
114
|
let transientErrors = [];
|
|
66
115
|
if ((b.console || []).some(isErrorLine)) {
|
|
67
116
|
try {
|
|
@@ -76,7 +125,11 @@ export const pageInspectCommand = new Command('inspect')
|
|
|
76
125
|
}
|
|
77
126
|
}
|
|
78
127
|
if (opts.json) {
|
|
79
|
-
console.log(JSON.stringify(
|
|
128
|
+
console.log(JSON.stringify({
|
|
129
|
+
...b,
|
|
130
|
+
...(transientErrors.length ? { transientConsole: transientErrors } : {}),
|
|
131
|
+
...(crossOriginErrors.length ? { crossOriginConsole: crossOriginErrors } : {}),
|
|
132
|
+
}));
|
|
80
133
|
return;
|
|
81
134
|
}
|
|
82
135
|
const timing = b.timing || { ttfb: 0, domReady: 0, load: 0 };
|
|
@@ -112,7 +165,12 @@ export const pageInspectCommand = new Command('inspect')
|
|
|
112
165
|
for (const line of transientErrors) {
|
|
113
166
|
console.log(muted(line));
|
|
114
167
|
}
|
|
115
|
-
console.log(muted('One-time cold-load artifact (first hit of freshly-deployed assets
|
|
168
|
+
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.'));
|
|
169
|
+
}
|
|
170
|
+
// ── Cross-origin console errors (message-less; source hidden by the browser) ──
|
|
171
|
+
if (crossOriginErrors.length > 0) {
|
|
172
|
+
console.log(`\n${bold('Cross-origin console errors')} ${muted(`(${crossOriginErrors.length}, source hidden by the browser)`)}:`);
|
|
173
|
+
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
174
|
}
|
|
117
175
|
// ── Failed Resources ──
|
|
118
176
|
// Browsers auto-request /favicon.ico at the site root for every page, so a
|
|
@@ -100,6 +100,7 @@ export const pageScreenshotCommand = new Command('screenshot')
|
|
|
100
100
|
// so the `?? opts.wait` merge below would never see the --wait alias. Default
|
|
101
101
|
// is applied in the merge instead.
|
|
102
102
|
.option('--post-load-delay <ms>', 'Delay after DOMContentLoaded before capture, in ms (default: 1000)')
|
|
103
|
+
.option('--action <js>', 'Run JS in the page before capturing — e.g. click a button to enter a state ("document.getElementById(\'play\').click()"). Runs after the post-load delay, then settles again before the shot.')
|
|
103
104
|
.option('--full', 'Capture the full scrollable page (default: viewport only)')
|
|
104
105
|
.option('-o, --output <file>', 'Output path (single viewport only; default .gipity/screenshots/ss-<host>-<timestamp>.png)')
|
|
105
106
|
.option('--device <names>', `Viewport preset(s): ${Object.keys(DEVICE_PRESETS).join(', ')} (comma-separated or repeat flag)`, appendOption, [])
|
|
@@ -140,6 +141,7 @@ export const pageScreenshotCommand = new Command('screenshot')
|
|
|
140
141
|
reloadBetween: opts.reloadBetween !== false,
|
|
141
142
|
...(userSpecifiedViewports ? { viewports: customViewports } : {}),
|
|
142
143
|
...(opts.fakeMedia ? { fakeMedia: true } : {}),
|
|
144
|
+
...(opts.action ? { action: opts.action } : {}),
|
|
143
145
|
};
|
|
144
146
|
const entries = await postForTarEntries('/tools/browser/screenshot', body);
|
|
145
147
|
const metaEntry = entries.find((e) => e.name === 'meta.json');
|
|
@@ -231,4 +233,28 @@ export const pageScreenshotCommand = new Command('screenshot')
|
|
|
231
233
|
console.log(`${label('Screenshot file')} ${success(savedFiles[i])}`);
|
|
232
234
|
}
|
|
233
235
|
}));
|
|
236
|
+
// `screenshot` captures the page AFTER load + settle (+ optional --action). It does
|
|
237
|
+
// NOT scroll or wait for a selector before capture (agents reach for --scroll/
|
|
238
|
+
// --selector and get an unknown-option detour). State the supported levers right
|
|
239
|
+
// here, so the help (rendered on any bad flag, and this 'after' block survives
|
|
240
|
+
// `| tail`/`| grep`) ends the hunt in one shot. --action covers "click, then shoot";
|
|
241
|
+
// --full + crop covers off-screen regions; `page eval` reads data without a picture.
|
|
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
|
+
gipity page screenshot "https://dev.gipity.ai/me/app/" \\
|
|
248
|
+
--action "document.getElementById('play').click()" # capture an in-game frame
|
|
249
|
+
|
|
250
|
+
Capturing a state that needs an interaction (start a game, open a menu, dismiss a modal)?
|
|
251
|
+
Use --action to run JS in the page before the shot — it fires after the post-load
|
|
252
|
+
delay, then settles again so the result has painted. Do NOT hand-roll a 'page eval'
|
|
253
|
+
that returns a base64 image: the eval result is capped (~16KB) and truncates the PNG.
|
|
254
|
+
|
|
255
|
+
Capturing an off-screen region or reading element data?
|
|
256
|
+
• --full captures the ENTIRE scrollable page (then crop to the region).
|
|
257
|
+
• 'gipity page eval <url> "<expr>"' reads any (even off-screen) element's
|
|
258
|
+
data/state/rect without a picture — e.g. read the chart's bar values
|
|
259
|
+
directly instead of screenshotting the slide.`);
|
|
234
260
|
//# sourceMappingURL=page-screenshot.js.map
|
package/dist/commands/sandbox.js
CHANGED
|
@@ -150,6 +150,17 @@ GCC/Rust).
|
|
|
150
150
|
}
|
|
151
151
|
const timeout = parseInt(opts.timeout, 10);
|
|
152
152
|
const cwd = resolveRelativeCwd();
|
|
153
|
+
// Push local working-tree changes up before executing. The sandbox mirrors
|
|
154
|
+
// the *server* (VFS), not the local cwd, so any input staged outside Claude's
|
|
155
|
+
// Write/Edit auto-push hook - a Bash `cp`/`ffmpeg`/redirect, or any external
|
|
156
|
+
// process - would otherwise be invisible to the run and the first invocation
|
|
157
|
+
// would silently miss its inputs. Syncing first makes the auto-mirror reflect
|
|
158
|
+
// local state regardless of how files got there ("no manual copy needed").
|
|
159
|
+
// Bidirectional + CAS, so it's a cheap manifest check when nothing changed.
|
|
160
|
+
// Symmetric with the post-run pull below. Skip in one-off mode (no project).
|
|
161
|
+
if (getConfigPath()) {
|
|
162
|
+
await sync({ interactive: false });
|
|
163
|
+
}
|
|
153
164
|
const res = await post(`/projects/${config.projectGuid}/sandbox/execute`, {
|
|
154
165
|
code: source,
|
|
155
166
|
language,
|
|
@@ -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/helpers/index.js
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
* CLI helpers - shared patterns across all commands.
|
|
3
3
|
*/
|
|
4
4
|
export { run } from './command.js';
|
|
5
|
-
export { printOutput, printList, printResult } from './output.js';
|
|
5
|
+
export { printOutput, printList, printResult, pluckField, emitField } from './output.js';
|
|
6
6
|
export { syncBeforeAction } from './sync.js';
|
|
7
7
|
//# sourceMappingURL=index.js.map
|
package/dist/helpers/output.js
CHANGED
|
@@ -63,6 +63,29 @@ export function printResult(text, opts, jsonData) {
|
|
|
63
63
|
}
|
|
64
64
|
console.log(text);
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Pluck a nested value out of a result by dot-path. Numeric segments index into
|
|
68
|
+
* arrays, so `items.0.short_guid` reaches the first item's guid. Returns
|
|
69
|
+
* `undefined` if any segment along the way is missing. This is what lets
|
|
70
|
+
* `--field` replace the `... | node -e "JSON.parse(...)"` extraction dance.
|
|
71
|
+
*/
|
|
72
|
+
export function pluckField(data, path) {
|
|
73
|
+
return path.split('.').reduce((acc, key) => (acc == null ? undefined : acc[key]), data);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Emit a single plucked field for `--field`. Scalars print raw (no quotes) so
|
|
77
|
+
* the output drops straight into `$(...)` / pipes; objects/arrays print as
|
|
78
|
+
* compact JSON. A missing path is an error (exit 1) so scripts fail loudly
|
|
79
|
+
* instead of silently consuming an empty string.
|
|
80
|
+
*/
|
|
81
|
+
export function emitField(data, path) {
|
|
82
|
+
const value = pluckField(data, path);
|
|
83
|
+
if (value === undefined) {
|
|
84
|
+
console.error(`Field not found: ${path}`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
console.log(typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value));
|
|
88
|
+
}
|
|
66
89
|
/**
|
|
67
90
|
* Print a list with JSON mode, empty state, and per-item formatting.
|
|
68
91
|
* Replaces the most common output pattern across all commands.
|
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
|
@@ -20,6 +20,7 @@ Templates:
|
|
|
20
20
|
- \`3d-world\` - Multiplayer world, 3D sandbox, shooter, exploration, virtual showroom (Three.js + Rapier + Colyseus)
|
|
21
21
|
- \`api\` - Backend service, webhook, data pipeline, chatbot, cron job - no frontend
|
|
22
22
|
- \`karaoke-captions\` - Forced-alignment app - karaoke captions, subtitle timing, language learning, dubbing alignment
|
|
23
|
+
- \`outreach-agent\` - AI outreach / drip-email funnel - reach a list of people with personalized, human-approved emails that auto-send on a schedule and a self-improving agent that learns from your edits
|
|
23
24
|
When unsure, default to \`web-simple\`. After adding the template, edit the generated files, then \`gipity deploy dev\`.
|
|
24
25
|
Only skip this on a build request if the user explicitly says not to.
|
|
25
26
|
|
|
@@ -36,7 +37,8 @@ Kits are reusable building blocks added to an existing app, not whole templates
|
|
|
36
37
|
- \`gipity add i18n\` - Multi-language for web apps - language picker, locale persistence, RTL, plural/translation lookup. Scaffolds src/js/strings.js and wires it up; move your copy there and read it with t('key'). Web only.
|
|
37
38
|
- \`gipity add records\` - Registry-driven records: declare objects/fields as data, get generic CRUD functions with validation, full-text search, soft delete, ACTOR provenance, and an audit event spine - every write is transactional (row + event). Field types include relations ({id,label}), currency, emails/phones/links composites. Ships backend functions + migrations. Needs a database (web-fullstack/api template).
|
|
38
39
|
- \`gipity add views\` - Generic UI over records-kit objects: sortable/filterable table with full-text search, create/edit/delete forms with type-appropriate widgets, kanban board with drag-to-update. Renders entirely from the field registry - zero per-object UI code. Requires the records kit.
|
|
39
|
-
- \`gipity add agent-api\` - Make your app agent-operable: named API keys (kit_api_keys) let agents and scripts write through the records kit's single write path with AGENT/API actor attribution - machine writes land on the same audit spine as human edits. Requires the records kit
|
|
40
|
+
- \`gipity add agent-api\` - Make your app agent-operable: named API keys (kit_api_keys) let agents and scripts write through the records kit's single write path with AGENT/API actor attribution - machine writes land on the same audit spine as human edits. Requires the records kit.
|
|
41
|
+
- \`gipity add contacts\` - Source-agnostic contact data layer for lead-gen/CRM apps: import people from LinkedIn CSV + Gmail + pasted lists, resolve duplicates into one person while keeping EVERY value from every source with provenance (multi-valued attributes, never overwrites). Exact email/URL auto-merge; fuzzy name+company goes to a human merge-review queue (reversible). Re-imports detect job changes and emit signals. User-definable tags, full-text search, and a transactional event spine. Ships backend functions + migrations. Needs a database (web-fullstack/api template).`;
|
|
40
42
|
export const SKILLS_CONTENT = `# Gipity Integration
|
|
41
43
|
|
|
42
44
|
Gipity is the cloud platform your project runs on - hosting, databases, deployment, file storage, code execution, workflows, and monitoring. Gip is the cloud agent that runs on Gipity.
|
|
@@ -45,7 +47,7 @@ Prefer the cheapest option that works - CLI and sandbox are instant and free, ap
|
|
|
45
47
|
|
|
46
48
|
1. CLI commands (fast, no agent overhead). The \`gipity\` CLI covers add, deploy, db, fn, logs, browser, sync, memory, skill, and more. All commands support \`--json\`.
|
|
47
49
|
2. Cloud sandbox via \`gipity sandbox run\` - Docker container with pre-installed tools for media (ffmpeg, ImageMagick, sox), documents (pandoc, LibreOffice), and data (pandas, matplotlib, sqlite3). Run \`gipity skill read sandbox-tools\` for the full toolkit. No network from inside the sandbox - fetch what you need before sending it in.
|
|
48
|
-
3. App services - runtime HTTP endpoints your deployed app calls directly at \`https://a.gipity.ai/api/<PROJECT_GUID>/services/*\`. Available: LLM, TTS, image, sound, music, transcribe, video, file upload, realtime, location. Load the matching skill (\`app-llm\`, \`app-tts\`, etc.) before writing service code - they have the schemas, auth pattern, and common-mistake guards. For one-off generation during development, prefer \`gipity generate <image|video|speech|music>\` or \`gipity chat\`. \`gipity generate\` saves to a generic file in the current directory by default (e.g. \`./generated.png\`)
|
|
50
|
+
3. App services - runtime HTTP endpoints your deployed app calls directly at \`https://a.gipity.ai/api/<PROJECT_GUID>/services/*\`. Available: LLM, TTS, image, sound, music, transcribe, video, file upload, realtime, location. Load the matching skill (\`app-llm\`, \`app-tts\`, etc.) before writing service code - they have the schemas, auth pattern, and common-mistake guards. For one-off generation during development, prefer \`gipity generate <image|video|speech|music>\` or \`gipity chat\`. \`gipity generate\` saves to a generic file in the current directory by default (e.g. \`./generated.png\`) - pass \`-o <path>\` to write it straight into your source tree so it deploys (e.g. \`gipity generate image "hero banner" -o src/assets/images/hero.png\`) instead of generating at cwd and moving it.
|
|
49
51
|
4. Delegate to Gip (\`gipity chat "<task>"\`) - only when the work genuinely needs agent reasoning or a tool not in the CLI, sandbox, or app services. Required for: Twitter/X search, Gmail, calendar, push notifications, video understanding, audio source isolation, cross-model second opinions, multi-step orchestration. Don't use \`gipity chat\` for anything the sandbox can do - it's slower and burns tokens.
|
|
50
52
|
|
|
51
53
|
You are the developer. Write files in this directory - the Gipity Claude Code plugin's hooks auto-sync them to Gipity. Don't run \`npm install\`, \`npm start\`, \`node\`, or \`python\` locally; there is no local runtime. Code runs in the Gipity sandbox.
|
|
@@ -78,21 +80,32 @@ The full "when to add a template" rule and the definition of done are spelled ou
|
|
|
78
80
|
|
|
79
81
|
Build loop: \`gipity add\` → edit files → \`gipity deploy dev\` → \`gipity page inspect <url>\` → fix any errors → repeat until the definition of done is met.
|
|
80
82
|
|
|
81
|
-
\`add\` writes real files to disk
|
|
83
|
+
\`add\` writes real files to disk - Read a scaffolded file before your first Write/Edit to it, or the call fails \`"File has not been read yet"\`. Don't rewrite from memory of the template.
|
|
82
84
|
|
|
83
85
|
Make your file changes and verify they landed, then run \`gipity deploy dev\` once. \`0 uploaded, N unchanged\` means nothing changed on disk - fix the files, don't re-run deploy or probe the environment.
|
|
84
86
|
|
|
85
87
|
Before telling the user the app is online, verify the source tree is consistent: no files named like \`* (conflict from *)*\`, and every package directory has its expected canonical entry file. If a conflict artifact exists, resolve it (keep one copy), re-deploy, and re-inspect before reporting done.
|
|
86
88
|
|
|
89
|
+
## Work on an existing project that isn't local yet
|
|
90
|
+
|
|
91
|
+
If you're pointed at a project that already exists on Gipity but has no local copy - e.g. the user gives a live URL \`https://dev.gipity.ai/<account>/<slug>/\` (or \`app.gipity.ai\` for prod) and you need its files to edit them - the last path segment is the project **slug**. Pull it down by adopting it into a directory named for the slug:
|
|
92
|
+
|
|
93
|
+
\`\`\`
|
|
94
|
+
mkdir -p ~/GipityProjects/<slug> && cd ~/GipityProjects/<slug> && gipity init <slug>
|
|
95
|
+
\`\`\`
|
|
96
|
+
|
|
97
|
+
\`init\` matches the existing remote project by slug, links this directory to it, and syncs its files down (you'll see \`Found existing project ...\` and \`Synced N changes\`). There's no separate \`clone\`/\`pull\` - \`init\` against a matching slug *is* the pull. After it finishes, the files are in cwd; edit and \`gipity deploy dev\` as usual. (Already linked to a different project in this dir? Switch and pull instead: \`gipity project <slug>\` then \`gipity sync\`. List your projects with \`gipity project --json\`.)
|
|
98
|
+
|
|
87
99
|
## CLI quick reference
|
|
88
100
|
|
|
89
101
|
Key commands: \`gipity add <template|kit>\`, \`gipity deploy dev\`, \`gipity sandbox run\`, \`gipity page inspect <url>\`, \`gipity page screenshot <url>\`, \`gipity db query "SQL"\`, \`gipity fn call <name>\`, \`gipity logs fn <name>\`, \`gipity skill read <name>\`.
|
|
102
|
+
Pull an existing remote project local (given its URL/slug): \`mkdir -p ~/GipityProjects/<slug> && cd ~/GipityProjects/<slug> && gipity init <slug>\` (adopts the matching project and syncs files down - this is the "clone").
|
|
90
103
|
For deterministic text questions (letter/word counts, substring occurrences, nth word/char, anagrams), use \`gipity text analyze "<text>"\` - local and instant, no sandbox or LLM needed.
|
|
91
104
|
Run \`gipity --help\` for the full list. Use \`--help\` on any command for details.
|
|
92
105
|
|
|
93
|
-
Function return shape: \`gipity fn call\`, the in-test \`ctx.fn.call\`/\`callAs\`, and the client \`Gipity.fn\` all return your function's value **unwrapped**
|
|
106
|
+
Function return shape: \`gipity fn call\`, the in-test \`ctx.fn.call\`/\`callAs\`, and the client \`Gipity.fn\` all return your function's value **unwrapped** - read/assert \`result.field\`. Only raw HTTP/\`curl\` wraps it as \`{ data: ... }\`; never write \`result.data.field\` in a test.
|
|
94
107
|
|
|
95
|
-
Tests write to your real DB: \`gipity test\` runs the test code sandboxed, but \`ctx.fn.call\`/\`callAs\` hit your actual deployed functions, which write to the same project database the app reads from
|
|
108
|
+
Tests write to your real DB: \`gipity test\` runs the test code sandboxed, but \`ctx.fn.call\`/\`callAs\` hit your actual deployed functions, which write to the same project database the app reads from - rows a test creates persist and surface on the live page. Register \`ctx.cleanup(fn)\` in any write-test to delete what it made; the harness runs every cleanup after the suite (even on failure).
|
|
96
109
|
|
|
97
110
|
## Tool output is complete and synchronous
|
|
98
111
|
|
|
@@ -122,6 +135,7 @@ App services skills (load before calling \`/services/*\` endpoints):
|
|
|
122
135
|
- \`app-video\` - Gipity Video: models, aspect, resolution
|
|
123
136
|
|
|
124
137
|
App development skills:
|
|
138
|
+
- \`agent-deploy\` - headless auth via agent API tokens (GIPITY_TOKEN) for unattended deploys
|
|
125
139
|
- \`app-debugging\` - debug a deployed app: page inspect/eval, screenshots, function logs
|
|
126
140
|
- \`app-development\` - functions, database, and API
|
|
127
141
|
- \`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/relay/state.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Local state for `gipity relay`.
|
|
3
3
|
*
|
|
4
|
-
* One file, `~/.gipity/relay.json
|
|
4
|
+
* One file, `$GIPITY_DIR/relay.json` (default `~/.gipity/relay.json`), mode 0600:
|
|
5
5
|
* {
|
|
6
6
|
* device: { guid, name, platform, token, paired_at },
|
|
7
7
|
* // (no allowlist - daemon materializes any of the user's projects on demand)
|
|
@@ -16,7 +16,13 @@
|
|
|
16
16
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, unlinkSync } from 'fs';
|
|
17
17
|
import { join } from 'path';
|
|
18
18
|
import { homedir } from 'os';
|
|
19
|
-
|
|
19
|
+
// GIPITY_DIR scopes the relay/device state the same way it scopes auth.json (see
|
|
20
|
+
// auth.ts). Without this, a separate auth context (e.g. GIPITY_DIR=~/.giprunner-prod
|
|
21
|
+
// logged in as ec-giprunner@914-6.com) would still read the DEFAULT ~/.gipity device —
|
|
22
|
+
// which is paired to a DIFFERENT account — and project/chat creation fails with
|
|
23
|
+
// "deviceGuid does not match a paired device". Scoping it lets each auth context pair
|
|
24
|
+
// and own its own device. Unset GIPITY_DIR → ~/.gipity, unchanged for normal users.
|
|
25
|
+
const RELAY_DIR = process.env.GIPITY_DIR || join(homedir(), '.gipity');
|
|
20
26
|
const RELAY_FILE = join(RELAY_DIR, 'relay.json');
|
|
21
27
|
const FILE_MODE = 0o600;
|
|
22
28
|
function emptyState() {
|
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.394",
|
|
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
|
},
|