@mtreeai/msapling-cli 2.3.6-beta.6 → 2.3.6-beta.8

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 (2) hide show
  1. package/dist/index.js +90 -87
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -284,8 +284,19 @@ var init_src = __esm({
284
284
  }
285
285
  /**
286
286
  * Login with email and password.
287
- * Returns access_token on success, or indicates TOTP is required.
288
- * On 401: throws MSaplingError with code 'invalid_credentials'
287
+ *
288
+ * The JWT `mfa` claim is the *session* MFA state ("has this session completed
289
+ * TOTP yet"), not "TOTP is required". Treating `mfa:false` as TOTP-required
290
+ * was wrong: it blocked every user without 2FA enabled, including all
291
+ * guest/free accounts and the live backend which doesn't enforce TOTP yet.
292
+ *
293
+ * New contract: accept the token as fully usable. If the backend enforces
294
+ * MFA on a subsequent protected endpoint, that endpoint will reply with
295
+ * `mfa_required` and the caller can step up via verifyLoginTotp(). Until
296
+ * the backend exposes an explicit `mfa_required` field on the login
297
+ * response, we don't pre-emptively branch.
298
+ *
299
+ * On 401: throws MSaplingError with code 'invalid_credentials'.
289
300
  */
290
301
  async loginEmailPassword(email, password) {
291
302
  const params = new URLSearchParams();
@@ -305,10 +316,6 @@ var init_src = __esm({
305
316
  clearTimeout(timeout);
306
317
  if (response.ok) {
307
318
  const data = await response.json();
308
- const payload = this.decodeJwtPayload(data.access_token);
309
- if (payload && payload.mfa === false) {
310
- return { kind: "totp_required", partialToken: data.access_token };
311
- }
312
319
  return { kind: "success", token: data.access_token };
313
320
  }
314
321
  if (response.status === 401) {
@@ -330,6 +337,41 @@ var init_src = __esm({
330
337
  clearTimeout(timeout);
331
338
  }
332
339
  }
340
+ /**
341
+ * Sign in as a guest. Backend issues a guest-scoped JWT with limited tier
342
+ * capabilities. No prior credentials required.
343
+ */
344
+ async loginGuest() {
345
+ const controller = new AbortController();
346
+ const timeout = setTimeout(() => controller.abort(), 3e4);
347
+ try {
348
+ const response = await fetch(`${this.apiUrl}/auth/guest`, {
349
+ method: "POST",
350
+ headers: { "Content-Type": "application/json" },
351
+ body: "{}",
352
+ signal: controller.signal
353
+ });
354
+ clearTimeout(timeout);
355
+ if (response.ok) {
356
+ const data = await response.json();
357
+ return { kind: "success", token: data.access_token };
358
+ }
359
+ let detail = "Guest login failed";
360
+ try {
361
+ const err = await response.json();
362
+ detail = err.detail || detail;
363
+ } catch (e) {
364
+ }
365
+ throw new MSaplingError(detail, response.status, "guest_login_failed");
366
+ } catch (e) {
367
+ if (e.name === "AbortError") {
368
+ throw new MSaplingError("Request timed out after 30s.", 408, "timeout");
369
+ }
370
+ throw e;
371
+ } finally {
372
+ clearTimeout(timeout);
373
+ }
374
+ }
333
375
  /**
334
376
  * Verify TOTP code during login.
335
377
  * Requires a partial token from a prior /auth/login call when MFA is enabled.
@@ -339,7 +381,7 @@ var init_src = __esm({
339
381
  const controller = new AbortController();
340
382
  const timeout = setTimeout(() => controller.abort(), 3e4);
341
383
  try {
342
- const response = await fetch(`${this.apiUrl}/auth/login-verify`, {
384
+ const response = await fetch(`${this.apiUrl}/api/mfa/login-verify`, {
343
385
  method: "POST",
344
386
  headers: {
345
387
  "Authorization": `Bearer ${this.token}`,
@@ -3541,17 +3583,17 @@ async function backupFile(absPath) {
3541
3583
  try {
3542
3584
  const { readFile: readFile23, writeFile: writeFile12, mkdir: mkdir9 } = await import("fs/promises");
3543
3585
  const { homedir: homedir17 } = await import("os");
3544
- const { join: join30 } = await import("path");
3586
+ const { join: join31 } = await import("path");
3545
3587
  const filename = absPath.split(/[\\/]/).pop() ?? "file";
3546
3588
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3547
3589
  const suffix = randomBytes5(4).toString("hex");
3548
- const backupPath = join30(
3590
+ const backupPath = join31(
3549
3591
  homedir17(),
3550
3592
  ".msapling",
3551
3593
  "backups",
3552
3594
  `${filename}.backup-${stamp}-${suffix}.bak`
3553
3595
  );
3554
- await mkdir9(join30(homedir17(), ".msapling", "backups"), { recursive: true });
3596
+ await mkdir9(join31(homedir17(), ".msapling", "backups"), { recursive: true });
3555
3597
  const content = await readFile23(absPath, "utf8");
3556
3598
  await writeFile12(backupPath, content, "utf8");
3557
3599
  return backupPath;
@@ -7964,37 +8006,6 @@ async function promptPassword(prompt) {
7964
8006
  stdin.on("data", onData);
7965
8007
  });
7966
8008
  }
7967
- async function promptTotp(prompt) {
7968
- return new Promise((resolve18) => {
7969
- const stdin = process.stdin;
7970
- const stdout = process.stdout;
7971
- stdout.write(prompt);
7972
- const wasRaw = stdin.isRaw ?? false;
7973
- setRawModeGuarded(stdin, true);
7974
- let code = "";
7975
- const onData = (chunk) => {
7976
- const char = chunk.toString();
7977
- if (char === "\n" || char === "\r") {
7978
- stdin.setRawMode(wasRaw);
7979
- stdin.removeListener("data", onData);
7980
- stdout.write("\n");
7981
- resolve18(code);
7982
- } else if (char === "") {
7983
- setRawModeGuarded(stdin, wasRaw);
7984
- stdin.removeListener("data", onData);
7985
- stdout.write("\n");
7986
- resolve18("");
7987
- } else if (char === "\x7F" || char === "\b") {
7988
- code = code.slice(0, -1);
7989
- } else if (/^\d$/.test(char)) {
7990
- if (code.length < 6) {
7991
- code += char;
7992
- }
7993
- }
7994
- };
7995
- stdin.on("data", onData);
7996
- });
7997
- }
7998
8009
  async function loginWithEmailPassword(email, context) {
7999
8010
  context.addMessage("system", `Logging in as ${email}...`);
8000
8011
  let password = "";
@@ -8011,40 +8022,26 @@ async function loginWithEmailPassword(email, context) {
8011
8022
  try {
8012
8023
  const result = await context.client.loginEmailPassword(email, password);
8013
8024
  password = "";
8014
- if (result.kind === "totp_required") {
8015
- context.addMessage("system", "Two-factor authentication required.");
8016
- let totpCode = "";
8017
- try {
8018
- totpCode = await promptTotp("Enter your 6-digit code: ");
8019
- } catch (err) {
8020
- context.addMessage("error", `TOTP prompt failed: ${err}`);
8021
- return;
8022
- }
8023
- if (!totpCode || totpCode.length !== 6) {
8024
- context.addMessage("error", "Invalid TOTP code (must be 6 digits).");
8025
- return;
8026
- }
8027
- context.client.setToken(result.partialToken);
8028
- try {
8029
- const totpResult = await context.client.verifyLoginTotp(totpCode);
8030
- await context.storage.saveToken(totpResult.token);
8031
- context.client.setToken(totpResult.token);
8032
- await context.refreshOverview();
8033
- context.addMessage("system", `Logged in as ${email} (2FA verified).`);
8034
- } catch (err) {
8035
- context.client.setToken(null);
8036
- context.addMessage("error", `TOTP verification failed: ${err}`);
8037
- }
8038
- } else if (result.kind === "success") {
8039
- await context.storage.saveToken(result.token);
8040
- context.client.setToken(result.token);
8041
- await context.refreshOverview();
8042
- context.addMessage("system", `Logged in as ${email}.`);
8043
- }
8025
+ await context.storage.saveToken(result.token);
8026
+ context.client.setToken(result.token);
8027
+ await context.refreshOverview();
8028
+ context.addMessage("system", `Logged in as ${email}.`);
8044
8029
  } catch (err) {
8045
8030
  context.addMessage("error", `Login failed: ${err}`);
8046
8031
  }
8047
8032
  }
8033
+ async function loginAsGuest(context) {
8034
+ context.addMessage("system", "Signing in as guest...");
8035
+ try {
8036
+ const result = await context.client.loginGuest();
8037
+ await context.storage.saveToken(result.token);
8038
+ context.client.setToken(result.token);
8039
+ await context.refreshOverview();
8040
+ context.addMessage("system", "Signed in as guest (limited tier).");
8041
+ } catch (err) {
8042
+ context.addMessage("error", `Guest login failed: ${err}`);
8043
+ }
8044
+ }
8048
8045
  async function loginWithGithubDevice(context) {
8049
8046
  context.addMessage("system", "Starting GitHub device flow...");
8050
8047
  let deviceResp;
@@ -8139,8 +8136,8 @@ var init_login = __esm({
8139
8136
  GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token";
8140
8137
  loginCommand = {
8141
8138
  name: "login",
8142
- args: "[email|github|token]",
8143
- description: "Authenticate: /login your@email.com | /login github | /login <token>",
8139
+ args: "[email|github|guest|token]",
8140
+ description: "Authenticate: /login your@email.com | /login github | /login guest | /login <token>",
8144
8141
  category: "auth",
8145
8142
  handler: async (args2, context) => {
8146
8143
  const arg = args2[0];
@@ -8150,12 +8147,15 @@ var init_login = __esm({
8150
8147
  `Usage:
8151
8148
  /login your@email.com Sign in with email + password
8152
8149
  /login github GitHub device flow
8150
+ /login guest Anonymous guest session (limited tier)
8153
8151
  /login <token> Paste an API token from msapling.com \u2192 Settings \u2192 API Keys`
8154
8152
  );
8155
8153
  return;
8156
8154
  }
8157
8155
  if (arg === "github") {
8158
8156
  await loginWithGithubDevice(context);
8157
+ } else if (arg === "guest") {
8158
+ await loginAsGuest(context);
8159
8159
  } else if (isEmailAddress(arg)) {
8160
8160
  await loginWithEmailPassword(arg, context);
8161
8161
  } else if (isTokenLike(arg)) {
@@ -8166,7 +8166,7 @@ var init_login = __esm({
8166
8166
  } else {
8167
8167
  context.addMessage(
8168
8168
  "error",
8169
- `Unrecognized argument: "${arg}". Expected email, 'github', or token string.`
8169
+ `Unrecognized argument: "${arg}". Expected email, 'github', 'guest', or token string.`
8170
8170
  );
8171
8171
  }
8172
8172
  }
@@ -13134,7 +13134,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
13134
13134
  var Header = () => /* @__PURE__ */ jsxs(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 1, marginBottom: 1, children: [
13135
13135
  /* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
13136
13136
  "\u25CF MSapling CLI v",
13137
- "2.3.6-beta.6"
13137
+ "2.3.6-beta.8"
13138
13138
  ] }),
13139
13139
  /* @__PURE__ */ jsx(Box, { marginLeft: 2, children: /* @__PURE__ */ jsx(Text, { color: "gray", children: "Platinum Tier Architecture" }) })
13140
13140
  ] });
@@ -13779,8 +13779,8 @@ async function initSession(ctx) {
13779
13779
  }
13780
13780
  try {
13781
13781
  const { homedir: homedir17 } = await import("os");
13782
- const { join: join30 } = await import("path");
13783
- const userSettingsPath = join30(homedir17(), ".msapling", "settings.json");
13782
+ const { join: join31 } = await import("path");
13783
+ const userSettingsPath = join31(homedir17(), ".msapling", "settings.json");
13784
13784
  if (existsSync25(userSettingsPath)) {
13785
13785
  const userText = await readFile22(userSettingsPath, "utf8");
13786
13786
  let parsed;
@@ -14077,18 +14077,21 @@ var App = ({ compact: compact2 = false }) => {
14077
14077
 
14078
14078
  // src/runtime/bootstrap.ts
14079
14079
  init_esm_shims();
14080
- import { createRequire } from "module";
14080
+ import { readFileSync as readFileSync3 } from "fs";
14081
+ import { fileURLToPath as fileURLToPath2 } from "url";
14082
+ import { dirname as dirname3, join as join30 } from "path";
14081
14083
  function readCliVersion2() {
14082
- try {
14083
- const require2 = createRequire(import.meta.url);
14084
- const pkg = require2("../../package.json");
14085
- return {
14086
- name: typeof pkg.name === "string" && pkg.name ? pkg.name : "@msapling/cli",
14087
- version: typeof pkg.version === "string" && pkg.version ? pkg.version : "unknown"
14088
- };
14089
- } catch {
14090
- return { name: "@msapling/cli", version: "unknown" };
14084
+ const here = dirname3(fileURLToPath2(import.meta.url));
14085
+ for (const rel of ["../package.json", "../../package.json"]) {
14086
+ try {
14087
+ const pkg = JSON.parse(readFileSync3(join30(here, rel), "utf8"));
14088
+ if (pkg.name && pkg.version) {
14089
+ return { name: pkg.name, version: pkg.version };
14090
+ }
14091
+ } catch {
14092
+ }
14091
14093
  }
14094
+ return { name: "@mtreeai/msapling-cli", version: "unknown" };
14092
14095
  }
14093
14096
  var CLI_PKG = readCliVersion2();
14094
14097
  function handleCliArgs(args2) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mtreeai/msapling-cli",
3
- "version": "2.3.6-beta.6",
3
+ "version": "2.3.6-beta.8",
4
4
  "description": "MSapling CLI — React/Ink terminal client for the MSapling backend (chat, projects, MDrive, agent tools). Proprietary; redistribution prohibited.",
5
5
  "license": "UNLICENSED",
6
6
  "author": "MSapling Team",