@typeship-ax/cli 0.23.1 → 0.24.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.
Files changed (57) hide show
  1. package/AGENTS.md +2 -2
  2. package/README.md +2 -2
  3. package/api.md +2 -2
  4. package/dist/arguments.d.ts +9 -2
  5. package/dist/arguments.d.ts.map +1 -1
  6. package/dist/arguments.js +17 -6
  7. package/dist/cli-agent.d.ts +42 -1
  8. package/dist/cli-agent.d.ts.map +1 -1
  9. package/dist/cli-agent.js +187 -18
  10. package/dist/cli.js +341 -76
  11. package/dist/fields.d.ts +8 -1
  12. package/dist/fields.d.ts.map +1 -1
  13. package/dist/fields.js +91 -5
  14. package/dist/index.d.ts +2 -2
  15. package/dist/index.js +3 -3
  16. package/dist/ops.d.ts +5 -0
  17. package/dist/ops.d.ts.map +1 -1
  18. package/dist/ops.js +66 -6
  19. package/dist/resources/deliveries.d.ts +1 -1
  20. package/dist/resources/deliveries.d.ts.map +1 -1
  21. package/dist/resources/deliveries.js +1 -1
  22. package/dist/resources/drafts.d.ts +1 -1
  23. package/dist/resources/drafts.d.ts.map +1 -1
  24. package/dist/resources/drafts.js +1 -1
  25. package/dist/resources/generations.d.ts +2 -2
  26. package/dist/resources/generations.d.ts.map +1 -1
  27. package/dist/resources/generations.js +2 -2
  28. package/dist/resources/releases.d.ts +1 -1
  29. package/dist/resources/releases.d.ts.map +1 -1
  30. package/dist/resources/releases.js +1 -1
  31. package/dist/resources/spec-revisions.d.ts +1 -1
  32. package/dist/resources/spec-revisions.d.ts.map +1 -1
  33. package/dist/resources/spec-revisions.js +1 -1
  34. package/dist/resources/targets.d.ts +1 -1
  35. package/dist/resources/targets.d.ts.map +1 -1
  36. package/dist/resources/targets.js +1 -1
  37. package/dist/table.d.ts +28 -0
  38. package/dist/table.d.ts.map +1 -0
  39. package/dist/table.js +167 -0
  40. package/dist/type-docs.d.ts +61 -0
  41. package/dist/type-docs.d.ts.map +1 -0
  42. package/dist/type-docs.js +174 -0
  43. package/package.json +1 -1
  44. package/src/arguments.ts +19 -7
  45. package/src/cli-agent.ts +196 -17
  46. package/src/cli.ts +330 -76
  47. package/src/fields.ts +81 -5
  48. package/src/index.ts +3 -3
  49. package/src/ops.ts +69 -6
  50. package/src/resources/deliveries.ts +2 -2
  51. package/src/resources/drafts.ts +2 -2
  52. package/src/resources/generations.ts +4 -4
  53. package/src/resources/releases.ts +2 -2
  54. package/src/resources/spec-revisions.ts +2 -2
  55. package/src/resources/targets.ts +2 -2
  56. package/src/table.ts +167 -0
  57. package/src/type-docs.ts +205 -0
@@ -90,7 +90,7 @@ export class DeliveriesResource {
90
90
  query: {
91
91
  limit: params?.limit,
92
92
  cursor: params?.cursor,
93
- target_id: params?.targetId,
93
+ target_id: params?.target_id,
94
94
  },
95
95
  idempotent: true,
96
96
  schemaKey: "deliveries.list",
@@ -239,7 +239,7 @@ export interface DeliveriesListParams {
239
239
  */
240
240
  cursor?: string;
241
241
  /** Only Deliveries of this Target. */
242
- targetId?: TargetId;
242
+ target_id?: TargetId;
243
243
  }
244
244
 
245
245
  /** Typed errors `list` can throw. */
@@ -58,7 +58,7 @@ export class DraftsResource {
58
58
  query: {
59
59
  limit: params?.limit,
60
60
  cursor: params?.cursor,
61
- target_id: params?.targetId,
61
+ target_id: params?.target_id,
62
62
  status: params?.status,
63
63
  },
64
64
  idempotent: true,
@@ -249,7 +249,7 @@ export interface DraftsListParams {
249
249
  */
250
250
  cursor?: string;
251
251
  /** Only Drafts of this Target. */
252
- targetId?: TargetId;
252
+ target_id?: TargetId;
253
253
  /** Only Drafts with this status. */
254
254
  status?: DraftStatus;
255
255
  }
@@ -72,8 +72,8 @@ export class GenerationsResource {
72
72
  query: {
73
73
  limit: params?.limit,
74
74
  cursor: params?.cursor,
75
- project_id: params?.projectId,
76
- target_id: params?.targetId,
75
+ project_id: params?.project_id,
76
+ target_id: params?.target_id,
77
77
  status: params?.status,
78
78
  },
79
79
  idempotent: true,
@@ -173,9 +173,9 @@ export interface GenerationsListParams {
173
173
  */
174
174
  cursor?: string;
175
175
  /** Only Generations in this Project. */
176
- projectId?: ProjectId;
176
+ project_id?: ProjectId;
177
177
  /** Only Generations of this Target. */
178
- targetId?: TargetId;
178
+ target_id?: TargetId;
179
179
  /** Only Generations with this status. */
180
180
  status?: GenerationStatus;
181
181
  }
@@ -45,7 +45,7 @@ export class ReleasesResource {
45
45
  query: {
46
46
  limit: params?.limit,
47
47
  cursor: params?.cursor,
48
- target_id: params?.targetId,
48
+ target_id: params?.target_id,
49
49
  },
50
50
  idempotent: true,
51
51
  schemaKey: "releases.list",
@@ -128,7 +128,7 @@ export interface ReleasesListParams {
128
128
  */
129
129
  cursor?: string;
130
130
  /** Only releases of this Target. */
131
- targetId?: TargetId;
131
+ target_id?: TargetId;
132
132
  }
133
133
 
134
134
  /** Typed errors `list` can throw. */
@@ -54,7 +54,7 @@ export class SpecRevisionsResource {
54
54
  query: {
55
55
  limit: params?.limit,
56
56
  cursor: params?.cursor,
57
- spec_id: params?.specId,
57
+ spec_id: params?.spec_id,
58
58
  },
59
59
  idempotent: true,
60
60
  schemaKey: "specRevisions.list",
@@ -151,7 +151,7 @@ export interface SpecRevisionsListParams {
151
151
  */
152
152
  cursor?: string;
153
153
  /** Only revisions of this Spec. */
154
- specId?: SpecId;
154
+ spec_id?: SpecId;
155
155
  }
156
156
 
157
157
  /** Typed errors `list` can throw. */
@@ -84,7 +84,7 @@ export class TargetsResource {
84
84
  query: {
85
85
  limit: params?.limit,
86
86
  cursor: params?.cursor,
87
- project_id: params?.projectId,
87
+ project_id: params?.project_id,
88
88
  },
89
89
  idempotent: true,
90
90
  schemaKey: "targets.list",
@@ -260,7 +260,7 @@ export interface TargetsListParams {
260
260
  */
261
261
  cursor?: string;
262
262
  /** Only Targets in this Project. */
263
- projectId?: ProjectId;
263
+ project_id?: ProjectId;
264
264
  }
265
265
 
266
266
  /** Typed errors `list` can throw. */
package/src/table.ts ADDED
@@ -0,0 +1,167 @@
1
+ /**
2
+ * `--format table` for the generated CLI: an API result as text for a
3
+ * person at a terminal. Generated by Typeship — https://typeship.dev
4
+ *
5
+ * JSON stays the default and the automation format; this view is opt-in and
6
+ * lossy by design. Lists become columns chosen by name (identifiers, then
7
+ * names, then status-like fields) while they fit the terminal width; a single
8
+ * object becomes one "field value" line per field in the same order. Cells longer than
9
+ * MAX_CELL characters end in "…", and the footer names every column or field
10
+ * that was left out. Pure: no I/O, so tests render without a terminal.
11
+ */
12
+
13
+ /** The longest cell before it is cut with "…". */
14
+ export const MAX_CELL = 40;
15
+ /** A single object shows at most this many (flattened) fields. */
16
+ export const MAX_FIELDS = 60;
17
+
18
+ export interface TableOptions {
19
+ /** Terminal width in columns; lists add columns until it is used. */
20
+ width?: number;
21
+ /** Wraps header text, e.g. in bold when color is on. */
22
+ heading?: (text: string) => string;
23
+ /** The array property that holds a collection response's items ({data: [...]}). */
24
+ collectionField?: string | null;
25
+ /** The command's resource (shipments), so shipment_id is recognized as the row's identifier. */
26
+ resource?: string;
27
+ }
28
+
29
+ type Row = Record<string, unknown>;
30
+
31
+ const ID_KEYS = new Set(["id", "sid", "uuid", "key", "slug", "code", "symbol", "number", "handle"]);
32
+ const NAME_KEYS = new Set(["name", "display_name", "displayname", "friendly_name", "friendlyname", "title", "label", "email", "username", "login", "to", "from"]);
33
+ const STATUS_KEYS = new Set(["status", "state"]);
34
+ const DESCRIPTOR_KEYS = new Set(["type", "kind", "direction", "enabled", "active", "amount", "currency", "price", "total"]);
35
+
36
+ /**
37
+ * Lower is shown first: identifiers, names, status, descriptors, everything
38
+ * else, dates, then references and URLs. `primaryId` is the row's own
39
+ * identifier when it is spelled like a reference (shipment_id with no id).
40
+ */
41
+ function rank(key: string, primaryId: string | undefined): number {
42
+ const k = key.toLowerCase();
43
+ if (ID_KEYS.has(k) || key === primaryId) return 0;
44
+ if (NAME_KEYS.has(k)) return 1;
45
+ if (STATUS_KEYS.has(k)) return 2;
46
+ if (DESCRIPTOR_KEYS.has(k)) return 3;
47
+ if (/(^|_)(created|updated)(_at|_on)?$|^date_|^(created|updated)|At$/.test(key)) return 5;
48
+ if (/(_id|_sid|Id|_ids|_uri|_url|Url|Uri)$/.test(key) || k === "uri" || k === "url") return 6;
49
+ return 4;
50
+ }
51
+
52
+ /** Keys in display order. The resource's own *_id (shipment_id for shipments) counts as the identifier when no id-like key exists. */
53
+ function byRank(keys: string[], resource: string | undefined): string[] {
54
+ const bare = (text: string) => text.toLowerCase().replace(/[^a-z0-9]/g, "");
55
+ const own = resource ? bare(resource).replace(/ies$/, "y").replace(/(ses|xes|s)$/, (m) => m === "s" ? "" : m.slice(0, -2)) + "id" : undefined;
56
+ const primaryId = keys.some((key) => ID_KEYS.has(key.toLowerCase())) ? undefined
57
+ : keys.find((key) => bare(key) === own) ?? keys.find((key) => /(_id|Id)$/.test(key));
58
+ return keys.map((key, index) => ({ key, index, rank: rank(key, primaryId) })).sort((a, b) => a.rank - b.rank || a.index - b.index).map((c) => c.key);
59
+ }
60
+
61
+ function isPlainObject(value: unknown): value is Row {
62
+ return value !== null && typeof value === "object" && !Array.isArray(value);
63
+ }
64
+
65
+ function scalar(value: unknown): string {
66
+ if (value === null || value === undefined) return "";
67
+ if (typeof value === "string") return value.replace(/\s+/g, " ").trim();
68
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
69
+ if (Array.isArray(value)) {
70
+ if (value.length === 0) return "[]";
71
+ return value.every((v) => v === null || typeof v !== "object") ? value.map(scalar).join(", ") : "[" + value.length + " items]";
72
+ }
73
+ if (isPlainObject(value)) {
74
+ const entries = Object.entries(value);
75
+ if (entries.length === 0) return "{}";
76
+ return entries.every(([, v]) => v === null || typeof v !== "object")
77
+ ? entries.map(([k, v]) => k + "=" + scalar(v)).join(" ")
78
+ : "{" + entries.length + " fields}";
79
+ }
80
+ return String(value);
81
+ }
82
+
83
+ function cut(text: string, max: number): string {
84
+ return text.length > max ? text.slice(0, Math.max(1, max - 1)) + "…" : text;
85
+ }
86
+
87
+ function pad(text: string, width: number): string {
88
+ return text + " ".repeat(Math.max(0, width - text.length));
89
+ }
90
+
91
+ /** The rows and footer lines of a list-shaped result, or null for a single object. */
92
+ function listOf(value: unknown, collectionField: string | null | undefined): { rows: unknown[]; footer: string[] } | null {
93
+ if (Array.isArray(value)) return { rows: value, footer: [] };
94
+ if (!isPlainObject(value)) return null;
95
+ // The CLI's page envelope: {items, hasMore, nextPage?, nextCommand?, request_id?}.
96
+ if (Array.isArray(value.items) && typeof value.hasMore === "boolean") {
97
+ return { rows: value.items, footer: typeof value.nextCommand === "string" ? ["More results: " + value.nextCommand] : [] };
98
+ }
99
+ if (collectionField && Array.isArray(value[collectionField])) {
100
+ const rest = Object.entries(value).filter(([k, v]) => k !== collectionField && v !== null && typeof v !== "object");
101
+ return { rows: value[collectionField] as unknown[], footer: rest.length ? [rest.map(([k, v]) => k + ": " + scalar(v)).join(" ")] : [] };
102
+ }
103
+ return null;
104
+ }
105
+
106
+ function renderRows(rows: unknown[], footer: string[], options: TableOptions): string {
107
+ const heading = options.heading ?? ((text: string) => text);
108
+ if (rows.length === 0) return ["No items.", ...footer].join("\n") + "\n";
109
+ if (!rows.every(isPlainObject)) {
110
+ return [...rows.map((row) => cut(scalar(row), Math.max(MAX_CELL, options.width ?? 120))), "", rows.length + (rows.length === 1 ? " item" : " items"), ...footer].join("\n") + "\n";
111
+ }
112
+ const keys: string[] = [];
113
+ for (const row of rows as Row[]) for (const key of Object.keys(row)) if (!keys.includes(key)) keys.push(key);
114
+ const ordered = byRank(keys, options.resource);
115
+ const width = Math.max(40, options.width ?? 120);
116
+ const shown: { key: string; width: number }[] = [];
117
+ let used = 0;
118
+ for (const key of ordered) {
119
+ const cells = (rows as Row[]).map((row) => cut(scalar(row[key]), MAX_CELL));
120
+ // A column empty in every row says nothing.
121
+ if (cells.every((cell) => cell === "")) continue;
122
+ const columnWidth = Math.max(Math.min(key.length, MAX_CELL), ...cells.map((cell) => cell.length));
123
+ if (shown.length > 0 && used + 2 + columnWidth > width) continue;
124
+ shown.push({ key, width: columnWidth });
125
+ used += (shown.length > 1 ? 2 : 0) + columnWidth;
126
+ }
127
+ const line = (cells: string[]) => cells.map((cell, i) => i === cells.length - 1 ? cell : pad(cell, shown[i]!.width)).join(" ").trimEnd();
128
+ const lines = [
129
+ heading(line(shown.map((c) => cut(c.key.toUpperCase(), c.width)))),
130
+ ...(rows as Row[]).map((row) => line(shown.map((c) => cut(scalar(row[c.key]), MAX_CELL)))),
131
+ "",
132
+ ];
133
+ const hidden = ordered.filter((key) => !shown.some((c) => c.key === key));
134
+ lines.push(rows.length + (rows.length === 1 ? " item" : " items") + (hidden.length ? "; not shown: " + hidden.join(", ") : ""));
135
+ return [...lines, ...footer].join("\n") + "\n";
136
+ }
137
+
138
+ /** One object as "field value" lines, identifiers and status first; nested objects flatten to dotted paths. */
139
+ function renderObject(value: Row, options: TableOptions): string {
140
+ const fields: [string, string][] = [];
141
+ const walk = (object: Row, prefix: string, depth: number) => {
142
+ const keys = depth === 0 ? byRank(Object.keys(object), options.resource) : Object.keys(object);
143
+ for (const key of keys) {
144
+ const v = object[key];
145
+ const path = prefix + key;
146
+ if (isPlainObject(v) && depth < 2 && Object.keys(v).length > 0) walk(v, path + ".", depth + 1);
147
+ else fields.push([path, scalar(v)]);
148
+ }
149
+ };
150
+ walk(value, "", 0);
151
+ if (fields.length === 0) return "No fields.\n";
152
+ const heading = options.heading ?? ((text: string) => text);
153
+ const shown = fields.slice(0, MAX_FIELDS);
154
+ const keyWidth = Math.min(MAX_CELL, Math.max(...shown.map(([key]) => key.length)));
155
+ const valueWidth = Math.max(MAX_CELL, (options.width ?? 120) - keyWidth - 2);
156
+ const lines = shown.map(([key, text]) => (heading(pad(cut(key, keyWidth), keyWidth)) + " " + cut(text, valueWidth)).trimEnd());
157
+ if (fields.length > shown.length) lines.push("", (fields.length - shown.length) + " more fields not shown; use --format json or --fields.");
158
+ return lines.join("\n") + "\n";
159
+ }
160
+
161
+ /** The text --format table prints for a result that would otherwise print as JSON. */
162
+ export function renderTable(value: unknown, options: TableOptions = {}): string {
163
+ const list = listOf(value, options.collectionField);
164
+ if (list) return renderRows(list.rows, list.footer, options);
165
+ if (isPlainObject(value)) return renderObject(value, options);
166
+ return scalar(value) + "\n";
167
+ }
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Named input types for the docs surfaces (MCP read_docs, the CLI's docs
3
+ * command): a type's reference by name, and an argument path such as
4
+ * "filter.team.key" resolved to its type. Each type is described once with
5
+ * its fields typed by name, so a lookup is bounded and every type it
6
+ * mentions can be looked up the same way.
7
+ */
8
+
9
+ export interface InputTypeField {
10
+ /** As an agent writes it: a type name, string[], "a"|"b", object. */
11
+ type: string;
12
+ required?: true;
13
+ description?: string;
14
+ default?: unknown;
15
+ deprecated?: true;
16
+ }
17
+
18
+ export interface InputTypeDoc {
19
+ description?: string;
20
+ /** An input object's fields. */
21
+ fields?: Record<string, InputTypeField>;
22
+ /** An enum's values. */
23
+ values?: unknown[];
24
+ /** A union's member types. */
25
+ variants?: string[];
26
+ }
27
+
28
+ export interface InputTypes {
29
+ types: Record<string, InputTypeDoc>;
30
+ /** Per tool, each argument whose type is (or contains) a named type. */
31
+ args: Record<string, Record<string, string>>;
32
+ }
33
+
34
+ /** Enum values a type page lists before a count. */
35
+ const TYPE_ENUM_VALUES = 200;
36
+
37
+ /** One line of prose: links reduced to their text. */
38
+ function prose(text: string | undefined): string {
39
+ return (text ?? "").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\s+/g, " ").trim();
40
+ }
41
+
42
+ /** The table's type names an expression mentions, in order. */
43
+ export function namedTypesIn(table: InputTypes | undefined, expression: string | undefined): string[] {
44
+ if (!table || !expression) return [];
45
+ return [...new Set(expression.split(/[|()[\]\s]+/).filter((part) => Object.hasOwn(table.types, part)))];
46
+ }
47
+
48
+ /** The one input object an expression names (IssueFilter, IssueFilter[]),
49
+ * whose fields a path continues into. */
50
+ function objectTypeOf(table: InputTypes, expression: string | undefined): string | undefined {
51
+ const objects = namedTypesIn(table, expression).filter((name) => table.types[name]!.fields);
52
+ return objects.length === 1 ? objects[0] : undefined;
53
+ }
54
+
55
+ /** A type by exact name, else by a case-insensitive match. */
56
+ export function findInputType(table: InputTypes | undefined, name: string): string | undefined {
57
+ if (!table) return undefined;
58
+ if (Object.hasOwn(table.types, name)) return name;
59
+ const lower = name.toLowerCase();
60
+ return Object.keys(table.types).find((candidate) => candidate.toLowerCase() === lower);
61
+ }
62
+
63
+ function fieldLine(name: string, field: InputTypeField): string {
64
+ const extras = [
65
+ ...(field.required ? ["required"] : []),
66
+ ...(field.default !== undefined ? ["default " + JSON.stringify(field.default)] : []),
67
+ ...(field.deprecated ? ["deprecated"] : []),
68
+ ];
69
+ const description = prose(field.description);
70
+ return " " + name + " (" + field.type + (extras.length ? ", " + extras.join(", ") : "") + ")" + (description ? ": " + description : "");
71
+ }
72
+
73
+ /** How to read another type, in the caller's own syntax. */
74
+ export type TypeLookupHint = (name: string) => string;
75
+
76
+ /** A named type's reference: description, then its fields, values or
77
+ * members, then how to read the named types it mentions. */
78
+ export function inputTypeText(table: InputTypes, name: string, lookup: TypeLookupHint): string {
79
+ const doc = table.types[name]!;
80
+ const kind = doc.fields ? "input object" : doc.values ? "enum" : "union";
81
+ const lines = [name + " (" + kind + ")"];
82
+ if (doc.description) lines.push(prose(doc.description));
83
+ const mentioned: string[] = [];
84
+ if (doc.fields) {
85
+ const entries = Object.entries(doc.fields);
86
+ lines.push("", entries.length === 0 ? "No fields." : "Fields (" + entries.length + "):");
87
+ for (const [field, spec] of entries) {
88
+ lines.push(fieldLine(field, spec));
89
+ mentioned.push(...namedTypesIn(table, spec.type));
90
+ }
91
+ } else if (doc.values) {
92
+ const values = doc.values.slice(0, TYPE_ENUM_VALUES).map((value) => JSON.stringify(value));
93
+ lines.push("", "Values: " + values.join(", ") + (doc.values.length > TYPE_ENUM_VALUES ? ", … " + (doc.values.length - TYPE_ENUM_VALUES) + " more" : ""));
94
+ } else if (doc.variants) {
95
+ lines.push("", "One of: " + doc.variants.join(" | "));
96
+ for (const variant of doc.variants) mentioned.push(...namedTypesIn(table, variant));
97
+ }
98
+ const others = [...new Set(mentioned)].filter((other) => other !== name);
99
+ if (others.length > 0) {
100
+ lines.push("", "Types used above (" + others.length + "): " + others.join(", ") + ". Read one with " + lookup("<type>") + ".");
101
+ }
102
+ return lines.join("\n");
103
+ }
104
+
105
+ /** A field of an anonymous object in an inline JSON Schema, one level. */
106
+ interface InlineSchema { type?: unknown; properties?: Record<string, InlineSchema>; required?: string[]; items?: InlineSchema; anyOf?: InlineSchema[]; oneOf?: InlineSchema[]; enum?: unknown[]; description?: string }
107
+
108
+ function inlineObject(schema: InlineSchema | undefined): InlineSchema | undefined {
109
+ if (!schema || typeof schema !== "object") return undefined;
110
+ if (schema.properties && typeof schema.properties === "object") return schema;
111
+ if (schema.items) return inlineObject(schema.items);
112
+ const variants = schema.anyOf ?? schema.oneOf;
113
+ if (Array.isArray(variants)) {
114
+ const objects = variants.map(inlineObject).filter((variant) => variant !== undefined);
115
+ if (objects.length === 1) return objects[0];
116
+ }
117
+ return undefined;
118
+ }
119
+
120
+ function inlineType(schema: InlineSchema): string {
121
+ if (Array.isArray(schema.enum)) return schema.enum.filter((value) => value !== null).map((value) => JSON.stringify(value)).join("|");
122
+ const variants = schema.anyOf ?? schema.oneOf;
123
+ if (Array.isArray(variants)) return [...new Set(variants.map(inlineType))].filter((t) => t !== "null").join("|") || "null";
124
+ const types = (Array.isArray(schema.type) ? schema.type : typeof schema.type === "string" ? [schema.type] : []).filter((t) => t !== "null") as string[];
125
+ if (types.includes("array")) {
126
+ const inner = schema.items ? inlineType(schema.items) : "any";
127
+ return (/[| ]/.test(inner) ? "(" + inner + ")" : inner) + "[]";
128
+ }
129
+ return types.join("|") || (schema.properties ? "object" : "any");
130
+ }
131
+
132
+ export type PathLookup =
133
+ | { ok: true; text: string }
134
+ | { ok: false; message: string; available: string[] };
135
+
136
+ /**
137
+ * What one argument path means: the field's type, whether it is required,
138
+ * its description, and, when its type is named, that type's reference in
139
+ * full (a comparator's operators, an enum's values). `root` is an
140
+ * operation (its tool name, inline input schema, and how the caller names
141
+ * it) or a named type.
142
+ */
143
+ export function argumentPathText(
144
+ table: InputTypes | undefined,
145
+ root: { tool: string; inputSchema: Record<string, unknown>; label?: string } | { type: string },
146
+ path: string,
147
+ lookup: TypeLookupHint,
148
+ ): PathLookup {
149
+ const segments = path.split(".").map((segment) => segment.trim()).filter((segment) => segment.length > 0);
150
+ if (segments.length === 0) return { ok: false, message: "The path is empty.", available: [] };
151
+ // Walk the named-type table where the path has a type name, else the
152
+ // operation's inline schema.
153
+ let typeName: string | undefined = "type" in root ? root.type : undefined;
154
+ let inline: InlineSchema | undefined = "type" in root ? undefined : root.inputSchema as InlineSchema;
155
+ let expression: string | undefined;
156
+ let field: InputTypeField | undefined;
157
+ const label = "type" in root ? root.type : root.label ?? root.tool;
158
+ for (let index = 0; index < segments.length; index += 1) {
159
+ const segment = segments[index]!;
160
+ const at = index === 0 ? label : label + " " + segments.slice(0, index).join(".");
161
+ if (typeName && table) {
162
+ const fields = table.types[typeName]!.fields ?? {};
163
+ if (!Object.hasOwn(fields, segment)) return { ok: false, message: at + " (" + typeName + ") has no field \"" + segment + "\".", available: Object.keys(fields) };
164
+ field = fields[segment]!;
165
+ expression = field.type;
166
+ inline = undefined;
167
+ } else {
168
+ const object = inlineObject(inline);
169
+ const properties = object?.properties ?? {};
170
+ if (!object || !Object.hasOwn(properties, segment)) {
171
+ return { ok: false, message: object ? at + " has no field \"" + segment + "\"." : at + " is not an object; the path cannot continue past it.", available: Object.keys(properties) };
172
+ }
173
+ const child = properties[segment]!;
174
+ const topLevel = index === 0 && !("type" in root) ? table?.args[root.tool]?.[segment] : undefined;
175
+ expression = topLevel ?? inlineType(child);
176
+ field = {
177
+ type: expression,
178
+ ...(Array.isArray(object.required) && object.required.includes(segment) ? { required: true as const } : {}),
179
+ ...(typeof child.description === "string" ? { description: child.description } : {}),
180
+ };
181
+ inline = child;
182
+ }
183
+ typeName = table ? objectTypeOf(table, expression) : undefined;
184
+ if (!typeName && index < segments.length - 1 && !inlineObject(inline)) {
185
+ return { ok: false, message: label + " " + segments.slice(0, index + 1).join(".") + " is " + expression + ", not an object; the path cannot continue past it.", available: [] };
186
+ }
187
+ }
188
+ const lines = [label + " " + segments.join(".") + ": " + field!.type + (field!.required ? " (required)" : "")];
189
+ const description = prose(field!.description);
190
+ if (description) lines.push(description);
191
+ const named = namedTypesIn(table, expression);
192
+ if (named.length > 0 && table) {
193
+ for (const name of named) lines.push("", inputTypeText(table, name, lookup));
194
+ } else {
195
+ const object = inlineObject(inline);
196
+ if (object) {
197
+ const required = new Set(object.required ?? []);
198
+ lines.push("", "Fields:");
199
+ for (const [name, child] of Object.entries(object.properties ?? {})) {
200
+ lines.push(fieldLine(name, { type: inlineType(child), ...(required.has(name) ? { required: true } : {}), ...(child.description ? { description: child.description } : {}) }));
201
+ }
202
+ }
203
+ }
204
+ return { ok: true, text: lines.join("\n") };
205
+ }