@lotics/cli 0.188.0 → 0.190.0

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.
@@ -78,6 +78,34 @@ export interface ToolInfo {
78
78
  description: string;
79
79
  input_schema: unknown;
80
80
  }
81
+ /**
82
+ * What `POST /v1/apps/{app_id}/upgrade` reports it did — an offer applied in
83
+ * part, and every part it declined to touch named back.
84
+ *
85
+ * Both `skipped_*` lists are aliases the owner has EDITED since the copy, so
86
+ * the new version's body was left alone; `dropped_fields` are columns the new
87
+ * version stopped declaring and that still hold the owner's data. Neither is an
88
+ * error, and both are the reason this result is printed rather than counted.
89
+ */
90
+ export interface AppUpgradeResult {
91
+ app_id: string;
92
+ from_version: number;
93
+ to_version: number;
94
+ deployed: {
95
+ version_id: string;
96
+ version_number: number;
97
+ };
98
+ created_fields: Array<{
99
+ entity: string;
100
+ field: string;
101
+ }>;
102
+ skipped_workflows: string[];
103
+ skipped_agents: string[];
104
+ dropped_fields: Array<{
105
+ entity: string;
106
+ field: string;
107
+ }>;
108
+ }
81
109
  /**
82
110
  * A single knowledge doc with its HYDRATED body — the shape of
83
111
  * `GET /v1/knowledge_docs/{id}`, and the one content-read path a non-sandbox
@@ -114,13 +142,15 @@ export interface FileUploadResult {
114
142
  error: string;
115
143
  }>;
116
144
  }
117
- /** One finding from the extract behind a starter publish. */
145
+ /** One finding from the extract behind a package publish. */
118
146
  export interface ExtractFinding {
119
147
  severity: "error" | "warning" | "info";
120
148
  area: string;
121
149
  message: string;
122
150
  }
123
151
  export interface StarterPublishRequest {
152
+ /** The package to release into. Omitted, the origin apps resolve it. */
153
+ starter_id?: string;
124
154
  /** The origin apps that ship, in alias-minting order. */
125
155
  app_ids: string[];
126
156
  /** Live knowledge doc ids to bundle. Omitted keeps the previous version's set; [] drops them all. */
@@ -144,6 +174,11 @@ export interface StarterPublishPreview {
144
174
  starter_id: string | null;
145
175
  starter_name: string;
146
176
  version: number;
177
+ /** The entities it would carry. */
178
+ entities: Array<{
179
+ alias: string;
180
+ label: string;
181
+ }>;
147
182
  apps: Array<{
148
183
  alias: string;
149
184
  app_id: string;
@@ -199,6 +234,7 @@ export interface StarterVersionContract {
199
234
  entities: Array<{
200
235
  alias: string;
201
236
  label: string;
237
+ description?: string;
202
238
  fields: Array<{
203
239
  alias: string;
204
240
  label: string;
@@ -235,21 +271,105 @@ export interface KnowledgeWarnings {
235
271
  /** `knowledge_expects` doc names with no matching workspace doc. */
236
272
  missing_expected_docs: string[];
237
273
  }
274
+ /**
275
+ * A workspace MODEL on the wire — a package contract with no apps, plus the
276
+ * first rows that travel beside it keyed by entity alias.
277
+ *
278
+ * The contract's authoritative shape is `packageContractSchema` in
279
+ * `@lotics/shared`, a specifier a published `.d.ts` cannot resolve — so the wire
280
+ * body is typed structurally here, the same trade `StarterVersionContract`
281
+ * makes. The CLI passes a value it has already parsed through that schema.
282
+ */
283
+ export interface ScaffoldWorkspaceRequest {
284
+ contract: Record<string, unknown>;
285
+ rows?: Record<string, Array<{
286
+ ref: string;
287
+ fields: Record<string, unknown>;
288
+ }>>;
289
+ /** Bind an entity whose label already names a table here. Absent, a colliding label is refused. */
290
+ adopt?: boolean;
291
+ }
292
+ export interface ScaffoldWorkspaceResult {
293
+ /** Every entity the model declares, in contract order. */
294
+ entities: Array<{
295
+ alias: string;
296
+ table_id: string;
297
+ /** False when a table of that label already existed and was adopted. */
298
+ created: boolean;
299
+ }>;
300
+ roles: Array<{
301
+ alias: string;
302
+ group_id: string;
303
+ }>;
304
+ /** Ids of the rows written, per entity alias — the handle for deleting them again. */
305
+ record_ids: Record<string, string[]>;
306
+ /** Rows were sent and none were written, because the run adopted a table. */
307
+ rows_skipped: boolean;
308
+ }
309
+ /**
310
+ * A workspace read BACK as a model — the inverse of the scaffold above.
311
+ *
312
+ * `entities` and `roles` are the model file's own two keys, whose authoritative
313
+ * shapes are `contractEntitySchema` / `contractRoleSchema` in `@lotics/shared` —
314
+ * specifiers a published `.d.ts` cannot resolve, so they are typed here as what
315
+ * this client does with them, which is hand them on whole. The same trade
316
+ * `ScaffoldWorkspaceRequest` makes in the other direction.
317
+ *
318
+ * `findings` are about the export rather than part of it: a workspace holds
319
+ * things a model file cannot express, and a file that dropped them silently
320
+ * would be read as the whole workspace.
321
+ */
322
+ export interface WorkspaceModelExport {
323
+ contract: {
324
+ entities: Array<Record<string, unknown>>;
325
+ roles: Array<Record<string, unknown>>;
326
+ templates: Array<Record<string, unknown>>;
327
+ };
328
+ findings: Array<{
329
+ severity: "error" | "warning" | "info";
330
+ area: string;
331
+ message: string;
332
+ }>;
333
+ }
334
+ /**
335
+ * A request the API refused. The message carries the status and the server's
336
+ * sentence, which is what reaches a person; `status` and `body` are for the
337
+ * few callers that branch on a refusal — a 409 whose `body.reason` says the
338
+ * run had nothing to do, rather than that it failed.
339
+ */
340
+ export declare class LoticsRequestError extends Error {
341
+ readonly status: number;
342
+ readonly body: Record<string, unknown>;
343
+ constructor(message: string, status: number, body: Record<string, unknown>);
344
+ }
238
345
  export declare const API_BASE_URL: string;
239
346
  /**
240
- * Where a person goes when the CLI cannot finish the job the one case being a
241
- * returning user on a machine that holds no key, since every route back in
242
- * needs one. Held beside the API base so the pair is read together, and named
243
- * once rather than inlined at each message that has to point somewhere.
347
+ * The website: where a person goes when the CLI cannot finish the job, and where
348
+ * the presets are SERVED FROM. Held beside the API base so the pair is read
349
+ * together, named once rather than inlined at each message that points
350
+ * somewhere, and overridable for the same reason the API base is — a run against
351
+ * a local site must read that site's presets, not production's.
244
352
  */
245
- export declare const WEB_APP_URL = "https://lotics.ai";
246
- /** One starter on the public shelf — what a chooser decides with, nothing else. */
353
+ export declare const WEB_APP_URL: string;
354
+ /** One package on the public shelf — what a chooser decides with, nothing else. */
247
355
  export interface OfficialStarter {
248
356
  id: string;
249
357
  name: string;
250
358
  description: string | null;
251
359
  icon: string | null;
252
360
  latest_version: number;
361
+ /**
362
+ * The apps the copy creates. A name and a sentence leave the chooser
363
+ * guessing; these are what someone's own words get matched against when
364
+ * deciding between a package and a model.
365
+ */
366
+ apps: Array<{
367
+ alias: string;
368
+ name: string;
369
+ description?: string;
370
+ }>;
371
+ /** How many tables the copy creates — the other half of "what is in this?". */
372
+ table_count: number;
253
373
  }
254
374
  /**
255
375
  * The starters Lotics publishes, fetched with no credential.
@@ -260,6 +380,152 @@ export interface OfficialStarter {
260
380
  * exists, so needing one to ask means signing up to find out the answer was no.
261
381
  */
262
382
  export declare function fetchOfficialStarters(): Promise<OfficialStarter[]>;
383
+ /**
384
+ * One keyless GET of JSON, bounded — the shape every read that predates an
385
+ * account takes.
386
+ *
387
+ * Bounded because these are the FIRST commands a new user runs and a hang with
388
+ * nothing on screen is the worst version of the failure; `config.ts` bounds its
389
+ * own unattended fetch for the same reason, and `docs/network_reliability.md` is
390
+ * a log of connections from our users' networks degrading rather than refusing,
391
+ * which is the shape that hangs.
392
+ *
393
+ * A transport failure THROWS and a status is RETURNED, because only the first is
394
+ * "check your connection": a status means the server replied and the network is
395
+ * demonstrably fine, and a caller that answered both the same way would tell an
396
+ * owner their package is missing every time the link drops.
397
+ */
398
+ export declare function getPublicJson(url: string, what: string): Promise<{
399
+ ok: true;
400
+ body: unknown;
401
+ } | {
402
+ ok: false;
403
+ status: number;
404
+ }>;
405
+ /**
406
+ * One official package READ WHOLE, with no credential — or the status that says
407
+ * the shelf does not serve it.
408
+ *
409
+ * The shelf row says what a package is; this says what is in it — entities with
410
+ * their fields, the roles, each template's metadata and each app's name. Never a
411
+ * workflow body: this is a read for deciding and for writing a model from, not a
412
+ * copy.
413
+ *
414
+ * Unauthenticated for the same reason the shelf is: a preset is READ rather than
415
+ * instantiated, and the whole point of reading one is that it happens before an
416
+ * account exists. Only official packages are served — the `is_official` filter,
417
+ * exactly as on the shelf — so a caller's OWN package answers a status here and
418
+ * is read through the authenticated pair instead. A transport failure still
419
+ * throws: "unreachable" is not "not on the shelf", and answering both the same
420
+ * way would tell an owner their package is missing every time the link drops.
421
+ */
422
+ export declare function readOfficialStarter(starter_id: string): Promise<{
423
+ ok: true;
424
+ package: OfficialStarterRead;
425
+ } | {
426
+ ok: false;
427
+ status: number;
428
+ }>;
429
+ /** 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>;
431
+ /** A column as the public read shows it: enough to write a model field from. */
432
+ export interface OfficialStarterField {
433
+ alias: string;
434
+ label: string;
435
+ type: string;
436
+ }
437
+ /** A table as the public read shows it: its columns. */
438
+ export interface OfficialStarterEntity {
439
+ alias: string;
440
+ label: string;
441
+ description?: string;
442
+ fields: OfficialStarterField[];
443
+ }
444
+ /**
445
+ * What the public read of one package carries.
446
+ *
447
+ * Every namespace is declared structurally, NARROWED to what a reader turning
448
+ * this into a model needs — fields carry their type, apps only their name — the
449
+ * same trade `StarterVersionContract` makes: a published `.d.ts` cannot resolve
450
+ * `@lotics/shared`. `packageContractSchema` is the definition, and the CLI
451
+ * parses a preset's block through `modelPresetSchema` before resolving a `from`
452
+ * file against it.
453
+ */
454
+ export interface OfficialStarterRead {
455
+ id: string;
456
+ name: string;
457
+ description: string | null;
458
+ latest_version: number;
459
+ contract: {
460
+ entities: OfficialStarterEntity[];
461
+ roles: Array<{
462
+ alias: string;
463
+ label: string;
464
+ }>;
465
+ templates: Array<{
466
+ alias: string;
467
+ label: string;
468
+ type: string;
469
+ }>;
470
+ /** The apps a copy creates. */
471
+ apps: Array<{
472
+ alias: string;
473
+ name: string;
474
+ description?: string;
475
+ }>;
476
+ /** Sample rows per entity alias — how many, never the rows. */
477
+ fixtures: Record<string, {
478
+ row_count: number;
479
+ }>;
480
+ knowledge: Record<string, {
481
+ name: string;
482
+ description: string;
483
+ }>;
484
+ knowledge_expects: string[];
485
+ };
486
+ }
487
+ /**
488
+ * What a terminal holds while it waits to be let in: the handle the server knows
489
+ * the request by, the `secret` that proves this is the same terminal that asked,
490
+ * and the `code` the person matches against the confirm page before pressing it.
491
+ *
492
+ * `secret` is a credential and is never printed — the `code` is what a person
493
+ * reads, and it proves nothing on its own.
494
+ */
495
+ export interface CliLoginRequest {
496
+ request_id: string;
497
+ secret: string;
498
+ code: string;
499
+ expires_at: string;
500
+ }
501
+ /** Where a login request stands. `approved` carries the key, and only once. */
502
+ export type CliLoginState = {
503
+ status: "pending";
504
+ } | {
505
+ status: "expired";
506
+ } | {
507
+ status: "claimed";
508
+ } | {
509
+ status: "approved";
510
+ api_key: string;
511
+ organization_id: string;
512
+ organization_name: string;
513
+ workspace_id: string;
514
+ };
515
+ /**
516
+ * Ask Lotics to mail a sign-in link, and read back whether it was confirmed.
517
+ *
518
+ * Plain functions rather than `LoticsClient` methods for the same reason
519
+ * `fetchOfficialStarters` is one: there is no key to build a client around, and
520
+ * that is the whole point — this is the pair a terminal holding NO credential
521
+ * uses to obtain one. Sending an `Authorization` header would make the endpoint
522
+ * answerable only to callers who no longer need it.
523
+ *
524
+ * The same answer whether or not the address has an account: it would
525
+ * otherwise tell any stranger which emails are registered here.
526
+ */
527
+ export declare function startCliLogin(email: string): Promise<CliLoginRequest>;
528
+ export declare function pollCliLogin(request_id: string, secret: string): Promise<CliLoginState>;
263
529
  export declare class LoticsClient {
264
530
  private apiKey;
265
531
  private workspaceId;
@@ -322,6 +588,26 @@ export declare class LoticsClient {
322
588
  name: string;
323
589
  timezone?: string;
324
590
  }): Promise<WorkspaceInfo>;
591
+ /**
592
+ * Apply a workspace MODEL to the current workspace — the from-scratch half of
593
+ * the starter pipeline, on a contract nobody published.
594
+ *
595
+ * Additive: with `adopt`, an entity whose label already names a table here
596
+ * binds to it and gains the fields, options and views it is missing; without
597
+ * `adopt`, that label is refused. Nothing is ever modified or deleted, and
598
+ * rows land only where every bound table is empty. Admin-only.
599
+ */
600
+ scaffoldWorkspace(body: ScaffoldWorkspaceRequest): Promise<ScaffoldWorkspaceResult>;
601
+ /**
602
+ * Read this workspace's schema back as a model — the tables it has (or only
603
+ * the ones named), their fields, options and views, plus its roles.
604
+ *
605
+ * A pure read, and admin-only for the same reason the scaffold is: the whole
606
+ * schema is what comes back.
607
+ */
608
+ exportWorkspaceModel(opts?: {
609
+ tables?: string[];
610
+ }): Promise<WorkspaceModelExport>;
325
611
  /** Renames (and re-settings) the CURRENT workspace — the endpoint reads the
326
612
  * target from the request's workspace, never a path id. */
327
613
  updateWorkspace(body: {
@@ -455,7 +741,7 @@ export declare class LoticsClient {
455
741
  }>;
456
742
  /**
457
743
  * Take a starter off the shelf, or `undo` to put it back (backs `opctl
458
- * starter unpublish`). It hides from non-owning orgs and can no longer be
744
+ * library unpublish`). It hides from non-owning orgs and can no longer be
459
745
  * copied; copies already made are unaffected — they never linked back.
460
746
  * Owner-org admin-only.
461
747
  */
@@ -499,6 +785,14 @@ export declare class LoticsClient {
499
785
  latest_version: number;
500
786
  is_official: boolean;
501
787
  owned_by_caller: boolean;
788
+ /** The apps a copy creates — the same contents the public shelf names. */
789
+ apps: Array<{
790
+ alias: string;
791
+ name: string;
792
+ description?: string;
793
+ }>;
794
+ /** How many tables the copy creates. */
795
+ table_count: number;
502
796
  }>>;
503
797
  /**
504
798
  * Copy a starter into the current workspace.
@@ -513,6 +807,20 @@ export declare class LoticsClient {
513
807
  version?: number;
514
808
  no_sample_data?: boolean;
515
809
  adopt?: boolean;
810
+ /**
811
+ * The labels THIS workspace calls the package's entities and fields.
812
+ *
813
+ * Scaffold adopts by label, so binding renames the contract's labels
814
+ * before it runs and the copy lands on the tables the caller already has.
815
+ * A bound field whose type differs from the one the contract declares is
816
+ * refused — the type is the contract, only the naming moves. Structural
817
+ * for the same reason the rest of this file's wire shapes are;
818
+ * `packageBindSchema` in `@lotics/shared` is the definition.
819
+ */
820
+ bind?: Record<string, {
821
+ label?: string;
822
+ fields?: Record<string, string>;
823
+ }>;
516
824
  }): Promise<{
517
825
  /** Each app's deploy, in contract order. `error` set and `deployed` null when one did not land. */
518
826
  apps: Array<{
@@ -541,6 +849,21 @@ export declare class LoticsClient {
541
849
  starter_id: string;
542
850
  latest_version: number;
543
851
  }>;
852
+ /**
853
+ * Apply the latest version of the package this app was copied from.
854
+ *
855
+ * The other provenance direction from `getAppOriginStarter`, and the only one
856
+ * that writes: that one answers which package this app PUBLISHED, this one
857
+ * reads what a copy recorded and re-derives the app against a later release.
858
+ *
859
+ * The offer is partial by design and the result says how partial: schema is
860
+ * additive (`created_fields`, and a `dropped_fields` the new version stopped
861
+ * declaring is left standing), and an artifact the owner has edited is kept
862
+ * and named (`skipped_workflows` / `skipped_agents`). An app with no
863
+ * provenance is a 400, and one already on the latest release a 409 — both
864
+ * refusals, both before any write. Admin-only.
865
+ */
866
+ upgradeApp(app_id: string): Promise<AppUpgradeResult>;
544
867
  /**
545
868
  * Capture live records from this workspace as a starter's sample data.
546
869
  *
@@ -587,7 +910,7 @@ export declare class LoticsClient {
587
910
  created_at: string;
588
911
  updated_at: string;
589
912
  }>;
590
- /** Version history newest-first (no contract payloads) — backs `opctl starter show`. Admin-only. */
913
+ /** Version history newest-first (no contract payloads) — backs `opctl library show`. Admin-only. */
591
914
  listStarterVersions(starter_id: string): Promise<{
592
915
  versions: Array<{
593
916
  version: number;
@@ -622,7 +945,7 @@ export declare class LoticsClient {
622
945
  }>>;
623
946
  /**
624
947
  * Preview publishing a set of this workspace's apps as one starter version —
625
- * the GET behind `opctl starter publish` (no `--yes`). The server runs the
948
+ * the GET behind `opctl library publish` (no `--yes`). The server runs the
626
949
  * same extraction the publish runs and reports which starter it would
627
950
  * release into (null: it would mint one), the next version, the aliases a
628
951
  * first publish can still rename, the diff against the current version, the
@@ -139,27 +139,94 @@ function getMimeType(filename) {
139
139
  return MIME_MAP[ext] ?? "application/octet-stream";
140
140
  }
141
141
  var MULTIPART_THRESHOLD_BYTES = 8 * 1024 * 1024;
142
+ var LoticsRequestError = class extends Error {
143
+ constructor(message, status, body) {
144
+ super(message);
145
+ this.status = status;
146
+ this.body = body;
147
+ this.name = "LoticsRequestError";
148
+ }
149
+ status;
150
+ body;
151
+ };
142
152
  var API_BASE_URL = process.env.LOTICS_API_URL ?? "https://api.lotics.ai";
143
- var WEB_APP_URL = "https://lotics.ai";
153
+ var WEB_APP_URL = process.env.LOTICS_WEB_URL ?? "https://lotics.ai";
144
154
  async function fetchOfficialStarters() {
155
+ const read = await getPublicJson(`${API_BASE_URL}/v1/starters/official`, "list the packages");
156
+ if (!read.ok) {
157
+ throw new Error(
158
+ `Lotics answered ${read.status} listing the packages. If this keeps happening, browse ${WEB_APP_URL}/docs/cli.`
159
+ );
160
+ }
161
+ return read.body;
162
+ }
163
+ async function getPublicJson(url, what) {
145
164
  let response;
146
165
  try {
147
- response = await fetch(`${API_BASE_URL}/v1/starters/official`, {
148
- signal: AbortSignal.timeout(SHELF_FETCH_TIMEOUT_MS)
166
+ response = await fetch(url, { signal: AbortSignal.timeout(PUBLIC_FETCH_TIMEOUT_MS) });
167
+ } catch (error) {
168
+ throw new Error(
169
+ `Could not reach Lotics to ${what} (${error instanceof Error ? error.message : String(error)}). Check your connection, or browse ${WEB_APP_URL}/docs/cli.`
170
+ );
171
+ }
172
+ if (!response.ok) return { ok: false, status: response.status };
173
+ return { ok: true, body: await response.json() };
174
+ }
175
+ async function readOfficialStarter(starter_id) {
176
+ const read = await getPublicJson(
177
+ `${API_BASE_URL}/v1/starters/official/${encodeURIComponent(starter_id)}`,
178
+ `read ${starter_id}`
179
+ );
180
+ if (!read.ok) return read;
181
+ return { ok: true, package: read.body };
182
+ }
183
+ async function getOfficialStarter(starter_id) {
184
+ const read = await readOfficialStarter(starter_id);
185
+ if (!read.ok) {
186
+ throw new Error(
187
+ `Lotics answered ${read.status} reading ${starter_id}. Only packages Lotics publishes can be read without an account.`
188
+ );
189
+ }
190
+ return read.package;
191
+ }
192
+ var PUBLIC_FETCH_TIMEOUT_MS = 1e4;
193
+ async function startCliLogin(email) {
194
+ let response;
195
+ try {
196
+ response = await fetch(`${API_BASE_URL}/v1/cli/login_requests`, {
197
+ method: "POST",
198
+ headers: { "Content-Type": "application/json" },
199
+ body: JSON.stringify({ email }),
200
+ signal: AbortSignal.timeout(PUBLIC_FETCH_TIMEOUT_MS)
149
201
  });
150
202
  } catch (error) {
151
203
  throw new Error(
152
- `Could not reach Lotics to list the starters (${error instanceof Error ? error.message : String(error)}). Check your connection, or browse ${WEB_APP_URL}/docs/cli.`
204
+ `Could not reach Lotics to send a sign-in link (${error instanceof Error ? error.message : String(error)}). Check your connection, or sign in at ${WEB_APP_URL}.`
153
205
  );
154
206
  }
155
207
  if (!response.ok) {
208
+ throw new Error(`Lotics answered ${response.status} asking for a sign-in link.`);
209
+ }
210
+ return await response.json();
211
+ }
212
+ async function pollCliLogin(request_id, secret) {
213
+ const url = `${API_BASE_URL}/v1/cli/login_requests/${encodeURIComponent(request_id)}`;
214
+ let response;
215
+ try {
216
+ response = await fetch(url, {
217
+ headers: { "x-lotics-login-secret": secret },
218
+ signal: AbortSignal.timeout(PUBLIC_FETCH_TIMEOUT_MS)
219
+ });
220
+ } catch (error) {
156
221
  throw new Error(
157
- `Lotics answered ${response.status} listing the starters. If this keeps happening, browse ${WEB_APP_URL}/docs/cli.`
222
+ `Could not reach Lotics while waiting for the sign-in (${error instanceof Error ? error.message : String(error)}). Run "lotics auth login" again once the connection is back.`
158
223
  );
159
224
  }
225
+ if (!response.ok) {
226
+ throw new Error(`Lotics answered ${response.status} waiting for the sign-in.`);
227
+ }
160
228
  return await response.json();
161
229
  }
162
- var SHELF_FETCH_TIMEOUT_MS = 1e4;
163
230
  function newRequestId() {
164
231
  return crypto.randomUUID().replace(/-/g, "").slice(0, 12);
165
232
  }
@@ -192,17 +259,18 @@ var LoticsClient = class {
192
259
  */
193
260
  async throwResponseError(response, requestId) {
194
261
  const text = await response.text();
195
- let message;
262
+ let body = {};
196
263
  try {
197
264
  const json = JSON.parse(text);
198
- message = json.message ?? text;
265
+ if (typeof json === "object" && json !== null && !Array.isArray(json)) body = { ...json };
199
266
  } catch {
200
- message = text;
267
+ body = {};
201
268
  }
269
+ const message = typeof body.message === "string" ? body.message : text;
202
270
  const trace = requestId === void 0 ? "" : `
203
271
 
204
272
  Request id: ${requestId} \u2014 quote this to Lotics support.`;
205
- throw new Error(`${response.status}: ${message}${trace}`);
273
+ throw new LoticsRequestError(`${response.status}: ${message}${trace}`, response.status, body);
206
274
  }
207
275
  /**
208
276
  * The backend's `log()` middleware registers `user-agent`,
@@ -276,6 +344,33 @@ var LoticsClient = class {
276
344
  async createWorkspace(body) {
277
345
  return this.request("POST", "/v1/workspaces", body);
278
346
  }
347
+ /**
348
+ * Apply a workspace MODEL to the current workspace — the from-scratch half of
349
+ * the starter pipeline, on a contract nobody published.
350
+ *
351
+ * Additive: with `adopt`, an entity whose label already names a table here
352
+ * binds to it and gains the fields, options and views it is missing; without
353
+ * `adopt`, that label is refused. Nothing is ever modified or deleted, and
354
+ * rows land only where every bound table is empty. Admin-only.
355
+ */
356
+ async scaffoldWorkspace(body) {
357
+ return this.request("POST", "/v1/workspaces/scaffold", body);
358
+ }
359
+ /**
360
+ * Read this workspace's schema back as a model — the tables it has (or only
361
+ * the ones named), their fields, options and views, plus its roles.
362
+ *
363
+ * A pure read, and admin-only for the same reason the scaffold is: the whole
364
+ * schema is what comes back.
365
+ */
366
+ async exportWorkspaceModel(opts = {}) {
367
+ const params = new URLSearchParams();
368
+ if (opts.tables !== void 0 && opts.tables.length > 0) {
369
+ params.set("tables", opts.tables.join(","));
370
+ }
371
+ const qs = params.toString();
372
+ return this.request("GET", `/v1/workspaces/model${qs ? `?${qs}` : ""}`);
373
+ }
279
374
  /** Renames (and re-settings) the CURRENT workspace — the endpoint reads the
280
375
  * target from the request's workspace, never a path id. */
281
376
  async updateWorkspace(body) {
@@ -355,7 +450,7 @@ var LoticsClient = class {
355
450
  }
356
451
  /**
357
452
  * Take a starter off the shelf, or `undo` to put it back (backs `opctl
358
- * starter unpublish`). It hides from non-owning orgs and can no longer be
453
+ * library unpublish`). It hides from non-owning orgs and can no longer be
359
454
  * copied; copies already made are unaffected — they never linked back.
360
455
  * Owner-org admin-only.
361
456
  */
@@ -408,6 +503,23 @@ var LoticsClient = class {
408
503
  async getAppOriginStarter(app_id) {
409
504
  return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/origin-starter`);
410
505
  }
506
+ /**
507
+ * Apply the latest version of the package this app was copied from.
508
+ *
509
+ * The other provenance direction from `getAppOriginStarter`, and the only one
510
+ * that writes: that one answers which package this app PUBLISHED, this one
511
+ * reads what a copy recorded and re-derives the app against a later release.
512
+ *
513
+ * The offer is partial by design and the result says how partial: schema is
514
+ * additive (`created_fields`, and a `dropped_fields` the new version stopped
515
+ * declaring is left standing), and an artifact the owner has edited is kept
516
+ * and named (`skipped_workflows` / `skipped_agents`). An app with no
517
+ * provenance is a 400, and one already on the latest release a 409 — both
518
+ * refusals, both before any write. Admin-only.
519
+ */
520
+ async upgradeApp(app_id) {
521
+ return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/upgrade`, {});
522
+ }
411
523
  /**
412
524
  * Capture live records from this workspace as a starter's sample data.
413
525
  *
@@ -436,7 +548,7 @@ var LoticsClient = class {
436
548
  async getStarter(starter_id) {
437
549
  return this.request("GET", `/v1/starters/${encodeURIComponent(starter_id)}`);
438
550
  }
439
- /** Version history newest-first (no contract payloads) — backs `opctl starter show`. Admin-only. */
551
+ /** Version history newest-first (no contract payloads) — backs `opctl library show`. Admin-only. */
440
552
  async listStarterVersions(starter_id) {
441
553
  return this.request("GET", `/v1/starters/${encodeURIComponent(starter_id)}/versions`);
442
554
  }
@@ -464,7 +576,7 @@ var LoticsClient = class {
464
576
  // --- Starter publishing (the authoring verbs; copying is `instantiateStarter`) ---
465
577
  /**
466
578
  * Preview publishing a set of this workspace's apps as one starter version —
467
- * the GET behind `opctl starter publish` (no `--yes`). The server runs the
579
+ * the GET behind `opctl library publish` (no `--yes`). The server runs the
468
580
  * same extraction the publish runs and reports which starter it would
469
581
  * release into (null: it would mint one), the next version, the aliases a
470
582
  * first publish can still rename, the diff against the current version, the
@@ -474,6 +586,7 @@ var LoticsClient = class {
474
586
  async previewStarterPublish(opts) {
475
587
  const params = new URLSearchParams();
476
588
  params.set("app_ids", opts.app_ids.join(","));
589
+ if (opts.starter_id !== void 0) params.set("starter_id", opts.starter_id);
477
590
  if (opts.knowledge_doc_ids !== void 0) params.set("knowledge_doc_ids", opts.knowledge_doc_ids.join(","));
478
591
  if (opts.renames !== void 0 && opts.renames.length > 0) params.set("renames", JSON.stringify(opts.renames));
479
592
  if (opts.name !== void 0) params.set("name", opts.name);
@@ -1050,6 +1163,12 @@ var LoticsClient = class {
1050
1163
  export {
1051
1164
  API_BASE_URL,
1052
1165
  LoticsClient,
1166
+ LoticsRequestError,
1053
1167
  WEB_APP_URL,
1054
- fetchOfficialStarters
1168
+ fetchOfficialStarters,
1169
+ getOfficialStarter,
1170
+ getPublicJson,
1171
+ pollCliLogin,
1172
+ readOfficialStarter,
1173
+ startCliLogin
1055
1174
  };
@@ -237,7 +237,7 @@ actually carries, not what the source says it should.
237
237
  ```
238
238
  npm run typecheck && npm run lint && npm test
239
239
  lotics app check # every pre-flight a deploy runs, without building or shipping,
240
- # plus the portability gate a starter publish applies
240
+ # plus the portability gate a library publish applies
241
241
  lotics app deploy -m "<what changed + why>"
242
242
  ```
243
243