forge-remote 2.1.6 → 2.2.0

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/src/cli.js CHANGED
@@ -12,7 +12,7 @@ import { hostname, homedir } from "os";
12
12
  import qrcode from "qrcode-terminal";
13
13
 
14
14
  import { runInit } from "./init.js";
15
- import { initFirebase, getDb } from "./firebase.js";
15
+ import { initFirebase } from "./firebase.js";
16
16
  import {
17
17
  registerDesktop,
18
18
  markOffline,
@@ -32,19 +32,13 @@ import { stopAllCaptures } from "./screenshot-manager.js";
32
32
  import { scanProjects } from "./project-scanner.js";
33
33
  import { startWebhookServer, stopWebhookServer } from "./webhook-server.js";
34
34
  import { watchWebhookConfigs, stopWatching } from "./webhook-watcher.js";
35
+ import {
36
+ startClaudeSessionWatcher,
37
+ stopClaudeSessionWatcher,
38
+ } from "./claude-session-watcher.js";
35
39
  import * as log from "./logger.js";
36
40
  import { checkForUpdate } from "./update-checker.js";
37
41
  import { deployFirestoreRules } from "./google-auth.js";
38
- import { pairWithCloud } from "./cloud-mode.js";
39
- import { watchMobileAiRequests } from "./mobile-ai-watcher.js";
40
- import {
41
- saveTestflightCreds,
42
- openXcodeSigning,
43
- checkIosReadiness,
44
- } from "./ios-setup.js";
45
- import { readFile as fsReadFile } from "node:fs/promises";
46
- import { createInterface } from "node:readline/promises";
47
- import { stdin as input, stdout as output } from "node:process";
48
42
  // Channel auto-registration disabled — channels require
49
43
  // --dangerously-load-development-channels flag during research preview.
50
44
  // import { ensureChannelRegistered } from "./channel-setup.js";
@@ -174,30 +168,8 @@ program
174
168
  .description(
175
169
  "Start the desktop relay (register, heartbeat, listen for commands)",
176
170
  )
177
- .option("--cloud", "Use Forge Remote Cloud (managed mode) instead of BYOF")
178
- .action(async (opts) => {
171
+ .action(async () => {
179
172
  await checkForUpdate();
180
-
181
- if (opts.cloud) {
182
- // Managed mode — pair via 6-digit code, then run a minimal loop
183
- // that streams sessions to the cloud project. The full cloud
184
- // session-manager is wired up once auth + pairing land.
185
- try {
186
- const { userUid, desktopId } = await pairWithCloud();
187
- log.success(`Cloud relay running for ${userUid} (${desktopId})`);
188
- log.info(
189
- "Cloud session-manager streaming is wired in a follow-up — " +
190
- "pair-only mode for now.",
191
- );
192
- // Keep the process alive so the user can Ctrl+C to exit.
193
- await new Promise(() => {});
194
- } catch (e) {
195
- log.error(e.message);
196
- process.exit(1);
197
- }
198
- return;
199
- }
200
-
201
173
  initFirebase();
202
174
  const desktopId = await registerDesktop();
203
175
 
@@ -225,25 +197,6 @@ program
225
197
 
226
198
  listenForCommands(desktopId);
227
199
 
228
- // Start the Mobile AI watcher so the phone's "Ask Claude" features
229
- // can route through this relay (using the local Claude subscription).
230
- let stopMobileAi = () => {};
231
- try {
232
- const desktopDoc = await getDb()
233
- .collection("desktops")
234
- .doc(desktopId)
235
- .get();
236
- const ownerUid = desktopDoc.data()?.ownerUid;
237
- if (ownerUid) {
238
- stopMobileAi = watchMobileAiRequests(ownerUid);
239
- log.info("Mobile AI watcher started (relay-via-Claude)");
240
- } else {
241
- log.info("Skipping Mobile AI watcher — desktop has no ownerUid yet");
242
- }
243
- } catch (e) {
244
- log.warn(`Mobile AI watcher init failed: ${e.message}`);
245
- }
246
-
247
200
  // Start webhook server with tunnel for external integrations.
248
201
  const webhookServer = await startWebhookServer(desktopId);
249
202
  if (webhookServer) {
@@ -251,6 +204,12 @@ program
251
204
  log.info(`Webhook server ready at ${webhookServer.tunnelUrl}`);
252
205
  }
253
206
 
207
+ // Mirror externally-spawned Claude Code sessions (those launched in
208
+ // a terminal, not via the mobile app). Reads from ~/.claude/projects
209
+ // and surfaces messages to Firestore for projects the user opted in
210
+ // to mirror. Per-project opt-in lives on the desktop doc.
211
+ await startClaudeSessionWatcher(desktopId);
212
+
254
213
  // Print startup banner.
255
214
  log.banner(hostname(), getPlatformName(), desktopId, projects.length);
256
215
 
@@ -274,10 +233,8 @@ program
274
233
 
275
234
  try {
276
235
  stopWatching();
277
- try {
278
- stopMobileAi();
279
- } catch {}
280
236
  await stopWebhookServer();
237
+ await stopClaudeSessionWatcher();
281
238
  await shutdownAllSessions();
282
239
  stopAllTunnels();
283
240
  stopAllCaptures();
@@ -422,72 +379,4 @@ program
422
379
  }
423
380
  });
424
381
 
425
- program
426
- .command("setup-testflight")
427
- .description(
428
- "Save App Store Connect API credentials so iOS builds can upload to TestFlight",
429
- )
430
- .option("--key-id <id>", "App Store Connect API Key ID (10-char uppercase)")
431
- .option("--issuer-id <id>", "App Store Connect Issuer ID (UUID)")
432
- .option("--key-file <path>", "Path to AuthKey_XYZ.p8 from App Store Connect")
433
- .action(async (opts) => {
434
- const rl = createInterface({ input, output });
435
- try {
436
- const apiKeyId =
437
- opts.keyId || (await rl.question("App Store Connect Key ID: ")).trim();
438
- const apiIssuerId =
439
- opts.issuerId ||
440
- (await rl.question("App Store Connect Issuer ID (UUID): ")).trim();
441
- const keyPath =
442
- opts.keyFile ||
443
- (await rl.question("Path to AuthKey_XYZ.p8 file: ")).trim();
444
-
445
- if (!apiKeyId || !apiIssuerId || !keyPath) {
446
- log.error("All three values are required.");
447
- process.exit(1);
448
- }
449
- const apiKeyContent = await fsReadFile(keyPath, "utf-8");
450
- saveTestflightCreds({ apiKeyId, apiIssuerId, apiKeyContent });
451
- log.success(
452
- "TestFlight credentials saved. Run an iOS build then choose " +
453
- "'Upload to TestFlight' on your phone.",
454
- );
455
- } catch (e) {
456
- log.error(`Setup failed: ${e.message}`);
457
- process.exit(1);
458
- } finally {
459
- rl.close();
460
- }
461
- });
462
-
463
- program
464
- .command("setup-ios-signing")
465
- .description(
466
- "Open Xcode at the Runner project so you can configure code signing",
467
- )
468
- .option("--project <path>", "Project root (defaults to current directory)")
469
- .action(async (opts) => {
470
- const projectPath = opts.project || process.cwd();
471
- try {
472
- const opened = openXcodeSigning(projectPath);
473
- log.info("");
474
- log.info("In Xcode:");
475
- log.info(" 1. Click 'Runner' in the project navigator");
476
- log.info(" 2. Open the 'Signing & Capabilities' tab");
477
- log.info(" 3. Pick your Team and a unique bundle identifier");
478
- log.info(" 4. Make sure 'Automatically manage signing' is checked");
479
- log.info(`Opened: ${opened}`);
480
-
481
- const report = checkIosReadiness(projectPath);
482
- log.info("");
483
- log.info(`Signing ready: ${report.signing.ok ? "yes" : "not yet"}`);
484
- if (!report.signing.ok && report.signing.hint) {
485
- log.info(`Hint: ${report.signing.hint}`);
486
- }
487
- } catch (e) {
488
- log.error(`Couldn't open Xcode: ${e.message}`);
489
- process.exit(1);
490
- }
491
- });
492
-
493
382
  program.parse();
@@ -41,13 +41,10 @@ import {
41
41
  listFirebaseProjects,
42
42
  initFirebaseHosting,
43
43
  startFirebaseLogin,
44
+ deployToCloudflarePages,
45
+ saveCloudflareCreds,
46
+ slugifyForCloudflarePages,
44
47
  } from "./build-manager.js";
45
- import { uploadToTestFlight, distribute } from "./distribution.js";
46
- import {
47
- checkIosReadiness,
48
- saveTestflightCreds,
49
- openXcodeSigning,
50
- } from "./ios-setup.js";
51
48
 
52
49
  // ---------------------------------------------------------------------------
53
50
  // Resolve the user's shell environment so spawned processes inherit PATH,
@@ -185,6 +182,26 @@ export function getActiveSessionCount() {
185
182
  return count;
186
183
  }
187
184
 
185
+ /**
186
+ * Look up the projectPath for a session. Tries the in-memory activeSessions
187
+ * map first (cheap), then falls back to reading the session doc from
188
+ * Firestore (one round-trip, but works for idle/interrupted/completed
189
+ * sessions whose process is gone).
190
+ *
191
+ * Returns null if no projectPath can be resolved.
192
+ */
193
+ async function resolveProjectPath(sessionId) {
194
+ const sess = activeSessions.get(sessionId);
195
+ if (sess?.projectPath) return sess.projectPath;
196
+ try {
197
+ const db = getDb();
198
+ const doc = await db.collection("sessions").doc(sessionId).get();
199
+ return doc.exists ? doc.data()?.projectPath || null : null;
200
+ } catch {
201
+ return null;
202
+ }
203
+ }
204
+
188
205
  /**
189
206
  * Find an existing active/idle session for a given webhookId.
190
207
  * Returns { sessionId, status } or null if no matching session exists.
@@ -491,11 +508,8 @@ const VALID_SESSION_COMMANDS = new Set([
491
508
  "build_project",
492
509
  "deploy_hosting",
493
510
  "deploy_app_dist",
494
- "deploy_testflight",
495
- "deploy_distribute",
496
- "check_ios_ready",
497
- "save_testflight_creds",
498
- "open_xcode_signing",
511
+ "deploy_cloudflare_pages",
512
+ "save_cloudflare_creds",
499
513
  "check_adb",
500
514
  "install_adb",
501
515
  "check_deploy_ready",
@@ -600,9 +614,13 @@ async function handleSessionCommand(sessionId, commandDoc) {
600
614
  break;
601
615
  case "git_status": {
602
616
  log.command("git_status", sessionId.slice(0, 8));
603
- const sess = activeSessions.get(sessionId);
604
- if (!sess) throw new Error("Session not found");
605
- const status = getGitStatus(sess.projectPath);
617
+ // Read-only git commands shouldn't require the session to be in
618
+ // memory the user might be on the mobile app looking at an idle/
619
+ // interrupted session whose process is gone. Fall back to the
620
+ // session doc's projectPath when activeSessions doesn't have it.
621
+ const projectPath = await resolveProjectPath(sessionId);
622
+ if (!projectPath) throw new Error("Session not found");
623
+ const status = getGitStatus(projectPath);
606
624
  await db
607
625
  .collection("sessions")
608
626
  .doc(sessionId)
@@ -613,10 +631,10 @@ async function handleSessionCommand(sessionId, commandDoc) {
613
631
  }
614
632
  case "git_diff": {
615
633
  log.command("git_diff", sessionId.slice(0, 8));
616
- const sess = activeSessions.get(sessionId);
617
- if (!sess) throw new Error("Session not found");
634
+ const projectPath = await resolveProjectPath(sessionId);
635
+ if (!projectPath) throw new Error("Session not found");
618
636
  const filePath = data.payload?.filePath || null;
619
- const diffResult = getGitDiff(sess.projectPath, filePath);
637
+ const diffResult = getGitDiff(projectPath, filePath);
620
638
  const encodedPath = encodeURIComponent(filePath || "_all").replace(
621
639
  /\./g,
622
640
  "_",
@@ -631,10 +649,10 @@ async function handleSessionCommand(sessionId, commandDoc) {
631
649
  }
632
650
  case "git_log": {
633
651
  log.command("git_log", sessionId.slice(0, 8));
634
- const sess = activeSessions.get(sessionId);
635
- if (!sess) throw new Error("Session not found");
652
+ const projectPath = await resolveProjectPath(sessionId);
653
+ if (!projectPath) throw new Error("Session not found");
636
654
  const count = data.payload?.count || 10;
637
- const commits = getGitLog(sess.projectPath, count);
655
+ const commits = getGitLog(projectPath, count);
638
656
  await db
639
657
  .collection("sessions")
640
658
  .doc(sessionId)
@@ -865,8 +883,8 @@ async function handleSessionCommand(sessionId, commandDoc) {
865
883
  break;
866
884
  }
867
885
 
868
- case "deploy_app_dist": {
869
- log.command("deploy_app_dist", sessionId.slice(0, 8));
886
+ case "deploy_cloudflare_pages": {
887
+ log.command("deploy_cloudflare_pages", sessionId.slice(0, 8));
870
888
  const sess = await getSessionInfo();
871
889
 
872
890
  const deployRef = db
@@ -875,7 +893,7 @@ async function handleSessionCommand(sessionId, commandDoc) {
875
893
  .collection("deploys")
876
894
  .doc();
877
895
  await deployRef.set({
878
- type: "app_distribution",
896
+ type: "cloudflare_pages",
879
897
  status: "deploying",
880
898
  startedAt: FieldValue.serverTimestamp(),
881
899
  });
@@ -883,37 +901,26 @@ async function handleSessionCommand(sessionId, commandDoc) {
883
901
  try {
884
902
  const buildPath = data.payload?.buildPath;
885
903
  if (!buildPath) throw new Error("buildPath is required");
904
+ const projectName =
905
+ data.payload?.projectName || sess.projectName || "site";
906
+ const slug = slugifyForCloudflarePages(projectName);
886
907
 
887
- const projectId = data.payload?.projectId;
888
- const groups = data.payload?.groups;
889
- const testers = data.payload?.testers;
890
- const releaseNotes = data.payload?.releaseNotes;
891
-
892
- const distResult = await deployToAppDistribution(
893
- sess.projectPath,
894
- buildPath,
895
- {
896
- projectId,
897
- groups,
898
- testers,
899
- releaseNotes,
900
- },
901
- );
908
+ const result = await deployToCloudflarePages(buildPath, slug);
902
909
 
903
910
  await deployRef.update({
904
911
  status: "success",
905
- consoleUrl: distResult.consoleUrl || null,
906
- testerUrl: distResult.testerUrl || null,
907
- appTesterUrl: distResult.appTesterUrl || null,
908
- appId: distResult.appId || null,
912
+ url: result.url,
913
+ projectSlug: result.projectSlug,
909
914
  completedAt: FieldValue.serverTimestamp(),
910
915
  });
911
916
 
912
917
  if (sess.desktopId) {
913
918
  notifySessionComplete(sess.desktopId, sessionId, {
914
919
  projectName: sess.projectName || "Unknown",
915
- customTitle: "App Distributed",
916
- customBody: "New build available for testers",
920
+ customTitle: "Deploy Complete",
921
+ customBody: result.url
922
+ ? `Live at: ${result.url}`
923
+ : "Cloudflare Pages deployed",
917
924
  }).catch(() => {});
918
925
  }
919
926
  } catch (err) {
@@ -927,87 +934,25 @@ async function handleSessionCommand(sessionId, commandDoc) {
927
934
  break;
928
935
  }
929
936
 
930
- case "check_ios_ready": {
931
- log.command("check_ios_ready", sessionId.slice(0, 8));
932
- const sess = await getSessionInfo();
933
- const report = checkIosReadiness(sess.projectPath);
934
- await db
935
- .collection("sessions")
936
- .doc(sessionId)
937
- .collection("buildData")
938
- .doc("iosReadiness")
939
- .set({
940
- ...report,
941
- checkedAt: FieldValue.serverTimestamp(),
942
- });
943
- break;
944
- }
945
-
946
- case "save_testflight_creds": {
947
- log.command("save_testflight_creds", sessionId.slice(0, 8));
937
+ case "save_cloudflare_creds": {
938
+ log.command("save_cloudflare_creds", sessionId.slice(0, 8));
948
939
  try {
949
- const manifest = saveTestflightCreds({
950
- apiKeyId: data.payload?.apiKeyId,
951
- apiIssuerId: data.payload?.apiIssuerId,
952
- apiKeyContent: data.payload?.apiKeyContent,
953
- appStoreConnectUrl: data.payload?.appStoreConnectUrl,
954
- });
955
- // Re-publish readiness so the wizard can advance.
956
- const sess = await getSessionInfo();
957
- const report = checkIosReadiness(sess.projectPath);
958
- await db
959
- .collection("sessions")
960
- .doc(sessionId)
961
- .collection("buildData")
962
- .doc("iosReadiness")
963
- .set({
964
- ...report,
965
- lastSavedKeyId: manifest.apiKeyId,
966
- checkedAt: FieldValue.serverTimestamp(),
967
- });
968
- } catch (err) {
969
- await db
970
- .collection("sessions")
971
- .doc(sessionId)
972
- .collection("buildData")
973
- .doc("iosReadiness")
974
- .set(
975
- {
976
- saveError: err.message,
977
- checkedAt: FieldValue.serverTimestamp(),
978
- },
979
- { merge: true },
980
- );
981
- throw err;
982
- }
983
- break;
984
- }
940
+ const token = data.payload?.token;
941
+ const accountId = data.payload?.accountId;
942
+ if (!token) throw new Error("token is required");
943
+ saveCloudflareCreds({ token, accountId });
985
944
 
986
- case "open_xcode_signing": {
987
- log.command("open_xcode_signing", sessionId.slice(0, 8));
988
- const sess = await getSessionInfo();
989
- try {
990
- const opened = openXcodeSigning(sess.projectPath);
991
- await db
992
- .collection("sessions")
993
- .doc(sessionId)
994
- .collection("buildData")
995
- .doc("iosReadiness")
996
- .set(
997
- {
998
- xcodeOpenedAt: FieldValue.serverTimestamp(),
999
- xcodeOpenedPath: opened,
1000
- },
1001
- { merge: true },
1002
- );
945
+ // Best-effort verification: list pages projects to confirm token works.
946
+ // (Failure here is surfaced to the caller via the catch below.)
1003
947
  } catch (err) {
948
+ log.error(`save_cloudflare_creds failed: ${err.message}`);
1004
949
  throw err;
1005
950
  }
1006
951
  break;
1007
952
  }
1008
953
 
1009
- case "deploy_testflight": {
1010
- log.command("deploy_testflight", sessionId.slice(0, 8));
954
+ case "deploy_app_dist": {
955
+ log.command("deploy_app_dist", sessionId.slice(0, 8));
1011
956
  const sess = await getSessionInfo();
1012
957
 
1013
958
  const deployRef = db
@@ -1016,7 +961,7 @@ async function handleSessionCommand(sessionId, commandDoc) {
1016
961
  .collection("deploys")
1017
962
  .doc();
1018
963
  await deployRef.set({
1019
- type: "testflight",
964
+ type: "app_distribution",
1020
965
  status: "deploying",
1021
966
  startedAt: FieldValue.serverTimestamp(),
1022
967
  });
@@ -1025,92 +970,36 @@ async function handleSessionCommand(sessionId, commandDoc) {
1025
970
  const buildPath = data.payload?.buildPath;
1026
971
  if (!buildPath) throw new Error("buildPath is required");
1027
972
 
1028
- const result = await uploadToTestFlight(sess.projectPath, buildPath, {
1029
- apiKeyPath: data.payload?.apiKeyPath,
1030
- apiKeyId: data.payload?.apiKeyId,
1031
- apiIssuerId: data.payload?.apiIssuerId,
1032
- appStoreConnectUrl: data.payload?.appStoreConnectUrl,
1033
- });
1034
-
1035
- await deployRef.update({
1036
- status: "success",
1037
- testflightUrl: result.testflightUrl,
1038
- openUrl: result.openUrl,
1039
- completedAt: FieldValue.serverTimestamp(),
1040
- });
1041
-
1042
- if (sess.desktopId) {
1043
- notifySessionComplete(sess.desktopId, sessionId, {
1044
- projectName: sess.projectName || "Unknown",
1045
- customTitle: "TestFlight Upload Complete",
1046
- customBody: "Open TestFlight on your device to install",
1047
- }).catch(() => {});
1048
- }
1049
- } catch (err) {
1050
- await deployRef.update({
1051
- status: "failed",
1052
- error: err.message,
1053
- completedAt: FieldValue.serverTimestamp(),
1054
- });
1055
- throw err;
1056
- }
1057
- break;
1058
- }
1059
-
1060
- case "deploy_distribute": {
1061
- // Smart per-platform distribute: ios → TestFlight, web → hosting
1062
- // preview channel, android → App Distribution.
1063
- log.command("deploy_distribute", sessionId.slice(0, 8));
1064
- const sess = await getSessionInfo();
1065
-
1066
- const deployRef = db
1067
- .collection("sessions")
1068
- .doc(sessionId)
1069
- .collection("deploys")
1070
- .doc();
1071
- await deployRef.set({
1072
- type: "distribute",
1073
- status: "deploying",
1074
- startedAt: FieldValue.serverTimestamp(),
1075
- });
1076
-
1077
- try {
1078
- const platform = data.payload?.platform;
1079
- const buildPath = data.payload?.buildPath;
1080
- if (!platform) throw new Error("platform is required");
973
+ const projectId = data.payload?.projectId;
974
+ const groups = data.payload?.groups;
975
+ const testers = data.payload?.testers;
976
+ const releaseNotes = data.payload?.releaseNotes;
1081
977
 
1082
- const { backend, result } = await distribute({
1083
- platform,
1084
- projectPath: sess.projectPath,
978
+ const distResult = await deployToAppDistribution(
979
+ sess.projectPath,
1085
980
  buildPath,
1086
- options: data.payload?.options || {},
1087
- hostingFn: deployToFirebaseHosting,
1088
- appDistFn: deployToAppDistribution,
1089
- });
981
+ {
982
+ projectId,
983
+ groups,
984
+ testers,
985
+ releaseNotes,
986
+ },
987
+ );
1090
988
 
1091
989
  await deployRef.update({
1092
990
  status: "success",
1093
- backend,
1094
- url: result.url || null,
1095
- channelId: result.channelId || null,
1096
- testflightUrl: result.testflightUrl || null,
1097
- openUrl: result.openUrl || null,
1098
- consoleUrl: result.consoleUrl || null,
1099
- testerUrl: result.testerUrl || null,
1100
- appTesterUrl: result.appTesterUrl || null,
991
+ consoleUrl: distResult.consoleUrl || null,
992
+ testerUrl: distResult.testerUrl || null,
993
+ appTesterUrl: distResult.appTesterUrl || null,
994
+ appId: distResult.appId || null,
1101
995
  completedAt: FieldValue.serverTimestamp(),
1102
996
  });
1103
997
 
1104
998
  if (sess.desktopId) {
1105
- const titleByBackend = {
1106
- testflight: "TestFlight Upload Complete",
1107
- hosting_preview: "Preview Live",
1108
- app_distribution: "App Distributed",
1109
- };
1110
999
  notifySessionComplete(sess.desktopId, sessionId, {
1111
1000
  projectName: sess.projectName || "Unknown",
1112
- customTitle: titleByBackend[backend] || "Deploy Complete",
1113
- customBody: result.url || result.testflightUrl || "Done",
1001
+ customTitle: "App Distributed",
1002
+ customBody: "New build available for testers",
1114
1003
  }).catch(() => {});
1115
1004
  }
1116
1005
  } catch (err) {
@@ -1272,7 +1161,7 @@ async function handleSessionCommand(sessionId, commandDoc) {
1272
1161
 
1273
1162
  const MODEL_DISPLAY_NAMES = {
1274
1163
  sonnet: "Claude Sonnet 4.6",
1275
- opus: "Claude Opus 4.7",
1164
+ opus: "Claude Opus 4.6",
1276
1165
  haiku: "Claude Haiku 4.5",
1277
1166
  };
1278
1167
 
@@ -1301,6 +1190,12 @@ export async function startNewSession(desktopId, payload) {
1301
1190
  webhookMeta,
1302
1191
  allowedTools,
1303
1192
  createIfMissing,
1193
+ // Mobile takeover / "Continue Conversation" flow: when true, the first
1194
+ // prompt of this new session is fed to Claude with `--continue` so it
1195
+ // resumes the prior conversation in the same project. previousSessionId
1196
+ // is informational — stored on the session doc so the UI can link.
1197
+ continueLast,
1198
+ previousSessionId,
1304
1199
  } = payload || {};
1305
1200
  const db = getDb();
1306
1201
  const resolvedModel = model || "sonnet";
@@ -1341,6 +1236,9 @@ export async function startNewSession(desktopId, payload) {
1341
1236
  tokenUsage: { input: 0, output: 0, totalCost: 0 },
1342
1237
  startedAt: FieldValue.serverTimestamp(),
1343
1238
  lastActivity: FieldValue.serverTimestamp(),
1239
+ ...(continueLast
1240
+ ? { continuedFromSessionId: previousSessionId || null }
1241
+ : {}),
1344
1242
  });
1345
1243
 
1346
1244
  // Copy ownerUid from desktop.
@@ -1365,7 +1263,11 @@ export async function startNewSession(desktopId, payload) {
1365
1263
  turnToolCalls: 0, // Tool calls this turn (reset on each prompt)
1366
1264
  turnTokensInput: 0, // Input tokens this turn
1367
1265
  turnTokensOutput: 0, // Output tokens this turn
1368
- isFirstPrompt: true,
1266
+ // continueLast=true means the *very first* prompt should be sent with
1267
+ // `--continue` so Claude resumes the prior conversation in this project.
1268
+ // The follow-up-prompt path keys on `!isFirstPrompt`, so flipping this
1269
+ // false up front is enough — no other plumbing required.
1270
+ isFirstPrompt: !continueLast,
1369
1271
  lastToolCall: null, // Last tool_use block (for permission requests)
1370
1272
  permissionNeeded: false, // True when Claude reports permission denial
1371
1273
  permissionWatcher: null, // Firestore unsubscribe for permission doc
@@ -36,18 +36,12 @@ function isNewer(local, remote) {
36
36
  }
37
37
 
38
38
  /**
39
- * Check npm for a newer version. If one is found, re-launches the CLI
40
- * with `npx forge-remote@latest` and exits the current process.
39
+ * Check npm for a newer version. Logs a warning if one is found.
41
40
  * Never throws — silently returns on any failure.
42
- *
43
- * @returns {Promise<boolean>} true if an update was found and re-launch initiated
44
41
  */
45
42
  export async function checkForUpdate() {
46
43
  const localVersion = getLocalVersion();
47
- if (!localVersion) return false;
48
-
49
- // Skip if we're already running via @latest (avoid infinite loop)
50
- if (process.env.FORGE_REMOTE_UPDATED === "1") return false;
44
+ if (!localVersion) return;
51
45
 
52
46
  try {
53
47
  const controller = new AbortController();
@@ -58,29 +52,21 @@ export async function checkForUpdate() {
58
52
  });
59
53
  clearTimeout(timeout);
60
54
 
61
- if (!res.ok) return false;
55
+ if (!res.ok) return;
62
56
 
63
57
  const data = await res.json();
64
58
  const remoteVersion = data.version;
65
59
 
66
60
  if (remoteVersion && isNewer(localVersion, remoteVersion)) {
67
- log.info(`Updating forge-remote: ${localVersion} → ${remoteVersion}`);
68
-
69
- // Re-launch with @latest, passing through all original args
70
- const args = process.argv.slice(2);
71
- const { execSync } = await import("child_process");
72
- try {
73
- execSync(`npx forge-remote@latest ${args.join(" ")}`, {
74
- stdio: "inherit",
75
- env: { ...process.env, FORGE_REMOTE_UPDATED: "1" },
76
- });
77
- } catch {
78
- // Child process exited — that's expected when user Ctrl+C
79
- }
80
- process.exit(0);
61
+ console.log();
62
+ log.warn(
63
+ `A new version of forge-remote is available: ${localVersion} → ${remoteVersion}`,
64
+ );
65
+ log.warn(" Run: npm update -g forge-remote");
66
+ log.warn(" Then: forge-remote init (to update Firestore rules)");
67
+ console.log();
81
68
  }
82
69
  } catch {
83
70
  // Network error, timeout, etc. — don't bother the user.
84
71
  }
85
- return false;
86
72
  }