spacecode 2.0.0 → 2.0.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.
package/dist/index.js CHANGED
@@ -585,6 +585,7 @@ var config_exports = {};
585
585
  __export(config_exports, {
586
586
  addTask: () => addTask,
587
587
  addTasks: () => addTasks,
588
+ applyApiKeyFlag: () => applyApiKeyFlag,
588
589
  cancelTask: () => cancelTask,
589
590
  clearAuth: () => clearAuth,
590
591
  getConfigDir: () => getConfigDir,
@@ -837,6 +838,9 @@ async function clearAuth() {
837
838
  }
838
839
  }
839
840
  }
841
+ function applyApiKeyFlag(key) {
842
+ if (key?.startsWith("sk_")) process.env.SPACECODE_TOKEN = key;
843
+ }
840
844
  async function resolveAuthToken() {
841
845
  const envToken = process.env.SPACECODE_TOKEN;
842
846
  if (envToken) {
@@ -7421,17 +7425,6 @@ function renderEvent(event) {
7421
7425
  case "text": {
7422
7426
  break;
7423
7427
  }
7424
- case "text": {
7425
- if (phase === "thinking") {
7426
- stopThinkingTimer();
7427
- clearLine();
7428
- process.stderr.write("\n");
7429
- }
7430
- phase = "streaming";
7431
- process.stdout.write(renderMarkdown(event.content));
7432
- if (!event.content.endsWith("\n")) process.stdout.write("\n");
7433
- break;
7434
- }
7435
7428
  case "text_delta": {
7436
7429
  if (phase === "thinking") {
7437
7430
  stopThinkingTimer();
@@ -7904,6 +7897,143 @@ var init_init = __esm({
7904
7897
  }
7905
7898
  });
7906
7899
 
7900
+ // src/auth/device-protocol.ts
7901
+ function parseDeviceCode(status2, body) {
7902
+ if (status2 === 429) {
7903
+ throw new Error("Too many login attempts; wait a few minutes and retry");
7904
+ }
7905
+ if (status2 === 404 || status2 === 401 || status2 === 503) {
7906
+ throw new Error(
7907
+ `Browser sign-in is not available on the server yet (status ${status2}); use \`spacecode login --api-key\` instead`
7908
+ );
7909
+ }
7910
+ if (status2 !== 200) {
7911
+ throw new Error(`Could not start login: status ${status2}`);
7912
+ }
7913
+ if (typeof body !== "object" || body === null) {
7914
+ throw new Error(`Could not start login: status ${status2}`);
7915
+ }
7916
+ const obj = body;
7917
+ const requiredStringFields = [
7918
+ "device_code",
7919
+ "user_code",
7920
+ "verification_uri"
7921
+ ];
7922
+ for (const field of requiredStringFields) {
7923
+ const value = obj[field];
7924
+ if (typeof value !== "string") {
7925
+ throw new Error(`Could not start login: status ${status2}`);
7926
+ }
7927
+ }
7928
+ const interval = obj["interval"];
7929
+ if (typeof interval !== "number" || interval <= 0) {
7930
+ throw new Error(`Could not start login: status ${status2}`);
7931
+ }
7932
+ const expiresIn = obj["expires_in"];
7933
+ if (typeof expiresIn !== "number" || expiresIn <= 0) {
7934
+ throw new Error(`Could not start login: status ${status2}`);
7935
+ }
7936
+ const verificationUriComplete = typeof obj["verification_uri_complete"] === "string" ? obj["verification_uri_complete"].trim() : "";
7937
+ const url = verificationUriComplete !== "" ? verificationUriComplete : obj["verification_uri"];
7938
+ return {
7939
+ deviceCode: obj["device_code"],
7940
+ userCode: obj["user_code"],
7941
+ url,
7942
+ intervalSec: interval,
7943
+ expiresInSec: expiresIn
7944
+ };
7945
+ }
7946
+ function classifyTokenResponse(status2, body, intervalSec) {
7947
+ if (status2 === 200) {
7948
+ if (typeof body === "object" && body !== null) {
7949
+ const b = body;
7950
+ if (typeof b.key === "string" && b.key.startsWith("sk_")) {
7951
+ const result = {
7952
+ kind: "done",
7953
+ key: b.key
7954
+ };
7955
+ if (typeof b.keyPrefix === "string") {
7956
+ result.keyPrefix = b.keyPrefix;
7957
+ }
7958
+ return result;
7959
+ }
7960
+ }
7961
+ throw new Error("Login returned an invalid key");
7962
+ }
7963
+ const isObjectBody = typeof body === "object" && body !== null;
7964
+ const error = isObjectBody ? body.error : void 0;
7965
+ if (error === "authorization_pending") {
7966
+ return { kind: "pending", intervalSec };
7967
+ }
7968
+ if (error === "slow_down") {
7969
+ return { kind: "pending", intervalSec: intervalSec + 5 };
7970
+ }
7971
+ if (error === "expired_token") {
7972
+ throw new Error("Login code expired");
7973
+ }
7974
+ if (error === "access_denied") {
7975
+ throw new Error("Login was denied");
7976
+ }
7977
+ if (error === "invalid_grant") {
7978
+ throw new Error("Login code is no longer valid; run `spacecode login` again");
7979
+ }
7980
+ if (error === "max_keys_reached") {
7981
+ throw new Error("Your organization has reached its API key limit; revoke an old key in the dashboard and retry");
7982
+ }
7983
+ const errMsg = typeof error === "string" ? `: ${error}` : "";
7984
+ throw new Error(`Login failed (${status2})${errMsg}`);
7985
+ }
7986
+ var init_device_protocol = __esm({
7987
+ "src/auth/device-protocol.ts"() {
7988
+ "use strict";
7989
+ }
7990
+ });
7991
+
7992
+ // src/auth/device-login.ts
7993
+ async function deviceLogin(baseUrl, deps) {
7994
+ const start = await deps.post(`${baseUrl}/api/device/code`, {});
7995
+ const info = parseDeviceCode(start.status, start.body);
7996
+ deps.log(`Your code: ${info.userCode}
7997
+ Approve sign-in at: ${info.url}`);
7998
+ try {
7999
+ await deps.openBrowser(info.url);
8000
+ } catch (err) {
8001
+ const reason = err instanceof Error ? err.message : String(err);
8002
+ deps.log(`Could not open a browser (${reason}); open ${info.url} manually`);
8003
+ }
8004
+ const deadline = deps.now() + info.expiresInSec * 1e3;
8005
+ let interval = info.intervalSec;
8006
+ let failures = 0;
8007
+ for (; ; ) {
8008
+ if (deps.now() >= deadline) {
8009
+ throw new Error("Login code expired");
8010
+ }
8011
+ await deps.sleep(interval * 1e3);
8012
+ let response;
8013
+ try {
8014
+ response = await deps.post(`${baseUrl}/api/device/token`, { device_code: info.deviceCode });
8015
+ } catch (err) {
8016
+ failures += 1;
8017
+ if (failures >= MAX_CONSECUTIVE_POST_FAILURES) throw err;
8018
+ continue;
8019
+ }
8020
+ failures = 0;
8021
+ const result = classifyTokenResponse(response.status, response.body, interval);
8022
+ if (result.kind === "done") {
8023
+ return result.keyPrefix === void 0 ? { key: result.key } : { key: result.key, keyPrefix: result.keyPrefix };
8024
+ }
8025
+ interval = result.intervalSec;
8026
+ }
8027
+ }
8028
+ var MAX_CONSECUTIVE_POST_FAILURES;
8029
+ var init_device_login = __esm({
8030
+ "src/auth/device-login.ts"() {
8031
+ "use strict";
8032
+ init_device_protocol();
8033
+ MAX_CONSECUTIVE_POST_FAILURES = 3;
8034
+ }
8035
+ });
8036
+
7907
8037
  // src/commands/auth.ts
7908
8038
  async function login(options = {}) {
7909
8039
  const apiKey2 = options.apiKey?.trim();
@@ -7911,22 +8041,42 @@ async function login(options = {}) {
7911
8041
  if (!apiKey2.startsWith("sk_") || apiKey2.length < 12 || /\s/.test(apiKey2)) {
7912
8042
  throw new Error("That does not look like a SpaceCode API key (expected sk_...).");
7913
8043
  }
7914
- await saveAuth({
7915
- accessToken: apiKey2,
7916
- user: { id: "api-key", githubId: "", githubLogin: `api-key ${apiKey2.slice(0, 7)}...` }
7917
- });
8044
+ await saveApiKey(apiKey2);
7918
8045
  console.log(import_chalk17.default.green("API key saved. You are signed in."));
7919
8046
  return;
7920
8047
  }
7921
- console.error(import_chalk17.default.yellow("Interactive GitHub login is no longer supported."));
7922
- console.error("Authenticate with an API key:");
7923
- console.error(` ${import_chalk17.default.bold("spacecode login --api-key sk_...")} key from your SpaceCode dashboard (saved to ~/.spacecode/auth.json)`);
7924
- console.error(` ${import_chalk17.default.bold("export SPACECODE_TOKEN=sk_...")} or set it in the environment (CI, scripts)`);
7925
- console.error("Service credentials (internal use):");
7926
- console.error(` ${import_chalk17.default.bold("export SPACECODE_TOKEN=sct_...")} personal access token (exchanged for a short-lived JWT)`);
7927
- console.error(` ${import_chalk17.default.bold("export SPACECODE_TOKEN=<secret>")} sandbox shared secret (acts as the internal user)`);
7928
- console.error(` ${import_chalk17.default.bold("export SPACECODE_SHARED_TOKEN=<secret>")} backend gate secret (required for PAT exchange and backend calls)`);
7929
- throw new Error("No credentials given; run `spacecode login --api-key sk_...` or set SPACECODE_TOKEN");
8048
+ const { key } = await deviceLogin(process.env.SPACECODE_AUTH_URL ?? DEFAULT_AUTH_URL, {
8049
+ post: postJson,
8050
+ sleep: (ms) => new Promise((resolve2) => setTimeout(resolve2, ms)),
8051
+ now: () => Date.now(),
8052
+ openBrowser: async (url) => {
8053
+ const { default: open } = await import("open");
8054
+ await open(url);
8055
+ },
8056
+ log: (msg) => console.log(msg)
8057
+ });
8058
+ await saveApiKey(key);
8059
+ console.log(import_chalk17.default.green("Signed in. API key saved to ~/.spacecode/auth.json."));
8060
+ }
8061
+ async function saveApiKey(apiKey2) {
8062
+ await saveAuth({
8063
+ accessToken: apiKey2,
8064
+ user: { id: "api-key", githubId: "", githubLogin: `api-key ${apiKey2.slice(0, 7)}...` }
8065
+ });
8066
+ }
8067
+ async function postJson(url, body) {
8068
+ const res = await fetch(url, {
8069
+ method: "POST",
8070
+ headers: { "content-type": "application/json" },
8071
+ body: JSON.stringify(body),
8072
+ signal: AbortSignal.timeout(15e3)
8073
+ });
8074
+ let parsed = null;
8075
+ try {
8076
+ parsed = await res.json();
8077
+ } catch {
8078
+ }
8079
+ return { status: res.status, body: parsed };
7930
8080
  }
7931
8081
  async function logout() {
7932
8082
  const auth = await loadAuth();
@@ -7992,7 +8142,7 @@ async function requireAuth() {
7992
8142
  }
7993
8143
  return new BackendApiClient(apiUrl, freshAuth.accessToken);
7994
8144
  }
7995
- var import_chalk17;
8145
+ var import_chalk17, DEFAULT_AUTH_URL;
7996
8146
  var init_auth2 = __esm({
7997
8147
  "src/commands/auth.ts"() {
7998
8148
  "use strict";
@@ -8000,6 +8150,8 @@ var init_auth2 = __esm({
8000
8150
  init_remote();
8001
8151
  init_client();
8002
8152
  import_chalk17 = __toESM(require("chalk"));
8153
+ init_device_login();
8154
+ DEFAULT_AUTH_URL = "https://spacecode-app.g-a-l-a-c-t-i-c.com";
8003
8155
  }
8004
8156
  });
8005
8157
 
@@ -8600,7 +8752,7 @@ Agent error: ${message}
8600
8752
  fileSync.start();
8601
8753
  }
8602
8754
  const ttyHost = new URL(wsUrl.replace("wss://", "https://").replace("ws://", "http://")).host;
8603
- const version = "2.0.0";
8755
+ const version = "2.0.2";
8604
8756
  if (isResume) {
8605
8757
  let turnCount = 0;
8606
8758
  try {
@@ -23761,7 +23913,7 @@ async function startLocalRepl(opts = {}) {
23761
23913
  "Local REPL not built. Run `pnpm --filter @spacecode/agent-ui build` (or set SPACECODE_REPL_BIN), or use `spacecode --cloud` for a cloud session."
23762
23914
  );
23763
23915
  }
23764
- const args = [bin, ...opts.yolo ? ["--yolo"] : []];
23916
+ const args = [bin, ...opts.yolo ? ["--yolo"] : [], ...opts.resume ? ["--continue"] : []];
23765
23917
  return new Promise((resolve2, reject) => {
23766
23918
  const child = (0, import_node_child_process3.spawn)(process.execPath, args, { stdio: "inherit" });
23767
23919
  child.on("error", reject);
@@ -24042,11 +24194,12 @@ var init_index = __esm({
24042
24194
  init_token();
24043
24195
  init_agent2();
24044
24196
  init_sentry();
24197
+ init_config();
24045
24198
  init_config3();
24046
24199
  init_analytics();
24047
24200
  initPostHog();
24048
24201
  program = new import_commander.Command();
24049
- program.name("spacecode").description("SpaceCode - launch cloud dev environments with AI coding agents").version("2.0.0").enablePositionalOptions();
24202
+ program.name("spacecode").description("SpaceCode - launch cloud dev environments with AI coding agents").version("2.0.2").enablePositionalOptions();
24050
24203
  program.command("launch").description("Launch a new cloud session").option("-R, --repo <repo>", "Clone a GitHub repository into workspace (URL or owner/repo)").option("-n, --new [name]", "Start a new project (optionally specify name, or will prompt)").option("--name <name>", "Name this session for easy reconnection").option("--destroy", "Destroy the session on exit (default: session stays alive)").option("-t, --idle-timeout <minutes>", "Auto-shutdown timeout in minutes (default: 15)", "15").option("--proxy-mode <mode>", 'API proxy mode: "legacy" (round-robin GLM) or "pipeline" (multi-model router)', "pipeline").option("--own-key", "Bring your own API key (no proxy)").option("--api-key <key>", "Use a specific API key (bypasses proxy)").action(async (options) => {
24051
24204
  await launchWithOptions(options);
24052
24205
  });
@@ -24285,11 +24438,12 @@ Batch ${result.groupId}: ${result.dispatched}/${result.total} dispatched`));
24285
24438
  showConfigPaths();
24286
24439
  }
24287
24440
  });
24288
- program.argument("[target]", "Repo (owner/repo) to launch, or session name/ID to attach").option("-p, --prompt [prompt]", "Run headless (non-interactive)").option("--output-format <format>", "Output format for -p: text (default) or json", "text").option("-v, --verbose", "Show detailed progress (phases, stderr)").option("-R, --repo <repo>", "Clone a GitHub repository into workspace").option("-n, --new [name]", "Start a new project").option("--name <name>", "Name this session for easy reconnection").option("--destroy", "Destroy the session on exit").option("-b, --branch <branch>", "Branch name for the task").option("--no-commit", "Skip auto-commit and PR creation").option("-t, --idle-timeout <minutes>", "Auto-shutdown timeout (default: 15)", "15").option("--proxy-mode <mode>", "API proxy mode", "pipeline").option("--own-key", "Bring your own API key").option("--api-key <key>", "Use a specific API key").option("--session <id>", "Resume an existing agent session").option("--cloud", "With no target: start a cloud agent session instead of the local REPL").option("--yolo", "Local REPL: skip confirmation prompts (policy denies still apply)").action(async (target, options) => {
24441
+ program.argument("[target]", "Repo (owner/repo) to launch, or session name/ID to attach").option("-p, --prompt [prompt]", "Run headless (non-interactive)").option("--output-format <format>", "Output format for -p: text (default) or json", "text").option("-v, --verbose", "Show detailed progress (phases, stderr)").option("-R, --repo <repo>", "Clone a GitHub repository into workspace").option("-n, --new [name]", "Start a new project").option("--name <name>", "Name this session for easy reconnection").option("--destroy", "Destroy the session on exit").option("-b, --branch <branch>", "Branch name for the task").option("--no-commit", "Skip auto-commit and PR creation").option("-t, --idle-timeout <minutes>", "Auto-shutdown timeout (default: 15)", "15").option("--proxy-mode <mode>", "API proxy mode", "pipeline").option("--own-key", "Bring your own API key").option("--api-key <key>", "Use a specific API key").option("--session <id>", "Resume an existing agent session").option("--cloud", "With no target: start a cloud agent session instead of the local REPL").option("--yolo", "Local REPL: skip confirmation prompts (policy denies still apply)").option("--continue", "Local REPL: resume the newest saved conversation for this project").action(async (target, options) => {
24289
24442
  try {
24443
+ applyApiKeyFlag(options.apiKey);
24290
24444
  if (!target && !options.session && options.prompt === void 0 && !options.cloud && !options.repo && !options.new) {
24291
24445
  const { startLocalRepl: startLocalRepl2 } = await Promise.resolve().then(() => (init_repl(), repl_exports));
24292
- process.exit(await startLocalRepl2({ yolo: options.yolo }));
24446
+ process.exit(await startLocalRepl2({ yolo: options.yolo, resume: options.continue }));
24293
24447
  }
24294
24448
  if (options.session) {
24295
24449
  let sessionId = options.session;