@versionone/agility-sdk 1.0.0-alpha.3 → 1.0.0-alpha.4

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/CHANGELOG.md ADDED
@@ -0,0 +1,135 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0-alpha.4 — what the three adoptions asked for
4
+
5
+ ### Added
6
+
7
+ - **Two narrow rest-1 paths** (agility-rooms): `client.data.operate(oid,
8
+ op)` runs an operation for its result — api/asset performs a Copy but
9
+ reports only the source; rest-1 answers the copy's OID — and
10
+ `client.copy(oid)` is that for Copy. `client.data.template(type, {
11
+ ctx })` reads `New/<Type>?ctx=`: what the type's initializer fills in
12
+ for a context (a Timebox's next name and dates, schedule, state and
13
+ owner; a Story's carry-over from a Request), translated like a row,
14
+ without an `_oid`. Neither sends attribute values: rest-1 still carries
15
+ no update path.
16
+ - **`client.fields`**: `describe(type, scope)` joins what a form needs —
17
+ the attributes the server requires on a create (meta), the ones the
18
+ product shows for the type in that scope (`api/visibleFields`, custom
19
+ fields included once configured), and the scope's scheme's important
20
+ fields with whether they are enforced at close; `visible(types,
21
+ scopes)` and `visibleByAsset(oid)` are the endpoints alone. New wire
22
+ module `fields`.
23
+ - **`client.urls.asset` takes an asset Number** as well as an OID
24
+ (`assetdetail.v1?Number=S-01001`): the readable form a CLI prints and a
25
+ chat message shows (gh-v1, slack bot).
26
+ - **`Outcome.requestId`**: the server's id for a batch, worth logging
27
+ (slack bot). Null for an outcome merged from several sends.
28
+ - `outcomeOf` tolerating a partial envelope is now a stated contract
29
+ with a test: consumers' fakes may send only the fields they need.
30
+
31
+ ### Fixed
32
+
33
+ - `parseViolation`: `tag` is null for the token spelling
34
+ (`Violation'Invalid'Epic.Scope`); it was the token, which is not a loc-2
35
+ tag (gh-v1).
36
+ - `CHANGELOG.md` ships in the npm package.
37
+ - Doc notes: `kind: "usage"` means nothing was sent (status 0), so an
38
+ error formatter should not blame the server; `client.query`'s resend
39
+ after a failure needs `retries` above its default of 0; the deprecation
40
+ notes on `queryV1` and `assets.query` say where each endpoint is.
41
+
42
+ ### Migration note (from alpha.3's read path)
43
+
44
+ - A rejected query through `client.query` is an `ApiError` with kind
45
+ `invalid` and code `QUERY_REJECTED`; `message` holds the server's
46
+ sentences and `details` its whole error document. A formatter that
47
+ printed `details` for api/asset's `CommandFailureError` now prints a JSON
48
+ document; print `message`.
49
+
50
+ Alpha releases may break between versions. Every breaking change is listed
51
+ here with the one-line migration a consumer applies. A renamed export keeps
52
+ its old name as a `@deprecated` alias for one alpha and is removed in the
53
+ next; `1.0.0` follows agility-rooms' adoption, and semver after that.
54
+
55
+ ## 1.0.0-alpha.3 — the breaking batch (plan 005, step 2)
56
+
57
+ ### Breaking
58
+
59
+ - **`client.assets.send` answers an `Outcome`, not the api/asset envelope.**
60
+ `created`, `modified`, `operated` (momentless OIDs), `failures` (by index,
61
+ each with its parsed `violation`), `queryResults` (one slot per query
62
+ command, `null` where it failed), `durationSeconds`. Migration:
63
+ `res.assetsCreated.oidTokens` → `res.created`;
64
+ `res.commandFailures.count` → `res.failures.length`;
65
+ `res.queryResult.results[i]` → `res.queryResults[i]`. The wire module
66
+ `assetApi.send` still returns the envelope for a passthrough.
67
+ `CommandFailureError` gains `outcome` beside `response`.
68
+ - **Every SDK error is an `ApiError` with a `kind`** (`auth`, `forbidden`,
69
+ `notFound`, `invalid`, `transient`, `aborted`, `commandFailure`,
70
+ `protocol`, `usage`, `unknown`). Mistakes caught before any request
71
+ (an unpaged read, an empty batch, a bad OID token or asset number) throw
72
+ `UsageError`, not `Error`/`TypeError`. `UnguardableError` is a
73
+ `UsageError`; `GuardedPassError`, `BatchSendError`, `BatchRecordError`
74
+ and `AttachmentUploadError` are `WrappedError`s carrying the cause's
75
+ kind. Migration: a `catch` on `TypeError` becomes `instanceof
76
+ UsageError`; a branch on `status === 401` can become `kind === "auth"`.
77
+ - **rest-1 reads take a query's spelling**: `client.data.list(type, {
78
+ select, filter, … })`. `sel` and `where` still work this alpha
79
+ (deprecated) and go in the next.
80
+
81
+ ### Deprecated (removed in the next alpha)
82
+
83
+ - `client.queryV1.query` / `.queries` → `client.query` / `client.queries`.
84
+ - `client.assets.query` / `.queries` → `client.query` / `client.queries`
85
+ (or `client.queryPage` for a total).
86
+ - `client.attachments.url`, `client.embeddedImages.url` →
87
+ `client.urls.attachment`, `client.urls.embeddedImage`.
88
+ - rest-1 `sel` / `where` → `select` / `filter`.
89
+
90
+ ### Added
91
+
92
+ - The generated model is refreshed from meta.v1 26.3.1.13 (it was a
93
+ 0.0.0.0 dev build): `Workflow`, `TeamWorkflow` and `WorkflowTransition`
94
+ arrive, with `Status.AllowedStatusTransitions`,
95
+ `Status.WorkflowStatusTransitions`, `Role.AllowedStatusTransitions` and
96
+ `Team.TeamWorkflows`. Nothing was removed or changed.
97
+ `scripts/download-metamodel.mjs` now strips an instance's own
98
+ definitions and reports them, so a refresh is reviewable.
99
+ - Reads on the client: `client.query`, `queries` (query.v1: read-only,
100
+ retried like a GET), `client.get(oid, select)`, `client.queryPage(q)`
101
+ (total and `more`, via api/asset), `client.pages(q, { limit })` and
102
+ `client.all(q, { limit })` (page by page up to a limit, default 1000),
103
+ `client.me()` (the member the credentials belong to).
104
+ - Typed queries: a literal `from` completes `select` from the type's
105
+ attributes, and rows are typed by what was selected (`row.Name` a
106
+ string, `row.Nmae` a compile error, `Status.Name` unknown, an aggregate
107
+ a number). An OID as `from` types by its type. `from` still takes an
108
+ alias or an unknown type; variables and `query<Story>` work as before.
109
+ - Custom list types (`Custom_<Name>`, the one custom asset type Agility
110
+ allows, a subtype of List) type as `CustomList`: List's attributes and
111
+ the type's own token as `AssetType`, as a `from` or as an OID. Custom
112
+ attributes (`Custom_…`) and the reciprocal side of a custom relation
113
+ (`ReciprocalOf_…`, which the server adds on the related type) are
114
+ accepted on every type as attribute values.
115
+ - `client.urls.asset(oid)`: the asset's page (`assetdetail.v1?oid=`).
116
+ - `parseViolation(message)` and `CommandFailureDetail.violation`: the
117
+ rule, attribute token and loc-2 tag of a validation failure.
118
+ - `kindOf(status, code)`, `ErrorKind`, `UsageError`, `WrappedError`.
119
+
120
+ ## 1.0.0-alpha.2
121
+
122
+ - Releases are staged with `npm stage publish` and approved by a
123
+ maintainer with 2FA.
124
+
125
+ ## 1.0.0-alpha.1
126
+
127
+ - Batch reverse uses the type's operations and the close cascade; the
128
+ guard sends a second command on one asset as a second pass; Moment is
129
+ never a guard stamp.
130
+
131
+ ## 1.0.0-alpha.0
132
+
133
+ - First publish: `AgilityApiClient`, wire modules for api/asset,
134
+ query.v1, rest-1 reads, meta.v1, loc-2.v1 and attachments; services for
135
+ schema, resolve, guard, batch, attachments and embedded images.
package/README.md CHANGED
@@ -49,8 +49,10 @@ Changes between alphas are in `CHANGELOG.md`, each with its migration line.
49
49
  - `resolve.numbers` / `resolve.ref` - asset numbers and OID tokens to assets, in one request
50
50
  - `attachments.list` / `page` / `info` / `get` / `add` / `put` / `remove` - files attached to an asset; `add` sends a small file inline and a large one in two steps, and a failed upload throws `AttachmentUploadError` naming the empty attachment it left
51
51
  - `embeddedImages.upload` / `normalize` - images in rich text; they cannot be deleted
52
- - `urls.asset` / `attachment` / `embeddedImage` - where a browser opens each
53
- - `data.list` / `get` / `history` - rest-1 reads, for deleted assets and history; `meta.*` - meta.v1 raw; `schema` - the translated, cached view; `localize` - loc-2 labels for tags (a violation's `tag` is one)
52
+ - `copy(oid)` - the Copy operation, answering the copy's OID (api/asset performs it but never says what it made); `data.operate(oid, op)` is the general form
53
+ - `fields.describe(type, scope)` - what a form needs: required attributes (meta), the ones the product shows there (`api/visibleFields`, custom fields included), the scheme's important fields and whether they are enforced at close
54
+ - `urls.asset(oidOrNumber)` / `attachment` / `embeddedImage` - where a browser opens each
55
+ - `data.list` / `get` / `history` / `template` - rest-1 reads: deleted assets, history, and a new asset's template for a context (`New/<Type>?ctx=`); `meta.*` - meta.v1 raw; `schema` - the translated, cached view; `localize` - loc-2 labels for tags (a violation's `tag` is one)
54
56
  - Errors: everything thrown is an `ApiError` with a `kind` (`auth`, `forbidden`, `notFound`, `invalid`, `transient`, `aborted`, `commandFailure`, `protocol`, `usage`, `unknown`). A mistake caught before any request is a `UsageError`
55
57
  - `TypeCache` - one instance's loaded type definitions, shared by many clients (`new AgilityApiClient(config, { types })`)
56
58
  - `createFetchTransport` - configurable HTTP transport: `credentials` for same-origin cookie auth, `onUnauthorized` to re-authenticate on a 401 (resends idempotent requests once), and a per-request `signal`; `client.withSignal(signal)` ties a whole client to one
@@ -75,7 +77,11 @@ No registry package yet. Pin a commit: `npm install github:versionone/VersionOne
75
77
  Versions are published to npmjs by `.github/workflows/publish.yml` when a
76
78
  tag `v<version>` is pushed; the tag must equal `package.json`'s version. A
77
79
  prerelease (`1.0.0-alpha.0`, `-beta.N`, `-rc.N`) goes out on its own
78
- dist-tag (`alpha`, `beta`, `rc`), so `latest` is never a prerelease.
80
+ dist-tag (`alpha`, `beta`, `rc`). Until `1.0.0`, a maintainer moves
81
+ `latest` to the newest alpha by hand (`npm dist-tag add
82
+ @versionone/agility-sdk@<version> latest`), so a bare `npm install` gets
83
+ the current API rather than the first publish; from `1.0.0` on, `latest`
84
+ is never a prerelease.
79
85
 
80
86
  ```bash
81
87
  npm version 1.0.0-alpha.2 --no-git-tag-version # bump, commit, merge
package/dist/index.cjs CHANGED
@@ -42,7 +42,9 @@ __export(index_exports, {
42
42
  attachment: () => attachment_exports,
43
43
  createFetchTransport: () => createFetchTransport,
44
44
  describeBatch: () => describe,
45
+ fields: () => fields_exports,
45
46
  guard: () => guard,
47
+ importantName: () => importantName,
46
48
  isAssetNumber: () => isAssetNumber,
47
49
  isOidToken: () => isOidToken,
48
50
  kindOf: () => kindOf,
@@ -461,7 +463,7 @@ function parseViolation(message) {
461
463
  const rule = parts[1], rest = parts.slice(2).filter((p) => p !== "");
462
464
  if (rest[0] === "AttributeDefinition" && rest[1] && rest[2]) return { rule, attribute: `${rest[2]}.${rest[1]}`, tag: rest.join("'") };
463
465
  const token = rest.length === 1 && /^[A-Za-z][A-Za-z0-9_]*\.[A-Za-z][A-Za-z0-9_]*$/.test(rest[0]) ? rest[0] : null;
464
- return { rule, attribute: token, tag: rest.length ? rest.join("'") : null };
466
+ return { rule, attribute: token, tag: !token && rest.length > 1 ? rest.join("'") : null };
465
467
  }
466
468
  function failuresOf(response) {
467
469
  return (response.commandFailures?.commands ?? []).map(({ error, ...echoed }) => ({
@@ -473,6 +475,7 @@ function failuresOf(response) {
473
475
  }
474
476
  function outcomeOf(response) {
475
477
  return {
478
+ requestId: typeof response.requestId === "string" && response.requestId ? response.requestId : null,
476
479
  created: (response.assetsCreated?.oidTokens ?? []).map(momentless),
477
480
  modified: (response.assetsModified?.oidTokens ?? []).map(momentless),
478
481
  operated: (response.assetsOperatedOn?.oidTokens ?? []).map(momentless),
@@ -481,13 +484,14 @@ function outcomeOf(response) {
481
484
  durationSeconds: typeof response.durationSeconds === "number" ? response.durationSeconds : null
482
485
  };
483
486
  }
484
- var emptyOutcome = () => ({ created: [], modified: [], operated: [], failures: [], queryResults: [], durationSeconds: null });
487
+ var emptyOutcome = () => ({ requestId: null, created: [], modified: [], operated: [], failures: [], queryResults: [], durationSeconds: null });
485
488
  function mergeOutcome(into, done, offset) {
486
489
  into.created.push(...done.created);
487
490
  into.modified.push(...done.modified);
488
491
  into.operated.push(...done.operated);
489
492
  into.failures.push(...done.failures.map((f) => ({ ...f, index: f.index === null ? null : f.index + offset })));
490
493
  into.queryResults.push(...done.queryResults);
494
+ into.requestId = null;
491
495
  if (done.durationSeconds !== null) into.durationSeconds = (into.durationSeconds ?? 0) + done.durationSeconds;
492
496
  return into;
493
497
  }
@@ -1168,6 +1172,65 @@ async function reversible(transport, receipt, options = {}) {
1168
1172
  return { commands, drifted, reactivated };
1169
1173
  }
1170
1174
 
1175
+ // src/wire/fields.ts
1176
+ var fields_exports = {};
1177
+ __export(fields_exports, {
1178
+ visibleFields: () => visibleFields,
1179
+ visibleFieldsByAsset: () => visibleFieldsByAsset
1180
+ });
1181
+ async function visibleFields(transport, types, scopes) {
1182
+ if (!types.length) return {};
1183
+ const params = [...scopes.map((s) => `scopeOid=${encodeURIComponent(momentless(s))}`), ...types.map((t) => `assetTypeToken=${encodeURIComponent(t)}`)].join("&");
1184
+ const answer = await transport.request({ method: "GET", path: `/api/visibleFields?${params}`, responseType: "json" });
1185
+ if (!Array.isArray(answer)) throw new ApiError("api/visibleFields did not answer a list", 200, "BAD_RESPONSE", answer);
1186
+ const out = {};
1187
+ for (const entry of answer) if (entry && typeof entry.assetType === "string") out[entry.assetType] = Array.isArray(entry.fields) ? entry.fields.filter((f) => typeof f === "string") : [];
1188
+ return out;
1189
+ }
1190
+ async function visibleFieldsByAsset(transport, oid) {
1191
+ const answer = await transport.request({ method: "GET", path: "/api/visibleFieldsByAsset", query: { oidToken: momentless(oid) }, responseType: "json" });
1192
+ if (!Array.isArray(answer)) throw new ApiError("api/visibleFieldsByAsset did not answer a list", 200, "BAD_RESPONSE", answer);
1193
+ return answer.filter((f) => typeof f === "string");
1194
+ }
1195
+
1196
+ // src/services/fields.ts
1197
+ async function lineage(schema, type) {
1198
+ const out = [];
1199
+ for (let t = type; t && !out.includes(t); ) {
1200
+ out.push(t);
1201
+ const def = await schema.load(t);
1202
+ t = def?.Base;
1203
+ }
1204
+ return out;
1205
+ }
1206
+ function importantName(token, types) {
1207
+ const dot = token.indexOf(".");
1208
+ if (dot < 0) return token;
1209
+ return types.includes(token.slice(0, dot)) ? token.slice(dot + 1) : null;
1210
+ }
1211
+ async function describe2(transport, schema, type, scope) {
1212
+ const scopeOid = momentless(scope);
1213
+ const types = await lineage(schema, type);
1214
+ const definition = await schema.load(type);
1215
+ const required = definition ? Object.values(definition.Attributes).filter((a) => a.IsRequired && !a.IsReadOnly).map((a) => a.Name).sort() : [];
1216
+ const [visible, [row]] = await Promise.all([
1217
+ visibleFields(transport, [type], [scopeOid]),
1218
+ query(transport, { from: "Scope", select: [{ from: "Scheme", select: ["ImportantFields", "IsRequiredAtClose"] }], filter: ["ID=$scope"], with: { $scope: scopeOid }, page: { size: 1, start: 0 } }, { lookup: schema })
1219
+ ]);
1220
+ const scheme = row?.Scheme ?? null;
1221
+ const tokens = Array.isArray(scheme?.ImportantFields) ? scheme.ImportantFields.filter((t) => typeof t === "string") : [];
1222
+ const important = [...new Set(tokens.map((t) => importantName(t, types)).filter((n) => !!n))].sort();
1223
+ return {
1224
+ type,
1225
+ scope: scopeOid,
1226
+ scheme: scheme ? momentless(scheme._oid) : null,
1227
+ required,
1228
+ visible: visible[type] ?? [],
1229
+ important,
1230
+ requiredAtClose: scheme?.IsRequiredAtClose === true
1231
+ };
1232
+ }
1233
+
1171
1234
  // src/types/asset.ts
1172
1235
  var ASSET_STATE_CODES = { Future: 0, Active: 64, Closed: 128, Dead: 192, Template: 200, OldBrokenDownStory: 208, Deprecated: 254, Deleted: 255 };
1173
1236
  var STATE_OF_CODE = Object.fromEntries(Object.entries(ASSET_STATE_CODES).map(([name, code]) => [code, name]));
@@ -1493,6 +1556,8 @@ __export(rest1_exports, {
1493
1556
  history: () => history,
1494
1557
  list: () => list2,
1495
1558
  momentless: () => momentless,
1559
+ operate: () => operate,
1560
+ template: () => template,
1496
1561
  toQuery: () => toQuery
1497
1562
  });
1498
1563
  var toRef = (doc) => ({ _oid: momentless(doc.idref) });
@@ -1547,6 +1612,19 @@ async function get2(transport, oid, select) {
1547
1612
  const doc = await transport.request({ method: "GET", path: `/rest-1.v1/Data/${encodeURIComponent(type)}/${encodeURIComponent(id)}`, query: { sel: select?.join(",") }, responseType: "json" });
1548
1613
  return fromAssetDocument(doc);
1549
1614
  }
1615
+ async function operate(transport, oid, op) {
1616
+ const [type, id] = momentless(oid).split(":");
1617
+ if (!type || !id) throw new UsageError(`"${oid}" is not an OID token (Story:1234)`);
1618
+ const doc = await transport.request({ method: "POST", path: `/rest-1.v1/Data/${encodeURIComponent(type)}/${encodeURIComponent(id)}`, query: { op }, responseType: "json" });
1619
+ if (!doc || typeof doc.id !== "string" || !doc.id) throw new ApiError(`rest-1 ${op} on ${oid} did not answer with an asset`, 200, "BAD_RESPONSE", doc);
1620
+ const moment = doc.id.split(":")[2] ?? null;
1621
+ return { oid: momentless(doc.id), moment };
1622
+ }
1623
+ async function template(transport, type, options = {}) {
1624
+ const doc = await transport.request({ method: "GET", path: `/rest-1.v1/New/${encodeURIComponent(type)}`, query: { ctx: options.ctx ? momentless(options.ctx) : void 0 }, responseType: "json" });
1625
+ const { _oid: _ignored, ...values } = fromAssetDocument({ ...doc, id: doc.id || `${type}:0` });
1626
+ return values;
1627
+ }
1550
1628
  var history = (transport, target, params) => {
1551
1629
  const [type, id] = target.split(":");
1552
1630
  const path = id ? `/rest-1.v1/Hist/${encodeURIComponent(type)}/${encodeURIComponent(id)}` : `/rest-1.v1/Hist/${encodeURIComponent(type)}`;
@@ -1644,16 +1722,39 @@ var AgilityApiClient = class _AgilityApiClient {
1644
1722
  return localize(this.transport, tags, options);
1645
1723
  }
1646
1724
  /**
1647
- * rest-1.v1, for what query.v1 cannot do: deleted assets (`deleted:
1648
- * true`), one asset by OID without a page, and history (one row per
1649
- * moment). Parameters are spelled as a query's (`select`, `filter`).
1725
+ * rest-1.v1, for what the other endpoints cannot do: deleted assets
1726
+ * (`deleted: true`), one asset by OID without a page, history (one row
1727
+ * per moment), a new asset's template (`template`: what the type's
1728
+ * initializer fills in for a context — a Timebox's next name and dates,
1729
+ * a Story's carry-over from a Request), and an operation's result
1730
+ * (`operate`: api/asset performs a Copy but never says what it made;
1731
+ * rest-1 answers the new OID). Parameters are spelled as a query's
1732
+ * (`select`, `filter`).
1650
1733
  */
1651
1734
  data = {
1652
1735
  list: (type, params) => list2(this.transport, type, params),
1653
1736
  get: (oid, select) => get2(this.transport, oid, select),
1654
- history: (target, params) => history(this.transport, target, params)
1737
+ history: (target, params) => history(this.transport, target, params),
1738
+ template: (type, options) => template(this.transport, type, options),
1739
+ operate: (oid, op) => operate(this.transport, oid, op)
1740
+ };
1741
+ /** Copy an asset (the server's Copy operation) and answer the copy's OID: the one thing api/asset's `execute: "Copy"` does not say. */
1742
+ async copy(oid) {
1743
+ return (await operate(this.transport, oid, "Copy")).oid;
1744
+ }
1745
+ /**
1746
+ * What a form needs for an asset type in a scope: the attributes the
1747
+ * server requires to create one (meta), the ones the product shows there
1748
+ * (api/visibleFields, custom fields included once configured), and the
1749
+ * scope's scheme's important fields and whether they are enforced at
1750
+ * close. `visible` alone answers for several types and scopes at once.
1751
+ */
1752
+ fields = {
1753
+ describe: (type, scope) => describe2(this.transport, this.schema, type, scope),
1754
+ visible: (types, scopes) => visibleFields(this.transport, types, scopes),
1755
+ visibleByAsset: (oid) => visibleFieldsByAsset(this.transport, oid)
1655
1756
  };
1656
- /** @deprecated since 1.0.0-alpha.3: `client.query` / `client.queries` read through query.v1. Removed in 1.0.0. */
1757
+ /** @deprecated since 1.0.0-alpha.3: use `client.query` / `client.queries` (the same endpoint, query.v1; only the name moved). Removed in 1.0.0. */
1657
1758
  queryV1 = {
1658
1759
  query: (q) => this.query(q),
1659
1760
  queries: (qs) => this.queries(qs)
@@ -1685,7 +1786,7 @@ var AgilityApiClient = class _AgilityApiClient {
1685
1786
  await this.schema.ensureFor(commands);
1686
1787
  return sendGuarded(this.transport, commands, { ...options, lookup: this.schema });
1687
1788
  },
1688
- /** @deprecated since 1.0.0-alpha.3: use `client.query` (or `client.queryPage` for a total). Removed in 1.0.0. */
1789
+ /** @deprecated since 1.0.0-alpha.3: use `client.query` (rows, through query.v1) or `client.queryPage` (rows and a total, through api/asset as this does). Removed in 1.0.0. */
1689
1790
  query: async (q) => {
1690
1791
  await this.schema.ensureFor([q]);
1691
1792
  return query(this.transport, q, { lookup: this.schema });
@@ -1745,8 +1846,19 @@ var AgilityApiClient = class _AgilityApiClient {
1745
1846
  };
1746
1847
  /** Where a browser opens things on this instance: an asset's page, an attachment's bytes, an embedded image. */
1747
1848
  urls = {
1748
- /** The asset's page: assetdetail.v1 redirects to the type's own detail page (live 2026-11-12). */
1749
- asset: (oid) => `${this.config.baseUrl.replace(/\/+$/, "")}/assetdetail.v1?oid=${momentless(oid)}`,
1849
+ /**
1850
+ * The asset's page, from an OID token or an asset Number (S-01001,
1851
+ * what a person typed and what a message shows): assetdetail.v1
1852
+ * resolves either and redirects to the type's own detail page; a signed-
1853
+ * out reader lands there after logging in. Anything else is a UsageError.
1854
+ */
1855
+ asset: (ref) => {
1856
+ const base = this.config.baseUrl.replace(/\/+$/, "");
1857
+ const text = ref.trim();
1858
+ if (isOidToken(text)) return `${base}/assetdetail.v1?oid=${momentless(text)}`;
1859
+ if (isAssetNumber(text)) return `${base}/assetdetail.v1?Number=${encodeURIComponent(text)}`;
1860
+ throw new UsageError(`"${ref}" is neither an OID token (Story:1234) nor an asset number (S-1234)`);
1861
+ },
1750
1862
  attachment: (id) => attachmentUrl(this.config.baseUrl, id),
1751
1863
  embeddedImage: (id) => embeddedImageUrl(this.config.baseUrl, id)
1752
1864
  };
@@ -1775,7 +1887,9 @@ var AgilityApiClient = class _AgilityApiClient {
1775
1887
  attachment,
1776
1888
  createFetchTransport,
1777
1889
  describeBatch,
1890
+ fields,
1778
1891
  guard,
1892
+ importantName,
1779
1893
  isAssetNumber,
1780
1894
  isOidToken,
1781
1895
  kindOf,