omp-llm-role 1.0.0 → 1.2.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/README.md +202 -12
- package/agents/designer.md +4 -3
- package/docs/dev/README.md +44 -7
- package/docs/dev/agent-authoring.md +59 -1
- package/docs/dev/architecture.md +106 -28
- package/docs/dev/ci.md +163 -0
- package/docs/dev/current-state.md +364 -24
- package/docs/dev/data-sources.md +231 -38
- package/docs/dev/explorer.md +175 -46
- package/docs/dev/quirks.md +77 -4
- package/docs/dev/releasing.md +130 -17
- package/docs/dev/scoring.md +851 -27
- package/docs/dev/testing.md +253 -0
- package/docs/dev/writes-and-state.md +232 -28
- package/docs/llm-role-rankings.md +149 -148
- package/package.json +814 -80
- package/skills/omp-llm-role-create-agent/SKILL.md +7 -1
- package/src/agent-architect.ts +148 -19
- package/src/agent-create.ts +800 -40
- package/src/agent-file.ts +2 -5
- package/src/architect-provenance.ts +16 -0
- package/src/availability.ts +191 -15
- package/src/benchmark-author.ts +9 -4
- package/src/benchmark-sources.ts +283 -18
- package/src/cli/create-role.ts +29 -1
- package/src/cli/llm-role-rank.ts +40 -1
- package/src/config-edit.ts +207 -80
- package/src/engine.ts +519 -75
- package/src/error-report.ts +562 -0
- package/src/explorer/boot.ts +36 -24
- package/src/explorer/explain.ts +128 -13
- package/src/explorer/scopes.ts +110 -0
- package/src/explorer/server.ts +126 -33
- package/src/extension.ts +464 -48
- package/src/features.ts +105 -0
- package/src/project-registry.ts +65 -0
- package/src/project-setup.ts +382 -0
- package/src/prompts/agent-creation-architect.md +2 -0
- package/src/prompts/project-profile-architect.md +33 -0
- package/src/prompts/project-profile-user.md +8 -0
- package/src/provider-display.ts +67 -0
- package/src/role-archetypes.ts +12 -2
- package/src/role-settings.ts +22 -0
- package/src/settings.ts +211 -24
- package/src/state.ts +1 -1
- package/src/updater.ts +234 -37
- package/web/app.js +434 -12
- package/web/index.html +1 -0
- package/web/style.css +44 -1
- package/docs/dev/spec.md +0 -633
package/src/agent-file.ts
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
* the effort and prices it; a second pin would silently disagree with it.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { existsSync,
|
|
20
|
-
import {
|
|
19
|
+
import { existsSync, statSync, unlinkSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
21
|
import { writeConfigAtomic } from "./config-edit.ts";
|
|
22
22
|
import { agentDir } from "./state.ts";
|
|
23
23
|
|
|
@@ -99,9 +99,6 @@ export function writeAgentFile(path: string, text: string, force: boolean): Agen
|
|
|
99
99
|
if (existsSync(path) && !force) {
|
|
100
100
|
return { ok: false, error: `${path} already exists — pass --force to overwrite` };
|
|
101
101
|
}
|
|
102
|
-
// A first agent may be the first thing in the dir (~/.omp/agent/agents is not
|
|
103
|
-
// created by omp until something writes there).
|
|
104
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
105
102
|
const mtimeBefore = existsSync(path) ? statSync(path).mtimeMs : 0;
|
|
106
103
|
const result = writeConfigAtomic(path, text, mtimeBefore);
|
|
107
104
|
if (result === "conflict") return { ok: false, error: `${path} changed while writing — refusing to overwrite` };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provenance for the vendored agent-creation architect prompt.
|
|
3
|
+
*
|
|
4
|
+
* `src/prompts/agent-creation-architect.md` is copied verbatim from omp's
|
|
5
|
+
* `/agents` hub prompt. omp's extension loader does not resolve the SDK subpath
|
|
6
|
+
* that prompt lives under, so the plugin ships its own copy; this constant is the
|
|
7
|
+
* omp version it was copied from. Bump it and the prompt's header comment
|
|
8
|
+
* together when re-syncing after an omp upgrade (see
|
|
9
|
+
* `docs/dev/agent-authoring.md`), so a drift is a visible test failure rather
|
|
10
|
+
* than a silent one.
|
|
11
|
+
*
|
|
12
|
+
* Kept in its own module (not `agent-architect.ts`) because that module imports
|
|
13
|
+
* `@oh-my-pi/pi-coding-agent`, which only resolves inside omp — a test cannot
|
|
14
|
+
* import it.
|
|
15
|
+
*/
|
|
16
|
+
export const ARCHITECT_PROMPT_VERSION = "18.4.8";
|
package/src/availability.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Availability model
|
|
2
|
+
* Availability model: what the user's OpenRouter key can actually run.
|
|
3
3
|
*
|
|
4
4
|
* OpenRouter exposes no per-key model allowlist — availability is derived from
|
|
5
5
|
* key tier + budget (tier gate), then every candidate must resolve to a concrete
|
|
@@ -76,7 +76,7 @@ export async function fetchKeyMeta(token: string, fetchImpl: typeof fetch = fetc
|
|
|
76
76
|
};
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
/** Tier gate
|
|
79
|
+
/** Tier gate: billed budget wins, else free daily quota, else run aborts. */
|
|
80
80
|
export function tierGate(meta: KeyMeta): Tier {
|
|
81
81
|
const billedUsable = !meta.isFreeTier && meta.limitRemaining > 0 && meta.creditsRemaining > 0;
|
|
82
82
|
if (billedUsable) return "billed";
|
|
@@ -88,12 +88,30 @@ export type ProbeVerdict = "ok" | "blocked" | "unknown";
|
|
|
88
88
|
|
|
89
89
|
const OPENROUTER_COMPLETIONS_URL = "https://openrouter.ai/api/v1/chat/completions";
|
|
90
90
|
|
|
91
|
+
/** POST a one-token completion to OpenRouter with the shared request skeleton
|
|
92
|
+
* (Bearer auth, JSON body, 20s timeout). Shared by `probeModel` and
|
|
93
|
+
* `fetchAllowedProviders`. */
|
|
94
|
+
function completionRequest(token: string, body: unknown, fetchImpl: typeof fetch): Promise<Response> {
|
|
95
|
+
return fetchImpl(OPENROUTER_COMPLETIONS_URL, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
98
|
+
body: JSON.stringify(body),
|
|
99
|
+
signal: AbortSignal.timeout(20_000),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Extract `error.message` from a parsed OpenRouter error body, or "" when the
|
|
104
|
+
* body does not carry one. */
|
|
105
|
+
function errorMessage(body: unknown): string {
|
|
106
|
+
return isRecord(body) && isRecord(body.error) && typeof body.error.message === "string" ? body.error.message : "";
|
|
107
|
+
}
|
|
108
|
+
|
|
91
109
|
/** 404 body OpenRouter returns when the account's allowed-providers privacy
|
|
92
110
|
* whitelist (or a data-policy toggle) leaves a model without a runnable endpoint. */
|
|
93
111
|
const BLOCKED_RE = /no allowed providers/i;
|
|
94
112
|
|
|
95
113
|
/**
|
|
96
|
-
* One-token completion probe
|
|
114
|
+
* One-token completion probe: the only reliable signal for
|
|
97
115
|
* account-level provider restrictions — neither `/api/v1/key` nor the catalog
|
|
98
116
|
* endpoints expose the allowed-providers whitelist, but a real request's 404
|
|
99
117
|
* body names it. Only the narrow no-allowed-providers verdict blocks a
|
|
@@ -102,22 +120,165 @@ const BLOCKED_RE = /no allowed providers/i;
|
|
|
102
120
|
*/
|
|
103
121
|
export async function probeModel(token: string, catalogId: string, fetchImpl: typeof fetch = fetch): Promise<ProbeVerdict> {
|
|
104
122
|
try {
|
|
105
|
-
const res = await
|
|
106
|
-
method: "POST",
|
|
107
|
-
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
108
|
-
body: JSON.stringify({ model: catalogId, messages: [{ role: "user", content: "ok" }], max_tokens: 1 }),
|
|
109
|
-
signal: AbortSignal.timeout(20_000),
|
|
110
|
-
});
|
|
123
|
+
const res = await completionRequest(token, { model: catalogId, messages: [{ role: "user", content: "ok" }], max_tokens: 1 }, fetchImpl);
|
|
111
124
|
if (res.ok) return "ok";
|
|
112
125
|
if (res.status !== 404) return "unknown";
|
|
113
126
|
const body: unknown = await res.json().catch(() => null);
|
|
114
|
-
const message =
|
|
127
|
+
const message = errorMessage(body);
|
|
115
128
|
return BLOCKED_RE.test(message) ? "blocked" : "unknown";
|
|
116
129
|
} catch {
|
|
117
130
|
return "unknown";
|
|
118
131
|
}
|
|
119
132
|
}
|
|
120
133
|
|
|
134
|
+
/** 404 body OpenRouter returns when the account's allowed-providers privacy
|
|
135
|
+
* whitelist leaves the canary provider without a runnable endpoint; the message
|
|
136
|
+
* names the whitelist after "permits only:". */
|
|
137
|
+
const ALLOWED_PROVIDERS_RE = /permits only:\s*([^.]*)\./i;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Harvest the account's allowed-providers privacy whitelist. The whitelist is
|
|
141
|
+
* invisible to `/api/v1/key` and the catalog endpoints; the only place it
|
|
142
|
+
* appears is the 404 body of a request that fails because of it. A canary
|
|
143
|
+
* completion on a valid model with a nonexistent provider slug (`provider.only`)
|
|
144
|
+
* 404s and names the whitelist. Returns null when the body does not name a
|
|
145
|
+
* whitelist (no whitelist set), the response is not a 404, or the request
|
|
146
|
+
* throws — never throws.
|
|
147
|
+
*/
|
|
148
|
+
export async function fetchAllowedProviders(token: string, modelId: string, fetchImpl: typeof fetch = fetch): Promise<Set<string> | null> {
|
|
149
|
+
try {
|
|
150
|
+
const res = await completionRequest(
|
|
151
|
+
token,
|
|
152
|
+
{
|
|
153
|
+
model: modelId,
|
|
154
|
+
messages: [{ role: "user", content: "ok" }],
|
|
155
|
+
max_tokens: 1,
|
|
156
|
+
provider: { only: ["__omp-llm-role-canary__"] },
|
|
157
|
+
},
|
|
158
|
+
fetchImpl,
|
|
159
|
+
);
|
|
160
|
+
if (res.status !== 404) return null;
|
|
161
|
+
const body: unknown = await res.json().catch(() => null);
|
|
162
|
+
const message = errorMessage(body);
|
|
163
|
+
const match = ALLOWED_PROVIDERS_RE.exec(message);
|
|
164
|
+
if (!match) return null;
|
|
165
|
+
const providers = match[1]
|
|
166
|
+
.split(",")
|
|
167
|
+
.map((s) => s.trim())
|
|
168
|
+
.filter((s) => s.length > 0);
|
|
169
|
+
return providers.length > 0 ? new Set(providers) : null;
|
|
170
|
+
} catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Whether a route's `providerSlug` is permitted by the account's
|
|
177
|
+
* allowed-providers whitelist. Whitelist entries are bare lab names
|
|
178
|
+
* (`coreweave`) while route slugs are tiered (`coreweave/fp8`), so a match is
|
|
179
|
+
* the entry itself or an `entry/` prefix.
|
|
180
|
+
*/
|
|
181
|
+
export function providerAllowed(allowed: ReadonlySet<string>, slug: string): boolean {
|
|
182
|
+
for (const entry of allowed) {
|
|
183
|
+
if (slug === entry || slug.startsWith(`${entry}/`)) return true;
|
|
184
|
+
}
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// Keyed-catalog availability
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* What the user's OpenRouter key can actually run, derived from the account
|
|
194
|
+
* setting "Filter the model catalog for API keys": with it on, a
|
|
195
|
+
* Bearer-authenticated `GET /api/v1/models` returns only the models the key may
|
|
196
|
+
* use (provider restrictions and guardrails applied), while the unauthenticated
|
|
197
|
+
* request still sees the public catalog. Set membership is the only signal —
|
|
198
|
+
* the response carries no per-model access flag.
|
|
199
|
+
*
|
|
200
|
+
* `allowed`/`blocked` are populated only when `reason === "active"`; every other
|
|
201
|
+
* reason leaves them empty so a consumer that forgets to check `active` degrades
|
|
202
|
+
* to "unknown" rather than over-claiming "usable".
|
|
203
|
+
*/
|
|
204
|
+
export type KeyAvailability = {
|
|
205
|
+
/** keyed catalog ⊊ public catalog → the OpenRouter filter is on. */
|
|
206
|
+
active: boolean;
|
|
207
|
+
reason: "active" | "no-filter" | "unavailable";
|
|
208
|
+
/** ranking ids present in the keyed catalog (empty unless `active`). */
|
|
209
|
+
allowed: ReadonlySet<string>;
|
|
210
|
+
/** ranking ids present in the public catalog but not the keyed one (empty unless `active`). */
|
|
211
|
+
blocked: ReadonlySet<string>;
|
|
212
|
+
publicCount: number;
|
|
213
|
+
keyedCount: number;
|
|
214
|
+
/** ISO 8601. */
|
|
215
|
+
fetchedAt: string;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
|
|
219
|
+
|
|
220
|
+
/** Tolerant `data[].id` reader; null when the payload is not the expected shape. */
|
|
221
|
+
function modelIdsFromPayload(v: unknown): string[] | null {
|
|
222
|
+
if (!isRecord(v) || !Array.isArray(v.data)) return null;
|
|
223
|
+
const ids: string[] = [];
|
|
224
|
+
for (const row of v.data) {
|
|
225
|
+
if (isRecord(row) && typeof row.id === "string") ids.push(row.id);
|
|
226
|
+
}
|
|
227
|
+
return ids;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Pure derivation of the availability mark from the two catalog id lists. Both
|
|
232
|
+
* are normalized with `rankingIdOf` — the same identity space `resolveVariant`
|
|
233
|
+
* and the engine's `Model.id` use — so `~org/x-latest`, `org/x:free` and
|
|
234
|
+
* `org/x` collapse to one ranking id and aliases never produce a false "blocked".
|
|
235
|
+
*
|
|
236
|
+
* `active` requires a non-empty keyed list that is a **proper subset** of the
|
|
237
|
+
* public one; equality (the setting is off) or a keyed id absent from public is
|
|
238
|
+
* `no-filter`; an empty list is `unavailable`.
|
|
239
|
+
*/
|
|
240
|
+
export function computeKeyAvailability(publicIds: string[], keyedIds: string[], fetchedAt = new Date().toISOString()): KeyAvailability {
|
|
241
|
+
const publicSet = new Set(publicIds.map(rankingIdOf));
|
|
242
|
+
const keyedSet = new Set(keyedIds.map(rankingIdOf));
|
|
243
|
+
const base = { publicCount: publicSet.size, keyedCount: keyedSet.size, fetchedAt };
|
|
244
|
+
if (publicSet.size === 0 || keyedSet.size === 0) {
|
|
245
|
+
return { active: false, reason: "unavailable", allowed: new Set(), blocked: new Set(), ...base };
|
|
246
|
+
}
|
|
247
|
+
const subset = keyedSet.size < publicSet.size && [...keyedSet].every((id) => publicSet.has(id));
|
|
248
|
+
if (!subset) return { active: false, reason: "no-filter", allowed: new Set(), blocked: new Set(), ...base };
|
|
249
|
+
return {
|
|
250
|
+
active: true,
|
|
251
|
+
reason: "active",
|
|
252
|
+
allowed: keyedSet,
|
|
253
|
+
blocked: new Set([...publicSet].filter((id) => !keyedSet.has(id))),
|
|
254
|
+
...base,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Fetch the public and key-authenticated catalogs in parallel and derive the
|
|
260
|
+
* availability mark. **Never throws**: any transport, status, or shape failure
|
|
261
|
+
* yields `reason: "unavailable"` with empty sets, so both callers (the explorer
|
|
262
|
+
* and the updater) keep their current behavior on a transient OpenRouter outage.
|
|
263
|
+
*/
|
|
264
|
+
export async function fetchKeyAvailability(token: string, fetchImpl: typeof fetch = fetch): Promise<KeyAvailability> {
|
|
265
|
+
const fetchedAt = new Date().toISOString();
|
|
266
|
+
const unavailable: KeyAvailability = { active: false, reason: "unavailable", allowed: new Set(), blocked: new Set(), publicCount: 0, keyedCount: 0, fetchedAt };
|
|
267
|
+
try {
|
|
268
|
+
const [publicRes, keyedRes] = await Promise.all([
|
|
269
|
+
fetchImpl(OPENROUTER_MODELS_URL),
|
|
270
|
+
fetchImpl(OPENROUTER_MODELS_URL, { headers: { authorization: `Bearer ${token}` } }),
|
|
271
|
+
]);
|
|
272
|
+
if (!publicRes.ok || !keyedRes.ok) return unavailable;
|
|
273
|
+
const publicIds = modelIdsFromPayload(await publicRes.json());
|
|
274
|
+
const keyedIds = modelIdsFromPayload(await keyedRes.json());
|
|
275
|
+
if (publicIds === null || keyedIds === null) return unavailable;
|
|
276
|
+
return computeKeyAvailability(publicIds, keyedIds, fetchedAt);
|
|
277
|
+
} catch {
|
|
278
|
+
return unavailable;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
121
282
|
/** Keep openrouter catalog rows the tier may run: never `:batch`; `:free` only on free tier. */
|
|
122
283
|
export function filterCatalog(catalog: CatalogEntry[], tier: Tier): CatalogEntry[] {
|
|
123
284
|
if (tier === "none") return [];
|
|
@@ -134,12 +295,27 @@ export function rankingIdOf(catalogId: string): string {
|
|
|
134
295
|
return noFree.endsWith("-latest") ? noFree.slice(0, -"-latest".length) : noFree;
|
|
135
296
|
}
|
|
136
297
|
|
|
137
|
-
/**
|
|
138
|
-
|
|
298
|
+
/** Split a selector's trailing thinking suffix (`provider/model[:level]`):
|
|
299
|
+
* `base` without the suffix, `level` the suffix or null. The single home for
|
|
300
|
+
* the `:level` strip shared by `currentRankingId`, `providerPinOfSelector`, and
|
|
301
|
+
* the extension's selector handling. */
|
|
302
|
+
export function splitThinkingSuffix(selector: string): { base: string; level: string | null } {
|
|
139
303
|
const colon = selector.lastIndexOf(":");
|
|
140
|
-
|
|
304
|
+
if (colon !== -1 && selector.slice(colon + 1) in THINKING_LEVELS) {
|
|
305
|
+
return { base: selector.slice(0, colon), level: selector.slice(colon + 1) };
|
|
306
|
+
}
|
|
307
|
+
return { base: selector, level: null };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Identity of a configured selector for hysteresis/chain keys: drop `:level`,
|
|
311
|
+
* `openrouter/`, and a trailing `@<slug>` provider pin, then rankingIdOf. A
|
|
312
|
+
* tiered pin (`@deepinfra/fp8`) is dropped whole — everything from the last `@`. */
|
|
313
|
+
export function currentRankingId(selector: string): string {
|
|
314
|
+
const { base } = splitThinkingSuffix(selector);
|
|
141
315
|
const unprefixed = base.startsWith("openrouter/") ? base.slice("openrouter/".length) : base;
|
|
142
|
-
|
|
316
|
+
const at = unprefixed.lastIndexOf("@");
|
|
317
|
+
const unpinned = at !== -1 ? unprefixed.slice(0, at) : unprefixed;
|
|
318
|
+
return rankingIdOf(unpinned);
|
|
143
319
|
}
|
|
144
320
|
|
|
145
321
|
// "Newest dated" = trailing -MMDD or -YYYYMMDD (max numeric wins; ties lexicographic).
|
|
@@ -147,7 +323,7 @@ const DATED_RE = /-(\d{4}|\d{8})$/;
|
|
|
147
323
|
|
|
148
324
|
/**
|
|
149
325
|
* Map a ranking row id (llm-stats bare id) to a concrete catalog id among
|
|
150
|
-
* tier-eligible candidates
|
|
326
|
+
* tier-eligible candidates. Candidates match when their suffix after
|
|
151
327
|
* the last `/` equals the ranking id, possibly after stripping `:free` and one
|
|
152
328
|
* `-latest` (org prefixes, including `~`-prefixed, are ignored for matching).
|
|
153
329
|
* Order: exact id -> newest dated -> bare -> `~org/…-latest` alias (last resort —
|
package/src/benchmark-author.ts
CHANGED
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
* from the fetched content. The extension then dry-runs the declaration (fetch,
|
|
5
5
|
* parse, join, coverage/leader) and asks the user to confirm before it is saved.
|
|
6
6
|
*
|
|
7
|
-
* This module is extension-only: it imports `@oh-my-pi/pi-coding-agent
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* This module is extension-only: it imports `@oh-my-pi/pi-coding-agent`
|
|
8
|
+
* dynamically, inside the function (the SDK only resolves inside omp; a static
|
|
9
|
+
* import failure would be fatal to the whole `extension.ts` load). The tests
|
|
10
|
+
* import the pure validation/execution path in `src/benchmark-sources.ts`
|
|
11
|
+
* instead.
|
|
10
12
|
*/
|
|
11
13
|
|
|
12
|
-
import { createAgentSession } from "@oh-my-pi/pi-coding-agent";
|
|
13
14
|
import { readFileSync } from "node:fs";
|
|
14
15
|
import { fetchText, parseSourceDeclaration, type SourceDeclaration } from "./benchmark-sources.ts";
|
|
15
16
|
import { extractAssistantText, extractJsonObject } from "./agent-architect.ts";
|
|
@@ -47,6 +48,10 @@ export type BenchmarkAuthorOptions = {
|
|
|
47
48
|
*/
|
|
48
49
|
export async function authorBenchmarkSource(opts: BenchmarkAuthorOptions): Promise<SourceDeclaration> {
|
|
49
50
|
const content = (await fetchText(opts.link)).slice(0, MAX_CONTENT);
|
|
51
|
+
// Dynamic, inside the function: `@oh-my-pi/pi-coding-agent` only resolves
|
|
52
|
+
// inside omp, and a static import failure would be fatal to the whole
|
|
53
|
+
// `extension.ts` load (see `agent-architect.ts`).
|
|
54
|
+
const { createAgentSession } = await import("@oh-my-pi/pi-coding-agent");
|
|
50
55
|
const { session } = await createAgentSession({
|
|
51
56
|
cwd: opts.cwd,
|
|
52
57
|
model: opts.model as never,
|