gipity 1.0.429 → 1.1.2

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.
Files changed (45) hide show
  1. package/README.md +67 -29
  2. package/dist/agents/claude-code.js +48 -0
  3. package/dist/agents/codex.js +45 -0
  4. package/dist/agents/grok.js +50 -0
  5. package/dist/agents/index.js +23 -0
  6. package/dist/agents/types.js +18 -0
  7. package/dist/api.js +14 -4
  8. package/dist/capture/sources/codex.js +216 -0
  9. package/dist/capture/sources/grok.js +178 -0
  10. package/dist/catalog.js +46 -0
  11. package/dist/client-context.js +2 -0
  12. package/dist/commands/add.js +9 -22
  13. package/dist/commands/build.js +1330 -0
  14. package/dist/commands/chat.js +9 -3
  15. package/dist/commands/claude.js +4 -13
  16. package/dist/commands/db.js +22 -5
  17. package/dist/commands/deploy.js +7 -0
  18. package/dist/commands/doctor.js +8 -2
  19. package/dist/commands/fn.js +24 -1
  20. package/dist/commands/init.js +9 -15
  21. package/dist/commands/logs.js +21 -2
  22. package/dist/commands/page-eval.js +25 -10
  23. package/dist/commands/page-inspect.js +7 -3
  24. package/dist/commands/page-screenshot.js +62 -23
  25. package/dist/commands/project.js +1 -1
  26. package/dist/commands/relay-install.js +1 -1
  27. package/dist/commands/relay.js +3 -3
  28. package/dist/commands/sandbox.js +62 -16
  29. package/dist/commands/setup.js +16 -10
  30. package/dist/commands/status.js +35 -7
  31. package/dist/commands/test.js +7 -0
  32. package/dist/commands/uninstall.js +25 -3
  33. package/dist/flag-aliases.js +1 -0
  34. package/dist/hooks/capture-runner.js +127 -38
  35. package/dist/index.js +1118 -361
  36. package/dist/knowledge.js +3 -3
  37. package/dist/prefs.js +42 -0
  38. package/dist/project-setup.js +2 -10
  39. package/dist/relay/daemon.js +124 -16
  40. package/dist/relay/diagnostics.js +4 -2
  41. package/dist/relay/onboarding.js +9 -9
  42. package/dist/relay/setup.js +8 -8
  43. package/dist/relay/state.js +1 -1
  44. package/dist/setup.js +258 -17
  45. package/package.json +4 -3
package/dist/knowledge.js CHANGED
@@ -54,7 +54,7 @@ Prefer the cheapest option that works - CLI and sandbox are instant and free, ap
54
54
  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, push notifications (Gipity Notify - \`gipity add notify\`, send from a function with the injected \`notify()\`; see \`app-notify\`). 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|sound|music>\` or \`gipity chat\` - direct generation always bills you (the owner), regardless of any service \`billing_mode\` (that setting only governs the deployed app's runtime calls; never flip it just to create assets). \`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.
55
55
  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.
56
56
 
57
- 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.
57
+ You are the developer. Write files in this directory - Gipity's editor hooks (installed by \`gipity init\` into Claude Code, Grok, and Codex) 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.
58
58
 
59
59
  ## Use first-party services before reaching outside
60
60
 
@@ -90,7 +90,7 @@ The full "when to add a template" rule and the definition of done are spelled ou
90
90
 
91
91
  Build loop: \`gipity add\` → edit files → \`gipity deploy dev --inspect\` → fix any errors → repeat until the definition of done is met. \`deploy --inspect\` deploys and then runs the page-inspect report on the live URL in one command; use a standalone \`gipity page inspect <url>\` only when re-checking without deploying.
92
92
 
93
- \`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.
93
+ \`add\` writes real files to disk - Read a scaffolded file with the Read tool before your first Write/Edit to it, or the call fails \`"File has not been read yet"\` (viewing it with \`cat\`/\`head\` in Bash does not count - the harness only tracks the Read tool). Don't rewrite from memory of the template.
94
94
 
95
95
  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.
96
96
 
@@ -146,7 +146,7 @@ Every tool call returns its full output with that call. There is no output buffe
146
146
 
147
147
  This directory is the app root - it holds \`.gipity.json\` (and \`gipity.yaml\` for backends) and is already your cwd, so run app commands here. Don't \`cd\` to a git root (\`git rev-parse --show-toplevel\`): when the app is nested in a larger repo that resolves outside the app.
148
148
 
149
- Write files locally - the Gipity Claude Code plugin's hooks auto-push every save to Gipity and auto-pull remote changes (images, audio from \`gipity chat\`) before each turn. Use \`gipity sync\` if things get out of sync (or if the plugin isn't installed - \`gipity status --repair-hooks\` re-enables it). Deletes are safe - use \`rollback\` with a datetime to undo, or \`file_version_restore\` for individual files.
149
+ Write files locally - Gipity's editor hooks (installed into Claude Code, Grok, and Codex by \`gipity init\`) auto-push every save to Gipity and auto-pull remote changes (images, audio from \`gipity chat\`) before each turn. Use \`gipity sync\` if things get out of sync (or if the hooks aren't installed - \`gipity status --repair-hooks\` re-enables them). Deletes are safe - use \`rollback\` with a datetime to undo, or \`file_version_restore\` for individual files.
150
150
 
151
151
  To keep local-only material (research clones, scratch data, vendored references) in the project directory without syncing or deploying it, list it in a \`.gipityignore\` at the project root - gitignore-style, one pattern per line, \`#\` comments. Ignored paths are invisible to sync in both directions; anything that already synced before being ignored stays on the server until you delete it.
152
152
 
package/dist/prefs.js ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Machine-level user preferences (~/.gipity/prefs.json) - the `gipity build`
3
+ * agent picker's memory. Deliberately NOT in a project's .gipity.json:
4
+ * per-project storage would re-ask the agent question on every new project,
5
+ * which defeats "hit enter twice".
6
+ *
7
+ * Shape:
8
+ * { "lastAgent": "claude" }
9
+ *
10
+ * The model is never stored: `gipity build` doesn't ask, so the agent's own
11
+ * default (and its own model memory) stays authoritative unless the user
12
+ * passes --model, which we forward verbatim.
13
+ */
14
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
15
+ import { homedir } from 'os';
16
+ import { join } from 'path';
17
+ const PREFS_PATH = join(homedir(), '.gipity', 'prefs.json');
18
+ export function readPrefs() {
19
+ if (!existsSync(PREFS_PATH))
20
+ return {};
21
+ try {
22
+ const parsed = JSON.parse(readFileSync(PREFS_PATH, 'utf-8'));
23
+ return {
24
+ lastAgent: typeof parsed.lastAgent === 'string' ? parsed.lastAgent : undefined,
25
+ };
26
+ }
27
+ catch {
28
+ return {}; // unreadable prefs must never block a launch
29
+ }
30
+ }
31
+ export function writePrefs(update) {
32
+ try {
33
+ const current = readPrefs();
34
+ const next = { ...current, ...update };
35
+ mkdirSync(join(homedir(), '.gipity'), { recursive: true });
36
+ writeFileSync(PREFS_PATH, JSON.stringify(next, null, 2) + '\n');
37
+ }
38
+ catch {
39
+ /* best-effort - prefs are a convenience */
40
+ }
41
+ }
42
+ //# sourceMappingURL=prefs.js.map
@@ -7,7 +7,7 @@
7
7
  import { clearConfigCache, saveConfigAt, getApiBaseOverride, DEFAULT_API_BASE } from './config.js';
8
8
  import { sync } from './sync.js';
9
9
  import { createProgressReporter } from './progress.js';
10
- import { setupClaudeHooks, setupGitignore, DEFAULT_TOOLS, DEFAULT_SYNC_IGNORE } from './setup.js';
10
+ import { setupProjectTools, DEFAULT_TOOLS, DEFAULT_SYNC_IGNORE } from './setup.js';
11
11
  import { substituteDir } from './template-vars.js';
12
12
  import { muted } from './colors.js';
13
13
  /** Write `.gipity.json` in `dir`, chdir into it so the hook/skill writers
@@ -61,15 +61,7 @@ export async function finalizeLocalProject(opts) {
61
61
  throw err;
62
62
  // soft mode - swallow; caller can log
63
63
  }
64
- const tools = opts.tools ?? DEFAULT_TOOLS;
65
- // Claude hooks (file-sync + capture) only matter if the Claude primer is
66
- // being written. Skipping them for Codex/Gemini/Cursor-only inits avoids
67
- // littering `.claude/settings.json` into projects that don't use Claude.
68
- if (tools.some(t => t.key === 'claude'))
69
- setupClaudeHooks();
70
- for (const t of tools)
71
- t.setup();
72
- setupGitignore();
64
+ setupProjectTools(opts.tools ?? DEFAULT_TOOLS);
73
65
  return { applied };
74
66
  }
75
67
  //# sourceMappingURL=project-setup.js.map
@@ -6,7 +6,7 @@ import { homedir, hostname, platform as osPlatform, loadavg, freemem, totalmem,
6
6
  import { join } from 'path';
7
7
  import { getApiBaseOverride, DEFAULT_API_BASE } from '../config.js';
8
8
  import { getProjectsRoot } from './paths.js';
9
- import { setupClaudeHooks, setupClaudeMd, setupAgentsMd, setupGitignore, DEFAULT_SYNC_IGNORE } from '../setup.js';
9
+ import { setupProjectTools, DEFAULT_SYNC_IGNORE } from '../setup.js';
10
10
  import { getAuth, readAuthFresh, refreshTokenIfNeeded, accessTokenExpired } from '../auth.js';
11
11
  import { post } from '../api.js';
12
12
  import * as state from './state.js';
@@ -24,7 +24,8 @@ import { SessionPool, PoolFullError } from './session-pool.js';
24
24
  import { getConfig } from '../config.js';
25
25
  import { getAccountSlug } from '../api.js';
26
26
  import { buildFreshWrap, buildResumeWrap, buildProjectContextBlock } from '../prompts.js';
27
- import { fetchProjectStats } from '../commands/claude.js';
27
+ import { fetchProjectStats } from '../commands/build.js';
28
+ import { getAdapterBySource } from '../agents/index.js';
28
29
  // Re-exported so the existing `relay-bridge-abort.test.ts` keeps working.
29
30
  // New callers should import from device-http.js directly.
30
31
  export const bridgeAbort = bridgeAbortImpl;
@@ -949,6 +950,15 @@ async function handleDispatch(claimed) {
949
950
  if (!syncFailed)
950
951
  pushSystem(bootstrapped ? 'Project files synced.' : 'Attached files synced.');
951
952
  }
953
+ // Which agent runs this dispatch. Older servers omit remote_type - they
954
+ // only ever dispatched Claude Code.
955
+ const adapter = getAdapterBySource(d.remote_type ?? 'claude_code');
956
+ // Non-Claude agents take the simpler hook-capture dispatch path: no
957
+ // stream-json ingest, no session pool.
958
+ if (adapter.key !== 'claude') {
959
+ await handleNonClaudeDispatch(adapter, d, cwd, queue, pushSystem, flushQueue);
960
+ return;
961
+ }
952
962
  // Phase 2: if the session pool is enabled, try to run this turn in a
953
963
  // long-lived Claude Code session (fast follow-up + clean interrupt). Any
954
964
  // failure - pool saturated, SDK error, wrap failure - falls through to the
@@ -958,8 +968,11 @@ async function handleDispatch(claimed) {
958
968
  if (handled)
959
969
  return;
960
970
  }
961
- // Build argv for `gipity claude -p …` (or with --resume). No shell - argv
962
- // as array so the message string can't be interpreted as shell syntax.
971
+ // Build argv for `gipity build --agent claude -p …` (or with --resume).
972
+ // No shell - argv as array so the message string can't be interpreted as
973
+ // shell syntax. (`build` is the renamed launcher; the flags below pass
974
+ // through it to the `claude` binary unchanged, exactly as the legacy
975
+ // `gipity claude -p` spawn did.)
963
976
  //
964
977
  // `--permission-mode bypassPermissions`: a relay dispatch has no
965
978
  // human on the other end to click "Approve" - Claude prompting would
@@ -967,7 +980,7 @@ async function handleDispatch(claimed) {
967
980
  // the device and dispatching the message; skipping the interactive
968
981
  // prompt is correct (same authority as running `claude -p` in a local
969
982
  // terminal yourself).
970
- const args = ['claude', '-p', d.message, '--permission-mode', 'bypassPermissions'];
983
+ const args = ['build', '--agent', 'claude', '-p', d.message, '--permission-mode', 'bypassPermissions'];
971
984
  // Relay sessions run in -p mode, where Claude Code's AskUserQuestion tool
972
985
  // is unavailable - so without help the model would ask clarifying
973
986
  // questions as prose the user just types back. This system-prompt
@@ -984,7 +997,7 @@ async function handleDispatch(claimed) {
984
997
  if (d.kind === 'resume' && d.remote_session_id) {
985
998
  args.push('--resume', d.remote_session_id);
986
999
  }
987
- log('debug', 'spawning gipity claude', {
1000
+ log('debug', 'spawning gipity build --agent claude', {
988
1001
  id: d.short_guid,
989
1002
  cwd,
990
1003
  args,
@@ -1123,7 +1136,92 @@ async function handleDispatch(claimed) {
1123
1136
  }
1124
1137
  else {
1125
1138
  log('warn', 'dispatch child exited nonzero', { id: d.short_guid, exitCode, ms });
1126
- await ack(d.short_guid, 'error', `gipity claude exited with code ${exitCode}${stderrNote}`);
1139
+ await ack(d.short_guid, 'error', `Claude Code exited with code ${exitCode}${stderrNote}`);
1140
+ }
1141
+ }
1142
+ /**
1143
+ * Dispatch for non-Claude agents (Codex, Grok). Deliberately simpler than
1144
+ * the Claude path: there is no stream-json ingest mapper for these agents -
1145
+ * hook-based session capture (Phase C) owns the transcript. The spawned
1146
+ * `gipity build --agent <key> -p …` child gets GIPITY_CONVERSATION_GUID (so
1147
+ * capture posts into the right conversation) and capture is NOT turned off.
1148
+ * The daemon contributes: the "Running <agent>" marker, byte-level progress
1149
+ * heartbeats (spawnGipityClaude's generic plumbing with streamJson:false),
1150
+ * post-run file sync, the tail marker, and the ack. The prompt entry itself
1151
+ * arrives via capture from the agent's own transcript - the daemon doesn't
1152
+ * push one, or it would render twice.
1153
+ */
1154
+ async function handleNonClaudeDispatch(adapter, d, cwd, queue, pushSystem, flushQueue) {
1155
+ // `gipity build` translates these into the agent's own argv via the same
1156
+ // adapter (Codex: `exec …`; Grok: `-p … --always-approve`). Raw message -
1157
+ // AGENTS.md carries the Gipity context for these agents; no wrapping.
1158
+ const args = ['build', '--agent', adapter.key, '-p', d.message, '--bypass-approvals'];
1159
+ if (d.model)
1160
+ args.push('--model', d.model);
1161
+ if (d.kind === 'resume' && d.remote_session_id)
1162
+ args.push('--resume', d.remote_session_id);
1163
+ log('debug', `spawning gipity build --agent ${adapter.key}`, {
1164
+ id: d.short_guid, cwd, args, conv: d.conversation_guid,
1165
+ chain: d.kind === 'resume' ? `resume ${d.remote_session_id}` : 'start (fresh session)',
1166
+ });
1167
+ const words = d.message.trim().split(/\s+/).filter(Boolean).length;
1168
+ pushSystem(`Running ${adapter.displayName} - ${words.toLocaleString('en-US')} words`);
1169
+ const t0 = Date.now();
1170
+ let exitCode = 1;
1171
+ let spawnErr = null;
1172
+ let killed = false;
1173
+ let runtimeLimit = false;
1174
+ let stderrTail = '';
1175
+ try {
1176
+ const result = await spawnGipityClaude(args, cwd, d, queue, { streamJson: false });
1177
+ exitCode = result.exitCode;
1178
+ killed = result.killed;
1179
+ runtimeLimit = result.runtimeLimit ?? false;
1180
+ stderrTail = result.stderrTail ?? '';
1181
+ }
1182
+ catch (err) {
1183
+ spawnErr = err?.message || String(err);
1184
+ log('error', 'dispatch spawn failed', { id: d.short_guid, err: spawnErr });
1185
+ }
1186
+ const dur = formatDuration(Date.now() - t0);
1187
+ // Same post-run reconcile as the Claude path: push files the agent wrote
1188
+ // via Bash/scripts back to VFS before the ack (hooks only cover its
1189
+ // native edit tools). Skipped on kill - the replacement dispatch owns the tree.
1190
+ if (!spawnErr && !killed) {
1191
+ try {
1192
+ await spawnSync(cwd, PROJECT_SYNC_TIMEOUT_MS);
1193
+ }
1194
+ catch (err) {
1195
+ log('warn', 'sync after dispatch failed', { id: d.short_guid, err: err?.message });
1196
+ }
1197
+ }
1198
+ const stderrNote = stderrTail ? `: ${stderrTail.slice(0, 300)}` : '';
1199
+ const tail = runtimeLimit
1200
+ ? `stopped after ${dur} (runtime limit)`
1201
+ : killed
1202
+ ? `cancelled (${dur})`
1203
+ : spawnErr
1204
+ ? `failed (${dur}: ${spawnErr})`
1205
+ : exitCode === 0
1206
+ ? `finished (${dur})`
1207
+ : `failed (${dur}, exit ${exitCode}${stderrNote})`;
1208
+ pushSystem(`${adapter.displayName} ${tail}`);
1209
+ await flushQueue();
1210
+ if (runtimeLimit) {
1211
+ await ack(d.short_guid, 'error', `${adapter.displayName} stopped after ${dur} (runtime limit)`);
1212
+ }
1213
+ else if (killed) {
1214
+ await ack(d.short_guid, 'cancelled');
1215
+ }
1216
+ else if (spawnErr) {
1217
+ await ack(d.short_guid, 'error', spawnErr);
1218
+ }
1219
+ else if (exitCode === 0) {
1220
+ log('info', 'dispatch done', { id: d.short_guid, agent: adapter.key });
1221
+ await ack(d.short_guid, 'done');
1222
+ }
1223
+ else {
1224
+ await ack(d.short_guid, 'error', `${adapter.displayName} exited with code ${exitCode}${stderrNote}`);
1127
1225
  }
1128
1226
  }
1129
1227
  /**
@@ -1372,10 +1470,7 @@ async function resolveCwdForProject(d) {
1372
1470
  const origCwd = process.cwd();
1373
1471
  try {
1374
1472
  process.chdir(path);
1375
- setupClaudeHooks();
1376
- setupClaudeMd();
1377
- setupAgentsMd();
1378
- setupGitignore();
1473
+ setupProjectTools();
1379
1474
  }
1380
1475
  finally {
1381
1476
  process.chdir(origCwd);
@@ -1562,6 +1657,11 @@ async function spawnSync(cwd, timeoutMs, onSpawn) {
1562
1657
  });
1563
1658
  }
1564
1659
  export async function spawnGipityClaude(args, cwd, d, queue, meta) {
1660
+ // streamJson (default true) = the Claude path: parse the child's
1661
+ // stream-json stdout into ingest entries and stand hook capture down.
1662
+ // false = non-Claude agents (Codex/Grok): hook capture owns the
1663
+ // transcript; stdout only feeds the byte-level progress heartbeat.
1664
+ const streamJson = meta?.streamJson !== false;
1565
1665
  // resolveCommand: on Windows the bare `gipity` is a .cmd shim that spawn
1566
1666
  // can't launch without an explicit path. An explicit env override is used
1567
1667
  // verbatim (it may be a full path); only the default name is resolved.
@@ -1572,13 +1672,19 @@ export async function spawnGipityClaude(args, cwd, d, queue, meta) {
1572
1672
  // `--include-partial-messages` adds stream_event token deltas, which
1573
1673
  // feed the ephemeral live-typing channel (see stream-delta.ts); whole
1574
1674
  // assistant/tool events still arrive unchanged for the persistent path.
1575
- const fullArgs = [...args, '--output-format', 'stream-json', '--verbose', '--include-partial-messages'];
1675
+ const fullArgs = streamJson
1676
+ ? [...args, '--output-format', 'stream-json', '--verbose', '--include-partial-messages']
1677
+ : [...args];
1576
1678
  // GIPITY_CAPTURE=off: the daemon owns capture for this dispatch (it
1577
1679
  // parses the stream-json on stdout), so the plugin's lifecycle-hook
1578
1680
  // capture must stand down. `gipity claude` sets the same sentinel on the
1579
1681
  // Claude child when it detects a daemon spawn; setting it here too keeps
1580
- // dispatches double-post-free even across CLI version skew.
1581
- const env = childEnv({ GIPITY_CONVERSATION_GUID: d.conversation_guid, GIPITY_CAPTURE: 'off' });
1682
+ // dispatches double-post-free even across CLI version skew. Non-Claude
1683
+ // agents are the opposite: hook capture IS the transcript path, so the
1684
+ // sentinel must NOT be set.
1685
+ const env = streamJson
1686
+ ? childEnv({ GIPITY_CONVERSATION_GUID: d.conversation_guid, GIPITY_CAPTURE: 'off' })
1687
+ : childEnv({ GIPITY_CONVERSATION_GUID: d.conversation_guid });
1582
1688
  return new Promise((resolve, reject) => {
1583
1689
  const child = spawnCommand(cmd, fullArgs, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
1584
1690
  // `exited` fires when the child fully unwinds (exit event). Callers
@@ -1780,9 +1886,11 @@ export async function spawnGipityClaude(args, cwd, d, queue, meta) {
1780
1886
  child.stdout?.on('data', (chunk) => {
1781
1887
  stdoutBytesTotal += chunk.length;
1782
1888
  lastStdoutByteAt = Date.now();
1783
- splitter.push(chunk);
1889
+ if (streamJson)
1890
+ splitter.push(chunk);
1784
1891
  });
1785
- child.stdout?.on('end', () => splitter.flush());
1892
+ child.stdout?.on('end', () => { if (streamJson)
1893
+ splitter.flush(); });
1786
1894
  // Stderr: human-readable only (Claude's progress bars, errors).
1787
1895
  // Kept on the daemon's own stderr for `gipity relay log`, plus a
1788
1896
  // small tail ring so a crash with no stream output can include the
@@ -137,14 +137,16 @@ export async function collectDiagnostics() {
137
137
  } })();
138
138
  // Run the subprocess probes concurrently (each already bounded + best-effort)
139
139
  // so the whole snapshot costs one timeout, not the sum of four.
140
- const [claude, codex, cursor, gpu] = await Promise.all([
141
- probeVersion('claude'), probeVersion('codex'), probeVersion('cursor'), detectGpu(),
140
+ const [claude, codex, grok, cursor, gpu] = await Promise.all([
141
+ probeVersion('claude'), probeVersion('codex'), probeVersion('grok'), probeVersion('cursor'), detectGpu(),
142
142
  ]);
143
143
  const agents = {};
144
144
  if (claude)
145
145
  agents.claude_code = claude;
146
146
  if (codex)
147
147
  agents.codex = codex;
148
+ if (grok)
149
+ agents.grok = grok;
148
150
  if (cursor)
149
151
  agents.cursor = cursor;
150
152
  return {
@@ -75,7 +75,7 @@ export async function runRelaySetup(opts) {
75
75
  console.log(` ${dim('To register this computer again — for example under a different name —')}`);
76
76
  console.log(` ${dim('unregister it first, then re-run setup:')}`);
77
77
  console.log(` ${brand('gipity relay revoke')} ${dim('# unpairs this computer and removes the login service')}`);
78
- console.log(` ${brand('gipity setup')} ${dim('# register it again (asks for a new name)')}`);
78
+ console.log(` ${brand('gipity connect')} ${dim('# register it again (asks for a new name)')}`);
79
79
  console.log('');
80
80
  }
81
81
  return true;
@@ -84,14 +84,14 @@ export async function runRelaySetup(opts) {
84
84
  // `gipity claude` first-run frames it as an optional add-on it's offering.
85
85
  if (opts.mode === 'run-now') {
86
86
  console.log(` ${bold('Set up this computer as a relay')}`);
87
- 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.')}`);
88
- console.log(` ${dim('It uses your Claude or Codex subscription — the cheapest way to pay for tokens.')}`);
87
+ console.log(` ${dim('A relay runs your coding agent (Claude Code, Codex, or Grok) here so you can drive it from the web (')}${brand('gipity.ai')}${dim(') on any browser.')}`);
88
+ console.log(` ${dim('It uses your Claude, Codex, or Grok subscription — the cheapest way to pay for tokens.')}`);
89
89
  }
90
90
  else {
91
- console.log(` ${bold('Remote control of Claude Code')}`);
92
- console.log(` ${dim('Drive this Claude Code from the web (')}${brand('gipity.ai')}${dim(') on any browser (desktop or phone).')}`);
91
+ console.log(` ${bold('Remote control of your coding agent')}`);
92
+ console.log(` ${dim('Drive your coding agent on this computer from the web (')}${brand('gipity.ai')}${dim(') on any browser (desktop or phone).')}`);
93
93
  console.log('');
94
- console.log(` ${dim('Enable now (takes 2 seconds) or turn on later with')} ${brand('gipity setup')}`);
94
+ console.log(` ${dim('Enable now (takes 2 seconds) or turn on later with')} ${brand('gipity connect')}`);
95
95
  }
96
96
  console.log('');
97
97
  const promptText = opts.mode === 'run-now'
@@ -121,7 +121,7 @@ export async function runRelaySetup(opts) {
121
121
  }
122
122
  catch (err) {
123
123
  console.error(`\n ${clrError(`Could not create device: ${err?.message || err}`)}`);
124
- console.error(` ${dim('Skipping for now - we\'ll offer again next time. Or turn it on with `gipity setup`.')}`);
124
+ console.error(` ${dim('Skipping for now - we\'ll offer again next time. Or turn it on with `gipity connect`.')}`);
125
125
  // Deliberately DON'T persist relay_enabled here: the user SAID YES, and
126
126
  // this is a transient failure (network blip, server down). Leaving the
127
127
  // tri-state unset means onboarding re-offers on the next `gipity claude`
@@ -129,7 +129,7 @@ export async function runRelaySetup(opts) {
129
129
  return false;
130
130
  }
131
131
  // Start the daemon for this session.
132
- const startNow = await confirm(' Start the relay now (and on future `gipity claude` runs)?', { default: 'yes' });
132
+ const startNow = await confirm(' Start the relay now (and on future `gipity build` runs)?', { default: 'yes' });
133
133
  if (startNow) {
134
134
  startDaemon();
135
135
  }
@@ -145,7 +145,7 @@ export async function runRelaySetup(opts) {
145
145
  // generic non-zero. The relay itself is unaffected (it's already
146
146
  // running this session, and `gipity claude` restarts it).
147
147
  console.log(` ${muted('Auto-start needs systemd, which this WSL distro has off. The relay still runs -')}`);
148
- console.log(` ${muted('it started just now and `gipity claude` restarts it. For boot-time auto-start:')}`);
148
+ console.log(` ${muted('it started just now and `gipity build` restarts it. For boot-time auto-start:')}`);
149
149
  console.log(` ${muted('add [boot] systemd=true to /etc/wsl.conf, restart WSL, then run')} ${brand('gipity relay install')}${muted('.')}`);
150
150
  }
151
151
  else {
@@ -60,8 +60,10 @@ export function resolveCliPath() {
60
60
  /**
61
61
  * Create + persist a relay device, marking the relay opted-in. Idempotent
62
62
  * unless `force`: if this machine is already paired and `!force`, the
63
- * existing device is returned untouched (no server call). With `force`, the
64
- * old device is revoked server-side (best-effort) before re-pairing.
63
+ * existing device is returned untouched (no server call). With `force`, we
64
+ * simply re-register: the server's machine_id dedup reattaches the same
65
+ * device row and rotates its token (no pre-revoke - revoking first forced a
66
+ * NEW row and orphaned the conversations bound to the old one, issue #291).
65
67
  *
66
68
  * Throws on a bad name or if the `/remote-devices` call fails (e.g. 401 when
67
69
  * the user isn't logged in) — callers translate that into their own UX.
@@ -78,12 +80,10 @@ export async function pairDevice(opts = {}) {
78
80
  };
79
81
  }
80
82
  if (existing && opts.force) {
81
- try {
82
- await post(`/remote-devices/${encodeURIComponent(existing.guid)}/revoke`, {});
83
- }
84
- catch {
85
- // Best-effort: a stale server-side device is harmless; proceed to re-pair.
86
- }
83
+ // Re-register in place: POST /remote-devices with this machine_id
84
+ // reattaches the existing row and rotates the token, which invalidates
85
+ // the old bearer anyway - a separate revoke round-trip only detached
86
+ // the row and orphaned its conversations.
87
87
  state.clearDevice();
88
88
  }
89
89
  const name = (opts.name?.trim() || friendlyDeviceName()).trim();
@@ -8,7 +8,7 @@
8
8
  * paused: boolean,
9
9
  * }
10
10
  *
11
- * The `token` field is the raw device bearer returned by /pair/claim - it
11
+ * The `token` field is the raw device bearer returned by POST /remote-devices - it
12
12
  * never leaves this file or the Authorization header. A future chunk will
13
13
  * move it to OS keychain (macOS Keychain, libsecret, wincred); the state
14
14
  * module's public surface is designed to absorb that change.