gipity 1.0.411 → 1.0.413
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/auth.js +14 -5
- package/dist/commands/claude.js +36 -43
- package/dist/commands/relay.js +7 -0
- package/dist/commands/setup.js +59 -0
- package/dist/index.js +3 -1
- package/dist/login-flow.js +42 -0
- package/dist/relay/agent-token.js +75 -0
- package/dist/relay/daemon.js +64 -3
- package/dist/relay/onboarding.js +51 -23
- package/dist/relay/state.js +14 -2
- package/package.json +2 -2
package/dist/auth.js
CHANGED
|
@@ -166,6 +166,15 @@ export async function refreshTokenIfNeeded(force = false) {
|
|
|
166
166
|
const fresh = (a) => Date.now() <= new Date(a.expiresAt).getTime() - buffer;
|
|
167
167
|
if (!force && fresh(auth))
|
|
168
168
|
return; // fast path: token still good, no lock needed
|
|
169
|
+
// The specific access token the caller saw (the one the server just rejected on the
|
|
170
|
+
// force path). A locally-"fresh" token EQUAL to this can't be trusted - it's exactly
|
|
171
|
+
// what got a 401 - but a DIFFERENT fresh token means a sibling successfully rotated
|
|
172
|
+
// while we waited, and we should adopt it instead of re-refreshing with the (now
|
|
173
|
+
// single-use-consumed) refresh token. Without this, N concurrent relay dispatches
|
|
174
|
+
// that all 401 at once serialize on the lock, the first refreshes, and every other
|
|
175
|
+
// one re-tries the consumed token and hard-fails with "Session expired".
|
|
176
|
+
const rejectedAccess = auth.accessToken;
|
|
177
|
+
const adoptable = (a) => !!a && fresh(a) && (!force || a.accessToken !== rejectedAccess);
|
|
169
178
|
// If the refresh token itself has expired, re-login is genuinely required; leave the
|
|
170
179
|
// expired auth in place so the caller's existing 401 path prompts `gipity login`.
|
|
171
180
|
const refreshExp = decodeJwtExp(auth.refreshToken);
|
|
@@ -175,10 +184,10 @@ export async function refreshTokenIfNeeded(force = false) {
|
|
|
175
184
|
const release = await acquireRefreshLock();
|
|
176
185
|
try {
|
|
177
186
|
// Re-check under the lock: a sibling may have refreshed while we waited. Under
|
|
178
|
-
// force we
|
|
179
|
-
//
|
|
187
|
+
// force we adopt only a token DIFFERENT from the one that was just rejected (a
|
|
188
|
+
// sibling's freshly-rotated token), never the rejected one itself.
|
|
180
189
|
const held = readAuthFresh();
|
|
181
|
-
if (
|
|
190
|
+
if (adoptable(held)) {
|
|
182
191
|
cached = held;
|
|
183
192
|
return;
|
|
184
193
|
}
|
|
@@ -186,7 +195,7 @@ export async function refreshTokenIfNeeded(force = false) {
|
|
|
186
195
|
const apiBase = resolveApiBase();
|
|
187
196
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
188
197
|
const cur = readAuthFresh(); // a sibling may have just refreshed for us
|
|
189
|
-
if (
|
|
198
|
+
if (adoptable(cur)) {
|
|
190
199
|
cached = cur;
|
|
191
200
|
return;
|
|
192
201
|
}
|
|
@@ -218,7 +227,7 @@ export async function refreshTokenIfNeeded(force = false) {
|
|
|
218
227
|
// sibling's fresh token just landed; otherwise stop and let the caller re-login.
|
|
219
228
|
if (res.status === 401 || res.status === 403) {
|
|
220
229
|
const after = readAuthFresh();
|
|
221
|
-
if (
|
|
230
|
+
if (adoptable(after)) {
|
|
222
231
|
cached = after;
|
|
223
232
|
return;
|
|
224
233
|
}
|
package/dist/commands/claude.js
CHANGED
|
@@ -4,15 +4,16 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, readdir
|
|
|
4
4
|
import { homedir } from 'os';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { resolveCommand, spawnCommand } from '../platform.js';
|
|
7
|
-
import { getAuth,
|
|
8
|
-
import { get, post,
|
|
7
|
+
import { getAuth, sessionExpired, accessTokenExpired, refreshTokenIfNeeded } from '../auth.js';
|
|
8
|
+
import { get, post, ApiError, getAccountSlug } from '../api.js';
|
|
9
|
+
import { interactiveLogin } from '../login-flow.js';
|
|
9
10
|
import { getConfig, saveConfigAt, clearConfigCache, getApiBaseOverride, DEFAULT_API_BASE, getConfigPath } from '../config.js';
|
|
10
11
|
import { sync } from '../sync.js';
|
|
11
12
|
import { slugify, setupClaudeHooks, ensureGipityPluginInstalled, setupClaudeMd, setupAgentsMd, setupGitignore, DEFAULT_SYNC_IGNORE, isSyncIgnored } from '../setup.js';
|
|
12
13
|
import { buildProjectContextBlock as buildProjectContextBlockText, buildNewProjectPrompt, buildResumeWrap, buildFreshWrap, } from '../prompts.js';
|
|
13
14
|
import * as relayState from '../relay/state.js';
|
|
14
15
|
import { maybeOfferRelayOn, ensureDaemonRunning } from '../relay/onboarding.js';
|
|
15
|
-
import { prompt, pickOne,
|
|
16
|
+
import { prompt, pickOne, confirm } from '../utils.js';
|
|
16
17
|
import { brand, bold, info, success, warning, error as clrError, muted } from '../colors.js';
|
|
17
18
|
import { createProgressReporter } from '../progress.js';
|
|
18
19
|
import { printBanner } from '../banner.js';
|
|
@@ -118,33 +119,9 @@ async function buildProjectContextBlock(opts) {
|
|
|
118
119
|
const stats = await fetchProjectStats(opts.projectGuid, opts.cwd);
|
|
119
120
|
return buildProjectContextBlockText({ ...opts, ...stats });
|
|
120
121
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
async function interactiveLogin() {
|
|
125
|
-
const email = await prompt(' Email: ');
|
|
126
|
-
if (!email) {
|
|
127
|
-
console.error(`\n ${clrError('Email required.')}`);
|
|
128
|
-
process.exit(1);
|
|
129
|
-
}
|
|
130
|
-
await publicPost('/auth/login', { email });
|
|
131
|
-
console.log(' Check your email for a 6-digit code.\n');
|
|
132
|
-
const code = await prompt(' Code: ');
|
|
133
|
-
if (!code) {
|
|
134
|
-
console.error(`\n ${clrError('Code required.')}`);
|
|
135
|
-
process.exit(1);
|
|
136
|
-
}
|
|
137
|
-
const res = await publicPost('/auth/verify', { email, code });
|
|
138
|
-
const exp = decodeJwtExp(res.accessToken);
|
|
139
|
-
if (!exp) {
|
|
140
|
-
console.error(`\n ${clrError('Invalid token received.')}`);
|
|
141
|
-
process.exit(1);
|
|
142
|
-
}
|
|
143
|
-
const expiresAt = new Date(exp * 1000).toISOString();
|
|
144
|
-
saveAuth({ accessToken: res.accessToken, refreshToken: res.refreshToken, email, expiresAt });
|
|
145
|
-
console.log(` ${success(`Logged in (${email}).`)}`);
|
|
146
|
-
return getAuth();
|
|
147
|
-
}
|
|
122
|
+
// Interactive email+code login now lives in `login-flow.ts` (shared with
|
|
123
|
+
// `gipity setup`). Used on first login and when the server returns 401
|
|
124
|
+
// mid-command (session expired).
|
|
148
125
|
// First-run relay onboarding now lives in `relay/onboarding.ts`
|
|
149
126
|
// (`maybeOfferRelayOn`). `gipity claude` invokes it after project
|
|
150
127
|
// selection, and also calls `ensureDaemonRunning` unconditionally before
|
|
@@ -310,26 +287,42 @@ export const claudeCommand = new Command('claude')
|
|
|
310
287
|
// saved session's expiry is irrelevant when one is set — never re-login or
|
|
311
288
|
// warn about expiry in that case.
|
|
312
289
|
const hasEnvToken = Boolean(process.env.GIPITY_TOKEN?.trim());
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
// can look valid locally (refresh token not past its JWT
|
|
316
|
-
// sessionExpired() is false) yet be dead on the server
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
|
|
290
|
+
// Renew the access token UP FRONT so the login line we're about to print is
|
|
291
|
+
// actually true — in BOTH interactive and headless/relay runs. A saved
|
|
292
|
+
// session can look valid locally (refresh token not past its 7-day JWT
|
|
293
|
+
// expiry, so sessionExpired() is false) yet be dead on the server: a sibling
|
|
294
|
+
// process sharing this auth.json already consumed the single-use refresh
|
|
295
|
+
// token (GipRunner and the relay run many gipity processes against one auth
|
|
296
|
+
// dir), the user logged in elsewhere, or — in dev — the server's session
|
|
297
|
+
// store was flushed/restarted. refreshTokenIfNeeded() round-trips to the
|
|
298
|
+
// server and adopts any freshly-rotated token; if it fails, the access token
|
|
299
|
+
// stays expired and accessTokenExpired() catches it below. Headless runs
|
|
300
|
+
// USED to skip this and print "Logged in" off the cheap local check alone —
|
|
301
|
+
// then the very next sync would 401 with "Session expired" and the relay
|
|
302
|
+
// would run Claude anyway against a tree it could neither pull nor push.
|
|
303
|
+
if (auth && !hasEnvToken && !sessionExpired()) {
|
|
322
304
|
await refreshTokenIfNeeded();
|
|
323
305
|
auth = getAuth();
|
|
324
306
|
}
|
|
325
307
|
// Re-login is genuinely required when the refresh token has lapsed, OR the
|
|
326
308
|
// proactive renewal above could not produce a still-valid access token
|
|
327
|
-
// (refresh token rejected
|
|
328
|
-
//
|
|
329
|
-
//
|
|
309
|
+
// (refresh token rejected — rotated away, revoked, or the server forgot it).
|
|
310
|
+
// Checked identically for headless and interactive now: a dead session must
|
|
311
|
+
// not silently proceed in either. Never triggered while a GIPITY_TOKEN env
|
|
330
312
|
// token is supplying auth.
|
|
331
313
|
const reloginRequired = auth != null && !hasEnvToken &&
|
|
332
|
-
(sessionExpired() ||
|
|
314
|
+
(sessionExpired() || accessTokenExpired());
|
|
315
|
+
if (auth && reloginRequired && nonInteractive) {
|
|
316
|
+
// Headless (relay dispatch, CI): we can't prompt for a re-login, and
|
|
317
|
+
// running Claude now would work against a project tree we can neither pull
|
|
318
|
+
// nor push (every authenticated call 401s). Fail fast with the actionable
|
|
319
|
+
// message instead of printing "Logged in" and silently losing the user's
|
|
320
|
+
// changes. The relay surfaces this stderr line to the web CLI as the
|
|
321
|
+
// dispatch error, so the user sees "run: gipity login" instead of a run
|
|
322
|
+
// that appeared to work but synced nothing.
|
|
323
|
+
console.error(` ${clrError('Your Gipity session has expired. Run: gipity login')}`);
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
333
326
|
if (auth && reloginRequired) {
|
|
334
327
|
// The saved session is dead (refresh token lapsed, or rotated away by a
|
|
335
328
|
// sibling process), so the first API call below would 401. Re-login up
|
package/dist/commands/relay.js
CHANGED
|
@@ -13,6 +13,7 @@ import { confirm } from '../utils.js';
|
|
|
13
13
|
import { bold, brand, dim, success, error as clrError, muted, } from '../colors.js';
|
|
14
14
|
import * as state from '../relay/state.js';
|
|
15
15
|
import * as daemon from '../relay/daemon.js';
|
|
16
|
+
import { revokeRelayAgentToken } from '../relay/agent-token.js';
|
|
16
17
|
import { UnsupportedPlatformError } from '../relay/installers.js';
|
|
17
18
|
import { pairDevice, startDaemon, installAutostart, removeAutostart } from '../relay/setup.js';
|
|
18
19
|
import { registerInstallCommands } from './relay-install.js';
|
|
@@ -268,6 +269,12 @@ relayCommand
|
|
|
268
269
|
console.error(clrError(`Server revoke failed: ${err?.message || err}`));
|
|
269
270
|
console.error(muted('Local token cleared anyway. Visit the web CLI to confirm the server-side revoke.'));
|
|
270
271
|
}
|
|
272
|
+
// Also revoke the relay's agent token (minted for spawned children).
|
|
273
|
+
// Best-effort: on failure it stays visible in `gipity token list`.
|
|
274
|
+
const agentTokenRevoked = await revokeRelayAgentToken();
|
|
275
|
+
if (!agentTokenRevoked && state.getAgentToken()) {
|
|
276
|
+
console.error(muted('Could not revoke the relay agent token - check `gipity token list`.'));
|
|
277
|
+
}
|
|
271
278
|
state.clearDevice();
|
|
272
279
|
// Remove the OS autostart unit too. Otherwise the login service relaunches
|
|
273
280
|
// `relay run`, which - finding no device but a valid login - silently
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gipity setup` — get this computer ready as a relay, then stop.
|
|
3
|
+
*
|
|
4
|
+
* Runs the SAME first steps as `gipity claude` (log in, then pair + start the
|
|
5
|
+
* relay + install the OS login service) but does NOT pick a project or launch
|
|
6
|
+
* Claude Code. For the user who wants their machine available as a relay in the
|
|
7
|
+
* web CLI without being dropped into a coding session.
|
|
8
|
+
*
|
|
9
|
+
* Shares one implementation with `gipity claude`: auth via `login-flow.ts`,
|
|
10
|
+
* relay setup via `relay/onboarding.ts` (`runRelaySetup`). Nothing to maintain
|
|
11
|
+
* twice.
|
|
12
|
+
*/
|
|
13
|
+
import { Command } from 'commander';
|
|
14
|
+
import { getAuth, sessionExpired, refreshTokenIfNeeded } from '../auth.js';
|
|
15
|
+
import { interactiveLogin } from '../login-flow.js';
|
|
16
|
+
import { runRelaySetup } from '../relay/onboarding.js';
|
|
17
|
+
import * as relayState from '../relay/state.js';
|
|
18
|
+
import { bold, brand, success, muted, error as clrError } from '../colors.js';
|
|
19
|
+
export const setupCommand = new Command('setup')
|
|
20
|
+
.description('Set up this computer as a relay (no project, no launch)')
|
|
21
|
+
.action(async () => {
|
|
22
|
+
try {
|
|
23
|
+
console.log('');
|
|
24
|
+
console.log(` ${bold('Gipity setup')} ${muted('- get this computer ready as a relay')}`);
|
|
25
|
+
console.log('');
|
|
26
|
+
// ── Step 1: Auth ──────────────────────────────────────────────────
|
|
27
|
+
let auth = getAuth();
|
|
28
|
+
if (auth && !sessionExpired()) {
|
|
29
|
+
await refreshTokenIfNeeded();
|
|
30
|
+
auth = getAuth();
|
|
31
|
+
console.log(` Logged in (${auth?.email}).`);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
if (auth)
|
|
35
|
+
console.log(` ${muted('Your session expired. Let\'s sign you back in.')}\n`);
|
|
36
|
+
else
|
|
37
|
+
console.log(' Let\'s get you logged in.\n');
|
|
38
|
+
auth = await interactiveLogin();
|
|
39
|
+
}
|
|
40
|
+
console.log('');
|
|
41
|
+
// ── Step 2: Relay setup (always run — the user asked for it) ───────
|
|
42
|
+
const enabled = await runRelaySetup({ mode: 'run-now' });
|
|
43
|
+
// ── Step 3: Done. No project, no Claude Code launch. ──────────────
|
|
44
|
+
if (enabled) {
|
|
45
|
+
const running = relayState.isRelayEnabled() && !relayState.isPaused();
|
|
46
|
+
console.log(` ${success('Done')} — your relay ${running ? 'is running in the background' : 'is set up'} and will start with your computer.`);
|
|
47
|
+
console.log(` ${muted('Open')} ${brand('gipity.ai')} ${muted('and start a chat to drive Claude Code here. Manage it with `gipity relay status`.')}`);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
console.log(` ${muted('No relay set up. Run `gipity setup` again anytime, or `gipity claude` to build with Gipity.')}`);
|
|
51
|
+
}
|
|
52
|
+
console.log('');
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
console.error(`\n ${clrError(`Error: ${err.message}`)}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
//# sourceMappingURL=setup.js.map
|
package/dist/index.js
CHANGED
|
@@ -52,6 +52,7 @@ import { locationCommand } from './commands/location.js';
|
|
|
52
52
|
import { doctorCommand } from './commands/doctor.js';
|
|
53
53
|
import { updateCommand } from './commands/update.js';
|
|
54
54
|
import { relayCommand } from './commands/relay.js';
|
|
55
|
+
import { setupCommand } from './commands/setup.js';
|
|
55
56
|
import { uninstallCommand } from './commands/uninstall.js';
|
|
56
57
|
import { approvalCommand } from './commands/approval.js';
|
|
57
58
|
import { gmailCommand } from './commands/gmail.js';
|
|
@@ -125,7 +126,7 @@ const program = new Command();
|
|
|
125
126
|
program.enablePositionalOptions();
|
|
126
127
|
// ── Command groups (logical ordering within each) ──────────────────────
|
|
127
128
|
const commonGroup = [skillCommand, projectCommand, addCommand, removeCommand, deployCommand];
|
|
128
|
-
const connectGroup = [claudeCommand, relayCommand];
|
|
129
|
+
const connectGroup = [claudeCommand, setupCommand, relayCommand];
|
|
129
130
|
const projectGroup = [domainCommand, statusCommand, initCommand];
|
|
130
131
|
const filesGroup = [fileCommand, storageCommand, syncCommand, pushCommand, uploadCommand];
|
|
131
132
|
const appBuildingGroup = [testCommand, fnCommand, serviceCommand, secretsCommand, notifyCommand, paymentsCommand, jobCommand, dbCommand, logsCommand, workflowCommand, realtimeCommand, rbacCommand, auditCommand, recordsCommand];
|
|
@@ -180,6 +181,7 @@ program.configureHelp({
|
|
|
180
181
|
lines.push(bold('Quick start:'));
|
|
181
182
|
lines.push(` ${brand('gipity login')} ${dim('- authenticate first if you haven\'t already')}`);
|
|
182
183
|
lines.push(` ${brand('gipity init')} ${dim('- link this dir + write CLAUDE.md/AGENTS.md primers for your AI tool')}`);
|
|
184
|
+
lines.push(` ${brand('gipity setup')} ${dim('- set up this computer as a relay to drive from gipity.ai (no launch)')}`);
|
|
183
185
|
lines.push(` ${brand('gipity claude')} ${dim('- or launch Claude Code with Gipity tools wired in')}`);
|
|
184
186
|
lines.push('');
|
|
185
187
|
lines.push(bold('Usage:'));
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive email+code login, shared by every command that may need to log a
|
|
3
|
+
* user in mid-run (`gipity claude`, `gipity setup`). Kept in one place so the
|
|
4
|
+
* prompts, indentation, and token handling never drift between entry points.
|
|
5
|
+
*
|
|
6
|
+
* Distinct from `commands/login.ts` (the standalone `gipity login`, which also
|
|
7
|
+
* supports non-interactive `--email/--code`): this variant is the two-space
|
|
8
|
+
* indented, always-interactive flow used as a sub-step of a larger command, and
|
|
9
|
+
* it returns the fresh `AuthData` instead of exiting.
|
|
10
|
+
*/
|
|
11
|
+
import { publicPost } from './api.js';
|
|
12
|
+
import { getAuth, saveAuth } from './auth.js';
|
|
13
|
+
import { prompt, decodeJwtExp } from './utils.js';
|
|
14
|
+
import { success, error as clrError } from './colors.js';
|
|
15
|
+
/** Prompt for email + 6-digit code, persist the tokens, and return the new
|
|
16
|
+
* auth. Exits the process on empty input or a bad token (the caller can't
|
|
17
|
+
* proceed without a session). */
|
|
18
|
+
export async function interactiveLogin() {
|
|
19
|
+
const email = await prompt(' Email: ');
|
|
20
|
+
if (!email) {
|
|
21
|
+
console.error(`\n ${clrError('Email required.')}`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
await publicPost('/auth/login', { email });
|
|
25
|
+
console.log(' Check your email for a 6-digit code.\n');
|
|
26
|
+
const code = await prompt(' Code: ');
|
|
27
|
+
if (!code) {
|
|
28
|
+
console.error(`\n ${clrError('Code required.')}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
const res = await publicPost('/auth/verify', { email, code });
|
|
32
|
+
const exp = decodeJwtExp(res.accessToken);
|
|
33
|
+
if (!exp) {
|
|
34
|
+
console.error(`\n ${clrError('Invalid token received.')}`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
const expiresAt = new Date(exp * 1000).toISOString();
|
|
38
|
+
saveAuth({ accessToken: res.accessToken, refreshToken: res.refreshToken, email, expiresAt });
|
|
39
|
+
console.log(` ${success(`Logged in (${email}).`)}`);
|
|
40
|
+
return getAuth();
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=login-flow.js.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Long-lived agent API token for relay-spawned children.
|
|
3
|
+
*
|
|
4
|
+
* The daemon itself authenticates with its device token, but the children it
|
|
5
|
+
* spawns (`gipity sync`, `gipity claude -p`) used to fall back to the shared
|
|
6
|
+
* session in ~/.gipity/auth.json - and with up to 6 concurrent dispatches,
|
|
7
|
+
* sibling processes raced each other on the session's SINGLE-USE refresh
|
|
8
|
+
* token, intermittently failing with "Session expired" mid-dispatch.
|
|
9
|
+
*
|
|
10
|
+
* Instead, the daemon holds a long-lived gip_at_* agent API token (minted
|
|
11
|
+
* once, stored in relay.json 0600 next to the device token) and exports it
|
|
12
|
+
* to every child as GIPITY_TOKEN. The CLI's auth layer prefers an env token
|
|
13
|
+
* over the session, so children authenticate statelessly - no refresh, no
|
|
14
|
+
* lock, no race. If minting fails (e.g. daemon started at boot with an
|
|
15
|
+
* expired session), we return null and children fall back to the session
|
|
16
|
+
* path, which is exactly the old behavior.
|
|
17
|
+
*/
|
|
18
|
+
import { hostname } from 'os';
|
|
19
|
+
import { post, del } from '../api.js';
|
|
20
|
+
import { resolveApiBase } from '../config.js';
|
|
21
|
+
import * as state from './state.js';
|
|
22
|
+
/** Validate a stored token against the API. Only a definitive 401 counts as
|
|
23
|
+
* invalid - transient failures (network, 5xx) must not discard a good token
|
|
24
|
+
* and trigger a re-mint storm. */
|
|
25
|
+
async function tokenValid(token) {
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetch(`${resolveApiBase()}/users/me`, {
|
|
28
|
+
headers: { 'Authorization': `Bearer ${token}` },
|
|
29
|
+
});
|
|
30
|
+
return res.status !== 401;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return true; // network blip - assume still valid
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Return the relay's agent token, minting one if absent or revoked. Minting
|
|
38
|
+
* uses the caller's normal session auth (the daemon process is logged in),
|
|
39
|
+
* so it can fail when the session is dead - callers treat null as "children
|
|
40
|
+
* use session auth" (graceful degradation to the old behavior).
|
|
41
|
+
*/
|
|
42
|
+
export async function ensureRelayAgentToken() {
|
|
43
|
+
const existing = state.getAgentToken();
|
|
44
|
+
if (existing) {
|
|
45
|
+
if (await tokenValid(existing.token))
|
|
46
|
+
return existing.token;
|
|
47
|
+
state.setAgentToken(null, null); // revoked/expired - drop and re-mint
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const name = `Relay on ${state.getDevice()?.name ?? hostname()}`;
|
|
51
|
+
const res = await post('/auth/agent-tokens', { name });
|
|
52
|
+
state.setAgentToken(res.data.token, res.data.shortGuid);
|
|
53
|
+
return res.data.token;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Best-effort server-side revocation of the relay's agent token (used when
|
|
60
|
+
* the device is unpaired). Local state is cleared by clearDevice() either
|
|
61
|
+
* way; a failed server call just leaves a dead-weight token the user can
|
|
62
|
+
* still see and revoke with `gipity token list` / `revoke`. */
|
|
63
|
+
export async function revokeRelayAgentToken() {
|
|
64
|
+
const existing = state.getAgentToken();
|
|
65
|
+
if (!existing?.guid)
|
|
66
|
+
return false;
|
|
67
|
+
try {
|
|
68
|
+
await del(`/auth/agent-tokens/${encodeURIComponent(existing.guid)}`);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=agent-token.js.map
|
package/dist/relay/daemon.js
CHANGED
|
@@ -7,7 +7,7 @@ import { join } from 'path';
|
|
|
7
7
|
import { getApiBaseOverride, DEFAULT_API_BASE } from '../config.js';
|
|
8
8
|
import { getProjectsRoot } from './paths.js';
|
|
9
9
|
import { setupClaudeHooks, setupClaudeMd, setupAgentsMd, setupGitignore, DEFAULT_SYNC_IGNORE } from '../setup.js';
|
|
10
|
-
import { getAuth, readAuthFresh } from '../auth.js';
|
|
10
|
+
import { getAuth, readAuthFresh, refreshTokenIfNeeded, accessTokenExpired } from '../auth.js';
|
|
11
11
|
import { post } from '../api.js';
|
|
12
12
|
import * as state from './state.js';
|
|
13
13
|
import { createLineSplitter, parseEvent, mapEventToEntries, } from './stream-json.js';
|
|
@@ -15,6 +15,7 @@ import { IngestQueue } from './ingest-queue.js';
|
|
|
15
15
|
import { DeltaAccumulator, DeltaBatcher } from './stream-delta.js';
|
|
16
16
|
import { randomUUID } from 'crypto';
|
|
17
17
|
import { deviceFetch, bridgeAbort as bridgeAbortImpl } from './device-http.js';
|
|
18
|
+
import { ensureRelayAgentToken } from './agent-token.js';
|
|
18
19
|
import { redactEntries, redactString, normalizeSecrets } from './redact.js';
|
|
19
20
|
import { getMachineId } from './machine-id.js';
|
|
20
21
|
// Re-exported so the existing `relay-bridge-abort.test.ts` keeps working.
|
|
@@ -84,6 +85,19 @@ async function registerDevice() {
|
|
|
84
85
|
// so routine runs don't spam the log, but `gipity relay run --verbose`
|
|
85
86
|
// surfaces every dispatch decision for live troubleshooting.
|
|
86
87
|
let verboseMode = process.env.GIPITY_RELAY_VERBOSE === '1';
|
|
88
|
+
// Agent token exported to spawned children as GIPITY_TOKEN (see agent-token.ts).
|
|
89
|
+
// Resolved once at daemon startup; null = fall back to shared session auth.
|
|
90
|
+
let relayAgentToken = null;
|
|
91
|
+
/** Environment for relay-spawned `gipity` children. Adds GIPITY_TOKEN when the
|
|
92
|
+
* daemon holds an agent token so children authenticate statelessly instead of
|
|
93
|
+
* racing siblings on the session's single-use refresh token. */
|
|
94
|
+
function childEnv(extra = {}) {
|
|
95
|
+
return {
|
|
96
|
+
...process.env,
|
|
97
|
+
...(relayAgentToken ? { GIPITY_TOKEN: relayAgentToken } : {}),
|
|
98
|
+
...extra,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
87
101
|
function setVerbose(on) { verboseMode = verboseMode || on; }
|
|
88
102
|
// ANSI helpers - only colorize when stderr is a TTY.
|
|
89
103
|
const TTY = !!process.stderr.isTTY;
|
|
@@ -239,6 +253,18 @@ export async function run(opts = {}) {
|
|
|
239
253
|
process.on('exit', releasePid);
|
|
240
254
|
// Also release on our shutdown signals (exit handler sometimes doesn't fire).
|
|
241
255
|
ctx.abort.signal.addEventListener('abort', releasePid, { once: true });
|
|
256
|
+
// Long-lived agent token for spawned children (gipity sync / gipity claude).
|
|
257
|
+
// With it, children authenticate via GIPITY_TOKEN - stateless, no refresh -
|
|
258
|
+
// instead of racing sibling processes on the session's single-use refresh
|
|
259
|
+
// token. Minting needs a live session; on failure children fall back to
|
|
260
|
+
// session auth (the old behavior), so the daemon still runs.
|
|
261
|
+
relayAgentToken = await ensureRelayAgentToken();
|
|
262
|
+
if (relayAgentToken) {
|
|
263
|
+
log('info', 'agent token ready - spawned children use GIPITY_TOKEN');
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
log('warn', 'no agent token (mint failed or session dead) - children fall back to session auth');
|
|
267
|
+
}
|
|
242
268
|
log('info', 'relay started', { device: device.guid, name: device.name, pid: process.pid });
|
|
243
269
|
// Run all loops concurrently; exit when any returns (or abort fires).
|
|
244
270
|
// Cancellation poller runs alongside the dispatch loop so user-initiated
|
|
@@ -255,6 +281,9 @@ export async function run(opts = {}) {
|
|
|
255
281
|
// ─── Heartbeat loop ────────────────────────────────────────────────────
|
|
256
282
|
async function heartbeatLoop(ctx) {
|
|
257
283
|
let backoff = 0;
|
|
284
|
+
// Log the "session expired" warning only on the transition into that state,
|
|
285
|
+
// not every 60s, so a genuinely-lapsed session doesn't spam the relay log.
|
|
286
|
+
let sessionWarnLogged = false;
|
|
258
287
|
while (!ctx.abort.signal.aborted) {
|
|
259
288
|
try {
|
|
260
289
|
const r = await deviceFetch('POST', '/remote-devices/heartbeat', {}, 10_000, ctx.abort.signal);
|
|
@@ -266,6 +295,38 @@ async function heartbeatLoop(ctx) {
|
|
|
266
295
|
if (!r.ok)
|
|
267
296
|
throw new Error(`heartbeat ${r.status}`);
|
|
268
297
|
backoff = 0;
|
|
298
|
+
// Keep the USER session warm alongside the device heartbeat. The device
|
|
299
|
+
// token (used for this heartbeat and to claim dispatches) is a SEPARATE,
|
|
300
|
+
// long-lived credential from the user's OAuth session in auth.json — a
|
|
301
|
+
// healthy heartbeat says nothing about whether `gipity sync` / `gipity
|
|
302
|
+
// claude` can authenticate. The relay is long-lived but the access token
|
|
303
|
+
// lives only ~1h and the refresh token ~7d; left idle between dispatches
|
|
304
|
+
// the session lapses and the next dispatch's child scrambles to refresh
|
|
305
|
+
// (or finds it dead). Refreshing here — in the long-lived PARENT (never
|
|
306
|
+
// SIGKILL'd mid-rotate the way a per-dispatch child can be), under the
|
|
307
|
+
// shared cross-process auth lock — renews BOTH tokens well inside their
|
|
308
|
+
// windows (each refresh mints a fresh 7-day token), so a continuously
|
|
309
|
+
// running relay never gets bumped out from disuse, and dispatch children
|
|
310
|
+
// hit refreshTokenIfNeeded's fast path instead of racing to rotate the
|
|
311
|
+
// single-use token. Best-effort: it no-ops while the token is still fresh
|
|
312
|
+
// and never throws. If it can't renew, the session is genuinely dead —
|
|
313
|
+
// log it once so `gipity relay log` shows why dispatches will fail until
|
|
314
|
+
// the user re-logs in (nothing here can revive it without a TTY).
|
|
315
|
+
if (getAuth()) {
|
|
316
|
+
try {
|
|
317
|
+
await refreshTokenIfNeeded();
|
|
318
|
+
if (accessTokenExpired() && !sessionWarnLogged) {
|
|
319
|
+
log('warn', 'user session expired - dispatches will fail to sync until re-login (run: gipity login)');
|
|
320
|
+
sessionWarnLogged = true;
|
|
321
|
+
}
|
|
322
|
+
else if (!accessTokenExpired()) {
|
|
323
|
+
sessionWarnLogged = false;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
catch (err) {
|
|
327
|
+
log('debug', 'session warm failed', { err: err?.message });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
269
330
|
}
|
|
270
331
|
catch (err) {
|
|
271
332
|
if (ctx.abort.signal.aborted)
|
|
@@ -1096,7 +1157,7 @@ async function spawnSync(cwd, timeoutMs, onSpawn) {
|
|
|
1096
1157
|
return new Promise((resolve, reject) => {
|
|
1097
1158
|
const child = spawnCommand(cmd, ['sync', '--json'], {
|
|
1098
1159
|
cwd,
|
|
1099
|
-
env:
|
|
1160
|
+
env: childEnv(),
|
|
1100
1161
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1101
1162
|
});
|
|
1102
1163
|
// Hand the child to the caller so it can register the sync in `running`,
|
|
@@ -1152,7 +1213,7 @@ export async function spawnGipityClaude(args, cwd, d, queue, meta) {
|
|
|
1152
1213
|
// feed the ephemeral live-typing channel (see stream-delta.ts); whole
|
|
1153
1214
|
// assistant/tool events still arrive unchanged for the persistent path.
|
|
1154
1215
|
const fullArgs = [...args, '--output-format', 'stream-json', '--verbose', '--include-partial-messages'];
|
|
1155
|
-
const env = {
|
|
1216
|
+
const env = childEnv({ GIPITY_CONVERSATION_GUID: d.conversation_guid });
|
|
1156
1217
|
return new Promise((resolve, reject) => {
|
|
1157
1218
|
const child = spawnCommand(cmd, fullArgs, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
1158
1219
|
// `exited` fires when the child fully unwinds (exit event). Callers
|
package/dist/relay/onboarding.js
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* Interactive relay setup — the single flow that pairs this computer as a
|
|
3
|
+
* relay, starts the daemon, and (optionally) installs the OS login service.
|
|
4
|
+
* ONE implementation, shared by both entry points:
|
|
5
|
+
*
|
|
6
|
+
* - `gipity claude` → `maybeOfferRelayOn()` (mode 'offer-once': runs on the
|
|
7
|
+
* very first launch, then never nags again).
|
|
8
|
+
* - `gipity setup` → `runRelaySetup({ mode: 'run-now' })` (the user asked
|
|
9
|
+
* for it explicitly, so always run — even to re-pair or
|
|
10
|
+
* fix autostart on a machine that already answered).
|
|
11
|
+
*
|
|
12
|
+
* All the non-interactive primitives live in `setup.ts` (`pairDevice`,
|
|
13
|
+
* `startDaemon`, `installAutostart`); this module is only the prompts + copy.
|
|
7
14
|
*/
|
|
8
15
|
import { hostname } from 'os';
|
|
9
16
|
import { prompt, confirm } from '../utils.js';
|
|
10
|
-
import { bold, brand, dim, success, error as clrError, muted
|
|
17
|
+
import { bold, brand, dim, success, error as clrError, muted } from '../colors.js';
|
|
11
18
|
import * as state from './state.js';
|
|
12
19
|
import { UnsupportedPlatformError } from './installers.js';
|
|
13
20
|
import { pairDevice, startDaemon, installAutostart } from './setup.js';
|
|
@@ -16,28 +23,41 @@ import { pairDevice, startDaemon, installAutostart } from './setup.js';
|
|
|
16
23
|
* keep their import path. */
|
|
17
24
|
export const ensureDaemonRunning = startDaemon;
|
|
18
25
|
/**
|
|
19
|
-
*
|
|
20
|
-
* (`
|
|
21
|
-
* (e.g. `gipity claude -p`)
|
|
26
|
+
* Pair this computer as a relay and get it running. Returns `true` if the relay
|
|
27
|
+
* ended up enabled (or was already), `false` if the user declined or a step
|
|
28
|
+
* failed. Non-interactive flows (e.g. `gipity claude -p`) must not call this.
|
|
22
29
|
*/
|
|
23
|
-
export async function
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
export async function runRelaySetup(opts) {
|
|
31
|
+
const alreadyAnswered = state.getRelayEnabled() !== undefined;
|
|
32
|
+
// `gipity claude` first-run: honor the user's prior choice and don't re-ask.
|
|
33
|
+
if (opts.mode === 'offer-once' && alreadyAnswered) {
|
|
26
34
|
if (state.isRelayEnabled() && !state.isPaused())
|
|
27
35
|
ensureDaemonRunning();
|
|
28
|
-
return;
|
|
36
|
+
return state.isRelayEnabled();
|
|
37
|
+
}
|
|
38
|
+
// Header. `gipity setup` frames it as the deliberate action it is; the
|
|
39
|
+
// `gipity claude` first-run frames it as an optional add-on it's offering.
|
|
40
|
+
if (opts.mode === 'run-now') {
|
|
41
|
+
console.log(` ${bold('Set up this computer as a relay')}`);
|
|
42
|
+
console.log(` ${dim('A relay runs Claude Code (or Codex) here so you can drive it from the web (')}${brand('gipity.ai')}${dim(') on any browser.')}`);
|
|
43
|
+
console.log(` ${dim('It uses your Claude or Codex subscription — the cheapest way to pay for tokens.')}`);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
console.log(` ${bold('Remote control of Claude Code')}`);
|
|
47
|
+
console.log(` ${dim('Drive this Claude Code from the web (')}${brand('gipity.ai')}${dim(') on any browser (desktop or phone).')}`);
|
|
48
|
+
console.log('');
|
|
49
|
+
console.log(` ${dim('Enable now (takes 2 seconds) or turn on later with')} ${brand('gipity setup')}`);
|
|
29
50
|
}
|
|
30
|
-
console.log(` ${bold('Remote control of Claude Code')}`);
|
|
31
|
-
console.log(` ${dim('Drive this Claude Code from the web (')}${brand('gipity.ai')}${dim(') on any browser (desktop or phone).')}`);
|
|
32
|
-
console.log('');
|
|
33
|
-
console.log(` ${dim('Enable now (takes 2 seconds) or turn on later with')} ${brand('gipity relay install')}`);
|
|
34
51
|
console.log('');
|
|
35
|
-
const
|
|
52
|
+
const promptText = opts.mode === 'run-now'
|
|
53
|
+
? ' Set up remote control on this computer?'
|
|
54
|
+
: ' Enable remote control?';
|
|
55
|
+
const enable = await confirm(promptText, { default: 'yes' });
|
|
36
56
|
if (!enable) {
|
|
37
57
|
state.setRelayEnabled(false);
|
|
38
58
|
console.log(` ${muted('Skipped.')}`);
|
|
39
59
|
console.log('');
|
|
40
|
-
return;
|
|
60
|
+
return false;
|
|
41
61
|
}
|
|
42
62
|
// Device name - show hostname as the default; Enter accepts.
|
|
43
63
|
const defaultName = hostname() || 'my-pc';
|
|
@@ -46,7 +66,7 @@ export async function maybeOfferRelayOn() {
|
|
|
46
66
|
if (!name || name.length > 100) {
|
|
47
67
|
console.error(` ${clrError('Device name must be 1–100 non-whitespace characters. Skipping.')}`);
|
|
48
68
|
state.setRelayEnabled(false);
|
|
49
|
-
return;
|
|
69
|
+
return false;
|
|
50
70
|
}
|
|
51
71
|
// Create the device directly (user-auth, no pair code). `pairDevice` writes
|
|
52
72
|
// the device + flips relay_enabled on; we only have to render the outcome.
|
|
@@ -56,12 +76,12 @@ export async function maybeOfferRelayOn() {
|
|
|
56
76
|
}
|
|
57
77
|
catch (err) {
|
|
58
78
|
console.error(`\n ${clrError(`Could not create device: ${err?.message || err}`)}`);
|
|
59
|
-
console.error(` ${dim('Skipping for now - we\'ll offer again next time. Or turn it on with `gipity
|
|
79
|
+
console.error(` ${dim('Skipping for now - we\'ll offer again next time. Or turn it on with `gipity setup`.')}`);
|
|
60
80
|
// Deliberately DON'T persist relay_enabled here: the user SAID YES, and
|
|
61
81
|
// this is a transient failure (network blip, server down). Leaving the
|
|
62
82
|
// tri-state unset means onboarding re-offers on the next `gipity claude`
|
|
63
83
|
// run instead of silently opting the user out forever.
|
|
64
|
-
return;
|
|
84
|
+
return false;
|
|
65
85
|
}
|
|
66
86
|
// Start the daemon for this session.
|
|
67
87
|
const startNow = await confirm(' Start the relay now (and on future `gipity claude` runs)?', { default: 'yes' });
|
|
@@ -93,6 +113,14 @@ export async function maybeOfferRelayOn() {
|
|
|
93
113
|
console.log(` ${success(`Registered as ${bold(device.name)} (${device.guid}).`)}`);
|
|
94
114
|
console.log(` ${dim('In the Gipity web CLI, type `/claude` to dispatch messages to this PC.')}`);
|
|
95
115
|
console.log('');
|
|
96
|
-
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* First-run prompt block for `gipity claude`. Idempotent: if the user has
|
|
120
|
+
* already answered, this is a no-op (beyond keeping the daemon alive).
|
|
121
|
+
* Non-interactive flows (e.g. `gipity claude -p`) should skip calling this.
|
|
122
|
+
*/
|
|
123
|
+
export async function maybeOfferRelayOn() {
|
|
124
|
+
await runRelaySetup({ mode: 'offer-once' });
|
|
97
125
|
}
|
|
98
126
|
//# sourceMappingURL=onboarding.js.map
|
package/dist/relay/state.js
CHANGED
|
@@ -38,6 +38,8 @@ export function loadState() {
|
|
|
38
38
|
paused: Boolean(raw.paused),
|
|
39
39
|
relay_enabled: typeof raw.relay_enabled === 'boolean' ? raw.relay_enabled : undefined,
|
|
40
40
|
onboard_shown: Boolean(raw.onboard_shown),
|
|
41
|
+
agent_token: typeof raw.agent_token === 'string' ? raw.agent_token : null,
|
|
42
|
+
agent_token_guid: typeof raw.agent_token_guid === 'string' ? raw.agent_token_guid : null,
|
|
41
43
|
};
|
|
42
44
|
}
|
|
43
45
|
catch {
|
|
@@ -73,8 +75,18 @@ export function setDevice(device) {
|
|
|
73
75
|
mutate(s => { s.device = device; });
|
|
74
76
|
}
|
|
75
77
|
export function clearDevice() {
|
|
76
|
-
// Forget the device → also clear pause flag
|
|
77
|
-
|
|
78
|
+
// Forget the device → also clear the pause flag and the agent token
|
|
79
|
+
// (both scoped to the device). Server-side token revocation is the
|
|
80
|
+
// caller's job (best-effort, see revokeRelayAgentToken).
|
|
81
|
+
mutate(s => { s.device = null; s.paused = false; s.agent_token = null; s.agent_token_guid = null; });
|
|
82
|
+
}
|
|
83
|
+
// ─── Agent token (exported to spawned children as GIPITY_TOKEN) ────────
|
|
84
|
+
export function getAgentToken() {
|
|
85
|
+
const s = loadState();
|
|
86
|
+
return s.agent_token ? { token: s.agent_token, guid: s.agent_token_guid ?? null } : null;
|
|
87
|
+
}
|
|
88
|
+
export function setAgentToken(token, guid) {
|
|
89
|
+
mutate(s => { s.agent_token = token; s.agent_token_guid = guid; });
|
|
78
90
|
}
|
|
79
91
|
// ─── Pause ─────────────────────────────────────────────────────────────
|
|
80
92
|
export function isPaused() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gipity",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.413",
|
|
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",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"postbuild": "node scripts/gen-build-info.mjs",
|
|
14
14
|
"dev": "tsc --watch",
|
|
15
15
|
"test": "npm run test:smoke",
|
|
16
|
-
"test:smoke": "tsc && node --test dist/__tests__/utils.test.js dist/__tests__/platform.test.js dist/__tests__/colors.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__/auth-lock.test.js dist/__tests__/api-401-retry.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__/ingest-queue.test.js dist/__tests__/stream-delta.test.js dist/__tests__/phase-tracker.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__/client-context.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-storage.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-doctor.test.js dist/__tests__/claude-setup.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
|
+
"test:smoke": "tsc && node --test dist/__tests__/utils.test.js dist/__tests__/platform.test.js dist/__tests__/colors.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__/auth-lock.test.js dist/__tests__/api-401-retry.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__/ingest-queue.test.js dist/__tests__/stream-delta.test.js dist/__tests__/phase-tracker.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__/client-context.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-storage.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-setup.test.js dist/__tests__/cli-cmd-doctor.test.js dist/__tests__/claude-setup.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",
|
|
17
17
|
"test:smoke:quick": "node scripts/smoke-quick.mjs",
|
|
18
18
|
"test:e2e": "tsc && GIPITY_E2E=1 node --test --test-timeout=180000 dist/__tests__/cli-e2e-live.test.js dist/__tests__/cli-e2e-sync-live.test.js dist/__tests__/cli-e2e-sync-stress-live.test.js dist/__tests__/cli-e2e-rollback-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",
|
|
19
19
|
"test:e2e:sync": "tsc && GIPITY_E2E=1 node --test --test-timeout=180000 dist/__tests__/cli-e2e-sync-live.test.js",
|