gipity 1.0.411 → 1.0.412

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.
@@ -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, saveAuth, sessionExpired, accessTokenExpired, refreshTokenIfNeeded } from '../auth.js';
8
- import { get, post, publicPost, ApiError, getAccountSlug } from '../api.js';
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, decodeJwtExp, confirm } from '../utils.js';
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
- /** Interactive email+code login flow. Used on first login and when the
122
- * server returns 401 mid-command (session expired). Writes the new auth
123
- * to disk and returns it. */
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
- // For an interactive saved-session run, renew the access token UP FRONT so
314
- // the login line we're about to print is actually true. A saved session
315
- // can look valid locally (refresh token not past its JWT expiry, so
316
- // sessionExpired() is false) yet be dead on the server most often because
317
- // a sibling process sharing this auth.json already consumed the single-use
318
- // refresh token (GipRunner runs many gipity processes against one auth dir).
319
- // refreshTokenIfNeeded() attempts the rotation here; if it fails, the
320
- // access token stays expired and accessTokenExpired() catches it below.
321
- if (auth && !hasEnvToken && !nonInteractive && !sessionExpired()) {
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/rotated away). The access-token check is gated to
328
- // interactive runs: headless runs can't prompt, and their downstream API
329
- // call renews the token itself. Never triggered while a GIPITY_TOKEN env
309
+ // (refresh token rejectedrotated 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() || (!nonInteractive && accessTokenExpired()));
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
@@ -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
@@ -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';
@@ -255,6 +255,9 @@ export async function run(opts = {}) {
255
255
  // ─── Heartbeat loop ────────────────────────────────────────────────────
256
256
  async function heartbeatLoop(ctx) {
257
257
  let backoff = 0;
258
+ // Log the "session expired" warning only on the transition into that state,
259
+ // not every 60s, so a genuinely-lapsed session doesn't spam the relay log.
260
+ let sessionWarnLogged = false;
258
261
  while (!ctx.abort.signal.aborted) {
259
262
  try {
260
263
  const r = await deviceFetch('POST', '/remote-devices/heartbeat', {}, 10_000, ctx.abort.signal);
@@ -266,6 +269,38 @@ async function heartbeatLoop(ctx) {
266
269
  if (!r.ok)
267
270
  throw new Error(`heartbeat ${r.status}`);
268
271
  backoff = 0;
272
+ // Keep the USER session warm alongside the device heartbeat. The device
273
+ // token (used for this heartbeat and to claim dispatches) is a SEPARATE,
274
+ // long-lived credential from the user's OAuth session in auth.json — a
275
+ // healthy heartbeat says nothing about whether `gipity sync` / `gipity
276
+ // claude` can authenticate. The relay is long-lived but the access token
277
+ // lives only ~1h and the refresh token ~7d; left idle between dispatches
278
+ // the session lapses and the next dispatch's child scrambles to refresh
279
+ // (or finds it dead). Refreshing here — in the long-lived PARENT (never
280
+ // SIGKILL'd mid-rotate the way a per-dispatch child can be), under the
281
+ // shared cross-process auth lock — renews BOTH tokens well inside their
282
+ // windows (each refresh mints a fresh 7-day token), so a continuously
283
+ // running relay never gets bumped out from disuse, and dispatch children
284
+ // hit refreshTokenIfNeeded's fast path instead of racing to rotate the
285
+ // single-use token. Best-effort: it no-ops while the token is still fresh
286
+ // and never throws. If it can't renew, the session is genuinely dead —
287
+ // log it once so `gipity relay log` shows why dispatches will fail until
288
+ // the user re-logs in (nothing here can revive it without a TTY).
289
+ if (getAuth()) {
290
+ try {
291
+ await refreshTokenIfNeeded();
292
+ if (accessTokenExpired() && !sessionWarnLogged) {
293
+ log('warn', 'user session expired - dispatches will fail to sync until re-login (run: gipity login)');
294
+ sessionWarnLogged = true;
295
+ }
296
+ else if (!accessTokenExpired()) {
297
+ sessionWarnLogged = false;
298
+ }
299
+ }
300
+ catch (err) {
301
+ log('debug', 'session warm failed', { err: err?.message });
302
+ }
303
+ }
269
304
  }
270
305
  catch (err) {
271
306
  if (ctx.abort.signal.aborted)
@@ -1,13 +1,20 @@
1
1
  /**
2
- * One-time first-run relay onboarding. Called from `gipity claude` after
3
- * auth + project selection. Asks up to four Y/n questions (all default Y);
4
- * pressing Enter four times leaves the user with a paired + running
5
- * daemon that auto-starts on every subsequent `gipity claude` invocation,
6
- * and - if they said yes to the last question - also starts at OS login.
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, info } from '../colors.js';
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
- * First-run prompt block. Idempotent: if the user has already answered
20
- * (`relay_enabled` is a boolean), this is a no-op. Non-interactive flows
21
- * (e.g. `gipity claude -p`) should skip calling this.
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 maybeOfferRelayOn() {
24
- if (state.getRelayEnabled() !== undefined) {
25
- // Already answered - just ensure the daemon is running if they're opted in.
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 enable = await confirm(' Enable remote control?', { default: 'yes' });
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 relay install`.')}`);
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
- void info;
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gipity",
3
- "version": "1.0.411",
3
+ "version": "1.0.412",
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",