@lotics/cli 0.188.0 → 0.189.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,80 @@ 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 request the API refused. The message carries the status and the server's
311
+ * sentence, which is what reaches a person; `status` and `body` are for the
312
+ * few callers that branch on a refusal — a 409 whose `body.reason` says the
313
+ * run had nothing to do, rather than that it failed.
314
+ */
315
+ export declare class LoticsRequestError extends Error {
316
+ readonly status: number;
317
+ readonly body: Record<string, unknown>;
318
+ constructor(message: string, status: number, body: Record<string, unknown>);
319
+ }
238
320
  export declare const API_BASE_URL: string;
239
321
  /**
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.
322
+ * The website: where a person goes when the CLI cannot finish the job, and where
323
+ * the presets are SERVED FROM. Held beside the API base so the pair is read
324
+ * together, named once rather than inlined at each message that points
325
+ * somewhere, and overridable for the same reason the API base is — a run against
326
+ * a local site must read that site's presets, not production's.
244
327
  */
245
- export declare const WEB_APP_URL = "https://lotics.ai";
246
- /** One starter on the public shelf — what a chooser decides with, nothing else. */
328
+ export declare const WEB_APP_URL: string;
329
+ /** One package on the public shelf — what a chooser decides with, nothing else. */
247
330
  export interface OfficialStarter {
248
331
  id: string;
249
332
  name: string;
250
333
  description: string | null;
251
334
  icon: string | null;
252
335
  latest_version: number;
336
+ /**
337
+ * The apps the copy creates. A name and a sentence leave the chooser
338
+ * guessing; these are what someone's own words get matched against when
339
+ * deciding between a package and a model.
340
+ */
341
+ apps: Array<{
342
+ alias: string;
343
+ name: string;
344
+ description?: string;
345
+ }>;
346
+ /** How many tables the copy creates — the other half of "what is in this?". */
347
+ table_count: number;
253
348
  }
254
349
  /**
255
350
  * The starters Lotics publishes, fetched with no credential.
@@ -260,6 +355,152 @@ export interface OfficialStarter {
260
355
  * exists, so needing one to ask means signing up to find out the answer was no.
261
356
  */
262
357
  export declare function fetchOfficialStarters(): Promise<OfficialStarter[]>;
358
+ /**
359
+ * One keyless GET of JSON, bounded — the shape every read that predates an
360
+ * account takes.
361
+ *
362
+ * Bounded because these are the FIRST commands a new user runs and a hang with
363
+ * nothing on screen is the worst version of the failure; `config.ts` bounds its
364
+ * own unattended fetch for the same reason, and `docs/network_reliability.md` is
365
+ * a log of connections from our users' networks degrading rather than refusing,
366
+ * which is the shape that hangs.
367
+ *
368
+ * A transport failure THROWS and a status is RETURNED, because only the first is
369
+ * "check your connection": a status means the server replied and the network is
370
+ * demonstrably fine, and a caller that answered both the same way would tell an
371
+ * owner their package is missing every time the link drops.
372
+ */
373
+ export declare function getPublicJson(url: string, what: string): Promise<{
374
+ ok: true;
375
+ body: unknown;
376
+ } | {
377
+ ok: false;
378
+ status: number;
379
+ }>;
380
+ /**
381
+ * One official package READ WHOLE, with no credential — or the status that says
382
+ * the shelf does not serve it.
383
+ *
384
+ * The shelf row says what a package is; this says what is in it — entities with
385
+ * their fields, the roles, each template's metadata and each app's name. Never a
386
+ * workflow body: this is a read for deciding and for writing a model from, not a
387
+ * copy.
388
+ *
389
+ * Unauthenticated for the same reason the shelf is: a preset is READ rather than
390
+ * instantiated, and the whole point of reading one is that it happens before an
391
+ * account exists. Only official packages are served — the `is_official` filter,
392
+ * exactly as on the shelf — so a caller's OWN package answers a status here and
393
+ * is read through the authenticated pair instead. A transport failure still
394
+ * throws: "unreachable" is not "not on the shelf", and answering both the same
395
+ * way would tell an owner their package is missing every time the link drops.
396
+ */
397
+ export declare function readOfficialStarter(starter_id: string): Promise<{
398
+ ok: true;
399
+ package: OfficialStarterRead;
400
+ } | {
401
+ ok: false;
402
+ status: number;
403
+ }>;
404
+ /** The same read for a caller with nothing to fall back to — a refusal is the end. */
405
+ export declare function getOfficialStarter(starter_id: string): Promise<OfficialStarterRead>;
406
+ /** A column as the public read shows it: enough to write a model field from. */
407
+ export interface OfficialStarterField {
408
+ alias: string;
409
+ label: string;
410
+ type: string;
411
+ }
412
+ /** A table as the public read shows it: its columns. */
413
+ export interface OfficialStarterEntity {
414
+ alias: string;
415
+ label: string;
416
+ description?: string;
417
+ fields: OfficialStarterField[];
418
+ }
419
+ /**
420
+ * What the public read of one package carries.
421
+ *
422
+ * Every namespace is declared structurally, NARROWED to what a reader turning
423
+ * this into a model needs — fields carry their type, apps only their name — the
424
+ * same trade `StarterVersionContract` makes: a published `.d.ts` cannot resolve
425
+ * `@lotics/shared`. `packageContractSchema` is the definition, and the CLI
426
+ * parses a preset's block through `modelPresetSchema` before resolving a `from`
427
+ * file against it.
428
+ */
429
+ export interface OfficialStarterRead {
430
+ id: string;
431
+ name: string;
432
+ description: string | null;
433
+ latest_version: number;
434
+ contract: {
435
+ entities: OfficialStarterEntity[];
436
+ roles: Array<{
437
+ alias: string;
438
+ label: string;
439
+ }>;
440
+ templates: Array<{
441
+ alias: string;
442
+ label: string;
443
+ type: string;
444
+ }>;
445
+ /** The apps a copy creates. */
446
+ apps: Array<{
447
+ alias: string;
448
+ name: string;
449
+ description?: string;
450
+ }>;
451
+ /** Sample rows per entity alias — how many, never the rows. */
452
+ fixtures: Record<string, {
453
+ row_count: number;
454
+ }>;
455
+ knowledge: Record<string, {
456
+ name: string;
457
+ description: string;
458
+ }>;
459
+ knowledge_expects: string[];
460
+ };
461
+ }
462
+ /**
463
+ * What a terminal holds while it waits to be let in: the handle the server knows
464
+ * the request by, the `secret` that proves this is the same terminal that asked,
465
+ * and the `code` the person matches against the confirm page before pressing it.
466
+ *
467
+ * `secret` is a credential and is never printed — the `code` is what a person
468
+ * reads, and it proves nothing on its own.
469
+ */
470
+ export interface CliLoginRequest {
471
+ request_id: string;
472
+ secret: string;
473
+ code: string;
474
+ expires_at: string;
475
+ }
476
+ /** Where a login request stands. `approved` carries the key, and only once. */
477
+ export type CliLoginState = {
478
+ status: "pending";
479
+ } | {
480
+ status: "expired";
481
+ } | {
482
+ status: "claimed";
483
+ } | {
484
+ status: "approved";
485
+ api_key: string;
486
+ organization_id: string;
487
+ organization_name: string;
488
+ workspace_id: string;
489
+ };
490
+ /**
491
+ * Ask Lotics to mail a sign-in link, and read back whether it was confirmed.
492
+ *
493
+ * Plain functions rather than `LoticsClient` methods for the same reason
494
+ * `fetchOfficialStarters` is one: there is no key to build a client around, and
495
+ * that is the whole point — this is the pair a terminal holding NO credential
496
+ * uses to obtain one. Sending an `Authorization` header would make the endpoint
497
+ * answerable only to callers who no longer need it.
498
+ *
499
+ * The same answer whether or not the address has an account: it would
500
+ * otherwise tell any stranger which emails are registered here.
501
+ */
502
+ export declare function startCliLogin(email: string): Promise<CliLoginRequest>;
503
+ export declare function pollCliLogin(request_id: string, secret: string): Promise<CliLoginState>;
263
504
  export declare class LoticsClient {
264
505
  private apiKey;
265
506
  private workspaceId;
@@ -322,6 +563,16 @@ export declare class LoticsClient {
322
563
  name: string;
323
564
  timezone?: string;
324
565
  }): Promise<WorkspaceInfo>;
566
+ /**
567
+ * Apply a workspace MODEL to the current workspace — the from-scratch half of
568
+ * the starter pipeline, on a contract nobody published.
569
+ *
570
+ * Additive: with `adopt`, an entity whose label already names a table here
571
+ * binds to it and gains the fields, options and views it is missing; without
572
+ * `adopt`, that label is refused. Nothing is ever modified or deleted, and
573
+ * rows land only where every bound table is empty. Admin-only.
574
+ */
575
+ scaffoldWorkspace(body: ScaffoldWorkspaceRequest): Promise<ScaffoldWorkspaceResult>;
325
576
  /** Renames (and re-settings) the CURRENT workspace — the endpoint reads the
326
577
  * target from the request's workspace, never a path id. */
327
578
  updateWorkspace(body: {
@@ -455,7 +706,7 @@ export declare class LoticsClient {
455
706
  }>;
456
707
  /**
457
708
  * 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
709
+ * library unpublish`). It hides from non-owning orgs and can no longer be
459
710
  * copied; copies already made are unaffected — they never linked back.
460
711
  * Owner-org admin-only.
461
712
  */
@@ -499,6 +750,14 @@ export declare class LoticsClient {
499
750
  latest_version: number;
500
751
  is_official: boolean;
501
752
  owned_by_caller: boolean;
753
+ /** The apps a copy creates — the same contents the public shelf names. */
754
+ apps: Array<{
755
+ alias: string;
756
+ name: string;
757
+ description?: string;
758
+ }>;
759
+ /** How many tables the copy creates. */
760
+ table_count: number;
502
761
  }>>;
503
762
  /**
504
763
  * Copy a starter into the current workspace.
@@ -513,6 +772,20 @@ export declare class LoticsClient {
513
772
  version?: number;
514
773
  no_sample_data?: boolean;
515
774
  adopt?: boolean;
775
+ /**
776
+ * The labels THIS workspace calls the package's entities and fields.
777
+ *
778
+ * Scaffold adopts by label, so binding renames the contract's labels
779
+ * before it runs and the copy lands on the tables the caller already has.
780
+ * A bound field whose type differs from the one the contract declares is
781
+ * refused — the type is the contract, only the naming moves. Structural
782
+ * for the same reason the rest of this file's wire shapes are;
783
+ * `packageBindSchema` in `@lotics/shared` is the definition.
784
+ */
785
+ bind?: Record<string, {
786
+ label?: string;
787
+ fields?: Record<string, string>;
788
+ }>;
516
789
  }): Promise<{
517
790
  /** Each app's deploy, in contract order. `error` set and `deployed` null when one did not land. */
518
791
  apps: Array<{
@@ -541,6 +814,21 @@ export declare class LoticsClient {
541
814
  starter_id: string;
542
815
  latest_version: number;
543
816
  }>;
817
+ /**
818
+ * Apply the latest version of the package this app was copied from.
819
+ *
820
+ * The other provenance direction from `getAppOriginStarter`, and the only one
821
+ * that writes: that one answers which package this app PUBLISHED, this one
822
+ * reads what a copy recorded and re-derives the app against a later release.
823
+ *
824
+ * The offer is partial by design and the result says how partial: schema is
825
+ * additive (`created_fields`, and a `dropped_fields` the new version stopped
826
+ * declaring is left standing), and an artifact the owner has edited is kept
827
+ * and named (`skipped_workflows` / `skipped_agents`). An app with no
828
+ * provenance is a 400, and one already on the latest release a 409 — both
829
+ * refusals, both before any write. Admin-only.
830
+ */
831
+ upgradeApp(app_id: string): Promise<AppUpgradeResult>;
544
832
  /**
545
833
  * Capture live records from this workspace as a starter's sample data.
546
834
  *
@@ -587,7 +875,7 @@ export declare class LoticsClient {
587
875
  created_at: string;
588
876
  updated_at: string;
589
877
  }>;
590
- /** Version history newest-first (no contract payloads) — backs `opctl starter show`. Admin-only. */
878
+ /** Version history newest-first (no contract payloads) — backs `opctl library show`. Admin-only. */
591
879
  listStarterVersions(starter_id: string): Promise<{
592
880
  versions: Array<{
593
881
  version: number;
@@ -622,7 +910,7 @@ export declare class LoticsClient {
622
910
  }>>;
623
911
  /**
624
912
  * 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
913
+ * the GET behind `opctl library publish` (no `--yes`). The server runs the
626
914
  * same extraction the publish runs and reports which starter it would
627
915
  * release into (null: it would mint one), the next version, the aliases a
628
916
  * 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) {
164
+ let response;
165
+ try {
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) {
145
194
  let response;
146
195
  try {
147
- response = await fetch(`${API_BASE_URL}/v1/starters/official`, {
148
- signal: AbortSignal.timeout(SHELF_FETCH_TIMEOUT_MS)
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,18 @@ 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
+ }
279
359
  /** Renames (and re-settings) the CURRENT workspace — the endpoint reads the
280
360
  * target from the request's workspace, never a path id. */
281
361
  async updateWorkspace(body) {
@@ -355,7 +435,7 @@ var LoticsClient = class {
355
435
  }
356
436
  /**
357
437
  * 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
438
+ * library unpublish`). It hides from non-owning orgs and can no longer be
359
439
  * copied; copies already made are unaffected — they never linked back.
360
440
  * Owner-org admin-only.
361
441
  */
@@ -408,6 +488,23 @@ var LoticsClient = class {
408
488
  async getAppOriginStarter(app_id) {
409
489
  return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/origin-starter`);
410
490
  }
491
+ /**
492
+ * Apply the latest version of the package this app was copied from.
493
+ *
494
+ * The other provenance direction from `getAppOriginStarter`, and the only one
495
+ * that writes: that one answers which package this app PUBLISHED, this one
496
+ * reads what a copy recorded and re-derives the app against a later release.
497
+ *
498
+ * The offer is partial by design and the result says how partial: schema is
499
+ * additive (`created_fields`, and a `dropped_fields` the new version stopped
500
+ * declaring is left standing), and an artifact the owner has edited is kept
501
+ * and named (`skipped_workflows` / `skipped_agents`). An app with no
502
+ * provenance is a 400, and one already on the latest release a 409 — both
503
+ * refusals, both before any write. Admin-only.
504
+ */
505
+ async upgradeApp(app_id) {
506
+ return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/upgrade`, {});
507
+ }
411
508
  /**
412
509
  * Capture live records from this workspace as a starter's sample data.
413
510
  *
@@ -436,7 +533,7 @@ var LoticsClient = class {
436
533
  async getStarter(starter_id) {
437
534
  return this.request("GET", `/v1/starters/${encodeURIComponent(starter_id)}`);
438
535
  }
439
- /** Version history newest-first (no contract payloads) — backs `opctl starter show`. Admin-only. */
536
+ /** Version history newest-first (no contract payloads) — backs `opctl library show`. Admin-only. */
440
537
  async listStarterVersions(starter_id) {
441
538
  return this.request("GET", `/v1/starters/${encodeURIComponent(starter_id)}/versions`);
442
539
  }
@@ -464,7 +561,7 @@ var LoticsClient = class {
464
561
  // --- Starter publishing (the authoring verbs; copying is `instantiateStarter`) ---
465
562
  /**
466
563
  * 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
564
+ * the GET behind `opctl library publish` (no `--yes`). The server runs the
468
565
  * same extraction the publish runs and reports which starter it would
469
566
  * release into (null: it would mint one), the next version, the aliases a
470
567
  * first publish can still rename, the diff against the current version, the
@@ -474,6 +571,7 @@ var LoticsClient = class {
474
571
  async previewStarterPublish(opts) {
475
572
  const params = new URLSearchParams();
476
573
  params.set("app_ids", opts.app_ids.join(","));
574
+ if (opts.starter_id !== void 0) params.set("starter_id", opts.starter_id);
477
575
  if (opts.knowledge_doc_ids !== void 0) params.set("knowledge_doc_ids", opts.knowledge_doc_ids.join(","));
478
576
  if (opts.renames !== void 0 && opts.renames.length > 0) params.set("renames", JSON.stringify(opts.renames));
479
577
  if (opts.name !== void 0) params.set("name", opts.name);
@@ -1050,6 +1148,12 @@ var LoticsClient = class {
1050
1148
  export {
1051
1149
  API_BASE_URL,
1052
1150
  LoticsClient,
1151
+ LoticsRequestError,
1053
1152
  WEB_APP_URL,
1054
- fetchOfficialStarters
1153
+ fetchOfficialStarters,
1154
+ getOfficialStarter,
1155
+ getPublicJson,
1156
+ pollCliLogin,
1157
+ readOfficialStarter,
1158
+ startCliLogin
1055
1159
  };
@@ -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