@h-rig/cli 0.0.6-alpha.82 → 0.0.6-alpha.83

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 (100) hide show
  1. package/dist/bin/build-rig-binaries.js +40 -8
  2. package/dist/bin/rig.js +2798 -1440
  3. package/dist/src/app/board.js +62 -13
  4. package/dist/src/app-opentui/adapters/command.d.ts +2 -0
  5. package/dist/src/app-opentui/adapters/command.js +329 -0
  6. package/dist/src/app-opentui/adapters/common.js +2 -2
  7. package/dist/src/app-opentui/adapters/doctor.js +63 -14
  8. package/dist/src/app-opentui/adapters/fleet.js +84 -20
  9. package/dist/src/app-opentui/adapters/inbox.js +83 -19
  10. package/dist/src/app-opentui/adapters/init.js +87 -23
  11. package/dist/src/app-opentui/adapters/pi-attach.js +96 -34
  12. package/dist/src/app-opentui/adapters/run-detail.js +83 -19
  13. package/dist/src/app-opentui/adapters/server.js +100 -23
  14. package/dist/src/app-opentui/adapters/tasks.d.ts +11 -0
  15. package/dist/src/app-opentui/adapters/tasks.js +742 -94
  16. package/dist/src/app-opentui/bootstrap.d.ts +1 -6
  17. package/dist/src/app-opentui/bootstrap.js +13769 -12368
  18. package/dist/src/app-opentui/command-pty-host.d.ts +62 -0
  19. package/dist/src/app-opentui/command-pty-host.js +248 -0
  20. package/dist/src/app-opentui/events.js +1 -1
  21. package/dist/src/app-opentui/focus-manager.d.ts +14 -0
  22. package/dist/src/app-opentui/focus-manager.js +24 -0
  23. package/dist/src/app-opentui/index.js +3802 -2882
  24. package/dist/src/app-opentui/intent.js +154 -48
  25. package/dist/src/app-opentui/keymap.d.ts +20 -0
  26. package/dist/src/app-opentui/keymap.js +707 -0
  27. package/dist/src/app-opentui/launch-routing.d.ts +16 -0
  28. package/dist/src/app-opentui/launch-routing.js +55 -0
  29. package/dist/src/app-opentui/layout.js +2 -2
  30. package/dist/src/app-opentui/pi-host-child.js +66 -16
  31. package/dist/src/app-opentui/pi-pty-host.d.ts +9 -0
  32. package/dist/src/app-opentui/pi-pty-host.js +9 -11
  33. package/dist/src/app-opentui/registry.js +3231 -2403
  34. package/dist/src/app-opentui/render/ascii-fleet.d.ts +15 -0
  35. package/dist/src/app-opentui/render/ascii-fleet.js +82 -0
  36. package/dist/src/app-opentui/render/graphics.d.ts +1 -1
  37. package/dist/src/app-opentui/render/graphics.js +131 -16
  38. package/dist/src/app-opentui/render/image-visual-layer-worker.js +408 -957
  39. package/dist/src/app-opentui/render/image-visual-layer.d.ts +18 -10
  40. package/dist/src/app-opentui/render/image-visual-layer.js +386 -356
  41. package/dist/src/app-opentui/render/panel-layout.d.ts +38 -0
  42. package/dist/src/app-opentui/render/panel-layout.js +48 -0
  43. package/dist/src/app-opentui/render/panels.d.ts +2 -0
  44. package/dist/src/app-opentui/render/panels.js +62 -29
  45. package/dist/src/app-opentui/render/preloader.d.ts +10 -0
  46. package/dist/src/app-opentui/render/preloader.js +163 -0
  47. package/dist/src/app-opentui/render/scene.d.ts +3 -0
  48. package/dist/src/app-opentui/render/scene.js +12 -0
  49. package/dist/src/app-opentui/render/text.js +5 -1
  50. package/dist/src/app-opentui/render/type-bar.d.ts +2 -1
  51. package/dist/src/app-opentui/render/type-bar.js +51 -16
  52. package/dist/src/app-opentui/runtime-resources.d.ts +16 -0
  53. package/dist/src/app-opentui/runtime-resources.js +62 -0
  54. package/dist/src/app-opentui/runtime.js +2322 -1513
  55. package/dist/src/app-opentui/scenes/command.d.ts +3 -0
  56. package/dist/src/app-opentui/scenes/command.js +103 -0
  57. package/dist/src/app-opentui/scenes/doctor.d.ts +2 -1
  58. package/dist/src/app-opentui/scenes/doctor.js +53 -14
  59. package/dist/src/app-opentui/scenes/error.js +44 -14
  60. package/dist/src/app-opentui/scenes/fleet.js +40 -21
  61. package/dist/src/app-opentui/scenes/handoff.js +142 -27
  62. package/dist/src/app-opentui/scenes/help.js +63 -48
  63. package/dist/src/app-opentui/scenes/inbox.d.ts +2 -1
  64. package/dist/src/app-opentui/scenes/inbox.js +64 -17
  65. package/dist/src/app-opentui/scenes/init.d.ts +2 -1
  66. package/dist/src/app-opentui/scenes/init.js +62 -21
  67. package/dist/src/app-opentui/scenes/main.d.ts +2 -1
  68. package/dist/src/app-opentui/scenes/main.js +80 -24
  69. package/dist/src/app-opentui/scenes/run-detail.d.ts +2 -1
  70. package/dist/src/app-opentui/scenes/run-detail.js +39 -25
  71. package/dist/src/app-opentui/scenes/server.d.ts +2 -1
  72. package/dist/src/app-opentui/scenes/server.js +71 -20
  73. package/dist/src/app-opentui/scenes/tasks.js +44 -22
  74. package/dist/src/app-opentui/state.js +70 -30
  75. package/dist/src/app-opentui/terminal-capabilities.d.ts +7 -0
  76. package/dist/src/app-opentui/terminal-capabilities.js +15 -0
  77. package/dist/src/app-opentui/types.d.ts +10 -2
  78. package/dist/src/commands/_doctor-checks.js +62 -13
  79. package/dist/src/commands/_operator-view.js +63 -13
  80. package/dist/src/commands/_pi-frontend.js +63 -13
  81. package/dist/src/commands/_preflight.js +64 -14
  82. package/dist/src/commands/_server-client.js +82 -18
  83. package/dist/src/commands/_snapshot-upload.js +62 -13
  84. package/dist/src/commands/connect.js +7 -1
  85. package/dist/src/commands/doctor.js +62 -13
  86. package/dist/src/commands/github.js +144 -23
  87. package/dist/src/commands/inbox.js +62 -13
  88. package/dist/src/commands/init.js +82 -18
  89. package/dist/src/commands/inspect.js +62 -13
  90. package/dist/src/commands/run.js +66 -13
  91. package/dist/src/commands/server.js +69 -14
  92. package/dist/src/commands/setup.js +62 -13
  93. package/dist/src/commands/stats.js +62 -13
  94. package/dist/src/commands/task-run-driver.js +62 -13
  95. package/dist/src/commands/task.js +65 -14
  96. package/dist/src/commands.js +227 -92
  97. package/dist/src/index.js +234 -99
  98. package/package.json +8 -9
  99. package/dist/src/app-opentui/render/image-visual-layer-native-canvas.d.ts +0 -2
  100. package/dist/src/app-opentui/render/image-visual-layer-native-canvas.js +0 -1484
@@ -134,17 +134,21 @@ function cleanToken(value) {
134
134
  const trimmed = value?.trim();
135
135
  return trimmed ? trimmed : null;
136
136
  }
137
- function readPrivateRemoteSessionToken(projectRoot) {
137
+ function readRemoteAuthState(projectRoot) {
138
138
  const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
139
139
  if (!existsSync2(path))
140
140
  return null;
141
141
  try {
142
142
  const parsed = JSON.parse(readFileSync2(path, "utf8"));
143
- return cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined);
143
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
144
144
  } catch {
145
145
  return null;
146
146
  }
147
147
  }
148
+ function readPrivateRemoteSessionToken(projectRoot) {
149
+ const parsed = readRemoteAuthState(projectRoot);
150
+ return parsed ? cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined) : null;
151
+ }
148
152
  function readGitHubBearerTokenForRemote(projectRoot) {
149
153
  const scopedKey = resolve2(projectRoot);
150
154
  if (scopedGitHubBearerTokens.has(scopedKey))
@@ -155,15 +159,25 @@ function readGitHubBearerTokenForRemote(projectRoot) {
155
159
  return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
156
160
  }
157
161
  function readStoredGitHubAuthToken(projectRoot) {
158
- const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
159
- if (!existsSync2(path))
162
+ const parsed = readRemoteAuthState(projectRoot);
163
+ return parsed ? cleanToken(typeof parsed.token === "string" ? parsed.token : undefined) : null;
164
+ }
165
+ function inferRemoteServerProjectRootFromAuthState(projectRoot) {
166
+ const repo = readRepoConnection(projectRoot);
167
+ const slug = repo?.project?.trim();
168
+ if (!slug || !/^[^/]+\/[^/]+$/.test(slug))
160
169
  return null;
161
- try {
162
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
163
- return cleanToken(typeof parsed.token === "string" ? parsed.token : undefined);
164
- } catch {
170
+ const auth = readRemoteAuthState(projectRoot);
171
+ const authUpdatedAt = typeof auth?.updatedAt === "string" ? Date.parse(auth.updatedAt) : Number.NaN;
172
+ const repoLinkedAt = typeof repo?.linkedAt === "string" ? Date.parse(repo.linkedAt) : Number.NaN;
173
+ if (Number.isFinite(authUpdatedAt) && Number.isFinite(repoLinkedAt) && authUpdatedAt + 1000 < repoLinkedAt)
165
174
  return null;
166
- }
175
+ const checkoutBaseDir = typeof auth?.checkoutBaseDir === "string" && auth.checkoutBaseDir.trim() ? auth.checkoutBaseDir.trim() : null;
176
+ if (!checkoutBaseDir)
177
+ return null;
178
+ const inferred = `${checkoutBaseDir.replace(/\/+$/, "")}/${slug}`;
179
+ writeRepoServerProjectRoot(projectRoot, inferred);
180
+ return inferred;
167
181
  }
168
182
  function readLocalConnectionFallbackToken(projectRoot) {
169
183
  return readGitHubBearerTokenForRemote(projectRoot) ?? cleanToken(process.env.RIG_GITHUB_TOKEN) ?? readStoredGitHubAuthToken(projectRoot);
@@ -174,7 +188,7 @@ async function ensureServerForCli(projectRoot) {
174
188
  if (selected?.connection.kind === "remote") {
175
189
  reportServerPhase(`Connecting to ${selected.alias}\u2026`);
176
190
  const authToken = readGitHubBearerTokenForRemote(projectRoot);
177
- const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
191
+ const serverProjectRoot = selected.serverProjectRoot ?? inferRemoteServerProjectRootFromAuthState(projectRoot) ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
178
192
  return {
179
193
  baseUrl: selected.connection.baseUrl,
180
194
  authToken,
@@ -203,7 +217,10 @@ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken)
203
217
  if (!slug)
204
218
  return null;
205
219
  try {
206
- const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
220
+ const url = new URL(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`);
221
+ if (authToken && queryAuthFallbackEnabled())
222
+ url.searchParams.set("rt", authToken);
223
+ const response = await fetch(url, {
207
224
  headers: mergeHeaders(undefined, authToken)
208
225
  });
209
226
  if (!response.ok)
@@ -220,10 +237,23 @@ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken)
220
237
  return null;
221
238
  }
222
239
  }
240
+ function mergeCookie(existing, name, value) {
241
+ const encoded = `${name}=${encodeURIComponent(value)}`;
242
+ if (!existing?.trim())
243
+ return encoded;
244
+ const parts = existing.split(";").map((part) => part.trim()).filter((part) => part && !part.startsWith(`${name}=`));
245
+ return [...parts, encoded].join("; ");
246
+ }
247
+ function queryAuthFallbackEnabled(env = process.env) {
248
+ return env.RIG_ENABLE_QUERY_AUTH_FALLBACK === "1" || env.RIG_QUERY_AUTH_FALLBACK === "1";
249
+ }
223
250
  function mergeHeaders(headers, authToken) {
224
251
  const merged = new Headers(headers);
225
252
  if (authToken) {
226
- merged.set("authorization", `Bearer ${authToken}`);
253
+ const bearer = `Bearer ${authToken}`;
254
+ merged.set("authorization", bearer);
255
+ merged.set("x-auth", bearer);
256
+ merged.set("cookie", mergeCookie(merged.get("cookie"), "rig_auth", authToken));
227
257
  }
228
258
  return merged;
229
259
  }
@@ -284,15 +314,34 @@ async function buildServerFailureContext(projectRoot, server) {
284
314
  hint: "Check the selected server with `rig server status`, or switch with `rig server use <alias|local>`."
285
315
  };
286
316
  }
317
+ function isLoopbackRemoteBaseUrl(baseUrl) {
318
+ try {
319
+ const host = new URL(baseUrl).hostname.toLowerCase();
320
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
321
+ } catch {
322
+ return false;
323
+ }
324
+ }
325
+ function canUseRemoteWithoutProjectRoot(pathname) {
326
+ return pathname === "/health" || pathname === "/api/health" || pathname === "/api/server/status" || pathname === "/api/server/project-root" || pathname.startsWith("/api/github/auth/") || pathname === "/api/projects" || pathname.startsWith("/api/projects/");
327
+ }
287
328
  async function requestServerJson(context, pathname, init = {}) {
288
329
  const server = await ensureServerForCli(context.projectRoot);
330
+ const requestUrl = new URL(`${server.baseUrl}${pathname}`);
331
+ if (server.connectionKind === "remote" && !server.serverProjectRoot && !canUseRemoteWithoutProjectRoot(requestUrl.pathname) && (server.authToken || !isLoopbackRemoteBaseUrl(server.baseUrl))) {
332
+ const repo = readRepoConnection(context.projectRoot);
333
+ throw new CliError(`Remote server is selected for ${repo?.project ?? "this repo"}, but this checkout has no server-host project root link.`, 1, { hint: "Run `rig init --repair` or `rig init --yes --repo owner/repo --server remote` to repair the remote project link before task/run commands." });
334
+ }
289
335
  const headers = mergeHeaders(init.headers, server.authToken);
290
336
  if (server.serverProjectRoot)
291
337
  headers.set("x-rig-project-root", server.serverProjectRoot);
338
+ if (server.connectionKind === "remote" && server.authToken && queryAuthFallbackEnabled()) {
339
+ requestUrl.searchParams.set("rt", server.authToken);
340
+ }
292
341
  reportServerPhase(`${(init.method ?? "GET").toUpperCase()} ${pathname.split("?")[0]}\u2026`);
293
342
  let response;
294
343
  try {
295
- response = await fetch(`${server.baseUrl}${pathname}`, {
344
+ response = await fetch(requestUrl, {
296
345
  ...init,
297
346
  headers
298
347
  });
@@ -943,6 +992,7 @@ async function attachRunBundledPiFrontend(context, input) {
943
992
  let detached = false;
944
993
  try {
945
994
  await runPiMain([
995
+ "--offline",
946
996
  "--no-extensions",
947
997
  "--no-skills",
948
998
  "--no-prompt-templates",
@@ -141,17 +141,21 @@ function cleanToken(value) {
141
141
  const trimmed = value?.trim();
142
142
  return trimmed ? trimmed : null;
143
143
  }
144
- function readPrivateRemoteSessionToken(projectRoot) {
144
+ function readRemoteAuthState(projectRoot) {
145
145
  const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
146
146
  if (!existsSync2(path))
147
147
  return null;
148
148
  try {
149
149
  const parsed = JSON.parse(readFileSync2(path, "utf8"));
150
- return cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined);
150
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
151
151
  } catch {
152
152
  return null;
153
153
  }
154
154
  }
155
+ function readPrivateRemoteSessionToken(projectRoot) {
156
+ const parsed = readRemoteAuthState(projectRoot);
157
+ return parsed ? cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined) : null;
158
+ }
155
159
  function readGitHubBearerTokenForRemote(projectRoot) {
156
160
  const scopedKey = resolve2(projectRoot);
157
161
  if (scopedGitHubBearerTokens.has(scopedKey))
@@ -162,15 +166,25 @@ function readGitHubBearerTokenForRemote(projectRoot) {
162
166
  return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
163
167
  }
164
168
  function readStoredGitHubAuthToken(projectRoot) {
165
- const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
166
- if (!existsSync2(path))
169
+ const parsed = readRemoteAuthState(projectRoot);
170
+ return parsed ? cleanToken(typeof parsed.token === "string" ? parsed.token : undefined) : null;
171
+ }
172
+ function inferRemoteServerProjectRootFromAuthState(projectRoot) {
173
+ const repo = readRepoConnection(projectRoot);
174
+ const slug = repo?.project?.trim();
175
+ if (!slug || !/^[^/]+\/[^/]+$/.test(slug))
167
176
  return null;
168
- try {
169
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
170
- return cleanToken(typeof parsed.token === "string" ? parsed.token : undefined);
171
- } catch {
177
+ const auth = readRemoteAuthState(projectRoot);
178
+ const authUpdatedAt = typeof auth?.updatedAt === "string" ? Date.parse(auth.updatedAt) : Number.NaN;
179
+ const repoLinkedAt = typeof repo?.linkedAt === "string" ? Date.parse(repo.linkedAt) : Number.NaN;
180
+ if (Number.isFinite(authUpdatedAt) && Number.isFinite(repoLinkedAt) && authUpdatedAt + 1000 < repoLinkedAt)
172
181
  return null;
173
- }
182
+ const checkoutBaseDir = typeof auth?.checkoutBaseDir === "string" && auth.checkoutBaseDir.trim() ? auth.checkoutBaseDir.trim() : null;
183
+ if (!checkoutBaseDir)
184
+ return null;
185
+ const inferred = `${checkoutBaseDir.replace(/\/+$/, "")}/${slug}`;
186
+ writeRepoServerProjectRoot(projectRoot, inferred);
187
+ return inferred;
174
188
  }
175
189
  function readLocalConnectionFallbackToken(projectRoot) {
176
190
  return readGitHubBearerTokenForRemote(projectRoot) ?? cleanToken(process.env.RIG_GITHUB_TOKEN) ?? readStoredGitHubAuthToken(projectRoot);
@@ -181,7 +195,7 @@ async function ensureServerForCli(projectRoot) {
181
195
  if (selected?.connection.kind === "remote") {
182
196
  reportServerPhase(`Connecting to ${selected.alias}\u2026`);
183
197
  const authToken = readGitHubBearerTokenForRemote(projectRoot);
184
- const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
198
+ const serverProjectRoot = selected.serverProjectRoot ?? inferRemoteServerProjectRootFromAuthState(projectRoot) ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
185
199
  return {
186
200
  baseUrl: selected.connection.baseUrl,
187
201
  authToken,
@@ -210,7 +224,10 @@ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken)
210
224
  if (!slug)
211
225
  return null;
212
226
  try {
213
- const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
227
+ const url = new URL(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`);
228
+ if (authToken && queryAuthFallbackEnabled())
229
+ url.searchParams.set("rt", authToken);
230
+ const response = await fetch(url, {
214
231
  headers: mergeHeaders(undefined, authToken)
215
232
  });
216
233
  if (!response.ok)
@@ -227,10 +244,23 @@ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken)
227
244
  return null;
228
245
  }
229
246
  }
247
+ function mergeCookie(existing, name, value) {
248
+ const encoded = `${name}=${encodeURIComponent(value)}`;
249
+ if (!existing?.trim())
250
+ return encoded;
251
+ const parts = existing.split(";").map((part) => part.trim()).filter((part) => part && !part.startsWith(`${name}=`));
252
+ return [...parts, encoded].join("; ");
253
+ }
254
+ function queryAuthFallbackEnabled(env = process.env) {
255
+ return env.RIG_ENABLE_QUERY_AUTH_FALLBACK === "1" || env.RIG_QUERY_AUTH_FALLBACK === "1";
256
+ }
230
257
  function mergeHeaders(headers, authToken) {
231
258
  const merged = new Headers(headers);
232
259
  if (authToken) {
233
- merged.set("authorization", `Bearer ${authToken}`);
260
+ const bearer = `Bearer ${authToken}`;
261
+ merged.set("authorization", bearer);
262
+ merged.set("x-auth", bearer);
263
+ merged.set("cookie", mergeCookie(merged.get("cookie"), "rig_auth", authToken));
234
264
  }
235
265
  return merged;
236
266
  }
@@ -291,15 +321,34 @@ async function buildServerFailureContext(projectRoot, server) {
291
321
  hint: "Check the selected server with `rig server status`, or switch with `rig server use <alias|local>`."
292
322
  };
293
323
  }
324
+ function isLoopbackRemoteBaseUrl(baseUrl) {
325
+ try {
326
+ const host = new URL(baseUrl).hostname.toLowerCase();
327
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
328
+ } catch {
329
+ return false;
330
+ }
331
+ }
332
+ function canUseRemoteWithoutProjectRoot(pathname) {
333
+ return pathname === "/health" || pathname === "/api/health" || pathname === "/api/server/status" || pathname === "/api/server/project-root" || pathname.startsWith("/api/github/auth/") || pathname === "/api/projects" || pathname.startsWith("/api/projects/");
334
+ }
294
335
  async function requestServerJson(context, pathname, init = {}) {
295
336
  const server = await ensureServerForCli(context.projectRoot);
337
+ const requestUrl = new URL(`${server.baseUrl}${pathname}`);
338
+ if (server.connectionKind === "remote" && !server.serverProjectRoot && !canUseRemoteWithoutProjectRoot(requestUrl.pathname) && (server.authToken || !isLoopbackRemoteBaseUrl(server.baseUrl))) {
339
+ const repo = readRepoConnection(context.projectRoot);
340
+ throw new CliError(`Remote server is selected for ${repo?.project ?? "this repo"}, but this checkout has no server-host project root link.`, 1, { hint: "Run `rig init --repair` or `rig init --yes --repo owner/repo --server remote` to repair the remote project link before task/run commands." });
341
+ }
296
342
  const headers = mergeHeaders(init.headers, server.authToken);
297
343
  if (server.serverProjectRoot)
298
344
  headers.set("x-rig-project-root", server.serverProjectRoot);
345
+ if (server.connectionKind === "remote" && server.authToken && queryAuthFallbackEnabled()) {
346
+ requestUrl.searchParams.set("rt", server.authToken);
347
+ }
299
348
  reportServerPhase(`${(init.method ?? "GET").toUpperCase()} ${pathname.split("?")[0]}\u2026`);
300
349
  let response;
301
350
  try {
302
- response = await fetch(`${server.baseUrl}${pathname}`, {
351
+ response = await fetch(requestUrl, {
303
352
  ...init,
304
353
  headers
305
354
  });
@@ -709,6 +758,7 @@ async function attachRunBundledPiFrontend(context, input) {
709
758
  let detached = false;
710
759
  try {
711
760
  await runPiMain([
761
+ "--offline",
712
762
  "--no-extensions",
713
763
  "--no-skills",
714
764
  "--no-prompt-templates",
@@ -128,17 +128,21 @@ function cleanToken(value) {
128
128
  const trimmed = value?.trim();
129
129
  return trimmed ? trimmed : null;
130
130
  }
131
- function readPrivateRemoteSessionToken(projectRoot) {
131
+ function readRemoteAuthState(projectRoot) {
132
132
  const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
133
133
  if (!existsSync2(path))
134
134
  return null;
135
135
  try {
136
136
  const parsed = JSON.parse(readFileSync2(path, "utf8"));
137
- return cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined);
137
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
138
138
  } catch {
139
139
  return null;
140
140
  }
141
141
  }
142
+ function readPrivateRemoteSessionToken(projectRoot) {
143
+ const parsed = readRemoteAuthState(projectRoot);
144
+ return parsed ? cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined) : null;
145
+ }
142
146
  function readGitHubBearerTokenForRemote(projectRoot) {
143
147
  const scopedKey = resolve2(projectRoot);
144
148
  if (scopedGitHubBearerTokens.has(scopedKey))
@@ -149,15 +153,25 @@ function readGitHubBearerTokenForRemote(projectRoot) {
149
153
  return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
150
154
  }
151
155
  function readStoredGitHubAuthToken(projectRoot) {
152
- const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
153
- if (!existsSync2(path))
156
+ const parsed = readRemoteAuthState(projectRoot);
157
+ return parsed ? cleanToken(typeof parsed.token === "string" ? parsed.token : undefined) : null;
158
+ }
159
+ function inferRemoteServerProjectRootFromAuthState(projectRoot) {
160
+ const repo = readRepoConnection(projectRoot);
161
+ const slug = repo?.project?.trim();
162
+ if (!slug || !/^[^/]+\/[^/]+$/.test(slug))
154
163
  return null;
155
- try {
156
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
157
- return cleanToken(typeof parsed.token === "string" ? parsed.token : undefined);
158
- } catch {
164
+ const auth = readRemoteAuthState(projectRoot);
165
+ const authUpdatedAt = typeof auth?.updatedAt === "string" ? Date.parse(auth.updatedAt) : Number.NaN;
166
+ const repoLinkedAt = typeof repo?.linkedAt === "string" ? Date.parse(repo.linkedAt) : Number.NaN;
167
+ if (Number.isFinite(authUpdatedAt) && Number.isFinite(repoLinkedAt) && authUpdatedAt + 1000 < repoLinkedAt)
159
168
  return null;
160
- }
169
+ const checkoutBaseDir = typeof auth?.checkoutBaseDir === "string" && auth.checkoutBaseDir.trim() ? auth.checkoutBaseDir.trim() : null;
170
+ if (!checkoutBaseDir)
171
+ return null;
172
+ const inferred = `${checkoutBaseDir.replace(/\/+$/, "")}/${slug}`;
173
+ writeRepoServerProjectRoot(projectRoot, inferred);
174
+ return inferred;
161
175
  }
162
176
  function readLocalConnectionFallbackToken(projectRoot) {
163
177
  return readGitHubBearerTokenForRemote(projectRoot) ?? cleanToken(process.env.RIG_GITHUB_TOKEN) ?? readStoredGitHubAuthToken(projectRoot);
@@ -168,7 +182,7 @@ async function ensureServerForCli(projectRoot) {
168
182
  if (selected?.connection.kind === "remote") {
169
183
  reportServerPhase(`Connecting to ${selected.alias}\u2026`);
170
184
  const authToken = readGitHubBearerTokenForRemote(projectRoot);
171
- const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
185
+ const serverProjectRoot = selected.serverProjectRoot ?? inferRemoteServerProjectRootFromAuthState(projectRoot) ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
172
186
  return {
173
187
  baseUrl: selected.connection.baseUrl,
174
188
  authToken,
@@ -197,7 +211,10 @@ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken)
197
211
  if (!slug)
198
212
  return null;
199
213
  try {
200
- const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
214
+ const url = new URL(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`);
215
+ if (authToken && queryAuthFallbackEnabled())
216
+ url.searchParams.set("rt", authToken);
217
+ const response = await fetch(url, {
201
218
  headers: mergeHeaders(undefined, authToken)
202
219
  });
203
220
  if (!response.ok)
@@ -214,10 +231,23 @@ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken)
214
231
  return null;
215
232
  }
216
233
  }
234
+ function mergeCookie(existing, name, value) {
235
+ const encoded = `${name}=${encodeURIComponent(value)}`;
236
+ if (!existing?.trim())
237
+ return encoded;
238
+ const parts = existing.split(";").map((part) => part.trim()).filter((part) => part && !part.startsWith(`${name}=`));
239
+ return [...parts, encoded].join("; ");
240
+ }
241
+ function queryAuthFallbackEnabled(env = process.env) {
242
+ return env.RIG_ENABLE_QUERY_AUTH_FALLBACK === "1" || env.RIG_QUERY_AUTH_FALLBACK === "1";
243
+ }
217
244
  function mergeHeaders(headers, authToken) {
218
245
  const merged = new Headers(headers);
219
246
  if (authToken) {
220
- merged.set("authorization", `Bearer ${authToken}`);
247
+ const bearer = `Bearer ${authToken}`;
248
+ merged.set("authorization", bearer);
249
+ merged.set("x-auth", bearer);
250
+ merged.set("cookie", mergeCookie(merged.get("cookie"), "rig_auth", authToken));
221
251
  }
222
252
  return merged;
223
253
  }
@@ -278,15 +308,34 @@ async function buildServerFailureContext(projectRoot, server) {
278
308
  hint: "Check the selected server with `rig server status`, or switch with `rig server use <alias|local>`."
279
309
  };
280
310
  }
311
+ function isLoopbackRemoteBaseUrl(baseUrl) {
312
+ try {
313
+ const host = new URL(baseUrl).hostname.toLowerCase();
314
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
315
+ } catch {
316
+ return false;
317
+ }
318
+ }
319
+ function canUseRemoteWithoutProjectRoot(pathname) {
320
+ return pathname === "/health" || pathname === "/api/health" || pathname === "/api/server/status" || pathname === "/api/server/project-root" || pathname.startsWith("/api/github/auth/") || pathname === "/api/projects" || pathname.startsWith("/api/projects/");
321
+ }
281
322
  async function requestServerJson(context, pathname, init = {}) {
282
323
  const server = await ensureServerForCli(context.projectRoot);
324
+ const requestUrl = new URL(`${server.baseUrl}${pathname}`);
325
+ if (server.connectionKind === "remote" && !server.serverProjectRoot && !canUseRemoteWithoutProjectRoot(requestUrl.pathname) && (server.authToken || !isLoopbackRemoteBaseUrl(server.baseUrl))) {
326
+ const repo = readRepoConnection(context.projectRoot);
327
+ throw new CliError(`Remote server is selected for ${repo?.project ?? "this repo"}, but this checkout has no server-host project root link.`, 1, { hint: "Run `rig init --repair` or `rig init --yes --repo owner/repo --server remote` to repair the remote project link before task/run commands." });
328
+ }
283
329
  const headers = mergeHeaders(init.headers, server.authToken);
284
330
  if (server.serverProjectRoot)
285
331
  headers.set("x-rig-project-root", server.serverProjectRoot);
332
+ if (server.connectionKind === "remote" && server.authToken && queryAuthFallbackEnabled()) {
333
+ requestUrl.searchParams.set("rt", server.authToken);
334
+ }
286
335
  reportServerPhase(`${(init.method ?? "GET").toUpperCase()} ${pathname.split("?")[0]}\u2026`);
287
336
  let response;
288
337
  try {
289
- response = await fetch(`${server.baseUrl}${pathname}`, {
338
+ response = await fetch(requestUrl, {
290
339
  ...init,
291
340
  headers
292
341
  });
@@ -437,7 +486,8 @@ async function runFastTaskRunPreflight(context, options = {}) {
437
486
  }
438
487
  }
439
488
  const repo = readRepoConnection(context.projectRoot);
440
- checks.push(repo ? preflightCheck("project-link", "project linked to Rig server", repo.project ? "pass" : "warn", `${repo.selected}${repo.project ? ` -> ${repo.project}` : ""}`, "Run `rig init --yes --repo owner/repo` to record the GitHub repo slug.") : preflightCheck("project-link", "project linked to Rig server", legacyServerCompatibility ? "warn" : "fail", "missing .rig/state/connection.json", "Run `rig init` or `rig server use <alias|local>`."));
489
+ const remoteWithoutProjectLink = selectedServer?.connectionKind === "remote" && !repo?.project;
490
+ checks.push(repo ? preflightCheck("project-link", "project linked to selected server", repo.project ? "pass" : remoteWithoutProjectLink ? "fail" : "warn", `${repo.selected}${repo.project ? ` -> ${repo.project}` : " (missing project link)"}`, remoteWithoutProjectLink ? "Run `rig init --repair` or `rig init --yes --repo owner/repo --server remote` to restore the remote project link." : "Run `rig init --yes --repo owner/repo` to record the GitHub repo slug.") : preflightCheck("project-link", "project linked to selected server", legacyServerCompatibility ? "warn" : "fail", "missing .rig/state/connection.json", "Run `rig init` or `rig server use <alias|local>`."));
441
491
  try {
442
492
  const auth = await request("/api/github/auth/status");
443
493
  checks.push(isAuthenticated(auth) ? preflightCheck("github-auth", "GitHub auth valid", "pass") : preflightCheck("github-auth", "GitHub auth valid", legacyServerCompatibility ? "warn" : "fail", "not authenticated", "Run `rig github auth import-gh` or `rig github auth token --token <token>`."));
@@ -138,17 +138,21 @@ function setGitHubBearerTokenForCurrentProcess(token, projectRoot) {
138
138
  const scopedKey = resolve2(projectRoot ?? process.cwd());
139
139
  scopedGitHubBearerTokens.set(scopedKey, cleanToken(token ?? undefined));
140
140
  }
141
- function readPrivateRemoteSessionToken(projectRoot) {
141
+ function readRemoteAuthState(projectRoot) {
142
142
  const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
143
143
  if (!existsSync2(path))
144
144
  return null;
145
145
  try {
146
146
  const parsed = JSON.parse(readFileSync2(path, "utf8"));
147
- return cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined);
147
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
148
148
  } catch {
149
149
  return null;
150
150
  }
151
151
  }
152
+ function readPrivateRemoteSessionToken(projectRoot) {
153
+ const parsed = readRemoteAuthState(projectRoot);
154
+ return parsed ? cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined) : null;
155
+ }
152
156
  function readGitHubBearerTokenForRemote(projectRoot) {
153
157
  const scopedKey = resolve2(projectRoot);
154
158
  if (scopedGitHubBearerTokens.has(scopedKey))
@@ -159,15 +163,25 @@ function readGitHubBearerTokenForRemote(projectRoot) {
159
163
  return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
160
164
  }
161
165
  function readStoredGitHubAuthToken(projectRoot) {
162
- const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
163
- if (!existsSync2(path))
166
+ const parsed = readRemoteAuthState(projectRoot);
167
+ return parsed ? cleanToken(typeof parsed.token === "string" ? parsed.token : undefined) : null;
168
+ }
169
+ function inferRemoteServerProjectRootFromAuthState(projectRoot) {
170
+ const repo = readRepoConnection(projectRoot);
171
+ const slug = repo?.project?.trim();
172
+ if (!slug || !/^[^/]+\/[^/]+$/.test(slug))
164
173
  return null;
165
- try {
166
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
167
- return cleanToken(typeof parsed.token === "string" ? parsed.token : undefined);
168
- } catch {
174
+ const auth = readRemoteAuthState(projectRoot);
175
+ const authUpdatedAt = typeof auth?.updatedAt === "string" ? Date.parse(auth.updatedAt) : Number.NaN;
176
+ const repoLinkedAt = typeof repo?.linkedAt === "string" ? Date.parse(repo.linkedAt) : Number.NaN;
177
+ if (Number.isFinite(authUpdatedAt) && Number.isFinite(repoLinkedAt) && authUpdatedAt + 1000 < repoLinkedAt)
169
178
  return null;
170
- }
179
+ const checkoutBaseDir = typeof auth?.checkoutBaseDir === "string" && auth.checkoutBaseDir.trim() ? auth.checkoutBaseDir.trim() : null;
180
+ if (!checkoutBaseDir)
181
+ return null;
182
+ const inferred = `${checkoutBaseDir.replace(/\/+$/, "")}/${slug}`;
183
+ writeRepoServerProjectRoot(projectRoot, inferred);
184
+ return inferred;
171
185
  }
172
186
  function readLocalConnectionFallbackToken(projectRoot) {
173
187
  return readGitHubBearerTokenForRemote(projectRoot) ?? cleanToken(process.env.RIG_GITHUB_TOKEN) ?? readStoredGitHubAuthToken(projectRoot);
@@ -178,7 +192,7 @@ async function ensureServerForCli(projectRoot) {
178
192
  if (selected?.connection.kind === "remote") {
179
193
  reportServerPhase(`Connecting to ${selected.alias}\u2026`);
180
194
  const authToken = readGitHubBearerTokenForRemote(projectRoot);
181
- const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
195
+ const serverProjectRoot = selected.serverProjectRoot ?? inferRemoteServerProjectRootFromAuthState(projectRoot) ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
182
196
  return {
183
197
  baseUrl: selected.connection.baseUrl,
184
198
  authToken,
@@ -207,7 +221,10 @@ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken)
207
221
  if (!slug)
208
222
  return null;
209
223
  try {
210
- const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
224
+ const url = new URL(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`);
225
+ if (authToken && queryAuthFallbackEnabled())
226
+ url.searchParams.set("rt", authToken);
227
+ const response = await fetch(url, {
211
228
  headers: mergeHeaders(undefined, authToken)
212
229
  });
213
230
  if (!response.ok)
@@ -234,10 +251,23 @@ function appendTaskFilterParams(url, filters) {
234
251
  if (filters.limit !== undefined)
235
252
  url.searchParams.set("limit", String(filters.limit));
236
253
  }
254
+ function mergeCookie(existing, name, value) {
255
+ const encoded = `${name}=${encodeURIComponent(value)}`;
256
+ if (!existing?.trim())
257
+ return encoded;
258
+ const parts = existing.split(";").map((part) => part.trim()).filter((part) => part && !part.startsWith(`${name}=`));
259
+ return [...parts, encoded].join("; ");
260
+ }
261
+ function queryAuthFallbackEnabled(env = process.env) {
262
+ return env.RIG_ENABLE_QUERY_AUTH_FALLBACK === "1" || env.RIG_QUERY_AUTH_FALLBACK === "1";
263
+ }
237
264
  function mergeHeaders(headers, authToken) {
238
265
  const merged = new Headers(headers);
239
266
  if (authToken) {
240
- merged.set("authorization", `Bearer ${authToken}`);
267
+ const bearer = `Bearer ${authToken}`;
268
+ merged.set("authorization", bearer);
269
+ merged.set("x-auth", bearer);
270
+ merged.set("cookie", mergeCookie(merged.get("cookie"), "rig_auth", authToken));
241
271
  }
242
272
  return merged;
243
273
  }
@@ -298,15 +328,34 @@ async function buildServerFailureContext(projectRoot, server) {
298
328
  hint: "Check the selected server with `rig server status`, or switch with `rig server use <alias|local>`."
299
329
  };
300
330
  }
331
+ function isLoopbackRemoteBaseUrl(baseUrl) {
332
+ try {
333
+ const host = new URL(baseUrl).hostname.toLowerCase();
334
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
335
+ } catch {
336
+ return false;
337
+ }
338
+ }
339
+ function canUseRemoteWithoutProjectRoot(pathname) {
340
+ return pathname === "/health" || pathname === "/api/health" || pathname === "/api/server/status" || pathname === "/api/server/project-root" || pathname.startsWith("/api/github/auth/") || pathname === "/api/projects" || pathname.startsWith("/api/projects/");
341
+ }
301
342
  async function requestServerJson(context, pathname, init = {}) {
302
343
  const server = await ensureServerForCli(context.projectRoot);
344
+ const requestUrl = new URL(`${server.baseUrl}${pathname}`);
345
+ if (server.connectionKind === "remote" && !server.serverProjectRoot && !canUseRemoteWithoutProjectRoot(requestUrl.pathname) && (server.authToken || !isLoopbackRemoteBaseUrl(server.baseUrl))) {
346
+ const repo = readRepoConnection(context.projectRoot);
347
+ throw new CliError(`Remote server is selected for ${repo?.project ?? "this repo"}, but this checkout has no server-host project root link.`, 1, { hint: "Run `rig init --repair` or `rig init --yes --repo owner/repo --server remote` to repair the remote project link before task/run commands." });
348
+ }
303
349
  const headers = mergeHeaders(init.headers, server.authToken);
304
350
  if (server.serverProjectRoot)
305
351
  headers.set("x-rig-project-root", server.serverProjectRoot);
352
+ if (server.connectionKind === "remote" && server.authToken && queryAuthFallbackEnabled()) {
353
+ requestUrl.searchParams.set("rt", server.authToken);
354
+ }
306
355
  reportServerPhase(`${(init.method ?? "GET").toUpperCase()} ${pathname.split("?")[0]}\u2026`);
307
356
  let response;
308
357
  try {
309
- response = await fetch(`${server.baseUrl}${pathname}`, {
358
+ response = await fetch(requestUrl, {
310
359
  ...init,
311
360
  headers
312
361
  });
@@ -368,11 +417,26 @@ async function getGitHubAuthStatusViaServer(context) {
368
417
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
369
418
  }
370
419
  async function postGitHubTokenViaServer(context, token, options = {}) {
371
- const payload = await requestServerJson(context, "/api/github/auth/token", {
372
- method: "POST",
373
- headers: { "content-type": "application/json" },
374
- body: JSON.stringify({ token, selectedRepo: options.selectedRepo, projectRoot: options.projectRoot })
375
- });
420
+ const server = await ensureServerForCli(context.projectRoot);
421
+ const requestUrl = new URL(`${server.baseUrl}/api/github/auth/token`);
422
+ reportServerPhase("POST /api/github/auth/token\u2026");
423
+ let response;
424
+ try {
425
+ response = await fetch(requestUrl, {
426
+ method: "POST",
427
+ headers: { "content-type": "application/json" },
428
+ body: JSON.stringify({ token, selectedRepo: options.selectedRepo, projectRoot: options.projectRoot ?? server.serverProjectRoot ?? undefined })
429
+ });
430
+ } catch (error) {
431
+ const failure = await buildServerFailureContext(context.projectRoot, server);
432
+ throw new CliError(`Rig server auth bootstrap failed: ${error instanceof Error ? error.message : String(error)}
433
+ ${failure.contextLine}`, 1, { hint: failure.hint });
434
+ }
435
+ const payload = await response.json().catch(() => null);
436
+ if (!response.ok) {
437
+ const detail = payload && typeof payload === "object" && !Array.isArray(payload) ? String(payload.error ?? JSON.stringify(payload)) : `HTTP ${response.status}`;
438
+ throw new CliError(`Rig server auth bootstrap failed (${response.status}): ${detail}`, 1, { hint: "Re-run `rig github auth import-gh`, or check the selected server with `rig server status`." });
439
+ }
376
440
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
377
441
  }
378
442
  async function prepareRemoteCheckoutViaServer(context, input) {