pi-incoai 0.1.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.
- package/LICENSE +21 -0
- package/README.md +99 -0
- package/extensions/pi-incoai/index.ts +225 -0
- package/package.json +37 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sergiu Truta
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# pi-incoai
|
|
2
|
+
|
|
3
|
+
[Inco AI](https://inco.ai) (`inco.ai`) model provider extension for the
|
|
4
|
+
[Pi coding agent](https://pi.dev).
|
|
5
|
+
|
|
6
|
+
Inco serves open-weight models (Kimi K3, GLM 5.3, MiniMax M3, DeepSeek V4.1, …)
|
|
7
|
+
over an OpenAI-compatible API at `https://api.inco.ai/v1`. This package registers
|
|
8
|
+
the `incoai` provider, adds the public model catalog, and refreshes it from
|
|
9
|
+
`GET /v1/models` so workspace/private models show up too.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pi install npm:pi-incoai # from npm
|
|
15
|
+
pi install git:github.com/serggiu/pi-incoai # from git
|
|
16
|
+
pi install /path/to/pi-incoai # from a local checkout
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
or try it for one run without installing:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pi -e /path/to/pi-incoai
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Authenticate
|
|
26
|
+
|
|
27
|
+
1. Create an API key at <https://platform.inco.ai/keys> (it looks like `sk-inco-…`).
|
|
28
|
+
2. Either set the environment variable in the shell that starts Pi:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
export INCO_API_KEY=sk-inco-...
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
or sign in interactively:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
/login
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Pick **incoai** → *Sign in with an API key* → paste the key. Pi verifies the
|
|
41
|
+
key against `GET /v1/models` before saving it, then refreshes the model
|
|
42
|
+
catalog. Credentials are stored in `~/.pi/agent/auth.json`.
|
|
43
|
+
|
|
44
|
+
## Use
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
/model # search for "incoai"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pi --model incoai/kimi-k3 "…"
|
|
52
|
+
pi --model incoai/glm-5.3:fast "…"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Model IDs keep their `:fast` suffix; Pi resolves the full id before treating a
|
|
56
|
+
trailing `:level` as a thinking level.
|
|
57
|
+
|
|
58
|
+
## Models
|
|
59
|
+
|
|
60
|
+
The bundled baseline catalog is replaced by the live catalog on refresh. Pricing,
|
|
61
|
+
context length, and reasoning support come from `GET /v1/models`.
|
|
62
|
+
|
|
63
|
+
| Model | Context | Max output | Image input |
|
|
64
|
+
|---|---|---|---|
|
|
65
|
+
| `kimi-k3` | 1.0M | 131K | yes |
|
|
66
|
+
| `kimi-k3:fast` | 1.0M | 131K | yes |
|
|
67
|
+
| `deepseek-v4.1-flash` | 1.0M | 384K | yes |
|
|
68
|
+
| `deepseek-v4.1-flash:fast` | 1.0M | 384K | yes |
|
|
69
|
+
| `glm-5.3` | 1.0M | 131K | no |
|
|
70
|
+
| `glm-5.3:fast` | 1.0M | 131K | no |
|
|
71
|
+
| `glm-5.3-flash` | 1.0M | 131K | yes |
|
|
72
|
+
| `glm-5.3-flash:fast` | 1.0M | 131K | yes |
|
|
73
|
+
| `minimax-m3` | 1.0M | 512K | yes |
|
|
74
|
+
| `minimax-m3:fast` | 1.0M | 512K | yes |
|
|
75
|
+
|
|
76
|
+
## Notes
|
|
77
|
+
|
|
78
|
+
- Inco is prepaid: requests return `402` when the balance is empty.
|
|
79
|
+
- Inco exposes both OpenAI Chat Completions and Anthropic Messages. This
|
|
80
|
+
extension uses Chat Completions, the surface its catalog metadata
|
|
81
|
+
(`reasoning_effort`, `reasoning_content`) is documented for.
|
|
82
|
+
- Models that advertise `capabilities.reasoning_effort` receive `reasoning_effort`.
|
|
83
|
+
On the others Pi still surfaces streamed `reasoning_content` but sends no effort.
|
|
84
|
+
- `--list-models` and `pi auth check` do not load extensions, so they show the
|
|
85
|
+
bundled baseline catalog only. Opening `/model` triggers the live refresh.
|
|
86
|
+
|
|
87
|
+
## Development
|
|
88
|
+
|
|
89
|
+
```text
|
|
90
|
+
extensions/pi-incoai/index.ts # the provider extension
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Pi loads extensions from the `extensions/` directory of a package. Edit the file
|
|
94
|
+
and run `/reload` in an active session, or restart Pi. To exercise the catalog
|
|
95
|
+
mapping without a key, `GET https://api.inco.ai/v1/models` is public.
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
MIT
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inco AI (inco.ai) provider extension for the Pi coding agent.
|
|
3
|
+
*
|
|
4
|
+
* Inco serves open-weight models (Kimi K3, GLM 5.3, MiniMax M3, DeepSeek V4.1,
|
|
5
|
+
* …) through an OpenAI-compatible API at https://api.inco.ai/v1.
|
|
6
|
+
*
|
|
7
|
+
* This extension:
|
|
8
|
+
* - registers the `incoai` provider using pi-ai's OpenAI Chat Completions implementation;
|
|
9
|
+
* - authenticates with `INCO_API_KEY` or `pi`'s `/login incoai` flow, verifying the
|
|
10
|
+
* key against `GET /v1/models` before it is stored;
|
|
11
|
+
* - ships a baseline catalog of the public models, refreshed from `GET /v1/models`
|
|
12
|
+
* (the live catalog also includes private/workspace models when a key is configured).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
createProvider,
|
|
17
|
+
envApiKeyAuth,
|
|
18
|
+
openAICompletionsApi,
|
|
19
|
+
type ApiKeyAuth,
|
|
20
|
+
type Model,
|
|
21
|
+
type RefreshModelsContext,
|
|
22
|
+
} from "@earendil-works/pi-ai/compat";
|
|
23
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
24
|
+
|
|
25
|
+
const PROVIDER_ID = "incoai";
|
|
26
|
+
const PROVIDER_NAME = "incoai";
|
|
27
|
+
const BASE_URL = "https://api.inco.ai/v1";
|
|
28
|
+
const MODELS_URL = `${BASE_URL}/models`;
|
|
29
|
+
|
|
30
|
+
const DEFAULT_CONTEXT_WINDOW = 1_000_000;
|
|
31
|
+
const DEFAULT_MAX_TOKENS = 131_072;
|
|
32
|
+
|
|
33
|
+
/** One entry of `GET /v1/models` (fields are optional; the API may omit them). */
|
|
34
|
+
interface IncoCatalogEntry {
|
|
35
|
+
id: string;
|
|
36
|
+
name?: string;
|
|
37
|
+
context_length?: number;
|
|
38
|
+
pricing?: {
|
|
39
|
+
input?: number;
|
|
40
|
+
cached_input?: number;
|
|
41
|
+
output?: number;
|
|
42
|
+
};
|
|
43
|
+
capabilities?: {
|
|
44
|
+
reasoning_effort?: boolean;
|
|
45
|
+
};
|
|
46
|
+
modalities?: {
|
|
47
|
+
input?: string[];
|
|
48
|
+
output?: string[];
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Metadata the Inco catalog does not report, kept per model id.
|
|
54
|
+
*
|
|
55
|
+
* `maxTokens` and text/image support are not part of `GET /v1/models`, so this
|
|
56
|
+
* supplies conservative values and fills gaps when `modalities` is absent.
|
|
57
|
+
*/
|
|
58
|
+
interface CuratedModel {
|
|
59
|
+
maxTokens: number;
|
|
60
|
+
input: ("text" | "image")[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const CURATED: Record<string, CuratedModel> = {
|
|
64
|
+
"kimi-k3": { maxTokens: 131_072, input: ["text", "image"] },
|
|
65
|
+
"kimi-k3:fast": { maxTokens: 131_072, input: ["text", "image"] },
|
|
66
|
+
"deepseek-v4.1-flash": { maxTokens: 384_000, input: ["text", "image"] },
|
|
67
|
+
"deepseek-v4.1-flash:fast": { maxTokens: 384_000, input: ["text", "image"] },
|
|
68
|
+
"glm-5.3": { maxTokens: 131_072, input: ["text"] },
|
|
69
|
+
"glm-5.3:fast": { maxTokens: 131_072, input: ["text"] },
|
|
70
|
+
"glm-5.3-flash": { maxTokens: 131_072, input: ["text", "image"] },
|
|
71
|
+
"glm-5.3-flash:fast": { maxTokens: 131_072, input: ["text", "image"] },
|
|
72
|
+
"minimax-m3": { maxTokens: 512_000, input: ["text", "image"] },
|
|
73
|
+
"minimax-m3:fast": { maxTokens: 512_000, input: ["text", "image"] },
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Snapshot of the public catalog, used as the baseline model list so models
|
|
78
|
+
* appear immediately. A successful `GET /v1/models` refresh replaces entries
|
|
79
|
+
* with the same id and adds any workspace-private models.
|
|
80
|
+
*/
|
|
81
|
+
const CATALOG_SNAPSHOT: IncoCatalogEntry[] = [
|
|
82
|
+
{ id: "kimi-k3", name: "Kimi K3", context_length: 1048576, pricing: { input: 3, cached_input: 0.3, output: 15 }, capabilities: { reasoning_effort: true } },
|
|
83
|
+
{ id: "deepseek-v4.1-flash", name: "DeepSeek V4.1 Flash", context_length: 1048576, pricing: { input: 0.3, cached_input: 0.006, output: 1.2 }, capabilities: { reasoning_effort: true } },
|
|
84
|
+
{ id: "glm-5.3-flash", name: "GLM 5.3 Flash", context_length: 1048576, pricing: { input: 0.15, cached_input: 0.03, output: 0.5 }, capabilities: { reasoning_effort: false } },
|
|
85
|
+
{ id: "kimi-k3:fast", name: "Kimi K3 (Fast)", context_length: 1048576, pricing: { input: 6, cached_input: 0.6, output: 30 }, capabilities: { reasoning_effort: false } },
|
|
86
|
+
{ id: "deepseek-v4.1-flash:fast", name: "DeepSeek V4.1 Flash (Fast)", context_length: 1048576, pricing: { input: 0.6, cached_input: 0.012, output: 2.4 }, capabilities: { reasoning_effort: false }, modalities: { input: ["text", "image"] } },
|
|
87
|
+
{ id: "glm-5.3", name: "GLM 5.3", context_length: 1048576, pricing: { input: 1.4, cached_input: 0.26, output: 4.4 }, capabilities: { reasoning_effort: true } },
|
|
88
|
+
{ id: "glm-5.3:fast", name: "GLM 5.3 (Fast)", context_length: 1048576, pricing: { input: 2.8, cached_input: 0.52, output: 8.8 }, capabilities: { reasoning_effort: false } },
|
|
89
|
+
{ id: "glm-5.3-flash:fast", name: "GLM 5.3 Flash (Fast)", context_length: 1048576, pricing: { input: 0.15, cached_input: 0.03, output: 0.5 }, capabilities: { reasoning_effort: false }, modalities: { input: ["text", "image"] } },
|
|
90
|
+
{ id: "minimax-m3", name: "MiniMax M3", context_length: 1048576, pricing: { input: 0.3, cached_input: 0.06, output: 1.2 }, capabilities: { reasoning_effort: true }, modalities: { input: ["text", "image"] } },
|
|
91
|
+
{ id: "minimax-m3:fast", name: "MiniMax M3 (Fast)", context_length: 1048576, pricing: { input: 0.6, cached_input: 0.12, output: 2.4 }, capabilities: { reasoning_effort: false }, modalities: { input: ["text", "image"] } },
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
/** Build an error from a failed catalog response, preferring Inco's error envelope. */
|
|
95
|
+
async function catalogError(response: Response): Promise<Error> {
|
|
96
|
+
const status = `HTTP ${response.status} ${response.statusText}`.trim();
|
|
97
|
+
let message = `Inco model catalog request failed: ${status}`;
|
|
98
|
+
try {
|
|
99
|
+
const body = (await response.json()) as { error?: { message?: string; code?: string } };
|
|
100
|
+
const detail = body?.error?.message;
|
|
101
|
+
const code = body?.error?.code;
|
|
102
|
+
if (detail) {
|
|
103
|
+
message = `Inco model catalog request failed: ${status}${code ? ` (${code})` : ""} - ${detail}`;
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// Non-JSON error body; keep the status line.
|
|
107
|
+
}
|
|
108
|
+
return new Error(message);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Authenticated (or public) catalog request, used by both login and refresh. */
|
|
112
|
+
function requestCatalog(apiKey: string | undefined, signal: AbortSignal): Promise<Response> {
|
|
113
|
+
const headers: Record<string, string> = { Accept: "application/json" };
|
|
114
|
+
if (apiKey) {
|
|
115
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
116
|
+
}
|
|
117
|
+
return fetch(MODELS_URL, { headers, signal });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function isInputModality(value: string): value is "text" | "image" {
|
|
121
|
+
return value === "text" || value === "image";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Resolve the input modalities the catalog reports, falling back to curated metadata. */
|
|
125
|
+
function resolveInput(entry: IncoCatalogEntry, curated: CuratedModel | undefined): ("text" | "image")[] {
|
|
126
|
+
const reported = entry.modalities?.input?.filter(isInputModality) ?? [];
|
|
127
|
+
if (reported.length === 0) {
|
|
128
|
+
return curated?.input ?? ["text"];
|
|
129
|
+
}
|
|
130
|
+
return reported.includes("text") ? reported : (["text", ...reported] as ("text" | "image")[]);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Convert one Inco catalog entry into a Pi chat model. */
|
|
134
|
+
function toModel(entry: IncoCatalogEntry): Model<"openai-completions"> {
|
|
135
|
+
const curated = CURATED[entry.id];
|
|
136
|
+
const reasoningEffort = entry.capabilities?.reasoning_effort === true;
|
|
137
|
+
return {
|
|
138
|
+
id: entry.id,
|
|
139
|
+
name: entry.name || entry.id,
|
|
140
|
+
api: "openai-completions",
|
|
141
|
+
provider: PROVIDER_ID,
|
|
142
|
+
baseUrl: BASE_URL,
|
|
143
|
+
reasoning: true,
|
|
144
|
+
input: resolveInput(entry, curated),
|
|
145
|
+
cost: {
|
|
146
|
+
input: entry.pricing?.input ?? 0,
|
|
147
|
+
output: entry.pricing?.output ?? 0,
|
|
148
|
+
cacheRead: entry.pricing?.cached_input ?? 0,
|
|
149
|
+
cacheWrite: 0,
|
|
150
|
+
},
|
|
151
|
+
contextWindow: entry.context_length ?? DEFAULT_CONTEXT_WINDOW,
|
|
152
|
+
maxTokens: curated?.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
153
|
+
compat: {
|
|
154
|
+
// Inco is not OpenAI: send only fields its OpenAI-compatible surface documents.
|
|
155
|
+
supportsStore: false,
|
|
156
|
+
supportsDeveloperRole: false,
|
|
157
|
+
supportsReasoningEffort: reasoningEffort,
|
|
158
|
+
supportsUsageInStreaming: true,
|
|
159
|
+
maxTokensField: "max_completion_tokens",
|
|
160
|
+
// Inco returns reasoning in `reasoning_content` and expects it replayed.
|
|
161
|
+
requiresReasoningContentOnAssistantMessages: true,
|
|
162
|
+
thinkingFormat: "openai",
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Fetch the live catalog, authenticating when a key is available. */
|
|
168
|
+
async function fetchIncoModels(context: RefreshModelsContext): Promise<Model<"openai-completions">[]> {
|
|
169
|
+
const apiKey = context.credential?.type === "oauth" ? context.credential.access : context.credential?.key;
|
|
170
|
+
const response = await requestCatalog(apiKey, context.signal);
|
|
171
|
+
if (!response.ok) {
|
|
172
|
+
throw await catalogError(response);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const payload = (await response.json()) as { data?: IncoCatalogEntry[] };
|
|
176
|
+
const entries = Array.isArray(payload?.data) ? payload.data : [];
|
|
177
|
+
if (entries.length === 0) {
|
|
178
|
+
throw new Error("Inco model catalog returned no models");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return entries.filter((entry) => entry && typeof entry.id === "string").map(toModel);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Standard api-key auth plus a login that verifies the key against the catalog
|
|
186
|
+
* before Pi persists it, so a typo fails in the login dialog instead of on the
|
|
187
|
+
* first message.
|
|
188
|
+
*/
|
|
189
|
+
function incoApiKeyAuth(): ApiKeyAuth {
|
|
190
|
+
const standard = envApiKeyAuth("Inco AI API key", ["INCO_API_KEY"]);
|
|
191
|
+
return {
|
|
192
|
+
...standard,
|
|
193
|
+
async login(interaction) {
|
|
194
|
+
if (!standard.login) {
|
|
195
|
+
throw new Error("Inco AI API key login is unavailable");
|
|
196
|
+
}
|
|
197
|
+
const credential = await standard.login(interaction);
|
|
198
|
+
const key = credential.key?.trim();
|
|
199
|
+
if (!key) {
|
|
200
|
+
throw new Error("No API key provided");
|
|
201
|
+
}
|
|
202
|
+
interaction.notify({ type: "progress", message: "Verifying API key…" });
|
|
203
|
+
const response = await requestCatalog(key, interaction.signal);
|
|
204
|
+
if (!response.ok) {
|
|
205
|
+
throw await catalogError(response);
|
|
206
|
+
}
|
|
207
|
+
await response.arrayBuffer();
|
|
208
|
+
return { ...credential, key };
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export default function incoProviderExtension(pi: ExtensionAPI): void {
|
|
214
|
+
pi.registerProvider(
|
|
215
|
+
createProvider({
|
|
216
|
+
id: PROVIDER_ID,
|
|
217
|
+
name: PROVIDER_NAME,
|
|
218
|
+
baseUrl: BASE_URL,
|
|
219
|
+
auth: { apiKey: incoApiKeyAuth() },
|
|
220
|
+
models: CATALOG_SNAPSHOT.map(toModel),
|
|
221
|
+
api: openAICompletionsApi(),
|
|
222
|
+
fetchModels: fetchIncoModels,
|
|
223
|
+
}),
|
|
224
|
+
);
|
|
225
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-incoai",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Inco AI (inco.ai) model provider extension for the Pi coding agent",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi",
|
|
8
|
+
"pi-coding-agent",
|
|
9
|
+
"inco",
|
|
10
|
+
"incoai",
|
|
11
|
+
"inference",
|
|
12
|
+
"provider"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": "Sergiu Truta",
|
|
16
|
+
"homepage": "https://github.com/serggiu/pi-incoai#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/serggiu/pi-incoai/issues"
|
|
19
|
+
},
|
|
20
|
+
"type": "module",
|
|
21
|
+
"files": [
|
|
22
|
+
"extensions",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/serggiu/pi-incoai.git"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@earendil-works/pi-ai": "*",
|
|
35
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
36
|
+
}
|
|
37
|
+
}
|