umbrella-context 0.1.34 → 0.1.36

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.
@@ -0,0 +1,5 @@
1
+ import { type UmbrellaAuthSnapshot } from "../auth-state.js";
2
+ export declare function getStoredUmbrellaAuthSnapshot(): UmbrellaAuthSnapshot;
3
+ export declare function setUmbrellaAuthChecking(serverUrl: string, email?: string | null): UmbrellaAuthSnapshot;
4
+ export declare function setUmbrellaAuthAuthorized(serverUrl: string, email: string | null, companiesVisible: number): UmbrellaAuthSnapshot;
5
+ export declare function setUmbrellaAuthFailure(serverUrl: string, error: unknown, email?: string | null): UmbrellaAuthSnapshot;
@@ -0,0 +1,116 @@
1
+ import { configManager } from "../config.js";
2
+ import { createDefaultUmbrellaAuthSnapshot, } from "../auth-state.js";
3
+ import { UmbrellaRequestError } from "../umbrella.js";
4
+ function nowIso() {
5
+ return new Date().toISOString();
6
+ }
7
+ function pathnameFor(url) {
8
+ try {
9
+ return new URL(url).pathname;
10
+ }
11
+ catch {
12
+ return url;
13
+ }
14
+ }
15
+ export function getStoredUmbrellaAuthSnapshot() {
16
+ return configManager.authSnapshot ?? createDefaultUmbrellaAuthSnapshot();
17
+ }
18
+ export function setUmbrellaAuthChecking(serverUrl, email) {
19
+ const snapshot = {
20
+ checkedAt: nowIso(),
21
+ companiesVisible: null,
22
+ email: email?.trim() || null,
23
+ hint: "Checking the live Umbrella sign-in flow.",
24
+ message: "Trying to sign in and validate company access.",
25
+ serverUrl,
26
+ status: "checking",
27
+ };
28
+ configManager.setAuthSnapshot(snapshot);
29
+ return snapshot;
30
+ }
31
+ export function setUmbrellaAuthAuthorized(serverUrl, email, companiesVisible) {
32
+ const snapshot = {
33
+ checkedAt: nowIso(),
34
+ companiesVisible,
35
+ email: email?.trim() || null,
36
+ hint: companiesVisible > 0
37
+ ? "This account can see companies and should be able to continue setup."
38
+ : "This account signed in, but no companies are visible yet.",
39
+ message: companiesVisible > 0
40
+ ? `Signed in successfully and loaded ${companiesVisible} compan${companiesVisible === 1 ? "y" : "ies"}.`
41
+ : "Signed in successfully, but this account does not currently see any companies.",
42
+ serverUrl,
43
+ status: "authorized",
44
+ };
45
+ configManager.setAuthSnapshot(snapshot);
46
+ return snapshot;
47
+ }
48
+ export function setUmbrellaAuthFailure(serverUrl, error, email) {
49
+ let snapshot;
50
+ if (error instanceof UmbrellaRequestError) {
51
+ const path = pathnameFor(error.url);
52
+ if (error.status === 401 && error.code === "INVALID_EMAIL_OR_PASSWORD") {
53
+ snapshot = {
54
+ checkedAt: nowIso(),
55
+ companiesVisible: null,
56
+ email: email?.trim() || null,
57
+ hint: "Double-check the email address and password on that other computer.",
58
+ message: "Umbrella rejected the email or password.",
59
+ serverUrl,
60
+ status: "unauthorized",
61
+ };
62
+ configManager.setAuthSnapshot(snapshot);
63
+ return snapshot;
64
+ }
65
+ if (error.status === 401 && path.endsWith("/api/auth/get-session")) {
66
+ snapshot = {
67
+ checkedAt: nowIso(),
68
+ companiesVisible: null,
69
+ email: email?.trim() || null,
70
+ hint: "The live app accepted the sign-in request, but it did not turn into a usable board session afterward.",
71
+ message: "The session was not established after sign-in.",
72
+ serverUrl,
73
+ status: "session_error",
74
+ };
75
+ configManager.setAuthSnapshot(snapshot);
76
+ return snapshot;
77
+ }
78
+ if (error.status === 403 && path.endsWith("/api/companies")) {
79
+ snapshot = {
80
+ checkedAt: nowIso(),
81
+ companiesVisible: null,
82
+ email: email?.trim() || null,
83
+ hint: "This usually means the account exists, but it still needs board ownership or company membership on the live Umbrella server.",
84
+ message: "Signed in reached Umbrella, but company access was blocked.",
85
+ serverUrl,
86
+ status: "forbidden",
87
+ };
88
+ configManager.setAuthSnapshot(snapshot);
89
+ return snapshot;
90
+ }
91
+ if (error.status === 403 && path.includes("/learning")) {
92
+ snapshot = {
93
+ checkedAt: nowIso(),
94
+ companiesVisible: null,
95
+ email: email?.trim() || null,
96
+ hint: "The account can reach Umbrella, but it is blocked from this company's Context area.",
97
+ message: "Company Context access was blocked.",
98
+ serverUrl,
99
+ status: "forbidden",
100
+ };
101
+ configManager.setAuthSnapshot(snapshot);
102
+ return snapshot;
103
+ }
104
+ }
105
+ snapshot = {
106
+ checkedAt: nowIso(),
107
+ companiesVisible: null,
108
+ email: email?.trim() || null,
109
+ hint: "Check the server URL and the live Umbrella deployment state.",
110
+ message: error instanceof Error ? error.message : "Unexpected auth failure",
111
+ serverUrl,
112
+ status: "session_error",
113
+ };
114
+ configManager.setAuthSnapshot(snapshot);
115
+ return snapshot;
116
+ }
@@ -0,0 +1,12 @@
1
+ export type UmbrellaAuthStatus = "not_initialized" | "checking" | "unauthorized" | "authorized" | "forbidden" | "session_error";
2
+ export interface UmbrellaAuthSnapshot {
3
+ checkedAt: string;
4
+ companiesVisible: number | null;
5
+ email: string | null;
6
+ hint: string | null;
7
+ message: string | null;
8
+ serverUrl: string | null;
9
+ status: UmbrellaAuthStatus;
10
+ }
11
+ export declare function createDefaultUmbrellaAuthSnapshot(): UmbrellaAuthSnapshot;
12
+ export declare function formatUmbrellaAuthStatus(status: UmbrellaAuthStatus): "Checking" | "Unauthorized" | "Authorized" | "Access Blocked" | "Session Problem" | "Not initialized";
@@ -0,0 +1,27 @@
1
+ export function createDefaultUmbrellaAuthSnapshot() {
2
+ return {
3
+ checkedAt: new Date(0).toISOString(),
4
+ companiesVisible: null,
5
+ email: null,
6
+ hint: null,
7
+ message: null,
8
+ serverUrl: null,
9
+ status: "not_initialized",
10
+ };
11
+ }
12
+ export function formatUmbrellaAuthStatus(status) {
13
+ switch (status) {
14
+ case "checking":
15
+ return "Checking";
16
+ case "unauthorized":
17
+ return "Unauthorized";
18
+ case "authorized":
19
+ return "Authorized";
20
+ case "forbidden":
21
+ return "Access Blocked";
22
+ case "session_error":
23
+ return "Session Problem";
24
+ default:
25
+ return "Not initialized";
26
+ }
27
+ }
@@ -1,6 +1,9 @@
1
1
  import chalk from "chalk";
2
2
  import prompts from "prompts";
3
+ import { formatUmbrellaAuthStatus } from "../auth-state.js";
3
4
  import { configManager } from "../config.js";
5
+ import { UmbrellaRequestError } from "../umbrella.js";
6
+ import { setUmbrellaAuthAuthorized, setUmbrellaAuthChecking, setUmbrellaAuthFailure, } from "../adapters/umbrella-auth-runtime.js";
4
7
  import { checkUmbrellaOnboardingServer, connectUmbrellaDevice, createUmbrellaOnboardingCompany, createUmbrellaOnboardingSpace, loadUmbrellaOnboardingSpaces, normalizeUmbrellaServerUrl, signInUmbrellaOnboarding, } from "../adapters/umbrella-onboarding.js";
5
8
  const DEFAULT_UMBRELLA_SERVER_URL = "http://5.161.55.138:3100";
6
9
  function isLocalOnlyServerUrl(value) {
@@ -22,6 +25,35 @@ function printConnected(setup) {
22
25
  console.log(chalk.gray(" umbrella-context curate \"We learned that...\""));
23
26
  console.log(chalk.gray(" umbrella-context mcp"));
24
27
  }
28
+ function describeSetupFailure(error) {
29
+ if (!(error instanceof UmbrellaRequestError)) {
30
+ return error instanceof Error ? error.message : "Unexpected setup failure";
31
+ }
32
+ const path = (() => {
33
+ try {
34
+ return new URL(error.url).pathname;
35
+ }
36
+ catch {
37
+ return error.url;
38
+ }
39
+ })();
40
+ if (error.status === 401 && error.code === "INVALID_EMAIL_OR_PASSWORD") {
41
+ return "Umbrella rejected that email or password. Double-check both and try again.";
42
+ }
43
+ if (error.status === 401 && path.endsWith("/api/auth/get-session")) {
44
+ return "Umbrella accepted the sign-in request, but no board session was established afterward. This usually means the live auth cookie is not being created the way the CLI expects.";
45
+ }
46
+ if (error.status === 403 && path.endsWith("/api/companies")) {
47
+ return "This account reached Umbrella, but it is not allowed to load companies yet. Usually that means the account is signed in but does not have board ownership or company access on the live server.";
48
+ }
49
+ if (error.status === 403 && path.includes("/learning")) {
50
+ return "This account can reach Umbrella, but it is blocked from that company's Context area. Usually that means the account is not attached to that company yet.";
51
+ }
52
+ if (error.status === 403) {
53
+ return `Umbrella refused this request with 403 Forbidden while calling ${path}. Usually that means this account is authenticated but does not have permission for that step yet.`;
54
+ }
55
+ return error.message;
56
+ }
25
57
  async function chooseCompany(serverUrl, companies, cookie, initialCompanyId) {
26
58
  if (initialCompanyId) {
27
59
  const matched = companies.find((company) => company.id === initialCompanyId);
@@ -122,6 +154,7 @@ export function setupCommand(cli) {
122
154
  try {
123
155
  let cookie = null;
124
156
  let companies = [];
157
+ setUmbrellaAuthChecking(serverUrl);
125
158
  const serverCheck = await checkUmbrellaOnboardingServer(serverUrl);
126
159
  if (serverCheck.deploymentMode === "authenticated") {
127
160
  const emailPrompt = await prompts({
@@ -143,8 +176,16 @@ export function setupCommand(cli) {
143
176
  const signIn = await signInUmbrellaOnboarding(serverUrl, email, password);
144
177
  cookie = signIn.cookie;
145
178
  companies = signIn.companies;
179
+ const authSnapshot = setUmbrellaAuthAuthorized(serverUrl, email, companies.length);
180
+ console.log(chalk.gray(` Auth state: ${formatUmbrellaAuthStatus(authSnapshot.status)}`));
181
+ if (companies.length === 0) {
182
+ console.log(chalk.yellow("\n Signed in, but this account does not currently see any companies. That usually means the account exists but has not been given board access or company membership yet."));
183
+ console.log(chalk.yellow(" If this should be your main Umbrella account, it may still need the board-claim/admin step on the live server."));
184
+ }
146
185
  }
147
186
  else {
187
+ const authSnapshot = setUmbrellaAuthAuthorized(serverUrl, null, serverCheck.companies.length);
188
+ console.log(chalk.gray(` Auth state: ${formatUmbrellaAuthStatus(authSnapshot.status)}`));
148
189
  console.log(chalk.gray(" Local trusted mode detected. Using the local board access path."));
149
190
  companies = serverCheck.companies;
150
191
  }
@@ -167,7 +208,12 @@ export function setupCommand(cli) {
167
208
  console.log(chalk.yellow("\n Hint: 127.0.0.1 points to this same computer only."));
168
209
  console.log(chalk.yellow(" If Umbrella is running on another machine or server, re-run setup and enter that machine's real URL or IP."));
169
210
  }
170
- console.log(chalk.red(`\n Error: ${err.message}`));
211
+ const authSnapshot = setUmbrellaAuthFailure(serverUrl, err, options.email?.trim() || null);
212
+ console.log(chalk.red(` Auth state: ${formatUmbrellaAuthStatus(authSnapshot.status)}`));
213
+ if (authSnapshot.hint) {
214
+ console.log(chalk.yellow(` Hint: ${authSnapshot.hint}`));
215
+ }
216
+ console.log(chalk.red(`\n Error: ${describeSetupFailure(err)}`));
171
217
  }
172
218
  });
173
219
  }
@@ -1,4 +1,6 @@
1
+ import { type UmbrellaAuthSnapshot } from "../auth-state.js";
1
2
  export type StatusSnapshot = {
3
+ authSnapshot: UmbrellaAuthSnapshot;
2
4
  companyName: string;
3
5
  spaceName: string;
4
6
  umbrellaUrl: string | null;
@@ -1,6 +1,8 @@
1
1
  import chalk from "chalk";
2
2
  import { promises as fs } from "fs";
3
3
  import path from "path";
4
+ import { formatUmbrellaAuthStatus } from "../auth-state.js";
5
+ import { getStoredUmbrellaAuthSnapshot } from "../adapters/umbrella-auth-runtime.js";
4
6
  import { configManager } from "../config.js";
5
7
  import { getConnectorRuns, getContextTreeState, getInstalledConnectors, getInstalledHubEntries, getPendingMemories, getPulledFixes, getPulledMemories, getRepoContext, ensureSessionState, getTransportState, } from "../repo-state.js";
6
8
  export async function getStatusSnapshot() {
@@ -44,6 +46,7 @@ export async function getStatusSnapshot() {
44
46
  if (hubEntries.length === 0)
45
47
  nextSteps.push('Browse reusable bundles with "umbrella-context hub list".');
46
48
  return {
49
+ authSnapshot: getStoredUmbrellaAuthSnapshot(),
47
50
  companyName: config.companyName,
48
51
  spaceName: config.projectName,
49
52
  umbrellaUrl: config.umbrellaUrl ?? null,
@@ -84,6 +87,15 @@ export async function statusCommandAction() {
84
87
  return;
85
88
  }
86
89
  console.log(chalk.bold("\n Umbrella Context Status\n"));
90
+ console.log(chalk.cyan(" Auth"));
91
+ console.log(` State: ${formatUmbrellaAuthStatus(snapshot.authSnapshot.status)}`);
92
+ console.log(` Checked At: ${snapshot.authSnapshot.checkedAt === new Date(0).toISOString() ? "Never" : snapshot.authSnapshot.checkedAt}`);
93
+ console.log(` Auth Server: ${snapshot.authSnapshot.serverUrl ?? "Not checked yet"}`);
94
+ console.log(` Email: ${snapshot.authSnapshot.email ?? "Not saved"}`);
95
+ console.log(` Companies Visible: ${snapshot.authSnapshot.companiesVisible === null ? "Unknown" : String(snapshot.authSnapshot.companiesVisible)}`);
96
+ console.log(` Auth Message: ${snapshot.authSnapshot.message ?? "No auth check has been saved yet"}`);
97
+ console.log(` Auth Hint: ${snapshot.authSnapshot.hint ?? "No auth hint recorded yet"}`);
98
+ console.log("");
87
99
  console.log(chalk.cyan(" Connection"));
88
100
  console.log(` Company: ${snapshot.companyName}`);
89
101
  console.log(` Space: ${snapshot.spaceName}`);
@@ -4,11 +4,13 @@ import { Box, render, Text, useApp, useInput } from "ink";
4
4
  import TextInput from "ink-text-input";
5
5
  import Spinner from "ink-spinner";
6
6
  import chalk from "chalk";
7
+ import { formatUmbrellaAuthStatus } from "../auth-state.js";
7
8
  import { getUmbrellaContextRuntimeSummary, useUmbrellaContextRuntimeBridgeStore, hydrateUmbrellaContextRuntimeBridgeFromSnapshot, } from "../adapters/byterover-context-runtime-store.js";
8
9
  import { getUmbrellaTransportTaskBridgeSummary, useUmbrellaTransportTaskBridgeStore, hydrateUmbrellaTransportTaskBridgeFromSnapshot, } from "../adapters/byterover-transport-task-store.js";
9
10
  import { buildVendorRuntimeBridgeSnapshot, } from "../adapters/byterover-runtime-bridge.js";
10
11
  import { addPendingMemory, completeTask, createTask, ensureRepoContext, getContextTreeState, ensureSessionState, getConnectorRuns, getInstalledConnectors, getInstalledHubEntries, getPendingMemories, getPulledFixes, getPulledMemories, getRepoContext, getSessionState, getTasks, getTransportState, recordSessionEvent, setSessionPanel, summarizeLocalMemoryMatches, } from "../repo-state.js";
11
12
  import { configManager } from "../config.js";
13
+ import { getStoredUmbrellaAuthSnapshot, setUmbrellaAuthAuthorized, setUmbrellaAuthChecking, setUmbrellaAuthFailure, } from "../adapters/umbrella-auth-runtime.js";
12
14
  import { checkUmbrellaOnboardingServer, connectUmbrellaDevice, createUmbrellaOnboardingCompany, createUmbrellaOnboardingSpace, loadUmbrellaOnboardingSpaces, signInUmbrellaOnboarding, } from "../adapters/umbrella-onboarding.js";
13
15
  import { buildProviderDraftValue, connectSavedProviderFromDraft, createEmptyProviderConnectDraft, UMBRELLA_PROVIDER_CHOICES, updateProviderDraftValue, } from "../adapters/umbrella-provider-runtime.js";
14
16
  import { connectorsCommandAction, listConnectorTemplates } from "./connectors.js";
@@ -200,8 +202,8 @@ function PanelShell({ activePanel, children, footer, message, selectedPanelIndex
200
202
  }) }), _jsx(Box, { flexDirection: "column", flexGrow: 1, borderStyle: "round", paddingX: 1, paddingY: 1, children: children })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [message ? _jsx(Text, { color: "green", children: message }) : null, _jsx(Text, { color: "gray", children: footer })] })] }));
201
203
  }
202
204
  function SetupView(props) {
203
- const { busyLabel, companies, config, inputValue, message, onInputChange, selectedCompanyIndex, selectedSpaceIndex, serverUrl, spaces, step, } = props;
204
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: config ? "Connected Device" : "Connect This Device" }), _jsx(Text, { color: "gray", children: "Sign into Umbrella, choose a company, then choose the team space for this repo." }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(StatusLine, { label: "Server", value: serverUrl || "Not set" }), _jsx(StatusLine, { label: "Step", value: step }), config ? _jsx(StatusLine, { label: "Current link", value: `${config.companyName} / ${config.projectName}` }) : null] }), busyLabel ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "cyan", children: [_jsx(Spinner, { type: "dots" }), " ", busyLabel] }) })) : null, message ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "green", children: message }) })) : null, (step === "server" || step === "email" || step === "password" || step === "company-create" || step === "space-create") ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: step === "server" ? "Server URL" :
205
+ const { busyLabel, companies, config, authSnapshot, inputValue, message, onInputChange, selectedCompanyIndex, selectedSpaceIndex, serverUrl, spaces, step, } = props;
206
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: config ? "Connected Device" : "Connect This Device" }), _jsx(Text, { color: "gray", children: "Sign into Umbrella, choose a company, then choose the team space for this repo." }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(StatusLine, { label: "Server", value: serverUrl || "Not set" }), _jsx(StatusLine, { label: "Step", value: step }), _jsx(StatusLine, { label: "Auth state", value: formatUmbrellaAuthStatus(authSnapshot.status) }), _jsx(StatusLine, { label: "Auth message", value: authSnapshot.message ?? "No auth check yet" }), _jsx(StatusLine, { label: "Auth hint", value: authSnapshot.hint ?? "No auth hint yet" }), config ? _jsx(StatusLine, { label: "Current link", value: `${config.companyName} / ${config.projectName}` }) : null] }), busyLabel ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "cyan", children: [_jsx(Spinner, { type: "dots" }), " ", busyLabel] }) })) : null, message ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "green", children: message }) })) : null, (step === "server" || step === "email" || step === "password" || step === "company-create" || step === "space-create") ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: step === "server" ? "Server URL" :
205
207
  step === "email" ? "Umbrella email" :
206
208
  step === "password" ? "Umbrella password" :
207
209
  step === "company-create" ? "New company name" :
@@ -224,6 +226,7 @@ function App() {
224
226
  const [setupStep, setSetupStep] = useState(configManager.config ? "done" : "server");
225
227
  const [serverUrl, setServerUrl] = useState(configManager.config?.umbrellaUrl ?? configManager.config?.serverUrl ?? "http://127.0.0.1:3100");
226
228
  const [setupEmail, setSetupEmail] = useState("");
229
+ const [authSnapshot, setAuthSnapshot] = useState(getStoredUmbrellaAuthSnapshot());
227
230
  const [setupCookie, setSetupCookie] = useState(null);
228
231
  const [companies, setCompanies] = useState([]);
229
232
  const [selectedCompanyIndex, setSelectedCompanyIndex] = useState(0);
@@ -254,6 +257,7 @@ function App() {
254
257
  if (configManager.config) {
255
258
  await ensureRepoContext(configManager.config);
256
259
  }
260
+ setAuthSnapshot(getStoredUmbrellaAuthSnapshot());
257
261
  const vendorSnapshot = await buildVendorRuntimeBridgeSnapshot();
258
262
  hydrateUmbrellaTransportTaskBridgeFromSnapshot(vendorSnapshot);
259
263
  hydrateUmbrellaContextRuntimeBridgeFromSnapshot(vendorSnapshot);
@@ -499,18 +503,24 @@ function App() {
499
503
  setInputValue("");
500
504
  setBusyLabel("Checking Umbrella server...");
501
505
  try {
506
+ setAuthSnapshot(setUmbrellaAuthChecking(nextServer, setupEmail || null));
502
507
  const serverCheck = await checkUmbrellaOnboardingServer(nextServer);
503
508
  if (serverCheck.deploymentMode === "authenticated") {
504
509
  setSetupStep("email");
510
+ setMessage("Umbrella is reachable. Enter the email for the account you want to use on this device.");
505
511
  }
506
512
  else {
513
+ setAuthSnapshot(setUmbrellaAuthAuthorized(nextServer, null, serverCheck.companies.length));
507
514
  setCompanies(serverCheck.companies);
508
515
  setSelectedCompanyIndex(0);
509
516
  setSetupStep("company");
517
+ setMessage("Umbrella is reachable and trusted mode is active. Choose a company next.");
510
518
  }
511
519
  }
512
520
  catch (error) {
513
- setMessage(`Could not reach Umbrella: ${error.message}`);
521
+ const snapshot = setUmbrellaAuthFailure(nextServer, error, setupEmail || null);
522
+ setAuthSnapshot(snapshot);
523
+ setMessage(`Could not reach Umbrella: ${snapshot.message}${snapshot.hint ? ` ${snapshot.hint}` : ""}`);
514
524
  }
515
525
  finally {
516
526
  setBusyLabel(null);
@@ -527,14 +537,21 @@ function App() {
527
537
  setInputValue("");
528
538
  setBusyLabel("Signing into Umbrella...");
529
539
  try {
540
+ setAuthSnapshot(setUmbrellaAuthChecking(serverUrl, setupEmail));
530
541
  const signIn = await signInUmbrellaOnboarding(serverUrl, setupEmail, nextValue);
531
542
  setSetupCookie(signIn.cookie);
532
543
  setCompanies(signIn.companies);
533
544
  setSelectedCompanyIndex(0);
545
+ setAuthSnapshot(setUmbrellaAuthAuthorized(serverUrl, setupEmail, signIn.companies.length));
534
546
  setSetupStep("company");
547
+ setMessage(signIn.companies.length > 0
548
+ ? `Signed in successfully. Choose one of the ${signIn.companies.length} compan${signIn.companies.length === 1 ? "y" : "ies"} next.`
549
+ : "Signed in successfully, but this account does not see any companies yet.");
535
550
  }
536
551
  catch (error) {
537
- setMessage(`Sign-in failed: ${error.message}`);
552
+ const snapshot = setUmbrellaAuthFailure(serverUrl, error, setupEmail);
553
+ setAuthSnapshot(snapshot);
554
+ setMessage(`Sign-in failed: ${snapshot.message}${snapshot.hint ? ` ${snapshot.hint}` : ""}`);
538
555
  }
539
556
  finally {
540
557
  setBusyLabel(null);
@@ -553,6 +570,7 @@ function App() {
553
570
  setSelectedSpaceIndex(0);
554
571
  setInputValue("");
555
572
  setSetupStep("space");
573
+ setMessage(`Created ${company.name}. Choose the space this repo should use next.`);
556
574
  }
557
575
  catch (error) {
558
576
  setMessage(`Could not create company: ${error.message}`);
@@ -571,6 +589,7 @@ function App() {
571
589
  setSelectedSpaceIndex(Math.max(0, nextSpaces.findIndex((space) => space.id === created.activeSpaceId)));
572
590
  setInputValue("");
573
591
  setSetupStep("space");
592
+ setMessage("Space created. Confirm it to finish linking this device.");
574
593
  }
575
594
  catch (error) {
576
595
  setMessage(`Could not create space: ${error.message}`);
@@ -597,6 +616,7 @@ function App() {
597
616
  setSpaces(nextSpaces);
598
617
  setSelectedSpaceIndex(0);
599
618
  setSetupStep("space");
619
+ setMessage(`Loaded ${nextSpaces.length} space${nextSpaces.length === 1 ? "" : "s"} for ${company.name}.`);
600
620
  }
601
621
  catch (error) {
602
622
  setMessage(`Could not load spaces: ${error.message}`);
@@ -626,6 +646,7 @@ function App() {
626
646
  focus: setup.activeSpaceName,
627
647
  status: "success",
628
648
  });
649
+ setAuthSnapshot(setUmbrellaAuthAuthorized(serverUrl, setupEmail || null, companies.length));
629
650
  setSetupStep("done");
630
651
  setMessage(`Connected to ${setup.companyName} / ${setup.activeSpaceName}.`);
631
652
  setActivePanel("home");
@@ -1214,7 +1235,7 @@ function App() {
1214
1235
  const config = configManager.config;
1215
1236
  let mainContent = null;
1216
1237
  if (activePanel === "setup") {
1217
- mainContent = (_jsx(SetupView, { busyLabel: busyLabel, companies: companies, config: config, inputValue: inputValue, message: message, onInputChange: setInputValue, selectedCompanyIndex: selectedCompanyIndex, selectedSpaceIndex: selectedSpaceIndex, serverUrl: serverUrl, spaces: spaces, step: setupStep }));
1238
+ mainContent = (_jsx(SetupView, { authSnapshot: authSnapshot, busyLabel: busyLabel, companies: companies, config: config, inputValue: inputValue, message: message, onInputChange: setInputValue, selectedCompanyIndex: selectedCompanyIndex, selectedSpaceIndex: selectedSpaceIndex, serverUrl: serverUrl, spaces: spaces, step: setupStep }));
1218
1239
  }
1219
1240
  else if (!config || !refreshState) {
1220
1241
  mainContent = (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: "This device is not linked yet." }), _jsx(Text, { color: "gray", children: "Move to Setup and connect the repo to a company and space first." })] }));
package/dist/config.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { UmbrellaAuthSnapshot } from "./auth-state.js";
1
2
  export interface AgentMemoryConfig {
2
3
  umbrellaUrl?: string;
3
4
  serverUrl: string;
@@ -25,6 +26,9 @@ export interface HubRegistry {
25
26
  url: string;
26
27
  updatedAt: string;
27
28
  }
29
+ export interface StoredCliState {
30
+ authSnapshot?: UmbrellaAuthSnapshot;
31
+ }
28
32
  export interface SavedLocation {
29
33
  repoRoot: string;
30
34
  companyId: string;
@@ -49,5 +53,8 @@ export declare class ConfigManager {
49
53
  get hubRegistries(): HubRegistry[];
50
54
  upsertHubRegistry(registry: HubRegistry): void;
51
55
  removeHubRegistry(id: string): void;
56
+ get authSnapshot(): UmbrellaAuthSnapshot | null;
57
+ setAuthSnapshot(snapshot: UmbrellaAuthSnapshot): void;
58
+ clearAuthSnapshot(): void;
52
59
  }
53
60
  export declare const configManager: ConfigManager;
package/dist/config.js CHANGED
@@ -128,5 +128,14 @@ export class ConfigManager {
128
128
  const registries = this.hubRegistries.filter((entry) => entry.id !== id);
129
129
  this.conf.set("hubRegistries", registries);
130
130
  }
131
+ get authSnapshot() {
132
+ return this.conf.get("authSnapshot") ?? null;
133
+ }
134
+ setAuthSnapshot(snapshot) {
135
+ this.conf.set("authSnapshot", snapshot);
136
+ }
137
+ clearAuthSnapshot() {
138
+ this.conf.delete("authSnapshot");
139
+ }
131
140
  }
132
141
  export const configManager = new ConfigManager();
package/dist/index.d.ts CHANGED
@@ -1 +1,2 @@
1
+ #!/usr/bin/env node
1
2
  export {};
package/dist/index.js CHANGED
@@ -66,7 +66,7 @@ cli.command("query [...args]", "Alias for search").action(async (args) => {
66
66
  await searchCommandAction(args.join(" "));
67
67
  });
68
68
  cli.help();
69
- cli.version("0.1.32");
69
+ cli.version("0.1.36");
70
70
  const argv = process.argv.slice(2);
71
71
  if (argv[0] === "curate" && argv[1] === "view" && argv.length === 2) {
72
72
  await curateViewCommandAction();
@@ -21,6 +21,18 @@ export type UmbrellaCliSetup = {
21
21
  baseUrl: string;
22
22
  apiKey: string;
23
23
  };
24
+ type RequestFailureDetails = {
25
+ code?: string;
26
+ message?: string;
27
+ status: number;
28
+ url: string;
29
+ };
30
+ export declare class UmbrellaRequestError extends Error {
31
+ status: number;
32
+ code?: string;
33
+ url: string;
34
+ constructor(details: RequestFailureDetails);
35
+ }
24
36
  export declare function getUmbrellaHealth(serverUrl: string): Promise<DeploymentHealth>;
25
37
  export declare function signInToUmbrella(serverUrl: string, email: string, password: string): Promise<{
26
38
  cookie: string | null;
package/dist/umbrella.js CHANGED
@@ -1,3 +1,15 @@
1
+ export class UmbrellaRequestError extends Error {
2
+ status;
3
+ code;
4
+ url;
5
+ constructor(details) {
6
+ super(details.message ?? `Request failed (${details.status})`);
7
+ this.name = "UmbrellaRequestError";
8
+ this.status = details.status;
9
+ this.code = details.code;
10
+ this.url = details.url;
11
+ }
12
+ }
1
13
  function trimTrailingSlash(value) {
2
14
  return value.replace(/\/+$/, "");
3
15
  }
@@ -12,8 +24,12 @@ function isLocalOnlyUmbrellaUrl(value) {
12
24
  }
13
25
  }
14
26
  async function requestJson(url, options) {
27
+ const parsedUrl = new URL(url);
28
+ const requestOrigin = `${parsedUrl.protocol}//${parsedUrl.host}`;
15
29
  const headers = {
16
30
  Accept: "application/json",
31
+ Origin: requestOrigin,
32
+ Referer: `${requestOrigin}/`,
17
33
  };
18
34
  if (options?.body !== undefined) {
19
35
  headers["Content-Type"] = "application/json";
@@ -30,8 +46,7 @@ async function requestJson(url, options) {
30
46
  });
31
47
  }
32
48
  catch (error) {
33
- const parsed = new URL(url);
34
- const origin = `${parsed.protocol}//${parsed.host}`;
49
+ const origin = requestOrigin;
35
50
  const localHint = isLocalOnlyUmbrellaUrl(origin)
36
51
  ? " That address only works if Umbrella is running on this same computer. If Umbrella lives on another machine, use that machine's real URL or IP instead."
37
52
  : "";
@@ -47,21 +62,38 @@ async function requestJson(url, options) {
47
62
  .join("; ") || null;
48
63
  const payload = await response.json().catch(() => null);
49
64
  if (!response.ok) {
50
- const message = payload && typeof payload === "object"
65
+ const details = payload && typeof payload === "object"
51
66
  ? (() => {
52
67
  const record = payload;
53
68
  const errorValue = record.error;
54
- if (typeof errorValue === "string")
55
- return errorValue;
69
+ const codeValue = typeof record.code === "string" ? record.code : undefined;
70
+ const messageValue = typeof record.message === "string" ? record.message : undefined;
71
+ if (typeof errorValue === "string") {
72
+ return {
73
+ code: codeValue,
74
+ message: errorValue,
75
+ };
76
+ }
56
77
  if (errorValue && typeof errorValue === "object") {
57
- const messageValue = errorValue.message;
58
- if (typeof messageValue === "string")
59
- return messageValue;
78
+ const nestedMessageValue = errorValue.message;
79
+ const nestedCodeValue = errorValue.code;
80
+ return {
81
+ code: typeof nestedCodeValue === "string" ? nestedCodeValue : codeValue,
82
+ message: typeof nestedMessageValue === "string" ? nestedMessageValue : messageValue,
83
+ };
60
84
  }
61
- return `Request failed (${response.status})`;
85
+ return {
86
+ code: codeValue,
87
+ message: messageValue,
88
+ };
62
89
  })()
63
- : `Request failed (${response.status})`;
64
- throw new Error(message);
90
+ : { code: undefined, message: undefined };
91
+ throw new UmbrellaRequestError({
92
+ code: details.code,
93
+ message: details.message ?? `Request failed (${response.status})`,
94
+ status: response.status,
95
+ url,
96
+ });
65
97
  }
66
98
  return {
67
99
  data: payload,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "umbrella-context",
3
- "version": "0.1.34",
3
+ "version": "0.1.36",
4
4
  "description": "Umbrella Context CLI for connecting a device to company context spaces, querying saved context, and syncing MCP access.",
5
5
  "type": "module",
6
6
  "bin": {