@lotics/cli 0.192.0 → 0.192.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/README.md CHANGED
@@ -195,7 +195,7 @@ Every request identifies the CLI (`user-agent: lotics-cli/<version> node/<v> <pl
195
195
  **`LOTICS_TELEMETRY=1` additionally records the session.** Off by default. Set it in your shell profile rather than per command — each invocation is its own process. When set:
196
196
 
197
197
  - Requests carry a session id shared by every command in the sitting — under an agent harness it adopts the harness's own session id, otherwise it rolls over after 30 minutes idle (`~/.lotics/session.json`).
198
- - Each invocation appends one record to `~/.lotics/telemetry.ndjson` — the command, its exit code, how long it took, and, on a failure, the message that was printed. Batches are sent on a later run; a failed send is retried, never dropped silently.
198
+ - Each invocation appends one record to `~/.lotics/telemetry-<host>.ndjson` — the command, its exit code, how long it took, and, on a failure, the message that was printed. One spool per Lotics the CLI talks to, so a record made against one instance is only ever sent to that instance. Batches are sent on a later run against the same host; a failed send is retried, never dropped silently.
199
199
 
200
200
  Arguments contribute a **hash** and a **shape** (`records[].data.name:string`) and nothing else — no values, no file contents, no record data. The hash is the point: two failures in a row with the same hash mean the error message did not tell you enough to fix the call.
201
201
 
package/dist/src/cli.js CHANGED
@@ -44372,10 +44372,9 @@ var LoticsRequestError = class extends Error {
44372
44372
  status;
44373
44373
  body;
44374
44374
  };
44375
- var API_BASE_URL = process.env.LOTICS_API_URL ?? "https://api.lotics.ai";
44376
44375
  var WEB_APP_URL = process.env.LOTICS_WEB_URL ?? "https://lotics.ai";
44377
- async function fetchOfficialStarters() {
44378
- const read = await getPublicJson(`${API_BASE_URL}/v1/starters/official`, "list the packages");
44376
+ async function fetchOfficialStarters(apiUrl) {
44377
+ const read = await getPublicJson(`${apiUrl}/v1/starters/official`, "list the packages");
44379
44378
  if (!read.ok) {
44380
44379
  throw new Error(
44381
44380
  `Lotics answered ${read.status} listing the packages. If this keeps happening, browse ${WEB_APP_URL}/docs/cli.`
@@ -44395,16 +44394,16 @@ async function getPublicJson(url2, what) {
44395
44394
  if (!response.ok) return { ok: false, status: response.status };
44396
44395
  return { ok: true, body: await response.json() };
44397
44396
  }
44398
- async function readOfficialStarter(starter_id) {
44397
+ async function readOfficialStarter(apiUrl, starter_id) {
44399
44398
  const read = await getPublicJson(
44400
- `${API_BASE_URL}/v1/starters/official/${encodeURIComponent(starter_id)}`,
44399
+ `${apiUrl}/v1/starters/official/${encodeURIComponent(starter_id)}`,
44401
44400
  `read ${starter_id}`
44402
44401
  );
44403
44402
  if (!read.ok) return read;
44404
44403
  return { ok: true, package: read.body };
44405
44404
  }
44406
- async function getOfficialStarter(starter_id) {
44407
- const read = await readOfficialStarter(starter_id);
44405
+ async function getOfficialStarter(apiUrl, starter_id) {
44406
+ const read = await readOfficialStarter(apiUrl, starter_id);
44408
44407
  if (!read.ok) {
44409
44408
  throw new Error(
44410
44409
  `Lotics answered ${read.status} reading ${starter_id}. Only packages Lotics publishes can be read without an account.`
@@ -44413,10 +44412,10 @@ async function getOfficialStarter(starter_id) {
44413
44412
  return read.package;
44414
44413
  }
44415
44414
  var PUBLIC_FETCH_TIMEOUT_MS = 1e4;
44416
- async function startCliLogin(email3) {
44415
+ async function startCliLogin(apiUrl, email3) {
44417
44416
  let response;
44418
44417
  try {
44419
- response = await fetch(`${API_BASE_URL}/v1/cli/login_requests`, {
44418
+ response = await fetch(`${apiUrl}/v1/cli/login_requests`, {
44420
44419
  method: "POST",
44421
44420
  headers: { "Content-Type": "application/json" },
44422
44421
  body: JSON.stringify({ email: email3 }),
@@ -44432,8 +44431,8 @@ async function startCliLogin(email3) {
44432
44431
  }
44433
44432
  return await response.json();
44434
44433
  }
44435
- async function pollCliLogin(request_id, secret) {
44436
- const url2 = `${API_BASE_URL}/v1/cli/login_requests/${encodeURIComponent(request_id)}`;
44434
+ async function pollCliLogin(apiUrl, request_id, secret) {
44435
+ const url2 = `${apiUrl}/v1/cli/login_requests/${encodeURIComponent(request_id)}`;
44437
44436
  let response;
44438
44437
  try {
44439
44438
  response = await fetch(url2, {
@@ -44460,14 +44459,14 @@ var LoticsClient = class {
44460
44459
  * construction — surfaced so `lotics app dev` can show it in the banner. */
44461
44460
  viewAsMemberId;
44462
44461
  /** API URL the client is configured against. Read-only after construction.
44463
- * Surfaced for callers that need to display or log it (e.g., `lotics app dev`
44464
- * shows it in the banner). */
44462
+ * Surfaced for callers that need to display it (`lotics app dev`'s banner) or
44463
+ * to hand it on (the wrapper page's RPC target, the scaffold's font proxy). */
44465
44464
  baseUrl;
44466
44465
  constructor(options) {
44467
44466
  this.apiKey = options.apiKey;
44468
44467
  this.workspaceId = options.workspaceId;
44469
44468
  this.viewAsMemberId = options.viewAsMemberId;
44470
- this.baseUrl = API_BASE_URL;
44469
+ this.baseUrl = options.apiUrl;
44471
44470
  }
44472
44471
  /**
44473
44472
  * The id is appended to the MESSAGE rather than carried on a field, because
@@ -44870,9 +44869,10 @@ var LoticsClient = class {
44870
44869
  return tables.filter((t) => t !== null);
44871
44870
  }
44872
44871
  /**
44873
- * Rename an app's public subdomain — its `<slug>.lotics.app` address.
44874
- * Mirrors PUT /v1/apps/{app_id}/subdomain. The old subdomain stops
44875
- * resolving once the change lands.
44872
+ * Rename an app's public subdomain — the label its origin is built on.
44873
+ * Mirrors PUT /v1/apps/{app_id}/subdomain, and returns the finished `origin`
44874
+ * because only the instance knows the domain and scheme it serves apps on.
44875
+ * The old subdomain stops resolving once the change lands.
44876
44876
  */
44877
44877
  async setAppSubdomain(app_id, public_subdomain) {
44878
44878
  return this.request(
@@ -45500,8 +45500,16 @@ function canonicalize(value2) {
45500
45500
  var MAX_ERROR_CHARS = 2e3;
45501
45501
  var MAX_SPOOL_BYTES = 1024 * 1024;
45502
45502
  var MAX_FLUSH_EVENTS = 500;
45503
- function spoolPath() {
45504
- return path3.join(os2.homedir(), ".lotics", "telemetry.ndjson");
45503
+ function spoolPath(apiUrl) {
45504
+ const shard = crypto2.createHash("sha256").update(apiUrl).digest("hex").slice(0, 12);
45505
+ return path3.join(os2.homedir(), ".lotics", `telemetry-${shard}.ndjson`);
45506
+ }
45507
+ var targetApiUrl = "";
45508
+ function noteTarget(apiUrl) {
45509
+ targetApiUrl = apiUrl;
45510
+ }
45511
+ function invocationTarget() {
45512
+ return targetApiUrl;
45505
45513
  }
45506
45514
  var stderrTail = "";
45507
45515
  function recordStderr(text) {
@@ -45561,8 +45569,8 @@ function buildEvent(exitCode, durationMs, now) {
45561
45569
  ...pendingScope
45562
45570
  };
45563
45571
  }
45564
- function appendEvent(event) {
45565
- const file2 = spoolPath();
45572
+ function appendEvent(event, apiUrl) {
45573
+ const file2 = spoolPath(apiUrl);
45566
45574
  try {
45567
45575
  fs3.mkdirSync(path3.dirname(file2), { recursive: true });
45568
45576
  if ((fs3.statSync(file2, { throwIfNoEntry: false })?.size ?? 0) > MAX_SPOOL_BYTES) {
@@ -45573,11 +45581,11 @@ function appendEvent(event) {
45573
45581
  } catch {
45574
45582
  }
45575
45583
  }
45576
- function readSpool() {
45584
+ function readSpool(apiUrl) {
45577
45585
  try {
45578
45586
  const events = [];
45579
45587
  const lines = [];
45580
- for (const line of fs3.readFileSync(spoolPath(), "utf-8").split("\n")) {
45588
+ for (const line of fs3.readFileSync(spoolPath(apiUrl), "utf-8").split("\n")) {
45581
45589
  if (line.trim() === "") continue;
45582
45590
  try {
45583
45591
  events.push(JSON.parse(line));
@@ -45590,32 +45598,34 @@ function readSpool() {
45590
45598
  return { events: [], lines: [] };
45591
45599
  }
45592
45600
  }
45593
- function clearSpool(sent) {
45601
+ function clearSpool(apiUrl, sent) {
45594
45602
  try {
45595
- const current = fs3.readFileSync(spoolPath(), "utf-8");
45596
- fs3.writeFileSync(spoolPath(), current.startsWith(sent) ? current.slice(sent.length) : "", {
45603
+ const file2 = spoolPath(apiUrl);
45604
+ const current = fs3.readFileSync(file2, "utf-8");
45605
+ fs3.writeFileSync(file2, current.startsWith(sent) ? current.slice(sent.length) : "", {
45597
45606
  mode: 384
45598
45607
  });
45599
45608
  } catch {
45600
45609
  }
45601
45610
  }
45602
- function installTelemetry(startedAt2 = Date.now()) {
45611
+ function installTelemetry(target, startedAt2 = Date.now()) {
45603
45612
  if (!telemetryEnabled()) return;
45613
+ noteTarget(target);
45604
45614
  installStderrCapture();
45605
45615
  process.on("exit", (code) => {
45606
45616
  const event = buildEvent(code, Date.now() - startedAt2, /* @__PURE__ */ new Date());
45607
- if (event !== null) appendEvent(event);
45617
+ if (event !== null) appendEvent(event, invocationTarget());
45608
45618
  });
45609
45619
  }
45610
- function flushSpool(post, minEvents = 5) {
45620
+ function flushSpool(apiUrl, post, minEvents = 5) {
45611
45621
  if (!telemetryEnabled()) return;
45612
- const { events, lines } = readSpool();
45622
+ const { events, lines } = readSpool(apiUrl);
45613
45623
  if (events.length < minEvents) return;
45614
45624
  const batch = events.slice(0, MAX_FLUSH_EVENTS);
45615
45625
  const sent = `${lines.slice(0, batch.length).join("\n")}
45616
45626
  `;
45617
45627
  void post(batch).then((outcome) => {
45618
- if (outcome !== "retry") clearSpool(sent);
45628
+ if (outcome !== "retry") clearSpool(apiUrl, sent);
45619
45629
  }).catch(() => {
45620
45630
  });
45621
45631
  }
@@ -45641,6 +45651,10 @@ async function postEvents(baseUrl, apiKey, events, timeoutMs = 5e3) {
45641
45651
  import fs4 from "node:fs";
45642
45652
  import path4 from "node:path";
45643
45653
  import os3 from "node:os";
45654
+ var HOSTED_API_URL = "https://api.lotics.ai";
45655
+ function envApiUrl() {
45656
+ return process.env.LOTICS_API_URL ?? HOSTED_API_URL;
45657
+ }
45644
45658
  function globalConfigFile() {
45645
45659
  return path4.join(os3.homedir(), ".lotics", "config.json");
45646
45660
  }
@@ -45663,12 +45677,34 @@ function configFileForScope(scope) {
45663
45677
  return globalConfigFile();
45664
45678
  }
45665
45679
  function readConfigFile(file2) {
45680
+ let parsed;
45666
45681
  try {
45667
45682
  const raw = fs4.readFileSync(file2, "utf-8");
45668
- return JSON.parse(raw);
45683
+ parsed = JSON.parse(raw);
45669
45684
  } catch {
45670
45685
  return null;
45671
45686
  }
45687
+ return upgradeProfileApiUrls(file2, parsed);
45688
+ }
45689
+ function upgradeProfileApiUrls(file2, config2) {
45690
+ const profiles = config2.profiles;
45691
+ if (!profiles) return config2;
45692
+ const entries2 = Object.entries(profiles);
45693
+ if (!entries2.some(([, profile]) => !profile.api_url)) return config2;
45694
+ const upgraded = {
45695
+ ...config2,
45696
+ profiles: Object.fromEntries(
45697
+ entries2.map(([orgId, profile]) => [
45698
+ orgId,
45699
+ { ...profile, api_url: profile.api_url || HOSTED_API_URL }
45700
+ ])
45701
+ )
45702
+ };
45703
+ try {
45704
+ writeConfigFile(file2, upgraded);
45705
+ } catch {
45706
+ }
45707
+ return upgraded;
45672
45708
  }
45673
45709
  function writeConfigFile(file2, config2) {
45674
45710
  fs4.mkdirSync(path4.dirname(file2), { recursive: true, mode: 448 });
@@ -45715,6 +45751,11 @@ function resolveProfileByNameOrId(profiles, nameOrId) {
45715
45751
  return null;
45716
45752
  }
45717
45753
  function resolveContext(flags, appWorkspaceId) {
45754
+ const resolved = resolveCredential(flags, appWorkspaceId);
45755
+ if (resolved) noteTarget(resolved.apiUrl);
45756
+ return resolved;
45757
+ }
45758
+ function resolveCredential(flags, appWorkspaceId) {
45718
45759
  const envKey = process.env.LOTICS_API_KEY;
45719
45760
  const envOrg = process.env.LOTICS_ORG;
45720
45761
  const envWorkspace = process.env.LOTICS_WORKSPACE;
@@ -45732,6 +45773,7 @@ function resolveContext(flags, appWorkspaceId) {
45732
45773
  const [orgId, profile] = resolved;
45733
45774
  return {
45734
45775
  apiKey: profile.api_key,
45776
+ apiUrl: profile.api_url,
45735
45777
  orgId,
45736
45778
  orgName: profile.org_name,
45737
45779
  workspaceId: wsOverride ?? profile.workspace_id,
@@ -45739,10 +45781,10 @@ function resolveContext(flags, appWorkspaceId) {
45739
45781
  };
45740
45782
  }
45741
45783
  if (flags.apiKey) {
45742
- return { apiKey: flags.apiKey, workspaceId: wsOverride, source: "flag" };
45784
+ return { apiKey: flags.apiKey, apiUrl: envApiUrl(), workspaceId: wsOverride, source: "flag" };
45743
45785
  }
45744
45786
  if (envKey) {
45745
- return { apiKey: envKey, workspaceId: wsOverride, source: "env_key" };
45787
+ return { apiKey: envKey, apiUrl: envApiUrl(), workspaceId: wsOverride, source: "env_key" };
45746
45788
  }
45747
45789
  const global2 = loadGlobalConfig();
45748
45790
  const profiles = global2?.profiles ?? {};
@@ -45756,6 +45798,7 @@ function resolveContext(flags, appWorkspaceId) {
45756
45798
  const [orgId, profile] = resolved;
45757
45799
  return {
45758
45800
  apiKey: profile.api_key,
45801
+ apiUrl: profile.api_url,
45759
45802
  orgId,
45760
45803
  orgName: profile.org_name,
45761
45804
  workspaceId: wsOverride ?? profile.workspace_id,
@@ -45772,6 +45815,7 @@ function resolveContext(flags, appWorkspaceId) {
45772
45815
  }
45773
45816
  return {
45774
45817
  apiKey: profile.api_key,
45818
+ apiUrl: profile.api_url,
45775
45819
  orgId: local.active_org,
45776
45820
  orgName: profile.org_name,
45777
45821
  workspaceId: wsOverride ?? local.workspace_id ?? profile.workspace_id,
@@ -45789,6 +45833,7 @@ function resolveContext(flags, appWorkspaceId) {
45789
45833
  const [orgId, profile] = matches[0];
45790
45834
  return {
45791
45835
  apiKey: profile.api_key,
45836
+ apiUrl: profile.api_url,
45792
45837
  orgId,
45793
45838
  orgName: profile.org_name,
45794
45839
  workspaceId: appWorkspaceId,
@@ -45805,6 +45850,7 @@ function resolveContext(flags, appWorkspaceId) {
45805
45850
  }
45806
45851
  return {
45807
45852
  apiKey: profile.api_key,
45853
+ apiUrl: profile.api_url,
45808
45854
  orgId: global2.active_org,
45809
45855
  orgName: profile.org_name,
45810
45856
  workspaceId: wsOverride ?? profile.workspace_id,
@@ -45819,6 +45865,7 @@ function upsertProfile(orgId, fields) {
45819
45865
  const profile = {
45820
45866
  api_key: fields.api_key,
45821
45867
  org_name: fields.org_name,
45868
+ api_url: fields.api_url,
45822
45869
  workspace_id: fields.workspace_id ?? existing?.workspace_id
45823
45870
  };
45824
45871
  saveGlobalConfig({
@@ -46001,7 +46048,7 @@ function resultSideEffects(result) {
46001
46048
  }
46002
46049
 
46003
46050
  // src/version.ts
46004
- var VERSION = "0.192.0";
46051
+ var VERSION = "0.192.2";
46005
46052
 
46006
46053
  // src/timezone.ts
46007
46054
  function machineTimezone() {
@@ -46562,7 +46609,7 @@ var COMMANDS = [
46562
46609
  " fields keep whatever is bound (a manifest is a",
46563
46610
  " snapshot; replaying it would revert them). Change",
46564
46611
  " those with set_app_agent, then pull.",
46565
- " lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address",
46612
+ " lotics app subdomain <new-subdomain> Rename the app's public address (its subdomain)",
46566
46613
  ` lotics app rename "<new name>" Rename the app's display name (launcher title)`,
46567
46614
  " lotics app dev [path] Run the app locally with HMR (RPC forwarded to prod)"
46568
46615
  ]
@@ -46997,8 +47044,8 @@ export default defineConfig({
46997
47044
  server: {
46998
47045
  // Allow the sandboxed null-origin iframe used by \`lotics app dev\` to
46999
47046
  // fetch HMR client + source modules from the dev server. Production
47000
- // iframe loads modules from api.lotics.ai which already permits null
47001
- // origin via CORS.
47047
+ // iframe loads modules from the API, which already permits null origin
47048
+ // via CORS.
47002
47049
  cors: { origin: "*" },
47003
47050
  // Chat-authoring previews reach this dev server through the sandbox proxy
47004
47051
  // at \`5173-<id>-<token>.lotics-sandbox.app\`. Vite \u22655.4.12 rejects HMR
@@ -65067,7 +65114,7 @@ var appSchema = zod_default.object({
65067
65114
  "Pointer to the currently-published app_versions row in R2. Set once the app has been deployed at least once via `lotics app deploy`; null before the first deploy."
65068
65115
  ),
65069
65116
  public_subdomain: zod_default.string().describe(
65070
- "DNS label for the app's public origin: `<public_subdomain>.lotics.app`. Server-generated, high-entropy, and distinct from `id` (app_ids are not valid DNS labels). Assigned at creation for every app."
65117
+ "DNS label for the app's public origin, under the instance's apps domain. Server-generated, high-entropy, and distinct from `id` (app_ids are not valid DNS labels). Assigned at creation for every app."
65071
65118
  ),
65072
65119
  public_password_set: zod_default.boolean().optional().describe(
65073
65120
  "Whether a shared password gates the public binding. True \u2192 anonymous visitors must authenticate at `/v1/apps/{app_id}/public/authenticate` before any publicAppAccess endpoint resolves. The hash itself is never sent over the wire; only this flag is exposed (and only on authenticated owner-side reads \u2014 the public by-subdomain response surfaces the same fact as `requires_password`)."
@@ -68942,8 +68989,8 @@ function noteCopiedContent(templates, knowledgeDocs) {
68942
68989
  );
68943
68990
  }
68944
68991
  }
68945
- async function libraryListPublic() {
68946
- const [presets, packages] = await Promise.all([fetchPresetIndex(), fetchOfficialStarters()]);
68992
+ async function libraryListPublic(apiUrl) {
68993
+ const [presets, packages] = await Promise.all([fetchPresetIndex(), fetchOfficialStarters(apiUrl)]);
68947
68994
  printPresetSection(presets);
68948
68995
  printPackageSection(packages, "published by Lotics");
68949
68996
  console.error(
@@ -69071,8 +69118,8 @@ function printPackageHead(pkg) {
69071
69118
  console.log(`
69072
69119
  version ${pkg.latest_version}`);
69073
69120
  }
69074
- async function libraryShowPublic(starter_id) {
69075
- const pkg = await getOfficialStarter(starter_id);
69121
+ async function libraryShowPublic(apiUrl, starter_id) {
69122
+ const pkg = await getOfficialStarter(apiUrl, starter_id);
69076
69123
  printPackageHead(pkg);
69077
69124
  console.log(`trust official \u2014 reviewed by Lotics`);
69078
69125
  printPackageEntities(pkg.contract.entities);
@@ -77541,7 +77588,7 @@ function withPlanFiles(starter, plan) {
77541
77588
  async function appSetSubdomain(client, args) {
77542
77589
  const meta3 = readAppMeta(process.cwd());
77543
77590
  const result = await client.setAppSubdomain(meta3.app_id, args.subdomain);
77544
- console.error(`Public address set: https://${result.public_subdomain}.lotics.app`);
77591
+ console.error(`Public address set: ${result.origin}`);
77545
77592
  }
77546
77593
  async function appRename(client, args) {
77547
77594
  const meta3 = readAppMeta(process.cwd());
@@ -79363,33 +79410,37 @@ function signInInstructions(pending, options) {
79363
79410
  ${then}`;
79364
79411
  }
79365
79412
  async function startPendingLogin(email3) {
79366
- const request = await startCliLogin(email3);
79413
+ const apiUrl = envApiUrl();
79414
+ const request = await startCliLogin(apiUrl, email3);
79367
79415
  const pending = {
79368
79416
  request_id: request.request_id,
79369
79417
  secret: request.secret,
79370
79418
  code: request.code,
79371
79419
  expires_at: request.expires_at,
79372
79420
  email: email3,
79373
- api_url: API_BASE_URL
79421
+ api_url: apiUrl
79374
79422
  };
79375
79423
  savePendingLogin(pending);
79376
79424
  return pending;
79377
79425
  }
79378
- function saveApproval(state, email3, options) {
79426
+ function saveApproval(state, pending, options) {
79379
79427
  requireNonEmpty(state.api_key, "api_key");
79380
79428
  requireNonEmpty(state.organization_id, "organization_id");
79381
79429
  requireNonEmpty(state.workspace_id, "workspace_id");
79382
79430
  upsertProfile(state.organization_id, {
79383
79431
  api_key: state.api_key,
79384
79432
  org_name: state.organization_name,
79433
+ // The host that minted the key, taken from the request rather than re-read:
79434
+ // this is the whole of "which Lotics does this registration belong to".
79435
+ api_url: pending.api_url,
79385
79436
  workspace_id: state.workspace_id
79386
79437
  });
79387
79438
  const existing = loadGlobalConfig() ?? {};
79388
- saveGlobalConfig({ ...existing, email: email3 });
79439
+ saveGlobalConfig({ ...existing, email: pending.email });
79389
79440
  setActiveOrg(state.organization_id, options?.local === true ? "local" : "global");
79390
79441
  clearPendingLogin();
79391
79442
  return {
79392
- email: email3,
79443
+ email: pending.email,
79393
79444
  orgId: state.organization_id,
79394
79445
  orgName: state.organization_name,
79395
79446
  workspaceId: state.workspace_id
@@ -79399,14 +79450,15 @@ async function claimPendingLogin() {
79399
79450
  const pending = loadPendingLogin();
79400
79451
  if (!pending) return { status: "none" };
79401
79452
  const expired = `The sign-in request expired. Run again: lotics auth login ${pending.email}`;
79453
+ const target = envApiUrl();
79402
79454
  if (Date.parse(pending.expires_at) <= Date.now()) {
79403
79455
  clearPendingLogin();
79404
- return pending.api_url === API_BASE_URL ? { status: "gone", message: expired } : { status: "none" };
79456
+ return pending.api_url === target ? { status: "gone", message: expired } : { status: "none" };
79405
79457
  }
79406
- if (pending.api_url !== API_BASE_URL) return { status: "none" };
79407
- const state = await pollCliLogin(pending.request_id, pending.secret);
79458
+ if (pending.api_url !== target) return { status: "none" };
79459
+ const state = await pollCliLogin(pending.api_url, pending.request_id, pending.secret);
79408
79460
  if (state.status === "approved") {
79409
- return { status: "signed_in", account: saveApproval(state, pending.email) };
79461
+ return { status: "signed_in", account: saveApproval(state, pending) };
79410
79462
  }
79411
79463
  if (state.status === "pending") {
79412
79464
  return {
@@ -79427,14 +79479,14 @@ async function awaitCliLogin(email3, options) {
79427
79479
  for (; ; ) {
79428
79480
  let state;
79429
79481
  try {
79430
- state = await pollCliLogin(pending.request_id, pending.secret);
79482
+ state = await pollCliLogin(pending.api_url, pending.request_id, pending.secret);
79431
79483
  } catch (error52) {
79432
79484
  if (!(Date.now() < expiresAt)) throw error52;
79433
79485
  await new Promise((resolve2) => setTimeout(resolve2, POLL_INTERVAL_MS));
79434
79486
  continue;
79435
79487
  }
79436
79488
  if (state.status === "approved") {
79437
- return saveApproval(state, email3, options);
79489
+ return saveApproval(state, pending, options);
79438
79490
  }
79439
79491
  if (state.status === "expired") {
79440
79492
  throw new Error(`That sign-in link expired. Run "lotics auth login ${email3}" again.`);
@@ -108451,8 +108503,8 @@ function prompt(question) {
108451
108503
  });
108452
108504
  });
108453
108505
  }
108454
- async function publicPost(path14, body) {
108455
- const response = await fetch(`${API_BASE_URL}${path14}`, {
108506
+ async function publicPost(apiUrl, path14, body) {
108507
+ const response = await fetch(`${apiUrl}${path14}`, {
108456
108508
  method: "POST",
108457
108509
  headers: { "Content-Type": "application/json" },
108458
108510
  body: JSON.stringify(body)
@@ -108465,7 +108517,8 @@ async function signupAccount(args) {
108465
108517
  if (args.name) body.name = args.name;
108466
108518
  const timezone = args.timezone ?? machineTimezone();
108467
108519
  if (timezone) body.timezone = timezone;
108468
- const { ok, status, data: data2 } = await publicPost("/v1/cli/signup", body);
108520
+ const apiUrl = envApiUrl();
108521
+ const { ok, status, data: data2 } = await publicPost(apiUrl, "/v1/cli/signup", body);
108469
108522
  if (!ok) {
108470
108523
  if (status === 409) {
108471
108524
  return { kind: "email_taken" };
@@ -108479,7 +108532,7 @@ async function signupAccount(args) {
108479
108532
  const orgId = data2.organization_id;
108480
108533
  const workspaceId = data2.workspace_id;
108481
108534
  const orgName = args.name || args.email.split("@")[0];
108482
- upsertProfile(orgId, { api_key: apiKey, org_name: orgName, workspace_id: workspaceId });
108535
+ upsertProfile(orgId, { api_key: apiKey, org_name: orgName, api_url: apiUrl, workspace_id: workspaceId });
108483
108536
  const existing = loadGlobalConfig() ?? {};
108484
108537
  saveGlobalConfig({ ...existing, email: args.email });
108485
108538
  setActiveOrg(orgId, args.local ? "local" : "global");
@@ -108621,7 +108674,8 @@ async function handleSetup(providedKey, local) {
108621
108674
  console.error("No API key provided.");
108622
108675
  process.exit(1);
108623
108676
  }
108624
- const client = new LoticsClient({ apiKey });
108677
+ const apiUrl = envApiUrl();
108678
+ const client = new LoticsClient({ apiKey, apiUrl });
108625
108679
  let info;
108626
108680
  try {
108627
108681
  info = await client.whoami();
@@ -108647,6 +108701,7 @@ Multiple workspaces in ${info.organization_name}. Run "lotics workspace select <
108647
108701
  upsertProfile(info.organization_id, {
108648
108702
  api_key: apiKey,
108649
108703
  org_name: info.organization_name,
108704
+ api_url: apiUrl,
108650
108705
  workspace_id: workspaceId
108651
108706
  });
108652
108707
  const existing = loadGlobalConfig() ?? {};
@@ -108684,9 +108739,9 @@ async function requireClient(flags, appWorkspaceId) {
108684
108739
  }
108685
108740
  const viewAsMemberId = flags.viewAs ?? process.env.LOTICS_VIEW_AS;
108686
108741
  noteScope({ org_id: ctx.orgId ?? void 0, workspace_id: ctx.workspaceId });
108687
- flushSpool((events) => postEvents(API_BASE_URL, ctx.apiKey, events));
108742
+ flushSpool(ctx.apiUrl, (events) => postEvents(ctx.apiUrl, ctx.apiKey, events));
108688
108743
  return {
108689
- client: new LoticsClient({ apiKey: ctx.apiKey, workspaceId: ctx.workspaceId, viewAsMemberId }),
108744
+ client: new LoticsClient({ apiKey: ctx.apiKey, apiUrl: ctx.apiUrl, workspaceId: ctx.workspaceId, viewAsMemberId }),
108690
108745
  ctx
108691
108746
  };
108692
108747
  }
@@ -108775,7 +108830,7 @@ function looksLikeModelFile(arg) {
108775
108830
  }
108776
108831
  async function main() {
108777
108832
  setInvocation(commandPath(process.argv.slice(2)), sessionId(), VERSION);
108778
- installTelemetry(startedAt);
108833
+ installTelemetry(envApiUrl(), startedAt);
108779
108834
  const parsed = parseArgs(process.argv.slice(2));
108780
108835
  const { restArgs, flags } = parsed;
108781
108836
  let { command, subcommand, toolArgs } = parsed;
@@ -108995,7 +109050,7 @@ async function main() {
108995
109050
  if (command === "library") {
108996
109051
  if (subcommand === "list") {
108997
109052
  if (resolveContext(flags) === null) {
108998
- await libraryListPublic();
109053
+ await libraryListPublic(envApiUrl());
108999
109054
  return;
109000
109055
  }
109001
109056
  const { client: client2, ctx: ctx2 } = await requireClient(flags);
@@ -109026,11 +109081,12 @@ async function main() {
109026
109081
  }
109027
109082
  if (flags.json) {
109028
109083
  setMachineOutput(true);
109029
- if (resolveContext(flags) === null) {
109030
- emitJson(await getOfficialStarter(toolArgs));
109084
+ const shelfCtx = resolveContext(flags);
109085
+ if (shelfCtx === null) {
109086
+ emitJson(await getOfficialStarter(envApiUrl(), toolArgs));
109031
109087
  return;
109032
109088
  }
109033
- const published = await readOfficialStarter(toolArgs);
109089
+ const published = await readOfficialStarter(shelfCtx.apiUrl, toolArgs);
109034
109090
  if (published.ok) {
109035
109091
  emitJson(published.package);
109036
109092
  return;
@@ -109052,7 +109108,7 @@ async function main() {
109052
109108
  return;
109053
109109
  }
109054
109110
  if (resolveContext(flags) === null) {
109055
- await libraryShowPublic(toolArgs);
109111
+ await libraryShowPublic(envApiUrl(), toolArgs);
109056
109112
  return;
109057
109113
  }
109058
109114
  const { client: client2, ctx: ctx2 } = await requireClient(flags);
@@ -109173,7 +109229,7 @@ async function main() {
109173
109229
  }
109174
109230
  noteScope({ org_id: reportCtx.orgId ?? void 0, workspace_id: reportCtx.workspaceId });
109175
109231
  await reportCommand(
109176
- new LoticsClient({ apiKey: reportCtx.apiKey, workspaceId: reportCtx.workspaceId }),
109232
+ new LoticsClient({ apiKey: reportCtx.apiKey, apiUrl: reportCtx.apiUrl, workspaceId: reportCtx.workspaceId }),
109177
109233
  {
109178
109234
  report: parsed2.report,
109179
109235
  version: VERSION,
@@ -109205,23 +109261,28 @@ async function main() {
109205
109261
  console.error('Not authenticated. Run "lotics auth signup", "lotics auth api-key <key>", or set LOTICS_API_KEY.');
109206
109262
  process.exit(1);
109207
109263
  }
109208
- const client2 = new LoticsClient({ apiKey: ctx2.apiKey });
109264
+ const client2 = new LoticsClient({ apiKey: ctx2.apiKey, apiUrl: ctx2.apiUrl });
109209
109265
  const info = await client2.whoami();
109210
109266
  const existing = loadGlobalConfig() ?? {};
109211
109267
  saveGlobalConfig({ ...existing, email: info.email });
109212
109268
  const cached2 = existing.profiles?.[info.organization_id];
109213
109269
  if (cached2 && cached2.org_name !== info.organization_name) {
109214
- upsertProfile(info.organization_id, { api_key: cached2.api_key, org_name: info.organization_name });
109270
+ upsertProfile(info.organization_id, {
109271
+ api_key: cached2.api_key,
109272
+ org_name: info.organization_name,
109273
+ api_url: cached2.api_url
109274
+ });
109215
109275
  console.error(`Renamed since this key was saved: "${cached2.org_name}" \u2192 "${info.organization_name}" (updated locally)`);
109216
109276
  }
109217
109277
  const viewAs = flags.viewAs ?? process.env.LOTICS_VIEW_AS;
109218
109278
  if (flags.json) {
109219
- console.log(JSON.stringify({ ...info, workspace_id: ctx2.workspaceId ?? null, source: ctx2.source, view_as: viewAs ?? null }, null, 2));
109279
+ console.log(JSON.stringify({ ...info, workspace_id: ctx2.workspaceId ?? null, api_url: ctx2.apiUrl, source: ctx2.source, view_as: viewAs ?? null }, null, 2));
109220
109280
  } else {
109221
109281
  console.log(`Name: ${info.name}`);
109222
109282
  console.log(`Email: ${info.email}`);
109223
109283
  console.log(`Org: ${info.organization_name} (${info.organization_id})`);
109224
109284
  console.log(`Workspace: ${ctx2.workspaceId ?? "(none selected)"}`);
109285
+ console.log(`API: ${ctx2.apiUrl}`);
109225
109286
  console.log(`Source: ${SOURCE_LABELS[ctx2.source]}`);
109226
109287
  if (viewAs) console.log(`View as: ${viewAs} (admin preview; requires an admin key)`);
109227
109288
  }
@@ -109302,7 +109363,7 @@ async function main() {
109302
109363
  try {
109303
109364
  const ctx2 = resolveContext(flags, appManifestWorkspaceId(command, subcommand, void 0, flags));
109304
109365
  if (ctx2) {
109305
- client2 = new LoticsClient({ apiKey: ctx2.apiKey, workspaceId: ctx2.workspaceId });
109366
+ client2 = new LoticsClient({ apiKey: ctx2.apiKey, apiUrl: ctx2.apiUrl, workspaceId: ctx2.workspaceId });
109306
109367
  await resolveWorkspace(client2, ctx2);
109307
109368
  }
109308
109369
  } catch (err2) {
@@ -109323,7 +109384,7 @@ async function main() {
109323
109384
  }
109324
109385
  applyAppManifestWorkspace(ctx2, command, subcommand, projectDir, flags);
109325
109386
  const viewAsMemberId = flags.viewAs ?? process.env.LOTICS_VIEW_AS;
109326
- const client2 = new LoticsClient({ apiKey: ctx2.apiKey, workspaceId: ctx2.workspaceId, viewAsMemberId });
109387
+ const client2 = new LoticsClient({ apiKey: ctx2.apiKey, apiUrl: ctx2.apiUrl, workspaceId: ctx2.workspaceId, viewAsMemberId });
109327
109388
  await resolveWorkspace(client2, ctx2);
109328
109389
  await appCodegen({ projectDir, client: client2 });
109329
109390
  return;
@@ -109353,7 +109414,11 @@ async function main() {
109353
109414
  console.error("Note: a local pin (.lotics/config.json) overrides the global default in this directory. Use --local to change the pin here.");
109354
109415
  }
109355
109416
  }
109356
- await validateOrgWorkspacePin(new LoticsClient({ apiKey: profile.api_key }), orgId, profile);
109417
+ await validateOrgWorkspacePin(
109418
+ new LoticsClient({ apiKey: profile.api_key, apiUrl: profile.api_url }),
109419
+ orgId,
109420
+ profile
109421
+ );
109357
109422
  return;
109358
109423
  }
109359
109424
  if (subcommand && subcommand !== "list") {
@@ -109372,6 +109437,7 @@ async function main() {
109372
109437
  orgIds.map((id) => ({
109373
109438
  org_id: id,
109374
109439
  org_name: profiles[id].org_name,
109440
+ api_url: profiles[id].api_url,
109375
109441
  workspace_id: profiles[id].workspace_id ?? null,
109376
109442
  active: id === effectiveActive
109377
109443
  })),
@@ -109381,7 +109447,7 @@ async function main() {
109381
109447
  } else {
109382
109448
  for (const id of orgIds) {
109383
109449
  const marker = id === effectiveActive ? " (active)" : "";
109384
- console.log(`${id} ${profiles[id].org_name}${marker}`);
109450
+ console.log(`${id} ${profiles[id].org_name} ${profiles[id].api_url}${marker}`);
109385
109451
  }
109386
109452
  }
109387
109453
  return;
@@ -109414,7 +109480,7 @@ async function main() {
109414
109480
  console.error(" lotics app workflow pull Rewrite src/workflows/*.ts from the server");
109415
109481
  console.error(" lotics app workflow check [alias] Typecheck src/workflows bodies locally");
109416
109482
  console.error(" lotics app query set <alias> Push lotics.queries.<alias> to apps.queries (no deploy)");
109417
- console.error(" lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address");
109483
+ console.error(" lotics app subdomain <new-subdomain> Rename the app's public address (its subdomain)");
109418
109484
  console.error(` lotics app rename "<new name>" Rename the app's display name (launcher title)`);
109419
109485
  console.error(" lotics app dev [path] Run the app locally with HMR + RPC forwarding");
109420
109486
  process.exit(1);
@@ -109634,7 +109700,7 @@ Available workspaces:`);
109634
109700
  const newSubdomain = toolArgs;
109635
109701
  if (!newSubdomain) {
109636
109702
  console.error("Usage: lotics app subdomain <new-subdomain>");
109637
- console.error("Renames the app's public address \u2014 https://<slug>.lotics.app");
109703
+ console.error("Renames the app's public address \u2014 its subdomain under the apps domain");
109638
109704
  process.exit(1);
109639
109705
  }
109640
109706
  await appSetSubdomain(client, { subdomain: newSubdomain });
@@ -21,6 +21,11 @@ export interface AppQueryFilterGroup {
21
21
  export type AppQueryFilter = AppQueryFilterCondition | AppQueryFilterGroup;
22
22
  export interface LoticsClientOptions {
23
23
  apiKey: string;
24
+ /** The Lotics this key belongs to — `ResolvedContext.apiUrl`, which is the
25
+ * registration's own `api_url`. Required and never defaulted: a client that
26
+ * can fall back to an ambient host is a client that can send a private
27
+ * instance's key to production (`docs/on_premise.md` section 8). */
28
+ apiUrl: string;
24
29
  workspaceId?: string;
25
30
  /** Admin "View as": when set, every request carries `x-view-as-member-id`, so
26
31
  * the backend evaluates IAM scoping (and `is_current_member`) as this member.
@@ -342,7 +347,6 @@ export declare class LoticsRequestError extends Error {
342
347
  readonly body: Record<string, unknown>;
343
348
  constructor(message: string, status: number, body: Record<string, unknown>);
344
349
  }
345
- export declare const API_BASE_URL: string;
346
350
  /**
347
351
  * The website: where a person goes when the CLI cannot finish the job, and where
348
352
  * the presets are SERVED FROM. Held beside the API base so the pair is read
@@ -379,7 +383,7 @@ export interface OfficialStarter {
379
383
  * starter for what I do, or should I build?" is decided before an account
380
384
  * exists, so needing one to ask means signing up to find out the answer was no.
381
385
  */
382
- export declare function fetchOfficialStarters(): Promise<OfficialStarter[]>;
386
+ export declare function fetchOfficialStarters(apiUrl: string): Promise<OfficialStarter[]>;
383
387
  /**
384
388
  * One keyless GET of JSON, bounded — the shape every read that predates an
385
389
  * account takes.
@@ -419,7 +423,7 @@ export declare function getPublicJson(url: string, what: string): Promise<{
419
423
  * throws: "unreachable" is not "not on the shelf", and answering both the same
420
424
  * way would tell an owner their package is missing every time the link drops.
421
425
  */
422
- export declare function readOfficialStarter(starter_id: string): Promise<{
426
+ export declare function readOfficialStarter(apiUrl: string, starter_id: string): Promise<{
423
427
  ok: true;
424
428
  package: OfficialStarterRead;
425
429
  } | {
@@ -427,7 +431,7 @@ export declare function readOfficialStarter(starter_id: string): Promise<{
427
431
  status: number;
428
432
  }>;
429
433
  /** The same read for a caller with nothing to fall back to — a refusal is the end. */
430
- export declare function getOfficialStarter(starter_id: string): Promise<OfficialStarterRead>;
434
+ export declare function getOfficialStarter(apiUrl: string, starter_id: string): Promise<OfficialStarterRead>;
431
435
  /** A column as the public read shows it: enough to write a model field from. */
432
436
  export interface OfficialStarterField {
433
437
  alias: string;
@@ -524,8 +528,8 @@ export type CliLoginState = {
524
528
  * The same answer whether or not the address has an account: it would
525
529
  * otherwise tell any stranger which emails are registered here.
526
530
  */
527
- export declare function startCliLogin(email: string): Promise<CliLoginRequest>;
528
- export declare function pollCliLogin(request_id: string, secret: string): Promise<CliLoginState>;
531
+ export declare function startCliLogin(apiUrl: string, email: string): Promise<CliLoginRequest>;
532
+ export declare function pollCliLogin(apiUrl: string, request_id: string, secret: string): Promise<CliLoginState>;
529
533
  export declare class LoticsClient {
530
534
  private apiKey;
531
535
  private workspaceId;
@@ -533,8 +537,8 @@ export declare class LoticsClient {
533
537
  * construction — surfaced so `lotics app dev` can show it in the banner. */
534
538
  readonly viewAsMemberId: string | undefined;
535
539
  /** API URL the client is configured against. Read-only after construction.
536
- * Surfaced for callers that need to display or log it (e.g., `lotics app dev`
537
- * shows it in the banner). */
540
+ * Surfaced for callers that need to display it (`lotics app dev`'s banner) or
541
+ * to hand it on (the wrapper page's RPC target, the scaffold's font proxy). */
538
542
  readonly baseUrl: string;
539
543
  constructor(options: LoticsClientOptions);
540
544
  private throwResponseError;
@@ -987,13 +991,15 @@ export declare class LoticsClient {
987
991
  }>;
988
992
  }>>;
989
993
  /**
990
- * Rename an app's public subdomain — its `<slug>.lotics.app` address.
991
- * Mirrors PUT /v1/apps/{app_id}/subdomain. The old subdomain stops
992
- * resolving once the change lands.
994
+ * Rename an app's public subdomain — the label its origin is built on.
995
+ * Mirrors PUT /v1/apps/{app_id}/subdomain, and returns the finished `origin`
996
+ * because only the instance knows the domain and scheme it serves apps on.
997
+ * The old subdomain stops resolving once the change lands.
993
998
  */
994
999
  setAppSubdomain(app_id: string, public_subdomain: string): Promise<{
995
1000
  app_id: string;
996
1001
  public_subdomain: string;
1002
+ origin: string;
997
1003
  }>;
998
1004
  getAppVersion(app_id: string, version_id: string): Promise<{
999
1005
  id: string;
@@ -149,10 +149,9 @@ var LoticsRequestError = class extends Error {
149
149
  status;
150
150
  body;
151
151
  };
152
- var API_BASE_URL = process.env.LOTICS_API_URL ?? "https://api.lotics.ai";
153
152
  var WEB_APP_URL = process.env.LOTICS_WEB_URL ?? "https://lotics.ai";
154
- async function fetchOfficialStarters() {
155
- const read = await getPublicJson(`${API_BASE_URL}/v1/starters/official`, "list the packages");
153
+ async function fetchOfficialStarters(apiUrl) {
154
+ const read = await getPublicJson(`${apiUrl}/v1/starters/official`, "list the packages");
156
155
  if (!read.ok) {
157
156
  throw new Error(
158
157
  `Lotics answered ${read.status} listing the packages. If this keeps happening, browse ${WEB_APP_URL}/docs/cli.`
@@ -172,16 +171,16 @@ async function getPublicJson(url, what) {
172
171
  if (!response.ok) return { ok: false, status: response.status };
173
172
  return { ok: true, body: await response.json() };
174
173
  }
175
- async function readOfficialStarter(starter_id) {
174
+ async function readOfficialStarter(apiUrl, starter_id) {
176
175
  const read = await getPublicJson(
177
- `${API_BASE_URL}/v1/starters/official/${encodeURIComponent(starter_id)}`,
176
+ `${apiUrl}/v1/starters/official/${encodeURIComponent(starter_id)}`,
178
177
  `read ${starter_id}`
179
178
  );
180
179
  if (!read.ok) return read;
181
180
  return { ok: true, package: read.body };
182
181
  }
183
- async function getOfficialStarter(starter_id) {
184
- const read = await readOfficialStarter(starter_id);
182
+ async function getOfficialStarter(apiUrl, starter_id) {
183
+ const read = await readOfficialStarter(apiUrl, starter_id);
185
184
  if (!read.ok) {
186
185
  throw new Error(
187
186
  `Lotics answered ${read.status} reading ${starter_id}. Only packages Lotics publishes can be read without an account.`
@@ -190,10 +189,10 @@ async function getOfficialStarter(starter_id) {
190
189
  return read.package;
191
190
  }
192
191
  var PUBLIC_FETCH_TIMEOUT_MS = 1e4;
193
- async function startCliLogin(email) {
192
+ async function startCliLogin(apiUrl, email) {
194
193
  let response;
195
194
  try {
196
- response = await fetch(`${API_BASE_URL}/v1/cli/login_requests`, {
195
+ response = await fetch(`${apiUrl}/v1/cli/login_requests`, {
197
196
  method: "POST",
198
197
  headers: { "Content-Type": "application/json" },
199
198
  body: JSON.stringify({ email }),
@@ -209,8 +208,8 @@ async function startCliLogin(email) {
209
208
  }
210
209
  return await response.json();
211
210
  }
212
- async function pollCliLogin(request_id, secret) {
213
- const url = `${API_BASE_URL}/v1/cli/login_requests/${encodeURIComponent(request_id)}`;
211
+ async function pollCliLogin(apiUrl, request_id, secret) {
212
+ const url = `${apiUrl}/v1/cli/login_requests/${encodeURIComponent(request_id)}`;
214
213
  let response;
215
214
  try {
216
215
  response = await fetch(url, {
@@ -237,14 +236,14 @@ var LoticsClient = class {
237
236
  * construction — surfaced so `lotics app dev` can show it in the banner. */
238
237
  viewAsMemberId;
239
238
  /** API URL the client is configured against. Read-only after construction.
240
- * Surfaced for callers that need to display or log it (e.g., `lotics app dev`
241
- * shows it in the banner). */
239
+ * Surfaced for callers that need to display it (`lotics app dev`'s banner) or
240
+ * to hand it on (the wrapper page's RPC target, the scaffold's font proxy). */
242
241
  baseUrl;
243
242
  constructor(options) {
244
243
  this.apiKey = options.apiKey;
245
244
  this.workspaceId = options.workspaceId;
246
245
  this.viewAsMemberId = options.viewAsMemberId;
247
- this.baseUrl = API_BASE_URL;
246
+ this.baseUrl = options.apiUrl;
248
247
  }
249
248
  /**
250
249
  * The id is appended to the MESSAGE rather than carried on a field, because
@@ -647,9 +646,10 @@ var LoticsClient = class {
647
646
  return tables.filter((t) => t !== null);
648
647
  }
649
648
  /**
650
- * Rename an app's public subdomain — its `<slug>.lotics.app` address.
651
- * Mirrors PUT /v1/apps/{app_id}/subdomain. The old subdomain stops
652
- * resolving once the change lands.
649
+ * Rename an app's public subdomain — the label its origin is built on.
650
+ * Mirrors PUT /v1/apps/{app_id}/subdomain, and returns the finished `origin`
651
+ * because only the instance knows the domain and scheme it serves apps on.
652
+ * The old subdomain stops resolving once the change lands.
653
653
  */
654
654
  async setAppSubdomain(app_id, public_subdomain) {
655
655
  return this.request(
@@ -1161,7 +1161,6 @@ var LoticsClient = class {
1161
1161
  }
1162
1162
  };
1163
1163
  export {
1164
- API_BASE_URL,
1165
1164
  LoticsClient,
1166
1165
  LoticsRequestError,
1167
1166
  WEB_APP_URL,
@@ -7,11 +7,11 @@ Per-command syntax, flags, contracts, and gotchas for the public `lotics` CLI. S
7
7
  | `lotics` / `lotics --help` | Show full help with capabilities, tool categories, workflow |
8
8
  | `lotics auth signup <email>` | Create account + org + API key, sends magic link email. Registers the new org as a profile; `--local` pins this directory to it (pointer) instead of setting the global default. |
9
9
  | `lotics auth login <email>` | Sign in an account that already exists, on a machine holding no key. **Two steps, and it does not wait for the person.** The first prints the page to open — `https://lotics.ai/cli_login/<request_id>`, also mailed — and the code that page must show, records the request, and exits 0. They sign in there if asked, check the code and press Confirm. **Then the next command that needs a credential collects the key** before it does its own work, so the second step is just re-running whatever was wanted; a command run before Confirm exits 1 naming the page and the code again, and once the 15 minutes are up it says to ask again. The handful that run WITHOUT a credential — `library list`/`show`, `scaffold docs`/`check`, `app codegen`, `app workflow check` — claim nothing, so one of those run after Confirm still answers as though signed out. `--wait` keeps one command instead, holding the terminal until Confirm; `--local` pins this directory to that org rather than setting the global default, and implies `--wait` (a pin names THIS directory, so only the terminal that stays in it can write one). `--json` prints `organization_id`, `workspace_id` and `organization_name` when it finishes signed in, and `request_id`, `confirm_url`, `code`, `email`, `expires_at` when it is the first step. The request's secret is never printed and the org's key never leaves the store. |
10
- | `lotics auth api-key [key]` | `whoami` → **upsert** the key's org as a profile in the global store (never overwrites). `--local` additionally pins this directory to it (pointer) instead of setting the global default. |
10
+ | `lotics auth api-key [key]` | `whoami` → **upsert** the key's org as a profile in the global store (never overwrites). The profile records the instance the key was verified against (`LOTICS_API_URL`, default `https://api.lotics.ai`), and every later command for that org goes there. `--local` additionally pins this directory to it (pointer) instead of setting the global default. |
11
11
  | `lotics auth web` | Send a magic link email to access the web app (requires auth) |
12
- | `lotics auth whoami` | Print active account name, email, org, resolved workspace, and the resolution **source** (flag/env/local/app-manifest/global). `--json` adds `workspace_id` + `source`. |
12
+ | `lotics auth whoami` | Print active account name, email, org, resolved workspace, the instance the credential belongs to, and the resolution **source** (flag/env/local/app-manifest/global). `--json` adds `workspace_id`, `api_url` + `source`. |
13
13
  | `lotics auth logout [<name\|id>]` | In a pinned dir: delete the local pin. Else: remove one profile (default the active org). `--all`: wipe the global store. |
14
- | `lotics org` | List saved orgs (profiles) from the global store, marks active for this directory (a local pin wins over the global default). |
14
+ | `lotics org` | List saved orgs (profiles) from the global store with the instance each belongs to, marks active for this directory (a local pin wins over the global default). |
15
15
  | `lotics org use <name\|id> [--local]` | Switch the active org by org name (case-insensitive, ambiguous → error) or id. No flag → global `active_org`; `--local` → a `.lotics/config.json` pointer in the current dir. |
16
16
  | `lotics workspace` | List workspaces in the active org, marks current with `(current)` |
17
17
  | `lotics workspace select <id>` | Set the workspace in the **active scope** — a local pin if the dir has one, else the active org's global profile |
@@ -61,7 +61,7 @@ Per-command syntax, flags, contracts, and gotchas for the public `lotics` CLI. S
61
61
  | `lotics app query set <alias>` \| `--all` | Push `package.json#lotics.queries` (`{ ast, params? }` per alias) to `apps.queries` through `set_app_query` — **the only author of a query binding**, the mirror of `app workflow set`. A deploy pushes a DRIFTED declaration through this same verb before it ships (see `app deploy`), so this is the explicit single-alias path, not the only way a query reaches the app. The **server** validates each one exactly as it always did (alias identifier, workspace-only tables, resolvable fields, declared params). `--all` pushes every declared alias, alias-sorted, stopping at the first failure and naming what already landed. Clear error + non-zero exit on an alias absent from the manifest or a validation failure. **The declaration's fields MERGE**, so the manifest is not a snapshot: deleting `params` from an alias and pushing leaves the live params exactly where they were, because an absent key means "unchanged". Clear one with `params: null`, or replace the map with the set you want. After the push it regenerates `.lotics/app_queries.d.ts` from the manifest, so the types the next `npm run typecheck` reads match what was just pushed. |
62
62
  | `lotics app workflow pull` | Rewrite every `src/workflows/<alias>.ts` from the server (faithful body per bound alias via `get_app_workflow`) **+ its `.lotics/workflows/<alias>.globals.d.ts`** (via `getAppWorkflowDts`, so the body is locally typecheckable via `lotics app workflow check`) without a full `app pull` (no source archive, no npm install). A legacy alias with no rendered source warns and is skipped; a dts-fetch failure is non-fatal (body still written with the fallback wrapper, typecheck degraded). Each alias's `description` is folded back into `package.json#lotics.workflows.<alias>` from the same read — the alias binding the manifest is otherwise stamped from carries `inputs`/`outputs` but not the description, which lives on the workflow ROW, so without this a pull would erase an authored one. The server's GENERATED default is skipped, so an app that never described its workflows gains no manifest noise. Also idempotently patches the main `tsconfig.json` `exclude` to cover `src/workflows` + `.lotics/workflows` so a pre-existing app's `npm run typecheck` never loads the bodies or the colliding per-alias globals. |
63
63
  | `lotics app workflow check [alias]` | Check the editable workflow bodies locally, no auth / no network, in the **server's own order** — parse, then type-check. **Parse** runs `parseWorkflowJs` from `@lotics/shared` (the SAME module `verifyWorkflow` calls, never a second implementation) over the stripped body `set` would upload, with `toolNames: undefined` (the CLI ships no tool registry, so tool-name resolution stays a server check while every shape/scope rule runs here). A body the subset rejects reports **that error alone** and skips the compiler — it never reaches the server's compiler either, so tsc's opinion of it is noise. **Type-check** then builds an **isolated** `ts.Program` per alias from exactly that alias's `{body, globals}` pair — mirroring the server, which verifies one body at a time — so the per-alias ambient `trigger` never collides and `trigger.app_workflow.inputs` is checked against the right alias. All aliases run in ONE node process (N programs, not N `tsc` spawns), with the SAME compile options the server uses at set-time verify (lib `es2022` with no DOM, target ES2022, strict, NodeNext, `types:[]`, skipLibCheck) and the app's OWN `typescript` (resolved from its `node_modules`, never bundled into the CLI). What the compiler sees is the **checked source**, not the file: `rewriteAccumulatorAppends` from `@lotics/shared` — the SAME transform the server applies before its set-time compile — is applied in memory, so a pulled body's canonical `out = concat(out, [item])` accumulator checks green here exactly as it saves there, and the body on disk is never rewritten. Reports `<file>:<line>:<col> - <TS####\|subset>` at the **physical** line in `src/workflows/<alias>.ts`, so an editor jump lands on the offending code (these are deliberately NOT `set`'s body-relative numbers — `set` prints no file path, so there is no format to agree with); exits non-zero if any alias fails. Green is honest but not total: `set` additionally resolves names, lints and structurally validates against the live workspace — passes that need its tables and tool schemas, so they cannot run offline, and the success line says so. A bound alias with no body file yet warns + skips; a body with no globals errors (naming `lotics app codegen`, which refreshes types WITHOUT touching the body — a pull would overwrite it). **It also keeps the types honest.** Each alias's `.lotics/workflows/<alias>.globals.d.ts` carries a `// lotics:declaration <hash>` stamp of the manifest declaration it was rendered from; `check` compares it to `package.json#lotics.workflows.<alias>` and, when they differ, re-renders that alias's dts from the LOCAL declaration before compiling. Without it the verdict was confidently wrong in the exact case an author needs it — declare an input, run `check`, and get `TS2339: Property 'x' does not exist` pointing at your body for a schema the types have never been told about. The server renders a dts from a SUPPLIED declaration, so this works before the manifest has ever been deployed, which is when it matters (the order is edit → check → set). This is the ONE thing `check` uses the API for: it is skipped entirely when the stamps match (the common case, so `check` stays instant and offline), and with no credentials or a failed fetch it WARNS and checks against the older types rather than blocking. A file written before the stamp existed reads as unknown, never as matching, so a pre-existing checkout heals on its first run. |
64
- | `lotics app subdomain <new-subdomain>` | Rename the app's public `<slug>.lotics.app` address via `PUT /v1/apps/{id}/subdomain`. app_id comes from the local `package.json` manifest; the chosen slug must be a valid DNS label and free; the old address stops resolving. |
64
+ | `lotics app subdomain <new-subdomain>` | Rename the app's public address under the instance's apps domain via `PUT /v1/apps/{id}/subdomain`. app_id comes from the local `package.json` manifest; the chosen slug must be a valid DNS label and free; the old address stops resolving. |
65
65
  | `lotics app rename "<new name>"` | Change the app's display name (launcher/title) via the `update_app` tool. app_id comes from the local `package.json` manifest; the public address (`subdomain`) and code (`deploy`) are unchanged. |
66
66
  | `lotics app dev [path] [--port=N] [--vite-port=N] [--view-as=<member_id>]` | Spawn Vite dev server + an RPC-forwarding HTTP server. The wrapper page embeds the iframe with `sandbox="allow-scripts allow-same-origin"` matching production; postMessage ops (query / workflow / members / context / upload / openExternal / urlState / agentRun) are forwarded to api.lotics.ai using the CLI's API key — file bytes move in **both** directions through the dev server's own relays, never browser↔storage: dev runs against the PROD bucket, whose CORS admits `https://*.lotics.app` and not `http://localhost:<port>`, so a direct browser transfer is blocked — no upload could complete and no preview engine (PDF/Word/Excel all FETCH the bytes) could read a file. `upload` mints a presigned URL and PUTs it **to `PUT /_upload/<file_id>`** (`dev/upload_relay.ts`) from the wrapper page — same-origin, so no preflight and no CORS — and Node forwards it on; every presigned `url`/`thumbnail_url`/`preview_url` on a **file object** in an RPC result is rewritten to **`GET /_file/<token>`** (`dev/file_relay.ts`, absolute — the iframe would resolve a relative path against Vite), which streams the bytes back with `Range` passthrough (206s intact, so PDF seeking works) and an `Access-Control-Allow-Origin` for the Vite origin (the one cross-origin hop left is OUR response to allow). Neither relay ever takes a destination from the client — it gets a `file_id`/token and transfers only to/from a URL it minted or observed itself, so there is no client-controlled target and no SSRF surface. A URL in a record's own text cell is NOT rewritten. Production is unchanged (direct-to-storage, no bytes through the API server); `openExternal` and `urlState.get/set` are handled locally (the latter read/write the wrapper page's own address bar — `set` writes in place via `replaceState` and browser back/forward broadcast a `url-state` message back, so `useUrlState` survives refresh and is shareable in the dev loop; in-app *routing* is the app's own (the iframe owns its url via `@lotics/app-sdk/router`), and the wrapper bakes the saved screen (`_loc`) into the iframe src on load so a refresh restores it, mirroring production); `agentRun` (streaming) is proxied through `POST /_agent_run`, which opens the run's SSE with the CLI key and pipes chunks back to the iframe (`stream-chunk`* → `stream-end`), so `useAgentRun` works in the dev loop just like production; `context` resolves the viewer (`member_id` from `cli/whoami` + `comments_enabled` from the local manifest) and fetches the app's stored `config` live from the app row, so `useConfig()` renders the same values as production. `--view-as` (global flag; also `LOTICS_VIEW_AS`) threads `x-view-as-member-id` so `is_current_member` + `context` resolve to that member — **admin key only** (the server 403s a non-admin), writes stay attributed to the key owner. Hot reload via Vite; full DevTools / Playwright access via plain localhost. **Every forwarded op logs one line naming its ALIAS** — `[rpc] query applicants 231ms` — and `query applicants (count)` for a count request, which is a SECOND full execution of the same query rather than a cheap lookup. When requests overlap the line carries `· N in flight`. That number is the one to watch: the server bounds how many app queries run at once, so requests past the bound wait and the wait lands inside each request's own duration — a burst reads as "every query got slower", which looks like a slow database and is not one. A screen firing its list plus three facet counts on one keystroke shows up here as eight lines over one or two aliases; see `@lotics/app-sdk` `docs/data_fetching.md` (`useCount`, and handing `usePaginatedQuery` a `total`) and `docs/queries.md` §10 for collapsing them. **Holds no realtime connection** — push belongs to the product frontend, so an app previewed here never updates on an external write (a CLI run, another tab, an agent): reload to see it. Deliberate rather than missing, since the alternative is a second implementation of the channel in the wrapper page, and a blanket poll here would hide an app whose queries do not declare their tables — the one mistake the real host punishes. The startup banner says `realtime: off` so this is visible without reading this table. The scaffold's `vite.config.ts` carries no dev-optimizer list: @lotics/ui ships built ESM, so Vite's own dep scanner reaches its CJS-interop imports and pre-bundles them without being told to. Binds **loopback only** (`127.0.0.1`) — `/_rpc` dispatches with the developer's API key, so a socket on every interface would hand anyone on the network full read/write on the workspace. |
67
67
  | `LOTICS_UI_SRC=<abs path to packages/ui/src>` (env, not a command) | Dev-link `@lotics/ui` to a monorepo checkout for the length of ONE command, **for every tool at once**. The app's `vite.config.ts` gets its whole `resolve` block from the kit (`resolve: loticsResolve()` — `@lotics/ui/vite`), which reads the variable at call time and adds the `@lotics/ui/*` → working-copy alias, so kit edits go live under `lotics app dev` (HMR) and bundle under `lotics app deploy`. In the same breath, every command that regenerates types (`create`/`pull`/`dev`/`deploy`/`codegen`, all via `writeAppDts`) writes **`.lotics/tsconfig.link.json`** — the matching `paths`, which the app's `tsconfig.json` `extends` — so `tsc`, vitest, eslint and your EDITOR resolve the same copy Vite does. Unset ⇒ every one of them goes back to `node_modules`, and the generated file is rewritten inert. **Why `paths` and not `npm link`:** under the dev-link a kit file sits OUTSIDE the app's `node_modules` and resolves its OWN `react` from the monorepo — two copies in one program and every shared type stops matching ("Two different types with this name exist, but they are unrelated"). The generated file therefore also pins every peer @lotics/ui declares to the APP's copy, types-package first (`react` → `@types/react`; pinning the runtime package instead strands tsc on a `.js` with no declarations). The pin set is derived from the installed kit's `peerDependencies`, so it tracks the kit rather than rotting. **Nothing hand-written is touched** — the generated file lives in `.lotics/` (the CLI's own dir) and no config is edited by regex. Identical for a monorepo app and an EXTERNAL one (e.g. `~/lotics_apps`). `app deploy` still warns whenever the variable is set — that the bundle carries kit code from your working copy, or that the app's config predates `loticsResolve()` and never reads it, so the PUBLISHED kit is going out. An app whose `tsconfig.json` already `extends` something else is told rather than rewritten: add `./.lotics/tsconfig.link.json` to the array yourself. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/cli",
3
- "version": "0.192.0",
3
+ "version": "0.192.2",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {