pi-spark 0.9.5 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/extensions/models/index.ts +136 -70
- package/extensions/name/index.ts +15 -19
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -80,8 +80,8 @@ Example:
|
|
|
80
80
|
### Models
|
|
81
81
|
|
|
82
82
|
- The agent can call the `model` tool with two actions:
|
|
83
|
-
- `
|
|
84
|
-
- `
|
|
83
|
+
- `active`: gets the active provider, model, and thinking level.
|
|
84
|
+
- `list`: lists models with their metadata, with an optional [Liqe](https://github.com/gajus/liqe) (Lucene-like) `query` over model fields and `offset`/`limit` paging.
|
|
85
85
|
|
|
86
86
|
### Name
|
|
87
87
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { clampThinkingLevel, getSupportedThinkingLevels, StringEnum } from "@earendil-works/pi-ai";
|
|
2
|
-
import {
|
|
2
|
+
import { keyText, truncateHead } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
4
|
+
import { filter, parse } from "liqe";
|
|
4
5
|
import { Type } from "typebox";
|
|
5
6
|
|
|
6
7
|
import { loadConfig } from "../shared/config";
|
|
7
|
-
import { formatModel } from "../shared/format";
|
|
8
|
+
import { formatModel, formatTokens } from "../shared/format";
|
|
8
9
|
|
|
9
10
|
import type { Api, Model, ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
10
11
|
import type { ExtensionAPI, TruncationResult } from "@earendil-works/pi-coding-agent";
|
|
@@ -12,6 +13,12 @@ import type { ExtensionAPI, TruncationResult } from "@earendil-works/pi-coding-a
|
|
|
12
13
|
/** Pi's built-in default thinking level, clamped per model. Not exported by pi's public API. */
|
|
13
14
|
const DEFAULT_THINKING_LEVEL: ModelThinkingLevel = "medium";
|
|
14
15
|
|
|
16
|
+
/** Maximum number of models returned per list call; byte size is unrestricted. */
|
|
17
|
+
const LIST_MAX_LINES = 200;
|
|
18
|
+
|
|
19
|
+
/** Maximum number of model rows shown when the result is collapsed. */
|
|
20
|
+
const COLLAPSED_MAX_ROWS = 10;
|
|
21
|
+
|
|
15
22
|
type ModelMetadata = Omit<Model<Api>, "headers" | "compat"> & {
|
|
16
23
|
thinkingLevels: ModelThinkingLevel[];
|
|
17
24
|
defaultThinkingLevel: ModelThinkingLevel;
|
|
@@ -19,9 +26,17 @@ type ModelMetadata = Omit<Model<Api>, "headers" | "compat"> & {
|
|
|
19
26
|
};
|
|
20
27
|
|
|
21
28
|
type ModelToolDetails =
|
|
22
|
-
| { action: "
|
|
29
|
+
| { action: "active"; active: { model: ModelMetadata; thinkingLevel: ModelThinkingLevel } }
|
|
23
30
|
| { action: "list"; models: ModelMetadata[]; total: number; truncation?: TruncationResult }
|
|
24
31
|
|
|
32
|
+
/** One display row of model metadata: label, cost per 1M tokens, and context window. */
|
|
33
|
+
type ModelRow = {
|
|
34
|
+
label: string;
|
|
35
|
+
cost: string;
|
|
36
|
+
context: string;
|
|
37
|
+
available: boolean;
|
|
38
|
+
};
|
|
39
|
+
|
|
25
40
|
function toMetadata(model: Model<Api>, available: boolean): ModelMetadata {
|
|
26
41
|
const { headers: _headers, compat: _compat, ...metadata } = model;
|
|
27
42
|
|
|
@@ -33,6 +48,30 @@ function toMetadata(model: Model<Api>, available: boolean): ModelMetadata {
|
|
|
33
48
|
};
|
|
34
49
|
}
|
|
35
50
|
|
|
51
|
+
function toModelRow(model: ModelMetadata, thinkingLevel?: ModelThinkingLevel): ModelRow {
|
|
52
|
+
return {
|
|
53
|
+
label: formatModel(model.provider, model.id, thinkingLevel),
|
|
54
|
+
cost: `$${formatPrice(model.cost.input)}/$${formatPrice(model.cost.output)}`,
|
|
55
|
+
context: formatTokens(model.contextWindow),
|
|
56
|
+
available: model.available,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Round to at most 2 decimals and trim float noise: 0.7999... -> 0.8, 0.0983 -> 0.1, 15 -> 15. */
|
|
61
|
+
function formatPrice(value: number): string {
|
|
62
|
+
return String(parseFloat(value.toFixed(2)));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Notice line describing truncation or remaining pages, shared by the text result and the TUI. */
|
|
66
|
+
function formatListNotice(truncated: boolean, startIndex: number, endDisplay: number, total: number): string | undefined {
|
|
67
|
+
const nextOffset = endDisplay + 1;
|
|
68
|
+
|
|
69
|
+
if (truncated) return `[Truncated: showing models ${startIndex + 1}-${endDisplay} of ${total}. Use offset=${nextOffset} to continue.]`;
|
|
70
|
+
if (endDisplay < total) return `[${total - endDisplay} more models in list. Use offset=${nextOffset} to continue.]`;
|
|
71
|
+
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
36
75
|
export default function (pi: ExtensionAPI) {
|
|
37
76
|
pi.on("session_start", (_event, ctx) => {
|
|
38
77
|
const config = loadConfig(ctx, "models");
|
|
@@ -42,48 +81,52 @@ export default function (pi: ExtensionAPI) {
|
|
|
42
81
|
name: "model",
|
|
43
82
|
label: "model",
|
|
44
83
|
description:
|
|
45
|
-
"
|
|
46
|
-
"and thinking level.
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
`
|
|
50
|
-
|
|
51
|
-
promptSnippet: "List models or show the current model in use",
|
|
84
|
+
"Show the active model or list pi models. \"active\" returns the active model with " +
|
|
85
|
+
"metadata and thinking level. \"list\" returns models with metadata such as provider, id, name, " +
|
|
86
|
+
"API type, reasoning support, cost, context window, thinking levels, and availability, " +
|
|
87
|
+
"filterable with a Liqe (Lucene-like) query and paged with offset/limit. List output is " +
|
|
88
|
+
`one JSON object per line, truncated to ${LIST_MAX_LINES} models.`,
|
|
89
|
+
promptSnippet: "Show the active model or list pi models",
|
|
52
90
|
promptGuidelines: [
|
|
53
|
-
"Use model when you need
|
|
91
|
+
"Use the model tool when you need pi model metadata, the active model, or the thinking level, rather than guessing.",
|
|
92
|
+
"When listing models with the model tool, include available:true in the query unless unavailable models are explicitly needed.",
|
|
54
93
|
],
|
|
55
94
|
parameters: Type.Object({
|
|
56
|
-
action: StringEnum(["
|
|
57
|
-
description:
|
|
58
|
-
"\"current\" returns the active model with metadata and thinking level; " +
|
|
59
|
-
"\"list\" returns models with metadata.",
|
|
95
|
+
action: StringEnum(["active", "list"] as const, {
|
|
96
|
+
description: "\"active\" shows the active model; \"list\" lists the model catalog.",
|
|
60
97
|
}),
|
|
61
|
-
|
|
98
|
+
query: Type.Optional(Type.String({
|
|
62
99
|
description:
|
|
63
|
-
"For list:
|
|
64
|
-
"
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
100
|
+
"For list: filter models with a Liqe (Lucene-like) query. Bare terms match any " +
|
|
101
|
+
"field, and unquoted terms are case-insensitive substrings. The syntax supports " +
|
|
102
|
+
"AND/OR/NOT, grouping, wildcards, and numeric comparisons; quote values containing " +
|
|
103
|
+
"special characters (e.g., id:\"deepseek/deepseek-v4\"). Queryable fields are id, " +
|
|
104
|
+
"name, provider, api, reasoning (boolean), input (modalities), cost.input and " +
|
|
105
|
+
"cost.output (USD per 1M tokens), contextWindow, maxTokens, thinkingLevels, and " +
|
|
106
|
+
"available (boolean, true when auth is configured). The catalog is large, so " +
|
|
107
|
+
"include available:true unless every model is needed (e.g., \"claude " +
|
|
108
|
+
"available:true\", \"provider:openrouter cost.input:<1\").",
|
|
71
109
|
})),
|
|
72
110
|
offset: Type.Optional(Type.Number({
|
|
73
|
-
description: "For list:
|
|
111
|
+
description: "For list: start from this model number (1-indexed)."
|
|
74
112
|
})),
|
|
75
113
|
limit: Type.Optional(Type.Number({
|
|
76
|
-
description: "For list:
|
|
114
|
+
description: "For list: return at most this many models."
|
|
77
115
|
})),
|
|
78
116
|
}),
|
|
79
117
|
renderCall(args, theme) {
|
|
80
118
|
let text = `${theme.bold(theme.fg("toolTitle", "model"))} ${theme.fg("accent", args.action)}`;
|
|
81
119
|
|
|
82
|
-
if (args.action === "list")
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
120
|
+
if (args.action === "list") {
|
|
121
|
+
const query = args.query?.trim();
|
|
122
|
+
if (query) text += theme.fg("muted", ` ${query}`);
|
|
123
|
+
|
|
124
|
+
if (args.offset !== undefined || args.limit !== undefined) {
|
|
125
|
+
const start = args.offset ?? 1;
|
|
126
|
+
const end = args.limit !== undefined ? start + args.limit - 1 : "end";
|
|
127
|
+
text += theme.fg("warning", ` ${start}-${end}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
87
130
|
|
|
88
131
|
return new Text(text, 0, 0);
|
|
89
132
|
},
|
|
@@ -103,65 +146,92 @@ export default function (pi: ExtensionAPI) {
|
|
|
103
146
|
return container;
|
|
104
147
|
}
|
|
105
148
|
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
149
|
+
const addRows = (rows: ModelRow[]) => {
|
|
150
|
+
const widths = {
|
|
151
|
+
label: Math.max(...rows.map((row) => row.label.length)),
|
|
152
|
+
cost: Math.max(...rows.map((row) => row.cost.length)),
|
|
153
|
+
context: Math.max(...rows.map((row) => row.context.length)),
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
for (const row of rows) {
|
|
157
|
+
const cells = [
|
|
158
|
+
row.label.padEnd(widths.label),
|
|
159
|
+
row.cost.padStart(widths.cost),
|
|
160
|
+
row.context.padStart(widths.context),
|
|
161
|
+
].join(" ");
|
|
162
|
+
const noAuthHint = row.available ? "" : theme.fg("dim", " (no auth)");
|
|
163
|
+
container.addChild(new Text(theme.fg("muted", cells) + noAuthHint, 0, 0));
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
if (details.action === "active") {
|
|
168
|
+
const { model, thinkingLevel } = details.active;
|
|
169
|
+
addRows([toModelRow(model, thinkingLevel)]);
|
|
109
170
|
|
|
110
171
|
return container;
|
|
111
172
|
}
|
|
112
173
|
|
|
113
|
-
const filtered = context.args.
|
|
174
|
+
const filtered = Boolean(context.args.query?.trim());
|
|
114
175
|
const models = details.models ?? [];
|
|
115
176
|
const total = details.total ?? models.length;
|
|
116
177
|
|
|
117
|
-
if (models.length > 0
|
|
118
|
-
models.
|
|
119
|
-
|
|
120
|
-
const noAuthHint = !model.available ? theme.fg("dim", " (unavailable)") : "";
|
|
121
|
-
container.addChild(new Text(theme.fg("muted", label) + noAuthHint, 0, 0));
|
|
122
|
-
});
|
|
178
|
+
if (models.length > 0) {
|
|
179
|
+
const maxRows = expanded ? models.length : Math.min(models.length, COLLAPSED_MAX_ROWS);
|
|
180
|
+
addRows(models.slice(0, maxRows).map((model) => toModelRow(model)));
|
|
123
181
|
|
|
182
|
+
const hiddenRows = models.length - maxRows;
|
|
183
|
+
if (hiddenRows > 0) container.addChild(new Text(theme.fg("dim", `... (${hiddenRows} more, ${keyText("app.tools.expand")} to expand)`), 0, 0));
|
|
124
184
|
container.addChild(new Spacer(1));
|
|
125
185
|
}
|
|
126
186
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
187
|
+
if (details.truncation?.truncated) {
|
|
188
|
+
const startIndex = context.args.offset ? Math.max(0, context.args.offset - 1) : 0;
|
|
189
|
+
const endDisplay = startIndex + models.length;
|
|
190
|
+
const notice = formatListNotice(true, startIndex, endDisplay, total);
|
|
191
|
+
if (notice) {
|
|
192
|
+
container.addChild(new Text(theme.fg("warning", notice), 0, 0));
|
|
193
|
+
container.addChild(new Spacer(1));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const summary = `${models.length !== total ? `${models.length} of ` : ""}${total} ${filtered ? "matched " : ""}model${total === 1 ? "" : "s"} listed.`;
|
|
198
|
+
container.addChild(new Text(theme.fg("muted", total > 0 ? summary : `No models ${filtered ? "matched" : "found"}.`), 0, 0));
|
|
131
199
|
|
|
132
200
|
return container;
|
|
133
201
|
},
|
|
134
202
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
135
|
-
if (params.action === "
|
|
203
|
+
if (params.action === "active") {
|
|
136
204
|
const model = ctx.model;
|
|
137
|
-
if (!model) throw new Error("No model is
|
|
205
|
+
if (!model) throw new Error("No model is active.");
|
|
138
206
|
|
|
139
207
|
const thinkingLevel = pi.getThinkingLevel();
|
|
140
|
-
const
|
|
208
|
+
const active = { model: toMetadata(model, true), thinkingLevel };
|
|
141
209
|
|
|
142
210
|
return {
|
|
143
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
144
|
-
details: { action: "
|
|
211
|
+
content: [{ type: "text", text: JSON.stringify(active) }],
|
|
212
|
+
details: { action: "active", active } satisfies ModelToolDetails,
|
|
145
213
|
};
|
|
146
214
|
}
|
|
147
215
|
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
const filtered = providerQuery !== undefined || modelQuery !== undefined;
|
|
216
|
+
const queryText = params.query?.trim();
|
|
217
|
+
const filtered = Boolean(queryText);
|
|
151
218
|
|
|
152
|
-
|
|
153
|
-
|
|
219
|
+
let listQuery = null;
|
|
220
|
+
if (queryText) {
|
|
221
|
+
try {
|
|
222
|
+
listQuery = parse(queryText);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
throw new Error(`Invalid Liqe query ${JSON.stringify(queryText)}: ${error instanceof Error ? error.message : String(error)}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
154
227
|
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
if (modelQuery && !model.id.toLowerCase().includes(modelQuery) && !model.name.toLowerCase().includes(modelQuery)) return false;
|
|
158
|
-
return true;
|
|
159
|
-
}).map((model) => toMetadata(model, scope === "all" ? ctx.modelRegistry.hasConfiguredAuth(model) : true));
|
|
228
|
+
const models = ctx.modelRegistry.getAll().map((model) => toMetadata(model, ctx.modelRegistry.hasConfiguredAuth(model)));
|
|
229
|
+
const matched = listQuery ? filter(listQuery, models) : models;
|
|
160
230
|
const total = matched.length;
|
|
161
231
|
|
|
162
232
|
if (total === 0) {
|
|
163
233
|
return {
|
|
164
|
-
content: [{ type: "text", text: `
|
|
234
|
+
content: [{ type: "text", text: `No models ${filtered ? "matched" : "found"}.` }],
|
|
165
235
|
details: { action: "list", models: [], total } satisfies ModelToolDetails,
|
|
166
236
|
};
|
|
167
237
|
}
|
|
@@ -169,7 +239,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
169
239
|
// Convert from 1-indexed offset to 0-indexed array access.
|
|
170
240
|
const startIndex = params.offset ? Math.max(0, params.offset - 1) : 0;
|
|
171
241
|
if (startIndex >= total) {
|
|
172
|
-
throw new Error(`Offset ${params.offset} is beyond end of list (${total} models total)
|
|
242
|
+
throw new Error(`Offset ${params.offset} is beyond the end of the list (${total} models total).`);
|
|
173
243
|
}
|
|
174
244
|
|
|
175
245
|
const endIndex = params.limit !== undefined ? Math.min(startIndex + params.limit, total) : total;
|
|
@@ -177,20 +247,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
177
247
|
|
|
178
248
|
// JSONL: one compact object per line, so truncation cuts at record boundaries.
|
|
179
249
|
const truncation = truncateHead(selected.map((model) => JSON.stringify(model)).join("\n"), {
|
|
180
|
-
maxLines:
|
|
181
|
-
maxBytes:
|
|
250
|
+
maxLines: LIST_MAX_LINES,
|
|
251
|
+
maxBytes: Infinity,
|
|
182
252
|
});
|
|
183
253
|
|
|
184
254
|
const delivered = truncation.truncated ? selected.slice(0, truncation.outputLines) : selected;
|
|
185
255
|
const endDisplay = startIndex + delivered.length;
|
|
186
|
-
const nextOffset = endDisplay + 1;
|
|
187
256
|
|
|
188
257
|
let text = truncation.content;
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
} else if (endDisplay < total) {
|
|
192
|
-
text += `\n\n[${total - endDisplay} more models in list; use offset=${nextOffset} to continue]`;
|
|
193
|
-
}
|
|
258
|
+
const notice = formatListNotice(truncation.truncated, startIndex, endDisplay, total);
|
|
259
|
+
if (notice) text += `\n\n${notice}`;
|
|
194
260
|
|
|
195
261
|
return {
|
|
196
262
|
content: [{ type: "text", text }],
|
package/extensions/name/index.ts
CHANGED
|
@@ -17,33 +17,29 @@ export default function (pi: ExtensionAPI) {
|
|
|
17
17
|
name: "name",
|
|
18
18
|
label: "name",
|
|
19
19
|
description:
|
|
20
|
-
"
|
|
21
|
-
"instead of the first-message preview.",
|
|
22
|
-
promptSnippet: "
|
|
20
|
+
"Name or rename the current session. The name is a concise label shown in the session " +
|
|
21
|
+
"selector instead of the first-message preview.",
|
|
22
|
+
promptSnippet: "Name or rename the current session",
|
|
23
23
|
promptGuidelines: [
|
|
24
|
-
"Use name when the session
|
|
25
|
-
"Use name to
|
|
26
|
-
"
|
|
24
|
+
"Use the name tool when the session needs a concise, recognizable label, especially after a long, vague, or pasted opening prompt.",
|
|
25
|
+
"Use the name tool to rename the session only after a substantial shift in the conversation's focus, not for minor follow-ups.",
|
|
26
|
+
"Use the name tool with a reason when it helps explain why the name identifies the session.",
|
|
27
27
|
],
|
|
28
28
|
parameters: Type.Object({
|
|
29
29
|
name: Type.String({
|
|
30
30
|
minLength: 1,
|
|
31
31
|
maxLength: 120,
|
|
32
32
|
description:
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"generic prefixes like \"Chat about\".
|
|
36
|
-
"\"Debug flaky CI pipeline\", \"Draft Q3 planning doc\".",
|
|
33
|
+
"Use a short, recognizable phrase in sentence case, ideally <= 72 characters " +
|
|
34
|
+
"(e.g., \"Refactor auth module\", \"Debug flaky CI pipeline\"). Do not use " +
|
|
35
|
+
"surrounding quotes, trailing punctuation, or generic prefixes like \"Chat about\".",
|
|
37
36
|
}),
|
|
38
37
|
reason: Type.Optional(Type.String({
|
|
39
38
|
maxLength: 240,
|
|
40
39
|
description:
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"and user-facing. Examples: \"The long pasted prompt needed a stable label.\", " +
|
|
45
|
-
"\"The focus shifted from debugging to README updates.\", " +
|
|
46
|
-
"\"The task migrated from a previous session.\"",
|
|
40
|
+
"Explain briefly why the session was named or renamed, such as a long pasted " +
|
|
41
|
+
"prompt, an ambiguous first message, or a topic shift. Write one user-facing " +
|
|
42
|
+
"sentence (e.g., \"The focus shifted from debugging to README updates.\").",
|
|
47
43
|
})),
|
|
48
44
|
}),
|
|
49
45
|
renderCall(args, theme) {
|
|
@@ -70,12 +66,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
70
66
|
},
|
|
71
67
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
72
68
|
const name = sanitizeText(params.name);
|
|
73
|
-
if (!name) throw new Error("Session name was empty after normalization; provide a short, non-empty phrase");
|
|
69
|
+
if (!name) throw new Error("Session name was empty after normalization; provide a short, non-empty phrase.");
|
|
74
70
|
|
|
75
71
|
const previous = pi.getSessionName() ?? null;
|
|
76
72
|
if (previous === name) {
|
|
77
73
|
return {
|
|
78
|
-
content: [{ type: "text", text: `Session is already named "${name}"
|
|
74
|
+
content: [{ type: "text", text: `Session is already named "${name}". Nothing changed.` }],
|
|
79
75
|
details: { changed: false, previous },
|
|
80
76
|
};
|
|
81
77
|
}
|
|
@@ -83,7 +79,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
83
79
|
pi.setSessionName(name);
|
|
84
80
|
|
|
85
81
|
return {
|
|
86
|
-
content: [{ type: "text", text: `${previous ? `Renamed session from "${previous}" to "${name}"
|
|
82
|
+
content: [{ type: "text", text: `${previous ? `Renamed session from "${previous}" to "${name}".` : `Named session "${name}".`}` }],
|
|
87
83
|
details: { changed: true, previous },
|
|
88
84
|
};
|
|
89
85
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-spark",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "A small, opinionated collection of pi extensions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-coding-agent",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"typecheck": "tsc --noEmit"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
+
"liqe": "^3.8.7",
|
|
31
32
|
"ms": "4.0.0-nightly.202508271359",
|
|
32
33
|
"zod": "^4.4.3"
|
|
33
34
|
},
|