@opendatalabs/connect 0.10.0 → 0.10.1-canary.6ecf979

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/cli/index.js CHANGED
@@ -29,11 +29,12 @@ const vanaPromptTheme = {
29
29
  import { createConnectRenderer, createHumanRenderer, formatDisplayPath, formatRelativeTime, } from "./render/index.js";
30
30
  import { CliOutcomeStatus, migrateLegacyDataHome, getBrowserProfilesDir, getConnectorCacheDir, getLogsDir, getSessionsDir, getSourceResultPath, readCliState, readCliConfig, updateCliConfig, updateSourceState, } from "../core/index.js";
31
31
  import { fetchConnectorToCache, listAvailableSources, readCachedConnectorMetadata, } from "../connectors/registry.js";
32
- import { detectPersonalServerTarget, ingestResult, } from "../personal-server/index.js";
32
+ import { detectPersonalServerTarget, ingestResult, resolvePersonalServerAuthConfig, } from "../personal-server/index.js";
33
33
  import { findDataConnectorsDir, ManagedPlaywrightRuntime, } from "../runtime/index.js";
34
34
  import { listAvailableSkills, installSkill, readInstalledSkills, } from "../skills/index.js";
35
35
  import { queryStatus, querySources, queryDataList, queryDataShow, queryDoctor, } from "./queries.js";
36
36
  import { checkForUpdate, readUpdateCheck, isNewerVersion, } from "./update-check.js";
37
+ import { loadCredentials, saveCredentials, clearCredentials, isExpired, formatAddress, formatExpiresIn, getAuthTarget, resolvePersonalServerUrl, runDeviceCodeFlow, runSelfHostedLoginFlow, } from "./auth.js";
37
38
  function cleanDescription(desc) {
38
39
  return desc
39
40
  .replace(/ using Playwright browser automation\.?/i, ".")
@@ -73,6 +74,7 @@ export async function runCli(argv = process.argv) {
73
74
  .showSuggestionAfterError(true)
74
75
  .addHelpText("after", `
75
76
  Quick start:
77
+ vana login Log in to your Vana account
76
78
  vana connect Connect a source and collect data
77
79
  vana sources Browse available sources
78
80
  vana status Check system health
@@ -330,6 +332,19 @@ Examples:
330
332
  .action(async (scope) => {
331
333
  process.exitCode = await runServerData(scope, parsedOptions);
332
334
  });
335
+ program
336
+ .command("login")
337
+ .description("Log in to your Vana account or a self-hosted Personal Server")
338
+ .option("-s, --server <url>", "Self-hosted Personal Server URL")
339
+ .action(async (loginOptions) => {
340
+ process.exitCode = await runLogin(parsedOptions, loginOptions.server);
341
+ });
342
+ program
343
+ .command("logout")
344
+ .description("Log out and remove saved credentials")
345
+ .action(async () => {
346
+ process.exitCode = await runLogout(parsedOptions);
347
+ });
333
348
  program
334
349
  .command("mcp")
335
350
  .description("Start MCP server for agent integration")
@@ -1151,10 +1166,18 @@ async function runStatus(options) {
1151
1166
  }
1152
1167
  }
1153
1168
  if (options.json) {
1169
+ const jsonAuthCreds = loadCredentials();
1154
1170
  const compactJson = {
1155
1171
  runtime: status.runtime,
1156
1172
  personalServer: status.personalServer,
1157
1173
  personalServerUrl: status.personalServerUrl,
1174
+ auth: jsonAuthCreds
1175
+ ? {
1176
+ authenticated: !isExpired(jsonAuthCreds),
1177
+ address: jsonAuthCreds.account.address,
1178
+ expires_at: jsonAuthCreds.account.expires_at,
1179
+ }
1180
+ : { authenticated: false },
1158
1181
  sources: {
1159
1182
  connected: status.summary?.connectedCount ?? 0,
1160
1183
  needsAttention: status.summary?.needsAttentionCount ?? 0,
@@ -1177,6 +1200,16 @@ async function runStatus(options) {
1177
1200
  else {
1178
1201
  emit.keyValue("Personal Server", "not connected", "warning");
1179
1202
  }
1203
+ // Auth state
1204
+ const authCreds = loadCredentials();
1205
+ if (authCreds && !isExpired(authCreds)) {
1206
+ emit.keyValue("Account", formatAddress(authCreds.account.address), "success");
1207
+ emit.keyValue("Auth", `Authenticated (expires in ${formatExpiresIn(authCreds.account.expires_at)})`, "success");
1208
+ }
1209
+ else {
1210
+ emit.keyValue("Account", "Not logged in", "muted");
1211
+ emit.keyValue("Auth", "Run `vana login` to authenticate", "muted");
1212
+ }
1180
1213
  const connectedCount = status.summary?.connectedCount ?? 0;
1181
1214
  const attentionCount = status.summary?.needsAttentionCount ?? 0;
1182
1215
  const sourceParts = [
@@ -1204,8 +1237,18 @@ async function runStatus(options) {
1204
1237
  const displayName = displaySource(sourceId, sourceLabels);
1205
1238
  const sourceStatus = status.sources.find((s) => s.source === sourceId);
1206
1239
  const sourceOverdue = sourceStatus?.isOverdue ?? false;
1207
- const healthTone = sourceOverdue ? "warning" : toneForHealth(health);
1208
- const healthLabel = health === "needs_reauth" ? "needs login" : health;
1240
+ const dataState = stored?.dataState;
1241
+ // Show worst of connectionHealth and dataState
1242
+ let healthLabel;
1243
+ let healthTone;
1244
+ if (dataState === "ingest_failed") {
1245
+ healthLabel = "sync failed";
1246
+ healthTone = "warning";
1247
+ }
1248
+ else {
1249
+ healthTone = sourceOverdue ? "warning" : toneForHealth(health);
1250
+ healthLabel = health === "needs_reauth" ? "needs login" : health;
1251
+ }
1209
1252
  const staleTag = sourceOverdue
1210
1253
  ? ` ${emit.badge("stale", "warning")}`
1211
1254
  : "";
@@ -1213,7 +1256,11 @@ async function runStatus(options) {
1213
1256
  ? `collected ${formatRelativeTime(stored.lastCollectedAt)}`
1214
1257
  : "";
1215
1258
  emit.keyValue(` ${displayName}`, `${healthLabel}${staleTag} ${collectedAgo}`, healthTone);
1216
- if ((health === "needs_reauth" || health === "error") &&
1259
+ if (dataState === "ingest_failed") {
1260
+ const errMsg = stored?.lastError ?? "sync failed";
1261
+ emit.detail(` \u21b3 ${errMsg}. Run \`vana connect ${sourceId}\``);
1262
+ }
1263
+ else if ((health === "needs_reauth" || health === "error") &&
1217
1264
  stored?.connectionHealthReason) {
1218
1265
  const msg = formatHealthMessage(stored.connectionHealthReason);
1219
1266
  const ago = stored.connectionHealthChangedAt
@@ -1233,6 +1280,47 @@ async function runStatus(options) {
1233
1280
  return 0;
1234
1281
  }
1235
1282
  }
1283
+ // Show attention-needing sources that weren't already displayed above
1284
+ const displayedSourceIds = new Set(connectedSources.map(([id]) => id));
1285
+ const hiddenAttentionSources = status.sources.filter((s) => rankSourceStatus(s) <= 4 && !displayedSourceIds.has(s.source));
1286
+ if (hiddenAttentionSources.length > 0) {
1287
+ if (connectedSources.length === 0) {
1288
+ emit.blank();
1289
+ }
1290
+ for (const source of hiddenAttentionSources) {
1291
+ const displayName = displaySource(source.source, sourceLabels);
1292
+ const presentation = getSourceStatusPresentation(source);
1293
+ const collectedAgo = source.lastCollectedAt
1294
+ ? `collected ${formatRelativeTime(source.lastCollectedAt)}`
1295
+ : "";
1296
+ emit.keyValue(` ${displayName}`, `${presentation.label}${collectedAgo ? ` ${collectedAgo}` : ""}`, presentation.tone);
1297
+ // Show actionable detail line
1298
+ if (source.lastRunOutcome === CliOutcomeStatus.INGEST_FAILED &&
1299
+ source.ingestScopes) {
1300
+ const failedScopes = source.ingestScopes.filter((s) => s.status === "failed");
1301
+ for (const scope of failedScopes) {
1302
+ const errMsg = scope.error ?? "sync failed";
1303
+ emit.detail(` \u21b3 ${source.source}.${scope.scope}: ${errMsg}. Run \`vana connect ${source.source}\``);
1304
+ }
1305
+ if (failedScopes.length === 0) {
1306
+ emit.detail(` \u21b3 Sync failed. Run \`vana connect ${source.source}\``);
1307
+ }
1308
+ }
1309
+ else if (source.lastRunOutcome === CliOutcomeStatus.CONNECTOR_UNAVAILABLE) {
1310
+ emit.detail(` \u21b3 No connector available. Run \`vana sources\``);
1311
+ }
1312
+ else if (source.lastRunOutcome === CliOutcomeStatus.RUNTIME_ERROR) {
1313
+ const reason = source.lastError ?? "runtime error";
1314
+ emit.detail(` \u21b3 ${reason}. Run \`vana connect ${source.source}\``);
1315
+ }
1316
+ else if (source.lastRunOutcome === CliOutcomeStatus.NEEDS_INPUT) {
1317
+ emit.detail(` \u21b3 Requires interactive login. Run \`vana connect ${source.source}\``);
1318
+ }
1319
+ else if (source.lastRunOutcome === CliOutcomeStatus.LEGACY_AUTH) {
1320
+ emit.detail(` \u21b3 Manual auth step required. Run \`vana connect ${source.source}\``);
1321
+ }
1322
+ }
1323
+ }
1236
1324
  if (nextSteps.length > 0) {
1237
1325
  emit.blank();
1238
1326
  const command = extractCommand(nextSteps[0]);
@@ -1353,9 +1441,11 @@ async function runServerStatus(options) {
1353
1441
  ? "(auto-detected)"
1354
1442
  : target.source === "config"
1355
1443
  ? "(saved)"
1356
- : target.source === "env"
1357
- ? "(from VANA_PERSONAL_SERVER_URL)"
1358
- : `(${target.source ?? "unknown"})`;
1444
+ : target.source === "auth"
1445
+ ? "(from vana login)"
1446
+ : target.source === "env"
1447
+ ? "(from VANA_PERSONAL_SERVER_URL)"
1448
+ : `(${target.source ?? "unknown"})`;
1359
1449
  emit.keyValue("URL", `${target.url} ${urlSuffix}`, "muted");
1360
1450
  }
1361
1451
  const stateLabel = target.state === "available" ? "healthy" : "Not connected";
@@ -2078,7 +2168,10 @@ async function runServerData(scope, options) {
2078
2168
  if (target.state === "available" && target.url) {
2079
2169
  try {
2080
2170
  const { createPersonalServerClient: createClient } = await import("../personal-server/client.js");
2081
- const client = createClient({ url: target.url });
2171
+ const client = createClient({
2172
+ url: target.url,
2173
+ auth: resolvePersonalServerAuthConfig(target.url),
2174
+ });
2082
2175
  remoteScopes = await client.listScopes(scope);
2083
2176
  }
2084
2177
  catch (err) {
@@ -2872,6 +2965,11 @@ export function getSourceStatusPresentation(source) {
2872
2965
  if (!source.installed && !source.lastRunOutcome) {
2873
2966
  return { label: "not connected", tone: "muted" };
2874
2967
  }
2968
+ // Check dataState before early-returning on missing lastRunOutcome —
2969
+ // ingest can fail even when lastRunOutcome is unset
2970
+ if (source.dataState === "ingest_failed") {
2971
+ return { label: "sync failed", tone: "error" };
2972
+ }
2875
2973
  if (!source.lastRunOutcome) {
2876
2974
  return { label: "installed", tone: "success" };
2877
2975
  }
@@ -2904,9 +3002,6 @@ export function getSourceStatusPresentation(source) {
2904
3002
  if (source.dataState === "collected_local") {
2905
3003
  return { label: "local", tone: "muted" };
2906
3004
  }
2907
- if (source.dataState === "ingest_failed") {
2908
- return { label: "sync failed", tone: "error" };
2909
- }
2910
3005
  return { label: "connected", tone: "success" };
2911
3006
  }
2912
3007
  export function toneForRuntime(runtime) {
@@ -3009,6 +3104,9 @@ export function rankSourceStatus(source) {
3009
3104
  if (source.lastRunOutcome === CliOutcomeStatus.CONNECTOR_UNAVAILABLE) {
3010
3105
  return 4;
3011
3106
  }
3107
+ if (source.dataState === "ingest_failed") {
3108
+ return 2;
3109
+ }
3012
3110
  if (source.dataState === "ingested_personal_server") {
3013
3111
  return 5;
3014
3112
  }
@@ -3899,4 +3997,185 @@ async function runSkillShow(name, options) {
3899
3997
  return 1;
3900
3998
  }
3901
3999
  }
4000
+ // ── Login / Logout ─────────────────────────────────────────────────────
4001
+ async function runLogin(options, serverUrl) {
4002
+ // Determine auth target: cloud (account.vana.org) or self-hosted (PS directly)
4003
+ const psUrl = serverUrl ?? resolvePersonalServerUrl() ?? null;
4004
+ const authTarget = getAuthTarget(psUrl);
4005
+ // If self-hosted, use /auth/device flow against the PS
4006
+ if (authTarget === "self-hosted" && psUrl) {
4007
+ const emit = createEmitter(options);
4008
+ emit.blank();
4009
+ emit.info(` Logging in to ${psUrl}...`);
4010
+ emit.blank();
4011
+ try {
4012
+ const result = await runSelfHostedLoginFlow(psUrl, (url) => {
4013
+ emit.info(` ! Open this URL in your browser:`);
4014
+ emit.info(` ${url}`);
4015
+ emit.blank();
4016
+ emit.info(` Waiting for authorization...`);
4017
+ // Try to open browser — use spawn with args array to prevent shell injection
4018
+ // (a malicious self-hosted PS could return a URL with shell metacharacters)
4019
+ try {
4020
+ const { spawn } = require("node:child_process");
4021
+ const opener = process.platform === "darwin"
4022
+ ? "open"
4023
+ : process.platform === "win32"
4024
+ ? "start"
4025
+ : "xdg-open";
4026
+ spawn(opener, [url], { detached: true, stdio: "ignore" }).unref();
4027
+ }
4028
+ catch {
4029
+ // Browser open failed — user will open manually
4030
+ }
4031
+ });
4032
+ await saveCredentials({
4033
+ account: {
4034
+ address: result.address,
4035
+ session_token: "",
4036
+ expires_at: result.expires_at,
4037
+ },
4038
+ personal_server: {
4039
+ url: psUrl,
4040
+ session_token: result.session_token,
4041
+ expires_at: result.expires_at,
4042
+ },
4043
+ });
4044
+ emit.success(`Logged in to ${psUrl}`);
4045
+ emit.success(`Credentials saved to ~/.vana/auth.json`);
4046
+ return 0;
4047
+ }
4048
+ catch (err) {
4049
+ emit.info(` ✗ Login failed: ${err instanceof Error ? err.message : String(err)}`);
4050
+ return 1;
4051
+ }
4052
+ }
4053
+ // Cloud flow (account.vana.org)
4054
+ // Check env var shortcut
4055
+ const envToken = process.env.VANA_SESSION_TOKEN;
4056
+ if (envToken) {
4057
+ const creds = loadCredentials();
4058
+ if (creds) {
4059
+ if (options.json) {
4060
+ process.stdout.write(`${JSON.stringify({ status: "authenticated", source: "env", address: creds.account.address })}\n`);
4061
+ }
4062
+ else {
4063
+ const emit = createEmitter(options);
4064
+ emit.success(`Already authenticated via VANA_SESSION_TOKEN env var`);
4065
+ }
4066
+ return 0;
4067
+ }
4068
+ }
4069
+ // Check if already logged in
4070
+ const existing = loadCredentials();
4071
+ if (existing && !isExpired(existing)) {
4072
+ if (options.json) {
4073
+ process.stdout.write(`${JSON.stringify({
4074
+ status: "authenticated",
4075
+ address: existing.account.address,
4076
+ personal_server: existing.personal_server?.url ?? null,
4077
+ expires_at: existing.account.expires_at,
4078
+ })}\n`);
4079
+ }
4080
+ else {
4081
+ const emit = createEmitter(options);
4082
+ emit.success(`Already logged in as ${formatAddress(existing.account.address)}`);
4083
+ if (existing.personal_server) {
4084
+ emit.keyValue("Personal Server", existing.personal_server.url, "success");
4085
+ }
4086
+ emit.info(` Auth expires in ${formatExpiresIn(existing.account.expires_at)}`);
4087
+ emit.blank();
4088
+ emit.info(" Run `vana logout` first to re-authenticate.");
4089
+ }
4090
+ return 0;
4091
+ }
4092
+ if (options.json) {
4093
+ // JSON mode: run flow and output result
4094
+ const creds = await runDeviceCodeFlow({
4095
+ onCode: (code, uri) => {
4096
+ process.stderr.write(JSON.stringify({ event: "device_code", code, uri }) + "\n");
4097
+ },
4098
+ onWaiting: () => { },
4099
+ onAuthorized: () => { },
4100
+ onExpired: () => {
4101
+ process.stdout.write(JSON.stringify({ status: "expired", error: "Device code expired" }) +
4102
+ "\n");
4103
+ },
4104
+ onError: (err) => {
4105
+ process.stdout.write(JSON.stringify({ status: "error", error: err.message }) + "\n");
4106
+ },
4107
+ });
4108
+ if (creds) {
4109
+ await saveCredentials(creds);
4110
+ process.stdout.write(`${JSON.stringify({
4111
+ status: "authenticated",
4112
+ address: creds.account.address,
4113
+ personal_server: creds.personal_server?.url ?? null,
4114
+ expires_at: creds.account.expires_at,
4115
+ })}\n`);
4116
+ return 0;
4117
+ }
4118
+ return 1;
4119
+ }
4120
+ // Interactive mode
4121
+ const emit = createEmitter(options);
4122
+ emit.blank();
4123
+ emit.info(" Logging in to Vana...");
4124
+ emit.blank();
4125
+ const creds = await runDeviceCodeFlow({
4126
+ onCode: (code, uri) => {
4127
+ emit.info(` ! Open this URL in your browser:`);
4128
+ emit.info(` ${uri}`);
4129
+ emit.blank();
4130
+ emit.info(` ! Enter this code: ${BOLD}${code}${RESET}`);
4131
+ emit.blank();
4132
+ },
4133
+ onWaiting: () => {
4134
+ emit.info(" Waiting for authorization...");
4135
+ },
4136
+ onAuthorized: async (authedCreds) => {
4137
+ await saveCredentials(authedCreds);
4138
+ emit.success(`Logged in as ${formatAddress(authedCreds.account.address)}`);
4139
+ if (authedCreds.personal_server) {
4140
+ emit.blank();
4141
+ emit.keyValue("Personal Server", authedCreds.personal_server.url, "success");
4142
+ }
4143
+ emit.success(`Credentials saved to ~/.vana/auth.json`);
4144
+ },
4145
+ onExpired: () => {
4146
+ emit.info(` Device code expired. Run ${emit.code("vana login")} to try again.`);
4147
+ },
4148
+ onError: (err) => {
4149
+ emit.info(` Error: ${err.message}`);
4150
+ },
4151
+ });
4152
+ return creds ? 0 : 1;
4153
+ }
4154
+ async function runLogout(options) {
4155
+ // Revoke the token server-side before clearing local credentials
4156
+ const creds = loadCredentials();
4157
+ if (creds?.personal_server?.url && creds.personal_server.session_token) {
4158
+ try {
4159
+ await fetch(`${creds.personal_server.url.replace(/\/$/, "")}/auth/device/token`, {
4160
+ method: "DELETE",
4161
+ headers: {
4162
+ Authorization: `Bearer ${creds.personal_server.session_token}`,
4163
+ },
4164
+ signal: AbortSignal.timeout(5000),
4165
+ });
4166
+ }
4167
+ catch {
4168
+ // Best-effort — server may be down, but we still clear local creds
4169
+ }
4170
+ }
4171
+ await clearCredentials();
4172
+ if (options.json) {
4173
+ process.stdout.write(`${JSON.stringify({ status: "logged_out" })}\n`);
4174
+ }
4175
+ else {
4176
+ const emit = createEmitter(options);
4177
+ emit.success("Logged out. Credentials removed.");
4178
+ }
4179
+ return 0;
4180
+ }
3902
4181
  //# sourceMappingURL=index.js.map